##// END OF EJS Templates
drafts: sidebar functionality
milka -
r4562:20bc1204 default
parent child Browse files
Show More
@@ -1,543 +1,548 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2016-2020 RhodeCode GmbH
3 # Copyright (C) 2016-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 from rhodecode.apps._base import add_route_with_slash
20 from rhodecode.apps._base import add_route_with_slash
21
21
22
22
23 def includeme(config):
23 def includeme(config):
24
24
25 # repo creating checks, special cases that aren't repo routes
25 # repo creating checks, special cases that aren't repo routes
26 config.add_route(
26 config.add_route(
27 name='repo_creating',
27 name='repo_creating',
28 pattern='/{repo_name:.*?[^/]}/repo_creating')
28 pattern='/{repo_name:.*?[^/]}/repo_creating')
29
29
30 config.add_route(
30 config.add_route(
31 name='repo_creating_check',
31 name='repo_creating_check',
32 pattern='/{repo_name:.*?[^/]}/repo_creating_check')
32 pattern='/{repo_name:.*?[^/]}/repo_creating_check')
33
33
34 # Summary
34 # Summary
35 # NOTE(marcink): one additional route is defined in very bottom, catch
35 # NOTE(marcink): one additional route is defined in very bottom, catch
36 # all pattern
36 # all pattern
37 config.add_route(
37 config.add_route(
38 name='repo_summary_explicit',
38 name='repo_summary_explicit',
39 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
39 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
40 config.add_route(
40 config.add_route(
41 name='repo_summary_commits',
41 name='repo_summary_commits',
42 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
42 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
43
43
44 # Commits
44 # Commits
45 config.add_route(
45 config.add_route(
46 name='repo_commit',
46 name='repo_commit',
47 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
47 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
48
48
49 config.add_route(
49 config.add_route(
50 name='repo_commit_children',
50 name='repo_commit_children',
51 pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
51 pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
52
52
53 config.add_route(
53 config.add_route(
54 name='repo_commit_parents',
54 name='repo_commit_parents',
55 pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
55 pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
56
56
57 config.add_route(
57 config.add_route(
58 name='repo_commit_raw',
58 name='repo_commit_raw',
59 pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
59 pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
60
60
61 config.add_route(
61 config.add_route(
62 name='repo_commit_patch',
62 name='repo_commit_patch',
63 pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
63 pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
64
64
65 config.add_route(
65 config.add_route(
66 name='repo_commit_download',
66 name='repo_commit_download',
67 pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
67 pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
68
68
69 config.add_route(
69 config.add_route(
70 name='repo_commit_data',
70 name='repo_commit_data',
71 pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
71 pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
72
72
73 config.add_route(
73 config.add_route(
74 name='repo_commit_comment_create',
74 name='repo_commit_comment_create',
75 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
75 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
76
76
77 config.add_route(
77 config.add_route(
78 name='repo_commit_comment_preview',
78 name='repo_commit_comment_preview',
79 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
79 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
80
80
81 config.add_route(
81 config.add_route(
82 name='repo_commit_comment_history_view',
82 name='repo_commit_comment_history_view',
83 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_history_id}/history_view', repo_route=True)
83 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_history_id}/history_view', repo_route=True)
84
84
85 config.add_route(
85 config.add_route(
86 name='repo_commit_comment_attachment_upload',
86 name='repo_commit_comment_attachment_upload',
87 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/attachment_upload', repo_route=True)
87 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/attachment_upload', repo_route=True)
88
88
89 config.add_route(
89 config.add_route(
90 name='repo_commit_comment_delete',
90 name='repo_commit_comment_delete',
91 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
91 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
92
92
93 config.add_route(
93 config.add_route(
94 name='repo_commit_comment_edit',
94 name='repo_commit_comment_edit',
95 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/edit', repo_route=True)
95 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/edit', repo_route=True)
96
96
97 # still working url for backward compat.
97 # still working url for backward compat.
98 config.add_route(
98 config.add_route(
99 name='repo_commit_raw_deprecated',
99 name='repo_commit_raw_deprecated',
100 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
100 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
101
101
102 # Files
102 # Files
103 config.add_route(
103 config.add_route(
104 name='repo_archivefile',
104 name='repo_archivefile',
105 pattern='/{repo_name:.*?[^/]}/archive/{fname:.*}', repo_route=True)
105 pattern='/{repo_name:.*?[^/]}/archive/{fname:.*}', repo_route=True)
106
106
107 config.add_route(
107 config.add_route(
108 name='repo_files_diff',
108 name='repo_files_diff',
109 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
109 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
110 config.add_route( # legacy route to make old links work
110 config.add_route( # legacy route to make old links work
111 name='repo_files_diff_2way_redirect',
111 name='repo_files_diff_2way_redirect',
112 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
112 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
113
113
114 config.add_route(
114 config.add_route(
115 name='repo_files',
115 name='repo_files',
116 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
116 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
117 config.add_route(
117 config.add_route(
118 name='repo_files:default_path',
118 name='repo_files:default_path',
119 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
119 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
120 config.add_route(
120 config.add_route(
121 name='repo_files:default_commit',
121 name='repo_files:default_commit',
122 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
122 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
123
123
124 config.add_route(
124 config.add_route(
125 name='repo_files:rendered',
125 name='repo_files:rendered',
126 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
126 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
127
127
128 config.add_route(
128 config.add_route(
129 name='repo_files:annotated',
129 name='repo_files:annotated',
130 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
130 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
131 config.add_route(
131 config.add_route(
132 name='repo_files:annotated_previous',
132 name='repo_files:annotated_previous',
133 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
133 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
134
134
135 config.add_route(
135 config.add_route(
136 name='repo_nodetree_full',
136 name='repo_nodetree_full',
137 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
137 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
138 config.add_route(
138 config.add_route(
139 name='repo_nodetree_full:default_path',
139 name='repo_nodetree_full:default_path',
140 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
140 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
141
141
142 config.add_route(
142 config.add_route(
143 name='repo_files_nodelist',
143 name='repo_files_nodelist',
144 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
144 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
145
145
146 config.add_route(
146 config.add_route(
147 name='repo_file_raw',
147 name='repo_file_raw',
148 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
148 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
149
149
150 config.add_route(
150 config.add_route(
151 name='repo_file_download',
151 name='repo_file_download',
152 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
152 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
153 config.add_route( # backward compat to keep old links working
153 config.add_route( # backward compat to keep old links working
154 name='repo_file_download:legacy',
154 name='repo_file_download:legacy',
155 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
155 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
156 repo_route=True)
156 repo_route=True)
157
157
158 config.add_route(
158 config.add_route(
159 name='repo_file_history',
159 name='repo_file_history',
160 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
160 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
161
161
162 config.add_route(
162 config.add_route(
163 name='repo_file_authors',
163 name='repo_file_authors',
164 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
164 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
165
165
166 config.add_route(
166 config.add_route(
167 name='repo_files_check_head',
167 name='repo_files_check_head',
168 pattern='/{repo_name:.*?[^/]}/check_head/{commit_id}/{f_path:.*}',
168 pattern='/{repo_name:.*?[^/]}/check_head/{commit_id}/{f_path:.*}',
169 repo_route=True)
169 repo_route=True)
170 config.add_route(
170 config.add_route(
171 name='repo_files_remove_file',
171 name='repo_files_remove_file',
172 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
172 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
173 repo_route=True)
173 repo_route=True)
174 config.add_route(
174 config.add_route(
175 name='repo_files_delete_file',
175 name='repo_files_delete_file',
176 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
176 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
177 repo_route=True)
177 repo_route=True)
178 config.add_route(
178 config.add_route(
179 name='repo_files_edit_file',
179 name='repo_files_edit_file',
180 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
180 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
181 repo_route=True)
181 repo_route=True)
182 config.add_route(
182 config.add_route(
183 name='repo_files_update_file',
183 name='repo_files_update_file',
184 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
184 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
185 repo_route=True)
185 repo_route=True)
186 config.add_route(
186 config.add_route(
187 name='repo_files_add_file',
187 name='repo_files_add_file',
188 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
188 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
189 repo_route=True)
189 repo_route=True)
190 config.add_route(
190 config.add_route(
191 name='repo_files_upload_file',
191 name='repo_files_upload_file',
192 pattern='/{repo_name:.*?[^/]}/upload_file/{commit_id}/{f_path:.*}',
192 pattern='/{repo_name:.*?[^/]}/upload_file/{commit_id}/{f_path:.*}',
193 repo_route=True)
193 repo_route=True)
194 config.add_route(
194 config.add_route(
195 name='repo_files_create_file',
195 name='repo_files_create_file',
196 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
196 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
197 repo_route=True)
197 repo_route=True)
198
198
199 # Refs data
199 # Refs data
200 config.add_route(
200 config.add_route(
201 name='repo_refs_data',
201 name='repo_refs_data',
202 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
202 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
203
203
204 config.add_route(
204 config.add_route(
205 name='repo_refs_changelog_data',
205 name='repo_refs_changelog_data',
206 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
206 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
207
207
208 config.add_route(
208 config.add_route(
209 name='repo_stats',
209 name='repo_stats',
210 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
210 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
211
211
212 # Commits
212 # Commits
213 config.add_route(
213 config.add_route(
214 name='repo_commits',
214 name='repo_commits',
215 pattern='/{repo_name:.*?[^/]}/commits', repo_route=True)
215 pattern='/{repo_name:.*?[^/]}/commits', repo_route=True)
216 config.add_route(
216 config.add_route(
217 name='repo_commits_file',
217 name='repo_commits_file',
218 pattern='/{repo_name:.*?[^/]}/commits/{commit_id}/{f_path:.*}', repo_route=True)
218 pattern='/{repo_name:.*?[^/]}/commits/{commit_id}/{f_path:.*}', repo_route=True)
219 config.add_route(
219 config.add_route(
220 name='repo_commits_elements',
220 name='repo_commits_elements',
221 pattern='/{repo_name:.*?[^/]}/commits_elements', repo_route=True)
221 pattern='/{repo_name:.*?[^/]}/commits_elements', repo_route=True)
222 config.add_route(
222 config.add_route(
223 name='repo_commits_elements_file',
223 name='repo_commits_elements_file',
224 pattern='/{repo_name:.*?[^/]}/commits_elements/{commit_id}/{f_path:.*}', repo_route=True)
224 pattern='/{repo_name:.*?[^/]}/commits_elements/{commit_id}/{f_path:.*}', repo_route=True)
225
225
226 # Changelog (old deprecated name for commits page)
226 # Changelog (old deprecated name for commits page)
227 config.add_route(
227 config.add_route(
228 name='repo_changelog',
228 name='repo_changelog',
229 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
229 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
230 config.add_route(
230 config.add_route(
231 name='repo_changelog_file',
231 name='repo_changelog_file',
232 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
232 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
233
233
234 # Compare
234 # Compare
235 config.add_route(
235 config.add_route(
236 name='repo_compare_select',
236 name='repo_compare_select',
237 pattern='/{repo_name:.*?[^/]}/compare', repo_route=True)
237 pattern='/{repo_name:.*?[^/]}/compare', repo_route=True)
238
238
239 config.add_route(
239 config.add_route(
240 name='repo_compare',
240 name='repo_compare',
241 pattern='/{repo_name:.*?[^/]}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', repo_route=True)
241 pattern='/{repo_name:.*?[^/]}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', repo_route=True)
242
242
243 # Tags
243 # Tags
244 config.add_route(
244 config.add_route(
245 name='tags_home',
245 name='tags_home',
246 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
246 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
247
247
248 # Branches
248 # Branches
249 config.add_route(
249 config.add_route(
250 name='branches_home',
250 name='branches_home',
251 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
251 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
252
252
253 # Bookmarks
253 # Bookmarks
254 config.add_route(
254 config.add_route(
255 name='bookmarks_home',
255 name='bookmarks_home',
256 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
256 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
257
257
258 # Forks
258 # Forks
259 config.add_route(
259 config.add_route(
260 name='repo_fork_new',
260 name='repo_fork_new',
261 pattern='/{repo_name:.*?[^/]}/fork', repo_route=True,
261 pattern='/{repo_name:.*?[^/]}/fork', repo_route=True,
262 repo_forbid_when_archived=True,
262 repo_forbid_when_archived=True,
263 repo_accepted_types=['hg', 'git'])
263 repo_accepted_types=['hg', 'git'])
264
264
265 config.add_route(
265 config.add_route(
266 name='repo_fork_create',
266 name='repo_fork_create',
267 pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True,
267 pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True,
268 repo_forbid_when_archived=True,
268 repo_forbid_when_archived=True,
269 repo_accepted_types=['hg', 'git'])
269 repo_accepted_types=['hg', 'git'])
270
270
271 config.add_route(
271 config.add_route(
272 name='repo_forks_show_all',
272 name='repo_forks_show_all',
273 pattern='/{repo_name:.*?[^/]}/forks', repo_route=True,
273 pattern='/{repo_name:.*?[^/]}/forks', repo_route=True,
274 repo_accepted_types=['hg', 'git'])
274 repo_accepted_types=['hg', 'git'])
275 config.add_route(
275 config.add_route(
276 name='repo_forks_data',
276 name='repo_forks_data',
277 pattern='/{repo_name:.*?[^/]}/forks/data', repo_route=True,
277 pattern='/{repo_name:.*?[^/]}/forks/data', repo_route=True,
278 repo_accepted_types=['hg', 'git'])
278 repo_accepted_types=['hg', 'git'])
279
279
280 # Pull Requests
280 # Pull Requests
281 config.add_route(
281 config.add_route(
282 name='pullrequest_show',
282 name='pullrequest_show',
283 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}',
283 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}',
284 repo_route=True)
284 repo_route=True)
285
285
286 config.add_route(
286 config.add_route(
287 name='pullrequest_show_all',
287 name='pullrequest_show_all',
288 pattern='/{repo_name:.*?[^/]}/pull-request',
288 pattern='/{repo_name:.*?[^/]}/pull-request',
289 repo_route=True, repo_accepted_types=['hg', 'git'])
289 repo_route=True, repo_accepted_types=['hg', 'git'])
290
290
291 config.add_route(
291 config.add_route(
292 name='pullrequest_show_all_data',
292 name='pullrequest_show_all_data',
293 pattern='/{repo_name:.*?[^/]}/pull-request-data',
293 pattern='/{repo_name:.*?[^/]}/pull-request-data',
294 repo_route=True, repo_accepted_types=['hg', 'git'])
294 repo_route=True, repo_accepted_types=['hg', 'git'])
295
295
296 config.add_route(
296 config.add_route(
297 name='pullrequest_repo_refs',
297 name='pullrequest_repo_refs',
298 pattern='/{repo_name:.*?[^/]}/pull-request/refs/{target_repo_name:.*?[^/]}',
298 pattern='/{repo_name:.*?[^/]}/pull-request/refs/{target_repo_name:.*?[^/]}',
299 repo_route=True)
299 repo_route=True)
300
300
301 config.add_route(
301 config.add_route(
302 name='pullrequest_repo_targets',
302 name='pullrequest_repo_targets',
303 pattern='/{repo_name:.*?[^/]}/pull-request/repo-targets',
303 pattern='/{repo_name:.*?[^/]}/pull-request/repo-targets',
304 repo_route=True)
304 repo_route=True)
305
305
306 config.add_route(
306 config.add_route(
307 name='pullrequest_new',
307 name='pullrequest_new',
308 pattern='/{repo_name:.*?[^/]}/pull-request/new',
308 pattern='/{repo_name:.*?[^/]}/pull-request/new',
309 repo_route=True, repo_accepted_types=['hg', 'git'],
309 repo_route=True, repo_accepted_types=['hg', 'git'],
310 repo_forbid_when_archived=True)
310 repo_forbid_when_archived=True)
311
311
312 config.add_route(
312 config.add_route(
313 name='pullrequest_create',
313 name='pullrequest_create',
314 pattern='/{repo_name:.*?[^/]}/pull-request/create',
314 pattern='/{repo_name:.*?[^/]}/pull-request/create',
315 repo_route=True, repo_accepted_types=['hg', 'git'],
315 repo_route=True, repo_accepted_types=['hg', 'git'],
316 repo_forbid_when_archived=True)
316 repo_forbid_when_archived=True)
317
317
318 config.add_route(
318 config.add_route(
319 name='pullrequest_update',
319 name='pullrequest_update',
320 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
320 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
321 repo_route=True, repo_forbid_when_archived=True)
321 repo_route=True, repo_forbid_when_archived=True)
322
322
323 config.add_route(
323 config.add_route(
324 name='pullrequest_merge',
324 name='pullrequest_merge',
325 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
325 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
326 repo_route=True, repo_forbid_when_archived=True)
326 repo_route=True, repo_forbid_when_archived=True)
327
327
328 config.add_route(
328 config.add_route(
329 name='pullrequest_delete',
329 name='pullrequest_delete',
330 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
330 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
331 repo_route=True, repo_forbid_when_archived=True)
331 repo_route=True, repo_forbid_when_archived=True)
332
332
333 config.add_route(
333 config.add_route(
334 name='pullrequest_comment_create',
334 name='pullrequest_comment_create',
335 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment',
335 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment',
336 repo_route=True)
336 repo_route=True)
337
337
338 config.add_route(
338 config.add_route(
339 name='pullrequest_comment_edit',
339 name='pullrequest_comment_edit',
340 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/edit',
340 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/edit',
341 repo_route=True, repo_accepted_types=['hg', 'git'])
341 repo_route=True, repo_accepted_types=['hg', 'git'])
342
342
343 config.add_route(
343 config.add_route(
344 name='pullrequest_comment_delete',
344 name='pullrequest_comment_delete',
345 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
345 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
346 repo_route=True, repo_accepted_types=['hg', 'git'])
346 repo_route=True, repo_accepted_types=['hg', 'git'])
347
347
348 config.add_route(
348 config.add_route(
349 name='pullrequest_comments',
349 name='pullrequest_comments',
350 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comments',
350 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comments',
351 repo_route=True)
351 repo_route=True)
352
352
353 config.add_route(
353 config.add_route(
354 name='pullrequest_todos',
354 name='pullrequest_todos',
355 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/todos',
355 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/todos',
356 repo_route=True)
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 # Artifacts, (EE feature)
363 # Artifacts, (EE feature)
359 config.add_route(
364 config.add_route(
360 name='repo_artifacts_list',
365 name='repo_artifacts_list',
361 pattern='/{repo_name:.*?[^/]}/artifacts', repo_route=True)
366 pattern='/{repo_name:.*?[^/]}/artifacts', repo_route=True)
362
367
363 # Settings
368 # Settings
364 config.add_route(
369 config.add_route(
365 name='edit_repo',
370 name='edit_repo',
366 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
371 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
367 # update is POST on edit_repo
372 # update is POST on edit_repo
368
373
369 # Settings advanced
374 # Settings advanced
370 config.add_route(
375 config.add_route(
371 name='edit_repo_advanced',
376 name='edit_repo_advanced',
372 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
377 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
373 config.add_route(
378 config.add_route(
374 name='edit_repo_advanced_archive',
379 name='edit_repo_advanced_archive',
375 pattern='/{repo_name:.*?[^/]}/settings/advanced/archive', repo_route=True)
380 pattern='/{repo_name:.*?[^/]}/settings/advanced/archive', repo_route=True)
376 config.add_route(
381 config.add_route(
377 name='edit_repo_advanced_delete',
382 name='edit_repo_advanced_delete',
378 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
383 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
379 config.add_route(
384 config.add_route(
380 name='edit_repo_advanced_locking',
385 name='edit_repo_advanced_locking',
381 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
386 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
382 config.add_route(
387 config.add_route(
383 name='edit_repo_advanced_journal',
388 name='edit_repo_advanced_journal',
384 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
389 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
385 config.add_route(
390 config.add_route(
386 name='edit_repo_advanced_fork',
391 name='edit_repo_advanced_fork',
387 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
392 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
388
393
389 config.add_route(
394 config.add_route(
390 name='edit_repo_advanced_hooks',
395 name='edit_repo_advanced_hooks',
391 pattern='/{repo_name:.*?[^/]}/settings/advanced/hooks', repo_route=True)
396 pattern='/{repo_name:.*?[^/]}/settings/advanced/hooks', repo_route=True)
392
397
393 # Caches
398 # Caches
394 config.add_route(
399 config.add_route(
395 name='edit_repo_caches',
400 name='edit_repo_caches',
396 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
401 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
397
402
398 # Permissions
403 # Permissions
399 config.add_route(
404 config.add_route(
400 name='edit_repo_perms',
405 name='edit_repo_perms',
401 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
406 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
402
407
403 config.add_route(
408 config.add_route(
404 name='edit_repo_perms_set_private',
409 name='edit_repo_perms_set_private',
405 pattern='/{repo_name:.*?[^/]}/settings/permissions/set_private', repo_route=True)
410 pattern='/{repo_name:.*?[^/]}/settings/permissions/set_private', repo_route=True)
406
411
407 # Permissions Branch (EE feature)
412 # Permissions Branch (EE feature)
408 config.add_route(
413 config.add_route(
409 name='edit_repo_perms_branch',
414 name='edit_repo_perms_branch',
410 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions', repo_route=True)
415 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions', repo_route=True)
411 config.add_route(
416 config.add_route(
412 name='edit_repo_perms_branch_delete',
417 name='edit_repo_perms_branch_delete',
413 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions/{rule_id}/delete',
418 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions/{rule_id}/delete',
414 repo_route=True)
419 repo_route=True)
415
420
416 # Maintenance
421 # Maintenance
417 config.add_route(
422 config.add_route(
418 name='edit_repo_maintenance',
423 name='edit_repo_maintenance',
419 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
424 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
420
425
421 config.add_route(
426 config.add_route(
422 name='edit_repo_maintenance_execute',
427 name='edit_repo_maintenance_execute',
423 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
428 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
424
429
425 # Fields
430 # Fields
426 config.add_route(
431 config.add_route(
427 name='edit_repo_fields',
432 name='edit_repo_fields',
428 pattern='/{repo_name:.*?[^/]}/settings/fields', repo_route=True)
433 pattern='/{repo_name:.*?[^/]}/settings/fields', repo_route=True)
429 config.add_route(
434 config.add_route(
430 name='edit_repo_fields_create',
435 name='edit_repo_fields_create',
431 pattern='/{repo_name:.*?[^/]}/settings/fields/create', repo_route=True)
436 pattern='/{repo_name:.*?[^/]}/settings/fields/create', repo_route=True)
432 config.add_route(
437 config.add_route(
433 name='edit_repo_fields_delete',
438 name='edit_repo_fields_delete',
434 pattern='/{repo_name:.*?[^/]}/settings/fields/{field_id}/delete', repo_route=True)
439 pattern='/{repo_name:.*?[^/]}/settings/fields/{field_id}/delete', repo_route=True)
435
440
436 # Locking
441 # Locking
437 config.add_route(
442 config.add_route(
438 name='repo_edit_toggle_locking',
443 name='repo_edit_toggle_locking',
439 pattern='/{repo_name:.*?[^/]}/settings/toggle_locking', repo_route=True)
444 pattern='/{repo_name:.*?[^/]}/settings/toggle_locking', repo_route=True)
440
445
441 # Remote
446 # Remote
442 config.add_route(
447 config.add_route(
443 name='edit_repo_remote',
448 name='edit_repo_remote',
444 pattern='/{repo_name:.*?[^/]}/settings/remote', repo_route=True)
449 pattern='/{repo_name:.*?[^/]}/settings/remote', repo_route=True)
445 config.add_route(
450 config.add_route(
446 name='edit_repo_remote_pull',
451 name='edit_repo_remote_pull',
447 pattern='/{repo_name:.*?[^/]}/settings/remote/pull', repo_route=True)
452 pattern='/{repo_name:.*?[^/]}/settings/remote/pull', repo_route=True)
448 config.add_route(
453 config.add_route(
449 name='edit_repo_remote_push',
454 name='edit_repo_remote_push',
450 pattern='/{repo_name:.*?[^/]}/settings/remote/push', repo_route=True)
455 pattern='/{repo_name:.*?[^/]}/settings/remote/push', repo_route=True)
451
456
452 # Statistics
457 # Statistics
453 config.add_route(
458 config.add_route(
454 name='edit_repo_statistics',
459 name='edit_repo_statistics',
455 pattern='/{repo_name:.*?[^/]}/settings/statistics', repo_route=True)
460 pattern='/{repo_name:.*?[^/]}/settings/statistics', repo_route=True)
456 config.add_route(
461 config.add_route(
457 name='edit_repo_statistics_reset',
462 name='edit_repo_statistics_reset',
458 pattern='/{repo_name:.*?[^/]}/settings/statistics/update', repo_route=True)
463 pattern='/{repo_name:.*?[^/]}/settings/statistics/update', repo_route=True)
459
464
460 # Issue trackers
465 # Issue trackers
461 config.add_route(
466 config.add_route(
462 name='edit_repo_issuetracker',
467 name='edit_repo_issuetracker',
463 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers', repo_route=True)
468 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers', repo_route=True)
464 config.add_route(
469 config.add_route(
465 name='edit_repo_issuetracker_test',
470 name='edit_repo_issuetracker_test',
466 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/test', repo_route=True)
471 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/test', repo_route=True)
467 config.add_route(
472 config.add_route(
468 name='edit_repo_issuetracker_delete',
473 name='edit_repo_issuetracker_delete',
469 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/delete', repo_route=True)
474 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/delete', repo_route=True)
470 config.add_route(
475 config.add_route(
471 name='edit_repo_issuetracker_update',
476 name='edit_repo_issuetracker_update',
472 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/update', repo_route=True)
477 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/update', repo_route=True)
473
478
474 # VCS Settings
479 # VCS Settings
475 config.add_route(
480 config.add_route(
476 name='edit_repo_vcs',
481 name='edit_repo_vcs',
477 pattern='/{repo_name:.*?[^/]}/settings/vcs', repo_route=True)
482 pattern='/{repo_name:.*?[^/]}/settings/vcs', repo_route=True)
478 config.add_route(
483 config.add_route(
479 name='edit_repo_vcs_update',
484 name='edit_repo_vcs_update',
480 pattern='/{repo_name:.*?[^/]}/settings/vcs/update', repo_route=True)
485 pattern='/{repo_name:.*?[^/]}/settings/vcs/update', repo_route=True)
481
486
482 # svn pattern
487 # svn pattern
483 config.add_route(
488 config.add_route(
484 name='edit_repo_vcs_svn_pattern_delete',
489 name='edit_repo_vcs_svn_pattern_delete',
485 pattern='/{repo_name:.*?[^/]}/settings/vcs/svn_pattern/delete', repo_route=True)
490 pattern='/{repo_name:.*?[^/]}/settings/vcs/svn_pattern/delete', repo_route=True)
486
491
487 # Repo Review Rules (EE feature)
492 # Repo Review Rules (EE feature)
488 config.add_route(
493 config.add_route(
489 name='repo_reviewers',
494 name='repo_reviewers',
490 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
495 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
491
496
492 config.add_route(
497 config.add_route(
493 name='repo_default_reviewers_data',
498 name='repo_default_reviewers_data',
494 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
499 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
495
500
496 # Repo Automation (EE feature)
501 # Repo Automation (EE feature)
497 config.add_route(
502 config.add_route(
498 name='repo_automation',
503 name='repo_automation',
499 pattern='/{repo_name:.*?[^/]}/settings/automation', repo_route=True)
504 pattern='/{repo_name:.*?[^/]}/settings/automation', repo_route=True)
500
505
501 # Strip
506 # Strip
502 config.add_route(
507 config.add_route(
503 name='edit_repo_strip',
508 name='edit_repo_strip',
504 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
509 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
505
510
506 config.add_route(
511 config.add_route(
507 name='strip_check',
512 name='strip_check',
508 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
513 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
509
514
510 config.add_route(
515 config.add_route(
511 name='strip_execute',
516 name='strip_execute',
512 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
517 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
513
518
514 # Audit logs
519 # Audit logs
515 config.add_route(
520 config.add_route(
516 name='edit_repo_audit_logs',
521 name='edit_repo_audit_logs',
517 pattern='/{repo_name:.*?[^/]}/settings/audit_logs', repo_route=True)
522 pattern='/{repo_name:.*?[^/]}/settings/audit_logs', repo_route=True)
518
523
519 # ATOM/RSS Feed, shouldn't contain slashes for outlook compatibility
524 # ATOM/RSS Feed, shouldn't contain slashes for outlook compatibility
520 config.add_route(
525 config.add_route(
521 name='rss_feed_home',
526 name='rss_feed_home',
522 pattern='/{repo_name:.*?[^/]}/feed-rss', repo_route=True)
527 pattern='/{repo_name:.*?[^/]}/feed-rss', repo_route=True)
523
528
524 config.add_route(
529 config.add_route(
525 name='atom_feed_home',
530 name='atom_feed_home',
526 pattern='/{repo_name:.*?[^/]}/feed-atom', repo_route=True)
531 pattern='/{repo_name:.*?[^/]}/feed-atom', repo_route=True)
527
532
528 config.add_route(
533 config.add_route(
529 name='rss_feed_home_old',
534 name='rss_feed_home_old',
530 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
535 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
531
536
532 config.add_route(
537 config.add_route(
533 name='atom_feed_home_old',
538 name='atom_feed_home_old',
534 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
539 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
535
540
536 # NOTE(marcink): needs to be at the end for catch-all
541 # NOTE(marcink): needs to be at the end for catch-all
537 add_route_with_slash(
542 add_route_with_slash(
538 config,
543 config,
539 name='repo_summary',
544 name='repo_summary',
540 pattern='/{repo_name:.*?[^/]}', repo_route=True)
545 pattern='/{repo_name:.*?[^/]}', repo_route=True)
541
546
542 # Scan module for configuration decorators.
547 # Scan module for configuration decorators.
543 config.scan('.views', ignore='.tests')
548 config.scan('.views', ignore='.tests')
@@ -1,1851 +1,1898 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 import logging
21 import logging
22 import collections
22 import collections
23
23
24 import formencode
24 import formencode
25 import formencode.htmlfill
25 import formencode.htmlfill
26 import peppercorn
26 import peppercorn
27 from pyramid.httpexceptions import (
27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 from pyramid.view import view_config
29 from pyramid.view import view_config
30 from pyramid.renderers import render
30 from pyramid.renderers import render
31
31
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
32 from rhodecode.apps._base import RepoAppView, DataGridAppView
33
33
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 from rhodecode.lib.base import vcs_operation_context
35 from rhodecode.lib.base import vcs_operation_context
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.exceptions import CommentVersionMismatch
38 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.ext_json import json
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 from rhodecode.lib.vcs.backends.base import (
43 from rhodecode.lib.vcs.backends.base import (
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
45 from rhodecode.lib.vcs.exceptions import (
45 from rhodecode.lib.vcs.exceptions import (
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.changeset_status import ChangesetStatusModel
48 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.comment import CommentsModel
49 from rhodecode.model.db import (
49 from rhodecode.model.db import (
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 func, false, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 PullRequestReviewers)
51 PullRequestReviewers)
52 from rhodecode.model.forms import PullRequestForm
52 from rhodecode.model.forms import PullRequestForm
53 from rhodecode.model.meta import Session
53 from rhodecode.model.meta import Session
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
55 from rhodecode.model.scm import ScmModel
55 from rhodecode.model.scm import ScmModel
56
56
57 log = logging.getLogger(__name__)
57 log = logging.getLogger(__name__)
58
58
59
59
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
60 class RepoPullRequestsView(RepoAppView, DataGridAppView):
61
61
62 def load_default_context(self):
62 def load_default_context(self):
63 c = self._get_local_tmpl_context(include_app_defaults=True)
63 c = self._get_local_tmpl_context(include_app_defaults=True)
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
64 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
65 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
66 # backward compat., we use for OLD PRs a plain renderer
66 # backward compat., we use for OLD PRs a plain renderer
67 c.renderer = 'plain'
67 c.renderer = 'plain'
68 return c
68 return c
69
69
70 def _get_pull_requests_list(
70 def _get_pull_requests_list(
71 self, repo_name, source, filter_type, opened_by, statuses):
71 self, repo_name, source, filter_type, opened_by, statuses):
72
72
73 draw, start, limit = self._extract_chunk(self.request)
73 draw, start, limit = self._extract_chunk(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
74 search_q, order_by, order_dir = self._extract_ordering(self.request)
75 _render = self.request.get_partial_renderer(
75 _render = self.request.get_partial_renderer(
76 'rhodecode:templates/data_table/_dt_elements.mako')
76 'rhodecode:templates/data_table/_dt_elements.mako')
77
77
78 # pagination
78 # pagination
79
79
80 if filter_type == 'awaiting_review':
80 if filter_type == 'awaiting_review':
81 pull_requests = PullRequestModel().get_awaiting_review(
81 pull_requests = PullRequestModel().get_awaiting_review(
82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
82 repo_name, search_q=search_q, source=source, opened_by=opened_by,
83 statuses=statuses, offset=start, length=limit,
83 statuses=statuses, offset=start, length=limit,
84 order_by=order_by, order_dir=order_dir)
84 order_by=order_by, order_dir=order_dir)
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
85 pull_requests_total_count = PullRequestModel().count_awaiting_review(
86 repo_name, search_q=search_q, source=source, statuses=statuses,
86 repo_name, search_q=search_q, source=source, statuses=statuses,
87 opened_by=opened_by)
87 opened_by=opened_by)
88 elif filter_type == 'awaiting_my_review':
88 elif filter_type == 'awaiting_my_review':
89 pull_requests = PullRequestModel().get_awaiting_my_review(
89 pull_requests = PullRequestModel().get_awaiting_my_review(
90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
90 repo_name, search_q=search_q, source=source, opened_by=opened_by,
91 user_id=self._rhodecode_user.user_id, statuses=statuses,
91 user_id=self._rhodecode_user.user_id, statuses=statuses,
92 offset=start, length=limit, order_by=order_by,
92 offset=start, length=limit, order_by=order_by,
93 order_dir=order_dir)
93 order_dir=order_dir)
94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
94 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
95 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
96 statuses=statuses, opened_by=opened_by)
96 statuses=statuses, opened_by=opened_by)
97 else:
97 else:
98 pull_requests = PullRequestModel().get_all(
98 pull_requests = PullRequestModel().get_all(
99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
99 repo_name, search_q=search_q, source=source, opened_by=opened_by,
100 statuses=statuses, offset=start, length=limit,
100 statuses=statuses, offset=start, length=limit,
101 order_by=order_by, order_dir=order_dir)
101 order_by=order_by, order_dir=order_dir)
102 pull_requests_total_count = PullRequestModel().count_all(
102 pull_requests_total_count = PullRequestModel().count_all(
103 repo_name, search_q=search_q, source=source, statuses=statuses,
103 repo_name, search_q=search_q, source=source, statuses=statuses,
104 opened_by=opened_by)
104 opened_by=opened_by)
105
105
106 data = []
106 data = []
107 comments_model = CommentsModel()
107 comments_model = CommentsModel()
108 for pr in pull_requests:
108 for pr in pull_requests:
109 comments_count = comments_model.get_all_comments(
109 comments_count = comments_model.get_all_comments(
110 self.db_repo.repo_id, pull_request=pr,
110 self.db_repo.repo_id, pull_request=pr,
111 include_drafts=False, count_only=True)
111 include_drafts=False, count_only=True)
112
112
113 data.append({
113 data.append({
114 'name': _render('pullrequest_name',
114 'name': _render('pullrequest_name',
115 pr.pull_request_id, pr.pull_request_state,
115 pr.pull_request_id, pr.pull_request_state,
116 pr.work_in_progress, pr.target_repo.repo_name,
116 pr.work_in_progress, pr.target_repo.repo_name,
117 short=True),
117 short=True),
118 'name_raw': pr.pull_request_id,
118 'name_raw': pr.pull_request_id,
119 'status': _render('pullrequest_status',
119 'status': _render('pullrequest_status',
120 pr.calculated_review_status()),
120 pr.calculated_review_status()),
121 'title': _render('pullrequest_title', pr.title, pr.description),
121 'title': _render('pullrequest_title', pr.title, pr.description),
122 'description': h.escape(pr.description),
122 'description': h.escape(pr.description),
123 'updated_on': _render('pullrequest_updated_on',
123 'updated_on': _render('pullrequest_updated_on',
124 h.datetime_to_time(pr.updated_on),
124 h.datetime_to_time(pr.updated_on),
125 pr.versions_count),
125 pr.versions_count),
126 'updated_on_raw': h.datetime_to_time(pr.updated_on),
126 'updated_on_raw': h.datetime_to_time(pr.updated_on),
127 'created_on': _render('pullrequest_updated_on',
127 'created_on': _render('pullrequest_updated_on',
128 h.datetime_to_time(pr.created_on)),
128 h.datetime_to_time(pr.created_on)),
129 'created_on_raw': h.datetime_to_time(pr.created_on),
129 'created_on_raw': h.datetime_to_time(pr.created_on),
130 'state': pr.pull_request_state,
130 'state': pr.pull_request_state,
131 'author': _render('pullrequest_author',
131 'author': _render('pullrequest_author',
132 pr.author.full_contact, ),
132 pr.author.full_contact, ),
133 'author_raw': pr.author.full_name,
133 'author_raw': pr.author.full_name,
134 'comments': _render('pullrequest_comments', comments_count),
134 'comments': _render('pullrequest_comments', comments_count),
135 'comments_raw': comments_count,
135 'comments_raw': comments_count,
136 'closed': pr.is_closed(),
136 'closed': pr.is_closed(),
137 })
137 })
138
138
139 data = ({
139 data = ({
140 'draw': draw,
140 'draw': draw,
141 'data': data,
141 'data': data,
142 'recordsTotal': pull_requests_total_count,
142 'recordsTotal': pull_requests_total_count,
143 'recordsFiltered': pull_requests_total_count,
143 'recordsFiltered': pull_requests_total_count,
144 })
144 })
145 return data
145 return data
146
146
147 @LoginRequired()
147 @LoginRequired()
148 @HasRepoPermissionAnyDecorator(
148 @HasRepoPermissionAnyDecorator(
149 'repository.read', 'repository.write', 'repository.admin')
149 'repository.read', 'repository.write', 'repository.admin')
150 @view_config(
150 @view_config(
151 route_name='pullrequest_show_all', request_method='GET',
151 route_name='pullrequest_show_all', request_method='GET',
152 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
152 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
153 def pull_request_list(self):
153 def pull_request_list(self):
154 c = self.load_default_context()
154 c = self.load_default_context()
155
155
156 req_get = self.request.GET
156 req_get = self.request.GET
157 c.source = str2bool(req_get.get('source'))
157 c.source = str2bool(req_get.get('source'))
158 c.closed = str2bool(req_get.get('closed'))
158 c.closed = str2bool(req_get.get('closed'))
159 c.my = str2bool(req_get.get('my'))
159 c.my = str2bool(req_get.get('my'))
160 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
160 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
161 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
161 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
162
162
163 c.active = 'open'
163 c.active = 'open'
164 if c.my:
164 if c.my:
165 c.active = 'my'
165 c.active = 'my'
166 if c.closed:
166 if c.closed:
167 c.active = 'closed'
167 c.active = 'closed'
168 if c.awaiting_review and not c.source:
168 if c.awaiting_review and not c.source:
169 c.active = 'awaiting'
169 c.active = 'awaiting'
170 if c.source and not c.awaiting_review:
170 if c.source and not c.awaiting_review:
171 c.active = 'source'
171 c.active = 'source'
172 if c.awaiting_my_review:
172 if c.awaiting_my_review:
173 c.active = 'awaiting_my'
173 c.active = 'awaiting_my'
174
174
175 return self._get_template_context(c)
175 return self._get_template_context(c)
176
176
177 @LoginRequired()
177 @LoginRequired()
178 @HasRepoPermissionAnyDecorator(
178 @HasRepoPermissionAnyDecorator(
179 'repository.read', 'repository.write', 'repository.admin')
179 'repository.read', 'repository.write', 'repository.admin')
180 @view_config(
180 @view_config(
181 route_name='pullrequest_show_all_data', request_method='GET',
181 route_name='pullrequest_show_all_data', request_method='GET',
182 renderer='json_ext', xhr=True)
182 renderer='json_ext', xhr=True)
183 def pull_request_list_data(self):
183 def pull_request_list_data(self):
184 self.load_default_context()
184 self.load_default_context()
185
185
186 # additional filters
186 # additional filters
187 req_get = self.request.GET
187 req_get = self.request.GET
188 source = str2bool(req_get.get('source'))
188 source = str2bool(req_get.get('source'))
189 closed = str2bool(req_get.get('closed'))
189 closed = str2bool(req_get.get('closed'))
190 my = str2bool(req_get.get('my'))
190 my = str2bool(req_get.get('my'))
191 awaiting_review = str2bool(req_get.get('awaiting_review'))
191 awaiting_review = str2bool(req_get.get('awaiting_review'))
192 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
192 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
193
193
194 filter_type = 'awaiting_review' if awaiting_review \
194 filter_type = 'awaiting_review' if awaiting_review \
195 else 'awaiting_my_review' if awaiting_my_review \
195 else 'awaiting_my_review' if awaiting_my_review \
196 else None
196 else None
197
197
198 opened_by = None
198 opened_by = None
199 if my:
199 if my:
200 opened_by = [self._rhodecode_user.user_id]
200 opened_by = [self._rhodecode_user.user_id]
201
201
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
202 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
203 if closed:
203 if closed:
204 statuses = [PullRequest.STATUS_CLOSED]
204 statuses = [PullRequest.STATUS_CLOSED]
205
205
206 data = self._get_pull_requests_list(
206 data = self._get_pull_requests_list(
207 repo_name=self.db_repo_name, source=source,
207 repo_name=self.db_repo_name, source=source,
208 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
208 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
209
209
210 return data
210 return data
211
211
212 def _is_diff_cache_enabled(self, target_repo):
212 def _is_diff_cache_enabled(self, target_repo):
213 caching_enabled = self._get_general_setting(
213 caching_enabled = self._get_general_setting(
214 target_repo, 'rhodecode_diff_cache')
214 target_repo, 'rhodecode_diff_cache')
215 log.debug('Diff caching enabled: %s', caching_enabled)
215 log.debug('Diff caching enabled: %s', caching_enabled)
216 return caching_enabled
216 return caching_enabled
217
217
218 def _get_diffset(self, source_repo_name, source_repo,
218 def _get_diffset(self, source_repo_name, source_repo,
219 ancestor_commit,
219 ancestor_commit,
220 source_ref_id, target_ref_id,
220 source_ref_id, target_ref_id,
221 target_commit, source_commit, diff_limit, file_limit,
221 target_commit, source_commit, diff_limit, file_limit,
222 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
222 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
223
223
224 if use_ancestor:
224 if use_ancestor:
225 # we might want to not use it for versions
225 # we might want to not use it for versions
226 target_ref_id = ancestor_commit.raw_id
226 target_ref_id = ancestor_commit.raw_id
227
227
228 vcs_diff = PullRequestModel().get_diff(
228 vcs_diff = PullRequestModel().get_diff(
229 source_repo, source_ref_id, target_ref_id,
229 source_repo, source_ref_id, target_ref_id,
230 hide_whitespace_changes, diff_context)
230 hide_whitespace_changes, diff_context)
231
231
232 diff_processor = diffs.DiffProcessor(
232 diff_processor = diffs.DiffProcessor(
233 vcs_diff, format='newdiff', diff_limit=diff_limit,
233 vcs_diff, format='newdiff', diff_limit=diff_limit,
234 file_limit=file_limit, show_full_diff=fulldiff)
234 file_limit=file_limit, show_full_diff=fulldiff)
235
235
236 _parsed = diff_processor.prepare()
236 _parsed = diff_processor.prepare()
237
237
238 diffset = codeblocks.DiffSet(
238 diffset = codeblocks.DiffSet(
239 repo_name=self.db_repo_name,
239 repo_name=self.db_repo_name,
240 source_repo_name=source_repo_name,
240 source_repo_name=source_repo_name,
241 source_node_getter=codeblocks.diffset_node_getter(target_commit),
241 source_node_getter=codeblocks.diffset_node_getter(target_commit),
242 target_node_getter=codeblocks.diffset_node_getter(source_commit),
242 target_node_getter=codeblocks.diffset_node_getter(source_commit),
243 )
243 )
244 diffset = self.path_filter.render_patchset_filtered(
244 diffset = self.path_filter.render_patchset_filtered(
245 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
245 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
246
246
247 return diffset
247 return diffset
248
248
249 def _get_range_diffset(self, source_scm, source_repo,
249 def _get_range_diffset(self, source_scm, source_repo,
250 commit1, commit2, diff_limit, file_limit,
250 commit1, commit2, diff_limit, file_limit,
251 fulldiff, hide_whitespace_changes, diff_context):
251 fulldiff, hide_whitespace_changes, diff_context):
252 vcs_diff = source_scm.get_diff(
252 vcs_diff = source_scm.get_diff(
253 commit1, commit2,
253 commit1, commit2,
254 ignore_whitespace=hide_whitespace_changes,
254 ignore_whitespace=hide_whitespace_changes,
255 context=diff_context)
255 context=diff_context)
256
256
257 diff_processor = diffs.DiffProcessor(
257 diff_processor = diffs.DiffProcessor(
258 vcs_diff, format='newdiff', diff_limit=diff_limit,
258 vcs_diff, format='newdiff', diff_limit=diff_limit,
259 file_limit=file_limit, show_full_diff=fulldiff)
259 file_limit=file_limit, show_full_diff=fulldiff)
260
260
261 _parsed = diff_processor.prepare()
261 _parsed = diff_processor.prepare()
262
262
263 diffset = codeblocks.DiffSet(
263 diffset = codeblocks.DiffSet(
264 repo_name=source_repo.repo_name,
264 repo_name=source_repo.repo_name,
265 source_node_getter=codeblocks.diffset_node_getter(commit1),
265 source_node_getter=codeblocks.diffset_node_getter(commit1),
266 target_node_getter=codeblocks.diffset_node_getter(commit2))
266 target_node_getter=codeblocks.diffset_node_getter(commit2))
267
267
268 diffset = self.path_filter.render_patchset_filtered(
268 diffset = self.path_filter.render_patchset_filtered(
269 diffset, _parsed, commit1.raw_id, commit2.raw_id)
269 diffset, _parsed, commit1.raw_id, commit2.raw_id)
270
270
271 return diffset
271 return diffset
272
272
273 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
273 def register_comments_vars(self, c, pull_request, versions, include_drafts=True):
274 comments_model = CommentsModel()
274 comments_model = CommentsModel()
275
275
276 # GENERAL COMMENTS with versions #
276 # GENERAL COMMENTS with versions #
277 q = comments_model._all_general_comments_of_pull_request(pull_request)
277 q = comments_model._all_general_comments_of_pull_request(pull_request)
278 q = q.order_by(ChangesetComment.comment_id.asc())
278 q = q.order_by(ChangesetComment.comment_id.asc())
279 if not include_drafts:
279 if not include_drafts:
280 q = q.filter(ChangesetComment.draft == false())
280 q = q.filter(ChangesetComment.draft == false())
281 general_comments = q
281 general_comments = q
282
282
283 # pick comments we want to render at current version
283 # pick comments we want to render at current version
284 c.comment_versions = comments_model.aggregate_comments(
284 c.comment_versions = comments_model.aggregate_comments(
285 general_comments, versions, c.at_version_num)
285 general_comments, versions, c.at_version_num)
286
286
287 # INLINE COMMENTS with versions #
287 # INLINE COMMENTS with versions #
288 q = comments_model._all_inline_comments_of_pull_request(pull_request)
288 q = comments_model._all_inline_comments_of_pull_request(pull_request)
289 q = q.order_by(ChangesetComment.comment_id.asc())
289 q = q.order_by(ChangesetComment.comment_id.asc())
290 if not include_drafts:
290 if not include_drafts:
291 q = q.filter(ChangesetComment.draft == false())
291 q = q.filter(ChangesetComment.draft == false())
292 inline_comments = q
292 inline_comments = q
293
293
294 c.inline_versions = comments_model.aggregate_comments(
294 c.inline_versions = comments_model.aggregate_comments(
295 inline_comments, versions, c.at_version_num, inline=True)
295 inline_comments, versions, c.at_version_num, inline=True)
296
296
297 # Comments inline+general
297 # Comments inline+general
298 if c.at_version:
298 if c.at_version:
299 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
299 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
300 c.comments = c.comment_versions[c.at_version_num]['display']
300 c.comments = c.comment_versions[c.at_version_num]['display']
301 else:
301 else:
302 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
302 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
303 c.comments = c.comment_versions[c.at_version_num]['until']
303 c.comments = c.comment_versions[c.at_version_num]['until']
304
304
305 return general_comments, inline_comments
305 return general_comments, inline_comments
306
306
307 @LoginRequired()
307 @LoginRequired()
308 @HasRepoPermissionAnyDecorator(
308 @HasRepoPermissionAnyDecorator(
309 'repository.read', 'repository.write', 'repository.admin')
309 'repository.read', 'repository.write', 'repository.admin')
310 @view_config(
310 @view_config(
311 route_name='pullrequest_show', request_method='GET',
311 route_name='pullrequest_show', request_method='GET',
312 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
312 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
313 def pull_request_show(self):
313 def pull_request_show(self):
314 _ = self.request.translate
314 _ = self.request.translate
315 c = self.load_default_context()
315 c = self.load_default_context()
316
316
317 pull_request = PullRequest.get_or_404(
317 pull_request = PullRequest.get_or_404(
318 self.request.matchdict['pull_request_id'])
318 self.request.matchdict['pull_request_id'])
319 pull_request_id = pull_request.pull_request_id
319 pull_request_id = pull_request.pull_request_id
320
320
321 c.state_progressing = pull_request.is_state_changing()
321 c.state_progressing = pull_request.is_state_changing()
322 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
322 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
323
323
324 _new_state = {
324 _new_state = {
325 'created': PullRequest.STATE_CREATED,
325 'created': PullRequest.STATE_CREATED,
326 }.get(self.request.GET.get('force_state'))
326 }.get(self.request.GET.get('force_state'))
327
327
328 if c.is_super_admin and _new_state:
328 if c.is_super_admin and _new_state:
329 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
329 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
330 h.flash(
330 h.flash(
331 _('Pull Request state was force changed to `{}`').format(_new_state),
331 _('Pull Request state was force changed to `{}`').format(_new_state),
332 category='success')
332 category='success')
333 Session().commit()
333 Session().commit()
334
334
335 raise HTTPFound(h.route_path(
335 raise HTTPFound(h.route_path(
336 'pullrequest_show', repo_name=self.db_repo_name,
336 'pullrequest_show', repo_name=self.db_repo_name,
337 pull_request_id=pull_request_id))
337 pull_request_id=pull_request_id))
338
338
339 version = self.request.GET.get('version')
339 version = self.request.GET.get('version')
340 from_version = self.request.GET.get('from_version') or version
340 from_version = self.request.GET.get('from_version') or version
341 merge_checks = self.request.GET.get('merge_checks')
341 merge_checks = self.request.GET.get('merge_checks')
342 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
342 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
343 force_refresh = str2bool(self.request.GET.get('force_refresh'))
343 force_refresh = str2bool(self.request.GET.get('force_refresh'))
344 c.range_diff_on = self.request.GET.get('range-diff') == "1"
344 c.range_diff_on = self.request.GET.get('range-diff') == "1"
345
345
346 # fetch global flags of ignore ws or context lines
346 # fetch global flags of ignore ws or context lines
347 diff_context = diffs.get_diff_context(self.request)
347 diff_context = diffs.get_diff_context(self.request)
348 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
348 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
349
349
350 (pull_request_latest,
350 (pull_request_latest,
351 pull_request_at_ver,
351 pull_request_at_ver,
352 pull_request_display_obj,
352 pull_request_display_obj,
353 at_version) = PullRequestModel().get_pr_version(
353 at_version) = PullRequestModel().get_pr_version(
354 pull_request_id, version=version)
354 pull_request_id, version=version)
355
355
356 pr_closed = pull_request_latest.is_closed()
356 pr_closed = pull_request_latest.is_closed()
357
357
358 if pr_closed and (version or from_version):
358 if pr_closed and (version or from_version):
359 # not allow to browse versions for closed PR
359 # not allow to browse versions for closed PR
360 raise HTTPFound(h.route_path(
360 raise HTTPFound(h.route_path(
361 'pullrequest_show', repo_name=self.db_repo_name,
361 'pullrequest_show', repo_name=self.db_repo_name,
362 pull_request_id=pull_request_id))
362 pull_request_id=pull_request_id))
363
363
364 versions = pull_request_display_obj.versions()
364 versions = pull_request_display_obj.versions()
365 # used to store per-commit range diffs
365 # used to store per-commit range diffs
366 c.changes = collections.OrderedDict()
366 c.changes = collections.OrderedDict()
367
367
368 c.at_version = at_version
368 c.at_version = at_version
369 c.at_version_num = (at_version
369 c.at_version_num = (at_version
370 if at_version and at_version != PullRequest.LATEST_VER
370 if at_version and at_version != PullRequest.LATEST_VER
371 else None)
371 else None)
372
372
373 c.at_version_index = ChangesetComment.get_index_from_version(
373 c.at_version_index = ChangesetComment.get_index_from_version(
374 c.at_version_num, versions)
374 c.at_version_num, versions)
375
375
376 (prev_pull_request_latest,
376 (prev_pull_request_latest,
377 prev_pull_request_at_ver,
377 prev_pull_request_at_ver,
378 prev_pull_request_display_obj,
378 prev_pull_request_display_obj,
379 prev_at_version) = PullRequestModel().get_pr_version(
379 prev_at_version) = PullRequestModel().get_pr_version(
380 pull_request_id, version=from_version)
380 pull_request_id, version=from_version)
381
381
382 c.from_version = prev_at_version
382 c.from_version = prev_at_version
383 c.from_version_num = (prev_at_version
383 c.from_version_num = (prev_at_version
384 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
384 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
385 else None)
385 else None)
386 c.from_version_index = ChangesetComment.get_index_from_version(
386 c.from_version_index = ChangesetComment.get_index_from_version(
387 c.from_version_num, versions)
387 c.from_version_num, versions)
388
388
389 # define if we're in COMPARE mode or VIEW at version mode
389 # define if we're in COMPARE mode or VIEW at version mode
390 compare = at_version != prev_at_version
390 compare = at_version != prev_at_version
391
391
392 # pull_requests repo_name we opened it against
392 # pull_requests repo_name we opened it against
393 # ie. target_repo must match
393 # ie. target_repo must match
394 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
394 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
395 log.warning('Mismatch between the current repo: %s, and target %s',
395 log.warning('Mismatch between the current repo: %s, and target %s',
396 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
396 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
397 raise HTTPNotFound()
397 raise HTTPNotFound()
398
398
399 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
399 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
400
400
401 c.pull_request = pull_request_display_obj
401 c.pull_request = pull_request_display_obj
402 c.renderer = pull_request_at_ver.description_renderer or c.renderer
402 c.renderer = pull_request_at_ver.description_renderer or c.renderer
403 c.pull_request_latest = pull_request_latest
403 c.pull_request_latest = pull_request_latest
404
404
405 # inject latest version
405 # inject latest version
406 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
406 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
407 c.versions = versions + [latest_ver]
407 c.versions = versions + [latest_ver]
408
408
409 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
409 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
410 c.allowed_to_change_status = False
410 c.allowed_to_change_status = False
411 c.allowed_to_update = False
411 c.allowed_to_update = False
412 c.allowed_to_merge = False
412 c.allowed_to_merge = False
413 c.allowed_to_delete = False
413 c.allowed_to_delete = False
414 c.allowed_to_comment = False
414 c.allowed_to_comment = False
415 c.allowed_to_close = False
415 c.allowed_to_close = False
416 else:
416 else:
417 can_change_status = PullRequestModel().check_user_change_status(
417 can_change_status = PullRequestModel().check_user_change_status(
418 pull_request_at_ver, self._rhodecode_user)
418 pull_request_at_ver, self._rhodecode_user)
419 c.allowed_to_change_status = can_change_status and not pr_closed
419 c.allowed_to_change_status = can_change_status and not pr_closed
420
420
421 c.allowed_to_update = PullRequestModel().check_user_update(
421 c.allowed_to_update = PullRequestModel().check_user_update(
422 pull_request_latest, self._rhodecode_user) and not pr_closed
422 pull_request_latest, self._rhodecode_user) and not pr_closed
423 c.allowed_to_merge = PullRequestModel().check_user_merge(
423 c.allowed_to_merge = PullRequestModel().check_user_merge(
424 pull_request_latest, self._rhodecode_user) and not pr_closed
424 pull_request_latest, self._rhodecode_user) and not pr_closed
425 c.allowed_to_delete = PullRequestModel().check_user_delete(
425 c.allowed_to_delete = PullRequestModel().check_user_delete(
426 pull_request_latest, self._rhodecode_user) and not pr_closed
426 pull_request_latest, self._rhodecode_user) and not pr_closed
427 c.allowed_to_comment = not pr_closed
427 c.allowed_to_comment = not pr_closed
428 c.allowed_to_close = c.allowed_to_merge and not pr_closed
428 c.allowed_to_close = c.allowed_to_merge and not pr_closed
429
429
430 c.forbid_adding_reviewers = False
430 c.forbid_adding_reviewers = False
431
431
432 if pull_request_latest.reviewer_data and \
432 if pull_request_latest.reviewer_data and \
433 'rules' in pull_request_latest.reviewer_data:
433 'rules' in pull_request_latest.reviewer_data:
434 rules = pull_request_latest.reviewer_data['rules'] or {}
434 rules = pull_request_latest.reviewer_data['rules'] or {}
435 try:
435 try:
436 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
436 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
437 except Exception:
437 except Exception:
438 pass
438 pass
439
439
440 # check merge capabilities
440 # check merge capabilities
441 _merge_check = MergeCheck.validate(
441 _merge_check = MergeCheck.validate(
442 pull_request_latest, auth_user=self._rhodecode_user,
442 pull_request_latest, auth_user=self._rhodecode_user,
443 translator=self.request.translate,
443 translator=self.request.translate,
444 force_shadow_repo_refresh=force_refresh)
444 force_shadow_repo_refresh=force_refresh)
445
445
446 c.pr_merge_errors = _merge_check.error_details
446 c.pr_merge_errors = _merge_check.error_details
447 c.pr_merge_possible = not _merge_check.failed
447 c.pr_merge_possible = not _merge_check.failed
448 c.pr_merge_message = _merge_check.merge_msg
448 c.pr_merge_message = _merge_check.merge_msg
449 c.pr_merge_source_commit = _merge_check.source_commit
449 c.pr_merge_source_commit = _merge_check.source_commit
450 c.pr_merge_target_commit = _merge_check.target_commit
450 c.pr_merge_target_commit = _merge_check.target_commit
451
451
452 c.pr_merge_info = MergeCheck.get_merge_conditions(
452 c.pr_merge_info = MergeCheck.get_merge_conditions(
453 pull_request_latest, translator=self.request.translate)
453 pull_request_latest, translator=self.request.translate)
454
454
455 c.pull_request_review_status = _merge_check.review_status
455 c.pull_request_review_status = _merge_check.review_status
456 if merge_checks:
456 if merge_checks:
457 self.request.override_renderer = \
457 self.request.override_renderer = \
458 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
458 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
459 return self._get_template_context(c)
459 return self._get_template_context(c)
460
460
461 c.reviewers_count = pull_request.reviewers_count
461 c.reviewers_count = pull_request.reviewers_count
462 c.observers_count = pull_request.observers_count
462 c.observers_count = pull_request.observers_count
463
463
464 # reviewers and statuses
464 # reviewers and statuses
465 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
465 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
466 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
466 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
467 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
467 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
468
468
469 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
469 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
470 member_reviewer = h.reviewer_as_json(
470 member_reviewer = h.reviewer_as_json(
471 member, reasons=reasons, mandatory=mandatory,
471 member, reasons=reasons, mandatory=mandatory,
472 role=review_obj.role,
472 role=review_obj.role,
473 user_group=review_obj.rule_user_group_data()
473 user_group=review_obj.rule_user_group_data()
474 )
474 )
475
475
476 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
476 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
477 member_reviewer['review_status'] = current_review_status
477 member_reviewer['review_status'] = current_review_status
478 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
478 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
479 member_reviewer['allowed_to_update'] = c.allowed_to_update
479 member_reviewer['allowed_to_update'] = c.allowed_to_update
480 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
480 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
481
481
482 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
482 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
483
483
484 for observer_obj, member in pull_request_at_ver.observers():
484 for observer_obj, member in pull_request_at_ver.observers():
485 member_observer = h.reviewer_as_json(
485 member_observer = h.reviewer_as_json(
486 member, reasons=[], mandatory=False,
486 member, reasons=[], mandatory=False,
487 role=observer_obj.role,
487 role=observer_obj.role,
488 user_group=observer_obj.rule_user_group_data()
488 user_group=observer_obj.rule_user_group_data()
489 )
489 )
490 member_observer['allowed_to_update'] = c.allowed_to_update
490 member_observer['allowed_to_update'] = c.allowed_to_update
491 c.pull_request_set_observers_data_json['observers'].append(member_observer)
491 c.pull_request_set_observers_data_json['observers'].append(member_observer)
492
492
493 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
493 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
494
494
495 general_comments, inline_comments = \
495 general_comments, inline_comments = \
496 self.register_comments_vars(c, pull_request_latest, versions)
496 self.register_comments_vars(c, pull_request_latest, versions)
497
497
498 # TODOs
498 # TODOs
499 c.unresolved_comments = CommentsModel() \
499 c.unresolved_comments = CommentsModel() \
500 .get_pull_request_unresolved_todos(pull_request_latest)
500 .get_pull_request_unresolved_todos(pull_request_latest)
501 c.resolved_comments = CommentsModel() \
501 c.resolved_comments = CommentsModel() \
502 .get_pull_request_resolved_todos(pull_request_latest)
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 # if we use version, then do not show later comments
509 # if we use version, then do not show later comments
505 # than current version
510 # than current version
506 display_inline_comments = collections.defaultdict(
511 display_inline_comments = collections.defaultdict(
507 lambda: collections.defaultdict(list))
512 lambda: collections.defaultdict(list))
508 for co in inline_comments:
513 for co in inline_comments:
509 if c.at_version_num:
514 if c.at_version_num:
510 # pick comments that are at least UPTO given version, so we
515 # pick comments that are at least UPTO given version, so we
511 # don't render comments for higher version
516 # don't render comments for higher version
512 should_render = co.pull_request_version_id and \
517 should_render = co.pull_request_version_id and \
513 co.pull_request_version_id <= c.at_version_num
518 co.pull_request_version_id <= c.at_version_num
514 else:
519 else:
515 # showing all, for 'latest'
520 # showing all, for 'latest'
516 should_render = True
521 should_render = True
517
522
518 if should_render:
523 if should_render:
519 display_inline_comments[co.f_path][co.line_no].append(co)
524 display_inline_comments[co.f_path][co.line_no].append(co)
520
525
521 # load diff data into template context, if we use compare mode then
526 # load diff data into template context, if we use compare mode then
522 # diff is calculated based on changes between versions of PR
527 # diff is calculated based on changes between versions of PR
523
528
524 source_repo = pull_request_at_ver.source_repo
529 source_repo = pull_request_at_ver.source_repo
525 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
530 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
526
531
527 target_repo = pull_request_at_ver.target_repo
532 target_repo = pull_request_at_ver.target_repo
528 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
533 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
529
534
530 if compare:
535 if compare:
531 # in compare switch the diff base to latest commit from prev version
536 # in compare switch the diff base to latest commit from prev version
532 target_ref_id = prev_pull_request_display_obj.revisions[0]
537 target_ref_id = prev_pull_request_display_obj.revisions[0]
533
538
534 # despite opening commits for bookmarks/branches/tags, we always
539 # despite opening commits for bookmarks/branches/tags, we always
535 # convert this to rev to prevent changes after bookmark or branch change
540 # convert this to rev to prevent changes after bookmark or branch change
536 c.source_ref_type = 'rev'
541 c.source_ref_type = 'rev'
537 c.source_ref = source_ref_id
542 c.source_ref = source_ref_id
538
543
539 c.target_ref_type = 'rev'
544 c.target_ref_type = 'rev'
540 c.target_ref = target_ref_id
545 c.target_ref = target_ref_id
541
546
542 c.source_repo = source_repo
547 c.source_repo = source_repo
543 c.target_repo = target_repo
548 c.target_repo = target_repo
544
549
545 c.commit_ranges = []
550 c.commit_ranges = []
546 source_commit = EmptyCommit()
551 source_commit = EmptyCommit()
547 target_commit = EmptyCommit()
552 target_commit = EmptyCommit()
548 c.missing_requirements = False
553 c.missing_requirements = False
549
554
550 source_scm = source_repo.scm_instance()
555 source_scm = source_repo.scm_instance()
551 target_scm = target_repo.scm_instance()
556 target_scm = target_repo.scm_instance()
552
557
553 shadow_scm = None
558 shadow_scm = None
554 try:
559 try:
555 shadow_scm = pull_request_latest.get_shadow_repo()
560 shadow_scm = pull_request_latest.get_shadow_repo()
556 except Exception:
561 except Exception:
557 log.debug('Failed to get shadow repo', exc_info=True)
562 log.debug('Failed to get shadow repo', exc_info=True)
558 # try first the existing source_repo, and then shadow
563 # try first the existing source_repo, and then shadow
559 # repo if we can obtain one
564 # repo if we can obtain one
560 commits_source_repo = source_scm
565 commits_source_repo = source_scm
561 if shadow_scm:
566 if shadow_scm:
562 commits_source_repo = shadow_scm
567 commits_source_repo = shadow_scm
563
568
564 c.commits_source_repo = commits_source_repo
569 c.commits_source_repo = commits_source_repo
565 c.ancestor = None # set it to None, to hide it from PR view
570 c.ancestor = None # set it to None, to hide it from PR view
566
571
567 # empty version means latest, so we keep this to prevent
572 # empty version means latest, so we keep this to prevent
568 # double caching
573 # double caching
569 version_normalized = version or PullRequest.LATEST_VER
574 version_normalized = version or PullRequest.LATEST_VER
570 from_version_normalized = from_version or PullRequest.LATEST_VER
575 from_version_normalized = from_version or PullRequest.LATEST_VER
571
576
572 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
577 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
573 cache_file_path = diff_cache_exist(
578 cache_file_path = diff_cache_exist(
574 cache_path, 'pull_request', pull_request_id, version_normalized,
579 cache_path, 'pull_request', pull_request_id, version_normalized,
575 from_version_normalized, source_ref_id, target_ref_id,
580 from_version_normalized, source_ref_id, target_ref_id,
576 hide_whitespace_changes, diff_context, c.fulldiff)
581 hide_whitespace_changes, diff_context, c.fulldiff)
577
582
578 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
583 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
579 force_recache = self.get_recache_flag()
584 force_recache = self.get_recache_flag()
580
585
581 cached_diff = None
586 cached_diff = None
582 if caching_enabled:
587 if caching_enabled:
583 cached_diff = load_cached_diff(cache_file_path)
588 cached_diff = load_cached_diff(cache_file_path)
584
589
585 has_proper_commit_cache = (
590 has_proper_commit_cache = (
586 cached_diff and cached_diff.get('commits')
591 cached_diff and cached_diff.get('commits')
587 and len(cached_diff.get('commits', [])) == 5
592 and len(cached_diff.get('commits', [])) == 5
588 and cached_diff.get('commits')[0]
593 and cached_diff.get('commits')[0]
589 and cached_diff.get('commits')[3])
594 and cached_diff.get('commits')[3])
590
595
591 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
596 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
592 diff_commit_cache = \
597 diff_commit_cache = \
593 (ancestor_commit, commit_cache, missing_requirements,
598 (ancestor_commit, commit_cache, missing_requirements,
594 source_commit, target_commit) = cached_diff['commits']
599 source_commit, target_commit) = cached_diff['commits']
595 else:
600 else:
596 # NOTE(marcink): we reach potentially unreachable errors when a PR has
601 # NOTE(marcink): we reach potentially unreachable errors when a PR has
597 # merge errors resulting in potentially hidden commits in the shadow repo.
602 # merge errors resulting in potentially hidden commits in the shadow repo.
598 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
603 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
599 and _merge_check.merge_response
604 and _merge_check.merge_response
600 maybe_unreachable = maybe_unreachable \
605 maybe_unreachable = maybe_unreachable \
601 and _merge_check.merge_response.metadata.get('unresolved_files')
606 and _merge_check.merge_response.metadata.get('unresolved_files')
602 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
607 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
603 diff_commit_cache = \
608 diff_commit_cache = \
604 (ancestor_commit, commit_cache, missing_requirements,
609 (ancestor_commit, commit_cache, missing_requirements,
605 source_commit, target_commit) = self.get_commits(
610 source_commit, target_commit) = self.get_commits(
606 commits_source_repo,
611 commits_source_repo,
607 pull_request_at_ver,
612 pull_request_at_ver,
608 source_commit,
613 source_commit,
609 source_ref_id,
614 source_ref_id,
610 source_scm,
615 source_scm,
611 target_commit,
616 target_commit,
612 target_ref_id,
617 target_ref_id,
613 target_scm,
618 target_scm,
614 maybe_unreachable=maybe_unreachable)
619 maybe_unreachable=maybe_unreachable)
615
620
616 # register our commit range
621 # register our commit range
617 for comm in commit_cache.values():
622 for comm in commit_cache.values():
618 c.commit_ranges.append(comm)
623 c.commit_ranges.append(comm)
619
624
620 c.missing_requirements = missing_requirements
625 c.missing_requirements = missing_requirements
621 c.ancestor_commit = ancestor_commit
626 c.ancestor_commit = ancestor_commit
622 c.statuses = source_repo.statuses(
627 c.statuses = source_repo.statuses(
623 [x.raw_id for x in c.commit_ranges])
628 [x.raw_id for x in c.commit_ranges])
624
629
625 # auto collapse if we have more than limit
630 # auto collapse if we have more than limit
626 collapse_limit = diffs.DiffProcessor._collapse_commits_over
631 collapse_limit = diffs.DiffProcessor._collapse_commits_over
627 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
632 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
628 c.compare_mode = compare
633 c.compare_mode = compare
629
634
630 # diff_limit is the old behavior, will cut off the whole diff
635 # diff_limit is the old behavior, will cut off the whole diff
631 # if the limit is applied otherwise will just hide the
636 # if the limit is applied otherwise will just hide the
632 # big files from the front-end
637 # big files from the front-end
633 diff_limit = c.visual.cut_off_limit_diff
638 diff_limit = c.visual.cut_off_limit_diff
634 file_limit = c.visual.cut_off_limit_file
639 file_limit = c.visual.cut_off_limit_file
635
640
636 c.missing_commits = False
641 c.missing_commits = False
637 if (c.missing_requirements
642 if (c.missing_requirements
638 or isinstance(source_commit, EmptyCommit)
643 or isinstance(source_commit, EmptyCommit)
639 or source_commit == target_commit):
644 or source_commit == target_commit):
640
645
641 c.missing_commits = True
646 c.missing_commits = True
642 else:
647 else:
643 c.inline_comments = display_inline_comments
648 c.inline_comments = display_inline_comments
644
649
645 use_ancestor = True
650 use_ancestor = True
646 if from_version_normalized != version_normalized:
651 if from_version_normalized != version_normalized:
647 use_ancestor = False
652 use_ancestor = False
648
653
649 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
654 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
650 if not force_recache and has_proper_diff_cache:
655 if not force_recache and has_proper_diff_cache:
651 c.diffset = cached_diff['diff']
656 c.diffset = cached_diff['diff']
652 else:
657 else:
653 try:
658 try:
654 c.diffset = self._get_diffset(
659 c.diffset = self._get_diffset(
655 c.source_repo.repo_name, commits_source_repo,
660 c.source_repo.repo_name, commits_source_repo,
656 c.ancestor_commit,
661 c.ancestor_commit,
657 source_ref_id, target_ref_id,
662 source_ref_id, target_ref_id,
658 target_commit, source_commit,
663 target_commit, source_commit,
659 diff_limit, file_limit, c.fulldiff,
664 diff_limit, file_limit, c.fulldiff,
660 hide_whitespace_changes, diff_context,
665 hide_whitespace_changes, diff_context,
661 use_ancestor=use_ancestor
666 use_ancestor=use_ancestor
662 )
667 )
663
668
664 # save cached diff
669 # save cached diff
665 if caching_enabled:
670 if caching_enabled:
666 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
671 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
667 except CommitDoesNotExistError:
672 except CommitDoesNotExistError:
668 log.exception('Failed to generate diffset')
673 log.exception('Failed to generate diffset')
669 c.missing_commits = True
674 c.missing_commits = True
670
675
671 if not c.missing_commits:
676 if not c.missing_commits:
672
677
673 c.limited_diff = c.diffset.limited_diff
678 c.limited_diff = c.diffset.limited_diff
674
679
675 # calculate removed files that are bound to comments
680 # calculate removed files that are bound to comments
676 comment_deleted_files = [
681 comment_deleted_files = [
677 fname for fname in display_inline_comments
682 fname for fname in display_inline_comments
678 if fname not in c.diffset.file_stats]
683 if fname not in c.diffset.file_stats]
679
684
680 c.deleted_files_comments = collections.defaultdict(dict)
685 c.deleted_files_comments = collections.defaultdict(dict)
681 for fname, per_line_comments in display_inline_comments.items():
686 for fname, per_line_comments in display_inline_comments.items():
682 if fname in comment_deleted_files:
687 if fname in comment_deleted_files:
683 c.deleted_files_comments[fname]['stats'] = 0
688 c.deleted_files_comments[fname]['stats'] = 0
684 c.deleted_files_comments[fname]['comments'] = list()
689 c.deleted_files_comments[fname]['comments'] = list()
685 for lno, comments in per_line_comments.items():
690 for lno, comments in per_line_comments.items():
686 c.deleted_files_comments[fname]['comments'].extend(comments)
691 c.deleted_files_comments[fname]['comments'].extend(comments)
687
692
688 # maybe calculate the range diff
693 # maybe calculate the range diff
689 if c.range_diff_on:
694 if c.range_diff_on:
690 # TODO(marcink): set whitespace/context
695 # TODO(marcink): set whitespace/context
691 context_lcl = 3
696 context_lcl = 3
692 ign_whitespace_lcl = False
697 ign_whitespace_lcl = False
693
698
694 for commit in c.commit_ranges:
699 for commit in c.commit_ranges:
695 commit2 = commit
700 commit2 = commit
696 commit1 = commit.first_parent
701 commit1 = commit.first_parent
697
702
698 range_diff_cache_file_path = diff_cache_exist(
703 range_diff_cache_file_path = diff_cache_exist(
699 cache_path, 'diff', commit.raw_id,
704 cache_path, 'diff', commit.raw_id,
700 ign_whitespace_lcl, context_lcl, c.fulldiff)
705 ign_whitespace_lcl, context_lcl, c.fulldiff)
701
706
702 cached_diff = None
707 cached_diff = None
703 if caching_enabled:
708 if caching_enabled:
704 cached_diff = load_cached_diff(range_diff_cache_file_path)
709 cached_diff = load_cached_diff(range_diff_cache_file_path)
705
710
706 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
711 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
707 if not force_recache and has_proper_diff_cache:
712 if not force_recache and has_proper_diff_cache:
708 diffset = cached_diff['diff']
713 diffset = cached_diff['diff']
709 else:
714 else:
710 diffset = self._get_range_diffset(
715 diffset = self._get_range_diffset(
711 commits_source_repo, source_repo,
716 commits_source_repo, source_repo,
712 commit1, commit2, diff_limit, file_limit,
717 commit1, commit2, diff_limit, file_limit,
713 c.fulldiff, ign_whitespace_lcl, context_lcl
718 c.fulldiff, ign_whitespace_lcl, context_lcl
714 )
719 )
715
720
716 # save cached diff
721 # save cached diff
717 if caching_enabled:
722 if caching_enabled:
718 cache_diff(range_diff_cache_file_path, diffset, None)
723 cache_diff(range_diff_cache_file_path, diffset, None)
719
724
720 c.changes[commit.raw_id] = diffset
725 c.changes[commit.raw_id] = diffset
721
726
722 # this is a hack to properly display links, when creating PR, the
727 # this is a hack to properly display links, when creating PR, the
723 # compare view and others uses different notation, and
728 # compare view and others uses different notation, and
724 # compare_commits.mako renders links based on the target_repo.
729 # compare_commits.mako renders links based on the target_repo.
725 # We need to swap that here to generate it properly on the html side
730 # We need to swap that here to generate it properly on the html side
726 c.target_repo = c.source_repo
731 c.target_repo = c.source_repo
727
732
728 c.commit_statuses = ChangesetStatus.STATUSES
733 c.commit_statuses = ChangesetStatus.STATUSES
729
734
730 c.show_version_changes = not pr_closed
735 c.show_version_changes = not pr_closed
731 if c.show_version_changes:
736 if c.show_version_changes:
732 cur_obj = pull_request_at_ver
737 cur_obj = pull_request_at_ver
733 prev_obj = prev_pull_request_at_ver
738 prev_obj = prev_pull_request_at_ver
734
739
735 old_commit_ids = prev_obj.revisions
740 old_commit_ids = prev_obj.revisions
736 new_commit_ids = cur_obj.revisions
741 new_commit_ids = cur_obj.revisions
737 commit_changes = PullRequestModel()._calculate_commit_id_changes(
742 commit_changes = PullRequestModel()._calculate_commit_id_changes(
738 old_commit_ids, new_commit_ids)
743 old_commit_ids, new_commit_ids)
739 c.commit_changes_summary = commit_changes
744 c.commit_changes_summary = commit_changes
740
745
741 # calculate the diff for commits between versions
746 # calculate the diff for commits between versions
742 c.commit_changes = []
747 c.commit_changes = []
743
748
744 def mark(cs, fw):
749 def mark(cs, fw):
745 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
750 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
746
751
747 for c_type, raw_id in mark(commit_changes.added, 'a') \
752 for c_type, raw_id in mark(commit_changes.added, 'a') \
748 + mark(commit_changes.removed, 'r') \
753 + mark(commit_changes.removed, 'r') \
749 + mark(commit_changes.common, 'c'):
754 + mark(commit_changes.common, 'c'):
750
755
751 if raw_id in commit_cache:
756 if raw_id in commit_cache:
752 commit = commit_cache[raw_id]
757 commit = commit_cache[raw_id]
753 else:
758 else:
754 try:
759 try:
755 commit = commits_source_repo.get_commit(raw_id)
760 commit = commits_source_repo.get_commit(raw_id)
756 except CommitDoesNotExistError:
761 except CommitDoesNotExistError:
757 # in case we fail extracting still use "dummy" commit
762 # in case we fail extracting still use "dummy" commit
758 # for display in commit diff
763 # for display in commit diff
759 commit = h.AttributeDict(
764 commit = h.AttributeDict(
760 {'raw_id': raw_id,
765 {'raw_id': raw_id,
761 'message': 'EMPTY or MISSING COMMIT'})
766 'message': 'EMPTY or MISSING COMMIT'})
762 c.commit_changes.append([c_type, commit])
767 c.commit_changes.append([c_type, commit])
763
768
764 # current user review statuses for each version
769 # current user review statuses for each version
765 c.review_versions = {}
770 c.review_versions = {}
766 is_reviewer = PullRequestModel().is_user_reviewer(
771 is_reviewer = PullRequestModel().is_user_reviewer(
767 pull_request, self._rhodecode_user)
772 pull_request, self._rhodecode_user)
768 if is_reviewer:
773 if is_reviewer:
769 for co in general_comments:
774 for co in general_comments:
770 if co.author.user_id == self._rhodecode_user.user_id:
775 if co.author.user_id == self._rhodecode_user.user_id:
771 status = co.status_change
776 status = co.status_change
772 if status:
777 if status:
773 _ver_pr = status[0].comment.pull_request_version_id
778 _ver_pr = status[0].comment.pull_request_version_id
774 c.review_versions[_ver_pr] = status[0]
779 c.review_versions[_ver_pr] = status[0]
775
780
776 return self._get_template_context(c)
781 return self._get_template_context(c)
777
782
778 def get_commits(
783 def get_commits(
779 self, commits_source_repo, pull_request_at_ver, source_commit,
784 self, commits_source_repo, pull_request_at_ver, source_commit,
780 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
785 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
781 maybe_unreachable=False):
786 maybe_unreachable=False):
782
787
783 commit_cache = collections.OrderedDict()
788 commit_cache = collections.OrderedDict()
784 missing_requirements = False
789 missing_requirements = False
785
790
786 try:
791 try:
787 pre_load = ["author", "date", "message", "branch", "parents"]
792 pre_load = ["author", "date", "message", "branch", "parents"]
788
793
789 pull_request_commits = pull_request_at_ver.revisions
794 pull_request_commits = pull_request_at_ver.revisions
790 log.debug('Loading %s commits from %s',
795 log.debug('Loading %s commits from %s',
791 len(pull_request_commits), commits_source_repo)
796 len(pull_request_commits), commits_source_repo)
792
797
793 for rev in pull_request_commits:
798 for rev in pull_request_commits:
794 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
799 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
795 maybe_unreachable=maybe_unreachable)
800 maybe_unreachable=maybe_unreachable)
796 commit_cache[comm.raw_id] = comm
801 commit_cache[comm.raw_id] = comm
797
802
798 # Order here matters, we first need to get target, and then
803 # Order here matters, we first need to get target, and then
799 # the source
804 # the source
800 target_commit = commits_source_repo.get_commit(
805 target_commit = commits_source_repo.get_commit(
801 commit_id=safe_str(target_ref_id))
806 commit_id=safe_str(target_ref_id))
802
807
803 source_commit = commits_source_repo.get_commit(
808 source_commit = commits_source_repo.get_commit(
804 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
809 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
805 except CommitDoesNotExistError:
810 except CommitDoesNotExistError:
806 log.warning('Failed to get commit from `{}` repo'.format(
811 log.warning('Failed to get commit from `{}` repo'.format(
807 commits_source_repo), exc_info=True)
812 commits_source_repo), exc_info=True)
808 except RepositoryRequirementError:
813 except RepositoryRequirementError:
809 log.warning('Failed to get all required data from repo', exc_info=True)
814 log.warning('Failed to get all required data from repo', exc_info=True)
810 missing_requirements = True
815 missing_requirements = True
811
816
812 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
817 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
813
818
814 try:
819 try:
815 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
820 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
816 except Exception:
821 except Exception:
817 ancestor_commit = None
822 ancestor_commit = None
818
823
819 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
824 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
820
825
821 def assure_not_empty_repo(self):
826 def assure_not_empty_repo(self):
822 _ = self.request.translate
827 _ = self.request.translate
823
828
824 try:
829 try:
825 self.db_repo.scm_instance().get_commit()
830 self.db_repo.scm_instance().get_commit()
826 except EmptyRepositoryError:
831 except EmptyRepositoryError:
827 h.flash(h.literal(_('There are no commits yet')),
832 h.flash(h.literal(_('There are no commits yet')),
828 category='warning')
833 category='warning')
829 raise HTTPFound(
834 raise HTTPFound(
830 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
835 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
831
836
832 @LoginRequired()
837 @LoginRequired()
833 @NotAnonymous()
838 @NotAnonymous()
834 @HasRepoPermissionAnyDecorator(
839 @HasRepoPermissionAnyDecorator(
835 'repository.read', 'repository.write', 'repository.admin')
840 'repository.read', 'repository.write', 'repository.admin')
836 @view_config(
841 @view_config(
837 route_name='pullrequest_new', request_method='GET',
842 route_name='pullrequest_new', request_method='GET',
838 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
843 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
839 def pull_request_new(self):
844 def pull_request_new(self):
840 _ = self.request.translate
845 _ = self.request.translate
841 c = self.load_default_context()
846 c = self.load_default_context()
842
847
843 self.assure_not_empty_repo()
848 self.assure_not_empty_repo()
844 source_repo = self.db_repo
849 source_repo = self.db_repo
845
850
846 commit_id = self.request.GET.get('commit')
851 commit_id = self.request.GET.get('commit')
847 branch_ref = self.request.GET.get('branch')
852 branch_ref = self.request.GET.get('branch')
848 bookmark_ref = self.request.GET.get('bookmark')
853 bookmark_ref = self.request.GET.get('bookmark')
849
854
850 try:
855 try:
851 source_repo_data = PullRequestModel().generate_repo_data(
856 source_repo_data = PullRequestModel().generate_repo_data(
852 source_repo, commit_id=commit_id,
857 source_repo, commit_id=commit_id,
853 branch=branch_ref, bookmark=bookmark_ref,
858 branch=branch_ref, bookmark=bookmark_ref,
854 translator=self.request.translate)
859 translator=self.request.translate)
855 except CommitDoesNotExistError as e:
860 except CommitDoesNotExistError as e:
856 log.exception(e)
861 log.exception(e)
857 h.flash(_('Commit does not exist'), 'error')
862 h.flash(_('Commit does not exist'), 'error')
858 raise HTTPFound(
863 raise HTTPFound(
859 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
864 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
860
865
861 default_target_repo = source_repo
866 default_target_repo = source_repo
862
867
863 if source_repo.parent and c.has_origin_repo_read_perm:
868 if source_repo.parent and c.has_origin_repo_read_perm:
864 parent_vcs_obj = source_repo.parent.scm_instance()
869 parent_vcs_obj = source_repo.parent.scm_instance()
865 if parent_vcs_obj and not parent_vcs_obj.is_empty():
870 if parent_vcs_obj and not parent_vcs_obj.is_empty():
866 # change default if we have a parent repo
871 # change default if we have a parent repo
867 default_target_repo = source_repo.parent
872 default_target_repo = source_repo.parent
868
873
869 target_repo_data = PullRequestModel().generate_repo_data(
874 target_repo_data = PullRequestModel().generate_repo_data(
870 default_target_repo, translator=self.request.translate)
875 default_target_repo, translator=self.request.translate)
871
876
872 selected_source_ref = source_repo_data['refs']['selected_ref']
877 selected_source_ref = source_repo_data['refs']['selected_ref']
873 title_source_ref = ''
878 title_source_ref = ''
874 if selected_source_ref:
879 if selected_source_ref:
875 title_source_ref = selected_source_ref.split(':', 2)[1]
880 title_source_ref = selected_source_ref.split(':', 2)[1]
876 c.default_title = PullRequestModel().generate_pullrequest_title(
881 c.default_title = PullRequestModel().generate_pullrequest_title(
877 source=source_repo.repo_name,
882 source=source_repo.repo_name,
878 source_ref=title_source_ref,
883 source_ref=title_source_ref,
879 target=default_target_repo.repo_name
884 target=default_target_repo.repo_name
880 )
885 )
881
886
882 c.default_repo_data = {
887 c.default_repo_data = {
883 'source_repo_name': source_repo.repo_name,
888 'source_repo_name': source_repo.repo_name,
884 'source_refs_json': json.dumps(source_repo_data),
889 'source_refs_json': json.dumps(source_repo_data),
885 'target_repo_name': default_target_repo.repo_name,
890 'target_repo_name': default_target_repo.repo_name,
886 'target_refs_json': json.dumps(target_repo_data),
891 'target_refs_json': json.dumps(target_repo_data),
887 }
892 }
888 c.default_source_ref = selected_source_ref
893 c.default_source_ref = selected_source_ref
889
894
890 return self._get_template_context(c)
895 return self._get_template_context(c)
891
896
892 @LoginRequired()
897 @LoginRequired()
893 @NotAnonymous()
898 @NotAnonymous()
894 @HasRepoPermissionAnyDecorator(
899 @HasRepoPermissionAnyDecorator(
895 'repository.read', 'repository.write', 'repository.admin')
900 'repository.read', 'repository.write', 'repository.admin')
896 @view_config(
901 @view_config(
897 route_name='pullrequest_repo_refs', request_method='GET',
902 route_name='pullrequest_repo_refs', request_method='GET',
898 renderer='json_ext', xhr=True)
903 renderer='json_ext', xhr=True)
899 def pull_request_repo_refs(self):
904 def pull_request_repo_refs(self):
900 self.load_default_context()
905 self.load_default_context()
901 target_repo_name = self.request.matchdict['target_repo_name']
906 target_repo_name = self.request.matchdict['target_repo_name']
902 repo = Repository.get_by_repo_name(target_repo_name)
907 repo = Repository.get_by_repo_name(target_repo_name)
903 if not repo:
908 if not repo:
904 raise HTTPNotFound()
909 raise HTTPNotFound()
905
910
906 target_perm = HasRepoPermissionAny(
911 target_perm = HasRepoPermissionAny(
907 'repository.read', 'repository.write', 'repository.admin')(
912 'repository.read', 'repository.write', 'repository.admin')(
908 target_repo_name)
913 target_repo_name)
909 if not target_perm:
914 if not target_perm:
910 raise HTTPNotFound()
915 raise HTTPNotFound()
911
916
912 return PullRequestModel().generate_repo_data(
917 return PullRequestModel().generate_repo_data(
913 repo, translator=self.request.translate)
918 repo, translator=self.request.translate)
914
919
915 @LoginRequired()
920 @LoginRequired()
916 @NotAnonymous()
921 @NotAnonymous()
917 @HasRepoPermissionAnyDecorator(
922 @HasRepoPermissionAnyDecorator(
918 'repository.read', 'repository.write', 'repository.admin')
923 'repository.read', 'repository.write', 'repository.admin')
919 @view_config(
924 @view_config(
920 route_name='pullrequest_repo_targets', request_method='GET',
925 route_name='pullrequest_repo_targets', request_method='GET',
921 renderer='json_ext', xhr=True)
926 renderer='json_ext', xhr=True)
922 def pullrequest_repo_targets(self):
927 def pullrequest_repo_targets(self):
923 _ = self.request.translate
928 _ = self.request.translate
924 filter_query = self.request.GET.get('query')
929 filter_query = self.request.GET.get('query')
925
930
926 # get the parents
931 # get the parents
927 parent_target_repos = []
932 parent_target_repos = []
928 if self.db_repo.parent:
933 if self.db_repo.parent:
929 parents_query = Repository.query() \
934 parents_query = Repository.query() \
930 .order_by(func.length(Repository.repo_name)) \
935 .order_by(func.length(Repository.repo_name)) \
931 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
936 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
932
937
933 if filter_query:
938 if filter_query:
934 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
939 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
935 parents_query = parents_query.filter(
940 parents_query = parents_query.filter(
936 Repository.repo_name.ilike(ilike_expression))
941 Repository.repo_name.ilike(ilike_expression))
937 parents = parents_query.limit(20).all()
942 parents = parents_query.limit(20).all()
938
943
939 for parent in parents:
944 for parent in parents:
940 parent_vcs_obj = parent.scm_instance()
945 parent_vcs_obj = parent.scm_instance()
941 if parent_vcs_obj and not parent_vcs_obj.is_empty():
946 if parent_vcs_obj and not parent_vcs_obj.is_empty():
942 parent_target_repos.append(parent)
947 parent_target_repos.append(parent)
943
948
944 # get other forks, and repo itself
949 # get other forks, and repo itself
945 query = Repository.query() \
950 query = Repository.query() \
946 .order_by(func.length(Repository.repo_name)) \
951 .order_by(func.length(Repository.repo_name)) \
947 .filter(
952 .filter(
948 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
953 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
949 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
954 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
950 ) \
955 ) \
951 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
956 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
952
957
953 if filter_query:
958 if filter_query:
954 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
959 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
955 query = query.filter(Repository.repo_name.ilike(ilike_expression))
960 query = query.filter(Repository.repo_name.ilike(ilike_expression))
956
961
957 limit = max(20 - len(parent_target_repos), 5) # not less then 5
962 limit = max(20 - len(parent_target_repos), 5) # not less then 5
958 target_repos = query.limit(limit).all()
963 target_repos = query.limit(limit).all()
959
964
960 all_target_repos = target_repos + parent_target_repos
965 all_target_repos = target_repos + parent_target_repos
961
966
962 repos = []
967 repos = []
963 # This checks permissions to the repositories
968 # This checks permissions to the repositories
964 for obj in ScmModel().get_repos(all_target_repos):
969 for obj in ScmModel().get_repos(all_target_repos):
965 repos.append({
970 repos.append({
966 'id': obj['name'],
971 'id': obj['name'],
967 'text': obj['name'],
972 'text': obj['name'],
968 'type': 'repo',
973 'type': 'repo',
969 'repo_id': obj['dbrepo']['repo_id'],
974 'repo_id': obj['dbrepo']['repo_id'],
970 'repo_type': obj['dbrepo']['repo_type'],
975 'repo_type': obj['dbrepo']['repo_type'],
971 'private': obj['dbrepo']['private'],
976 'private': obj['dbrepo']['private'],
972
977
973 })
978 })
974
979
975 data = {
980 data = {
976 'more': False,
981 'more': False,
977 'results': [{
982 'results': [{
978 'text': _('Repositories'),
983 'text': _('Repositories'),
979 'children': repos
984 'children': repos
980 }] if repos else []
985 }] if repos else []
981 }
986 }
982 return data
987 return data
983
988
984 @classmethod
989 @classmethod
985 def get_comment_ids(cls, post_data):
990 def get_comment_ids(cls, post_data):
986 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
991 return filter(lambda e: e > 0, map(safe_int, aslist(post_data.get('comments'), ',')))
987
992
988 @LoginRequired()
993 @LoginRequired()
989 @NotAnonymous()
994 @NotAnonymous()
990 @HasRepoPermissionAnyDecorator(
995 @HasRepoPermissionAnyDecorator(
991 'repository.read', 'repository.write', 'repository.admin')
996 'repository.read', 'repository.write', 'repository.admin')
992 @view_config(
997 @view_config(
993 route_name='pullrequest_comments', request_method='POST',
998 route_name='pullrequest_comments', request_method='POST',
994 renderer='string_html', xhr=True)
999 renderer='string_html', xhr=True)
995 def pullrequest_comments(self):
1000 def pullrequest_comments(self):
996 self.load_default_context()
1001 self.load_default_context()
997
1002
998 pull_request = PullRequest.get_or_404(
1003 pull_request = PullRequest.get_or_404(
999 self.request.matchdict['pull_request_id'])
1004 self.request.matchdict['pull_request_id'])
1000 pull_request_id = pull_request.pull_request_id
1005 pull_request_id = pull_request.pull_request_id
1001 version = self.request.GET.get('version')
1006 version = self.request.GET.get('version')
1002
1007
1003 _render = self.request.get_partial_renderer(
1008 _render = self.request.get_partial_renderer(
1004 'rhodecode:templates/base/sidebar.mako')
1009 'rhodecode:templates/base/sidebar.mako')
1005 c = _render.get_call_context()
1010 c = _render.get_call_context()
1006
1011
1007 (pull_request_latest,
1012 (pull_request_latest,
1008 pull_request_at_ver,
1013 pull_request_at_ver,
1009 pull_request_display_obj,
1014 pull_request_display_obj,
1010 at_version) = PullRequestModel().get_pr_version(
1015 at_version) = PullRequestModel().get_pr_version(
1011 pull_request_id, version=version)
1016 pull_request_id, version=version)
1012 versions = pull_request_display_obj.versions()
1017 versions = pull_request_display_obj.versions()
1013 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1018 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1014 c.versions = versions + [latest_ver]
1019 c.versions = versions + [latest_ver]
1015
1020
1016 c.at_version = at_version
1021 c.at_version = at_version
1017 c.at_version_num = (at_version
1022 c.at_version_num = (at_version
1018 if at_version and at_version != PullRequest.LATEST_VER
1023 if at_version and at_version != PullRequest.LATEST_VER
1019 else None)
1024 else None)
1020
1025
1021 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1026 self.register_comments_vars(c, pull_request_latest, versions, include_drafts=False)
1022 all_comments = c.inline_comments_flat + c.comments
1027 all_comments = c.inline_comments_flat + c.comments
1023
1028
1024 existing_ids = self.get_comment_ids(self.request.POST)
1029 existing_ids = self.get_comment_ids(self.request.POST)
1025 return _render('comments_table', all_comments, len(all_comments),
1030 return _render('comments_table', all_comments, len(all_comments),
1026 existing_ids=existing_ids)
1031 existing_ids=existing_ids)
1027
1032
1028 @LoginRequired()
1033 @LoginRequired()
1029 @NotAnonymous()
1034 @NotAnonymous()
1030 @HasRepoPermissionAnyDecorator(
1035 @HasRepoPermissionAnyDecorator(
1031 'repository.read', 'repository.write', 'repository.admin')
1036 'repository.read', 'repository.write', 'repository.admin')
1032 @view_config(
1037 @view_config(
1033 route_name='pullrequest_todos', request_method='POST',
1038 route_name='pullrequest_todos', request_method='POST',
1034 renderer='string_html', xhr=True)
1039 renderer='string_html', xhr=True)
1035 def pullrequest_todos(self):
1040 def pullrequest_todos(self):
1036 self.load_default_context()
1041 self.load_default_context()
1037
1042
1038 pull_request = PullRequest.get_or_404(
1043 pull_request = PullRequest.get_or_404(
1039 self.request.matchdict['pull_request_id'])
1044 self.request.matchdict['pull_request_id'])
1040 pull_request_id = pull_request.pull_request_id
1045 pull_request_id = pull_request.pull_request_id
1041 version = self.request.GET.get('version')
1046 version = self.request.GET.get('version')
1042
1047
1043 _render = self.request.get_partial_renderer(
1048 _render = self.request.get_partial_renderer(
1044 'rhodecode:templates/base/sidebar.mako')
1049 'rhodecode:templates/base/sidebar.mako')
1045 c = _render.get_call_context()
1050 c = _render.get_call_context()
1046 (pull_request_latest,
1051 (pull_request_latest,
1047 pull_request_at_ver,
1052 pull_request_at_ver,
1048 pull_request_display_obj,
1053 pull_request_display_obj,
1049 at_version) = PullRequestModel().get_pr_version(
1054 at_version) = PullRequestModel().get_pr_version(
1050 pull_request_id, version=version)
1055 pull_request_id, version=version)
1051 versions = pull_request_display_obj.versions()
1056 versions = pull_request_display_obj.versions()
1052 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1057 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1053 c.versions = versions + [latest_ver]
1058 c.versions = versions + [latest_ver]
1054
1059
1055 c.at_version = at_version
1060 c.at_version = at_version
1056 c.at_version_num = (at_version
1061 c.at_version_num = (at_version
1057 if at_version and at_version != PullRequest.LATEST_VER
1062 if at_version and at_version != PullRequest.LATEST_VER
1058 else None)
1063 else None)
1059
1064
1060 c.unresolved_comments = CommentsModel() \
1065 c.unresolved_comments = CommentsModel() \
1061 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1066 .get_pull_request_unresolved_todos(pull_request, include_drafts=False)
1062 c.resolved_comments = CommentsModel() \
1067 c.resolved_comments = CommentsModel() \
1063 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1068 .get_pull_request_resolved_todos(pull_request, include_drafts=False)
1064
1069
1065 all_comments = c.unresolved_comments + c.resolved_comments
1070 all_comments = c.unresolved_comments + c.resolved_comments
1066 existing_ids = self.get_comment_ids(self.request.POST)
1071 existing_ids = self.get_comment_ids(self.request.POST)
1067 return _render('comments_table', all_comments, len(c.unresolved_comments),
1072 return _render('comments_table', all_comments, len(c.unresolved_comments),
1068 todo_comments=True, existing_ids=existing_ids)
1073 todo_comments=True, existing_ids=existing_ids)
1069
1074
1070 @LoginRequired()
1075 @LoginRequired()
1071 @NotAnonymous()
1076 @NotAnonymous()
1072 @HasRepoPermissionAnyDecorator(
1077 @HasRepoPermissionAnyDecorator(
1073 'repository.read', 'repository.write', 'repository.admin')
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 @CSRFRequired()
1121 @CSRFRequired()
1075 @view_config(
1122 @view_config(
1076 route_name='pullrequest_create', request_method='POST',
1123 route_name='pullrequest_create', request_method='POST',
1077 renderer=None)
1124 renderer=None)
1078 def pull_request_create(self):
1125 def pull_request_create(self):
1079 _ = self.request.translate
1126 _ = self.request.translate
1080 self.assure_not_empty_repo()
1127 self.assure_not_empty_repo()
1081 self.load_default_context()
1128 self.load_default_context()
1082
1129
1083 controls = peppercorn.parse(self.request.POST.items())
1130 controls = peppercorn.parse(self.request.POST.items())
1084
1131
1085 try:
1132 try:
1086 form = PullRequestForm(
1133 form = PullRequestForm(
1087 self.request.translate, self.db_repo.repo_id)()
1134 self.request.translate, self.db_repo.repo_id)()
1088 _form = form.to_python(controls)
1135 _form = form.to_python(controls)
1089 except formencode.Invalid as errors:
1136 except formencode.Invalid as errors:
1090 if errors.error_dict.get('revisions'):
1137 if errors.error_dict.get('revisions'):
1091 msg = 'Revisions: %s' % errors.error_dict['revisions']
1138 msg = 'Revisions: %s' % errors.error_dict['revisions']
1092 elif errors.error_dict.get('pullrequest_title'):
1139 elif errors.error_dict.get('pullrequest_title'):
1093 msg = errors.error_dict.get('pullrequest_title')
1140 msg = errors.error_dict.get('pullrequest_title')
1094 else:
1141 else:
1095 msg = _('Error creating pull request: {}').format(errors)
1142 msg = _('Error creating pull request: {}').format(errors)
1096 log.exception(msg)
1143 log.exception(msg)
1097 h.flash(msg, 'error')
1144 h.flash(msg, 'error')
1098
1145
1099 # would rather just go back to form ...
1146 # would rather just go back to form ...
1100 raise HTTPFound(
1147 raise HTTPFound(
1101 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1148 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1102
1149
1103 source_repo = _form['source_repo']
1150 source_repo = _form['source_repo']
1104 source_ref = _form['source_ref']
1151 source_ref = _form['source_ref']
1105 target_repo = _form['target_repo']
1152 target_repo = _form['target_repo']
1106 target_ref = _form['target_ref']
1153 target_ref = _form['target_ref']
1107 commit_ids = _form['revisions'][::-1]
1154 commit_ids = _form['revisions'][::-1]
1108 common_ancestor_id = _form['common_ancestor']
1155 common_ancestor_id = _form['common_ancestor']
1109
1156
1110 # find the ancestor for this pr
1157 # find the ancestor for this pr
1111 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1158 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1112 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1159 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1113
1160
1114 if not (source_db_repo or target_db_repo):
1161 if not (source_db_repo or target_db_repo):
1115 h.flash(_('source_repo or target repo not found'), category='error')
1162 h.flash(_('source_repo or target repo not found'), category='error')
1116 raise HTTPFound(
1163 raise HTTPFound(
1117 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1164 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1118
1165
1119 # re-check permissions again here
1166 # re-check permissions again here
1120 # source_repo we must have read permissions
1167 # source_repo we must have read permissions
1121
1168
1122 source_perm = HasRepoPermissionAny(
1169 source_perm = HasRepoPermissionAny(
1123 'repository.read', 'repository.write', 'repository.admin')(
1170 'repository.read', 'repository.write', 'repository.admin')(
1124 source_db_repo.repo_name)
1171 source_db_repo.repo_name)
1125 if not source_perm:
1172 if not source_perm:
1126 msg = _('Not Enough permissions to source repo `{}`.'.format(
1173 msg = _('Not Enough permissions to source repo `{}`.'.format(
1127 source_db_repo.repo_name))
1174 source_db_repo.repo_name))
1128 h.flash(msg, category='error')
1175 h.flash(msg, category='error')
1129 # copy the args back to redirect
1176 # copy the args back to redirect
1130 org_query = self.request.GET.mixed()
1177 org_query = self.request.GET.mixed()
1131 raise HTTPFound(
1178 raise HTTPFound(
1132 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1179 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1133 _query=org_query))
1180 _query=org_query))
1134
1181
1135 # target repo we must have read permissions, and also later on
1182 # target repo we must have read permissions, and also later on
1136 # we want to check branch permissions here
1183 # we want to check branch permissions here
1137 target_perm = HasRepoPermissionAny(
1184 target_perm = HasRepoPermissionAny(
1138 'repository.read', 'repository.write', 'repository.admin')(
1185 'repository.read', 'repository.write', 'repository.admin')(
1139 target_db_repo.repo_name)
1186 target_db_repo.repo_name)
1140 if not target_perm:
1187 if not target_perm:
1141 msg = _('Not Enough permissions to target repo `{}`.'.format(
1188 msg = _('Not Enough permissions to target repo `{}`.'.format(
1142 target_db_repo.repo_name))
1189 target_db_repo.repo_name))
1143 h.flash(msg, category='error')
1190 h.flash(msg, category='error')
1144 # copy the args back to redirect
1191 # copy the args back to redirect
1145 org_query = self.request.GET.mixed()
1192 org_query = self.request.GET.mixed()
1146 raise HTTPFound(
1193 raise HTTPFound(
1147 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1194 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1148 _query=org_query))
1195 _query=org_query))
1149
1196
1150 source_scm = source_db_repo.scm_instance()
1197 source_scm = source_db_repo.scm_instance()
1151 target_scm = target_db_repo.scm_instance()
1198 target_scm = target_db_repo.scm_instance()
1152
1199
1153 source_ref_obj = unicode_to_reference(source_ref)
1200 source_ref_obj = unicode_to_reference(source_ref)
1154 target_ref_obj = unicode_to_reference(target_ref)
1201 target_ref_obj = unicode_to_reference(target_ref)
1155
1202
1156 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1203 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1157 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1204 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1158
1205
1159 ancestor = source_scm.get_common_ancestor(
1206 ancestor = source_scm.get_common_ancestor(
1160 source_commit.raw_id, target_commit.raw_id, target_scm)
1207 source_commit.raw_id, target_commit.raw_id, target_scm)
1161
1208
1162 # recalculate target ref based on ancestor
1209 # recalculate target ref based on ancestor
1163 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1210 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1164
1211
1165 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1212 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1166 PullRequestModel().get_reviewer_functions()
1213 PullRequestModel().get_reviewer_functions()
1167
1214
1168 # recalculate reviewers logic, to make sure we can validate this
1215 # recalculate reviewers logic, to make sure we can validate this
1169 reviewer_rules = get_default_reviewers_data(
1216 reviewer_rules = get_default_reviewers_data(
1170 self._rhodecode_db_user,
1217 self._rhodecode_db_user,
1171 source_db_repo,
1218 source_db_repo,
1172 source_ref_obj,
1219 source_ref_obj,
1173 target_db_repo,
1220 target_db_repo,
1174 target_ref_obj,
1221 target_ref_obj,
1175 include_diff_info=False)
1222 include_diff_info=False)
1176
1223
1177 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1224 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1178 observers = validate_observers(_form['observer_members'], reviewer_rules)
1225 observers = validate_observers(_form['observer_members'], reviewer_rules)
1179
1226
1180 pullrequest_title = _form['pullrequest_title']
1227 pullrequest_title = _form['pullrequest_title']
1181 title_source_ref = source_ref_obj.name
1228 title_source_ref = source_ref_obj.name
1182 if not pullrequest_title:
1229 if not pullrequest_title:
1183 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1230 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1184 source=source_repo,
1231 source=source_repo,
1185 source_ref=title_source_ref,
1232 source_ref=title_source_ref,
1186 target=target_repo
1233 target=target_repo
1187 )
1234 )
1188
1235
1189 description = _form['pullrequest_desc']
1236 description = _form['pullrequest_desc']
1190 description_renderer = _form['description_renderer']
1237 description_renderer = _form['description_renderer']
1191
1238
1192 try:
1239 try:
1193 pull_request = PullRequestModel().create(
1240 pull_request = PullRequestModel().create(
1194 created_by=self._rhodecode_user.user_id,
1241 created_by=self._rhodecode_user.user_id,
1195 source_repo=source_repo,
1242 source_repo=source_repo,
1196 source_ref=source_ref,
1243 source_ref=source_ref,
1197 target_repo=target_repo,
1244 target_repo=target_repo,
1198 target_ref=target_ref,
1245 target_ref=target_ref,
1199 revisions=commit_ids,
1246 revisions=commit_ids,
1200 common_ancestor_id=common_ancestor_id,
1247 common_ancestor_id=common_ancestor_id,
1201 reviewers=reviewers,
1248 reviewers=reviewers,
1202 observers=observers,
1249 observers=observers,
1203 title=pullrequest_title,
1250 title=pullrequest_title,
1204 description=description,
1251 description=description,
1205 description_renderer=description_renderer,
1252 description_renderer=description_renderer,
1206 reviewer_data=reviewer_rules,
1253 reviewer_data=reviewer_rules,
1207 auth_user=self._rhodecode_user
1254 auth_user=self._rhodecode_user
1208 )
1255 )
1209 Session().commit()
1256 Session().commit()
1210
1257
1211 h.flash(_('Successfully opened new pull request'),
1258 h.flash(_('Successfully opened new pull request'),
1212 category='success')
1259 category='success')
1213 except Exception:
1260 except Exception:
1214 msg = _('Error occurred during creation of this pull request.')
1261 msg = _('Error occurred during creation of this pull request.')
1215 log.exception(msg)
1262 log.exception(msg)
1216 h.flash(msg, category='error')
1263 h.flash(msg, category='error')
1217
1264
1218 # copy the args back to redirect
1265 # copy the args back to redirect
1219 org_query = self.request.GET.mixed()
1266 org_query = self.request.GET.mixed()
1220 raise HTTPFound(
1267 raise HTTPFound(
1221 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1268 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1222 _query=org_query))
1269 _query=org_query))
1223
1270
1224 raise HTTPFound(
1271 raise HTTPFound(
1225 h.route_path('pullrequest_show', repo_name=target_repo,
1272 h.route_path('pullrequest_show', repo_name=target_repo,
1226 pull_request_id=pull_request.pull_request_id))
1273 pull_request_id=pull_request.pull_request_id))
1227
1274
1228 @LoginRequired()
1275 @LoginRequired()
1229 @NotAnonymous()
1276 @NotAnonymous()
1230 @HasRepoPermissionAnyDecorator(
1277 @HasRepoPermissionAnyDecorator(
1231 'repository.read', 'repository.write', 'repository.admin')
1278 'repository.read', 'repository.write', 'repository.admin')
1232 @CSRFRequired()
1279 @CSRFRequired()
1233 @view_config(
1280 @view_config(
1234 route_name='pullrequest_update', request_method='POST',
1281 route_name='pullrequest_update', request_method='POST',
1235 renderer='json_ext')
1282 renderer='json_ext')
1236 def pull_request_update(self):
1283 def pull_request_update(self):
1237 pull_request = PullRequest.get_or_404(
1284 pull_request = PullRequest.get_or_404(
1238 self.request.matchdict['pull_request_id'])
1285 self.request.matchdict['pull_request_id'])
1239 _ = self.request.translate
1286 _ = self.request.translate
1240
1287
1241 c = self.load_default_context()
1288 c = self.load_default_context()
1242 redirect_url = None
1289 redirect_url = None
1243
1290
1244 if pull_request.is_closed():
1291 if pull_request.is_closed():
1245 log.debug('update: forbidden because pull request is closed')
1292 log.debug('update: forbidden because pull request is closed')
1246 msg = _(u'Cannot update closed pull requests.')
1293 msg = _(u'Cannot update closed pull requests.')
1247 h.flash(msg, category='error')
1294 h.flash(msg, category='error')
1248 return {'response': True,
1295 return {'response': True,
1249 'redirect_url': redirect_url}
1296 'redirect_url': redirect_url}
1250
1297
1251 is_state_changing = pull_request.is_state_changing()
1298 is_state_changing = pull_request.is_state_changing()
1252 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1299 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1253
1300
1254 # only owner or admin can update it
1301 # only owner or admin can update it
1255 allowed_to_update = PullRequestModel().check_user_update(
1302 allowed_to_update = PullRequestModel().check_user_update(
1256 pull_request, self._rhodecode_user)
1303 pull_request, self._rhodecode_user)
1257
1304
1258 if allowed_to_update:
1305 if allowed_to_update:
1259 controls = peppercorn.parse(self.request.POST.items())
1306 controls = peppercorn.parse(self.request.POST.items())
1260 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1307 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1261
1308
1262 if 'review_members' in controls:
1309 if 'review_members' in controls:
1263 self._update_reviewers(
1310 self._update_reviewers(
1264 c,
1311 c,
1265 pull_request, controls['review_members'],
1312 pull_request, controls['review_members'],
1266 pull_request.reviewer_data,
1313 pull_request.reviewer_data,
1267 PullRequestReviewers.ROLE_REVIEWER)
1314 PullRequestReviewers.ROLE_REVIEWER)
1268 elif 'observer_members' in controls:
1315 elif 'observer_members' in controls:
1269 self._update_reviewers(
1316 self._update_reviewers(
1270 c,
1317 c,
1271 pull_request, controls['observer_members'],
1318 pull_request, controls['observer_members'],
1272 pull_request.reviewer_data,
1319 pull_request.reviewer_data,
1273 PullRequestReviewers.ROLE_OBSERVER)
1320 PullRequestReviewers.ROLE_OBSERVER)
1274 elif str2bool(self.request.POST.get('update_commits', 'false')):
1321 elif str2bool(self.request.POST.get('update_commits', 'false')):
1275 if is_state_changing:
1322 if is_state_changing:
1276 log.debug('commits update: forbidden because pull request is in state %s',
1323 log.debug('commits update: forbidden because pull request is in state %s',
1277 pull_request.pull_request_state)
1324 pull_request.pull_request_state)
1278 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1325 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1279 u'Current state is: `{}`').format(
1326 u'Current state is: `{}`').format(
1280 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1327 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1281 h.flash(msg, category='error')
1328 h.flash(msg, category='error')
1282 return {'response': True,
1329 return {'response': True,
1283 'redirect_url': redirect_url}
1330 'redirect_url': redirect_url}
1284
1331
1285 self._update_commits(c, pull_request)
1332 self._update_commits(c, pull_request)
1286 if force_refresh:
1333 if force_refresh:
1287 redirect_url = h.route_path(
1334 redirect_url = h.route_path(
1288 'pullrequest_show', repo_name=self.db_repo_name,
1335 'pullrequest_show', repo_name=self.db_repo_name,
1289 pull_request_id=pull_request.pull_request_id,
1336 pull_request_id=pull_request.pull_request_id,
1290 _query={"force_refresh": 1})
1337 _query={"force_refresh": 1})
1291 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1338 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1292 self._edit_pull_request(pull_request)
1339 self._edit_pull_request(pull_request)
1293 else:
1340 else:
1294 log.error('Unhandled update data.')
1341 log.error('Unhandled update data.')
1295 raise HTTPBadRequest()
1342 raise HTTPBadRequest()
1296
1343
1297 return {'response': True,
1344 return {'response': True,
1298 'redirect_url': redirect_url}
1345 'redirect_url': redirect_url}
1299 raise HTTPForbidden()
1346 raise HTTPForbidden()
1300
1347
1301 def _edit_pull_request(self, pull_request):
1348 def _edit_pull_request(self, pull_request):
1302 """
1349 """
1303 Edit title and description
1350 Edit title and description
1304 """
1351 """
1305 _ = self.request.translate
1352 _ = self.request.translate
1306
1353
1307 try:
1354 try:
1308 PullRequestModel().edit(
1355 PullRequestModel().edit(
1309 pull_request,
1356 pull_request,
1310 self.request.POST.get('title'),
1357 self.request.POST.get('title'),
1311 self.request.POST.get('description'),
1358 self.request.POST.get('description'),
1312 self.request.POST.get('description_renderer'),
1359 self.request.POST.get('description_renderer'),
1313 self._rhodecode_user)
1360 self._rhodecode_user)
1314 except ValueError:
1361 except ValueError:
1315 msg = _(u'Cannot update closed pull requests.')
1362 msg = _(u'Cannot update closed pull requests.')
1316 h.flash(msg, category='error')
1363 h.flash(msg, category='error')
1317 return
1364 return
1318 else:
1365 else:
1319 Session().commit()
1366 Session().commit()
1320
1367
1321 msg = _(u'Pull request title & description updated.')
1368 msg = _(u'Pull request title & description updated.')
1322 h.flash(msg, category='success')
1369 h.flash(msg, category='success')
1323 return
1370 return
1324
1371
1325 def _update_commits(self, c, pull_request):
1372 def _update_commits(self, c, pull_request):
1326 _ = self.request.translate
1373 _ = self.request.translate
1327
1374
1328 with pull_request.set_state(PullRequest.STATE_UPDATING):
1375 with pull_request.set_state(PullRequest.STATE_UPDATING):
1329 resp = PullRequestModel().update_commits(
1376 resp = PullRequestModel().update_commits(
1330 pull_request, self._rhodecode_db_user)
1377 pull_request, self._rhodecode_db_user)
1331
1378
1332 if resp.executed:
1379 if resp.executed:
1333
1380
1334 if resp.target_changed and resp.source_changed:
1381 if resp.target_changed and resp.source_changed:
1335 changed = 'target and source repositories'
1382 changed = 'target and source repositories'
1336 elif resp.target_changed and not resp.source_changed:
1383 elif resp.target_changed and not resp.source_changed:
1337 changed = 'target repository'
1384 changed = 'target repository'
1338 elif not resp.target_changed and resp.source_changed:
1385 elif not resp.target_changed and resp.source_changed:
1339 changed = 'source repository'
1386 changed = 'source repository'
1340 else:
1387 else:
1341 changed = 'nothing'
1388 changed = 'nothing'
1342
1389
1343 msg = _(u'Pull request updated to "{source_commit_id}" with '
1390 msg = _(u'Pull request updated to "{source_commit_id}" with '
1344 u'{count_added} added, {count_removed} removed commits. '
1391 u'{count_added} added, {count_removed} removed commits. '
1345 u'Source of changes: {change_source}.')
1392 u'Source of changes: {change_source}.')
1346 msg = msg.format(
1393 msg = msg.format(
1347 source_commit_id=pull_request.source_ref_parts.commit_id,
1394 source_commit_id=pull_request.source_ref_parts.commit_id,
1348 count_added=len(resp.changes.added),
1395 count_added=len(resp.changes.added),
1349 count_removed=len(resp.changes.removed),
1396 count_removed=len(resp.changes.removed),
1350 change_source=changed)
1397 change_source=changed)
1351 h.flash(msg, category='success')
1398 h.flash(msg, category='success')
1352 channelstream.pr_update_channelstream_push(
1399 channelstream.pr_update_channelstream_push(
1353 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1400 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1354 else:
1401 else:
1355 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1402 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1356 warning_reasons = [
1403 warning_reasons = [
1357 UpdateFailureReason.NO_CHANGE,
1404 UpdateFailureReason.NO_CHANGE,
1358 UpdateFailureReason.WRONG_REF_TYPE,
1405 UpdateFailureReason.WRONG_REF_TYPE,
1359 ]
1406 ]
1360 category = 'warning' if resp.reason in warning_reasons else 'error'
1407 category = 'warning' if resp.reason in warning_reasons else 'error'
1361 h.flash(msg, category=category)
1408 h.flash(msg, category=category)
1362
1409
1363 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1410 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1364 _ = self.request.translate
1411 _ = self.request.translate
1365
1412
1366 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1413 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1367 PullRequestModel().get_reviewer_functions()
1414 PullRequestModel().get_reviewer_functions()
1368
1415
1369 if role == PullRequestReviewers.ROLE_REVIEWER:
1416 if role == PullRequestReviewers.ROLE_REVIEWER:
1370 try:
1417 try:
1371 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1418 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1372 except ValueError as e:
1419 except ValueError as e:
1373 log.error('Reviewers Validation: {}'.format(e))
1420 log.error('Reviewers Validation: {}'.format(e))
1374 h.flash(e, category='error')
1421 h.flash(e, category='error')
1375 return
1422 return
1376
1423
1377 old_calculated_status = pull_request.calculated_review_status()
1424 old_calculated_status = pull_request.calculated_review_status()
1378 PullRequestModel().update_reviewers(
1425 PullRequestModel().update_reviewers(
1379 pull_request, reviewers, self._rhodecode_db_user)
1426 pull_request, reviewers, self._rhodecode_db_user)
1380
1427
1381 Session().commit()
1428 Session().commit()
1382
1429
1383 msg = _('Pull request reviewers updated.')
1430 msg = _('Pull request reviewers updated.')
1384 h.flash(msg, category='success')
1431 h.flash(msg, category='success')
1385 channelstream.pr_update_channelstream_push(
1432 channelstream.pr_update_channelstream_push(
1386 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1433 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1387
1434
1388 # trigger status changed if change in reviewers changes the status
1435 # trigger status changed if change in reviewers changes the status
1389 calculated_status = pull_request.calculated_review_status()
1436 calculated_status = pull_request.calculated_review_status()
1390 if old_calculated_status != calculated_status:
1437 if old_calculated_status != calculated_status:
1391 PullRequestModel().trigger_pull_request_hook(
1438 PullRequestModel().trigger_pull_request_hook(
1392 pull_request, self._rhodecode_user, 'review_status_change',
1439 pull_request, self._rhodecode_user, 'review_status_change',
1393 data={'status': calculated_status})
1440 data={'status': calculated_status})
1394
1441
1395 elif role == PullRequestReviewers.ROLE_OBSERVER:
1442 elif role == PullRequestReviewers.ROLE_OBSERVER:
1396 try:
1443 try:
1397 observers = validate_observers(review_members, reviewer_rules)
1444 observers = validate_observers(review_members, reviewer_rules)
1398 except ValueError as e:
1445 except ValueError as e:
1399 log.error('Observers Validation: {}'.format(e))
1446 log.error('Observers Validation: {}'.format(e))
1400 h.flash(e, category='error')
1447 h.flash(e, category='error')
1401 return
1448 return
1402
1449
1403 PullRequestModel().update_observers(
1450 PullRequestModel().update_observers(
1404 pull_request, observers, self._rhodecode_db_user)
1451 pull_request, observers, self._rhodecode_db_user)
1405
1452
1406 Session().commit()
1453 Session().commit()
1407 msg = _('Pull request observers updated.')
1454 msg = _('Pull request observers updated.')
1408 h.flash(msg, category='success')
1455 h.flash(msg, category='success')
1409 channelstream.pr_update_channelstream_push(
1456 channelstream.pr_update_channelstream_push(
1410 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1457 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1411
1458
1412 @LoginRequired()
1459 @LoginRequired()
1413 @NotAnonymous()
1460 @NotAnonymous()
1414 @HasRepoPermissionAnyDecorator(
1461 @HasRepoPermissionAnyDecorator(
1415 'repository.read', 'repository.write', 'repository.admin')
1462 'repository.read', 'repository.write', 'repository.admin')
1416 @CSRFRequired()
1463 @CSRFRequired()
1417 @view_config(
1464 @view_config(
1418 route_name='pullrequest_merge', request_method='POST',
1465 route_name='pullrequest_merge', request_method='POST',
1419 renderer='json_ext')
1466 renderer='json_ext')
1420 def pull_request_merge(self):
1467 def pull_request_merge(self):
1421 """
1468 """
1422 Merge will perform a server-side merge of the specified
1469 Merge will perform a server-side merge of the specified
1423 pull request, if the pull request is approved and mergeable.
1470 pull request, if the pull request is approved and mergeable.
1424 After successful merging, the pull request is automatically
1471 After successful merging, the pull request is automatically
1425 closed, with a relevant comment.
1472 closed, with a relevant comment.
1426 """
1473 """
1427 pull_request = PullRequest.get_or_404(
1474 pull_request = PullRequest.get_or_404(
1428 self.request.matchdict['pull_request_id'])
1475 self.request.matchdict['pull_request_id'])
1429 _ = self.request.translate
1476 _ = self.request.translate
1430
1477
1431 if pull_request.is_state_changing():
1478 if pull_request.is_state_changing():
1432 log.debug('show: forbidden because pull request is in state %s',
1479 log.debug('show: forbidden because pull request is in state %s',
1433 pull_request.pull_request_state)
1480 pull_request.pull_request_state)
1434 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1481 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1435 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1482 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1436 pull_request.pull_request_state)
1483 pull_request.pull_request_state)
1437 h.flash(msg, category='error')
1484 h.flash(msg, category='error')
1438 raise HTTPFound(
1485 raise HTTPFound(
1439 h.route_path('pullrequest_show',
1486 h.route_path('pullrequest_show',
1440 repo_name=pull_request.target_repo.repo_name,
1487 repo_name=pull_request.target_repo.repo_name,
1441 pull_request_id=pull_request.pull_request_id))
1488 pull_request_id=pull_request.pull_request_id))
1442
1489
1443 self.load_default_context()
1490 self.load_default_context()
1444
1491
1445 with pull_request.set_state(PullRequest.STATE_UPDATING):
1492 with pull_request.set_state(PullRequest.STATE_UPDATING):
1446 check = MergeCheck.validate(
1493 check = MergeCheck.validate(
1447 pull_request, auth_user=self._rhodecode_user,
1494 pull_request, auth_user=self._rhodecode_user,
1448 translator=self.request.translate)
1495 translator=self.request.translate)
1449 merge_possible = not check.failed
1496 merge_possible = not check.failed
1450
1497
1451 for err_type, error_msg in check.errors:
1498 for err_type, error_msg in check.errors:
1452 h.flash(error_msg, category=err_type)
1499 h.flash(error_msg, category=err_type)
1453
1500
1454 if merge_possible:
1501 if merge_possible:
1455 log.debug("Pre-conditions checked, trying to merge.")
1502 log.debug("Pre-conditions checked, trying to merge.")
1456 extras = vcs_operation_context(
1503 extras = vcs_operation_context(
1457 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1504 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1458 username=self._rhodecode_db_user.username, action='push',
1505 username=self._rhodecode_db_user.username, action='push',
1459 scm=pull_request.target_repo.repo_type)
1506 scm=pull_request.target_repo.repo_type)
1460 with pull_request.set_state(PullRequest.STATE_UPDATING):
1507 with pull_request.set_state(PullRequest.STATE_UPDATING):
1461 self._merge_pull_request(
1508 self._merge_pull_request(
1462 pull_request, self._rhodecode_db_user, extras)
1509 pull_request, self._rhodecode_db_user, extras)
1463 else:
1510 else:
1464 log.debug("Pre-conditions failed, NOT merging.")
1511 log.debug("Pre-conditions failed, NOT merging.")
1465
1512
1466 raise HTTPFound(
1513 raise HTTPFound(
1467 h.route_path('pullrequest_show',
1514 h.route_path('pullrequest_show',
1468 repo_name=pull_request.target_repo.repo_name,
1515 repo_name=pull_request.target_repo.repo_name,
1469 pull_request_id=pull_request.pull_request_id))
1516 pull_request_id=pull_request.pull_request_id))
1470
1517
1471 def _merge_pull_request(self, pull_request, user, extras):
1518 def _merge_pull_request(self, pull_request, user, extras):
1472 _ = self.request.translate
1519 _ = self.request.translate
1473 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1520 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1474
1521
1475 if merge_resp.executed:
1522 if merge_resp.executed:
1476 log.debug("The merge was successful, closing the pull request.")
1523 log.debug("The merge was successful, closing the pull request.")
1477 PullRequestModel().close_pull_request(
1524 PullRequestModel().close_pull_request(
1478 pull_request.pull_request_id, user)
1525 pull_request.pull_request_id, user)
1479 Session().commit()
1526 Session().commit()
1480 msg = _('Pull request was successfully merged and closed.')
1527 msg = _('Pull request was successfully merged and closed.')
1481 h.flash(msg, category='success')
1528 h.flash(msg, category='success')
1482 else:
1529 else:
1483 log.debug(
1530 log.debug(
1484 "The merge was not successful. Merge response: %s", merge_resp)
1531 "The merge was not successful. Merge response: %s", merge_resp)
1485 msg = merge_resp.merge_status_message
1532 msg = merge_resp.merge_status_message
1486 h.flash(msg, category='error')
1533 h.flash(msg, category='error')
1487
1534
1488 @LoginRequired()
1535 @LoginRequired()
1489 @NotAnonymous()
1536 @NotAnonymous()
1490 @HasRepoPermissionAnyDecorator(
1537 @HasRepoPermissionAnyDecorator(
1491 'repository.read', 'repository.write', 'repository.admin')
1538 'repository.read', 'repository.write', 'repository.admin')
1492 @CSRFRequired()
1539 @CSRFRequired()
1493 @view_config(
1540 @view_config(
1494 route_name='pullrequest_delete', request_method='POST',
1541 route_name='pullrequest_delete', request_method='POST',
1495 renderer='json_ext')
1542 renderer='json_ext')
1496 def pull_request_delete(self):
1543 def pull_request_delete(self):
1497 _ = self.request.translate
1544 _ = self.request.translate
1498
1545
1499 pull_request = PullRequest.get_or_404(
1546 pull_request = PullRequest.get_or_404(
1500 self.request.matchdict['pull_request_id'])
1547 self.request.matchdict['pull_request_id'])
1501 self.load_default_context()
1548 self.load_default_context()
1502
1549
1503 pr_closed = pull_request.is_closed()
1550 pr_closed = pull_request.is_closed()
1504 allowed_to_delete = PullRequestModel().check_user_delete(
1551 allowed_to_delete = PullRequestModel().check_user_delete(
1505 pull_request, self._rhodecode_user) and not pr_closed
1552 pull_request, self._rhodecode_user) and not pr_closed
1506
1553
1507 # only owner can delete it !
1554 # only owner can delete it !
1508 if allowed_to_delete:
1555 if allowed_to_delete:
1509 PullRequestModel().delete(pull_request, self._rhodecode_user)
1556 PullRequestModel().delete(pull_request, self._rhodecode_user)
1510 Session().commit()
1557 Session().commit()
1511 h.flash(_('Successfully deleted pull request'),
1558 h.flash(_('Successfully deleted pull request'),
1512 category='success')
1559 category='success')
1513 raise HTTPFound(h.route_path('pullrequest_show_all',
1560 raise HTTPFound(h.route_path('pullrequest_show_all',
1514 repo_name=self.db_repo_name))
1561 repo_name=self.db_repo_name))
1515
1562
1516 log.warning('user %s tried to delete pull request without access',
1563 log.warning('user %s tried to delete pull request without access',
1517 self._rhodecode_user)
1564 self._rhodecode_user)
1518 raise HTTPNotFound()
1565 raise HTTPNotFound()
1519
1566
1520 def _pull_request_comments_create(self, pull_request, comments):
1567 def _pull_request_comments_create(self, pull_request, comments):
1521 _ = self.request.translate
1568 _ = self.request.translate
1522 data = {}
1569 data = {}
1523 if not comments:
1570 if not comments:
1524 return
1571 return
1525 pull_request_id = pull_request.pull_request_id
1572 pull_request_id = pull_request.pull_request_id
1526
1573
1527 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1574 all_drafts = len([x for x in comments if str2bool(x['is_draft'])]) == len(comments)
1528
1575
1529 for entry in comments:
1576 for entry in comments:
1530 c = self.load_default_context()
1577 c = self.load_default_context()
1531 comment_type = entry['comment_type']
1578 comment_type = entry['comment_type']
1532 text = entry['text']
1579 text = entry['text']
1533 status = entry['status']
1580 status = entry['status']
1534 is_draft = str2bool(entry['is_draft'])
1581 is_draft = str2bool(entry['is_draft'])
1535 resolves_comment_id = entry['resolves_comment_id']
1582 resolves_comment_id = entry['resolves_comment_id']
1536 close_pull_request = entry['close_pull_request']
1583 close_pull_request = entry['close_pull_request']
1537 f_path = entry['f_path']
1584 f_path = entry['f_path']
1538 line_no = entry['line']
1585 line_no = entry['line']
1539 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1586 target_elem_id = 'file-{}'.format(h.safeid(h.safe_unicode(f_path)))
1540
1587
1541 # the logic here should work like following, if we submit close
1588 # the logic here should work like following, if we submit close
1542 # pr comment, use `close_pull_request_with_comment` function
1589 # pr comment, use `close_pull_request_with_comment` function
1543 # else handle regular comment logic
1590 # else handle regular comment logic
1544
1591
1545 if close_pull_request:
1592 if close_pull_request:
1546 # only owner or admin or person with write permissions
1593 # only owner or admin or person with write permissions
1547 allowed_to_close = PullRequestModel().check_user_update(
1594 allowed_to_close = PullRequestModel().check_user_update(
1548 pull_request, self._rhodecode_user)
1595 pull_request, self._rhodecode_user)
1549 if not allowed_to_close:
1596 if not allowed_to_close:
1550 log.debug('comment: forbidden because not allowed to close '
1597 log.debug('comment: forbidden because not allowed to close '
1551 'pull request %s', pull_request_id)
1598 'pull request %s', pull_request_id)
1552 raise HTTPForbidden()
1599 raise HTTPForbidden()
1553
1600
1554 # This also triggers `review_status_change`
1601 # This also triggers `review_status_change`
1555 comment, status = PullRequestModel().close_pull_request_with_comment(
1602 comment, status = PullRequestModel().close_pull_request_with_comment(
1556 pull_request, self._rhodecode_user, self.db_repo, message=text,
1603 pull_request, self._rhodecode_user, self.db_repo, message=text,
1557 auth_user=self._rhodecode_user)
1604 auth_user=self._rhodecode_user)
1558 Session().flush()
1605 Session().flush()
1559 is_inline = comment.is_inline
1606 is_inline = comment.is_inline
1560
1607
1561 PullRequestModel().trigger_pull_request_hook(
1608 PullRequestModel().trigger_pull_request_hook(
1562 pull_request, self._rhodecode_user, 'comment',
1609 pull_request, self._rhodecode_user, 'comment',
1563 data={'comment': comment})
1610 data={'comment': comment})
1564
1611
1565 else:
1612 else:
1566 # regular comment case, could be inline, or one with status.
1613 # regular comment case, could be inline, or one with status.
1567 # for that one we check also permissions
1614 # for that one we check also permissions
1568 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1615 # Additionally ENSURE if somehow draft is sent we're then unable to change status
1569 allowed_to_change_status = PullRequestModel().check_user_change_status(
1616 allowed_to_change_status = PullRequestModel().check_user_change_status(
1570 pull_request, self._rhodecode_user) and not is_draft
1617 pull_request, self._rhodecode_user) and not is_draft
1571
1618
1572 if status and allowed_to_change_status:
1619 if status and allowed_to_change_status:
1573 message = (_('Status change %(transition_icon)s %(status)s')
1620 message = (_('Status change %(transition_icon)s %(status)s')
1574 % {'transition_icon': '>',
1621 % {'transition_icon': '>',
1575 'status': ChangesetStatus.get_status_lbl(status)})
1622 'status': ChangesetStatus.get_status_lbl(status)})
1576 text = text or message
1623 text = text or message
1577
1624
1578 comment = CommentsModel().create(
1625 comment = CommentsModel().create(
1579 text=text,
1626 text=text,
1580 repo=self.db_repo.repo_id,
1627 repo=self.db_repo.repo_id,
1581 user=self._rhodecode_user.user_id,
1628 user=self._rhodecode_user.user_id,
1582 pull_request=pull_request,
1629 pull_request=pull_request,
1583 f_path=f_path,
1630 f_path=f_path,
1584 line_no=line_no,
1631 line_no=line_no,
1585 status_change=(ChangesetStatus.get_status_lbl(status)
1632 status_change=(ChangesetStatus.get_status_lbl(status)
1586 if status and allowed_to_change_status else None),
1633 if status and allowed_to_change_status else None),
1587 status_change_type=(status
1634 status_change_type=(status
1588 if status and allowed_to_change_status else None),
1635 if status and allowed_to_change_status else None),
1589 comment_type=comment_type,
1636 comment_type=comment_type,
1590 is_draft=is_draft,
1637 is_draft=is_draft,
1591 resolves_comment_id=resolves_comment_id,
1638 resolves_comment_id=resolves_comment_id,
1592 auth_user=self._rhodecode_user,
1639 auth_user=self._rhodecode_user,
1593 send_email=not is_draft, # skip notification for draft comments
1640 send_email=not is_draft, # skip notification for draft comments
1594 )
1641 )
1595 is_inline = comment.is_inline
1642 is_inline = comment.is_inline
1596
1643
1597 if allowed_to_change_status:
1644 if allowed_to_change_status:
1598 # calculate old status before we change it
1645 # calculate old status before we change it
1599 old_calculated_status = pull_request.calculated_review_status()
1646 old_calculated_status = pull_request.calculated_review_status()
1600
1647
1601 # get status if set !
1648 # get status if set !
1602 if status:
1649 if status:
1603 ChangesetStatusModel().set_status(
1650 ChangesetStatusModel().set_status(
1604 self.db_repo.repo_id,
1651 self.db_repo.repo_id,
1605 status,
1652 status,
1606 self._rhodecode_user.user_id,
1653 self._rhodecode_user.user_id,
1607 comment,
1654 comment,
1608 pull_request=pull_request
1655 pull_request=pull_request
1609 )
1656 )
1610
1657
1611 Session().flush()
1658 Session().flush()
1612 # this is somehow required to get access to some relationship
1659 # this is somehow required to get access to some relationship
1613 # loaded on comment
1660 # loaded on comment
1614 Session().refresh(comment)
1661 Session().refresh(comment)
1615
1662
1616 # skip notifications for drafts
1663 # skip notifications for drafts
1617 if not is_draft:
1664 if not is_draft:
1618 PullRequestModel().trigger_pull_request_hook(
1665 PullRequestModel().trigger_pull_request_hook(
1619 pull_request, self._rhodecode_user, 'comment',
1666 pull_request, self._rhodecode_user, 'comment',
1620 data={'comment': comment})
1667 data={'comment': comment})
1621
1668
1622 # we now calculate the status of pull request, and based on that
1669 # we now calculate the status of pull request, and based on that
1623 # calculation we set the commits status
1670 # calculation we set the commits status
1624 calculated_status = pull_request.calculated_review_status()
1671 calculated_status = pull_request.calculated_review_status()
1625 if old_calculated_status != calculated_status:
1672 if old_calculated_status != calculated_status:
1626 PullRequestModel().trigger_pull_request_hook(
1673 PullRequestModel().trigger_pull_request_hook(
1627 pull_request, self._rhodecode_user, 'review_status_change',
1674 pull_request, self._rhodecode_user, 'review_status_change',
1628 data={'status': calculated_status})
1675 data={'status': calculated_status})
1629
1676
1630 comment_id = comment.comment_id
1677 comment_id = comment.comment_id
1631 data[comment_id] = {
1678 data[comment_id] = {
1632 'target_id': target_elem_id
1679 'target_id': target_elem_id
1633 }
1680 }
1634 Session().flush()
1681 Session().flush()
1635
1682
1636 c.co = comment
1683 c.co = comment
1637 c.at_version_num = None
1684 c.at_version_num = None
1638 c.is_new = True
1685 c.is_new = True
1639 rendered_comment = render(
1686 rendered_comment = render(
1640 'rhodecode:templates/changeset/changeset_comment_block.mako',
1687 'rhodecode:templates/changeset/changeset_comment_block.mako',
1641 self._get_template_context(c), self.request)
1688 self._get_template_context(c), self.request)
1642
1689
1643 data[comment_id].update(comment.get_dict())
1690 data[comment_id].update(comment.get_dict())
1644 data[comment_id].update({'rendered_text': rendered_comment})
1691 data[comment_id].update({'rendered_text': rendered_comment})
1645
1692
1646 Session().commit()
1693 Session().commit()
1647
1694
1648 # skip channelstream for draft comments
1695 # skip channelstream for draft comments
1649 if not all_drafts:
1696 if not all_drafts:
1650 comment_broadcast_channel = channelstream.comment_channel(
1697 comment_broadcast_channel = channelstream.comment_channel(
1651 self.db_repo_name, pull_request_obj=pull_request)
1698 self.db_repo_name, pull_request_obj=pull_request)
1652
1699
1653 comment_data = data
1700 comment_data = data
1654 posted_comment_type = 'inline' if is_inline else 'general'
1701 posted_comment_type = 'inline' if is_inline else 'general'
1655 if len(data) == 1:
1702 if len(data) == 1:
1656 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1703 msg = _('posted {} new {} comment').format(len(data), posted_comment_type)
1657 else:
1704 else:
1658 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1705 msg = _('posted {} new {} comments').format(len(data), posted_comment_type)
1659
1706
1660 channelstream.comment_channelstream_push(
1707 channelstream.comment_channelstream_push(
1661 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1708 self.request, comment_broadcast_channel, self._rhodecode_user, msg,
1662 comment_data=comment_data)
1709 comment_data=comment_data)
1663
1710
1664 return data
1711 return data
1665
1712
1666 @LoginRequired()
1713 @LoginRequired()
1667 @NotAnonymous()
1714 @NotAnonymous()
1668 @HasRepoPermissionAnyDecorator(
1715 @HasRepoPermissionAnyDecorator(
1669 'repository.read', 'repository.write', 'repository.admin')
1716 'repository.read', 'repository.write', 'repository.admin')
1670 @CSRFRequired()
1717 @CSRFRequired()
1671 @view_config(
1718 @view_config(
1672 route_name='pullrequest_comment_create', request_method='POST',
1719 route_name='pullrequest_comment_create', request_method='POST',
1673 renderer='json_ext')
1720 renderer='json_ext')
1674 def pull_request_comment_create(self):
1721 def pull_request_comment_create(self):
1675 _ = self.request.translate
1722 _ = self.request.translate
1676
1723
1677 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1724 pull_request = PullRequest.get_or_404(self.request.matchdict['pull_request_id'])
1678
1725
1679 if pull_request.is_closed():
1726 if pull_request.is_closed():
1680 log.debug('comment: forbidden because pull request is closed')
1727 log.debug('comment: forbidden because pull request is closed')
1681 raise HTTPForbidden()
1728 raise HTTPForbidden()
1682
1729
1683 allowed_to_comment = PullRequestModel().check_user_comment(
1730 allowed_to_comment = PullRequestModel().check_user_comment(
1684 pull_request, self._rhodecode_user)
1731 pull_request, self._rhodecode_user)
1685 if not allowed_to_comment:
1732 if not allowed_to_comment:
1686 log.debug('comment: forbidden because pull request is from forbidden repo')
1733 log.debug('comment: forbidden because pull request is from forbidden repo')
1687 raise HTTPForbidden()
1734 raise HTTPForbidden()
1688
1735
1689 comment_data = {
1736 comment_data = {
1690 'comment_type': self.request.POST.get('comment_type'),
1737 'comment_type': self.request.POST.get('comment_type'),
1691 'text': self.request.POST.get('text'),
1738 'text': self.request.POST.get('text'),
1692 'status': self.request.POST.get('changeset_status', None),
1739 'status': self.request.POST.get('changeset_status', None),
1693 'is_draft': self.request.POST.get('draft'),
1740 'is_draft': self.request.POST.get('draft'),
1694 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1741 'resolves_comment_id': self.request.POST.get('resolves_comment_id', None),
1695 'close_pull_request': self.request.POST.get('close_pull_request'),
1742 'close_pull_request': self.request.POST.get('close_pull_request'),
1696 'f_path': self.request.POST.get('f_path'),
1743 'f_path': self.request.POST.get('f_path'),
1697 'line': self.request.POST.get('line'),
1744 'line': self.request.POST.get('line'),
1698 }
1745 }
1699 data = self._pull_request_comments_create(pull_request, [comment_data])
1746 data = self._pull_request_comments_create(pull_request, [comment_data])
1700
1747
1701 return data
1748 return data
1702
1749
1703 @LoginRequired()
1750 @LoginRequired()
1704 @NotAnonymous()
1751 @NotAnonymous()
1705 @HasRepoPermissionAnyDecorator(
1752 @HasRepoPermissionAnyDecorator(
1706 'repository.read', 'repository.write', 'repository.admin')
1753 'repository.read', 'repository.write', 'repository.admin')
1707 @CSRFRequired()
1754 @CSRFRequired()
1708 @view_config(
1755 @view_config(
1709 route_name='pullrequest_comment_delete', request_method='POST',
1756 route_name='pullrequest_comment_delete', request_method='POST',
1710 renderer='json_ext')
1757 renderer='json_ext')
1711 def pull_request_comment_delete(self):
1758 def pull_request_comment_delete(self):
1712 pull_request = PullRequest.get_or_404(
1759 pull_request = PullRequest.get_or_404(
1713 self.request.matchdict['pull_request_id'])
1760 self.request.matchdict['pull_request_id'])
1714
1761
1715 comment = ChangesetComment.get_or_404(
1762 comment = ChangesetComment.get_or_404(
1716 self.request.matchdict['comment_id'])
1763 self.request.matchdict['comment_id'])
1717 comment_id = comment.comment_id
1764 comment_id = comment.comment_id
1718
1765
1719 if comment.immutable:
1766 if comment.immutable:
1720 # don't allow deleting comments that are immutable
1767 # don't allow deleting comments that are immutable
1721 raise HTTPForbidden()
1768 raise HTTPForbidden()
1722
1769
1723 if pull_request.is_closed():
1770 if pull_request.is_closed():
1724 log.debug('comment: forbidden because pull request is closed')
1771 log.debug('comment: forbidden because pull request is closed')
1725 raise HTTPForbidden()
1772 raise HTTPForbidden()
1726
1773
1727 if not comment:
1774 if not comment:
1728 log.debug('Comment with id:%s not found, skipping', comment_id)
1775 log.debug('Comment with id:%s not found, skipping', comment_id)
1729 # comment already deleted in another call probably
1776 # comment already deleted in another call probably
1730 return True
1777 return True
1731
1778
1732 if comment.pull_request.is_closed():
1779 if comment.pull_request.is_closed():
1733 # don't allow deleting comments on closed pull request
1780 # don't allow deleting comments on closed pull request
1734 raise HTTPForbidden()
1781 raise HTTPForbidden()
1735
1782
1736 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1783 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1737 super_admin = h.HasPermissionAny('hg.admin')()
1784 super_admin = h.HasPermissionAny('hg.admin')()
1738 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1785 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1739 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1786 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1740 comment_repo_admin = is_repo_admin and is_repo_comment
1787 comment_repo_admin = is_repo_admin and is_repo_comment
1741
1788
1742 if super_admin or comment_owner or comment_repo_admin:
1789 if super_admin or comment_owner or comment_repo_admin:
1743 old_calculated_status = comment.pull_request.calculated_review_status()
1790 old_calculated_status = comment.pull_request.calculated_review_status()
1744 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1791 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1745 Session().commit()
1792 Session().commit()
1746 calculated_status = comment.pull_request.calculated_review_status()
1793 calculated_status = comment.pull_request.calculated_review_status()
1747 if old_calculated_status != calculated_status:
1794 if old_calculated_status != calculated_status:
1748 PullRequestModel().trigger_pull_request_hook(
1795 PullRequestModel().trigger_pull_request_hook(
1749 comment.pull_request, self._rhodecode_user, 'review_status_change',
1796 comment.pull_request, self._rhodecode_user, 'review_status_change',
1750 data={'status': calculated_status})
1797 data={'status': calculated_status})
1751 return True
1798 return True
1752 else:
1799 else:
1753 log.warning('No permissions for user %s to delete comment_id: %s',
1800 log.warning('No permissions for user %s to delete comment_id: %s',
1754 self._rhodecode_db_user, comment_id)
1801 self._rhodecode_db_user, comment_id)
1755 raise HTTPNotFound()
1802 raise HTTPNotFound()
1756
1803
1757 @LoginRequired()
1804 @LoginRequired()
1758 @NotAnonymous()
1805 @NotAnonymous()
1759 @HasRepoPermissionAnyDecorator(
1806 @HasRepoPermissionAnyDecorator(
1760 'repository.read', 'repository.write', 'repository.admin')
1807 'repository.read', 'repository.write', 'repository.admin')
1761 @CSRFRequired()
1808 @CSRFRequired()
1762 @view_config(
1809 @view_config(
1763 route_name='pullrequest_comment_edit', request_method='POST',
1810 route_name='pullrequest_comment_edit', request_method='POST',
1764 renderer='json_ext')
1811 renderer='json_ext')
1765 def pull_request_comment_edit(self):
1812 def pull_request_comment_edit(self):
1766 self.load_default_context()
1813 self.load_default_context()
1767
1814
1768 pull_request = PullRequest.get_or_404(
1815 pull_request = PullRequest.get_or_404(
1769 self.request.matchdict['pull_request_id']
1816 self.request.matchdict['pull_request_id']
1770 )
1817 )
1771 comment = ChangesetComment.get_or_404(
1818 comment = ChangesetComment.get_or_404(
1772 self.request.matchdict['comment_id']
1819 self.request.matchdict['comment_id']
1773 )
1820 )
1774 comment_id = comment.comment_id
1821 comment_id = comment.comment_id
1775
1822
1776 if comment.immutable:
1823 if comment.immutable:
1777 # don't allow deleting comments that are immutable
1824 # don't allow deleting comments that are immutable
1778 raise HTTPForbidden()
1825 raise HTTPForbidden()
1779
1826
1780 if pull_request.is_closed():
1827 if pull_request.is_closed():
1781 log.debug('comment: forbidden because pull request is closed')
1828 log.debug('comment: forbidden because pull request is closed')
1782 raise HTTPForbidden()
1829 raise HTTPForbidden()
1783
1830
1784 if comment.pull_request.is_closed():
1831 if comment.pull_request.is_closed():
1785 # don't allow deleting comments on closed pull request
1832 # don't allow deleting comments on closed pull request
1786 raise HTTPForbidden()
1833 raise HTTPForbidden()
1787
1834
1788 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1835 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1789 super_admin = h.HasPermissionAny('hg.admin')()
1836 super_admin = h.HasPermissionAny('hg.admin')()
1790 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1837 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1791 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1838 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1792 comment_repo_admin = is_repo_admin and is_repo_comment
1839 comment_repo_admin = is_repo_admin and is_repo_comment
1793
1840
1794 if super_admin or comment_owner or comment_repo_admin:
1841 if super_admin or comment_owner or comment_repo_admin:
1795 text = self.request.POST.get('text')
1842 text = self.request.POST.get('text')
1796 version = self.request.POST.get('version')
1843 version = self.request.POST.get('version')
1797 if text == comment.text:
1844 if text == comment.text:
1798 log.warning(
1845 log.warning(
1799 'Comment(PR): '
1846 'Comment(PR): '
1800 'Trying to create new version '
1847 'Trying to create new version '
1801 'with the same comment body {}'.format(
1848 'with the same comment body {}'.format(
1802 comment_id,
1849 comment_id,
1803 )
1850 )
1804 )
1851 )
1805 raise HTTPNotFound()
1852 raise HTTPNotFound()
1806
1853
1807 if version.isdigit():
1854 if version.isdigit():
1808 version = int(version)
1855 version = int(version)
1809 else:
1856 else:
1810 log.warning(
1857 log.warning(
1811 'Comment(PR): Wrong version type {} {} '
1858 'Comment(PR): Wrong version type {} {} '
1812 'for comment {}'.format(
1859 'for comment {}'.format(
1813 version,
1860 version,
1814 type(version),
1861 type(version),
1815 comment_id,
1862 comment_id,
1816 )
1863 )
1817 )
1864 )
1818 raise HTTPNotFound()
1865 raise HTTPNotFound()
1819
1866
1820 try:
1867 try:
1821 comment_history = CommentsModel().edit(
1868 comment_history = CommentsModel().edit(
1822 comment_id=comment_id,
1869 comment_id=comment_id,
1823 text=text,
1870 text=text,
1824 auth_user=self._rhodecode_user,
1871 auth_user=self._rhodecode_user,
1825 version=version,
1872 version=version,
1826 )
1873 )
1827 except CommentVersionMismatch:
1874 except CommentVersionMismatch:
1828 raise HTTPConflict()
1875 raise HTTPConflict()
1829
1876
1830 if not comment_history:
1877 if not comment_history:
1831 raise HTTPNotFound()
1878 raise HTTPNotFound()
1832
1879
1833 Session().commit()
1880 Session().commit()
1834 if not comment.draft:
1881 if not comment.draft:
1835 PullRequestModel().trigger_pull_request_hook(
1882 PullRequestModel().trigger_pull_request_hook(
1836 pull_request, self._rhodecode_user, 'comment_edit',
1883 pull_request, self._rhodecode_user, 'comment_edit',
1837 data={'comment': comment})
1884 data={'comment': comment})
1838
1885
1839 return {
1886 return {
1840 'comment_history_id': comment_history.comment_history_id,
1887 'comment_history_id': comment_history.comment_history_id,
1841 'comment_id': comment.comment_id,
1888 'comment_id': comment.comment_id,
1842 'comment_version': comment_history.version,
1889 'comment_version': comment_history.version,
1843 'comment_author_username': comment_history.author.username,
1890 'comment_author_username': comment_history.author.username,
1844 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1891 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1845 'comment_created_on': h.age_component(comment_history.created_on,
1892 'comment_created_on': h.age_component(comment_history.created_on,
1846 time_is_local=True),
1893 time_is_local=True),
1847 }
1894 }
1848 else:
1895 else:
1849 log.warning('No permissions for user %s to edit comment_id: %s',
1896 log.warning('No permissions for user %s to edit comment_id: %s',
1850 self._rhodecode_db_user, comment_id)
1897 self._rhodecode_db_user, comment_id)
1851 raise HTTPNotFound()
1898 raise HTTPNotFound()
@@ -1,845 +1,852 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2011-2020 RhodeCode GmbH
3 # Copyright (C) 2011-2020 RhodeCode GmbH
4 #
4 #
5 # This program is free software: you can redistribute it and/or modify
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
7 # (only), as published by the Free Software Foundation.
8 #
8 #
9 # This program is distributed in the hope that it will be useful,
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
12 # GNU General Public License for more details.
13 #
13 #
14 # You should have received a copy of the GNU Affero General Public License
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
16 #
17 # This program is dual-licensed. If you wish to learn more about the
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
20
21 """
21 """
22 comments model for RhodeCode
22 comments model for RhodeCode
23 """
23 """
24 import datetime
24 import datetime
25
25
26 import logging
26 import logging
27 import traceback
27 import traceback
28 import collections
28 import collections
29
29
30 from pyramid.threadlocal import get_current_registry, get_current_request
30 from pyramid.threadlocal import get_current_registry, get_current_request
31 from sqlalchemy.sql.expression import null
31 from sqlalchemy.sql.expression import null
32 from sqlalchemy.sql.functions import coalesce
32 from sqlalchemy.sql.functions import coalesce
33
33
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
35 from rhodecode.lib import audit_logger
35 from rhodecode.lib import audit_logger
36 from rhodecode.lib.exceptions import CommentVersionMismatch
36 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
38 from rhodecode.model import BaseModel
38 from rhodecode.model import BaseModel
39 from rhodecode.model.db import (
39 from rhodecode.model.db import (
40 false,
40 false, true,
41 ChangesetComment,
41 ChangesetComment,
42 User,
42 User,
43 Notification,
43 Notification,
44 PullRequest,
44 PullRequest,
45 AttributeDict,
45 AttributeDict,
46 ChangesetCommentHistory,
46 ChangesetCommentHistory,
47 )
47 )
48 from rhodecode.model.notification import NotificationModel
48 from rhodecode.model.notification import NotificationModel
49 from rhodecode.model.meta import Session
49 from rhodecode.model.meta import Session
50 from rhodecode.model.settings import VcsSettingsModel
50 from rhodecode.model.settings import VcsSettingsModel
51 from rhodecode.model.notification import EmailNotificationModel
51 from rhodecode.model.notification import EmailNotificationModel
52 from rhodecode.model.validation_schema.schemas import comment_schema
52 from rhodecode.model.validation_schema.schemas import comment_schema
53
53
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class CommentsModel(BaseModel):
58 class CommentsModel(BaseModel):
59
59
60 cls = ChangesetComment
60 cls = ChangesetComment
61
61
62 DIFF_CONTEXT_BEFORE = 3
62 DIFF_CONTEXT_BEFORE = 3
63 DIFF_CONTEXT_AFTER = 3
63 DIFF_CONTEXT_AFTER = 3
64
64
65 def __get_commit_comment(self, changeset_comment):
65 def __get_commit_comment(self, changeset_comment):
66 return self._get_instance(ChangesetComment, changeset_comment)
66 return self._get_instance(ChangesetComment, changeset_comment)
67
67
68 def __get_pull_request(self, pull_request):
68 def __get_pull_request(self, pull_request):
69 return self._get_instance(PullRequest, pull_request)
69 return self._get_instance(PullRequest, pull_request)
70
70
71 def _extract_mentions(self, s):
71 def _extract_mentions(self, s):
72 user_objects = []
72 user_objects = []
73 for username in extract_mentioned_users(s):
73 for username in extract_mentioned_users(s):
74 user_obj = User.get_by_username(username, case_insensitive=True)
74 user_obj = User.get_by_username(username, case_insensitive=True)
75 if user_obj:
75 if user_obj:
76 user_objects.append(user_obj)
76 user_objects.append(user_obj)
77 return user_objects
77 return user_objects
78
78
79 def _get_renderer(self, global_renderer='rst', request=None):
79 def _get_renderer(self, global_renderer='rst', request=None):
80 request = request or get_current_request()
80 request = request or get_current_request()
81
81
82 try:
82 try:
83 global_renderer = request.call_context.visual.default_renderer
83 global_renderer = request.call_context.visual.default_renderer
84 except AttributeError:
84 except AttributeError:
85 log.debug("Renderer not set, falling back "
85 log.debug("Renderer not set, falling back "
86 "to default renderer '%s'", global_renderer)
86 "to default renderer '%s'", global_renderer)
87 except Exception:
87 except Exception:
88 log.error(traceback.format_exc())
88 log.error(traceback.format_exc())
89 return global_renderer
89 return global_renderer
90
90
91 def aggregate_comments(self, comments, versions, show_version, inline=False):
91 def aggregate_comments(self, comments, versions, show_version, inline=False):
92 # group by versions, and count until, and display objects
92 # group by versions, and count until, and display objects
93
93
94 comment_groups = collections.defaultdict(list)
94 comment_groups = collections.defaultdict(list)
95 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
95 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
96
96
97 def yield_comments(pos):
97 def yield_comments(pos):
98 for co in comment_groups[pos]:
98 for co in comment_groups[pos]:
99 yield co
99 yield co
100
100
101 comment_versions = collections.defaultdict(
101 comment_versions = collections.defaultdict(
102 lambda: collections.defaultdict(list))
102 lambda: collections.defaultdict(list))
103 prev_prvid = -1
103 prev_prvid = -1
104 # fake last entry with None, to aggregate on "latest" version which
104 # fake last entry with None, to aggregate on "latest" version which
105 # doesn't have an pull_request_version_id
105 # doesn't have an pull_request_version_id
106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
107 prvid = ver.pull_request_version_id
107 prvid = ver.pull_request_version_id
108 if prev_prvid == -1:
108 if prev_prvid == -1:
109 prev_prvid = prvid
109 prev_prvid = prvid
110
110
111 for co in yield_comments(prvid):
111 for co in yield_comments(prvid):
112 comment_versions[prvid]['at'].append(co)
112 comment_versions[prvid]['at'].append(co)
113
113
114 # save until
114 # save until
115 current = comment_versions[prvid]['at']
115 current = comment_versions[prvid]['at']
116 prev_until = comment_versions[prev_prvid]['until']
116 prev_until = comment_versions[prev_prvid]['until']
117 cur_until = prev_until + current
117 cur_until = prev_until + current
118 comment_versions[prvid]['until'].extend(cur_until)
118 comment_versions[prvid]['until'].extend(cur_until)
119
119
120 # save outdated
120 # save outdated
121 if inline:
121 if inline:
122 outdated = [x for x in cur_until
122 outdated = [x for x in cur_until
123 if x.outdated_at_version(show_version)]
123 if x.outdated_at_version(show_version)]
124 else:
124 else:
125 outdated = [x for x in cur_until
125 outdated = [x for x in cur_until
126 if x.older_than_version(show_version)]
126 if x.older_than_version(show_version)]
127 display = [x for x in cur_until if x not in outdated]
127 display = [x for x in cur_until if x not in outdated]
128
128
129 comment_versions[prvid]['outdated'] = outdated
129 comment_versions[prvid]['outdated'] = outdated
130 comment_versions[prvid]['display'] = display
130 comment_versions[prvid]['display'] = display
131
131
132 prev_prvid = prvid
132 prev_prvid = prvid
133
133
134 return comment_versions
134 return comment_versions
135
135
136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
137 qry = Session().query(ChangesetComment) \
137 qry = Session().query(ChangesetComment) \
138 .filter(ChangesetComment.repo == repo)
138 .filter(ChangesetComment.repo == repo)
139
139
140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
142
142
143 if user:
143 if user:
144 user = self._get_user(user)
144 user = self._get_user(user)
145 if user:
145 if user:
146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
147
147
148 if commit_id:
148 if commit_id:
149 qry = qry.filter(ChangesetComment.revision == commit_id)
149 qry = qry.filter(ChangesetComment.revision == commit_id)
150
150
151 qry = qry.order_by(ChangesetComment.created_on)
151 qry = qry.order_by(ChangesetComment.created_on)
152 return qry.all()
152 return qry.all()
153
153
154 def get_repository_unresolved_todos(self, repo):
154 def get_repository_unresolved_todos(self, repo):
155 todos = Session().query(ChangesetComment) \
155 todos = Session().query(ChangesetComment) \
156 .filter(ChangesetComment.repo == repo) \
156 .filter(ChangesetComment.repo == repo) \
157 .filter(ChangesetComment.resolved_by == None) \
157 .filter(ChangesetComment.resolved_by == None) \
158 .filter(ChangesetComment.comment_type
158 .filter(ChangesetComment.comment_type
159 == ChangesetComment.COMMENT_TYPE_TODO)
159 == ChangesetComment.COMMENT_TYPE_TODO)
160 todos = todos.all()
160 todos = todos.all()
161
161
162 return todos
162 return todos
163
163
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
165
165
166 todos = Session().query(ChangesetComment) \
166 todos = Session().query(ChangesetComment) \
167 .filter(ChangesetComment.pull_request == pull_request) \
167 .filter(ChangesetComment.pull_request == pull_request) \
168 .filter(ChangesetComment.resolved_by == None) \
168 .filter(ChangesetComment.resolved_by == None) \
169 .filter(ChangesetComment.comment_type
169 .filter(ChangesetComment.comment_type
170 == ChangesetComment.COMMENT_TYPE_TODO)
170 == ChangesetComment.COMMENT_TYPE_TODO)
171
171
172 if not include_drafts:
172 if not include_drafts:
173 todos = todos.filter(ChangesetComment.draft == false())
173 todos = todos.filter(ChangesetComment.draft == false())
174
174
175 if not show_outdated:
175 if not show_outdated:
176 todos = todos.filter(
176 todos = todos.filter(
177 coalesce(ChangesetComment.display_state, '') !=
177 coalesce(ChangesetComment.display_state, '') !=
178 ChangesetComment.COMMENT_OUTDATED)
178 ChangesetComment.COMMENT_OUTDATED)
179
179
180 todos = todos.all()
180 todos = todos.all()
181
181
182 return todos
182 return todos
183
183
184 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
184 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True, include_drafts=True):
185
185
186 todos = Session().query(ChangesetComment) \
186 todos = Session().query(ChangesetComment) \
187 .filter(ChangesetComment.pull_request == pull_request) \
187 .filter(ChangesetComment.pull_request == pull_request) \
188 .filter(ChangesetComment.resolved_by != None) \
188 .filter(ChangesetComment.resolved_by != None) \
189 .filter(ChangesetComment.comment_type
189 .filter(ChangesetComment.comment_type
190 == ChangesetComment.COMMENT_TYPE_TODO)
190 == ChangesetComment.COMMENT_TYPE_TODO)
191
191
192 if not include_drafts:
192 if not include_drafts:
193 todos = todos.filter(ChangesetComment.draft == false())
193 todos = todos.filter(ChangesetComment.draft == false())
194
194
195 if not show_outdated:
195 if not show_outdated:
196 todos = todos.filter(
196 todos = todos.filter(
197 coalesce(ChangesetComment.display_state, '') !=
197 coalesce(ChangesetComment.display_state, '') !=
198 ChangesetComment.COMMENT_OUTDATED)
198 ChangesetComment.COMMENT_OUTDATED)
199
199
200 todos = todos.all()
200 todos = todos.all()
201
201
202 return todos
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 def get_commit_unresolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
211 def get_commit_unresolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
205
212
206 todos = Session().query(ChangesetComment) \
213 todos = Session().query(ChangesetComment) \
207 .filter(ChangesetComment.revision == commit_id) \
214 .filter(ChangesetComment.revision == commit_id) \
208 .filter(ChangesetComment.resolved_by == None) \
215 .filter(ChangesetComment.resolved_by == None) \
209 .filter(ChangesetComment.comment_type
216 .filter(ChangesetComment.comment_type
210 == ChangesetComment.COMMENT_TYPE_TODO)
217 == ChangesetComment.COMMENT_TYPE_TODO)
211
218
212 if not include_drafts:
219 if not include_drafts:
213 todos = todos.filter(ChangesetComment.draft == false())
220 todos = todos.filter(ChangesetComment.draft == false())
214
221
215 if not show_outdated:
222 if not show_outdated:
216 todos = todos.filter(
223 todos = todos.filter(
217 coalesce(ChangesetComment.display_state, '') !=
224 coalesce(ChangesetComment.display_state, '') !=
218 ChangesetComment.COMMENT_OUTDATED)
225 ChangesetComment.COMMENT_OUTDATED)
219
226
220 todos = todos.all()
227 todos = todos.all()
221
228
222 return todos
229 return todos
223
230
224 def get_commit_resolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
231 def get_commit_resolved_todos(self, commit_id, show_outdated=True, include_drafts=True):
225
232
226 todos = Session().query(ChangesetComment) \
233 todos = Session().query(ChangesetComment) \
227 .filter(ChangesetComment.revision == commit_id) \
234 .filter(ChangesetComment.revision == commit_id) \
228 .filter(ChangesetComment.resolved_by != None) \
235 .filter(ChangesetComment.resolved_by != None) \
229 .filter(ChangesetComment.comment_type
236 .filter(ChangesetComment.comment_type
230 == ChangesetComment.COMMENT_TYPE_TODO)
237 == ChangesetComment.COMMENT_TYPE_TODO)
231
238
232 if not include_drafts:
239 if not include_drafts:
233 todos = todos.filter(ChangesetComment.draft == false())
240 todos = todos.filter(ChangesetComment.draft == false())
234
241
235 if not show_outdated:
242 if not show_outdated:
236 todos = todos.filter(
243 todos = todos.filter(
237 coalesce(ChangesetComment.display_state, '') !=
244 coalesce(ChangesetComment.display_state, '') !=
238 ChangesetComment.COMMENT_OUTDATED)
245 ChangesetComment.COMMENT_OUTDATED)
239
246
240 todos = todos.all()
247 todos = todos.all()
241
248
242 return todos
249 return todos
243
250
244 def get_commit_inline_comments(self, commit_id, include_drafts=True):
251 def get_commit_inline_comments(self, commit_id, include_drafts=True):
245 inline_comments = Session().query(ChangesetComment) \
252 inline_comments = Session().query(ChangesetComment) \
246 .filter(ChangesetComment.line_no != None) \
253 .filter(ChangesetComment.line_no != None) \
247 .filter(ChangesetComment.f_path != None) \
254 .filter(ChangesetComment.f_path != None) \
248 .filter(ChangesetComment.revision == commit_id)
255 .filter(ChangesetComment.revision == commit_id)
249
256
250 if not include_drafts:
257 if not include_drafts:
251 inline_comments = inline_comments.filter(ChangesetComment.draft == false())
258 inline_comments = inline_comments.filter(ChangesetComment.draft == false())
252
259
253 inline_comments = inline_comments.all()
260 inline_comments = inline_comments.all()
254 return inline_comments
261 return inline_comments
255
262
256 def _log_audit_action(self, action, action_data, auth_user, comment):
263 def _log_audit_action(self, action, action_data, auth_user, comment):
257 audit_logger.store(
264 audit_logger.store(
258 action=action,
265 action=action,
259 action_data=action_data,
266 action_data=action_data,
260 user=auth_user,
267 user=auth_user,
261 repo=comment.repo)
268 repo=comment.repo)
262
269
263 def create(self, text, repo, user, commit_id=None, pull_request=None,
270 def create(self, text, repo, user, commit_id=None, pull_request=None,
264 f_path=None, line_no=None, status_change=None,
271 f_path=None, line_no=None, status_change=None,
265 status_change_type=None, comment_type=None, is_draft=False,
272 status_change_type=None, comment_type=None, is_draft=False,
266 resolves_comment_id=None, closing_pr=False, send_email=True,
273 resolves_comment_id=None, closing_pr=False, send_email=True,
267 renderer=None, auth_user=None, extra_recipients=None):
274 renderer=None, auth_user=None, extra_recipients=None):
268 """
275 """
269 Creates new comment for commit or pull request.
276 Creates new comment for commit or pull request.
270 IF status_change is not none this comment is associated with a
277 IF status_change is not none this comment is associated with a
271 status change of commit or commit associated with pull request
278 status change of commit or commit associated with pull request
272
279
273 :param text:
280 :param text:
274 :param repo:
281 :param repo:
275 :param user:
282 :param user:
276 :param commit_id:
283 :param commit_id:
277 :param pull_request:
284 :param pull_request:
278 :param f_path:
285 :param f_path:
279 :param line_no:
286 :param line_no:
280 :param status_change: Label for status change
287 :param status_change: Label for status change
281 :param comment_type: Type of comment
288 :param comment_type: Type of comment
282 :param is_draft: is comment a draft only
289 :param is_draft: is comment a draft only
283 :param resolves_comment_id: id of comment which this one will resolve
290 :param resolves_comment_id: id of comment which this one will resolve
284 :param status_change_type: type of status change
291 :param status_change_type: type of status change
285 :param closing_pr:
292 :param closing_pr:
286 :param send_email:
293 :param send_email:
287 :param renderer: pick renderer for this comment
294 :param renderer: pick renderer for this comment
288 :param auth_user: current authenticated user calling this method
295 :param auth_user: current authenticated user calling this method
289 :param extra_recipients: list of extra users to be added to recipients
296 :param extra_recipients: list of extra users to be added to recipients
290 """
297 """
291
298
292 if not text:
299 if not text:
293 log.warning('Missing text for comment, skipping...')
300 log.warning('Missing text for comment, skipping...')
294 return
301 return
295 request = get_current_request()
302 request = get_current_request()
296 _ = request.translate
303 _ = request.translate
297
304
298 if not renderer:
305 if not renderer:
299 renderer = self._get_renderer(request=request)
306 renderer = self._get_renderer(request=request)
300
307
301 repo = self._get_repo(repo)
308 repo = self._get_repo(repo)
302 user = self._get_user(user)
309 user = self._get_user(user)
303 auth_user = auth_user or user
310 auth_user = auth_user or user
304
311
305 schema = comment_schema.CommentSchema()
312 schema = comment_schema.CommentSchema()
306 validated_kwargs = schema.deserialize(dict(
313 validated_kwargs = schema.deserialize(dict(
307 comment_body=text,
314 comment_body=text,
308 comment_type=comment_type,
315 comment_type=comment_type,
309 is_draft=is_draft,
316 is_draft=is_draft,
310 comment_file=f_path,
317 comment_file=f_path,
311 comment_line=line_no,
318 comment_line=line_no,
312 renderer_type=renderer,
319 renderer_type=renderer,
313 status_change=status_change_type,
320 status_change=status_change_type,
314 resolves_comment_id=resolves_comment_id,
321 resolves_comment_id=resolves_comment_id,
315 repo=repo.repo_id,
322 repo=repo.repo_id,
316 user=user.user_id,
323 user=user.user_id,
317 ))
324 ))
318 is_draft = validated_kwargs['is_draft']
325 is_draft = validated_kwargs['is_draft']
319
326
320 comment = ChangesetComment()
327 comment = ChangesetComment()
321 comment.renderer = validated_kwargs['renderer_type']
328 comment.renderer = validated_kwargs['renderer_type']
322 comment.text = validated_kwargs['comment_body']
329 comment.text = validated_kwargs['comment_body']
323 comment.f_path = validated_kwargs['comment_file']
330 comment.f_path = validated_kwargs['comment_file']
324 comment.line_no = validated_kwargs['comment_line']
331 comment.line_no = validated_kwargs['comment_line']
325 comment.comment_type = validated_kwargs['comment_type']
332 comment.comment_type = validated_kwargs['comment_type']
326 comment.draft = is_draft
333 comment.draft = is_draft
327
334
328 comment.repo = repo
335 comment.repo = repo
329 comment.author = user
336 comment.author = user
330 resolved_comment = self.__get_commit_comment(
337 resolved_comment = self.__get_commit_comment(
331 validated_kwargs['resolves_comment_id'])
338 validated_kwargs['resolves_comment_id'])
332 # check if the comment actually belongs to this PR
339 # check if the comment actually belongs to this PR
333 if resolved_comment and resolved_comment.pull_request and \
340 if resolved_comment and resolved_comment.pull_request and \
334 resolved_comment.pull_request != pull_request:
341 resolved_comment.pull_request != pull_request:
335 log.warning('Comment tried to resolved unrelated todo comment: %s',
342 log.warning('Comment tried to resolved unrelated todo comment: %s',
336 resolved_comment)
343 resolved_comment)
337 # comment not bound to this pull request, forbid
344 # comment not bound to this pull request, forbid
338 resolved_comment = None
345 resolved_comment = None
339
346
340 elif resolved_comment and resolved_comment.repo and \
347 elif resolved_comment and resolved_comment.repo and \
341 resolved_comment.repo != repo:
348 resolved_comment.repo != repo:
342 log.warning('Comment tried to resolved unrelated todo comment: %s',
349 log.warning('Comment tried to resolved unrelated todo comment: %s',
343 resolved_comment)
350 resolved_comment)
344 # comment not bound to this repo, forbid
351 # comment not bound to this repo, forbid
345 resolved_comment = None
352 resolved_comment = None
346
353
347 comment.resolved_comment = resolved_comment
354 comment.resolved_comment = resolved_comment
348
355
349 pull_request_id = pull_request
356 pull_request_id = pull_request
350
357
351 commit_obj = None
358 commit_obj = None
352 pull_request_obj = None
359 pull_request_obj = None
353
360
354 if commit_id:
361 if commit_id:
355 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
362 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
356 # do a lookup, so we don't pass something bad here
363 # do a lookup, so we don't pass something bad here
357 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
364 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
358 comment.revision = commit_obj.raw_id
365 comment.revision = commit_obj.raw_id
359
366
360 elif pull_request_id:
367 elif pull_request_id:
361 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
368 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
362 pull_request_obj = self.__get_pull_request(pull_request_id)
369 pull_request_obj = self.__get_pull_request(pull_request_id)
363 comment.pull_request = pull_request_obj
370 comment.pull_request = pull_request_obj
364 else:
371 else:
365 raise Exception('Please specify commit or pull_request_id')
372 raise Exception('Please specify commit or pull_request_id')
366
373
367 Session().add(comment)
374 Session().add(comment)
368 Session().flush()
375 Session().flush()
369 kwargs = {
376 kwargs = {
370 'user': user,
377 'user': user,
371 'renderer_type': renderer,
378 'renderer_type': renderer,
372 'repo_name': repo.repo_name,
379 'repo_name': repo.repo_name,
373 'status_change': status_change,
380 'status_change': status_change,
374 'status_change_type': status_change_type,
381 'status_change_type': status_change_type,
375 'comment_body': text,
382 'comment_body': text,
376 'comment_file': f_path,
383 'comment_file': f_path,
377 'comment_line': line_no,
384 'comment_line': line_no,
378 'comment_type': comment_type or 'note',
385 'comment_type': comment_type or 'note',
379 'comment_id': comment.comment_id
386 'comment_id': comment.comment_id
380 }
387 }
381
388
382 if commit_obj:
389 if commit_obj:
383 recipients = ChangesetComment.get_users(
390 recipients = ChangesetComment.get_users(
384 revision=commit_obj.raw_id)
391 revision=commit_obj.raw_id)
385 # add commit author if it's in RhodeCode system
392 # add commit author if it's in RhodeCode system
386 cs_author = User.get_from_cs_author(commit_obj.author)
393 cs_author = User.get_from_cs_author(commit_obj.author)
387 if not cs_author:
394 if not cs_author:
388 # use repo owner if we cannot extract the author correctly
395 # use repo owner if we cannot extract the author correctly
389 cs_author = repo.user
396 cs_author = repo.user
390 recipients += [cs_author]
397 recipients += [cs_author]
391
398
392 commit_comment_url = self.get_url(comment, request=request)
399 commit_comment_url = self.get_url(comment, request=request)
393 commit_comment_reply_url = self.get_url(
400 commit_comment_reply_url = self.get_url(
394 comment, request=request,
401 comment, request=request,
395 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
402 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
396
403
397 target_repo_url = h.link_to(
404 target_repo_url = h.link_to(
398 repo.repo_name,
405 repo.repo_name,
399 h.route_url('repo_summary', repo_name=repo.repo_name))
406 h.route_url('repo_summary', repo_name=repo.repo_name))
400
407
401 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
408 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
402 commit_id=commit_id)
409 commit_id=commit_id)
403
410
404 # commit specifics
411 # commit specifics
405 kwargs.update({
412 kwargs.update({
406 'commit': commit_obj,
413 'commit': commit_obj,
407 'commit_message': commit_obj.message,
414 'commit_message': commit_obj.message,
408 'commit_target_repo_url': target_repo_url,
415 'commit_target_repo_url': target_repo_url,
409 'commit_comment_url': commit_comment_url,
416 'commit_comment_url': commit_comment_url,
410 'commit_comment_reply_url': commit_comment_reply_url,
417 'commit_comment_reply_url': commit_comment_reply_url,
411 'commit_url': commit_url,
418 'commit_url': commit_url,
412 'thread_ids': [commit_url, commit_comment_url],
419 'thread_ids': [commit_url, commit_comment_url],
413 })
420 })
414
421
415 elif pull_request_obj:
422 elif pull_request_obj:
416 # get the current participants of this pull request
423 # get the current participants of this pull request
417 recipients = ChangesetComment.get_users(
424 recipients = ChangesetComment.get_users(
418 pull_request_id=pull_request_obj.pull_request_id)
425 pull_request_id=pull_request_obj.pull_request_id)
419 # add pull request author
426 # add pull request author
420 recipients += [pull_request_obj.author]
427 recipients += [pull_request_obj.author]
421
428
422 # add the reviewers to notification
429 # add the reviewers to notification
423 recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()]
430 recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()]
424
431
425 pr_target_repo = pull_request_obj.target_repo
432 pr_target_repo = pull_request_obj.target_repo
426 pr_source_repo = pull_request_obj.source_repo
433 pr_source_repo = pull_request_obj.source_repo
427
434
428 pr_comment_url = self.get_url(comment, request=request)
435 pr_comment_url = self.get_url(comment, request=request)
429 pr_comment_reply_url = self.get_url(
436 pr_comment_reply_url = self.get_url(
430 comment, request=request,
437 comment, request=request,
431 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
438 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
432
439
433 pr_url = h.route_url(
440 pr_url = h.route_url(
434 'pullrequest_show',
441 'pullrequest_show',
435 repo_name=pr_target_repo.repo_name,
442 repo_name=pr_target_repo.repo_name,
436 pull_request_id=pull_request_obj.pull_request_id, )
443 pull_request_id=pull_request_obj.pull_request_id, )
437
444
438 # set some variables for email notification
445 # set some variables for email notification
439 pr_target_repo_url = h.route_url(
446 pr_target_repo_url = h.route_url(
440 'repo_summary', repo_name=pr_target_repo.repo_name)
447 'repo_summary', repo_name=pr_target_repo.repo_name)
441
448
442 pr_source_repo_url = h.route_url(
449 pr_source_repo_url = h.route_url(
443 'repo_summary', repo_name=pr_source_repo.repo_name)
450 'repo_summary', repo_name=pr_source_repo.repo_name)
444
451
445 # pull request specifics
452 # pull request specifics
446 kwargs.update({
453 kwargs.update({
447 'pull_request': pull_request_obj,
454 'pull_request': pull_request_obj,
448 'pr_id': pull_request_obj.pull_request_id,
455 'pr_id': pull_request_obj.pull_request_id,
449 'pull_request_url': pr_url,
456 'pull_request_url': pr_url,
450 'pull_request_target_repo': pr_target_repo,
457 'pull_request_target_repo': pr_target_repo,
451 'pull_request_target_repo_url': pr_target_repo_url,
458 'pull_request_target_repo_url': pr_target_repo_url,
452 'pull_request_source_repo': pr_source_repo,
459 'pull_request_source_repo': pr_source_repo,
453 'pull_request_source_repo_url': pr_source_repo_url,
460 'pull_request_source_repo_url': pr_source_repo_url,
454 'pr_comment_url': pr_comment_url,
461 'pr_comment_url': pr_comment_url,
455 'pr_comment_reply_url': pr_comment_reply_url,
462 'pr_comment_reply_url': pr_comment_reply_url,
456 'pr_closing': closing_pr,
463 'pr_closing': closing_pr,
457 'thread_ids': [pr_url, pr_comment_url],
464 'thread_ids': [pr_url, pr_comment_url],
458 })
465 })
459
466
460 if send_email:
467 if send_email:
461 recipients += [self._get_user(u) for u in (extra_recipients or [])]
468 recipients += [self._get_user(u) for u in (extra_recipients or [])]
462
469
463 mention_recipients = set(
470 mention_recipients = set(
464 self._extract_mentions(text)).difference(recipients)
471 self._extract_mentions(text)).difference(recipients)
465
472
466 # create notification objects, and emails
473 # create notification objects, and emails
467 NotificationModel().create(
474 NotificationModel().create(
468 created_by=user,
475 created_by=user,
469 notification_subject='', # Filled in based on the notification_type
476 notification_subject='', # Filled in based on the notification_type
470 notification_body='', # Filled in based on the notification_type
477 notification_body='', # Filled in based on the notification_type
471 notification_type=notification_type,
478 notification_type=notification_type,
472 recipients=recipients,
479 recipients=recipients,
473 mention_recipients=mention_recipients,
480 mention_recipients=mention_recipients,
474 email_kwargs=kwargs,
481 email_kwargs=kwargs,
475 )
482 )
476
483
477 Session().flush()
484 Session().flush()
478 if comment.pull_request:
485 if comment.pull_request:
479 action = 'repo.pull_request.comment.create'
486 action = 'repo.pull_request.comment.create'
480 else:
487 else:
481 action = 'repo.commit.comment.create'
488 action = 'repo.commit.comment.create'
482
489
483 if not is_draft:
490 if not is_draft:
484 comment_data = comment.get_api_data()
491 comment_data = comment.get_api_data()
485
492
486 self._log_audit_action(
493 self._log_audit_action(
487 action, {'data': comment_data}, auth_user, comment)
494 action, {'data': comment_data}, auth_user, comment)
488
495
489 return comment
496 return comment
490
497
491 def edit(self, comment_id, text, auth_user, version):
498 def edit(self, comment_id, text, auth_user, version):
492 """
499 """
493 Change existing comment for commit or pull request.
500 Change existing comment for commit or pull request.
494
501
495 :param comment_id:
502 :param comment_id:
496 :param text:
503 :param text:
497 :param auth_user: current authenticated user calling this method
504 :param auth_user: current authenticated user calling this method
498 :param version: last comment version
505 :param version: last comment version
499 """
506 """
500 if not text:
507 if not text:
501 log.warning('Missing text for comment, skipping...')
508 log.warning('Missing text for comment, skipping...')
502 return
509 return
503
510
504 comment = ChangesetComment.get(comment_id)
511 comment = ChangesetComment.get(comment_id)
505 old_comment_text = comment.text
512 old_comment_text = comment.text
506 comment.text = text
513 comment.text = text
507 comment.modified_at = datetime.datetime.now()
514 comment.modified_at = datetime.datetime.now()
508 version = safe_int(version)
515 version = safe_int(version)
509
516
510 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
517 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
511 # would return 3 here
518 # would return 3 here
512 comment_version = ChangesetCommentHistory.get_version(comment_id)
519 comment_version = ChangesetCommentHistory.get_version(comment_id)
513
520
514 if isinstance(version, (int, long)) and (comment_version - version) != 1:
521 if isinstance(version, (int, long)) and (comment_version - version) != 1:
515 log.warning(
522 log.warning(
516 'Version mismatch comment_version {} submitted {}, skipping'.format(
523 'Version mismatch comment_version {} submitted {}, skipping'.format(
517 comment_version-1, # -1 since note above
524 comment_version-1, # -1 since note above
518 version
525 version
519 )
526 )
520 )
527 )
521 raise CommentVersionMismatch()
528 raise CommentVersionMismatch()
522
529
523 comment_history = ChangesetCommentHistory()
530 comment_history = ChangesetCommentHistory()
524 comment_history.comment_id = comment_id
531 comment_history.comment_id = comment_id
525 comment_history.version = comment_version
532 comment_history.version = comment_version
526 comment_history.created_by_user_id = auth_user.user_id
533 comment_history.created_by_user_id = auth_user.user_id
527 comment_history.text = old_comment_text
534 comment_history.text = old_comment_text
528 # TODO add email notification
535 # TODO add email notification
529 Session().add(comment_history)
536 Session().add(comment_history)
530 Session().add(comment)
537 Session().add(comment)
531 Session().flush()
538 Session().flush()
532
539
533 if comment.pull_request:
540 if comment.pull_request:
534 action = 'repo.pull_request.comment.edit'
541 action = 'repo.pull_request.comment.edit'
535 else:
542 else:
536 action = 'repo.commit.comment.edit'
543 action = 'repo.commit.comment.edit'
537
544
538 comment_data = comment.get_api_data()
545 comment_data = comment.get_api_data()
539 comment_data['old_comment_text'] = old_comment_text
546 comment_data['old_comment_text'] = old_comment_text
540 self._log_audit_action(
547 self._log_audit_action(
541 action, {'data': comment_data}, auth_user, comment)
548 action, {'data': comment_data}, auth_user, comment)
542
549
543 return comment_history
550 return comment_history
544
551
545 def delete(self, comment, auth_user):
552 def delete(self, comment, auth_user):
546 """
553 """
547 Deletes given comment
554 Deletes given comment
548 """
555 """
549 comment = self.__get_commit_comment(comment)
556 comment = self.__get_commit_comment(comment)
550 old_data = comment.get_api_data()
557 old_data = comment.get_api_data()
551 Session().delete(comment)
558 Session().delete(comment)
552
559
553 if comment.pull_request:
560 if comment.pull_request:
554 action = 'repo.pull_request.comment.delete'
561 action = 'repo.pull_request.comment.delete'
555 else:
562 else:
556 action = 'repo.commit.comment.delete'
563 action = 'repo.commit.comment.delete'
557
564
558 self._log_audit_action(
565 self._log_audit_action(
559 action, {'old_data': old_data}, auth_user, comment)
566 action, {'old_data': old_data}, auth_user, comment)
560
567
561 return comment
568 return comment
562
569
563 def get_all_comments(self, repo_id, revision=None, pull_request=None,
570 def get_all_comments(self, repo_id, revision=None, pull_request=None,
564 include_drafts=True, count_only=False):
571 include_drafts=True, count_only=False):
565 q = ChangesetComment.query()\
572 q = ChangesetComment.query()\
566 .filter(ChangesetComment.repo_id == repo_id)
573 .filter(ChangesetComment.repo_id == repo_id)
567 if revision:
574 if revision:
568 q = q.filter(ChangesetComment.revision == revision)
575 q = q.filter(ChangesetComment.revision == revision)
569 elif pull_request:
576 elif pull_request:
570 pull_request = self.__get_pull_request(pull_request)
577 pull_request = self.__get_pull_request(pull_request)
571 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
578 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
572 else:
579 else:
573 raise Exception('Please specify commit or pull_request')
580 raise Exception('Please specify commit or pull_request')
574 if not include_drafts:
581 if not include_drafts:
575 q = q.filter(ChangesetComment.draft == false())
582 q = q.filter(ChangesetComment.draft == false())
576 q = q.order_by(ChangesetComment.created_on)
583 q = q.order_by(ChangesetComment.created_on)
577 if count_only:
584 if count_only:
578 return q.count()
585 return q.count()
579
586
580 return q.all()
587 return q.all()
581
588
582 def get_url(self, comment, request=None, permalink=False, anchor=None):
589 def get_url(self, comment, request=None, permalink=False, anchor=None):
583 if not request:
590 if not request:
584 request = get_current_request()
591 request = get_current_request()
585
592
586 comment = self.__get_commit_comment(comment)
593 comment = self.__get_commit_comment(comment)
587 if anchor is None:
594 if anchor is None:
588 anchor = 'comment-{}'.format(comment.comment_id)
595 anchor = 'comment-{}'.format(comment.comment_id)
589
596
590 if comment.pull_request:
597 if comment.pull_request:
591 pull_request = comment.pull_request
598 pull_request = comment.pull_request
592 if permalink:
599 if permalink:
593 return request.route_url(
600 return request.route_url(
594 'pull_requests_global',
601 'pull_requests_global',
595 pull_request_id=pull_request.pull_request_id,
602 pull_request_id=pull_request.pull_request_id,
596 _anchor=anchor)
603 _anchor=anchor)
597 else:
604 else:
598 return request.route_url(
605 return request.route_url(
599 'pullrequest_show',
606 'pullrequest_show',
600 repo_name=safe_str(pull_request.target_repo.repo_name),
607 repo_name=safe_str(pull_request.target_repo.repo_name),
601 pull_request_id=pull_request.pull_request_id,
608 pull_request_id=pull_request.pull_request_id,
602 _anchor=anchor)
609 _anchor=anchor)
603
610
604 else:
611 else:
605 repo = comment.repo
612 repo = comment.repo
606 commit_id = comment.revision
613 commit_id = comment.revision
607
614
608 if permalink:
615 if permalink:
609 return request.route_url(
616 return request.route_url(
610 'repo_commit', repo_name=safe_str(repo.repo_id),
617 'repo_commit', repo_name=safe_str(repo.repo_id),
611 commit_id=commit_id,
618 commit_id=commit_id,
612 _anchor=anchor)
619 _anchor=anchor)
613
620
614 else:
621 else:
615 return request.route_url(
622 return request.route_url(
616 'repo_commit', repo_name=safe_str(repo.repo_name),
623 'repo_commit', repo_name=safe_str(repo.repo_name),
617 commit_id=commit_id,
624 commit_id=commit_id,
618 _anchor=anchor)
625 _anchor=anchor)
619
626
620 def get_comments(self, repo_id, revision=None, pull_request=None):
627 def get_comments(self, repo_id, revision=None, pull_request=None):
621 """
628 """
622 Gets main comments based on revision or pull_request_id
629 Gets main comments based on revision or pull_request_id
623
630
624 :param repo_id:
631 :param repo_id:
625 :param revision:
632 :param revision:
626 :param pull_request:
633 :param pull_request:
627 """
634 """
628
635
629 q = ChangesetComment.query()\
636 q = ChangesetComment.query()\
630 .filter(ChangesetComment.repo_id == repo_id)\
637 .filter(ChangesetComment.repo_id == repo_id)\
631 .filter(ChangesetComment.line_no == None)\
638 .filter(ChangesetComment.line_no == None)\
632 .filter(ChangesetComment.f_path == None)
639 .filter(ChangesetComment.f_path == None)
633 if revision:
640 if revision:
634 q = q.filter(ChangesetComment.revision == revision)
641 q = q.filter(ChangesetComment.revision == revision)
635 elif pull_request:
642 elif pull_request:
636 pull_request = self.__get_pull_request(pull_request)
643 pull_request = self.__get_pull_request(pull_request)
637 q = q.filter(ChangesetComment.pull_request == pull_request)
644 q = q.filter(ChangesetComment.pull_request == pull_request)
638 else:
645 else:
639 raise Exception('Please specify commit or pull_request')
646 raise Exception('Please specify commit or pull_request')
640 q = q.order_by(ChangesetComment.created_on)
647 q = q.order_by(ChangesetComment.created_on)
641 return q.all()
648 return q.all()
642
649
643 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
650 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
644 q = self._get_inline_comments_query(repo_id, revision, pull_request)
651 q = self._get_inline_comments_query(repo_id, revision, pull_request)
645 return self._group_comments_by_path_and_line_number(q)
652 return self._group_comments_by_path_and_line_number(q)
646
653
647 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
654 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
648 version=None):
655 version=None):
649 inline_comms = []
656 inline_comms = []
650 for fname, per_line_comments in inline_comments.iteritems():
657 for fname, per_line_comments in inline_comments.iteritems():
651 for lno, comments in per_line_comments.iteritems():
658 for lno, comments in per_line_comments.iteritems():
652 for comm in comments:
659 for comm in comments:
653 if not comm.outdated_at_version(version) and skip_outdated:
660 if not comm.outdated_at_version(version) and skip_outdated:
654 inline_comms.append(comm)
661 inline_comms.append(comm)
655
662
656 return inline_comms
663 return inline_comms
657
664
658 def get_outdated_comments(self, repo_id, pull_request):
665 def get_outdated_comments(self, repo_id, pull_request):
659 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
666 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
660 # of a pull request.
667 # of a pull request.
661 q = self._all_inline_comments_of_pull_request(pull_request)
668 q = self._all_inline_comments_of_pull_request(pull_request)
662 q = q.filter(
669 q = q.filter(
663 ChangesetComment.display_state ==
670 ChangesetComment.display_state ==
664 ChangesetComment.COMMENT_OUTDATED
671 ChangesetComment.COMMENT_OUTDATED
665 ).order_by(ChangesetComment.comment_id.asc())
672 ).order_by(ChangesetComment.comment_id.asc())
666
673
667 return self._group_comments_by_path_and_line_number(q)
674 return self._group_comments_by_path_and_line_number(q)
668
675
669 def _get_inline_comments_query(self, repo_id, revision, pull_request):
676 def _get_inline_comments_query(self, repo_id, revision, pull_request):
670 # TODO: johbo: Split this into two methods: One for PR and one for
677 # TODO: johbo: Split this into two methods: One for PR and one for
671 # commit.
678 # commit.
672 if revision:
679 if revision:
673 q = Session().query(ChangesetComment).filter(
680 q = Session().query(ChangesetComment).filter(
674 ChangesetComment.repo_id == repo_id,
681 ChangesetComment.repo_id == repo_id,
675 ChangesetComment.line_no != null(),
682 ChangesetComment.line_no != null(),
676 ChangesetComment.f_path != null(),
683 ChangesetComment.f_path != null(),
677 ChangesetComment.revision == revision)
684 ChangesetComment.revision == revision)
678
685
679 elif pull_request:
686 elif pull_request:
680 pull_request = self.__get_pull_request(pull_request)
687 pull_request = self.__get_pull_request(pull_request)
681 if not CommentsModel.use_outdated_comments(pull_request):
688 if not CommentsModel.use_outdated_comments(pull_request):
682 q = self._visible_inline_comments_of_pull_request(pull_request)
689 q = self._visible_inline_comments_of_pull_request(pull_request)
683 else:
690 else:
684 q = self._all_inline_comments_of_pull_request(pull_request)
691 q = self._all_inline_comments_of_pull_request(pull_request)
685
692
686 else:
693 else:
687 raise Exception('Please specify commit or pull_request_id')
694 raise Exception('Please specify commit or pull_request_id')
688 q = q.order_by(ChangesetComment.comment_id.asc())
695 q = q.order_by(ChangesetComment.comment_id.asc())
689 return q
696 return q
690
697
691 def _group_comments_by_path_and_line_number(self, q):
698 def _group_comments_by_path_and_line_number(self, q):
692 comments = q.all()
699 comments = q.all()
693 paths = collections.defaultdict(lambda: collections.defaultdict(list))
700 paths = collections.defaultdict(lambda: collections.defaultdict(list))
694 for co in comments:
701 for co in comments:
695 paths[co.f_path][co.line_no].append(co)
702 paths[co.f_path][co.line_no].append(co)
696 return paths
703 return paths
697
704
698 @classmethod
705 @classmethod
699 def needed_extra_diff_context(cls):
706 def needed_extra_diff_context(cls):
700 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
707 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
701
708
702 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
709 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
703 if not CommentsModel.use_outdated_comments(pull_request):
710 if not CommentsModel.use_outdated_comments(pull_request):
704 return
711 return
705
712
706 comments = self._visible_inline_comments_of_pull_request(pull_request)
713 comments = self._visible_inline_comments_of_pull_request(pull_request)
707 comments_to_outdate = comments.all()
714 comments_to_outdate = comments.all()
708
715
709 for comment in comments_to_outdate:
716 for comment in comments_to_outdate:
710 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
717 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
711
718
712 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
719 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
713 diff_line = _parse_comment_line_number(comment.line_no)
720 diff_line = _parse_comment_line_number(comment.line_no)
714
721
715 try:
722 try:
716 old_context = old_diff_proc.get_context_of_line(
723 old_context = old_diff_proc.get_context_of_line(
717 path=comment.f_path, diff_line=diff_line)
724 path=comment.f_path, diff_line=diff_line)
718 new_context = new_diff_proc.get_context_of_line(
725 new_context = new_diff_proc.get_context_of_line(
719 path=comment.f_path, diff_line=diff_line)
726 path=comment.f_path, diff_line=diff_line)
720 except (diffs.LineNotInDiffException,
727 except (diffs.LineNotInDiffException,
721 diffs.FileNotInDiffException):
728 diffs.FileNotInDiffException):
722 if not comment.draft:
729 if not comment.draft:
723 comment.display_state = ChangesetComment.COMMENT_OUTDATED
730 comment.display_state = ChangesetComment.COMMENT_OUTDATED
724 return
731 return
725
732
726 if old_context == new_context:
733 if old_context == new_context:
727 return
734 return
728
735
729 if self._should_relocate_diff_line(diff_line):
736 if self._should_relocate_diff_line(diff_line):
730 new_diff_lines = new_diff_proc.find_context(
737 new_diff_lines = new_diff_proc.find_context(
731 path=comment.f_path, context=old_context,
738 path=comment.f_path, context=old_context,
732 offset=self.DIFF_CONTEXT_BEFORE)
739 offset=self.DIFF_CONTEXT_BEFORE)
733 if not new_diff_lines and not comment.draft:
740 if not new_diff_lines and not comment.draft:
734 comment.display_state = ChangesetComment.COMMENT_OUTDATED
741 comment.display_state = ChangesetComment.COMMENT_OUTDATED
735 else:
742 else:
736 new_diff_line = self._choose_closest_diff_line(
743 new_diff_line = self._choose_closest_diff_line(
737 diff_line, new_diff_lines)
744 diff_line, new_diff_lines)
738 comment.line_no = _diff_to_comment_line_number(new_diff_line)
745 comment.line_no = _diff_to_comment_line_number(new_diff_line)
739 else:
746 else:
740 if not comment.draft:
747 if not comment.draft:
741 comment.display_state = ChangesetComment.COMMENT_OUTDATED
748 comment.display_state = ChangesetComment.COMMENT_OUTDATED
742
749
743 def _should_relocate_diff_line(self, diff_line):
750 def _should_relocate_diff_line(self, diff_line):
744 """
751 """
745 Checks if relocation shall be tried for the given `diff_line`.
752 Checks if relocation shall be tried for the given `diff_line`.
746
753
747 If a comment points into the first lines, then we can have a situation
754 If a comment points into the first lines, then we can have a situation
748 that after an update another line has been added on top. In this case
755 that after an update another line has been added on top. In this case
749 we would find the context still and move the comment around. This
756 we would find the context still and move the comment around. This
750 would be wrong.
757 would be wrong.
751 """
758 """
752 should_relocate = (
759 should_relocate = (
753 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
760 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
754 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
761 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
755 return should_relocate
762 return should_relocate
756
763
757 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
764 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
758 candidate = new_diff_lines[0]
765 candidate = new_diff_lines[0]
759 best_delta = _diff_line_delta(diff_line, candidate)
766 best_delta = _diff_line_delta(diff_line, candidate)
760 for new_diff_line in new_diff_lines[1:]:
767 for new_diff_line in new_diff_lines[1:]:
761 delta = _diff_line_delta(diff_line, new_diff_line)
768 delta = _diff_line_delta(diff_line, new_diff_line)
762 if delta < best_delta:
769 if delta < best_delta:
763 candidate = new_diff_line
770 candidate = new_diff_line
764 best_delta = delta
771 best_delta = delta
765 return candidate
772 return candidate
766
773
767 def _visible_inline_comments_of_pull_request(self, pull_request):
774 def _visible_inline_comments_of_pull_request(self, pull_request):
768 comments = self._all_inline_comments_of_pull_request(pull_request)
775 comments = self._all_inline_comments_of_pull_request(pull_request)
769 comments = comments.filter(
776 comments = comments.filter(
770 coalesce(ChangesetComment.display_state, '') !=
777 coalesce(ChangesetComment.display_state, '') !=
771 ChangesetComment.COMMENT_OUTDATED)
778 ChangesetComment.COMMENT_OUTDATED)
772 return comments
779 return comments
773
780
774 def _all_inline_comments_of_pull_request(self, pull_request):
781 def _all_inline_comments_of_pull_request(self, pull_request):
775 comments = Session().query(ChangesetComment)\
782 comments = Session().query(ChangesetComment)\
776 .filter(ChangesetComment.line_no != None)\
783 .filter(ChangesetComment.line_no != None)\
777 .filter(ChangesetComment.f_path != None)\
784 .filter(ChangesetComment.f_path != None)\
778 .filter(ChangesetComment.pull_request == pull_request)
785 .filter(ChangesetComment.pull_request == pull_request)
779 return comments
786 return comments
780
787
781 def _all_general_comments_of_pull_request(self, pull_request):
788 def _all_general_comments_of_pull_request(self, pull_request):
782 comments = Session().query(ChangesetComment)\
789 comments = Session().query(ChangesetComment)\
783 .filter(ChangesetComment.line_no == None)\
790 .filter(ChangesetComment.line_no == None)\
784 .filter(ChangesetComment.f_path == None)\
791 .filter(ChangesetComment.f_path == None)\
785 .filter(ChangesetComment.pull_request == pull_request)
792 .filter(ChangesetComment.pull_request == pull_request)
786
793
787 return comments
794 return comments
788
795
789 @staticmethod
796 @staticmethod
790 def use_outdated_comments(pull_request):
797 def use_outdated_comments(pull_request):
791 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
798 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
792 settings = settings_model.get_general_settings()
799 settings = settings_model.get_general_settings()
793 return settings.get('rhodecode_use_outdated_comments', False)
800 return settings.get('rhodecode_use_outdated_comments', False)
794
801
795 def trigger_commit_comment_hook(self, repo, user, action, data=None):
802 def trigger_commit_comment_hook(self, repo, user, action, data=None):
796 repo = self._get_repo(repo)
803 repo = self._get_repo(repo)
797 target_scm = repo.scm_instance()
804 target_scm = repo.scm_instance()
798 if action == 'create':
805 if action == 'create':
799 trigger_hook = hooks_utils.trigger_comment_commit_hooks
806 trigger_hook = hooks_utils.trigger_comment_commit_hooks
800 elif action == 'edit':
807 elif action == 'edit':
801 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
808 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
802 else:
809 else:
803 return
810 return
804
811
805 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
812 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
806 repo, action, trigger_hook)
813 repo, action, trigger_hook)
807 trigger_hook(
814 trigger_hook(
808 username=user.username,
815 username=user.username,
809 repo_name=repo.repo_name,
816 repo_name=repo.repo_name,
810 repo_type=target_scm.alias,
817 repo_type=target_scm.alias,
811 repo=repo,
818 repo=repo,
812 data=data)
819 data=data)
813
820
814
821
815 def _parse_comment_line_number(line_no):
822 def _parse_comment_line_number(line_no):
816 """
823 """
817 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
824 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
818 """
825 """
819 old_line = None
826 old_line = None
820 new_line = None
827 new_line = None
821 if line_no.startswith('o'):
828 if line_no.startswith('o'):
822 old_line = int(line_no[1:])
829 old_line = int(line_no[1:])
823 elif line_no.startswith('n'):
830 elif line_no.startswith('n'):
824 new_line = int(line_no[1:])
831 new_line = int(line_no[1:])
825 else:
832 else:
826 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
833 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
827 return diffs.DiffLineNumber(old_line, new_line)
834 return diffs.DiffLineNumber(old_line, new_line)
828
835
829
836
830 def _diff_to_comment_line_number(diff_line):
837 def _diff_to_comment_line_number(diff_line):
831 if diff_line.new is not None:
838 if diff_line.new is not None:
832 return u'n{}'.format(diff_line.new)
839 return u'n{}'.format(diff_line.new)
833 elif diff_line.old is not None:
840 elif diff_line.old is not None:
834 return u'o{}'.format(diff_line.old)
841 return u'o{}'.format(diff_line.old)
835 return u''
842 return u''
836
843
837
844
838 def _diff_line_delta(a, b):
845 def _diff_line_delta(a, b):
839 if None not in (a.new, b.new):
846 if None not in (a.new, b.new):
840 return abs(a.new - b.new)
847 return abs(a.new - b.new)
841 elif None not in (a.old, b.old):
848 elif None not in (a.old, b.old):
842 return abs(a.old - b.old)
849 return abs(a.old - b.old)
843 else:
850 else:
844 raise ValueError(
851 raise ValueError(
845 "Cannot compute delta between {} and {}".format(a, b))
852 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,404 +1,405 b''
1
1
2 /******************************************************************************
2 /******************************************************************************
3 * *
3 * *
4 * DO NOT CHANGE THIS FILE MANUALLY *
4 * DO NOT CHANGE THIS FILE MANUALLY *
5 * *
5 * *
6 * *
6 * *
7 * This file is automatically generated when the app starts up with *
7 * This file is automatically generated when the app starts up with *
8 * generate_js_files = true *
8 * generate_js_files = true *
9 * *
9 * *
10 * To add a route here pass jsroute=True to the route definition in the app *
10 * To add a route here pass jsroute=True to the route definition in the app *
11 * *
11 * *
12 ******************************************************************************/
12 ******************************************************************************/
13 function registerRCRoutes() {
13 function registerRCRoutes() {
14 // routes registration
14 // routes registration
15 pyroutes.register('favicon', '/favicon.ico', []);
15 pyroutes.register('favicon', '/favicon.ico', []);
16 pyroutes.register('robots', '/robots.txt', []);
16 pyroutes.register('robots', '/robots.txt', []);
17 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
17 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
18 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
18 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
19 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
19 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
20 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
20 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
21 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
21 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
22 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
22 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
23 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/_settings/integrations', ['repo_group_name']);
23 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/_settings/integrations', ['repo_group_name']);
24 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
24 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
25 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
25 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
26 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/_settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
26 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/_settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
27 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/_settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
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 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
28 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
29 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
29 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
30 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
30 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
31 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
31 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
32 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
32 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
33 pyroutes.register('hovercard_user', '/_hovercard/user/%(user_id)s', ['user_id']);
33 pyroutes.register('hovercard_user', '/_hovercard/user/%(user_id)s', ['user_id']);
34 pyroutes.register('hovercard_username', '/_hovercard/username/%(username)s', ['username']);
34 pyroutes.register('hovercard_username', '/_hovercard/username/%(username)s', ['username']);
35 pyroutes.register('hovercard_user_group', '/_hovercard/user_group/%(user_group_id)s', ['user_group_id']);
35 pyroutes.register('hovercard_user_group', '/_hovercard/user_group/%(user_group_id)s', ['user_group_id']);
36 pyroutes.register('hovercard_pull_request', '/_hovercard/pull_request/%(pull_request_id)s', ['pull_request_id']);
36 pyroutes.register('hovercard_pull_request', '/_hovercard/pull_request/%(pull_request_id)s', ['pull_request_id']);
37 pyroutes.register('hovercard_repo_commit', '/_hovercard/commit/%(repo_name)s/%(commit_id)s', ['repo_name', 'commit_id']);
37 pyroutes.register('hovercard_repo_commit', '/_hovercard/commit/%(repo_name)s/%(commit_id)s', ['repo_name', 'commit_id']);
38 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
38 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
39 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
39 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
40 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
40 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
41 pyroutes.register('ops_ping_legacy', '/_admin/ping', []);
41 pyroutes.register('ops_ping_legacy', '/_admin/ping', []);
42 pyroutes.register('ops_error_test_legacy', '/_admin/error_test', []);
42 pyroutes.register('ops_error_test_legacy', '/_admin/error_test', []);
43 pyroutes.register('admin_home', '/_admin', []);
43 pyroutes.register('admin_home', '/_admin', []);
44 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
44 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
45 pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
45 pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
46 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
46 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
47 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
47 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
48 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
48 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
49 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
49 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
50 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
50 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
51 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
51 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
52 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
52 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
53 pyroutes.register('admin_settings_exception_tracker', '/_admin/settings/exceptions', []);
53 pyroutes.register('admin_settings_exception_tracker', '/_admin/settings/exceptions', []);
54 pyroutes.register('admin_settings_exception_tracker_delete_all', '/_admin/settings/exceptions/delete', []);
54 pyroutes.register('admin_settings_exception_tracker_delete_all', '/_admin/settings/exceptions/delete', []);
55 pyroutes.register('admin_settings_exception_tracker_show', '/_admin/settings/exceptions/%(exception_id)s', ['exception_id']);
55 pyroutes.register('admin_settings_exception_tracker_show', '/_admin/settings/exceptions/%(exception_id)s', ['exception_id']);
56 pyroutes.register('admin_settings_exception_tracker_delete', '/_admin/settings/exceptions/%(exception_id)s/delete', ['exception_id']);
56 pyroutes.register('admin_settings_exception_tracker_delete', '/_admin/settings/exceptions/%(exception_id)s/delete', ['exception_id']);
57 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
57 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
58 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
58 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
59 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
59 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
60 pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []);
60 pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []);
61 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
61 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
62 pyroutes.register('admin_settings_process_management_master_signal', '/_admin/settings/process_management/master_signal', []);
62 pyroutes.register('admin_settings_process_management_master_signal', '/_admin/settings/process_management/master_signal', []);
63 pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);
63 pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);
64 pyroutes.register('admin_defaults_repositories_update', '/_admin/defaults/repositories/update', []);
64 pyroutes.register('admin_defaults_repositories_update', '/_admin/defaults/repositories/update', []);
65 pyroutes.register('admin_settings', '/_admin/settings', []);
65 pyroutes.register('admin_settings', '/_admin/settings', []);
66 pyroutes.register('admin_settings_update', '/_admin/settings/update', []);
66 pyroutes.register('admin_settings_update', '/_admin/settings/update', []);
67 pyroutes.register('admin_settings_global', '/_admin/settings/global', []);
67 pyroutes.register('admin_settings_global', '/_admin/settings/global', []);
68 pyroutes.register('admin_settings_global_update', '/_admin/settings/global/update', []);
68 pyroutes.register('admin_settings_global_update', '/_admin/settings/global/update', []);
69 pyroutes.register('admin_settings_vcs', '/_admin/settings/vcs', []);
69 pyroutes.register('admin_settings_vcs', '/_admin/settings/vcs', []);
70 pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []);
70 pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []);
71 pyroutes.register('admin_settings_vcs_svn_pattern_delete', '/_admin/settings/vcs/svn_pattern_delete', []);
71 pyroutes.register('admin_settings_vcs_svn_pattern_delete', '/_admin/settings/vcs/svn_pattern_delete', []);
72 pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []);
72 pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []);
73 pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []);
73 pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []);
74 pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []);
74 pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []);
75 pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []);
75 pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []);
76 pyroutes.register('admin_settings_issuetracker', '/_admin/settings/issue-tracker', []);
76 pyroutes.register('admin_settings_issuetracker', '/_admin/settings/issue-tracker', []);
77 pyroutes.register('admin_settings_issuetracker_update', '/_admin/settings/issue-tracker/update', []);
77 pyroutes.register('admin_settings_issuetracker_update', '/_admin/settings/issue-tracker/update', []);
78 pyroutes.register('admin_settings_issuetracker_test', '/_admin/settings/issue-tracker/test', []);
78 pyroutes.register('admin_settings_issuetracker_test', '/_admin/settings/issue-tracker/test', []);
79 pyroutes.register('admin_settings_issuetracker_delete', '/_admin/settings/issue-tracker/delete', []);
79 pyroutes.register('admin_settings_issuetracker_delete', '/_admin/settings/issue-tracker/delete', []);
80 pyroutes.register('admin_settings_email', '/_admin/settings/email', []);
80 pyroutes.register('admin_settings_email', '/_admin/settings/email', []);
81 pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []);
81 pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []);
82 pyroutes.register('admin_settings_hooks', '/_admin/settings/hooks', []);
82 pyroutes.register('admin_settings_hooks', '/_admin/settings/hooks', []);
83 pyroutes.register('admin_settings_hooks_update', '/_admin/settings/hooks/update', []);
83 pyroutes.register('admin_settings_hooks_update', '/_admin/settings/hooks/update', []);
84 pyroutes.register('admin_settings_hooks_delete', '/_admin/settings/hooks/delete', []);
84 pyroutes.register('admin_settings_hooks_delete', '/_admin/settings/hooks/delete', []);
85 pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
85 pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
86 pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
86 pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
87 pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
87 pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
88 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
88 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
89 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
89 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
90 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
90 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
91 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
91 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
92 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
92 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
93 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
93 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
94 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
94 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
95 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
95 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
96 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
96 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
97 pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
97 pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
98 pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
98 pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
99 pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
99 pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
100 pyroutes.register('users', '/_admin/users', []);
100 pyroutes.register('users', '/_admin/users', []);
101 pyroutes.register('users_data', '/_admin/users_data', []);
101 pyroutes.register('users_data', '/_admin/users_data', []);
102 pyroutes.register('users_create', '/_admin/users/create', []);
102 pyroutes.register('users_create', '/_admin/users/create', []);
103 pyroutes.register('users_new', '/_admin/users/new', []);
103 pyroutes.register('users_new', '/_admin/users/new', []);
104 pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']);
104 pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']);
105 pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']);
105 pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']);
106 pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']);
106 pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']);
107 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
107 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
108 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
108 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
109 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
109 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
110 pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
110 pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
111 pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
111 pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
112 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
112 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
113 pyroutes.register('user_notice_dismiss', '/_admin/users/%(user_id)s/notice_dismiss', ['user_id']);
113 pyroutes.register('user_notice_dismiss', '/_admin/users/%(user_id)s/notice_dismiss', ['user_id']);
114 pyroutes.register('edit_user_auth_tokens_view', '/_admin/users/%(user_id)s/edit/auth_tokens/view', ['user_id']);
114 pyroutes.register('edit_user_auth_tokens_view', '/_admin/users/%(user_id)s/edit/auth_tokens/view', ['user_id']);
115 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
115 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
116 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
116 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
117 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
117 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
118 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
118 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
119 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
119 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
120 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
120 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
121 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
121 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
122 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
122 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
123 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
123 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
124 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
124 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
125 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
125 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
126 pyroutes.register('edit_user_perms_summary', '/_admin/users/%(user_id)s/edit/permissions_summary', ['user_id']);
126 pyroutes.register('edit_user_perms_summary', '/_admin/users/%(user_id)s/edit/permissions_summary', ['user_id']);
127 pyroutes.register('edit_user_perms_summary_json', '/_admin/users/%(user_id)s/edit/permissions_summary/json', ['user_id']);
127 pyroutes.register('edit_user_perms_summary_json', '/_admin/users/%(user_id)s/edit/permissions_summary/json', ['user_id']);
128 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
128 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
129 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
129 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
130 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
130 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
131 pyroutes.register('edit_user_audit_logs_download', '/_admin/users/%(user_id)s/edit/audit/download', ['user_id']);
131 pyroutes.register('edit_user_audit_logs_download', '/_admin/users/%(user_id)s/edit/audit/download', ['user_id']);
132 pyroutes.register('edit_user_caches', '/_admin/users/%(user_id)s/edit/caches', ['user_id']);
132 pyroutes.register('edit_user_caches', '/_admin/users/%(user_id)s/edit/caches', ['user_id']);
133 pyroutes.register('edit_user_caches_update', '/_admin/users/%(user_id)s/edit/caches/update', ['user_id']);
133 pyroutes.register('edit_user_caches_update', '/_admin/users/%(user_id)s/edit/caches/update', ['user_id']);
134 pyroutes.register('user_groups', '/_admin/user_groups', []);
134 pyroutes.register('user_groups', '/_admin/user_groups', []);
135 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
135 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
136 pyroutes.register('user_groups_new', '/_admin/user_groups/new', []);
136 pyroutes.register('user_groups_new', '/_admin/user_groups/new', []);
137 pyroutes.register('user_groups_create', '/_admin/user_groups/create', []);
137 pyroutes.register('user_groups_create', '/_admin/user_groups/create', []);
138 pyroutes.register('repos', '/_admin/repos', []);
138 pyroutes.register('repos', '/_admin/repos', []);
139 pyroutes.register('repos_data', '/_admin/repos_data', []);
139 pyroutes.register('repos_data', '/_admin/repos_data', []);
140 pyroutes.register('repo_new', '/_admin/repos/new', []);
140 pyroutes.register('repo_new', '/_admin/repos/new', []);
141 pyroutes.register('repo_create', '/_admin/repos/create', []);
141 pyroutes.register('repo_create', '/_admin/repos/create', []);
142 pyroutes.register('repo_groups', '/_admin/repo_groups', []);
142 pyroutes.register('repo_groups', '/_admin/repo_groups', []);
143 pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []);
143 pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []);
144 pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
144 pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
145 pyroutes.register('repo_group_create', '/_admin/repo_group/create', []);
145 pyroutes.register('repo_group_create', '/_admin/repo_group/create', []);
146 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
146 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
147 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
147 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
148 pyroutes.register('channelstream_proxy', '/_channelstream', []);
148 pyroutes.register('channelstream_proxy', '/_channelstream', []);
149 pyroutes.register('upload_file', '/_file_store/upload', []);
149 pyroutes.register('upload_file', '/_file_store/upload', []);
150 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
150 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
151 pyroutes.register('download_file_by_token', '/_file_store/token-download/%(_auth_token)s/%(fid)s', ['_auth_token', 'fid']);
151 pyroutes.register('download_file_by_token', '/_file_store/token-download/%(_auth_token)s/%(fid)s', ['_auth_token', 'fid']);
152 pyroutes.register('logout', '/_admin/logout', []);
152 pyroutes.register('logout', '/_admin/logout', []);
153 pyroutes.register('reset_password', '/_admin/password_reset', []);
153 pyroutes.register('reset_password', '/_admin/password_reset', []);
154 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
154 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
155 pyroutes.register('home', '/', []);
155 pyroutes.register('home', '/', []);
156 pyroutes.register('main_page_repos_data', '/_home_repos', []);
156 pyroutes.register('main_page_repos_data', '/_home_repos', []);
157 pyroutes.register('main_page_repo_groups_data', '/_home_repo_groups', []);
157 pyroutes.register('main_page_repo_groups_data', '/_home_repo_groups', []);
158 pyroutes.register('user_autocomplete_data', '/_users', []);
158 pyroutes.register('user_autocomplete_data', '/_users', []);
159 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
159 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
160 pyroutes.register('repo_list_data', '/_repos', []);
160 pyroutes.register('repo_list_data', '/_repos', []);
161 pyroutes.register('repo_group_list_data', '/_repo_groups', []);
161 pyroutes.register('repo_group_list_data', '/_repo_groups', []);
162 pyroutes.register('goto_switcher_data', '/_goto_data', []);
162 pyroutes.register('goto_switcher_data', '/_goto_data', []);
163 pyroutes.register('markup_preview', '/_markup_preview', []);
163 pyroutes.register('markup_preview', '/_markup_preview', []);
164 pyroutes.register('file_preview', '/_file_preview', []);
164 pyroutes.register('file_preview', '/_file_preview', []);
165 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
165 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
166 pyroutes.register('journal', '/_admin/journal', []);
166 pyroutes.register('journal', '/_admin/journal', []);
167 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
167 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
168 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
168 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
169 pyroutes.register('journal_public', '/_admin/public_journal', []);
169 pyroutes.register('journal_public', '/_admin/public_journal', []);
170 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
170 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
171 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
171 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
172 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
172 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
173 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
173 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
174 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
174 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
175 pyroutes.register('repo_creating', '/%(repo_name)s/repo_creating', ['repo_name']);
175 pyroutes.register('repo_creating', '/%(repo_name)s/repo_creating', ['repo_name']);
176 pyroutes.register('repo_creating_check', '/%(repo_name)s/repo_creating_check', ['repo_name']);
176 pyroutes.register('repo_creating_check', '/%(repo_name)s/repo_creating_check', ['repo_name']);
177 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
177 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
178 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
178 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
179 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
179 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
180 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
180 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
181 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
181 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
182 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
182 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
183 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
183 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
184 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
184 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
185 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
185 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
186 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
186 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
187 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
187 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
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']);
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 pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
189 pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
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']);
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 pyroutes.register('repo_commit_comment_edit', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/edit', ['repo_name', 'commit_id', 'comment_id']);
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 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
192 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
193 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
193 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
194 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
194 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
195 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
195 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
196 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
196 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
197 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
197 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
198 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
198 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
199 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
199 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
200 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
200 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
201 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
202 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
203 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
203 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
204 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
204 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
205 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
205 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
206 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
206 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
207 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
207 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
208 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
208 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
209 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
209 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
210 pyroutes.register('repo_files_check_head', '/%(repo_name)s/check_head/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_files_upload_file', '/%(repo_name)s/upload_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
218 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
219 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
219 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
220 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
220 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
221 pyroutes.register('repo_commits', '/%(repo_name)s/commits', ['repo_name']);
221 pyroutes.register('repo_commits', '/%(repo_name)s/commits', ['repo_name']);
222 pyroutes.register('repo_commits_file', '/%(repo_name)s/commits/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
222 pyroutes.register('repo_commits_file', '/%(repo_name)s/commits/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
223 pyroutes.register('repo_commits_elements', '/%(repo_name)s/commits_elements', ['repo_name']);
223 pyroutes.register('repo_commits_elements', '/%(repo_name)s/commits_elements', ['repo_name']);
224 pyroutes.register('repo_commits_elements_file', '/%(repo_name)s/commits_elements/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
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 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
225 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
226 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
226 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
227 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
227 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
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']);
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 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
229 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
230 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
230 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
231 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
231 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
232 pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']);
232 pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']);
233 pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']);
233 pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']);
234 pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']);
234 pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']);
235 pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']);
235 pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']);
236 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
236 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
237 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
237 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
238 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
238 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
239 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
239 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
240 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
240 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
241 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
241 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
242 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
242 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
243 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
243 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
244 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
244 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
245 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
245 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
246 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
246 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
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']);
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 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']);
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 pyroutes.register('pullrequest_comments', '/%(repo_name)s/pull-request/%(pull_request_id)s/comments', ['repo_name', 'pull_request_id']);
249 pyroutes.register('pullrequest_comments', '/%(repo_name)s/pull-request/%(pull_request_id)s/comments', ['repo_name', 'pull_request_id']);
250 pyroutes.register('pullrequest_todos', '/%(repo_name)s/pull-request/%(pull_request_id)s/todos', ['repo_name', 'pull_request_id']);
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 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
252 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
252 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
253 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
253 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
254 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
254 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
255 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
255 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
256 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
256 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
257 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
257 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
258 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
258 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
259 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
259 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
260 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
260 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
261 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
261 pyroutes.register('edit_repo_perms_set_private', '/%(repo_name)s/settings/permissions/set_private', ['repo_name']);
262 pyroutes.register('edit_repo_perms_set_private', '/%(repo_name)s/settings/permissions/set_private', ['repo_name']);
262 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
263 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
263 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
264 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
264 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
265 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
265 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
266 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
266 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
267 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
267 pyroutes.register('repo_edit_toggle_locking', '/%(repo_name)s/settings/toggle_locking', ['repo_name']);
268 pyroutes.register('repo_edit_toggle_locking', '/%(repo_name)s/settings/toggle_locking', ['repo_name']);
268 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
269 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
269 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
270 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
270 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
271 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
271 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
272 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
272 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
273 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
273 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
274 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
274 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
275 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
275 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
276 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
276 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
277 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
277 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
278 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
278 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
279 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
279 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
280 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
280 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
281 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
281 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
282 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
282 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
283 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
283 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
284 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
284 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
285 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
285 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed-rss', ['repo_name']);
286 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed-rss', ['repo_name']);
286 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed-atom', ['repo_name']);
287 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed-atom', ['repo_name']);
287 pyroutes.register('rss_feed_home_old', '/%(repo_name)s/feed/rss', ['repo_name']);
288 pyroutes.register('rss_feed_home_old', '/%(repo_name)s/feed/rss', ['repo_name']);
288 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
289 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
289 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
290 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
290 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
291 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
291 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
292 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
292 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
293 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
293 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
294 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
294 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
295 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
295 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
296 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
296 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
297 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
297 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
298 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
298 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
299 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
299 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
300 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
300 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['user_group_id']);
301 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['user_group_id']);
301 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
302 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
302 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
303 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
303 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
304 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
304 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['user_group_id']);
305 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['user_group_id']);
305 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
306 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
306 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
307 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
307 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
308 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
308 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
309 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
309 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
310 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
310 pyroutes.register('search', '/_admin/search', []);
311 pyroutes.register('search', '/_admin/search', []);
311 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
312 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
312 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
313 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
313 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
314 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
314 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
315 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
315 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
316 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
316 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
317 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
317 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
318 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
318 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
319 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
319 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
320 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
320 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
321 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
321 pyroutes.register('my_account_auth_tokens_view', '/_admin/my_account/auth_tokens/view', []);
322 pyroutes.register('my_account_auth_tokens_view', '/_admin/my_account/auth_tokens/view', []);
322 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
323 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
323 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
324 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
324 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
325 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
325 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
326 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
326 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
327 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
327 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
328 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
328 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
329 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
329 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
330 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
330 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
331 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
331 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
332 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
332 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
333 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
333 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
334 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
334 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
335 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
335 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
336 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
336 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
337 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
337 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
338 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
338 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
339 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
339 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
340 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
340 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
341 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
341 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
342 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
342 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
343 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
343 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
344 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
344 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
345 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
345 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
346 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
346 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
347 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
347 pyroutes.register('gists_show', '/_admin/gists', []);
348 pyroutes.register('gists_show', '/_admin/gists', []);
348 pyroutes.register('gists_new', '/_admin/gists/new', []);
349 pyroutes.register('gists_new', '/_admin/gists/new', []);
349 pyroutes.register('gists_create', '/_admin/gists/create', []);
350 pyroutes.register('gists_create', '/_admin/gists/create', []);
350 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
351 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
351 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
352 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
352 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
353 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
353 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
354 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
354 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
355 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
355 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
356 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
356 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
357 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
357 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 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 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
359 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
359 pyroutes.register('debug_style_email', '/_admin/debug_style/email/%(email_id)s', ['email_id']);
360 pyroutes.register('debug_style_email', '/_admin/debug_style/email/%(email_id)s', ['email_id']);
360 pyroutes.register('debug_style_email_plain_rendered', '/_admin/debug_style/email-rendered/%(email_id)s', ['email_id']);
361 pyroutes.register('debug_style_email_plain_rendered', '/_admin/debug_style/email-rendered/%(email_id)s', ['email_id']);
361 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
362 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
362 pyroutes.register('apiv2', '/_admin/api', []);
363 pyroutes.register('apiv2', '/_admin/api', []);
363 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
364 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
364 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
365 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
365 pyroutes.register('login', '/_admin/login', []);
366 pyroutes.register('login', '/_admin/login', []);
366 pyroutes.register('register', '/_admin/register', []);
367 pyroutes.register('register', '/_admin/register', []);
367 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
368 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
368 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
369 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
369 pyroutes.register('repo_reviewers_review_rule_delete', '/%(repo_name)s/settings/review/rules/%(rule_id)s/delete', ['repo_name', 'rule_id']);
370 pyroutes.register('repo_reviewers_review_rule_delete', '/%(repo_name)s/settings/review/rules/%(rule_id)s/delete', ['repo_name', 'rule_id']);
370 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
371 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
371 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
372 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
372 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
373 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
373 pyroutes.register('admin_settings_scheduler_show_tasks', '/_admin/settings/scheduler/_tasks', []);
374 pyroutes.register('admin_settings_scheduler_show_tasks', '/_admin/settings/scheduler/_tasks', []);
374 pyroutes.register('admin_settings_scheduler_show_all', '/_admin/settings/scheduler', []);
375 pyroutes.register('admin_settings_scheduler_show_all', '/_admin/settings/scheduler', []);
375 pyroutes.register('admin_settings_scheduler_new', '/_admin/settings/scheduler/new', []);
376 pyroutes.register('admin_settings_scheduler_new', '/_admin/settings/scheduler/new', []);
376 pyroutes.register('admin_settings_scheduler_create', '/_admin/settings/scheduler/create', []);
377 pyroutes.register('admin_settings_scheduler_create', '/_admin/settings/scheduler/create', []);
377 pyroutes.register('admin_settings_scheduler_edit', '/_admin/settings/scheduler/%(schedule_id)s', ['schedule_id']);
378 pyroutes.register('admin_settings_scheduler_edit', '/_admin/settings/scheduler/%(schedule_id)s', ['schedule_id']);
378 pyroutes.register('admin_settings_scheduler_update', '/_admin/settings/scheduler/%(schedule_id)s/update', ['schedule_id']);
379 pyroutes.register('admin_settings_scheduler_update', '/_admin/settings/scheduler/%(schedule_id)s/update', ['schedule_id']);
379 pyroutes.register('admin_settings_scheduler_delete', '/_admin/settings/scheduler/%(schedule_id)s/delete', ['schedule_id']);
380 pyroutes.register('admin_settings_scheduler_delete', '/_admin/settings/scheduler/%(schedule_id)s/delete', ['schedule_id']);
380 pyroutes.register('admin_settings_scheduler_execute', '/_admin/settings/scheduler/%(schedule_id)s/execute', ['schedule_id']);
381 pyroutes.register('admin_settings_scheduler_execute', '/_admin/settings/scheduler/%(schedule_id)s/execute', ['schedule_id']);
381 pyroutes.register('admin_settings_automation', '/_admin/settings/automation', []);
382 pyroutes.register('admin_settings_automation', '/_admin/settings/automation', []);
382 pyroutes.register('admin_settings_automation_update', '/_admin/settings/automation/%(entry_id)s/update', ['entry_id']);
383 pyroutes.register('admin_settings_automation_update', '/_admin/settings/automation/%(entry_id)s/update', ['entry_id']);
383 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
384 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
384 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
385 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
385 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
386 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
386 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
387 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
387 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
388 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
388 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
389 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
389 pyroutes.register('pullrequest_draft_comments_submit', '/%(repo_name)s/pull-request/%(pull_request_id)s/draft_comments_submit', ['repo_name', 'pull_request_id']);
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 pyroutes.register('commit_draft_comments_submit', '/%(repo_name)s/changeset/%(commit_id)s/draft_comments_submit', ['repo_name', 'commit_id']);
391 pyroutes.register('commit_draft_comments_submit', '/%(repo_name)s/changeset/%(commit_id)s/draft_comments_submit', ['repo_name', 'commit_id']);
391 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
392 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
392 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
393 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
393 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
394 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
394 pyroutes.register('repo_artifacts_get', '/%(repo_name)s/artifacts/download/%(uid)s', ['repo_name', 'uid']);
395 pyroutes.register('repo_artifacts_get', '/%(repo_name)s/artifacts/download/%(uid)s', ['repo_name', 'uid']);
395 pyroutes.register('repo_artifacts_store', '/%(repo_name)s/artifacts/store', ['repo_name']);
396 pyroutes.register('repo_artifacts_store', '/%(repo_name)s/artifacts/store', ['repo_name']);
396 pyroutes.register('repo_artifacts_info', '/%(repo_name)s/artifacts/info/%(uid)s', ['repo_name', 'uid']);
397 pyroutes.register('repo_artifacts_info', '/%(repo_name)s/artifacts/info/%(uid)s', ['repo_name', 'uid']);
397 pyroutes.register('repo_artifacts_delete', '/%(repo_name)s/artifacts/delete/%(uid)s', ['repo_name', 'uid']);
398 pyroutes.register('repo_artifacts_delete', '/%(repo_name)s/artifacts/delete/%(uid)s', ['repo_name', 'uid']);
398 pyroutes.register('repo_artifacts_update', '/%(repo_name)s/artifacts/update/%(uid)s', ['repo_name', 'uid']);
399 pyroutes.register('repo_artifacts_update', '/%(repo_name)s/artifacts/update/%(uid)s', ['repo_name', 'uid']);
399 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
400 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
400 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
401 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
401 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
402 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
402 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
403 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
403 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
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 // # Copyright (C) 2010-2020 RhodeCode GmbH
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19 var firefoxAnchorFix = function() {
19 var firefoxAnchorFix = function() {
20 // hack to make anchor links behave properly on firefox, in our inline
20 // hack to make anchor links behave properly on firefox, in our inline
21 // comments generation when comments are injected firefox is misbehaving
21 // comments generation when comments are injected firefox is misbehaving
22 // when jumping to anchor links
22 // when jumping to anchor links
23 if (location.href.indexOf('#') > -1) {
23 if (location.href.indexOf('#') > -1) {
24 location.href += '';
24 location.href += '';
25 }
25 }
26 };
26 };
27
27
28 var linkifyComments = function(comments) {
28 var linkifyComments = function(comments) {
29 var firstCommentId = null;
29 var firstCommentId = null;
30 if (comments) {
30 if (comments) {
31 firstCommentId = $(comments[0]).data('comment-id');
31 firstCommentId = $(comments[0]).data('comment-id');
32 }
32 }
33
33
34 if (firstCommentId){
34 if (firstCommentId){
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 }
36 }
37 };
37 };
38
38
39 var bindToggleButtons = function() {
39 var bindToggleButtons = function() {
40 $('.comment-toggle').on('click', function() {
40 $('.comment-toggle').on('click', function() {
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 });
42 });
43 };
43 };
44
44
45
45
46
46
47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
48 failHandler = failHandler || function() {};
48 failHandler = failHandler || function() {};
49 postData = toQueryString(postData);
49 postData = toQueryString(postData);
50 var request = $.ajax({
50 var request = $.ajax({
51 url: url,
51 url: url,
52 type: 'POST',
52 type: 'POST',
53 data: postData,
53 data: postData,
54 headers: {'X-PARTIAL-XHR': true}
54 headers: {'X-PARTIAL-XHR': true}
55 })
55 })
56 .done(function (data) {
56 .done(function (data) {
57 successHandler(data);
57 successHandler(data);
58 })
58 })
59 .fail(function (data, textStatus, errorThrown) {
59 .fail(function (data, textStatus, errorThrown) {
60 failHandler(data, textStatus, errorThrown)
60 failHandler(data, textStatus, errorThrown)
61 });
61 });
62 return request;
62 return request;
63 };
63 };
64
64
65
65
66
66
67
67
68 /* Comment form for main and inline comments */
68 /* Comment form for main and inline comments */
69 (function(mod) {
69 (function(mod) {
70
70
71 if (typeof exports == "object" && typeof module == "object") {
71 if (typeof exports == "object" && typeof module == "object") {
72 // CommonJS
72 // CommonJS
73 module.exports = mod();
73 module.exports = mod();
74 }
74 }
75 else {
75 else {
76 // Plain browser env
76 // Plain browser env
77 (this || window).CommentForm = mod();
77 (this || window).CommentForm = mod();
78 }
78 }
79
79
80 })(function() {
80 })(function() {
81 "use strict";
81 "use strict";
82
82
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id) {
84
84
85 if (!(this instanceof CommentForm)) {
85 if (!(this instanceof CommentForm)) {
86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
86 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId, edit, comment_id);
87 }
87 }
88
88
89 // bind the element instance to our Form
89 // bind the element instance to our Form
90 $(formElement).get(0).CommentForm = this;
90 $(formElement).get(0).CommentForm = this;
91
91
92 this.withLineNo = function(selector) {
92 this.withLineNo = function(selector) {
93 var lineNo = this.lineNo;
93 var lineNo = this.lineNo;
94 if (lineNo === undefined) {
94 if (lineNo === undefined) {
95 return selector
95 return selector
96 } else {
96 } else {
97 return selector + '_' + lineNo;
97 return selector + '_' + lineNo;
98 }
98 }
99 };
99 };
100
100
101 this.commitId = commitId;
101 this.commitId = commitId;
102 this.pullRequestId = pullRequestId;
102 this.pullRequestId = pullRequestId;
103 this.lineNo = lineNo;
103 this.lineNo = lineNo;
104 this.initAutocompleteActions = initAutocompleteActions;
104 this.initAutocompleteActions = initAutocompleteActions;
105
105
106 this.previewButton = this.withLineNo('#preview-btn');
106 this.previewButton = this.withLineNo('#preview-btn');
107 this.previewContainer = this.withLineNo('#preview-container');
107 this.previewContainer = this.withLineNo('#preview-container');
108
108
109 this.previewBoxSelector = this.withLineNo('#preview-box');
109 this.previewBoxSelector = this.withLineNo('#preview-box');
110
110
111 this.editButton = this.withLineNo('#edit-btn');
111 this.editButton = this.withLineNo('#edit-btn');
112 this.editContainer = this.withLineNo('#edit-container');
112 this.editContainer = this.withLineNo('#edit-container');
113 this.cancelButton = this.withLineNo('#cancel-btn');
113 this.cancelButton = this.withLineNo('#cancel-btn');
114 this.commentType = this.withLineNo('#comment_type');
114 this.commentType = this.withLineNo('#comment_type');
115
115
116 this.resolvesId = null;
116 this.resolvesId = null;
117 this.resolvesActionId = null;
117 this.resolvesActionId = null;
118
118
119 this.closesPr = '#close_pull_request';
119 this.closesPr = '#close_pull_request';
120
120
121 this.cmBox = this.withLineNo('#text');
121 this.cmBox = this.withLineNo('#text');
122 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
122 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
123
123
124 this.statusChange = this.withLineNo('#change_status');
124 this.statusChange = this.withLineNo('#change_status');
125
125
126 this.submitForm = formElement;
126 this.submitForm = formElement;
127
127
128 this.submitButton = $(this.submitForm).find('.submit-comment-action');
128 this.submitButton = $(this.submitForm).find('.submit-comment-action');
129 this.submitButtonText = this.submitButton.val();
129 this.submitButtonText = this.submitButton.val();
130
130
131 this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
131 this.submitDraftButton = $(this.submitForm).find('.submit-draft-action');
132 this.submitDraftButtonText = this.submitDraftButton.val();
132 this.submitDraftButtonText = this.submitDraftButton.val();
133
133
134 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
134 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
135 {'repo_name': templateContext.repo_name,
135 {'repo_name': templateContext.repo_name,
136 'commit_id': templateContext.commit_data.commit_id});
136 'commit_id': templateContext.commit_data.commit_id});
137
137
138 if (edit){
138 if (edit){
139 this.submitDraftButton.hide();
139 this.submitDraftButton.hide();
140 this.submitButtonText = _gettext('Update Comment');
140 this.submitButtonText = _gettext('Update Comment');
141 $(this.commentType).prop('disabled', true);
141 $(this.commentType).prop('disabled', true);
142 $(this.commentType).addClass('disabled');
142 $(this.commentType).addClass('disabled');
143 var editInfo =
143 var editInfo =
144 '';
144 '';
145 $(editInfo).insertBefore($(this.editButton).parent());
145 $(editInfo).insertBefore($(this.editButton).parent());
146 }
146 }
147
147
148 if (resolvesCommentId){
148 if (resolvesCommentId){
149 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
149 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
150 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
150 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
151 $(this.commentType).prop('disabled', true);
151 $(this.commentType).prop('disabled', true);
152 $(this.commentType).addClass('disabled');
152 $(this.commentType).addClass('disabled');
153
153
154 // disable select
154 // disable select
155 setTimeout(function() {
155 setTimeout(function() {
156 $(self.statusChange).select2('readonly', true);
156 $(self.statusChange).select2('readonly', true);
157 }, 10);
157 }, 10);
158
158
159 var resolvedInfo = (
159 var resolvedInfo = (
160 '<li class="resolve-action">' +
160 '<li class="resolve-action">' +
161 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
161 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
162 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
162 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
163 '</li>'
163 '</li>'
164 ).format(resolvesCommentId, _gettext('resolve comment'));
164 ).format(resolvesCommentId, _gettext('resolve comment'));
165 $(resolvedInfo).insertAfter($(this.commentType).parent());
165 $(resolvedInfo).insertAfter($(this.commentType).parent());
166 }
166 }
167
167
168 // based on commitId, or pullRequestId decide where do we submit
168 // based on commitId, or pullRequestId decide where do we submit
169 // out data
169 // out data
170 if (this.commitId){
170 if (this.commitId){
171 var pyurl = 'repo_commit_comment_create';
171 var pyurl = 'repo_commit_comment_create';
172 if(edit){
172 if(edit){
173 pyurl = 'repo_commit_comment_edit';
173 pyurl = 'repo_commit_comment_edit';
174 }
174 }
175 this.submitUrl = pyroutes.url(pyurl,
175 this.submitUrl = pyroutes.url(pyurl,
176 {'repo_name': templateContext.repo_name,
176 {'repo_name': templateContext.repo_name,
177 'commit_id': this.commitId,
177 'commit_id': this.commitId,
178 'comment_id': comment_id});
178 'comment_id': comment_id});
179 this.selfUrl = pyroutes.url('repo_commit',
179 this.selfUrl = pyroutes.url('repo_commit',
180 {'repo_name': templateContext.repo_name,
180 {'repo_name': templateContext.repo_name,
181 'commit_id': this.commitId});
181 'commit_id': this.commitId});
182
182
183 } else if (this.pullRequestId) {
183 } else if (this.pullRequestId) {
184 var pyurl = 'pullrequest_comment_create';
184 var pyurl = 'pullrequest_comment_create';
185 if(edit){
185 if(edit){
186 pyurl = 'pullrequest_comment_edit';
186 pyurl = 'pullrequest_comment_edit';
187 }
187 }
188 this.submitUrl = pyroutes.url(pyurl,
188 this.submitUrl = pyroutes.url(pyurl,
189 {'repo_name': templateContext.repo_name,
189 {'repo_name': templateContext.repo_name,
190 'pull_request_id': this.pullRequestId,
190 'pull_request_id': this.pullRequestId,
191 'comment_id': comment_id});
191 'comment_id': comment_id});
192 this.selfUrl = pyroutes.url('pullrequest_show',
192 this.selfUrl = pyroutes.url('pullrequest_show',
193 {'repo_name': templateContext.repo_name,
193 {'repo_name': templateContext.repo_name,
194 'pull_request_id': this.pullRequestId});
194 'pull_request_id': this.pullRequestId});
195
195
196 } else {
196 } else {
197 throw new Error(
197 throw new Error(
198 'CommentForm requires pullRequestId, or commitId to be specified.')
198 'CommentForm requires pullRequestId, or commitId to be specified.')
199 }
199 }
200
200
201 // FUNCTIONS and helpers
201 // FUNCTIONS and helpers
202 var self = this;
202 var self = this;
203
203
204 this.isInline = function(){
204 this.isInline = function(){
205 return this.lineNo && this.lineNo != 'general';
205 return this.lineNo && this.lineNo != 'general';
206 };
206 };
207
207
208 this.getCmInstance = function(){
208 this.getCmInstance = function(){
209 return this.cm
209 return this.cm
210 };
210 };
211
211
212 this.setPlaceholder = function(placeholder) {
212 this.setPlaceholder = function(placeholder) {
213 var cm = this.getCmInstance();
213 var cm = this.getCmInstance();
214 if (cm){
214 if (cm){
215 cm.setOption('placeholder', placeholder);
215 cm.setOption('placeholder', placeholder);
216 }
216 }
217 };
217 };
218
218
219 this.getCommentStatus = function() {
219 this.getCommentStatus = function() {
220 return $(this.submitForm).find(this.statusChange).val();
220 return $(this.submitForm).find(this.statusChange).val();
221 };
221 };
222
222
223 this.getCommentType = function() {
223 this.getCommentType = function() {
224 return $(this.submitForm).find(this.commentType).val();
224 return $(this.submitForm).find(this.commentType).val();
225 };
225 };
226
226
227 this.getDraftState = function () {
227 this.getDraftState = function () {
228 var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
228 var submitterElem = $(this.submitForm).find('input[type="submit"].submitter');
229 var data = $(submitterElem).data('isDraft');
229 var data = $(submitterElem).data('isDraft');
230 return data
230 return data
231 }
231 }
232
232
233 this.getResolvesId = function() {
233 this.getResolvesId = function() {
234 return $(this.submitForm).find(this.resolvesId).val() || null;
234 return $(this.submitForm).find(this.resolvesId).val() || null;
235 };
235 };
236
236
237 this.getClosePr = function() {
237 this.getClosePr = function() {
238 return $(this.submitForm).find(this.closesPr).val() || null;
238 return $(this.submitForm).find(this.closesPr).val() || null;
239 };
239 };
240
240
241 this.markCommentResolved = function(resolvedCommentId){
241 this.markCommentResolved = function(resolvedCommentId){
242 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
242 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
243 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
243 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
244 };
244 };
245
245
246 this.isAllowedToSubmit = function() {
246 this.isAllowedToSubmit = function() {
247 var commentDisabled = $(this.submitButton).prop('disabled');
247 var commentDisabled = $(this.submitButton).prop('disabled');
248 var draftDisabled = $(this.submitDraftButton).prop('disabled');
248 var draftDisabled = $(this.submitDraftButton).prop('disabled');
249 return !commentDisabled && !draftDisabled;
249 return !commentDisabled && !draftDisabled;
250 };
250 };
251
251
252 this.initStatusChangeSelector = function(){
252 this.initStatusChangeSelector = function(){
253 var formatChangeStatus = function(state, escapeMarkup) {
253 var formatChangeStatus = function(state, escapeMarkup) {
254 var originalOption = state.element;
254 var originalOption = state.element;
255 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
255 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
256 return tmpl
256 return tmpl
257 };
257 };
258 var formatResult = function(result, container, query, escapeMarkup) {
258 var formatResult = function(result, container, query, escapeMarkup) {
259 return formatChangeStatus(result, escapeMarkup);
259 return formatChangeStatus(result, escapeMarkup);
260 };
260 };
261
261
262 var formatSelection = function(data, container, escapeMarkup) {
262 var formatSelection = function(data, container, escapeMarkup) {
263 return formatChangeStatus(data, escapeMarkup);
263 return formatChangeStatus(data, escapeMarkup);
264 };
264 };
265
265
266 $(this.submitForm).find(this.statusChange).select2({
266 $(this.submitForm).find(this.statusChange).select2({
267 placeholder: _gettext('Status Review'),
267 placeholder: _gettext('Status Review'),
268 formatResult: formatResult,
268 formatResult: formatResult,
269 formatSelection: formatSelection,
269 formatSelection: formatSelection,
270 containerCssClass: "drop-menu status_box_menu",
270 containerCssClass: "drop-menu status_box_menu",
271 dropdownCssClass: "drop-menu-dropdown",
271 dropdownCssClass: "drop-menu-dropdown",
272 dropdownAutoWidth: true,
272 dropdownAutoWidth: true,
273 minimumResultsForSearch: -1
273 minimumResultsForSearch: -1
274 });
274 });
275
275
276 $(this.submitForm).find(this.statusChange).on('change', function() {
276 $(this.submitForm).find(this.statusChange).on('change', function() {
277 var status = self.getCommentStatus();
277 var status = self.getCommentStatus();
278
278
279 if (status && !self.isInline()) {
279 if (status && !self.isInline()) {
280 $(self.submitButton).prop('disabled', false);
280 $(self.submitButton).prop('disabled', false);
281 $(self.submitDraftButton).prop('disabled', false);
281 $(self.submitDraftButton).prop('disabled', false);
282 }
282 }
283
283
284 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
284 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
285 self.setPlaceholder(placeholderText)
285 self.setPlaceholder(placeholderText)
286 })
286 })
287 };
287 };
288
288
289 // reset the comment form into it's original state
289 // reset the comment form into it's original state
290 this.resetCommentFormState = function(content) {
290 this.resetCommentFormState = function(content) {
291 content = content || '';
291 content = content || '';
292
292
293 $(this.editContainer).show();
293 $(this.editContainer).show();
294 $(this.editButton).parent().addClass('active');
294 $(this.editButton).parent().addClass('active');
295
295
296 $(this.previewContainer).hide();
296 $(this.previewContainer).hide();
297 $(this.previewButton).parent().removeClass('active');
297 $(this.previewButton).parent().removeClass('active');
298
298
299 this.setActionButtonsDisabled(true);
299 this.setActionButtonsDisabled(true);
300 self.cm.setValue(content);
300 self.cm.setValue(content);
301 self.cm.setOption("readOnly", false);
301 self.cm.setOption("readOnly", false);
302
302
303 if (this.resolvesId) {
303 if (this.resolvesId) {
304 // destroy the resolve action
304 // destroy the resolve action
305 $(this.resolvesId).parent().remove();
305 $(this.resolvesId).parent().remove();
306 }
306 }
307 // reset closingPR flag
307 // reset closingPR flag
308 $('.close-pr-input').remove();
308 $('.close-pr-input').remove();
309
309
310 $(this.statusChange).select2('readonly', false);
310 $(this.statusChange).select2('readonly', false);
311 };
311 };
312
312
313 this.globalSubmitSuccessCallback = function(comment){
313 this.globalSubmitSuccessCallback = function(comment){
314 // default behaviour is to call GLOBAL hook, if it's registered.
314 // default behaviour is to call GLOBAL hook, if it's registered.
315 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
315 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
316 commentFormGlobalSubmitSuccessCallback(comment);
316 commentFormGlobalSubmitSuccessCallback(comment);
317 }
317 }
318 };
318 };
319
319
320 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
320 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
321 return _submitAjaxPOST(url, postData, successHandler, failHandler);
321 return _submitAjaxPOST(url, postData, successHandler, failHandler);
322 };
322 };
323
323
324 // overwrite a submitHandler, we need to do it for inline comments
324 // overwrite a submitHandler, we need to do it for inline comments
325 this.setHandleFormSubmit = function(callback) {
325 this.setHandleFormSubmit = function(callback) {
326 this.handleFormSubmit = callback;
326 this.handleFormSubmit = callback;
327 };
327 };
328
328
329 // overwrite a submitSuccessHandler
329 // overwrite a submitSuccessHandler
330 this.setGlobalSubmitSuccessCallback = function(callback) {
330 this.setGlobalSubmitSuccessCallback = function(callback) {
331 this.globalSubmitSuccessCallback = callback;
331 this.globalSubmitSuccessCallback = callback;
332 };
332 };
333
333
334 // default handler for for submit for main comments
334 // default handler for for submit for main comments
335 this.handleFormSubmit = function() {
335 this.handleFormSubmit = function() {
336 var text = self.cm.getValue();
336 var text = self.cm.getValue();
337 var status = self.getCommentStatus();
337 var status = self.getCommentStatus();
338 var commentType = self.getCommentType();
338 var commentType = self.getCommentType();
339 var isDraft = self.getDraftState();
339 var isDraft = self.getDraftState();
340 var resolvesCommentId = self.getResolvesId();
340 var resolvesCommentId = self.getResolvesId();
341 var closePullRequest = self.getClosePr();
341 var closePullRequest = self.getClosePr();
342
342
343 if (text === "" && !status) {
343 if (text === "" && !status) {
344 return;
344 return;
345 }
345 }
346
346
347 var excludeCancelBtn = false;
347 var excludeCancelBtn = false;
348 var submitEvent = true;
348 var submitEvent = true;
349 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
349 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
350 self.cm.setOption("readOnly", true);
350 self.cm.setOption("readOnly", true);
351
351
352 var postData = {
352 var postData = {
353 'text': text,
353 'text': text,
354 'changeset_status': status,
354 'changeset_status': status,
355 'comment_type': commentType,
355 'comment_type': commentType,
356 'csrf_token': CSRF_TOKEN
356 'csrf_token': CSRF_TOKEN
357 };
357 };
358
358
359 if (resolvesCommentId) {
359 if (resolvesCommentId) {
360 postData['resolves_comment_id'] = resolvesCommentId;
360 postData['resolves_comment_id'] = resolvesCommentId;
361 }
361 }
362
362
363 if (closePullRequest) {
363 if (closePullRequest) {
364 postData['close_pull_request'] = true;
364 postData['close_pull_request'] = true;
365 }
365 }
366
366
367 // submitSuccess for general comments
367 // submitSuccess for general comments
368 var submitSuccessCallback = function(json_data) {
368 var submitSuccessCallback = function(json_data) {
369 // reload page if we change status for single commit.
369 // reload page if we change status for single commit.
370 if (status && self.commitId) {
370 if (status && self.commitId) {
371 location.reload(true);
371 location.reload(true);
372 } else {
372 } else {
373 // inject newly created comments, json_data is {<comment_id>: {}}
373 // inject newly created comments, json_data is {<comment_id>: {}}
374 self.attachGeneralComment(json_data)
374 self.attachGeneralComment(json_data)
375
375
376 self.resetCommentFormState();
376 self.resetCommentFormState();
377 timeagoActivate();
377 timeagoActivate();
378 tooltipActivate();
378 tooltipActivate();
379
379
380 // mark visually which comment was resolved
380 // mark visually which comment was resolved
381 if (resolvesCommentId) {
381 if (resolvesCommentId) {
382 self.markCommentResolved(resolvesCommentId);
382 self.markCommentResolved(resolvesCommentId);
383 }
383 }
384 }
384 }
385
385
386 // run global callback on submit
386 // run global callback on submit
387 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
387 self.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
388
388
389 };
389 };
390 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
390 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
391 var prefix = "Error while submitting comment.\n"
391 var prefix = "Error while submitting comment.\n"
392 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
392 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
393 ajaxErrorSwal(message);
393 ajaxErrorSwal(message);
394 self.resetCommentFormState(text);
394 self.resetCommentFormState(text);
395 };
395 };
396 self.submitAjaxPOST(
396 self.submitAjaxPOST(
397 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
397 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
398 };
398 };
399
399
400 this.previewSuccessCallback = function(o) {
400 this.previewSuccessCallback = function(o) {
401 $(self.previewBoxSelector).html(o);
401 $(self.previewBoxSelector).html(o);
402 $(self.previewBoxSelector).removeClass('unloaded');
402 $(self.previewBoxSelector).removeClass('unloaded');
403
403
404 // swap buttons, making preview active
404 // swap buttons, making preview active
405 $(self.previewButton).parent().addClass('active');
405 $(self.previewButton).parent().addClass('active');
406 $(self.editButton).parent().removeClass('active');
406 $(self.editButton).parent().removeClass('active');
407
407
408 // unlock buttons
408 // unlock buttons
409 self.setActionButtonsDisabled(false);
409 self.setActionButtonsDisabled(false);
410 };
410 };
411
411
412 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
412 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
413 excludeCancelBtn = excludeCancelBtn || false;
413 excludeCancelBtn = excludeCancelBtn || false;
414 submitEvent = submitEvent || false;
414 submitEvent = submitEvent || false;
415
415
416 $(this.editButton).prop('disabled', state);
416 $(this.editButton).prop('disabled', state);
417 $(this.previewButton).prop('disabled', state);
417 $(this.previewButton).prop('disabled', state);
418
418
419 if (!excludeCancelBtn) {
419 if (!excludeCancelBtn) {
420 $(this.cancelButton).prop('disabled', state);
420 $(this.cancelButton).prop('disabled', state);
421 }
421 }
422
422
423 var submitState = state;
423 var submitState = state;
424 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
424 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
425 // if the value of commit review status is set, we allow
425 // if the value of commit review status is set, we allow
426 // submit button, but only on Main form, isInline means inline
426 // submit button, but only on Main form, isInline means inline
427 submitState = false
427 submitState = false
428 }
428 }
429
429
430 $(this.submitButton).prop('disabled', submitState);
430 $(this.submitButton).prop('disabled', submitState);
431 $(this.submitDraftButton).prop('disabled', submitState);
431 $(this.submitDraftButton).prop('disabled', submitState);
432
432
433 if (submitEvent) {
433 if (submitEvent) {
434 var isDraft = self.getDraftState();
434 var isDraft = self.getDraftState();
435
435
436 if (isDraft) {
436 if (isDraft) {
437 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
437 $(this.submitDraftButton).val(_gettext('Saving Draft...'));
438 } else {
438 } else {
439 $(this.submitButton).val(_gettext('Submitting...'));
439 $(this.submitButton).val(_gettext('Submitting...'));
440 }
440 }
441
441
442 } else {
442 } else {
443 $(this.submitButton).val(this.submitButtonText);
443 $(this.submitButton).val(this.submitButtonText);
444 $(this.submitDraftButton).val(this.submitDraftButtonText);
444 $(this.submitDraftButton).val(this.submitDraftButtonText);
445 }
445 }
446
446
447 };
447 };
448
448
449 // lock preview/edit/submit buttons on load, but exclude cancel button
449 // lock preview/edit/submit buttons on load, but exclude cancel button
450 var excludeCancelBtn = true;
450 var excludeCancelBtn = true;
451 this.setActionButtonsDisabled(true, excludeCancelBtn);
451 this.setActionButtonsDisabled(true, excludeCancelBtn);
452
452
453 // anonymous users don't have access to initialized CM instance
453 // anonymous users don't have access to initialized CM instance
454 if (this.cm !== undefined){
454 if (this.cm !== undefined){
455 this.cm.on('change', function(cMirror) {
455 this.cm.on('change', function(cMirror) {
456 if (cMirror.getValue() === "") {
456 if (cMirror.getValue() === "") {
457 self.setActionButtonsDisabled(true, excludeCancelBtn)
457 self.setActionButtonsDisabled(true, excludeCancelBtn)
458 } else {
458 } else {
459 self.setActionButtonsDisabled(false, excludeCancelBtn)
459 self.setActionButtonsDisabled(false, excludeCancelBtn)
460 }
460 }
461 });
461 });
462 }
462 }
463
463
464 $(this.editButton).on('click', function(e) {
464 $(this.editButton).on('click', function(e) {
465 e.preventDefault();
465 e.preventDefault();
466
466
467 $(self.previewButton).parent().removeClass('active');
467 $(self.previewButton).parent().removeClass('active');
468 $(self.previewContainer).hide();
468 $(self.previewContainer).hide();
469
469
470 $(self.editButton).parent().addClass('active');
470 $(self.editButton).parent().addClass('active');
471 $(self.editContainer).show();
471 $(self.editContainer).show();
472
472
473 });
473 });
474
474
475 $(this.previewButton).on('click', function(e) {
475 $(this.previewButton).on('click', function(e) {
476 e.preventDefault();
476 e.preventDefault();
477 var text = self.cm.getValue();
477 var text = self.cm.getValue();
478
478
479 if (text === "") {
479 if (text === "") {
480 return;
480 return;
481 }
481 }
482
482
483 var postData = {
483 var postData = {
484 'text': text,
484 'text': text,
485 'renderer': templateContext.visual.default_renderer,
485 'renderer': templateContext.visual.default_renderer,
486 'csrf_token': CSRF_TOKEN
486 'csrf_token': CSRF_TOKEN
487 };
487 };
488
488
489 // lock ALL buttons on preview
489 // lock ALL buttons on preview
490 self.setActionButtonsDisabled(true);
490 self.setActionButtonsDisabled(true);
491
491
492 $(self.previewBoxSelector).addClass('unloaded');
492 $(self.previewBoxSelector).addClass('unloaded');
493 $(self.previewBoxSelector).html(_gettext('Loading ...'));
493 $(self.previewBoxSelector).html(_gettext('Loading ...'));
494
494
495 $(self.editContainer).hide();
495 $(self.editContainer).hide();
496 $(self.previewContainer).show();
496 $(self.previewContainer).show();
497
497
498 // by default we reset state of comment preserving the text
498 // by default we reset state of comment preserving the text
499 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
499 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
500 var prefix = "Error while preview of comment.\n"
500 var prefix = "Error while preview of comment.\n"
501 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
501 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
502 ajaxErrorSwal(message);
502 ajaxErrorSwal(message);
503
503
504 self.resetCommentFormState(text)
504 self.resetCommentFormState(text)
505 };
505 };
506 self.submitAjaxPOST(
506 self.submitAjaxPOST(
507 self.previewUrl, postData, self.previewSuccessCallback,
507 self.previewUrl, postData, self.previewSuccessCallback,
508 previewFailCallback);
508 previewFailCallback);
509
509
510 $(self.previewButton).parent().addClass('active');
510 $(self.previewButton).parent().addClass('active');
511 $(self.editButton).parent().removeClass('active');
511 $(self.editButton).parent().removeClass('active');
512 });
512 });
513
513
514 $(this.submitForm).submit(function(e) {
514 $(this.submitForm).submit(function(e) {
515 e.preventDefault();
515 e.preventDefault();
516 var allowedToSubmit = self.isAllowedToSubmit();
516 var allowedToSubmit = self.isAllowedToSubmit();
517 if (!allowedToSubmit){
517 if (!allowedToSubmit){
518 return false;
518 return false;
519 }
519 }
520
520
521 self.handleFormSubmit();
521 self.handleFormSubmit();
522 });
522 });
523
523
524 }
524 }
525
525
526 return CommentForm;
526 return CommentForm;
527 });
527 });
528
528
529 /* selector for comment versions */
529 /* selector for comment versions */
530 var initVersionSelector = function(selector, initialData) {
530 var initVersionSelector = function(selector, initialData) {
531
531
532 var formatResult = function(result, container, query, escapeMarkup) {
532 var formatResult = function(result, container, query, escapeMarkup) {
533
533
534 return renderTemplate('commentVersion', {
534 return renderTemplate('commentVersion', {
535 show_disabled: true,
535 show_disabled: true,
536 version: result.comment_version,
536 version: result.comment_version,
537 user_name: result.comment_author_username,
537 user_name: result.comment_author_username,
538 gravatar_url: result.comment_author_gravatar,
538 gravatar_url: result.comment_author_gravatar,
539 size: 16,
539 size: 16,
540 timeago_component: result.comment_created_on,
540 timeago_component: result.comment_created_on,
541 })
541 })
542 };
542 };
543
543
544 $(selector).select2({
544 $(selector).select2({
545 placeholder: "Edited",
545 placeholder: "Edited",
546 containerCssClass: "drop-menu-comment-history",
546 containerCssClass: "drop-menu-comment-history",
547 dropdownCssClass: "drop-menu-dropdown",
547 dropdownCssClass: "drop-menu-dropdown",
548 dropdownAutoWidth: true,
548 dropdownAutoWidth: true,
549 minimumResultsForSearch: -1,
549 minimumResultsForSearch: -1,
550 data: initialData,
550 data: initialData,
551 formatResult: formatResult,
551 formatResult: formatResult,
552 });
552 });
553
553
554 $(selector).on('select2-selecting', function (e) {
554 $(selector).on('select2-selecting', function (e) {
555 // hide the mast as we later do preventDefault()
555 // hide the mast as we later do preventDefault()
556 $("#select2-drop-mask").click();
556 $("#select2-drop-mask").click();
557 e.preventDefault();
557 e.preventDefault();
558 e.choice.action();
558 e.choice.action();
559 });
559 });
560
560
561 $(selector).on("select2-open", function() {
561 $(selector).on("select2-open", function() {
562 timeagoActivate();
562 timeagoActivate();
563 });
563 });
564 };
564 };
565
565
566 /* comments controller */
566 /* comments controller */
567 var CommentsController = function() {
567 var CommentsController = function() {
568 var mainComment = '#text';
568 var mainComment = '#text';
569 var self = this;
569 var self = this;
570
570
571 this.showVersion = function (comment_id, comment_history_id) {
571 this.showVersion = function (comment_id, comment_history_id) {
572
572
573 var historyViewUrl = pyroutes.url(
573 var historyViewUrl = pyroutes.url(
574 'repo_commit_comment_history_view',
574 'repo_commit_comment_history_view',
575 {
575 {
576 'repo_name': templateContext.repo_name,
576 'repo_name': templateContext.repo_name,
577 'commit_id': comment_id,
577 'commit_id': comment_id,
578 'comment_history_id': comment_history_id,
578 'comment_history_id': comment_history_id,
579 }
579 }
580 );
580 );
581 successRenderCommit = function (data) {
581 successRenderCommit = function (data) {
582 SwalNoAnimation.fire({
582 SwalNoAnimation.fire({
583 html: data,
583 html: data,
584 title: '',
584 title: '',
585 });
585 });
586 };
586 };
587 failRenderCommit = function () {
587 failRenderCommit = function () {
588 SwalNoAnimation.fire({
588 SwalNoAnimation.fire({
589 html: 'Error while loading comment history',
589 html: 'Error while loading comment history',
590 title: '',
590 title: '',
591 });
591 });
592 };
592 };
593 _submitAjaxPOST(
593 _submitAjaxPOST(
594 historyViewUrl, {'csrf_token': CSRF_TOKEN},
594 historyViewUrl, {'csrf_token': CSRF_TOKEN},
595 successRenderCommit,
595 successRenderCommit,
596 failRenderCommit
596 failRenderCommit
597 );
597 );
598 };
598 };
599
599
600 this.getLineNumber = function(node) {
600 this.getLineNumber = function(node) {
601 var $node = $(node);
601 var $node = $(node);
602 var lineNo = $node.closest('td').attr('data-line-no');
602 var lineNo = $node.closest('td').attr('data-line-no');
603 if (lineNo === undefined && $node.data('commentInline')){
603 if (lineNo === undefined && $node.data('commentInline')){
604 lineNo = $node.data('commentLineNo')
604 lineNo = $node.data('commentLineNo')
605 }
605 }
606
606
607 return lineNo
607 return lineNo
608 };
608 };
609
609
610 this.scrollToComment = function(node, offset, outdated) {
610 this.scrollToComment = function(node, offset, outdated) {
611 if (offset === undefined) {
611 if (offset === undefined) {
612 offset = 0;
612 offset = 0;
613 }
613 }
614 var outdated = outdated || false;
614 var outdated = outdated || false;
615 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
615 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
616
616
617 if (!node) {
617 if (!node) {
618 node = $('.comment-selected');
618 node = $('.comment-selected');
619 if (!node.length) {
619 if (!node.length) {
620 node = $('comment-current')
620 node = $('comment-current')
621 }
621 }
622 }
622 }
623
623
624 $wrapper = $(node).closest('div.comment');
624 $wrapper = $(node).closest('div.comment');
625
625
626 // show hidden comment when referenced.
626 // show hidden comment when referenced.
627 if (!$wrapper.is(':visible')){
627 if (!$wrapper.is(':visible')){
628 $wrapper.show();
628 $wrapper.show();
629 }
629 }
630
630
631 $comment = $(node).closest(klass);
631 $comment = $(node).closest(klass);
632 $comments = $(klass);
632 $comments = $(klass);
633
633
634 $('.comment-selected').removeClass('comment-selected');
634 $('.comment-selected').removeClass('comment-selected');
635
635
636 var nextIdx = $(klass).index($comment) + offset;
636 var nextIdx = $(klass).index($comment) + offset;
637 if (nextIdx >= $comments.length) {
637 if (nextIdx >= $comments.length) {
638 nextIdx = 0;
638 nextIdx = 0;
639 }
639 }
640 var $next = $(klass).eq(nextIdx);
640 var $next = $(klass).eq(nextIdx);
641
641
642 var $cb = $next.closest('.cb');
642 var $cb = $next.closest('.cb');
643 $cb.removeClass('cb-collapsed');
643 $cb.removeClass('cb-collapsed');
644
644
645 var $filediffCollapseState = $cb.closest('.filediff').prev();
645 var $filediffCollapseState = $cb.closest('.filediff').prev();
646 $filediffCollapseState.prop('checked', false);
646 $filediffCollapseState.prop('checked', false);
647 $next.addClass('comment-selected');
647 $next.addClass('comment-selected');
648 scrollToElement($next);
648 scrollToElement($next);
649 return false;
649 return false;
650 };
650 };
651
651
652 this.nextComment = function(node) {
652 this.nextComment = function(node) {
653 return self.scrollToComment(node, 1);
653 return self.scrollToComment(node, 1);
654 };
654 };
655
655
656 this.prevComment = function(node) {
656 this.prevComment = function(node) {
657 return self.scrollToComment(node, -1);
657 return self.scrollToComment(node, -1);
658 };
658 };
659
659
660 this.nextOutdatedComment = function(node) {
660 this.nextOutdatedComment = function(node) {
661 return self.scrollToComment(node, 1, true);
661 return self.scrollToComment(node, 1, true);
662 };
662 };
663
663
664 this.prevOutdatedComment = function(node) {
664 this.prevOutdatedComment = function(node) {
665 return self.scrollToComment(node, -1, true);
665 return self.scrollToComment(node, -1, true);
666 };
666 };
667
667
668 this.cancelComment = function (node) {
668 this.cancelComment = function (node) {
669 var $node = $(node);
669 var $node = $(node);
670 var edit = $(this).attr('edit');
670 var edit = $(this).attr('edit');
671 var $inlineComments = $node.closest('div.inline-comments');
671 var $inlineComments = $node.closest('div.inline-comments');
672
672
673 if (edit) {
673 if (edit) {
674 var $general_comments = null;
674 var $general_comments = null;
675 if (!$inlineComments.length) {
675 if (!$inlineComments.length) {
676 $general_comments = $('#comments');
676 $general_comments = $('#comments');
677 var $comment = $general_comments.parent().find('div.comment:hidden');
677 var $comment = $general_comments.parent().find('div.comment:hidden');
678 // show hidden general comment form
678 // show hidden general comment form
679 $('#cb-comment-general-form-placeholder').show();
679 $('#cb-comment-general-form-placeholder').show();
680 } else {
680 } else {
681 var $comment = $inlineComments.find('div.comment:hidden');
681 var $comment = $inlineComments.find('div.comment:hidden');
682 }
682 }
683 $comment.show();
683 $comment.show();
684 }
684 }
685 var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
685 var $replyWrapper = $node.closest('.comment-inline-form').closest('.reply-thread-container-wrapper')
686 $replyWrapper.removeClass('comment-form-active');
686 $replyWrapper.removeClass('comment-form-active');
687
687
688 var lastComment = $inlineComments.find('.comment-inline').last();
688 var lastComment = $inlineComments.find('.comment-inline').last();
689 if ($(lastComment).hasClass('comment-outdated')) {
689 if ($(lastComment).hasClass('comment-outdated')) {
690 $replyWrapper.hide();
690 $replyWrapper.hide();
691 }
691 }
692
692
693 $node.closest('.comment-inline-form').remove();
693 $node.closest('.comment-inline-form').remove();
694 return false;
694 return false;
695 };
695 };
696
696
697 this._deleteComment = function(node) {
697 this._deleteComment = function(node) {
698 var $node = $(node);
698 var $node = $(node);
699 var $td = $node.closest('td');
699 var $td = $node.closest('td');
700 var $comment = $node.closest('.comment');
700 var $comment = $node.closest('.comment');
701 var comment_id = $($comment).data('commentId');
701 var comment_id = $($comment).data('commentId');
702 var isDraft = $($comment).data('commentDraft');
702 var isDraft = $($comment).data('commentDraft');
703
703
704 var pullRequestId = templateContext.pull_request_data.pull_request_id;
704 var pullRequestId = templateContext.pull_request_data.pull_request_id;
705 var commitId = templateContext.commit_data.commit_id;
705 var commitId = templateContext.commit_data.commit_id;
706
706
707 if (pullRequestId) {
707 if (pullRequestId) {
708 var url = pyroutes.url('pullrequest_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
708 var url = pyroutes.url('pullrequest_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
709 } else if (commitId) {
709 } else if (commitId) {
710 var url = pyroutes.url('repo_commit_comment_delete', {"comment_id": comment_id, "repo_name": templateContext.repo_name, "commit_id": commitId})
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 var postData = {
713 var postData = {
714 'csrf_token': CSRF_TOKEN
714 'csrf_token': CSRF_TOKEN
715 };
715 };
716
716
717 $comment.addClass('comment-deleting');
717 $comment.addClass('comment-deleting');
718 $comment.hide('fast');
718 $comment.hide('fast');
719
719
720 var success = function(response) {
720 var success = function(response) {
721 $comment.remove();
721 $comment.remove();
722
722
723 if (window.updateSticky !== undefined) {
723 if (window.updateSticky !== undefined) {
724 // potentially our comments change the active window size, so we
724 // potentially our comments change the active window size, so we
725 // notify sticky elements
725 // notify sticky elements
726 updateSticky()
726 updateSticky()
727 }
727 }
728
728
729 if (window.refreshAllComments !== undefined && !isDraft) {
729 if (window.refreshAllComments !== undefined && !isDraft) {
730 // if we have this handler, run it, and refresh all comments boxes
730 // if we have this handler, run it, and refresh all comments boxes
731 refreshAllComments()
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 return false;
737 return false;
734 };
738 };
735
739
736 var failure = function(jqXHR, textStatus, errorThrown) {
740 var failure = function(jqXHR, textStatus, errorThrown) {
737 var prefix = "Error while deleting this comment.\n"
741 var prefix = "Error while deleting this comment.\n"
738 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
742 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
739 ajaxErrorSwal(message);
743 ajaxErrorSwal(message);
740
744
741 $comment.show('fast');
745 $comment.show('fast');
742 $comment.removeClass('comment-deleting');
746 $comment.removeClass('comment-deleting');
743 return false;
747 return false;
744 };
748 };
745 ajaxPOST(url, postData, success, failure);
749 ajaxPOST(url, postData, success, failure);
746
750
747 }
751 }
748
752
749 this.deleteComment = function(node) {
753 this.deleteComment = function(node) {
750 var $comment = $(node).closest('.comment');
754 var $comment = $(node).closest('.comment');
751 var comment_id = $comment.attr('data-comment-id');
755 var comment_id = $comment.attr('data-comment-id');
752
756
753 SwalNoAnimation.fire({
757 SwalNoAnimation.fire({
754 title: 'Delete this comment?',
758 title: 'Delete this comment?',
755 icon: 'warning',
759 icon: 'warning',
756 showCancelButton: true,
760 showCancelButton: true,
757 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
761 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
758
762
759 }).then(function(result) {
763 }).then(function(result) {
760 if (result.value) {
764 if (result.value) {
761 self._deleteComment(node);
765 self._deleteComment(node);
762 }
766 }
763 })
767 })
764 };
768 };
765
769
766 this._finalizeDrafts = function(commentIds) {
770 this._finalizeDrafts = function(commentIds) {
767
771
768 var pullRequestId = templateContext.pull_request_data.pull_request_id;
772 var pullRequestId = templateContext.pull_request_data.pull_request_id;
769 var commitId = templateContext.commit_data.commit_id;
773 var commitId = templateContext.commit_data.commit_id;
770
774
771 if (pullRequestId) {
775 if (pullRequestId) {
772 var url = pyroutes.url('pullrequest_draft_comments_submit', {"repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
776 var url = pyroutes.url('pullrequest_draft_comments_submit', {"repo_name": templateContext.repo_name, "pull_request_id": pullRequestId})
773 } else if (commitId) {
777 } else if (commitId) {
774 var url = pyroutes.url('commit_draft_comments_submit', {"repo_name": templateContext.repo_name, "commit_id": commitId})
778 var url = pyroutes.url('commit_draft_comments_submit', {"repo_name": templateContext.repo_name, "commit_id": commitId})
775 }
779 }
776
780
777 // remove the drafts so we can lock them before submit.
781 // remove the drafts so we can lock them before submit.
778 $.each(commentIds, function(idx, val){
782 $.each(commentIds, function(idx, val){
779 $('#comment-{0}'.format(val)).remove();
783 $('#comment-{0}'.format(val)).remove();
780 })
784 })
781
785
782 var postData = {'comments': commentIds, 'csrf_token': CSRF_TOKEN};
786 var postData = {'comments': commentIds, 'csrf_token': CSRF_TOKEN};
783
787
784 var submitSuccessCallback = function(json_data) {
788 var submitSuccessCallback = function(json_data) {
785 self.attachInlineComment(json_data);
789 self.attachInlineComment(json_data);
786
790
787 if (window.refreshDraftComments !== undefined) {
791 if (window.refreshDraftComments !== undefined) {
788 // if we have this handler, run it, and refresh all comments boxes
792 // if we have this handler, run it, and refresh all comments boxes
789 refreshDraftComments()
793 refreshDraftComments()
790 }
794 }
791
795
792 return false;
796 return false;
793 };
797 };
794
798
795 ajaxPOST(url, postData, submitSuccessCallback)
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 SwalNoAnimation.fire({
805 SwalNoAnimation.fire({
802 title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
806 title: _ngettext('Submit {0} draft comment.', 'Submit {0} draft comments.', commentIds.length).format(commentIds.length),
803 icon: 'warning',
807 icon: 'warning',
804 showCancelButton: true,
808 showCancelButton: true,
805 confirmButtonText: _gettext('Yes'),
809 confirmButtonText: _gettext('Yes'),
806
810
807 }).then(function(result) {
811 }).then(function(result) {
808 if (result.value) {
812 if (result.value) {
813 if (callback !== undefined) {
814 callback(result)
815 }
809 self._finalizeDrafts(commentIds);
816 self._finalizeDrafts(commentIds);
810 }
817 }
811 })
818 })
812 };
819 };
813
820
814 this.toggleWideMode = function (node) {
821 this.toggleWideMode = function (node) {
815
822
816 if ($('#content').hasClass('wrapper')) {
823 if ($('#content').hasClass('wrapper')) {
817 $('#content').removeClass("wrapper");
824 $('#content').removeClass("wrapper");
818 $('#content').addClass("wide-mode-wrapper");
825 $('#content').addClass("wide-mode-wrapper");
819 $(node).addClass('btn-success');
826 $(node).addClass('btn-success');
820 return true
827 return true
821 } else {
828 } else {
822 $('#content').removeClass("wide-mode-wrapper");
829 $('#content').removeClass("wide-mode-wrapper");
823 $('#content').addClass("wrapper");
830 $('#content').addClass("wrapper");
824 $(node).removeClass('btn-success');
831 $(node).removeClass('btn-success');
825 return false
832 return false
826 }
833 }
827
834
828 };
835 };
829
836
830 /**
837 /**
831 * Turn off/on all comments in file diff
838 * Turn off/on all comments in file diff
832 */
839 */
833 this.toggleDiffComments = function(node) {
840 this.toggleDiffComments = function(node) {
834 // Find closes filediff container
841 // Find closes filediff container
835 var $filediff = $(node).closest('.filediff');
842 var $filediff = $(node).closest('.filediff');
836 if ($(node).hasClass('toggle-on')) {
843 if ($(node).hasClass('toggle-on')) {
837 var show = false;
844 var show = false;
838 } else if ($(node).hasClass('toggle-off')) {
845 } else if ($(node).hasClass('toggle-off')) {
839 var show = true;
846 var show = true;
840 }
847 }
841
848
842 // Toggle each individual comment block, so we can un-toggle single ones
849 // Toggle each individual comment block, so we can un-toggle single ones
843 $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
850 $.each($filediff.find('.toggle-comment-action'), function(idx, val) {
844 self.toggleLineComments($(val), show)
851 self.toggleLineComments($(val), show)
845 })
852 })
846
853
847 // since we change the height of the diff container that has anchor points for upper
854 // since we change the height of the diff container that has anchor points for upper
848 // sticky header, we need to tell it to re-calculate those
855 // sticky header, we need to tell it to re-calculate those
849 if (window.updateSticky !== undefined) {
856 if (window.updateSticky !== undefined) {
850 // potentially our comments change the active window size, so we
857 // potentially our comments change the active window size, so we
851 // notify sticky elements
858 // notify sticky elements
852 updateSticky()
859 updateSticky()
853 }
860 }
854
861
855 return false;
862 return false;
856 }
863 }
857
864
858 this.toggleLineComments = function(node, show) {
865 this.toggleLineComments = function(node, show) {
859
866
860 var trElem = $(node).closest('tr')
867 var trElem = $(node).closest('tr')
861
868
862 if (show === true) {
869 if (show === true) {
863 // mark outdated comments as visible before the toggle;
870 // mark outdated comments as visible before the toggle;
864 $(trElem).find('.comment-outdated').show();
871 $(trElem).find('.comment-outdated').show();
865 $(trElem).removeClass('hide-line-comments');
872 $(trElem).removeClass('hide-line-comments');
866 } else if (show === false) {
873 } else if (show === false) {
867 $(trElem).find('.comment-outdated').hide();
874 $(trElem).find('.comment-outdated').hide();
868 $(trElem).addClass('hide-line-comments');
875 $(trElem).addClass('hide-line-comments');
869 } else {
876 } else {
870 // mark outdated comments as visible before the toggle;
877 // mark outdated comments as visible before the toggle;
871 $(trElem).find('.comment-outdated').show();
878 $(trElem).find('.comment-outdated').show();
872 $(trElem).toggleClass('hide-line-comments');
879 $(trElem).toggleClass('hide-line-comments');
873 }
880 }
874
881
875 // since we change the height of the diff container that has anchor points for upper
882 // since we change the height of the diff container that has anchor points for upper
876 // sticky header, we need to tell it to re-calculate those
883 // sticky header, we need to tell it to re-calculate those
877 if (window.updateSticky !== undefined) {
884 if (window.updateSticky !== undefined) {
878 // potentially our comments change the active window size, so we
885 // potentially our comments change the active window size, so we
879 // notify sticky elements
886 // notify sticky elements
880 updateSticky()
887 updateSticky()
881 }
888 }
882
889
883 };
890 };
884
891
885 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
892 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
886 var pullRequestId = templateContext.pull_request_data.pull_request_id;
893 var pullRequestId = templateContext.pull_request_data.pull_request_id;
887 var commitId = templateContext.commit_data.commit_id;
894 var commitId = templateContext.commit_data.commit_id;
888
895
889 var commentForm = new CommentForm(
896 var commentForm = new CommentForm(
890 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
897 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
891 var cm = commentForm.getCmInstance();
898 var cm = commentForm.getCmInstance();
892
899
893 if (resolvesCommentId){
900 if (resolvesCommentId){
894 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
901 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
895 }
902 }
896
903
897 setTimeout(function() {
904 setTimeout(function() {
898 // callbacks
905 // callbacks
899 if (cm !== undefined) {
906 if (cm !== undefined) {
900 commentForm.setPlaceholder(placeholderText);
907 commentForm.setPlaceholder(placeholderText);
901 if (commentForm.isInline()) {
908 if (commentForm.isInline()) {
902 cm.focus();
909 cm.focus();
903 cm.refresh();
910 cm.refresh();
904 }
911 }
905 }
912 }
906 }, 10);
913 }, 10);
907
914
908 // trigger scrolldown to the resolve comment, since it might be away
915 // trigger scrolldown to the resolve comment, since it might be away
909 // from the clicked
916 // from the clicked
910 if (resolvesCommentId){
917 if (resolvesCommentId){
911 var actionNode = $(commentForm.resolvesActionId).offset();
918 var actionNode = $(commentForm.resolvesActionId).offset();
912
919
913 setTimeout(function() {
920 setTimeout(function() {
914 if (actionNode) {
921 if (actionNode) {
915 $('body, html').animate({scrollTop: actionNode.top}, 10);
922 $('body, html').animate({scrollTop: actionNode.top}, 10);
916 }
923 }
917 }, 100);
924 }, 100);
918 }
925 }
919
926
920 // add dropzone support
927 // add dropzone support
921 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
928 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
922 var renderer = templateContext.visual.default_renderer;
929 var renderer = templateContext.visual.default_renderer;
923 if (renderer == 'rst') {
930 if (renderer == 'rst') {
924 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
931 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
925 if (isRendered){
932 if (isRendered){
926 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
933 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
927 }
934 }
928 } else if (renderer == 'markdown') {
935 } else if (renderer == 'markdown') {
929 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
936 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
930 if (isRendered){
937 if (isRendered){
931 attachmentUrl = '!' + attachmentUrl;
938 attachmentUrl = '!' + attachmentUrl;
932 }
939 }
933 } else {
940 } else {
934 var attachmentUrl = '{}'.format(attachmentStoreUrl);
941 var attachmentUrl = '{}'.format(attachmentStoreUrl);
935 }
942 }
936 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
943 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
937
944
938 return false;
945 return false;
939 };
946 };
940
947
941 //see: https://www.dropzonejs.com/#configuration
948 //see: https://www.dropzonejs.com/#configuration
942 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
949 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
943 {'repo_name': templateContext.repo_name,
950 {'repo_name': templateContext.repo_name,
944 'commit_id': templateContext.commit_data.commit_id})
951 'commit_id': templateContext.commit_data.commit_id})
945
952
946 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
953 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
947 if (previewTmpl !== undefined){
954 if (previewTmpl !== undefined){
948 var selectLink = $(formElement).find('.pick-attachment').get(0);
955 var selectLink = $(formElement).find('.pick-attachment').get(0);
949 $(formElement).find('.comment-attachment-uploader').dropzone({
956 $(formElement).find('.comment-attachment-uploader').dropzone({
950 url: storeUrl,
957 url: storeUrl,
951 headers: {"X-CSRF-Token": CSRF_TOKEN},
958 headers: {"X-CSRF-Token": CSRF_TOKEN},
952 paramName: function () {
959 paramName: function () {
953 return "attachment"
960 return "attachment"
954 }, // The name that will be used to transfer the file
961 }, // The name that will be used to transfer the file
955 clickable: selectLink,
962 clickable: selectLink,
956 parallelUploads: 1,
963 parallelUploads: 1,
957 maxFiles: 10,
964 maxFiles: 10,
958 maxFilesize: templateContext.attachment_store.max_file_size_mb,
965 maxFilesize: templateContext.attachment_store.max_file_size_mb,
959 uploadMultiple: false,
966 uploadMultiple: false,
960 autoProcessQueue: true, // if false queue will not be processed automatically.
967 autoProcessQueue: true, // if false queue will not be processed automatically.
961 createImageThumbnails: false,
968 createImageThumbnails: false,
962 previewTemplate: previewTmpl.innerHTML,
969 previewTemplate: previewTmpl.innerHTML,
963
970
964 accept: function (file, done) {
971 accept: function (file, done) {
965 done();
972 done();
966 },
973 },
967 init: function () {
974 init: function () {
968
975
969 this.on("sending", function (file, xhr, formData) {
976 this.on("sending", function (file, xhr, formData) {
970 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
977 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
971 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
978 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
972 });
979 });
973
980
974 this.on("success", function (file, response) {
981 this.on("success", function (file, response) {
975 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
982 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
976 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
983 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
977
984
978 var isRendered = false;
985 var isRendered = false;
979 var ext = file.name.split('.').pop();
986 var ext = file.name.split('.').pop();
980 var imageExts = templateContext.attachment_store.image_ext;
987 var imageExts = templateContext.attachment_store.image_ext;
981 if (imageExts.indexOf(ext) !== -1){
988 if (imageExts.indexOf(ext) !== -1){
982 isRendered = true;
989 isRendered = true;
983 }
990 }
984
991
985 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
992 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
986 });
993 });
987
994
988 this.on("error", function (file, errorMessage, xhr) {
995 this.on("error", function (file, errorMessage, xhr) {
989 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
996 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
990
997
991 var error = null;
998 var error = null;
992
999
993 if (xhr !== undefined){
1000 if (xhr !== undefined){
994 var httpStatus = xhr.status + " " + xhr.statusText;
1001 var httpStatus = xhr.status + " " + xhr.statusText;
995 if (xhr !== undefined && xhr.status >= 500) {
1002 if (xhr !== undefined && xhr.status >= 500) {
996 error = httpStatus;
1003 error = httpStatus;
997 }
1004 }
998 }
1005 }
999
1006
1000 if (error === null) {
1007 if (error === null) {
1001 error = errorMessage.error || errorMessage || httpStatus;
1008 error = errorMessage.error || errorMessage || httpStatus;
1002 }
1009 }
1003 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
1010 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
1004
1011
1005 });
1012 });
1006 }
1013 }
1007 });
1014 });
1008 }
1015 }
1009 return commentForm;
1016 return commentForm;
1010 };
1017 };
1011
1018
1012 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
1019 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
1013
1020
1014 var tmpl = $('#cb-comment-general-form-template').html();
1021 var tmpl = $('#cb-comment-general-form-template').html();
1015 tmpl = tmpl.format(null, 'general');
1022 tmpl = tmpl.format(null, 'general');
1016 var $form = $(tmpl);
1023 var $form = $(tmpl);
1017
1024
1018 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
1025 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
1019 var curForm = $formPlaceholder.find('form');
1026 var curForm = $formPlaceholder.find('form');
1020 if (curForm){
1027 if (curForm){
1021 curForm.remove();
1028 curForm.remove();
1022 }
1029 }
1023 $formPlaceholder.append($form);
1030 $formPlaceholder.append($form);
1024
1031
1025 var _form = $($form[0]);
1032 var _form = $($form[0]);
1026 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
1033 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
1027 var edit = false;
1034 var edit = false;
1028 var comment_id = null;
1035 var comment_id = null;
1029 var commentForm = this.createCommentForm(
1036 var commentForm = this.createCommentForm(
1030 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
1037 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
1031 commentForm.initStatusChangeSelector();
1038 commentForm.initStatusChangeSelector();
1032
1039
1033 return commentForm;
1040 return commentForm;
1034 };
1041 };
1035
1042
1036 this.editComment = function(node, line_no, f_path) {
1043 this.editComment = function(node, line_no, f_path) {
1037 self.edit = true;
1044 self.edit = true;
1038 var $node = $(node);
1045 var $node = $(node);
1039 var $td = $node.closest('td');
1046 var $td = $node.closest('td');
1040
1047
1041 var $comment = $(node).closest('.comment');
1048 var $comment = $(node).closest('.comment');
1042 var comment_id = $($comment).data('commentId');
1049 var comment_id = $($comment).data('commentId');
1043 var isDraft = $($comment).data('commentDraft');
1050 var isDraft = $($comment).data('commentDraft');
1044 var $editForm = null
1051 var $editForm = null
1045
1052
1046 var $comments = $node.closest('div.inline-comments');
1053 var $comments = $node.closest('div.inline-comments');
1047 var $general_comments = null;
1054 var $general_comments = null;
1048
1055
1049 if($comments.length){
1056 if($comments.length){
1050 // inline comments setup
1057 // inline comments setup
1051 $editForm = $comments.find('.comment-inline-form');
1058 $editForm = $comments.find('.comment-inline-form');
1052 line_no = self.getLineNumber(node)
1059 line_no = self.getLineNumber(node)
1053 }
1060 }
1054 else{
1061 else{
1055 // general comments setup
1062 // general comments setup
1056 $comments = $('#comments');
1063 $comments = $('#comments');
1057 $editForm = $comments.find('.comment-inline-form');
1064 $editForm = $comments.find('.comment-inline-form');
1058 line_no = $comment[0].id
1065 line_no = $comment[0].id
1059 $('#cb-comment-general-form-placeholder').hide();
1066 $('#cb-comment-general-form-placeholder').hide();
1060 }
1067 }
1061
1068
1062 if ($editForm.length === 0) {
1069 if ($editForm.length === 0) {
1063
1070
1064 // unhide all comments if they are hidden for a proper REPLY mode
1071 // unhide all comments if they are hidden for a proper REPLY mode
1065 var $filediff = $node.closest('.filediff');
1072 var $filediff = $node.closest('.filediff');
1066 $filediff.removeClass('hide-comments');
1073 $filediff.removeClass('hide-comments');
1067
1074
1068 $editForm = self.createNewFormWrapper(f_path, line_no);
1075 $editForm = self.createNewFormWrapper(f_path, line_no);
1069 if(f_path && line_no) {
1076 if(f_path && line_no) {
1070 $editForm.addClass('comment-inline-form-edit')
1077 $editForm.addClass('comment-inline-form-edit')
1071 }
1078 }
1072
1079
1073 $comment.after($editForm)
1080 $comment.after($editForm)
1074
1081
1075 var _form = $($editForm[0]).find('form');
1082 var _form = $($editForm[0]).find('form');
1076 var autocompleteActions = ['as_note',];
1083 var autocompleteActions = ['as_note',];
1077 var commentForm = this.createCommentForm(
1084 var commentForm = this.createCommentForm(
1078 _form, line_no, '', autocompleteActions, resolvesCommentId,
1085 _form, line_no, '', autocompleteActions, resolvesCommentId,
1079 this.edit, comment_id);
1086 this.edit, comment_id);
1080 var old_comment_text_binary = $comment.attr('data-comment-text');
1087 var old_comment_text_binary = $comment.attr('data-comment-text');
1081 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1088 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
1082 commentForm.cm.setValue(old_comment_text);
1089 commentForm.cm.setValue(old_comment_text);
1083 $comment.hide();
1090 $comment.hide();
1084 tooltipActivate();
1091 tooltipActivate();
1085
1092
1086 // set a CUSTOM submit handler for inline comment edit action.
1093 // set a CUSTOM submit handler for inline comment edit action.
1087 commentForm.setHandleFormSubmit(function(o) {
1094 commentForm.setHandleFormSubmit(function(o) {
1088 var text = commentForm.cm.getValue();
1095 var text = commentForm.cm.getValue();
1089 var commentType = commentForm.getCommentType();
1096 var commentType = commentForm.getCommentType();
1090
1097
1091 if (text === "") {
1098 if (text === "") {
1092 return;
1099 return;
1093 }
1100 }
1094
1101
1095 if (old_comment_text == text) {
1102 if (old_comment_text == text) {
1096 SwalNoAnimation.fire({
1103 SwalNoAnimation.fire({
1097 title: 'Unable to edit comment',
1104 title: 'Unable to edit comment',
1098 html: _gettext('Comment body was not changed.'),
1105 html: _gettext('Comment body was not changed.'),
1099 });
1106 });
1100 return;
1107 return;
1101 }
1108 }
1102 var excludeCancelBtn = false;
1109 var excludeCancelBtn = false;
1103 var submitEvent = true;
1110 var submitEvent = true;
1104 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1111 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1105 commentForm.cm.setOption("readOnly", true);
1112 commentForm.cm.setOption("readOnly", true);
1106
1113
1107 // Read last version known
1114 // Read last version known
1108 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
1115 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
1109 var version = versionSelector.data('lastVersion');
1116 var version = versionSelector.data('lastVersion');
1110
1117
1111 if (!version) {
1118 if (!version) {
1112 version = 0;
1119 version = 0;
1113 }
1120 }
1114
1121
1115 var postData = {
1122 var postData = {
1116 'text': text,
1123 'text': text,
1117 'f_path': f_path,
1124 'f_path': f_path,
1118 'line': line_no,
1125 'line': line_no,
1119 'comment_type': commentType,
1126 'comment_type': commentType,
1120 'draft': isDraft,
1127 'draft': isDraft,
1121 'version': version,
1128 'version': version,
1122 'csrf_token': CSRF_TOKEN
1129 'csrf_token': CSRF_TOKEN
1123 };
1130 };
1124
1131
1125 var submitSuccessCallback = function(json_data) {
1132 var submitSuccessCallback = function(json_data) {
1126 $editForm.remove();
1133 $editForm.remove();
1127 $comment.show();
1134 $comment.show();
1128 var postData = {
1135 var postData = {
1129 'text': text,
1136 'text': text,
1130 'renderer': $comment.attr('data-comment-renderer'),
1137 'renderer': $comment.attr('data-comment-renderer'),
1131 'csrf_token': CSRF_TOKEN
1138 'csrf_token': CSRF_TOKEN
1132 };
1139 };
1133
1140
1134 /* Inject new edited version selector */
1141 /* Inject new edited version selector */
1135 var updateCommentVersionDropDown = function () {
1142 var updateCommentVersionDropDown = function () {
1136 var versionSelectId = '#comment_versions_'+comment_id;
1143 var versionSelectId = '#comment_versions_'+comment_id;
1137 var preLoadVersionData = [
1144 var preLoadVersionData = [
1138 {
1145 {
1139 id: json_data['comment_version'],
1146 id: json_data['comment_version'],
1140 text: "v{0}".format(json_data['comment_version']),
1147 text: "v{0}".format(json_data['comment_version']),
1141 action: function () {
1148 action: function () {
1142 Rhodecode.comments.showVersion(
1149 Rhodecode.comments.showVersion(
1143 json_data['comment_id'],
1150 json_data['comment_id'],
1144 json_data['comment_history_id']
1151 json_data['comment_history_id']
1145 )
1152 )
1146 },
1153 },
1147 comment_version: json_data['comment_version'],
1154 comment_version: json_data['comment_version'],
1148 comment_author_username: json_data['comment_author_username'],
1155 comment_author_username: json_data['comment_author_username'],
1149 comment_author_gravatar: json_data['comment_author_gravatar'],
1156 comment_author_gravatar: json_data['comment_author_gravatar'],
1150 comment_created_on: json_data['comment_created_on'],
1157 comment_created_on: json_data['comment_created_on'],
1151 },
1158 },
1152 ]
1159 ]
1153
1160
1154
1161
1155 if ($(versionSelectId).data('select2')) {
1162 if ($(versionSelectId).data('select2')) {
1156 var oldData = $(versionSelectId).data('select2').opts.data.results;
1163 var oldData = $(versionSelectId).data('select2').opts.data.results;
1157 $(versionSelectId).select2("destroy");
1164 $(versionSelectId).select2("destroy");
1158 preLoadVersionData = oldData.concat(preLoadVersionData)
1165 preLoadVersionData = oldData.concat(preLoadVersionData)
1159 }
1166 }
1160
1167
1161 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1168 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1162
1169
1163 $comment.attr('data-comment-text', utf8ToB64(text));
1170 $comment.attr('data-comment-text', utf8ToB64(text));
1164
1171
1165 var versionSelector = $('#comment_versions_'+comment_id);
1172 var versionSelector = $('#comment_versions_'+comment_id);
1166
1173
1167 // set lastVersion so we know our last edit version
1174 // set lastVersion so we know our last edit version
1168 versionSelector.data('lastVersion', json_data['comment_version'])
1175 versionSelector.data('lastVersion', json_data['comment_version'])
1169 versionSelector.parent().show();
1176 versionSelector.parent().show();
1170 }
1177 }
1171 updateCommentVersionDropDown();
1178 updateCommentVersionDropDown();
1172
1179
1173 // by default we reset state of comment preserving the text
1180 // by default we reset state of comment preserving the text
1174 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1181 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1175 var prefix = "Error while editing this comment.\n"
1182 var prefix = "Error while editing this comment.\n"
1176 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1183 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1177 ajaxErrorSwal(message);
1184 ajaxErrorSwal(message);
1178 };
1185 };
1179
1186
1180 var successRenderCommit = function(o){
1187 var successRenderCommit = function(o){
1181 $comment.show();
1188 $comment.show();
1182 $comment[0].lastElementChild.innerHTML = o;
1189 $comment[0].lastElementChild.innerHTML = o;
1183 };
1190 };
1184
1191
1185 var previewUrl = pyroutes.url(
1192 var previewUrl = pyroutes.url(
1186 'repo_commit_comment_preview',
1193 'repo_commit_comment_preview',
1187 {'repo_name': templateContext.repo_name,
1194 {'repo_name': templateContext.repo_name,
1188 'commit_id': templateContext.commit_data.commit_id});
1195 'commit_id': templateContext.commit_data.commit_id});
1189
1196
1190 _submitAjaxPOST(
1197 _submitAjaxPOST(
1191 previewUrl, postData, successRenderCommit, failRenderCommit
1198 previewUrl, postData, successRenderCommit, failRenderCommit
1192 );
1199 );
1193
1200
1194 try {
1201 try {
1195 var html = json_data.rendered_text;
1202 var html = json_data.rendered_text;
1196 var lineno = json_data.line_no;
1203 var lineno = json_data.line_no;
1197 var target_id = json_data.target_id;
1204 var target_id = json_data.target_id;
1198
1205
1199 $comments.find('.cb-comment-add-button').before(html);
1206 $comments.find('.cb-comment-add-button').before(html);
1200
1207
1201 // run global callback on submit
1208 // run global callback on submit
1202 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1209 commentForm.globalSubmitSuccessCallback({draft: isDraft, comment_id: comment_id});
1203
1210
1204 } catch (e) {
1211 } catch (e) {
1205 console.error(e);
1212 console.error(e);
1206 }
1213 }
1207
1214
1208 // re trigger the linkification of next/prev navigation
1215 // re trigger the linkification of next/prev navigation
1209 linkifyComments($('.inline-comment-injected'));
1216 linkifyComments($('.inline-comment-injected'));
1210 timeagoActivate();
1217 timeagoActivate();
1211 tooltipActivate();
1218 tooltipActivate();
1212
1219
1213 if (window.updateSticky !== undefined) {
1220 if (window.updateSticky !== undefined) {
1214 // potentially our comments change the active window size, so we
1221 // potentially our comments change the active window size, so we
1215 // notify sticky elements
1222 // notify sticky elements
1216 updateSticky()
1223 updateSticky()
1217 }
1224 }
1218
1225
1219 if (window.refreshAllComments !== undefined && !isDraft) {
1226 if (window.refreshAllComments !== undefined && !isDraft) {
1220 // if we have this handler, run it, and refresh all comments boxes
1227 // if we have this handler, run it, and refresh all comments boxes
1221 refreshAllComments()
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 commentForm.setActionButtonsDisabled(false);
1235 commentForm.setActionButtonsDisabled(false);
1225
1236
1226 };
1237 };
1227
1238
1228 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1239 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1229 var prefix = "Error while editing comment.\n"
1240 var prefix = "Error while editing comment.\n"
1230 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1241 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1231 if (jqXHR.status == 409){
1242 if (jqXHR.status == 409){
1232 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1243 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1233 ajaxErrorSwal(message, 'Comment version mismatch.');
1244 ajaxErrorSwal(message, 'Comment version mismatch.');
1234 } else {
1245 } else {
1235 ajaxErrorSwal(message);
1246 ajaxErrorSwal(message);
1236 }
1247 }
1237
1248
1238 commentForm.resetCommentFormState(text)
1249 commentForm.resetCommentFormState(text)
1239 };
1250 };
1240 commentForm.submitAjaxPOST(
1251 commentForm.submitAjaxPOST(
1241 commentForm.submitUrl, postData,
1252 commentForm.submitUrl, postData,
1242 submitSuccessCallback,
1253 submitSuccessCallback,
1243 submitFailCallback);
1254 submitFailCallback);
1244 });
1255 });
1245 }
1256 }
1246
1257
1247 $editForm.addClass('comment-inline-form-open');
1258 $editForm.addClass('comment-inline-form-open');
1248 };
1259 };
1249
1260
1250 this.attachComment = function(json_data) {
1261 this.attachComment = function(json_data) {
1251 var self = this;
1262 var self = this;
1252 $.each(json_data, function(idx, val) {
1263 $.each(json_data, function(idx, val) {
1253 var json_data_elem = [val]
1264 var json_data_elem = [val]
1254 var isInline = val.comment_f_path && val.comment_lineno
1265 var isInline = val.comment_f_path && val.comment_lineno
1255
1266
1256 if (isInline) {
1267 if (isInline) {
1257 self.attachInlineComment(json_data_elem)
1268 self.attachInlineComment(json_data_elem)
1258 } else {
1269 } else {
1259 self.attachGeneralComment(json_data_elem)
1270 self.attachGeneralComment(json_data_elem)
1260 }
1271 }
1261 })
1272 })
1262
1273
1263 }
1274 }
1264
1275
1265 this.attachGeneralComment = function(json_data) {
1276 this.attachGeneralComment = function(json_data) {
1266 $.each(json_data, function(idx, val) {
1277 $.each(json_data, function(idx, val) {
1267 $('#injected_page_comments').append(val.rendered_text);
1278 $('#injected_page_comments').append(val.rendered_text);
1268 })
1279 })
1269 }
1280 }
1270
1281
1271 this.attachInlineComment = function(json_data) {
1282 this.attachInlineComment = function(json_data) {
1272
1283
1273 $.each(json_data, function (idx, val) {
1284 $.each(json_data, function (idx, val) {
1274 var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
1285 var line_qry = '*[data-line-no="{0}"]'.format(val.line_no);
1275 var html = val.rendered_text;
1286 var html = val.rendered_text;
1276 var $inlineComments = $('#' + val.target_id)
1287 var $inlineComments = $('#' + val.target_id)
1277 .find(line_qry)
1288 .find(line_qry)
1278 .find('.inline-comments');
1289 .find('.inline-comments');
1279
1290
1280 var lastComment = $inlineComments.find('.comment-inline').last();
1291 var lastComment = $inlineComments.find('.comment-inline').last();
1281
1292
1282 if (lastComment.length === 0) {
1293 if (lastComment.length === 0) {
1283 // first comment, we append simply
1294 // first comment, we append simply
1284 $inlineComments.find('.reply-thread-container-wrapper').before(html);
1295 $inlineComments.find('.reply-thread-container-wrapper').before(html);
1285 } else {
1296 } else {
1286 $(lastComment).after(html)
1297 $(lastComment).after(html)
1287 }
1298 }
1288
1299
1289 })
1300 })
1290
1301
1291 };
1302 };
1292
1303
1293 this.createNewFormWrapper = function(f_path, line_no) {
1304 this.createNewFormWrapper = function(f_path, line_no) {
1294 // create a new reply HTML form from template
1305 // create a new reply HTML form from template
1295 var tmpl = $('#cb-comment-inline-form-template').html();
1306 var tmpl = $('#cb-comment-inline-form-template').html();
1296 tmpl = tmpl.format(escapeHtml(f_path), line_no);
1307 tmpl = tmpl.format(escapeHtml(f_path), line_no);
1297 return $(tmpl);
1308 return $(tmpl);
1298 }
1309 }
1299
1310
1300 this.createComment = function(node, f_path, line_no, resolutionComment) {
1311 this.createComment = function(node, f_path, line_no, resolutionComment) {
1301 self.edit = false;
1312 self.edit = false;
1302 var $node = $(node);
1313 var $node = $(node);
1303 var $td = $node.closest('td');
1314 var $td = $node.closest('td');
1304 var resolvesCommentId = resolutionComment || null;
1315 var resolvesCommentId = resolutionComment || null;
1305
1316
1306 var $replyForm = $td.find('.comment-inline-form');
1317 var $replyForm = $td.find('.comment-inline-form');
1307
1318
1308 // if form isn't existing, we're generating a new one and injecting it.
1319 // if form isn't existing, we're generating a new one and injecting it.
1309 if ($replyForm.length === 0) {
1320 if ($replyForm.length === 0) {
1310
1321
1311 // unhide/expand all comments if they are hidden for a proper REPLY mode
1322 // unhide/expand all comments if they are hidden for a proper REPLY mode
1312 self.toggleLineComments($node, true);
1323 self.toggleLineComments($node, true);
1313
1324
1314 $replyForm = self.createNewFormWrapper(f_path, line_no);
1325 $replyForm = self.createNewFormWrapper(f_path, line_no);
1315
1326
1316 var $comments = $td.find('.inline-comments');
1327 var $comments = $td.find('.inline-comments');
1317
1328
1318 // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
1329 // There aren't any comments, we init the `.inline-comments` with `reply-thread-container` first
1319 if ($comments.length===0) {
1330 if ($comments.length===0) {
1320 var replBtn = '<button class="cb-comment-add-button" onclick="return Rhodecode.comments.createComment(this, \'{0}\', \'{1}\', null)">Reply...</button>'.format(f_path, line_no)
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 var $reply_container = $('#cb-comments-inline-container-template')
1332 var $reply_container = $('#cb-comments-inline-container-template')
1322 $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
1333 $reply_container.find('button.cb-comment-add-button').replaceWith(replBtn);
1323 $td.append($($reply_container).html());
1334 $td.append($($reply_container).html());
1324 }
1335 }
1325
1336
1326 // default comment button exists, so we prepend the form for leaving initial comment
1337 // default comment button exists, so we prepend the form for leaving initial comment
1327 $td.find('.cb-comment-add-button').before($replyForm);
1338 $td.find('.cb-comment-add-button').before($replyForm);
1328 // set marker, that we have a open form
1339 // set marker, that we have a open form
1329 var $replyWrapper = $td.find('.reply-thread-container-wrapper')
1340 var $replyWrapper = $td.find('.reply-thread-container-wrapper')
1330 $replyWrapper.addClass('comment-form-active');
1341 $replyWrapper.addClass('comment-form-active');
1331
1342
1332 var lastComment = $comments.find('.comment-inline').last();
1343 var lastComment = $comments.find('.comment-inline').last();
1333 if ($(lastComment).hasClass('comment-outdated')) {
1344 if ($(lastComment).hasClass('comment-outdated')) {
1334 $replyWrapper.show();
1345 $replyWrapper.show();
1335 }
1346 }
1336
1347
1337 var _form = $($replyForm[0]).find('form');
1348 var _form = $($replyForm[0]).find('form');
1338 var autocompleteActions = ['as_note', 'as_todo'];
1349 var autocompleteActions = ['as_note', 'as_todo'];
1339 var comment_id=null;
1350 var comment_id=null;
1340 var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
1351 var placeholderText = _gettext('Leave a comment on file {0} line {1}.').format(f_path, line_no);
1341 var commentForm = self.createCommentForm(
1352 var commentForm = self.createCommentForm(
1342 _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
1353 _form, line_no, placeholderText, autocompleteActions, resolvesCommentId,
1343 self.edit, comment_id);
1354 self.edit, comment_id);
1344
1355
1345 // set a CUSTOM submit handler for inline comments.
1356 // set a CUSTOM submit handler for inline comments.
1346 commentForm.setHandleFormSubmit(function(o) {
1357 commentForm.setHandleFormSubmit(function(o) {
1347 var text = commentForm.cm.getValue();
1358 var text = commentForm.cm.getValue();
1348 var commentType = commentForm.getCommentType();
1359 var commentType = commentForm.getCommentType();
1349 var resolvesCommentId = commentForm.getResolvesId();
1360 var resolvesCommentId = commentForm.getResolvesId();
1350 var isDraft = commentForm.getDraftState();
1361 var isDraft = commentForm.getDraftState();
1351
1362
1352 if (text === "") {
1363 if (text === "") {
1353 return;
1364 return;
1354 }
1365 }
1355
1366
1356 if (line_no === undefined) {
1367 if (line_no === undefined) {
1357 alert('Error: unable to fetch line number for this inline comment !');
1368 alert('Error: unable to fetch line number for this inline comment !');
1358 return;
1369 return;
1359 }
1370 }
1360
1371
1361 if (f_path === undefined) {
1372 if (f_path === undefined) {
1362 alert('Error: unable to fetch file path for this inline comment !');
1373 alert('Error: unable to fetch file path for this inline comment !');
1363 return;
1374 return;
1364 }
1375 }
1365
1376
1366 var excludeCancelBtn = false;
1377 var excludeCancelBtn = false;
1367 var submitEvent = true;
1378 var submitEvent = true;
1368 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1379 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1369 commentForm.cm.setOption("readOnly", true);
1380 commentForm.cm.setOption("readOnly", true);
1370 var postData = {
1381 var postData = {
1371 'text': text,
1382 'text': text,
1372 'f_path': f_path,
1383 'f_path': f_path,
1373 'line': line_no,
1384 'line': line_no,
1374 'comment_type': commentType,
1385 'comment_type': commentType,
1375 'draft': isDraft,
1386 'draft': isDraft,
1376 'csrf_token': CSRF_TOKEN
1387 'csrf_token': CSRF_TOKEN
1377 };
1388 };
1378 if (resolvesCommentId){
1389 if (resolvesCommentId){
1379 postData['resolves_comment_id'] = resolvesCommentId;
1390 postData['resolves_comment_id'] = resolvesCommentId;
1380 }
1391 }
1381
1392
1382 // submitSuccess for inline commits
1393 // submitSuccess for inline commits
1383 var submitSuccessCallback = function(json_data) {
1394 var submitSuccessCallback = function(json_data) {
1384
1395
1385 $replyForm.remove();
1396 $replyForm.remove();
1386 $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
1397 $td.find('.reply-thread-container-wrapper').removeClass('comment-form-active');
1387
1398
1388 try {
1399 try {
1389
1400
1390 // inject newly created comments, json_data is {<comment_id>: {}}
1401 // inject newly created comments, json_data is {<comment_id>: {}}
1391 self.attachInlineComment(json_data)
1402 self.attachInlineComment(json_data)
1392
1403
1393 //mark visually which comment was resolved
1404 //mark visually which comment was resolved
1394 if (resolvesCommentId) {
1405 if (resolvesCommentId) {
1395 commentForm.markCommentResolved(resolvesCommentId);
1406 commentForm.markCommentResolved(resolvesCommentId);
1396 }
1407 }
1397
1408
1398 // run global callback on submit
1409 // run global callback on submit
1399 commentForm.globalSubmitSuccessCallback({
1410 commentForm.globalSubmitSuccessCallback({
1400 draft: isDraft,
1411 draft: isDraft,
1401 comment_id: comment_id
1412 comment_id: comment_id
1402 });
1413 });
1403
1414
1404 } catch (e) {
1415 } catch (e) {
1405 console.error(e);
1416 console.error(e);
1406 }
1417 }
1407
1418
1408 if (window.updateSticky !== undefined) {
1419 if (window.updateSticky !== undefined) {
1409 // potentially our comments change the active window size, so we
1420 // potentially our comments change the active window size, so we
1410 // notify sticky elements
1421 // notify sticky elements
1411 updateSticky()
1422 updateSticky()
1412 }
1423 }
1413
1424
1414 if (window.refreshAllComments !== undefined && !isDraft) {
1425 if (window.refreshAllComments !== undefined && !isDraft) {
1415 // if we have this handler, run it, and refresh all comments boxes
1426 // if we have this handler, run it, and refresh all comments boxes
1416 refreshAllComments()
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 commentForm.setActionButtonsDisabled(false);
1434 commentForm.setActionButtonsDisabled(false);
1420
1435
1421 // re trigger the linkification of next/prev navigation
1436 // re trigger the linkification of next/prev navigation
1422 linkifyComments($('.inline-comment-injected'));
1437 linkifyComments($('.inline-comment-injected'));
1423 timeagoActivate();
1438 timeagoActivate();
1424 tooltipActivate();
1439 tooltipActivate();
1425 };
1440 };
1426
1441
1427 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1442 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1428 var prefix = "Error while submitting comment.\n"
1443 var prefix = "Error while submitting comment.\n"
1429 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1444 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1430 ajaxErrorSwal(message);
1445 ajaxErrorSwal(message);
1431 commentForm.resetCommentFormState(text)
1446 commentForm.resetCommentFormState(text)
1432 };
1447 };
1433
1448
1434 commentForm.submitAjaxPOST(
1449 commentForm.submitAjaxPOST(
1435 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1450 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1436 });
1451 });
1437 }
1452 }
1438
1453
1439 // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
1454 // Finally "open" our reply form, since we know there are comments and we have the "attached" old form
1440 $replyForm.addClass('comment-inline-form-open');
1455 $replyForm.addClass('comment-inline-form-open');
1441 tooltipActivate();
1456 tooltipActivate();
1442 };
1457 };
1443
1458
1444 this.createResolutionComment = function(commentId){
1459 this.createResolutionComment = function(commentId){
1445 // hide the trigger text
1460 // hide the trigger text
1446 $('#resolve-comment-{0}'.format(commentId)).hide();
1461 $('#resolve-comment-{0}'.format(commentId)).hide();
1447
1462
1448 var comment = $('#comment-'+commentId);
1463 var comment = $('#comment-'+commentId);
1449 var commentData = comment.data();
1464 var commentData = comment.data();
1450 if (commentData.commentInline) {
1465 if (commentData.commentInline) {
1451 var f_path = commentData.fPath;
1466 var f_path = commentData.fPath;
1452 var line_no = commentData.lineNo;
1467 var line_no = commentData.lineNo;
1453 //TODO check this if we need to give f_path/line_no
1468 //TODO check this if we need to give f_path/line_no
1454 this.createComment(comment, f_path, line_no, commentId)
1469 this.createComment(comment, f_path, line_no, commentId)
1455 } else {
1470 } else {
1456 this.createGeneralComment('general', "$placeholder", commentId)
1471 this.createGeneralComment('general', "$placeholder", commentId)
1457 }
1472 }
1458
1473
1459 return false;
1474 return false;
1460 };
1475 };
1461
1476
1462 this.submitResolution = function(commentId){
1477 this.submitResolution = function(commentId){
1463 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1478 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1464 var commentForm = form.get(0).CommentForm;
1479 var commentForm = form.get(0).CommentForm;
1465
1480
1466 var cm = commentForm.getCmInstance();
1481 var cm = commentForm.getCmInstance();
1467 var renderer = templateContext.visual.default_renderer;
1482 var renderer = templateContext.visual.default_renderer;
1468 if (renderer == 'rst'){
1483 if (renderer == 'rst'){
1469 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1484 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1470 } else if (renderer == 'markdown') {
1485 } else if (renderer == 'markdown') {
1471 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1486 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1472 } else {
1487 } else {
1473 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1488 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1474 }
1489 }
1475
1490
1476 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1491 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1477 form.submit();
1492 form.submit();
1478 return false;
1493 return false;
1479 };
1494 };
1480
1495
1481 };
1496 };
1482
1497
1483 window.commentHelp = function(renderer) {
1498 window.commentHelp = function(renderer) {
1484 var funcData = {'renderer': renderer}
1499 var funcData = {'renderer': renderer}
1485 return renderTemplate('commentHelpHovercard', funcData)
1500 return renderTemplate('commentHelpHovercard', funcData)
1486 } No newline at end of file
1501 }
@@ -1,1146 +1,1170 b''
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
1 // # Copyright (C) 2010-2020 RhodeCode GmbH
2 // #
2 // #
3 // # This program is free software: you can redistribute it and/or modify
3 // # This program is free software: you can redistribute it and/or modify
4 // # it under the terms of the GNU Affero General Public License, version 3
4 // # it under the terms of the GNU Affero General Public License, version 3
5 // # (only), as published by the Free Software Foundation.
5 // # (only), as published by the Free Software Foundation.
6 // #
6 // #
7 // # This program is distributed in the hope that it will be useful,
7 // # This program is distributed in the hope that it will be useful,
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 // # GNU General Public License for more details.
10 // # GNU General Public License for more details.
11 // #
11 // #
12 // # You should have received a copy of the GNU Affero General Public License
12 // # You should have received a copy of the GNU Affero General Public License
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 // #
14 // #
15 // # This program is dual-licensed. If you wish to learn more about the
15 // # This program is dual-licensed. If you wish to learn more about the
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18
18
19
19
20 var prButtonLockChecks = {
20 var prButtonLockChecks = {
21 'compare': false,
21 'compare': false,
22 'reviewers': false
22 'reviewers': false
23 };
23 };
24
24
25 /**
25 /**
26 * lock button until all checks and loads are made. E.g reviewer calculation
26 * lock button until all checks and loads are made. E.g reviewer calculation
27 * should prevent from submitting a PR
27 * should prevent from submitting a PR
28 * @param lockEnabled
28 * @param lockEnabled
29 * @param msg
29 * @param msg
30 * @param scope
30 * @param scope
31 */
31 */
32 var prButtonLock = function(lockEnabled, msg, scope) {
32 var prButtonLock = function(lockEnabled, msg, scope) {
33 scope = scope || 'all';
33 scope = scope || 'all';
34 if (scope == 'all'){
34 if (scope == 'all'){
35 prButtonLockChecks['compare'] = !lockEnabled;
35 prButtonLockChecks['compare'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
36 prButtonLockChecks['reviewers'] = !lockEnabled;
37 } else if (scope == 'compare') {
37 } else if (scope == 'compare') {
38 prButtonLockChecks['compare'] = !lockEnabled;
38 prButtonLockChecks['compare'] = !lockEnabled;
39 } else if (scope == 'reviewers'){
39 } else if (scope == 'reviewers'){
40 prButtonLockChecks['reviewers'] = !lockEnabled;
40 prButtonLockChecks['reviewers'] = !lockEnabled;
41 }
41 }
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
42 var checksMeet = prButtonLockChecks.compare && prButtonLockChecks.reviewers;
43 if (lockEnabled) {
43 if (lockEnabled) {
44 $('#pr_submit').attr('disabled', 'disabled');
44 $('#pr_submit').attr('disabled', 'disabled');
45 }
45 }
46 else if (checksMeet) {
46 else if (checksMeet) {
47 $('#pr_submit').removeAttr('disabled');
47 $('#pr_submit').removeAttr('disabled');
48 }
48 }
49
49
50 if (msg) {
50 if (msg) {
51 $('#pr_open_message').html(msg);
51 $('#pr_open_message').html(msg);
52 }
52 }
53 };
53 };
54
54
55
55
56 /**
56 /**
57 Generate Title and Description for a PullRequest.
57 Generate Title and Description for a PullRequest.
58 In case of 1 commits, the title and description is that one commit
58 In case of 1 commits, the title and description is that one commit
59 in case of multiple commits, we iterate on them with max N number of commits,
59 in case of multiple commits, we iterate on them with max N number of commits,
60 and build description in a form
60 and build description in a form
61 - commitN
61 - commitN
62 - commitN+1
62 - commitN+1
63 ...
63 ...
64
64
65 Title is then constructed from branch names, or other references,
65 Title is then constructed from branch names, or other references,
66 replacing '-' and '_' into spaces
66 replacing '-' and '_' into spaces
67
67
68 * @param sourceRef
68 * @param sourceRef
69 * @param elements
69 * @param elements
70 * @param limit
70 * @param limit
71 * @returns {*[]}
71 * @returns {*[]}
72 */
72 */
73 var getTitleAndDescription = function(sourceRefType, sourceRef, elements, limit) {
73 var getTitleAndDescription = function(sourceRefType, sourceRef, elements, limit) {
74 var title = '';
74 var title = '';
75 var desc = '';
75 var desc = '';
76
76
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
77 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
78 var rawMessage = value['message'];
78 var rawMessage = value['message'];
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
79 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
80 });
80 });
81 // only 1 commit, use commit message as title
81 // only 1 commit, use commit message as title
82 if (elements.length === 1) {
82 if (elements.length === 1) {
83 var rawMessage = elements[0]['message'];
83 var rawMessage = elements[0]['message'];
84 title = rawMessage.split('\n')[0];
84 title = rawMessage.split('\n')[0];
85 }
85 }
86 else {
86 else {
87 // use reference name
87 // use reference name
88 var normalizedRef = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter()
88 var normalizedRef = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter()
89 var refType = sourceRefType;
89 var refType = sourceRefType;
90 title = 'Changes from {0}: {1}'.format(refType, normalizedRef);
90 title = 'Changes from {0}: {1}'.format(refType, normalizedRef);
91 }
91 }
92
92
93 return [title, desc]
93 return [title, desc]
94 };
94 };
95
95
96
96
97 window.ReviewersController = function () {
97 window.ReviewersController = function () {
98 var self = this;
98 var self = this;
99 this.$loadingIndicator = $('.calculate-reviewers');
99 this.$loadingIndicator = $('.calculate-reviewers');
100 this.$reviewRulesContainer = $('#review_rules');
100 this.$reviewRulesContainer = $('#review_rules');
101 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
101 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
102 this.$userRule = $('.pr-user-rule-container');
102 this.$userRule = $('.pr-user-rule-container');
103 this.$reviewMembers = $('#review_members');
103 this.$reviewMembers = $('#review_members');
104 this.$observerMembers = $('#observer_members');
104 this.$observerMembers = $('#observer_members');
105
105
106 this.currentRequest = null;
106 this.currentRequest = null;
107 this.diffData = null;
107 this.diffData = null;
108 this.enabledRules = [];
108 this.enabledRules = [];
109 // sync with db.py entries
109 // sync with db.py entries
110 this.ROLE_REVIEWER = 'reviewer';
110 this.ROLE_REVIEWER = 'reviewer';
111 this.ROLE_OBSERVER = 'observer'
111 this.ROLE_OBSERVER = 'observer'
112
112
113 //dummy handler, we might register our own later
113 //dummy handler, we might register our own later
114 this.diffDataHandler = function (data) {};
114 this.diffDataHandler = function (data) {};
115
115
116 this.defaultForbidUsers = function () {
116 this.defaultForbidUsers = function () {
117 return [
117 return [
118 {
118 {
119 'username': 'default',
119 'username': 'default',
120 'user_id': templateContext.default_user.user_id
120 'user_id': templateContext.default_user.user_id
121 }
121 }
122 ];
122 ];
123 };
123 };
124
124
125 // init default forbidden users
125 // init default forbidden users
126 this.forbidUsers = this.defaultForbidUsers();
126 this.forbidUsers = this.defaultForbidUsers();
127
127
128 this.hideReviewRules = function () {
128 this.hideReviewRules = function () {
129 self.$reviewRulesContainer.hide();
129 self.$reviewRulesContainer.hide();
130 $(self.$userRule.selector).hide();
130 $(self.$userRule.selector).hide();
131 };
131 };
132
132
133 this.showReviewRules = function () {
133 this.showReviewRules = function () {
134 self.$reviewRulesContainer.show();
134 self.$reviewRulesContainer.show();
135 $(self.$userRule.selector).show();
135 $(self.$userRule.selector).show();
136 };
136 };
137
137
138 this.addRule = function (ruleText) {
138 this.addRule = function (ruleText) {
139 self.showReviewRules();
139 self.showReviewRules();
140 self.enabledRules.push(ruleText);
140 self.enabledRules.push(ruleText);
141 return '<div>- {0}</div>'.format(ruleText)
141 return '<div>- {0}</div>'.format(ruleText)
142 };
142 };
143
143
144 this.increaseCounter = function(role) {
144 this.increaseCounter = function(role) {
145 if (role === self.ROLE_REVIEWER) {
145 if (role === self.ROLE_REVIEWER) {
146 var $elem = $('#reviewers-cnt')
146 var $elem = $('#reviewers-cnt')
147 var cnt = parseInt($elem.data('count') || 0)
147 var cnt = parseInt($elem.data('count') || 0)
148 cnt +=1
148 cnt +=1
149 $elem.html(cnt);
149 $elem.html(cnt);
150 $elem.data('count', cnt);
150 $elem.data('count', cnt);
151 }
151 }
152 else if (role === self.ROLE_OBSERVER) {
152 else if (role === self.ROLE_OBSERVER) {
153 var $elem = $('#observers-cnt');
153 var $elem = $('#observers-cnt');
154 var cnt = parseInt($elem.data('count') || 0)
154 var cnt = parseInt($elem.data('count') || 0)
155 cnt +=1
155 cnt +=1
156 $elem.html(cnt);
156 $elem.html(cnt);
157 $elem.data('count', cnt);
157 $elem.data('count', cnt);
158 }
158 }
159 }
159 }
160
160
161 this.resetCounter = function () {
161 this.resetCounter = function () {
162 var $elem = $('#reviewers-cnt');
162 var $elem = $('#reviewers-cnt');
163
163
164 $elem.data('count', 0);
164 $elem.data('count', 0);
165 $elem.html(0);
165 $elem.html(0);
166
166
167 var $elem = $('#observers-cnt');
167 var $elem = $('#observers-cnt');
168
168
169 $elem.data('count', 0);
169 $elem.data('count', 0);
170 $elem.html(0);
170 $elem.html(0);
171 }
171 }
172
172
173 this.loadReviewRules = function (data) {
173 this.loadReviewRules = function (data) {
174 self.diffData = data;
174 self.diffData = data;
175
175
176 // reset forbidden Users
176 // reset forbidden Users
177 this.forbidUsers = self.defaultForbidUsers();
177 this.forbidUsers = self.defaultForbidUsers();
178
178
179 // reset state of review rules
179 // reset state of review rules
180 self.$rulesList.html('');
180 self.$rulesList.html('');
181
181
182 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
182 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
183 // default rule, case for older repo that don't have any rules stored
183 // default rule, case for older repo that don't have any rules stored
184 self.$rulesList.append(
184 self.$rulesList.append(
185 self.addRule(_gettext('All reviewers must vote.'))
185 self.addRule(_gettext('All reviewers must vote.'))
186 );
186 );
187 return self.forbidUsers
187 return self.forbidUsers
188 }
188 }
189
189
190 if (data.rules.forbid_adding_reviewers) {
190 if (data.rules.forbid_adding_reviewers) {
191 $('#add_reviewer_input').remove();
191 $('#add_reviewer_input').remove();
192 }
192 }
193
193
194 if (data.rules_data !== undefined && data.rules_data.forbidden_users !== undefined) {
194 if (data.rules_data !== undefined && data.rules_data.forbidden_users !== undefined) {
195 $.each(data.rules_data.forbidden_users, function(idx, val){
195 $.each(data.rules_data.forbidden_users, function(idx, val){
196 self.forbidUsers.push(val)
196 self.forbidUsers.push(val)
197 })
197 })
198 }
198 }
199
199
200 if (data.rules_humanized !== undefined && data.rules_humanized.length > 0) {
200 if (data.rules_humanized !== undefined && data.rules_humanized.length > 0) {
201 $.each(data.rules_humanized, function(idx, val) {
201 $.each(data.rules_humanized, function(idx, val) {
202 self.$rulesList.append(
202 self.$rulesList.append(
203 self.addRule(val)
203 self.addRule(val)
204 )
204 )
205 })
205 })
206 } else {
206 } else {
207 // we don't have any rules set, so we inform users about it
207 // we don't have any rules set, so we inform users about it
208 self.$rulesList.append(
208 self.$rulesList.append(
209 self.addRule(_gettext('No additional review rules set.'))
209 self.addRule(_gettext('No additional review rules set.'))
210 )
210 )
211 }
211 }
212
212
213 return self.forbidUsers
213 return self.forbidUsers
214 };
214 };
215
215
216 this.emptyTables = function () {
216 this.emptyTables = function () {
217 self.emptyReviewersTable();
217 self.emptyReviewersTable();
218 self.emptyObserversTable();
218 self.emptyObserversTable();
219
219
220 // Also reset counters.
220 // Also reset counters.
221 self.resetCounter();
221 self.resetCounter();
222 }
222 }
223
223
224 this.emptyReviewersTable = function (withText) {
224 this.emptyReviewersTable = function (withText) {
225 self.$reviewMembers.empty();
225 self.$reviewMembers.empty();
226 if (withText !== undefined) {
226 if (withText !== undefined) {
227 self.$reviewMembers.html(withText)
227 self.$reviewMembers.html(withText)
228 }
228 }
229 };
229 };
230
230
231 this.emptyObserversTable = function (withText) {
231 this.emptyObserversTable = function (withText) {
232 self.$observerMembers.empty();
232 self.$observerMembers.empty();
233 if (withText !== undefined) {
233 if (withText !== undefined) {
234 self.$observerMembers.html(withText)
234 self.$observerMembers.html(withText)
235 }
235 }
236 }
236 }
237
237
238 this.loadDefaultReviewers = function (sourceRepo, sourceRef, targetRepo, targetRef) {
238 this.loadDefaultReviewers = function (sourceRepo, sourceRef, targetRepo, targetRef) {
239
239
240 if (self.currentRequest) {
240 if (self.currentRequest) {
241 // make sure we cleanup old running requests before triggering this again
241 // make sure we cleanup old running requests before triggering this again
242 self.currentRequest.abort();
242 self.currentRequest.abort();
243 }
243 }
244
244
245 self.$loadingIndicator.show();
245 self.$loadingIndicator.show();
246
246
247 // reset reviewer/observe members
247 // reset reviewer/observe members
248 self.emptyTables();
248 self.emptyTables();
249
249
250 prButtonLock(true, null, 'reviewers');
250 prButtonLock(true, null, 'reviewers');
251 $('#user').hide(); // hide user autocomplete before load
251 $('#user').hide(); // hide user autocomplete before load
252 $('#observer').hide(); //hide observer autocomplete before load
252 $('#observer').hide(); //hide observer autocomplete before load
253
253
254 // lock PR button, so we cannot send PR before it's calculated
254 // lock PR button, so we cannot send PR before it's calculated
255 prButtonLock(true, _gettext('Loading diff ...'), 'compare');
255 prButtonLock(true, _gettext('Loading diff ...'), 'compare');
256
256
257 if (sourceRef.length !== 3 || targetRef.length !== 3) {
257 if (sourceRef.length !== 3 || targetRef.length !== 3) {
258 // don't load defaults in case we're missing some refs...
258 // don't load defaults in case we're missing some refs...
259 self.$loadingIndicator.hide();
259 self.$loadingIndicator.hide();
260 return
260 return
261 }
261 }
262
262
263 var url = pyroutes.url('repo_default_reviewers_data',
263 var url = pyroutes.url('repo_default_reviewers_data',
264 {
264 {
265 'repo_name': templateContext.repo_name,
265 'repo_name': templateContext.repo_name,
266 'source_repo': sourceRepo,
266 'source_repo': sourceRepo,
267 'source_ref_type': sourceRef[0],
267 'source_ref_type': sourceRef[0],
268 'source_ref_name': sourceRef[1],
268 'source_ref_name': sourceRef[1],
269 'source_ref': sourceRef[2],
269 'source_ref': sourceRef[2],
270 'target_repo': targetRepo,
270 'target_repo': targetRepo,
271 'target_ref': targetRef[2],
271 'target_ref': targetRef[2],
272 'target_ref_type': sourceRef[0],
272 'target_ref_type': sourceRef[0],
273 'target_ref_name': sourceRef[1]
273 'target_ref_name': sourceRef[1]
274 });
274 });
275
275
276 self.currentRequest = $.ajax({
276 self.currentRequest = $.ajax({
277 url: url,
277 url: url,
278 headers: {'X-PARTIAL-XHR': true},
278 headers: {'X-PARTIAL-XHR': true},
279 type: 'GET',
279 type: 'GET',
280 success: function (data) {
280 success: function (data) {
281
281
282 self.currentRequest = null;
282 self.currentRequest = null;
283
283
284 // review rules
284 // review rules
285 self.loadReviewRules(data);
285 self.loadReviewRules(data);
286 var diffHandled = self.handleDiffData(data["diff_info"]);
286 var diffHandled = self.handleDiffData(data["diff_info"]);
287 if (diffHandled === false) {
287 if (diffHandled === false) {
288 return
288 return
289 }
289 }
290
290
291 for (var i = 0; i < data.reviewers.length; i++) {
291 for (var i = 0; i < data.reviewers.length; i++) {
292 var reviewer = data.reviewers[i];
292 var reviewer = data.reviewers[i];
293 // load reviewer rules from the repo data
293 // load reviewer rules from the repo data
294 self.addMember(reviewer, reviewer.reasons, reviewer.mandatory, reviewer.role);
294 self.addMember(reviewer, reviewer.reasons, reviewer.mandatory, reviewer.role);
295 }
295 }
296
296
297
297
298 self.$loadingIndicator.hide();
298 self.$loadingIndicator.hide();
299 prButtonLock(false, null, 'reviewers');
299 prButtonLock(false, null, 'reviewers');
300
300
301 $('#user').show(); // show user autocomplete before load
301 $('#user').show(); // show user autocomplete before load
302 $('#observer').show(); // show observer autocomplete before load
302 $('#observer').show(); // show observer autocomplete before load
303
303
304 var commitElements = data["diff_info"]['commits'];
304 var commitElements = data["diff_info"]['commits'];
305
305
306 if (commitElements.length === 0) {
306 if (commitElements.length === 0) {
307 var noCommitsMsg = '<span class="alert-text-warning">{0}</span>'.format(
307 var noCommitsMsg = '<span class="alert-text-warning">{0}</span>'.format(
308 _gettext('There are no commits to merge.'));
308 _gettext('There are no commits to merge.'));
309 prButtonLock(true, noCommitsMsg, 'all');
309 prButtonLock(true, noCommitsMsg, 'all');
310
310
311 } else {
311 } else {
312 // un-lock PR button, so we cannot send PR before it's calculated
312 // un-lock PR button, so we cannot send PR before it's calculated
313 prButtonLock(false, null, 'compare');
313 prButtonLock(false, null, 'compare');
314 }
314 }
315
315
316 },
316 },
317 error: function (jqXHR, textStatus, errorThrown) {
317 error: function (jqXHR, textStatus, errorThrown) {
318 var prefix = "Loading diff and reviewers/observers failed\n"
318 var prefix = "Loading diff and reviewers/observers failed\n"
319 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
319 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
320 ajaxErrorSwal(message);
320 ajaxErrorSwal(message);
321 }
321 }
322 });
322 });
323
323
324 };
324 };
325
325
326 // check those, refactor
326 // check those, refactor
327 this.removeMember = function (reviewer_id, mark_delete) {
327 this.removeMember = function (reviewer_id, mark_delete) {
328 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
328 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
329
329
330 if (typeof (mark_delete) === undefined) {
330 if (typeof (mark_delete) === undefined) {
331 mark_delete = false;
331 mark_delete = false;
332 }
332 }
333
333
334 if (mark_delete === true) {
334 if (mark_delete === true) {
335 if (reviewer) {
335 if (reviewer) {
336 // now delete the input
336 // now delete the input
337 $('#reviewer_{0} input'.format(reviewer_id)).remove();
337 $('#reviewer_{0} input'.format(reviewer_id)).remove();
338 $('#reviewer_{0}_rules input'.format(reviewer_id)).remove();
338 $('#reviewer_{0}_rules input'.format(reviewer_id)).remove();
339 // mark as to-delete
339 // mark as to-delete
340 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
340 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
341 obj.addClass('to-delete');
341 obj.addClass('to-delete');
342 obj.css({"text-decoration": "line-through", "opacity": 0.5});
342 obj.css({"text-decoration": "line-through", "opacity": 0.5});
343 }
343 }
344 } else {
344 } else {
345 $('#reviewer_{0}'.format(reviewer_id)).remove();
345 $('#reviewer_{0}'.format(reviewer_id)).remove();
346 }
346 }
347 };
347 };
348
348
349 this.addMember = function (reviewer_obj, reasons, mandatory, role) {
349 this.addMember = function (reviewer_obj, reasons, mandatory, role) {
350
350
351 var id = reviewer_obj.user_id;
351 var id = reviewer_obj.user_id;
352 var username = reviewer_obj.username;
352 var username = reviewer_obj.username;
353
353
354 reasons = reasons || [];
354 reasons = reasons || [];
355 mandatory = mandatory || false;
355 mandatory = mandatory || false;
356 role = role || self.ROLE_REVIEWER
356 role = role || self.ROLE_REVIEWER
357
357
358 // register current set IDS to check if we don't have this ID already in
358 // register current set IDS to check if we don't have this ID already in
359 // and prevent duplicates
359 // and prevent duplicates
360 var currentIds = [];
360 var currentIds = [];
361
361
362 $.each($('.reviewer_entry'), function (index, value) {
362 $.each($('.reviewer_entry'), function (index, value) {
363 currentIds.push($(value).data('reviewerUserId'))
363 currentIds.push($(value).data('reviewerUserId'))
364 })
364 })
365
365
366 var userAllowedReview = function (userId) {
366 var userAllowedReview = function (userId) {
367 var allowed = true;
367 var allowed = true;
368 $.each(self.forbidUsers, function (index, member_data) {
368 $.each(self.forbidUsers, function (index, member_data) {
369 if (parseInt(userId) === member_data['user_id']) {
369 if (parseInt(userId) === member_data['user_id']) {
370 allowed = false;
370 allowed = false;
371 return false // breaks the loop
371 return false // breaks the loop
372 }
372 }
373 });
373 });
374 return allowed
374 return allowed
375 };
375 };
376
376
377 var userAllowed = userAllowedReview(id);
377 var userAllowed = userAllowedReview(id);
378
378
379 if (!userAllowed) {
379 if (!userAllowed) {
380 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
380 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
381 } else {
381 } else {
382 // only add if it's not there
382 // only add if it's not there
383 var alreadyReviewer = currentIds.indexOf(id) != -1;
383 var alreadyReviewer = currentIds.indexOf(id) != -1;
384
384
385 if (alreadyReviewer) {
385 if (alreadyReviewer) {
386 alert(_gettext('User `{0}` already in reviewers/observers').format(username));
386 alert(_gettext('User `{0}` already in reviewers/observers').format(username));
387 } else {
387 } else {
388
388
389 var reviewerEntry = renderTemplate('reviewMemberEntry', {
389 var reviewerEntry = renderTemplate('reviewMemberEntry', {
390 'member': reviewer_obj,
390 'member': reviewer_obj,
391 'mandatory': mandatory,
391 'mandatory': mandatory,
392 'role': role,
392 'role': role,
393 'reasons': reasons,
393 'reasons': reasons,
394 'allowed_to_update': true,
394 'allowed_to_update': true,
395 'review_status': 'not_reviewed',
395 'review_status': 'not_reviewed',
396 'review_status_label': _gettext('Not Reviewed'),
396 'review_status_label': _gettext('Not Reviewed'),
397 'user_group': reviewer_obj.user_group,
397 'user_group': reviewer_obj.user_group,
398 'create': true,
398 'create': true,
399 'rule_show': true,
399 'rule_show': true,
400 })
400 })
401
401
402 if (role === self.ROLE_REVIEWER) {
402 if (role === self.ROLE_REVIEWER) {
403 $(self.$reviewMembers.selector).append(reviewerEntry);
403 $(self.$reviewMembers.selector).append(reviewerEntry);
404 self.increaseCounter(self.ROLE_REVIEWER);
404 self.increaseCounter(self.ROLE_REVIEWER);
405 $('#reviewer-empty-msg').remove()
405 $('#reviewer-empty-msg').remove()
406 }
406 }
407 else if (role === self.ROLE_OBSERVER) {
407 else if (role === self.ROLE_OBSERVER) {
408 $(self.$observerMembers.selector).append(reviewerEntry);
408 $(self.$observerMembers.selector).append(reviewerEntry);
409 self.increaseCounter(self.ROLE_OBSERVER);
409 self.increaseCounter(self.ROLE_OBSERVER);
410 $('#observer-empty-msg').remove();
410 $('#observer-empty-msg').remove();
411 }
411 }
412
412
413 tooltipActivate();
413 tooltipActivate();
414 }
414 }
415 }
415 }
416
416
417 };
417 };
418
418
419 this.updateReviewers = function (repo_name, pull_request_id, role) {
419 this.updateReviewers = function (repo_name, pull_request_id, role) {
420 if (role === 'reviewer') {
420 if (role === 'reviewer') {
421 var postData = $('#reviewers input').serialize();
421 var postData = $('#reviewers input').serialize();
422 _updatePullRequest(repo_name, pull_request_id, postData);
422 _updatePullRequest(repo_name, pull_request_id, postData);
423 } else if (role === 'observer') {
423 } else if (role === 'observer') {
424 var postData = $('#observers input').serialize();
424 var postData = $('#observers input').serialize();
425 _updatePullRequest(repo_name, pull_request_id, postData);
425 _updatePullRequest(repo_name, pull_request_id, postData);
426 }
426 }
427 };
427 };
428
428
429 this.handleDiffData = function (data) {
429 this.handleDiffData = function (data) {
430 return self.diffDataHandler(data)
430 return self.diffDataHandler(data)
431 }
431 }
432 };
432 };
433
433
434
434
435 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
435 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
436 var url = pyroutes.url(
436 var url = pyroutes.url(
437 'pullrequest_update',
437 'pullrequest_update',
438 {"repo_name": repo_name, "pull_request_id": pull_request_id});
438 {"repo_name": repo_name, "pull_request_id": pull_request_id});
439 if (typeof postData === 'string' ) {
439 if (typeof postData === 'string' ) {
440 postData += '&csrf_token=' + CSRF_TOKEN;
440 postData += '&csrf_token=' + CSRF_TOKEN;
441 } else {
441 } else {
442 postData.csrf_token = CSRF_TOKEN;
442 postData.csrf_token = CSRF_TOKEN;
443 }
443 }
444
444
445 var success = function(o) {
445 var success = function(o) {
446 var redirectUrl = o['redirect_url'];
446 var redirectUrl = o['redirect_url'];
447 if (redirectUrl !== undefined && redirectUrl !== null && redirectUrl !== '') {
447 if (redirectUrl !== undefined && redirectUrl !== null && redirectUrl !== '') {
448 window.location = redirectUrl;
448 window.location = redirectUrl;
449 } else {
449 } else {
450 window.location.reload();
450 window.location.reload();
451 }
451 }
452 };
452 };
453
453
454 ajaxPOST(url, postData, success);
454 ajaxPOST(url, postData, success);
455 };
455 };
456
456
457 /**
457 /**
458 * PULL REQUEST update commits
458 * PULL REQUEST update commits
459 */
459 */
460 var updateCommits = function(repo_name, pull_request_id, force) {
460 var updateCommits = function(repo_name, pull_request_id, force) {
461 var postData = {
461 var postData = {
462 'update_commits': true
462 'update_commits': true
463 };
463 };
464 if (force !== undefined && force === true) {
464 if (force !== undefined && force === true) {
465 postData['force_refresh'] = true
465 postData['force_refresh'] = true
466 }
466 }
467 _updatePullRequest(repo_name, pull_request_id, postData);
467 _updatePullRequest(repo_name, pull_request_id, postData);
468 };
468 };
469
469
470
470
471 /**
471 /**
472 * PULL REQUEST edit info
472 * PULL REQUEST edit info
473 */
473 */
474 var editPullRequest = function(repo_name, pull_request_id, title, description, renderer) {
474 var editPullRequest = function(repo_name, pull_request_id, title, description, renderer) {
475 var url = pyroutes.url(
475 var url = pyroutes.url(
476 'pullrequest_update',
476 'pullrequest_update',
477 {"repo_name": repo_name, "pull_request_id": pull_request_id});
477 {"repo_name": repo_name, "pull_request_id": pull_request_id});
478
478
479 var postData = {
479 var postData = {
480 'title': title,
480 'title': title,
481 'description': description,
481 'description': description,
482 'description_renderer': renderer,
482 'description_renderer': renderer,
483 'edit_pull_request': true,
483 'edit_pull_request': true,
484 'csrf_token': CSRF_TOKEN
484 'csrf_token': CSRF_TOKEN
485 };
485 };
486 var success = function(o) {
486 var success = function(o) {
487 window.location.reload();
487 window.location.reload();
488 };
488 };
489 ajaxPOST(url, postData, success);
489 ajaxPOST(url, postData, success);
490 };
490 };
491
491
492
492
493 /**
493 /**
494 * autocomplete handler for reviewers/observers
494 * autocomplete handler for reviewers/observers
495 */
495 */
496 var autoCompleteHandler = function (inputId, controller, role) {
496 var autoCompleteHandler = function (inputId, controller, role) {
497
497
498 return function (element, data) {
498 return function (element, data) {
499 var mandatory = false;
499 var mandatory = false;
500 var reasons = [_gettext('added manually by "{0}"').format(
500 var reasons = [_gettext('added manually by "{0}"').format(
501 templateContext.rhodecode_user.username)];
501 templateContext.rhodecode_user.username)];
502
502
503 // add whole user groups
503 // add whole user groups
504 if (data.value_type == 'user_group') {
504 if (data.value_type == 'user_group') {
505 reasons.push(_gettext('member of "{0}"').format(data.value_display));
505 reasons.push(_gettext('member of "{0}"').format(data.value_display));
506
506
507 $.each(data.members, function (index, member_data) {
507 $.each(data.members, function (index, member_data) {
508 var reviewer = member_data;
508 var reviewer = member_data;
509 reviewer['user_id'] = member_data['id'];
509 reviewer['user_id'] = member_data['id'];
510 reviewer['gravatar_link'] = member_data['icon_link'];
510 reviewer['gravatar_link'] = member_data['icon_link'];
511 reviewer['user_link'] = member_data['profile_link'];
511 reviewer['user_link'] = member_data['profile_link'];
512 reviewer['rules'] = [];
512 reviewer['rules'] = [];
513 controller.addMember(reviewer, reasons, mandatory, role);
513 controller.addMember(reviewer, reasons, mandatory, role);
514 })
514 })
515 }
515 }
516 // add single user
516 // add single user
517 else {
517 else {
518 var reviewer = data;
518 var reviewer = data;
519 reviewer['user_id'] = data['id'];
519 reviewer['user_id'] = data['id'];
520 reviewer['gravatar_link'] = data['icon_link'];
520 reviewer['gravatar_link'] = data['icon_link'];
521 reviewer['user_link'] = data['profile_link'];
521 reviewer['user_link'] = data['profile_link'];
522 reviewer['rules'] = [];
522 reviewer['rules'] = [];
523 controller.addMember(reviewer, reasons, mandatory, role);
523 controller.addMember(reviewer, reasons, mandatory, role);
524 }
524 }
525
525
526 $(inputId).val('');
526 $(inputId).val('');
527 }
527 }
528 }
528 }
529
529
530 /**
530 /**
531 * Reviewer autocomplete
531 * Reviewer autocomplete
532 */
532 */
533 var ReviewerAutoComplete = function (inputId, controller) {
533 var ReviewerAutoComplete = function (inputId, controller) {
534 var self = this;
534 var self = this;
535 self.controller = controller;
535 self.controller = controller;
536 self.inputId = inputId;
536 self.inputId = inputId;
537 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_REVIEWER);
537 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_REVIEWER);
538
538
539 $(inputId).autocomplete({
539 $(inputId).autocomplete({
540 serviceUrl: pyroutes.url('user_autocomplete_data'),
540 serviceUrl: pyroutes.url('user_autocomplete_data'),
541 minChars: 2,
541 minChars: 2,
542 maxHeight: 400,
542 maxHeight: 400,
543 deferRequestBy: 300, //miliseconds
543 deferRequestBy: 300, //miliseconds
544 showNoSuggestionNotice: true,
544 showNoSuggestionNotice: true,
545 tabDisabled: true,
545 tabDisabled: true,
546 autoSelectFirst: true,
546 autoSelectFirst: true,
547 params: {
547 params: {
548 user_id: templateContext.rhodecode_user.user_id,
548 user_id: templateContext.rhodecode_user.user_id,
549 user_groups: true,
549 user_groups: true,
550 user_groups_expand: true,
550 user_groups_expand: true,
551 skip_default_user: true
551 skip_default_user: true
552 },
552 },
553 formatResult: autocompleteFormatResult,
553 formatResult: autocompleteFormatResult,
554 lookupFilter: autocompleteFilterResult,
554 lookupFilter: autocompleteFilterResult,
555 onSelect: handler
555 onSelect: handler
556 });
556 });
557 };
557 };
558
558
559 /**
559 /**
560 * Observers autocomplete
560 * Observers autocomplete
561 */
561 */
562 var ObserverAutoComplete = function(inputId, controller) {
562 var ObserverAutoComplete = function(inputId, controller) {
563 var self = this;
563 var self = this;
564 self.controller = controller;
564 self.controller = controller;
565 self.inputId = inputId;
565 self.inputId = inputId;
566 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_OBSERVER);
566 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_OBSERVER);
567
567
568 $(inputId).autocomplete({
568 $(inputId).autocomplete({
569 serviceUrl: pyroutes.url('user_autocomplete_data'),
569 serviceUrl: pyroutes.url('user_autocomplete_data'),
570 minChars: 2,
570 minChars: 2,
571 maxHeight: 400,
571 maxHeight: 400,
572 deferRequestBy: 300, //miliseconds
572 deferRequestBy: 300, //miliseconds
573 showNoSuggestionNotice: true,
573 showNoSuggestionNotice: true,
574 tabDisabled: true,
574 tabDisabled: true,
575 autoSelectFirst: true,
575 autoSelectFirst: true,
576 params: {
576 params: {
577 user_id: templateContext.rhodecode_user.user_id,
577 user_id: templateContext.rhodecode_user.user_id,
578 user_groups: true,
578 user_groups: true,
579 user_groups_expand: true,
579 user_groups_expand: true,
580 skip_default_user: true
580 skip_default_user: true
581 },
581 },
582 formatResult: autocompleteFormatResult,
582 formatResult: autocompleteFormatResult,
583 lookupFilter: autocompleteFilterResult,
583 lookupFilter: autocompleteFilterResult,
584 onSelect: handler
584 onSelect: handler
585 });
585 });
586 }
586 }
587
587
588
588
589 window.VersionController = function () {
589 window.VersionController = function () {
590 var self = this;
590 var self = this;
591 this.$verSource = $('input[name=ver_source]');
591 this.$verSource = $('input[name=ver_source]');
592 this.$verTarget = $('input[name=ver_target]');
592 this.$verTarget = $('input[name=ver_target]');
593 this.$showVersionDiff = $('#show-version-diff');
593 this.$showVersionDiff = $('#show-version-diff');
594
594
595 this.adjustRadioSelectors = function (curNode) {
595 this.adjustRadioSelectors = function (curNode) {
596 var getVal = function (item) {
596 var getVal = function (item) {
597 if (item === 'latest') {
597 if (item === 'latest') {
598 return Number.MAX_SAFE_INTEGER
598 return Number.MAX_SAFE_INTEGER
599 }
599 }
600 else {
600 else {
601 return parseInt(item)
601 return parseInt(item)
602 }
602 }
603 };
603 };
604
604
605 var curVal = getVal($(curNode).val());
605 var curVal = getVal($(curNode).val());
606 var cleared = false;
606 var cleared = false;
607
607
608 $.each(self.$verSource, function (index, value) {
608 $.each(self.$verSource, function (index, value) {
609 var elVal = getVal($(value).val());
609 var elVal = getVal($(value).val());
610
610
611 if (elVal > curVal) {
611 if (elVal > curVal) {
612 if ($(value).is(':checked')) {
612 if ($(value).is(':checked')) {
613 cleared = true;
613 cleared = true;
614 }
614 }
615 $(value).attr('disabled', 'disabled');
615 $(value).attr('disabled', 'disabled');
616 $(value).removeAttr('checked');
616 $(value).removeAttr('checked');
617 $(value).css({'opacity': 0.1});
617 $(value).css({'opacity': 0.1});
618 }
618 }
619 else {
619 else {
620 $(value).css({'opacity': 1});
620 $(value).css({'opacity': 1});
621 $(value).removeAttr('disabled');
621 $(value).removeAttr('disabled');
622 }
622 }
623 });
623 });
624
624
625 if (cleared) {
625 if (cleared) {
626 // if we unchecked an active, set the next one to same loc.
626 // if we unchecked an active, set the next one to same loc.
627 $(this.$verSource).filter('[value={0}]'.format(
627 $(this.$verSource).filter('[value={0}]'.format(
628 curVal)).attr('checked', 'checked');
628 curVal)).attr('checked', 'checked');
629 }
629 }
630
630
631 self.setLockAction(false,
631 self.setLockAction(false,
632 $(curNode).data('verPos'),
632 $(curNode).data('verPos'),
633 $(this.$verSource).filter(':checked').data('verPos')
633 $(this.$verSource).filter(':checked').data('verPos')
634 );
634 );
635 };
635 };
636
636
637
637
638 this.attachVersionListener = function () {
638 this.attachVersionListener = function () {
639 self.$verTarget.change(function (e) {
639 self.$verTarget.change(function (e) {
640 self.adjustRadioSelectors(this)
640 self.adjustRadioSelectors(this)
641 });
641 });
642 self.$verSource.change(function (e) {
642 self.$verSource.change(function (e) {
643 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
643 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
644 });
644 });
645 };
645 };
646
646
647 this.init = function () {
647 this.init = function () {
648
648
649 var curNode = self.$verTarget.filter(':checked');
649 var curNode = self.$verTarget.filter(':checked');
650 self.adjustRadioSelectors(curNode);
650 self.adjustRadioSelectors(curNode);
651 self.setLockAction(true);
651 self.setLockAction(true);
652 self.attachVersionListener();
652 self.attachVersionListener();
653
653
654 };
654 };
655
655
656 this.setLockAction = function (state, selectedVersion, otherVersion) {
656 this.setLockAction = function (state, selectedVersion, otherVersion) {
657 var $showVersionDiff = this.$showVersionDiff;
657 var $showVersionDiff = this.$showVersionDiff;
658
658
659 if (state) {
659 if (state) {
660 $showVersionDiff.attr('disabled', 'disabled');
660 $showVersionDiff.attr('disabled', 'disabled');
661 $showVersionDiff.addClass('disabled');
661 $showVersionDiff.addClass('disabled');
662 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
662 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
663 }
663 }
664 else {
664 else {
665 $showVersionDiff.removeAttr('disabled');
665 $showVersionDiff.removeAttr('disabled');
666 $showVersionDiff.removeClass('disabled');
666 $showVersionDiff.removeClass('disabled');
667
667
668 if (selectedVersion == otherVersion) {
668 if (selectedVersion == otherVersion) {
669 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
669 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
670 } else {
670 } else {
671 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
671 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
672 }
672 }
673 }
673 }
674
674
675 };
675 };
676
676
677 this.showVersionDiff = function () {
677 this.showVersionDiff = function () {
678 var target = self.$verTarget.filter(':checked');
678 var target = self.$verTarget.filter(':checked');
679 var source = self.$verSource.filter(':checked');
679 var source = self.$verSource.filter(':checked');
680
680
681 if (target.val() && source.val()) {
681 if (target.val() && source.val()) {
682 var params = {
682 var params = {
683 'pull_request_id': templateContext.pull_request_data.pull_request_id,
683 'pull_request_id': templateContext.pull_request_data.pull_request_id,
684 'repo_name': templateContext.repo_name,
684 'repo_name': templateContext.repo_name,
685 'version': target.val(),
685 'version': target.val(),
686 'from_version': source.val()
686 'from_version': source.val()
687 };
687 };
688 window.location = pyroutes.url('pullrequest_show', params)
688 window.location = pyroutes.url('pullrequest_show', params)
689 }
689 }
690
690
691 return false;
691 return false;
692 };
692 };
693
693
694 this.toggleVersionView = function (elem) {
694 this.toggleVersionView = function (elem) {
695
695
696 if (this.$showVersionDiff.is(':visible')) {
696 if (this.$showVersionDiff.is(':visible')) {
697 $('.version-pr').hide();
697 $('.version-pr').hide();
698 this.$showVersionDiff.hide();
698 this.$showVersionDiff.hide();
699 $(elem).html($(elem).data('toggleOn'))
699 $(elem).html($(elem).data('toggleOn'))
700 } else {
700 } else {
701 $('.version-pr').show();
701 $('.version-pr').show();
702 this.$showVersionDiff.show();
702 this.$showVersionDiff.show();
703 $(elem).html($(elem).data('toggleOff'))
703 $(elem).html($(elem).data('toggleOff'))
704 }
704 }
705
705
706 return false
706 return false
707 };
707 };
708
708
709 };
709 };
710
710
711
711
712 window.UpdatePrController = function () {
712 window.UpdatePrController = function () {
713 var self = this;
713 var self = this;
714 this.$updateCommits = $('#update_commits');
714 this.$updateCommits = $('#update_commits');
715 this.$updateCommitsSwitcher = $('#update_commits_switcher');
715 this.$updateCommitsSwitcher = $('#update_commits_switcher');
716
716
717 this.lockUpdateButton = function (label) {
717 this.lockUpdateButton = function (label) {
718 self.$updateCommits.attr('disabled', 'disabled');
718 self.$updateCommits.attr('disabled', 'disabled');
719 self.$updateCommitsSwitcher.attr('disabled', 'disabled');
719 self.$updateCommitsSwitcher.attr('disabled', 'disabled');
720
720
721 self.$updateCommits.addClass('disabled');
721 self.$updateCommits.addClass('disabled');
722 self.$updateCommitsSwitcher.addClass('disabled');
722 self.$updateCommitsSwitcher.addClass('disabled');
723
723
724 self.$updateCommits.removeClass('btn-primary');
724 self.$updateCommits.removeClass('btn-primary');
725 self.$updateCommitsSwitcher.removeClass('btn-primary');
725 self.$updateCommitsSwitcher.removeClass('btn-primary');
726
726
727 self.$updateCommits.text(_gettext(label));
727 self.$updateCommits.text(_gettext(label));
728 };
728 };
729
729
730 this.isUpdateLocked = function () {
730 this.isUpdateLocked = function () {
731 return self.$updateCommits.attr('disabled') !== undefined;
731 return self.$updateCommits.attr('disabled') !== undefined;
732 };
732 };
733
733
734 this.updateCommits = function (curNode) {
734 this.updateCommits = function (curNode) {
735 if (self.isUpdateLocked()) {
735 if (self.isUpdateLocked()) {
736 return
736 return
737 }
737 }
738 self.lockUpdateButton(_gettext('Updating...'));
738 self.lockUpdateButton(_gettext('Updating...'));
739 updateCommits(
739 updateCommits(
740 templateContext.repo_name,
740 templateContext.repo_name,
741 templateContext.pull_request_data.pull_request_id);
741 templateContext.pull_request_data.pull_request_id);
742 };
742 };
743
743
744 this.forceUpdateCommits = function () {
744 this.forceUpdateCommits = function () {
745 if (self.isUpdateLocked()) {
745 if (self.isUpdateLocked()) {
746 return
746 return
747 }
747 }
748 self.lockUpdateButton(_gettext('Force updating...'));
748 self.lockUpdateButton(_gettext('Force updating...'));
749 var force = true;
749 var force = true;
750 updateCommits(
750 updateCommits(
751 templateContext.repo_name,
751 templateContext.repo_name,
752 templateContext.pull_request_data.pull_request_id, force);
752 templateContext.pull_request_data.pull_request_id, force);
753 };
753 };
754 };
754 };
755
755
756
756
757 /**
757 /**
758 * Reviewer display panel
758 * Reviewer display panel
759 */
759 */
760 window.ReviewersPanel = {
760 window.ReviewersPanel = {
761 editButton: null,
761 editButton: null,
762 closeButton: null,
762 closeButton: null,
763 addButton: null,
763 addButton: null,
764 removeButtons: null,
764 removeButtons: null,
765 reviewRules: null,
765 reviewRules: null,
766 setReviewers: null,
766 setReviewers: null,
767 controller: null,
767 controller: null,
768
768
769 setSelectors: function () {
769 setSelectors: function () {
770 var self = this;
770 var self = this;
771 self.editButton = $('#open_edit_reviewers');
771 self.editButton = $('#open_edit_reviewers');
772 self.closeButton =$('#close_edit_reviewers');
772 self.closeButton =$('#close_edit_reviewers');
773 self.addButton = $('#add_reviewer');
773 self.addButton = $('#add_reviewer');
774 self.removeButtons = $('.reviewer_member_remove,.reviewer_member_mandatory_remove');
774 self.removeButtons = $('.reviewer_member_remove,.reviewer_member_mandatory_remove');
775 },
775 },
776
776
777 init: function (controller, reviewRules, setReviewers) {
777 init: function (controller, reviewRules, setReviewers) {
778 var self = this;
778 var self = this;
779 self.setSelectors();
779 self.setSelectors();
780
780
781 self.controller = controller;
781 self.controller = controller;
782 self.reviewRules = reviewRules;
782 self.reviewRules = reviewRules;
783 self.setReviewers = setReviewers;
783 self.setReviewers = setReviewers;
784
784
785 self.editButton.on('click', function (e) {
785 self.editButton.on('click', function (e) {
786 self.edit();
786 self.edit();
787 });
787 });
788 self.closeButton.on('click', function (e) {
788 self.closeButton.on('click', function (e) {
789 self.close();
789 self.close();
790 self.renderReviewers();
790 self.renderReviewers();
791 });
791 });
792
792
793 self.renderReviewers();
793 self.renderReviewers();
794
794
795 },
795 },
796
796
797 renderReviewers: function () {
797 renderReviewers: function () {
798 var self = this;
798 var self = this;
799
799
800 if (self.setReviewers.reviewers === undefined) {
800 if (self.setReviewers.reviewers === undefined) {
801 return
801 return
802 }
802 }
803 if (self.setReviewers.reviewers.length === 0) {
803 if (self.setReviewers.reviewers.length === 0) {
804 self.controller.emptyReviewersTable('<tr id="reviewer-empty-msg"><td colspan="6">No reviewers</td></tr>');
804 self.controller.emptyReviewersTable('<tr id="reviewer-empty-msg"><td colspan="6">No reviewers</td></tr>');
805 return
805 return
806 }
806 }
807
807
808 self.controller.emptyReviewersTable();
808 self.controller.emptyReviewersTable();
809
809
810 $.each(self.setReviewers.reviewers, function (key, val) {
810 $.each(self.setReviewers.reviewers, function (key, val) {
811
811
812 var member = val;
812 var member = val;
813 if (member.role === self.controller.ROLE_REVIEWER) {
813 if (member.role === self.controller.ROLE_REVIEWER) {
814 var entry = renderTemplate('reviewMemberEntry', {
814 var entry = renderTemplate('reviewMemberEntry', {
815 'member': member,
815 'member': member,
816 'mandatory': member.mandatory,
816 'mandatory': member.mandatory,
817 'role': member.role,
817 'role': member.role,
818 'reasons': member.reasons,
818 'reasons': member.reasons,
819 'allowed_to_update': member.allowed_to_update,
819 'allowed_to_update': member.allowed_to_update,
820 'review_status': member.review_status,
820 'review_status': member.review_status,
821 'review_status_label': member.review_status_label,
821 'review_status_label': member.review_status_label,
822 'user_group': member.user_group,
822 'user_group': member.user_group,
823 'create': false
823 'create': false
824 });
824 });
825
825
826 $(self.controller.$reviewMembers.selector).append(entry)
826 $(self.controller.$reviewMembers.selector).append(entry)
827 }
827 }
828 });
828 });
829
829
830 tooltipActivate();
830 tooltipActivate();
831 },
831 },
832
832
833 edit: function (event) {
833 edit: function (event) {
834 var self = this;
834 var self = this;
835 self.editButton.hide();
835 self.editButton.hide();
836 self.closeButton.show();
836 self.closeButton.show();
837 self.addButton.show();
837 self.addButton.show();
838 $(self.removeButtons.selector).css('visibility', 'visible');
838 $(self.removeButtons.selector).css('visibility', 'visible');
839 // review rules
839 // review rules
840 self.controller.loadReviewRules(this.reviewRules);
840 self.controller.loadReviewRules(this.reviewRules);
841 },
841 },
842
842
843 close: function (event) {
843 close: function (event) {
844 var self = this;
844 var self = this;
845 this.editButton.show();
845 this.editButton.show();
846 this.closeButton.hide();
846 this.closeButton.hide();
847 this.addButton.hide();
847 this.addButton.hide();
848 $(this.removeButtons.selector).css('visibility', 'hidden');
848 $(this.removeButtons.selector).css('visibility', 'hidden');
849 // hide review rules
849 // hide review rules
850 self.controller.hideReviewRules();
850 self.controller.hideReviewRules();
851 }
851 }
852 };
852 };
853
853
854 /**
854 /**
855 * Reviewer display panel
855 * Reviewer display panel
856 */
856 */
857 window.ObserversPanel = {
857 window.ObserversPanel = {
858 editButton: null,
858 editButton: null,
859 closeButton: null,
859 closeButton: null,
860 addButton: null,
860 addButton: null,
861 removeButtons: null,
861 removeButtons: null,
862 reviewRules: null,
862 reviewRules: null,
863 setReviewers: null,
863 setReviewers: null,
864 controller: null,
864 controller: null,
865
865
866 setSelectors: function () {
866 setSelectors: function () {
867 var self = this;
867 var self = this;
868 self.editButton = $('#open_edit_observers');
868 self.editButton = $('#open_edit_observers');
869 self.closeButton =$('#close_edit_observers');
869 self.closeButton =$('#close_edit_observers');
870 self.addButton = $('#add_observer');
870 self.addButton = $('#add_observer');
871 self.removeButtons = $('.observer_member_remove,.observer_member_mandatory_remove');
871 self.removeButtons = $('.observer_member_remove,.observer_member_mandatory_remove');
872 },
872 },
873
873
874 init: function (controller, reviewRules, setReviewers) {
874 init: function (controller, reviewRules, setReviewers) {
875 var self = this;
875 var self = this;
876 self.setSelectors();
876 self.setSelectors();
877
877
878 self.controller = controller;
878 self.controller = controller;
879 self.reviewRules = reviewRules;
879 self.reviewRules = reviewRules;
880 self.setReviewers = setReviewers;
880 self.setReviewers = setReviewers;
881
881
882 self.editButton.on('click', function (e) {
882 self.editButton.on('click', function (e) {
883 self.edit();
883 self.edit();
884 });
884 });
885 self.closeButton.on('click', function (e) {
885 self.closeButton.on('click', function (e) {
886 self.close();
886 self.close();
887 self.renderObservers();
887 self.renderObservers();
888 });
888 });
889
889
890 self.renderObservers();
890 self.renderObservers();
891
891
892 },
892 },
893
893
894 renderObservers: function () {
894 renderObservers: function () {
895 var self = this;
895 var self = this;
896 if (self.setReviewers.observers === undefined) {
896 if (self.setReviewers.observers === undefined) {
897 return
897 return
898 }
898 }
899 if (self.setReviewers.observers.length === 0) {
899 if (self.setReviewers.observers.length === 0) {
900 self.controller.emptyObserversTable('<tr id="observer-empty-msg"><td colspan="6">No observers</td></tr>');
900 self.controller.emptyObserversTable('<tr id="observer-empty-msg"><td colspan="6">No observers</td></tr>');
901 return
901 return
902 }
902 }
903
903
904 self.controller.emptyObserversTable();
904 self.controller.emptyObserversTable();
905
905
906 $.each(self.setReviewers.observers, function (key, val) {
906 $.each(self.setReviewers.observers, function (key, val) {
907 var member = val;
907 var member = val;
908 if (member.role === self.controller.ROLE_OBSERVER) {
908 if (member.role === self.controller.ROLE_OBSERVER) {
909 var entry = renderTemplate('reviewMemberEntry', {
909 var entry = renderTemplate('reviewMemberEntry', {
910 'member': member,
910 'member': member,
911 'mandatory': member.mandatory,
911 'mandatory': member.mandatory,
912 'role': member.role,
912 'role': member.role,
913 'reasons': member.reasons,
913 'reasons': member.reasons,
914 'allowed_to_update': member.allowed_to_update,
914 'allowed_to_update': member.allowed_to_update,
915 'review_status': member.review_status,
915 'review_status': member.review_status,
916 'review_status_label': member.review_status_label,
916 'review_status_label': member.review_status_label,
917 'user_group': member.user_group,
917 'user_group': member.user_group,
918 'create': false
918 'create': false
919 });
919 });
920
920
921 $(self.controller.$observerMembers.selector).append(entry)
921 $(self.controller.$observerMembers.selector).append(entry)
922 }
922 }
923 });
923 });
924
924
925 tooltipActivate();
925 tooltipActivate();
926 },
926 },
927
927
928 edit: function (event) {
928 edit: function (event) {
929 this.editButton.hide();
929 this.editButton.hide();
930 this.closeButton.show();
930 this.closeButton.show();
931 this.addButton.show();
931 this.addButton.show();
932 $(this.removeButtons.selector).css('visibility', 'visible');
932 $(this.removeButtons.selector).css('visibility', 'visible');
933 },
933 },
934
934
935 close: function (event) {
935 close: function (event) {
936 this.editButton.show();
936 this.editButton.show();
937 this.closeButton.hide();
937 this.closeButton.hide();
938 this.addButton.hide();
938 this.addButton.hide();
939 $(this.removeButtons.selector).css('visibility', 'hidden');
939 $(this.removeButtons.selector).css('visibility', 'hidden');
940 }
940 }
941
941
942 };
942 };
943
943
944 window.PRDetails = {
944 window.PRDetails = {
945 editButton: null,
945 editButton: null,
946 closeButton: null,
946 closeButton: null,
947 deleteButton: null,
947 deleteButton: null,
948 viewFields: null,
948 viewFields: null,
949 editFields: null,
949 editFields: null,
950
950
951 setSelectors: function () {
951 setSelectors: function () {
952 var self = this;
952 var self = this;
953 self.editButton = $('#open_edit_pullrequest')
953 self.editButton = $('#open_edit_pullrequest')
954 self.closeButton = $('#close_edit_pullrequest')
954 self.closeButton = $('#close_edit_pullrequest')
955 self.deleteButton = $('#delete_pullrequest')
955 self.deleteButton = $('#delete_pullrequest')
956 self.viewFields = $('#pr-desc, #pr-title')
956 self.viewFields = $('#pr-desc, #pr-title')
957 self.editFields = $('#pr-desc-edit, #pr-title-edit, .pr-save')
957 self.editFields = $('#pr-desc-edit, #pr-title-edit, .pr-save')
958 },
958 },
959
959
960 init: function () {
960 init: function () {
961 var self = this;
961 var self = this;
962 self.setSelectors();
962 self.setSelectors();
963 self.editButton.on('click', function (e) {
963 self.editButton.on('click', function (e) {
964 self.edit();
964 self.edit();
965 });
965 });
966 self.closeButton.on('click', function (e) {
966 self.closeButton.on('click', function (e) {
967 self.view();
967 self.view();
968 });
968 });
969 },
969 },
970
970
971 edit: function (event) {
971 edit: function (event) {
972 var cmInstance = $('#pr-description-input').get(0).MarkupForm.cm;
972 var cmInstance = $('#pr-description-input').get(0).MarkupForm.cm;
973 this.viewFields.hide();
973 this.viewFields.hide();
974 this.editButton.hide();
974 this.editButton.hide();
975 this.deleteButton.hide();
975 this.deleteButton.hide();
976 this.closeButton.show();
976 this.closeButton.show();
977 this.editFields.show();
977 this.editFields.show();
978 cmInstance.refresh();
978 cmInstance.refresh();
979 },
979 },
980
980
981 view: function (event) {
981 view: function (event) {
982 this.editButton.show();
982 this.editButton.show();
983 this.deleteButton.show();
983 this.deleteButton.show();
984 this.editFields.hide();
984 this.editFields.hide();
985 this.closeButton.hide();
985 this.closeButton.hide();
986 this.viewFields.show();
986 this.viewFields.show();
987 }
987 }
988 };
988 };
989
989
990 /**
990 /**
991 * OnLine presence using channelstream
991 * OnLine presence using channelstream
992 */
992 */
993 window.ReviewerPresenceController = function (channel) {
993 window.ReviewerPresenceController = function (channel) {
994 var self = this;
994 var self = this;
995 this.channel = channel;
995 this.channel = channel;
996 this.users = {};
996 this.users = {};
997
997
998 this.storeUsers = function (users) {
998 this.storeUsers = function (users) {
999 self.users = {}
999 self.users = {}
1000 $.each(users, function (index, value) {
1000 $.each(users, function (index, value) {
1001 var userId = value.state.id;
1001 var userId = value.state.id;
1002 self.users[userId] = value.state;
1002 self.users[userId] = value.state;
1003 })
1003 })
1004 }
1004 }
1005
1005
1006 this.render = function () {
1006 this.render = function () {
1007 $.each($('.reviewer_entry'), function (index, value) {
1007 $.each($('.reviewer_entry'), function (index, value) {
1008 var userData = $(value).data();
1008 var userData = $(value).data();
1009 if (self.users[userData.reviewerUserId] !== undefined) {
1009 if (self.users[userData.reviewerUserId] !== undefined) {
1010 $(value).find('.presence-state').show();
1010 $(value).find('.presence-state').show();
1011 } else {
1011 } else {
1012 $(value).find('.presence-state').hide();
1012 $(value).find('.presence-state').hide();
1013 }
1013 }
1014 })
1014 })
1015 };
1015 };
1016
1016
1017 this.handlePresence = function (data) {
1017 this.handlePresence = function (data) {
1018 if (data.type == 'presence' && data.channel === self.channel) {
1018 if (data.type == 'presence' && data.channel === self.channel) {
1019 this.storeUsers(data.users);
1019 this.storeUsers(data.users);
1020 this.render();
1020 this.render();
1021 }
1021 }
1022 };
1022 };
1023
1023
1024 this.handleChannelUpdate = function (data) {
1024 this.handleChannelUpdate = function (data) {
1025 if (data.channel === this.channel) {
1025 if (data.channel === this.channel) {
1026 this.storeUsers(data.state.users);
1026 this.storeUsers(data.state.users);
1027 this.render();
1027 this.render();
1028 }
1028 }
1029
1029
1030 };
1030 };
1031
1031
1032 /* subscribe to the current presence */
1032 /* subscribe to the current presence */
1033 $.Topic('/connection_controller/presence').subscribe(this.handlePresence.bind(this));
1033 $.Topic('/connection_controller/presence').subscribe(this.handlePresence.bind(this));
1034 /* subscribe to updates e.g connect/disconnect */
1034 /* subscribe to updates e.g connect/disconnect */
1035 $.Topic('/connection_controller/channel_update').subscribe(this.handleChannelUpdate.bind(this));
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 window.refreshComments = function (version) {
1063 window.refreshComments = function (version) {
1040 version = version || templateContext.pull_request_data.pull_request_version || '';
1064 version = version || templateContext.pull_request_data.pull_request_version || '';
1041
1065
1042 // Pull request case
1066 // Pull request case
1043 if (templateContext.pull_request_data.pull_request_id !== null) {
1067 if (templateContext.pull_request_data.pull_request_id !== null) {
1044 var params = {
1068 var params = {
1045 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1069 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1046 'repo_name': templateContext.repo_name,
1070 'repo_name': templateContext.repo_name,
1047 'version': version,
1071 'version': version,
1048 };
1072 };
1049 var loadUrl = pyroutes.url('pullrequest_comments', params);
1073 var loadUrl = pyroutes.url('pullrequest_comments', params);
1050 } // commit case
1074 } // commit case
1051 else {
1075 else {
1052 return
1076 return
1053 }
1077 }
1054
1078
1055 var currentIDs = []
1079 var currentIDs = []
1056 $.each($('.comment'), function (idx, element) {
1080 $.each($('.comment'), function (idx, element) {
1057 currentIDs.push($(element).data('commentId'));
1081 currentIDs.push($(element).data('commentId'));
1058 });
1082 });
1059 var data = {"comments": currentIDs};
1083 var data = {"comments": currentIDs};
1060
1084
1061 var $targetElem = $('.comments-content-table');
1085 var $targetElem = $('.comments-content-table');
1062 $targetElem.css('opacity', 0.3);
1086 $targetElem.css('opacity', 0.3);
1063
1087 var $counterElem = $('#comments-count');
1064 var success = function (data) {
1088 var success = refreshCommentsSuccess($targetElem, $counterElem);
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
1080 ajaxPOST(loadUrl, data, success, null, {})
1089 ajaxPOST(loadUrl, data, success, null, {})
1081
1090
1082 }
1091 }
1083
1092
1084 window.refreshTODOs = function (version) {
1093 window.refreshTODOs = function (version) {
1085 version = version || templateContext.pull_request_data.pull_request_version || '';
1094 version = version || templateContext.pull_request_data.pull_request_version || '';
1086 // Pull request case
1095 // Pull request case
1087 if (templateContext.pull_request_data.pull_request_id !== null) {
1096 if (templateContext.pull_request_data.pull_request_id !== null) {
1088 var params = {
1097 var params = {
1089 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1098 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1090 'repo_name': templateContext.repo_name,
1099 'repo_name': templateContext.repo_name,
1091 'version': version,
1100 'version': version,
1092 };
1101 };
1093 var loadUrl = pyroutes.url('pullrequest_todos', params);
1102 var loadUrl = pyroutes.url('pullrequest_todos', params);
1094 } // commit case
1103 } // commit case
1095 else {
1104 else {
1096 return
1105 return
1097 }
1106 }
1098
1107
1099 var currentIDs = []
1108 var currentIDs = []
1100 $.each($('.comment'), function (idx, element) {
1109 $.each($('.comment'), function (idx, element) {
1101 currentIDs.push($(element).data('commentId'));
1110 currentIDs.push($(element).data('commentId'));
1102 });
1111 });
1103
1112
1104 var data = {"comments": currentIDs};
1113 var data = {"comments": currentIDs};
1105 var $targetElem = $('.todos-content-table');
1114 var $targetElem = $('.todos-content-table');
1106 $targetElem.css('opacity', 0.3);
1115 $targetElem.css('opacity', 0.3);
1107
1116 var $counterElem = $('#todos-count');
1108 var success = function (data) {
1117 var success = refreshCommentsSuccess($targetElem, $counterElem);
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 }
1123
1118
1124 ajaxPOST(loadUrl, data, success, null, {})
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 window.refreshAllComments = function (version) {
1156 window.refreshAllComments = function (version) {
1129 version = version || templateContext.pull_request_data.pull_request_version || '';
1157 version = version || templateContext.pull_request_data.pull_request_version || '';
1130
1158
1131 refreshComments(version);
1159 refreshComments(version);
1132 refreshTODOs(version);
1160 refreshTODOs(version);
1133 };
1161 };
1134
1162
1135 window.refreshDraftComments = function () {
1136 alert('TODO: refresh Draft Comments needs implementation')
1137 };
1138
1139 window.sidebarComment = function (commentId) {
1163 window.sidebarComment = function (commentId) {
1140 var jsonData = $('#commentHovercard{0}'.format(commentId)).data('commentJsonB64');
1164 var jsonData = $('#commentHovercard{0}'.format(commentId)).data('commentJsonB64');
1141 if (!jsonData) {
1165 if (!jsonData) {
1142 return 'Failed to load comment {0}'.format(commentId)
1166 return 'Failed to load comment {0}'.format(commentId)
1143 }
1167 }
1144 var funcData = JSON.parse(atob(jsonData));
1168 var funcData = JSON.parse(atob(jsonData));
1145 return renderTemplate('sideBarCommentHovercard', funcData)
1169 return renderTemplate('sideBarCommentHovercard', funcData)
1146 };
1170 };
@@ -1,151 +1,159 b''
1 ## snippet for sidebar elements
1 ## snippet for sidebar elements
2 ## usage:
2 ## usage:
3 ## <%namespace name="sidebar" file="/base/sidebar.mako"/>
3 ## <%namespace name="sidebar" file="/base/sidebar.mako"/>
4 ## ${sidebar.comments_table()}
4 ## ${sidebar.comments_table()}
5 <%namespace name="base" file="/base/base.mako"/>
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 if todo_comments:
9 if todo_comments:
10 cls_ = 'todos-content-table'
10 cls_ = 'todos-content-table'
11 def sorter(entry):
11 def sorter(entry):
12 user_id = entry.author.user_id
12 user_id = entry.author.user_id
13 resolved = '1' if entry.resolved else '0'
13 resolved = '1' if entry.resolved else '0'
14 if user_id == c.rhodecode_user.user_id:
14 if user_id == c.rhodecode_user.user_id:
15 # own comments first
15 # own comments first
16 user_id = 0
16 user_id = 0
17 return '{}'.format(str(entry.comment_id).zfill(10000))
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 else:
22 else:
19 cls_ = 'comments-content-table'
23 cls_ = 'comments-content-table'
20 def sorter(entry):
24 def sorter(entry):
21 user_id = entry.author.user_id
22 return '{}'.format(str(entry.comment_id).zfill(10000))
25 return '{}'.format(str(entry.comment_id).zfill(10000))
23
26
24 existing_ids = existing_ids or []
27 existing_ids = existing_ids or []
25
28
26 %>
29 %>
27
30
28 <table class="todo-table ${cls_}" data-total-count="${len(comments)}" data-counter="${counter_num}">
31 <table class="todo-table ${cls_}" data-total-count="${len(comments)}" data-counter="${counter_num}">
29
32
30 % for loop_obj, comment_obj in h.looper(reversed(sorted(comments, key=sorter))):
33 % for loop_obj, comment_obj in h.looper(reversed(sorted(comments, key=sorter))):
31 <%
34 <%
32 display = ''
35 display = ''
33 _cls = ''
36 _cls = ''
34 ## Extra precaution to not show drafts in the sidebar for todo/comments
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 continue
39 continue
37 %>
40 %>
38
41
39
42
40 <%
43 <%
41 comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', []))
44 comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', []))
42 prev_comment_ver_index = 0
45 prev_comment_ver_index = 0
43 if loop_obj.previous:
46 if loop_obj.previous:
44 prev_comment_ver_index = loop_obj.previous.get_index_version(getattr(c, 'versions', []))
47 prev_comment_ver_index = loop_obj.previous.get_index_version(getattr(c, 'versions', []))
45
48
46 ver_info = None
49 ver_info = None
47 if getattr(c, 'versions', []):
50 if getattr(c, 'versions', []):
48 ver_info = c.versions[comment_ver_index-1] if comment_ver_index else None
51 ver_info = c.versions[comment_ver_index-1] if comment_ver_index else None
49 %>
52 %>
50 <% hidden_at_ver = comment_obj.outdated_at_version_js(c.at_version_num) %>
53 <% hidden_at_ver = comment_obj.outdated_at_version_js(c.at_version_num) %>
51 <% is_from_old_ver = comment_obj.older_than_version_js(c.at_version_num) %>
54 <% is_from_old_ver = comment_obj.older_than_version_js(c.at_version_num) %>
52 <%
55 <%
53 if (prev_comment_ver_index > comment_ver_index):
56 if (prev_comment_ver_index > comment_ver_index):
54 comments_ver_divider = comment_ver_index
57 comments_ver_divider = comment_ver_index
55 else:
58 else:
56 comments_ver_divider = None
59 comments_ver_divider = None
57 %>
60 %>
58
61
59 % if todo_comments:
62 % if todo_comments:
60 % if comment_obj.resolved:
63 % if comment_obj.resolved:
61 <% _cls = 'resolved-todo' %>
64 <% _cls = 'resolved-todo' %>
62 <% display = 'none' %>
65 <% display = 'none' %>
63 % endif
66 % endif
64 % else:
67 % else:
65 ## SKIP TODOs we display them in other area
68 ## SKIP TODOs we display them in other area
66 % if comment_obj.is_todo:
69 % if comment_obj.is_todo:
67 <% display = 'none' %>
70 <% display = 'none' %>
68 % endif
71 % endif
69 ## Skip outdated comments
72 ## Skip outdated comments
70 % if comment_obj.outdated:
73 % if comment_obj.outdated:
71 <% display = 'none' %>
74 <% display = 'none' %>
72 <% _cls = 'hidden-comment' %>
75 <% _cls = 'hidden-comment' %>
73 % endif
76 % endif
74 % endif
77 % endif
75
78
76 % if not todo_comments and comments_ver_divider:
79 % if not todo_comments and comments_ver_divider:
77 <tr class="old-comments-marker">
80 <tr class="old-comments-marker">
78 <td colspan="3">
81 <td colspan="3">
79 % if ver_info:
82 % if ver_info:
80 <code>v${comments_ver_divider} ${h.age_component(ver_info.created_on, time_is_local=True, tooltip=False)}</code>
83 <code>v${comments_ver_divider} ${h.age_component(ver_info.created_on, time_is_local=True, tooltip=False)}</code>
81 % else:
84 % else:
82 <code>v${comments_ver_divider}</code>
85 <code>v${comments_ver_divider}</code>
83 % endif
86 % endif
84 </td>
87 </td>
85 </tr>
88 </tr>
86
89
87 % endif
90 % endif
88
91
89 <tr class="${_cls}" style="display: ${display};" data-sidebar-comment-id="${comment_obj.comment_id}">
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 <td class="td-todo-number">
98 <td class="td-todo-number">
91 <%
99 <%
92 version_info = ''
100 version_info = ''
93 if is_pr:
101 if is_pr:
94 version_info = (' made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else ' made in this version')
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 ## new comments, since refresh
104 ## new comments, since refresh
97 % if existing_ids and comment_obj.comment_id not in existing_ids:
105 % if existing_ids and comment_obj.comment_id not in existing_ids:
98 <div class="tooltip" style="position: absolute; left: 8px; color: #682668" title="New comment">
106 <div class="tooltip" style="position: absolute; left: 8px; color: #682668" title="New comment">
99 !
107 !
100 </div>
108 </div>
101 % endif
109 % endif
102
110
103 <%
111 <%
104 data = h.json.dumps({
112 data = h.json.dumps({
105 'comment_id': comment_obj.comment_id,
113 'comment_id': comment_obj.comment_id,
106 'version_info': version_info,
114 'version_info': version_info,
107 'file_name': comment_obj.f_path,
115 'file_name': comment_obj.f_path,
108 'line_no': comment_obj.line_no,
116 'line_no': comment_obj.line_no,
109 'outdated': comment_obj.outdated,
117 'outdated': comment_obj.outdated,
110 'inline': comment_obj.is_inline,
118 'inline': comment_obj.is_inline,
111 'is_todo': comment_obj.is_todo,
119 'is_todo': comment_obj.is_todo,
112 'created_on': h.format_date(comment_obj.created_on),
120 'created_on': h.format_date(comment_obj.created_on),
113 'datetime': '{}{}'.format(comment_obj.created_on, h.get_timezone(comment_obj.created_on, time_is_local=True)),
121 'datetime': '{}{}'.format(comment_obj.created_on, h.get_timezone(comment_obj.created_on, time_is_local=True)),
114 'review_status': (comment_obj.review_status or '')
122 'review_status': (comment_obj.review_status or '')
115 })
123 })
116
124
117 if comment_obj.outdated:
125 if comment_obj.outdated:
118 icon = 'icon-comment-toggle'
126 icon = 'icon-comment-toggle'
119 elif comment_obj.is_inline:
127 elif comment_obj.is_inline:
120 icon = 'icon-code'
128 icon = 'icon-code'
121 else:
129 else:
122 icon = 'icon-comment'
130 icon = 'icon-comment'
123 %>
131 %>
124
132
125 <i id="commentHovercard${comment_obj.comment_id}"
133 <i id="commentHovercard${comment_obj.comment_id}"
126 class="${icon} tooltip-hovercard"
134 class="${icon} tooltip-hovercard"
127 data-hovercard-url="javascript:sidebarComment(${comment_obj.comment_id})"
135 data-hovercard-url="javascript:sidebarComment(${comment_obj.comment_id})"
128 data-comment-json-b64='${h.b64(data)}'>
136 data-comment-json-b64='${h.b64(data)}'>
129 </i>
137 </i>
130
138
131 </td>
139 </td>
132
140
133 <td class="td-todo-gravatar">
141 <td class="td-todo-gravatar">
134 ${base.gravatar(comment_obj.author.email, 16, user=comment_obj.author, tooltip=True, extra_class=['no-margin'])}
142 ${base.gravatar(comment_obj.author.email, 16, user=comment_obj.author, tooltip=True, extra_class=['no-margin'])}
135 </td>
143 </td>
136 <td class="todo-comment-text-wrapper">
144 <td class="todo-comment-text-wrapper">
137 <div class="todo-comment-text ${('todo-resolved' if comment_obj.resolved else '')}">
145 <div class="todo-comment-text ${('todo-resolved' if comment_obj.resolved else '')}">
138 <a class="${('todo-resolved' if comment_obj.resolved else '')} permalink"
146 <a class="${('todo-resolved' if comment_obj.resolved else '')} permalink"
139 href="#comment-${comment_obj.comment_id}"
147 href="#comment-${comment_obj.comment_id}"
140 onclick="return Rhodecode.comments.scrollToComment($('#comment-${comment_obj.comment_id}'), 0, ${hidden_at_ver})">
148 onclick="return Rhodecode.comments.scrollToComment($('#comment-${comment_obj.comment_id}'), 0, ${hidden_at_ver})">
141
149
142 ${h.chop_at_smart(comment_obj.text, '\n', suffix_if_chopped='...')}
150 ${h.chop_at_smart(comment_obj.text, '\n', suffix_if_chopped='...')}
143 </a>
151 </a>
144 </div>
152 </div>
145 </td>
153 </td>
146 </tr>
154 </tr>
147 % endfor
155 % endfor
148
156
149 </table>
157 </table>
150
158
151 </%def> No newline at end of file
159 </%def>
@@ -1,1033 +1,1056 b''
1 <%inherit file="/base/base.mako"/>
1 <%inherit file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
3 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
4 <%namespace name="sidebar" file="/base/sidebar.mako"/>
4 <%namespace name="sidebar" file="/base/sidebar.mako"/>
5
5
6
6
7 <%def name="title()">
7 <%def name="title()">
8 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
8 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
9 %if c.rhodecode_name:
9 %if c.rhodecode_name:
10 &middot; ${h.branding(c.rhodecode_name)}
10 &middot; ${h.branding(c.rhodecode_name)}
11 %endif
11 %endif
12 </%def>
12 </%def>
13
13
14 <%def name="breadcrumbs_links()">
14 <%def name="breadcrumbs_links()">
15
15
16 </%def>
16 </%def>
17
17
18 <%def name="menu_bar_nav()">
18 <%def name="menu_bar_nav()">
19 ${self.menu_items(active='repositories')}
19 ${self.menu_items(active='repositories')}
20 </%def>
20 </%def>
21
21
22 <%def name="menu_bar_subnav()">
22 <%def name="menu_bar_subnav()">
23 ${self.repo_menu(active='showpullrequest')}
23 ${self.repo_menu(active='showpullrequest')}
24 </%def>
24 </%def>
25
25
26
26
27 <%def name="main()">
27 <%def name="main()">
28 ## Container to gather extracted Tickets
28 ## Container to gather extracted Tickets
29 <%
29 <%
30 c.referenced_commit_issues = []
30 c.referenced_commit_issues = []
31 c.referenced_desc_issues = []
31 c.referenced_desc_issues = []
32 %>
32 %>
33
33
34 <script type="text/javascript">
34 <script type="text/javascript">
35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
35 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
36 templateContext.pull_request_data.pull_request_version = '${request.GET.get('version', '')}';
36 templateContext.pull_request_data.pull_request_version = '${request.GET.get('version', '')}';
37 </script>
37 </script>
38
38
39 <div class="box">
39 <div class="box">
40
40
41 <div class="box pr-summary">
41 <div class="box pr-summary">
42
42
43 <div class="summary-details block-left">
43 <div class="summary-details block-left">
44 <div id="pr-title">
44 <div id="pr-title">
45 % if c.pull_request.is_closed():
45 % if c.pull_request.is_closed():
46 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
46 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
47 % endif
47 % endif
48 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
48 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
49 </div>
49 </div>
50 <div id="pr-title-edit" class="input" style="display: none;">
50 <div id="pr-title-edit" class="input" style="display: none;">
51 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
51 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
52 </div>
52 </div>
53
53
54 <% summary = lambda n:{False:'summary-short'}.get(n) %>
54 <% summary = lambda n:{False:'summary-short'}.get(n) %>
55 <div class="pr-details-title">
55 <div class="pr-details-title">
56 <div class="pull-left">
56 <div class="pull-left">
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>
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 ${_('Created on')}
58 ${_('Created on')}
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>
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 <span class="pr-details-title-author-pref">${_('by')}</span>
60 <span class="pr-details-title-author-pref">${_('by')}</span>
61 </div>
61 </div>
62
62
63 <div class="pull-left">
63 <div class="pull-left">
64 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
64 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
65 </div>
65 </div>
66
66
67 %if c.allowed_to_update:
67 %if c.allowed_to_update:
68 <div class="pull-right">
68 <div class="pull-right">
69 <div id="edit_pull_request" class="action_button pr-save" style="display: none;">${_('Update title & description')}</div>
69 <div id="edit_pull_request" class="action_button pr-save" style="display: none;">${_('Update title & description')}</div>
70 <div id="delete_pullrequest" class="action_button pr-save ${('' if c.allowed_to_delete else 'disabled' )}" style="display: none;">
70 <div id="delete_pullrequest" class="action_button pr-save ${('' if c.allowed_to_delete else 'disabled' )}" style="display: none;">
71 % if c.allowed_to_delete:
71 % if c.allowed_to_delete:
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)}
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 <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}"
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 onclick="submitConfirm(event, this, _gettext('Confirm to delete this pull request'), _gettext('Delete'), '${'!{}'.format(c.pull_request.pull_request_id)}')"
74 onclick="submitConfirm(event, this, _gettext('Confirm to delete this pull request'), _gettext('Delete'), '${'!{}'.format(c.pull_request.pull_request_id)}')"
75 type="submit" value="${_('Delete pull request')}">
75 type="submit" value="${_('Delete pull request')}">
76 ${h.end_form()}
76 ${h.end_form()}
77 % else:
77 % else:
78 <span class="tooltip" title="${_('Not allowed to delete this pull request')}">${_('Delete pull request')}</span>
78 <span class="tooltip" title="${_('Not allowed to delete this pull request')}">${_('Delete pull request')}</span>
79 % endif
79 % endif
80 </div>
80 </div>
81 <div id="open_edit_pullrequest" class="action_button">${_('Edit')}</div>
81 <div id="open_edit_pullrequest" class="action_button">${_('Edit')}</div>
82 <div id="close_edit_pullrequest" class="action_button" style="display: none;">${_('Cancel')}</div>
82 <div id="close_edit_pullrequest" class="action_button" style="display: none;">${_('Cancel')}</div>
83 </div>
83 </div>
84
84
85 %endif
85 %endif
86 </div>
86 </div>
87
87
88 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
88 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
89 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name, issues_container=c.referenced_desc_issues)}
89 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name, issues_container=c.referenced_desc_issues)}
90 </div>
90 </div>
91
91
92 <div id="pr-desc-edit" class="input textarea" style="display: none;">
92 <div id="pr-desc-edit" class="input textarea" style="display: none;">
93 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
93 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
94 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
94 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
95 </div>
95 </div>
96
96
97 <div id="summary" class="fields pr-details-content">
97 <div id="summary" class="fields pr-details-content">
98
98
99 ## source
99 ## source
100 <div class="field">
100 <div class="field">
101 <div class="label-pr-detail">
101 <div class="label-pr-detail">
102 <label>${_('Commit flow')}:</label>
102 <label>${_('Commit flow')}:</label>
103 </div>
103 </div>
104 <div class="input">
104 <div class="input">
105 <div class="pr-commit-flow">
105 <div class="pr-commit-flow">
106 ## Source
106 ## Source
107 %if c.pull_request.source_ref_parts.type == 'branch':
107 %if c.pull_request.source_ref_parts.type == 'branch':
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>
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 %else:
109 %else:
110 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
110 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
111 %endif
111 %endif
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>
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 &rarr;
113 &rarr;
114 ## Target
114 ## Target
115 %if c.pull_request.target_ref_parts.type == 'branch':
115 %if c.pull_request.target_ref_parts.type == 'branch':
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>
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 %else:
117 %else:
118 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
118 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
119 %endif
119 %endif
120
120
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>
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 <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>'>
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 <i class="icon-angle-down">more details</i>
124 <i class="icon-angle-down">more details</i>
125 </a>
125 </a>
126
126
127 </div>
127 </div>
128
128
129 <div class="source-details" style="display: none">
129 <div class="source-details" style="display: none">
130
130
131 <ul>
131 <ul>
132
132
133 ## common ancestor
133 ## common ancestor
134 <li>
134 <li>
135 ${_('Common ancestor')}:
135 ${_('Common ancestor')}:
136 % if c.ancestor_commit:
136 % if c.ancestor_commit:
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>
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 % else:
138 % else:
139 ${_('not available')}
139 ${_('not available')}
140 % endif
140 % endif
141 </li>
141 </li>
142
142
143 ## pull url
143 ## pull url
144 <li>
144 <li>
145 %if h.is_hg(c.pull_request.source_repo):
145 %if h.is_hg(c.pull_request.source_repo):
146 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
146 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
147 %elif h.is_git(c.pull_request.source_repo):
147 %elif h.is_git(c.pull_request.source_repo):
148 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
148 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
149 %endif
149 %endif
150
150
151 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
151 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
152 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
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 </li>
153 </li>
154
154
155 ## Shadow repo
155 ## Shadow repo
156 <li>
156 <li>
157 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
157 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
158 %if h.is_hg(c.pull_request.target_repo):
158 %if h.is_hg(c.pull_request.target_repo):
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) %>
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 %elif h.is_git(c.pull_request.target_repo):
160 %elif h.is_git(c.pull_request.target_repo):
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) %>
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 %endif
162 %endif
163
163
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">
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 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
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 % else:
167 % else:
168 <div class="">
168 <div class="">
169 ${_('Shadow repository data not available')}.
169 ${_('Shadow repository data not available')}.
170 </div>
170 </div>
171 % endif
171 % endif
172 </li>
172 </li>
173
173
174 </ul>
174 </ul>
175
175
176 </div>
176 </div>
177
177
178 </div>
178 </div>
179
179
180 </div>
180 </div>
181
181
182 ## versions
182 ## versions
183 <div class="field">
183 <div class="field">
184 <div class="label-pr-detail">
184 <div class="label-pr-detail">
185 <label>${_('Versions')}:</label>
185 <label>${_('Versions')}:</label>
186 </div>
186 </div>
187
187
188 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
188 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
189 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
189 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
190
190
191 <div class="pr-versions">
191 <div class="pr-versions">
192 % if c.show_version_changes:
192 % if c.show_version_changes:
193 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
193 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
194 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
194 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
195 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
195 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
196 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
196 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
197 data-toggle-on="${_('show versions')}."
197 data-toggle-on="${_('show versions')}."
198 data-toggle-off="${_('hide versions')}.">
198 data-toggle-off="${_('hide versions')}.">
199 ${_('show versions')}.
199 ${_('show versions')}.
200 </a>
200 </a>
201 <table>
201 <table>
202 ## SHOW ALL VERSIONS OF PR
202 ## SHOW ALL VERSIONS OF PR
203 <% ver_pr = None %>
203 <% ver_pr = None %>
204
204
205 % for data in reversed(list(enumerate(c.versions, 1))):
205 % for data in reversed(list(enumerate(c.versions, 1))):
206 <% ver_pos = data[0] %>
206 <% ver_pos = data[0] %>
207 <% ver = data[1] %>
207 <% ver = data[1] %>
208 <% ver_pr = ver.pull_request_version_id %>
208 <% ver_pr = ver.pull_request_version_id %>
209 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
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 <tr class="version-pr" style="display: ${display_row}">
211 <tr class="version-pr" style="display: ${display_row}">
212 <td>
212 <td>
213 <code>
213 <code>
214 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
214 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
215 </code>
215 </code>
216 </td>
216 </td>
217 <td>
217 <td>
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}"/>
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 <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}"/>
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 </td>
220 </td>
221 <td>
221 <td>
222 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
222 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
223 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
223 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
224
224
225 </td>
225 </td>
226 <td>
226 <td>
227 % if c.at_version_num != ver_pr:
227 % if c.at_version_num != ver_pr:
228 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
228 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
229 <code>
229 <code>
230 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
230 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
231 </code>
231 </code>
232 % endif
232 % endif
233 </td>
233 </td>
234 <td>
234 <td>
235 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
235 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
236 </td>
236 </td>
237 <td>
237 <td>
238 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
238 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
239 </td>
239 </td>
240 </tr>
240 </tr>
241 % endfor
241 % endfor
242
242
243 <tr>
243 <tr>
244 <td colspan="6">
244 <td colspan="6">
245 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
245 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
246 data-label-text-locked="${_('select versions to show changes')}"
246 data-label-text-locked="${_('select versions to show changes')}"
247 data-label-text-diff="${_('show changes between versions')}"
247 data-label-text-diff="${_('show changes between versions')}"
248 data-label-text-show="${_('show pull request for this version')}"
248 data-label-text-show="${_('show pull request for this version')}"
249 >
249 >
250 ${_('select versions to show changes')}
250 ${_('select versions to show changes')}
251 </button>
251 </button>
252 </td>
252 </td>
253 </tr>
253 </tr>
254 </table>
254 </table>
255 % else:
255 % else:
256 <div>
256 <div>
257 ${_('Pull request versions not available')}.
257 ${_('Pull request versions not available')}.
258 </div>
258 </div>
259 % endif
259 % endif
260 </div>
260 </div>
261 </div>
261 </div>
262
262
263 </div>
263 </div>
264
264
265 </div>
265 </div>
266
266
267
267
268 </div>
268 </div>
269
269
270 </div>
270 </div>
271
271
272 <div class="box">
272 <div class="box">
273
273
274 % if c.state_progressing:
274 % if c.state_progressing:
275
275
276 <h2 style="text-align: center">
276 <h2 style="text-align: center">
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>
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 % if c.is_super_admin:
279 % if c.is_super_admin:
280 <br/>
280 <br/>
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.
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 % endif
282 % endif
283 </h2>
283 </h2>
284
284
285 % else:
285 % else:
286
286
287 ## Diffs rendered here
287 ## Diffs rendered here
288 <div class="table" >
288 <div class="table" >
289 <div id="changeset_compare_view_content">
289 <div id="changeset_compare_view_content">
290 ##CS
290 ##CS
291 % if c.missing_requirements:
291 % if c.missing_requirements:
292 <div class="box">
292 <div class="box">
293 <div class="alert alert-warning">
293 <div class="alert alert-warning">
294 <div>
294 <div>
295 <strong>${_('Missing requirements:')}</strong>
295 <strong>${_('Missing requirements:')}</strong>
296 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
296 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
297 </div>
297 </div>
298 </div>
298 </div>
299 </div>
299 </div>
300 % elif c.missing_commits:
300 % elif c.missing_commits:
301 <div class="box">
301 <div class="box">
302 <div class="alert alert-warning">
302 <div class="alert alert-warning">
303 <div>
303 <div>
304 <strong>${_('Missing commits')}:</strong>
304 <strong>${_('Missing commits')}:</strong>
305 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}<br/>
305 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}<br/>
306 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}<br/>
306 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}<br/>
307 ${_('Consider doing a `force update commits` in case you think this is an error.')}
307 ${_('Consider doing a `force update commits` in case you think this is an error.')}
308 </div>
308 </div>
309 </div>
309 </div>
310 </div>
310 </div>
311 % elif c.pr_merge_source_commit.changed and not c.pull_request.is_closed():
311 % elif c.pr_merge_source_commit.changed and not c.pull_request.is_closed():
312 <div class="box">
312 <div class="box">
313 <div class="alert alert-info">
313 <div class="alert alert-info">
314 <div>
314 <div>
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>
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 </div>
316 </div>
317 </div>
317 </div>
318 </div>
318 </div>
319 % endif
319 % endif
320
320
321 <div class="compare_view_commits_title">
321 <div class="compare_view_commits_title">
322 % if not c.compare_mode:
322 % if not c.compare_mode:
323
323
324 % if c.at_version_index:
324 % if c.at_version_index:
325 <h4>
325 <h4>
326 ${_('Showing changes at v{}, commenting is disabled.').format(c.at_version_index)}
326 ${_('Showing changes at v{}, commenting is disabled.').format(c.at_version_index)}
327 </h4>
327 </h4>
328 % endif
328 % endif
329
329
330 <div class="pull-left">
330 <div class="pull-left">
331 <div class="btn-group">
331 <div class="btn-group">
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)} >
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 % if c.collapse_all_commits:
333 % if c.collapse_all_commits:
334 <i class="icon-plus-squared-alt icon-no-margin"></i>
334 <i class="icon-plus-squared-alt icon-no-margin"></i>
335 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
335 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
336 % else:
336 % else:
337 <i class="icon-minus-squared-alt icon-no-margin"></i>
337 <i class="icon-minus-squared-alt icon-no-margin"></i>
338 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
338 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
339 % endif
339 % endif
340 </a>
340 </a>
341 </div>
341 </div>
342 </div>
342 </div>
343
343
344 <div class="pull-right">
344 <div class="pull-right">
345 % if c.allowed_to_update and not c.pull_request.is_closed():
345 % if c.allowed_to_update and not c.pull_request.is_closed():
346
346
347 <div class="btn-group btn-group-actions">
347 <div class="btn-group btn-group-actions">
348 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
348 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
349 ${_('Update commits')}
349 ${_('Update commits')}
350 </a>
350 </a>
351
351
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')}">
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 <i class="icon-down"></i>
353 <i class="icon-down"></i>
354 </a>
354 </a>
355
355
356 <div class="btn-action-switcher-container right-align" id="update-commits-switcher">
356 <div class="btn-action-switcher-container right-align" id="update-commits-switcher">
357 <ul class="btn-action-switcher" role="menu" style="min-width: 300px;">
357 <ul class="btn-action-switcher" role="menu" style="min-width: 300px;">
358 <li>
358 <li>
359 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
359 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
360 ${_('Force update commits')}
360 ${_('Force update commits')}
361 </a>
361 </a>
362 <div class="action-help-block">
362 <div class="action-help-block">
363 ${_('Update commits and force refresh this pull request.')}
363 ${_('Update commits and force refresh this pull request.')}
364 </div>
364 </div>
365 </li>
365 </li>
366 </ul>
366 </ul>
367 </div>
367 </div>
368 </div>
368 </div>
369
369
370 % else:
370 % else:
371 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
371 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
372 % endif
372 % endif
373
373
374 </div>
374 </div>
375 % endif
375 % endif
376 </div>
376 </div>
377
377
378 % if not c.missing_commits:
378 % if not c.missing_commits:
379 ## COMPARE RANGE DIFF MODE
379 ## COMPARE RANGE DIFF MODE
380 % if c.compare_mode:
380 % if c.compare_mode:
381 % if c.at_version:
381 % if c.at_version:
382 <h4>
382 <h4>
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')}:
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 </h4>
384 </h4>
385
385
386 <div class="subtitle-compare">
386 <div class="subtitle-compare">
387 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
387 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
388 </div>
388 </div>
389
389
390 <div class="container">
390 <div class="container">
391 <table class="rctable compare_view_commits">
391 <table class="rctable compare_view_commits">
392 <tr>
392 <tr>
393 <th></th>
393 <th></th>
394 <th>${_('Time')}</th>
394 <th>${_('Time')}</th>
395 <th>${_('Author')}</th>
395 <th>${_('Author')}</th>
396 <th>${_('Commit')}</th>
396 <th>${_('Commit')}</th>
397 <th></th>
397 <th></th>
398 <th>${_('Description')}</th>
398 <th>${_('Description')}</th>
399 </tr>
399 </tr>
400
400
401 % for c_type, commit in c.commit_changes:
401 % for c_type, commit in c.commit_changes:
402 % if c_type in ['a', 'r']:
402 % if c_type in ['a', 'r']:
403 <%
403 <%
404 if c_type == 'a':
404 if c_type == 'a':
405 cc_title = _('Commit added in displayed changes')
405 cc_title = _('Commit added in displayed changes')
406 elif c_type == 'r':
406 elif c_type == 'r':
407 cc_title = _('Commit removed in displayed changes')
407 cc_title = _('Commit removed in displayed changes')
408 else:
408 else:
409 cc_title = ''
409 cc_title = ''
410 %>
410 %>
411 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
411 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
412 <td>
412 <td>
413 <div class="commit-change-indicator color-${c_type}-border">
413 <div class="commit-change-indicator color-${c_type}-border">
414 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
414 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
415 ${c_type.upper()}
415 ${c_type.upper()}
416 </div>
416 </div>
417 </div>
417 </div>
418 </td>
418 </td>
419 <td class="td-time">
419 <td class="td-time">
420 ${h.age_component(commit.date)}
420 ${h.age_component(commit.date)}
421 </td>
421 </td>
422 <td class="td-user">
422 <td class="td-user">
423 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
423 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
424 </td>
424 </td>
425 <td class="td-hash">
425 <td class="td-hash">
426 <code>
426 <code>
427 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
427 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
428 r${commit.idx}:${h.short_id(commit.raw_id)}
428 r${commit.idx}:${h.short_id(commit.raw_id)}
429 </a>
429 </a>
430 ${h.hidden('revisions', commit.raw_id)}
430 ${h.hidden('revisions', commit.raw_id)}
431 </code>
431 </code>
432 </td>
432 </td>
433 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
433 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
434 <i class="icon-expand-linked"></i>
434 <i class="icon-expand-linked"></i>
435 </td>
435 </td>
436 <td class="mid td-description">
436 <td class="mid td-description">
437 <div class="log-container truncate-wrap">
437 <div class="log-container truncate-wrap">
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>
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 </div>
439 </div>
440 </td>
440 </td>
441 </tr>
441 </tr>
442 % endif
442 % endif
443 % endfor
443 % endfor
444 </table>
444 </table>
445 </div>
445 </div>
446
446
447 % endif
447 % endif
448
448
449 ## Regular DIFF
449 ## Regular DIFF
450 % else:
450 % else:
451 <%include file="/compare/compare_commits.mako" />
451 <%include file="/compare/compare_commits.mako" />
452 % endif
452 % endif
453
453
454 <div class="cs_files">
454 <div class="cs_files">
455 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
455 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
456
456
457 <%
457 <%
458 pr_menu_data = {
458 pr_menu_data = {
459 'outdated_comm_count_ver': outdated_comm_count_ver,
459 'outdated_comm_count_ver': outdated_comm_count_ver,
460 'pull_request': c.pull_request
460 'pull_request': c.pull_request
461 }
461 }
462 %>
462 %>
463
463
464 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on, pull_request_menu=pr_menu_data)}
464 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on, pull_request_menu=pr_menu_data)}
465
465
466 % if c.range_diff_on:
466 % if c.range_diff_on:
467 % for commit in c.commit_ranges:
467 % for commit in c.commit_ranges:
468 ${cbdiffs.render_diffset(
468 ${cbdiffs.render_diffset(
469 c.changes[commit.raw_id],
469 c.changes[commit.raw_id],
470 commit=commit, use_comments=True,
470 commit=commit, use_comments=True,
471 collapse_when_files_over=5,
471 collapse_when_files_over=5,
472 disable_new_comments=True,
472 disable_new_comments=True,
473 deleted_files_comments=c.deleted_files_comments,
473 deleted_files_comments=c.deleted_files_comments,
474 inline_comments=c.inline_comments,
474 inline_comments=c.inline_comments,
475 pull_request_menu=pr_menu_data, show_todos=False)}
475 pull_request_menu=pr_menu_data, show_todos=False)}
476 % endfor
476 % endfor
477 % else:
477 % else:
478 ${cbdiffs.render_diffset(
478 ${cbdiffs.render_diffset(
479 c.diffset, use_comments=True,
479 c.diffset, use_comments=True,
480 collapse_when_files_over=30,
480 collapse_when_files_over=30,
481 disable_new_comments=not c.allowed_to_comment,
481 disable_new_comments=not c.allowed_to_comment,
482 deleted_files_comments=c.deleted_files_comments,
482 deleted_files_comments=c.deleted_files_comments,
483 inline_comments=c.inline_comments,
483 inline_comments=c.inline_comments,
484 pull_request_menu=pr_menu_data, show_todos=False)}
484 pull_request_menu=pr_menu_data, show_todos=False)}
485 % endif
485 % endif
486
486
487 </div>
487 </div>
488 % else:
488 % else:
489 ## skipping commits we need to clear the view for missing commits
489 ## skipping commits we need to clear the view for missing commits
490 <div style="clear:both;"></div>
490 <div style="clear:both;"></div>
491 % endif
491 % endif
492
492
493 </div>
493 </div>
494 </div>
494 </div>
495
495
496 ## template for inline comment form
496 ## template for inline comment form
497 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
497 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
498
498
499 ## comments heading with count
499 ## comments heading with count
500 <div class="comments-heading">
500 <div class="comments-heading">
501 <i class="icon-comment"></i>
501 <i class="icon-comment"></i>
502 ${_('General Comments')} ${len(c.comments)}
502 ${_('General Comments')} ${len(c.comments)}
503 </div>
503 </div>
504
504
505 ## render general comments
505 ## render general comments
506 <div id="comment-tr-show">
506 <div id="comment-tr-show">
507 % if general_outdated_comm_count_ver:
507 % if general_outdated_comm_count_ver:
508 <div class="info-box">
508 <div class="info-box">
509 % if general_outdated_comm_count_ver == 1:
509 % if general_outdated_comm_count_ver == 1:
510 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
510 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
511 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
511 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
512 % else:
512 % else:
513 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
513 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
514 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
514 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
515 % endif
515 % endif
516 </div>
516 </div>
517 % endif
517 % endif
518 </div>
518 </div>
519
519
520 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
520 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
521
521
522 % if not c.pull_request.is_closed():
522 % if not c.pull_request.is_closed():
523 ## main comment form and it status
523 ## main comment form and it status
524 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
524 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
525 pull_request_id=c.pull_request.pull_request_id),
525 pull_request_id=c.pull_request.pull_request_id),
526 c.pull_request_review_status,
526 c.pull_request_review_status,
527 is_pull_request=True, change_status=c.allowed_to_change_status)}
527 is_pull_request=True, change_status=c.allowed_to_change_status)}
528
528
529 ## merge status, and merge action
529 ## merge status, and merge action
530 <div class="pull-request-merge">
530 <div class="pull-request-merge">
531 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
531 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
532 </div>
532 </div>
533
533
534 %endif
534 %endif
535
535
536 % endif
536 % endif
537 </div>
537 </div>
538
538
539
539
540 ### NAV SIDEBAR
540 ### NAV SIDEBAR
541 <aside class="right-sidebar right-sidebar-expanded" id="pr-nav-sticky" style="display: none">
541 <aside class="right-sidebar right-sidebar-expanded" id="pr-nav-sticky" style="display: none">
542 <div class="sidenav navbar__inner" >
542 <div class="sidenav navbar__inner" >
543 ## TOGGLE
543 ## TOGGLE
544 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
544 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
545 <a href="#toggleSidebar" class="grey-link-action">
545 <a href="#toggleSidebar" class="grey-link-action">
546
546
547 </a>
547 </a>
548 </div>
548 </div>
549
549
550 ## CONTENT
550 ## CONTENT
551 <div class="sidebar-content">
551 <div class="sidebar-content">
552
552
553 ## Drafts
553 ## Drafts
554 % if c.rhodecode_edition_id == 'EE':
554 % if c.rhodecode_edition_id == 'EE':
555 <div class="sidebar-element clear-both">
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')}">
556 <div class="tooltip right-sidebar-collapsed-state" style="display: none;" onclick="toggleSidebar(); return false" title="${_('Drafts')}">
557 <i class="icon-comment icon-draft"></i>
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 </div>
559 </div>
560
560
561 <div class="right-sidebar-expanded-state pr-details-title">
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 <i class="icon-comment icon-draft"></i>
566 <i class="icon-comment icon-draft"></i>
564 ${_('Drafts')}
567 ${_('Drafts')}
565 </span>
568 </span>
569 <span class="block-right action_button last-item" onclick="submitDrafts(event)">${_('Submit')}</span>
566 </div>
570 </div>
567
571
568 <div id="drafts" class="right-sidebar-expanded-state pr-details-content reviewers">
572 <div id="drafts" class="right-sidebar-expanded-state pr-details-content reviewers">
569 ## members redering block
573 % if c.draft_comments:
570
574 ${sidebar.comments_table(c.draft_comments, len(c.draft_comments), draft_comments=True)}
571
575 % else:
572 ???
576 <table class="drafts-content-table">
573
577 <tr>
574
578 <td>
575 ## end members redering block
579 ${_('No TODOs yet')}
576
580 </td>
581 </tr>
582 </table>
583 % endif
577 </div>
584 </div>
578
585
579 </div>
586 </div>
580 % endif
587 % endif
581
588
582 ## RULES SUMMARY/RULES
589 ## RULES SUMMARY/RULES
583 <div class="sidebar-element clear-both">
590 <div class="sidebar-element clear-both">
584 <% vote_title = _ungettext(
591 <% vote_title = _ungettext(
585 'Status calculated based on votes from {} reviewer',
592 'Status calculated based on votes from {} reviewer',
586 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
593 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
587 %>
594 %>
588
595
589 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
596 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
590 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
597 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
591 ${c.reviewers_count}
598 ${c.reviewers_count}
592 </div>
599 </div>
593
600
594 ## REVIEW RULES
601 ## REVIEW RULES
595 <div id="review_rules" style="display: none" class="">
602 <div id="review_rules" style="display: none" class="">
596 <div class="right-sidebar-expanded-state pr-details-title">
603 <div class="right-sidebar-expanded-state pr-details-title">
597 <span class="sidebar-heading">
604 <span class="sidebar-heading">
598 ${_('Reviewer rules')}
605 ${_('Reviewer rules')}
599 </span>
606 </span>
600
607
601 </div>
608 </div>
602 <div class="pr-reviewer-rules">
609 <div class="pr-reviewer-rules">
603 ## review rules will be appended here, by default reviewers logic
610 ## review rules will be appended here, by default reviewers logic
604 </div>
611 </div>
605 <input id="review_data" type="hidden" name="review_data" value="">
612 <input id="review_data" type="hidden" name="review_data" value="">
606 </div>
613 </div>
607
614
608 ## REVIEWERS
615 ## REVIEWERS
609 <div class="right-sidebar-expanded-state pr-details-title">
616 <div class="right-sidebar-expanded-state pr-details-title">
610 <span class="tooltip sidebar-heading" title="${vote_title}">
617 <span class="tooltip sidebar-heading" title="${vote_title}">
611 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
618 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
612 ${_('Reviewers')}
619 ${_('Reviewers')}
613 </span>
620 </span>
614 %if c.allowed_to_update:
621 %if c.allowed_to_update:
615 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
622 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
616 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
623 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
617 %else:
624 %else:
618 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Show rules')}</span>
625 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Show rules')}</span>
619 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
626 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
620 %endif
627 %endif
621 </div>
628 </div>
622
629
623 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
630 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
624
631
625 ## members redering block
632 ## members redering block
626 <input type="hidden" name="__start__" value="review_members:sequence">
633 <input type="hidden" name="__start__" value="review_members:sequence">
627
634
628 <table id="review_members" class="group_members">
635 <table id="review_members" class="group_members">
629 ## This content is loaded via JS and ReviewersPanel
636 ## This content is loaded via JS and ReviewersPanel
630 </table>
637 </table>
631
638
632 <input type="hidden" name="__end__" value="review_members:sequence">
639 <input type="hidden" name="__end__" value="review_members:sequence">
633 ## end members redering block
640 ## end members redering block
634
641
635 %if not c.pull_request.is_closed():
642 %if not c.pull_request.is_closed():
636 <div id="add_reviewer" class="ac" style="display: none;">
643 <div id="add_reviewer" class="ac" style="display: none;">
637 %if c.allowed_to_update:
644 %if c.allowed_to_update:
638 % if not c.forbid_adding_reviewers:
645 % if not c.forbid_adding_reviewers:
639 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px">
646 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px">
640 <input class="ac-input" id="user" name="user" placeholder="${_('Add reviewer or reviewer group')}" type="text" autocomplete="off">
647 <input class="ac-input" id="user" name="user" placeholder="${_('Add reviewer or reviewer group')}" type="text" autocomplete="off">
641 <div id="reviewers_container"></div>
648 <div id="reviewers_container"></div>
642 </div>
649 </div>
643 % endif
650 % endif
644 <div class="pull-right" style="margin-bottom: 15px">
651 <div class="pull-right" style="margin-bottom: 15px">
645 <button data-role="reviewer" id="update_reviewers" class="btn btn-small no-margin">${_('Save Changes')}</button>
652 <button data-role="reviewer" id="update_reviewers" class="btn btn-small no-margin">${_('Save Changes')}</button>
646 </div>
653 </div>
647 %endif
654 %endif
648 </div>
655 </div>
649 %endif
656 %endif
650 </div>
657 </div>
651 </div>
658 </div>
652
659
653 ## OBSERVERS
660 ## OBSERVERS
654 % if c.rhodecode_edition_id == 'EE':
661 % if c.rhodecode_edition_id == 'EE':
655 <div class="sidebar-element clear-both">
662 <div class="sidebar-element clear-both">
656 <% vote_title = _ungettext(
663 <% vote_title = _ungettext(
657 '{} observer without voting right.',
664 '{} observer without voting right.',
658 '{} observers without voting right.', c.observers_count).format(c.observers_count)
665 '{} observers without voting right.', c.observers_count).format(c.observers_count)
659 %>
666 %>
660
667
661 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
668 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
662 <i class="icon-circle-thin"></i>
669 <i class="icon-circle-thin"></i>
663 ${c.observers_count}
670 ${c.observers_count}
664 </div>
671 </div>
665
672
666 <div class="right-sidebar-expanded-state pr-details-title">
673 <div class="right-sidebar-expanded-state pr-details-title">
667 <span class="tooltip sidebar-heading" title="${vote_title}">
674 <span class="tooltip sidebar-heading" title="${vote_title}">
668 <i class="icon-circle-thin"></i>
675 <i class="icon-circle-thin"></i>
669 ${_('Observers')}
676 ${_('Observers')}
670 </span>
677 </span>
671 %if c.allowed_to_update:
678 %if c.allowed_to_update:
672 <span id="open_edit_observers" class="block-right action_button last-item">${_('Edit')}</span>
679 <span id="open_edit_observers" class="block-right action_button last-item">${_('Edit')}</span>
673 <span id="close_edit_observers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
680 <span id="close_edit_observers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
674 %endif
681 %endif
675 </div>
682 </div>
676
683
677 <div id="observers" class="right-sidebar-expanded-state pr-details-content reviewers">
684 <div id="observers" class="right-sidebar-expanded-state pr-details-content reviewers">
678 ## members redering block
685 ## members redering block
679 <input type="hidden" name="__start__" value="observer_members:sequence">
686 <input type="hidden" name="__start__" value="observer_members:sequence">
680
687
681 <table id="observer_members" class="group_members">
688 <table id="observer_members" class="group_members">
682 ## This content is loaded via JS and ReviewersPanel
689 ## This content is loaded via JS and ReviewersPanel
683 </table>
690 </table>
684
691
685 <input type="hidden" name="__end__" value="observer_members:sequence">
692 <input type="hidden" name="__end__" value="observer_members:sequence">
686 ## end members redering block
693 ## end members redering block
687
694
688 %if not c.pull_request.is_closed():
695 %if not c.pull_request.is_closed():
689 <div id="add_observer" class="ac" style="display: none;">
696 <div id="add_observer" class="ac" style="display: none;">
690 %if c.allowed_to_update:
697 %if c.allowed_to_update:
691 % if not c.forbid_adding_reviewers or 1:
698 % if not c.forbid_adding_reviewers or 1:
692 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px" >
699 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px" >
693 <input class="ac-input" id="observer" name="observer" placeholder="${_('Add observer or observer group')}" type="text" autocomplete="off">
700 <input class="ac-input" id="observer" name="observer" placeholder="${_('Add observer or observer group')}" type="text" autocomplete="off">
694 <div id="observers_container"></div>
701 <div id="observers_container"></div>
695 </div>
702 </div>
696 % endif
703 % endif
697 <div class="pull-right" style="margin-bottom: 15px">
704 <div class="pull-right" style="margin-bottom: 15px">
698 <button data-role="observer" id="update_observers" class="btn btn-small no-margin">${_('Save Changes')}</button>
705 <button data-role="observer" id="update_observers" class="btn btn-small no-margin">${_('Save Changes')}</button>
699 </div>
706 </div>
700 %endif
707 %endif
701 </div>
708 </div>
702 %endif
709 %endif
703 </div>
710 </div>
704 </div>
711 </div>
705 % endif
712 % endif
706
713
707 ## TODOs
714 ## TODOs
708 <div class="sidebar-element clear-both">
715 <div id="todosTable" class="sidebar-element clear-both">
709 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
716 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
710 <i class="icon-flag-filled"></i>
717 <i class="icon-flag-filled"></i>
711 <span id="todos-count">${len(c.unresolved_comments)}</span>
718 <span id="todos-count">${len(c.unresolved_comments)}</span>
712 </div>
719 </div>
713
720
714 <div class="right-sidebar-expanded-state pr-details-title">
721 <div class="right-sidebar-expanded-state pr-details-title">
715 ## Only show unresolved, that is only what matters
722 ## Only show unresolved, that is only what matters
716 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
723 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
717 <i class="icon-flag-filled"></i>
724 <i class="icon-flag-filled"></i>
718 TODOs
725 TODOs
719 </span>
726 </span>
720
727
721 % if not c.at_version:
728 % if not c.at_version:
722 % if c.resolved_comments:
729 % if c.resolved_comments:
723 <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>
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 % else:
731 % else:
725 <span class="block-right last-item noselect">Show resolved</span>
732 <span class="block-right last-item noselect">Show resolved</span>
726 % endif
733 % endif
727 % endif
734 % endif
728 </div>
735 </div>
729
736
730 <div class="right-sidebar-expanded-state pr-details-content">
737 <div class="right-sidebar-expanded-state pr-details-content">
731
738
732 % if c.at_version:
739 % if c.at_version:
733 <table>
740 <table>
734 <tr>
741 <tr>
735 <td class="unresolved-todo-text">${_('TODOs unavailable when browsing versions')}.</td>
742 <td class="unresolved-todo-text">${_('TODOs unavailable when browsing versions')}.</td>
736 </tr>
743 </tr>
737 </table>
744 </table>
738 % else:
745 % else:
739 % if c.unresolved_comments + c.resolved_comments:
746 % if c.unresolved_comments + c.resolved_comments:
740 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
747 ${sidebar.comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
741 % else:
748 % else:
742 <table>
749 <table class="todos-content-table">
743 <tr>
750 <tr>
744 <td>
751 <td>
745 ${_('No TODOs yet')}
752 ${_('No TODOs yet')}
746 </td>
753 </td>
747 </tr>
754 </tr>
748 </table>
755 </table>
749 % endif
756 % endif
750 % endif
757 % endif
751 </div>
758 </div>
752 </div>
759 </div>
753
760
754 ## COMMENTS
761 ## COMMENTS
755 <div class="sidebar-element clear-both">
762 <div id="commentsTable" class="sidebar-element clear-both">
756 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
763 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
757 <i class="icon-comment" style="color: #949494"></i>
764 <i class="icon-comment" style="color: #949494"></i>
758 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
765 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
759 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
766 <span class="display-none" id="general-comments-count">${len(c.comments)}</span>
760 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
767 <span class="display-none" id="inline-comments-count">${len(c.inline_comments_flat)}</span>
761 </div>
768 </div>
762
769
763 <div class="right-sidebar-expanded-state pr-details-title">
770 <div class="right-sidebar-expanded-state pr-details-title">
764 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
771 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
765 <i class="icon-comment" style="color: #949494"></i>
772 <i class="icon-comment" style="color: #949494"></i>
766 ${_('Comments')}
773 ${_('Comments')}
767
774
768 ## % if outdated_comm_count_ver:
775 ## % if outdated_comm_count_ver:
769 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
776 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
770 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
777 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
771 ## </a>
778 ## </a>
772 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
779 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
773 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
780 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
774
781
775 ## % else:
782 ## % else:
776 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
783 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
777 ## % endif
784 ## % endif
778
785
779 </span>
786 </span>
780
787
781 % if outdated_comm_count_ver:
788 % if outdated_comm_count_ver:
782 <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>
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 % else:
790 % else:
784 <span class="block-right last-item noselect">Show hidden</span>
791 <span class="block-right last-item noselect">Show hidden</span>
785 % endif
792 % endif
786
793
787 </div>
794 </div>
788
795
789 <div class="right-sidebar-expanded-state pr-details-content">
796 <div class="right-sidebar-expanded-state pr-details-content">
790 % if c.inline_comments_flat + c.comments:
797 % if c.inline_comments_flat + c.comments:
791 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
798 ${sidebar.comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
792 % else:
799 % else:
793 <table>
800 <table class="comments-content-table">
794 <tr>
801 <tr>
795 <td>
802 <td>
796 ${_('No Comments yet')}
803 ${_('No Comments yet')}
797 </td>
804 </td>
798 </tr>
805 </tr>
799 </table>
806 </table>
800 % endif
807 % endif
801 </div>
808 </div>
802
809
803 </div>
810 </div>
804
811
805 ## Referenced Tickets
812 ## Referenced Tickets
806 <div class="sidebar-element clear-both">
813 <div class="sidebar-element clear-both">
807 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Referenced Tickets')}">
814 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Referenced Tickets')}">
808 <i class="icon-info-circled"></i>
815 <i class="icon-info-circled"></i>
809 ${(len(c.referenced_desc_issues) + len(c.referenced_commit_issues))}
816 ${(len(c.referenced_desc_issues) + len(c.referenced_commit_issues))}
810 </div>
817 </div>
811
818
812 <div class="right-sidebar-expanded-state pr-details-title">
819 <div class="right-sidebar-expanded-state pr-details-title">
813 <span class="sidebar-heading">
820 <span class="sidebar-heading">
814 <i class="icon-info-circled"></i>
821 <i class="icon-info-circled"></i>
815 ${_('Referenced Tickets')}
822 ${_('Referenced Tickets')}
816 </span>
823 </span>
817 </div>
824 </div>
818 <div class="right-sidebar-expanded-state pr-details-content">
825 <div class="right-sidebar-expanded-state pr-details-content">
819 <table>
826 <table>
820
827
821 <tr><td><code>${_('In pull request description')}:</code></td></tr>
828 <tr><td><code>${_('In pull request description')}:</code></td></tr>
822 % if c.referenced_desc_issues:
829 % if c.referenced_desc_issues:
823 % for ticket_dict in sorted(c.referenced_desc_issues):
830 % for ticket_dict in sorted(c.referenced_desc_issues):
824 <tr>
831 <tr>
825 <td>
832 <td>
826 <a href="${ticket_dict.get('url')}">
833 <a href="${ticket_dict.get('url')}">
827 ${ticket_dict.get('id')}
834 ${ticket_dict.get('id')}
828 </a>
835 </a>
829 </td>
836 </td>
830 </tr>
837 </tr>
831 % endfor
838 % endfor
832 % else:
839 % else:
833 <tr>
840 <tr>
834 <td>
841 <td>
835 ${_('No Ticket data found.')}
842 ${_('No Ticket data found.')}
836 </td>
843 </td>
837 </tr>
844 </tr>
838 % endif
845 % endif
839
846
840 <tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
847 <tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
841 % if c.referenced_commit_issues:
848 % if c.referenced_commit_issues:
842 % for ticket_dict in sorted(c.referenced_commit_issues):
849 % for ticket_dict in sorted(c.referenced_commit_issues):
843 <tr>
850 <tr>
844 <td>
851 <td>
845 <a href="${ticket_dict.get('url')}">
852 <a href="${ticket_dict.get('url')}">
846 ${ticket_dict.get('id')}
853 ${ticket_dict.get('id')}
847 </a>
854 </a>
848 </td>
855 </td>
849 </tr>
856 </tr>
850 % endfor
857 % endfor
851 % else:
858 % else:
852 <tr>
859 <tr>
853 <td>
860 <td>
854 ${_('No Ticket data found.')}
861 ${_('No Ticket data found.')}
855 </td>
862 </td>
856 </tr>
863 </tr>
857 % endif
864 % endif
858 </table>
865 </table>
859
866
860 </div>
867 </div>
861 </div>
868 </div>
862
869
863 </div>
870 </div>
864
871
865 </div>
872 </div>
866 </aside>
873 </aside>
867
874
868 ## This JS needs to be at the end
875 ## This JS needs to be at the end
869 <script type="text/javascript">
876 <script type="text/javascript">
870
877
871 versionController = new VersionController();
878 versionController = new VersionController();
872 versionController.init();
879 versionController.init();
873
880
874 reviewersController = new ReviewersController();
881 reviewersController = new ReviewersController();
875 commitsController = new CommitsController();
882 commitsController = new CommitsController();
876 commentsController = new CommentsController();
883 commentsController = new CommentsController();
877
884
878 updateController = new UpdatePrController();
885 updateController = new UpdatePrController();
879
886
880 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
887 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
881 window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
888 window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
882 window.setObserversData = ${c.pull_request_set_observers_data_json | n};
889 window.setObserversData = ${c.pull_request_set_observers_data_json | n};
883
890
884 (function () {
891 (function () {
885 "use strict";
892 "use strict";
886
893
887 // custom code mirror
894 // custom code mirror
888 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
895 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
889
896
890 PRDetails.init();
897 PRDetails.init();
891 ReviewersPanel.init(reviewersController, reviewerRulesData, setReviewersData);
898 ReviewersPanel.init(reviewersController, reviewerRulesData, setReviewersData);
892 ObserversPanel.init(reviewersController, reviewerRulesData, setObserversData);
899 ObserversPanel.init(reviewersController, reviewerRulesData, setObserversData);
893
900
894 window.showOutdated = function (self) {
901 window.showOutdated = function (self) {
895 $('.comment-inline.comment-outdated').show();
902 $('.comment-inline.comment-outdated').show();
896 $('.filediff-outdated').show();
903 $('.filediff-outdated').show();
897 $('.showOutdatedComments').hide();
904 $('.showOutdatedComments').hide();
898 $('.hideOutdatedComments').show();
905 $('.hideOutdatedComments').show();
899 };
906 };
900
907
901 window.hideOutdated = function (self) {
908 window.hideOutdated = function (self) {
902 $('.comment-inline.comment-outdated').hide();
909 $('.comment-inline.comment-outdated').hide();
903 $('.filediff-outdated').hide();
910 $('.filediff-outdated').hide();
904 $('.hideOutdatedComments').hide();
911 $('.hideOutdatedComments').hide();
905 $('.showOutdatedComments').show();
912 $('.showOutdatedComments').show();
906 };
913 };
907
914
908 window.refreshMergeChecks = function () {
915 window.refreshMergeChecks = function () {
909 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
916 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
910 $('.pull-request-merge').css('opacity', 0.3);
917 $('.pull-request-merge').css('opacity', 0.3);
911 $('.action-buttons-extra').css('opacity', 0.3);
918 $('.action-buttons-extra').css('opacity', 0.3);
912
919
913 $('.pull-request-merge').load(
920 $('.pull-request-merge').load(
914 loadUrl, function () {
921 loadUrl, function () {
915 $('.pull-request-merge').css('opacity', 1);
922 $('.pull-request-merge').css('opacity', 1);
916
923
917 $('.action-buttons-extra').css('opacity', 1);
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 window.closePullRequest = function (status) {
946 window.closePullRequest = function (status) {
923 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
947 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
924 return false;
948 return false;
925 }
949 }
926 // inject closing flag
950 // inject closing flag
927 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
951 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
928 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
952 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
929 $(generalCommentForm.submitForm).submit();
953 $(generalCommentForm.submitForm).submit();
930 };
954 };
931
955
932 //TODO this functionality is now missing
956 //TODO this functionality is now missing
933 $('#show-outdated-comments').on('click', function (e) {
957 $('#show-outdated-comments').on('click', function (e) {
934 var button = $(this);
958 var button = $(this);
935 var outdated = $('.comment-outdated');
959 var outdated = $('.comment-outdated');
936
960
937 if (button.html() === "(Show)") {
961 if (button.html() === "(Show)") {
938 button.html("(Hide)");
962 button.html("(Hide)");
939 outdated.show();
963 outdated.show();
940 } else {
964 } else {
941 button.html("(Show)");
965 button.html("(Show)");
942 outdated.hide();
966 outdated.hide();
943 }
967 }
944 });
968 });
945
969
946 $('#merge_pull_request_form').submit(function () {
970 $('#merge_pull_request_form').submit(function () {
947 if (!$('#merge_pull_request').attr('disabled')) {
971 if (!$('#merge_pull_request').attr('disabled')) {
948 $('#merge_pull_request').attr('disabled', 'disabled');
972 $('#merge_pull_request').attr('disabled', 'disabled');
949 }
973 }
950 return true;
974 return true;
951 });
975 });
952
976
953 $('#edit_pull_request').on('click', function (e) {
977 $('#edit_pull_request').on('click', function (e) {
954 var title = $('#pr-title-input').val();
978 var title = $('#pr-title-input').val();
955 var description = codeMirrorInstance.getValue();
979 var description = codeMirrorInstance.getValue();
956 var renderer = $('#pr-renderer-input').val();
980 var renderer = $('#pr-renderer-input').val();
957 editPullRequest(
981 editPullRequest(
958 "${c.repo_name}", "${c.pull_request.pull_request_id}",
982 "${c.repo_name}", "${c.pull_request.pull_request_id}",
959 title, description, renderer);
983 title, description, renderer);
960 });
984 });
961
985
962 var $updateButtons = $('#update_reviewers,#update_observers');
986 var $updateButtons = $('#update_reviewers,#update_observers');
963 $updateButtons.on('click', function (e) {
987 $updateButtons.on('click', function (e) {
964 var role = $(this).data('role');
988 var role = $(this).data('role');
965 $updateButtons.attr('disabled', 'disabled');
989 $updateButtons.attr('disabled', 'disabled');
966 $updateButtons.addClass('disabled');
990 $updateButtons.addClass('disabled');
967 $updateButtons.html(_gettext('Saving...'));
991 $updateButtons.html(_gettext('Saving...'));
968 reviewersController.updateReviewers(
992 reviewersController.updateReviewers(
969 templateContext.repo_name,
993 templateContext.repo_name,
970 templateContext.pull_request_data.pull_request_id,
994 templateContext.pull_request_data.pull_request_id,
971 role
995 role
972 );
996 );
973 });
997 });
974
998
975 // fixing issue with caches on firefox
999 // fixing issue with caches on firefox
976 $('#update_commits').removeAttr("disabled");
1000 $('#update_commits').removeAttr("disabled");
977
1001
978 $('.show-inline-comments').on('click', function (e) {
1002 $('.show-inline-comments').on('click', function (e) {
979 var boxid = $(this).attr('data-comment-id');
1003 var boxid = $(this).attr('data-comment-id');
980 var button = $(this);
1004 var button = $(this);
981
1005
982 if (button.hasClass("comments-visible")) {
1006 if (button.hasClass("comments-visible")) {
983 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
1007 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
984 $(this).hide();
1008 $(this).hide();
985 });
1009 });
986 button.removeClass("comments-visible");
1010 button.removeClass("comments-visible");
987 } else {
1011 } else {
988 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
1012 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
989 $(this).show();
1013 $(this).show();
990 });
1014 });
991 button.addClass("comments-visible");
1015 button.addClass("comments-visible");
992 }
1016 }
993 });
1017 });
994
1018
995 $('.show-inline-comments').on('change', function (e) {
1019 $('.show-inline-comments').on('change', function (e) {
996 var show = 'none';
1020 var show = 'none';
997 var target = e.currentTarget;
1021 var target = e.currentTarget;
998 if (target.checked) {
1022 if (target.checked) {
999 show = ''
1023 show = ''
1000 }
1024 }
1001 var boxid = $(target).attr('id_for');
1025 var boxid = $(target).attr('id_for');
1002 var comments = $('#{0} .inline-comments'.format(boxid));
1026 var comments = $('#{0} .inline-comments'.format(boxid));
1003 var fn_display = function (idx) {
1027 var fn_display = function (idx) {
1004 $(this).css('display', show);
1028 $(this).css('display', show);
1005 };
1029 };
1006 $(comments).each(fn_display);
1030 $(comments).each(fn_display);
1007 var btns = $('#{0} .inline-comments-button'.format(boxid));
1031 var btns = $('#{0} .inline-comments-button'.format(boxid));
1008 $(btns).each(fn_display);
1032 $(btns).each(fn_display);
1009 });
1033 });
1010
1034
1011 // register submit callback on commentForm form to track TODOs, and refresh mergeChecks conditions
1035 // register submit callback on commentForm form to track TODOs, and refresh mergeChecks conditions
1012 window.commentFormGlobalSubmitSuccessCallback = function (comment) {
1036 window.commentFormGlobalSubmitSuccessCallback = function (comment) {
1013 if (!comment.draft) {
1037 if (!comment.draft) {
1014 refreshMergeChecks();
1038 refreshMergeChecks();
1015 }
1039 }
1016 };
1040 };
1017
1041
1018 ReviewerAutoComplete('#user', reviewersController);
1042 ReviewerAutoComplete('#user', reviewersController);
1019 ObserverAutoComplete('#observer', reviewersController);
1043 ObserverAutoComplete('#observer', reviewersController);
1020
1044
1021 })();
1045 })();
1022
1046
1023 $(document).ready(function () {
1047 $(document).ready(function () {
1024
1048
1025 var channel = '${c.pr_broadcast_channel}';
1049 var channel = '${c.pr_broadcast_channel}';
1026 new ReviewerPresenceController(channel)
1050 new ReviewerPresenceController(channel)
1027 // register globally so inject comment logic can re-use it.
1051 // register globally so inject comment logic can re-use it.
1028 window.commentsController = commentsController;
1052 window.commentsController = commentsController;
1029
1030 })
1053 })
1031 </script>
1054 </script>
1032
1055
1033 </%def>
1056 </%def>
General Comments 0
You need to be logged in to leave comments. Login now