##// END OF EJS Templates
drafts: sidebar functionality
milka -
r4562:20bc1204 default
parent child Browse files
Show More
@@ -1,543 +1,548 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2020 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 from rhodecode.apps._base import add_route_with_slash
21 21
22 22
23 23 def includeme(config):
24 24
25 25 # repo creating checks, special cases that aren't repo routes
26 26 config.add_route(
27 27 name='repo_creating',
28 28 pattern='/{repo_name:.*?[^/]}/repo_creating')
29 29
30 30 config.add_route(
31 31 name='repo_creating_check',
32 32 pattern='/{repo_name:.*?[^/]}/repo_creating_check')
33 33
34 34 # Summary
35 35 # NOTE(marcink): one additional route is defined in very bottom, catch
36 36 # all pattern
37 37 config.add_route(
38 38 name='repo_summary_explicit',
39 39 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
40 40 config.add_route(
41 41 name='repo_summary_commits',
42 42 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
43 43
44 44 # Commits
45 45 config.add_route(
46 46 name='repo_commit',
47 47 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
48 48
49 49 config.add_route(
50 50 name='repo_commit_children',
51 51 pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
52 52
53 53 config.add_route(
54 54 name='repo_commit_parents',
55 55 pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
56 56
57 57 config.add_route(
58 58 name='repo_commit_raw',
59 59 pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
60 60
61 61 config.add_route(
62 62 name='repo_commit_patch',
63 63 pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
64 64
65 65 config.add_route(
66 66 name='repo_commit_download',
67 67 pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
68 68
69 69 config.add_route(
70 70 name='repo_commit_data',
71 71 pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
72 72
73 73 config.add_route(
74 74 name='repo_commit_comment_create',
75 75 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
76 76
77 77 config.add_route(
78 78 name='repo_commit_comment_preview',
79 79 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
80 80
81 81 config.add_route(
82 82 name='repo_commit_comment_history_view',
83 83 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_history_id}/history_view', repo_route=True)
84 84
85 85 config.add_route(
86 86 name='repo_commit_comment_attachment_upload',
87 87 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/attachment_upload', repo_route=True)
88 88
89 89 config.add_route(
90 90 name='repo_commit_comment_delete',
91 91 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
92 92
93 93 config.add_route(
94 94 name='repo_commit_comment_edit',
95 95 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/edit', repo_route=True)
96 96
97 97 # still working url for backward compat.
98 98 config.add_route(
99 99 name='repo_commit_raw_deprecated',
100 100 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
101 101
102 102 # Files
103 103 config.add_route(
104 104 name='repo_archivefile',
105 105 pattern='/{repo_name:.*?[^/]}/archive/{fname:.*}', repo_route=True)
106 106
107 107 config.add_route(
108 108 name='repo_files_diff',
109 109 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
110 110 config.add_route( # legacy route to make old links work
111 111 name='repo_files_diff_2way_redirect',
112 112 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
113 113
114 114 config.add_route(
115 115 name='repo_files',
116 116 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
117 117 config.add_route(
118 118 name='repo_files:default_path',
119 119 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
120 120 config.add_route(
121 121 name='repo_files:default_commit',
122 122 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
123 123
124 124 config.add_route(
125 125 name='repo_files:rendered',
126 126 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
127 127
128 128 config.add_route(
129 129 name='repo_files:annotated',
130 130 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
131 131 config.add_route(
132 132 name='repo_files:annotated_previous',
133 133 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
134 134
135 135 config.add_route(
136 136 name='repo_nodetree_full',
137 137 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
138 138 config.add_route(
139 139 name='repo_nodetree_full:default_path',
140 140 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
141 141
142 142 config.add_route(
143 143 name='repo_files_nodelist',
144 144 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
145 145
146 146 config.add_route(
147 147 name='repo_file_raw',
148 148 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
149 149
150 150 config.add_route(
151 151 name='repo_file_download',
152 152 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
153 153 config.add_route( # backward compat to keep old links working
154 154 name='repo_file_download:legacy',
155 155 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
156 156 repo_route=True)
157 157
158 158 config.add_route(
159 159 name='repo_file_history',
160 160 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
161 161
162 162 config.add_route(
163 163 name='repo_file_authors',
164 164 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
165 165
166 166 config.add_route(
167 167 name='repo_files_check_head',
168 168 pattern='/{repo_name:.*?[^/]}/check_head/{commit_id}/{f_path:.*}',
169 169 repo_route=True)
170 170 config.add_route(
171 171 name='repo_files_remove_file',
172 172 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
173 173 repo_route=True)
174 174 config.add_route(
175 175 name='repo_files_delete_file',
176 176 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
177 177 repo_route=True)
178 178 config.add_route(
179 179 name='repo_files_edit_file',
180 180 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
181 181 repo_route=True)
182 182 config.add_route(
183 183 name='repo_files_update_file',
184 184 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
185 185 repo_route=True)
186 186 config.add_route(
187 187 name='repo_files_add_file',
188 188 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
189 189 repo_route=True)
190 190 config.add_route(
191 191 name='repo_files_upload_file',
192 192 pattern='/{repo_name:.*?[^/]}/upload_file/{commit_id}/{f_path:.*}',
193 193 repo_route=True)
194 194 config.add_route(
195 195 name='repo_files_create_file',
196 196 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
197 197 repo_route=True)
198 198
199 199 # Refs data
200 200 config.add_route(
201 201 name='repo_refs_data',
202 202 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
203 203
204 204 config.add_route(
205 205 name='repo_refs_changelog_data',
206 206 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
207 207
208 208 config.add_route(
209 209 name='repo_stats',
210 210 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
211 211
212 212 # Commits
213 213 config.add_route(
214 214 name='repo_commits',
215 215 pattern='/{repo_name:.*?[^/]}/commits', repo_route=True)
216 216 config.add_route(
217 217 name='repo_commits_file',
218 218 pattern='/{repo_name:.*?[^/]}/commits/{commit_id}/{f_path:.*}', repo_route=True)
219 219 config.add_route(
220 220 name='repo_commits_elements',
221 221 pattern='/{repo_name:.*?[^/]}/commits_elements', repo_route=True)
222 222 config.add_route(
223 223 name='repo_commits_elements_file',
224 224 pattern='/{repo_name:.*?[^/]}/commits_elements/{commit_id}/{f_path:.*}', repo_route=True)
225 225
226 226 # Changelog (old deprecated name for commits page)
227 227 config.add_route(
228 228 name='repo_changelog',
229 229 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
230 230 config.add_route(
231 231 name='repo_changelog_file',
232 232 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
233 233
234 234 # Compare
235 235 config.add_route(
236 236 name='repo_compare_select',
237 237 pattern='/{repo_name:.*?[^/]}/compare', repo_route=True)
238 238
239 239 config.add_route(
240 240 name='repo_compare',
241 241 pattern='/{repo_name:.*?[^/]}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', repo_route=True)
242 242
243 243 # Tags
244 244 config.add_route(
245 245 name='tags_home',
246 246 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
247 247
248 248 # Branches
249 249 config.add_route(
250 250 name='branches_home',
251 251 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
252 252
253 253 # Bookmarks
254 254 config.add_route(
255 255 name='bookmarks_home',
256 256 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
257 257
258 258 # Forks
259 259 config.add_route(
260 260 name='repo_fork_new',
261 261 pattern='/{repo_name:.*?[^/]}/fork', repo_route=True,
262 262 repo_forbid_when_archived=True,
263 263 repo_accepted_types=['hg', 'git'])
264 264
265 265 config.add_route(
266 266 name='repo_fork_create',
267 267 pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True,
268 268 repo_forbid_when_archived=True,
269 269 repo_accepted_types=['hg', 'git'])
270 270
271 271 config.add_route(
272 272 name='repo_forks_show_all',
273 273 pattern='/{repo_name:.*?[^/]}/forks', repo_route=True,
274 274 repo_accepted_types=['hg', 'git'])
275 275 config.add_route(
276 276 name='repo_forks_data',
277 277 pattern='/{repo_name:.*?[^/]}/forks/data', repo_route=True,
278 278 repo_accepted_types=['hg', 'git'])
279 279
280 280 # Pull Requests
281 281 config.add_route(
282 282 name='pullrequest_show',
283 283 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}',
284 284 repo_route=True)
285 285
286 286 config.add_route(
287 287 name='pullrequest_show_all',
288 288 pattern='/{repo_name:.*?[^/]}/pull-request',
289 289 repo_route=True, repo_accepted_types=['hg', 'git'])
290 290
291 291 config.add_route(
292 292 name='pullrequest_show_all_data',
293 293 pattern='/{repo_name:.*?[^/]}/pull-request-data',
294 294 repo_route=True, repo_accepted_types=['hg', 'git'])
295 295
296 296 config.add_route(
297 297 name='pullrequest_repo_refs',
298 298 pattern='/{repo_name:.*?[^/]}/pull-request/refs/{target_repo_name:.*?[^/]}',
299 299 repo_route=True)
300 300
301 301 config.add_route(
302 302 name='pullrequest_repo_targets',
303 303 pattern='/{repo_name:.*?[^/]}/pull-request/repo-targets',
304 304 repo_route=True)
305 305
306 306 config.add_route(
307 307 name='pullrequest_new',
308 308 pattern='/{repo_name:.*?[^/]}/pull-request/new',
309 309 repo_route=True, repo_accepted_types=['hg', 'git'],
310 310 repo_forbid_when_archived=True)
311 311
312 312 config.add_route(
313 313 name='pullrequest_create',
314 314 pattern='/{repo_name:.*?[^/]}/pull-request/create',
315 315 repo_route=True, repo_accepted_types=['hg', 'git'],
316 316 repo_forbid_when_archived=True)
317 317
318 318 config.add_route(
319 319 name='pullrequest_update',
320 320 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
321 321 repo_route=True, repo_forbid_when_archived=True)
322 322
323 323 config.add_route(
324 324 name='pullrequest_merge',
325 325 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
326 326 repo_route=True, repo_forbid_when_archived=True)
327 327
328 328 config.add_route(
329 329 name='pullrequest_delete',
330 330 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
331 331 repo_route=True, repo_forbid_when_archived=True)
332 332
333 333 config.add_route(
334 334 name='pullrequest_comment_create',
335 335 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment',
336 336 repo_route=True)
337 337
338 338 config.add_route(
339 339 name='pullrequest_comment_edit',
340 340 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/edit',
341 341 repo_route=True, repo_accepted_types=['hg', 'git'])
342 342
343 343 config.add_route(
344 344 name='pullrequest_comment_delete',
345 345 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
346 346 repo_route=True, repo_accepted_types=['hg', 'git'])
347 347
348 348 config.add_route(
349 349 name='pullrequest_comments',
350 350 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comments',
351 351 repo_route=True)
352 352
353 353 config.add_route(
354 354 name='pullrequest_todos',
355 355 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/todos',
356 356 repo_route=True)
357 357
358 config.add_route(
359 name='pullrequest_drafts',
360 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/drafts',
361 repo_route=True)
362
358 363 # Artifacts, (EE feature)
359 364 config.add_route(
360 365 name='repo_artifacts_list',
361 366 pattern='/{repo_name:.*?[^/]}/artifacts', repo_route=True)
362 367
363 368 # Settings
364 369 config.add_route(
365 370 name='edit_repo',
366 371 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
367 372 # update is POST on edit_repo
368 373
369 374 # Settings advanced
370 375 config.add_route(
371 376 name='edit_repo_advanced',
372 377 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
373 378 config.add_route(
374 379 name='edit_repo_advanced_archive',
375 380 pattern='/{repo_name:.*?[^/]}/settings/advanced/archive', repo_route=True)
376 381 config.add_route(
377 382 name='edit_repo_advanced_delete',
378 383 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
379 384 config.add_route(
380 385 name='edit_repo_advanced_locking',
381 386 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
382 387 config.add_route(
383 388 name='edit_repo_advanced_journal',
384 389 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
385 390 config.add_route(
386 391 name='edit_repo_advanced_fork',
387 392 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
388 393
389 394 config.add_route(
390 395 name='edit_repo_advanced_hooks',
391 396 pattern='/{repo_name:.*?[^/]}/settings/advanced/hooks', repo_route=True)
392 397
393 398 # Caches
394 399 config.add_route(
395 400 name='edit_repo_caches',
396 401 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
397 402
398 403 # Permissions
399 404 config.add_route(
400 405 name='edit_repo_perms',
401 406 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
402 407
403 408 config.add_route(
404 409 name='edit_repo_perms_set_private',
405 410 pattern='/{repo_name:.*?[^/]}/settings/permissions/set_private', repo_route=True)
406 411
407 412 # Permissions Branch (EE feature)
408 413 config.add_route(
409 414 name='edit_repo_perms_branch',
410 415 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions', repo_route=True)
411 416 config.add_route(
412 417 name='edit_repo_perms_branch_delete',
413 418 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions/{rule_id}/delete',
414 419 repo_route=True)
415 420
416 421 # Maintenance
417 422 config.add_route(
418 423 name='edit_repo_maintenance',
419 424 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
420 425
421 426 config.add_route(
422 427 name='edit_repo_maintenance_execute',
423 428 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
424 429
425 430 # Fields
426 431 config.add_route(
427 432 name='edit_repo_fields',
428 433 pattern='/{repo_name:.*?[^/]}/settings/fields', repo_route=True)
429 434 config.add_route(
430 435 name='edit_repo_fields_create',
431 436 pattern='/{repo_name:.*?[^/]}/settings/fields/create', repo_route=True)
432 437 config.add_route(
433 438 name='edit_repo_fields_delete',
434 439 pattern='/{repo_name:.*?[^/]}/settings/fields/{field_id}/delete', repo_route=True)
435 440
436 441 # Locking
437 442 config.add_route(
438 443 name='repo_edit_toggle_locking',
439 444 pattern='/{repo_name:.*?[^/]}/settings/toggle_locking', repo_route=True)
440 445
441 446 # Remote
442 447 config.add_route(
443 448 name='edit_repo_remote',
444 449 pattern='/{repo_name:.*?[^/]}/settings/remote', repo_route=True)
445 450 config.add_route(
446 451 name='edit_repo_remote_pull',
447 452 pattern='/{repo_name:.*?[^/]}/settings/remote/pull', repo_route=True)
448 453 config.add_route(
449 454 name='edit_repo_remote_push',
450 455 pattern='/{repo_name:.*?[^/]}/settings/remote/push', repo_route=True)
451 456
452 457 # Statistics
453 458 config.add_route(
454 459 name='edit_repo_statistics',
455 460 pattern='/{repo_name:.*?[^/]}/settings/statistics', repo_route=True)
456 461 config.add_route(
457 462 name='edit_repo_statistics_reset',
458 463 pattern='/{repo_name:.*?[^/]}/settings/statistics/update', repo_route=True)
459 464
460 465 # Issue trackers
461 466 config.add_route(
462 467 name='edit_repo_issuetracker',
463 468 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers', repo_route=True)
464 469 config.add_route(
465 470 name='edit_repo_issuetracker_test',
466 471 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/test', repo_route=True)
467 472 config.add_route(
468 473 name='edit_repo_issuetracker_delete',
469 474 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/delete', repo_route=True)
470 475 config.add_route(
471 476 name='edit_repo_issuetracker_update',
472 477 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/update', repo_route=True)
473 478
474 479 # VCS Settings
475 480 config.add_route(
476 481 name='edit_repo_vcs',
477 482 pattern='/{repo_name:.*?[^/]}/settings/vcs', repo_route=True)
478 483 config.add_route(
479 484 name='edit_repo_vcs_update',
480 485 pattern='/{repo_name:.*?[^/]}/settings/vcs/update', repo_route=True)
481 486
482 487 # svn pattern
483 488 config.add_route(
484 489 name='edit_repo_vcs_svn_pattern_delete',
485 490 pattern='/{repo_name:.*?[^/]}/settings/vcs/svn_pattern/delete', repo_route=True)
486 491
487 492 # Repo Review Rules (EE feature)
488 493 config.add_route(
489 494 name='repo_reviewers',
490 495 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
491 496
492 497 config.add_route(
493 498 name='repo_default_reviewers_data',
494 499 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
495 500
496 501 # Repo Automation (EE feature)
497 502 config.add_route(
498 503 name='repo_automation',
499 504 pattern='/{repo_name:.*?[^/]}/settings/automation', repo_route=True)
500 505
501 506 # Strip
502 507 config.add_route(
503 508 name='edit_repo_strip',
504 509 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
505 510
506 511 config.add_route(
507 512 name='strip_check',
508 513 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
509 514
510 515 config.add_route(
511 516 name='strip_execute',
512 517 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
513 518
514 519 # Audit logs
515 520 config.add_route(
516 521 name='edit_repo_audit_logs',
517 522 pattern='/{repo_name:.*?[^/]}/settings/audit_logs', repo_route=True)
518 523
519 524 # ATOM/RSS Feed, shouldn't contain slashes for outlook compatibility
520 525 config.add_route(
521 526 name='rss_feed_home',
522 527 pattern='/{repo_name:.*?[^/]}/feed-rss', repo_route=True)
523 528
524 529 config.add_route(
525 530 name='atom_feed_home',
526 531 pattern='/{repo_name:.*?[^/]}/feed-atom', repo_route=True)
527 532
528 533 config.add_route(
529 534 name='rss_feed_home_old',
530 535 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
531 536
532 537 config.add_route(
533 538 name='atom_feed_home_old',
534 539 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
535 540
536 541 # NOTE(marcink): needs to be at the end for catch-all
537 542 add_route_with_slash(
538 543 config,
539 544 name='repo_summary',
540 545 pattern='/{repo_name:.*?[^/]}', repo_route=True)
541 546
542 547 # Scan module for configuration decorators.
543 548 config.scan('.views', ignore='.tests')
@@ -1,1851 +1,1898 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import collections
23 23
24 24 import formencode
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
32 32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33 33
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 43 from rhodecode.lib.vcs.backends.base import (
44 44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 45 from rhodecode.lib.vcs.exceptions import (
46 46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 48 from rhodecode.model.comment import CommentsModel
49 49 from rhodecode.model.db import (
50 50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 51 PullRequestReviewers)
52 52 from rhodecode.model.forms import PullRequestForm
53 53 from rhodecode.model.meta import Session
54 54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 55 from rhodecode.model.scm import ScmModel
56 56
57 57 log = logging.getLogger(__name__)
58 58
59 59
60 60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61 61
62 62 def load_default_context(self):
63 63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 66 # backward compat., we use for OLD PRs a plain renderer
67 67 c.renderer = 'plain'
68 68 return c
69 69
70 70 def _get_pull_requests_list(
71 71 self, repo_name, source, filter_type, opened_by, statuses):
72 72
73 73 draw, start, limit = self._extract_chunk(self.request)
74 74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 75 _render = self.request.get_partial_renderer(
76 76 'rhodecode:templates/data_table/_dt_elements.mako')
77 77
78 78 # pagination
79 79
80 80 if filter_type == 'awaiting_review':
81 81 pull_requests = PullRequestModel().get_awaiting_review(
82 82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
83 83 statuses=statuses, offset=start, length=limit,
84 84 order_by=order_by, order_dir=order_dir)
85 85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 86 repo_name, search_q=search_q, source=source, statuses=statuses,
87 87 opened_by=opened_by)
88 88 elif filter_type == 'awaiting_my_review':
89 89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
91 91 user_id=self._rhodecode_user.user_id, statuses=statuses,
92 92 offset=start, length=limit, order_by=order_by,
93 93 order_dir=order_dir)
94 94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
95 95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
96 96 statuses=statuses, opened_by=opened_by)
97 97 else:
98 98 pull_requests = PullRequestModel().get_all(
99 99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
100 100 statuses=statuses, offset=start, length=limit,
101 101 order_by=order_by, order_dir=order_dir)
102 102 pull_requests_total_count = PullRequestModel().count_all(
103 103 repo_name, search_q=search_q, source=source, statuses=statuses,
104 104 opened_by=opened_by)
105 105
106 106 data = []
107 107 comments_model = CommentsModel()
108 108 for pr in pull_requests:
109 109 comments_count = comments_model.get_all_comments(
110 110 self.db_repo.repo_id, pull_request=pr,
111 111 include_drafts=False, count_only=True)
112 112
113 113 data.append({
114 114 'name': _render('pullrequest_name',
115 115 pr.pull_request_id, pr.pull_request_state,
116 116 pr.work_in_progress, pr.target_repo.repo_name,
117 117 short=True),
118 118 'name_raw': pr.pull_request_id,
119 119 'status': _render('pullrequest_status',
120 120 pr.calculated_review_status()),
121 121 'title': _render('pullrequest_title', pr.title, pr.description),
122 122 'description': h.escape(pr.description),
123 123 'updated_on': _render('pullrequest_updated_on',
124 124 h.datetime_to_time(pr.updated_on),
125 125 pr.versions_count),
126 126 'updated_on_raw': h.datetime_to_time(pr.updated_on),
127 127 'created_on': _render('pullrequest_updated_on',
128 128 h.datetime_to_time(pr.created_on)),
129 129 'created_on_raw': h.datetime_to_time(pr.created_on),
130 130 'state': pr.pull_request_state,
131 131 'author': _render('pullrequest_author',
132 132 pr.author.full_contact, ),
133 133 'author_raw': pr.author.full_name,
134 134 'comments': _render('pullrequest_comments', comments_count),
135 135 'comments_raw': comments_count,
136 136 'closed': pr.is_closed(),
137 137 })
138 138
139 139 data = ({
140 140 'draw': draw,
141 141 'data': data,
142 142 'recordsTotal': pull_requests_total_count,
143 143 'recordsFiltered': pull_requests_total_count,
144 144 })
145 145 return data
146 146
147 147 @LoginRequired()
148 148 @HasRepoPermissionAnyDecorator(
149 149 'repository.read', 'repository.write', 'repository.admin')
150 150 @view_config(
151 151 route_name='pullrequest_show_all', request_method='GET',
152 152 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
153 153 def pull_request_list(self):
154 154 c = self.load_default_context()
155 155
156 156 req_get = self.request.GET
157 157 c.source = str2bool(req_get.get('source'))
158 158 c.closed = str2bool(req_get.get('closed'))
159 159 c.my = str2bool(req_get.get('my'))
160 160 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
161 161 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
162 162
163 163 c.active = 'open'
164 164 if c.my:
165 165 c.active = 'my'
166 166 if c.closed:
167 167 c.active = 'closed'
168 168 if c.awaiting_review and not c.source:
169 169 c.active = 'awaiting'
170 170 if c.source and not c.awaiting_review:
171 171 c.active = 'source'
172 172 if c.awaiting_my_review:
173 173 c.active = 'awaiting_my'
174 174
175 175 return self._get_template_context(c)
176 176
177 177 @LoginRequired()
178 178 @HasRepoPermissionAnyDecorator(
179 179 'repository.read', 'repository.write', 'repository.admin')
180 180 @view_config(
181 181 route_name='pullrequest_show_all_data', request_method='GET',
182 182 renderer='json_ext', xhr=True)
183 183 def pull_request_list_data(self):
184 184 self.load_default_context()
185 185
186 186 # additional filters
187 187 req_get = self.request.GET
188 188 source = str2bool(req_get.get('source'))
189 189 closed = str2bool(req_get.get('closed'))
190 190 my = str2bool(req_get.get('my'))
191 191 awaiting_review = str2bool(req_get.get('awaiting_review'))
192 192 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
193 193
194 194 filter_type = 'awaiting_review' if awaiting_review \
195 195 else 'awaiting_my_review' if awaiting_my_review \
196 196 else None
197 197
198 198 opened_by = None
199 199 if my:
200 200 opened_by = [self._rhodecode_user.user_id]
201 201
202 202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 203 if closed:
204 204 statuses = [PullRequest.STATUS_CLOSED]
205 205
206 206 data = self._get_pull_requests_list(
207 207 repo_name=self.db_repo_name, source=source,
208 208 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
209 209
210 210 return data
211 211
212 212 def _is_diff_cache_enabled(self, target_repo):
213 213 caching_enabled = self._get_general_setting(
214 214 target_repo, 'rhodecode_diff_cache')
215 215 log.debug('Diff caching enabled: %s', caching_enabled)
216 216 return caching_enabled
217 217
218 218 def _get_diffset(self, source_repo_name, source_repo,
219 219 ancestor_commit,
220 220 source_ref_id, target_ref_id,
221 221 target_commit, source_commit, diff_limit, file_limit,
222 222 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
223 223
224 224 if use_ancestor:
225 225 # we might want to not use it for versions
226 226 target_ref_id = ancestor_commit.raw_id
227 227
228 228 vcs_diff = PullRequestModel().get_diff(
229 229 source_repo, source_ref_id, target_ref_id,
230 230 hide_whitespace_changes, diff_context)
231 231
232 232 diff_processor = diffs.DiffProcessor(
233 233 vcs_diff, format='newdiff', diff_limit=diff_limit,
234 234 file_limit=file_limit, show_full_diff=fulldiff)
235 235
236 236 _parsed = diff_processor.prepare()
237 237
238 238 diffset = codeblocks.DiffSet(
239 239 repo_name=self.db_repo_name,
240 240 source_repo_name=source_repo_name,
241 241 source_node_getter=codeblocks.diffset_node_getter(target_commit),
242 242 target_node_getter=codeblocks.diffset_node_getter(source_commit),
243 243 )
244 244 diffset = self.path_filter.render_patchset_filtered(
245 245 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
246 246
247 247 return diffset
248 248
249 249 def _get_range_diffset(self, source_scm, source_repo,
250 250 commit1, commit2, diff_limit, file_limit,
251 251 fulldiff, hide_whitespace_changes, diff_context):
252 252 vcs_diff = source_scm.get_diff(
253 253 commit1, commit2,
254 254 ignore_whitespace=hide_whitespace_changes,
255 255 context=diff_context)
256 256
257 257 diff_processor = diffs.DiffProcessor(
258 258 vcs_diff, format='newdiff', diff_limit=diff_limit,
259 259 file_limit=file_limit, show_full_diff=fulldiff)
260 260
261 261 _parsed = diff_processor.prepare()
262 262
263 263 diffset = codeblocks.DiffSet(
264 264 repo_name=source_repo.repo_name,
265 265 source_node_getter=codeblocks.diffset_node_getter(commit1),
266 266 target_node_getter=codeblocks.diffset_node_getter(commit2))
267 267
268 268 diffset = self.path_filter.render_patchset_filtered(
269 269 diffset, _parsed, commit1.raw_id, commit2.raw_id)
270 270
271 271 return diffset
272 272
273 273 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
274 274 comments_model = CommentsModel()
275 275
276 276 # GENERAL COMMENTS with versions #
277 277 q = comments_model._all_general_comments_of_pull_request(pull_request)
278 278 q = q.order_by(ChangesetComment.comment_id.asc())
279 279 if not include_drafts:
280 280 q = q.filter(ChangesetComment.draft == false())
281 281 general_comments = q
282 282
283 283 # pick comments we want to render at current version
284 284 c.comment_versions = comments_model.aggregate_comments(
285 285 general_comments, versions, c.at_version_num)
286 286
287 287 # INLINE COMMENTS with versions #
288 288 q = comments_model._all_inline_comments_of_pull_request(pull_request)
289 289 q = q.order_by(ChangesetComment.comment_id.asc())
290 290 if not include_drafts:
291 291 q = q.filter(ChangesetComment.draft == false())
292 292 inline_comments = q
293 293
294 294 c.inline_versions = comments_model.aggregate_comments(
295 295 inline_comments, versions, c.at_version_num, inline=True)
296 296
297 297 # Comments inline+general
298 298 if c.at_version:
299 299 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
300 300 c.comments = c.comment_versions[c.at_version_num]['display']
301 301 else:
302 302 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
303 303 c.comments = c.comment_versions[c.at_version_num]['until']
304 304
305 305 return general_comments, inline_comments
306 306
307 307 @LoginRequired()
308 308 @HasRepoPermissionAnyDecorator(
309 309 'repository.read', 'repository.write', 'repository.admin')
310 310 @view_config(
311 311 route_name='pullrequest_show', request_method='GET',
312 312 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
313 313 def pull_request_show(self):
314 314 _ = self.request.translate
315 315 c = self.load_default_context()
316 316
317 317 pull_request = PullRequest.get_or_404(
318 318 self.request.matchdict['pull_request_id'])
319 319 pull_request_id = pull_request.pull_request_id
320 320
321 321 c.state_progressing = pull_request.is_state_changing()
322 322 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
323 323
324 324 _new_state = {
325 325 'created': PullRequest.STATE_CREATED,
326 326 }.get(self.request.GET.get('force_state'))
327 327
328 328 if c.is_super_admin and _new_state:
329 329 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
330 330 h.flash(
331 331 _('Pull Request state was force changed to `{}`').format(_new_state),
332 332 category='success')
333 333 Session().commit()
334 334
335 335 raise HTTPFound(h.route_path(
336 336 'pullrequest_show', repo_name=self.db_repo_name,
337 337 pull_request_id=pull_request_id))
338 338
339 339 version = self.request.GET.get('version')
340 340 from_version = self.request.GET.get('from_version') or version
341 341 merge_checks = self.request.GET.get('merge_checks')
342 342 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
343 343 force_refresh = str2bool(self.request.GET.get('force_refresh'))
344 344 c.range_diff_on = self.request.GET.get('range-diff') == "1"
345 345
346 346 # fetch global flags of ignore ws or context lines
347 347 diff_context = diffs.get_diff_context(self.request)
348 348 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
349 349
350 350 (pull_request_latest,
351 351 pull_request_at_ver,
352 352 pull_request_display_obj,
353 353 at_version) = PullRequestModel().get_pr_version(
354 354 pull_request_id, version=version)
355 355
356 356 pr_closed = pull_request_latest.is_closed()
357 357
358 358 if pr_closed and (version or from_version):
359 359 # not allow to browse versions for closed PR
360 360 raise HTTPFound(h.route_path(
361 361 'pullrequest_show', repo_name=self.db_repo_name,
362 362 pull_request_id=pull_request_id))
363 363
364 364 versions = pull_request_display_obj.versions()
365 365 # used to store per-commit range diffs
366 366 c.changes = collections.OrderedDict()
367 367
368 368 c.at_version = at_version
369 369 c.at_version_num = (at_version
370 370 if at_version and at_version != PullRequest.LATEST_VER
371 371 else None)
372 372
373 373 c.at_version_index = ChangesetComment.get_index_from_version(
374 374 c.at_version_num, versions)
375 375
376 376 (prev_pull_request_latest,
377 377 prev_pull_request_at_ver,
378 378 prev_pull_request_display_obj,
379 379 prev_at_version) = PullRequestModel().get_pr_version(
380 380 pull_request_id, version=from_version)
381 381
382 382 c.from_version = prev_at_version
383 383 c.from_version_num = (prev_at_version
384 384 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
385 385 else None)
386 386 c.from_version_index = ChangesetComment.get_index_from_version(
387 387 c.from_version_num, versions)
388 388
389 389 # define if we're in COMPARE mode or VIEW at version mode
390 390 compare = at_version != prev_at_version
391 391
392 392 # pull_requests repo_name we opened it against
393 393 # ie. target_repo must match
394 394 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
395 395 log.warning('Mismatch between the current repo: %s, and target %s',
396 396 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
397 397 raise HTTPNotFound()
398 398
399 399 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
400 400
401 401 c.pull_request = pull_request_display_obj
402 402 c.renderer = pull_request_at_ver.description_renderer or c.renderer
403 403 c.pull_request_latest = pull_request_latest
404 404
405 405 # inject latest version
406 406 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
407 407 c.versions = versions + [latest_ver]
408 408
409 409 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
410 410 c.allowed_to_change_status = False
411 411 c.allowed_to_update = False
412 412 c.allowed_to_merge = False
413 413 c.allowed_to_delete = False
414 414 c.allowed_to_comment = False
415 415 c.allowed_to_close = False
416 416 else:
417 417 can_change_status = PullRequestModel().check_user_change_status(
418 418 pull_request_at_ver, self._rhodecode_user)
419 419 c.allowed_to_change_status = can_change_status and not pr_closed
420 420
421 421 c.allowed_to_update = PullRequestModel().check_user_update(
422 422 pull_request_latest, self._rhodecode_user) and not pr_closed
423 423 c.allowed_to_merge = PullRequestModel().check_user_merge(
424 424 pull_request_latest, self._rhodecode_user) and not pr_closed
425 425 c.allowed_to_delete = PullRequestModel().check_user_delete(
426 426 pull_request_latest, self._rhodecode_user) and not pr_closed
427 427 c.allowed_to_comment = not pr_closed
428 428 c.allowed_to_close = c.allowed_to_merge and not pr_closed
429 429
430 430 c.forbid_adding_reviewers = False
431 431
432 432 if pull_request_latest.reviewer_data and \
433 433 'rules' in pull_request_latest.reviewer_data:
434 434 rules = pull_request_latest.reviewer_data['rules'] or {}
435 435 try:
436 436 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
437 437 except Exception:
438 438 pass
439 439
440 440 # check merge capabilities
441 441 _merge_check = MergeCheck.validate(
442 442 pull_request_latest, auth_user=self._rhodecode_user,
443 443 translator=self.request.translate,
444 444 force_shadow_repo_refresh=force_refresh)
445 445
446 446 c.pr_merge_errors = _merge_check.error_details
447 447 c.pr_merge_possible = not _merge_check.failed
448 448 c.pr_merge_message = _merge_check.merge_msg
449 449 c.pr_merge_source_commit = _merge_check.source_commit
450 450 c.pr_merge_target_commit = _merge_check.target_commit
451 451
452 452 c.pr_merge_info = MergeCheck.get_merge_conditions(
453 453 pull_request_latest, translator=self.request.translate)
454 454
455 455 c.pull_request_review_status = _merge_check.review_status
456 456 if merge_checks:
457 457 self.request.override_renderer = \
458 458 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
459 459 return self._get_template_context(c)
460 460
461 461 c.reviewers_count = pull_request.reviewers_count
462 462 c.observers_count = pull_request.observers_count
463 463
464 464 # reviewers and statuses
465 465 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
466 466 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
467 467 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
468 468
469 469 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
470 470 member_reviewer = h.reviewer_as_json(
471 471 member, reasons=reasons, mandatory=mandatory,
472 472 role=review_obj.role,
473 473 user_group=review_obj.rule_user_group_data()
474 474 )
475 475
476 476 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
477 477 member_reviewer['review_status'] = current_review_status
478 478 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
479 479 member_reviewer['allowed_to_update'] = c.allowed_to_update
480 480 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
481 481
482 482 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
483 483
484 484 for observer_obj, member in pull_request_at_ver.observers():
485 485 member_observer = h.reviewer_as_json(
486 486 member, reasons=[], mandatory=False,
487 487 role=observer_obj.role,
488 488 user_group=observer_obj.rule_user_group_data()
489 489 )
490 490 member_observer['allowed_to_update'] = c.allowed_to_update
491 491 c.pull_request_set_observers_data_json['observers'].append(member_observer)
492 492
493 493 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
494 494
495 495 general_comments, inline_comments = \
496 496 self.register_comments_vars(c, pull_request_latest, versions)
497 497
498 498 # TODOs
499 499 c.unresolved_comments = CommentsModel() \
500 500 .get_pull_request_unresolved_todos(pull_request_latest)
501 501 c.resolved_comments = CommentsModel() \
502 502 .get_pull_request_resolved_todos(pull_request_latest)
503 503
504 # Drafts
505 c.draft_comments = CommentsModel().get_pull_request_drafts(
506 self._rhodecode_db_user.user_id,
507 pull_request_latest)
508
504 509 # if we use version, then do not show later comments
505 510 # than current version
506 511 display_inline_comments = collections.defaultdict(
507 512 lambda: collections.defaultdict(list))
508 513 for co in inline_comments:
509 514 if c.at_version_num:
510 515 # pick comments that are at least UPTO given version, so we
511 516 # don't render comments for higher version
512 517 should_render = co.pull_request_version_id and \
513 518 co.pull_request_version_id <= c.at_version_num
514 519 else:
515 520 # showing all, for 'latest'
516 521 should_render = True
517 522
518 523 if should_render:
519 524 display_inline_comments[co.f_path][co.line_no].append(co)
520 525
521 526 # load diff data into template context, if we use compare mode then
522 527 # diff is calculated based on changes between versions of PR
523 528
524 529 source_repo = pull_request_at_ver.source_repo
525 530 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
526 531
527 532 target_repo = pull_request_at_ver.target_repo
528 533 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
529 534
530 535 if compare:
531 536 # in compare switch the diff base to latest commit from prev version
532 537 target_ref_id = prev_pull_request_display_obj.revisions[0]
533 538
534 539 # despite opening commits for bookmarks/branches/tags, we always
535 540 # convert this to rev to prevent changes after bookmark or branch change
536 541 c.source_ref_type = 'rev'
537 542 c.source_ref = source_ref_id
538 543
539 544 c.target_ref_type = 'rev'
540 545 c.target_ref = target_ref_id
541 546
542 547 c.source_repo = source_repo
543 548 c.target_repo = target_repo
544 549
545 550 c.commit_ranges = []
546 551 source_commit = EmptyCommit()
547 552 target_commit = EmptyCommit()
548 553 c.missing_requirements = False
549 554
550 555 source_scm = source_repo.scm_instance()
551 556 target_scm = target_repo.scm_instance()
552 557
553 558 shadow_scm = None
554 559 try:
555 560 shadow_scm = pull_request_latest.get_shadow_repo()
556 561 except Exception:
557 562 log.debug('Failed to get shadow repo', exc_info=True)
558 563 # try first the existing source_repo, and then shadow
559 564 # repo if we can obtain one
560 565 commits_source_repo = source_scm
561 566 if shadow_scm:
562 567 commits_source_repo = shadow_scm
563 568
564 569 c.commits_source_repo = commits_source_repo
565 570 c.ancestor = None # set it to None, to hide it from PR view
566 571
567 572 # empty version means latest, so we keep this to prevent
568 573 # double caching
569 574 version_normalized = version or PullRequest.LATEST_VER
570 575 from_version_normalized = from_version or PullRequest.LATEST_VER
571 576
572 577 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
573 578 cache_file_path = diff_cache_exist(
574 579 cache_path, 'pull_request', pull_request_id, version_normalized,
575 580 from_version_normalized, source_ref_id, target_ref_id,
576 581 hide_whitespace_changes, diff_context, c.fulldiff)
577 582
578 583 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
579 584 force_recache = self.get_recache_flag()
580 585
581 586 cached_diff = None
582 587 if caching_enabled:
583 588 cached_diff = load_cached_diff(cache_file_path)
584 589
585 590 has_proper_commit_cache = (
586 591 cached_diff and cached_diff.get('commits')
587 592 and len(cached_diff.get('commits', [])) == 5
588 593 and cached_diff.get('commits')[0]
589 594 and cached_diff.get('commits')[3])
590 595
591 596 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
592 597 diff_commit_cache = \
593 598 (ancestor_commit, commit_cache, missing_requirements,
594 599 source_commit, target_commit) = cached_diff['commits']
595 600 else:
596 601 # NOTE(marcink): we reach potentially unreachable errors when a PR has
597 602 # merge errors resulting in potentially hidden commits in the shadow repo.
598 603 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
599 604 and _merge_check.merge_response
600 605 maybe_unreachable = maybe_unreachable \
601 606 and _merge_check.merge_response.metadata.get('unresolved_files')
602 607 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
603 608 diff_commit_cache = \
604 609 (ancestor_commit, commit_cache, missing_requirements,
605 610 source_commit, target_commit) = self.get_commits(
606 611 commits_source_repo,
607 612 pull_request_at_ver,
608 613 source_commit,
609 614 source_ref_id,
610 615 source_scm,
611 616 target_commit,
612 617 target_ref_id,
613 618 target_scm,
614 619 maybe_unreachable=maybe_unreachable)
615 620
616 621 # register our commit range
617 622 for comm in commit_cache.values():
618 623 c.commit_ranges.append(comm)
619 624
620 625 c.missing_requirements = missing_requirements
621 626 c.ancestor_commit = ancestor_commit
622 627 c.statuses = source_repo.statuses(
623 628 [x.raw_id for x in c.commit_ranges])
624 629
625 630 # auto collapse if we have more than limit
626 631 collapse_limit = diffs.DiffProcessor._collapse_commits_over
627 632 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
628 633 c.compare_mode = compare
629 634
630 635 # diff_limit is the old behavior, will cut off the whole diff
631 636 # if the limit is applied otherwise will just hide the
632 637 # big files from the front-end
633 638 diff_limit = c.visual.cut_off_limit_diff
634 639 file_limit = c.visual.cut_off_limit_file
635 640
636 641 c.missing_commits = False
637 642 if (c.missing_requirements
638 643 or isinstance(source_commit, EmptyCommit)
639 644 or source_commit == target_commit):
640 645
641 646 c.missing_commits = True
642 647 else:
643 648 c.inline_comments = display_inline_comments
644 649
645 650 use_ancestor = True
646 651 if from_version_normalized != version_normalized:
647 652 use_ancestor = False
648 653
649 654 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
650 655 if not force_recache and has_proper_diff_cache:
651 656 c.diffset = cached_diff['diff']
652 657 else:
653 658 try:
654 659 c.diffset = self._get_diffset(
655 660 c.source_repo.repo_name, commits_source_repo,
656 661 c.ancestor_commit,
657 662 source_ref_id, target_ref_id,
658 663 target_commit, source_commit,
659 664 diff_limit, file_limit, c.fulldiff,
660 665 hide_whitespace_changes, diff_context,
661 666 use_ancestor=use_ancestor
662 667 )
663 668
664 669 # save cached diff
665 670 if caching_enabled:
666 671 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
667 672 except CommitDoesNotExistError:
668 673 log.exception('Failed to generate diffset')
669 674 c.missing_commits = True
670 675
671 676 if not c.missing_commits:
672 677
673 678 c.limited_diff = c.diffset.limited_diff
674 679
675 680 # calculate removed files that are bound to comments
676 681 comment_deleted_files = [
677 682 fname for fname in display_inline_comments
678 683 if fname not in c.diffset.file_stats]
679 684
680 685 c.deleted_files_comments = collections.defaultdict(dict)
681 686 for fname, per_line_comments in display_inline_comments.items():
682 687 if fname in comment_deleted_files:
683 688 c.deleted_files_comments[fname]['stats'] = 0
684 689 c.deleted_files_comments[fname]['comments'] = list()
685 690 for lno, comments in per_line_comments.items():
686 691 c.deleted_files_comments[fname]['comments'].extend(comments)
687 692
688 693 # maybe calculate the range diff
689 694 if c.range_diff_on:
690 695 # TODO(marcink): set whitespace/context
691 696 context_lcl = 3
692 697 ign_whitespace_lcl = False
693 698
694 699 for commit in c.commit_ranges:
695 700 commit2 = commit
696 701 commit1 = commit.first_parent
697 702
698 703 range_diff_cache_file_path = diff_cache_exist(
699 704 cache_path, 'diff', commit.raw_id,
700 705 ign_whitespace_lcl, context_lcl, c.fulldiff)
701 706
702 707 cached_diff = None
703 708 if caching_enabled:
704 709 cached_diff = load_cached_diff(range_diff_cache_file_path)
705 710
706 711 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
707 712 if not force_recache and has_proper_diff_cache:
708 713 diffset = cached_diff['diff']
709 714 else:
710 715 diffset = self._get_range_diffset(
711 716 commits_source_repo, source_repo,
712 717 commit1, commit2, diff_limit, file_limit,
713 718 c.fulldiff, ign_whitespace_lcl, context_lcl
714 719 )
715 720
716 721 # save cached diff
717 722 if caching_enabled:
718 723 cache_diff(range_diff_cache_file_path, diffset, None)
719 724
720 725 c.changes[commit.raw_id] = diffset
721 726
722 727 # this is a hack to properly display links, when creating PR, the
723 728 # compare view and others uses different notation, and
724 729 # compare_commits.mako renders links based on the target_repo.
725 730 # We need to swap that here to generate it properly on the html side
726 731 c.target_repo = c.source_repo
727 732
728 733 c.commit_statuses = ChangesetStatus.STATUSES
729 734
730 735 c.show_version_changes = not pr_closed
731 736 if c.show_version_changes:
732 737 cur_obj = pull_request_at_ver
733 738 prev_obj = prev_pull_request_at_ver
734 739
735 740 old_commit_ids = prev_obj.revisions
736 741 new_commit_ids = cur_obj.revisions
737 742 commit_changes = PullRequestModel()._calculate_commit_id_changes(
738 743 old_commit_ids, new_commit_ids)
739 744 c.commit_changes_summary = commit_changes
740 745
741 746 # calculate the diff for commits between versions
742 747 c.commit_changes = []
743 748
744 749 def mark(cs, fw):
745 750 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
746 751
747 752 for c_type, raw_id in mark(commit_changes.added, 'a') \
748 753 + mark(commit_changes.removed, 'r') \
749 754 + mark(commit_changes.common, 'c'):
750 755
751 756 if raw_id in commit_cache:
752 757 commit = commit_cache[raw_id]
753 758 else:
754 759 try:
755 760 commit = commits_source_repo.get_commit(raw_id)
756 761 except CommitDoesNotExistError:
757 762 # in case we fail extracting still use "dummy" commit
758 763 # for display in commit diff
759 764 commit = h.AttributeDict(
760 765 {'raw_id': raw_id,
761 766 'message': 'EMPTY or MISSING COMMIT'})
762 767 c.commit_changes.append([c_type, commit])
763 768
764 769 # current user review statuses for each version
765 770 c.review_versions = {}
766 771 is_reviewer = PullRequestModel().is_user_reviewer(
767 772 pull_request, self._rhodecode_user)
768 773 if is_reviewer:
769 774 for co in general_comments:
770 775 if co.author.user_id == self._rhodecode_user.user_id:
771 776 status = co.status_change
772 777 if status:
773 778 _ver_pr = status[0].comment.pull_request_version_id
774 779 c.review_versions[_ver_pr] = status[0]
775 780
776 781 return self._get_template_context(c)
777 782
778 783 def get_commits(
779 784 self, commits_source_repo, pull_request_at_ver, source_commit,
780 785 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
781 786 maybe_unreachable=False):
782 787
783 788 commit_cache = collections.OrderedDict()
784 789 missing_requirements = False
785 790
786 791 try:
787 792 pre_load = ["author", "date", "message", "branch", "parents"]
788 793
789 794 pull_request_commits = pull_request_at_ver.revisions
790 795 log.debug('Loading %s commits from %s',
791 796 len(pull_request_commits), commits_source_repo)
792 797
793 798 for rev in pull_request_commits:
794 799 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
795 800 maybe_unreachable=maybe_unreachable)
796 801 commit_cache[comm.raw_id] = comm
797 802
798 803 # Order here matters, we first need to get target, and then
799 804 # the source
800 805 target_commit = commits_source_repo.get_commit(
801 806 commit_id=safe_str(target_ref_id))
802 807
803 808 source_commit = commits_source_repo.get_commit(
804 809 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
805 810 except CommitDoesNotExistError:
806 811 log.warning('Failed to get commit from `{}` repo'.format(
807 812 commits_source_repo), exc_info=True)
808 813 except RepositoryRequirementError:
809 814 log.warning('Failed to get all required data from repo', exc_info=True)
810 815 missing_requirements = True
811 816
812 817 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
813 818
814 819 try:
815 820 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
816 821 except Exception:
817 822 ancestor_commit = None
818 823
819 824 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
820 825
821 826 def assure_not_empty_repo(self):
822 827 _ = self.request.translate
823 828
824 829 try:
825 830 self.db_repo.scm_instance().get_commit()
826 831 except EmptyRepositoryError:
827 832 h.flash(h.literal(_('There are no commits yet')),
828 833 category='warning')
829 834 raise HTTPFound(
830 835 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
831 836
832 837 @LoginRequired()
833 838 @NotAnonymous()
834 839 @HasRepoPermissionAnyDecorator(
835 840 'repository.read', 'repository.write', 'repository.admin')
836 841 @view_config(
837 842 route_name='pullrequest_new', request_method='GET',
838 843 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
839 844 def pull_request_new(self):
840 845 _ = self.request.translate
841 846 c = self.load_default_context()
842 847
843 848 self.assure_not_empty_repo()
844 849 source_repo = self.db_repo
845 850
846 851 commit_id = self.request.GET.get('commit')
847 852 branch_ref = self.request.GET.get('branch')
848 853 bookmark_ref = self.request.GET.get('bookmark')
849 854
850 855 try:
851 856 source_repo_data = PullRequestModel().generate_repo_data(
852 857 source_repo, commit_id=commit_id,
853 858 branch=branch_ref, bookmark=bookmark_ref,
854 859 translator=self.request.translate)
855 860 except CommitDoesNotExistError as e:
856 861 log.exception(e)
857 862 h.flash(_('Commit does not exist'), 'error')
858 863 raise HTTPFound(
859 864 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
860 865
861 866 default_target_repo = source_repo
862 867
863 868 if source_repo.parent and c.has_origin_repo_read_perm:
864 869 parent_vcs_obj = source_repo.parent.scm_instance()
865 870 if parent_vcs_obj and not parent_vcs_obj.is_empty():
866 871 # change default if we have a parent repo
867 872 default_target_repo = source_repo.parent
868 873
869 874 target_repo_data = PullRequestModel().generate_repo_data(
870 875 default_target_repo, translator=self.request.translate)
871 876
872 877 selected_source_ref = source_repo_data['refs']['selected_ref']
873 878 title_source_ref = ''
874 879 if selected_source_ref:
875 880 title_source_ref = selected_source_ref.split(':', 2)[1]
876 881 c.default_title = PullRequestModel().generate_pullrequest_title(
877 882 source=source_repo.repo_name,
878 883 source_ref=title_source_ref,
879 884 target=default_target_repo.repo_name
880 885 )
881 886
882 887 c.default_repo_data = {
883 888 'source_repo_name': source_repo.repo_name,
884 889 'source_refs_json': json.dumps(source_repo_data),
885 890 'target_repo_name': default_target_repo.repo_name,
886 891 'target_refs_json': json.dumps(target_repo_data),
887 892 }
888 893 c.default_source_ref = selected_source_ref
889 894
890 895 return self._get_template_context(c)
891 896
892 897 @LoginRequired()
893 898 @NotAnonymous()
894 899 @HasRepoPermissionAnyDecorator(
895 900 'repository.read', 'repository.write', 'repository.admin')
896 901 @view_config(
897 902 route_name='pullrequest_repo_refs', request_method='GET',
898 903 renderer='json_ext', xhr=True)
899 904 def pull_request_repo_refs(self):
900 905 self.load_default_context()
901 906 target_repo_name = self.request.matchdict['target_repo_name']
902 907 repo = Repository.get_by_repo_name(target_repo_name)
903 908 if not repo:
904 909 raise HTTPNotFound()
905 910
906 911 target_perm = HasRepoPermissionAny(
907 912 'repository.read', 'repository.write', 'repository.admin')(
908 913 target_repo_name)
909 914 if not target_perm:
910 915 raise HTTPNotFound()
911 916
912 917 return PullRequestModel().generate_repo_data(
913 918 repo, translator=self.request.translate)
914 919
915 920 @LoginRequired()
916 921 @NotAnonymous()
917 922 @HasRepoPermissionAnyDecorator(
918 923 'repository.read', 'repository.write', 'repository.admin')
919 924 @view_config(
920 925 route_name='pullrequest_repo_targets', request_method='GET',
921 926 renderer='json_ext', xhr=True)
922 927 def pullrequest_repo_targets(self):
923 928 _ = self.request.translate
924 929 filter_query = self.request.GET.get('query')
925 930
926 931 # get the parents
927 932 parent_target_repos = []
928 933 if self.db_repo.parent:
929 934 parents_query = Repository.query() \
930 935 .order_by(func.length(Repository.repo_name)) \
931 936 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
932 937
933 938 if filter_query:
934 939 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
935 940 parents_query = parents_query.filter(
936 941 Repository.repo_name.ilike(ilike_expression))
937 942 parents = parents_query.limit(20).all()
938 943
939 944 for parent in parents:
940 945 parent_vcs_obj = parent.scm_instance()
941 946 if parent_vcs_obj and not parent_vcs_obj.is_empty():
942 947 parent_target_repos.append(parent)
943 948
944 949 # get other forks, and repo itself
945 950 query = Repository.query() \
946 951 .order_by(func.length(Repository.repo_name)) \
947 952 .filter(
948 953 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
949 954 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
950 955 ) \
951 956 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
952 957
953 958 if filter_query:
954 959 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
955 960 query = query.filter(Repository.repo_name.ilike(ilike_expression))
956 961
957 962 limit = max(20 - len(parent_target_repos), 5) # not less then 5
958 963 target_repos = query.limit(limit).all()
959 964
960 965 all_target_repos = target_repos + parent_target_repos
961 966
962 967 repos = []
963 968 # This checks permissions to the repositories
964 969 for obj in ScmModel().get_repos(all_target_repos):
965 970 repos.append({
966 971 'id': obj['name'],
967 972 'text': obj['name'],
968 973 'type': 'repo',
969 974 'repo_id': obj['dbrepo']['repo_id'],
970 975 'repo_type': obj['dbrepo']['repo_type'],
971 976 'private': obj['dbrepo']['private'],
972 977
973 978 })
974 979
975 980 data = {
976 981 'more': False,
977 982 'results': [{
978 983 'text': _('Repositories'),
979 984 'children': repos
980 985 }] if repos else []
981 986 }
982 987 return data
983 988
984 989 @classmethod
985 990 def get_comment_ids(cls, post_data):
986 991 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
987 992
988 993 @LoginRequired()
989 994 @NotAnonymous()
990 995 @HasRepoPermissionAnyDecorator(
991 996 'repository.read', 'repository.write', 'repository.admin')
992 997 @view_config(
993 998 route_name='pullrequest_comments', request_method='POST',
994 999 renderer='string_html', xhr=True)
995 1000 def pullrequest_comments(self):
996 1001 self.load_default_context()
997 1002
998 1003 pull_request = PullRequest.get_or_404(
999 1004 self.request.matchdict['pull_request_id'])
1000 1005 pull_request_id = pull_request.pull_request_id
1001 1006 version = self.request.GET.get('version')
1002 1007
1003 1008 _render = self.request.get_partial_renderer(
1004 1009 'rhodecode:templates/base/sidebar.mako')
1005 1010 c = _render.get_call_context()
1006 1011
1007 1012 (pull_request_latest,
1008 1013 pull_request_at_ver,
1009 1014 pull_request_display_obj,
1010 1015 at_version) = PullRequestModel().get_pr_version(
1011 1016 pull_request_id, version=version)
1012 1017 versions = pull_request_display_obj.versions()
1013 1018 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1014 1019 c.versions = versions + [latest_ver]
1015 1020
1016 1021 c.at_version = at_version
1017 1022 c.at_version_num = (at_version
1018 1023 if at_version and at_version != PullRequest.LATEST_VER
1019 1024 else None)
1020 1025
1021 1026 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1022 1027 all_comments = c.inline_comments_flat + c.comments
1023 1028
1024 1029 existing_ids = self.get_comment_ids(self.request.POST)
1025 1030 return _render('comments_table', all_comments, len(all_comments),
1026 1031 existing_ids=existing_ids)
1027 1032
1028 1033 @LoginRequired()
1029 1034 @NotAnonymous()
1030 1035 @HasRepoPermissionAnyDecorator(
1031 1036 'repository.read', 'repository.write', 'repository.admin')
1032 1037 @view_config(
1033 1038 route_name='pullrequest_todos', request_method='POST',
1034 1039 renderer='string_html', xhr=True)
1035 1040 def pullrequest_todos(self):
1036 1041 self.load_default_context()
1037 1042
1038 1043 pull_request = PullRequest.get_or_404(
1039 1044 self.request.matchdict['pull_request_id'])
1040 1045 pull_request_id = pull_request.pull_request_id
1041 1046 version = self.request.GET.get('version')
1042 1047
1043 1048 _render = self.request.get_partial_renderer(
1044 1049 'rhodecode:templates/base/sidebar.mako')
1045 1050 c = _render.get_call_context()
1046 1051 (pull_request_latest,
1047 1052 pull_request_at_ver,
1048 1053 pull_request_display_obj,
1049 1054 at_version) = PullRequestModel().get_pr_version(
1050 1055 pull_request_id, version=version)
1051 1056 versions = pull_request_display_obj.versions()
1052 1057 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1053 1058 c.versions = versions + [latest_ver]
1054 1059
1055 1060 c.at_version = at_version
1056 1061 c.at_version_num = (at_version
1057 1062 if at_version and at_version != PullRequest.LATEST_VER
1058 1063 else None)
1059 1064
1060 1065 c.unresolved_comments = CommentsModel() \
1061 1066 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1062 1067 c.resolved_comments = CommentsModel() \
1063 1068 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1064 1069
1065 1070 all_comments = c.unresolved_comments + c.resolved_comments
1066 1071 existing_ids = self.get_comment_ids(self.request.POST)
1067 1072 return _render('comments_table', all_comments, len(c.unresolved_comments),
1068 1073 todo_comments=True, existing_ids=existing_ids)
1069 1074
1070 1075 @LoginRequired()
1071 1076 @NotAnonymous()
1072 1077 @HasRepoPermissionAnyDecorator(
1073 1078 'repository.read', 'repository.write', 'repository.admin')
1079 @view_config(
1080 route_name='pullrequest_drafts', request_method='POST',
1081 renderer='string_html', xhr=True)
1082 def pullrequest_drafts(self):
1083 self.load_default_context()
1084
1085 pull_request = PullRequest.get_or_404(
1086 self.request.matchdict['pull_request_id'])
1087 pull_request_id = pull_request.pull_request_id
1088 version = self.request.GET.get('version')
1089
1090 _render = self.request.get_partial_renderer(
1091 'rhodecode:templates/base/sidebar.mako')
1092 c = _render.get_call_context()
1093
1094 (pull_request_latest,
1095 pull_request_at_ver,
1096 pull_request_display_obj,
1097 at_version) = PullRequestModel().get_pr_version(
1098 pull_request_id, version=version)
1099 versions = pull_request_display_obj.versions()
1100 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1101 c.versions = versions + [latest_ver]
1102
1103 c.at_version = at_version
1104 c.at_version_num = (at_version
1105 if at_version and at_version != PullRequest.LATEST_VER
1106 else None)
1107
1108 c.draft_comments = CommentsModel() \
1109 .get_pull_request_drafts(self._rhodecode_db_user.user_id, pull_request)
1110
1111 all_comments = c.draft_comments
1112
1113 existing_ids = self.get_comment_ids(self.request.POST)
1114 return _render('comments_table', all_comments, len(all_comments),
1115 existing_ids=existing_ids, draft_comments=True)
1116
1117 @LoginRequired()
1118 @NotAnonymous()
1119 @HasRepoPermissionAnyDecorator(
1120 'repository.read', 'repository.write', 'repository.admin')
1074 1121 @CSRFRequired()
1075 1122 @view_config(
1076 1123 route_name='pullrequest_create', request_method='POST',
1077 1124 renderer=None)
1078 1125 def pull_request_create(self):
1079 1126 _ = self.request.translate
1080 1127 self.assure_not_empty_repo()
1081 1128 self.load_default_context()
1082 1129
1083 1130 controls = peppercorn.parse(self.request.POST.items())
1084 1131
1085 1132 try:
1086 1133 form = PullRequestForm(
1087 1134 self.request.translate, self.db_repo.repo_id)()
1088 1135 _form = form.to_python(controls)
1089 1136 except formencode.Invalid as errors:
1090 1137 if errors.error_dict.get('revisions'):
1091 1138 msg = 'Revisions: %s' % errors.error_dict['revisions']
1092 1139 elif errors.error_dict.get('pullrequest_title'):
1093 1140 msg = errors.error_dict.get('pullrequest_title')
1094 1141 else:
1095 1142 msg = _('Error creating pull request: {}').format(errors)
1096 1143 log.exception(msg)
1097 1144 h.flash(msg, 'error')
1098 1145
1099 1146 # would rather just go back to form ...
1100 1147 raise HTTPFound(
1101 1148 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1102 1149
1103 1150 source_repo = _form['source_repo']
1104 1151 source_ref = _form['source_ref']
1105 1152 target_repo = _form['target_repo']
1106 1153 target_ref = _form['target_ref']
1107 1154 commit_ids = _form['revisions'][::-1]
1108 1155 common_ancestor_id = _form['common_ancestor']
1109 1156
1110 1157 # find the ancestor for this pr
1111 1158 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1112 1159 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1113 1160
1114 1161 if not (source_db_repo or target_db_repo):
1115 1162 h.flash(_('source_repo or target repo not found'), category='error')
1116 1163 raise HTTPFound(
1117 1164 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1118 1165
1119 1166 # re-check permissions again here
1120 1167 # source_repo we must have read permissions
1121 1168
1122 1169 source_perm = HasRepoPermissionAny(
1123 1170 'repository.read', 'repository.write', 'repository.admin')(
1124 1171 source_db_repo.repo_name)
1125 1172 if not source_perm:
1126 1173 msg = _('Not Enough permissions to source repo `{}`.'.format(
1127 1174 source_db_repo.repo_name))
1128 1175 h.flash(msg, category='error')
1129 1176 # copy the args back to redirect
1130 1177 org_query = self.request.GET.mixed()
1131 1178 raise HTTPFound(
1132 1179 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1133 1180 _query=org_query))
1134 1181
1135 1182 # target repo we must have read permissions, and also later on
1136 1183 # we want to check branch permissions here
1137 1184 target_perm = HasRepoPermissionAny(
1138 1185 'repository.read', 'repository.write', 'repository.admin')(
1139 1186 target_db_repo.repo_name)
1140 1187 if not target_perm:
1141 1188 msg = _('Not Enough permissions to target repo `{}`.'.format(
1142 1189 target_db_repo.repo_name))
1143 1190 h.flash(msg, category='error')
1144 1191 # copy the args back to redirect
1145 1192 org_query = self.request.GET.mixed()
1146 1193 raise HTTPFound(
1147 1194 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1148 1195 _query=org_query))
1149 1196
1150 1197 source_scm = source_db_repo.scm_instance()
1151 1198 target_scm = target_db_repo.scm_instance()
1152 1199
1153 1200 source_ref_obj = unicode_to_reference(source_ref)
1154 1201 target_ref_obj = unicode_to_reference(target_ref)
1155 1202
1156 1203 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1157 1204 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1158 1205
1159 1206 ancestor = source_scm.get_common_ancestor(
1160 1207 source_commit.raw_id, target_commit.raw_id, target_scm)
1161 1208
1162 1209 # recalculate target ref based on ancestor
1163 1210 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1164 1211
1165 1212 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1166 1213 PullRequestModel().get_reviewer_functions()
1167 1214
1168 1215 # recalculate reviewers logic, to make sure we can validate this
1169 1216 reviewer_rules = get_default_reviewers_data(
1170 1217 self._rhodecode_db_user,
1171 1218 source_db_repo,
1172 1219 source_ref_obj,
1173 1220 target_db_repo,
1174 1221 target_ref_obj,
1175 1222 include_diff_info=False)
1176 1223
1177 1224 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1178 1225 observers = validate_observers(_form['observer_members'], reviewer_rules)
1179 1226
1180 1227 pullrequest_title = _form['pullrequest_title']
1181 1228 title_source_ref = source_ref_obj.name
1182 1229 if not pullrequest_title:
1183 1230 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1184 1231 source=source_repo,
1185 1232 source_ref=title_source_ref,
1186 1233 target=target_repo
1187 1234 )
1188 1235
1189 1236 description = _form['pullrequest_desc']
1190 1237 description_renderer = _form['description_renderer']
1191 1238
1192 1239 try:
1193 1240 pull_request = PullRequestModel().create(
1194 1241 created_by=self._rhodecode_user.user_id,
1195 1242 source_repo=source_repo,
1196 1243 source_ref=source_ref,
1197 1244 target_repo=target_repo,
1198 1245 target_ref=target_ref,
1199 1246 revisions=commit_ids,
1200 1247 common_ancestor_id=common_ancestor_id,
1201 1248 reviewers=reviewers,
1202 1249 observers=observers,
1203 1250 title=pullrequest_title,
1204 1251 description=description,
1205 1252 description_renderer=description_renderer,
1206 1253 reviewer_data=reviewer_rules,
1207 1254 auth_user=self._rhodecode_user
1208 1255 )
1209 1256 Session().commit()
1210 1257
1211 1258 h.flash(_('Successfully opened new pull request'),
1212 1259 category='success')
1213 1260 except Exception:
1214 1261 msg = _('Error occurred during creation of this pull request.')
1215 1262 log.exception(msg)
1216 1263 h.flash(msg, category='error')
1217 1264
1218 1265 # copy the args back to redirect
1219 1266 org_query = self.request.GET.mixed()
1220 1267 raise HTTPFound(
1221 1268 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1222 1269 _query=org_query))
1223 1270
1224 1271 raise HTTPFound(
1225 1272 h.route_path('pullrequest_show', repo_name=target_repo,
1226 1273 pull_request_id=pull_request.pull_request_id))
1227 1274
1228 1275 @LoginRequired()
1229 1276 @NotAnonymous()
1230 1277 @HasRepoPermissionAnyDecorator(
1231 1278 'repository.read', 'repository.write', 'repository.admin')
1232 1279 @CSRFRequired()
1233 1280 @view_config(
1234 1281 route_name='pullrequest_update', request_method='POST',
1235 1282 renderer='json_ext')
1236 1283 def pull_request_update(self):
1237 1284 pull_request = PullRequest.get_or_404(
1238 1285 self.request.matchdict['pull_request_id'])
1239 1286 _ = self.request.translate
1240 1287
1241 1288 c = self.load_default_context()
1242 1289 redirect_url = None
1243 1290
1244 1291 if pull_request.is_closed():
1245 1292 log.debug('update: forbidden because pull request is closed')
1246 1293 msg = _(u'Cannot update closed pull requests.')
1247 1294 h.flash(msg, category='error')
1248 1295 return {'response': True,
1249 1296 'redirect_url': redirect_url}
1250 1297
1251 1298 is_state_changing = pull_request.is_state_changing()
1252 1299 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1253 1300
1254 1301 # only owner or admin can update it
1255 1302 allowed_to_update = PullRequestModel().check_user_update(
1256 1303 pull_request, self._rhodecode_user)
1257 1304
1258 1305 if allowed_to_update:
1259 1306 controls = peppercorn.parse(self.request.POST.items())
1260 1307 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1261 1308
1262 1309 if 'review_members' in controls:
1263 1310 self._update_reviewers(
1264 1311 c,
1265 1312 pull_request, controls['review_members'],
1266 1313 pull_request.reviewer_data,
1267 1314 PullRequestReviewers.ROLE_REVIEWER)
1268 1315 elif 'observer_members' in controls:
1269 1316 self._update_reviewers(
1270 1317 c,
1271 1318 pull_request, controls['observer_members'],
1272 1319 pull_request.reviewer_data,
1273 1320 PullRequestReviewers.ROLE_OBSERVER)
1274 1321 elif str2bool(self.request.POST.get('update_commits', 'false')):
1275 1322 if is_state_changing:
1276 1323 log.debug('commits update: forbidden because pull request is in state %s',
1277 1324 pull_request.pull_request_state)
1278 1325 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1279 1326 u'Current state is: `{}`').format(
1280 1327 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1281 1328 h.flash(msg, category='error')
1282 1329 return {'response': True,
1283 1330 'redirect_url': redirect_url}
1284 1331
1285 1332 self._update_commits(c, pull_request)
1286 1333 if force_refresh:
1287 1334 redirect_url = h.route_path(
1288 1335 'pullrequest_show', repo_name=self.db_repo_name,
1289 1336 pull_request_id=pull_request.pull_request_id,
1290 1337 _query={"force_refresh": 1})
1291 1338 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1292 1339 self._edit_pull_request(pull_request)
1293 1340 else:
1294 1341 log.error('Unhandled update data.')
1295 1342 raise HTTPBadRequest()
1296 1343
1297 1344 return {'response': True,
1298 1345 'redirect_url': redirect_url}
1299 1346 raise HTTPForbidden()
1300 1347
1301 1348 def _edit_pull_request(self, pull_request):
1302 1349 """
1303 1350 Edit title and description
1304 1351 """
1305 1352 _ = self.request.translate
1306 1353
1307 1354 try:
1308 1355 PullRequestModel().edit(
1309 1356 pull_request,
1310 1357 self.request.POST.get('title'),
1311 1358 self.request.POST.get('description'),
1312 1359 self.request.POST.get('description_renderer'),
1313 1360 self._rhodecode_user)
1314 1361 except ValueError:
1315 1362 msg = _(u'Cannot update closed pull requests.')
1316 1363 h.flash(msg, category='error')
1317 1364 return
1318 1365 else:
1319 1366 Session().commit()
1320 1367
1321 1368 msg = _(u'Pull request title & description updated.')
1322 1369 h.flash(msg, category='success')
1323 1370 return
1324 1371
1325 1372 def _update_commits(self, c, pull_request):
1326 1373 _ = self.request.translate
1327 1374
1328 1375 with pull_request.set_state(PullRequest.STATE_UPDATING):
1329 1376 resp = PullRequestModel().update_commits(
1330 1377 pull_request, self._rhodecode_db_user)
1331 1378
1332 1379 if resp.executed:
1333 1380
1334 1381 if resp.target_changed and resp.source_changed:
1335 1382 changed = 'target and source repositories'
1336 1383 elif resp.target_changed and not resp.source_changed:
1337 1384 changed = 'target repository'
1338 1385 elif not resp.target_changed and resp.source_changed:
1339 1386 changed = 'source repository'
1340 1387 else:
1341 1388 changed = 'nothing'
1342 1389
1343 1390 msg = _(u'Pull request updated to "{source_commit_id}" with '
1344 1391 u'{count_added} added, {count_removed} removed commits. '
1345 1392 u'Source of changes: {change_source}.')
1346 1393 msg = msg.format(
1347 1394 source_commit_id=pull_request.source_ref_parts.commit_id,
1348 1395 count_added=len(resp.changes.added),
1349 1396 count_removed=len(resp.changes.removed),
1350 1397 change_source=changed)
1351 1398 h.flash(msg, category='success')
1352 1399 channelstream.pr_update_channelstream_push(
1353 1400 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1354 1401 else:
1355 1402 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1356 1403 warning_reasons = [
1357 1404 UpdateFailureReason.NO_CHANGE,
1358 1405 UpdateFailureReason.WRONG_REF_TYPE,
1359 1406 ]
1360 1407 category = 'warning' if resp.reason in warning_reasons else 'error'
1361 1408 h.flash(msg, category=category)
1362 1409
1363 1410 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1364 1411 _ = self.request.translate
1365 1412
1366 1413 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1367 1414 PullRequestModel().get_reviewer_functions()
1368 1415
1369 1416 if role == PullRequestReviewers.ROLE_REVIEWER:
1370 1417 try:
1371 1418 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1372 1419 except ValueError as e:
1373 1420 log.error('Reviewers Validation: {}'.format(e))
1374 1421 h.flash(e, category='error')
1375 1422 return
1376 1423
1377 1424 old_calculated_status = pull_request.calculated_review_status()
1378 1425 PullRequestModel().update_reviewers(
1379 1426 pull_request, reviewers, self._rhodecode_db_user)
1380 1427
1381 1428 Session().commit()
1382 1429
1383 1430 msg = _('Pull request reviewers updated.')
1384 1431 h.flash(msg, category='success')
1385 1432 channelstream.pr_update_channelstream_push(
1386 1433 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1387 1434
1388 1435 # trigger status changed if change in reviewers changes the status
1389 1436 calculated_status = pull_request.calculated_review_status()
1390 1437 if old_calculated_status != calculated_status:
1391 1438 PullRequestModel().trigger_pull_request_hook(
1392 1439 pull_request, self._rhodecode_user, 'review_status_change',
1393 1440 data={'status': calculated_status})
1394 1441
1395 1442 elif role == PullRequestReviewers.ROLE_OBSERVER:
1396 1443 try:
1397 1444 observers = validate_observers(review_members, reviewer_rules)
1398 1445 except ValueError as e:
1399 1446 log.error('Observers Validation: {}'.format(e))
1400 1447 h.flash(e, category='error')
1401 1448 return
1402 1449
1403 1450 PullRequestModel().update_observers(
1404 1451 pull_request, observers, self._rhodecode_db_user)
1405 1452
1406 1453 Session().commit()
1407 1454 msg = _('Pull request observers updated.')
1408 1455 h.flash(msg, category='success')
1409 1456 channelstream.pr_update_channelstream_push(
1410 1457 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1411 1458
1412 1459 @LoginRequired()
1413 1460 @NotAnonymous()
1414 1461 @HasRepoPermissionAnyDecorator(
1415 1462 'repository.read', 'repository.write', 'repository.admin')
1416 1463 @CSRFRequired()
1417 1464 @view_config(
1418 1465 route_name='pullrequest_merge', request_method='POST',
1419 1466 renderer='json_ext')
1420 1467 def pull_request_merge(self):
1421 1468 """
1422 1469 Merge will perform a server-side merge of the specified
1423 1470 pull request, if the pull request is approved and mergeable.
1424 1471 After successful merging, the pull request is automatically
1425 1472 closed, with a relevant comment.
1426 1473 """
1427 1474 pull_request = PullRequest.get_or_404(
1428 1475 self.request.matchdict['pull_request_id'])
1429 1476 _ = self.request.translate
1430 1477
1431 1478 if pull_request.is_state_changing():
1432 1479 log.debug('show: forbidden because pull request is in state %s',
1433 1480 pull_request.pull_request_state)
1434 1481 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1435 1482 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1436 1483 pull_request.pull_request_state)
1437 1484 h.flash(msg, category='error')
1438 1485 raise HTTPFound(
1439 1486 h.route_path('pullrequest_show',
1440 1487 repo_name=pull_request.target_repo.repo_name,
1441 1488 pull_request_id=pull_request.pull_request_id))
1442 1489
1443 1490 self.load_default_context()
1444 1491
1445 1492 with pull_request.set_state(PullRequest.STATE_UPDATING):
1446 1493 check = MergeCheck.validate(
1447 1494 pull_request, auth_user=self._rhodecode_user,
1448 1495 translator=self.request.translate)
1449 1496 merge_possible = not check.failed
1450 1497
1451 1498 for err_type, error_msg in check.errors:
1452 1499 h.flash(error_msg, category=err_type)
1453 1500
1454 1501 if merge_possible:
1455 1502 log.debug("Pre-conditions checked, trying to merge.")
1456 1503 extras = vcs_operation_context(
1457 1504 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1458 1505 username=self._rhodecode_db_user.username, action='push',
1459 1506 scm=pull_request.target_repo.repo_type)
1460 1507 with pull_request.set_state(PullRequest.STATE_UPDATING):
1461 1508 self._merge_pull_request(
1462 1509 pull_request, self._rhodecode_db_user, extras)
1463 1510 else:
1464 1511 log.debug("Pre-conditions failed, NOT merging.")
1465 1512
1466 1513 raise HTTPFound(
1467 1514 h.route_path('pullrequest_show',
1468 1515 repo_name=pull_request.target_repo.repo_name,
1469 1516 pull_request_id=pull_request.pull_request_id))
1470 1517
1471 1518 def _merge_pull_request(self, pull_request, user, extras):
1472 1519 _ = self.request.translate
1473 1520 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1474 1521
1475 1522 if merge_resp.executed:
1476 1523 log.debug("The merge was successful, closing the pull request.")
1477 1524 PullRequestModel().close_pull_request(
1478 1525 pull_request.pull_request_id, user)
1479 1526 Session().commit()
1480 1527 msg = _('Pull request was successfully merged and closed.')
1481 1528 h.flash(msg, category='success')
1482 1529 else:
1483 1530 log.debug(
1484 1531 "The merge was not successful. Merge response: %s", merge_resp)
1485 1532 msg = merge_resp.merge_status_message
1486 1533 h.flash(msg, category='error')
1487 1534
1488 1535 @LoginRequired()
1489 1536 @NotAnonymous()
1490 1537 @HasRepoPermissionAnyDecorator(
1491 1538 'repository.read', 'repository.write', 'repository.admin')
1492 1539 @CSRFRequired()
1493 1540 @view_config(
1494 1541 route_name='pullrequest_delete', request_method='POST',
1495 1542 renderer='json_ext')
1496 1543 def pull_request_delete(self):
1497 1544 _ = self.request.translate
1498 1545
1499 1546 pull_request = PullRequest.get_or_404(
1500 1547 self.request.matchdict['pull_request_id'])
1501 1548 self.load_default_context()
1502 1549
1503 1550 pr_closed = pull_request.is_closed()
1504 1551 allowed_to_delete = PullRequestModel().check_user_delete(
1505 1552 pull_request, self._rhodecode_user) and not pr_closed
1506 1553
1507 1554 # only owner can delete it !
1508 1555 if allowed_to_delete:
1509 1556 PullRequestModel().delete(pull_request, self._rhodecode_user)
1510 1557 Session().commit()
1511 1558 h.flash(_('Successfully deleted pull request'),
1512 1559 category='success')
1513 1560 raise HTTPFound(h.route_path('pullrequest_show_all',
1514 1561 repo_name=self.db_repo_name))
1515 1562
1516 1563 log.warning('user %s tried to delete pull request without access',
1517 1564 self._rhodecode_user)
1518 1565 raise HTTPNotFound()
1519 1566
1520 1567 def _pull_request_comments_create(self, pull_request, comments):
1521 1568 _ = self.request.translate
1522 1569 data = {}
1523 1570 if not comments:
1524 1571 return
1525 1572 pull_request_id = pull_request.pull_request_id
1526 1573
1527 1574 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1528 1575
1529 1576 for entry in comments:
1530 1577 c = self.load_default_context()
1531 1578 comment_type = entry['comment_type']
1532 1579 text = entry['text']
1533 1580 status = entry['status']
1534 1581 is_draft = str2bool(entry['is_draft'])
1535 1582 resolves_comment_id = entry['resolves_comment_id']
1536 1583 close_pull_request = entry['close_pull_request']
1537 1584 f_path = entry['f_path']
1538 1585 line_no = entry['line']
1539 1586 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1540 1587
1541 1588 # the logic here should work like following, if we submit close
1542 1589 # pr comment, use `close_pull_request_with_comment` function
1543 1590 # else handle regular comment logic
1544 1591
1545 1592 if close_pull_request:
1546 1593 # only owner or admin or person with write permissions
1547 1594 allowed_to_close = PullRequestModel().check_user_update(
1548 1595 pull_request, self._rhodecode_user)
1549 1596 if not allowed_to_close:
1550 1597 log.debug('comment: forbidden because not allowed to close '
1551 1598 'pull request %s', pull_request_id)
1552 1599 raise HTTPForbidden()
1553 1600
1554 1601 # This also triggers `review_status_change`
1555 1602 comment, status = PullRequestModel().close_pull_request_with_comment(
1556 1603 pull_request, self._rhodecode_user, self.db_repo, message=text,
1557 1604 auth_user=self._rhodecode_user)
1558 1605 Session().flush()
1559 1606 is_inline = comment.is_inline
1560 1607
1561 1608 PullRequestModel().trigger_pull_request_hook(
1562 1609 pull_request, self._rhodecode_user, 'comment',
1563 1610 data={'comment': comment})
1564 1611
1565 1612 else:
1566 1613 # regular comment case, could be inline, or one with status.
1567 1614 # for that one we check also permissions
1568 1615 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1569 1616 allowed_to_change_status = PullRequestModel().check_user_change_status(
1570 1617 pull_request, self._rhodecode_user) and not is_draft
1571 1618
1572 1619 if status and allowed_to_change_status:
1573 1620 message = (_('Status change %(transition_icon)s %(status)s')
1574 1621 % {'transition_icon': '>',
1575 1622 'status': ChangesetStatus.get_status_lbl(status)})
1576 1623 text = text or message
1577 1624
1578 1625 comment = CommentsModel().create(
1579 1626 text=text,
1580 1627 repo=self.db_repo.repo_id,
1581 1628 user=self._rhodecode_user.user_id,
1582 1629 pull_request=pull_request,
1583 1630 f_path=f_path,
1584 1631 line_no=line_no,
1585 1632 status_change=(ChangesetStatus.get_status_lbl(status)
1586 1633 if status and allowed_to_change_status else None),
1587 1634 status_change_type=(status
1588 1635 if status and allowed_to_change_status else None),
1589 1636 comment_type=comment_type,
1590 1637 is_draft=is_draft,
1591 1638 resolves_comment_id=resolves_comment_id,
1592 1639 auth_user=self._rhodecode_user,
1593 1640 send_email=not is_draft, # skip notification for draft comments
1594 1641 )
1595 1642 is_inline = comment.is_inline
1596 1643
1597 1644 if allowed_to_change_status:
1598 1645 # calculate old status before we change it
1599 1646 old_calculated_status = pull_request.calculated_review_status()
1600 1647
1601 1648 # get status if set !
1602 1649 if status:
1603 1650 ChangesetStatusModel().set_status(
1604 1651 self.db_repo.repo_id,
1605 1652 status,
1606 1653 self._rhodecode_user.user_id,
1607 1654 comment,
1608 1655 pull_request=pull_request
1609 1656 )
1610 1657
1611 1658 Session().flush()
1612 1659 # this is somehow required to get access to some relationship
1613 1660 # loaded on comment
1614 1661 Session().refresh(comment)
1615 1662
1616 1663 # skip notifications for drafts
1617 1664 if not is_draft:
1618 1665 PullRequestModel().trigger_pull_request_hook(
1619 1666 pull_request, self._rhodecode_user, 'comment',
1620 1667 data={'comment': comment})
1621 1668
1622 1669 # we now calculate the status of pull request, and based on that
1623 1670 # calculation we set the commits status
1624 1671 calculated_status = pull_request.calculated_review_status()
1625 1672 if old_calculated_status != calculated_status:
1626 1673 PullRequestModel().trigger_pull_request_hook(
1627 1674 pull_request, self._rhodecode_user, 'review_status_change',
1628 1675 data={'status': calculated_status})
1629 1676
1630 1677 comment_id = comment.comment_id
1631 1678 data[comment_id] = {
1632 1679 'target_id': target_elem_id
1633 1680 }
1634 1681 Session().flush()
1635 1682
1636 1683 c.co = comment
1637 1684 c.at_version_num = None
1638 1685 c.is_new = True
1639 1686 rendered_comment = render(
1640 1687 'rhodecode:templates/changeset/changeset_comment_block.mako',
1641 1688 self._get_template_context(c), self.request)
1642 1689
1643 1690 data[comment_id].update(comment.get_dict())
1644 1691 data[comment_id].update({'rendered_text': rendered_comment})
1645 1692
1646 1693 Session().commit()
1647 1694
1648 1695 # skip channelstream for draft comments
1649 1696 if not all_drafts:
1650 1697 comment_broadcast_channel = channelstream.comment_channel(
1651 1698 self.db_repo_name, pull_request_obj=pull_request)
1652 1699
1653 1700 comment_data = data
1654 1701 posted_comment_type = 'inline' if is_inline else 'general'
1655 1702 if len(data) == 1:
1656 1703 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1657 1704 else:
1658 1705 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1659 1706
1660 1707 channelstream.comment_channelstream_push(
1661 1708 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1662 1709 comment_data=comment_data)
1663 1710
1664 1711 return data
1665 1712
1666 1713 @LoginRequired()
1667 1714 @NotAnonymous()
1668 1715 @HasRepoPermissionAnyDecorator(
1669 1716 'repository.read', 'repository.write', 'repository.admin')
1670 1717 @CSRFRequired()
1671 1718 @view_config(
1672 1719 route_name='pullrequest_comment_create', request_method='POST',
1673 1720 renderer='json_ext')
1674 1721 def pull_request_comment_create(self):
1675 1722 _ = self.request.translate
1676 1723
1677 1724 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1678 1725
1679 1726 if pull_request.is_closed():
1680 1727 log.debug('comment: forbidden because pull request is closed')
1681 1728 raise HTTPForbidden()
1682 1729
1683 1730 allowed_to_comment = PullRequestModel().check_user_comment(
1684 1731 pull_request, self._rhodecode_user)
1685 1732 if not allowed_to_comment:
1686 1733 log.debug('comment: forbidden because pull request is from forbidden repo')
1687 1734 raise HTTPForbidden()
1688 1735
1689 1736 comment_data = {
1690 1737 'comment_type': self.request.POST.get('comment_type'),
1691 1738 'text': self.request.POST.get('text'),
1692 1739 'status': self.request.POST.get('changeset_status', None),
1693 1740 'is_draft': self.request.POST.get('draft'),
1694 1741 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1695 1742 'close_pull_request': self.request.POST.get('close_pull_request'),
1696 1743 'f_path': self.request.POST.get('f_path'),
1697 1744 'line': self.request.POST.get('line'),
1698 1745 }
1699 1746 data = self._pull_request_comments_create(pull_request, [comment_data])
1700 1747
1701 1748 return data
1702 1749
1703 1750 @LoginRequired()
1704 1751 @NotAnonymous()
1705 1752 @HasRepoPermissionAnyDecorator(
1706 1753 'repository.read', 'repository.write', 'repository.admin')
1707 1754 @CSRFRequired()
1708 1755 @view_config(
1709 1756 route_name='pullrequest_comment_delete', request_method='POST',
1710 1757 renderer='json_ext')
1711 1758 def pull_request_comment_delete(self):
1712 1759 pull_request = PullRequest.get_or_404(
1713 1760 self.request.matchdict['pull_request_id'])
1714 1761
1715 1762 comment = ChangesetComment.get_or_404(
1716 1763 self.request.matchdict['comment_id'])
1717 1764 comment_id = comment.comment_id
1718 1765
1719 1766 if comment.immutable:
1720 1767 # don't allow deleting comments that are immutable
1721 1768 raise HTTPForbidden()
1722 1769
1723 1770 if pull_request.is_closed():
1724 1771 log.debug('comment: forbidden because pull request is closed')
1725 1772 raise HTTPForbidden()
1726 1773
1727 1774 if not comment:
1728 1775 log.debug('Comment with id:%s not found, skipping', comment_id)
1729 1776 # comment already deleted in another call probably
1730 1777 return True
1731 1778
1732 1779 if comment.pull_request.is_closed():
1733 1780 # don't allow deleting comments on closed pull request
1734 1781 raise HTTPForbidden()
1735 1782
1736 1783 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1737 1784 super_admin = h.HasPermissionAny('hg.admin')()
1738 1785 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1739 1786 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1740 1787 comment_repo_admin = is_repo_admin and is_repo_comment
1741 1788
1742 1789 if super_admin or comment_owner or comment_repo_admin:
1743 1790 old_calculated_status = comment.pull_request.calculated_review_status()
1744 1791 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1745 1792 Session().commit()
1746 1793 calculated_status = comment.pull_request.calculated_review_status()
1747 1794 if old_calculated_status != calculated_status:
1748 1795 PullRequestModel().trigger_pull_request_hook(
1749 1796 comment.pull_request, self._rhodecode_user, 'review_status_change',
1750 1797 data={'status': calculated_status})
1751 1798 return True
1752 1799 else:
1753 1800 log.warning('No permissions for user %s to delete comment_id: %s',
1754 1801 self._rhodecode_db_user, comment_id)
1755 1802 raise HTTPNotFound()
1756 1803
1757 1804 @LoginRequired()
1758 1805 @NotAnonymous()
1759 1806 @HasRepoPermissionAnyDecorator(
1760 1807 'repository.read', 'repository.write', 'repository.admin')
1761 1808 @CSRFRequired()
1762 1809 @view_config(
1763 1810 route_name='pullrequest_comment_edit', request_method='POST',
1764 1811 renderer='json_ext')
1765 1812 def pull_request_comment_edit(self):
1766 1813 self.load_default_context()
1767 1814
1768 1815 pull_request = PullRequest.get_or_404(
1769 1816 self.request.matchdict['pull_request_id']
1770 1817 )
1771 1818 comment = ChangesetComment.get_or_404(
1772 1819 self.request.matchdict['comment_id']
1773 1820 )
1774 1821 comment_id = comment.comment_id
1775 1822
1776 1823 if comment.immutable:
1777 1824 # don't allow deleting comments that are immutable
1778 1825 raise HTTPForbidden()
1779 1826
1780 1827 if pull_request.is_closed():
1781 1828 log.debug('comment: forbidden because pull request is closed')
1782 1829 raise HTTPForbidden()
1783 1830
1784 1831 if comment.pull_request.is_closed():
1785 1832 # don't allow deleting comments on closed pull request
1786 1833 raise HTTPForbidden()
1787 1834
1788 1835 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1789 1836 super_admin = h.HasPermissionAny('hg.admin')()
1790 1837 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1791 1838 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1792 1839 comment_repo_admin = is_repo_admin and is_repo_comment
1793 1840
1794 1841 if super_admin or comment_owner or comment_repo_admin:
1795 1842 text = self.request.POST.get('text')
1796 1843 version = self.request.POST.get('version')
1797 1844 if text == comment.text:
1798 1845 log.warning(
1799 1846 'Comment(PR): '
1800 1847 'Trying to create new version '
1801 1848 'with the same comment body {}'.format(
1802 1849 comment_id,
1803 1850 )
1804 1851 )
1805 1852 raise HTTPNotFound()
1806 1853
1807 1854 if version.isdigit():
1808 1855 version = int(version)
1809 1856 else:
1810 1857 log.warning(
1811 1858 'Comment(PR): Wrong version type {} {} '
1812 1859 'for comment {}'.format(
1813 1860 version,
1814 1861 type(version),
1815 1862 comment_id,
1816 1863 )
1817 1864 )
1818 1865 raise HTTPNotFound()
1819 1866
1820 1867 try:
1821 1868 comment_history = CommentsModel().edit(
1822 1869 comment_id=comment_id,
1823 1870 text=text,
1824 1871 auth_user=self._rhodecode_user,
1825 1872 version=version,
1826 1873 )
1827 1874 except CommentVersionMismatch:
1828 1875 raise HTTPConflict()
1829 1876
1830 1877 if not comment_history:
1831 1878 raise HTTPNotFound()
1832 1879
1833 1880 Session().commit()
1834 1881 if not comment.draft:
1835 1882 PullRequestModel().trigger_pull_request_hook(
1836 1883 pull_request, self._rhodecode_user, 'comment_edit',
1837 1884 data={'comment': comment})
1838 1885
1839 1886 return {
1840 1887 'comment_history_id': comment_history.comment_history_id,
1841 1888 'comment_id': comment.comment_id,
1842 1889 'comment_version': comment_history.version,
1843 1890 'comment_author_username': comment_history.author.username,
1844 1891 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1845 1892 'comment_created_on': h.age_component(comment_history.created_on,
1846 1893 time_is_local=True),
1847 1894 }
1848 1895 else:
1849 1896 log.warning('No permissions for user %s to edit comment_id: %s',
1850 1897 self._rhodecode_db_user, comment_id)
1851 1898 raise HTTPNotFound()
@@ -1,845 +1,852 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2020 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 import datetime
25 25
26 26 import logging
27 27 import traceback
28 28 import collections
29 29
30 30 from pyramid.threadlocal import get_current_registry, get_current_request
31 31 from sqlalchemy.sql.expression import null
32 32 from sqlalchemy.sql.functions import coalesce
33 33
34 34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
35 35 from rhodecode.lib import audit_logger
36 36 from rhodecode.lib.exceptions import CommentVersionMismatch
37 37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
38 38 from rhodecode.model import BaseModel
39 39 from rhodecode.model.db import (
40 false,
40 false, true,
41 41 ChangesetComment,
42 42 User,
43 43 Notification,
44 44 PullRequest,
45 45 AttributeDict,
46 46 ChangesetCommentHistory,
47 47 )
48 48 from rhodecode.model.notification import NotificationModel
49 49 from rhodecode.model.meta import Session
50 50 from rhodecode.model.settings import VcsSettingsModel
51 51 from rhodecode.model.notification import EmailNotificationModel
52 52 from rhodecode.model.validation_schema.schemas import comment_schema
53 53
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class CommentsModel(BaseModel):
59 59
60 60 cls = ChangesetComment
61 61
62 62 DIFF_CONTEXT_BEFORE = 3
63 63 DIFF_CONTEXT_AFTER = 3
64 64
65 65 def __get_commit_comment(self, changeset_comment):
66 66 return self._get_instance(ChangesetComment, changeset_comment)
67 67
68 68 def __get_pull_request(self, pull_request):
69 69 return self._get_instance(PullRequest, pull_request)
70 70
71 71 def _extract_mentions(self, s):
72 72 user_objects = []
73 73 for username in extract_mentioned_users(s):
74 74 user_obj = User.get_by_username(username, case_insensitive=True)
75 75 if user_obj:
76 76 user_objects.append(user_obj)
77 77 return user_objects
78 78
79 79 def _get_renderer(self, global_renderer='rst', request=None):
80 80 request = request or get_current_request()
81 81
82 82 try:
83 83 global_renderer = request.call_context.visual.default_renderer
84 84 except AttributeError:
85 85 log.debug("Renderer not set, falling back "
86 86 "to default renderer '%s'", global_renderer)
87 87 except Exception:
88 88 log.error(traceback.format_exc())
89 89 return global_renderer
90 90
91 91 def aggregate_comments(self, comments, versions, show_version, inline=False):
92 92 # group by versions, and count until, and display objects
93 93
94 94 comment_groups = collections.defaultdict(list)
95 95 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
96 96
97 97 def yield_comments(pos):
98 98 for co in comment_groups[pos]:
99 99 yield co
100 100
101 101 comment_versions = collections.defaultdict(
102 102 lambda: collections.defaultdict(list))
103 103 prev_prvid = -1
104 104 # fake last entry with None, to aggregate on "latest" version which
105 105 # doesn't have an pull_request_version_id
106 106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
107 107 prvid = ver.pull_request_version_id
108 108 if prev_prvid == -1:
109 109 prev_prvid = prvid
110 110
111 111 for co in yield_comments(prvid):
112 112 comment_versions[prvid]['at'].append(co)
113 113
114 114 # save until
115 115 current = comment_versions[prvid]['at']
116 116 prev_until = comment_versions[prev_prvid]['until']
117 117 cur_until = prev_until + current
118 118 comment_versions[prvid]['until'].extend(cur_until)
119 119
120 120 # save outdated
121 121 if inline:
122 122 outdated = [x for x in cur_until
123 123 if x.outdated_at_version(show_version)]
124 124 else:
125 125 outdated = [x for x in cur_until
126 126 if x.older_than_version(show_version)]
127 127 display = [x for x in cur_until if x not in outdated]
128 128
129 129 comment_versions[prvid]['outdated'] = outdated
130 130 comment_versions[prvid]['display'] = display
131 131
132 132 prev_prvid = prvid
133 133
134 134 return comment_versions
135 135
136 136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
137 137 qry = Session().query(ChangesetComment) \
138 138 .filter(ChangesetComment.repo == repo)
139 139
140 140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
141 141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
142 142
143 143 if user:
144 144 user = self._get_user(user)
145 145 if user:
146 146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
147 147
148 148 if commit_id:
149 149 qry = qry.filter(ChangesetComment.revision == commit_id)
150 150
151 151 qry = qry.order_by(ChangesetComment.created_on)
152 152 return qry.all()
153 153
154 154 def get_repository_unresolved_todos(self, repo):
155 155 todos = Session().query(ChangesetComment) \
156 156 .filter(ChangesetComment.repo == repo) \
157 157 .filter(ChangesetComment.resolved_by == None) \
158 158 .filter(ChangesetComment.comment_type
159 159 == ChangesetComment.COMMENT_TYPE_TODO)
160 160 todos = todos.all()
161 161
162 162 return todos
163 163
164 164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
165 165
166 166 todos = Session().query(ChangesetComment) \
167 167 .filter(ChangesetComment.pull_request == pull_request) \
168 168 .filter(ChangesetComment.resolved_by == None) \
169 169 .filter(ChangesetComment.comment_type
170 170 == ChangesetComment.COMMENT_TYPE_TODO)
171 171
172 172 if not include_drafts:
173 173 todos = todos.filter(ChangesetComment.draft == false())
174 174
175 175 if not show_outdated:
176 176 todos = todos.filter(
177 177 coalesce(ChangesetComment.display_state, '') !=
178 178 ChangesetComment.COMMENT_OUTDATED)
179 179
180 180 todos = todos.all()
181 181
182 182 return todos
183 183
184 184 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
185 185
186 186 todos = Session().query(ChangesetComment) \
187 187 .filter(ChangesetComment.pull_request == pull_request) \
188 188 .filter(ChangesetComment.resolved_by != None) \
189 189 .filter(ChangesetComment.comment_type
190 190 == ChangesetComment.COMMENT_TYPE_TODO)
191 191
192 192 if not include_drafts:
193 193 todos = todos.filter(ChangesetComment.draft == false())
194 194
195 195 if not show_outdated:
196 196 todos = todos.filter(
197 197 coalesce(ChangesetComment.display_state, '') !=
198 198 ChangesetComment.COMMENT_OUTDATED)
199 199
200 200 todos = todos.all()
201 201
202 202 return todos
203 203
204 def get_pull_request_drafts(self, user_id, pull_request):
205 drafts = Session().query(ChangesetComment) \
206 .filter(ChangesetComment.pull_request == pull_request) \
207 .filter(ChangesetComment.user_id == user_id) \
208 .filter(ChangesetComment.draft == true())
209 return drafts.all()
210
204 211 def get_commit_unresolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
205 212
206 213 todos = Session().query(ChangesetComment) \
207 214 .filter(ChangesetComment.revision == commit_id) \
208 215 .filter(ChangesetComment.resolved_by == None) \
209 216 .filter(ChangesetComment.comment_type
210 217 == ChangesetComment.COMMENT_TYPE_TODO)
211 218
212 219 if not include_drafts:
213 220 todos = todos.filter(ChangesetComment.draft == false())
214 221
215 222 if not show_outdated:
216 223 todos = todos.filter(
217 224 coalesce(ChangesetComment.display_state, '') !=
218 225 ChangesetComment.COMMENT_OUTDATED)
219 226
220 227 todos = todos.all()
221 228
222 229 return todos
223 230
224 231 def get_commit_resolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
225 232
226 233 todos = Session().query(ChangesetComment) \
227 234 .filter(ChangesetComment.revision == commit_id) \
228 235 .filter(ChangesetComment.resolved_by != None) \
229 236 .filter(ChangesetComment.comment_type
230 237 == ChangesetComment.COMMENT_TYPE_TODO)
231 238
232 239 if not include_drafts:
233 240 todos = todos.filter(ChangesetComment.draft == false())
234 241
235 242 if not show_outdated:
236 243 todos = todos.filter(
237 244 coalesce(ChangesetComment.display_state, '') !=
238 245 ChangesetComment.COMMENT_OUTDATED)
239 246
240 247 todos = todos.all()
241 248
242 249 return todos
243 250
244 251 def get_commit_inline_comments(self, commit_id, include_drafts=True):
245 252 inline_comments = Session().query(ChangesetComment) \
246 253 .filter(ChangesetComment.line_no != None) \
247 254 .filter(ChangesetComment.f_path != None) \
248 255 .filter(ChangesetComment.revision == commit_id)
249 256
250 257 if not include_drafts:
251 258 inline_comments = inline_comments.filter(ChangesetComment.draft == false())
252 259
253 260 inline_comments = inline_comments.all()
254 261 return inline_comments
255 262
256 263 def _log_audit_action(self, action, action_data, auth_user, comment):
257 264 audit_logger.store(
258 265 action=action,
259 266 action_data=action_data,
260 267 user=auth_user,
261 268 repo=comment.repo)
262 269
263 270 def create(self, text, repo, user, commit_id=None, pull_request=None,
264 271 f_path=None, line_no=None, status_change=None,
265 272 status_change_type=None, comment_type=None, is_draft=False,
266 273 resolves_comment_id=None, closing_pr=False, send_email=True,
267 274 renderer=None, auth_user=None, extra_recipients=None):
268 275 """
269 276 Creates new comment for commit or pull request.
270 277 IF status_change is not none this comment is associated with a
271 278 status change of commit or commit associated with pull request
272 279
273 280 :param text:
274 281 :param repo:
275 282 :param user:
276 283 :param commit_id:
277 284 :param pull_request:
278 285 :param f_path:
279 286 :param line_no:
280 287 :param status_change: Label for status change
281 288 :param comment_type: Type of comment
282 289 :param is_draft: is comment a draft only
283 290 :param resolves_comment_id: id of comment which this one will resolve
284 291 :param status_change_type: type of status change
285 292 :param closing_pr:
286 293 :param send_email:
287 294 :param renderer: pick renderer for this comment
288 295 :param auth_user: current authenticated user calling this method
289 296 :param extra_recipients: list of extra users to be added to recipients
290 297 """
291 298
292 299 if not text:
293 300 log.warning('Missing text for comment, skipping...')
294 301 return
295 302 request = get_current_request()
296 303 _ = request.translate
297 304
298 305 if not renderer:
299 306 renderer = self._get_renderer(request=request)
300 307
301 308 repo = self._get_repo(repo)
302 309 user = self._get_user(user)
303 310 auth_user = auth_user or user
304 311
305 312 schema = comment_schema.CommentSchema()
306 313 validated_kwargs = schema.deserialize(dict(
307 314 comment_body=text,
308 315 comment_type=comment_type,
309 316 is_draft=is_draft,
310 317 comment_file=f_path,
311 318 comment_line=line_no,
312 319 renderer_type=renderer,
313 320 status_change=status_change_type,
314 321 resolves_comment_id=resolves_comment_id,
315 322 repo=repo.repo_id,
316 323 user=user.user_id,
317 324 ))
318 325 is_draft = validated_kwargs['is_draft']
319 326
320 327 comment = ChangesetComment()
321 328 comment.renderer = validated_kwargs['renderer_type']
322 329 comment.text = validated_kwargs['comment_body']
323 330 comment.f_path = validated_kwargs['comment_file']
324 331 comment.line_no = validated_kwargs['comment_line']
325 332 comment.comment_type = validated_kwargs['comment_type']
326 333 comment.draft = is_draft
327 334
328 335 comment.repo = repo
329 336 comment.author = user
330 337 resolved_comment = self.__get_commit_comment(
331 338 validated_kwargs['resolves_comment_id'])
332 339 # check if the comment actually belongs to this PR
333 340 if resolved_comment and resolved_comment.pull_request and \
334 341 resolved_comment.pull_request != pull_request:
335 342 log.warning('Comment tried to resolved unrelated todo comment: %s',
336 343 resolved_comment)
337 344 # comment not bound to this pull request, forbid
338 345 resolved_comment = None
339 346
340 347 elif resolved_comment and resolved_comment.repo and \
341 348 resolved_comment.repo != repo:
342 349 log.warning('Comment tried to resolved unrelated todo comment: %s',
343 350 resolved_comment)
344 351 # comment not bound to this repo, forbid
345 352 resolved_comment = None
346 353
347 354 comment.resolved_comment = resolved_comment
348 355
349 356 pull_request_id = pull_request
350 357
351 358 commit_obj = None
352 359 pull_request_obj = None
353 360
354 361 if commit_id:
355 362 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
356 363 # do a lookup, so we don't pass something bad here
357 364 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
358 365 comment.revision = commit_obj.raw_id
359 366
360 367 elif pull_request_id:
361 368 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
362 369 pull_request_obj = self.__get_pull_request(pull_request_id)
363 370 comment.pull_request = pull_request_obj
364 371 else:
365 372 raise Exception('Please specify commit or pull_request_id')
366 373
367 374 Session().add(comment)
368 375 Session().flush()
369 376 kwargs = {
370 377 'user': user,
371 378 'renderer_type': renderer,
372 379 'repo_name': repo.repo_name,
373 380 'status_change': status_change,
374 381 'status_change_type': status_change_type,
375 382 'comment_body': text,
376 383 'comment_file': f_path,
377 384 'comment_line': line_no,
378 385 'comment_type': comment_type or 'note',
379 386 'comment_id': comment.comment_id
380 387 }
381 388
382 389 if commit_obj:
383 390 recipients = ChangesetComment.get_users(
384 391 revision=commit_obj.raw_id)
385 392 # add commit author if it's in RhodeCode system
386 393 cs_author = User.get_from_cs_author(commit_obj.author)
387 394 if not cs_author:
388 395 # use repo owner if we cannot extract the author correctly
389 396 cs_author = repo.user
390 397 recipients += [cs_author]
391 398
392 399 commit_comment_url = self.get_url(comment, request=request)
393 400 commit_comment_reply_url = self.get_url(
394 401 comment, request=request,
395 402 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
396 403
397 404 target_repo_url = h.link_to(
398 405 repo.repo_name,
399 406 h.route_url('repo_summary', repo_name=repo.repo_name))
400 407
401 408 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
402 409 commit_id=commit_id)
403 410
404 411 # commit specifics
405 412 kwargs.update({
406 413 'commit': commit_obj,
407 414 'commit_message': commit_obj.message,
408 415 'commit_target_repo_url': target_repo_url,
409 416 'commit_comment_url': commit_comment_url,
410 417 'commit_comment_reply_url': commit_comment_reply_url,
411 418 'commit_url': commit_url,
412 419 'thread_ids': [commit_url, commit_comment_url],
413 420 })
414 421
415 422 elif pull_request_obj:
416 423 # get the current participants of this pull request
417 424 recipients = ChangesetComment.get_users(
418 425 pull_request_id=pull_request_obj.pull_request_id)
419 426 # add pull request author
420 427 recipients += [pull_request_obj.author]
421 428
422 429 # add the reviewers to notification
423 430 recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()]
424 431
425 432 pr_target_repo = pull_request_obj.target_repo
426 433 pr_source_repo = pull_request_obj.source_repo
427 434
428 435 pr_comment_url = self.get_url(comment, request=request)
429 436 pr_comment_reply_url = self.get_url(
430 437 comment, request=request,
431 438 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
432 439
433 440 pr_url = h.route_url(
434 441 'pullrequest_show',
435 442 repo_name=pr_target_repo.repo_name,
436 443 pull_request_id=pull_request_obj.pull_request_id, )
437 444
438 445 # set some variables for email notification
439 446 pr_target_repo_url = h.route_url(
440 447 'repo_summary', repo_name=pr_target_repo.repo_name)
441 448
442 449 pr_source_repo_url = h.route_url(
443 450 'repo_summary', repo_name=pr_source_repo.repo_name)
444 451
445 452 # pull request specifics
446 453 kwargs.update({
447 454 'pull_request': pull_request_obj,
448 455 'pr_id': pull_request_obj.pull_request_id,
449 456 'pull_request_url': pr_url,
450 457 'pull_request_target_repo': pr_target_repo,
451 458 'pull_request_target_repo_url': pr_target_repo_url,
452 459 'pull_request_source_repo': pr_source_repo,
453 460 'pull_request_source_repo_url': pr_source_repo_url,
454 461 'pr_comment_url': pr_comment_url,
455 462 'pr_comment_reply_url': pr_comment_reply_url,
456 463 'pr_closing': closing_pr,
457 464 'thread_ids': [pr_url, pr_comment_url],
458 465 })
459 466
460 467 if send_email:
461 468 recipients += [self._get_user(u) for u in (extra_recipients or [])]
462 469
463 470 mention_recipients = set(
464 471 self._extract_mentions(text)).difference(recipients)
465 472
466 473 # create notification objects, and emails
467 474 NotificationModel().create(
468 475 created_by=user,
469 476 notification_subject='', # Filled in based on the notification_type
470 477 notification_body='', # Filled in based on the notification_type
471 478 notification_type=notification_type,
472 479 recipients=recipients,
473 480 mention_recipients=mention_recipients,
474 481 email_kwargs=kwargs,
475 482 )
476 483
477 484 Session().flush()
478 485 if comment.pull_request:
479 486 action = 'repo.pull_request.comment.create'
480 487 else:
481 488 action = 'repo.commit.comment.create'
482 489
483 490 if not is_draft:
484 491 comment_data = comment.get_api_data()
485 492
486 493 self._log_audit_action(
487 494 action, {'data': comment_data}, auth_user, comment)
488 495
489 496 return comment
490 497
491 498 def edit(self, comment_id, text, auth_user, version):
492 499 """
493 500 Change existing comment for commit or pull request.
494 501
495 502 :param comment_id:
496 503 :param text:
497 504 :param auth_user: current authenticated user calling this method
498 505 :param version: last comment version
499 506 """
500 507 if not text:
501 508 log.warning('Missing text for comment, skipping...')
502 509 return
503 510
504 511 comment = ChangesetComment.get(comment_id)
505 512 old_comment_text = comment.text
506 513 comment.text = text
507 514 comment.modified_at = datetime.datetime.now()
508 515 version = safe_int(version)
509 516
510 517 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
511 518 # would return 3 here
512 519 comment_version = ChangesetCommentHistory.get_version(comment_id)
513 520
514 521 if isinstance(version, (int, long)) and (comment_version - version) != 1:
515 522 log.warning(
516 523 'Version mismatch comment_version {} submitted {}, skipping'.format(
517 524 comment_version-1, # -1 since note above
518 525 version
519 526 )
520 527 )
521 528 raise CommentVersionMismatch()
522 529
523 530 comment_history = ChangesetCommentHistory()
524 531 comment_history.comment_id = comment_id
525 532 comment_history.version = comment_version
526 533 comment_history.created_by_user_id = auth_user.user_id
527 534 comment_history.text = old_comment_text
528 535 # TODO add email notification
529 536 Session().add(comment_history)
530 537 Session().add(comment)
531 538 Session().flush()
532 539
533 540 if comment.pull_request:
534 541 action = 'repo.pull_request.comment.edit'
535 542 else:
536 543 action = 'repo.commit.comment.edit'
537 544
538 545 comment_data = comment.get_api_data()
539 546 comment_data['old_comment_text'] = old_comment_text
540 547 self._log_audit_action(
541 548 action, {'data': comment_data}, auth_user, comment)
542 549
543 550 return comment_history
544 551
545 552 def delete(self, comment, auth_user):
546 553 """
547 554 Deletes given comment
548 555 """
549 556 comment = self.__get_commit_comment(comment)
550 557 old_data = comment.get_api_data()
551 558 Session().delete(comment)
552 559
553 560 if comment.pull_request:
554 561 action = 'repo.pull_request.comment.delete'
555 562 else:
556 563 action = 'repo.commit.comment.delete'
557 564
558 565 self._log_audit_action(
559 566 action, {'old_data': old_data}, auth_user, comment)
560 567
561 568 return comment
562 569
563 570 def get_all_comments(self, repo_id, revision=None, pull_request=None,
564 571 include_drafts=True, count_only=False):
565 572 q = ChangesetComment.query()\
566 573 .filter(ChangesetComment.repo_id == repo_id)
567 574 if revision:
568 575 q = q.filter(ChangesetComment.revision == revision)
569 576 elif pull_request:
570 577 pull_request = self.__get_pull_request(pull_request)
571 578 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
572 579 else:
573 580 raise Exception('Please specify commit or pull_request')
574 581 if not include_drafts:
575 582 q = q.filter(ChangesetComment.draft == false())
576 583 q = q.order_by(ChangesetComment.created_on)
577 584 if count_only:
578 585 return q.count()
579 586
580 587 return q.all()
581 588
582 589 def get_url(self, comment, request=None, permalink=False, anchor=None):
583 590 if not request:
584 591 request = get_current_request()
585 592
586 593 comment = self.__get_commit_comment(comment)
587 594 if anchor is None:
588 595 anchor = 'comment-{}'.format(comment.comment_id)
589 596
590 597 if comment.pull_request:
591 598 pull_request = comment.pull_request
592 599 if permalink:
593 600 return request.route_url(
594 601 'pull_requests_global',
595 602 pull_request_id=pull_request.pull_request_id,
596 603 _anchor=anchor)
597 604 else:
598 605 return request.route_url(
599 606 'pullrequest_show',
600 607 repo_name=safe_str(pull_request.target_repo.repo_name),
601 608 pull_request_id=pull_request.pull_request_id,
602 609 _anchor=anchor)
603 610
604 611 else:
605 612 repo = comment.repo
606 613 commit_id = comment.revision
607 614
608 615 if permalink:
609 616 return request.route_url(
610 617 'repo_commit', repo_name=safe_str(repo.repo_id),
611 618 commit_id=commit_id,
612 619 _anchor=anchor)
613 620
614 621 else:
615 622 return request.route_url(
616 623 'repo_commit', repo_name=safe_str(repo.repo_name),
617 624 commit_id=commit_id,
618 625 _anchor=anchor)
619 626
620 627 def get_comments(self, repo_id, revision=None, pull_request=None):
621 628 """
622 629 Gets main comments based on revision or pull_request_id
623 630
624 631 :param repo_id:
625 632 :param revision:
626 633 :param pull_request:
627 634 """
628 635
629 636 q = ChangesetComment.query()\
630 637 .filter(ChangesetComment.repo_id == repo_id)\
631 638 .filter(ChangesetComment.line_no == None)\
632 639 .filter(ChangesetComment.f_path == None)
633 640 if revision:
634 641 q = q.filter(ChangesetComment.revision == revision)
635 642 elif pull_request:
636 643 pull_request = self.__get_pull_request(pull_request)
637 644 q = q.filter(ChangesetComment.pull_request == pull_request)
638 645 else:
639 646 raise Exception('Please specify commit or pull_request')
640 647 q = q.order_by(ChangesetComment.created_on)
641 648 return q.all()
642 649
643 650 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
644 651 q = self._get_inline_comments_query(repo_id, revision, pull_request)
645 652 return self._group_comments_by_path_and_line_number(q)
646 653
647 654 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
648 655 version=None):
649 656 inline_comms = []
650 657 for fname, per_line_comments in inline_comments.iteritems():
651 658 for lno, comments in per_line_comments.iteritems():
652 659 for comm in comments:
653 660 if not comm.outdated_at_version(version) and skip_outdated:
654 661 inline_comms.append(comm)
655 662
656 663 return inline_comms
657 664
658 665 def get_outdated_comments(self, repo_id, pull_request):
659 666 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
660 667 # of a pull request.
661 668 q = self._all_inline_comments_of_pull_request(pull_request)
662 669 q = q.filter(
663 670 ChangesetComment.display_state ==
664 671 ChangesetComment.COMMENT_OUTDATED
665 672 ).order_by(ChangesetComment.comment_id.asc())
666 673
667 674 return self._group_comments_by_path_and_line_number(q)
668 675
669 676 def _get_inline_comments_query(self, repo_id, revision, pull_request):
670 677 # TODO: johbo: Split this into two methods: One for PR and one for
671 678 # commit.
672 679 if revision:
673 680 q = Session().query(ChangesetComment).filter(
674 681 ChangesetComment.repo_id == repo_id,
675 682 ChangesetComment.line_no != null(),
676 683 ChangesetComment.f_path != null(),
677 684 ChangesetComment.revision == revision)
678 685
679 686 elif pull_request:
680 687 pull_request = self.__get_pull_request(pull_request)
681 688 if not CommentsModel.use_outdated_comments(pull_request):
682 689 q = self._visible_inline_comments_of_pull_request(pull_request)
683 690 else:
684 691 q = self._all_inline_comments_of_pull_request(pull_request)
685 692
686 693 else:
687 694 raise Exception('Please specify commit or pull_request_id')
688 695 q = q.order_by(ChangesetComment.comment_id.asc())
689 696 return q
690 697
691 698 def _group_comments_by_path_and_line_number(self, q):
692 699 comments = q.all()
693 700 paths = collections.defaultdict(lambda: collections.defaultdict(list))
694 701 for co in comments:
695 702 paths[co.f_path][co.line_no].append(co)
696 703 return paths
697 704
698 705 @classmethod
699 706 def needed_extra_diff_context(cls):
700 707 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
701 708
702 709 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
703 710 if not CommentsModel.use_outdated_comments(pull_request):
704 711 return
705 712
706 713 comments = self._visible_inline_comments_of_pull_request(pull_request)
707 714 comments_to_outdate = comments.all()
708 715
709 716 for comment in comments_to_outdate:
710 717 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
711 718
712 719 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
713 720 diff_line = _parse_comment_line_number(comment.line_no)
714 721
715 722 try:
716 723 old_context = old_diff_proc.get_context_of_line(
717 724 path=comment.f_path, diff_line=diff_line)
718 725 new_context = new_diff_proc.get_context_of_line(
719 726 path=comment.f_path, diff_line=diff_line)
720 727 except (diffs.LineNotInDiffException,
721 728 diffs.FileNotInDiffException):
722 729 if not comment.draft:
723 730 comment.display_state = ChangesetComment.COMMENT_OUTDATED
724 731 return
725 732
726 733 if old_context == new_context:
727 734 return
728 735
729 736 if self._should_relocate_diff_line(diff_line):
730 737 new_diff_lines = new_diff_proc.find_context(
731 738 path=comment.f_path, context=old_context,
732 739 offset=self.DIFF_CONTEXT_BEFORE)
733 740 if not new_diff_lines and not comment.draft:
734 741 comment.display_state = ChangesetComment.COMMENT_OUTDATED
735 742 else:
736 743 new_diff_line = self._choose_closest_diff_line(
737 744 diff_line, new_diff_lines)
738 745 comment.line_no = _diff_to_comment_line_number(new_diff_line)
739 746 else:
740 747 if not comment.draft:
741 748 comment.display_state = ChangesetComment.COMMENT_OUTDATED
742 749
743 750 def _should_relocate_diff_line(self, diff_line):
744 751 """
745 752 Checks if relocation shall be tried for the given `diff_line`.
746 753
747 754 If a comment points into the first lines, then we can have a situation
748 755 that after an update another line has been added on top. In this case
749 756 we would find the context still and move the comment around. This
750 757 would be wrong.
751 758 """
752 759 should_relocate = (
753 760 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
754 761 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
755 762 return should_relocate
756 763
757 764 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
758 765 candidate = new_diff_lines[0]
759 766 best_delta = _diff_line_delta(diff_line, candidate)
760 767 for new_diff_line in new_diff_lines[1:]:
761 768 delta = _diff_line_delta(diff_line, new_diff_line)
762 769 if delta < best_delta:
763 770 candidate = new_diff_line
764 771 best_delta = delta
765 772 return candidate
766 773
767 774 def _visible_inline_comments_of_pull_request(self, pull_request):
768 775 comments = self._all_inline_comments_of_pull_request(pull_request)
769 776 comments = comments.filter(
770 777 coalesce(ChangesetComment.display_state, '') !=
771 778 ChangesetComment.COMMENT_OUTDATED)
772 779 return comments
773 780
774 781 def _all_inline_comments_of_pull_request(self, pull_request):
775 782 comments = Session().query(ChangesetComment)\
776 783 .filter(ChangesetComment.line_no != None)\
777 784 .filter(ChangesetComment.f_path != None)\
778 785 .filter(ChangesetComment.pull_request == pull_request)
779 786 return comments
780 787
781 788 def _all_general_comments_of_pull_request(self, pull_request):
782 789 comments = Session().query(ChangesetComment)\
783 790 .filter(ChangesetComment.line_no == None)\
784 791 .filter(ChangesetComment.f_path == None)\
785 792 .filter(ChangesetComment.pull_request == pull_request)
786 793
787 794 return comments
788 795
789 796 @staticmethod
790 797 def use_outdated_comments(pull_request):
791 798 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
792 799 settings = settings_model.get_general_settings()
793 800 return settings.get('rhodecode_use_outdated_comments', False)
794 801
795 802 def trigger_commit_comment_hook(self, repo, user, action, data=None):
796 803 repo = self._get_repo(repo)
797 804 target_scm = repo.scm_instance()
798 805 if action == 'create':
799 806 trigger_hook = hooks_utils.trigger_comment_commit_hooks
800 807 elif action == 'edit':
801 808 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
802 809 else:
803 810 return
804 811
805 812 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
806 813 repo, action, trigger_hook)
807 814 trigger_hook(
808 815 username=user.username,
809 816 repo_name=repo.repo_name,
810 817 repo_type=target_scm.alias,
811 818 repo=repo,
812 819 data=data)
813 820
814 821
815 822 def _parse_comment_line_number(line_no):
816 823 """
817 824 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
818 825 """
819 826 old_line = None
820 827 new_line = None
821 828 if line_no.startswith('o'):
822 829 old_line = int(line_no[1:])
823 830 elif line_no.startswith('n'):
824 831 new_line = int(line_no[1:])
825 832 else:
826 833 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
827 834 return diffs.DiffLineNumber(old_line, new_line)
828 835
829 836
830 837 def _diff_to_comment_line_number(diff_line):
831 838 if diff_line.new is not None:
832 839 return u'n{}'.format(diff_line.new)
833 840 elif diff_line.old is not None:
834 841 return u'o{}'.format(diff_line.old)
835 842 return u''
836 843
837 844
838 845 def _diff_line_delta(a, b):
839 846 if None not in (a.new, b.new):
840 847 return abs(a.new - b.new)
841 848 elif None not in (a.old, b.old):
842 849 return abs(a.old - b.old)
843 850 else:
844 851 raise ValueError(
845 852 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,404 +1,405 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('favicon', '/favicon.ico', []);
16 16 pyroutes.register('robots', '/robots.txt', []);
17 17 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
18 18 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
19 19 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
20 20 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
21 21 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
22 22 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
23 23 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/_settings/integrations', ['repo_group_name']);
24 24 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
25 25 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
26 26 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/_settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
27 27 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/_settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
28 28 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
29 29 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
30 30 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
31 31 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
32 32 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
33 33 pyroutes.register('hovercard_user', '/_hovercard/user/%(user_id)s', ['user_id']);
34 34 pyroutes.register('hovercard_username', '/_hovercard/username/%(username)s', ['username']);
35 35 pyroutes.register('hovercard_user_group', '/_hovercard/user_group/%(user_group_id)s', ['user_group_id']);
36 36 pyroutes.register('hovercard_pull_request', '/_hovercard/pull_request/%(pull_request_id)s', ['pull_request_id']);
37 37 pyroutes.register('hovercard_repo_commit', '/_hovercard/commit/%(repo_name)s/%(commit_id)s', ['repo_name', 'commit_id']);
38 38 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
39 39 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
40 40 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
41 41 pyroutes.register('ops_ping_legacy', '/_admin/ping', []);
42 42 pyroutes.register('ops_error_test_legacy', '/_admin/error_test', []);
43 43 pyroutes.register('admin_home', '/_admin', []);
44 44 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
45 45 pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
46 46 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
47 47 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
48 48 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
49 49 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
50 50 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
51 51 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
52 52 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
53 53 pyroutes.register('admin_settings_exception_tracker', '/_admin/settings/exceptions', []);
54 54 pyroutes.register('admin_settings_exception_tracker_delete_all', '/_admin/settings/exceptions/delete', []);
55 55 pyroutes.register('admin_settings_exception_tracker_show', '/_admin/settings/exceptions/%(exception_id)s', ['exception_id']);
56 56 pyroutes.register('admin_settings_exception_tracker_delete', '/_admin/settings/exceptions/%(exception_id)s/delete', ['exception_id']);
57 57 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
58 58 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
59 59 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
60 60 pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []);
61 61 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
62 62 pyroutes.register('admin_settings_process_management_master_signal', '/_admin/settings/process_management/master_signal', []);
63 63 pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);
64 64 pyroutes.register('admin_defaults_repositories_update', '/_admin/defaults/repositories/update', []);
65 65 pyroutes.register('admin_settings', '/_admin/settings', []);
66 66 pyroutes.register('admin_settings_update', '/_admin/settings/update', []);
67 67 pyroutes.register('admin_settings_global', '/_admin/settings/global', []);
68 68 pyroutes.register('admin_settings_global_update', '/_admin/settings/global/update', []);
69 69 pyroutes.register('admin_settings_vcs', '/_admin/settings/vcs', []);
70 70 pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []);
71 71 pyroutes.register('admin_settings_vcs_svn_pattern_delete', '/_admin/settings/vcs/svn_pattern_delete', []);
72 72 pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []);
73 73 pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []);
74 74 pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []);
75 75 pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []);
76 76 pyroutes.register('admin_settings_issuetracker', '/_admin/settings/issue-tracker', []);
77 77 pyroutes.register('admin_settings_issuetracker_update', '/_admin/settings/issue-tracker/update', []);
78 78 pyroutes.register('admin_settings_issuetracker_test', '/_admin/settings/issue-tracker/test', []);
79 79 pyroutes.register('admin_settings_issuetracker_delete', '/_admin/settings/issue-tracker/delete', []);
80 80 pyroutes.register('admin_settings_email', '/_admin/settings/email', []);
81 81 pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []);
82 82 pyroutes.register('admin_settings_hooks', '/_admin/settings/hooks', []);
83 83 pyroutes.register('admin_settings_hooks_update', '/_admin/settings/hooks/update', []);
84 84 pyroutes.register('admin_settings_hooks_delete', '/_admin/settings/hooks/delete', []);
85 85 pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
86 86 pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
87 87 pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
88 88 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
89 89 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
90 90 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
91 91 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
92 92 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
93 93 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
94 94 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
95 95 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
96 96 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
97 97 pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
98 98 pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
99 99 pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
100 100 pyroutes.register('users', '/_admin/users', []);
101 101 pyroutes.register('users_data', '/_admin/users_data', []);
102 102 pyroutes.register('users_create', '/_admin/users/create', []);
103 103 pyroutes.register('users_new', '/_admin/users/new', []);
104 104 pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']);
105 105 pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']);
106 106 pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']);
107 107 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
108 108 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
109 109 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
110 110 pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
111 111 pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
112 112 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
113 113 pyroutes.register('user_notice_dismiss', '/_admin/users/%(user_id)s/notice_dismiss', ['user_id']);
114 114 pyroutes.register('edit_user_auth_tokens_view', '/_admin/users/%(user_id)s/edit/auth_tokens/view', ['user_id']);
115 115 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
116 116 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
117 117 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
118 118 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
119 119 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
120 120 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
121 121 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
122 122 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
123 123 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
124 124 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
125 125 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
126 126 pyroutes.register('edit_user_perms_summary', '/_admin/users/%(user_id)s/edit/permissions_summary', ['user_id']);
127 127 pyroutes.register('edit_user_perms_summary_json', '/_admin/users/%(user_id)s/edit/permissions_summary/json', ['user_id']);
128 128 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
129 129 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
130 130 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
131 131 pyroutes.register('edit_user_audit_logs_download', '/_admin/users/%(user_id)s/edit/audit/download', ['user_id']);
132 132 pyroutes.register('edit_user_caches', '/_admin/users/%(user_id)s/edit/caches', ['user_id']);
133 133 pyroutes.register('edit_user_caches_update', '/_admin/users/%(user_id)s/edit/caches/update', ['user_id']);
134 134 pyroutes.register('user_groups', '/_admin/user_groups', []);
135 135 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
136 136 pyroutes.register('user_groups_new', '/_admin/user_groups/new', []);
137 137 pyroutes.register('user_groups_create', '/_admin/user_groups/create', []);
138 138 pyroutes.register('repos', '/_admin/repos', []);
139 139 pyroutes.register('repos_data', '/_admin/repos_data', []);
140 140 pyroutes.register('repo_new', '/_admin/repos/new', []);
141 141 pyroutes.register('repo_create', '/_admin/repos/create', []);
142 142 pyroutes.register('repo_groups', '/_admin/repo_groups', []);
143 143 pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []);
144 144 pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
145 145 pyroutes.register('repo_group_create', '/_admin/repo_group/create', []);
146 146 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
147 147 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
148 148 pyroutes.register('channelstream_proxy', '/_channelstream', []);
149 149 pyroutes.register('upload_file', '/_file_store/upload', []);
150 150 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
151 151 pyroutes.register('download_file_by_token', '/_file_store/token-download/%(_auth_token)s/%(fid)s', ['_auth_token', 'fid']);
152 152 pyroutes.register('logout', '/_admin/logout', []);
153 153 pyroutes.register('reset_password', '/_admin/password_reset', []);
154 154 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
155 155 pyroutes.register('home', '/', []);
156 156 pyroutes.register('main_page_repos_data', '/_home_repos', []);
157 157 pyroutes.register('main_page_repo_groups_data', '/_home_repo_groups', []);
158 158 pyroutes.register('user_autocomplete_data', '/_users', []);
159 159 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
160 160 pyroutes.register('repo_list_data', '/_repos', []);
161 161 pyroutes.register('repo_group_list_data', '/_repo_groups', []);
162 162 pyroutes.register('goto_switcher_data', '/_goto_data', []);
163 163 pyroutes.register('markup_preview', '/_markup_preview', []);
164 164 pyroutes.register('file_preview', '/_file_preview', []);
165 165 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
166 166 pyroutes.register('journal', '/_admin/journal', []);
167 167 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
168 168 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
169 169 pyroutes.register('journal_public', '/_admin/public_journal', []);
170 170 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
171 171 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
172 172 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
173 173 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
174 174 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
175 175 pyroutes.register('repo_creating', '/%(repo_name)s/repo_creating', ['repo_name']);
176 176 pyroutes.register('repo_creating_check', '/%(repo_name)s/repo_creating_check', ['repo_name']);
177 177 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
178 178 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
179 179 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
180 180 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
181 181 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
182 182 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
183 183 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
184 184 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
185 185 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
186 186 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
187 187 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
188 188 pyroutes.register('repo_commit_comment_history_view', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_history_id)s/history_view', ['repo_name', 'commit_id', 'comment_history_id']);
189 189 pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
190 190 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
191 191 pyroutes.register('repo_commit_comment_edit', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/edit', ['repo_name', 'commit_id', 'comment_id']);
192 192 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
193 193 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
194 194 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
195 195 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
196 196 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
197 197 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
198 198 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
199 199 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
200 200 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
201 201 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
202 202 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
203 203 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
204 204 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
205 205 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
206 206 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
207 207 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
208 208 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
209 209 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
210 210 pyroutes.register('repo_files_check_head', '/%(repo_name)s/check_head/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
211 211 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
212 212 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
213 213 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
214 214 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
215 215 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
216 216 pyroutes.register('repo_files_upload_file', '/%(repo_name)s/upload_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
217 217 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
218 218 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
219 219 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
220 220 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
221 221 pyroutes.register('repo_commits', '/%(repo_name)s/commits', ['repo_name']);
222 222 pyroutes.register('repo_commits_file', '/%(repo_name)s/commits/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
223 223 pyroutes.register('repo_commits_elements', '/%(repo_name)s/commits_elements', ['repo_name']);
224 224 pyroutes.register('repo_commits_elements_file', '/%(repo_name)s/commits_elements/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
225 225 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
226 226 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
227 227 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
228 228 pyroutes.register('repo_compare', '/%(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']);
229 229 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
230 230 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
231 231 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
232 232 pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']);
233 233 pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']);
234 234 pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']);
235 235 pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']);
236 236 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
237 237 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
238 238 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
239 239 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
240 240 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
241 241 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
242 242 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
243 243 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
244 244 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
245 245 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
246 246 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
247 247 pyroutes.register('pullrequest_comment_edit', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/edit', ['repo_name', 'pull_request_id', 'comment_id']);
248 248 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
249 249 pyroutes.register('pullrequest_comments', '/%(repo_name)s/pull-request/%(pull_request_id)s/comments', ['repo_name', 'pull_request_id']);
250 250 pyroutes.register('pullrequest_todos', '/%(repo_name)s/pull-request/%(pull_request_id)s/todos', ['repo_name', 'pull_request_id']);
251 pyroutes.register('pullrequest_drafts', '/%(repo_name)s/pull-request/%(pull_request_id)s/drafts', ['repo_name', 'pull_request_id']);
251 252 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
252 253 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
253 254 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
254 255 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
255 256 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
256 257 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
257 258 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
258 259 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
259 260 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
260 261 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
261 262 pyroutes.register('edit_repo_perms_set_private', '/%(repo_name)s/settings/permissions/set_private', ['repo_name']);
262 263 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
263 264 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
264 265 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
265 266 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
266 267 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
267 268 pyroutes.register('repo_edit_toggle_locking', '/%(repo_name)s/settings/toggle_locking', ['repo_name']);
268 269 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
269 270 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
270 271 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
271 272 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
272 273 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
273 274 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
274 275 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
275 276 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
276 277 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
277 278 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
278 279 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
279 280 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
280 281 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
281 282 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
282 283 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
283 284 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
284 285 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
285 286 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed-rss', ['repo_name']);
286 287 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed-atom', ['repo_name']);
287 288 pyroutes.register('rss_feed_home_old', '/%(repo_name)s/feed/rss', ['repo_name']);
288 289 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
289 290 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
290 291 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
291 292 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
292 293 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
293 294 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
294 295 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
295 296 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
296 297 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
297 298 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
298 299 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
299 300 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
300 301 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['user_group_id']);
301 302 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
302 303 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
303 304 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
304 305 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['user_group_id']);
305 306 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
306 307 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
307 308 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
308 309 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
309 310 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
310 311 pyroutes.register('search', '/_admin/search', []);
311 312 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
312 313 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
313 314 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
314 315 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
315 316 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
316 317 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
317 318 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
318 319 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
319 320 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
320 321 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
321 322 pyroutes.register('my_account_auth_tokens_view', '/_admin/my_account/auth_tokens/view', []);
322 323 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
323 324 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
324 325 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
325 326 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
326 327 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
327 328 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
328 329 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
329 330 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
330 331 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
331 332 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
332 333 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
333 334 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
334 335 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
335 336 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
336 337 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
337 338 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
338 339 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
339 340 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
340 341 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
341 342 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
342 343 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
343 344 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
344 345 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
345 346 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
346 347 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
347 348 pyroutes.register('gists_show', '/_admin/gists', []);
348 349 pyroutes.register('gists_new', '/_admin/gists/new', []);
349 350 pyroutes.register('gists_create', '/_admin/gists/create', []);
350 351 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
351 352 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
352 353 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
353 354 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
354 355 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
355 356 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
356 357 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
357 358 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
358 359 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
359 360 pyroutes.register('debug_style_email', '/_admin/debug_style/email/%(email_id)s', ['email_id']);
360 361 pyroutes.register('debug_style_email_plain_rendered', '/_admin/debug_style/email-rendered/%(email_id)s', ['email_id']);
361 362 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
362 363 pyroutes.register('apiv2', '/_admin/api', []);
363 364 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
364 365 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
365 366 pyroutes.register('login', '/_admin/login', []);
366 367 pyroutes.register('register', '/_admin/register', []);
367 368 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
368 369 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
369 370 pyroutes.register('repo_reviewers_review_rule_delete', '/%(repo_name)s/settings/review/rules/%(rule_id)s/delete', ['repo_name', 'rule_id']);
370 371 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
371 372 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
372 373 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
373 374 pyroutes.register('admin_settings_scheduler_show_tasks', '/_admin/settings/scheduler/_tasks', []);
374 375 pyroutes.register('admin_settings_scheduler_show_all', '/_admin/settings/scheduler', []);
375 376 pyroutes.register('admin_settings_scheduler_new', '/_admin/settings/scheduler/new', []);
376 377 pyroutes.register('admin_settings_scheduler_create', '/_admin/settings/scheduler/create', []);
377 378 pyroutes.register('admin_settings_scheduler_edit', '/_admin/settings/scheduler/%(schedule_id)s', ['schedule_id']);
378 379 pyroutes.register('admin_settings_scheduler_update', '/_admin/settings/scheduler/%(schedule_id)s/update', ['schedule_id']);
379 380 pyroutes.register('admin_settings_scheduler_delete', '/_admin/settings/scheduler/%(schedule_id)s/delete', ['schedule_id']);
380 381 pyroutes.register('admin_settings_scheduler_execute', '/_admin/settings/scheduler/%(schedule_id)s/execute', ['schedule_id']);
381 382 pyroutes.register('admin_settings_automation', '/_admin/settings/automation', []);
382 383 pyroutes.register('admin_settings_automation_update', '/_admin/settings/automation/%(entry_id)s/update', ['entry_id']);
383 384 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
384 385 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
385 386 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
386 387 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
387 388 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
388 389 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
389 390 pyroutes.register('pullrequest_draft_comments_submit', '/%(repo_name)s/pull-request/%(pull_request_id)s/draft_comments_submit', ['repo_name', 'pull_request_id']);
390 391 pyroutes.register('commit_draft_comments_submit', '/%(repo_name)s/changeset/%(commit_id)s/draft_comments_submit', ['repo_name', 'commit_id']);
391 392 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
392 393 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
393 394 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
394 395 pyroutes.register('repo_artifacts_get', '/%(repo_name)s/artifacts/download/%(uid)s', ['repo_name', 'uid']);
395 396 pyroutes.register('repo_artifacts_store', '/%(repo_name)s/artifacts/store', ['repo_name']);
396 397 pyroutes.register('repo_artifacts_info', '/%(repo_name)s/artifacts/info/%(uid)s', ['repo_name', 'uid']);
397 398 pyroutes.register('repo_artifacts_delete', '/%(repo_name)s/artifacts/delete/%(uid)s', ['repo_name', 'uid']);
398 399 pyroutes.register('repo_artifacts_update', '/%(repo_name)s/artifacts/update/%(uid)s', ['repo_name', 'uid']);
399 400 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
400 401 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
401 402 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
402 403 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
403 404 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
404 405 }
@@ -1,1486 +1,1501 b''
1 1 // # Copyright (C) 2010-2020 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 var linkifyComments = function(comments) {
29 29 var firstCommentId = null;
30 30 if (comments) {
31 31 firstCommentId = $(comments[0]).data('comment-id');
32 32 }
33 33
34 34 if (firstCommentId){
35 35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 36 }
37 37 };
38 38
39 39 var bindToggleButtons = function() {
40 40 $('.comment-toggle').on('click', function() {
41 41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 42 });
43 43 };
44 44
45 45
46 46
47 47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
48 48 failHandler = failHandler || function() {};
49 49 postData = toQueryString(postData);
50 50 var request = $.ajax({
51 51 url: url,
52 52 type: 'POST',
53 53 data: postData,
54 54 headers: {'X-PARTIAL-XHR': true}
55 55 })
56 56 .done(function (data) {
57 57 successHandler(data);
58 58 })
59 59 .fail(function (data, textStatus, errorThrown) {
60 60 failHandler(data, textStatus, errorThrown)
61 61 });
62 62 return request;
63 63 };
64 64
65 65
66 66
67 67
68 68 /* Comment form for main and inline comments */
69 69 (function(mod) {
70 70
71 71 if (typeof exports == "object" && typeof module == "object") {
72 72 // CommonJS
73 73 module.exports = mod();
74 74 }
75 75 else {
76 76 // Plain browser env
77 77 (this || window).CommentForm = mod();
78 78 }
79 79
80 80 })(function() {
81 81 "use strict";
82 82
83 83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
84 84
85 85 if (!(this instanceof CommentForm)) {
86 86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
87 87 }
88 88
89 89 // bind the element instance to our Form
90 90 $(formElement).get(0).CommentForm = this;
91 91
92 92 this.withLineNo = function(selector) {
93 93 var lineNo = this.lineNo;
94 94 if (lineNo === undefined) {
95 95 return selector
96 96 } else {
97 97 return selector + '_' + lineNo;
98 98 }
99 99 };
100 100
101 101 this.commitId = commitId;
102 102 this.pullRequestId = pullRequestId;
103 103 this.lineNo = lineNo;
104 104 this.initAutocompleteActions = initAutocompleteActions;
105 105
106 106 this.previewButton = this.withLineNo('#preview-btn');
107 107 this.previewContainer = this.withLineNo('#preview-container');
108 108
109 109 this.previewBoxSelector = this.withLineNo('#preview-box');
110 110
111 111 this.editButton = this.withLineNo('#edit-btn');
112 112 this.editContainer = this.withLineNo('#edit-container');
113 113 this.cancelButton = this.withLineNo('#cancel-btn');
114 114 this.commentType = this.withLineNo('#comment_type');
115 115
116 116 this.resolvesId = null;
117 117 this.resolvesActionId = null;
118 118
119 119 this.closesPr = '#close_pull_request';
120 120
121 121 this.cmBox = this.withLineNo('#text');
122 122 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
123 123
124 124 this.statusChange = this.withLineNo('#change_status');
125 125
126 126 this.submitForm = formElement;
127 127
128 128 this.submitButton = $(this.submitForm).find('.submit-comment-action');
129 129 this.submitButtonText = this.submitButton.val();
130 130
131 131 this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
132 132 this.submitDraftButtonText = this.submitDraftButton.val();
133 133
134 134 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
135 135 {'repo_name': templateContext.repo_name,
136 136 'commit_id': templateContext.commit_data.commit_id});
137 137
138 138 if (edit){
139 139 this.submitDraftButton.hide();
140 140 this.submitButtonText = _gettext('Update Comment');
141 141 $(this.commentType).prop('disabled', true);
142 142 $(this.commentType).addClass('disabled');
143 143 var editInfo =
144 144 '';
145 145 $(editInfo).insertBefore($(this.editButton).parent());
146 146 }
147 147
148 148 if (resolvesCommentId){
149 149 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
150 150 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
151 151 $(this.commentType).prop('disabled', true);
152 152 $(this.commentType).addClass('disabled');
153 153
154 154 // disable select
155 155 setTimeout(function() {
156 156 $(self.statusChange).select2('readonly', true);
157 157 }, 10);
158 158
159 159 var resolvedInfo = (
160 160 '<li class="resolve-action">' +
161 161 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
162 162 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
163 163 '</li>'
164 164 ).format(resolvesCommentId, _gettext('resolve comment'));
165 165 $(resolvedInfo).insertAfter($(this.commentType).parent());
166 166 }
167 167
168 168 // based on commitId, or pullRequestId decide where do we submit
169 169 // out data
170 170 if (this.commitId){
171 171 var pyurl = 'repo_commit_comment_create';
172 172 if(edit){
173 173 pyurl = 'repo_commit_comment_edit';
174 174 }
175 175 this.submitUrl = pyroutes.url(pyurl,
176 176 {'repo_name': templateContext.repo_name,
177 177 'commit_id': this.commitId,
178 178 'comment_id': comment_id});
179 179 this.selfUrl = pyroutes.url('repo_commit',
180 180 {'repo_name': templateContext.repo_name,
181 181 'commit_id': this.commitId});
182 182
183 183 } else if (this.pullRequestId) {
184 184 var pyurl = 'pullrequest_comment_create';
185 185 if(edit){
186 186 pyurl = 'pullrequest_comment_edit';
187 187 }
188 188 this.submitUrl = pyroutes.url(pyurl,
189 189 {'repo_name': templateContext.repo_name,
190 190 'pull_request_id': this.pullRequestId,
191 191 'comment_id': comment_id});
192 192 this.selfUrl = pyroutes.url('pullrequest_show',
193 193 {'repo_name': templateContext.repo_name,
194 194 'pull_request_id': this.pullRequestId});
195 195
196 196 } else {
197 197 throw new Error(
198 198 'CommentForm requires pullRequestId, or commitId to be specified.')
199 199 }
200 200
201 201 // FUNCTIONS and helpers
202 202 var self = this;
203 203
204 204 this.isInline = function(){
205 205 return this.lineNo && this.lineNo != 'general';
206 206 };
207 207
208 208 this.getCmInstance = function(){
209 209 return this.cm
210 210 };
211 211
212 212 this.setPlaceholder = function(placeholder) {
213 213 var cm = this.getCmInstance();
214 214 if (cm){
215 215 cm.setOption('placeholder', placeholder);
216 216 }
217 217 };
218 218
219 219 this.getCommentStatus = function() {
220 220 return $(this.submitForm).find(this.statusChange).val();
221 221 };
222 222
223 223 this.getCommentType = function() {
224 224 return $(this.submitForm).find(this.commentType).val();
225 225 };
226 226
227 227 this.getDraftState = function () {
228 228 var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
229 229 var data = $(submitterElem).data('isDraft');
230 230 return data
231 231 }
232 232
233 233 this.getResolvesId = function() {
234 234 return $(this.submitForm).find(this.resolvesId).val() || null;
235 235 };
236 236
237 237 this.getClosePr = function() {
238 238 return $(this.submitForm).find(this.closesPr).val() || null;
239 239 };
240 240
241 241 this.markCommentResolved = function(resolvedCommentId){
242 242 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
243 243 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
244 244 };
245 245
246 246 this.isAllowedToSubmit = function() {
247 247 var commentDisabled = $(this.submitButton).prop('disabled');
248 248 var draftDisabled = $(this.submitDraftButton).prop('disabled');
249 249 return !commentDisabled && !draftDisabled;
250 250 };
251 251
252 252 this.initStatusChangeSelector = function(){
253 253 var formatChangeStatus = function(state, escapeMarkup) {
254 254 var originalOption = state.element;
255 255 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
256 256 return tmpl
257 257 };
258 258 var formatResult = function(result, container, query, escapeMarkup) {
259 259 return formatChangeStatus(result, escapeMarkup);
260 260 };
261 261
262 262 var formatSelection = function(data, container, escapeMarkup) {
263 263 return formatChangeStatus(data, escapeMarkup);
264 264 };
265 265
266 266 $(this.submitForm).find(this.statusChange).select2({
267 267 placeholder: _gettext('Status Review'),
268 268 formatResult: formatResult,
269 269 formatSelection: formatSelection,
270 270 containerCssClass: "drop-menu status_box_menu",
271 271 dropdownCssClass: "drop-menu-dropdown",
272 272 dropdownAutoWidth: true,
273 273 minimumResultsForSearch: -1
274 274 });
275 275
276 276 $(this.submitForm).find(this.statusChange).on('change', function() {
277 277 var status = self.getCommentStatus();
278 278
279 279 if (status && !self.isInline()) {
280 280 $(self.submitButton).prop('disabled', false);
281 281 $(self.submitDraftButton).prop('disabled', false);
282 282 }
283 283
284 284 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
285 285 self.setPlaceholder(placeholderText)
286 286 })
287 287 };
288 288
289 289 // reset the comment form into it's original state
290 290 this.resetCommentFormState = function(content) {
291 291 content = content || '';
292 292
293 293 $(this.editContainer).show();
294 294 $(this.editButton).parent().addClass('active');
295 295
296 296 $(this.previewContainer).hide();
297 297 $(this.previewButton).parent().removeClass('active');
298 298
299 299 this.setActionButtonsDisabled(true);
300 300 self.cm.setValue(content);
301 301 self.cm.setOption("readOnly", false);
302 302
303 303 if (this.resolvesId) {
304 304 // destroy the resolve action
305 305 $(this.resolvesId).parent().remove();
306 306 }
307 307 // reset closingPR flag
308 308 $('.close-pr-input').remove();
309 309
310 310 $(this.statusChange).select2('readonly', false);
311 311 };
312 312
313 313 this.globalSubmitSuccessCallback = function(comment){
314 314 // default behaviour is to call GLOBAL hook, if it's registered.
315 315 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
316 316 commentFormGlobalSubmitSuccessCallback(comment);
317 317 }
318 318 };
319 319
320 320 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
321 321 return _submitAjaxPOST(url, postData, successHandler, failHandler);
322 322 };
323 323
324 324 // overwrite a submitHandler, we need to do it for inline comments
325 325 this.setHandleFormSubmit = function(callback) {
326 326 this.handleFormSubmit = callback;
327 327 };
328 328
329 329 // overwrite a submitSuccessHandler
330 330 this.setGlobalSubmitSuccessCallback = function(callback) {
331 331 this.globalSubmitSuccessCallback = callback;
332 332 };
333 333
334 334 // default handler for for submit for main comments
335 335 this.handleFormSubmit = function() {
336 336 var text = self.cm.getValue();
337 337 var status = self.getCommentStatus();
338 338 var commentType = self.getCommentType();
339 339 var isDraft = self.getDraftState();
340 340 var resolvesCommentId = self.getResolvesId();
341 341 var closePullRequest = self.getClosePr();
342 342
343 343 if (text === "" && !status) {
344 344 return;
345 345 }
346 346
347 347 var excludeCancelBtn = false;
348 348 var submitEvent = true;
349 349 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
350 350 self.cm.setOption("readOnly", true);
351 351
352 352 var postData = {
353 353 'text': text,
354 354 'changeset_status': status,
355 355 'comment_type': commentType,
356 356 'csrf_token': CSRF_TOKEN
357 357 };
358 358
359 359 if (resolvesCommentId) {
360 360 postData['resolves_comment_id'] = resolvesCommentId;
361 361 }
362 362
363 363 if (closePullRequest) {
364 364 postData['close_pull_request'] = true;
365 365 }
366 366
367 367 // submitSuccess for general comments
368 368 var submitSuccessCallback = function(json_data) {
369 369 // reload page if we change status for single commit.
370 370 if (status && self.commitId) {
371 371 location.reload(true);
372 372 } else {
373 373 // inject newly created comments, json_data is {<comment_id>: {}}
374 374 self.attachGeneralComment(json_data)
375 375
376 376 self.resetCommentFormState();
377 377 timeagoActivate();
378 378 tooltipActivate();
379 379
380 380 // mark visually which comment was resolved
381 381 if (resolvesCommentId) {
382 382 self.markCommentResolved(resolvesCommentId);
383 383 }
384 384 }
385 385
386 386 // run global callback on submit
387 387 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
388 388
389 389 };
390 390 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
391 391 var prefix = "Error while submitting comment.\n"
392 392 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
393 393 ajaxErrorSwal(message);
394 394 self.resetCommentFormState(text);
395 395 };
396 396 self.submitAjaxPOST(
397 397 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
398 398 };
399 399
400 400 this.previewSuccessCallback = function(o) {
401 401 $(self.previewBoxSelector).html(o);
402 402 $(self.previewBoxSelector).removeClass('unloaded');
403 403
404 404 // swap buttons, making preview active
405 405 $(self.previewButton).parent().addClass('active');
406 406 $(self.editButton).parent().removeClass('active');
407 407
408 408 // unlock buttons
409 409 self.setActionButtonsDisabled(false);
410 410 };
411 411
412 412 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
413 413 excludeCancelBtn = excludeCancelBtn || false;
414 414 submitEvent = submitEvent || false;
415 415
416 416 $(this.editButton).prop('disabled', state);
417 417 $(this.previewButton).prop('disabled', state);
418 418
419 419 if (!excludeCancelBtn) {
420 420 $(this.cancelButton).prop('disabled', state);
421 421 }
422 422
423 423 var submitState = state;
424 424 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
425 425 // if the value of commit review status is set, we allow
426 426 // submit button, but only on Main form, isInline means inline
427 427 submitState = false
428 428 }
429 429
430 430 $(this.submitButton).prop('disabled', submitState);
431 431 $(this.submitDraftButton).prop('disabled', submitState);
432 432
433 433 if (submitEvent) {
434 434 var isDraft = self.getDraftState();
435 435
436 436 if (isDraft) {
437 437 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
438 438 } else {
439 439 $(this.submitButton).val(_gettext('Submitting...'));
440 440 }
441 441
442 442 } else {
443 443 $(this.submitButton).val(this.submitButtonText);
444 444 $(this.submitDraftButton).val(this.submitDraftButtonText);
445 445 }
446 446
447 447 };
448 448
449 449 // lock preview/edit/submit buttons on load, but exclude cancel button
450 450 var excludeCancelBtn = true;
451 451 this.setActionButtonsDisabled(true, excludeCancelBtn);
452 452
453 453 // anonymous users don't have access to initialized CM instance
454 454 if (this.cm !== undefined){
455 455 this.cm.on('change', function(cMirror) {
456 456 if (cMirror.getValue() === "") {
457 457 self.setActionButtonsDisabled(true, excludeCancelBtn)
458 458 } else {
459 459 self.setActionButtonsDisabled(false, excludeCancelBtn)
460 460 }
461 461 });
462 462 }
463 463
464 464 $(this.editButton).on('click', function(e) {
465 465 e.preventDefault();
466 466
467 467 $(self.previewButton).parent().removeClass('active');
468 468 $(self.previewContainer).hide();
469 469
470 470 $(self.editButton).parent().addClass('active');
471 471 $(self.editContainer).show();
472 472
473 473 });
474 474
475 475 $(this.previewButton).on('click', function(e) {
476 476 e.preventDefault();
477 477 var text = self.cm.getValue();
478 478
479 479 if (text === "") {
480 480 return;
481 481 }
482 482
483 483 var postData = {
484 484 'text': text,
485 485 'renderer': templateContext.visual.default_renderer,
486 486 'csrf_token': CSRF_TOKEN
487 487 };
488 488
489 489 // lock ALL buttons on preview
490 490 self.setActionButtonsDisabled(true);
491 491
492 492 $(self.previewBoxSelector).addClass('unloaded');
493 493 $(self.previewBoxSelector).html(_gettext('Loading ...'));
494 494
495 495 $(self.editContainer).hide();
496 496 $(self.previewContainer).show();
497 497
498 498 // by default we reset state of comment preserving the text
499 499 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
500 500 var prefix = "Error while preview of comment.\n"
501 501 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
502 502 ajaxErrorSwal(message);
503 503
504 504 self.resetCommentFormState(text)
505 505 };
506 506 self.submitAjaxPOST(
507 507 self.previewUrl, postData, self.previewSuccessCallback,
508 508 previewFailCallback);
509 509
510 510 $(self.previewButton).parent().addClass('active');
511 511 $(self.editButton).parent().removeClass('active');
512 512 });
513 513
514 514 $(this.submitForm).submit(function(e) {
515 515 e.preventDefault();
516 516 var allowedToSubmit = self.isAllowedToSubmit();
517 517 if (!allowedToSubmit){
518 518 return false;
519 519 }
520 520
521 521 self.handleFormSubmit();
522 522 });
523 523
524 524 }
525 525
526 526 return CommentForm;
527 527 });
528 528
529 529 /* selector for comment versions */
530 530 var initVersionSelector = function(selector, initialData) {
531 531
532 532 var formatResult = function(result, container, query, escapeMarkup) {
533 533
534 534 return renderTemplate('commentVersion', {
535 535 show_disabled: true,
536 536 version: result.comment_version,
537 537 user_name: result.comment_author_username,
538 538 gravatar_url: result.comment_author_gravatar,
539 539 size: 16,
540 540 timeago_component: result.comment_created_on,
541 541 })
542 542 };
543 543
544 544 $(selector).select2({
545 545 placeholder: "Edited",
546 546 containerCssClass: "drop-menu-comment-history",
547 547 dropdownCssClass: "drop-menu-dropdown",
548 548 dropdownAutoWidth: true,
549 549 minimumResultsForSearch: -1,
550 550 data: initialData,
551 551 formatResult: formatResult,
552 552 });
553 553
554 554 $(selector).on('select2-selecting', function (e) {
555 555 // hide the mast as we later do preventDefault()
556 556 $("#select2-drop-mask").click();
557 557 e.preventDefault();
558 558 e.choice.action();
559 559 });
560 560
561 561 $(selector).on("select2-open", function() {
562 562 timeagoActivate();
563 563 });
564 564 };
565 565
566 566 /* comments controller */
567 567 var CommentsController = function() {
568 568 var mainComment = '#text';
569 569 var self = this;
570 570
571 571 this.showVersion = function (comment_id, comment_history_id) {
572 572
573 573 var historyViewUrl = pyroutes.url(
574 574 'repo_commit_comment_history_view',
575 575 {
576 576 'repo_name': templateContext.repo_name,
577 577 'commit_id': comment_id,
578 578 'comment_history_id': comment_history_id,
579 579 }
580 580 );
581 581 successRenderCommit = function (data) {
582 582 SwalNoAnimation.fire({
583 583 html: data,
584 584 title: '',
585 585 });
586 586 };
587 587 failRenderCommit = function () {
588 588 SwalNoAnimation.fire({
589 589 html: 'Error while loading comment history',
590 590 title: '',
591 591 });
592 592 };
593 593 _submitAjaxPOST(
594 594 historyViewUrl, {'csrf_token': CSRF_TOKEN},
595 595 successRenderCommit,
596 596 failRenderCommit
597 597 );
598 598 };
599 599
600 600 this.getLineNumber = function(node) {
601 601 var $node = $(node);
602 602 var lineNo = $node.closest('td').attr('data-line-no');
603 603 if (lineNo === undefined && $node.data('commentInline')){
604 604 lineNo = $node.data('commentLineNo')
605 605 }
606 606
607 607 return lineNo
608 608 };
609 609
610 610 this.scrollToComment = function(node, offset, outdated) {
611 611 if (offset === undefined) {
612 612 offset = 0;
613 613 }
614 614 var outdated = outdated || false;
615 615 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
616 616
617 617 if (!node) {
618 618 node = $('.comment-selected');
619 619 if (!node.length) {
620 620 node = $('comment-current')
621 621 }
622 622 }
623 623
624 624 $wrapper = $(node).closest('div.comment');
625 625
626 626 // show hidden comment when referenced.
627 627 if (!$wrapper.is(':visible')){
628 628 $wrapper.show();
629 629 }
630 630
631 631 $comment = $(node).closest(klass);
632 632 $comments = $(klass);
633 633
634 634 $('.comment-selected').removeClass('comment-selected');
635 635
636 636 var nextIdx = $(klass).index($comment) + offset;
637 637 if (nextIdx >= $comments.length) {
638 638 nextIdx = 0;
639 639 }
640 640 var $next = $(klass).eq(nextIdx);
641 641
642 642 var $cb = $next.closest('.cb');
643 643 $cb.removeClass('cb-collapsed');
644 644
645 645 var $filediffCollapseState = $cb.closest('.filediff').prev();
646 646 $filediffCollapseState.prop('checked', false);
647 647 $next.addClass('comment-selected');
648 648 scrollToElement($next);
649 649 return false;
650 650 };
651 651
652 652 this.nextComment = function(node) {
653 653 return self.scrollToComment(node, 1);
654 654 };
655 655
656 656 this.prevComment = function(node) {
657 657 return self.scrollToComment(node, -1);
658 658 };
659 659
660 660 this.nextOutdatedComment = function(node) {
661 661 return self.scrollToComment(node, 1, true);
662 662 };
663 663
664 664 this.prevOutdatedComment = function(node) {
665 665 return self.scrollToComment(node, -1, true);
666 666 };
667 667
668 668 this.cancelComment = function (node) {
669 669 var $node = $(node);
670 670 var edit = $(this).attr('edit');
671 671 var $inlineComments = $node.closest('div.inline-comments');
672 672
673 673 if (edit) {
674 674 var $general_comments = null;
675 675 if (!$inlineComments.length) {
676 676 $general_comments = $('#comments');
677 677 var $comment = $general_comments.parent().find('div.comment:hidden');
678 678 // show hidden general comment form
679 679 $('#cb-comment-general-form-placeholder').show();
680 680 } else {
681 681 var $comment = $inlineComments.find('div.comment:hidden');
682 682 }
683 683 $comment.show();
684 684 }
685 685 var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
686 686 $replyWrapper.removeClass('comment-form-active');
687 687
688 688 var lastComment = $inlineComments.find('.comment-inline').last();
689 689 if ($(lastComment).hasClass('comment-outdated')) {
690 690 $replyWrapper.hide();
691 691 }
692 692
693 693 $node.closest('.comment-inline-form').remove();
694 694 return false;
695 695 };
696 696
697 697 this._deleteComment = function(node) {
698 698 var $node = $(node);
699 699 var $td = $node.closest('td');
700 700 var $comment = $node.closest('.comment');
701 701 var comment_id = $($comment).data('commentId');
702 702 var isDraft = $($comment).data('commentDraft');
703 703
704 704 var pullRequestId = templateContext.pull_request_data.pull_request_id;
705 705 var commitId = templateContext.commit_data.commit_id;
706 706
707 707 if (pullRequestId) {
708 708 var url = pyroutes.url('pullrequest_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
709 709 } else if (commitId) {
710 710 var url = pyroutes.url('repo_commit_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "commit_id": commitId})
711 711 }
712 712
713 713 var postData = {
714 714 'csrf_token': CSRF_TOKEN
715 715 };
716 716
717 717 $comment.addClass('comment-deleting');
718 718 $comment.hide('fast');
719 719
720 720 var success = function(response) {
721 721 $comment.remove();
722 722
723 723 if (window.updateSticky !== undefined) {
724 724 // potentially our comments change the active window size, so we
725 725 // notify sticky elements
726 726 updateSticky()
727 727 }
728 728
729 729 if (window.refreshAllComments !== undefined && !isDraft) {
730 730 // if we have this handler, run it, and refresh all comments boxes
731 731 refreshAllComments()
732 732 }
733 else if (window.refreshDraftComments !== undefined && isDraft) {
734 // if we have this handler, run it, and refresh all comments boxes
735 refreshDraftComments();
736 }
733 737 return false;
734 738 };
735 739
736 740 var failure = function(jqXHR, textStatus, errorThrown) {
737 741 var prefix = "Error while deleting this comment.\n"
738 742 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
739 743 ajaxErrorSwal(message);
740 744
741 745 $comment.show('fast');
742 746 $comment.removeClass('comment-deleting');
743 747 return false;
744 748 };
745 749 ajaxPOST(url, postData, success, failure);
746 750
747 751 }
748 752
749 753 this.deleteComment = function(node) {
750 754 var $comment = $(node).closest('.comment');
751 755 var comment_id = $comment.attr('data-comment-id');
752 756
753 757 SwalNoAnimation.fire({
754 758 title: 'Delete this comment?',
755 759 icon: 'warning',
756 760 showCancelButton: true,
757 761 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
758 762
759 763 }).then(function(result) {
760 764 if (result.value) {
761 765 self._deleteComment(node);
762 766 }
763 767 })
764 768 };
765 769
766 770 this._finalizeDrafts = function(commentIds) {
767 771
768 772 var pullRequestId = templateContext.pull_request_data.pull_request_id;
769 773 var commitId = templateContext.commit_data.commit_id;
770 774
771 775 if (pullRequestId) {
772 776 var url = pyroutes.url('pullrequest_draft_comments_submit', {"repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
773 777 } else if (commitId) {
774 778 var url = pyroutes.url('commit_draft_comments_submit', {"repo_name": templateContext.repo_name, "commit_id": commitId})
775 779 }
776 780
777 781 // remove the drafts so we can lock them before submit.
778 782 $.each(commentIds, function(idx, val){
779 783 $('#comment-{0}'.format(val)).remove();
780 784 })
781 785
782 786 var postData = {'comments': commentIds, 'csrf_token': CSRF_TOKEN};
783 787
784 788 var submitSuccessCallback = function(json_data) {
785 789 self.attachInlineComment(json_data);
786 790
787 791 if (window.refreshDraftComments !== undefined) {
788 792 // if we have this handler, run it, and refresh all comments boxes
789 793 refreshDraftComments()
790 794 }
791 795
792 796 return false;
793 797 };
794 798
795 799 ajaxPOST(url, postData, submitSuccessCallback)
796 800
797 801 }
798 802
799 this.finalizeDrafts = function(commentIds) {
803 this.finalizeDrafts = function(commentIds, callback) {
800 804
801 805 SwalNoAnimation.fire({
802 806 title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
803 807 icon: 'warning',
804 808 showCancelButton: true,
805 809 confirmButtonText: _gettext('Yes'),
806 810
807 811 }).then(function(result) {
808 812 if (result.value) {
813 if (callback !== undefined) {
814 callback(result)
815 }
809 816 self._finalizeDrafts(commentIds);
810 817 }
811 818 })
812 819 };
813 820
814 821 this.toggleWideMode = function (node) {
815 822
816 823 if ($('#content').hasClass('wrapper')) {
817 824 $('#content').removeClass("wrapper");
818 825 $('#content').addClass("wide-mode-wrapper");
819 826 $(node).addClass('btn-success');
820 827 return true
821 828 } else {
822 829 $('#content').removeClass("wide-mode-wrapper");
823 830 $('#content').addClass("wrapper");
824 831 $(node).removeClass('btn-success');
825 832 return false
826 833 }
827 834
828 835 };
829 836
830 837 /**
831 838 * Turn off/on all comments in file diff
832 839 */
833 840 this.toggleDiffComments = function(node) {
834 841 // Find closes filediff container
835 842 var $filediff = $(node).closest('.filediff');
836 843 if ($(node).hasClass('toggle-on')) {
837 844 var show = false;
838 845 } else if ($(node).hasClass('toggle-off')) {
839 846 var show = true;
840 847 }
841 848
842 849 // Toggle each individual comment block, so we can un-toggle single ones
843 850 $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
844 851 self.toggleLineComments($(val), show)
845 852 })
846 853
847 854 // since we change the height of the diff container that has anchor points for upper
848 855 // sticky header, we need to tell it to re-calculate those
849 856 if (window.updateSticky !== undefined) {
850 857 // potentially our comments change the active window size, so we
851 858 // notify sticky elements
852 859 updateSticky()
853 860 }
854 861
855 862 return false;
856 863 }
857 864
858 865 this.toggleLineComments = function(node, show) {
859 866
860 867 var trElem = $(node).closest('tr')
861 868
862 869 if (show === true) {
863 870 // mark outdated comments as visible before the toggle;
864 871 $(trElem).find('.comment-outdated').show();
865 872 $(trElem).removeClass('hide-line-comments');
866 873 } else if (show === false) {
867 874 $(trElem).find('.comment-outdated').hide();
868 875 $(trElem).addClass('hide-line-comments');
869 876 } else {
870 877 // mark outdated comments as visible before the toggle;
871 878 $(trElem).find('.comment-outdated').show();
872 879 $(trElem).toggleClass('hide-line-comments');
873 880 }
874 881
875 882 // since we change the height of the diff container that has anchor points for upper
876 883 // sticky header, we need to tell it to re-calculate those
877 884 if (window.updateSticky !== undefined) {
878 885 // potentially our comments change the active window size, so we
879 886 // notify sticky elements
880 887 updateSticky()
881 888 }
882 889
883 890 };
884 891
885 892 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
886 893 var pullRequestId = templateContext.pull_request_data.pull_request_id;
887 894 var commitId = templateContext.commit_data.commit_id;
888 895
889 896 var commentForm = new CommentForm(
890 897 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
891 898 var cm = commentForm.getCmInstance();
892 899
893 900 if (resolvesCommentId){
894 901 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
895 902 }
896 903
897 904 setTimeout(function() {
898 905 // callbacks
899 906 if (cm !== undefined) {
900 907 commentForm.setPlaceholder(placeholderText);
901 908 if (commentForm.isInline()) {
902 909 cm.focus();
903 910 cm.refresh();
904 911 }
905 912 }
906 913 }, 10);
907 914
908 915 // trigger scrolldown to the resolve comment, since it might be away
909 916 // from the clicked
910 917 if (resolvesCommentId){
911 918 var actionNode = $(commentForm.resolvesActionId).offset();
912 919
913 920 setTimeout(function() {
914 921 if (actionNode) {
915 922 $('body, html').animate({scrollTop: actionNode.top}, 10);
916 923 }
917 924 }, 100);
918 925 }
919 926
920 927 // add dropzone support
921 928 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
922 929 var renderer = templateContext.visual.default_renderer;
923 930 if (renderer == 'rst') {
924 931 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
925 932 if (isRendered){
926 933 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
927 934 }
928 935 } else if (renderer == 'markdown') {
929 936 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
930 937 if (isRendered){
931 938 attachmentUrl = '!' + attachmentUrl;
932 939 }
933 940 } else {
934 941 var attachmentUrl = '{}'.format(attachmentStoreUrl);
935 942 }
936 943 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
937 944
938 945 return false;
939 946 };
940 947
941 948 //see: https://www.dropzonejs.com/#configuration
942 949 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
943 950 {'repo_name': templateContext.repo_name,
944 951 'commit_id': templateContext.commit_data.commit_id})
945 952
946 953 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
947 954 if (previewTmpl !== undefined){
948 955 var selectLink = $(formElement).find('.pick-attachment').get(0);
949 956 $(formElement).find('.comment-attachment-uploader').dropzone({
950 957 url: storeUrl,
951 958 headers: {"X-CSRF-Token": CSRF_TOKEN},
952 959 paramName: function () {
953 960 return "attachment"
954 961 }, // The name that will be used to transfer the file
955 962 clickable: selectLink,
956 963 parallelUploads: 1,
957 964 maxFiles: 10,
958 965 maxFilesize: templateContext.attachment_store.max_file_size_mb,
959 966 uploadMultiple: false,
960 967 autoProcessQueue: true, // if false queue will not be processed automatically.
961 968 createImageThumbnails: false,
962 969 previewTemplate: previewTmpl.innerHTML,
963 970
964 971 accept: function (file, done) {
965 972 done();
966 973 },
967 974 init: function () {
968 975
969 976 this.on("sending", function (file, xhr, formData) {
970 977 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
971 978 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
972 979 });
973 980
974 981 this.on("success", function (file, response) {
975 982 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
976 983 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
977 984
978 985 var isRendered = false;
979 986 var ext = file.name.split('.').pop();
980 987 var imageExts = templateContext.attachment_store.image_ext;
981 988 if (imageExts.indexOf(ext) !== -1){
982 989 isRendered = true;
983 990 }
984 991
985 992 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
986 993 });
987 994
988 995 this.on("error", function (file, errorMessage, xhr) {
989 996 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
990 997
991 998 var error = null;
992 999
993 1000 if (xhr !== undefined){
994 1001 var httpStatus = xhr.status + " " + xhr.statusText;
995 1002 if (xhr !== undefined && xhr.status >= 500) {
996 1003 error = httpStatus;
997 1004 }
998 1005 }
999 1006
1000 1007 if (error === null) {
1001 1008 error = errorMessage.error || errorMessage || httpStatus;
1002 1009 }
1003 1010 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
1004 1011
1005 1012 });
1006 1013 }
1007 1014 });
1008 1015 }
1009 1016 return commentForm;
1010 1017 };
1011 1018
1012 1019 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
1013 1020
1014 1021 var tmpl = $('#cb-comment-general-form-template').html();
1015 1022 tmpl = tmpl.format(null, 'general');
1016 1023 var $form = $(tmpl);
1017 1024
1018 1025 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
1019 1026 var curForm = $formPlaceholder.find('form');
1020 1027 if (curForm){
1021 1028 curForm.remove();
1022 1029 }
1023 1030 $formPlaceholder.append($form);
1024 1031
1025 1032 var _form = $($form[0]);
1026 1033 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
1027 1034 var edit = false;
1028 1035 var comment_id = null;
1029 1036 var commentForm = this.createCommentForm(
1030 1037 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
1031 1038 commentForm.initStatusChangeSelector();
1032 1039
1033 1040 return commentForm;
1034 1041 };
1035 1042
1036 1043 this.editComment = function(node, line_no, f_path) {
1037 1044 self.edit = true;
1038 1045 var $node = $(node);
1039 1046 var $td = $node.closest('td');
1040 1047
1041 1048 var $comment = $(node).closest('.comment');
1042 1049 var comment_id = $($comment).data('commentId');
1043 1050 var isDraft = $($comment).data('commentDraft');
1044 1051 var $editForm = null
1045 1052
1046 1053 var $comments = $node.closest('div.inline-comments');
1047 1054 var $general_comments = null;
1048 1055
1049 1056 if($comments.length){
1050 1057 // inline comments setup
1051 1058 $editForm = $comments.find('.comment-inline-form');
1052 1059 line_no = self.getLineNumber(node)
1053 1060 }
1054 1061 else{
1055 1062 // general comments setup
1056 1063 $comments = $('#comments');
1057 1064 $editForm = $comments.find('.comment-inline-form');
1058 1065 line_no = $comment[0].id
1059 1066 $('#cb-comment-general-form-placeholder').hide();
1060 1067 }
1061 1068
1062 1069 if ($editForm.length === 0) {
1063 1070
1064 1071 // unhide all comments if they are hidden for a proper REPLY mode
1065 1072 var $filediff = $node.closest('.filediff');
1066 1073 $filediff.removeClass('hide-comments');
1067 1074
1068 1075 $editForm = self.createNewFormWrapper(f_path, line_no);
1069 1076 if(f_path && line_no) {
1070 1077 $editForm.addClass('comment-inline-form-edit')
1071 1078 }
1072 1079
1073 1080 $comment.after($editForm)
1074 1081
1075 1082 var _form = $($editForm[0]).find('form');
1076 1083 var autocompleteActions = ['as_note',];
1077 1084 var commentForm = this.createCommentForm(
1078 1085 _form, line_no, '', autocompleteActions, resolvesCommentId,
1079 1086 this.edit, comment_id);
1080 1087 var old_comment_text_binary = $comment.attr('data-comment-text');
1081 1088 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1082 1089 commentForm.cm.setValue(old_comment_text);
1083 1090 $comment.hide();
1084 1091 tooltipActivate();
1085 1092
1086 1093 // set a CUSTOM submit handler for inline comment edit action.
1087 1094 commentForm.setHandleFormSubmit(function(o) {
1088 1095 var text = commentForm.cm.getValue();
1089 1096 var commentType = commentForm.getCommentType();
1090 1097
1091 1098 if (text === "") {
1092 1099 return;
1093 1100 }
1094 1101
1095 1102 if (old_comment_text == text) {
1096 1103 SwalNoAnimation.fire({
1097 1104 title: 'Unable to edit comment',
1098 1105 html: _gettext('Comment body was not changed.'),
1099 1106 });
1100 1107 return;
1101 1108 }
1102 1109 var excludeCancelBtn = false;
1103 1110 var submitEvent = true;
1104 1111 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1105 1112 commentForm.cm.setOption("readOnly", true);
1106 1113
1107 1114 // Read last version known
1108 1115 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
1109 1116 var version = versionSelector.data('lastVersion');
1110 1117
1111 1118 if (!version) {
1112 1119 version = 0;
1113 1120 }
1114 1121
1115 1122 var postData = {
1116 1123 'text': text,
1117 1124 'f_path': f_path,
1118 1125 'line': line_no,
1119 1126 'comment_type': commentType,
1120 1127 'draft': isDraft,
1121 1128 'version': version,
1122 1129 'csrf_token': CSRF_TOKEN
1123 1130 };
1124 1131
1125 1132 var submitSuccessCallback = function(json_data) {
1126 1133 $editForm.remove();
1127 1134 $comment.show();
1128 1135 var postData = {
1129 1136 'text': text,
1130 1137 'renderer': $comment.attr('data-comment-renderer'),
1131 1138 'csrf_token': CSRF_TOKEN
1132 1139 };
1133 1140
1134 1141 /* Inject new edited version selector */
1135 1142 var updateCommentVersionDropDown = function () {
1136 1143 var versionSelectId = '#comment_versions_'+comment_id;
1137 1144 var preLoadVersionData = [
1138 1145 {
1139 1146 id: json_data['comment_version'],
1140 1147 text: "v{0}".format(json_data['comment_version']),
1141 1148 action: function () {
1142 1149 Rhodecode.comments.showVersion(
1143 1150 json_data['comment_id'],
1144 1151 json_data['comment_history_id']
1145 1152 )
1146 1153 },
1147 1154 comment_version: json_data['comment_version'],
1148 1155 comment_author_username: json_data['comment_author_username'],
1149 1156 comment_author_gravatar: json_data['comment_author_gravatar'],
1150 1157 comment_created_on: json_data['comment_created_on'],
1151 1158 },
1152 1159 ]
1153 1160
1154 1161
1155 1162 if ($(versionSelectId).data('select2')) {
1156 1163 var oldData = $(versionSelectId).data('select2').opts.data.results;
1157 1164 $(versionSelectId).select2("destroy");
1158 1165 preLoadVersionData = oldData.concat(preLoadVersionData)
1159 1166 }
1160 1167
1161 1168 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1162 1169
1163 1170 $comment.attr('data-comment-text', utf8ToB64(text));
1164 1171
1165 1172 var versionSelector = $('#comment_versions_'+comment_id);
1166 1173
1167 1174 // set lastVersion so we know our last edit version
1168 1175 versionSelector.data('lastVersion', json_data['comment_version'])
1169 1176 versionSelector.parent().show();
1170 1177 }
1171 1178 updateCommentVersionDropDown();
1172 1179
1173 1180 // by default we reset state of comment preserving the text
1174 1181 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1175 1182 var prefix = "Error while editing this comment.\n"
1176 1183 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1177 1184 ajaxErrorSwal(message);
1178 1185 };
1179 1186
1180 1187 var successRenderCommit = function(o){
1181 1188 $comment.show();
1182 1189 $comment[0].lastElementChild.innerHTML = o;
1183 1190 };
1184 1191
1185 1192 var previewUrl = pyroutes.url(
1186 1193 'repo_commit_comment_preview',
1187 1194 {'repo_name': templateContext.repo_name,
1188 1195 'commit_id': templateContext.commit_data.commit_id});
1189 1196
1190 1197 _submitAjaxPOST(
1191 1198 previewUrl, postData, successRenderCommit, failRenderCommit
1192 1199 );
1193 1200
1194 1201 try {
1195 1202 var html = json_data.rendered_text;
1196 1203 var lineno = json_data.line_no;
1197 1204 var target_id = json_data.target_id;
1198 1205
1199 1206 $comments.find('.cb-comment-add-button').before(html);
1200 1207
1201 1208 // run global callback on submit
1202 1209 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1203 1210
1204 1211 } catch (e) {
1205 1212 console.error(e);
1206 1213 }
1207 1214
1208 1215 // re trigger the linkification of next/prev navigation
1209 1216 linkifyComments($('.inline-comment-injected'));
1210 1217 timeagoActivate();
1211 1218 tooltipActivate();
1212 1219
1213 1220 if (window.updateSticky !== undefined) {
1214 1221 // potentially our comments change the active window size, so we
1215 1222 // notify sticky elements
1216 1223 updateSticky()
1217 1224 }
1218 1225
1219 1226 if (window.refreshAllComments !== undefined && !isDraft) {
1220 1227 // if we have this handler, run it, and refresh all comments boxes
1221 1228 refreshAllComments()
1222 1229 }
1230 else if (window.refreshDraftComments !== undefined && isDraft) {
1231 // if we have this handler, run it, and refresh all comments boxes
1232 refreshDraftComments();
1233 }
1223 1234
1224 1235 commentForm.setActionButtonsDisabled(false);
1225 1236
1226 1237 };
1227 1238
1228 1239 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1229 1240 var prefix = "Error while editing comment.\n"
1230 1241 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1231 1242 if (jqXHR.status == 409){
1232 1243 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1233 1244 ajaxErrorSwal(message, 'Comment version mismatch.');
1234 1245 } else {
1235 1246 ajaxErrorSwal(message);
1236 1247 }
1237 1248
1238 1249 commentForm.resetCommentFormState(text)
1239 1250 };
1240 1251 commentForm.submitAjaxPOST(
1241 1252 commentForm.submitUrl, postData,
1242 1253 submitSuccessCallback,
1243 1254 submitFailCallback);
1244 1255 });
1245 1256 }
1246 1257
1247 1258 $editForm.addClass('comment-inline-form-open');
1248 1259 };
1249 1260
1250 1261 this.attachComment = function(json_data) {
1251 1262 var self = this;
1252 1263 $.each(json_data, function(idx, val) {
1253 1264 var json_data_elem = [val]
1254 1265 var isInline = val.comment_f_path && val.comment_lineno
1255 1266
1256 1267 if (isInline) {
1257 1268 self.attachInlineComment(json_data_elem)
1258 1269 } else {
1259 1270 self.attachGeneralComment(json_data_elem)
1260 1271 }
1261 1272 })
1262 1273
1263 1274 }
1264 1275
1265 1276 this.attachGeneralComment = function(json_data) {
1266 1277 $.each(json_data, function(idx, val) {
1267 1278 $('#injected_page_comments').append(val.rendered_text);
1268 1279 })
1269 1280 }
1270 1281
1271 1282 this.attachInlineComment = function(json_data) {
1272 1283
1273 1284 $.each(json_data, function (idx, val) {
1274 1285 var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
1275 1286 var html = val.rendered_text;
1276 1287 var $inlineComments = $('#' + val.target_id)
1277 1288 .find(line_qry)
1278 1289 .find('.inline-comments');
1279 1290
1280 1291 var lastComment = $inlineComments.find('.comment-inline').last();
1281 1292
1282 1293 if (lastComment.length === 0) {
1283 1294 // first comment, we append simply
1284 1295 $inlineComments.find('.reply-thread-container-wrapper').before(html);
1285 1296 } else {
1286 1297 $(lastComment).after(html)
1287 1298 }
1288 1299
1289 1300 })
1290 1301
1291 1302 };
1292 1303
1293 1304 this.createNewFormWrapper = function(f_path, line_no) {
1294 1305 // create a new reply HTML form from template
1295 1306 var tmpl = $('#cb-comment-inline-form-template').html();
1296 1307 tmpl = tmpl.format(escapeHtml(f_path), line_no);
1297 1308 return $(tmpl);
1298 1309 }
1299 1310
1300 1311 this.createComment = function(node, f_path, line_no, resolutionComment) {
1301 1312 self.edit = false;
1302 1313 var $node = $(node);
1303 1314 var $td = $node.closest('td');
1304 1315 var resolvesCommentId = resolutionComment || null;
1305 1316
1306 1317 var $replyForm = $td.find('.comment-inline-form');
1307 1318
1308 1319 // if form isn't existing, we're generating a new one and injecting it.
1309 1320 if ($replyForm.length === 0) {
1310 1321
1311 1322 // unhide/expand all comments if they are hidden for a proper REPLY mode
1312 1323 self.toggleLineComments($node, true);
1313 1324
1314 1325 $replyForm = self.createNewFormWrapper(f_path, line_no);
1315 1326
1316 1327 var $comments = $td.find('.inline-comments');
1317 1328
1318 1329 // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
1319 1330 if ($comments.length===0) {
1320 1331 var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(f_path, line_no)
1321 1332 var $reply_container = $('#cb-comments-inline-container-template')
1322 1333 $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
1323 1334 $td.append($($reply_container).html());
1324 1335 }
1325 1336
1326 1337 // default comment button exists, so we prepend the form for leaving initial comment
1327 1338 $td.find('.cb-comment-add-button').before($replyForm);
1328 1339 // set marker, that we have a open form
1329 1340 var $replyWrapper = $td.find('.reply-thread-container-wrapper')
1330 1341 $replyWrapper.addClass('comment-form-active');
1331 1342
1332 1343 var lastComment = $comments.find('.comment-inline').last();
1333 1344 if ($(lastComment).hasClass('comment-outdated')) {
1334 1345 $replyWrapper.show();
1335 1346 }
1336 1347
1337 1348 var _form = $($replyForm[0]).find('form');
1338 1349 var autocompleteActions = ['as_note', 'as_todo'];
1339 1350 var comment_id=null;
1340 1351 var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
1341 1352 var commentForm = self.createCommentForm(
1342 1353 _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
1343 1354 self.edit, comment_id);
1344 1355
1345 1356 // set a CUSTOM submit handler for inline comments.
1346 1357 commentForm.setHandleFormSubmit(function(o) {
1347 1358 var text = commentForm.cm.getValue();
1348 1359 var commentType = commentForm.getCommentType();
1349 1360 var resolvesCommentId = commentForm.getResolvesId();
1350 1361 var isDraft = commentForm.getDraftState();
1351 1362
1352 1363 if (text === "") {
1353 1364 return;
1354 1365 }
1355 1366
1356 1367 if (line_no === undefined) {
1357 1368 alert('Error: unable to fetch line number for this inline comment !');
1358 1369 return;
1359 1370 }
1360 1371
1361 1372 if (f_path === undefined) {
1362 1373 alert('Error: unable to fetch file path for this inline comment !');
1363 1374 return;
1364 1375 }
1365 1376
1366 1377 var excludeCancelBtn = false;
1367 1378 var submitEvent = true;
1368 1379 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1369 1380 commentForm.cm.setOption("readOnly", true);
1370 1381 var postData = {
1371 1382 'text': text,
1372 1383 'f_path': f_path,
1373 1384 'line': line_no,
1374 1385 'comment_type': commentType,
1375 1386 'draft': isDraft,
1376 1387 'csrf_token': CSRF_TOKEN
1377 1388 };
1378 1389 if (resolvesCommentId){
1379 1390 postData['resolves_comment_id'] = resolvesCommentId;
1380 1391 }
1381 1392
1382 1393 // submitSuccess for inline commits
1383 1394 var submitSuccessCallback = function(json_data) {
1384 1395
1385 1396 $replyForm.remove();
1386 1397 $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
1387 1398
1388 1399 try {
1389 1400
1390 1401 // inject newly created comments, json_data is {<comment_id>: {}}
1391 1402 self.attachInlineComment(json_data)
1392 1403
1393 1404 //mark visually which comment was resolved
1394 1405 if (resolvesCommentId) {
1395 1406 commentForm.markCommentResolved(resolvesCommentId);
1396 1407 }
1397 1408
1398 1409 // run global callback on submit
1399 1410 commentForm.globalSubmitSuccessCallback({
1400 1411 draft: isDraft,
1401 1412 comment_id: comment_id
1402 1413 });
1403 1414
1404 1415 } catch (e) {
1405 1416 console.error(e);
1406 1417 }
1407 1418
1408 1419 if (window.updateSticky !== undefined) {
1409 1420 // potentially our comments change the active window size, so we
1410 1421 // notify sticky elements
1411 1422 updateSticky()
1412 1423 }
1413 1424
1414 1425 if (window.refreshAllComments !== undefined && !isDraft) {
1415 1426 // if we have this handler, run it, and refresh all comments boxes
1416 1427 refreshAllComments()
1417 1428 }
1429 else if (window.refreshDraftComments !== undefined && isDraft) {
1430 // if we have this handler, run it, and refresh all comments boxes
1431 refreshDraftComments();
1432 }
1418 1433
1419 1434 commentForm.setActionButtonsDisabled(false);
1420 1435
1421 1436 // re trigger the linkification of next/prev navigation
1422 1437 linkifyComments($('.inline-comment-injected'));
1423 1438 timeagoActivate();
1424 1439 tooltipActivate();
1425 1440 };
1426 1441
1427 1442 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1428 1443 var prefix = "Error while submitting comment.\n"
1429 1444 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1430 1445 ajaxErrorSwal(message);
1431 1446 commentForm.resetCommentFormState(text)
1432 1447 };
1433 1448
1434 1449 commentForm.submitAjaxPOST(
1435 1450 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1436 1451 });
1437 1452 }
1438 1453
1439 1454 // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
1440 1455 $replyForm.addClass('comment-inline-form-open');
1441 1456 tooltipActivate();
1442 1457 };
1443 1458
1444 1459 this.createResolutionComment = function(commentId){
1445 1460 // hide the trigger text
1446 1461 $('#resolve-comment-{0}'.format(commentId)).hide();
1447 1462
1448 1463 var comment = $('#comment-'+commentId);
1449 1464 var commentData = comment.data();
1450 1465 if (commentData.commentInline) {
1451 1466 var f_path = commentData.fPath;
1452 1467 var line_no = commentData.lineNo;
1453 1468 //TODO check this if we need to give f_path/line_no
1454 1469 this.createComment(comment, f_path, line_no, commentId)
1455 1470 } else {
1456 1471 this.createGeneralComment('general', "$placeholder", commentId)
1457 1472 }
1458 1473
1459 1474 return false;
1460 1475 };
1461 1476
1462 1477 this.submitResolution = function(commentId){
1463 1478 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1464 1479 var commentForm = form.get(0).CommentForm;
1465 1480
1466 1481 var cm = commentForm.getCmInstance();
1467 1482 var renderer = templateContext.visual.default_renderer;
1468 1483 if (renderer == 'rst'){
1469 1484 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1470 1485 } else if (renderer == 'markdown') {
1471 1486 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1472 1487 } else {
1473 1488 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1474 1489 }
1475 1490
1476 1491 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1477 1492 form.submit();
1478 1493 return false;
1479 1494 };
1480 1495
1481 1496 };
1482 1497
1483 1498 window.commentHelp = function(renderer) {
1484 1499 var funcData = {'renderer': renderer}
1485 1500 return renderTemplate('commentHelpHovercard', funcData)
1486 1501 } No newline at end of file
@@ -1,1146 +1,1170 b''
1 1 // # Copyright (C) 2010-2020 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
20 20 var prButtonLockChecks = {
21 21 'compare': false,
22 22 'reviewers': false
23 23 };
24 24
25 25 /**
26 26 * lock button until all checks and loads are made. E.g reviewer calculation
27 27 * should prevent from submitting a PR
28 28 * @param lockEnabled
29 29 * @param msg
30 30 * @param scope
31 31 */
32 32 var prButtonLock = function(lockEnabled, msg, scope) {
33 33 scope = scope || 'all';
34 34 if (scope == 'all'){
35 35 prButtonLockChecks['compare'] = !lockEnabled;
36 36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 37 } else if (scope == 'compare') {
38 38 prButtonLockChecks['compare'] = !lockEnabled;
39 39 } else if (scope == 'reviewers'){
40 40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 41 }
42 42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 43 if (lockEnabled) {
44 44 $('#pr_submit').attr('disabled', 'disabled');
45 45 }
46 46 else if (checksMeet) {
47 47 $('#pr_submit').removeAttr('disabled');
48 48 }
49 49
50 50 if (msg) {
51 51 $('#pr_open_message').html(msg);
52 52 }
53 53 };
54 54
55 55
56 56 /**
57 57 Generate Title and Description for a PullRequest.
58 58 In case of 1 commits, the title and description is that one commit
59 59 in case of multiple commits, we iterate on them with max N number of commits,
60 60 and build description in a form
61 61 - commitN
62 62 - commitN+1
63 63 ...
64 64
65 65 Title is then constructed from branch names, or other references,
66 66 replacing '-' and '_' into spaces
67 67
68 68 * @param sourceRef
69 69 * @param elements
70 70 * @param limit
71 71 * @returns {*[]}
72 72 */
73 73 var getTitleAndDescription = function(sourceRefType, sourceRef, elements, limit) {
74 74 var title = '';
75 75 var desc = '';
76 76
77 77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 78 var rawMessage = value['message'];
79 79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 80 });
81 81 // only 1 commit, use commit message as title
82 82 if (elements.length === 1) {
83 83 var rawMessage = elements[0]['message'];
84 84 title = rawMessage.split('\n')[0];
85 85 }
86 86 else {
87 87 // use reference name
88 88 var normalizedRef = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter()
89 89 var refType = sourceRefType;
90 90 title = 'Changes from {0}: {1}'.format(refType, normalizedRef);
91 91 }
92 92
93 93 return [title, desc]
94 94 };
95 95
96 96
97 97 window.ReviewersController = function () {
98 98 var self = this;
99 99 this.$loadingIndicator = $('.calculate-reviewers');
100 100 this.$reviewRulesContainer = $('#review_rules');
101 101 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
102 102 this.$userRule = $('.pr-user-rule-container');
103 103 this.$reviewMembers = $('#review_members');
104 104 this.$observerMembers = $('#observer_members');
105 105
106 106 this.currentRequest = null;
107 107 this.diffData = null;
108 108 this.enabledRules = [];
109 109 // sync with db.py entries
110 110 this.ROLE_REVIEWER = 'reviewer';
111 111 this.ROLE_OBSERVER = 'observer'
112 112
113 113 //dummy handler, we might register our own later
114 114 this.diffDataHandler = function (data) {};
115 115
116 116 this.defaultForbidUsers = function () {
117 117 return [
118 118 {
119 119 'username': 'default',
120 120 'user_id': templateContext.default_user.user_id
121 121 }
122 122 ];
123 123 };
124 124
125 125 // init default forbidden users
126 126 this.forbidUsers = this.defaultForbidUsers();
127 127
128 128 this.hideReviewRules = function () {
129 129 self.$reviewRulesContainer.hide();
130 130 $(self.$userRule.selector).hide();
131 131 };
132 132
133 133 this.showReviewRules = function () {
134 134 self.$reviewRulesContainer.show();
135 135 $(self.$userRule.selector).show();
136 136 };
137 137
138 138 this.addRule = function (ruleText) {
139 139 self.showReviewRules();
140 140 self.enabledRules.push(ruleText);
141 141 return '<div>- {0}</div>'.format(ruleText)
142 142 };
143 143
144 144 this.increaseCounter = function(role) {
145 145 if (role === self.ROLE_REVIEWER) {
146 146 var $elem = $('#reviewers-cnt')
147 147 var cnt = parseInt($elem.data('count') || 0)
148 148 cnt +=1
149 149 $elem.html(cnt);
150 150 $elem.data('count', cnt);
151 151 }
152 152 else if (role === self.ROLE_OBSERVER) {
153 153 var $elem = $('#observers-cnt');
154 154 var cnt = parseInt($elem.data('count') || 0)
155 155 cnt +=1
156 156 $elem.html(cnt);
157 157 $elem.data('count', cnt);
158 158 }
159 159 }
160 160
161 161 this.resetCounter = function () {
162 162 var $elem = $('#reviewers-cnt');
163 163
164 164 $elem.data('count', 0);
165 165 $elem.html(0);
166 166
167 167 var $elem = $('#observers-cnt');
168 168
169 169 $elem.data('count', 0);
170 170 $elem.html(0);
171 171 }
172 172
173 173 this.loadReviewRules = function (data) {
174 174 self.diffData = data;
175 175
176 176 // reset forbidden Users
177 177 this.forbidUsers = self.defaultForbidUsers();
178 178
179 179 // reset state of review rules
180 180 self.$rulesList.html('');
181 181
182 182 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
183 183 // default rule, case for older repo that don't have any rules stored
184 184 self.$rulesList.append(
185 185 self.addRule(_gettext('All reviewers must vote.'))
186 186 );
187 187 return self.forbidUsers
188 188 }
189 189
190 190 if (data.rules.forbid_adding_reviewers) {
191 191 $('#add_reviewer_input').remove();
192 192 }
193 193
194 194 if (data.rules_data !== undefined && data.rules_data.forbidden_users !== undefined) {
195 195 $.each(data.rules_data.forbidden_users, function(idx, val){
196 196 self.forbidUsers.push(val)
197 197 })
198 198 }
199 199
200 200 if (data.rules_humanized !== undefined && data.rules_humanized.length > 0) {
201 201 $.each(data.rules_humanized, function(idx, val) {
202 202 self.$rulesList.append(
203 203 self.addRule(val)
204 204 )
205 205 })
206 206 } else {
207 207 // we don't have any rules set, so we inform users about it
208 208 self.$rulesList.append(
209 209 self.addRule(_gettext('No additional review rules set.'))
210 210 )
211 211 }
212 212
213 213 return self.forbidUsers
214 214 };
215 215
216 216 this.emptyTables = function () {
217 217 self.emptyReviewersTable();
218 218 self.emptyObserversTable();
219 219
220 220 // Also reset counters.
221 221 self.resetCounter();
222 222 }
223 223
224 224 this.emptyReviewersTable = function (withText) {
225 225 self.$reviewMembers.empty();
226 226 if (withText !== undefined) {
227 227 self.$reviewMembers.html(withText)
228 228 }
229 229 };
230 230
231 231 this.emptyObserversTable = function (withText) {
232 232 self.$observerMembers.empty();
233 233 if (withText !== undefined) {
234 234 self.$observerMembers.html(withText)
235 235 }
236 236 }
237 237
238 238 this.loadDefaultReviewers = function (sourceRepo, sourceRef, targetRepo, targetRef) {
239 239
240 240 if (self.currentRequest) {
241 241 // make sure we cleanup old running requests before triggering this again
242 242 self.currentRequest.abort();
243 243 }
244 244
245 245 self.$loadingIndicator.show();
246 246
247 247 // reset reviewer/observe members
248 248 self.emptyTables();
249 249
250 250 prButtonLock(true, null, 'reviewers');
251 251 $('#user').hide(); // hide user autocomplete before load
252 252 $('#observer').hide(); //hide observer autocomplete before load
253 253
254 254 // lock PR button, so we cannot send PR before it's calculated
255 255 prButtonLock(true, _gettext('Loading diff ...'), 'compare');
256 256
257 257 if (sourceRef.length !== 3 || targetRef.length !== 3) {
258 258 // don't load defaults in case we're missing some refs...
259 259 self.$loadingIndicator.hide();
260 260 return
261 261 }
262 262
263 263 var url = pyroutes.url('repo_default_reviewers_data',
264 264 {
265 265 'repo_name': templateContext.repo_name,
266 266 'source_repo': sourceRepo,
267 267 'source_ref_type': sourceRef[0],
268 268 'source_ref_name': sourceRef[1],
269 269 'source_ref': sourceRef[2],
270 270 'target_repo': targetRepo,
271 271 'target_ref': targetRef[2],
272 272 'target_ref_type': sourceRef[0],
273 273 'target_ref_name': sourceRef[1]
274 274 });
275 275
276 276 self.currentRequest = $.ajax({
277 277 url: url,
278 278 headers: {'X-PARTIAL-XHR': true},
279 279 type: 'GET',
280 280 success: function (data) {
281 281
282 282 self.currentRequest = null;
283 283
284 284 // review rules
285 285 self.loadReviewRules(data);
286 286 var diffHandled = self.handleDiffData(data["diff_info"]);
287 287 if (diffHandled === false) {
288 288 return
289 289 }
290 290
291 291 for (var i = 0; i < data.reviewers.length; i++) {
292 292 var reviewer = data.reviewers[i];
293 293 // load reviewer rules from the repo data
294 294 self.addMember(reviewer, reviewer.reasons, reviewer.mandatory, reviewer.role);
295 295 }
296 296
297 297
298 298 self.$loadingIndicator.hide();
299 299 prButtonLock(false, null, 'reviewers');
300 300
301 301 $('#user').show(); // show user autocomplete before load
302 302 $('#observer').show(); // show observer autocomplete before load
303 303
304 304 var commitElements = data["diff_info"]['commits'];
305 305
306 306 if (commitElements.length === 0) {
307 307 var noCommitsMsg = '<span class="alert-text-warning">{0}</span>'.format(
308 308 _gettext('There are no commits to merge.'));
309 309 prButtonLock(true, noCommitsMsg, 'all');
310 310
311 311 } else {
312 312 // un-lock PR button, so we cannot send PR before it's calculated
313 313 prButtonLock(false, null, 'compare');
314 314 }
315 315
316 316 },
317 317 error: function (jqXHR, textStatus, errorThrown) {
318 318 var prefix = "Loading diff and reviewers/observers failed\n"
319 319 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
320 320 ajaxErrorSwal(message);
321 321 }
322 322 });
323 323
324 324 };
325 325
326 326 // check those, refactor
327 327 this.removeMember = function (reviewer_id, mark_delete) {
328 328 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
329 329
330 330 if (typeof (mark_delete) === undefined) {
331 331 mark_delete = false;
332 332 }
333 333
334 334 if (mark_delete === true) {
335 335 if (reviewer) {
336 336 // now delete the input
337 337 $('#reviewer_{0} input'.format(reviewer_id)).remove();
338 338 $('#reviewer_{0}_rules input'.format(reviewer_id)).remove();
339 339 // mark as to-delete
340 340 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
341 341 obj.addClass('to-delete');
342 342 obj.css({"text-decoration": "line-through", "opacity": 0.5});
343 343 }
344 344 } else {
345 345 $('#reviewer_{0}'.format(reviewer_id)).remove();
346 346 }
347 347 };
348 348
349 349 this.addMember = function (reviewer_obj, reasons, mandatory, role) {
350 350
351 351 var id = reviewer_obj.user_id;
352 352 var username = reviewer_obj.username;
353 353
354 354 reasons = reasons || [];
355 355 mandatory = mandatory || false;
356 356 role = role || self.ROLE_REVIEWER
357 357
358 358 // register current set IDS to check if we don't have this ID already in
359 359 // and prevent duplicates
360 360 var currentIds = [];
361 361
362 362 $.each($('.reviewer_entry'), function (index, value) {
363 363 currentIds.push($(value).data('reviewerUserId'))
364 364 })
365 365
366 366 var userAllowedReview = function (userId) {
367 367 var allowed = true;
368 368 $.each(self.forbidUsers, function (index, member_data) {
369 369 if (parseInt(userId) === member_data['user_id']) {
370 370 allowed = false;
371 371 return false // breaks the loop
372 372 }
373 373 });
374 374 return allowed
375 375 };
376 376
377 377 var userAllowed = userAllowedReview(id);
378 378
379 379 if (!userAllowed) {
380 380 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
381 381 } else {
382 382 // only add if it's not there
383 383 var alreadyReviewer = currentIds.indexOf(id) != -1;
384 384
385 385 if (alreadyReviewer) {
386 386 alert(_gettext('User `{0}` already in reviewers/observers').format(username));
387 387 } else {
388 388
389 389 var reviewerEntry = renderTemplate('reviewMemberEntry', {
390 390 'member': reviewer_obj,
391 391 'mandatory': mandatory,
392 392 'role': role,
393 393 'reasons': reasons,
394 394 'allowed_to_update': true,
395 395 'review_status': 'not_reviewed',
396 396 'review_status_label': _gettext('Not Reviewed'),
397 397 'user_group': reviewer_obj.user_group,
398 398 'create': true,
399 399 'rule_show': true,
400 400 })
401 401
402 402 if (role === self.ROLE_REVIEWER) {
403 403 $(self.$reviewMembers.selector).append(reviewerEntry);
404 404 self.increaseCounter(self.ROLE_REVIEWER);
405 405 $('#reviewer-empty-msg').remove()
406 406 }
407 407 else if (role === self.ROLE_OBSERVER) {
408 408 $(self.$observerMembers.selector).append(reviewerEntry);
409 409 self.increaseCounter(self.ROLE_OBSERVER);
410 410 $('#observer-empty-msg').remove();
411 411 }
412 412
413 413 tooltipActivate();
414 414 }
415 415 }
416 416
417 417 };
418 418
419 419 this.updateReviewers = function (repo_name, pull_request_id, role) {
420 420 if (role === 'reviewer') {
421 421 var postData = $('#reviewers input').serialize();
422 422 _updatePullRequest(repo_name, pull_request_id, postData);
423 423 } else if (role === 'observer') {
424 424 var postData = $('#observers input').serialize();
425 425 _updatePullRequest(repo_name, pull_request_id, postData);
426 426 }
427 427 };
428 428
429 429 this.handleDiffData = function (data) {
430 430 return self.diffDataHandler(data)
431 431 }
432 432 };
433 433
434 434
435 435 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
436 436 var url = pyroutes.url(
437 437 'pullrequest_update',
438 438 {"repo_name": repo_name, "pull_request_id": pull_request_id});
439 439 if (typeof postData === 'string' ) {
440 440 postData += '&csrf_token=' + CSRF_TOKEN;
441 441 } else {
442 442 postData.csrf_token = CSRF_TOKEN;
443 443 }
444 444
445 445 var success = function(o) {
446 446 var redirectUrl = o['redirect_url'];
447 447 if (redirectUrl !== undefined && redirectUrl !== null && redirectUrl !== '') {
448 448 window.location = redirectUrl;
449 449 } else {
450 450 window.location.reload();
451 451 }
452 452 };
453 453
454 454 ajaxPOST(url, postData, success);
455 455 };
456 456
457 457 /**
458 458 * PULL REQUEST update commits
459 459 */
460 460 var updateCommits = function(repo_name, pull_request_id, force) {
461 461 var postData = {
462 462 'update_commits': true
463 463 };
464 464 if (force !== undefined && force === true) {
465 465 postData['force_refresh'] = true
466 466 }
467 467 _updatePullRequest(repo_name, pull_request_id, postData);
468 468 };
469 469
470 470
471 471 /**
472 472 * PULL REQUEST edit info
473 473 */
474 474 var editPullRequest = function(repo_name, pull_request_id, title, description, renderer) {
475 475 var url = pyroutes.url(
476 476 'pullrequest_update',
477 477 {"repo_name": repo_name, "pull_request_id": pull_request_id});
478 478
479 479 var postData = {
480 480 'title': title,
481 481 'description': description,
482 482 'description_renderer': renderer,
483 483 'edit_pull_request': true,
484 484 'csrf_token': CSRF_TOKEN
485 485 };
486 486 var success = function(o) {
487 487 window.location.reload();
488 488 };
489 489 ajaxPOST(url, postData, success);
490 490 };
491 491
492 492
493 493 /**
494 494 * autocomplete handler for reviewers/observers
495 495 */
496 496 var autoCompleteHandler = function (inputId, controller, role) {
497 497
498 498 return function (element, data) {
499 499 var mandatory = false;
500 500 var reasons = [_gettext('added manually by "{0}"').format(
501 501 templateContext.rhodecode_user.username)];
502 502
503 503 // add whole user groups
504 504 if (data.value_type == 'user_group') {
505 505 reasons.push(_gettext('member of "{0}"').format(data.value_display));
506 506
507 507 $.each(data.members, function (index, member_data) {
508 508 var reviewer = member_data;
509 509 reviewer['user_id'] = member_data['id'];
510 510 reviewer['gravatar_link'] = member_data['icon_link'];
511 511 reviewer['user_link'] = member_data['profile_link'];
512 512 reviewer['rules'] = [];
513 513 controller.addMember(reviewer, reasons, mandatory, role);
514 514 })
515 515 }
516 516 // add single user
517 517 else {
518 518 var reviewer = data;
519 519 reviewer['user_id'] = data['id'];
520 520 reviewer['gravatar_link'] = data['icon_link'];
521 521 reviewer['user_link'] = data['profile_link'];
522 522 reviewer['rules'] = [];
523 523 controller.addMember(reviewer, reasons, mandatory, role);
524 524 }
525 525
526 526 $(inputId).val('');
527 527 }
528 528 }
529 529
530 530 /**
531 531 * Reviewer autocomplete
532 532 */
533 533 var ReviewerAutoComplete = function (inputId, controller) {
534 534 var self = this;
535 535 self.controller = controller;
536 536 self.inputId = inputId;
537 537 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_REVIEWER);
538 538
539 539 $(inputId).autocomplete({
540 540 serviceUrl: pyroutes.url('user_autocomplete_data'),
541 541 minChars: 2,
542 542 maxHeight: 400,
543 543 deferRequestBy: 300, //miliseconds
544 544 showNoSuggestionNotice: true,
545 545 tabDisabled: true,
546 546 autoSelectFirst: true,
547 547 params: {
548 548 user_id: templateContext.rhodecode_user.user_id,
549 549 user_groups: true,
550 550 user_groups_expand: true,
551 551 skip_default_user: true
552 552 },
553 553 formatResult: autocompleteFormatResult,
554 554 lookupFilter: autocompleteFilterResult,
555 555 onSelect: handler
556 556 });
557 557 };
558 558
559 559 /**
560 560 * Observers autocomplete
561 561 */
562 562 var ObserverAutoComplete = function(inputId, controller) {
563 563 var self = this;
564 564 self.controller = controller;
565 565 self.inputId = inputId;
566 566 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_OBSERVER);
567 567
568 568 $(inputId).autocomplete({
569 569 serviceUrl: pyroutes.url('user_autocomplete_data'),
570 570 minChars: 2,
571 571 maxHeight: 400,
572 572 deferRequestBy: 300, //miliseconds
573 573 showNoSuggestionNotice: true,
574 574 tabDisabled: true,
575 575 autoSelectFirst: true,
576 576 params: {
577 577 user_id: templateContext.rhodecode_user.user_id,
578 578 user_groups: true,
579 579 user_groups_expand: true,
580 580 skip_default_user: true
581 581 },
582 582 formatResult: autocompleteFormatResult,
583 583 lookupFilter: autocompleteFilterResult,
584 584 onSelect: handler
585 585 });
586 586 }
587 587
588 588
589 589 window.VersionController = function () {
590 590 var self = this;
591 591 this.$verSource = $('input[name=ver_source]');
592 592 this.$verTarget = $('input[name=ver_target]');
593 593 this.$showVersionDiff = $('#show-version-diff');
594 594
595 595 this.adjustRadioSelectors = function (curNode) {
596 596 var getVal = function (item) {
597 597 if (item === 'latest') {
598 598 return Number.MAX_SAFE_INTEGER
599 599 }
600 600 else {
601 601 return parseInt(item)
602 602 }
603 603 };
604 604
605 605 var curVal = getVal($(curNode).val());
606 606 var cleared = false;
607 607
608 608 $.each(self.$verSource, function (index, value) {
609 609 var elVal = getVal($(value).val());
610 610
611 611 if (elVal > curVal) {
612 612 if ($(value).is(':checked')) {
613 613 cleared = true;
614 614 }
615 615 $(value).attr('disabled', 'disabled');
616 616 $(value).removeAttr('checked');
617 617 $(value).css({'opacity': 0.1});
618 618 }
619 619 else {
620 620 $(value).css({'opacity': 1});
621 621 $(value).removeAttr('disabled');
622 622 }
623 623 });
624 624
625 625 if (cleared) {
626 626 // if we unchecked an active, set the next one to same loc.
627 627 $(this.$verSource).filter('[value={0}]'.format(
628 628 curVal)).attr('checked', 'checked');
629 629 }
630 630
631 631 self.setLockAction(false,
632 632 $(curNode).data('verPos'),
633 633 $(this.$verSource).filter(':checked').data('verPos')
634 634 );
635 635 };
636 636
637 637
638 638 this.attachVersionListener = function () {
639 639 self.$verTarget.change(function (e) {
640 640 self.adjustRadioSelectors(this)
641 641 });
642 642 self.$verSource.change(function (e) {
643 643 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
644 644 });
645 645 };
646 646
647 647 this.init = function () {
648 648
649 649 var curNode = self.$verTarget.filter(':checked');
650 650 self.adjustRadioSelectors(curNode);
651 651 self.setLockAction(true);
652 652 self.attachVersionListener();
653 653
654 654 };
655 655
656 656 this.setLockAction = function (state, selectedVersion, otherVersion) {
657 657 var $showVersionDiff = this.$showVersionDiff;
658 658
659 659 if (state) {
660 660 $showVersionDiff.attr('disabled', 'disabled');
661 661 $showVersionDiff.addClass('disabled');
662 662 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
663 663 }
664 664 else {
665 665 $showVersionDiff.removeAttr('disabled');
666 666 $showVersionDiff.removeClass('disabled');
667 667
668 668 if (selectedVersion == otherVersion) {
669 669 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
670 670 } else {
671 671 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
672 672 }
673 673 }
674 674
675 675 };
676 676
677 677 this.showVersionDiff = function () {
678 678 var target = self.$verTarget.filter(':checked');
679 679 var source = self.$verSource.filter(':checked');
680 680
681 681 if (target.val() && source.val()) {
682 682 var params = {
683 683 'pull_request_id': templateContext.pull_request_data.pull_request_id,
684 684 'repo_name': templateContext.repo_name,
685 685 'version': target.val(),
686 686 'from_version': source.val()
687 687 };
688 688 window.location = pyroutes.url('pullrequest_show', params)
689 689 }
690 690
691 691 return false;
692 692 };
693 693
694 694 this.toggleVersionView = function (elem) {
695 695
696 696 if (this.$showVersionDiff.is(':visible')) {
697 697 $('.version-pr').hide();
698 698 this.$showVersionDiff.hide();
699 699 $(elem).html($(elem).data('toggleOn'))
700 700 } else {
701 701 $('.version-pr').show();
702 702 this.$showVersionDiff.show();
703 703 $(elem).html($(elem).data('toggleOff'))
704 704 }
705 705
706 706 return false
707 707 };
708 708
709 709 };
710 710
711 711
712 712 window.UpdatePrController = function () {
713 713 var self = this;
714 714 this.$updateCommits = $('#update_commits');
715 715 this.$updateCommitsSwitcher = $('#update_commits_switcher');
716 716
717 717 this.lockUpdateButton = function (label) {
718 718 self.$updateCommits.attr('disabled', 'disabled');
719 719 self.$updateCommitsSwitcher.attr('disabled', 'disabled');
720 720
721 721 self.$updateCommits.addClass('disabled');
722 722 self.$updateCommitsSwitcher.addClass('disabled');
723 723
724 724 self.$updateCommits.removeClass('btn-primary');
725 725 self.$updateCommitsSwitcher.removeClass('btn-primary');
726 726
727 727 self.$updateCommits.text(_gettext(label));
728 728 };
729 729
730 730 this.isUpdateLocked = function () {
731 731 return self.$updateCommits.attr('disabled') !== undefined;
732 732 };
733 733
734 734 this.updateCommits = function (curNode) {
735 735 if (self.isUpdateLocked()) {
736 736 return
737 737 }
738 738 self.lockUpdateButton(_gettext('Updating...'));
739 739 updateCommits(
740 740 templateContext.repo_name,
741 741 templateContext.pull_request_data.pull_request_id);
742 742 };
743 743
744 744 this.forceUpdateCommits = function () {
745 745 if (self.isUpdateLocked()) {
746 746 return
747 747 }
748 748 self.lockUpdateButton(_gettext('Force updating...'));
749 749 var force = true;
750 750 updateCommits(
751 751 templateContext.repo_name,
752 752 templateContext.pull_request_data.pull_request_id, force);
753 753 };
754 754 };
755 755
756 756
757 757 /**
758 758 * Reviewer display panel
759 759 */
760 760 window.ReviewersPanel = {
761 761 editButton: null,
762 762 closeButton: null,
763 763 addButton: null,
764 764 removeButtons: null,
765 765 reviewRules: null,
766 766 setReviewers: null,
767 767 controller: null,
768 768
769 769 setSelectors: function () {
770 770 var self = this;
771 771 self.editButton = $('#open_edit_reviewers');
772 772 self.closeButton =$('#close_edit_reviewers');
773 773 self.addButton = $('#add_reviewer');
774 774 self.removeButtons = $('.reviewer_member_remove,.reviewer_member_mandatory_remove');
775 775 },
776 776
777 777 init: function (controller, reviewRules, setReviewers) {
778 778 var self = this;
779 779 self.setSelectors();
780 780
781 781 self.controller = controller;
782 782 self.reviewRules = reviewRules;
783 783 self.setReviewers = setReviewers;
784 784
785 785 self.editButton.on('click', function (e) {
786 786 self.edit();
787 787 });
788 788 self.closeButton.on('click', function (e) {
789 789 self.close();
790 790 self.renderReviewers();
791 791 });
792 792
793 793 self.renderReviewers();
794 794
795 795 },
796 796
797 797 renderReviewers: function () {
798 798 var self = this;
799 799
800 800 if (self.setReviewers.reviewers === undefined) {
801 801 return
802 802 }
803 803 if (self.setReviewers.reviewers.length === 0) {
804 804 self.controller.emptyReviewersTable('<tr id="reviewer-empty-msg"><td colspan="6">No reviewers</td></tr>');
805 805 return
806 806 }
807 807
808 808 self.controller.emptyReviewersTable();
809 809
810 810 $.each(self.setReviewers.reviewers, function (key, val) {
811 811
812 812 var member = val;
813 813 if (member.role === self.controller.ROLE_REVIEWER) {
814 814 var entry = renderTemplate('reviewMemberEntry', {
815 815 'member': member,
816 816 'mandatory': member.mandatory,
817 817 'role': member.role,
818 818 'reasons': member.reasons,
819 819 'allowed_to_update': member.allowed_to_update,
820 820 'review_status': member.review_status,
821 821 'review_status_label': member.review_status_label,
822 822 'user_group': member.user_group,
823 823 'create': false
824 824 });
825 825
826 826 $(self.controller.$reviewMembers.selector).append(entry)
827 827 }
828 828 });
829 829
830 830 tooltipActivate();
831 831 },
832 832
833 833 edit: function (event) {
834 834 var self = this;
835 835 self.editButton.hide();
836 836 self.closeButton.show();
837 837 self.addButton.show();
838 838 $(self.removeButtons.selector).css('visibility', 'visible');
839 839 // review rules
840 840 self.controller.loadReviewRules(this.reviewRules);
841 841 },
842 842
843 843 close: function (event) {
844 844 var self = this;
845 845 this.editButton.show();
846 846 this.closeButton.hide();
847 847 this.addButton.hide();
848 848 $(this.removeButtons.selector).css('visibility', 'hidden');
849 849 // hide review rules
850 850 self.controller.hideReviewRules();
851 851 }
852 852 };
853 853
854 854 /**
855 855 * Reviewer display panel
856 856 */
857 857 window.ObserversPanel = {
858 858 editButton: null,
859 859 closeButton: null,
860 860 addButton: null,
861 861 removeButtons: null,
862 862 reviewRules: null,
863 863 setReviewers: null,
864 864 controller: null,
865 865
866 866 setSelectors: function () {
867 867 var self = this;
868 868 self.editButton = $('#open_edit_observers');
869 869 self.closeButton =$('#close_edit_observers');
870 870 self.addButton = $('#add_observer');
871 871 self.removeButtons = $('.observer_member_remove,.observer_member_mandatory_remove');
872 872 },
873 873
874 874 init: function (controller, reviewRules, setReviewers) {
875 875 var self = this;
876 876 self.setSelectors();
877 877
878 878 self.controller = controller;
879 879 self.reviewRules = reviewRules;
880 880 self.setReviewers = setReviewers;
881 881
882 882 self.editButton.on('click', function (e) {
883 883 self.edit();
884 884 });
885 885 self.closeButton.on('click', function (e) {
886 886 self.close();
887 887 self.renderObservers();
888 888 });
889 889
890 890 self.renderObservers();
891 891
892 892 },
893 893
894 894 renderObservers: function () {
895 895 var self = this;
896 896 if (self.setReviewers.observers === undefined) {
897 897 return
898 898 }
899 899 if (self.setReviewers.observers.length === 0) {
900 900 self.controller.emptyObserversTable('<tr id="observer-empty-msg"><td colspan="6">No observers</td></tr>');
901 901 return
902 902 }
903 903
904 904 self.controller.emptyObserversTable();
905 905
906 906 $.each(self.setReviewers.observers, function (key, val) {
907 907 var member = val;
908 908 if (member.role === self.controller.ROLE_OBSERVER) {
909 909 var entry = renderTemplate('reviewMemberEntry', {
910 910 'member': member,
911 911 'mandatory': member.mandatory,
912 912 'role': member.role,
913 913 'reasons': member.reasons,
914 914 'allowed_to_update': member.allowed_to_update,
915 915 'review_status': member.review_status,
916 916 'review_status_label': member.review_status_label,
917 917 'user_group': member.user_group,
918 918 'create': false
919 919 });
920 920
921 921 $(self.controller.$observerMembers.selector).append(entry)
922 922 }
923 923 });
924 924
925 925 tooltipActivate();
926 926 },
927 927
928 928 edit: function (event) {
929 929 this.editButton.hide();
930 930 this.closeButton.show();
931 931 this.addButton.show();
932 932 $(this.removeButtons.selector).css('visibility', 'visible');
933 933 },
934 934
935 935 close: function (event) {
936 936 this.editButton.show();
937 937 this.closeButton.hide();
938 938 this.addButton.hide();
939 939 $(this.removeButtons.selector).css('visibility', 'hidden');
940 940 }
941 941
942 942 };
943 943
944 944 window.PRDetails = {
945 945 editButton: null,
946 946 closeButton: null,
947 947 deleteButton: null,
948 948 viewFields: null,
949 949 editFields: null,
950 950
951 951 setSelectors: function () {
952 952 var self = this;
953 953 self.editButton = $('#open_edit_pullrequest')
954 954 self.closeButton = $('#close_edit_pullrequest')
955 955 self.deleteButton = $('#delete_pullrequest')
956 956 self.viewFields = $('#pr-desc, #pr-title')
957 957 self.editFields = $('#pr-desc-edit, #pr-title-edit, .pr-save')
958 958 },
959 959
960 960 init: function () {
961 961 var self = this;
962 962 self.setSelectors();
963 963 self.editButton.on('click', function (e) {
964 964 self.edit();
965 965 });
966 966 self.closeButton.on('click', function (e) {
967 967 self.view();
968 968 });
969 969 },
970 970
971 971 edit: function (event) {
972 972 var cmInstance = $('#pr-description-input').get(0).MarkupForm.cm;
973 973 this.viewFields.hide();
974 974 this.editButton.hide();
975 975 this.deleteButton.hide();
976 976 this.closeButton.show();
977 977 this.editFields.show();
978 978 cmInstance.refresh();
979 979 },
980 980
981 981 view: function (event) {
982 982 this.editButton.show();
983 983 this.deleteButton.show();
984 984 this.editFields.hide();
985 985 this.closeButton.hide();
986 986 this.viewFields.show();
987 987 }
988 988 };
989 989
990 990 /**
991 991 * OnLine presence using channelstream
992 992 */
993 993 window.ReviewerPresenceController = function (channel) {
994 994 var self = this;
995 995 this.channel = channel;
996 996 this.users = {};
997 997
998 998 this.storeUsers = function (users) {
999 999 self.users = {}
1000 1000 $.each(users, function (index, value) {
1001 1001 var userId = value.state.id;
1002 1002 self.users[userId] = value.state;
1003 1003 })
1004 1004 }
1005 1005
1006 1006 this.render = function () {
1007 1007 $.each($('.reviewer_entry'), function (index, value) {
1008 1008 var userData = $(value).data();
1009 1009 if (self.users[userData.reviewerUserId] !== undefined) {
1010 1010 $(value).find('.presence-state').show();
1011 1011 } else {
1012 1012 $(value).find('.presence-state').hide();
1013 1013 }
1014 1014 })
1015 1015 };
1016 1016
1017 1017 this.handlePresence = function (data) {
1018 1018 if (data.type == 'presence' && data.channel === self.channel) {
1019 1019 this.storeUsers(data.users);
1020 1020 this.render();
1021 1021 }
1022 1022 };
1023 1023
1024 1024 this.handleChannelUpdate = function (data) {
1025 1025 if (data.channel === this.channel) {
1026 1026 this.storeUsers(data.state.users);
1027 1027 this.render();
1028 1028 }
1029 1029
1030 1030 };
1031 1031
1032 1032 /* subscribe to the current presence */
1033 1033 $.Topic('/connection_controller/presence').subscribe(this.handlePresence.bind(this));
1034 1034 /* subscribe to updates e.g connect/disconnect */
1035 1035 $.Topic('/connection_controller/channel_update').subscribe(this.handleChannelUpdate.bind(this));
1036 1036
1037 1037 };
1038 1038
1039 window.refreshCommentsSuccess = function(targetNode, counterNode, extraCallback) {
1040 var $targetElem = targetNode;
1041 var $counterElem = counterNode;
1042
1043 return function (data) {
1044 var newCount = $(data).data('counter');
1045 if (newCount !== undefined) {
1046 var callback = function () {
1047 $counterElem.animate({'opacity': 1.00}, 200)
1048 $counterElem.html(newCount);
1049 };
1050 $counterElem.animate({'opacity': 0.15}, 200, callback);
1051 }
1052
1053 $targetElem.css('opacity', 1);
1054 $targetElem.html(data);
1055 tooltipActivate();
1056
1057 if (extraCallback !== undefined) {
1058 extraCallback(data)
1059 }
1060 }
1061 }
1062
1039 1063 window.refreshComments = function (version) {
1040 1064 version = version || templateContext.pull_request_data.pull_request_version || '';
1041 1065
1042 1066 // Pull request case
1043 1067 if (templateContext.pull_request_data.pull_request_id !== null) {
1044 1068 var params = {
1045 1069 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1046 1070 'repo_name': templateContext.repo_name,
1047 1071 'version': version,
1048 1072 };
1049 1073 var loadUrl = pyroutes.url('pullrequest_comments', params);
1050 1074 } // commit case
1051 1075 else {
1052 1076 return
1053 1077 }
1054 1078
1055 1079 var currentIDs = []
1056 1080 $.each($('.comment'), function (idx, element) {
1057 1081 currentIDs.push($(element).data('commentId'));
1058 1082 });
1059 1083 var data = {"comments": currentIDs};
1060 1084
1061 1085 var $targetElem = $('.comments-content-table');
1062 1086 $targetElem.css('opacity', 0.3);
1063
1064 var success = function (data) {
1065 var $counterElem = $('#comments-count');
1066 var newCount = $(data).data('counter');
1067 if (newCount !== undefined) {
1068 var callback = function () {
1069 $counterElem.animate({'opacity': 1.00}, 200)
1070 $counterElem.html(newCount);
1071 };
1072 $counterElem.animate({'opacity': 0.15}, 200, callback);
1073 }
1074
1075 $targetElem.css('opacity', 1);
1076 $targetElem.html(data);
1077 tooltipActivate();
1078 }
1079
1087 var $counterElem = $('#comments-count');
1088 var success = refreshCommentsSuccess($targetElem, $counterElem);
1080 1089 ajaxPOST(loadUrl, data, success, null, {})
1081 1090
1082 1091 }
1083 1092
1084 1093 window.refreshTODOs = function (version) {
1085 1094 version = version || templateContext.pull_request_data.pull_request_version || '';
1086 1095 // Pull request case
1087 1096 if (templateContext.pull_request_data.pull_request_id !== null) {
1088 1097 var params = {
1089 1098 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1090 1099 'repo_name': templateContext.repo_name,
1091 1100 'version': version,
1092 1101 };
1093 1102 var loadUrl = pyroutes.url('pullrequest_todos', params);
1094 1103 } // commit case
1095 1104 else {
1096 1105 return
1097 1106 }
1098 1107
1099 1108 var currentIDs = []
1100 1109 $.each($('.comment'), function (idx, element) {
1101 1110 currentIDs.push($(element).data('commentId'));
1102 1111 });
1103 1112
1104 1113 var data = {"comments": currentIDs};
1105 1114 var $targetElem = $('.todos-content-table');
1106 1115 $targetElem.css('opacity', 0.3);
1107
1108 var success = function (data) {
1109 var $counterElem = $('#todos-count')
1110 var newCount = $(data).data('counter');
1111 if (newCount !== undefined) {
1112 var callback = function () {
1113 $counterElem.animate({'opacity': 1.00}, 200)
1114 $counterElem.html(newCount);
1115 };
1116 $counterElem.animate({'opacity': 0.15}, 200, callback);
1117 }
1118
1119 $targetElem.css('opacity', 1);
1120 $targetElem.html(data);
1121 tooltipActivate();
1122 }
1116 var $counterElem = $('#todos-count');
1117 var success = refreshCommentsSuccess($targetElem, $counterElem);
1123 1118
1124 1119 ajaxPOST(loadUrl, data, success, null, {})
1125 1120
1126 1121 }
1127 1122
1123 window.refreshDraftComments = function () {
1124
1125 // Pull request case
1126 if (templateContext.pull_request_data.pull_request_id !== null) {
1127 var params = {
1128 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1129 'repo_name': templateContext.repo_name,
1130 };
1131 var loadUrl = pyroutes.url('pullrequest_drafts', params);
1132 } // commit case
1133 else {
1134 return
1135 }
1136
1137 var data = {};
1138
1139 var $targetElem = $('.drafts-content-table');
1140 $targetElem.css('opacity', 0.3);
1141 var $counterElem = $('#drafts-count');
1142 var extraCallback = function(data) {
1143 if ($(data).data('counter') == 0){
1144 $('#draftsTable').hide();
1145 } else {
1146 $('#draftsTable').show();
1147 }
1148 // uncheck on load the select all checkbox
1149 $('[name=select_all_drafts]').prop('checked', 0);
1150 }
1151 var success = refreshCommentsSuccess($targetElem, $counterElem, extraCallback);
1152
1153 ajaxPOST(loadUrl, data, success, null, {})
1154 };
1155
1128 1156 window.refreshAllComments = function (version) {
1129 1157 version = version || templateContext.pull_request_data.pull_request_version || '';
1130 1158
1131 1159 refreshComments(version);
1132 1160 refreshTODOs(version);
1133 1161 };
1134 1162
1135 window.refreshDraftComments = function () {
1136 alert('TODO: refresh Draft Comments needs implementation')
1137 };
1138
1139 1163 window.sidebarComment = function (commentId) {
1140 1164 var jsonData = $('#commentHovercard{0}'.format(commentId)).data('commentJsonB64');
1141 1165 if (!jsonData) {
1142 1166 return 'Failed to load comment {0}'.format(commentId)
1143 1167 }
1144 1168 var funcData = JSON.parse(atob(jsonData));
1145 1169 return renderTemplate('sideBarCommentHovercard', funcData)
1146 1170 };
@@ -1,151 +1,159 b''
1 1 ## snippet for sidebar elements
2 2 ## usage:
3 3 ## <%namespace name="sidebar" file="/base/sidebar.mako"/>
4 4 ## ${sidebar.comments_table()}
5 5 <%namespace name="base" file="/base/base.mako"/>
6 6
7 <%def name="comments_table(comments, counter_num, todo_comments=False, existing_ids=None, is_pr=True)">
7 <%def name="comments_table(comments, counter_num, todo_comments=False, draft_comments=False, existing_ids=None, is_pr=True)">
8 8 <%
9 9 if todo_comments:
10 10 cls_ = 'todos-content-table'
11 11 def sorter(entry):
12 12 user_id = entry.author.user_id
13 13 resolved = '1' if entry.resolved else '0'
14 14 if user_id == c.rhodecode_user.user_id:
15 15 # own comments first
16 16 user_id = 0
17 17 return '{}'.format(str(entry.comment_id).zfill(10000))
18 elif draft_comments:
19 cls_ = 'drafts-content-table'
20 def sorter(entry):
21 return '{}'.format(str(entry.comment_id).zfill(10000))
18 22 else:
19 23 cls_ = 'comments-content-table'
20 24 def sorter(entry):
21 user_id = entry.author.user_id
22 25 return '{}'.format(str(entry.comment_id).zfill(10000))
23 26
24 27 existing_ids = existing_ids or []
25 28
26 29 %>
27 30
28 31 <table class="todo-table ${cls_}" data-total-count="${len(comments)}" data-counter="${counter_num}">
29 32
30 33 % for loop_obj, comment_obj in h.looper(reversed(sorted(comments, key=sorter))):
31 34 <%
32 35 display = ''
33 36 _cls = ''
34 37 ## Extra precaution to not show drafts in the sidebar for todo/comments
35 if comment_obj.draft:
38 if comment_obj.draft and not draft_comments:
36 39 continue
37 40 %>
38 41
39 42
40 43 <%
41 44 comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', []))
42 45 prev_comment_ver_index = 0
43 46 if loop_obj.previous:
44 47 prev_comment_ver_index = loop_obj.previous.get_index_version(getattr(c, 'versions', []))
45 48
46 49 ver_info = None
47 50 if getattr(c, 'versions', []):
48 51 ver_info = c.versions[comment_ver_index-1] if comment_ver_index else None
49 52 %>
50 53 <% hidden_at_ver = comment_obj.outdated_at_version_js(c.at_version_num) %>
51 54 <% is_from_old_ver = comment_obj.older_than_version_js(c.at_version_num) %>
52 55 <%
53 56 if (prev_comment_ver_index > comment_ver_index):
54 57 comments_ver_divider = comment_ver_index
55 58 else:
56 59 comments_ver_divider = None
57 60 %>
58 61
59 62 % if todo_comments:
60 63 % if comment_obj.resolved:
61 64 <% _cls = 'resolved-todo' %>
62 65 <% display = 'none' %>
63 66 % endif
64 67 % else:
65 68 ## SKIP TODOs we display them in other area
66 69 % if comment_obj.is_todo:
67 70 <% display = 'none' %>
68 71 % endif
69 72 ## Skip outdated comments
70 73 % if comment_obj.outdated:
71 74 <% display = 'none' %>
72 75 <% _cls = 'hidden-comment' %>
73 76 % endif
74 77 % endif
75 78
76 79 % if not todo_comments and comments_ver_divider:
77 80 <tr class="old-comments-marker">
78 81 <td colspan="3">
79 82 % if ver_info:
80 83 <code>v${comments_ver_divider} ${h.age_component(ver_info.created_on, time_is_local=True, tooltip=False)}</code>
81 84 % else:
82 85 <code>v${comments_ver_divider}</code>
83 86 % endif
84 87 </td>
85 88 </tr>
86 89
87 90 % endif
88 91
89 92 <tr class="${_cls}" style="display: ${display};" data-sidebar-comment-id="${comment_obj.comment_id}">
93 % if draft_comments:
94 <td style="width: 15px;">
95 ${h.checkbox('submit_draft', id=None, value=comment_obj.comment_id)}
96 </td>
97 % endif
90 98 <td class="td-todo-number">
91 99 <%
92 100 version_info = ''
93 101 if is_pr:
94 102 version_info = (' made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else ' made in this version')
95 103 %>
96 104 ## new comments, since refresh
97 105 % if existing_ids and comment_obj.comment_id not in existing_ids:
98 106 <div class="tooltip" style="position: absolute; left: 8px; color: #682668" title="New comment">
99 107 !
100 108 </div>
101 109 % endif
102 110
103 111 <%
104 112 data = h.json.dumps({
105 113 'comment_id': comment_obj.comment_id,
106 114 'version_info': version_info,
107 115 'file_name': comment_obj.f_path,
108 116 'line_no': comment_obj.line_no,
109 117 'outdated': comment_obj.outdated,
110 118 'inline': comment_obj.is_inline,
111 119 'is_todo': comment_obj.is_todo,
112 120 'created_on': h.format_date(comment_obj.created_on),
113 121 'datetime': '{}{}'.format(comment_obj.created_on, h.get_timezone(comment_obj.created_on, time_is_local=True)),
114 122 'review_status': (comment_obj.review_status or '')
115 123 })
116 124
117 125 if comment_obj.outdated:
118 126 icon = 'icon-comment-toggle'
119 127 elif comment_obj.is_inline:
120 128 icon = 'icon-code'
121 129 else:
122 130 icon = 'icon-comment'
123 131 %>
124 132
125 133 <i id="commentHovercard${comment_obj.comment_id}"
126 134 class="${icon} tooltip-hovercard"
127 135 data-hovercard-url="javascript:sidebarComment(${comment_obj.comment_id})"
128 136 data-comment-json-b64='${h.b64(data)}'>
129 137 </i>
130 138
131 139 </td>
132 140
133 141 <td class="td-todo-gravatar">
134 142 ${base.gravatar(comment_obj.author.email, 16, user=comment_obj.author, tooltip=True, extra_class=['no-margin'])}
135 143 </td>
136 144 <td class="todo-comment-text-wrapper">
137 145 <div class="todo-comment-text ${('todo-resolved' if comment_obj.resolved else '')}">
138 146 <a class="${('todo-resolved' if comment_obj.resolved else '')} permalink"
139 147 href="#comment-${comment_obj.comment_id}"
140 148 onclick="return Rhodecode.comments.scrollToComment($('#comment-${comment_obj.comment_id}'), 0, ${hidden_at_ver})">
141 149
142 150 ${h.chop_at_smart(comment_obj.text, '\n', suffix_if_chopped='...')}
143 151 </a>
144 152 </div>
145 153 </td>
146 154 </tr>
147 155 % endfor
148 156
149 157 </table>
150 158
151 159 </%def> No newline at end of file
@@ -1,1033 +1,1056 b''
1 1 <%inherit file="/base/base.mako"/>
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 4 <%namespace name="sidebar" file="/base/sidebar.mako"/>
5 5
6 6
7 7 <%def name="title()">
8 8 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
9 9 %if c.rhodecode_name:
10 10 &middot; ${h.branding(c.rhodecode_name)}
11 11 %endif
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()">
15 15
16 16 </%def>
17 17
18 18 <%def name="menu_bar_nav()">
19 19 ${self.menu_items(active='repositories')}
20 20 </%def>
21 21
22 22 <%def name="menu_bar_subnav()">
23 23 ${self.repo_menu(active='showpullrequest')}
24 24 </%def>
25 25
26 26
27 27 <%def name="main()">
28 28 ## Container to gather extracted Tickets
29 29 <%
30 30 c.referenced_commit_issues = []
31 31 c.referenced_desc_issues = []
32 32 %>
33 33
34 34 <script type="text/javascript">
35 35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 36 templateContext.pull_request_data.pull_request_version = '${request.GET.get('version', '')}';
37 37 </script>
38 38
39 39 <div class="box">
40 40
41 41 <div class="box pr-summary">
42 42
43 43 <div class="summary-details block-left">
44 44 <div id="pr-title">
45 45 % if c.pull_request.is_closed():
46 46 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
47 47 % endif
48 48 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
49 49 </div>
50 50 <div id="pr-title-edit" class="input" style="display: none;">
51 51 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
52 52 </div>
53 53
54 54 <% summary = lambda n:{False:'summary-short'}.get(n) %>
55 55 <div class="pr-details-title">
56 56 <div class="pull-left">
57 57 <a href="${h.route_path('pull_requests_global', pull_request_id=c.pull_request.pull_request_id)}">${_('Pull request !{}').format(c.pull_request.pull_request_id)}</a>
58 58 ${_('Created on')}
59 59 <span class="tooltip" title="${_('Last updated on')} ${h.format_date(c.pull_request.updated_on)}">${h.format_date(c.pull_request.created_on)},</span>
60 60 <span class="pr-details-title-author-pref">${_('by')}</span>
61 61 </div>
62 62
63 63 <div class="pull-left">
64 64 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
65 65 </div>
66 66
67 67 %if c.allowed_to_update:
68 68 <div class="pull-right">
69 69 <div id="edit_pull_request" class="action_button pr-save" style="display: none;">${_('Update title & description')}</div>
70 70 <div id="delete_pullrequest" class="action_button pr-save ${('' if c.allowed_to_delete else 'disabled' )}" style="display: none;">
71 71 % if c.allowed_to_delete:
72 72 ${h.secure_form(h.route_path('pullrequest_delete', repo_name=c.pull_request.target_repo.repo_name, pull_request_id=c.pull_request.pull_request_id), request=request)}
73 73 <input class="btn btn-link btn-danger no-margin" id="remove_${c.pull_request.pull_request_id}" name="remove_${c.pull_request.pull_request_id}"
74 74 onclick="submitConfirm(event, this, _gettext('Confirm to delete this pull request'), _gettext('Delete'), '${'!{}'.format(c.pull_request.pull_request_id)}')"
75 75 type="submit" value="${_('Delete pull request')}">
76 76 ${h.end_form()}
77 77 % else:
78 78 <span class="tooltip" title="${_('Not allowed to delete this pull request')}">${_('Delete pull request')}</span>
79 79 % endif
80 80 </div>
81 81 <div id="open_edit_pullrequest" class="action_button">${_('Edit')}</div>
82 82 <div id="close_edit_pullrequest" class="action_button" style="display: none;">${_('Cancel')}</div>
83 83 </div>
84 84
85 85 %endif
86 86 </div>
87 87
88 88 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
89 89 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name, issues_container=c.referenced_desc_issues)}
90 90 </div>
91 91
92 92 <div id="pr-desc-edit" class="input textarea" style="display: none;">
93 93 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
94 94 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
95 95 </div>
96 96
97 97 <div id="summary" class="fields pr-details-content">
98 98
99 99 ## source
100 100 <div class="field">
101 101 <div class="label-pr-detail">
102 102 <label>${_('Commit flow')}:</label>
103 103 </div>
104 104 <div class="input">
105 105 <div class="pr-commit-flow">
106 106 ## Source
107 107 %if c.pull_request.source_ref_parts.type == 'branch':
108 108 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.source_repo.repo_name, _query=dict(branch=c.pull_request.source_ref_parts.name))}"><code class="pr-source-info">${c.pull_request.source_ref_parts.type}:${c.pull_request.source_ref_parts.name}</code></a>
109 109 %else:
110 110 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
111 111 %endif
112 112 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.source_repo.repo_name)}">${c.pull_request.source_repo.repo_name}</a>
113 113 &rarr;
114 114 ## Target
115 115 %if c.pull_request.target_ref_parts.type == 'branch':
116 116 <a href="${h.route_path('repo_commits', repo_name=c.pull_request.target_repo.repo_name, _query=dict(branch=c.pull_request.target_ref_parts.name))}"><code class="pr-target-info">${c.pull_request.target_ref_parts.type}:${c.pull_request.target_ref_parts.name}</code></a>
117 117 %else:
118 118 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
119 119 %endif
120 120
121 121 ${_('of')} <a href="${h.route_path('repo_summary', repo_name=c.pull_request.target_repo.repo_name)}">${c.pull_request.target_repo.repo_name}</a>
122 122
123 123 <a class="source-details-action" href="#expand-source-details" onclick="return toggleElement(this, '.source-details')" data-toggle-on='<i class="icon-angle-down">more details</i>' data-toggle-off='<i class="icon-angle-up">less details</i>'>
124 124 <i class="icon-angle-down">more details</i>
125 125 </a>
126 126
127 127 </div>
128 128
129 129 <div class="source-details" style="display: none">
130 130
131 131 <ul>
132 132
133 133 ## common ancestor
134 134 <li>
135 135 ${_('Common ancestor')}:
136 136 % if c.ancestor_commit:
137 137 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=c.ancestor_commit.raw_id)}">${h.show_id(c.ancestor_commit)}</a>
138 138 % else:
139 139 ${_('not available')}
140 140 % endif
141 141 </li>
142 142
143 143 ## pull url
144 144 <li>
145 145 %if h.is_hg(c.pull_request.source_repo):
146 146 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
147 147 %elif h.is_git(c.pull_request.source_repo):
148 148 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
149 149 %endif
150 150
151 151 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
152 152 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
153 153 </li>
154 154
155 155 ## Shadow repo
156 156 <li>
157 157 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
158 158 %if h.is_hg(c.pull_request.target_repo):
159 159 <% clone_url = 'hg clone --update {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
160 160 %elif h.is_git(c.pull_request.target_repo):
161 161 <% clone_url = 'git clone --branch {} {} pull-request-{}'.format(c.pull_request.shadow_merge_ref.name, c.shadow_clone_url, c.pull_request.pull_request_id) %>
162 162 %endif
163 163
164 164 <span class="tooltip" title="${_('Clone repository in its merged state using shadow repository')}">${_('Clone from shadow repository')}</span>: <input type="text" class="input-monospace pr-mergeinfo" value="${clone_url}" readonly="readonly">
165 165 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
166 166
167 167 % else:
168 168 <div class="">
169 169 ${_('Shadow repository data not available')}.
170 170 </div>
171 171 % endif
172 172 </li>
173 173
174 174 </ul>
175 175
176 176 </div>
177 177
178 178 </div>
179 179
180 180 </div>
181 181
182 182 ## versions
183 183 <div class="field">
184 184 <div class="label-pr-detail">
185 185 <label>${_('Versions')}:</label>
186 186 </div>
187 187
188 188 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
189 189 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
190 190
191 191 <div class="pr-versions">
192 192 % if c.show_version_changes:
193 193 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
194 194 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
195 195 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
196 196 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
197 197 data-toggle-on="${_('show versions')}."
198 198 data-toggle-off="${_('hide versions')}.">
199 199 ${_('show versions')}.
200 200 </a>
201 201 <table>
202 202 ## SHOW ALL VERSIONS OF PR
203 203 <% ver_pr = None %>
204 204
205 205 % for data in reversed(list(enumerate(c.versions, 1))):
206 206 <% ver_pos = data[0] %>
207 207 <% ver = data[1] %>
208 208 <% ver_pr = ver.pull_request_version_id %>
209 209 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
210 210
211 211 <tr class="version-pr" style="display: ${display_row}">
212 212 <td>
213 213 <code>
214 214 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
215 215 </code>
216 216 </td>
217 217 <td>
218 218 <input ${('checked="checked"' if c.from_version_index == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
219 219 <input ${('checked="checked"' if c.at_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_target" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
220 220 </td>
221 221 <td>
222 222 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
223 223 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
224 224
225 225 </td>
226 226 <td>
227 227 % if c.at_version_num != ver_pr:
228 228 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
229 229 <code>
230 230 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
231 231 </code>
232 232 % endif
233 233 </td>
234 234 <td>
235 235 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
236 236 </td>
237 237 <td>
238 238 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
239 239 </td>
240 240 </tr>
241 241 % endfor
242 242
243 243 <tr>
244 244 <td colspan="6">
245 245 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
246 246 data-label-text-locked="${_('select versions to show changes')}"
247 247 data-label-text-diff="${_('show changes between versions')}"
248 248 data-label-text-show="${_('show pull request for this version')}"
249 249 >
250 250 ${_('select versions to show changes')}
251 251 </button>
252 252 </td>
253 253 </tr>
254 254 </table>
255 255 % else:
256 256 <div>
257 257 ${_('Pull request versions not available')}.
258 258 </div>
259 259 % endif
260 260 </div>
261 261 </div>
262 262
263 263 </div>
264 264
265 265 </div>
266 266
267 267
268 268 </div>
269 269
270 270 </div>
271 271
272 272 <div class="box">
273 273
274 274 % if c.state_progressing:
275 275
276 276 <h2 style="text-align: center">
277 277 ${_('Cannot show diff when pull request state is changing. Current progress state')}: <span class="tag tag-merge-state-${c.pull_request.state}">${c.pull_request.state}</span>
278 278
279 279 % if c.is_super_admin:
280 280 <br/>
281 281 If you think this is an error try <a href="${h.current_route_path(request, force_state='created')}">forced state reset</a> to <span class="tag tag-merge-state-created">created</span> state.
282 282 % endif
283 283 </h2>
284 284
285 285 % else:
286 286
287 287 ## Diffs rendered here
288 288 <div class="table" >
289 289 <div id="changeset_compare_view_content">
290 290 ##CS
291 291 % if c.missing_requirements:
292 292 <div class="box">
293 293 <div class="alert alert-warning">
294 294 <div>
295 295 <strong>${_('Missing requirements:')}</strong>
296 296 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
297 297 </div>
298 298 </div>
299 299 </div>
300 300 % elif c.missing_commits:
301 301 <div class="box">
302 302 <div class="alert alert-warning">
303 303 <div>
304 304 <strong>${_('Missing commits')}:</strong>
305 305 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}<br/>
306 306 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}<br/>
307 307 ${_('Consider doing a `force update commits` in case you think this is an error.')}
308 308 </div>
309 309 </div>
310 310 </div>
311 311 % elif c.pr_merge_source_commit.changed and not c.pull_request.is_closed():
312 312 <div class="box">
313 313 <div class="alert alert-info">
314 314 <div>
315 315 <strong>${_('There are new changes for `{}:{}` in source repository, please consider updating this pull request.').format(c.pr_merge_source_commit.ref_spec.type, c.pr_merge_source_commit.ref_spec.name)}</strong>
316 316 </div>
317 317 </div>
318 318 </div>
319 319 % endif
320 320
321 321 <div class="compare_view_commits_title">
322 322 % if not c.compare_mode:
323 323
324 324 % if c.at_version_index:
325 325 <h4>
326 326 ${_('Showing changes at v{}, commenting is disabled.').format(c.at_version_index)}
327 327 </h4>
328 328 % endif
329 329
330 330 <div class="pull-left">
331 331 <div class="btn-group">
332 332 <a class="${('collapsed' if c.collapse_all_commits else '')}" href="#expand-commits" onclick="toggleCommitExpand(this); return false" data-toggle-commits-cnt=${len(c.commit_ranges)} >
333 333 % if c.collapse_all_commits:
334 334 <i class="icon-plus-squared-alt icon-no-margin"></i>
335 335 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
336 336 % else:
337 337 <i class="icon-minus-squared-alt icon-no-margin"></i>
338 338 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
339 339 % endif
340 340 </a>
341 341 </div>
342 342 </div>
343 343
344 344 <div class="pull-right">
345 345 % if c.allowed_to_update and not c.pull_request.is_closed():
346 346
347 347 <div class="btn-group btn-group-actions">
348 348 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
349 349 ${_('Update commits')}
350 350 </a>
351 351
352 352 <a id="update_commits_switcher" class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more update options')}">
353 353 <i class="icon-down"></i>
354 354 </a>
355 355
356 356 <div class="btn-action-switcher-container right-align" id="update-commits-switcher">
357 357 <ul class="btn-action-switcher" role="menu" style="min-width: 300px;">
358 358 <li>
359 359 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
360 360 ${_('Force update commits')}
361 361 </a>
362 362 <div class="action-help-block">
363 363 ${_('Update commits and force refresh this pull request.')}
364 364 </div>
365 365 </li>
366 366 </ul>
367 367 </div>
368 368 </div>
369 369
370 370 % else:
371 371 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
372 372 % endif
373 373
374 374 </div>
375 375 % endif
376 376 </div>
377 377
378 378 % if not c.missing_commits:
379 379 ## COMPARE RANGE DIFF MODE
380 380 % if c.compare_mode:
381 381 % if c.at_version:
382 382 <h4>
383 383 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_index, ver_to=c.at_version_index if c.at_version_index else 'latest')}:
384 384 </h4>
385 385
386 386 <div class="subtitle-compare">
387 387 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
388 388 </div>
389 389
390 390 <div class="container">
391 391 <table class="rctable compare_view_commits">
392 392 <tr>
393 393 <th></th>
394 394 <th>${_('Time')}</th>
395 395 <th>${_('Author')}</th>
396 396 <th>${_('Commit')}</th>
397 397 <th></th>
398 398 <th>${_('Description')}</th>
399 399 </tr>
400 400
401 401 % for c_type, commit in c.commit_changes:
402 402 % if c_type in ['a', 'r']:
403 403 <%
404 404 if c_type == 'a':
405 405 cc_title = _('Commit added in displayed changes')
406 406 elif c_type == 'r':
407 407 cc_title = _('Commit removed in displayed changes')
408 408 else:
409 409 cc_title = ''
410 410 %>
411 411 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
412 412 <td>
413 413 <div class="commit-change-indicator color-${c_type}-border">
414 414 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
415 415 ${c_type.upper()}
416 416 </div>
417 417 </div>
418 418 </td>
419 419 <td class="td-time">
420 420 ${h.age_component(commit.date)}
421 421 </td>
422 422 <td class="td-user">
423 423 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
424 424 </td>
425 425 <td class="td-hash">
426 426 <code>
427 427 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
428 428 r${commit.idx}:${h.short_id(commit.raw_id)}
429 429 </a>
430 430 ${h.hidden('revisions', commit.raw_id)}
431 431 </code>
432 432 </td>
433 433 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
434 434 <i class="icon-expand-linked"></i>
435 435 </td>
436 436 <td class="mid td-description">
437 437 <div class="log-container truncate-wrap">
438 438 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name, issues_container=c.referenced_commit_issues)}</div>
439 439 </div>
440 440 </td>
441 441 </tr>
442 442 % endif
443 443 % endfor
444 444 </table>
445 445 </div>
446 446
447 447 % endif
448 448
449 449 ## Regular DIFF
450 450 % else:
451 451 <%include file="/compare/compare_commits.mako" />
452 452 % endif
453 453
454 454 <div class="cs_files">
455 455 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
456 456
457 457 <%
458 458 pr_menu_data = {
459 459 'outdated_comm_count_ver': outdated_comm_count_ver,
460 460 'pull_request': c.pull_request
461 461 }
462 462 %>
463 463
464 464 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on, pull_request_menu=pr_menu_data)}
465 465
466 466 % if c.range_diff_on:
467 467 % for commit in c.commit_ranges:
468 468 ${cbdiffs.render_diffset(
469 469 c.changes[commit.raw_id],
470 470 commit=commit, use_comments=True,
471 471 collapse_when_files_over=5,
472 472 disable_new_comments=True,
473 473 deleted_files_comments=c.deleted_files_comments,
474 474 inline_comments=c.inline_comments,
475 475 pull_request_menu=pr_menu_data, show_todos=False)}
476 476 % endfor
477 477 % else:
478 478 ${cbdiffs.render_diffset(
479 479 c.diffset, use_comments=True,
480 480 collapse_when_files_over=30,
481 481 disable_new_comments=not c.allowed_to_comment,
482 482 deleted_files_comments=c.deleted_files_comments,
483 483 inline_comments=c.inline_comments,
484 484 pull_request_menu=pr_menu_data, show_todos=False)}
485 485 % endif
486 486
487 487 </div>
488 488 % else:
489 489 ## skipping commits we need to clear the view for missing commits
490 490 <div style="clear:both;"></div>
491 491 % endif
492 492
493 493 </div>
494 494 </div>
495 495
496 496 ## template for inline comment form
497 497 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
498 498
499 499 ## comments heading with count
500 500 <div class="comments-heading">
501 501 <i class="icon-comment"></i>
502 502 ${_('General Comments')} ${len(c.comments)}
503 503 </div>
504 504
505 505 ## render general comments
506 506 <div id="comment-tr-show">
507 507 % if general_outdated_comm_count_ver:
508 508 <div class="info-box">
509 509 % if general_outdated_comm_count_ver == 1:
510 510 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
511 511 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
512 512 % else:
513 513 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
514 514 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
515 515 % endif
516 516 </div>
517 517 % endif
518 518 </div>
519 519
520 520 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
521 521
522 522 % if not c.pull_request.is_closed():
523 523 ## main comment form and it status
524 524 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
525 525 pull_request_id=c.pull_request.pull_request_id),
526 526 c.pull_request_review_status,
527 527 is_pull_request=True, change_status=c.allowed_to_change_status)}
528 528
529 529 ## merge status, and merge action
530 530 <div class="pull-request-merge">
531 531 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
532 532 </div>
533 533
534 534 %endif
535 535
536 536 % endif
537 537 </div>
538 538
539 539
540 540 ### NAV SIDEBAR
541 541 <aside class="right-sidebar right-sidebar-expanded" id="pr-nav-sticky" style="display: none">
542 542 <div class="sidenav navbar__inner" >
543 543 ## TOGGLE
544 544 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
545 545 <a href="#toggleSidebar" class="grey-link-action">
546 546
547 547 </a>
548 548 </div>
549 549
550 550 ## CONTENT
551 551 <div class="sidebar-content">
552 552
553 553 ## Drafts
554 554 % if c.rhodecode_edition_id == 'EE':
555 <div class="sidebar-element clear-both">
556 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Drafts')}">
555 <div id="draftsTable" class="sidebar-element clear-both" style="display: ${'block' if c.draft_comments else 'none'}">
556 <div class="tooltip right-sidebar-collapsed-state" style="display: none;" onclick="toggleSidebar(); return false" title="${_('Drafts')}">
557 557 <i class="icon-comment icon-draft"></i>
558 <span id="comments-count">${0}</span>
558 <span id="drafts-count">${len(c.draft_comments)}</span>
559 559 </div>
560 560
561 561 <div class="right-sidebar-expanded-state pr-details-title">
562 <span class="sidebar-heading noselect">
562 <span style="padding-left: 2px">
563 <input name="select_all_drafts" type="checkbox" onclick="$('[name=submit_draft]').prop('checked', !$('[name=submit_draft]').prop('checked'))">
564 </span>
565 <span class="sidebar-heading noselect" onclick="refreshDraftComments(); return false">
563 566 <i class="icon-comment icon-draft"></i>
564 567 ${_('Drafts')}
565 568 </span>
569 <span class="block-right action_button last-item" onclick="submitDrafts(event)">${_('Submit')}</span>
566 570 </div>
567 571
568 572 <div id="drafts" class="right-sidebar-expanded-state pr-details-content reviewers">
569 ## members redering block
570
571
572 ???
573
574
575 ## end members redering block
576
573 % if c.draft_comments:
574 ${sidebar.comments_table(c.draft_comments, len(c.draft_comments), draft_comments=True)}
575 % else:
576 <table class="drafts-content-table">
577 <tr>
578 <td>
579 ${_('No TODOs yet')}
580 </td>
581 </tr>
582 </table>
583 % endif
577 584 </div>
578 585
579 586 </div>
580 587 % endif
581 588
582 589 ## RULES SUMMARY/RULES
583 590 <div class="sidebar-element clear-both">
584 591 <% vote_title = _ungettext(
585 592 'Status calculated based on votes from {} reviewer',
586 593 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
587 594 %>
588 595
589 596 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
590 597 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
591 598 ${c.reviewers_count}
592 599 </div>
593 600
594 601 ## REVIEW RULES
595 602 <div id="review_rules" style="display: none" class="">
596 603 <div class="right-sidebar-expanded-state pr-details-title">
597 604 <span class="sidebar-heading">
598 605 ${_('Reviewer rules')}
599 606 </span>
600 607
601 608 </div>
602 609 <div class="pr-reviewer-rules">
603 610 ## review rules will be appended here, by default reviewers logic
604 611 </div>
605 612 <input id="review_data" type="hidden" name="review_data" value="">
606 613 </div>
607 614
608 615 ## REVIEWERS
609 616 <div class="right-sidebar-expanded-state pr-details-title">
610 617 <span class="tooltip sidebar-heading" title="${vote_title}">
611 618 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
612 619 ${_('Reviewers')}
613 620 </span>
614 621 %if c.allowed_to_update:
615 622 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
616 623 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
617 624 %else:
618 625 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Show rules')}</span>
619 626 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
620 627 %endif
621 628 </div>
622 629
623 630 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
624 631
625 632 ## members redering block
626 633 <input type="hidden" name="__start__" value="review_members:sequence">
627 634
628 635 <table id="review_members" class="group_members">
629 636 ## This content is loaded via JS and ReviewersPanel
630 637 </table>
631 638
632 639 <input type="hidden" name="__end__" value="review_members:sequence">
633 640 ## end members redering block
634 641
635 642 %if not c.pull_request.is_closed():
636 643 <div id="add_reviewer" class="ac" style="display: none;">
637 644 %if c.allowed_to_update:
638 645 % if not c.forbid_adding_reviewers:
639 646 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px">
640 647 <input class="ac-input" id="user" name="user" placeholder="${_('Add reviewer or reviewer group')}" type="text" autocomplete="off">
641 648 <div id="reviewers_container"></div>
642 649 </div>
643 650 % endif
644 651 <div class="pull-right" style="margin-bottom: 15px">
645 652 <button data-role="reviewer" id="update_reviewers" class="btn btn-small no-margin">${_('Save Changes')}</button>
646 653 </div>
647 654 %endif
648 655 </div>
649 656 %endif
650 657 </div>
651 658 </div>
652 659
653 660 ## OBSERVERS
654 661 % if c.rhodecode_edition_id == 'EE':
655 662 <div class="sidebar-element clear-both">
656 663 <% vote_title = _ungettext(
657 664 '{} observer without voting right.',
658 665 '{} observers without voting right.', c.observers_count).format(c.observers_count)
659 666 %>
660 667
661 668 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
662 669 <i class="icon-circle-thin"></i>
663 670 ${c.observers_count}
664 671 </div>
665 672
666 673 <div class="right-sidebar-expanded-state pr-details-title">
667 674 <span class="tooltip sidebar-heading" title="${vote_title}">
668 675 <i class="icon-circle-thin"></i>
669 676 ${_('Observers')}
670 677 </span>
671 678 %if c.allowed_to_update:
672 679 <span id="open_edit_observers" class="block-right action_button last-item">${_('Edit')}</span>
673 680 <span id="close_edit_observers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
674 681 %endif
675 682 </div>
676 683
677 684 <div id="observers" class="right-sidebar-expanded-state pr-details-content reviewers">
678 685 ## members redering block
679 686 <input type="hidden" name="__start__" value="observer_members:sequence">
680 687
681 688 <table id="observer_members" class="group_members">
682 689 ## This content is loaded via JS and ReviewersPanel
683 690 </table>
684 691
685 692 <input type="hidden" name="__end__" value="observer_members:sequence">
686 693 ## end members redering block
687 694
688 695 %if not c.pull_request.is_closed():
689 696 <div id="add_observer" class="ac" style="display: none;">
690 697 %if c.allowed_to_update:
691 698 % if not c.forbid_adding_reviewers or 1:
692 699 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px" >
693 700 <input class="ac-input" id="observer" name="observer" placeholder="${_('Add observer or observer group')}" type="text" autocomplete="off">
694 701 <div id="observers_container"></div>
695 702 </div>
696 703 % endif
697 704 <div class="pull-right" style="margin-bottom: 15px">
698 705 <button data-role="observer" id="update_observers" class="btn btn-small no-margin">${_('Save Changes')}</button>
699 706 </div>
700 707 %endif
701 708 </div>
702 709 %endif
703 710 </div>
704 711 </div>
705 712 % endif
706 713
707 714 ## TODOs
708 <div class="sidebar-element clear-both">
715 <div id="todosTable" class="sidebar-element clear-both">
709 716 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
710 717 <i class="icon-flag-filled"></i>
711 718 <span id="todos-count">${len(c.unresolved_comments)}</span>
712 719 </div>
713 720
714 721 <div class="right-sidebar-expanded-state pr-details-title">
715 722 ## Only show unresolved, that is only what matters
716 723 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
717 724 <i class="icon-flag-filled"></i>
718 725 TODOs
719 726 </span>
720 727
721 728 % if not c.at_version:
722 729 % if c.resolved_comments:
723 730 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return toggleElement(this, '.resolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
724 731 % else:
725 732 <span class="block-right last-item noselect">Show resolved</span>
726 733 % endif
727 734 % endif
728 735 </div>
729 736
730 737 <div class="right-sidebar-expanded-state pr-details-content">
731 738
732 739 % if c.at_version:
733 740 <table>
734 741 <tr>
735 742 <td class="unresolved-todo-text">${_('TODOs unavailable when browsing versions')}.</td>
736 743 </tr>
737 744 </table>
738 745 % else:
739 746 % if c.unresolved_comments + c.resolved_comments:
740 747 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
741 748 % else:
742 <table>
749 <table class="todos-content-table">
743 750 <tr>
744 751 <td>
745 752 ${_('No TODOs yet')}
746 753 </td>
747 754 </tr>
748 755 </table>
749 756 % endif
750 757 % endif
751 758 </div>
752 759 </div>
753 760
754 761 ## COMMENTS
755 <div class="sidebar-element clear-both">
762 <div id="commentsTable" class="sidebar-element clear-both">
756 763 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
757 764 <i class="icon-comment" style="color: #949494"></i>
758 765 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
759 766 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
760 767 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
761 768 </div>
762 769
763 770 <div class="right-sidebar-expanded-state pr-details-title">
764 771 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
765 772 <i class="icon-comment" style="color: #949494"></i>
766 773 ${_('Comments')}
767 774
768 775 ## % if outdated_comm_count_ver:
769 776 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
770 777 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
771 778 ## </a>
772 779 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
773 780 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
774 781
775 782 ## % else:
776 783 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
777 784 ## % endif
778 785
779 786 </span>
780 787
781 788 % if outdated_comm_count_ver:
782 789 <span class="block-right action_button last-item noselect" onclick="return toggleElement(this, '.hidden-comment');" data-toggle-on="Show outdated" data-toggle-off="Hide outdated">Show outdated</span>
783 790 % else:
784 791 <span class="block-right last-item noselect">Show hidden</span>
785 792 % endif
786 793
787 794 </div>
788 795
789 796 <div class="right-sidebar-expanded-state pr-details-content">
790 797 % if c.inline_comments_flat + c.comments:
791 798 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
792 799 % else:
793 <table>
800 <table class="comments-content-table">
794 801 <tr>
795 802 <td>
796 803 ${_('No Comments yet')}
797 804 </td>
798 805 </tr>
799 806 </table>
800 807 % endif
801 808 </div>
802 809
803 810 </div>
804 811
805 812 ## Referenced Tickets
806 813 <div class="sidebar-element clear-both">
807 814 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Referenced Tickets')}">
808 815 <i class="icon-info-circled"></i>
809 816 ${(len(c.referenced_desc_issues) + len(c.referenced_commit_issues))}
810 817 </div>
811 818
812 819 <div class="right-sidebar-expanded-state pr-details-title">
813 820 <span class="sidebar-heading">
814 821 <i class="icon-info-circled"></i>
815 822 ${_('Referenced Tickets')}
816 823 </span>
817 824 </div>
818 825 <div class="right-sidebar-expanded-state pr-details-content">
819 826 <table>
820 827
821 828 <tr><td><code>${_('In pull request description')}:</code></td></tr>
822 829 % if c.referenced_desc_issues:
823 830 % for ticket_dict in sorted(c.referenced_desc_issues):
824 831 <tr>
825 832 <td>
826 833 <a href="${ticket_dict.get('url')}">
827 834 ${ticket_dict.get('id')}
828 835 </a>
829 836 </td>
830 837 </tr>
831 838 % endfor
832 839 % else:
833 840 <tr>
834 841 <td>
835 842 ${_('No Ticket data found.')}
836 843 </td>
837 844 </tr>
838 845 % endif
839 846
840 847 <tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
841 848 % if c.referenced_commit_issues:
842 849 % for ticket_dict in sorted(c.referenced_commit_issues):
843 850 <tr>
844 851 <td>
845 852 <a href="${ticket_dict.get('url')}">
846 853 ${ticket_dict.get('id')}
847 854 </a>
848 855 </td>
849 856 </tr>
850 857 % endfor
851 858 % else:
852 859 <tr>
853 860 <td>
854 861 ${_('No Ticket data found.')}
855 862 </td>
856 863 </tr>
857 864 % endif
858 865 </table>
859 866
860 867 </div>
861 868 </div>
862 869
863 870 </div>
864 871
865 872 </div>
866 873 </aside>
867 874
868 875 ## This JS needs to be at the end
869 876 <script type="text/javascript">
870 877
871 878 versionController = new VersionController();
872 879 versionController.init();
873 880
874 881 reviewersController = new ReviewersController();
875 882 commitsController = new CommitsController();
876 883 commentsController = new CommentsController();
877 884
878 885 updateController = new UpdatePrController();
879 886
880 887 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
881 888 window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
882 889 window.setObserversData = ${c.pull_request_set_observers_data_json | n};
883 890
884 891 (function () {
885 892 "use strict";
886 893
887 894 // custom code mirror
888 895 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
889 896
890 897 PRDetails.init();
891 898 ReviewersPanel.init(reviewersController, reviewerRulesData, setReviewersData);
892 899 ObserversPanel.init(reviewersController, reviewerRulesData, setObserversData);
893 900
894 901 window.showOutdated = function (self) {
895 902 $('.comment-inline.comment-outdated').show();
896 903 $('.filediff-outdated').show();
897 904 $('.showOutdatedComments').hide();
898 905 $('.hideOutdatedComments').show();
899 906 };
900 907
901 908 window.hideOutdated = function (self) {
902 909 $('.comment-inline.comment-outdated').hide();
903 910 $('.filediff-outdated').hide();
904 911 $('.hideOutdatedComments').hide();
905 912 $('.showOutdatedComments').show();
906 913 };
907 914
908 915 window.refreshMergeChecks = function () {
909 916 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
910 917 $('.pull-request-merge').css('opacity', 0.3);
911 918 $('.action-buttons-extra').css('opacity', 0.3);
912 919
913 920 $('.pull-request-merge').load(
914 921 loadUrl, function () {
915 922 $('.pull-request-merge').css('opacity', 1);
916 923
917 924 $('.action-buttons-extra').css('opacity', 1);
918 925 }
919 926 );
920 927 };
921 928
929 window.submitDrafts = function (event) {
930 var target = $(event.currentTarget);
931 var callback = function (result) {
932 target.removeAttr('onclick').html('saving...');
933 }
934 var draftIds = [];
935 $.each($('[name=submit_draft]:checked'), function (idx, val) {
936 draftIds.push(parseInt($(val).val()));
937 })
938 if (draftIds.length > 0) {
939 Rhodecode.comments.finalizeDrafts(draftIds, callback);
940 }
941 else {
942
943 }
944 }
945
922 946 window.closePullRequest = function (status) {
923 947 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
924 948 return false;
925 949 }
926 950 // inject closing flag
927 951 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
928 952 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
929 953 $(generalCommentForm.submitForm).submit();
930 954 };
931 955
932 956 //TODO this functionality is now missing
933 957 $('#show-outdated-comments').on('click', function (e) {
934 958 var button = $(this);
935 959 var outdated = $('.comment-outdated');
936 960
937 961 if (button.html() === "(Show)") {
938 962 button.html("(Hide)");
939 963 outdated.show();
940 964 } else {
941 965 button.html("(Show)");
942 966 outdated.hide();
943 967 }
944 968 });
945 969
946 970 $('#merge_pull_request_form').submit(function () {
947 971 if (!$('#merge_pull_request').attr('disabled')) {
948 972 $('#merge_pull_request').attr('disabled', 'disabled');
949 973 }
950 974 return true;
951 975 });
952 976
953 977 $('#edit_pull_request').on('click', function (e) {
954 978 var title = $('#pr-title-input').val();
955 979 var description = codeMirrorInstance.getValue();
956 980 var renderer = $('#pr-renderer-input').val();
957 981 editPullRequest(
958 982 "${c.repo_name}", "${c.pull_request.pull_request_id}",
959 983 title, description, renderer);
960 984 });
961 985
962 986 var $updateButtons = $('#update_reviewers,#update_observers');
963 987 $updateButtons.on('click', function (e) {
964 988 var role = $(this).data('role');
965 989 $updateButtons.attr('disabled', 'disabled');
966 990 $updateButtons.addClass('disabled');
967 991 $updateButtons.html(_gettext('Saving...'));
968 992 reviewersController.updateReviewers(
969 993 templateContext.repo_name,
970 994 templateContext.pull_request_data.pull_request_id,
971 995 role
972 996 );
973 997 });
974 998
975 999 // fixing issue with caches on firefox
976 1000 $('#update_commits').removeAttr("disabled");
977 1001
978 1002 $('.show-inline-comments').on('click', function (e) {
979 1003 var boxid = $(this).attr('data-comment-id');
980 1004 var button = $(this);
981 1005
982 1006 if (button.hasClass("comments-visible")) {
983 1007 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
984 1008 $(this).hide();
985 1009 });
986 1010 button.removeClass("comments-visible");
987 1011 } else {
988 1012 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
989 1013 $(this).show();
990 1014 });
991 1015 button.addClass("comments-visible");
992 1016 }
993 1017 });
994 1018
995 1019 $('.show-inline-comments').on('change', function (e) {
996 1020 var show = 'none';
997 1021 var target = e.currentTarget;
998 1022 if (target.checked) {
999 1023 show = ''
1000 1024 }
1001 1025 var boxid = $(target).attr('id_for');
1002 1026 var comments = $('#{0} .inline-comments'.format(boxid));
1003 1027 var fn_display = function (idx) {
1004 1028 $(this).css('display', show);
1005 1029 };
1006 1030 $(comments).each(fn_display);
1007 1031 var btns = $('#{0} .inline-comments-button'.format(boxid));
1008 1032 $(btns).each(fn_display);
1009 1033 });
1010 1034
1011 1035 // register submit callback on commentForm form to track TODOs, and refresh mergeChecks conditions
1012 1036 window.commentFormGlobalSubmitSuccessCallback = function (comment) {
1013 1037 if (!comment.draft) {
1014 1038 refreshMergeChecks();
1015 1039 }
1016 1040 };
1017 1041
1018 1042 ReviewerAutoComplete('#user', reviewersController);
1019 1043 ObserverAutoComplete('#observer', reviewersController);
1020 1044
1021 1045 })();
1022 1046
1023 1047 $(document).ready(function () {
1024 1048
1025 1049 var channel = '${c.pr_broadcast_channel}';
1026 1050 new ReviewerPresenceController(channel)
1027 1051 // register globally so inject comment logic can re-use it.
1028 1052 window.commentsController = commentsController;
1029
1030 1053 })
1031 1054 </script>
1032 1055
1033 1056 </%def>
General Comments 0
You need to be logged in to leave comments. Login now