##// END OF EJS Templates
pull-requests: overhaul of the UX by adding new sidebar...
marcink -
r4482:3b004b10 default
parent child Browse files
Show More
@@ -1,533 +1,543 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(
349 name='pullrequest_comments',
350 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comments',
351 repo_route=True)
352
353 config.add_route(
354 name='pullrequest_todos',
355 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/todos',
356 repo_route=True)
357
348 # Artifacts, (EE feature)
358 # Artifacts, (EE feature)
349 config.add_route(
359 config.add_route(
350 name='repo_artifacts_list',
360 name='repo_artifacts_list',
351 pattern='/{repo_name:.*?[^/]}/artifacts', repo_route=True)
361 pattern='/{repo_name:.*?[^/]}/artifacts', repo_route=True)
352
362
353 # Settings
363 # Settings
354 config.add_route(
364 config.add_route(
355 name='edit_repo',
365 name='edit_repo',
356 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
366 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
357 # update is POST on edit_repo
367 # update is POST on edit_repo
358
368
359 # Settings advanced
369 # Settings advanced
360 config.add_route(
370 config.add_route(
361 name='edit_repo_advanced',
371 name='edit_repo_advanced',
362 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
372 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
363 config.add_route(
373 config.add_route(
364 name='edit_repo_advanced_archive',
374 name='edit_repo_advanced_archive',
365 pattern='/{repo_name:.*?[^/]}/settings/advanced/archive', repo_route=True)
375 pattern='/{repo_name:.*?[^/]}/settings/advanced/archive', repo_route=True)
366 config.add_route(
376 config.add_route(
367 name='edit_repo_advanced_delete',
377 name='edit_repo_advanced_delete',
368 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
378 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
369 config.add_route(
379 config.add_route(
370 name='edit_repo_advanced_locking',
380 name='edit_repo_advanced_locking',
371 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
381 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
372 config.add_route(
382 config.add_route(
373 name='edit_repo_advanced_journal',
383 name='edit_repo_advanced_journal',
374 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
384 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
375 config.add_route(
385 config.add_route(
376 name='edit_repo_advanced_fork',
386 name='edit_repo_advanced_fork',
377 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
387 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
378
388
379 config.add_route(
389 config.add_route(
380 name='edit_repo_advanced_hooks',
390 name='edit_repo_advanced_hooks',
381 pattern='/{repo_name:.*?[^/]}/settings/advanced/hooks', repo_route=True)
391 pattern='/{repo_name:.*?[^/]}/settings/advanced/hooks', repo_route=True)
382
392
383 # Caches
393 # Caches
384 config.add_route(
394 config.add_route(
385 name='edit_repo_caches',
395 name='edit_repo_caches',
386 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
396 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
387
397
388 # Permissions
398 # Permissions
389 config.add_route(
399 config.add_route(
390 name='edit_repo_perms',
400 name='edit_repo_perms',
391 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
401 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
392
402
393 config.add_route(
403 config.add_route(
394 name='edit_repo_perms_set_private',
404 name='edit_repo_perms_set_private',
395 pattern='/{repo_name:.*?[^/]}/settings/permissions/set_private', repo_route=True)
405 pattern='/{repo_name:.*?[^/]}/settings/permissions/set_private', repo_route=True)
396
406
397 # Permissions Branch (EE feature)
407 # Permissions Branch (EE feature)
398 config.add_route(
408 config.add_route(
399 name='edit_repo_perms_branch',
409 name='edit_repo_perms_branch',
400 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions', repo_route=True)
410 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions', repo_route=True)
401 config.add_route(
411 config.add_route(
402 name='edit_repo_perms_branch_delete',
412 name='edit_repo_perms_branch_delete',
403 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions/{rule_id}/delete',
413 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions/{rule_id}/delete',
404 repo_route=True)
414 repo_route=True)
405
415
406 # Maintenance
416 # Maintenance
407 config.add_route(
417 config.add_route(
408 name='edit_repo_maintenance',
418 name='edit_repo_maintenance',
409 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
419 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
410
420
411 config.add_route(
421 config.add_route(
412 name='edit_repo_maintenance_execute',
422 name='edit_repo_maintenance_execute',
413 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
423 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
414
424
415 # Fields
425 # Fields
416 config.add_route(
426 config.add_route(
417 name='edit_repo_fields',
427 name='edit_repo_fields',
418 pattern='/{repo_name:.*?[^/]}/settings/fields', repo_route=True)
428 pattern='/{repo_name:.*?[^/]}/settings/fields', repo_route=True)
419 config.add_route(
429 config.add_route(
420 name='edit_repo_fields_create',
430 name='edit_repo_fields_create',
421 pattern='/{repo_name:.*?[^/]}/settings/fields/create', repo_route=True)
431 pattern='/{repo_name:.*?[^/]}/settings/fields/create', repo_route=True)
422 config.add_route(
432 config.add_route(
423 name='edit_repo_fields_delete',
433 name='edit_repo_fields_delete',
424 pattern='/{repo_name:.*?[^/]}/settings/fields/{field_id}/delete', repo_route=True)
434 pattern='/{repo_name:.*?[^/]}/settings/fields/{field_id}/delete', repo_route=True)
425
435
426 # Locking
436 # Locking
427 config.add_route(
437 config.add_route(
428 name='repo_edit_toggle_locking',
438 name='repo_edit_toggle_locking',
429 pattern='/{repo_name:.*?[^/]}/settings/toggle_locking', repo_route=True)
439 pattern='/{repo_name:.*?[^/]}/settings/toggle_locking', repo_route=True)
430
440
431 # Remote
441 # Remote
432 config.add_route(
442 config.add_route(
433 name='edit_repo_remote',
443 name='edit_repo_remote',
434 pattern='/{repo_name:.*?[^/]}/settings/remote', repo_route=True)
444 pattern='/{repo_name:.*?[^/]}/settings/remote', repo_route=True)
435 config.add_route(
445 config.add_route(
436 name='edit_repo_remote_pull',
446 name='edit_repo_remote_pull',
437 pattern='/{repo_name:.*?[^/]}/settings/remote/pull', repo_route=True)
447 pattern='/{repo_name:.*?[^/]}/settings/remote/pull', repo_route=True)
438 config.add_route(
448 config.add_route(
439 name='edit_repo_remote_push',
449 name='edit_repo_remote_push',
440 pattern='/{repo_name:.*?[^/]}/settings/remote/push', repo_route=True)
450 pattern='/{repo_name:.*?[^/]}/settings/remote/push', repo_route=True)
441
451
442 # Statistics
452 # Statistics
443 config.add_route(
453 config.add_route(
444 name='edit_repo_statistics',
454 name='edit_repo_statistics',
445 pattern='/{repo_name:.*?[^/]}/settings/statistics', repo_route=True)
455 pattern='/{repo_name:.*?[^/]}/settings/statistics', repo_route=True)
446 config.add_route(
456 config.add_route(
447 name='edit_repo_statistics_reset',
457 name='edit_repo_statistics_reset',
448 pattern='/{repo_name:.*?[^/]}/settings/statistics/update', repo_route=True)
458 pattern='/{repo_name:.*?[^/]}/settings/statistics/update', repo_route=True)
449
459
450 # Issue trackers
460 # Issue trackers
451 config.add_route(
461 config.add_route(
452 name='edit_repo_issuetracker',
462 name='edit_repo_issuetracker',
453 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers', repo_route=True)
463 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers', repo_route=True)
454 config.add_route(
464 config.add_route(
455 name='edit_repo_issuetracker_test',
465 name='edit_repo_issuetracker_test',
456 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/test', repo_route=True)
466 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/test', repo_route=True)
457 config.add_route(
467 config.add_route(
458 name='edit_repo_issuetracker_delete',
468 name='edit_repo_issuetracker_delete',
459 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/delete', repo_route=True)
469 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/delete', repo_route=True)
460 config.add_route(
470 config.add_route(
461 name='edit_repo_issuetracker_update',
471 name='edit_repo_issuetracker_update',
462 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/update', repo_route=True)
472 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/update', repo_route=True)
463
473
464 # VCS Settings
474 # VCS Settings
465 config.add_route(
475 config.add_route(
466 name='edit_repo_vcs',
476 name='edit_repo_vcs',
467 pattern='/{repo_name:.*?[^/]}/settings/vcs', repo_route=True)
477 pattern='/{repo_name:.*?[^/]}/settings/vcs', repo_route=True)
468 config.add_route(
478 config.add_route(
469 name='edit_repo_vcs_update',
479 name='edit_repo_vcs_update',
470 pattern='/{repo_name:.*?[^/]}/settings/vcs/update', repo_route=True)
480 pattern='/{repo_name:.*?[^/]}/settings/vcs/update', repo_route=True)
471
481
472 # svn pattern
482 # svn pattern
473 config.add_route(
483 config.add_route(
474 name='edit_repo_vcs_svn_pattern_delete',
484 name='edit_repo_vcs_svn_pattern_delete',
475 pattern='/{repo_name:.*?[^/]}/settings/vcs/svn_pattern/delete', repo_route=True)
485 pattern='/{repo_name:.*?[^/]}/settings/vcs/svn_pattern/delete', repo_route=True)
476
486
477 # Repo Review Rules (EE feature)
487 # Repo Review Rules (EE feature)
478 config.add_route(
488 config.add_route(
479 name='repo_reviewers',
489 name='repo_reviewers',
480 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
490 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
481
491
482 config.add_route(
492 config.add_route(
483 name='repo_default_reviewers_data',
493 name='repo_default_reviewers_data',
484 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
494 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
485
495
486 # Repo Automation (EE feature)
496 # Repo Automation (EE feature)
487 config.add_route(
497 config.add_route(
488 name='repo_automation',
498 name='repo_automation',
489 pattern='/{repo_name:.*?[^/]}/settings/automation', repo_route=True)
499 pattern='/{repo_name:.*?[^/]}/settings/automation', repo_route=True)
490
500
491 # Strip
501 # Strip
492 config.add_route(
502 config.add_route(
493 name='edit_repo_strip',
503 name='edit_repo_strip',
494 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
504 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
495
505
496 config.add_route(
506 config.add_route(
497 name='strip_check',
507 name='strip_check',
498 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
508 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
499
509
500 config.add_route(
510 config.add_route(
501 name='strip_execute',
511 name='strip_execute',
502 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
512 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
503
513
504 # Audit logs
514 # Audit logs
505 config.add_route(
515 config.add_route(
506 name='edit_repo_audit_logs',
516 name='edit_repo_audit_logs',
507 pattern='/{repo_name:.*?[^/]}/settings/audit_logs', repo_route=True)
517 pattern='/{repo_name:.*?[^/]}/settings/audit_logs', repo_route=True)
508
518
509 # ATOM/RSS Feed, shouldn't contain slashes for outlook compatibility
519 # ATOM/RSS Feed, shouldn't contain slashes for outlook compatibility
510 config.add_route(
520 config.add_route(
511 name='rss_feed_home',
521 name='rss_feed_home',
512 pattern='/{repo_name:.*?[^/]}/feed-rss', repo_route=True)
522 pattern='/{repo_name:.*?[^/]}/feed-rss', repo_route=True)
513
523
514 config.add_route(
524 config.add_route(
515 name='atom_feed_home',
525 name='atom_feed_home',
516 pattern='/{repo_name:.*?[^/]}/feed-atom', repo_route=True)
526 pattern='/{repo_name:.*?[^/]}/feed-atom', repo_route=True)
517
527
518 config.add_route(
528 config.add_route(
519 name='rss_feed_home_old',
529 name='rss_feed_home_old',
520 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
530 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
521
531
522 config.add_route(
532 config.add_route(
523 name='atom_feed_home_old',
533 name='atom_feed_home_old',
524 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
534 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
525
535
526 # NOTE(marcink): needs to be at the end for catch-all
536 # NOTE(marcink): needs to be at the end for catch-all
527 add_route_with_slash(
537 add_route_with_slash(
528 config,
538 config,
529 name='repo_summary',
539 name='repo_summary',
530 pattern='/{repo_name:.*?[^/]}', repo_route=True)
540 pattern='/{repo_name:.*?[^/]}', repo_route=True)
531
541
532 # Scan module for configuration decorators.
542 # Scan module for configuration decorators.
533 config.scan('.views', ignore='.tests')
543 config.scan('.views', ignore='.tests')
@@ -1,723 +1,725 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-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 import logging
22 import logging
23
23
24 from pyramid.httpexceptions import (
24 from pyramid.httpexceptions import (
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
26 from pyramid.view import view_config
26 from pyramid.view import view_config
27 from pyramid.renderers import render
27 from pyramid.renderers import render
28 from pyramid.response import Response
28 from pyramid.response import Response
29
29
30 from rhodecode.apps._base import RepoAppView
30 from rhodecode.apps._base import RepoAppView
31 from rhodecode.apps.file_store import utils as store_utils
31 from rhodecode.apps.file_store import utils as store_utils
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
33
33
34 from rhodecode.lib import diffs, codeblocks
34 from rhodecode.lib import diffs, codeblocks
35 from rhodecode.lib.auth import (
35 from rhodecode.lib.auth import (
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
37
37
38 from rhodecode.lib.compat import OrderedDict
38 from rhodecode.lib.compat import OrderedDict
39 from rhodecode.lib.diffs import (
39 from rhodecode.lib.diffs import (
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 get_diff_whitespace_flag)
41 get_diff_whitespace_flag)
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
43 import rhodecode.lib.helpers as h
43 import rhodecode.lib.helpers as h
44 from rhodecode.lib.utils2 import safe_unicode, str2bool
44 from rhodecode.lib.utils2 import safe_unicode, str2bool
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 from rhodecode.lib.vcs.exceptions import (
46 from rhodecode.lib.vcs.exceptions import (
47 RepositoryError, CommitDoesNotExistError)
47 RepositoryError, CommitDoesNotExistError)
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
49 ChangesetCommentHistory
49 ChangesetCommentHistory
50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 from rhodecode.model.changeset_status import ChangesetStatusModel
51 from rhodecode.model.comment import CommentsModel
51 from rhodecode.model.comment import CommentsModel
52 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
53 from rhodecode.model.settings import VcsSettingsModel
53 from rhodecode.model.settings import VcsSettingsModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 def _update_with_GET(params, request):
58 def _update_with_GET(params, request):
59 for k in ['diff1', 'diff2', 'diff']:
59 for k in ['diff1', 'diff2', 'diff']:
60 params[k] += request.GET.getall(k)
60 params[k] += request.GET.getall(k)
61
61
62
62
63 class RepoCommitsView(RepoAppView):
63 class RepoCommitsView(RepoAppView):
64 def load_default_context(self):
64 def load_default_context(self):
65 c = self._get_local_tmpl_context(include_app_defaults=True)
65 c = self._get_local_tmpl_context(include_app_defaults=True)
66 c.rhodecode_repo = self.rhodecode_vcs_repo
66 c.rhodecode_repo = self.rhodecode_vcs_repo
67
67
68 return c
68 return c
69
69
70 def _is_diff_cache_enabled(self, target_repo):
70 def _is_diff_cache_enabled(self, target_repo):
71 caching_enabled = self._get_general_setting(
71 caching_enabled = self._get_general_setting(
72 target_repo, 'rhodecode_diff_cache')
72 target_repo, 'rhodecode_diff_cache')
73 log.debug('Diff caching enabled: %s', caching_enabled)
73 log.debug('Diff caching enabled: %s', caching_enabled)
74 return caching_enabled
74 return caching_enabled
75
75
76 def _commit(self, commit_id_range, method):
76 def _commit(self, commit_id_range, method):
77 _ = self.request.translate
77 _ = self.request.translate
78 c = self.load_default_context()
78 c = self.load_default_context()
79 c.fulldiff = self.request.GET.get('fulldiff')
79 c.fulldiff = self.request.GET.get('fulldiff')
80
80
81 # fetch global flags of ignore ws or context lines
81 # fetch global flags of ignore ws or context lines
82 diff_context = get_diff_context(self.request)
82 diff_context = get_diff_context(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
83 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
84
84
85 # diff_limit will cut off the whole diff if the limit is applied
85 # diff_limit will cut off the whole diff if the limit is applied
86 # otherwise it will just hide the big files from the front-end
86 # otherwise it will just hide the big files from the front-end
87 diff_limit = c.visual.cut_off_limit_diff
87 diff_limit = c.visual.cut_off_limit_diff
88 file_limit = c.visual.cut_off_limit_file
88 file_limit = c.visual.cut_off_limit_file
89
89
90
90 # get ranges of commit ids if preset
91 # get ranges of commit ids if preset
91 commit_range = commit_id_range.split('...')[:2]
92 commit_range = commit_id_range.split('...')[:2]
92
93
93 try:
94 try:
94 pre_load = ['affected_files', 'author', 'branch', 'date',
95 pre_load = ['affected_files', 'author', 'branch', 'date',
95 'message', 'parents']
96 'message', 'parents']
96 if self.rhodecode_vcs_repo.alias == 'hg':
97 if self.rhodecode_vcs_repo.alias == 'hg':
97 pre_load += ['hidden', 'obsolete', 'phase']
98 pre_load += ['hidden', 'obsolete', 'phase']
98
99
99 if len(commit_range) == 2:
100 if len(commit_range) == 2:
100 commits = self.rhodecode_vcs_repo.get_commits(
101 commits = self.rhodecode_vcs_repo.get_commits(
101 start_id=commit_range[0], end_id=commit_range[1],
102 start_id=commit_range[0], end_id=commit_range[1],
102 pre_load=pre_load, translate_tags=False)
103 pre_load=pre_load, translate_tags=False)
103 commits = list(commits)
104 commits = list(commits)
104 else:
105 else:
105 commits = [self.rhodecode_vcs_repo.get_commit(
106 commits = [self.rhodecode_vcs_repo.get_commit(
106 commit_id=commit_id_range, pre_load=pre_load)]
107 commit_id=commit_id_range, pre_load=pre_load)]
107
108
108 c.commit_ranges = commits
109 c.commit_ranges = commits
109 if not c.commit_ranges:
110 if not c.commit_ranges:
110 raise RepositoryError('The commit range returned an empty result')
111 raise RepositoryError('The commit range returned an empty result')
111 except CommitDoesNotExistError as e:
112 except CommitDoesNotExistError as e:
112 msg = _('No such commit exists. Org exception: `{}`').format(e)
113 msg = _('No such commit exists. Org exception: `{}`').format(e)
113 h.flash(msg, category='error')
114 h.flash(msg, category='error')
114 raise HTTPNotFound()
115 raise HTTPNotFound()
115 except Exception:
116 except Exception:
116 log.exception("General failure")
117 log.exception("General failure")
117 raise HTTPNotFound()
118 raise HTTPNotFound()
118
119
119 c.changes = OrderedDict()
120 c.changes = OrderedDict()
120 c.lines_added = 0
121 c.lines_added = 0
121 c.lines_deleted = 0
122 c.lines_deleted = 0
122
123
123 # auto collapse if we have more than limit
124 # auto collapse if we have more than limit
124 collapse_limit = diffs.DiffProcessor._collapse_commits_over
125 collapse_limit = diffs.DiffProcessor._collapse_commits_over
125 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
126 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
126
127
127 c.commit_statuses = ChangesetStatus.STATUSES
128 c.commit_statuses = ChangesetStatus.STATUSES
128 c.inline_comments = []
129 c.inline_comments = []
129 c.files = []
130 c.files = []
130
131
131 c.statuses = []
132 c.statuses = []
132 c.comments = []
133 c.comments = []
133 c.unresolved_comments = []
134 c.unresolved_comments = []
134 c.resolved_comments = []
135 c.resolved_comments = []
135 if len(c.commit_ranges) == 1:
136 if len(c.commit_ranges) == 1:
136 commit = c.commit_ranges[0]
137 commit = c.commit_ranges[0]
137 c.comments = CommentsModel().get_comments(
138 c.comments = CommentsModel().get_comments(
138 self.db_repo.repo_id,
139 self.db_repo.repo_id,
139 revision=commit.raw_id)
140 revision=commit.raw_id)
140 c.statuses.append(ChangesetStatusModel().get_status(
141 c.statuses.append(ChangesetStatusModel().get_status(
141 self.db_repo.repo_id, commit.raw_id))
142 self.db_repo.repo_id, commit.raw_id))
142 # comments from PR
143 # comments from PR
143 statuses = ChangesetStatusModel().get_statuses(
144 statuses = ChangesetStatusModel().get_statuses(
144 self.db_repo.repo_id, commit.raw_id,
145 self.db_repo.repo_id, commit.raw_id,
145 with_revisions=True)
146 with_revisions=True)
146 prs = set(st.pull_request for st in statuses
147 prs = set(st.pull_request for st in statuses
147 if st.pull_request is not None)
148 if st.pull_request is not None)
148 # from associated statuses, check the pull requests, and
149 # from associated statuses, check the pull requests, and
149 # show comments from them
150 # show comments from them
150 for pr in prs:
151 for pr in prs:
151 c.comments.extend(pr.comments)
152 c.comments.extend(pr.comments)
152
153
153 c.unresolved_comments = CommentsModel()\
154 c.unresolved_comments = CommentsModel()\
154 .get_commit_unresolved_todos(commit.raw_id)
155 .get_commit_unresolved_todos(commit.raw_id)
155 c.resolved_comments = CommentsModel()\
156 c.resolved_comments = CommentsModel()\
156 .get_commit_resolved_todos(commit.raw_id)
157 .get_commit_resolved_todos(commit.raw_id)
157
158
158 diff = None
159 diff = None
159 # Iterate over ranges (default commit view is always one commit)
160 # Iterate over ranges (default commit view is always one commit)
160 for commit in c.commit_ranges:
161 for commit in c.commit_ranges:
161 c.changes[commit.raw_id] = []
162 c.changes[commit.raw_id] = []
162
163
163 commit2 = commit
164 commit2 = commit
164 commit1 = commit.first_parent
165 commit1 = commit.first_parent
165
166
166 if method == 'show':
167 if method == 'show':
167 inline_comments = CommentsModel().get_inline_comments(
168 inline_comments = CommentsModel().get_inline_comments(
168 self.db_repo.repo_id, revision=commit.raw_id)
169 self.db_repo.repo_id, revision=commit.raw_id)
169 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
170 c.inline_cnt = len(CommentsModel().get_inline_comments_as_list(
170 inline_comments))
171 inline_comments))
171 c.inline_comments = inline_comments
172 c.inline_comments = inline_comments
172
173
173 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
174 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
174 self.db_repo)
175 self.db_repo)
175 cache_file_path = diff_cache_exist(
176 cache_file_path = diff_cache_exist(
176 cache_path, 'diff', commit.raw_id,
177 cache_path, 'diff', commit.raw_id,
177 hide_whitespace_changes, diff_context, c.fulldiff)
178 hide_whitespace_changes, diff_context, c.fulldiff)
178
179
179 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
180 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
180 force_recache = str2bool(self.request.GET.get('force_recache'))
181 force_recache = str2bool(self.request.GET.get('force_recache'))
181
182
182 cached_diff = None
183 cached_diff = None
183 if caching_enabled:
184 if caching_enabled:
184 cached_diff = load_cached_diff(cache_file_path)
185 cached_diff = load_cached_diff(cache_file_path)
185
186
186 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
187 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
187 if not force_recache and has_proper_diff_cache:
188 if not force_recache and has_proper_diff_cache:
188 diffset = cached_diff['diff']
189 diffset = cached_diff['diff']
189 else:
190 else:
190 vcs_diff = self.rhodecode_vcs_repo.get_diff(
191 vcs_diff = self.rhodecode_vcs_repo.get_diff(
191 commit1, commit2,
192 commit1, commit2,
192 ignore_whitespace=hide_whitespace_changes,
193 ignore_whitespace=hide_whitespace_changes,
193 context=diff_context)
194 context=diff_context)
194
195
195 diff_processor = diffs.DiffProcessor(
196 diff_processor = diffs.DiffProcessor(
196 vcs_diff, format='newdiff', diff_limit=diff_limit,
197 vcs_diff, format='newdiff', diff_limit=diff_limit,
197 file_limit=file_limit, show_full_diff=c.fulldiff)
198 file_limit=file_limit, show_full_diff=c.fulldiff)
198
199
199 _parsed = diff_processor.prepare()
200 _parsed = diff_processor.prepare()
200
201
201 diffset = codeblocks.DiffSet(
202 diffset = codeblocks.DiffSet(
202 repo_name=self.db_repo_name,
203 repo_name=self.db_repo_name,
203 source_node_getter=codeblocks.diffset_node_getter(commit1),
204 source_node_getter=codeblocks.diffset_node_getter(commit1),
204 target_node_getter=codeblocks.diffset_node_getter(commit2))
205 target_node_getter=codeblocks.diffset_node_getter(commit2))
205
206
206 diffset = self.path_filter.render_patchset_filtered(
207 diffset = self.path_filter.render_patchset_filtered(
207 diffset, _parsed, commit1.raw_id, commit2.raw_id)
208 diffset, _parsed, commit1.raw_id, commit2.raw_id)
208
209
209 # save cached diff
210 # save cached diff
210 if caching_enabled:
211 if caching_enabled:
211 cache_diff(cache_file_path, diffset, None)
212 cache_diff(cache_file_path, diffset, None)
212
213
213 c.limited_diff = diffset.limited_diff
214 c.limited_diff = diffset.limited_diff
214 c.changes[commit.raw_id] = diffset
215 c.changes[commit.raw_id] = diffset
215 else:
216 else:
216 # TODO(marcink): no cache usage here...
217 # TODO(marcink): no cache usage here...
217 _diff = self.rhodecode_vcs_repo.get_diff(
218 _diff = self.rhodecode_vcs_repo.get_diff(
218 commit1, commit2,
219 commit1, commit2,
219 ignore_whitespace=hide_whitespace_changes, context=diff_context)
220 ignore_whitespace=hide_whitespace_changes, context=diff_context)
220 diff_processor = diffs.DiffProcessor(
221 diff_processor = diffs.DiffProcessor(
221 _diff, format='newdiff', diff_limit=diff_limit,
222 _diff, format='newdiff', diff_limit=diff_limit,
222 file_limit=file_limit, show_full_diff=c.fulldiff)
223 file_limit=file_limit, show_full_diff=c.fulldiff)
223 # downloads/raw we only need RAW diff nothing else
224 # downloads/raw we only need RAW diff nothing else
224 diff = self.path_filter.get_raw_patch(diff_processor)
225 diff = self.path_filter.get_raw_patch(diff_processor)
225 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
226 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
226
227
227 # sort comments by how they were generated
228 # sort comments by how they were generated
228 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
229 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
230 c.at_version_num = None
229
231
230 if len(c.commit_ranges) == 1:
232 if len(c.commit_ranges) == 1:
231 c.commit = c.commit_ranges[0]
233 c.commit = c.commit_ranges[0]
232 c.parent_tmpl = ''.join(
234 c.parent_tmpl = ''.join(
233 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
235 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
234
236
235 if method == 'download':
237 if method == 'download':
236 response = Response(diff)
238 response = Response(diff)
237 response.content_type = 'text/plain'
239 response.content_type = 'text/plain'
238 response.content_disposition = (
240 response.content_disposition = (
239 'attachment; filename=%s.diff' % commit_id_range[:12])
241 'attachment; filename=%s.diff' % commit_id_range[:12])
240 return response
242 return response
241 elif method == 'patch':
243 elif method == 'patch':
242 c.diff = safe_unicode(diff)
244 c.diff = safe_unicode(diff)
243 patch = render(
245 patch = render(
244 'rhodecode:templates/changeset/patch_changeset.mako',
246 'rhodecode:templates/changeset/patch_changeset.mako',
245 self._get_template_context(c), self.request)
247 self._get_template_context(c), self.request)
246 response = Response(patch)
248 response = Response(patch)
247 response.content_type = 'text/plain'
249 response.content_type = 'text/plain'
248 return response
250 return response
249 elif method == 'raw':
251 elif method == 'raw':
250 response = Response(diff)
252 response = Response(diff)
251 response.content_type = 'text/plain'
253 response.content_type = 'text/plain'
252 return response
254 return response
253 elif method == 'show':
255 elif method == 'show':
254 if len(c.commit_ranges) == 1:
256 if len(c.commit_ranges) == 1:
255 html = render(
257 html = render(
256 'rhodecode:templates/changeset/changeset.mako',
258 'rhodecode:templates/changeset/changeset.mako',
257 self._get_template_context(c), self.request)
259 self._get_template_context(c), self.request)
258 return Response(html)
260 return Response(html)
259 else:
261 else:
260 c.ancestor = None
262 c.ancestor = None
261 c.target_repo = self.db_repo
263 c.target_repo = self.db_repo
262 html = render(
264 html = render(
263 'rhodecode:templates/changeset/changeset_range.mako',
265 'rhodecode:templates/changeset/changeset_range.mako',
264 self._get_template_context(c), self.request)
266 self._get_template_context(c), self.request)
265 return Response(html)
267 return Response(html)
266
268
267 raise HTTPBadRequest()
269 raise HTTPBadRequest()
268
270
269 @LoginRequired()
271 @LoginRequired()
270 @HasRepoPermissionAnyDecorator(
272 @HasRepoPermissionAnyDecorator(
271 'repository.read', 'repository.write', 'repository.admin')
273 'repository.read', 'repository.write', 'repository.admin')
272 @view_config(
274 @view_config(
273 route_name='repo_commit', request_method='GET',
275 route_name='repo_commit', request_method='GET',
274 renderer=None)
276 renderer=None)
275 def repo_commit_show(self):
277 def repo_commit_show(self):
276 commit_id = self.request.matchdict['commit_id']
278 commit_id = self.request.matchdict['commit_id']
277 return self._commit(commit_id, method='show')
279 return self._commit(commit_id, method='show')
278
280
279 @LoginRequired()
281 @LoginRequired()
280 @HasRepoPermissionAnyDecorator(
282 @HasRepoPermissionAnyDecorator(
281 'repository.read', 'repository.write', 'repository.admin')
283 'repository.read', 'repository.write', 'repository.admin')
282 @view_config(
284 @view_config(
283 route_name='repo_commit_raw', request_method='GET',
285 route_name='repo_commit_raw', request_method='GET',
284 renderer=None)
286 renderer=None)
285 @view_config(
287 @view_config(
286 route_name='repo_commit_raw_deprecated', request_method='GET',
288 route_name='repo_commit_raw_deprecated', request_method='GET',
287 renderer=None)
289 renderer=None)
288 def repo_commit_raw(self):
290 def repo_commit_raw(self):
289 commit_id = self.request.matchdict['commit_id']
291 commit_id = self.request.matchdict['commit_id']
290 return self._commit(commit_id, method='raw')
292 return self._commit(commit_id, method='raw')
291
293
292 @LoginRequired()
294 @LoginRequired()
293 @HasRepoPermissionAnyDecorator(
295 @HasRepoPermissionAnyDecorator(
294 'repository.read', 'repository.write', 'repository.admin')
296 'repository.read', 'repository.write', 'repository.admin')
295 @view_config(
297 @view_config(
296 route_name='repo_commit_patch', request_method='GET',
298 route_name='repo_commit_patch', request_method='GET',
297 renderer=None)
299 renderer=None)
298 def repo_commit_patch(self):
300 def repo_commit_patch(self):
299 commit_id = self.request.matchdict['commit_id']
301 commit_id = self.request.matchdict['commit_id']
300 return self._commit(commit_id, method='patch')
302 return self._commit(commit_id, method='patch')
301
303
302 @LoginRequired()
304 @LoginRequired()
303 @HasRepoPermissionAnyDecorator(
305 @HasRepoPermissionAnyDecorator(
304 'repository.read', 'repository.write', 'repository.admin')
306 'repository.read', 'repository.write', 'repository.admin')
305 @view_config(
307 @view_config(
306 route_name='repo_commit_download', request_method='GET',
308 route_name='repo_commit_download', request_method='GET',
307 renderer=None)
309 renderer=None)
308 def repo_commit_download(self):
310 def repo_commit_download(self):
309 commit_id = self.request.matchdict['commit_id']
311 commit_id = self.request.matchdict['commit_id']
310 return self._commit(commit_id, method='download')
312 return self._commit(commit_id, method='download')
311
313
312 @LoginRequired()
314 @LoginRequired()
313 @NotAnonymous()
315 @NotAnonymous()
314 @HasRepoPermissionAnyDecorator(
316 @HasRepoPermissionAnyDecorator(
315 'repository.read', 'repository.write', 'repository.admin')
317 'repository.read', 'repository.write', 'repository.admin')
316 @CSRFRequired()
318 @CSRFRequired()
317 @view_config(
319 @view_config(
318 route_name='repo_commit_comment_create', request_method='POST',
320 route_name='repo_commit_comment_create', request_method='POST',
319 renderer='json_ext')
321 renderer='json_ext')
320 def repo_commit_comment_create(self):
322 def repo_commit_comment_create(self):
321 _ = self.request.translate
323 _ = self.request.translate
322 commit_id = self.request.matchdict['commit_id']
324 commit_id = self.request.matchdict['commit_id']
323
325
324 c = self.load_default_context()
326 c = self.load_default_context()
325 status = self.request.POST.get('changeset_status', None)
327 status = self.request.POST.get('changeset_status', None)
326 text = self.request.POST.get('text')
328 text = self.request.POST.get('text')
327 comment_type = self.request.POST.get('comment_type')
329 comment_type = self.request.POST.get('comment_type')
328 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
330 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
329
331
330 if status:
332 if status:
331 text = text or (_('Status change %(transition_icon)s %(status)s')
333 text = text or (_('Status change %(transition_icon)s %(status)s')
332 % {'transition_icon': '>',
334 % {'transition_icon': '>',
333 'status': ChangesetStatus.get_status_lbl(status)})
335 'status': ChangesetStatus.get_status_lbl(status)})
334
336
335 multi_commit_ids = []
337 multi_commit_ids = []
336 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
338 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
337 if _commit_id not in ['', None, EmptyCommit.raw_id]:
339 if _commit_id not in ['', None, EmptyCommit.raw_id]:
338 if _commit_id not in multi_commit_ids:
340 if _commit_id not in multi_commit_ids:
339 multi_commit_ids.append(_commit_id)
341 multi_commit_ids.append(_commit_id)
340
342
341 commit_ids = multi_commit_ids or [commit_id]
343 commit_ids = multi_commit_ids or [commit_id]
342
344
343 comment = None
345 comment = None
344 for current_id in filter(None, commit_ids):
346 for current_id in filter(None, commit_ids):
345 comment = CommentsModel().create(
347 comment = CommentsModel().create(
346 text=text,
348 text=text,
347 repo=self.db_repo.repo_id,
349 repo=self.db_repo.repo_id,
348 user=self._rhodecode_db_user.user_id,
350 user=self._rhodecode_db_user.user_id,
349 commit_id=current_id,
351 commit_id=current_id,
350 f_path=self.request.POST.get('f_path'),
352 f_path=self.request.POST.get('f_path'),
351 line_no=self.request.POST.get('line'),
353 line_no=self.request.POST.get('line'),
352 status_change=(ChangesetStatus.get_status_lbl(status)
354 status_change=(ChangesetStatus.get_status_lbl(status)
353 if status else None),
355 if status else None),
354 status_change_type=status,
356 status_change_type=status,
355 comment_type=comment_type,
357 comment_type=comment_type,
356 resolves_comment_id=resolves_comment_id,
358 resolves_comment_id=resolves_comment_id,
357 auth_user=self._rhodecode_user
359 auth_user=self._rhodecode_user
358 )
360 )
359
361
360 # get status if set !
362 # get status if set !
361 if status:
363 if status:
362 # if latest status was from pull request and it's closed
364 # if latest status was from pull request and it's closed
363 # disallow changing status !
365 # disallow changing status !
364 # dont_allow_on_closed_pull_request = True !
366 # dont_allow_on_closed_pull_request = True !
365
367
366 try:
368 try:
367 ChangesetStatusModel().set_status(
369 ChangesetStatusModel().set_status(
368 self.db_repo.repo_id,
370 self.db_repo.repo_id,
369 status,
371 status,
370 self._rhodecode_db_user.user_id,
372 self._rhodecode_db_user.user_id,
371 comment,
373 comment,
372 revision=current_id,
374 revision=current_id,
373 dont_allow_on_closed_pull_request=True
375 dont_allow_on_closed_pull_request=True
374 )
376 )
375 except StatusChangeOnClosedPullRequestError:
377 except StatusChangeOnClosedPullRequestError:
376 msg = _('Changing the status of a commit associated with '
378 msg = _('Changing the status of a commit associated with '
377 'a closed pull request is not allowed')
379 'a closed pull request is not allowed')
378 log.exception(msg)
380 log.exception(msg)
379 h.flash(msg, category='warning')
381 h.flash(msg, category='warning')
380 raise HTTPFound(h.route_path(
382 raise HTTPFound(h.route_path(
381 'repo_commit', repo_name=self.db_repo_name,
383 'repo_commit', repo_name=self.db_repo_name,
382 commit_id=current_id))
384 commit_id=current_id))
383
385
384 commit = self.db_repo.get_commit(current_id)
386 commit = self.db_repo.get_commit(current_id)
385 CommentsModel().trigger_commit_comment_hook(
387 CommentsModel().trigger_commit_comment_hook(
386 self.db_repo, self._rhodecode_user, 'create',
388 self.db_repo, self._rhodecode_user, 'create',
387 data={'comment': comment, 'commit': commit})
389 data={'comment': comment, 'commit': commit})
388
390
389 # finalize, commit and redirect
391 # finalize, commit and redirect
390 Session().commit()
392 Session().commit()
391
393
392 data = {
394 data = {
393 'target_id': h.safeid(h.safe_unicode(
395 'target_id': h.safeid(h.safe_unicode(
394 self.request.POST.get('f_path'))),
396 self.request.POST.get('f_path'))),
395 }
397 }
396 if comment:
398 if comment:
397 c.co = comment
399 c.co = comment
398 rendered_comment = render(
400 rendered_comment = render(
399 'rhodecode:templates/changeset/changeset_comment_block.mako',
401 'rhodecode:templates/changeset/changeset_comment_block.mako',
400 self._get_template_context(c), self.request)
402 self._get_template_context(c), self.request)
401
403
402 data.update(comment.get_dict())
404 data.update(comment.get_dict())
403 data.update({'rendered_text': rendered_comment})
405 data.update({'rendered_text': rendered_comment})
404
406
405 return data
407 return data
406
408
407 @LoginRequired()
409 @LoginRequired()
408 @NotAnonymous()
410 @NotAnonymous()
409 @HasRepoPermissionAnyDecorator(
411 @HasRepoPermissionAnyDecorator(
410 'repository.read', 'repository.write', 'repository.admin')
412 'repository.read', 'repository.write', 'repository.admin')
411 @CSRFRequired()
413 @CSRFRequired()
412 @view_config(
414 @view_config(
413 route_name='repo_commit_comment_preview', request_method='POST',
415 route_name='repo_commit_comment_preview', request_method='POST',
414 renderer='string', xhr=True)
416 renderer='string', xhr=True)
415 def repo_commit_comment_preview(self):
417 def repo_commit_comment_preview(self):
416 # Technically a CSRF token is not needed as no state changes with this
418 # Technically a CSRF token is not needed as no state changes with this
417 # call. However, as this is a POST is better to have it, so automated
419 # call. However, as this is a POST is better to have it, so automated
418 # tools don't flag it as potential CSRF.
420 # tools don't flag it as potential CSRF.
419 # Post is required because the payload could be bigger than the maximum
421 # Post is required because the payload could be bigger than the maximum
420 # allowed by GET.
422 # allowed by GET.
421
423
422 text = self.request.POST.get('text')
424 text = self.request.POST.get('text')
423 renderer = self.request.POST.get('renderer') or 'rst'
425 renderer = self.request.POST.get('renderer') or 'rst'
424 if text:
426 if text:
425 return h.render(text, renderer=renderer, mentions=True,
427 return h.render(text, renderer=renderer, mentions=True,
426 repo_name=self.db_repo_name)
428 repo_name=self.db_repo_name)
427 return ''
429 return ''
428
430
429 @LoginRequired()
431 @LoginRequired()
430 @NotAnonymous()
432 @NotAnonymous()
431 @HasRepoPermissionAnyDecorator(
433 @HasRepoPermissionAnyDecorator(
432 'repository.read', 'repository.write', 'repository.admin')
434 'repository.read', 'repository.write', 'repository.admin')
433 @CSRFRequired()
435 @CSRFRequired()
434 @view_config(
436 @view_config(
435 route_name='repo_commit_comment_history_view', request_method='POST',
437 route_name='repo_commit_comment_history_view', request_method='POST',
436 renderer='string', xhr=True)
438 renderer='string', xhr=True)
437 def repo_commit_comment_history_view(self):
439 def repo_commit_comment_history_view(self):
438 c = self.load_default_context()
440 c = self.load_default_context()
439
441
440 comment_history_id = self.request.matchdict['comment_history_id']
442 comment_history_id = self.request.matchdict['comment_history_id']
441 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
443 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
442 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
444 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
443
445
444 if is_repo_comment:
446 if is_repo_comment:
445 c.comment_history = comment_history
447 c.comment_history = comment_history
446
448
447 rendered_comment = render(
449 rendered_comment = render(
448 'rhodecode:templates/changeset/comment_history.mako',
450 'rhodecode:templates/changeset/comment_history.mako',
449 self._get_template_context(c)
451 self._get_template_context(c)
450 , self.request)
452 , self.request)
451 return rendered_comment
453 return rendered_comment
452 else:
454 else:
453 log.warning('No permissions for user %s to show comment_history_id: %s',
455 log.warning('No permissions for user %s to show comment_history_id: %s',
454 self._rhodecode_db_user, comment_history_id)
456 self._rhodecode_db_user, comment_history_id)
455 raise HTTPNotFound()
457 raise HTTPNotFound()
456
458
457 @LoginRequired()
459 @LoginRequired()
458 @NotAnonymous()
460 @NotAnonymous()
459 @HasRepoPermissionAnyDecorator(
461 @HasRepoPermissionAnyDecorator(
460 'repository.read', 'repository.write', 'repository.admin')
462 'repository.read', 'repository.write', 'repository.admin')
461 @CSRFRequired()
463 @CSRFRequired()
462 @view_config(
464 @view_config(
463 route_name='repo_commit_comment_attachment_upload', request_method='POST',
465 route_name='repo_commit_comment_attachment_upload', request_method='POST',
464 renderer='json_ext', xhr=True)
466 renderer='json_ext', xhr=True)
465 def repo_commit_comment_attachment_upload(self):
467 def repo_commit_comment_attachment_upload(self):
466 c = self.load_default_context()
468 c = self.load_default_context()
467 upload_key = 'attachment'
469 upload_key = 'attachment'
468
470
469 file_obj = self.request.POST.get(upload_key)
471 file_obj = self.request.POST.get(upload_key)
470
472
471 if file_obj is None:
473 if file_obj is None:
472 self.request.response.status = 400
474 self.request.response.status = 400
473 return {'store_fid': None,
475 return {'store_fid': None,
474 'access_path': None,
476 'access_path': None,
475 'error': '{} data field is missing'.format(upload_key)}
477 'error': '{} data field is missing'.format(upload_key)}
476
478
477 if not hasattr(file_obj, 'filename'):
479 if not hasattr(file_obj, 'filename'):
478 self.request.response.status = 400
480 self.request.response.status = 400
479 return {'store_fid': None,
481 return {'store_fid': None,
480 'access_path': None,
482 'access_path': None,
481 'error': 'filename cannot be read from the data field'}
483 'error': 'filename cannot be read from the data field'}
482
484
483 filename = file_obj.filename
485 filename = file_obj.filename
484 file_display_name = filename
486 file_display_name = filename
485
487
486 metadata = {
488 metadata = {
487 'user_uploaded': {'username': self._rhodecode_user.username,
489 'user_uploaded': {'username': self._rhodecode_user.username,
488 'user_id': self._rhodecode_user.user_id,
490 'user_id': self._rhodecode_user.user_id,
489 'ip': self._rhodecode_user.ip_addr}}
491 'ip': self._rhodecode_user.ip_addr}}
490
492
491 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
493 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
492 allowed_extensions = [
494 allowed_extensions = [
493 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
495 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
494 '.pptx', '.txt', '.xlsx', '.zip']
496 '.pptx', '.txt', '.xlsx', '.zip']
495 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
497 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
496
498
497 try:
499 try:
498 storage = store_utils.get_file_storage(self.request.registry.settings)
500 storage = store_utils.get_file_storage(self.request.registry.settings)
499 store_uid, metadata = storage.save_file(
501 store_uid, metadata = storage.save_file(
500 file_obj.file, filename, extra_metadata=metadata,
502 file_obj.file, filename, extra_metadata=metadata,
501 extensions=allowed_extensions, max_filesize=max_file_size)
503 extensions=allowed_extensions, max_filesize=max_file_size)
502 except FileNotAllowedException:
504 except FileNotAllowedException:
503 self.request.response.status = 400
505 self.request.response.status = 400
504 permitted_extensions = ', '.join(allowed_extensions)
506 permitted_extensions = ', '.join(allowed_extensions)
505 error_msg = 'File `{}` is not allowed. ' \
507 error_msg = 'File `{}` is not allowed. ' \
506 'Only following extensions are permitted: {}'.format(
508 'Only following extensions are permitted: {}'.format(
507 filename, permitted_extensions)
509 filename, permitted_extensions)
508 return {'store_fid': None,
510 return {'store_fid': None,
509 'access_path': None,
511 'access_path': None,
510 'error': error_msg}
512 'error': error_msg}
511 except FileOverSizeException:
513 except FileOverSizeException:
512 self.request.response.status = 400
514 self.request.response.status = 400
513 limit_mb = h.format_byte_size_binary(max_file_size)
515 limit_mb = h.format_byte_size_binary(max_file_size)
514 return {'store_fid': None,
516 return {'store_fid': None,
515 'access_path': None,
517 'access_path': None,
516 'error': 'File {} is exceeding allowed limit of {}.'.format(
518 'error': 'File {} is exceeding allowed limit of {}.'.format(
517 filename, limit_mb)}
519 filename, limit_mb)}
518
520
519 try:
521 try:
520 entry = FileStore.create(
522 entry = FileStore.create(
521 file_uid=store_uid, filename=metadata["filename"],
523 file_uid=store_uid, filename=metadata["filename"],
522 file_hash=metadata["sha256"], file_size=metadata["size"],
524 file_hash=metadata["sha256"], file_size=metadata["size"],
523 file_display_name=file_display_name,
525 file_display_name=file_display_name,
524 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
526 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
525 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
527 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
526 scope_repo_id=self.db_repo.repo_id
528 scope_repo_id=self.db_repo.repo_id
527 )
529 )
528 Session().add(entry)
530 Session().add(entry)
529 Session().commit()
531 Session().commit()
530 log.debug('Stored upload in DB as %s', entry)
532 log.debug('Stored upload in DB as %s', entry)
531 except Exception:
533 except Exception:
532 log.exception('Failed to store file %s', filename)
534 log.exception('Failed to store file %s', filename)
533 self.request.response.status = 400
535 self.request.response.status = 400
534 return {'store_fid': None,
536 return {'store_fid': None,
535 'access_path': None,
537 'access_path': None,
536 'error': 'File {} failed to store in DB.'.format(filename)}
538 'error': 'File {} failed to store in DB.'.format(filename)}
537
539
538 Session().commit()
540 Session().commit()
539
541
540 return {
542 return {
541 'store_fid': store_uid,
543 'store_fid': store_uid,
542 'access_path': h.route_path(
544 'access_path': h.route_path(
543 'download_file', fid=store_uid),
545 'download_file', fid=store_uid),
544 'fqn_access_path': h.route_url(
546 'fqn_access_path': h.route_url(
545 'download_file', fid=store_uid),
547 'download_file', fid=store_uid),
546 'repo_access_path': h.route_path(
548 'repo_access_path': h.route_path(
547 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
549 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
548 'repo_fqn_access_path': h.route_url(
550 'repo_fqn_access_path': h.route_url(
549 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
551 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
550 }
552 }
551
553
552 @LoginRequired()
554 @LoginRequired()
553 @NotAnonymous()
555 @NotAnonymous()
554 @HasRepoPermissionAnyDecorator(
556 @HasRepoPermissionAnyDecorator(
555 'repository.read', 'repository.write', 'repository.admin')
557 'repository.read', 'repository.write', 'repository.admin')
556 @CSRFRequired()
558 @CSRFRequired()
557 @view_config(
559 @view_config(
558 route_name='repo_commit_comment_delete', request_method='POST',
560 route_name='repo_commit_comment_delete', request_method='POST',
559 renderer='json_ext')
561 renderer='json_ext')
560 def repo_commit_comment_delete(self):
562 def repo_commit_comment_delete(self):
561 commit_id = self.request.matchdict['commit_id']
563 commit_id = self.request.matchdict['commit_id']
562 comment_id = self.request.matchdict['comment_id']
564 comment_id = self.request.matchdict['comment_id']
563
565
564 comment = ChangesetComment.get_or_404(comment_id)
566 comment = ChangesetComment.get_or_404(comment_id)
565 if not comment:
567 if not comment:
566 log.debug('Comment with id:%s not found, skipping', comment_id)
568 log.debug('Comment with id:%s not found, skipping', comment_id)
567 # comment already deleted in another call probably
569 # comment already deleted in another call probably
568 return True
570 return True
569
571
570 if comment.immutable:
572 if comment.immutable:
571 # don't allow deleting comments that are immutable
573 # don't allow deleting comments that are immutable
572 raise HTTPForbidden()
574 raise HTTPForbidden()
573
575
574 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
576 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
575 super_admin = h.HasPermissionAny('hg.admin')()
577 super_admin = h.HasPermissionAny('hg.admin')()
576 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
578 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
577 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
579 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
578 comment_repo_admin = is_repo_admin and is_repo_comment
580 comment_repo_admin = is_repo_admin and is_repo_comment
579
581
580 if super_admin or comment_owner or comment_repo_admin:
582 if super_admin or comment_owner or comment_repo_admin:
581 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
583 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
582 Session().commit()
584 Session().commit()
583 return True
585 return True
584 else:
586 else:
585 log.warning('No permissions for user %s to delete comment_id: %s',
587 log.warning('No permissions for user %s to delete comment_id: %s',
586 self._rhodecode_db_user, comment_id)
588 self._rhodecode_db_user, comment_id)
587 raise HTTPNotFound()
589 raise HTTPNotFound()
588
590
589 @LoginRequired()
591 @LoginRequired()
590 @NotAnonymous()
592 @NotAnonymous()
591 @HasRepoPermissionAnyDecorator(
593 @HasRepoPermissionAnyDecorator(
592 'repository.read', 'repository.write', 'repository.admin')
594 'repository.read', 'repository.write', 'repository.admin')
593 @CSRFRequired()
595 @CSRFRequired()
594 @view_config(
596 @view_config(
595 route_name='repo_commit_comment_edit', request_method='POST',
597 route_name='repo_commit_comment_edit', request_method='POST',
596 renderer='json_ext')
598 renderer='json_ext')
597 def repo_commit_comment_edit(self):
599 def repo_commit_comment_edit(self):
598 self.load_default_context()
600 self.load_default_context()
599
601
600 comment_id = self.request.matchdict['comment_id']
602 comment_id = self.request.matchdict['comment_id']
601 comment = ChangesetComment.get_or_404(comment_id)
603 comment = ChangesetComment.get_or_404(comment_id)
602
604
603 if comment.immutable:
605 if comment.immutable:
604 # don't allow deleting comments that are immutable
606 # don't allow deleting comments that are immutable
605 raise HTTPForbidden()
607 raise HTTPForbidden()
606
608
607 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
609 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
608 super_admin = h.HasPermissionAny('hg.admin')()
610 super_admin = h.HasPermissionAny('hg.admin')()
609 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
611 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
610 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
612 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
611 comment_repo_admin = is_repo_admin and is_repo_comment
613 comment_repo_admin = is_repo_admin and is_repo_comment
612
614
613 if super_admin or comment_owner or comment_repo_admin:
615 if super_admin or comment_owner or comment_repo_admin:
614 text = self.request.POST.get('text')
616 text = self.request.POST.get('text')
615 version = self.request.POST.get('version')
617 version = self.request.POST.get('version')
616 if text == comment.text:
618 if text == comment.text:
617 log.warning(
619 log.warning(
618 'Comment(repo): '
620 'Comment(repo): '
619 'Trying to create new version '
621 'Trying to create new version '
620 'with the same comment body {}'.format(
622 'with the same comment body {}'.format(
621 comment_id,
623 comment_id,
622 )
624 )
623 )
625 )
624 raise HTTPNotFound()
626 raise HTTPNotFound()
625
627
626 if version.isdigit():
628 if version.isdigit():
627 version = int(version)
629 version = int(version)
628 else:
630 else:
629 log.warning(
631 log.warning(
630 'Comment(repo): Wrong version type {} {} '
632 'Comment(repo): Wrong version type {} {} '
631 'for comment {}'.format(
633 'for comment {}'.format(
632 version,
634 version,
633 type(version),
635 type(version),
634 comment_id,
636 comment_id,
635 )
637 )
636 )
638 )
637 raise HTTPNotFound()
639 raise HTTPNotFound()
638
640
639 try:
641 try:
640 comment_history = CommentsModel().edit(
642 comment_history = CommentsModel().edit(
641 comment_id=comment_id,
643 comment_id=comment_id,
642 text=text,
644 text=text,
643 auth_user=self._rhodecode_user,
645 auth_user=self._rhodecode_user,
644 version=version,
646 version=version,
645 )
647 )
646 except CommentVersionMismatch:
648 except CommentVersionMismatch:
647 raise HTTPConflict()
649 raise HTTPConflict()
648
650
649 if not comment_history:
651 if not comment_history:
650 raise HTTPNotFound()
652 raise HTTPNotFound()
651
653
652 commit_id = self.request.matchdict['commit_id']
654 commit_id = self.request.matchdict['commit_id']
653 commit = self.db_repo.get_commit(commit_id)
655 commit = self.db_repo.get_commit(commit_id)
654 CommentsModel().trigger_commit_comment_hook(
656 CommentsModel().trigger_commit_comment_hook(
655 self.db_repo, self._rhodecode_user, 'edit',
657 self.db_repo, self._rhodecode_user, 'edit',
656 data={'comment': comment, 'commit': commit})
658 data={'comment': comment, 'commit': commit})
657
659
658 Session().commit()
660 Session().commit()
659 return {
661 return {
660 'comment_history_id': comment_history.comment_history_id,
662 'comment_history_id': comment_history.comment_history_id,
661 'comment_id': comment.comment_id,
663 'comment_id': comment.comment_id,
662 'comment_version': comment_history.version,
664 'comment_version': comment_history.version,
663 'comment_author_username': comment_history.author.username,
665 'comment_author_username': comment_history.author.username,
664 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
666 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
665 'comment_created_on': h.age_component(comment_history.created_on,
667 'comment_created_on': h.age_component(comment_history.created_on,
666 time_is_local=True),
668 time_is_local=True),
667 }
669 }
668 else:
670 else:
669 log.warning('No permissions for user %s to edit comment_id: %s',
671 log.warning('No permissions for user %s to edit comment_id: %s',
670 self._rhodecode_db_user, comment_id)
672 self._rhodecode_db_user, comment_id)
671 raise HTTPNotFound()
673 raise HTTPNotFound()
672
674
673 @LoginRequired()
675 @LoginRequired()
674 @HasRepoPermissionAnyDecorator(
676 @HasRepoPermissionAnyDecorator(
675 'repository.read', 'repository.write', 'repository.admin')
677 'repository.read', 'repository.write', 'repository.admin')
676 @view_config(
678 @view_config(
677 route_name='repo_commit_data', request_method='GET',
679 route_name='repo_commit_data', request_method='GET',
678 renderer='json_ext', xhr=True)
680 renderer='json_ext', xhr=True)
679 def repo_commit_data(self):
681 def repo_commit_data(self):
680 commit_id = self.request.matchdict['commit_id']
682 commit_id = self.request.matchdict['commit_id']
681 self.load_default_context()
683 self.load_default_context()
682
684
683 try:
685 try:
684 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
686 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
685 except CommitDoesNotExistError as e:
687 except CommitDoesNotExistError as e:
686 return EmptyCommit(message=str(e))
688 return EmptyCommit(message=str(e))
687
689
688 @LoginRequired()
690 @LoginRequired()
689 @HasRepoPermissionAnyDecorator(
691 @HasRepoPermissionAnyDecorator(
690 'repository.read', 'repository.write', 'repository.admin')
692 'repository.read', 'repository.write', 'repository.admin')
691 @view_config(
693 @view_config(
692 route_name='repo_commit_children', request_method='GET',
694 route_name='repo_commit_children', request_method='GET',
693 renderer='json_ext', xhr=True)
695 renderer='json_ext', xhr=True)
694 def repo_commit_children(self):
696 def repo_commit_children(self):
695 commit_id = self.request.matchdict['commit_id']
697 commit_id = self.request.matchdict['commit_id']
696 self.load_default_context()
698 self.load_default_context()
697
699
698 try:
700 try:
699 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
701 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
700 children = commit.children
702 children = commit.children
701 except CommitDoesNotExistError:
703 except CommitDoesNotExistError:
702 children = []
704 children = []
703
705
704 result = {"results": children}
706 result = {"results": children}
705 return result
707 return result
706
708
707 @LoginRequired()
709 @LoginRequired()
708 @HasRepoPermissionAnyDecorator(
710 @HasRepoPermissionAnyDecorator(
709 'repository.read', 'repository.write', 'repository.admin')
711 'repository.read', 'repository.write', 'repository.admin')
710 @view_config(
712 @view_config(
711 route_name='repo_commit_parents', request_method='GET',
713 route_name='repo_commit_parents', request_method='GET',
712 renderer='json_ext')
714 renderer='json_ext')
713 def repo_commit_parents(self):
715 def repo_commit_parents(self):
714 commit_id = self.request.matchdict['commit_id']
716 commit_id = self.request.matchdict['commit_id']
715 self.load_default_context()
717 self.load_default_context()
716
718
717 try:
719 try:
718 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
720 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
719 parents = commit.parents
721 parents = commit.parents
720 except CommitDoesNotExistError:
722 except CommitDoesNotExistError:
721 parents = []
723 parents = []
722 result = {"results": parents}
724 result = {"results": parents}
723 return result
725 return result
@@ -1,1639 +1,1753 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
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.db import (
48 from rhodecode.model.db import (
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
50 from rhodecode.model.forms import PullRequestForm
50 from rhodecode.model.forms import PullRequestForm
51 from rhodecode.model.meta import Session
51 from rhodecode.model.meta import Session
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 from rhodecode.model.scm import ScmModel
53 from rhodecode.model.scm import ScmModel
54
54
55 log = logging.getLogger(__name__)
55 log = logging.getLogger(__name__)
56
56
57
57
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
58 class RepoPullRequestsView(RepoAppView, DataGridAppView):
59
59
60 def load_default_context(self):
60 def load_default_context(self):
61 c = self._get_local_tmpl_context(include_app_defaults=True)
61 c = self._get_local_tmpl_context(include_app_defaults=True)
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
62 c.REVIEW_STATUS_APPROVED = ChangesetStatus.STATUS_APPROVED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
63 c.REVIEW_STATUS_REJECTED = ChangesetStatus.STATUS_REJECTED
64 # backward compat., we use for OLD PRs a plain renderer
64 # backward compat., we use for OLD PRs a plain renderer
65 c.renderer = 'plain'
65 c.renderer = 'plain'
66 return c
66 return c
67
67
68 def _get_pull_requests_list(
68 def _get_pull_requests_list(
69 self, repo_name, source, filter_type, opened_by, statuses):
69 self, repo_name, source, filter_type, opened_by, statuses):
70
70
71 draw, start, limit = self._extract_chunk(self.request)
71 draw, start, limit = self._extract_chunk(self.request)
72 search_q, order_by, order_dir = self._extract_ordering(self.request)
72 search_q, order_by, order_dir = self._extract_ordering(self.request)
73 _render = self.request.get_partial_renderer(
73 _render = self.request.get_partial_renderer(
74 'rhodecode:templates/data_table/_dt_elements.mako')
74 'rhodecode:templates/data_table/_dt_elements.mako')
75
75
76 # pagination
76 # pagination
77
77
78 if filter_type == 'awaiting_review':
78 if filter_type == 'awaiting_review':
79 pull_requests = PullRequestModel().get_awaiting_review(
79 pull_requests = PullRequestModel().get_awaiting_review(
80 repo_name, search_q=search_q, source=source, opened_by=opened_by,
80 repo_name, search_q=search_q, source=source, opened_by=opened_by,
81 statuses=statuses, offset=start, length=limit,
81 statuses=statuses, offset=start, length=limit,
82 order_by=order_by, order_dir=order_dir)
82 order_by=order_by, order_dir=order_dir)
83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
83 pull_requests_total_count = PullRequestModel().count_awaiting_review(
84 repo_name, search_q=search_q, source=source, statuses=statuses,
84 repo_name, search_q=search_q, source=source, statuses=statuses,
85 opened_by=opened_by)
85 opened_by=opened_by)
86 elif filter_type == 'awaiting_my_review':
86 elif filter_type == 'awaiting_my_review':
87 pull_requests = PullRequestModel().get_awaiting_my_review(
87 pull_requests = PullRequestModel().get_awaiting_my_review(
88 repo_name, search_q=search_q, source=source, opened_by=opened_by,
88 repo_name, search_q=search_q, source=source, opened_by=opened_by,
89 user_id=self._rhodecode_user.user_id, statuses=statuses,
89 user_id=self._rhodecode_user.user_id, statuses=statuses,
90 offset=start, length=limit, order_by=order_by,
90 offset=start, length=limit, order_by=order_by,
91 order_dir=order_dir)
91 order_dir=order_dir)
92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
92 pull_requests_total_count = PullRequestModel().count_awaiting_my_review(
93 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
93 repo_name, search_q=search_q, source=source, user_id=self._rhodecode_user.user_id,
94 statuses=statuses, opened_by=opened_by)
94 statuses=statuses, opened_by=opened_by)
95 else:
95 else:
96 pull_requests = PullRequestModel().get_all(
96 pull_requests = PullRequestModel().get_all(
97 repo_name, search_q=search_q, source=source, opened_by=opened_by,
97 repo_name, search_q=search_q, source=source, opened_by=opened_by,
98 statuses=statuses, offset=start, length=limit,
98 statuses=statuses, offset=start, length=limit,
99 order_by=order_by, order_dir=order_dir)
99 order_by=order_by, order_dir=order_dir)
100 pull_requests_total_count = PullRequestModel().count_all(
100 pull_requests_total_count = PullRequestModel().count_all(
101 repo_name, search_q=search_q, source=source, statuses=statuses,
101 repo_name, search_q=search_q, source=source, statuses=statuses,
102 opened_by=opened_by)
102 opened_by=opened_by)
103
103
104 data = []
104 data = []
105 comments_model = CommentsModel()
105 comments_model = CommentsModel()
106 for pr in pull_requests:
106 for pr in pull_requests:
107 comments = comments_model.get_all_comments(
107 comments = comments_model.get_all_comments(
108 self.db_repo.repo_id, pull_request=pr)
108 self.db_repo.repo_id, pull_request=pr)
109
109
110 data.append({
110 data.append({
111 'name': _render('pullrequest_name',
111 'name': _render('pullrequest_name',
112 pr.pull_request_id, pr.pull_request_state,
112 pr.pull_request_id, pr.pull_request_state,
113 pr.work_in_progress, pr.target_repo.repo_name),
113 pr.work_in_progress, pr.target_repo.repo_name),
114 'name_raw': pr.pull_request_id,
114 'name_raw': pr.pull_request_id,
115 'status': _render('pullrequest_status',
115 'status': _render('pullrequest_status',
116 pr.calculated_review_status()),
116 pr.calculated_review_status()),
117 'title': _render('pullrequest_title', pr.title, pr.description),
117 'title': _render('pullrequest_title', pr.title, pr.description),
118 'description': h.escape(pr.description),
118 'description': h.escape(pr.description),
119 'updated_on': _render('pullrequest_updated_on',
119 'updated_on': _render('pullrequest_updated_on',
120 h.datetime_to_time(pr.updated_on)),
120 h.datetime_to_time(pr.updated_on)),
121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
121 'updated_on_raw': h.datetime_to_time(pr.updated_on),
122 'created_on': _render('pullrequest_updated_on',
122 'created_on': _render('pullrequest_updated_on',
123 h.datetime_to_time(pr.created_on)),
123 h.datetime_to_time(pr.created_on)),
124 'created_on_raw': h.datetime_to_time(pr.created_on),
124 'created_on_raw': h.datetime_to_time(pr.created_on),
125 'state': pr.pull_request_state,
125 'state': pr.pull_request_state,
126 'author': _render('pullrequest_author',
126 'author': _render('pullrequest_author',
127 pr.author.full_contact, ),
127 pr.author.full_contact, ),
128 'author_raw': pr.author.full_name,
128 'author_raw': pr.author.full_name,
129 'comments': _render('pullrequest_comments', len(comments)),
129 'comments': _render('pullrequest_comments', len(comments)),
130 'comments_raw': len(comments),
130 'comments_raw': len(comments),
131 'closed': pr.is_closed(),
131 'closed': pr.is_closed(),
132 })
132 })
133
133
134 data = ({
134 data = ({
135 'draw': draw,
135 'draw': draw,
136 'data': data,
136 'data': data,
137 'recordsTotal': pull_requests_total_count,
137 'recordsTotal': pull_requests_total_count,
138 'recordsFiltered': pull_requests_total_count,
138 'recordsFiltered': pull_requests_total_count,
139 })
139 })
140 return data
140 return data
141
141
142 @LoginRequired()
142 @LoginRequired()
143 @HasRepoPermissionAnyDecorator(
143 @HasRepoPermissionAnyDecorator(
144 'repository.read', 'repository.write', 'repository.admin')
144 'repository.read', 'repository.write', 'repository.admin')
145 @view_config(
145 @view_config(
146 route_name='pullrequest_show_all', request_method='GET',
146 route_name='pullrequest_show_all', request_method='GET',
147 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
147 renderer='rhodecode:templates/pullrequests/pullrequests.mako')
148 def pull_request_list(self):
148 def pull_request_list(self):
149 c = self.load_default_context()
149 c = self.load_default_context()
150
150
151 req_get = self.request.GET
151 req_get = self.request.GET
152 c.source = str2bool(req_get.get('source'))
152 c.source = str2bool(req_get.get('source'))
153 c.closed = str2bool(req_get.get('closed'))
153 c.closed = str2bool(req_get.get('closed'))
154 c.my = str2bool(req_get.get('my'))
154 c.my = str2bool(req_get.get('my'))
155 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
155 c.awaiting_review = str2bool(req_get.get('awaiting_review'))
156 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
156 c.awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
157
157
158 c.active = 'open'
158 c.active = 'open'
159 if c.my:
159 if c.my:
160 c.active = 'my'
160 c.active = 'my'
161 if c.closed:
161 if c.closed:
162 c.active = 'closed'
162 c.active = 'closed'
163 if c.awaiting_review and not c.source:
163 if c.awaiting_review and not c.source:
164 c.active = 'awaiting'
164 c.active = 'awaiting'
165 if c.source and not c.awaiting_review:
165 if c.source and not c.awaiting_review:
166 c.active = 'source'
166 c.active = 'source'
167 if c.awaiting_my_review:
167 if c.awaiting_my_review:
168 c.active = 'awaiting_my'
168 c.active = 'awaiting_my'
169
169
170 return self._get_template_context(c)
170 return self._get_template_context(c)
171
171
172 @LoginRequired()
172 @LoginRequired()
173 @HasRepoPermissionAnyDecorator(
173 @HasRepoPermissionAnyDecorator(
174 'repository.read', 'repository.write', 'repository.admin')
174 'repository.read', 'repository.write', 'repository.admin')
175 @view_config(
175 @view_config(
176 route_name='pullrequest_show_all_data', request_method='GET',
176 route_name='pullrequest_show_all_data', request_method='GET',
177 renderer='json_ext', xhr=True)
177 renderer='json_ext', xhr=True)
178 def pull_request_list_data(self):
178 def pull_request_list_data(self):
179 self.load_default_context()
179 self.load_default_context()
180
180
181 # additional filters
181 # additional filters
182 req_get = self.request.GET
182 req_get = self.request.GET
183 source = str2bool(req_get.get('source'))
183 source = str2bool(req_get.get('source'))
184 closed = str2bool(req_get.get('closed'))
184 closed = str2bool(req_get.get('closed'))
185 my = str2bool(req_get.get('my'))
185 my = str2bool(req_get.get('my'))
186 awaiting_review = str2bool(req_get.get('awaiting_review'))
186 awaiting_review = str2bool(req_get.get('awaiting_review'))
187 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
187 awaiting_my_review = str2bool(req_get.get('awaiting_my_review'))
188
188
189 filter_type = 'awaiting_review' if awaiting_review \
189 filter_type = 'awaiting_review' if awaiting_review \
190 else 'awaiting_my_review' if awaiting_my_review \
190 else 'awaiting_my_review' if awaiting_my_review \
191 else None
191 else None
192
192
193 opened_by = None
193 opened_by = None
194 if my:
194 if my:
195 opened_by = [self._rhodecode_user.user_id]
195 opened_by = [self._rhodecode_user.user_id]
196
196
197 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
197 statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
198 if closed:
198 if closed:
199 statuses = [PullRequest.STATUS_CLOSED]
199 statuses = [PullRequest.STATUS_CLOSED]
200
200
201 data = self._get_pull_requests_list(
201 data = self._get_pull_requests_list(
202 repo_name=self.db_repo_name, source=source,
202 repo_name=self.db_repo_name, source=source,
203 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
203 filter_type=filter_type, opened_by=opened_by, statuses=statuses)
204
204
205 return data
205 return data
206
206
207 def _is_diff_cache_enabled(self, target_repo):
207 def _is_diff_cache_enabled(self, target_repo):
208 caching_enabled = self._get_general_setting(
208 caching_enabled = self._get_general_setting(
209 target_repo, 'rhodecode_diff_cache')
209 target_repo, 'rhodecode_diff_cache')
210 log.debug('Diff caching enabled: %s', caching_enabled)
210 log.debug('Diff caching enabled: %s', caching_enabled)
211 return caching_enabled
211 return caching_enabled
212
212
213 def _get_diffset(self, source_repo_name, source_repo,
213 def _get_diffset(self, source_repo_name, source_repo,
214 ancestor_commit,
214 ancestor_commit,
215 source_ref_id, target_ref_id,
215 source_ref_id, target_ref_id,
216 target_commit, source_commit, diff_limit, file_limit,
216 target_commit, source_commit, diff_limit, file_limit,
217 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
217 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
218
218
219 if use_ancestor:
219 if use_ancestor:
220 # we might want to not use it for versions
220 # we might want to not use it for versions
221 target_ref_id = ancestor_commit.raw_id
221 target_ref_id = ancestor_commit.raw_id
222
222
223 vcs_diff = PullRequestModel().get_diff(
223 vcs_diff = PullRequestModel().get_diff(
224 source_repo, source_ref_id, target_ref_id,
224 source_repo, source_ref_id, target_ref_id,
225 hide_whitespace_changes, diff_context)
225 hide_whitespace_changes, diff_context)
226
226
227 diff_processor = diffs.DiffProcessor(
227 diff_processor = diffs.DiffProcessor(
228 vcs_diff, format='newdiff', diff_limit=diff_limit,
228 vcs_diff, format='newdiff', diff_limit=diff_limit,
229 file_limit=file_limit, show_full_diff=fulldiff)
229 file_limit=file_limit, show_full_diff=fulldiff)
230
230
231 _parsed = diff_processor.prepare()
231 _parsed = diff_processor.prepare()
232
232
233 diffset = codeblocks.DiffSet(
233 diffset = codeblocks.DiffSet(
234 repo_name=self.db_repo_name,
234 repo_name=self.db_repo_name,
235 source_repo_name=source_repo_name,
235 source_repo_name=source_repo_name,
236 source_node_getter=codeblocks.diffset_node_getter(target_commit),
236 source_node_getter=codeblocks.diffset_node_getter(target_commit),
237 target_node_getter=codeblocks.diffset_node_getter(source_commit),
237 target_node_getter=codeblocks.diffset_node_getter(source_commit),
238 )
238 )
239 diffset = self.path_filter.render_patchset_filtered(
239 diffset = self.path_filter.render_patchset_filtered(
240 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
240 diffset, _parsed, target_commit.raw_id, source_commit.raw_id)
241
241
242 return diffset
242 return diffset
243
243
244 def _get_range_diffset(self, source_scm, source_repo,
244 def _get_range_diffset(self, source_scm, source_repo,
245 commit1, commit2, diff_limit, file_limit,
245 commit1, commit2, diff_limit, file_limit,
246 fulldiff, hide_whitespace_changes, diff_context):
246 fulldiff, hide_whitespace_changes, diff_context):
247 vcs_diff = source_scm.get_diff(
247 vcs_diff = source_scm.get_diff(
248 commit1, commit2,
248 commit1, commit2,
249 ignore_whitespace=hide_whitespace_changes,
249 ignore_whitespace=hide_whitespace_changes,
250 context=diff_context)
250 context=diff_context)
251
251
252 diff_processor = diffs.DiffProcessor(
252 diff_processor = diffs.DiffProcessor(
253 vcs_diff, format='newdiff', diff_limit=diff_limit,
253 vcs_diff, format='newdiff', diff_limit=diff_limit,
254 file_limit=file_limit, show_full_diff=fulldiff)
254 file_limit=file_limit, show_full_diff=fulldiff)
255
255
256 _parsed = diff_processor.prepare()
256 _parsed = diff_processor.prepare()
257
257
258 diffset = codeblocks.DiffSet(
258 diffset = codeblocks.DiffSet(
259 repo_name=source_repo.repo_name,
259 repo_name=source_repo.repo_name,
260 source_node_getter=codeblocks.diffset_node_getter(commit1),
260 source_node_getter=codeblocks.diffset_node_getter(commit1),
261 target_node_getter=codeblocks.diffset_node_getter(commit2))
261 target_node_getter=codeblocks.diffset_node_getter(commit2))
262
262
263 diffset = self.path_filter.render_patchset_filtered(
263 diffset = self.path_filter.render_patchset_filtered(
264 diffset, _parsed, commit1.raw_id, commit2.raw_id)
264 diffset, _parsed, commit1.raw_id, commit2.raw_id)
265
265
266 return diffset
266 return diffset
267
267
268 def register_comments_vars(self, c, pull_request, versions):
269 comments_model = CommentsModel()
270
271 # GENERAL COMMENTS with versions #
272 q = comments_model._all_general_comments_of_pull_request(pull_request)
273 q = q.order_by(ChangesetComment.comment_id.asc())
274 general_comments = q
275
276 # pick comments we want to render at current version
277 c.comment_versions = comments_model.aggregate_comments(
278 general_comments, versions, c.at_version_num)
279
280 # INLINE COMMENTS with versions #
281 q = comments_model._all_inline_comments_of_pull_request(pull_request)
282 q = q.order_by(ChangesetComment.comment_id.asc())
283 inline_comments = q
284
285 c.inline_versions = comments_model.aggregate_comments(
286 inline_comments, versions, c.at_version_num, inline=True)
287
288 # Comments inline+general
289 if c.at_version:
290 c.inline_comments_flat = c.inline_versions[c.at_version_num]['display']
291 c.comments = c.comment_versions[c.at_version_num]['display']
292 else:
293 c.inline_comments_flat = c.inline_versions[c.at_version_num]['until']
294 c.comments = c.comment_versions[c.at_version_num]['until']
295
296 return general_comments, inline_comments
297
268 @LoginRequired()
298 @LoginRequired()
269 @HasRepoPermissionAnyDecorator(
299 @HasRepoPermissionAnyDecorator(
270 'repository.read', 'repository.write', 'repository.admin')
300 'repository.read', 'repository.write', 'repository.admin')
271 @view_config(
301 @view_config(
272 route_name='pullrequest_show', request_method='GET',
302 route_name='pullrequest_show', request_method='GET',
273 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
303 renderer='rhodecode:templates/pullrequests/pullrequest_show.mako')
274 def pull_request_show(self):
304 def pull_request_show(self):
275 _ = self.request.translate
305 _ = self.request.translate
276 c = self.load_default_context()
306 c = self.load_default_context()
277
307
278 pull_request = PullRequest.get_or_404(
308 pull_request = PullRequest.get_or_404(
279 self.request.matchdict['pull_request_id'])
309 self.request.matchdict['pull_request_id'])
280 pull_request_id = pull_request.pull_request_id
310 pull_request_id = pull_request.pull_request_id
281
311
282 c.state_progressing = pull_request.is_state_changing()
312 c.state_progressing = pull_request.is_state_changing()
313 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
314 pull_request.target_repo.repo_name, pull_request.pull_request_id)
283
315
284 _new_state = {
316 _new_state = {
285 'created': PullRequest.STATE_CREATED,
317 'created': PullRequest.STATE_CREATED,
286 }.get(self.request.GET.get('force_state'))
318 }.get(self.request.GET.get('force_state'))
287
319
288 if c.is_super_admin and _new_state:
320 if c.is_super_admin and _new_state:
289 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
321 with pull_request.set_state(PullRequest.STATE_UPDATING, final_state=_new_state):
290 h.flash(
322 h.flash(
291 _('Pull Request state was force changed to `{}`').format(_new_state),
323 _('Pull Request state was force changed to `{}`').format(_new_state),
292 category='success')
324 category='success')
293 Session().commit()
325 Session().commit()
294
326
295 raise HTTPFound(h.route_path(
327 raise HTTPFound(h.route_path(
296 'pullrequest_show', repo_name=self.db_repo_name,
328 'pullrequest_show', repo_name=self.db_repo_name,
297 pull_request_id=pull_request_id))
329 pull_request_id=pull_request_id))
298
330
299 version = self.request.GET.get('version')
331 version = self.request.GET.get('version')
300 from_version = self.request.GET.get('from_version') or version
332 from_version = self.request.GET.get('from_version') or version
301 merge_checks = self.request.GET.get('merge_checks')
333 merge_checks = self.request.GET.get('merge_checks')
302 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
334 c.fulldiff = str2bool(self.request.GET.get('fulldiff'))
335 force_refresh = str2bool(self.request.GET.get('force_refresh'))
336 c.range_diff_on = self.request.GET.get('range-diff') == "1"
303
337
304 # fetch global flags of ignore ws or context lines
338 # fetch global flags of ignore ws or context lines
305 diff_context = diffs.get_diff_context(self.request)
339 diff_context = diffs.get_diff_context(self.request)
306 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
340 hide_whitespace_changes = diffs.get_diff_whitespace_flag(self.request)
307
341
308 force_refresh = str2bool(self.request.GET.get('force_refresh'))
309
310 (pull_request_latest,
342 (pull_request_latest,
311 pull_request_at_ver,
343 pull_request_at_ver,
312 pull_request_display_obj,
344 pull_request_display_obj,
313 at_version) = PullRequestModel().get_pr_version(
345 at_version) = PullRequestModel().get_pr_version(
314 pull_request_id, version=version)
346 pull_request_id, version=version)
347
315 pr_closed = pull_request_latest.is_closed()
348 pr_closed = pull_request_latest.is_closed()
316
349
317 if pr_closed and (version or from_version):
350 if pr_closed and (version or from_version):
318 # not allow to browse versions
351 # not allow to browse versions for closed PR
319 raise HTTPFound(h.route_path(
352 raise HTTPFound(h.route_path(
320 'pullrequest_show', repo_name=self.db_repo_name,
353 'pullrequest_show', repo_name=self.db_repo_name,
321 pull_request_id=pull_request_id))
354 pull_request_id=pull_request_id))
322
355
323 versions = pull_request_display_obj.versions()
356 versions = pull_request_display_obj.versions()
324 # used to store per-commit range diffs
357 # used to store per-commit range diffs
325 c.changes = collections.OrderedDict()
358 c.changes = collections.OrderedDict()
326 c.range_diff_on = self.request.GET.get('range-diff') == "1"
327
359
328 c.at_version = at_version
360 c.at_version = at_version
329 c.at_version_num = (at_version
361 c.at_version_num = (at_version
330 if at_version and at_version != 'latest'
362 if at_version and at_version != PullRequest.LATEST_VER
331 else None)
363 else None)
332 c.at_version_pos = ChangesetComment.get_index_from_version(
364
365 c.at_version_index = ChangesetComment.get_index_from_version(
333 c.at_version_num, versions)
366 c.at_version_num, versions)
334
367
335 (prev_pull_request_latest,
368 (prev_pull_request_latest,
336 prev_pull_request_at_ver,
369 prev_pull_request_at_ver,
337 prev_pull_request_display_obj,
370 prev_pull_request_display_obj,
338 prev_at_version) = PullRequestModel().get_pr_version(
371 prev_at_version) = PullRequestModel().get_pr_version(
339 pull_request_id, version=from_version)
372 pull_request_id, version=from_version)
340
373
341 c.from_version = prev_at_version
374 c.from_version = prev_at_version
342 c.from_version_num = (prev_at_version
375 c.from_version_num = (prev_at_version
343 if prev_at_version and prev_at_version != 'latest'
376 if prev_at_version and prev_at_version != PullRequest.LATEST_VER
344 else None)
377 else None)
345 c.from_version_pos = ChangesetComment.get_index_from_version(
378 c.from_version_index = ChangesetComment.get_index_from_version(
346 c.from_version_num, versions)
379 c.from_version_num, versions)
347
380
348 # define if we're in COMPARE mode or VIEW at version mode
381 # define if we're in COMPARE mode or VIEW at version mode
349 compare = at_version != prev_at_version
382 compare = at_version != prev_at_version
350
383
351 # pull_requests repo_name we opened it against
384 # pull_requests repo_name we opened it against
352 # ie. target_repo must match
385 # ie. target_repo must match
353 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
386 if self.db_repo_name != pull_request_at_ver.target_repo.repo_name:
354 log.warning('Mismatch between the current repo: %s, and target %s',
387 log.warning('Mismatch between the current repo: %s, and target %s',
355 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
388 self.db_repo_name, pull_request_at_ver.target_repo.repo_name)
356 raise HTTPNotFound()
389 raise HTTPNotFound()
357
390
358 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(
391 c.shadow_clone_url = PullRequestModel().get_shadow_clone_url(pull_request_at_ver)
359 pull_request_at_ver)
360
392
361 c.pull_request = pull_request_display_obj
393 c.pull_request = pull_request_display_obj
362 c.renderer = pull_request_at_ver.description_renderer or c.renderer
394 c.renderer = pull_request_at_ver.description_renderer or c.renderer
363 c.pull_request_latest = pull_request_latest
395 c.pull_request_latest = pull_request_latest
364
396
365 if compare or (at_version and not at_version == 'latest'):
397 # inject latest version
398 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
399 c.versions = versions + [latest_ver]
400
401 if compare or (at_version and not at_version == PullRequest.LATEST_VER):
366 c.allowed_to_change_status = False
402 c.allowed_to_change_status = False
367 c.allowed_to_update = False
403 c.allowed_to_update = False
368 c.allowed_to_merge = False
404 c.allowed_to_merge = False
369 c.allowed_to_delete = False
405 c.allowed_to_delete = False
370 c.allowed_to_comment = False
406 c.allowed_to_comment = False
371 c.allowed_to_close = False
407 c.allowed_to_close = False
372 else:
408 else:
373 can_change_status = PullRequestModel().check_user_change_status(
409 can_change_status = PullRequestModel().check_user_change_status(
374 pull_request_at_ver, self._rhodecode_user)
410 pull_request_at_ver, self._rhodecode_user)
375 c.allowed_to_change_status = can_change_status and not pr_closed
411 c.allowed_to_change_status = can_change_status and not pr_closed
376
412
377 c.allowed_to_update = PullRequestModel().check_user_update(
413 c.allowed_to_update = PullRequestModel().check_user_update(
378 pull_request_latest, self._rhodecode_user) and not pr_closed
414 pull_request_latest, self._rhodecode_user) and not pr_closed
379 c.allowed_to_merge = PullRequestModel().check_user_merge(
415 c.allowed_to_merge = PullRequestModel().check_user_merge(
380 pull_request_latest, self._rhodecode_user) and not pr_closed
416 pull_request_latest, self._rhodecode_user) and not pr_closed
381 c.allowed_to_delete = PullRequestModel().check_user_delete(
417 c.allowed_to_delete = PullRequestModel().check_user_delete(
382 pull_request_latest, self._rhodecode_user) and not pr_closed
418 pull_request_latest, self._rhodecode_user) and not pr_closed
383 c.allowed_to_comment = not pr_closed
419 c.allowed_to_comment = not pr_closed
384 c.allowed_to_close = c.allowed_to_merge and not pr_closed
420 c.allowed_to_close = c.allowed_to_merge and not pr_closed
385
421
386 c.forbid_adding_reviewers = False
422 c.forbid_adding_reviewers = False
387 c.forbid_author_to_review = False
423 c.forbid_author_to_review = False
388 c.forbid_commit_author_to_review = False
424 c.forbid_commit_author_to_review = False
389
425
390 if pull_request_latest.reviewer_data and \
426 if pull_request_latest.reviewer_data and \
391 'rules' in pull_request_latest.reviewer_data:
427 'rules' in pull_request_latest.reviewer_data:
392 rules = pull_request_latest.reviewer_data['rules'] or {}
428 rules = pull_request_latest.reviewer_data['rules'] or {}
393 try:
429 try:
394 c.forbid_adding_reviewers = rules.get(
430 c.forbid_adding_reviewers = rules.get('forbid_adding_reviewers')
395 'forbid_adding_reviewers')
431 c.forbid_author_to_review = rules.get('forbid_author_to_review')
396 c.forbid_author_to_review = rules.get(
432 c.forbid_commit_author_to_review = rules.get('forbid_commit_author_to_review')
397 'forbid_author_to_review')
398 c.forbid_commit_author_to_review = rules.get(
399 'forbid_commit_author_to_review')
400 except Exception:
433 except Exception:
401 pass
434 pass
402
435
403 # check merge capabilities
436 # check merge capabilities
404 _merge_check = MergeCheck.validate(
437 _merge_check = MergeCheck.validate(
405 pull_request_latest, auth_user=self._rhodecode_user,
438 pull_request_latest, auth_user=self._rhodecode_user,
406 translator=self.request.translate,
439 translator=self.request.translate,
407 force_shadow_repo_refresh=force_refresh)
440 force_shadow_repo_refresh=force_refresh)
408
441
409 c.pr_merge_errors = _merge_check.error_details
442 c.pr_merge_errors = _merge_check.error_details
410 c.pr_merge_possible = not _merge_check.failed
443 c.pr_merge_possible = not _merge_check.failed
411 c.pr_merge_message = _merge_check.merge_msg
444 c.pr_merge_message = _merge_check.merge_msg
412 c.pr_merge_source_commit = _merge_check.source_commit
445 c.pr_merge_source_commit = _merge_check.source_commit
413 c.pr_merge_target_commit = _merge_check.target_commit
446 c.pr_merge_target_commit = _merge_check.target_commit
414
447
415 c.pr_merge_info = MergeCheck.get_merge_conditions(
448 c.pr_merge_info = MergeCheck.get_merge_conditions(
416 pull_request_latest, translator=self.request.translate)
449 pull_request_latest, translator=self.request.translate)
417
450
418 c.pull_request_review_status = _merge_check.review_status
451 c.pull_request_review_status = _merge_check.review_status
419 if merge_checks:
452 if merge_checks:
420 self.request.override_renderer = \
453 self.request.override_renderer = \
421 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
454 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
422 return self._get_template_context(c)
455 return self._get_template_context(c)
423
456
424 comments_model = CommentsModel()
457 c.allowed_reviewers = [obj.user_id for obj in pull_request.reviewers if obj.user]
425
458
426 # reviewers and statuses
459 # reviewers and statuses
427 c.pull_request_reviewers = pull_request_at_ver.reviewers_statuses()
460 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
428 allowed_reviewers = [x[0].user_id for x in c.pull_request_reviewers]
461 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
429
462
430 # GENERAL COMMENTS with versions #
463 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
431 q = comments_model._all_general_comments_of_pull_request(pull_request_latest)
464 member_reviewer = h.reviewer_as_json(
432 q = q.order_by(ChangesetComment.comment_id.asc())
465 member, reasons=reasons, mandatory=mandatory,
433 general_comments = q
466 user_group=review_obj.rule_user_group_data()
467 )
434
468
435 # pick comments we want to render at current version
469 current_review_status = status[0][1].status if status else ChangesetStatus.STATUS_NOT_REVIEWED
436 c.comment_versions = comments_model.aggregate_comments(
470 member_reviewer['review_status'] = current_review_status
437 general_comments, versions, c.at_version_num)
471 member_reviewer['review_status_label'] = h.commit_status_lbl(current_review_status)
438 c.comments = c.comment_versions[c.at_version_num]['until']
472 member_reviewer['allowed_to_update'] = c.allowed_to_update
473 c.pull_request_set_reviewers_data_json['reviewers'].append(member_reviewer)
439
474
440 # INLINE COMMENTS with versions #
475 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
441 q = comments_model._all_inline_comments_of_pull_request(pull_request_latest)
476
442 q = q.order_by(ChangesetComment.comment_id.asc())
477
443 inline_comments = q
444
478
445 c.inline_versions = comments_model.aggregate_comments(
479
446 inline_comments, versions, c.at_version_num, inline=True)
480 general_comments, inline_comments = \
481 self.register_comments_vars(c, pull_request_latest, versions)
447
482
448 # TODOs
483 # TODOs
449 c.unresolved_comments = CommentsModel() \
484 c.unresolved_comments = CommentsModel() \
450 .get_pull_request_unresolved_todos(pull_request)
485 .get_pull_request_unresolved_todos(pull_request_latest)
451 c.resolved_comments = CommentsModel() \
486 c.resolved_comments = CommentsModel() \
452 .get_pull_request_resolved_todos(pull_request)
487 .get_pull_request_resolved_todos(pull_request_latest)
453
454 # inject latest version
455 latest_ver = PullRequest.get_pr_display_object(
456 pull_request_latest, pull_request_latest)
457
458 c.versions = versions + [latest_ver]
459
488
460 # if we use version, then do not show later comments
489 # if we use version, then do not show later comments
461 # than current version
490 # than current version
462 display_inline_comments = collections.defaultdict(
491 display_inline_comments = collections.defaultdict(
463 lambda: collections.defaultdict(list))
492 lambda: collections.defaultdict(list))
464 for co in inline_comments:
493 for co in inline_comments:
465 if c.at_version_num:
494 if c.at_version_num:
466 # pick comments that are at least UPTO given version, so we
495 # pick comments that are at least UPTO given version, so we
467 # don't render comments for higher version
496 # don't render comments for higher version
468 should_render = co.pull_request_version_id and \
497 should_render = co.pull_request_version_id and \
469 co.pull_request_version_id <= c.at_version_num
498 co.pull_request_version_id <= c.at_version_num
470 else:
499 else:
471 # showing all, for 'latest'
500 # showing all, for 'latest'
472 should_render = True
501 should_render = True
473
502
474 if should_render:
503 if should_render:
475 display_inline_comments[co.f_path][co.line_no].append(co)
504 display_inline_comments[co.f_path][co.line_no].append(co)
476
505
477 # load diff data into template context, if we use compare mode then
506 # load diff data into template context, if we use compare mode then
478 # diff is calculated based on changes between versions of PR
507 # diff is calculated based on changes between versions of PR
479
508
480 source_repo = pull_request_at_ver.source_repo
509 source_repo = pull_request_at_ver.source_repo
481 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
510 source_ref_id = pull_request_at_ver.source_ref_parts.commit_id
482
511
483 target_repo = pull_request_at_ver.target_repo
512 target_repo = pull_request_at_ver.target_repo
484 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
513 target_ref_id = pull_request_at_ver.target_ref_parts.commit_id
485
514
486 if compare:
515 if compare:
487 # in compare switch the diff base to latest commit from prev version
516 # in compare switch the diff base to latest commit from prev version
488 target_ref_id = prev_pull_request_display_obj.revisions[0]
517 target_ref_id = prev_pull_request_display_obj.revisions[0]
489
518
490 # despite opening commits for bookmarks/branches/tags, we always
519 # despite opening commits for bookmarks/branches/tags, we always
491 # convert this to rev to prevent changes after bookmark or branch change
520 # convert this to rev to prevent changes after bookmark or branch change
492 c.source_ref_type = 'rev'
521 c.source_ref_type = 'rev'
493 c.source_ref = source_ref_id
522 c.source_ref = source_ref_id
494
523
495 c.target_ref_type = 'rev'
524 c.target_ref_type = 'rev'
496 c.target_ref = target_ref_id
525 c.target_ref = target_ref_id
497
526
498 c.source_repo = source_repo
527 c.source_repo = source_repo
499 c.target_repo = target_repo
528 c.target_repo = target_repo
500
529
501 c.commit_ranges = []
530 c.commit_ranges = []
502 source_commit = EmptyCommit()
531 source_commit = EmptyCommit()
503 target_commit = EmptyCommit()
532 target_commit = EmptyCommit()
504 c.missing_requirements = False
533 c.missing_requirements = False
505
534
506 source_scm = source_repo.scm_instance()
535 source_scm = source_repo.scm_instance()
507 target_scm = target_repo.scm_instance()
536 target_scm = target_repo.scm_instance()
508
537
509 shadow_scm = None
538 shadow_scm = None
510 try:
539 try:
511 shadow_scm = pull_request_latest.get_shadow_repo()
540 shadow_scm = pull_request_latest.get_shadow_repo()
512 except Exception:
541 except Exception:
513 log.debug('Failed to get shadow repo', exc_info=True)
542 log.debug('Failed to get shadow repo', exc_info=True)
514 # try first the existing source_repo, and then shadow
543 # try first the existing source_repo, and then shadow
515 # repo if we can obtain one
544 # repo if we can obtain one
516 commits_source_repo = source_scm
545 commits_source_repo = source_scm
517 if shadow_scm:
546 if shadow_scm:
518 commits_source_repo = shadow_scm
547 commits_source_repo = shadow_scm
519
548
520 c.commits_source_repo = commits_source_repo
549 c.commits_source_repo = commits_source_repo
521 c.ancestor = None # set it to None, to hide it from PR view
550 c.ancestor = None # set it to None, to hide it from PR view
522
551
523 # empty version means latest, so we keep this to prevent
552 # empty version means latest, so we keep this to prevent
524 # double caching
553 # double caching
525 version_normalized = version or 'latest'
554 version_normalized = version or PullRequest.LATEST_VER
526 from_version_normalized = from_version or 'latest'
555 from_version_normalized = from_version or PullRequest.LATEST_VER
527
556
528 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
557 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(target_repo)
529 cache_file_path = diff_cache_exist(
558 cache_file_path = diff_cache_exist(
530 cache_path, 'pull_request', pull_request_id, version_normalized,
559 cache_path, 'pull_request', pull_request_id, version_normalized,
531 from_version_normalized, source_ref_id, target_ref_id,
560 from_version_normalized, source_ref_id, target_ref_id,
532 hide_whitespace_changes, diff_context, c.fulldiff)
561 hide_whitespace_changes, diff_context, c.fulldiff)
533
562
534 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
563 caching_enabled = self._is_diff_cache_enabled(c.target_repo)
535 force_recache = self.get_recache_flag()
564 force_recache = self.get_recache_flag()
536
565
537 cached_diff = None
566 cached_diff = None
538 if caching_enabled:
567 if caching_enabled:
539 cached_diff = load_cached_diff(cache_file_path)
568 cached_diff = load_cached_diff(cache_file_path)
540
569
541 has_proper_commit_cache = (
570 has_proper_commit_cache = (
542 cached_diff and cached_diff.get('commits')
571 cached_diff and cached_diff.get('commits')
543 and len(cached_diff.get('commits', [])) == 5
572 and len(cached_diff.get('commits', [])) == 5
544 and cached_diff.get('commits')[0]
573 and cached_diff.get('commits')[0]
545 and cached_diff.get('commits')[3])
574 and cached_diff.get('commits')[3])
546
575
547 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
576 if not force_recache and not c.range_diff_on and has_proper_commit_cache:
548 diff_commit_cache = \
577 diff_commit_cache = \
549 (ancestor_commit, commit_cache, missing_requirements,
578 (ancestor_commit, commit_cache, missing_requirements,
550 source_commit, target_commit) = cached_diff['commits']
579 source_commit, target_commit) = cached_diff['commits']
551 else:
580 else:
552 # NOTE(marcink): we reach potentially unreachable errors when a PR has
581 # NOTE(marcink): we reach potentially unreachable errors when a PR has
553 # merge errors resulting in potentially hidden commits in the shadow repo.
582 # merge errors resulting in potentially hidden commits in the shadow repo.
554 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
583 maybe_unreachable = _merge_check.MERGE_CHECK in _merge_check.error_details \
555 and _merge_check.merge_response
584 and _merge_check.merge_response
556 maybe_unreachable = maybe_unreachable \
585 maybe_unreachable = maybe_unreachable \
557 and _merge_check.merge_response.metadata.get('unresolved_files')
586 and _merge_check.merge_response.metadata.get('unresolved_files')
558 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
587 log.debug("Using unreachable commits due to MERGE_CHECK in merge simulation")
559 diff_commit_cache = \
588 diff_commit_cache = \
560 (ancestor_commit, commit_cache, missing_requirements,
589 (ancestor_commit, commit_cache, missing_requirements,
561 source_commit, target_commit) = self.get_commits(
590 source_commit, target_commit) = self.get_commits(
562 commits_source_repo,
591 commits_source_repo,
563 pull_request_at_ver,
592 pull_request_at_ver,
564 source_commit,
593 source_commit,
565 source_ref_id,
594 source_ref_id,
566 source_scm,
595 source_scm,
567 target_commit,
596 target_commit,
568 target_ref_id,
597 target_ref_id,
569 target_scm,
598 target_scm,
570 maybe_unreachable=maybe_unreachable)
599 maybe_unreachable=maybe_unreachable)
571
600
572 # register our commit range
601 # register our commit range
573 for comm in commit_cache.values():
602 for comm in commit_cache.values():
574 c.commit_ranges.append(comm)
603 c.commit_ranges.append(comm)
575
604
576 c.missing_requirements = missing_requirements
605 c.missing_requirements = missing_requirements
577 c.ancestor_commit = ancestor_commit
606 c.ancestor_commit = ancestor_commit
578 c.statuses = source_repo.statuses(
607 c.statuses = source_repo.statuses(
579 [x.raw_id for x in c.commit_ranges])
608 [x.raw_id for x in c.commit_ranges])
580
609
581 # auto collapse if we have more than limit
610 # auto collapse if we have more than limit
582 collapse_limit = diffs.DiffProcessor._collapse_commits_over
611 collapse_limit = diffs.DiffProcessor._collapse_commits_over
583 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
612 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
584 c.compare_mode = compare
613 c.compare_mode = compare
585
614
586 # diff_limit is the old behavior, will cut off the whole diff
615 # diff_limit is the old behavior, will cut off the whole diff
587 # if the limit is applied otherwise will just hide the
616 # if the limit is applied otherwise will just hide the
588 # big files from the front-end
617 # big files from the front-end
589 diff_limit = c.visual.cut_off_limit_diff
618 diff_limit = c.visual.cut_off_limit_diff
590 file_limit = c.visual.cut_off_limit_file
619 file_limit = c.visual.cut_off_limit_file
591
620
592 c.missing_commits = False
621 c.missing_commits = False
593 if (c.missing_requirements
622 if (c.missing_requirements
594 or isinstance(source_commit, EmptyCommit)
623 or isinstance(source_commit, EmptyCommit)
595 or source_commit == target_commit):
624 or source_commit == target_commit):
596
625
597 c.missing_commits = True
626 c.missing_commits = True
598 else:
627 else:
599 c.inline_comments = display_inline_comments
628 c.inline_comments = display_inline_comments
600
629
601 use_ancestor = True
630 use_ancestor = True
602 if from_version_normalized != version_normalized:
631 if from_version_normalized != version_normalized:
603 use_ancestor = False
632 use_ancestor = False
604
633
605 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
634 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
606 if not force_recache and has_proper_diff_cache:
635 if not force_recache and has_proper_diff_cache:
607 c.diffset = cached_diff['diff']
636 c.diffset = cached_diff['diff']
608 else:
637 else:
609 try:
638 try:
610 c.diffset = self._get_diffset(
639 c.diffset = self._get_diffset(
611 c.source_repo.repo_name, commits_source_repo,
640 c.source_repo.repo_name, commits_source_repo,
612 c.ancestor_commit,
641 c.ancestor_commit,
613 source_ref_id, target_ref_id,
642 source_ref_id, target_ref_id,
614 target_commit, source_commit,
643 target_commit, source_commit,
615 diff_limit, file_limit, c.fulldiff,
644 diff_limit, file_limit, c.fulldiff,
616 hide_whitespace_changes, diff_context,
645 hide_whitespace_changes, diff_context,
617 use_ancestor=use_ancestor
646 use_ancestor=use_ancestor
618 )
647 )
619
648
620 # save cached diff
649 # save cached diff
621 if caching_enabled:
650 if caching_enabled:
622 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
651 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
623 except CommitDoesNotExistError:
652 except CommitDoesNotExistError:
624 log.exception('Failed to generate diffset')
653 log.exception('Failed to generate diffset')
625 c.missing_commits = True
654 c.missing_commits = True
626
655
627 if not c.missing_commits:
656 if not c.missing_commits:
628
657
629 c.limited_diff = c.diffset.limited_diff
658 c.limited_diff = c.diffset.limited_diff
630
659
631 # calculate removed files that are bound to comments
660 # calculate removed files that are bound to comments
632 comment_deleted_files = [
661 comment_deleted_files = [
633 fname for fname in display_inline_comments
662 fname for fname in display_inline_comments
634 if fname not in c.diffset.file_stats]
663 if fname not in c.diffset.file_stats]
635
664
636 c.deleted_files_comments = collections.defaultdict(dict)
665 c.deleted_files_comments = collections.defaultdict(dict)
637 for fname, per_line_comments in display_inline_comments.items():
666 for fname, per_line_comments in display_inline_comments.items():
638 if fname in comment_deleted_files:
667 if fname in comment_deleted_files:
639 c.deleted_files_comments[fname]['stats'] = 0
668 c.deleted_files_comments[fname]['stats'] = 0
640 c.deleted_files_comments[fname]['comments'] = list()
669 c.deleted_files_comments[fname]['comments'] = list()
641 for lno, comments in per_line_comments.items():
670 for lno, comments in per_line_comments.items():
642 c.deleted_files_comments[fname]['comments'].extend(comments)
671 c.deleted_files_comments[fname]['comments'].extend(comments)
643
672
644 # maybe calculate the range diff
673 # maybe calculate the range diff
645 if c.range_diff_on:
674 if c.range_diff_on:
646 # TODO(marcink): set whitespace/context
675 # TODO(marcink): set whitespace/context
647 context_lcl = 3
676 context_lcl = 3
648 ign_whitespace_lcl = False
677 ign_whitespace_lcl = False
649
678
650 for commit in c.commit_ranges:
679 for commit in c.commit_ranges:
651 commit2 = commit
680 commit2 = commit
652 commit1 = commit.first_parent
681 commit1 = commit.first_parent
653
682
654 range_diff_cache_file_path = diff_cache_exist(
683 range_diff_cache_file_path = diff_cache_exist(
655 cache_path, 'diff', commit.raw_id,
684 cache_path, 'diff', commit.raw_id,
656 ign_whitespace_lcl, context_lcl, c.fulldiff)
685 ign_whitespace_lcl, context_lcl, c.fulldiff)
657
686
658 cached_diff = None
687 cached_diff = None
659 if caching_enabled:
688 if caching_enabled:
660 cached_diff = load_cached_diff(range_diff_cache_file_path)
689 cached_diff = load_cached_diff(range_diff_cache_file_path)
661
690
662 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
691 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
663 if not force_recache and has_proper_diff_cache:
692 if not force_recache and has_proper_diff_cache:
664 diffset = cached_diff['diff']
693 diffset = cached_diff['diff']
665 else:
694 else:
666 diffset = self._get_range_diffset(
695 diffset = self._get_range_diffset(
667 commits_source_repo, source_repo,
696 commits_source_repo, source_repo,
668 commit1, commit2, diff_limit, file_limit,
697 commit1, commit2, diff_limit, file_limit,
669 c.fulldiff, ign_whitespace_lcl, context_lcl
698 c.fulldiff, ign_whitespace_lcl, context_lcl
670 )
699 )
671
700
672 # save cached diff
701 # save cached diff
673 if caching_enabled:
702 if caching_enabled:
674 cache_diff(range_diff_cache_file_path, diffset, None)
703 cache_diff(range_diff_cache_file_path, diffset, None)
675
704
676 c.changes[commit.raw_id] = diffset
705 c.changes[commit.raw_id] = diffset
677
706
678 # this is a hack to properly display links, when creating PR, the
707 # this is a hack to properly display links, when creating PR, the
679 # compare view and others uses different notation, and
708 # compare view and others uses different notation, and
680 # compare_commits.mako renders links based on the target_repo.
709 # compare_commits.mako renders links based on the target_repo.
681 # We need to swap that here to generate it properly on the html side
710 # We need to swap that here to generate it properly on the html side
682 c.target_repo = c.source_repo
711 c.target_repo = c.source_repo
683
712
684 c.commit_statuses = ChangesetStatus.STATUSES
713 c.commit_statuses = ChangesetStatus.STATUSES
685
714
686 c.show_version_changes = not pr_closed
715 c.show_version_changes = not pr_closed
687 if c.show_version_changes:
716 if c.show_version_changes:
688 cur_obj = pull_request_at_ver
717 cur_obj = pull_request_at_ver
689 prev_obj = prev_pull_request_at_ver
718 prev_obj = prev_pull_request_at_ver
690
719
691 old_commit_ids = prev_obj.revisions
720 old_commit_ids = prev_obj.revisions
692 new_commit_ids = cur_obj.revisions
721 new_commit_ids = cur_obj.revisions
693 commit_changes = PullRequestModel()._calculate_commit_id_changes(
722 commit_changes = PullRequestModel()._calculate_commit_id_changes(
694 old_commit_ids, new_commit_ids)
723 old_commit_ids, new_commit_ids)
695 c.commit_changes_summary = commit_changes
724 c.commit_changes_summary = commit_changes
696
725
697 # calculate the diff for commits between versions
726 # calculate the diff for commits between versions
698 c.commit_changes = []
727 c.commit_changes = []
699
728
700 def mark(cs, fw):
729 def mark(cs, fw):
701 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
730 return list(h.itertools.izip_longest([], cs, fillvalue=fw))
702
731
703 for c_type, raw_id in mark(commit_changes.added, 'a') \
732 for c_type, raw_id in mark(commit_changes.added, 'a') \
704 + mark(commit_changes.removed, 'r') \
733 + mark(commit_changes.removed, 'r') \
705 + mark(commit_changes.common, 'c'):
734 + mark(commit_changes.common, 'c'):
706
735
707 if raw_id in commit_cache:
736 if raw_id in commit_cache:
708 commit = commit_cache[raw_id]
737 commit = commit_cache[raw_id]
709 else:
738 else:
710 try:
739 try:
711 commit = commits_source_repo.get_commit(raw_id)
740 commit = commits_source_repo.get_commit(raw_id)
712 except CommitDoesNotExistError:
741 except CommitDoesNotExistError:
713 # in case we fail extracting still use "dummy" commit
742 # in case we fail extracting still use "dummy" commit
714 # for display in commit diff
743 # for display in commit diff
715 commit = h.AttributeDict(
744 commit = h.AttributeDict(
716 {'raw_id': raw_id,
745 {'raw_id': raw_id,
717 'message': 'EMPTY or MISSING COMMIT'})
746 'message': 'EMPTY or MISSING COMMIT'})
718 c.commit_changes.append([c_type, commit])
747 c.commit_changes.append([c_type, commit])
719
748
720 # current user review statuses for each version
749 # current user review statuses for each version
721 c.review_versions = {}
750 c.review_versions = {}
722 if self._rhodecode_user.user_id in allowed_reviewers:
751 if self._rhodecode_user.user_id in c.allowed_reviewers:
723 for co in general_comments:
752 for co in general_comments:
724 if co.author.user_id == self._rhodecode_user.user_id:
753 if co.author.user_id == self._rhodecode_user.user_id:
725 status = co.status_change
754 status = co.status_change
726 if status:
755 if status:
727 _ver_pr = status[0].comment.pull_request_version_id
756 _ver_pr = status[0].comment.pull_request_version_id
728 c.review_versions[_ver_pr] = status[0]
757 c.review_versions[_ver_pr] = status[0]
729
758
730 return self._get_template_context(c)
759 return self._get_template_context(c)
731
760
732 def get_commits(
761 def get_commits(
733 self, commits_source_repo, pull_request_at_ver, source_commit,
762 self, commits_source_repo, pull_request_at_ver, source_commit,
734 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
763 source_ref_id, source_scm, target_commit, target_ref_id, target_scm,
735 maybe_unreachable=False):
764 maybe_unreachable=False):
736
765
737 commit_cache = collections.OrderedDict()
766 commit_cache = collections.OrderedDict()
738 missing_requirements = False
767 missing_requirements = False
739
768
740 try:
769 try:
741 pre_load = ["author", "date", "message", "branch", "parents"]
770 pre_load = ["author", "date", "message", "branch", "parents"]
742
771
743 pull_request_commits = pull_request_at_ver.revisions
772 pull_request_commits = pull_request_at_ver.revisions
744 log.debug('Loading %s commits from %s',
773 log.debug('Loading %s commits from %s',
745 len(pull_request_commits), commits_source_repo)
774 len(pull_request_commits), commits_source_repo)
746
775
747 for rev in pull_request_commits:
776 for rev in pull_request_commits:
748 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
777 comm = commits_source_repo.get_commit(commit_id=rev, pre_load=pre_load,
749 maybe_unreachable=maybe_unreachable)
778 maybe_unreachable=maybe_unreachable)
750 commit_cache[comm.raw_id] = comm
779 commit_cache[comm.raw_id] = comm
751
780
752 # Order here matters, we first need to get target, and then
781 # Order here matters, we first need to get target, and then
753 # the source
782 # the source
754 target_commit = commits_source_repo.get_commit(
783 target_commit = commits_source_repo.get_commit(
755 commit_id=safe_str(target_ref_id))
784 commit_id=safe_str(target_ref_id))
756
785
757 source_commit = commits_source_repo.get_commit(
786 source_commit = commits_source_repo.get_commit(
758 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
787 commit_id=safe_str(source_ref_id), maybe_unreachable=True)
759 except CommitDoesNotExistError:
788 except CommitDoesNotExistError:
760 log.warning('Failed to get commit from `{}` repo'.format(
789 log.warning('Failed to get commit from `{}` repo'.format(
761 commits_source_repo), exc_info=True)
790 commits_source_repo), exc_info=True)
762 except RepositoryRequirementError:
791 except RepositoryRequirementError:
763 log.warning('Failed to get all required data from repo', exc_info=True)
792 log.warning('Failed to get all required data from repo', exc_info=True)
764 missing_requirements = True
793 missing_requirements = True
765
794
766 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
795 pr_ancestor_id = pull_request_at_ver.common_ancestor_id
767
796
768 try:
797 try:
769 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
798 ancestor_commit = source_scm.get_commit(pr_ancestor_id)
770 except Exception:
799 except Exception:
771 ancestor_commit = None
800 ancestor_commit = None
772
801
773 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
802 return ancestor_commit, commit_cache, missing_requirements, source_commit, target_commit
774
803
775 def assure_not_empty_repo(self):
804 def assure_not_empty_repo(self):
776 _ = self.request.translate
805 _ = self.request.translate
777
806
778 try:
807 try:
779 self.db_repo.scm_instance().get_commit()
808 self.db_repo.scm_instance().get_commit()
780 except EmptyRepositoryError:
809 except EmptyRepositoryError:
781 h.flash(h.literal(_('There are no commits yet')),
810 h.flash(h.literal(_('There are no commits yet')),
782 category='warning')
811 category='warning')
783 raise HTTPFound(
812 raise HTTPFound(
784 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
813 h.route_path('repo_summary', repo_name=self.db_repo.repo_name))
785
814
786 @LoginRequired()
815 @LoginRequired()
787 @NotAnonymous()
816 @NotAnonymous()
788 @HasRepoPermissionAnyDecorator(
817 @HasRepoPermissionAnyDecorator(
789 'repository.read', 'repository.write', 'repository.admin')
818 'repository.read', 'repository.write', 'repository.admin')
790 @view_config(
819 @view_config(
791 route_name='pullrequest_new', request_method='GET',
820 route_name='pullrequest_new', request_method='GET',
792 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
821 renderer='rhodecode:templates/pullrequests/pullrequest.mako')
793 def pull_request_new(self):
822 def pull_request_new(self):
794 _ = self.request.translate
823 _ = self.request.translate
795 c = self.load_default_context()
824 c = self.load_default_context()
796
825
797 self.assure_not_empty_repo()
826 self.assure_not_empty_repo()
798 source_repo = self.db_repo
827 source_repo = self.db_repo
799
828
800 commit_id = self.request.GET.get('commit')
829 commit_id = self.request.GET.get('commit')
801 branch_ref = self.request.GET.get('branch')
830 branch_ref = self.request.GET.get('branch')
802 bookmark_ref = self.request.GET.get('bookmark')
831 bookmark_ref = self.request.GET.get('bookmark')
803
832
804 try:
833 try:
805 source_repo_data = PullRequestModel().generate_repo_data(
834 source_repo_data = PullRequestModel().generate_repo_data(
806 source_repo, commit_id=commit_id,
835 source_repo, commit_id=commit_id,
807 branch=branch_ref, bookmark=bookmark_ref,
836 branch=branch_ref, bookmark=bookmark_ref,
808 translator=self.request.translate)
837 translator=self.request.translate)
809 except CommitDoesNotExistError as e:
838 except CommitDoesNotExistError as e:
810 log.exception(e)
839 log.exception(e)
811 h.flash(_('Commit does not exist'), 'error')
840 h.flash(_('Commit does not exist'), 'error')
812 raise HTTPFound(
841 raise HTTPFound(
813 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
842 h.route_path('pullrequest_new', repo_name=source_repo.repo_name))
814
843
815 default_target_repo = source_repo
844 default_target_repo = source_repo
816
845
817 if source_repo.parent and c.has_origin_repo_read_perm:
846 if source_repo.parent and c.has_origin_repo_read_perm:
818 parent_vcs_obj = source_repo.parent.scm_instance()
847 parent_vcs_obj = source_repo.parent.scm_instance()
819 if parent_vcs_obj and not parent_vcs_obj.is_empty():
848 if parent_vcs_obj and not parent_vcs_obj.is_empty():
820 # change default if we have a parent repo
849 # change default if we have a parent repo
821 default_target_repo = source_repo.parent
850 default_target_repo = source_repo.parent
822
851
823 target_repo_data = PullRequestModel().generate_repo_data(
852 target_repo_data = PullRequestModel().generate_repo_data(
824 default_target_repo, translator=self.request.translate)
853 default_target_repo, translator=self.request.translate)
825
854
826 selected_source_ref = source_repo_data['refs']['selected_ref']
855 selected_source_ref = source_repo_data['refs']['selected_ref']
827 title_source_ref = ''
856 title_source_ref = ''
828 if selected_source_ref:
857 if selected_source_ref:
829 title_source_ref = selected_source_ref.split(':', 2)[1]
858 title_source_ref = selected_source_ref.split(':', 2)[1]
830 c.default_title = PullRequestModel().generate_pullrequest_title(
859 c.default_title = PullRequestModel().generate_pullrequest_title(
831 source=source_repo.repo_name,
860 source=source_repo.repo_name,
832 source_ref=title_source_ref,
861 source_ref=title_source_ref,
833 target=default_target_repo.repo_name
862 target=default_target_repo.repo_name
834 )
863 )
835
864
836 c.default_repo_data = {
865 c.default_repo_data = {
837 'source_repo_name': source_repo.repo_name,
866 'source_repo_name': source_repo.repo_name,
838 'source_refs_json': json.dumps(source_repo_data),
867 'source_refs_json': json.dumps(source_repo_data),
839 'target_repo_name': default_target_repo.repo_name,
868 'target_repo_name': default_target_repo.repo_name,
840 'target_refs_json': json.dumps(target_repo_data),
869 'target_refs_json': json.dumps(target_repo_data),
841 }
870 }
842 c.default_source_ref = selected_source_ref
871 c.default_source_ref = selected_source_ref
843
872
844 return self._get_template_context(c)
873 return self._get_template_context(c)
845
874
846 @LoginRequired()
875 @LoginRequired()
847 @NotAnonymous()
876 @NotAnonymous()
848 @HasRepoPermissionAnyDecorator(
877 @HasRepoPermissionAnyDecorator(
849 'repository.read', 'repository.write', 'repository.admin')
878 'repository.read', 'repository.write', 'repository.admin')
850 @view_config(
879 @view_config(
851 route_name='pullrequest_repo_refs', request_method='GET',
880 route_name='pullrequest_repo_refs', request_method='GET',
852 renderer='json_ext', xhr=True)
881 renderer='json_ext', xhr=True)
853 def pull_request_repo_refs(self):
882 def pull_request_repo_refs(self):
854 self.load_default_context()
883 self.load_default_context()
855 target_repo_name = self.request.matchdict['target_repo_name']
884 target_repo_name = self.request.matchdict['target_repo_name']
856 repo = Repository.get_by_repo_name(target_repo_name)
885 repo = Repository.get_by_repo_name(target_repo_name)
857 if not repo:
886 if not repo:
858 raise HTTPNotFound()
887 raise HTTPNotFound()
859
888
860 target_perm = HasRepoPermissionAny(
889 target_perm = HasRepoPermissionAny(
861 'repository.read', 'repository.write', 'repository.admin')(
890 'repository.read', 'repository.write', 'repository.admin')(
862 target_repo_name)
891 target_repo_name)
863 if not target_perm:
892 if not target_perm:
864 raise HTTPNotFound()
893 raise HTTPNotFound()
865
894
866 return PullRequestModel().generate_repo_data(
895 return PullRequestModel().generate_repo_data(
867 repo, translator=self.request.translate)
896 repo, translator=self.request.translate)
868
897
869 @LoginRequired()
898 @LoginRequired()
870 @NotAnonymous()
899 @NotAnonymous()
871 @HasRepoPermissionAnyDecorator(
900 @HasRepoPermissionAnyDecorator(
872 'repository.read', 'repository.write', 'repository.admin')
901 'repository.read', 'repository.write', 'repository.admin')
873 @view_config(
902 @view_config(
874 route_name='pullrequest_repo_targets', request_method='GET',
903 route_name='pullrequest_repo_targets', request_method='GET',
875 renderer='json_ext', xhr=True)
904 renderer='json_ext', xhr=True)
876 def pullrequest_repo_targets(self):
905 def pullrequest_repo_targets(self):
877 _ = self.request.translate
906 _ = self.request.translate
878 filter_query = self.request.GET.get('query')
907 filter_query = self.request.GET.get('query')
879
908
880 # get the parents
909 # get the parents
881 parent_target_repos = []
910 parent_target_repos = []
882 if self.db_repo.parent:
911 if self.db_repo.parent:
883 parents_query = Repository.query() \
912 parents_query = Repository.query() \
884 .order_by(func.length(Repository.repo_name)) \
913 .order_by(func.length(Repository.repo_name)) \
885 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
914 .filter(Repository.fork_id == self.db_repo.parent.repo_id)
886
915
887 if filter_query:
916 if filter_query:
888 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
917 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
889 parents_query = parents_query.filter(
918 parents_query = parents_query.filter(
890 Repository.repo_name.ilike(ilike_expression))
919 Repository.repo_name.ilike(ilike_expression))
891 parents = parents_query.limit(20).all()
920 parents = parents_query.limit(20).all()
892
921
893 for parent in parents:
922 for parent in parents:
894 parent_vcs_obj = parent.scm_instance()
923 parent_vcs_obj = parent.scm_instance()
895 if parent_vcs_obj and not parent_vcs_obj.is_empty():
924 if parent_vcs_obj and not parent_vcs_obj.is_empty():
896 parent_target_repos.append(parent)
925 parent_target_repos.append(parent)
897
926
898 # get other forks, and repo itself
927 # get other forks, and repo itself
899 query = Repository.query() \
928 query = Repository.query() \
900 .order_by(func.length(Repository.repo_name)) \
929 .order_by(func.length(Repository.repo_name)) \
901 .filter(
930 .filter(
902 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
931 or_(Repository.repo_id == self.db_repo.repo_id, # repo itself
903 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
932 Repository.fork_id == self.db_repo.repo_id) # forks of this repo
904 ) \
933 ) \
905 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
934 .filter(~Repository.repo_id.in_([x.repo_id for x in parent_target_repos]))
906
935
907 if filter_query:
936 if filter_query:
908 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
937 ilike_expression = u'%{}%'.format(safe_unicode(filter_query))
909 query = query.filter(Repository.repo_name.ilike(ilike_expression))
938 query = query.filter(Repository.repo_name.ilike(ilike_expression))
910
939
911 limit = max(20 - len(parent_target_repos), 5) # not less then 5
940 limit = max(20 - len(parent_target_repos), 5) # not less then 5
912 target_repos = query.limit(limit).all()
941 target_repos = query.limit(limit).all()
913
942
914 all_target_repos = target_repos + parent_target_repos
943 all_target_repos = target_repos + parent_target_repos
915
944
916 repos = []
945 repos = []
917 # This checks permissions to the repositories
946 # This checks permissions to the repositories
918 for obj in ScmModel().get_repos(all_target_repos):
947 for obj in ScmModel().get_repos(all_target_repos):
919 repos.append({
948 repos.append({
920 'id': obj['name'],
949 'id': obj['name'],
921 'text': obj['name'],
950 'text': obj['name'],
922 'type': 'repo',
951 'type': 'repo',
923 'repo_id': obj['dbrepo']['repo_id'],
952 'repo_id': obj['dbrepo']['repo_id'],
924 'repo_type': obj['dbrepo']['repo_type'],
953 'repo_type': obj['dbrepo']['repo_type'],
925 'private': obj['dbrepo']['private'],
954 'private': obj['dbrepo']['private'],
926
955
927 })
956 })
928
957
929 data = {
958 data = {
930 'more': False,
959 'more': False,
931 'results': [{
960 'results': [{
932 'text': _('Repositories'),
961 'text': _('Repositories'),
933 'children': repos
962 'children': repos
934 }] if repos else []
963 }] if repos else []
935 }
964 }
936 return data
965 return data
937
966
938 @LoginRequired()
967 @LoginRequired()
939 @NotAnonymous()
968 @NotAnonymous()
940 @HasRepoPermissionAnyDecorator(
969 @HasRepoPermissionAnyDecorator(
941 'repository.read', 'repository.write', 'repository.admin')
970 'repository.read', 'repository.write', 'repository.admin')
971 @view_config(
972 route_name='pullrequest_comments', request_method='POST',
973 renderer='string', xhr=True)
974 def pullrequest_comments(self):
975 self.load_default_context()
976
977 pull_request = PullRequest.get_or_404(
978 self.request.matchdict['pull_request_id'])
979 pull_request_id = pull_request.pull_request_id
980 version = self.request.GET.get('version')
981
982 _render = self.request.get_partial_renderer(
983 'rhodecode:templates/pullrequests/pullrequest_show.mako')
984 c = _render.get_call_context()
985
986 (pull_request_latest,
987 pull_request_at_ver,
988 pull_request_display_obj,
989 at_version) = PullRequestModel().get_pr_version(
990 pull_request_id, version=version)
991 versions = pull_request_display_obj.versions()
992 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
993 c.versions = versions + [latest_ver]
994
995 c.at_version = at_version
996 c.at_version_num = (at_version
997 if at_version and at_version != PullRequest.LATEST_VER
998 else None)
999
1000 self.register_comments_vars(c, pull_request_latest, versions)
1001 all_comments = c.inline_comments_flat + c.comments
1002 return _render('comments_table', all_comments, len(all_comments))
1003
1004 @LoginRequired()
1005 @NotAnonymous()
1006 @HasRepoPermissionAnyDecorator(
1007 'repository.read', 'repository.write', 'repository.admin')
1008 @view_config(
1009 route_name='pullrequest_todos', request_method='POST',
1010 renderer='string', xhr=True)
1011 def pullrequest_todos(self):
1012 self.load_default_context()
1013
1014 pull_request = PullRequest.get_or_404(
1015 self.request.matchdict['pull_request_id'])
1016 pull_request_id = pull_request.pull_request_id
1017 version = self.request.GET.get('version')
1018
1019 _render = self.request.get_partial_renderer(
1020 'rhodecode:templates/pullrequests/pullrequest_show.mako')
1021 c = _render.get_call_context()
1022 (pull_request_latest,
1023 pull_request_at_ver,
1024 pull_request_display_obj,
1025 at_version) = PullRequestModel().get_pr_version(
1026 pull_request_id, version=version)
1027 versions = pull_request_display_obj.versions()
1028 latest_ver = PullRequest.get_pr_display_object(pull_request_latest, pull_request_latest)
1029 c.versions = versions + [latest_ver]
1030
1031 c.at_version = at_version
1032 c.at_version_num = (at_version
1033 if at_version and at_version != PullRequest.LATEST_VER
1034 else None)
1035
1036 c.unresolved_comments = CommentsModel() \
1037 .get_pull_request_unresolved_todos(pull_request)
1038 c.resolved_comments = CommentsModel() \
1039 .get_pull_request_resolved_todos(pull_request)
1040
1041 all_comments = c.unresolved_comments + c.resolved_comments
1042 return _render('comments_table', all_comments, len(c.unresolved_comments), todo_comments=True)
1043
1044 @LoginRequired()
1045 @NotAnonymous()
1046 @HasRepoPermissionAnyDecorator(
1047 'repository.read', 'repository.write', 'repository.admin')
942 @CSRFRequired()
1048 @CSRFRequired()
943 @view_config(
1049 @view_config(
944 route_name='pullrequest_create', request_method='POST',
1050 route_name='pullrequest_create', request_method='POST',
945 renderer=None)
1051 renderer=None)
946 def pull_request_create(self):
1052 def pull_request_create(self):
947 _ = self.request.translate
1053 _ = self.request.translate
948 self.assure_not_empty_repo()
1054 self.assure_not_empty_repo()
949 self.load_default_context()
1055 self.load_default_context()
950
1056
951 controls = peppercorn.parse(self.request.POST.items())
1057 controls = peppercorn.parse(self.request.POST.items())
952
1058
953 try:
1059 try:
954 form = PullRequestForm(
1060 form = PullRequestForm(
955 self.request.translate, self.db_repo.repo_id)()
1061 self.request.translate, self.db_repo.repo_id)()
956 _form = form.to_python(controls)
1062 _form = form.to_python(controls)
957 except formencode.Invalid as errors:
1063 except formencode.Invalid as errors:
958 if errors.error_dict.get('revisions'):
1064 if errors.error_dict.get('revisions'):
959 msg = 'Revisions: %s' % errors.error_dict['revisions']
1065 msg = 'Revisions: %s' % errors.error_dict['revisions']
960 elif errors.error_dict.get('pullrequest_title'):
1066 elif errors.error_dict.get('pullrequest_title'):
961 msg = errors.error_dict.get('pullrequest_title')
1067 msg = errors.error_dict.get('pullrequest_title')
962 else:
1068 else:
963 msg = _('Error creating pull request: {}').format(errors)
1069 msg = _('Error creating pull request: {}').format(errors)
964 log.exception(msg)
1070 log.exception(msg)
965 h.flash(msg, 'error')
1071 h.flash(msg, 'error')
966
1072
967 # would rather just go back to form ...
1073 # would rather just go back to form ...
968 raise HTTPFound(
1074 raise HTTPFound(
969 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1075 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
970
1076
971 source_repo = _form['source_repo']
1077 source_repo = _form['source_repo']
972 source_ref = _form['source_ref']
1078 source_ref = _form['source_ref']
973 target_repo = _form['target_repo']
1079 target_repo = _form['target_repo']
974 target_ref = _form['target_ref']
1080 target_ref = _form['target_ref']
975 commit_ids = _form['revisions'][::-1]
1081 commit_ids = _form['revisions'][::-1]
976 common_ancestor_id = _form['common_ancestor']
1082 common_ancestor_id = _form['common_ancestor']
977
1083
978 # find the ancestor for this pr
1084 # find the ancestor for this pr
979 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
1085 source_db_repo = Repository.get_by_repo_name(_form['source_repo'])
980 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
1086 target_db_repo = Repository.get_by_repo_name(_form['target_repo'])
981
1087
982 if not (source_db_repo or target_db_repo):
1088 if not (source_db_repo or target_db_repo):
983 h.flash(_('source_repo or target repo not found'), category='error')
1089 h.flash(_('source_repo or target repo not found'), category='error')
984 raise HTTPFound(
1090 raise HTTPFound(
985 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
1091 h.route_path('pullrequest_new', repo_name=self.db_repo_name))
986
1092
987 # re-check permissions again here
1093 # re-check permissions again here
988 # source_repo we must have read permissions
1094 # source_repo we must have read permissions
989
1095
990 source_perm = HasRepoPermissionAny(
1096 source_perm = HasRepoPermissionAny(
991 'repository.read', 'repository.write', 'repository.admin')(
1097 'repository.read', 'repository.write', 'repository.admin')(
992 source_db_repo.repo_name)
1098 source_db_repo.repo_name)
993 if not source_perm:
1099 if not source_perm:
994 msg = _('Not Enough permissions to source repo `{}`.'.format(
1100 msg = _('Not Enough permissions to source repo `{}`.'.format(
995 source_db_repo.repo_name))
1101 source_db_repo.repo_name))
996 h.flash(msg, category='error')
1102 h.flash(msg, category='error')
997 # copy the args back to redirect
1103 # copy the args back to redirect
998 org_query = self.request.GET.mixed()
1104 org_query = self.request.GET.mixed()
999 raise HTTPFound(
1105 raise HTTPFound(
1000 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1106 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1001 _query=org_query))
1107 _query=org_query))
1002
1108
1003 # target repo we must have read permissions, and also later on
1109 # target repo we must have read permissions, and also later on
1004 # we want to check branch permissions here
1110 # we want to check branch permissions here
1005 target_perm = HasRepoPermissionAny(
1111 target_perm = HasRepoPermissionAny(
1006 'repository.read', 'repository.write', 'repository.admin')(
1112 'repository.read', 'repository.write', 'repository.admin')(
1007 target_db_repo.repo_name)
1113 target_db_repo.repo_name)
1008 if not target_perm:
1114 if not target_perm:
1009 msg = _('Not Enough permissions to target repo `{}`.'.format(
1115 msg = _('Not Enough permissions to target repo `{}`.'.format(
1010 target_db_repo.repo_name))
1116 target_db_repo.repo_name))
1011 h.flash(msg, category='error')
1117 h.flash(msg, category='error')
1012 # copy the args back to redirect
1118 # copy the args back to redirect
1013 org_query = self.request.GET.mixed()
1119 org_query = self.request.GET.mixed()
1014 raise HTTPFound(
1120 raise HTTPFound(
1015 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1121 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1016 _query=org_query))
1122 _query=org_query))
1017
1123
1018 source_scm = source_db_repo.scm_instance()
1124 source_scm = source_db_repo.scm_instance()
1019 target_scm = target_db_repo.scm_instance()
1125 target_scm = target_db_repo.scm_instance()
1020
1126
1021 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1127 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1022 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1128 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1023
1129
1024 ancestor = source_scm.get_common_ancestor(
1130 ancestor = source_scm.get_common_ancestor(
1025 source_commit.raw_id, target_commit.raw_id, target_scm)
1131 source_commit.raw_id, target_commit.raw_id, target_scm)
1026
1132
1027 # recalculate target ref based on ancestor
1133 # recalculate target ref based on ancestor
1028 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1134 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1029 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1135 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1030
1136
1031 get_default_reviewers_data, validate_default_reviewers = \
1137 get_default_reviewers_data, validate_default_reviewers = \
1032 PullRequestModel().get_reviewer_functions()
1138 PullRequestModel().get_reviewer_functions()
1033
1139
1034 # recalculate reviewers logic, to make sure we can validate this
1140 # recalculate reviewers logic, to make sure we can validate this
1035 reviewer_rules = get_default_reviewers_data(
1141 reviewer_rules = get_default_reviewers_data(
1036 self._rhodecode_db_user, source_db_repo,
1142 self._rhodecode_db_user, source_db_repo,
1037 source_commit, target_db_repo, target_commit)
1143 source_commit, target_db_repo, target_commit)
1038
1144
1039 given_reviewers = _form['review_members']
1145 given_reviewers = _form['review_members']
1040 reviewers = validate_default_reviewers(
1146 reviewers = validate_default_reviewers(
1041 given_reviewers, reviewer_rules)
1147 given_reviewers, reviewer_rules)
1042
1148
1043 pullrequest_title = _form['pullrequest_title']
1149 pullrequest_title = _form['pullrequest_title']
1044 title_source_ref = source_ref.split(':', 2)[1]
1150 title_source_ref = source_ref.split(':', 2)[1]
1045 if not pullrequest_title:
1151 if not pullrequest_title:
1046 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1152 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1047 source=source_repo,
1153 source=source_repo,
1048 source_ref=title_source_ref,
1154 source_ref=title_source_ref,
1049 target=target_repo
1155 target=target_repo
1050 )
1156 )
1051
1157
1052 description = _form['pullrequest_desc']
1158 description = _form['pullrequest_desc']
1053 description_renderer = _form['description_renderer']
1159 description_renderer = _form['description_renderer']
1054
1160
1055 try:
1161 try:
1056 pull_request = PullRequestModel().create(
1162 pull_request = PullRequestModel().create(
1057 created_by=self._rhodecode_user.user_id,
1163 created_by=self._rhodecode_user.user_id,
1058 source_repo=source_repo,
1164 source_repo=source_repo,
1059 source_ref=source_ref,
1165 source_ref=source_ref,
1060 target_repo=target_repo,
1166 target_repo=target_repo,
1061 target_ref=target_ref,
1167 target_ref=target_ref,
1062 revisions=commit_ids,
1168 revisions=commit_ids,
1063 common_ancestor_id=common_ancestor_id,
1169 common_ancestor_id=common_ancestor_id,
1064 reviewers=reviewers,
1170 reviewers=reviewers,
1065 title=pullrequest_title,
1171 title=pullrequest_title,
1066 description=description,
1172 description=description,
1067 description_renderer=description_renderer,
1173 description_renderer=description_renderer,
1068 reviewer_data=reviewer_rules,
1174 reviewer_data=reviewer_rules,
1069 auth_user=self._rhodecode_user
1175 auth_user=self._rhodecode_user
1070 )
1176 )
1071 Session().commit()
1177 Session().commit()
1072
1178
1073 h.flash(_('Successfully opened new pull request'),
1179 h.flash(_('Successfully opened new pull request'),
1074 category='success')
1180 category='success')
1075 except Exception:
1181 except Exception:
1076 msg = _('Error occurred during creation of this pull request.')
1182 msg = _('Error occurred during creation of this pull request.')
1077 log.exception(msg)
1183 log.exception(msg)
1078 h.flash(msg, category='error')
1184 h.flash(msg, category='error')
1079
1185
1080 # copy the args back to redirect
1186 # copy the args back to redirect
1081 org_query = self.request.GET.mixed()
1187 org_query = self.request.GET.mixed()
1082 raise HTTPFound(
1188 raise HTTPFound(
1083 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1189 h.route_path('pullrequest_new', repo_name=self.db_repo_name,
1084 _query=org_query))
1190 _query=org_query))
1085
1191
1086 raise HTTPFound(
1192 raise HTTPFound(
1087 h.route_path('pullrequest_show', repo_name=target_repo,
1193 h.route_path('pullrequest_show', repo_name=target_repo,
1088 pull_request_id=pull_request.pull_request_id))
1194 pull_request_id=pull_request.pull_request_id))
1089
1195
1090 @LoginRequired()
1196 @LoginRequired()
1091 @NotAnonymous()
1197 @NotAnonymous()
1092 @HasRepoPermissionAnyDecorator(
1198 @HasRepoPermissionAnyDecorator(
1093 'repository.read', 'repository.write', 'repository.admin')
1199 'repository.read', 'repository.write', 'repository.admin')
1094 @CSRFRequired()
1200 @CSRFRequired()
1095 @view_config(
1201 @view_config(
1096 route_name='pullrequest_update', request_method='POST',
1202 route_name='pullrequest_update', request_method='POST',
1097 renderer='json_ext')
1203 renderer='json_ext')
1098 def pull_request_update(self):
1204 def pull_request_update(self):
1099 pull_request = PullRequest.get_or_404(
1205 pull_request = PullRequest.get_or_404(
1100 self.request.matchdict['pull_request_id'])
1206 self.request.matchdict['pull_request_id'])
1101 _ = self.request.translate
1207 _ = self.request.translate
1102
1208
1103 self.load_default_context()
1209 c = self.load_default_context()
1104 redirect_url = None
1210 redirect_url = None
1105
1211
1106 if pull_request.is_closed():
1212 if pull_request.is_closed():
1107 log.debug('update: forbidden because pull request is closed')
1213 log.debug('update: forbidden because pull request is closed')
1108 msg = _(u'Cannot update closed pull requests.')
1214 msg = _(u'Cannot update closed pull requests.')
1109 h.flash(msg, category='error')
1215 h.flash(msg, category='error')
1110 return {'response': True,
1216 return {'response': True,
1111 'redirect_url': redirect_url}
1217 'redirect_url': redirect_url}
1112
1218
1113 is_state_changing = pull_request.is_state_changing()
1219 is_state_changing = pull_request.is_state_changing()
1220 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
1221 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1114
1222
1115 # only owner or admin can update it
1223 # only owner or admin can update it
1116 allowed_to_update = PullRequestModel().check_user_update(
1224 allowed_to_update = PullRequestModel().check_user_update(
1117 pull_request, self._rhodecode_user)
1225 pull_request, self._rhodecode_user)
1118 if allowed_to_update:
1226 if allowed_to_update:
1119 controls = peppercorn.parse(self.request.POST.items())
1227 controls = peppercorn.parse(self.request.POST.items())
1120 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1228 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1121
1229
1122 if 'review_members' in controls:
1230 if 'review_members' in controls:
1123 self._update_reviewers(
1231 self._update_reviewers(
1124 pull_request, controls['review_members'],
1232 pull_request, controls['review_members'],
1125 pull_request.reviewer_data)
1233 pull_request.reviewer_data)
1126 elif str2bool(self.request.POST.get('update_commits', 'false')):
1234 elif str2bool(self.request.POST.get('update_commits', 'false')):
1127 if is_state_changing:
1235 if is_state_changing:
1128 log.debug('commits update: forbidden because pull request is in state %s',
1236 log.debug('commits update: forbidden because pull request is in state %s',
1129 pull_request.pull_request_state)
1237 pull_request.pull_request_state)
1130 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1238 msg = _(u'Cannot update pull requests commits in state other than `{}`. '
1131 u'Current state is: `{}`').format(
1239 u'Current state is: `{}`').format(
1132 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1240 PullRequest.STATE_CREATED, pull_request.pull_request_state)
1133 h.flash(msg, category='error')
1241 h.flash(msg, category='error')
1134 return {'response': True,
1242 return {'response': True,
1135 'redirect_url': redirect_url}
1243 'redirect_url': redirect_url}
1136
1244
1137 self._update_commits(pull_request)
1245 self._update_commits(c, pull_request)
1138 if force_refresh:
1246 if force_refresh:
1139 redirect_url = h.route_path(
1247 redirect_url = h.route_path(
1140 'pullrequest_show', repo_name=self.db_repo_name,
1248 'pullrequest_show', repo_name=self.db_repo_name,
1141 pull_request_id=pull_request.pull_request_id,
1249 pull_request_id=pull_request.pull_request_id,
1142 _query={"force_refresh": 1})
1250 _query={"force_refresh": 1})
1143 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1251 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1144 self._edit_pull_request(pull_request)
1252 self._edit_pull_request(pull_request)
1145 else:
1253 else:
1146 raise HTTPBadRequest()
1254 raise HTTPBadRequest()
1147
1255
1148 return {'response': True,
1256 return {'response': True,
1149 'redirect_url': redirect_url}
1257 'redirect_url': redirect_url}
1150 raise HTTPForbidden()
1258 raise HTTPForbidden()
1151
1259
1152 def _edit_pull_request(self, pull_request):
1260 def _edit_pull_request(self, pull_request):
1153 _ = self.request.translate
1261 _ = self.request.translate
1154
1262
1155 try:
1263 try:
1156 PullRequestModel().edit(
1264 PullRequestModel().edit(
1157 pull_request,
1265 pull_request,
1158 self.request.POST.get('title'),
1266 self.request.POST.get('title'),
1159 self.request.POST.get('description'),
1267 self.request.POST.get('description'),
1160 self.request.POST.get('description_renderer'),
1268 self.request.POST.get('description_renderer'),
1161 self._rhodecode_user)
1269 self._rhodecode_user)
1162 except ValueError:
1270 except ValueError:
1163 msg = _(u'Cannot update closed pull requests.')
1271 msg = _(u'Cannot update closed pull requests.')
1164 h.flash(msg, category='error')
1272 h.flash(msg, category='error')
1165 return
1273 return
1166 else:
1274 else:
1167 Session().commit()
1275 Session().commit()
1168
1276
1169 msg = _(u'Pull request title & description updated.')
1277 msg = _(u'Pull request title & description updated.')
1170 h.flash(msg, category='success')
1278 h.flash(msg, category='success')
1171 return
1279 return
1172
1280
1173 def _update_commits(self, pull_request):
1281 def _update_commits(self, c, pull_request):
1174 _ = self.request.translate
1282 _ = self.request.translate
1175
1283
1176 with pull_request.set_state(PullRequest.STATE_UPDATING):
1284 with pull_request.set_state(PullRequest.STATE_UPDATING):
1177 resp = PullRequestModel().update_commits(
1285 resp = PullRequestModel().update_commits(
1178 pull_request, self._rhodecode_db_user)
1286 pull_request, self._rhodecode_db_user)
1179
1287
1180 if resp.executed:
1288 if resp.executed:
1181
1289
1182 if resp.target_changed and resp.source_changed:
1290 if resp.target_changed and resp.source_changed:
1183 changed = 'target and source repositories'
1291 changed = 'target and source repositories'
1184 elif resp.target_changed and not resp.source_changed:
1292 elif resp.target_changed and not resp.source_changed:
1185 changed = 'target repository'
1293 changed = 'target repository'
1186 elif not resp.target_changed and resp.source_changed:
1294 elif not resp.target_changed and resp.source_changed:
1187 changed = 'source repository'
1295 changed = 'source repository'
1188 else:
1296 else:
1189 changed = 'nothing'
1297 changed = 'nothing'
1190
1298
1191 msg = _(u'Pull request updated to "{source_commit_id}" with '
1299 msg = _(u'Pull request updated to "{source_commit_id}" with '
1192 u'{count_added} added, {count_removed} removed commits. '
1300 u'{count_added} added, {count_removed} removed commits. '
1193 u'Source of changes: {change_source}')
1301 u'Source of changes: {change_source}')
1194 msg = msg.format(
1302 msg = msg.format(
1195 source_commit_id=pull_request.source_ref_parts.commit_id,
1303 source_commit_id=pull_request.source_ref_parts.commit_id,
1196 count_added=len(resp.changes.added),
1304 count_added=len(resp.changes.added),
1197 count_removed=len(resp.changes.removed),
1305 count_removed=len(resp.changes.removed),
1198 change_source=changed)
1306 change_source=changed)
1199 h.flash(msg, category='success')
1307 h.flash(msg, category='success')
1200
1308
1201 channel = '/repo${}$/pr/{}'.format(
1202 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1203 message = msg + (
1309 message = msg + (
1204 ' - <a onclick="window.location.reload()">'
1310 ' - <a onclick="window.location.reload()">'
1205 '<strong>{}</strong></a>'.format(_('Reload page')))
1311 '<strong>{}</strong></a>'.format(_('Reload page')))
1312
1313 message_obj = {
1314 'message': message,
1315 'level': 'success',
1316 'topic': '/notifications'
1317 }
1318
1206 channelstream.post_message(
1319 channelstream.post_message(
1207 channel, message, self._rhodecode_user.username,
1320 c.pr_broadcast_channel, message_obj, self._rhodecode_user.username,
1208 registry=self.request.registry)
1321 registry=self.request.registry)
1209 else:
1322 else:
1210 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1323 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1211 warning_reasons = [
1324 warning_reasons = [
1212 UpdateFailureReason.NO_CHANGE,
1325 UpdateFailureReason.NO_CHANGE,
1213 UpdateFailureReason.WRONG_REF_TYPE,
1326 UpdateFailureReason.WRONG_REF_TYPE,
1214 ]
1327 ]
1215 category = 'warning' if resp.reason in warning_reasons else 'error'
1328 category = 'warning' if resp.reason in warning_reasons else 'error'
1216 h.flash(msg, category=category)
1329 h.flash(msg, category=category)
1217
1330
1218 @LoginRequired()
1331 @LoginRequired()
1219 @NotAnonymous()
1332 @NotAnonymous()
1220 @HasRepoPermissionAnyDecorator(
1333 @HasRepoPermissionAnyDecorator(
1221 'repository.read', 'repository.write', 'repository.admin')
1334 'repository.read', 'repository.write', 'repository.admin')
1222 @CSRFRequired()
1335 @CSRFRequired()
1223 @view_config(
1336 @view_config(
1224 route_name='pullrequest_merge', request_method='POST',
1337 route_name='pullrequest_merge', request_method='POST',
1225 renderer='json_ext')
1338 renderer='json_ext')
1226 def pull_request_merge(self):
1339 def pull_request_merge(self):
1227 """
1340 """
1228 Merge will perform a server-side merge of the specified
1341 Merge will perform a server-side merge of the specified
1229 pull request, if the pull request is approved and mergeable.
1342 pull request, if the pull request is approved and mergeable.
1230 After successful merging, the pull request is automatically
1343 After successful merging, the pull request is automatically
1231 closed, with a relevant comment.
1344 closed, with a relevant comment.
1232 """
1345 """
1233 pull_request = PullRequest.get_or_404(
1346 pull_request = PullRequest.get_or_404(
1234 self.request.matchdict['pull_request_id'])
1347 self.request.matchdict['pull_request_id'])
1235 _ = self.request.translate
1348 _ = self.request.translate
1236
1349
1237 if pull_request.is_state_changing():
1350 if pull_request.is_state_changing():
1238 log.debug('show: forbidden because pull request is in state %s',
1351 log.debug('show: forbidden because pull request is in state %s',
1239 pull_request.pull_request_state)
1352 pull_request.pull_request_state)
1240 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1353 msg = _(u'Cannot merge pull requests in state other than `{}`. '
1241 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1354 u'Current state is: `{}`').format(PullRequest.STATE_CREATED,
1242 pull_request.pull_request_state)
1355 pull_request.pull_request_state)
1243 h.flash(msg, category='error')
1356 h.flash(msg, category='error')
1244 raise HTTPFound(
1357 raise HTTPFound(
1245 h.route_path('pullrequest_show',
1358 h.route_path('pullrequest_show',
1246 repo_name=pull_request.target_repo.repo_name,
1359 repo_name=pull_request.target_repo.repo_name,
1247 pull_request_id=pull_request.pull_request_id))
1360 pull_request_id=pull_request.pull_request_id))
1248
1361
1249 self.load_default_context()
1362 self.load_default_context()
1250
1363
1251 with pull_request.set_state(PullRequest.STATE_UPDATING):
1364 with pull_request.set_state(PullRequest.STATE_UPDATING):
1252 check = MergeCheck.validate(
1365 check = MergeCheck.validate(
1253 pull_request, auth_user=self._rhodecode_user,
1366 pull_request, auth_user=self._rhodecode_user,
1254 translator=self.request.translate)
1367 translator=self.request.translate)
1255 merge_possible = not check.failed
1368 merge_possible = not check.failed
1256
1369
1257 for err_type, error_msg in check.errors:
1370 for err_type, error_msg in check.errors:
1258 h.flash(error_msg, category=err_type)
1371 h.flash(error_msg, category=err_type)
1259
1372
1260 if merge_possible:
1373 if merge_possible:
1261 log.debug("Pre-conditions checked, trying to merge.")
1374 log.debug("Pre-conditions checked, trying to merge.")
1262 extras = vcs_operation_context(
1375 extras = vcs_operation_context(
1263 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1376 self.request.environ, repo_name=pull_request.target_repo.repo_name,
1264 username=self._rhodecode_db_user.username, action='push',
1377 username=self._rhodecode_db_user.username, action='push',
1265 scm=pull_request.target_repo.repo_type)
1378 scm=pull_request.target_repo.repo_type)
1266 with pull_request.set_state(PullRequest.STATE_UPDATING):
1379 with pull_request.set_state(PullRequest.STATE_UPDATING):
1267 self._merge_pull_request(
1380 self._merge_pull_request(
1268 pull_request, self._rhodecode_db_user, extras)
1381 pull_request, self._rhodecode_db_user, extras)
1269 else:
1382 else:
1270 log.debug("Pre-conditions failed, NOT merging.")
1383 log.debug("Pre-conditions failed, NOT merging.")
1271
1384
1272 raise HTTPFound(
1385 raise HTTPFound(
1273 h.route_path('pullrequest_show',
1386 h.route_path('pullrequest_show',
1274 repo_name=pull_request.target_repo.repo_name,
1387 repo_name=pull_request.target_repo.repo_name,
1275 pull_request_id=pull_request.pull_request_id))
1388 pull_request_id=pull_request.pull_request_id))
1276
1389
1277 def _merge_pull_request(self, pull_request, user, extras):
1390 def _merge_pull_request(self, pull_request, user, extras):
1278 _ = self.request.translate
1391 _ = self.request.translate
1279 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1392 merge_resp = PullRequestModel().merge_repo(pull_request, user, extras=extras)
1280
1393
1281 if merge_resp.executed:
1394 if merge_resp.executed:
1282 log.debug("The merge was successful, closing the pull request.")
1395 log.debug("The merge was successful, closing the pull request.")
1283 PullRequestModel().close_pull_request(
1396 PullRequestModel().close_pull_request(
1284 pull_request.pull_request_id, user)
1397 pull_request.pull_request_id, user)
1285 Session().commit()
1398 Session().commit()
1286 msg = _('Pull request was successfully merged and closed.')
1399 msg = _('Pull request was successfully merged and closed.')
1287 h.flash(msg, category='success')
1400 h.flash(msg, category='success')
1288 else:
1401 else:
1289 log.debug(
1402 log.debug(
1290 "The merge was not successful. Merge response: %s", merge_resp)
1403 "The merge was not successful. Merge response: %s", merge_resp)
1291 msg = merge_resp.merge_status_message
1404 msg = merge_resp.merge_status_message
1292 h.flash(msg, category='error')
1405 h.flash(msg, category='error')
1293
1406
1294 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1407 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1295 _ = self.request.translate
1408 _ = self.request.translate
1296
1409
1297 get_default_reviewers_data, validate_default_reviewers = \
1410 get_default_reviewers_data, validate_default_reviewers = \
1298 PullRequestModel().get_reviewer_functions()
1411 PullRequestModel().get_reviewer_functions()
1299
1412
1300 try:
1413 try:
1301 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1414 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1302 except ValueError as e:
1415 except ValueError as e:
1303 log.error('Reviewers Validation: {}'.format(e))
1416 log.error('Reviewers Validation: {}'.format(e))
1304 h.flash(e, category='error')
1417 h.flash(e, category='error')
1305 return
1418 return
1306
1419
1307 old_calculated_status = pull_request.calculated_review_status()
1420 old_calculated_status = pull_request.calculated_review_status()
1308 PullRequestModel().update_reviewers(
1421 PullRequestModel().update_reviewers(
1309 pull_request, reviewers, self._rhodecode_user)
1422 pull_request, reviewers, self._rhodecode_user)
1310 h.flash(_('Pull request reviewers updated.'), category='success')
1423 h.flash(_('Pull request reviewers updated.'), category='success')
1311 Session().commit()
1424 Session().commit()
1312
1425
1313 # trigger status changed if change in reviewers changes the status
1426 # trigger status changed if change in reviewers changes the status
1314 calculated_status = pull_request.calculated_review_status()
1427 calculated_status = pull_request.calculated_review_status()
1315 if old_calculated_status != calculated_status:
1428 if old_calculated_status != calculated_status:
1316 PullRequestModel().trigger_pull_request_hook(
1429 PullRequestModel().trigger_pull_request_hook(
1317 pull_request, self._rhodecode_user, 'review_status_change',
1430 pull_request, self._rhodecode_user, 'review_status_change',
1318 data={'status': calculated_status})
1431 data={'status': calculated_status})
1319
1432
1320 @LoginRequired()
1433 @LoginRequired()
1321 @NotAnonymous()
1434 @NotAnonymous()
1322 @HasRepoPermissionAnyDecorator(
1435 @HasRepoPermissionAnyDecorator(
1323 'repository.read', 'repository.write', 'repository.admin')
1436 'repository.read', 'repository.write', 'repository.admin')
1324 @CSRFRequired()
1437 @CSRFRequired()
1325 @view_config(
1438 @view_config(
1326 route_name='pullrequest_delete', request_method='POST',
1439 route_name='pullrequest_delete', request_method='POST',
1327 renderer='json_ext')
1440 renderer='json_ext')
1328 def pull_request_delete(self):
1441 def pull_request_delete(self):
1329 _ = self.request.translate
1442 _ = self.request.translate
1330
1443
1331 pull_request = PullRequest.get_or_404(
1444 pull_request = PullRequest.get_or_404(
1332 self.request.matchdict['pull_request_id'])
1445 self.request.matchdict['pull_request_id'])
1333 self.load_default_context()
1446 self.load_default_context()
1334
1447
1335 pr_closed = pull_request.is_closed()
1448 pr_closed = pull_request.is_closed()
1336 allowed_to_delete = PullRequestModel().check_user_delete(
1449 allowed_to_delete = PullRequestModel().check_user_delete(
1337 pull_request, self._rhodecode_user) and not pr_closed
1450 pull_request, self._rhodecode_user) and not pr_closed
1338
1451
1339 # only owner can delete it !
1452 # only owner can delete it !
1340 if allowed_to_delete:
1453 if allowed_to_delete:
1341 PullRequestModel().delete(pull_request, self._rhodecode_user)
1454 PullRequestModel().delete(pull_request, self._rhodecode_user)
1342 Session().commit()
1455 Session().commit()
1343 h.flash(_('Successfully deleted pull request'),
1456 h.flash(_('Successfully deleted pull request'),
1344 category='success')
1457 category='success')
1345 raise HTTPFound(h.route_path('pullrequest_show_all',
1458 raise HTTPFound(h.route_path('pullrequest_show_all',
1346 repo_name=self.db_repo_name))
1459 repo_name=self.db_repo_name))
1347
1460
1348 log.warning('user %s tried to delete pull request without access',
1461 log.warning('user %s tried to delete pull request without access',
1349 self._rhodecode_user)
1462 self._rhodecode_user)
1350 raise HTTPNotFound()
1463 raise HTTPNotFound()
1351
1464
1352 @LoginRequired()
1465 @LoginRequired()
1353 @NotAnonymous()
1466 @NotAnonymous()
1354 @HasRepoPermissionAnyDecorator(
1467 @HasRepoPermissionAnyDecorator(
1355 'repository.read', 'repository.write', 'repository.admin')
1468 'repository.read', 'repository.write', 'repository.admin')
1356 @CSRFRequired()
1469 @CSRFRequired()
1357 @view_config(
1470 @view_config(
1358 route_name='pullrequest_comment_create', request_method='POST',
1471 route_name='pullrequest_comment_create', request_method='POST',
1359 renderer='json_ext')
1472 renderer='json_ext')
1360 def pull_request_comment_create(self):
1473 def pull_request_comment_create(self):
1361 _ = self.request.translate
1474 _ = self.request.translate
1362
1475
1363 pull_request = PullRequest.get_or_404(
1476 pull_request = PullRequest.get_or_404(
1364 self.request.matchdict['pull_request_id'])
1477 self.request.matchdict['pull_request_id'])
1365 pull_request_id = pull_request.pull_request_id
1478 pull_request_id = pull_request.pull_request_id
1366
1479
1367 if pull_request.is_closed():
1480 if pull_request.is_closed():
1368 log.debug('comment: forbidden because pull request is closed')
1481 log.debug('comment: forbidden because pull request is closed')
1369 raise HTTPForbidden()
1482 raise HTTPForbidden()
1370
1483
1371 allowed_to_comment = PullRequestModel().check_user_comment(
1484 allowed_to_comment = PullRequestModel().check_user_comment(
1372 pull_request, self._rhodecode_user)
1485 pull_request, self._rhodecode_user)
1373 if not allowed_to_comment:
1486 if not allowed_to_comment:
1374 log.debug(
1487 log.debug(
1375 'comment: forbidden because pull request is from forbidden repo')
1488 'comment: forbidden because pull request is from forbidden repo')
1376 raise HTTPForbidden()
1489 raise HTTPForbidden()
1377
1490
1378 c = self.load_default_context()
1491 c = self.load_default_context()
1379
1492
1380 status = self.request.POST.get('changeset_status', None)
1493 status = self.request.POST.get('changeset_status', None)
1381 text = self.request.POST.get('text')
1494 text = self.request.POST.get('text')
1382 comment_type = self.request.POST.get('comment_type')
1495 comment_type = self.request.POST.get('comment_type')
1383 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1496 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
1384 close_pull_request = self.request.POST.get('close_pull_request')
1497 close_pull_request = self.request.POST.get('close_pull_request')
1385
1498
1386 # the logic here should work like following, if we submit close
1499 # the logic here should work like following, if we submit close
1387 # pr comment, use `close_pull_request_with_comment` function
1500 # pr comment, use `close_pull_request_with_comment` function
1388 # else handle regular comment logic
1501 # else handle regular comment logic
1389
1502
1390 if close_pull_request:
1503 if close_pull_request:
1391 # only owner or admin or person with write permissions
1504 # only owner or admin or person with write permissions
1392 allowed_to_close = PullRequestModel().check_user_update(
1505 allowed_to_close = PullRequestModel().check_user_update(
1393 pull_request, self._rhodecode_user)
1506 pull_request, self._rhodecode_user)
1394 if not allowed_to_close:
1507 if not allowed_to_close:
1395 log.debug('comment: forbidden because not allowed to close '
1508 log.debug('comment: forbidden because not allowed to close '
1396 'pull request %s', pull_request_id)
1509 'pull request %s', pull_request_id)
1397 raise HTTPForbidden()
1510 raise HTTPForbidden()
1398
1511
1399 # This also triggers `review_status_change`
1512 # This also triggers `review_status_change`
1400 comment, status = PullRequestModel().close_pull_request_with_comment(
1513 comment, status = PullRequestModel().close_pull_request_with_comment(
1401 pull_request, self._rhodecode_user, self.db_repo, message=text,
1514 pull_request, self._rhodecode_user, self.db_repo, message=text,
1402 auth_user=self._rhodecode_user)
1515 auth_user=self._rhodecode_user)
1403 Session().flush()
1516 Session().flush()
1404
1517
1405 PullRequestModel().trigger_pull_request_hook(
1518 PullRequestModel().trigger_pull_request_hook(
1406 pull_request, self._rhodecode_user, 'comment',
1519 pull_request, self._rhodecode_user, 'comment',
1407 data={'comment': comment})
1520 data={'comment': comment})
1408
1521
1409 else:
1522 else:
1410 # regular comment case, could be inline, or one with status.
1523 # regular comment case, could be inline, or one with status.
1411 # for that one we check also permissions
1524 # for that one we check also permissions
1412
1525
1413 allowed_to_change_status = PullRequestModel().check_user_change_status(
1526 allowed_to_change_status = PullRequestModel().check_user_change_status(
1414 pull_request, self._rhodecode_user)
1527 pull_request, self._rhodecode_user)
1415
1528
1416 if status and allowed_to_change_status:
1529 if status and allowed_to_change_status:
1417 message = (_('Status change %(transition_icon)s %(status)s')
1530 message = (_('Status change %(transition_icon)s %(status)s')
1418 % {'transition_icon': '>',
1531 % {'transition_icon': '>',
1419 'status': ChangesetStatus.get_status_lbl(status)})
1532 'status': ChangesetStatus.get_status_lbl(status)})
1420 text = text or message
1533 text = text or message
1421
1534
1422 comment = CommentsModel().create(
1535 comment = CommentsModel().create(
1423 text=text,
1536 text=text,
1424 repo=self.db_repo.repo_id,
1537 repo=self.db_repo.repo_id,
1425 user=self._rhodecode_user.user_id,
1538 user=self._rhodecode_user.user_id,
1426 pull_request=pull_request,
1539 pull_request=pull_request,
1427 f_path=self.request.POST.get('f_path'),
1540 f_path=self.request.POST.get('f_path'),
1428 line_no=self.request.POST.get('line'),
1541 line_no=self.request.POST.get('line'),
1429 status_change=(ChangesetStatus.get_status_lbl(status)
1542 status_change=(ChangesetStatus.get_status_lbl(status)
1430 if status and allowed_to_change_status else None),
1543 if status and allowed_to_change_status else None),
1431 status_change_type=(status
1544 status_change_type=(status
1432 if status and allowed_to_change_status else None),
1545 if status and allowed_to_change_status else None),
1433 comment_type=comment_type,
1546 comment_type=comment_type,
1434 resolves_comment_id=resolves_comment_id,
1547 resolves_comment_id=resolves_comment_id,
1435 auth_user=self._rhodecode_user
1548 auth_user=self._rhodecode_user
1436 )
1549 )
1437
1550
1438 if allowed_to_change_status:
1551 if allowed_to_change_status:
1439 # calculate old status before we change it
1552 # calculate old status before we change it
1440 old_calculated_status = pull_request.calculated_review_status()
1553 old_calculated_status = pull_request.calculated_review_status()
1441
1554
1442 # get status if set !
1555 # get status if set !
1443 if status:
1556 if status:
1444 ChangesetStatusModel().set_status(
1557 ChangesetStatusModel().set_status(
1445 self.db_repo.repo_id,
1558 self.db_repo.repo_id,
1446 status,
1559 status,
1447 self._rhodecode_user.user_id,
1560 self._rhodecode_user.user_id,
1448 comment,
1561 comment,
1449 pull_request=pull_request
1562 pull_request=pull_request
1450 )
1563 )
1451
1564
1452 Session().flush()
1565 Session().flush()
1453 # this is somehow required to get access to some relationship
1566 # this is somehow required to get access to some relationship
1454 # loaded on comment
1567 # loaded on comment
1455 Session().refresh(comment)
1568 Session().refresh(comment)
1456
1569
1457 PullRequestModel().trigger_pull_request_hook(
1570 PullRequestModel().trigger_pull_request_hook(
1458 pull_request, self._rhodecode_user, 'comment',
1571 pull_request, self._rhodecode_user, 'comment',
1459 data={'comment': comment})
1572 data={'comment': comment})
1460
1573
1461 # we now calculate the status of pull request, and based on that
1574 # we now calculate the status of pull request, and based on that
1462 # calculation we set the commits status
1575 # calculation we set the commits status
1463 calculated_status = pull_request.calculated_review_status()
1576 calculated_status = pull_request.calculated_review_status()
1464 if old_calculated_status != calculated_status:
1577 if old_calculated_status != calculated_status:
1465 PullRequestModel().trigger_pull_request_hook(
1578 PullRequestModel().trigger_pull_request_hook(
1466 pull_request, self._rhodecode_user, 'review_status_change',
1579 pull_request, self._rhodecode_user, 'review_status_change',
1467 data={'status': calculated_status})
1580 data={'status': calculated_status})
1468
1581
1469 Session().commit()
1582 Session().commit()
1470
1583
1471 data = {
1584 data = {
1472 'target_id': h.safeid(h.safe_unicode(
1585 'target_id': h.safeid(h.safe_unicode(
1473 self.request.POST.get('f_path'))),
1586 self.request.POST.get('f_path'))),
1474 }
1587 }
1475 if comment:
1588 if comment:
1476 c.co = comment
1589 c.co = comment
1590 c.at_version_num = None
1477 rendered_comment = render(
1591 rendered_comment = render(
1478 'rhodecode:templates/changeset/changeset_comment_block.mako',
1592 'rhodecode:templates/changeset/changeset_comment_block.mako',
1479 self._get_template_context(c), self.request)
1593 self._get_template_context(c), self.request)
1480
1594
1481 data.update(comment.get_dict())
1595 data.update(comment.get_dict())
1482 data.update({'rendered_text': rendered_comment})
1596 data.update({'rendered_text': rendered_comment})
1483
1597
1484 return data
1598 return data
1485
1599
1486 @LoginRequired()
1600 @LoginRequired()
1487 @NotAnonymous()
1601 @NotAnonymous()
1488 @HasRepoPermissionAnyDecorator(
1602 @HasRepoPermissionAnyDecorator(
1489 'repository.read', 'repository.write', 'repository.admin')
1603 'repository.read', 'repository.write', 'repository.admin')
1490 @CSRFRequired()
1604 @CSRFRequired()
1491 @view_config(
1605 @view_config(
1492 route_name='pullrequest_comment_delete', request_method='POST',
1606 route_name='pullrequest_comment_delete', request_method='POST',
1493 renderer='json_ext')
1607 renderer='json_ext')
1494 def pull_request_comment_delete(self):
1608 def pull_request_comment_delete(self):
1495 pull_request = PullRequest.get_or_404(
1609 pull_request = PullRequest.get_or_404(
1496 self.request.matchdict['pull_request_id'])
1610 self.request.matchdict['pull_request_id'])
1497
1611
1498 comment = ChangesetComment.get_or_404(
1612 comment = ChangesetComment.get_or_404(
1499 self.request.matchdict['comment_id'])
1613 self.request.matchdict['comment_id'])
1500 comment_id = comment.comment_id
1614 comment_id = comment.comment_id
1501
1615
1502 if comment.immutable:
1616 if comment.immutable:
1503 # don't allow deleting comments that are immutable
1617 # don't allow deleting comments that are immutable
1504 raise HTTPForbidden()
1618 raise HTTPForbidden()
1505
1619
1506 if pull_request.is_closed():
1620 if pull_request.is_closed():
1507 log.debug('comment: forbidden because pull request is closed')
1621 log.debug('comment: forbidden because pull request is closed')
1508 raise HTTPForbidden()
1622 raise HTTPForbidden()
1509
1623
1510 if not comment:
1624 if not comment:
1511 log.debug('Comment with id:%s not found, skipping', comment_id)
1625 log.debug('Comment with id:%s not found, skipping', comment_id)
1512 # comment already deleted in another call probably
1626 # comment already deleted in another call probably
1513 return True
1627 return True
1514
1628
1515 if comment.pull_request.is_closed():
1629 if comment.pull_request.is_closed():
1516 # don't allow deleting comments on closed pull request
1630 # don't allow deleting comments on closed pull request
1517 raise HTTPForbidden()
1631 raise HTTPForbidden()
1518
1632
1519 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1633 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1520 super_admin = h.HasPermissionAny('hg.admin')()
1634 super_admin = h.HasPermissionAny('hg.admin')()
1521 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1635 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1522 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1636 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1523 comment_repo_admin = is_repo_admin and is_repo_comment
1637 comment_repo_admin = is_repo_admin and is_repo_comment
1524
1638
1525 if super_admin or comment_owner or comment_repo_admin:
1639 if super_admin or comment_owner or comment_repo_admin:
1526 old_calculated_status = comment.pull_request.calculated_review_status()
1640 old_calculated_status = comment.pull_request.calculated_review_status()
1527 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1641 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
1528 Session().commit()
1642 Session().commit()
1529 calculated_status = comment.pull_request.calculated_review_status()
1643 calculated_status = comment.pull_request.calculated_review_status()
1530 if old_calculated_status != calculated_status:
1644 if old_calculated_status != calculated_status:
1531 PullRequestModel().trigger_pull_request_hook(
1645 PullRequestModel().trigger_pull_request_hook(
1532 comment.pull_request, self._rhodecode_user, 'review_status_change',
1646 comment.pull_request, self._rhodecode_user, 'review_status_change',
1533 data={'status': calculated_status})
1647 data={'status': calculated_status})
1534 return True
1648 return True
1535 else:
1649 else:
1536 log.warning('No permissions for user %s to delete comment_id: %s',
1650 log.warning('No permissions for user %s to delete comment_id: %s',
1537 self._rhodecode_db_user, comment_id)
1651 self._rhodecode_db_user, comment_id)
1538 raise HTTPNotFound()
1652 raise HTTPNotFound()
1539
1653
1540 @LoginRequired()
1654 @LoginRequired()
1541 @NotAnonymous()
1655 @NotAnonymous()
1542 @HasRepoPermissionAnyDecorator(
1656 @HasRepoPermissionAnyDecorator(
1543 'repository.read', 'repository.write', 'repository.admin')
1657 'repository.read', 'repository.write', 'repository.admin')
1544 @CSRFRequired()
1658 @CSRFRequired()
1545 @view_config(
1659 @view_config(
1546 route_name='pullrequest_comment_edit', request_method='POST',
1660 route_name='pullrequest_comment_edit', request_method='POST',
1547 renderer='json_ext')
1661 renderer='json_ext')
1548 def pull_request_comment_edit(self):
1662 def pull_request_comment_edit(self):
1549 self.load_default_context()
1663 self.load_default_context()
1550
1664
1551 pull_request = PullRequest.get_or_404(
1665 pull_request = PullRequest.get_or_404(
1552 self.request.matchdict['pull_request_id']
1666 self.request.matchdict['pull_request_id']
1553 )
1667 )
1554 comment = ChangesetComment.get_or_404(
1668 comment = ChangesetComment.get_or_404(
1555 self.request.matchdict['comment_id']
1669 self.request.matchdict['comment_id']
1556 )
1670 )
1557 comment_id = comment.comment_id
1671 comment_id = comment.comment_id
1558
1672
1559 if comment.immutable:
1673 if comment.immutable:
1560 # don't allow deleting comments that are immutable
1674 # don't allow deleting comments that are immutable
1561 raise HTTPForbidden()
1675 raise HTTPForbidden()
1562
1676
1563 if pull_request.is_closed():
1677 if pull_request.is_closed():
1564 log.debug('comment: forbidden because pull request is closed')
1678 log.debug('comment: forbidden because pull request is closed')
1565 raise HTTPForbidden()
1679 raise HTTPForbidden()
1566
1680
1567 if not comment:
1681 if not comment:
1568 log.debug('Comment with id:%s not found, skipping', comment_id)
1682 log.debug('Comment with id:%s not found, skipping', comment_id)
1569 # comment already deleted in another call probably
1683 # comment already deleted in another call probably
1570 return True
1684 return True
1571
1685
1572 if comment.pull_request.is_closed():
1686 if comment.pull_request.is_closed():
1573 # don't allow deleting comments on closed pull request
1687 # don't allow deleting comments on closed pull request
1574 raise HTTPForbidden()
1688 raise HTTPForbidden()
1575
1689
1576 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1690 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1577 super_admin = h.HasPermissionAny('hg.admin')()
1691 super_admin = h.HasPermissionAny('hg.admin')()
1578 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1692 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1579 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1693 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1580 comment_repo_admin = is_repo_admin and is_repo_comment
1694 comment_repo_admin = is_repo_admin and is_repo_comment
1581
1695
1582 if super_admin or comment_owner or comment_repo_admin:
1696 if super_admin or comment_owner or comment_repo_admin:
1583 text = self.request.POST.get('text')
1697 text = self.request.POST.get('text')
1584 version = self.request.POST.get('version')
1698 version = self.request.POST.get('version')
1585 if text == comment.text:
1699 if text == comment.text:
1586 log.warning(
1700 log.warning(
1587 'Comment(PR): '
1701 'Comment(PR): '
1588 'Trying to create new version '
1702 'Trying to create new version '
1589 'with the same comment body {}'.format(
1703 'with the same comment body {}'.format(
1590 comment_id,
1704 comment_id,
1591 )
1705 )
1592 )
1706 )
1593 raise HTTPNotFound()
1707 raise HTTPNotFound()
1594
1708
1595 if version.isdigit():
1709 if version.isdigit():
1596 version = int(version)
1710 version = int(version)
1597 else:
1711 else:
1598 log.warning(
1712 log.warning(
1599 'Comment(PR): Wrong version type {} {} '
1713 'Comment(PR): Wrong version type {} {} '
1600 'for comment {}'.format(
1714 'for comment {}'.format(
1601 version,
1715 version,
1602 type(version),
1716 type(version),
1603 comment_id,
1717 comment_id,
1604 )
1718 )
1605 )
1719 )
1606 raise HTTPNotFound()
1720 raise HTTPNotFound()
1607
1721
1608 try:
1722 try:
1609 comment_history = CommentsModel().edit(
1723 comment_history = CommentsModel().edit(
1610 comment_id=comment_id,
1724 comment_id=comment_id,
1611 text=text,
1725 text=text,
1612 auth_user=self._rhodecode_user,
1726 auth_user=self._rhodecode_user,
1613 version=version,
1727 version=version,
1614 )
1728 )
1615 except CommentVersionMismatch:
1729 except CommentVersionMismatch:
1616 raise HTTPConflict()
1730 raise HTTPConflict()
1617
1731
1618 if not comment_history:
1732 if not comment_history:
1619 raise HTTPNotFound()
1733 raise HTTPNotFound()
1620
1734
1621 Session().commit()
1735 Session().commit()
1622
1736
1623 PullRequestModel().trigger_pull_request_hook(
1737 PullRequestModel().trigger_pull_request_hook(
1624 pull_request, self._rhodecode_user, 'comment_edit',
1738 pull_request, self._rhodecode_user, 'comment_edit',
1625 data={'comment': comment})
1739 data={'comment': comment})
1626
1740
1627 return {
1741 return {
1628 'comment_history_id': comment_history.comment_history_id,
1742 'comment_history_id': comment_history.comment_history_id,
1629 'comment_id': comment.comment_id,
1743 'comment_id': comment.comment_id,
1630 'comment_version': comment_history.version,
1744 'comment_version': comment_history.version,
1631 'comment_author_username': comment_history.author.username,
1745 'comment_author_username': comment_history.author.username,
1632 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1746 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1633 'comment_created_on': h.age_component(comment_history.created_on,
1747 'comment_created_on': h.age_component(comment_history.created_on,
1634 time_is_local=True),
1748 time_is_local=True),
1635 }
1749 }
1636 else:
1750 else:
1637 log.warning('No permissions for user %s to edit comment_id: %s',
1751 log.warning('No permissions for user %s to edit comment_id: %s',
1638 self._rhodecode_db_user, comment_id)
1752 self._rhodecode_db_user, comment_id)
1639 raise HTTPNotFound()
1753 raise HTTPNotFound()
@@ -1,2092 +1,2106 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-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 Helper functions
22 Helper functions
23
23
24 Consists of functions to typically be used within templates, but also
24 Consists of functions to typically be used within templates, but also
25 available to Controllers. This module is available to both as 'h'.
25 available to Controllers. This module is available to both as 'h'.
26 """
26 """
27 import base64
27 import base64
28
28
29 import os
29 import os
30 import random
30 import random
31 import hashlib
31 import hashlib
32 import StringIO
32 import StringIO
33 import textwrap
33 import textwrap
34 import urllib
34 import urllib
35 import math
35 import math
36 import logging
36 import logging
37 import re
37 import re
38 import time
38 import time
39 import string
39 import string
40 import hashlib
40 import hashlib
41 from collections import OrderedDict
41 from collections import OrderedDict
42
42
43 import pygments
43 import pygments
44 import itertools
44 import itertools
45 import fnmatch
45 import fnmatch
46 import bleach
46 import bleach
47
47
48 from pyramid import compat
48 from pyramid import compat
49 from datetime import datetime
49 from datetime import datetime
50 from functools import partial
50 from functools import partial
51 from pygments.formatters.html import HtmlFormatter
51 from pygments.formatters.html import HtmlFormatter
52 from pygments.lexers import (
52 from pygments.lexers import (
53 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
54
54
55 from pyramid.threadlocal import get_current_request
55 from pyramid.threadlocal import get_current_request
56 from tempita import looper
56 from tempita import looper
57 from webhelpers2.html import literal, HTML, escape
57 from webhelpers2.html import literal, HTML, escape
58 from webhelpers2.html._autolink import _auto_link_urls
58 from webhelpers2.html._autolink import _auto_link_urls
59 from webhelpers2.html.tools import (
59 from webhelpers2.html.tools import (
60 button_to, highlight, js_obfuscate, strip_links, strip_tags)
60 button_to, highlight, js_obfuscate, strip_links, strip_tags)
61
61
62 from webhelpers2.text import (
62 from webhelpers2.text import (
63 chop_at, collapse, convert_accented_entities,
63 chop_at, collapse, convert_accented_entities,
64 convert_misc_entities, lchop, plural, rchop, remove_formatting,
64 convert_misc_entities, lchop, plural, rchop, remove_formatting,
65 replace_whitespace, urlify, truncate, wrap_paragraphs)
65 replace_whitespace, urlify, truncate, wrap_paragraphs)
66 from webhelpers2.date import time_ago_in_words
66 from webhelpers2.date import time_ago_in_words
67
67
68 from webhelpers2.html.tags import (
68 from webhelpers2.html.tags import (
69 _input, NotGiven, _make_safe_id_component as safeid,
69 _input, NotGiven, _make_safe_id_component as safeid,
70 form as insecure_form,
70 form as insecure_form,
71 auto_discovery_link, checkbox, end_form, file,
71 auto_discovery_link, checkbox, end_form, file,
72 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
72 hidden, image, javascript_link, link_to, link_to_if, link_to_unless, ol,
73 select as raw_select, stylesheet_link, submit, text, password, textarea,
73 select as raw_select, stylesheet_link, submit, text, password, textarea,
74 ul, radio, Options)
74 ul, radio, Options)
75
75
76 from webhelpers2.number import format_byte_size
76 from webhelpers2.number import format_byte_size
77
77
78 from rhodecode.lib.action_parser import action_parser
78 from rhodecode.lib.action_parser import action_parser
79 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
79 from rhodecode.lib.pagination import Page, RepoPage, SqlPage
80 from rhodecode.lib.ext_json import json
80 from rhodecode.lib.ext_json import json
81 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
81 from rhodecode.lib.utils import repo_name_slug, get_custom_lexer
82 from rhodecode.lib.utils2 import (
82 from rhodecode.lib.utils2 import (
83 str2bool, safe_unicode, safe_str,
83 str2bool, safe_unicode, safe_str,
84 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
84 get_commit_safe, datetime_to_time, time_to_datetime, time_to_utcdatetime,
85 AttributeDict, safe_int, md5, md5_safe, get_host_info)
85 AttributeDict, safe_int, md5, md5_safe, get_host_info)
86 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
86 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
87 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
87 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
88 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
88 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
89 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
89 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
90 from rhodecode.lib.index.search_utils import get_matching_line_offsets
90 from rhodecode.lib.index.search_utils import get_matching_line_offsets
91 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
91 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
92 from rhodecode.model.changeset_status import ChangesetStatusModel
92 from rhodecode.model.changeset_status import ChangesetStatusModel
93 from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
93 from rhodecode.model.db import Permission, User, Repository, UserApiKeys, FileStore
94 from rhodecode.model.repo_group import RepoGroupModel
94 from rhodecode.model.repo_group import RepoGroupModel
95 from rhodecode.model.settings import IssueTrackerSettingsModel
95 from rhodecode.model.settings import IssueTrackerSettingsModel
96
96
97
97
98 log = logging.getLogger(__name__)
98 log = logging.getLogger(__name__)
99
99
100
100
101 DEFAULT_USER = User.DEFAULT_USER
101 DEFAULT_USER = User.DEFAULT_USER
102 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
102 DEFAULT_USER_EMAIL = User.DEFAULT_USER_EMAIL
103
103
104
104
105 def asset(path, ver=None, **kwargs):
105 def asset(path, ver=None, **kwargs):
106 """
106 """
107 Helper to generate a static asset file path for rhodecode assets
107 Helper to generate a static asset file path for rhodecode assets
108
108
109 eg. h.asset('images/image.png', ver='3923')
109 eg. h.asset('images/image.png', ver='3923')
110
110
111 :param path: path of asset
111 :param path: path of asset
112 :param ver: optional version query param to append as ?ver=
112 :param ver: optional version query param to append as ?ver=
113 """
113 """
114 request = get_current_request()
114 request = get_current_request()
115 query = {}
115 query = {}
116 query.update(kwargs)
116 query.update(kwargs)
117 if ver:
117 if ver:
118 query = {'ver': ver}
118 query = {'ver': ver}
119 return request.static_path(
119 return request.static_path(
120 'rhodecode:public/{}'.format(path), _query=query)
120 'rhodecode:public/{}'.format(path), _query=query)
121
121
122
122
123 default_html_escape_table = {
123 default_html_escape_table = {
124 ord('&'): u'&amp;',
124 ord('&'): u'&amp;',
125 ord('<'): u'&lt;',
125 ord('<'): u'&lt;',
126 ord('>'): u'&gt;',
126 ord('>'): u'&gt;',
127 ord('"'): u'&quot;',
127 ord('"'): u'&quot;',
128 ord("'"): u'&#39;',
128 ord("'"): u'&#39;',
129 }
129 }
130
130
131
131
132 def html_escape(text, html_escape_table=default_html_escape_table):
132 def html_escape(text, html_escape_table=default_html_escape_table):
133 """Produce entities within text."""
133 """Produce entities within text."""
134 return text.translate(html_escape_table)
134 return text.translate(html_escape_table)
135
135
136
136
137 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
137 def chop_at_smart(s, sub, inclusive=False, suffix_if_chopped=None):
138 """
138 """
139 Truncate string ``s`` at the first occurrence of ``sub``.
139 Truncate string ``s`` at the first occurrence of ``sub``.
140
140
141 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
141 If ``inclusive`` is true, truncate just after ``sub`` rather than at it.
142 """
142 """
143 suffix_if_chopped = suffix_if_chopped or ''
143 suffix_if_chopped = suffix_if_chopped or ''
144 pos = s.find(sub)
144 pos = s.find(sub)
145 if pos == -1:
145 if pos == -1:
146 return s
146 return s
147
147
148 if inclusive:
148 if inclusive:
149 pos += len(sub)
149 pos += len(sub)
150
150
151 chopped = s[:pos]
151 chopped = s[:pos]
152 left = s[pos:].strip()
152 left = s[pos:].strip()
153
153
154 if left and suffix_if_chopped:
154 if left and suffix_if_chopped:
155 chopped += suffix_if_chopped
155 chopped += suffix_if_chopped
156
156
157 return chopped
157 return chopped
158
158
159
159
160 def shorter(text, size=20, prefix=False):
160 def shorter(text, size=20, prefix=False):
161 postfix = '...'
161 postfix = '...'
162 if len(text) > size:
162 if len(text) > size:
163 if prefix:
163 if prefix:
164 # shorten in front
164 # shorten in front
165 return postfix + text[-(size - len(postfix)):]
165 return postfix + text[-(size - len(postfix)):]
166 else:
166 else:
167 return text[:size - len(postfix)] + postfix
167 return text[:size - len(postfix)] + postfix
168 return text
168 return text
169
169
170
170
171 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
171 def reset(name, value=None, id=NotGiven, type="reset", **attrs):
172 """
172 """
173 Reset button
173 Reset button
174 """
174 """
175 return _input(type, name, value, id, attrs)
175 return _input(type, name, value, id, attrs)
176
176
177
177
178 def select(name, selected_values, options, id=NotGiven, **attrs):
178 def select(name, selected_values, options, id=NotGiven, **attrs):
179
179
180 if isinstance(options, (list, tuple)):
180 if isinstance(options, (list, tuple)):
181 options_iter = options
181 options_iter = options
182 # Handle old value,label lists ... where value also can be value,label lists
182 # Handle old value,label lists ... where value also can be value,label lists
183 options = Options()
183 options = Options()
184 for opt in options_iter:
184 for opt in options_iter:
185 if isinstance(opt, tuple) and len(opt) == 2:
185 if isinstance(opt, tuple) and len(opt) == 2:
186 value, label = opt
186 value, label = opt
187 elif isinstance(opt, basestring):
187 elif isinstance(opt, basestring):
188 value = label = opt
188 value = label = opt
189 else:
189 else:
190 raise ValueError('invalid select option type %r' % type(opt))
190 raise ValueError('invalid select option type %r' % type(opt))
191
191
192 if isinstance(value, (list, tuple)):
192 if isinstance(value, (list, tuple)):
193 option_group = options.add_optgroup(label)
193 option_group = options.add_optgroup(label)
194 for opt2 in value:
194 for opt2 in value:
195 if isinstance(opt2, tuple) and len(opt2) == 2:
195 if isinstance(opt2, tuple) and len(opt2) == 2:
196 group_value, group_label = opt2
196 group_value, group_label = opt2
197 elif isinstance(opt2, basestring):
197 elif isinstance(opt2, basestring):
198 group_value = group_label = opt2
198 group_value = group_label = opt2
199 else:
199 else:
200 raise ValueError('invalid select option type %r' % type(opt2))
200 raise ValueError('invalid select option type %r' % type(opt2))
201
201
202 option_group.add_option(group_label, group_value)
202 option_group.add_option(group_label, group_value)
203 else:
203 else:
204 options.add_option(label, value)
204 options.add_option(label, value)
205
205
206 return raw_select(name, selected_values, options, id=id, **attrs)
206 return raw_select(name, selected_values, options, id=id, **attrs)
207
207
208
208
209 def branding(name, length=40):
209 def branding(name, length=40):
210 return truncate(name, length, indicator="")
210 return truncate(name, length, indicator="")
211
211
212
212
213 def FID(raw_id, path):
213 def FID(raw_id, path):
214 """
214 """
215 Creates a unique ID for filenode based on it's hash of path and commit
215 Creates a unique ID for filenode based on it's hash of path and commit
216 it's safe to use in urls
216 it's safe to use in urls
217
217
218 :param raw_id:
218 :param raw_id:
219 :param path:
219 :param path:
220 """
220 """
221
221
222 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
222 return 'c-%s-%s' % (short_id(raw_id), md5_safe(path)[:12])
223
223
224
224
225 class _GetError(object):
225 class _GetError(object):
226 """Get error from form_errors, and represent it as span wrapped error
226 """Get error from form_errors, and represent it as span wrapped error
227 message
227 message
228
228
229 :param field_name: field to fetch errors for
229 :param field_name: field to fetch errors for
230 :param form_errors: form errors dict
230 :param form_errors: form errors dict
231 """
231 """
232
232
233 def __call__(self, field_name, form_errors):
233 def __call__(self, field_name, form_errors):
234 tmpl = """<span class="error_msg">%s</span>"""
234 tmpl = """<span class="error_msg">%s</span>"""
235 if form_errors and field_name in form_errors:
235 if form_errors and field_name in form_errors:
236 return literal(tmpl % form_errors.get(field_name))
236 return literal(tmpl % form_errors.get(field_name))
237
237
238
238
239 get_error = _GetError()
239 get_error = _GetError()
240
240
241
241
242 class _ToolTip(object):
242 class _ToolTip(object):
243
243
244 def __call__(self, tooltip_title, trim_at=50):
244 def __call__(self, tooltip_title, trim_at=50):
245 """
245 """
246 Special function just to wrap our text into nice formatted
246 Special function just to wrap our text into nice formatted
247 autowrapped text
247 autowrapped text
248
248
249 :param tooltip_title:
249 :param tooltip_title:
250 """
250 """
251 tooltip_title = escape(tooltip_title)
251 tooltip_title = escape(tooltip_title)
252 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
252 tooltip_title = tooltip_title.replace('<', '&lt;').replace('>', '&gt;')
253 return tooltip_title
253 return tooltip_title
254
254
255
255
256 tooltip = _ToolTip()
256 tooltip = _ToolTip()
257
257
258 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
258 files_icon = u'<i class="file-breadcrumb-copy tooltip icon-clipboard clipboard-action" data-clipboard-text="{}" title="Copy file path"></i>'
259
259
260
260
261 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
261 def files_breadcrumbs(repo_name, repo_type, commit_id, file_path, landing_ref_name=None, at_ref=None,
262 limit_items=False, linkify_last_item=False, hide_last_item=False,
262 limit_items=False, linkify_last_item=False, hide_last_item=False,
263 copy_path_icon=True):
263 copy_path_icon=True):
264 if isinstance(file_path, str):
264 if isinstance(file_path, str):
265 file_path = safe_unicode(file_path)
265 file_path = safe_unicode(file_path)
266
266
267 if at_ref:
267 if at_ref:
268 route_qry = {'at': at_ref}
268 route_qry = {'at': at_ref}
269 default_landing_ref = at_ref or landing_ref_name or commit_id
269 default_landing_ref = at_ref or landing_ref_name or commit_id
270 else:
270 else:
271 route_qry = None
271 route_qry = None
272 default_landing_ref = commit_id
272 default_landing_ref = commit_id
273
273
274 # first segment is a `HOME` link to repo files root location
274 # first segment is a `HOME` link to repo files root location
275 root_name = literal(u'<i class="icon-home"></i>')
275 root_name = literal(u'<i class="icon-home"></i>')
276
276
277 url_segments = [
277 url_segments = [
278 link_to(
278 link_to(
279 root_name,
279 root_name,
280 repo_files_by_ref_url(
280 repo_files_by_ref_url(
281 repo_name,
281 repo_name,
282 repo_type,
282 repo_type,
283 f_path=None, # None here is a special case for SVN repos,
283 f_path=None, # None here is a special case for SVN repos,
284 # that won't prefix with a ref
284 # that won't prefix with a ref
285 ref_name=default_landing_ref,
285 ref_name=default_landing_ref,
286 commit_id=commit_id,
286 commit_id=commit_id,
287 query=route_qry
287 query=route_qry
288 )
288 )
289 )]
289 )]
290
290
291 path_segments = file_path.split('/')
291 path_segments = file_path.split('/')
292 last_cnt = len(path_segments) - 1
292 last_cnt = len(path_segments) - 1
293 for cnt, segment in enumerate(path_segments):
293 for cnt, segment in enumerate(path_segments):
294 if not segment:
294 if not segment:
295 continue
295 continue
296 segment_html = escape(segment)
296 segment_html = escape(segment)
297
297
298 last_item = cnt == last_cnt
298 last_item = cnt == last_cnt
299
299
300 if last_item and hide_last_item:
300 if last_item and hide_last_item:
301 # iterate over and hide last element
301 # iterate over and hide last element
302 continue
302 continue
303
303
304 if last_item and linkify_last_item is False:
304 if last_item and linkify_last_item is False:
305 # plain version
305 # plain version
306 url_segments.append(segment_html)
306 url_segments.append(segment_html)
307 else:
307 else:
308 url_segments.append(
308 url_segments.append(
309 link_to(
309 link_to(
310 segment_html,
310 segment_html,
311 repo_files_by_ref_url(
311 repo_files_by_ref_url(
312 repo_name,
312 repo_name,
313 repo_type,
313 repo_type,
314 f_path='/'.join(path_segments[:cnt + 1]),
314 f_path='/'.join(path_segments[:cnt + 1]),
315 ref_name=default_landing_ref,
315 ref_name=default_landing_ref,
316 commit_id=commit_id,
316 commit_id=commit_id,
317 query=route_qry
317 query=route_qry
318 ),
318 ),
319 ))
319 ))
320
320
321 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
321 limited_url_segments = url_segments[:1] + ['...'] + url_segments[-5:]
322 if limit_items and len(limited_url_segments) < len(url_segments):
322 if limit_items and len(limited_url_segments) < len(url_segments):
323 url_segments = limited_url_segments
323 url_segments = limited_url_segments
324
324
325 full_path = file_path
325 full_path = file_path
326 if copy_path_icon:
326 if copy_path_icon:
327 icon = files_icon.format(escape(full_path))
327 icon = files_icon.format(escape(full_path))
328 else:
328 else:
329 icon = ''
329 icon = ''
330
330
331 if file_path == '':
331 if file_path == '':
332 return root_name
332 return root_name
333 else:
333 else:
334 return literal(' / '.join(url_segments) + icon)
334 return literal(' / '.join(url_segments) + icon)
335
335
336
336
337 def files_url_data(request):
337 def files_url_data(request):
338 matchdict = request.matchdict
338 matchdict = request.matchdict
339
339
340 if 'f_path' not in matchdict:
340 if 'f_path' not in matchdict:
341 matchdict['f_path'] = ''
341 matchdict['f_path'] = ''
342
342
343 if 'commit_id' not in matchdict:
343 if 'commit_id' not in matchdict:
344 matchdict['commit_id'] = 'tip'
344 matchdict['commit_id'] = 'tip'
345
345
346 return json.dumps(matchdict)
346 return json.dumps(matchdict)
347
347
348
348
349 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
349 def repo_files_by_ref_url(db_repo_name, db_repo_type, f_path, ref_name, commit_id, query=None, ):
350 _is_svn = is_svn(db_repo_type)
350 _is_svn = is_svn(db_repo_type)
351 final_f_path = f_path
351 final_f_path = f_path
352
352
353 if _is_svn:
353 if _is_svn:
354 """
354 """
355 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
355 For SVN the ref_name cannot be used as a commit_id, it needs to be prefixed with
356 actually commit_id followed by the ref_name. This should be done only in case
356 actually commit_id followed by the ref_name. This should be done only in case
357 This is a initial landing url, without additional paths.
357 This is a initial landing url, without additional paths.
358
358
359 like: /1000/tags/1.0.0/?at=tags/1.0.0
359 like: /1000/tags/1.0.0/?at=tags/1.0.0
360 """
360 """
361
361
362 if ref_name and ref_name != 'tip':
362 if ref_name and ref_name != 'tip':
363 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
363 # NOTE(marcink): for svn the ref_name is actually the stored path, so we prefix it
364 # for SVN we only do this magic prefix if it's root, .eg landing revision
364 # for SVN we only do this magic prefix if it's root, .eg landing revision
365 # of files link. If we are in the tree we don't need this since we traverse the url
365 # of files link. If we are in the tree we don't need this since we traverse the url
366 # that has everything stored
366 # that has everything stored
367 if f_path in ['', '/']:
367 if f_path in ['', '/']:
368 final_f_path = '/'.join([ref_name, f_path])
368 final_f_path = '/'.join([ref_name, f_path])
369
369
370 # SVN always needs a commit_id explicitly, without a named REF
370 # SVN always needs a commit_id explicitly, without a named REF
371 default_commit_id = commit_id
371 default_commit_id = commit_id
372 else:
372 else:
373 """
373 """
374 For git and mercurial we construct a new URL using the names instead of commit_id
374 For git and mercurial we construct a new URL using the names instead of commit_id
375 like: /master/some_path?at=master
375 like: /master/some_path?at=master
376 """
376 """
377 # We currently do not support branches with slashes
377 # We currently do not support branches with slashes
378 if '/' in ref_name:
378 if '/' in ref_name:
379 default_commit_id = commit_id
379 default_commit_id = commit_id
380 else:
380 else:
381 default_commit_id = ref_name
381 default_commit_id = ref_name
382
382
383 # sometimes we pass f_path as None, to indicate explicit no prefix,
383 # sometimes we pass f_path as None, to indicate explicit no prefix,
384 # we translate it to string to not have None
384 # we translate it to string to not have None
385 final_f_path = final_f_path or ''
385 final_f_path = final_f_path or ''
386
386
387 files_url = route_path(
387 files_url = route_path(
388 'repo_files',
388 'repo_files',
389 repo_name=db_repo_name,
389 repo_name=db_repo_name,
390 commit_id=default_commit_id,
390 commit_id=default_commit_id,
391 f_path=final_f_path,
391 f_path=final_f_path,
392 _query=query
392 _query=query
393 )
393 )
394 return files_url
394 return files_url
395
395
396
396
397 def code_highlight(code, lexer, formatter, use_hl_filter=False):
397 def code_highlight(code, lexer, formatter, use_hl_filter=False):
398 """
398 """
399 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
399 Lex ``code`` with ``lexer`` and format it with the formatter ``formatter``.
400
400
401 If ``outfile`` is given and a valid file object (an object
401 If ``outfile`` is given and a valid file object (an object
402 with a ``write`` method), the result will be written to it, otherwise
402 with a ``write`` method), the result will be written to it, otherwise
403 it is returned as a string.
403 it is returned as a string.
404 """
404 """
405 if use_hl_filter:
405 if use_hl_filter:
406 # add HL filter
406 # add HL filter
407 from rhodecode.lib.index import search_utils
407 from rhodecode.lib.index import search_utils
408 lexer.add_filter(search_utils.ElasticSearchHLFilter())
408 lexer.add_filter(search_utils.ElasticSearchHLFilter())
409 return pygments.format(pygments.lex(code, lexer), formatter)
409 return pygments.format(pygments.lex(code, lexer), formatter)
410
410
411
411
412 class CodeHtmlFormatter(HtmlFormatter):
412 class CodeHtmlFormatter(HtmlFormatter):
413 """
413 """
414 My code Html Formatter for source codes
414 My code Html Formatter for source codes
415 """
415 """
416
416
417 def wrap(self, source, outfile):
417 def wrap(self, source, outfile):
418 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
418 return self._wrap_div(self._wrap_pre(self._wrap_code(source)))
419
419
420 def _wrap_code(self, source):
420 def _wrap_code(self, source):
421 for cnt, it in enumerate(source):
421 for cnt, it in enumerate(source):
422 i, t = it
422 i, t = it
423 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
423 t = '<div id="L%s">%s</div>' % (cnt + 1, t)
424 yield i, t
424 yield i, t
425
425
426 def _wrap_tablelinenos(self, inner):
426 def _wrap_tablelinenos(self, inner):
427 dummyoutfile = StringIO.StringIO()
427 dummyoutfile = StringIO.StringIO()
428 lncount = 0
428 lncount = 0
429 for t, line in inner:
429 for t, line in inner:
430 if t:
430 if t:
431 lncount += 1
431 lncount += 1
432 dummyoutfile.write(line)
432 dummyoutfile.write(line)
433
433
434 fl = self.linenostart
434 fl = self.linenostart
435 mw = len(str(lncount + fl - 1))
435 mw = len(str(lncount + fl - 1))
436 sp = self.linenospecial
436 sp = self.linenospecial
437 st = self.linenostep
437 st = self.linenostep
438 la = self.lineanchors
438 la = self.lineanchors
439 aln = self.anchorlinenos
439 aln = self.anchorlinenos
440 nocls = self.noclasses
440 nocls = self.noclasses
441 if sp:
441 if sp:
442 lines = []
442 lines = []
443
443
444 for i in range(fl, fl + lncount):
444 for i in range(fl, fl + lncount):
445 if i % st == 0:
445 if i % st == 0:
446 if i % sp == 0:
446 if i % sp == 0:
447 if aln:
447 if aln:
448 lines.append('<a href="#%s%d" class="special">%*d</a>' %
448 lines.append('<a href="#%s%d" class="special">%*d</a>' %
449 (la, i, mw, i))
449 (la, i, mw, i))
450 else:
450 else:
451 lines.append('<span class="special">%*d</span>' % (mw, i))
451 lines.append('<span class="special">%*d</span>' % (mw, i))
452 else:
452 else:
453 if aln:
453 if aln:
454 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
454 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
455 else:
455 else:
456 lines.append('%*d' % (mw, i))
456 lines.append('%*d' % (mw, i))
457 else:
457 else:
458 lines.append('')
458 lines.append('')
459 ls = '\n'.join(lines)
459 ls = '\n'.join(lines)
460 else:
460 else:
461 lines = []
461 lines = []
462 for i in range(fl, fl + lncount):
462 for i in range(fl, fl + lncount):
463 if i % st == 0:
463 if i % st == 0:
464 if aln:
464 if aln:
465 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
465 lines.append('<a href="#%s%d">%*d</a>' % (la, i, mw, i))
466 else:
466 else:
467 lines.append('%*d' % (mw, i))
467 lines.append('%*d' % (mw, i))
468 else:
468 else:
469 lines.append('')
469 lines.append('')
470 ls = '\n'.join(lines)
470 ls = '\n'.join(lines)
471
471
472 # in case you wonder about the seemingly redundant <div> here: since the
472 # in case you wonder about the seemingly redundant <div> here: since the
473 # content in the other cell also is wrapped in a div, some browsers in
473 # content in the other cell also is wrapped in a div, some browsers in
474 # some configurations seem to mess up the formatting...
474 # some configurations seem to mess up the formatting...
475 if nocls:
475 if nocls:
476 yield 0, ('<table class="%stable">' % self.cssclass +
476 yield 0, ('<table class="%stable">' % self.cssclass +
477 '<tr><td><div class="linenodiv" '
477 '<tr><td><div class="linenodiv" '
478 'style="background-color: #f0f0f0; padding-right: 10px">'
478 'style="background-color: #f0f0f0; padding-right: 10px">'
479 '<pre style="line-height: 125%">' +
479 '<pre style="line-height: 125%">' +
480 ls + '</pre></div></td><td id="hlcode" class="code">')
480 ls + '</pre></div></td><td id="hlcode" class="code">')
481 else:
481 else:
482 yield 0, ('<table class="%stable">' % self.cssclass +
482 yield 0, ('<table class="%stable">' % self.cssclass +
483 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
483 '<tr><td class="linenos"><div class="linenodiv"><pre>' +
484 ls + '</pre></div></td><td id="hlcode" class="code">')
484 ls + '</pre></div></td><td id="hlcode" class="code">')
485 yield 0, dummyoutfile.getvalue()
485 yield 0, dummyoutfile.getvalue()
486 yield 0, '</td></tr></table>'
486 yield 0, '</td></tr></table>'
487
487
488
488
489 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
489 class SearchContentCodeHtmlFormatter(CodeHtmlFormatter):
490 def __init__(self, **kw):
490 def __init__(self, **kw):
491 # only show these line numbers if set
491 # only show these line numbers if set
492 self.only_lines = kw.pop('only_line_numbers', [])
492 self.only_lines = kw.pop('only_line_numbers', [])
493 self.query_terms = kw.pop('query_terms', [])
493 self.query_terms = kw.pop('query_terms', [])
494 self.max_lines = kw.pop('max_lines', 5)
494 self.max_lines = kw.pop('max_lines', 5)
495 self.line_context = kw.pop('line_context', 3)
495 self.line_context = kw.pop('line_context', 3)
496 self.url = kw.pop('url', None)
496 self.url = kw.pop('url', None)
497
497
498 super(CodeHtmlFormatter, self).__init__(**kw)
498 super(CodeHtmlFormatter, self).__init__(**kw)
499
499
500 def _wrap_code(self, source):
500 def _wrap_code(self, source):
501 for cnt, it in enumerate(source):
501 for cnt, it in enumerate(source):
502 i, t = it
502 i, t = it
503 t = '<pre>%s</pre>' % t
503 t = '<pre>%s</pre>' % t
504 yield i, t
504 yield i, t
505
505
506 def _wrap_tablelinenos(self, inner):
506 def _wrap_tablelinenos(self, inner):
507 yield 0, '<table class="code-highlight %stable">' % self.cssclass
507 yield 0, '<table class="code-highlight %stable">' % self.cssclass
508
508
509 last_shown_line_number = 0
509 last_shown_line_number = 0
510 current_line_number = 1
510 current_line_number = 1
511
511
512 for t, line in inner:
512 for t, line in inner:
513 if not t:
513 if not t:
514 yield t, line
514 yield t, line
515 continue
515 continue
516
516
517 if current_line_number in self.only_lines:
517 if current_line_number in self.only_lines:
518 if last_shown_line_number + 1 != current_line_number:
518 if last_shown_line_number + 1 != current_line_number:
519 yield 0, '<tr>'
519 yield 0, '<tr>'
520 yield 0, '<td class="line">...</td>'
520 yield 0, '<td class="line">...</td>'
521 yield 0, '<td id="hlcode" class="code"></td>'
521 yield 0, '<td id="hlcode" class="code"></td>'
522 yield 0, '</tr>'
522 yield 0, '</tr>'
523
523
524 yield 0, '<tr>'
524 yield 0, '<tr>'
525 if self.url:
525 if self.url:
526 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
526 yield 0, '<td class="line"><a href="%s#L%i">%i</a></td>' % (
527 self.url, current_line_number, current_line_number)
527 self.url, current_line_number, current_line_number)
528 else:
528 else:
529 yield 0, '<td class="line"><a href="">%i</a></td>' % (
529 yield 0, '<td class="line"><a href="">%i</a></td>' % (
530 current_line_number)
530 current_line_number)
531 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
531 yield 0, '<td id="hlcode" class="code">' + line + '</td>'
532 yield 0, '</tr>'
532 yield 0, '</tr>'
533
533
534 last_shown_line_number = current_line_number
534 last_shown_line_number = current_line_number
535
535
536 current_line_number += 1
536 current_line_number += 1
537
537
538 yield 0, '</table>'
538 yield 0, '</table>'
539
539
540
540
541 def hsv_to_rgb(h, s, v):
541 def hsv_to_rgb(h, s, v):
542 """ Convert hsv color values to rgb """
542 """ Convert hsv color values to rgb """
543
543
544 if s == 0.0:
544 if s == 0.0:
545 return v, v, v
545 return v, v, v
546 i = int(h * 6.0) # XXX assume int() truncates!
546 i = int(h * 6.0) # XXX assume int() truncates!
547 f = (h * 6.0) - i
547 f = (h * 6.0) - i
548 p = v * (1.0 - s)
548 p = v * (1.0 - s)
549 q = v * (1.0 - s * f)
549 q = v * (1.0 - s * f)
550 t = v * (1.0 - s * (1.0 - f))
550 t = v * (1.0 - s * (1.0 - f))
551 i = i % 6
551 i = i % 6
552 if i == 0:
552 if i == 0:
553 return v, t, p
553 return v, t, p
554 if i == 1:
554 if i == 1:
555 return q, v, p
555 return q, v, p
556 if i == 2:
556 if i == 2:
557 return p, v, t
557 return p, v, t
558 if i == 3:
558 if i == 3:
559 return p, q, v
559 return p, q, v
560 if i == 4:
560 if i == 4:
561 return t, p, v
561 return t, p, v
562 if i == 5:
562 if i == 5:
563 return v, p, q
563 return v, p, q
564
564
565
565
566 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
566 def unique_color_generator(n=10000, saturation=0.10, lightness=0.95):
567 """
567 """
568 Generator for getting n of evenly distributed colors using
568 Generator for getting n of evenly distributed colors using
569 hsv color and golden ratio. It always return same order of colors
569 hsv color and golden ratio. It always return same order of colors
570
570
571 :param n: number of colors to generate
571 :param n: number of colors to generate
572 :param saturation: saturation of returned colors
572 :param saturation: saturation of returned colors
573 :param lightness: lightness of returned colors
573 :param lightness: lightness of returned colors
574 :returns: RGB tuple
574 :returns: RGB tuple
575 """
575 """
576
576
577 golden_ratio = 0.618033988749895
577 golden_ratio = 0.618033988749895
578 h = 0.22717784590367374
578 h = 0.22717784590367374
579
579
580 for _ in xrange(n):
580 for _ in xrange(n):
581 h += golden_ratio
581 h += golden_ratio
582 h %= 1
582 h %= 1
583 HSV_tuple = [h, saturation, lightness]
583 HSV_tuple = [h, saturation, lightness]
584 RGB_tuple = hsv_to_rgb(*HSV_tuple)
584 RGB_tuple = hsv_to_rgb(*HSV_tuple)
585 yield map(lambda x: str(int(x * 256)), RGB_tuple)
585 yield map(lambda x: str(int(x * 256)), RGB_tuple)
586
586
587
587
588 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
588 def color_hasher(n=10000, saturation=0.10, lightness=0.95):
589 """
589 """
590 Returns a function which when called with an argument returns a unique
590 Returns a function which when called with an argument returns a unique
591 color for that argument, eg.
591 color for that argument, eg.
592
592
593 :param n: number of colors to generate
593 :param n: number of colors to generate
594 :param saturation: saturation of returned colors
594 :param saturation: saturation of returned colors
595 :param lightness: lightness of returned colors
595 :param lightness: lightness of returned colors
596 :returns: css RGB string
596 :returns: css RGB string
597
597
598 >>> color_hash = color_hasher()
598 >>> color_hash = color_hasher()
599 >>> color_hash('hello')
599 >>> color_hash('hello')
600 'rgb(34, 12, 59)'
600 'rgb(34, 12, 59)'
601 >>> color_hash('hello')
601 >>> color_hash('hello')
602 'rgb(34, 12, 59)'
602 'rgb(34, 12, 59)'
603 >>> color_hash('other')
603 >>> color_hash('other')
604 'rgb(90, 224, 159)'
604 'rgb(90, 224, 159)'
605 """
605 """
606
606
607 color_dict = {}
607 color_dict = {}
608 cgenerator = unique_color_generator(
608 cgenerator = unique_color_generator(
609 saturation=saturation, lightness=lightness)
609 saturation=saturation, lightness=lightness)
610
610
611 def get_color_string(thing):
611 def get_color_string(thing):
612 if thing in color_dict:
612 if thing in color_dict:
613 col = color_dict[thing]
613 col = color_dict[thing]
614 else:
614 else:
615 col = color_dict[thing] = cgenerator.next()
615 col = color_dict[thing] = cgenerator.next()
616 return "rgb(%s)" % (', '.join(col))
616 return "rgb(%s)" % (', '.join(col))
617
617
618 return get_color_string
618 return get_color_string
619
619
620
620
621 def get_lexer_safe(mimetype=None, filepath=None):
621 def get_lexer_safe(mimetype=None, filepath=None):
622 """
622 """
623 Tries to return a relevant pygments lexer using mimetype/filepath name,
623 Tries to return a relevant pygments lexer using mimetype/filepath name,
624 defaulting to plain text if none could be found
624 defaulting to plain text if none could be found
625 """
625 """
626 lexer = None
626 lexer = None
627 try:
627 try:
628 if mimetype:
628 if mimetype:
629 lexer = get_lexer_for_mimetype(mimetype)
629 lexer = get_lexer_for_mimetype(mimetype)
630 if not lexer:
630 if not lexer:
631 lexer = get_lexer_for_filename(filepath)
631 lexer = get_lexer_for_filename(filepath)
632 except pygments.util.ClassNotFound:
632 except pygments.util.ClassNotFound:
633 pass
633 pass
634
634
635 if not lexer:
635 if not lexer:
636 lexer = get_lexer_by_name('text')
636 lexer = get_lexer_by_name('text')
637
637
638 return lexer
638 return lexer
639
639
640
640
641 def get_lexer_for_filenode(filenode):
641 def get_lexer_for_filenode(filenode):
642 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
642 lexer = get_custom_lexer(filenode.extension) or filenode.lexer
643 return lexer
643 return lexer
644
644
645
645
646 def pygmentize(filenode, **kwargs):
646 def pygmentize(filenode, **kwargs):
647 """
647 """
648 pygmentize function using pygments
648 pygmentize function using pygments
649
649
650 :param filenode:
650 :param filenode:
651 """
651 """
652 lexer = get_lexer_for_filenode(filenode)
652 lexer = get_lexer_for_filenode(filenode)
653 return literal(code_highlight(filenode.content, lexer,
653 return literal(code_highlight(filenode.content, lexer,
654 CodeHtmlFormatter(**kwargs)))
654 CodeHtmlFormatter(**kwargs)))
655
655
656
656
657 def is_following_repo(repo_name, user_id):
657 def is_following_repo(repo_name, user_id):
658 from rhodecode.model.scm import ScmModel
658 from rhodecode.model.scm import ScmModel
659 return ScmModel().is_following_repo(repo_name, user_id)
659 return ScmModel().is_following_repo(repo_name, user_id)
660
660
661
661
662 class _Message(object):
662 class _Message(object):
663 """A message returned by ``Flash.pop_messages()``.
663 """A message returned by ``Flash.pop_messages()``.
664
664
665 Converting the message to a string returns the message text. Instances
665 Converting the message to a string returns the message text. Instances
666 also have the following attributes:
666 also have the following attributes:
667
667
668 * ``message``: the message text.
668 * ``message``: the message text.
669 * ``category``: the category specified when the message was created.
669 * ``category``: the category specified when the message was created.
670 """
670 """
671
671
672 def __init__(self, category, message, sub_data=None):
672 def __init__(self, category, message, sub_data=None):
673 self.category = category
673 self.category = category
674 self.message = message
674 self.message = message
675 self.sub_data = sub_data or {}
675 self.sub_data = sub_data or {}
676
676
677 def __str__(self):
677 def __str__(self):
678 return self.message
678 return self.message
679
679
680 __unicode__ = __str__
680 __unicode__ = __str__
681
681
682 def __html__(self):
682 def __html__(self):
683 return escape(safe_unicode(self.message))
683 return escape(safe_unicode(self.message))
684
684
685
685
686 class Flash(object):
686 class Flash(object):
687 # List of allowed categories. If None, allow any category.
687 # List of allowed categories. If None, allow any category.
688 categories = ["warning", "notice", "error", "success"]
688 categories = ["warning", "notice", "error", "success"]
689
689
690 # Default category if none is specified.
690 # Default category if none is specified.
691 default_category = "notice"
691 default_category = "notice"
692
692
693 def __init__(self, session_key="flash", categories=None,
693 def __init__(self, session_key="flash", categories=None,
694 default_category=None):
694 default_category=None):
695 """
695 """
696 Instantiate a ``Flash`` object.
696 Instantiate a ``Flash`` object.
697
697
698 ``session_key`` is the key to save the messages under in the user's
698 ``session_key`` is the key to save the messages under in the user's
699 session.
699 session.
700
700
701 ``categories`` is an optional list which overrides the default list
701 ``categories`` is an optional list which overrides the default list
702 of categories.
702 of categories.
703
703
704 ``default_category`` overrides the default category used for messages
704 ``default_category`` overrides the default category used for messages
705 when none is specified.
705 when none is specified.
706 """
706 """
707 self.session_key = session_key
707 self.session_key = session_key
708 if categories is not None:
708 if categories is not None:
709 self.categories = categories
709 self.categories = categories
710 if default_category is not None:
710 if default_category is not None:
711 self.default_category = default_category
711 self.default_category = default_category
712 if self.categories and self.default_category not in self.categories:
712 if self.categories and self.default_category not in self.categories:
713 raise ValueError(
713 raise ValueError(
714 "unrecognized default category %r" % (self.default_category,))
714 "unrecognized default category %r" % (self.default_category,))
715
715
716 def pop_messages(self, session=None, request=None):
716 def pop_messages(self, session=None, request=None):
717 """
717 """
718 Return all accumulated messages and delete them from the session.
718 Return all accumulated messages and delete them from the session.
719
719
720 The return value is a list of ``Message`` objects.
720 The return value is a list of ``Message`` objects.
721 """
721 """
722 messages = []
722 messages = []
723
723
724 if not session:
724 if not session:
725 if not request:
725 if not request:
726 request = get_current_request()
726 request = get_current_request()
727 session = request.session
727 session = request.session
728
728
729 # Pop the 'old' pylons flash messages. They are tuples of the form
729 # Pop the 'old' pylons flash messages. They are tuples of the form
730 # (category, message)
730 # (category, message)
731 for cat, msg in session.pop(self.session_key, []):
731 for cat, msg in session.pop(self.session_key, []):
732 messages.append(_Message(cat, msg))
732 messages.append(_Message(cat, msg))
733
733
734 # Pop the 'new' pyramid flash messages for each category as list
734 # Pop the 'new' pyramid flash messages for each category as list
735 # of strings.
735 # of strings.
736 for cat in self.categories:
736 for cat in self.categories:
737 for msg in session.pop_flash(queue=cat):
737 for msg in session.pop_flash(queue=cat):
738 sub_data = {}
738 sub_data = {}
739 if hasattr(msg, 'rsplit'):
739 if hasattr(msg, 'rsplit'):
740 flash_data = msg.rsplit('|DELIM|', 1)
740 flash_data = msg.rsplit('|DELIM|', 1)
741 org_message = flash_data[0]
741 org_message = flash_data[0]
742 if len(flash_data) > 1:
742 if len(flash_data) > 1:
743 sub_data = json.loads(flash_data[1])
743 sub_data = json.loads(flash_data[1])
744 else:
744 else:
745 org_message = msg
745 org_message = msg
746
746
747 messages.append(_Message(cat, org_message, sub_data=sub_data))
747 messages.append(_Message(cat, org_message, sub_data=sub_data))
748
748
749 # Map messages from the default queue to the 'notice' category.
749 # Map messages from the default queue to the 'notice' category.
750 for msg in session.pop_flash():
750 for msg in session.pop_flash():
751 messages.append(_Message('notice', msg))
751 messages.append(_Message('notice', msg))
752
752
753 session.save()
753 session.save()
754 return messages
754 return messages
755
755
756 def json_alerts(self, session=None, request=None):
756 def json_alerts(self, session=None, request=None):
757 payloads = []
757 payloads = []
758 messages = flash.pop_messages(session=session, request=request) or []
758 messages = flash.pop_messages(session=session, request=request) or []
759 for message in messages:
759 for message in messages:
760 payloads.append({
760 payloads.append({
761 'message': {
761 'message': {
762 'message': u'{}'.format(message.message),
762 'message': u'{}'.format(message.message),
763 'level': message.category,
763 'level': message.category,
764 'force': True,
764 'force': True,
765 'subdata': message.sub_data
765 'subdata': message.sub_data
766 }
766 }
767 })
767 })
768 return json.dumps(payloads)
768 return json.dumps(payloads)
769
769
770 def __call__(self, message, category=None, ignore_duplicate=True,
770 def __call__(self, message, category=None, ignore_duplicate=True,
771 session=None, request=None):
771 session=None, request=None):
772
772
773 if not session:
773 if not session:
774 if not request:
774 if not request:
775 request = get_current_request()
775 request = get_current_request()
776 session = request.session
776 session = request.session
777
777
778 session.flash(
778 session.flash(
779 message, queue=category, allow_duplicate=not ignore_duplicate)
779 message, queue=category, allow_duplicate=not ignore_duplicate)
780
780
781
781
782 flash = Flash()
782 flash = Flash()
783
783
784 #==============================================================================
784 #==============================================================================
785 # SCM FILTERS available via h.
785 # SCM FILTERS available via h.
786 #==============================================================================
786 #==============================================================================
787 from rhodecode.lib.vcs.utils import author_name, author_email
787 from rhodecode.lib.vcs.utils import author_name, author_email
788 from rhodecode.lib.utils2 import age, age_from_seconds
788 from rhodecode.lib.utils2 import age, age_from_seconds
789 from rhodecode.model.db import User, ChangesetStatus
789 from rhodecode.model.db import User, ChangesetStatus
790
790
791
791
792 email = author_email
792 email = author_email
793
793
794
794
795 def capitalize(raw_text):
795 def capitalize(raw_text):
796 return raw_text.capitalize()
796 return raw_text.capitalize()
797
797
798
798
799 def short_id(long_id):
799 def short_id(long_id):
800 return long_id[:12]
800 return long_id[:12]
801
801
802
802
803 def hide_credentials(url):
803 def hide_credentials(url):
804 from rhodecode.lib.utils2 import credentials_filter
804 from rhodecode.lib.utils2 import credentials_filter
805 return credentials_filter(url)
805 return credentials_filter(url)
806
806
807
807
808 import pytz
808 import pytz
809 import tzlocal
809 import tzlocal
810 local_timezone = tzlocal.get_localzone()
810 local_timezone = tzlocal.get_localzone()
811
811
812
812
813 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
813 def get_timezone(datetime_iso, time_is_local=False):
814 title = value or format_date(datetime_iso)
815 tzinfo = '+00:00'
814 tzinfo = '+00:00'
816
815
817 # detect if we have a timezone info, otherwise, add it
816 # detect if we have a timezone info, otherwise, add it
818 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
817 if time_is_local and isinstance(datetime_iso, datetime) and not datetime_iso.tzinfo:
819 force_timezone = os.environ.get('RC_TIMEZONE', '')
818 force_timezone = os.environ.get('RC_TIMEZONE', '')
820 if force_timezone:
819 if force_timezone:
821 force_timezone = pytz.timezone(force_timezone)
820 force_timezone = pytz.timezone(force_timezone)
822 timezone = force_timezone or local_timezone
821 timezone = force_timezone or local_timezone
823 offset = timezone.localize(datetime_iso).strftime('%z')
822 offset = timezone.localize(datetime_iso).strftime('%z')
824 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
823 tzinfo = '{}:{}'.format(offset[:-2], offset[-2:])
824 return tzinfo
825
826
827 def age_component(datetime_iso, value=None, time_is_local=False, tooltip=True):
828 title = value or format_date(datetime_iso)
829 tzinfo = get_timezone(datetime_iso, time_is_local=time_is_local)
825
830
826 return literal(
831 return literal(
827 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
832 '<time class="timeago {cls}" title="{tt_title}" datetime="{dt}{tzinfo}">{title}</time>'.format(
828 cls='tooltip' if tooltip else '',
833 cls='tooltip' if tooltip else '',
829 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
834 tt_title=('{title}{tzinfo}'.format(title=title, tzinfo=tzinfo)) if tooltip else '',
830 title=title, dt=datetime_iso, tzinfo=tzinfo
835 title=title, dt=datetime_iso, tzinfo=tzinfo
831 ))
836 ))
832
837
833
838
834 def _shorten_commit_id(commit_id, commit_len=None):
839 def _shorten_commit_id(commit_id, commit_len=None):
835 if commit_len is None:
840 if commit_len is None:
836 request = get_current_request()
841 request = get_current_request()
837 commit_len = request.call_context.visual.show_sha_length
842 commit_len = request.call_context.visual.show_sha_length
838 return commit_id[:commit_len]
843 return commit_id[:commit_len]
839
844
840
845
841 def show_id(commit, show_idx=None, commit_len=None):
846 def show_id(commit, show_idx=None, commit_len=None):
842 """
847 """
843 Configurable function that shows ID
848 Configurable function that shows ID
844 by default it's r123:fffeeefffeee
849 by default it's r123:fffeeefffeee
845
850
846 :param commit: commit instance
851 :param commit: commit instance
847 """
852 """
848 if show_idx is None:
853 if show_idx is None:
849 request = get_current_request()
854 request = get_current_request()
850 show_idx = request.call_context.visual.show_revision_number
855 show_idx = request.call_context.visual.show_revision_number
851
856
852 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
857 raw_id = _shorten_commit_id(commit.raw_id, commit_len=commit_len)
853 if show_idx:
858 if show_idx:
854 return 'r%s:%s' % (commit.idx, raw_id)
859 return 'r%s:%s' % (commit.idx, raw_id)
855 else:
860 else:
856 return '%s' % (raw_id, )
861 return '%s' % (raw_id, )
857
862
858
863
859 def format_date(date):
864 def format_date(date):
860 """
865 """
861 use a standardized formatting for dates used in RhodeCode
866 use a standardized formatting for dates used in RhodeCode
862
867
863 :param date: date/datetime object
868 :param date: date/datetime object
864 :return: formatted date
869 :return: formatted date
865 """
870 """
866
871
867 if date:
872 if date:
868 _fmt = "%a, %d %b %Y %H:%M:%S"
873 _fmt = "%a, %d %b %Y %H:%M:%S"
869 return safe_unicode(date.strftime(_fmt))
874 return safe_unicode(date.strftime(_fmt))
870
875
871 return u""
876 return u""
872
877
873
878
874 class _RepoChecker(object):
879 class _RepoChecker(object):
875
880
876 def __init__(self, backend_alias):
881 def __init__(self, backend_alias):
877 self._backend_alias = backend_alias
882 self._backend_alias = backend_alias
878
883
879 def __call__(self, repository):
884 def __call__(self, repository):
880 if hasattr(repository, 'alias'):
885 if hasattr(repository, 'alias'):
881 _type = repository.alias
886 _type = repository.alias
882 elif hasattr(repository, 'repo_type'):
887 elif hasattr(repository, 'repo_type'):
883 _type = repository.repo_type
888 _type = repository.repo_type
884 else:
889 else:
885 _type = repository
890 _type = repository
886 return _type == self._backend_alias
891 return _type == self._backend_alias
887
892
888
893
889 is_git = _RepoChecker('git')
894 is_git = _RepoChecker('git')
890 is_hg = _RepoChecker('hg')
895 is_hg = _RepoChecker('hg')
891 is_svn = _RepoChecker('svn')
896 is_svn = _RepoChecker('svn')
892
897
893
898
894 def get_repo_type_by_name(repo_name):
899 def get_repo_type_by_name(repo_name):
895 repo = Repository.get_by_repo_name(repo_name)
900 repo = Repository.get_by_repo_name(repo_name)
896 if repo:
901 if repo:
897 return repo.repo_type
902 return repo.repo_type
898
903
899
904
900 def is_svn_without_proxy(repository):
905 def is_svn_without_proxy(repository):
901 if is_svn(repository):
906 if is_svn(repository):
902 from rhodecode.model.settings import VcsSettingsModel
907 from rhodecode.model.settings import VcsSettingsModel
903 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
908 conf = VcsSettingsModel().get_ui_settings_as_config_obj()
904 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
909 return not str2bool(conf.get('vcs_svn_proxy', 'http_requests_enabled'))
905 return False
910 return False
906
911
907
912
908 def discover_user(author):
913 def discover_user(author):
909 """
914 """
910 Tries to discover RhodeCode User based on the author string. Author string
915 Tries to discover RhodeCode User based on the author string. Author string
911 is typically `FirstName LastName <email@address.com>`
916 is typically `FirstName LastName <email@address.com>`
912 """
917 """
913
918
914 # if author is already an instance use it for extraction
919 # if author is already an instance use it for extraction
915 if isinstance(author, User):
920 if isinstance(author, User):
916 return author
921 return author
917
922
918 # Valid email in the attribute passed, see if they're in the system
923 # Valid email in the attribute passed, see if they're in the system
919 _email = author_email(author)
924 _email = author_email(author)
920 if _email != '':
925 if _email != '':
921 user = User.get_by_email(_email, case_insensitive=True, cache=True)
926 user = User.get_by_email(_email, case_insensitive=True, cache=True)
922 if user is not None:
927 if user is not None:
923 return user
928 return user
924
929
925 # Maybe it's a username, we try to extract it and fetch by username ?
930 # Maybe it's a username, we try to extract it and fetch by username ?
926 _author = author_name(author)
931 _author = author_name(author)
927 user = User.get_by_username(_author, case_insensitive=True, cache=True)
932 user = User.get_by_username(_author, case_insensitive=True, cache=True)
928 if user is not None:
933 if user is not None:
929 return user
934 return user
930
935
931 return None
936 return None
932
937
933
938
934 def email_or_none(author):
939 def email_or_none(author):
935 # extract email from the commit string
940 # extract email from the commit string
936 _email = author_email(author)
941 _email = author_email(author)
937
942
938 # If we have an email, use it, otherwise
943 # If we have an email, use it, otherwise
939 # see if it contains a username we can get an email from
944 # see if it contains a username we can get an email from
940 if _email != '':
945 if _email != '':
941 return _email
946 return _email
942 else:
947 else:
943 user = User.get_by_username(
948 user = User.get_by_username(
944 author_name(author), case_insensitive=True, cache=True)
949 author_name(author), case_insensitive=True, cache=True)
945
950
946 if user is not None:
951 if user is not None:
947 return user.email
952 return user.email
948
953
949 # No valid email, not a valid user in the system, none!
954 # No valid email, not a valid user in the system, none!
950 return None
955 return None
951
956
952
957
953 def link_to_user(author, length=0, **kwargs):
958 def link_to_user(author, length=0, **kwargs):
954 user = discover_user(author)
959 user = discover_user(author)
955 # user can be None, but if we have it already it means we can re-use it
960 # user can be None, but if we have it already it means we can re-use it
956 # in the person() function, so we save 1 intensive-query
961 # in the person() function, so we save 1 intensive-query
957 if user:
962 if user:
958 author = user
963 author = user
959
964
960 display_person = person(author, 'username_or_name_or_email')
965 display_person = person(author, 'username_or_name_or_email')
961 if length:
966 if length:
962 display_person = shorter(display_person, length)
967 display_person = shorter(display_person, length)
963
968
964 if user and user.username != user.DEFAULT_USER:
969 if user and user.username != user.DEFAULT_USER:
965 return link_to(
970 return link_to(
966 escape(display_person),
971 escape(display_person),
967 route_path('user_profile', username=user.username),
972 route_path('user_profile', username=user.username),
968 **kwargs)
973 **kwargs)
969 else:
974 else:
970 return escape(display_person)
975 return escape(display_person)
971
976
972
977
973 def link_to_group(users_group_name, **kwargs):
978 def link_to_group(users_group_name, **kwargs):
974 return link_to(
979 return link_to(
975 escape(users_group_name),
980 escape(users_group_name),
976 route_path('user_group_profile', user_group_name=users_group_name),
981 route_path('user_group_profile', user_group_name=users_group_name),
977 **kwargs)
982 **kwargs)
978
983
979
984
980 def person(author, show_attr="username_and_name"):
985 def person(author, show_attr="username_and_name"):
981 user = discover_user(author)
986 user = discover_user(author)
982 if user:
987 if user:
983 return getattr(user, show_attr)
988 return getattr(user, show_attr)
984 else:
989 else:
985 _author = author_name(author)
990 _author = author_name(author)
986 _email = email(author)
991 _email = email(author)
987 return _author or _email
992 return _author or _email
988
993
989
994
990 def author_string(email):
995 def author_string(email):
991 if email:
996 if email:
992 user = User.get_by_email(email, case_insensitive=True, cache=True)
997 user = User.get_by_email(email, case_insensitive=True, cache=True)
993 if user:
998 if user:
994 if user.first_name or user.last_name:
999 if user.first_name or user.last_name:
995 return '%s %s &lt;%s&gt;' % (
1000 return '%s %s &lt;%s&gt;' % (
996 user.first_name, user.last_name, email)
1001 user.first_name, user.last_name, email)
997 else:
1002 else:
998 return email
1003 return email
999 else:
1004 else:
1000 return email
1005 return email
1001 else:
1006 else:
1002 return None
1007 return None
1003
1008
1004
1009
1005 def person_by_id(id_, show_attr="username_and_name"):
1010 def person_by_id(id_, show_attr="username_and_name"):
1006 # attr to return from fetched user
1011 # attr to return from fetched user
1007 person_getter = lambda usr: getattr(usr, show_attr)
1012 person_getter = lambda usr: getattr(usr, show_attr)
1008
1013
1009 #maybe it's an ID ?
1014 #maybe it's an ID ?
1010 if str(id_).isdigit() or isinstance(id_, int):
1015 if str(id_).isdigit() or isinstance(id_, int):
1011 id_ = int(id_)
1016 id_ = int(id_)
1012 user = User.get(id_)
1017 user = User.get(id_)
1013 if user is not None:
1018 if user is not None:
1014 return person_getter(user)
1019 return person_getter(user)
1015 return id_
1020 return id_
1016
1021
1017
1022
1018 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1023 def gravatar_with_user(request, author, show_disabled=False, tooltip=False):
1019 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1024 _render = request.get_partial_renderer('rhodecode:templates/base/base.mako')
1020 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1025 return _render('gravatar_with_user', author, show_disabled=show_disabled, tooltip=tooltip)
1021
1026
1022
1027
1023 tags_paterns = OrderedDict((
1028 tags_paterns = OrderedDict((
1024 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1029 ('lang', (re.compile(r'\[(lang|language)\ \=\&gt;\ *([a-zA-Z\-\/\#\+\.]*)\]'),
1025 '<div class="metatag" tag="lang">\\2</div>')),
1030 '<div class="metatag" tag="lang">\\2</div>')),
1026
1031
1027 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1032 ('see', (re.compile(r'\[see\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1028 '<div class="metatag" tag="see">see: \\1 </div>')),
1033 '<div class="metatag" tag="see">see: \\1 </div>')),
1029
1034
1030 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1035 ('url', (re.compile(r'\[url\ \=\&gt;\ \[([a-zA-Z0-9\ \.\-\_]+)\]\((http://|https://|/)(.*?)\)\]'),
1031 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1036 '<div class="metatag" tag="url"> <a href="\\2\\3">\\1</a> </div>')),
1032
1037
1033 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1038 ('license', (re.compile(r'\[license\ \=\&gt;\ *([a-zA-Z0-9\/\=\?\&amp;\ \:\/\.\-]*)\]'),
1034 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1039 '<div class="metatag" tag="license"><a href="http:\/\/www.opensource.org/licenses/\\1">\\1</a></div>')),
1035
1040
1036 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1041 ('ref', (re.compile(r'\[(requires|recommends|conflicts|base)\ \=\&gt;\ *([a-zA-Z0-9\-\/]*)\]'),
1037 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1042 '<div class="metatag" tag="ref \\1">\\1: <a href="/\\2">\\2</a></div>')),
1038
1043
1039 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1044 ('state', (re.compile(r'\[(stable|featured|stale|dead|dev|deprecated)\]'),
1040 '<div class="metatag" tag="state \\1">\\1</div>')),
1045 '<div class="metatag" tag="state \\1">\\1</div>')),
1041
1046
1042 # label in grey
1047 # label in grey
1043 ('label', (re.compile(r'\[([a-z]+)\]'),
1048 ('label', (re.compile(r'\[([a-z]+)\]'),
1044 '<div class="metatag" tag="label">\\1</div>')),
1049 '<div class="metatag" tag="label">\\1</div>')),
1045
1050
1046 # generic catch all in grey
1051 # generic catch all in grey
1047 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1052 ('generic', (re.compile(r'\[([a-zA-Z0-9\.\-\_]+)\]'),
1048 '<div class="metatag" tag="generic">\\1</div>')),
1053 '<div class="metatag" tag="generic">\\1</div>')),
1049 ))
1054 ))
1050
1055
1051
1056
1052 def extract_metatags(value):
1057 def extract_metatags(value):
1053 """
1058 """
1054 Extract supported meta-tags from given text value
1059 Extract supported meta-tags from given text value
1055 """
1060 """
1056 tags = []
1061 tags = []
1057 if not value:
1062 if not value:
1058 return tags, ''
1063 return tags, ''
1059
1064
1060 for key, val in tags_paterns.items():
1065 for key, val in tags_paterns.items():
1061 pat, replace_html = val
1066 pat, replace_html = val
1062 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1067 tags.extend([(key, x.group()) for x in pat.finditer(value)])
1063 value = pat.sub('', value)
1068 value = pat.sub('', value)
1064
1069
1065 return tags, value
1070 return tags, value
1066
1071
1067
1072
1068 def style_metatag(tag_type, value):
1073 def style_metatag(tag_type, value):
1069 """
1074 """
1070 converts tags from value into html equivalent
1075 converts tags from value into html equivalent
1071 """
1076 """
1072 if not value:
1077 if not value:
1073 return ''
1078 return ''
1074
1079
1075 html_value = value
1080 html_value = value
1076 tag_data = tags_paterns.get(tag_type)
1081 tag_data = tags_paterns.get(tag_type)
1077 if tag_data:
1082 if tag_data:
1078 pat, replace_html = tag_data
1083 pat, replace_html = tag_data
1079 # convert to plain `unicode` instead of a markup tag to be used in
1084 # convert to plain `unicode` instead of a markup tag to be used in
1080 # regex expressions. safe_unicode doesn't work here
1085 # regex expressions. safe_unicode doesn't work here
1081 html_value = pat.sub(replace_html, unicode(value))
1086 html_value = pat.sub(replace_html, unicode(value))
1082
1087
1083 return html_value
1088 return html_value
1084
1089
1085
1090
1086 def bool2icon(value, show_at_false=True):
1091 def bool2icon(value, show_at_false=True):
1087 """
1092 """
1088 Returns boolean value of a given value, represented as html element with
1093 Returns boolean value of a given value, represented as html element with
1089 classes that will represent icons
1094 classes that will represent icons
1090
1095
1091 :param value: given value to convert to html node
1096 :param value: given value to convert to html node
1092 """
1097 """
1093
1098
1094 if value: # does bool conversion
1099 if value: # does bool conversion
1095 return HTML.tag('i', class_="icon-true", title='True')
1100 return HTML.tag('i', class_="icon-true", title='True')
1096 else: # not true as bool
1101 else: # not true as bool
1097 if show_at_false:
1102 if show_at_false:
1098 return HTML.tag('i', class_="icon-false", title='False')
1103 return HTML.tag('i', class_="icon-false", title='False')
1099 return HTML.tag('i')
1104 return HTML.tag('i')
1100
1105
1101 #==============================================================================
1106 #==============================================================================
1102 # PERMS
1107 # PERMS
1103 #==============================================================================
1108 #==============================================================================
1104 from rhodecode.lib.auth import (
1109 from rhodecode.lib.auth import (
1105 HasPermissionAny, HasPermissionAll,
1110 HasPermissionAny, HasPermissionAll,
1106 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1111 HasRepoPermissionAny, HasRepoPermissionAll, HasRepoGroupPermissionAll,
1107 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1112 HasRepoGroupPermissionAny, HasRepoPermissionAnyApi, get_csrf_token,
1108 csrf_token_key, AuthUser)
1113 csrf_token_key, AuthUser)
1109
1114
1110
1115
1111 #==============================================================================
1116 #==============================================================================
1112 # GRAVATAR URL
1117 # GRAVATAR URL
1113 #==============================================================================
1118 #==============================================================================
1114 class InitialsGravatar(object):
1119 class InitialsGravatar(object):
1115 def __init__(self, email_address, first_name, last_name, size=30,
1120 def __init__(self, email_address, first_name, last_name, size=30,
1116 background=None, text_color='#fff'):
1121 background=None, text_color='#fff'):
1117 self.size = size
1122 self.size = size
1118 self.first_name = first_name
1123 self.first_name = first_name
1119 self.last_name = last_name
1124 self.last_name = last_name
1120 self.email_address = email_address
1125 self.email_address = email_address
1121 self.background = background or self.str2color(email_address)
1126 self.background = background or self.str2color(email_address)
1122 self.text_color = text_color
1127 self.text_color = text_color
1123
1128
1124 def get_color_bank(self):
1129 def get_color_bank(self):
1125 """
1130 """
1126 returns a predefined list of colors that gravatars can use.
1131 returns a predefined list of colors that gravatars can use.
1127 Those are randomized distinct colors that guarantee readability and
1132 Those are randomized distinct colors that guarantee readability and
1128 uniqueness.
1133 uniqueness.
1129
1134
1130 generated with: http://phrogz.net/css/distinct-colors.html
1135 generated with: http://phrogz.net/css/distinct-colors.html
1131 """
1136 """
1132 return [
1137 return [
1133 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1138 '#bf3030', '#a67f53', '#00ff00', '#5989b3', '#392040', '#d90000',
1134 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1139 '#402910', '#204020', '#79baf2', '#a700b3', '#bf6060', '#7f5320',
1135 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1140 '#008000', '#003059', '#ee00ff', '#ff0000', '#8c4b00', '#007300',
1136 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1141 '#005fb3', '#de73e6', '#ff4040', '#ffaa00', '#3df255', '#203140',
1137 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1142 '#47004d', '#591616', '#664400', '#59b365', '#0d2133', '#83008c',
1138 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1143 '#592d2d', '#bf9f60', '#73e682', '#1d3f73', '#73006b', '#402020',
1139 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1144 '#b2862d', '#397341', '#597db3', '#e600d6', '#a60000', '#736039',
1140 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1145 '#00b318', '#79aaf2', '#330d30', '#ff8080', '#403010', '#16591f',
1141 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1146 '#002459', '#8c4688', '#e50000', '#ffbf40', '#00732e', '#102340',
1142 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1147 '#bf60ac', '#8c4646', '#cc8800', '#00a642', '#1d3473', '#b32d98',
1143 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1148 '#660e00', '#ffd580', '#80ffb2', '#7391e6', '#733967', '#d97b6c',
1144 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1149 '#8c5e00', '#59b389', '#3967e6', '#590047', '#73281d', '#665200',
1145 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1150 '#00e67a', '#2d50b3', '#8c2377', '#734139', '#b2982d', '#16593a',
1146 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1151 '#001859', '#ff00aa', '#a65e53', '#ffcc00', '#0d3321', '#2d3959',
1147 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1152 '#731d56', '#401610', '#4c3d00', '#468c6c', '#002ca6', '#d936a3',
1148 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1153 '#d94c36', '#403920', '#36d9a3', '#0d1733', '#592d4a', '#993626',
1149 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1154 '#cca300', '#00734d', '#46598c', '#8c005e', '#7f1100', '#8c7000',
1150 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1155 '#00a66f', '#7382e6', '#b32d74', '#d9896c', '#ffe680', '#1d7362',
1151 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1156 '#364cd9', '#73003d', '#d93a00', '#998a4d', '#59b3a1', '#5965b3',
1152 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1157 '#e5007a', '#73341d', '#665f00', '#00b38f', '#0018b3', '#59163a',
1153 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1158 '#b2502d', '#bfb960', '#00ffcc', '#23318c', '#a6537f', '#734939',
1154 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1159 '#b2a700', '#104036', '#3d3df2', '#402031', '#e56739', '#736f39',
1155 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1160 '#79f2ea', '#000059', '#401029', '#4c1400', '#ffee00', '#005953',
1156 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1161 '#101040', '#990052', '#402820', '#403d10', '#00ffee', '#0000d9',
1157 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1162 '#ff80c4', '#a66953', '#eeff00', '#00ccbe', '#8080ff', '#e673a1',
1158 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1163 '#a62c00', '#474d00', '#1a3331', '#46468c', '#733950', '#662900',
1159 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1164 '#858c23', '#238c85', '#0f0073', '#b20047', '#d9986c', '#becc00',
1160 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1165 '#396f73', '#281d73', '#ff0066', '#ff6600', '#dee673', '#59adb3',
1161 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1166 '#6559b3', '#590024', '#b2622d', '#98b32d', '#36ced9', '#332d59',
1162 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1167 '#40001a', '#733f1d', '#526600', '#005359', '#242040', '#bf6079',
1163 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1168 '#735039', '#cef23d', '#007780', '#5630bf', '#66001b', '#b24700',
1164 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1169 '#acbf60', '#1d6273', '#25008c', '#731d34', '#a67453', '#50592d',
1165 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1170 '#00ccff', '#6600ff', '#ff0044', '#4c1f00', '#8a994d', '#79daf2',
1166 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1171 '#a173e6', '#d93662', '#402310', '#aaff00', '#2d98b3', '#8c40ff',
1167 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1172 '#592d39', '#ff8c40', '#354020', '#103640', '#1a0040', '#331a20',
1168 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1173 '#331400', '#334d00', '#1d5673', '#583973', '#7f0022', '#4c3626',
1169 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1174 '#88cc00', '#36a3d9', '#3d0073', '#d9364c', '#33241a', '#698c23',
1170 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1175 '#5995b3', '#300059', '#e57382', '#7f3300', '#366600', '#00aaff',
1171 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1176 '#3a1659', '#733941', '#663600', '#74b32d', '#003c59', '#7f53a6',
1172 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1177 '#73000f', '#ff8800', '#baf279', '#79caf2', '#291040', '#a6293a',
1173 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1178 '#b2742d', '#587339', '#0077b3', '#632699', '#400009', '#d9a66c',
1174 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1179 '#294010', '#2d4a59', '#aa00ff', '#4c131b', '#b25f00', '#5ce600',
1175 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1180 '#267399', '#a336d9', '#990014', '#664e33', '#86bf60', '#0088ff',
1176 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1181 '#7700b3', '#593a16', '#073300', '#1d4b73', '#ac60bf', '#e59539',
1177 '#4f8c46', '#368dd9', '#5c0073'
1182 '#4f8c46', '#368dd9', '#5c0073'
1178 ]
1183 ]
1179
1184
1180 def rgb_to_hex_color(self, rgb_tuple):
1185 def rgb_to_hex_color(self, rgb_tuple):
1181 """
1186 """
1182 Converts an rgb_tuple passed to an hex color.
1187 Converts an rgb_tuple passed to an hex color.
1183
1188
1184 :param rgb_tuple: tuple with 3 ints represents rgb color space
1189 :param rgb_tuple: tuple with 3 ints represents rgb color space
1185 """
1190 """
1186 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1191 return '#' + ("".join(map(chr, rgb_tuple)).encode('hex'))
1187
1192
1188 def email_to_int_list(self, email_str):
1193 def email_to_int_list(self, email_str):
1189 """
1194 """
1190 Get every byte of the hex digest value of email and turn it to integer.
1195 Get every byte of the hex digest value of email and turn it to integer.
1191 It's going to be always between 0-255
1196 It's going to be always between 0-255
1192 """
1197 """
1193 digest = md5_safe(email_str.lower())
1198 digest = md5_safe(email_str.lower())
1194 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1199 return [int(digest[i * 2:i * 2 + 2], 16) for i in range(16)]
1195
1200
1196 def pick_color_bank_index(self, email_str, color_bank):
1201 def pick_color_bank_index(self, email_str, color_bank):
1197 return self.email_to_int_list(email_str)[0] % len(color_bank)
1202 return self.email_to_int_list(email_str)[0] % len(color_bank)
1198
1203
1199 def str2color(self, email_str):
1204 def str2color(self, email_str):
1200 """
1205 """
1201 Tries to map in a stable algorithm an email to color
1206 Tries to map in a stable algorithm an email to color
1202
1207
1203 :param email_str:
1208 :param email_str:
1204 """
1209 """
1205 color_bank = self.get_color_bank()
1210 color_bank = self.get_color_bank()
1206 # pick position (module it's length so we always find it in the
1211 # pick position (module it's length so we always find it in the
1207 # bank even if it's smaller than 256 values
1212 # bank even if it's smaller than 256 values
1208 pos = self.pick_color_bank_index(email_str, color_bank)
1213 pos = self.pick_color_bank_index(email_str, color_bank)
1209 return color_bank[pos]
1214 return color_bank[pos]
1210
1215
1211 def normalize_email(self, email_address):
1216 def normalize_email(self, email_address):
1212 import unicodedata
1217 import unicodedata
1213 # default host used to fill in the fake/missing email
1218 # default host used to fill in the fake/missing email
1214 default_host = u'localhost'
1219 default_host = u'localhost'
1215
1220
1216 if not email_address:
1221 if not email_address:
1217 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1222 email_address = u'%s@%s' % (User.DEFAULT_USER, default_host)
1218
1223
1219 email_address = safe_unicode(email_address)
1224 email_address = safe_unicode(email_address)
1220
1225
1221 if u'@' not in email_address:
1226 if u'@' not in email_address:
1222 email_address = u'%s@%s' % (email_address, default_host)
1227 email_address = u'%s@%s' % (email_address, default_host)
1223
1228
1224 if email_address.endswith(u'@'):
1229 if email_address.endswith(u'@'):
1225 email_address = u'%s%s' % (email_address, default_host)
1230 email_address = u'%s%s' % (email_address, default_host)
1226
1231
1227 email_address = unicodedata.normalize('NFKD', email_address)\
1232 email_address = unicodedata.normalize('NFKD', email_address)\
1228 .encode('ascii', 'ignore')
1233 .encode('ascii', 'ignore')
1229 return email_address
1234 return email_address
1230
1235
1231 def get_initials(self):
1236 def get_initials(self):
1232 """
1237 """
1233 Returns 2 letter initials calculated based on the input.
1238 Returns 2 letter initials calculated based on the input.
1234 The algorithm picks first given email address, and takes first letter
1239 The algorithm picks first given email address, and takes first letter
1235 of part before @, and then the first letter of server name. In case
1240 of part before @, and then the first letter of server name. In case
1236 the part before @ is in a format of `somestring.somestring2` it replaces
1241 the part before @ is in a format of `somestring.somestring2` it replaces
1237 the server letter with first letter of somestring2
1242 the server letter with first letter of somestring2
1238
1243
1239 In case function was initialized with both first and lastname, this
1244 In case function was initialized with both first and lastname, this
1240 overrides the extraction from email by first letter of the first and
1245 overrides the extraction from email by first letter of the first and
1241 last name. We add special logic to that functionality, In case Full name
1246 last name. We add special logic to that functionality, In case Full name
1242 is compound, like Guido Von Rossum, we use last part of the last name
1247 is compound, like Guido Von Rossum, we use last part of the last name
1243 (Von Rossum) picking `R`.
1248 (Von Rossum) picking `R`.
1244
1249
1245 Function also normalizes the non-ascii characters to they ascii
1250 Function also normalizes the non-ascii characters to they ascii
1246 representation, eg Δ„ => A
1251 representation, eg Δ„ => A
1247 """
1252 """
1248 import unicodedata
1253 import unicodedata
1249 # replace non-ascii to ascii
1254 # replace non-ascii to ascii
1250 first_name = unicodedata.normalize(
1255 first_name = unicodedata.normalize(
1251 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1256 'NFKD', safe_unicode(self.first_name)).encode('ascii', 'ignore')
1252 last_name = unicodedata.normalize(
1257 last_name = unicodedata.normalize(
1253 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1258 'NFKD', safe_unicode(self.last_name)).encode('ascii', 'ignore')
1254
1259
1255 # do NFKD encoding, and also make sure email has proper format
1260 # do NFKD encoding, and also make sure email has proper format
1256 email_address = self.normalize_email(self.email_address)
1261 email_address = self.normalize_email(self.email_address)
1257
1262
1258 # first push the email initials
1263 # first push the email initials
1259 prefix, server = email_address.split('@', 1)
1264 prefix, server = email_address.split('@', 1)
1260
1265
1261 # check if prefix is maybe a 'first_name.last_name' syntax
1266 # check if prefix is maybe a 'first_name.last_name' syntax
1262 _dot_split = prefix.rsplit('.', 1)
1267 _dot_split = prefix.rsplit('.', 1)
1263 if len(_dot_split) == 2 and _dot_split[1]:
1268 if len(_dot_split) == 2 and _dot_split[1]:
1264 initials = [_dot_split[0][0], _dot_split[1][0]]
1269 initials = [_dot_split[0][0], _dot_split[1][0]]
1265 else:
1270 else:
1266 initials = [prefix[0], server[0]]
1271 initials = [prefix[0], server[0]]
1267
1272
1268 # then try to replace either first_name or last_name
1273 # then try to replace either first_name or last_name
1269 fn_letter = (first_name or " ")[0].strip()
1274 fn_letter = (first_name or " ")[0].strip()
1270 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1275 ln_letter = (last_name.split(' ', 1)[-1] or " ")[0].strip()
1271
1276
1272 if fn_letter:
1277 if fn_letter:
1273 initials[0] = fn_letter
1278 initials[0] = fn_letter
1274
1279
1275 if ln_letter:
1280 if ln_letter:
1276 initials[1] = ln_letter
1281 initials[1] = ln_letter
1277
1282
1278 return ''.join(initials).upper()
1283 return ''.join(initials).upper()
1279
1284
1280 def get_img_data_by_type(self, font_family, img_type):
1285 def get_img_data_by_type(self, font_family, img_type):
1281 default_user = """
1286 default_user = """
1282 <svg xmlns="http://www.w3.org/2000/svg"
1287 <svg xmlns="http://www.w3.org/2000/svg"
1283 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1288 version="1.1" x="0px" y="0px" width="{size}" height="{size}"
1284 viewBox="-15 -10 439.165 429.164"
1289 viewBox="-15 -10 439.165 429.164"
1285
1290
1286 xml:space="preserve"
1291 xml:space="preserve"
1287 style="background:{background};" >
1292 style="background:{background};" >
1288
1293
1289 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1294 <path d="M204.583,216.671c50.664,0,91.74-48.075,
1290 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1295 91.74-107.378c0-82.237-41.074-107.377-91.74-107.377
1291 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1296 c-50.668,0-91.74,25.14-91.74,107.377C112.844,
1292 168.596,153.916,216.671,
1297 168.596,153.916,216.671,
1293 204.583,216.671z" fill="{text_color}"/>
1298 204.583,216.671z" fill="{text_color}"/>
1294 <path d="M407.164,374.717L360.88,
1299 <path d="M407.164,374.717L360.88,
1295 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1300 270.454c-2.117-4.771-5.836-8.728-10.465-11.138l-71.83-37.392
1296 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1301 c-1.584-0.823-3.502-0.663-4.926,0.415c-20.316,
1297 15.366-44.203,23.488-69.076,23.488c-24.877,
1302 15.366-44.203,23.488-69.076,23.488c-24.877,
1298 0-48.762-8.122-69.078-23.488
1303 0-48.762-8.122-69.078-23.488
1299 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1304 c-1.428-1.078-3.346-1.238-4.93-0.415L58.75,
1300 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1305 259.316c-4.631,2.41-8.346,6.365-10.465,11.138L2.001,374.717
1301 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1306 c-3.191,7.188-2.537,15.412,1.75,22.005c4.285,
1302 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1307 6.592,11.537,10.526,19.4,10.526h362.861c7.863,0,15.117-3.936,
1303 19.402-10.527 C409.699,390.129,
1308 19.402-10.527 C409.699,390.129,
1304 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1309 410.355,381.902,407.164,374.717z" fill="{text_color}"/>
1305 </svg>""".format(
1310 </svg>""".format(
1306 size=self.size,
1311 size=self.size,
1307 background='#979797', # @grey4
1312 background='#979797', # @grey4
1308 text_color=self.text_color,
1313 text_color=self.text_color,
1309 font_family=font_family)
1314 font_family=font_family)
1310
1315
1311 return {
1316 return {
1312 "default_user": default_user
1317 "default_user": default_user
1313 }[img_type]
1318 }[img_type]
1314
1319
1315 def get_img_data(self, svg_type=None):
1320 def get_img_data(self, svg_type=None):
1316 """
1321 """
1317 generates the svg metadata for image
1322 generates the svg metadata for image
1318 """
1323 """
1319 fonts = [
1324 fonts = [
1320 '-apple-system',
1325 '-apple-system',
1321 'BlinkMacSystemFont',
1326 'BlinkMacSystemFont',
1322 'Segoe UI',
1327 'Segoe UI',
1323 'Roboto',
1328 'Roboto',
1324 'Oxygen-Sans',
1329 'Oxygen-Sans',
1325 'Ubuntu',
1330 'Ubuntu',
1326 'Cantarell',
1331 'Cantarell',
1327 'Helvetica Neue',
1332 'Helvetica Neue',
1328 'sans-serif'
1333 'sans-serif'
1329 ]
1334 ]
1330 font_family = ','.join(fonts)
1335 font_family = ','.join(fonts)
1331 if svg_type:
1336 if svg_type:
1332 return self.get_img_data_by_type(font_family, svg_type)
1337 return self.get_img_data_by_type(font_family, svg_type)
1333
1338
1334 initials = self.get_initials()
1339 initials = self.get_initials()
1335 img_data = """
1340 img_data = """
1336 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1341 <svg xmlns="http://www.w3.org/2000/svg" pointer-events="none"
1337 width="{size}" height="{size}"
1342 width="{size}" height="{size}"
1338 style="width: 100%; height: 100%; background-color: {background}"
1343 style="width: 100%; height: 100%; background-color: {background}"
1339 viewBox="0 0 {size} {size}">
1344 viewBox="0 0 {size} {size}">
1340 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1345 <text text-anchor="middle" y="50%" x="50%" dy="0.35em"
1341 pointer-events="auto" fill="{text_color}"
1346 pointer-events="auto" fill="{text_color}"
1342 font-family="{font_family}"
1347 font-family="{font_family}"
1343 style="font-weight: 400; font-size: {f_size}px;">{text}
1348 style="font-weight: 400; font-size: {f_size}px;">{text}
1344 </text>
1349 </text>
1345 </svg>""".format(
1350 </svg>""".format(
1346 size=self.size,
1351 size=self.size,
1347 f_size=self.size/2.05, # scale the text inside the box nicely
1352 f_size=self.size/2.05, # scale the text inside the box nicely
1348 background=self.background,
1353 background=self.background,
1349 text_color=self.text_color,
1354 text_color=self.text_color,
1350 text=initials.upper(),
1355 text=initials.upper(),
1351 font_family=font_family)
1356 font_family=font_family)
1352
1357
1353 return img_data
1358 return img_data
1354
1359
1355 def generate_svg(self, svg_type=None):
1360 def generate_svg(self, svg_type=None):
1356 img_data = self.get_img_data(svg_type)
1361 img_data = self.get_img_data(svg_type)
1357 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1362 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1358
1363
1359
1364
1360 def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
1365 def initials_gravatar(request, email_address, first_name, last_name, size=30, store_on_disk=False):
1361
1366
1362 svg_type = None
1367 svg_type = None
1363 if email_address == User.DEFAULT_USER_EMAIL:
1368 if email_address == User.DEFAULT_USER_EMAIL:
1364 svg_type = 'default_user'
1369 svg_type = 'default_user'
1365
1370
1366 klass = InitialsGravatar(email_address, first_name, last_name, size)
1371 klass = InitialsGravatar(email_address, first_name, last_name, size)
1367
1372
1368 if store_on_disk:
1373 if store_on_disk:
1369 from rhodecode.apps.file_store import utils as store_utils
1374 from rhodecode.apps.file_store import utils as store_utils
1370 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
1375 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, \
1371 FileOverSizeException
1376 FileOverSizeException
1372 from rhodecode.model.db import Session
1377 from rhodecode.model.db import Session
1373
1378
1374 image_key = md5_safe(email_address.lower()
1379 image_key = md5_safe(email_address.lower()
1375 + first_name.lower() + last_name.lower())
1380 + first_name.lower() + last_name.lower())
1376
1381
1377 storage = store_utils.get_file_storage(request.registry.settings)
1382 storage = store_utils.get_file_storage(request.registry.settings)
1378 filename = '{}.svg'.format(image_key)
1383 filename = '{}.svg'.format(image_key)
1379 subdir = 'gravatars'
1384 subdir = 'gravatars'
1380 # since final name has a counter, we apply the 0
1385 # since final name has a counter, we apply the 0
1381 uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
1386 uid = storage.apply_counter(0, store_utils.uid_filename(filename, randomized=False))
1382 store_uid = os.path.join(subdir, uid)
1387 store_uid = os.path.join(subdir, uid)
1383
1388
1384 db_entry = FileStore.get_by_store_uid(store_uid)
1389 db_entry = FileStore.get_by_store_uid(store_uid)
1385 if db_entry:
1390 if db_entry:
1386 return request.route_path('download_file', fid=store_uid)
1391 return request.route_path('download_file', fid=store_uid)
1387
1392
1388 img_data = klass.get_img_data(svg_type=svg_type)
1393 img_data = klass.get_img_data(svg_type=svg_type)
1389 img_file = store_utils.bytes_to_file_obj(img_data)
1394 img_file = store_utils.bytes_to_file_obj(img_data)
1390
1395
1391 try:
1396 try:
1392 store_uid, metadata = storage.save_file(
1397 store_uid, metadata = storage.save_file(
1393 img_file, filename, directory=subdir,
1398 img_file, filename, directory=subdir,
1394 extensions=['.svg'], randomized_name=False)
1399 extensions=['.svg'], randomized_name=False)
1395 except (FileNotAllowedException, FileOverSizeException):
1400 except (FileNotAllowedException, FileOverSizeException):
1396 raise
1401 raise
1397
1402
1398 try:
1403 try:
1399 entry = FileStore.create(
1404 entry = FileStore.create(
1400 file_uid=store_uid, filename=metadata["filename"],
1405 file_uid=store_uid, filename=metadata["filename"],
1401 file_hash=metadata["sha256"], file_size=metadata["size"],
1406 file_hash=metadata["sha256"], file_size=metadata["size"],
1402 file_display_name=filename,
1407 file_display_name=filename,
1403 file_description=u'user gravatar `{}`'.format(safe_unicode(filename)),
1408 file_description=u'user gravatar `{}`'.format(safe_unicode(filename)),
1404 hidden=True, check_acl=False, user_id=1
1409 hidden=True, check_acl=False, user_id=1
1405 )
1410 )
1406 Session().add(entry)
1411 Session().add(entry)
1407 Session().commit()
1412 Session().commit()
1408 log.debug('Stored upload in DB as %s', entry)
1413 log.debug('Stored upload in DB as %s', entry)
1409 except Exception:
1414 except Exception:
1410 raise
1415 raise
1411
1416
1412 return request.route_path('download_file', fid=store_uid)
1417 return request.route_path('download_file', fid=store_uid)
1413
1418
1414 else:
1419 else:
1415 return klass.generate_svg(svg_type=svg_type)
1420 return klass.generate_svg(svg_type=svg_type)
1416
1421
1417
1422
1418 def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
1423 def gravatar_external(request, gravatar_url_tmpl, email_address, size=30):
1419 return safe_str(gravatar_url_tmpl)\
1424 return safe_str(gravatar_url_tmpl)\
1420 .replace('{email}', email_address) \
1425 .replace('{email}', email_address) \
1421 .replace('{md5email}', md5_safe(email_address.lower())) \
1426 .replace('{md5email}', md5_safe(email_address.lower())) \
1422 .replace('{netloc}', request.host) \
1427 .replace('{netloc}', request.host) \
1423 .replace('{scheme}', request.scheme) \
1428 .replace('{scheme}', request.scheme) \
1424 .replace('{size}', safe_str(size))
1429 .replace('{size}', safe_str(size))
1425
1430
1426
1431
1427 def gravatar_url(email_address, size=30, request=None):
1432 def gravatar_url(email_address, size=30, request=None):
1428 request = request or get_current_request()
1433 request = request or get_current_request()
1429 _use_gravatar = request.call_context.visual.use_gravatar
1434 _use_gravatar = request.call_context.visual.use_gravatar
1430
1435
1431 email_address = email_address or User.DEFAULT_USER_EMAIL
1436 email_address = email_address or User.DEFAULT_USER_EMAIL
1432 if isinstance(email_address, unicode):
1437 if isinstance(email_address, unicode):
1433 # hashlib crashes on unicode items
1438 # hashlib crashes on unicode items
1434 email_address = safe_str(email_address)
1439 email_address = safe_str(email_address)
1435
1440
1436 # empty email or default user
1441 # empty email or default user
1437 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1442 if not email_address or email_address == User.DEFAULT_USER_EMAIL:
1438 return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
1443 return initials_gravatar(request, User.DEFAULT_USER_EMAIL, '', '', size=size)
1439
1444
1440 if _use_gravatar:
1445 if _use_gravatar:
1441 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1446 gravatar_url_tmpl = request.call_context.visual.gravatar_url \
1442 or User.DEFAULT_GRAVATAR_URL
1447 or User.DEFAULT_GRAVATAR_URL
1443 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1448 return gravatar_external(request, gravatar_url_tmpl, email_address, size=size)
1444
1449
1445 else:
1450 else:
1446 return initials_gravatar(request, email_address, '', '', size=size)
1451 return initials_gravatar(request, email_address, '', '', size=size)
1447
1452
1448
1453
1449 def breadcrumb_repo_link(repo):
1454 def breadcrumb_repo_link(repo):
1450 """
1455 """
1451 Makes a breadcrumbs path link to repo
1456 Makes a breadcrumbs path link to repo
1452
1457
1453 ex::
1458 ex::
1454 group >> subgroup >> repo
1459 group >> subgroup >> repo
1455
1460
1456 :param repo: a Repository instance
1461 :param repo: a Repository instance
1457 """
1462 """
1458
1463
1459 path = [
1464 path = [
1460 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1465 link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name),
1461 title='last change:{}'.format(format_date(group.last_commit_change)))
1466 title='last change:{}'.format(format_date(group.last_commit_change)))
1462 for group in repo.groups_with_parents
1467 for group in repo.groups_with_parents
1463 ] + [
1468 ] + [
1464 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1469 link_to(repo.just_name, route_path('repo_summary', repo_name=repo.repo_name),
1465 title='last change:{}'.format(format_date(repo.last_commit_change)))
1470 title='last change:{}'.format(format_date(repo.last_commit_change)))
1466 ]
1471 ]
1467
1472
1468 return literal(' &raquo; '.join(path))
1473 return literal(' &raquo; '.join(path))
1469
1474
1470
1475
1471 def breadcrumb_repo_group_link(repo_group):
1476 def breadcrumb_repo_group_link(repo_group):
1472 """
1477 """
1473 Makes a breadcrumbs path link to repo
1478 Makes a breadcrumbs path link to repo
1474
1479
1475 ex::
1480 ex::
1476 group >> subgroup
1481 group >> subgroup
1477
1482
1478 :param repo_group: a Repository Group instance
1483 :param repo_group: a Repository Group instance
1479 """
1484 """
1480
1485
1481 path = [
1486 path = [
1482 link_to(group.name,
1487 link_to(group.name,
1483 route_path('repo_group_home', repo_group_name=group.group_name),
1488 route_path('repo_group_home', repo_group_name=group.group_name),
1484 title='last change:{}'.format(format_date(group.last_commit_change)))
1489 title='last change:{}'.format(format_date(group.last_commit_change)))
1485 for group in repo_group.parents
1490 for group in repo_group.parents
1486 ] + [
1491 ] + [
1487 link_to(repo_group.name,
1492 link_to(repo_group.name,
1488 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1493 route_path('repo_group_home', repo_group_name=repo_group.group_name),
1489 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1494 title='last change:{}'.format(format_date(repo_group.last_commit_change)))
1490 ]
1495 ]
1491
1496
1492 return literal(' &raquo; '.join(path))
1497 return literal(' &raquo; '.join(path))
1493
1498
1494
1499
1495 def format_byte_size_binary(file_size):
1500 def format_byte_size_binary(file_size):
1496 """
1501 """
1497 Formats file/folder sizes to standard.
1502 Formats file/folder sizes to standard.
1498 """
1503 """
1499 if file_size is None:
1504 if file_size is None:
1500 file_size = 0
1505 file_size = 0
1501
1506
1502 formatted_size = format_byte_size(file_size, binary=True)
1507 formatted_size = format_byte_size(file_size, binary=True)
1503 return formatted_size
1508 return formatted_size
1504
1509
1505
1510
1506 def urlify_text(text_, safe=True, **href_attrs):
1511 def urlify_text(text_, safe=True, **href_attrs):
1507 """
1512 """
1508 Extract urls from text and make html links out of them
1513 Extract urls from text and make html links out of them
1509 """
1514 """
1510
1515
1511 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1516 url_pat = re.compile(r'''(http[s]?://(?:[a-zA-Z]|[0-9]|[$-_@#.&+]'''
1512 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1517 '''|[!*\(\),]|(?:%[0-9a-fA-F][0-9a-fA-F]))+)''')
1513
1518
1514 def url_func(match_obj):
1519 def url_func(match_obj):
1515 url_full = match_obj.groups()[0]
1520 url_full = match_obj.groups()[0]
1516 a_options = dict(href_attrs)
1521 a_options = dict(href_attrs)
1517 a_options['href'] = url_full
1522 a_options['href'] = url_full
1518 a_text = url_full
1523 a_text = url_full
1519 return HTML.tag("a", a_text, **a_options)
1524 return HTML.tag("a", a_text, **a_options)
1520
1525
1521 _new_text = url_pat.sub(url_func, text_)
1526 _new_text = url_pat.sub(url_func, text_)
1522
1527
1523 if safe:
1528 if safe:
1524 return literal(_new_text)
1529 return literal(_new_text)
1525 return _new_text
1530 return _new_text
1526
1531
1527
1532
1528 def urlify_commits(text_, repo_name):
1533 def urlify_commits(text_, repo_name):
1529 """
1534 """
1530 Extract commit ids from text and make link from them
1535 Extract commit ids from text and make link from them
1531
1536
1532 :param text_:
1537 :param text_:
1533 :param repo_name: repo name to build the URL with
1538 :param repo_name: repo name to build the URL with
1534 """
1539 """
1535
1540
1536 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1541 url_pat = re.compile(r'(^|\s)([0-9a-fA-F]{12,40})($|\s)')
1537
1542
1538 def url_func(match_obj):
1543 def url_func(match_obj):
1539 commit_id = match_obj.groups()[1]
1544 commit_id = match_obj.groups()[1]
1540 pref = match_obj.groups()[0]
1545 pref = match_obj.groups()[0]
1541 suf = match_obj.groups()[2]
1546 suf = match_obj.groups()[2]
1542
1547
1543 tmpl = (
1548 tmpl = (
1544 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1549 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-alt="%(hovercard_alt)s" data-hovercard-url="%(hovercard_url)s">'
1545 '%(commit_id)s</a>%(suf)s'
1550 '%(commit_id)s</a>%(suf)s'
1546 )
1551 )
1547 return tmpl % {
1552 return tmpl % {
1548 'pref': pref,
1553 'pref': pref,
1549 'cls': 'revision-link',
1554 'cls': 'revision-link',
1550 'url': route_url(
1555 'url': route_url(
1551 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1556 'repo_commit', repo_name=repo_name, commit_id=commit_id),
1552 'commit_id': commit_id,
1557 'commit_id': commit_id,
1553 'suf': suf,
1558 'suf': suf,
1554 'hovercard_alt': 'Commit: {}'.format(commit_id),
1559 'hovercard_alt': 'Commit: {}'.format(commit_id),
1555 'hovercard_url': route_url(
1560 'hovercard_url': route_url(
1556 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1561 'hovercard_repo_commit', repo_name=repo_name, commit_id=commit_id)
1557 }
1562 }
1558
1563
1559 new_text = url_pat.sub(url_func, text_)
1564 new_text = url_pat.sub(url_func, text_)
1560
1565
1561 return new_text
1566 return new_text
1562
1567
1563
1568
1564 def _process_url_func(match_obj, repo_name, uid, entry,
1569 def _process_url_func(match_obj, repo_name, uid, entry,
1565 return_raw_data=False, link_format='html'):
1570 return_raw_data=False, link_format='html'):
1566 pref = ''
1571 pref = ''
1567 if match_obj.group().startswith(' '):
1572 if match_obj.group().startswith(' '):
1568 pref = ' '
1573 pref = ' '
1569
1574
1570 issue_id = ''.join(match_obj.groups())
1575 issue_id = ''.join(match_obj.groups())
1571
1576
1572 if link_format == 'html':
1577 if link_format == 'html':
1573 tmpl = (
1578 tmpl = (
1574 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1579 '%(pref)s<a class="tooltip %(cls)s" href="%(url)s" title="%(title)s">'
1575 '%(issue-prefix)s%(id-repr)s'
1580 '%(issue-prefix)s%(id-repr)s'
1576 '</a>')
1581 '</a>')
1577 elif link_format == 'html+hovercard':
1582 elif link_format == 'html+hovercard':
1578 tmpl = (
1583 tmpl = (
1579 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1584 '%(pref)s<a class="tooltip-hovercard %(cls)s" href="%(url)s" data-hovercard-url="%(hovercard_url)s">'
1580 '%(issue-prefix)s%(id-repr)s'
1585 '%(issue-prefix)s%(id-repr)s'
1581 '</a>')
1586 '</a>')
1582 elif link_format in ['rst', 'rst+hovercard']:
1587 elif link_format in ['rst', 'rst+hovercard']:
1583 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1588 tmpl = '`%(issue-prefix)s%(id-repr)s <%(url)s>`_'
1584 elif link_format in ['markdown', 'markdown+hovercard']:
1589 elif link_format in ['markdown', 'markdown+hovercard']:
1585 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1590 tmpl = '[%(pref)s%(issue-prefix)s%(id-repr)s](%(url)s)'
1586 else:
1591 else:
1587 raise ValueError('Bad link_format:{}'.format(link_format))
1592 raise ValueError('Bad link_format:{}'.format(link_format))
1588
1593
1589 (repo_name_cleaned,
1594 (repo_name_cleaned,
1590 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1595 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(repo_name)
1591
1596
1592 # variables replacement
1597 # variables replacement
1593 named_vars = {
1598 named_vars = {
1594 'id': issue_id,
1599 'id': issue_id,
1595 'repo': repo_name,
1600 'repo': repo_name,
1596 'repo_name': repo_name_cleaned,
1601 'repo_name': repo_name_cleaned,
1597 'group_name': parent_group_name,
1602 'group_name': parent_group_name,
1598 # set dummy keys so we always have them
1603 # set dummy keys so we always have them
1599 'hostname': '',
1604 'hostname': '',
1600 'netloc': '',
1605 'netloc': '',
1601 'scheme': ''
1606 'scheme': ''
1602 }
1607 }
1603
1608
1604 request = get_current_request()
1609 request = get_current_request()
1605 if request:
1610 if request:
1606 # exposes, hostname, netloc, scheme
1611 # exposes, hostname, netloc, scheme
1607 host_data = get_host_info(request)
1612 host_data = get_host_info(request)
1608 named_vars.update(host_data)
1613 named_vars.update(host_data)
1609
1614
1610 # named regex variables
1615 # named regex variables
1611 named_vars.update(match_obj.groupdict())
1616 named_vars.update(match_obj.groupdict())
1612 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1617 _url = string.Template(entry['url']).safe_substitute(**named_vars)
1613 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1618 desc = string.Template(entry['desc']).safe_substitute(**named_vars)
1614 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1619 hovercard_url = string.Template(entry.get('hovercard_url', '')).safe_substitute(**named_vars)
1615
1620
1616 def quote_cleaner(input_str):
1621 def quote_cleaner(input_str):
1617 """Remove quotes as it's HTML"""
1622 """Remove quotes as it's HTML"""
1618 return input_str.replace('"', '')
1623 return input_str.replace('"', '')
1619
1624
1620 data = {
1625 data = {
1621 'pref': pref,
1626 'pref': pref,
1622 'cls': quote_cleaner('issue-tracker-link'),
1627 'cls': quote_cleaner('issue-tracker-link'),
1623 'url': quote_cleaner(_url),
1628 'url': quote_cleaner(_url),
1624 'id-repr': issue_id,
1629 'id-repr': issue_id,
1625 'issue-prefix': entry['pref'],
1630 'issue-prefix': entry['pref'],
1626 'serv': entry['url'],
1631 'serv': entry['url'],
1627 'title': bleach.clean(desc, strip=True),
1632 'title': bleach.clean(desc, strip=True),
1628 'hovercard_url': hovercard_url
1633 'hovercard_url': hovercard_url
1629 }
1634 }
1630
1635
1631 if return_raw_data:
1636 if return_raw_data:
1632 return {
1637 return {
1633 'id': issue_id,
1638 'id': issue_id,
1634 'url': _url
1639 'url': _url
1635 }
1640 }
1636 return tmpl % data
1641 return tmpl % data
1637
1642
1638
1643
1639 def get_active_pattern_entries(repo_name):
1644 def get_active_pattern_entries(repo_name):
1640 repo = None
1645 repo = None
1641 if repo_name:
1646 if repo_name:
1642 # Retrieving repo_name to avoid invalid repo_name to explode on
1647 # Retrieving repo_name to avoid invalid repo_name to explode on
1643 # IssueTrackerSettingsModel but still passing invalid name further down
1648 # IssueTrackerSettingsModel but still passing invalid name further down
1644 repo = Repository.get_by_repo_name(repo_name, cache=True)
1649 repo = Repository.get_by_repo_name(repo_name, cache=True)
1645
1650
1646 settings_model = IssueTrackerSettingsModel(repo=repo)
1651 settings_model = IssueTrackerSettingsModel(repo=repo)
1647 active_entries = settings_model.get_settings(cache=True)
1652 active_entries = settings_model.get_settings(cache=True)
1648 return active_entries
1653 return active_entries
1649
1654
1650
1655
1651 pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
1656 pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
1652
1657
1658 allowed_link_formats = [
1659 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1660
1653
1661
1654 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1662 def process_patterns(text_string, repo_name, link_format='html', active_entries=None):
1655
1663
1656 allowed_formats = ['html', 'rst', 'markdown',
1664 if link_format not in allowed_link_formats:
1657 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
1658 if link_format not in allowed_formats:
1659 raise ValueError('Link format can be only one of:{} got {}'.format(
1665 raise ValueError('Link format can be only one of:{} got {}'.format(
1660 allowed_formats, link_format))
1666 allowed_link_formats, link_format))
1661
1667
1662 if active_entries is None:
1668 if active_entries is None:
1663 log.debug('Fetch active patterns for repo: %s', repo_name)
1669 log.debug('Fetch active issue tracker patterns for repo: %s', repo_name)
1664 active_entries = get_active_pattern_entries(repo_name)
1670 active_entries = get_active_pattern_entries(repo_name)
1665
1671
1666 issues_data = []
1672 issues_data = []
1667 new_text = text_string
1673 new_text = text_string
1668
1674
1669 log.debug('Got %s entries to process', len(active_entries))
1675 log.debug('Got %s entries to process', len(active_entries))
1670 for uid, entry in active_entries.items():
1676 for uid, entry in active_entries.items():
1671 log.debug('found issue tracker entry with uid %s', uid)
1677 log.debug('found issue tracker entry with uid %s', uid)
1672
1678
1673 if not (entry['pat'] and entry['url']):
1679 if not (entry['pat'] and entry['url']):
1674 log.debug('skipping due to missing data')
1680 log.debug('skipping due to missing data')
1675 continue
1681 continue
1676
1682
1677 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1683 log.debug('issue tracker entry: uid: `%s` PAT:%s URL:%s PREFIX:%s',
1678 uid, entry['pat'], entry['url'], entry['pref'])
1684 uid, entry['pat'], entry['url'], entry['pref'])
1679
1685
1680 if entry.get('pat_compiled'):
1686 if entry.get('pat_compiled'):
1681 pattern = entry['pat_compiled']
1687 pattern = entry['pat_compiled']
1682 else:
1688 else:
1683 try:
1689 try:
1684 pattern = re.compile(r'%s' % entry['pat'])
1690 pattern = re.compile(r'%s' % entry['pat'])
1685 except re.error:
1691 except re.error:
1686 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1692 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1687 continue
1693 continue
1688
1694
1689 data_func = partial(
1695 data_func = partial(
1690 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1696 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1691 return_raw_data=True)
1697 return_raw_data=True)
1692
1698
1693 for match_obj in pattern.finditer(text_string):
1699 for match_obj in pattern.finditer(text_string):
1694 issues_data.append(data_func(match_obj))
1700 issues_data.append(data_func(match_obj))
1695
1701
1696 url_func = partial(
1702 url_func = partial(
1697 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1703 _process_url_func, repo_name=repo_name, entry=entry, uid=uid,
1698 link_format=link_format)
1704 link_format=link_format)
1699
1705
1700 new_text = pattern.sub(url_func, new_text)
1706 new_text = pattern.sub(url_func, new_text)
1701 log.debug('processed prefix:uid `%s`', uid)
1707 log.debug('processed prefix:uid `%s`', uid)
1702
1708
1703 # finally use global replace, eg !123 -> pr-link, those will not catch
1709 # finally use global replace, eg !123 -> pr-link, those will not catch
1704 # if already similar pattern exists
1710 # if already similar pattern exists
1705 server_url = '${scheme}://${netloc}'
1711 server_url = '${scheme}://${netloc}'
1706 pr_entry = {
1712 pr_entry = {
1707 'pref': '!',
1713 'pref': '!',
1708 'url': server_url + '/_admin/pull-requests/${id}',
1714 'url': server_url + '/_admin/pull-requests/${id}',
1709 'desc': 'Pull Request !${id}',
1715 'desc': 'Pull Request !${id}',
1710 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1716 'hovercard_url': server_url + '/_hovercard/pull_request/${id}'
1711 }
1717 }
1712 pr_url_func = partial(
1718 pr_url_func = partial(
1713 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1719 _process_url_func, repo_name=repo_name, entry=pr_entry, uid=None,
1714 link_format=link_format+'+hovercard')
1720 link_format=link_format+'+hovercard')
1715 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1721 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1716 log.debug('processed !pr pattern')
1722 log.debug('processed !pr pattern')
1717
1723
1718 return new_text, issues_data
1724 return new_text, issues_data
1719
1725
1720
1726
1721 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None):
1727 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1728 issues_container=None):
1722 """
1729 """
1723 Parses given text message and makes proper links.
1730 Parses given text message and makes proper links.
1724 issues are linked to given issue-server, and rest is a commit link
1731 issues are linked to given issue-server, and rest is a commit link
1725 """
1732 """
1726
1733
1727 def escaper(_text):
1734 def escaper(_text):
1728 return _text.replace('<', '&lt;').replace('>', '&gt;')
1735 return _text.replace('<', '&lt;').replace('>', '&gt;')
1729
1736
1730 new_text = escaper(commit_text)
1737 new_text = escaper(commit_text)
1731
1738
1732 # extract http/https links and make them real urls
1739 # extract http/https links and make them real urls
1733 new_text = urlify_text(new_text, safe=False)
1740 new_text = urlify_text(new_text, safe=False)
1734
1741
1735 # urlify commits - extract commit ids and make link out of them, if we have
1742 # urlify commits - extract commit ids and make link out of them, if we have
1736 # the scope of repository present.
1743 # the scope of repository present.
1737 if repository:
1744 if repository:
1738 new_text = urlify_commits(new_text, repository)
1745 new_text = urlify_commits(new_text, repository)
1739
1746
1740 # process issue tracker patterns
1747 # process issue tracker patterns
1741 new_text, issues = process_patterns(new_text, repository or '',
1748 new_text, issues = process_patterns(new_text, repository or '',
1742 active_entries=active_pattern_entries)
1749 active_entries=active_pattern_entries)
1743
1750
1751 if issues_container is not None:
1752 issues_container.extend(issues)
1753
1744 return literal(new_text)
1754 return literal(new_text)
1745
1755
1746
1756
1747 def render_binary(repo_name, file_obj):
1757 def render_binary(repo_name, file_obj):
1748 """
1758 """
1749 Choose how to render a binary file
1759 Choose how to render a binary file
1750 """
1760 """
1751
1761
1752 # unicode
1762 # unicode
1753 filename = file_obj.name
1763 filename = file_obj.name
1754
1764
1755 # images
1765 # images
1756 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1766 for ext in ['*.png', '*.jpeg', '*.jpg', '*.ico', '*.gif']:
1757 if fnmatch.fnmatch(filename, pat=ext):
1767 if fnmatch.fnmatch(filename, pat=ext):
1758 src = route_path(
1768 src = route_path(
1759 'repo_file_raw', repo_name=repo_name,
1769 'repo_file_raw', repo_name=repo_name,
1760 commit_id=file_obj.commit.raw_id,
1770 commit_id=file_obj.commit.raw_id,
1761 f_path=file_obj.path)
1771 f_path=file_obj.path)
1762
1772
1763 return literal(
1773 return literal(
1764 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1774 '<img class="rendered-binary" alt="rendered-image" src="{}">'.format(src))
1765
1775
1766
1776
1767 def renderer_from_filename(filename, exclude=None):
1777 def renderer_from_filename(filename, exclude=None):
1768 """
1778 """
1769 choose a renderer based on filename, this works only for text based files
1779 choose a renderer based on filename, this works only for text based files
1770 """
1780 """
1771
1781
1772 # ipython
1782 # ipython
1773 for ext in ['*.ipynb']:
1783 for ext in ['*.ipynb']:
1774 if fnmatch.fnmatch(filename, pat=ext):
1784 if fnmatch.fnmatch(filename, pat=ext):
1775 return 'jupyter'
1785 return 'jupyter'
1776
1786
1777 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1787 is_markup = MarkupRenderer.renderer_from_filename(filename, exclude=exclude)
1778 if is_markup:
1788 if is_markup:
1779 return is_markup
1789 return is_markup
1780 return None
1790 return None
1781
1791
1782
1792
1783 def render(source, renderer='rst', mentions=False, relative_urls=None,
1793 def render(source, renderer='rst', mentions=False, relative_urls=None,
1784 repo_name=None, active_pattern_entries=None):
1794 repo_name=None, active_pattern_entries=None, issues_container=None):
1785
1795
1786 def maybe_convert_relative_links(html_source):
1796 def maybe_convert_relative_links(html_source):
1787 if relative_urls:
1797 if relative_urls:
1788 return relative_links(html_source, relative_urls)
1798 return relative_links(html_source, relative_urls)
1789 return html_source
1799 return html_source
1790
1800
1791 if renderer == 'plain':
1801 if renderer == 'plain':
1792 return literal(
1802 return literal(
1793 MarkupRenderer.plain(source, leading_newline=False))
1803 MarkupRenderer.plain(source, leading_newline=False))
1794
1804
1795 elif renderer == 'rst':
1805 elif renderer == 'rst':
1796 if repo_name:
1806 if repo_name:
1797 # process patterns on comments if we pass in repo name
1807 # process patterns on comments if we pass in repo name
1798 source, issues = process_patterns(
1808 source, issues = process_patterns(
1799 source, repo_name, link_format='rst',
1809 source, repo_name, link_format='rst',
1800 active_entries=active_pattern_entries)
1810 active_entries=active_pattern_entries)
1811 if issues_container is not None:
1812 issues_container.extend(issues)
1801
1813
1802 return literal(
1814 return literal(
1803 '<div class="rst-block">%s</div>' %
1815 '<div class="rst-block">%s</div>' %
1804 maybe_convert_relative_links(
1816 maybe_convert_relative_links(
1805 MarkupRenderer.rst(source, mentions=mentions)))
1817 MarkupRenderer.rst(source, mentions=mentions)))
1806
1818
1807 elif renderer == 'markdown':
1819 elif renderer == 'markdown':
1808 if repo_name:
1820 if repo_name:
1809 # process patterns on comments if we pass in repo name
1821 # process patterns on comments if we pass in repo name
1810 source, issues = process_patterns(
1822 source, issues = process_patterns(
1811 source, repo_name, link_format='markdown',
1823 source, repo_name, link_format='markdown',
1812 active_entries=active_pattern_entries)
1824 active_entries=active_pattern_entries)
1825 if issues_container is not None:
1826 issues_container.extend(issues)
1813
1827
1814 return literal(
1828 return literal(
1815 '<div class="markdown-block">%s</div>' %
1829 '<div class="markdown-block">%s</div>' %
1816 maybe_convert_relative_links(
1830 maybe_convert_relative_links(
1817 MarkupRenderer.markdown(source, flavored=True,
1831 MarkupRenderer.markdown(source, flavored=True,
1818 mentions=mentions)))
1832 mentions=mentions)))
1819
1833
1820 elif renderer == 'jupyter':
1834 elif renderer == 'jupyter':
1821 return literal(
1835 return literal(
1822 '<div class="ipynb">%s</div>' %
1836 '<div class="ipynb">%s</div>' %
1823 maybe_convert_relative_links(
1837 maybe_convert_relative_links(
1824 MarkupRenderer.jupyter(source)))
1838 MarkupRenderer.jupyter(source)))
1825
1839
1826 # None means just show the file-source
1840 # None means just show the file-source
1827 return None
1841 return None
1828
1842
1829
1843
1830 def commit_status(repo, commit_id):
1844 def commit_status(repo, commit_id):
1831 return ChangesetStatusModel().get_status(repo, commit_id)
1845 return ChangesetStatusModel().get_status(repo, commit_id)
1832
1846
1833
1847
1834 def commit_status_lbl(commit_status):
1848 def commit_status_lbl(commit_status):
1835 return dict(ChangesetStatus.STATUSES).get(commit_status)
1849 return dict(ChangesetStatus.STATUSES).get(commit_status)
1836
1850
1837
1851
1838 def commit_time(repo_name, commit_id):
1852 def commit_time(repo_name, commit_id):
1839 repo = Repository.get_by_repo_name(repo_name)
1853 repo = Repository.get_by_repo_name(repo_name)
1840 commit = repo.get_commit(commit_id=commit_id)
1854 commit = repo.get_commit(commit_id=commit_id)
1841 return commit.date
1855 return commit.date
1842
1856
1843
1857
1844 def get_permission_name(key):
1858 def get_permission_name(key):
1845 return dict(Permission.PERMS).get(key)
1859 return dict(Permission.PERMS).get(key)
1846
1860
1847
1861
1848 def journal_filter_help(request):
1862 def journal_filter_help(request):
1849 _ = request.translate
1863 _ = request.translate
1850 from rhodecode.lib.audit_logger import ACTIONS
1864 from rhodecode.lib.audit_logger import ACTIONS
1851 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1865 actions = '\n'.join(textwrap.wrap(', '.join(sorted(ACTIONS.keys())), 80))
1852
1866
1853 return _(
1867 return _(
1854 'Example filter terms:\n' +
1868 'Example filter terms:\n' +
1855 ' repository:vcs\n' +
1869 ' repository:vcs\n' +
1856 ' username:marcin\n' +
1870 ' username:marcin\n' +
1857 ' username:(NOT marcin)\n' +
1871 ' username:(NOT marcin)\n' +
1858 ' action:*push*\n' +
1872 ' action:*push*\n' +
1859 ' ip:127.0.0.1\n' +
1873 ' ip:127.0.0.1\n' +
1860 ' date:20120101\n' +
1874 ' date:20120101\n' +
1861 ' date:[20120101100000 TO 20120102]\n' +
1875 ' date:[20120101100000 TO 20120102]\n' +
1862 '\n' +
1876 '\n' +
1863 'Actions: {actions}\n' +
1877 'Actions: {actions}\n' +
1864 '\n' +
1878 '\n' +
1865 'Generate wildcards using \'*\' character:\n' +
1879 'Generate wildcards using \'*\' character:\n' +
1866 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1880 ' "repository:vcs*" - search everything starting with \'vcs\'\n' +
1867 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1881 ' "repository:*vcs*" - search for repository containing \'vcs\'\n' +
1868 '\n' +
1882 '\n' +
1869 'Optional AND / OR operators in queries\n' +
1883 'Optional AND / OR operators in queries\n' +
1870 ' "repository:vcs OR repository:test"\n' +
1884 ' "repository:vcs OR repository:test"\n' +
1871 ' "username:test AND repository:test*"\n'
1885 ' "username:test AND repository:test*"\n'
1872 ).format(actions=actions)
1886 ).format(actions=actions)
1873
1887
1874
1888
1875 def not_mapped_error(repo_name):
1889 def not_mapped_error(repo_name):
1876 from rhodecode.translation import _
1890 from rhodecode.translation import _
1877 flash(_('%s repository is not mapped to db perhaps'
1891 flash(_('%s repository is not mapped to db perhaps'
1878 ' it was created or renamed from the filesystem'
1892 ' it was created or renamed from the filesystem'
1879 ' please run the application again'
1893 ' please run the application again'
1880 ' in order to rescan repositories') % repo_name, category='error')
1894 ' in order to rescan repositories') % repo_name, category='error')
1881
1895
1882
1896
1883 def ip_range(ip_addr):
1897 def ip_range(ip_addr):
1884 from rhodecode.model.db import UserIpMap
1898 from rhodecode.model.db import UserIpMap
1885 s, e = UserIpMap._get_ip_range(ip_addr)
1899 s, e = UserIpMap._get_ip_range(ip_addr)
1886 return '%s - %s' % (s, e)
1900 return '%s - %s' % (s, e)
1887
1901
1888
1902
1889 def form(url, method='post', needs_csrf_token=True, **attrs):
1903 def form(url, method='post', needs_csrf_token=True, **attrs):
1890 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1904 """Wrapper around webhelpers.tags.form to prevent CSRF attacks."""
1891 if method.lower() != 'get' and needs_csrf_token:
1905 if method.lower() != 'get' and needs_csrf_token:
1892 raise Exception(
1906 raise Exception(
1893 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1907 'Forms to POST/PUT/DELETE endpoints should have (in general) a ' +
1894 'CSRF token. If the endpoint does not require such token you can ' +
1908 'CSRF token. If the endpoint does not require such token you can ' +
1895 'explicitly set the parameter needs_csrf_token to false.')
1909 'explicitly set the parameter needs_csrf_token to false.')
1896
1910
1897 return insecure_form(url, method=method, **attrs)
1911 return insecure_form(url, method=method, **attrs)
1898
1912
1899
1913
1900 def secure_form(form_url, method="POST", multipart=False, **attrs):
1914 def secure_form(form_url, method="POST", multipart=False, **attrs):
1901 """Start a form tag that points the action to an url. This
1915 """Start a form tag that points the action to an url. This
1902 form tag will also include the hidden field containing
1916 form tag will also include the hidden field containing
1903 the auth token.
1917 the auth token.
1904
1918
1905 The url options should be given either as a string, or as a
1919 The url options should be given either as a string, or as a
1906 ``url()`` function. The method for the form defaults to POST.
1920 ``url()`` function. The method for the form defaults to POST.
1907
1921
1908 Options:
1922 Options:
1909
1923
1910 ``multipart``
1924 ``multipart``
1911 If set to True, the enctype is set to "multipart/form-data".
1925 If set to True, the enctype is set to "multipart/form-data".
1912 ``method``
1926 ``method``
1913 The method to use when submitting the form, usually either
1927 The method to use when submitting the form, usually either
1914 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1928 "GET" or "POST". If "PUT", "DELETE", or another verb is used, a
1915 hidden input with name _method is added to simulate the verb
1929 hidden input with name _method is added to simulate the verb
1916 over POST.
1930 over POST.
1917
1931
1918 """
1932 """
1919
1933
1920 if 'request' in attrs:
1934 if 'request' in attrs:
1921 session = attrs['request'].session
1935 session = attrs['request'].session
1922 del attrs['request']
1936 del attrs['request']
1923 else:
1937 else:
1924 raise ValueError(
1938 raise ValueError(
1925 'Calling this form requires request= to be passed as argument')
1939 'Calling this form requires request= to be passed as argument')
1926
1940
1927 _form = insecure_form(form_url, method, multipart, **attrs)
1941 _form = insecure_form(form_url, method, multipart, **attrs)
1928 token = literal(
1942 token = literal(
1929 '<input type="hidden" name="{}" value="{}">'.format(
1943 '<input type="hidden" name="{}" value="{}">'.format(
1930 csrf_token_key, get_csrf_token(session)))
1944 csrf_token_key, get_csrf_token(session)))
1931
1945
1932 return literal("%s\n%s" % (_form, token))
1946 return literal("%s\n%s" % (_form, token))
1933
1947
1934
1948
1935 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1949 def dropdownmenu(name, selected, options, enable_filter=False, **attrs):
1936 select_html = select(name, selected, options, **attrs)
1950 select_html = select(name, selected, options, **attrs)
1937
1951
1938 select2 = """
1952 select2 = """
1939 <script>
1953 <script>
1940 $(document).ready(function() {
1954 $(document).ready(function() {
1941 $('#%s').select2({
1955 $('#%s').select2({
1942 containerCssClass: 'drop-menu %s',
1956 containerCssClass: 'drop-menu %s',
1943 dropdownCssClass: 'drop-menu-dropdown',
1957 dropdownCssClass: 'drop-menu-dropdown',
1944 dropdownAutoWidth: true%s
1958 dropdownAutoWidth: true%s
1945 });
1959 });
1946 });
1960 });
1947 </script>
1961 </script>
1948 """
1962 """
1949
1963
1950 filter_option = """,
1964 filter_option = """,
1951 minimumResultsForSearch: -1
1965 minimumResultsForSearch: -1
1952 """
1966 """
1953 input_id = attrs.get('id') or name
1967 input_id = attrs.get('id') or name
1954 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1968 extra_classes = ' '.join(attrs.pop('extra_classes', []))
1955 filter_enabled = "" if enable_filter else filter_option
1969 filter_enabled = "" if enable_filter else filter_option
1956 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1970 select_script = literal(select2 % (input_id, extra_classes, filter_enabled))
1957
1971
1958 return literal(select_html+select_script)
1972 return literal(select_html+select_script)
1959
1973
1960
1974
1961 def get_visual_attr(tmpl_context_var, attr_name):
1975 def get_visual_attr(tmpl_context_var, attr_name):
1962 """
1976 """
1963 A safe way to get a variable from visual variable of template context
1977 A safe way to get a variable from visual variable of template context
1964
1978
1965 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1979 :param tmpl_context_var: instance of tmpl_context, usually present as `c`
1966 :param attr_name: name of the attribute we fetch from the c.visual
1980 :param attr_name: name of the attribute we fetch from the c.visual
1967 """
1981 """
1968 visual = getattr(tmpl_context_var, 'visual', None)
1982 visual = getattr(tmpl_context_var, 'visual', None)
1969 if not visual:
1983 if not visual:
1970 return
1984 return
1971 else:
1985 else:
1972 return getattr(visual, attr_name, None)
1986 return getattr(visual, attr_name, None)
1973
1987
1974
1988
1975 def get_last_path_part(file_node):
1989 def get_last_path_part(file_node):
1976 if not file_node.path:
1990 if not file_node.path:
1977 return u'/'
1991 return u'/'
1978
1992
1979 path = safe_unicode(file_node.path.split('/')[-1])
1993 path = safe_unicode(file_node.path.split('/')[-1])
1980 return u'../' + path
1994 return u'../' + path
1981
1995
1982
1996
1983 def route_url(*args, **kwargs):
1997 def route_url(*args, **kwargs):
1984 """
1998 """
1985 Wrapper around pyramids `route_url` (fully qualified url) function.
1999 Wrapper around pyramids `route_url` (fully qualified url) function.
1986 """
2000 """
1987 req = get_current_request()
2001 req = get_current_request()
1988 return req.route_url(*args, **kwargs)
2002 return req.route_url(*args, **kwargs)
1989
2003
1990
2004
1991 def route_path(*args, **kwargs):
2005 def route_path(*args, **kwargs):
1992 """
2006 """
1993 Wrapper around pyramids `route_path` function.
2007 Wrapper around pyramids `route_path` function.
1994 """
2008 """
1995 req = get_current_request()
2009 req = get_current_request()
1996 return req.route_path(*args, **kwargs)
2010 return req.route_path(*args, **kwargs)
1997
2011
1998
2012
1999 def route_path_or_none(*args, **kwargs):
2013 def route_path_or_none(*args, **kwargs):
2000 try:
2014 try:
2001 return route_path(*args, **kwargs)
2015 return route_path(*args, **kwargs)
2002 except KeyError:
2016 except KeyError:
2003 return None
2017 return None
2004
2018
2005
2019
2006 def current_route_path(request, **kw):
2020 def current_route_path(request, **kw):
2007 new_args = request.GET.mixed()
2021 new_args = request.GET.mixed()
2008 new_args.update(kw)
2022 new_args.update(kw)
2009 return request.current_route_path(_query=new_args)
2023 return request.current_route_path(_query=new_args)
2010
2024
2011
2025
2012 def curl_api_example(method, args):
2026 def curl_api_example(method, args):
2013 args_json = json.dumps(OrderedDict([
2027 args_json = json.dumps(OrderedDict([
2014 ('id', 1),
2028 ('id', 1),
2015 ('auth_token', 'SECRET'),
2029 ('auth_token', 'SECRET'),
2016 ('method', method),
2030 ('method', method),
2017 ('args', args)
2031 ('args', args)
2018 ]))
2032 ]))
2019
2033
2020 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2034 return "curl {api_url} -X POST -H 'content-type:text/plain' --data-binary '{args_json}'".format(
2021 api_url=route_url('apiv2'),
2035 api_url=route_url('apiv2'),
2022 args_json=args_json
2036 args_json=args_json
2023 )
2037 )
2024
2038
2025
2039
2026 def api_call_example(method, args):
2040 def api_call_example(method, args):
2027 """
2041 """
2028 Generates an API call example via CURL
2042 Generates an API call example via CURL
2029 """
2043 """
2030 curl_call = curl_api_example(method, args)
2044 curl_call = curl_api_example(method, args)
2031
2045
2032 return literal(
2046 return literal(
2033 curl_call +
2047 curl_call +
2034 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2048 "<br/><br/>SECRET can be found in <a href=\"{token_url}\">auth-tokens</a> page, "
2035 "and needs to be of `api calls` role."
2049 "and needs to be of `api calls` role."
2036 .format(token_url=route_url('my_account_auth_tokens')))
2050 .format(token_url=route_url('my_account_auth_tokens')))
2037
2051
2038
2052
2039 def notification_description(notification, request):
2053 def notification_description(notification, request):
2040 """
2054 """
2041 Generate notification human readable description based on notification type
2055 Generate notification human readable description based on notification type
2042 """
2056 """
2043 from rhodecode.model.notification import NotificationModel
2057 from rhodecode.model.notification import NotificationModel
2044 return NotificationModel().make_description(
2058 return NotificationModel().make_description(
2045 notification, translate=request.translate)
2059 notification, translate=request.translate)
2046
2060
2047
2061
2048 def go_import_header(request, db_repo=None):
2062 def go_import_header(request, db_repo=None):
2049 """
2063 """
2050 Creates a header for go-import functionality in Go Lang
2064 Creates a header for go-import functionality in Go Lang
2051 """
2065 """
2052
2066
2053 if not db_repo:
2067 if not db_repo:
2054 return
2068 return
2055 if 'go-get' not in request.GET:
2069 if 'go-get' not in request.GET:
2056 return
2070 return
2057
2071
2058 clone_url = db_repo.clone_url()
2072 clone_url = db_repo.clone_url()
2059 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2073 prefix = re.split(r'^https?:\/\/', clone_url)[-1]
2060 # we have a repo and go-get flag,
2074 # we have a repo and go-get flag,
2061 return literal('<meta name="go-import" content="{} {} {}">'.format(
2075 return literal('<meta name="go-import" content="{} {} {}">'.format(
2062 prefix, db_repo.repo_type, clone_url))
2076 prefix, db_repo.repo_type, clone_url))
2063
2077
2064
2078
2065 def reviewer_as_json(*args, **kwargs):
2079 def reviewer_as_json(*args, **kwargs):
2066 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2080 from rhodecode.apps.repository.utils import reviewer_as_json as _reviewer_as_json
2067 return _reviewer_as_json(*args, **kwargs)
2081 return _reviewer_as_json(*args, **kwargs)
2068
2082
2069
2083
2070 def get_repo_view_type(request):
2084 def get_repo_view_type(request):
2071 route_name = request.matched_route.name
2085 route_name = request.matched_route.name
2072 route_to_view_type = {
2086 route_to_view_type = {
2073 'repo_changelog': 'commits',
2087 'repo_changelog': 'commits',
2074 'repo_commits': 'commits',
2088 'repo_commits': 'commits',
2075 'repo_files': 'files',
2089 'repo_files': 'files',
2076 'repo_summary': 'summary',
2090 'repo_summary': 'summary',
2077 'repo_commit': 'commit'
2091 'repo_commit': 'commit'
2078 }
2092 }
2079
2093
2080 return route_to_view_type.get(route_name)
2094 return route_to_view_type.get(route_name)
2081
2095
2082
2096
2083 def is_active(menu_entry, selected):
2097 def is_active(menu_entry, selected):
2084 """
2098 """
2085 Returns active class for selecting menus in templates
2099 Returns active class for selecting menus in templates
2086 <li class=${h.is_active('settings', current_active)}></li>
2100 <li class=${h.is_active('settings', current_active)}></li>
2087 """
2101 """
2088 if not isinstance(menu_entry, list):
2102 if not isinstance(menu_entry, list):
2089 menu_entry = [menu_entry]
2103 menu_entry = [menu_entry]
2090
2104
2091 if selected in menu_entry:
2105 if selected in menu_entry:
2092 return "active"
2106 return "active"
@@ -1,840 +1,855 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 ChangesetComment,
40 ChangesetComment,
41 User,
41 User,
42 Notification,
42 Notification,
43 PullRequest,
43 PullRequest,
44 AttributeDict,
44 AttributeDict,
45 ChangesetCommentHistory,
45 ChangesetCommentHistory,
46 )
46 )
47 from rhodecode.model.notification import NotificationModel
47 from rhodecode.model.notification import NotificationModel
48 from rhodecode.model.meta import Session
48 from rhodecode.model.meta import Session
49 from rhodecode.model.settings import VcsSettingsModel
49 from rhodecode.model.settings import VcsSettingsModel
50 from rhodecode.model.notification import EmailNotificationModel
50 from rhodecode.model.notification import EmailNotificationModel
51 from rhodecode.model.validation_schema.schemas import comment_schema
51 from rhodecode.model.validation_schema.schemas import comment_schema
52
52
53
53
54 log = logging.getLogger(__name__)
54 log = logging.getLogger(__name__)
55
55
56
56
57 class CommentsModel(BaseModel):
57 class CommentsModel(BaseModel):
58
58
59 cls = ChangesetComment
59 cls = ChangesetComment
60
60
61 DIFF_CONTEXT_BEFORE = 3
61 DIFF_CONTEXT_BEFORE = 3
62 DIFF_CONTEXT_AFTER = 3
62 DIFF_CONTEXT_AFTER = 3
63
63
64 def __get_commit_comment(self, changeset_comment):
64 def __get_commit_comment(self, changeset_comment):
65 return self._get_instance(ChangesetComment, changeset_comment)
65 return self._get_instance(ChangesetComment, changeset_comment)
66
66
67 def __get_pull_request(self, pull_request):
67 def __get_pull_request(self, pull_request):
68 return self._get_instance(PullRequest, pull_request)
68 return self._get_instance(PullRequest, pull_request)
69
69
70 def _extract_mentions(self, s):
70 def _extract_mentions(self, s):
71 user_objects = []
71 user_objects = []
72 for username in extract_mentioned_users(s):
72 for username in extract_mentioned_users(s):
73 user_obj = User.get_by_username(username, case_insensitive=True)
73 user_obj = User.get_by_username(username, case_insensitive=True)
74 if user_obj:
74 if user_obj:
75 user_objects.append(user_obj)
75 user_objects.append(user_obj)
76 return user_objects
76 return user_objects
77
77
78 def _get_renderer(self, global_renderer='rst', request=None):
78 def _get_renderer(self, global_renderer='rst', request=None):
79 request = request or get_current_request()
79 request = request or get_current_request()
80
80
81 try:
81 try:
82 global_renderer = request.call_context.visual.default_renderer
82 global_renderer = request.call_context.visual.default_renderer
83 except AttributeError:
83 except AttributeError:
84 log.debug("Renderer not set, falling back "
84 log.debug("Renderer not set, falling back "
85 "to default renderer '%s'", global_renderer)
85 "to default renderer '%s'", global_renderer)
86 except Exception:
86 except Exception:
87 log.error(traceback.format_exc())
87 log.error(traceback.format_exc())
88 return global_renderer
88 return global_renderer
89
89
90 def aggregate_comments(self, comments, versions, show_version, inline=False):
90 def aggregate_comments(self, comments, versions, show_version, inline=False):
91 # group by versions, and count until, and display objects
91 # group by versions, and count until, and display objects
92
92
93 comment_groups = collections.defaultdict(list)
93 comment_groups = collections.defaultdict(list)
94 [comment_groups[
94 [comment_groups[_co.pull_request_version_id].append(_co) for _co in comments]
95 _co.pull_request_version_id].append(_co) for _co in comments]
96
95
97 def yield_comments(pos):
96 def yield_comments(pos):
98 for co in comment_groups[pos]:
97 for co in comment_groups[pos]:
99 yield co
98 yield co
100
99
101 comment_versions = collections.defaultdict(
100 comment_versions = collections.defaultdict(
102 lambda: collections.defaultdict(list))
101 lambda: collections.defaultdict(list))
103 prev_prvid = -1
102 prev_prvid = -1
104 # fake last entry with None, to aggregate on "latest" version which
103 # fake last entry with None, to aggregate on "latest" version which
105 # doesn't have an pull_request_version_id
104 # doesn't have an pull_request_version_id
106 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
105 for ver in versions + [AttributeDict({'pull_request_version_id': None})]:
107 prvid = ver.pull_request_version_id
106 prvid = ver.pull_request_version_id
108 if prev_prvid == -1:
107 if prev_prvid == -1:
109 prev_prvid = prvid
108 prev_prvid = prvid
110
109
111 for co in yield_comments(prvid):
110 for co in yield_comments(prvid):
112 comment_versions[prvid]['at'].append(co)
111 comment_versions[prvid]['at'].append(co)
113
112
114 # save until
113 # save until
115 current = comment_versions[prvid]['at']
114 current = comment_versions[prvid]['at']
116 prev_until = comment_versions[prev_prvid]['until']
115 prev_until = comment_versions[prev_prvid]['until']
117 cur_until = prev_until + current
116 cur_until = prev_until + current
118 comment_versions[prvid]['until'].extend(cur_until)
117 comment_versions[prvid]['until'].extend(cur_until)
119
118
120 # save outdated
119 # save outdated
121 if inline:
120 if inline:
122 outdated = [x for x in cur_until
121 outdated = [x for x in cur_until
123 if x.outdated_at_version(show_version)]
122 if x.outdated_at_version(show_version)]
124 else:
123 else:
125 outdated = [x for x in cur_until
124 outdated = [x for x in cur_until
126 if x.older_than_version(show_version)]
125 if x.older_than_version(show_version)]
127 display = [x for x in cur_until if x not in outdated]
126 display = [x for x in cur_until if x not in outdated]
128
127
129 comment_versions[prvid]['outdated'] = outdated
128 comment_versions[prvid]['outdated'] = outdated
130 comment_versions[prvid]['display'] = display
129 comment_versions[prvid]['display'] = display
131
130
132 prev_prvid = prvid
131 prev_prvid = prvid
133
132
134 return comment_versions
133 return comment_versions
135
134
136 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
135 def get_repository_comments(self, repo, comment_type=None, user=None, commit_id=None):
137 qry = Session().query(ChangesetComment) \
136 qry = Session().query(ChangesetComment) \
138 .filter(ChangesetComment.repo == repo)
137 .filter(ChangesetComment.repo == repo)
139
138
140 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
139 if comment_type and comment_type in ChangesetComment.COMMENT_TYPES:
141 qry = qry.filter(ChangesetComment.comment_type == comment_type)
140 qry = qry.filter(ChangesetComment.comment_type == comment_type)
142
141
143 if user:
142 if user:
144 user = self._get_user(user)
143 user = self._get_user(user)
145 if user:
144 if user:
146 qry = qry.filter(ChangesetComment.user_id == user.user_id)
145 qry = qry.filter(ChangesetComment.user_id == user.user_id)
147
146
148 if commit_id:
147 if commit_id:
149 qry = qry.filter(ChangesetComment.revision == commit_id)
148 qry = qry.filter(ChangesetComment.revision == commit_id)
150
149
151 qry = qry.order_by(ChangesetComment.created_on)
150 qry = qry.order_by(ChangesetComment.created_on)
152 return qry.all()
151 return qry.all()
153
152
154 def get_repository_unresolved_todos(self, repo):
153 def get_repository_unresolved_todos(self, repo):
155 todos = Session().query(ChangesetComment) \
154 todos = Session().query(ChangesetComment) \
156 .filter(ChangesetComment.repo == repo) \
155 .filter(ChangesetComment.repo == repo) \
157 .filter(ChangesetComment.resolved_by == None) \
156 .filter(ChangesetComment.resolved_by == None) \
158 .filter(ChangesetComment.comment_type
157 .filter(ChangesetComment.comment_type
159 == ChangesetComment.COMMENT_TYPE_TODO)
158 == ChangesetComment.COMMENT_TYPE_TODO)
160 todos = todos.all()
159 todos = todos.all()
161
160
162 return todos
161 return todos
163
162
164 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
163 def get_pull_request_unresolved_todos(self, pull_request, show_outdated=True):
165
164
166 todos = Session().query(ChangesetComment) \
165 todos = Session().query(ChangesetComment) \
167 .filter(ChangesetComment.pull_request == pull_request) \
166 .filter(ChangesetComment.pull_request == pull_request) \
168 .filter(ChangesetComment.resolved_by == None) \
167 .filter(ChangesetComment.resolved_by == None) \
169 .filter(ChangesetComment.comment_type
168 .filter(ChangesetComment.comment_type
170 == ChangesetComment.COMMENT_TYPE_TODO)
169 == ChangesetComment.COMMENT_TYPE_TODO)
171
170
172 if not show_outdated:
171 if not show_outdated:
173 todos = todos.filter(
172 todos = todos.filter(
174 coalesce(ChangesetComment.display_state, '') !=
173 coalesce(ChangesetComment.display_state, '') !=
175 ChangesetComment.COMMENT_OUTDATED)
174 ChangesetComment.COMMENT_OUTDATED)
176
175
177 todos = todos.all()
176 todos = todos.all()
178
177
179 return todos
178 return todos
180
179
181 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
180 def get_pull_request_resolved_todos(self, pull_request, show_outdated=True):
182
181
183 todos = Session().query(ChangesetComment) \
182 todos = Session().query(ChangesetComment) \
184 .filter(ChangesetComment.pull_request == pull_request) \
183 .filter(ChangesetComment.pull_request == pull_request) \
185 .filter(ChangesetComment.resolved_by != None) \
184 .filter(ChangesetComment.resolved_by != None) \
186 .filter(ChangesetComment.comment_type
185 .filter(ChangesetComment.comment_type
187 == ChangesetComment.COMMENT_TYPE_TODO)
186 == ChangesetComment.COMMENT_TYPE_TODO)
188
187
189 if not show_outdated:
188 if not show_outdated:
190 todos = todos.filter(
189 todos = todos.filter(
191 coalesce(ChangesetComment.display_state, '') !=
190 coalesce(ChangesetComment.display_state, '') !=
192 ChangesetComment.COMMENT_OUTDATED)
191 ChangesetComment.COMMENT_OUTDATED)
193
192
194 todos = todos.all()
193 todos = todos.all()
195
194
196 return todos
195 return todos
197
196
198 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
197 def get_commit_unresolved_todos(self, commit_id, show_outdated=True):
199
198
200 todos = Session().query(ChangesetComment) \
199 todos = Session().query(ChangesetComment) \
201 .filter(ChangesetComment.revision == commit_id) \
200 .filter(ChangesetComment.revision == commit_id) \
202 .filter(ChangesetComment.resolved_by == None) \
201 .filter(ChangesetComment.resolved_by == None) \
203 .filter(ChangesetComment.comment_type
202 .filter(ChangesetComment.comment_type
204 == ChangesetComment.COMMENT_TYPE_TODO)
203 == ChangesetComment.COMMENT_TYPE_TODO)
205
204
206 if not show_outdated:
205 if not show_outdated:
207 todos = todos.filter(
206 todos = todos.filter(
208 coalesce(ChangesetComment.display_state, '') !=
207 coalesce(ChangesetComment.display_state, '') !=
209 ChangesetComment.COMMENT_OUTDATED)
208 ChangesetComment.COMMENT_OUTDATED)
210
209
211 todos = todos.all()
210 todos = todos.all()
212
211
213 return todos
212 return todos
214
213
215 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
214 def get_commit_resolved_todos(self, commit_id, show_outdated=True):
216
215
217 todos = Session().query(ChangesetComment) \
216 todos = Session().query(ChangesetComment) \
218 .filter(ChangesetComment.revision == commit_id) \
217 .filter(ChangesetComment.revision == commit_id) \
219 .filter(ChangesetComment.resolved_by != None) \
218 .filter(ChangesetComment.resolved_by != None) \
220 .filter(ChangesetComment.comment_type
219 .filter(ChangesetComment.comment_type
221 == ChangesetComment.COMMENT_TYPE_TODO)
220 == ChangesetComment.COMMENT_TYPE_TODO)
222
221
223 if not show_outdated:
222 if not show_outdated:
224 todos = todos.filter(
223 todos = todos.filter(
225 coalesce(ChangesetComment.display_state, '') !=
224 coalesce(ChangesetComment.display_state, '') !=
226 ChangesetComment.COMMENT_OUTDATED)
225 ChangesetComment.COMMENT_OUTDATED)
227
226
228 todos = todos.all()
227 todos = todos.all()
229
228
230 return todos
229 return todos
231
230
232 def _log_audit_action(self, action, action_data, auth_user, comment):
231 def _log_audit_action(self, action, action_data, auth_user, comment):
233 audit_logger.store(
232 audit_logger.store(
234 action=action,
233 action=action,
235 action_data=action_data,
234 action_data=action_data,
236 user=auth_user,
235 user=auth_user,
237 repo=comment.repo)
236 repo=comment.repo)
238
237
239 def create(self, text, repo, user, commit_id=None, pull_request=None,
238 def create(self, text, repo, user, commit_id=None, pull_request=None,
240 f_path=None, line_no=None, status_change=None,
239 f_path=None, line_no=None, status_change=None,
241 status_change_type=None, comment_type=None,
240 status_change_type=None, comment_type=None,
242 resolves_comment_id=None, closing_pr=False, send_email=True,
241 resolves_comment_id=None, closing_pr=False, send_email=True,
243 renderer=None, auth_user=None, extra_recipients=None):
242 renderer=None, auth_user=None, extra_recipients=None):
244 """
243 """
245 Creates new comment for commit or pull request.
244 Creates new comment for commit or pull request.
246 IF status_change is not none this comment is associated with a
245 IF status_change is not none this comment is associated with a
247 status change of commit or commit associated with pull request
246 status change of commit or commit associated with pull request
248
247
249 :param text:
248 :param text:
250 :param repo:
249 :param repo:
251 :param user:
250 :param user:
252 :param commit_id:
251 :param commit_id:
253 :param pull_request:
252 :param pull_request:
254 :param f_path:
253 :param f_path:
255 :param line_no:
254 :param line_no:
256 :param status_change: Label for status change
255 :param status_change: Label for status change
257 :param comment_type: Type of comment
256 :param comment_type: Type of comment
258 :param resolves_comment_id: id of comment which this one will resolve
257 :param resolves_comment_id: id of comment which this one will resolve
259 :param status_change_type: type of status change
258 :param status_change_type: type of status change
260 :param closing_pr:
259 :param closing_pr:
261 :param send_email:
260 :param send_email:
262 :param renderer: pick renderer for this comment
261 :param renderer: pick renderer for this comment
263 :param auth_user: current authenticated user calling this method
262 :param auth_user: current authenticated user calling this method
264 :param extra_recipients: list of extra users to be added to recipients
263 :param extra_recipients: list of extra users to be added to recipients
265 """
264 """
266
265
267 if not text:
266 if not text:
268 log.warning('Missing text for comment, skipping...')
267 log.warning('Missing text for comment, skipping...')
269 return
268 return
270 request = get_current_request()
269 request = get_current_request()
271 _ = request.translate
270 _ = request.translate
272
271
273 if not renderer:
272 if not renderer:
274 renderer = self._get_renderer(request=request)
273 renderer = self._get_renderer(request=request)
275
274
276 repo = self._get_repo(repo)
275 repo = self._get_repo(repo)
277 user = self._get_user(user)
276 user = self._get_user(user)
278 auth_user = auth_user or user
277 auth_user = auth_user or user
279
278
280 schema = comment_schema.CommentSchema()
279 schema = comment_schema.CommentSchema()
281 validated_kwargs = schema.deserialize(dict(
280 validated_kwargs = schema.deserialize(dict(
282 comment_body=text,
281 comment_body=text,
283 comment_type=comment_type,
282 comment_type=comment_type,
284 comment_file=f_path,
283 comment_file=f_path,
285 comment_line=line_no,
284 comment_line=line_no,
286 renderer_type=renderer,
285 renderer_type=renderer,
287 status_change=status_change_type,
286 status_change=status_change_type,
288 resolves_comment_id=resolves_comment_id,
287 resolves_comment_id=resolves_comment_id,
289 repo=repo.repo_id,
288 repo=repo.repo_id,
290 user=user.user_id,
289 user=user.user_id,
291 ))
290 ))
292
291
293 comment = ChangesetComment()
292 comment = ChangesetComment()
294 comment.renderer = validated_kwargs['renderer_type']
293 comment.renderer = validated_kwargs['renderer_type']
295 comment.text = validated_kwargs['comment_body']
294 comment.text = validated_kwargs['comment_body']
296 comment.f_path = validated_kwargs['comment_file']
295 comment.f_path = validated_kwargs['comment_file']
297 comment.line_no = validated_kwargs['comment_line']
296 comment.line_no = validated_kwargs['comment_line']
298 comment.comment_type = validated_kwargs['comment_type']
297 comment.comment_type = validated_kwargs['comment_type']
299
298
300 comment.repo = repo
299 comment.repo = repo
301 comment.author = user
300 comment.author = user
302 resolved_comment = self.__get_commit_comment(
301 resolved_comment = self.__get_commit_comment(
303 validated_kwargs['resolves_comment_id'])
302 validated_kwargs['resolves_comment_id'])
304 # check if the comment actually belongs to this PR
303 # check if the comment actually belongs to this PR
305 if resolved_comment and resolved_comment.pull_request and \
304 if resolved_comment and resolved_comment.pull_request and \
306 resolved_comment.pull_request != pull_request:
305 resolved_comment.pull_request != pull_request:
307 log.warning('Comment tried to resolved unrelated todo comment: %s',
306 log.warning('Comment tried to resolved unrelated todo comment: %s',
308 resolved_comment)
307 resolved_comment)
309 # comment not bound to this pull request, forbid
308 # comment not bound to this pull request, forbid
310 resolved_comment = None
309 resolved_comment = None
311
310
312 elif resolved_comment and resolved_comment.repo and \
311 elif resolved_comment and resolved_comment.repo and \
313 resolved_comment.repo != repo:
312 resolved_comment.repo != repo:
314 log.warning('Comment tried to resolved unrelated todo comment: %s',
313 log.warning('Comment tried to resolved unrelated todo comment: %s',
315 resolved_comment)
314 resolved_comment)
316 # comment not bound to this repo, forbid
315 # comment not bound to this repo, forbid
317 resolved_comment = None
316 resolved_comment = None
318
317
319 comment.resolved_comment = resolved_comment
318 comment.resolved_comment = resolved_comment
320
319
321 pull_request_id = pull_request
320 pull_request_id = pull_request
322
321
323 commit_obj = None
322 commit_obj = None
324 pull_request_obj = None
323 pull_request_obj = None
325
324
326 if commit_id:
325 if commit_id:
327 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
326 notification_type = EmailNotificationModel.TYPE_COMMIT_COMMENT
328 # do a lookup, so we don't pass something bad here
327 # do a lookup, so we don't pass something bad here
329 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
328 commit_obj = repo.scm_instance().get_commit(commit_id=commit_id)
330 comment.revision = commit_obj.raw_id
329 comment.revision = commit_obj.raw_id
331
330
332 elif pull_request_id:
331 elif pull_request_id:
333 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
332 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST_COMMENT
334 pull_request_obj = self.__get_pull_request(pull_request_id)
333 pull_request_obj = self.__get_pull_request(pull_request_id)
335 comment.pull_request = pull_request_obj
334 comment.pull_request = pull_request_obj
336 else:
335 else:
337 raise Exception('Please specify commit or pull_request_id')
336 raise Exception('Please specify commit or pull_request_id')
338
337
339 Session().add(comment)
338 Session().add(comment)
340 Session().flush()
339 Session().flush()
341 kwargs = {
340 kwargs = {
342 'user': user,
341 'user': user,
343 'renderer_type': renderer,
342 'renderer_type': renderer,
344 'repo_name': repo.repo_name,
343 'repo_name': repo.repo_name,
345 'status_change': status_change,
344 'status_change': status_change,
346 'status_change_type': status_change_type,
345 'status_change_type': status_change_type,
347 'comment_body': text,
346 'comment_body': text,
348 'comment_file': f_path,
347 'comment_file': f_path,
349 'comment_line': line_no,
348 'comment_line': line_no,
350 'comment_type': comment_type or 'note',
349 'comment_type': comment_type or 'note',
351 'comment_id': comment.comment_id
350 'comment_id': comment.comment_id
352 }
351 }
353
352
354 if commit_obj:
353 if commit_obj:
355 recipients = ChangesetComment.get_users(
354 recipients = ChangesetComment.get_users(
356 revision=commit_obj.raw_id)
355 revision=commit_obj.raw_id)
357 # add commit author if it's in RhodeCode system
356 # add commit author if it's in RhodeCode system
358 cs_author = User.get_from_cs_author(commit_obj.author)
357 cs_author = User.get_from_cs_author(commit_obj.author)
359 if not cs_author:
358 if not cs_author:
360 # use repo owner if we cannot extract the author correctly
359 # use repo owner if we cannot extract the author correctly
361 cs_author = repo.user
360 cs_author = repo.user
362 recipients += [cs_author]
361 recipients += [cs_author]
363
362
364 commit_comment_url = self.get_url(comment, request=request)
363 commit_comment_url = self.get_url(comment, request=request)
365 commit_comment_reply_url = self.get_url(
364 commit_comment_reply_url = self.get_url(
366 comment, request=request,
365 comment, request=request,
367 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
366 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
368
367
369 target_repo_url = h.link_to(
368 target_repo_url = h.link_to(
370 repo.repo_name,
369 repo.repo_name,
371 h.route_url('repo_summary', repo_name=repo.repo_name))
370 h.route_url('repo_summary', repo_name=repo.repo_name))
372
371
373 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
372 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
374 commit_id=commit_id)
373 commit_id=commit_id)
375
374
376 # commit specifics
375 # commit specifics
377 kwargs.update({
376 kwargs.update({
378 'commit': commit_obj,
377 'commit': commit_obj,
379 'commit_message': commit_obj.message,
378 'commit_message': commit_obj.message,
380 'commit_target_repo_url': target_repo_url,
379 'commit_target_repo_url': target_repo_url,
381 'commit_comment_url': commit_comment_url,
380 'commit_comment_url': commit_comment_url,
382 'commit_comment_reply_url': commit_comment_reply_url,
381 'commit_comment_reply_url': commit_comment_reply_url,
383 'commit_url': commit_url,
382 'commit_url': commit_url,
384 'thread_ids': [commit_url, commit_comment_url],
383 'thread_ids': [commit_url, commit_comment_url],
385 })
384 })
386
385
387 elif pull_request_obj:
386 elif pull_request_obj:
388 # get the current participants of this pull request
387 # get the current participants of this pull request
389 recipients = ChangesetComment.get_users(
388 recipients = ChangesetComment.get_users(
390 pull_request_id=pull_request_obj.pull_request_id)
389 pull_request_id=pull_request_obj.pull_request_id)
391 # add pull request author
390 # add pull request author
392 recipients += [pull_request_obj.author]
391 recipients += [pull_request_obj.author]
393
392
394 # add the reviewers to notification
393 # add the reviewers to notification
395 recipients += [x.user for x in pull_request_obj.reviewers]
394 recipients += [x.user for x in pull_request_obj.reviewers]
396
395
397 pr_target_repo = pull_request_obj.target_repo
396 pr_target_repo = pull_request_obj.target_repo
398 pr_source_repo = pull_request_obj.source_repo
397 pr_source_repo = pull_request_obj.source_repo
399
398
400 pr_comment_url = self.get_url(comment, request=request)
399 pr_comment_url = self.get_url(comment, request=request)
401 pr_comment_reply_url = self.get_url(
400 pr_comment_reply_url = self.get_url(
402 comment, request=request,
401 comment, request=request,
403 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
402 anchor='comment-{}/?/ReplyToComment'.format(comment.comment_id))
404
403
405 pr_url = h.route_url(
404 pr_url = h.route_url(
406 'pullrequest_show',
405 'pullrequest_show',
407 repo_name=pr_target_repo.repo_name,
406 repo_name=pr_target_repo.repo_name,
408 pull_request_id=pull_request_obj.pull_request_id, )
407 pull_request_id=pull_request_obj.pull_request_id, )
409
408
410 # set some variables for email notification
409 # set some variables for email notification
411 pr_target_repo_url = h.route_url(
410 pr_target_repo_url = h.route_url(
412 'repo_summary', repo_name=pr_target_repo.repo_name)
411 'repo_summary', repo_name=pr_target_repo.repo_name)
413
412
414 pr_source_repo_url = h.route_url(
413 pr_source_repo_url = h.route_url(
415 'repo_summary', repo_name=pr_source_repo.repo_name)
414 'repo_summary', repo_name=pr_source_repo.repo_name)
416
415
417 # pull request specifics
416 # pull request specifics
418 kwargs.update({
417 kwargs.update({
419 'pull_request': pull_request_obj,
418 'pull_request': pull_request_obj,
420 'pr_id': pull_request_obj.pull_request_id,
419 'pr_id': pull_request_obj.pull_request_id,
421 'pull_request_url': pr_url,
420 'pull_request_url': pr_url,
422 'pull_request_target_repo': pr_target_repo,
421 'pull_request_target_repo': pr_target_repo,
423 'pull_request_target_repo_url': pr_target_repo_url,
422 'pull_request_target_repo_url': pr_target_repo_url,
424 'pull_request_source_repo': pr_source_repo,
423 'pull_request_source_repo': pr_source_repo,
425 'pull_request_source_repo_url': pr_source_repo_url,
424 'pull_request_source_repo_url': pr_source_repo_url,
426 'pr_comment_url': pr_comment_url,
425 'pr_comment_url': pr_comment_url,
427 'pr_comment_reply_url': pr_comment_reply_url,
426 'pr_comment_reply_url': pr_comment_reply_url,
428 'pr_closing': closing_pr,
427 'pr_closing': closing_pr,
429 'thread_ids': [pr_url, pr_comment_url],
428 'thread_ids': [pr_url, pr_comment_url],
430 })
429 })
431
430
432 recipients += [self._get_user(u) for u in (extra_recipients or [])]
431 recipients += [self._get_user(u) for u in (extra_recipients or [])]
433
432
434 if send_email:
433 if send_email:
435 # pre-generate the subject for notification itself
434 # pre-generate the subject for notification itself
436 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
435 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
437 notification_type, **kwargs)
436 notification_type, **kwargs)
438
437
439 mention_recipients = set(
438 mention_recipients = set(
440 self._extract_mentions(text)).difference(recipients)
439 self._extract_mentions(text)).difference(recipients)
441
440
442 # create notification objects, and emails
441 # create notification objects, and emails
443 NotificationModel().create(
442 NotificationModel().create(
444 created_by=user,
443 created_by=user,
445 notification_subject=subject,
444 notification_subject=subject,
446 notification_body=body_plaintext,
445 notification_body=body_plaintext,
447 notification_type=notification_type,
446 notification_type=notification_type,
448 recipients=recipients,
447 recipients=recipients,
449 mention_recipients=mention_recipients,
448 mention_recipients=mention_recipients,
450 email_kwargs=kwargs,
449 email_kwargs=kwargs,
451 )
450 )
452
451
453 Session().flush()
452 Session().flush()
454 if comment.pull_request:
453 if comment.pull_request:
455 action = 'repo.pull_request.comment.create'
454 action = 'repo.pull_request.comment.create'
456 else:
455 else:
457 action = 'repo.commit.comment.create'
456 action = 'repo.commit.comment.create'
458
457
458 comment_id = comment.comment_id
459 comment_data = comment.get_api_data()
459 comment_data = comment.get_api_data()
460
460 self._log_audit_action(
461 self._log_audit_action(
461 action, {'data': comment_data}, auth_user, comment)
462 action, {'data': comment_data}, auth_user, comment)
462
463
463 msg_url = ''
464 channel = None
464 channel = None
465 if commit_obj:
465 if commit_obj:
466 msg_url = commit_comment_url
467 repo_name = repo.repo_name
466 repo_name = repo.repo_name
468 channel = u'/repo${}$/commit/{}'.format(
467 channel = u'/repo${}$/commit/{}'.format(
469 repo_name,
468 repo_name,
470 commit_obj.raw_id
469 commit_obj.raw_id
471 )
470 )
472 elif pull_request_obj:
471 elif pull_request_obj:
473 msg_url = pr_comment_url
474 repo_name = pr_target_repo.repo_name
472 repo_name = pr_target_repo.repo_name
475 channel = u'/repo${}$/pr/{}'.format(
473 channel = u'/repo${}$/pr/{}'.format(
476 repo_name,
474 repo_name,
477 pull_request_id
475 pull_request_obj.pull_request_id
478 )
476 )
479
477
480 message = '<strong>{}</strong> {} - ' \
478 if channel:
481 '<a onclick="window.location=\'{}\';' \
479 username = user.username
482 'window.location.reload()">' \
480 message = '<strong>{}</strong> {} #{}, {}'
483 '<strong>{}</strong></a>'
481 message = message.format(
484 message = message.format(
482 username,
485 user.username, _('made a comment'), msg_url,
483 _('posted a new comment'),
486 _('Show it now'))
484 comment_id,
485 _('Refresh the page to see new comments.'))
487
486
488 channelstream.post_message(
487 message_obj = {
489 channel, message, user.username,
488 'message': message,
490 registry=get_current_registry())
489 'level': 'success',
490 'topic': '/notifications'
491 }
492
493 channelstream.post_message(
494 channel, message_obj, user.username,
495 registry=get_current_registry())
496
497 message_obj = {
498 'message': None,
499 'user': username,
500 'comment_id': comment_id,
501 'topic': '/comment'
502 }
503 channelstream.post_message(
504 channel, message_obj, user.username,
505 registry=get_current_registry())
491
506
492 return comment
507 return comment
493
508
494 def edit(self, comment_id, text, auth_user, version):
509 def edit(self, comment_id, text, auth_user, version):
495 """
510 """
496 Change existing comment for commit or pull request.
511 Change existing comment for commit or pull request.
497
512
498 :param comment_id:
513 :param comment_id:
499 :param text:
514 :param text:
500 :param auth_user: current authenticated user calling this method
515 :param auth_user: current authenticated user calling this method
501 :param version: last comment version
516 :param version: last comment version
502 """
517 """
503 if not text:
518 if not text:
504 log.warning('Missing text for comment, skipping...')
519 log.warning('Missing text for comment, skipping...')
505 return
520 return
506
521
507 comment = ChangesetComment.get(comment_id)
522 comment = ChangesetComment.get(comment_id)
508 old_comment_text = comment.text
523 old_comment_text = comment.text
509 comment.text = text
524 comment.text = text
510 comment.modified_at = datetime.datetime.now()
525 comment.modified_at = datetime.datetime.now()
511 version = safe_int(version)
526 version = safe_int(version)
512
527
513 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
528 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
514 # would return 3 here
529 # would return 3 here
515 comment_version = ChangesetCommentHistory.get_version(comment_id)
530 comment_version = ChangesetCommentHistory.get_version(comment_id)
516
531
517 if isinstance(version, (int, long)) and (comment_version - version) != 1:
532 if isinstance(version, (int, long)) and (comment_version - version) != 1:
518 log.warning(
533 log.warning(
519 'Version mismatch comment_version {} submitted {}, skipping'.format(
534 'Version mismatch comment_version {} submitted {}, skipping'.format(
520 comment_version-1, # -1 since note above
535 comment_version-1, # -1 since note above
521 version
536 version
522 )
537 )
523 )
538 )
524 raise CommentVersionMismatch()
539 raise CommentVersionMismatch()
525
540
526 comment_history = ChangesetCommentHistory()
541 comment_history = ChangesetCommentHistory()
527 comment_history.comment_id = comment_id
542 comment_history.comment_id = comment_id
528 comment_history.version = comment_version
543 comment_history.version = comment_version
529 comment_history.created_by_user_id = auth_user.user_id
544 comment_history.created_by_user_id = auth_user.user_id
530 comment_history.text = old_comment_text
545 comment_history.text = old_comment_text
531 # TODO add email notification
546 # TODO add email notification
532 Session().add(comment_history)
547 Session().add(comment_history)
533 Session().add(comment)
548 Session().add(comment)
534 Session().flush()
549 Session().flush()
535
550
536 if comment.pull_request:
551 if comment.pull_request:
537 action = 'repo.pull_request.comment.edit'
552 action = 'repo.pull_request.comment.edit'
538 else:
553 else:
539 action = 'repo.commit.comment.edit'
554 action = 'repo.commit.comment.edit'
540
555
541 comment_data = comment.get_api_data()
556 comment_data = comment.get_api_data()
542 comment_data['old_comment_text'] = old_comment_text
557 comment_data['old_comment_text'] = old_comment_text
543 self._log_audit_action(
558 self._log_audit_action(
544 action, {'data': comment_data}, auth_user, comment)
559 action, {'data': comment_data}, auth_user, comment)
545
560
546 return comment_history
561 return comment_history
547
562
548 def delete(self, comment, auth_user):
563 def delete(self, comment, auth_user):
549 """
564 """
550 Deletes given comment
565 Deletes given comment
551 """
566 """
552 comment = self.__get_commit_comment(comment)
567 comment = self.__get_commit_comment(comment)
553 old_data = comment.get_api_data()
568 old_data = comment.get_api_data()
554 Session().delete(comment)
569 Session().delete(comment)
555
570
556 if comment.pull_request:
571 if comment.pull_request:
557 action = 'repo.pull_request.comment.delete'
572 action = 'repo.pull_request.comment.delete'
558 else:
573 else:
559 action = 'repo.commit.comment.delete'
574 action = 'repo.commit.comment.delete'
560
575
561 self._log_audit_action(
576 self._log_audit_action(
562 action, {'old_data': old_data}, auth_user, comment)
577 action, {'old_data': old_data}, auth_user, comment)
563
578
564 return comment
579 return comment
565
580
566 def get_all_comments(self, repo_id, revision=None, pull_request=None):
581 def get_all_comments(self, repo_id, revision=None, pull_request=None):
567 q = ChangesetComment.query()\
582 q = ChangesetComment.query()\
568 .filter(ChangesetComment.repo_id == repo_id)
583 .filter(ChangesetComment.repo_id == repo_id)
569 if revision:
584 if revision:
570 q = q.filter(ChangesetComment.revision == revision)
585 q = q.filter(ChangesetComment.revision == revision)
571 elif pull_request:
586 elif pull_request:
572 pull_request = self.__get_pull_request(pull_request)
587 pull_request = self.__get_pull_request(pull_request)
573 q = q.filter(ChangesetComment.pull_request == pull_request)
588 q = q.filter(ChangesetComment.pull_request == pull_request)
574 else:
589 else:
575 raise Exception('Please specify commit or pull_request')
590 raise Exception('Please specify commit or pull_request')
576 q = q.order_by(ChangesetComment.created_on)
591 q = q.order_by(ChangesetComment.created_on)
577 return q.all()
592 return q.all()
578
593
579 def get_url(self, comment, request=None, permalink=False, anchor=None):
594 def get_url(self, comment, request=None, permalink=False, anchor=None):
580 if not request:
595 if not request:
581 request = get_current_request()
596 request = get_current_request()
582
597
583 comment = self.__get_commit_comment(comment)
598 comment = self.__get_commit_comment(comment)
584 if anchor is None:
599 if anchor is None:
585 anchor = 'comment-{}'.format(comment.comment_id)
600 anchor = 'comment-{}'.format(comment.comment_id)
586
601
587 if comment.pull_request:
602 if comment.pull_request:
588 pull_request = comment.pull_request
603 pull_request = comment.pull_request
589 if permalink:
604 if permalink:
590 return request.route_url(
605 return request.route_url(
591 'pull_requests_global',
606 'pull_requests_global',
592 pull_request_id=pull_request.pull_request_id,
607 pull_request_id=pull_request.pull_request_id,
593 _anchor=anchor)
608 _anchor=anchor)
594 else:
609 else:
595 return request.route_url(
610 return request.route_url(
596 'pullrequest_show',
611 'pullrequest_show',
597 repo_name=safe_str(pull_request.target_repo.repo_name),
612 repo_name=safe_str(pull_request.target_repo.repo_name),
598 pull_request_id=pull_request.pull_request_id,
613 pull_request_id=pull_request.pull_request_id,
599 _anchor=anchor)
614 _anchor=anchor)
600
615
601 else:
616 else:
602 repo = comment.repo
617 repo = comment.repo
603 commit_id = comment.revision
618 commit_id = comment.revision
604
619
605 if permalink:
620 if permalink:
606 return request.route_url(
621 return request.route_url(
607 'repo_commit', repo_name=safe_str(repo.repo_id),
622 'repo_commit', repo_name=safe_str(repo.repo_id),
608 commit_id=commit_id,
623 commit_id=commit_id,
609 _anchor=anchor)
624 _anchor=anchor)
610
625
611 else:
626 else:
612 return request.route_url(
627 return request.route_url(
613 'repo_commit', repo_name=safe_str(repo.repo_name),
628 'repo_commit', repo_name=safe_str(repo.repo_name),
614 commit_id=commit_id,
629 commit_id=commit_id,
615 _anchor=anchor)
630 _anchor=anchor)
616
631
617 def get_comments(self, repo_id, revision=None, pull_request=None):
632 def get_comments(self, repo_id, revision=None, pull_request=None):
618 """
633 """
619 Gets main comments based on revision or pull_request_id
634 Gets main comments based on revision or pull_request_id
620
635
621 :param repo_id:
636 :param repo_id:
622 :param revision:
637 :param revision:
623 :param pull_request:
638 :param pull_request:
624 """
639 """
625
640
626 q = ChangesetComment.query()\
641 q = ChangesetComment.query()\
627 .filter(ChangesetComment.repo_id == repo_id)\
642 .filter(ChangesetComment.repo_id == repo_id)\
628 .filter(ChangesetComment.line_no == None)\
643 .filter(ChangesetComment.line_no == None)\
629 .filter(ChangesetComment.f_path == None)
644 .filter(ChangesetComment.f_path == None)
630 if revision:
645 if revision:
631 q = q.filter(ChangesetComment.revision == revision)
646 q = q.filter(ChangesetComment.revision == revision)
632 elif pull_request:
647 elif pull_request:
633 pull_request = self.__get_pull_request(pull_request)
648 pull_request = self.__get_pull_request(pull_request)
634 q = q.filter(ChangesetComment.pull_request == pull_request)
649 q = q.filter(ChangesetComment.pull_request == pull_request)
635 else:
650 else:
636 raise Exception('Please specify commit or pull_request')
651 raise Exception('Please specify commit or pull_request')
637 q = q.order_by(ChangesetComment.created_on)
652 q = q.order_by(ChangesetComment.created_on)
638 return q.all()
653 return q.all()
639
654
640 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
655 def get_inline_comments(self, repo_id, revision=None, pull_request=None):
641 q = self._get_inline_comments_query(repo_id, revision, pull_request)
656 q = self._get_inline_comments_query(repo_id, revision, pull_request)
642 return self._group_comments_by_path_and_line_number(q)
657 return self._group_comments_by_path_and_line_number(q)
643
658
644 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
659 def get_inline_comments_as_list(self, inline_comments, skip_outdated=True,
645 version=None):
660 version=None):
646 inline_cnt = 0
661 inline_comms = []
647 for fname, per_line_comments in inline_comments.iteritems():
662 for fname, per_line_comments in inline_comments.iteritems():
648 for lno, comments in per_line_comments.iteritems():
663 for lno, comments in per_line_comments.iteritems():
649 for comm in comments:
664 for comm in comments:
650 if not comm.outdated_at_version(version) and skip_outdated:
665 if not comm.outdated_at_version(version) and skip_outdated:
651 inline_cnt += 1
666 inline_comms.append(comm)
652
667
653 return inline_cnt
668 return inline_comms
654
669
655 def get_outdated_comments(self, repo_id, pull_request):
670 def get_outdated_comments(self, repo_id, pull_request):
656 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
671 # TODO: johbo: Remove `repo_id`, it is not needed to find the comments
657 # of a pull request.
672 # of a pull request.
658 q = self._all_inline_comments_of_pull_request(pull_request)
673 q = self._all_inline_comments_of_pull_request(pull_request)
659 q = q.filter(
674 q = q.filter(
660 ChangesetComment.display_state ==
675 ChangesetComment.display_state ==
661 ChangesetComment.COMMENT_OUTDATED
676 ChangesetComment.COMMENT_OUTDATED
662 ).order_by(ChangesetComment.comment_id.asc())
677 ).order_by(ChangesetComment.comment_id.asc())
663
678
664 return self._group_comments_by_path_and_line_number(q)
679 return self._group_comments_by_path_and_line_number(q)
665
680
666 def _get_inline_comments_query(self, repo_id, revision, pull_request):
681 def _get_inline_comments_query(self, repo_id, revision, pull_request):
667 # TODO: johbo: Split this into two methods: One for PR and one for
682 # TODO: johbo: Split this into two methods: One for PR and one for
668 # commit.
683 # commit.
669 if revision:
684 if revision:
670 q = Session().query(ChangesetComment).filter(
685 q = Session().query(ChangesetComment).filter(
671 ChangesetComment.repo_id == repo_id,
686 ChangesetComment.repo_id == repo_id,
672 ChangesetComment.line_no != null(),
687 ChangesetComment.line_no != null(),
673 ChangesetComment.f_path != null(),
688 ChangesetComment.f_path != null(),
674 ChangesetComment.revision == revision)
689 ChangesetComment.revision == revision)
675
690
676 elif pull_request:
691 elif pull_request:
677 pull_request = self.__get_pull_request(pull_request)
692 pull_request = self.__get_pull_request(pull_request)
678 if not CommentsModel.use_outdated_comments(pull_request):
693 if not CommentsModel.use_outdated_comments(pull_request):
679 q = self._visible_inline_comments_of_pull_request(pull_request)
694 q = self._visible_inline_comments_of_pull_request(pull_request)
680 else:
695 else:
681 q = self._all_inline_comments_of_pull_request(pull_request)
696 q = self._all_inline_comments_of_pull_request(pull_request)
682
697
683 else:
698 else:
684 raise Exception('Please specify commit or pull_request_id')
699 raise Exception('Please specify commit or pull_request_id')
685 q = q.order_by(ChangesetComment.comment_id.asc())
700 q = q.order_by(ChangesetComment.comment_id.asc())
686 return q
701 return q
687
702
688 def _group_comments_by_path_and_line_number(self, q):
703 def _group_comments_by_path_and_line_number(self, q):
689 comments = q.all()
704 comments = q.all()
690 paths = collections.defaultdict(lambda: collections.defaultdict(list))
705 paths = collections.defaultdict(lambda: collections.defaultdict(list))
691 for co in comments:
706 for co in comments:
692 paths[co.f_path][co.line_no].append(co)
707 paths[co.f_path][co.line_no].append(co)
693 return paths
708 return paths
694
709
695 @classmethod
710 @classmethod
696 def needed_extra_diff_context(cls):
711 def needed_extra_diff_context(cls):
697 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
712 return max(cls.DIFF_CONTEXT_BEFORE, cls.DIFF_CONTEXT_AFTER)
698
713
699 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
714 def outdate_comments(self, pull_request, old_diff_data, new_diff_data):
700 if not CommentsModel.use_outdated_comments(pull_request):
715 if not CommentsModel.use_outdated_comments(pull_request):
701 return
716 return
702
717
703 comments = self._visible_inline_comments_of_pull_request(pull_request)
718 comments = self._visible_inline_comments_of_pull_request(pull_request)
704 comments_to_outdate = comments.all()
719 comments_to_outdate = comments.all()
705
720
706 for comment in comments_to_outdate:
721 for comment in comments_to_outdate:
707 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
722 self._outdate_one_comment(comment, old_diff_data, new_diff_data)
708
723
709 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
724 def _outdate_one_comment(self, comment, old_diff_proc, new_diff_proc):
710 diff_line = _parse_comment_line_number(comment.line_no)
725 diff_line = _parse_comment_line_number(comment.line_no)
711
726
712 try:
727 try:
713 old_context = old_diff_proc.get_context_of_line(
728 old_context = old_diff_proc.get_context_of_line(
714 path=comment.f_path, diff_line=diff_line)
729 path=comment.f_path, diff_line=diff_line)
715 new_context = new_diff_proc.get_context_of_line(
730 new_context = new_diff_proc.get_context_of_line(
716 path=comment.f_path, diff_line=diff_line)
731 path=comment.f_path, diff_line=diff_line)
717 except (diffs.LineNotInDiffException,
732 except (diffs.LineNotInDiffException,
718 diffs.FileNotInDiffException):
733 diffs.FileNotInDiffException):
719 comment.display_state = ChangesetComment.COMMENT_OUTDATED
734 comment.display_state = ChangesetComment.COMMENT_OUTDATED
720 return
735 return
721
736
722 if old_context == new_context:
737 if old_context == new_context:
723 return
738 return
724
739
725 if self._should_relocate_diff_line(diff_line):
740 if self._should_relocate_diff_line(diff_line):
726 new_diff_lines = new_diff_proc.find_context(
741 new_diff_lines = new_diff_proc.find_context(
727 path=comment.f_path, context=old_context,
742 path=comment.f_path, context=old_context,
728 offset=self.DIFF_CONTEXT_BEFORE)
743 offset=self.DIFF_CONTEXT_BEFORE)
729 if not new_diff_lines:
744 if not new_diff_lines:
730 comment.display_state = ChangesetComment.COMMENT_OUTDATED
745 comment.display_state = ChangesetComment.COMMENT_OUTDATED
731 else:
746 else:
732 new_diff_line = self._choose_closest_diff_line(
747 new_diff_line = self._choose_closest_diff_line(
733 diff_line, new_diff_lines)
748 diff_line, new_diff_lines)
734 comment.line_no = _diff_to_comment_line_number(new_diff_line)
749 comment.line_no = _diff_to_comment_line_number(new_diff_line)
735 else:
750 else:
736 comment.display_state = ChangesetComment.COMMENT_OUTDATED
751 comment.display_state = ChangesetComment.COMMENT_OUTDATED
737
752
738 def _should_relocate_diff_line(self, diff_line):
753 def _should_relocate_diff_line(self, diff_line):
739 """
754 """
740 Checks if relocation shall be tried for the given `diff_line`.
755 Checks if relocation shall be tried for the given `diff_line`.
741
756
742 If a comment points into the first lines, then we can have a situation
757 If a comment points into the first lines, then we can have a situation
743 that after an update another line has been added on top. In this case
758 that after an update another line has been added on top. In this case
744 we would find the context still and move the comment around. This
759 we would find the context still and move the comment around. This
745 would be wrong.
760 would be wrong.
746 """
761 """
747 should_relocate = (
762 should_relocate = (
748 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
763 (diff_line.new and diff_line.new > self.DIFF_CONTEXT_BEFORE) or
749 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
764 (diff_line.old and diff_line.old > self.DIFF_CONTEXT_BEFORE))
750 return should_relocate
765 return should_relocate
751
766
752 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
767 def _choose_closest_diff_line(self, diff_line, new_diff_lines):
753 candidate = new_diff_lines[0]
768 candidate = new_diff_lines[0]
754 best_delta = _diff_line_delta(diff_line, candidate)
769 best_delta = _diff_line_delta(diff_line, candidate)
755 for new_diff_line in new_diff_lines[1:]:
770 for new_diff_line in new_diff_lines[1:]:
756 delta = _diff_line_delta(diff_line, new_diff_line)
771 delta = _diff_line_delta(diff_line, new_diff_line)
757 if delta < best_delta:
772 if delta < best_delta:
758 candidate = new_diff_line
773 candidate = new_diff_line
759 best_delta = delta
774 best_delta = delta
760 return candidate
775 return candidate
761
776
762 def _visible_inline_comments_of_pull_request(self, pull_request):
777 def _visible_inline_comments_of_pull_request(self, pull_request):
763 comments = self._all_inline_comments_of_pull_request(pull_request)
778 comments = self._all_inline_comments_of_pull_request(pull_request)
764 comments = comments.filter(
779 comments = comments.filter(
765 coalesce(ChangesetComment.display_state, '') !=
780 coalesce(ChangesetComment.display_state, '') !=
766 ChangesetComment.COMMENT_OUTDATED)
781 ChangesetComment.COMMENT_OUTDATED)
767 return comments
782 return comments
768
783
769 def _all_inline_comments_of_pull_request(self, pull_request):
784 def _all_inline_comments_of_pull_request(self, pull_request):
770 comments = Session().query(ChangesetComment)\
785 comments = Session().query(ChangesetComment)\
771 .filter(ChangesetComment.line_no != None)\
786 .filter(ChangesetComment.line_no != None)\
772 .filter(ChangesetComment.f_path != None)\
787 .filter(ChangesetComment.f_path != None)\
773 .filter(ChangesetComment.pull_request == pull_request)
788 .filter(ChangesetComment.pull_request == pull_request)
774 return comments
789 return comments
775
790
776 def _all_general_comments_of_pull_request(self, pull_request):
791 def _all_general_comments_of_pull_request(self, pull_request):
777 comments = Session().query(ChangesetComment)\
792 comments = Session().query(ChangesetComment)\
778 .filter(ChangesetComment.line_no == None)\
793 .filter(ChangesetComment.line_no == None)\
779 .filter(ChangesetComment.f_path == None)\
794 .filter(ChangesetComment.f_path == None)\
780 .filter(ChangesetComment.pull_request == pull_request)
795 .filter(ChangesetComment.pull_request == pull_request)
781
796
782 return comments
797 return comments
783
798
784 @staticmethod
799 @staticmethod
785 def use_outdated_comments(pull_request):
800 def use_outdated_comments(pull_request):
786 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
801 settings_model = VcsSettingsModel(repo=pull_request.target_repo)
787 settings = settings_model.get_general_settings()
802 settings = settings_model.get_general_settings()
788 return settings.get('rhodecode_use_outdated_comments', False)
803 return settings.get('rhodecode_use_outdated_comments', False)
789
804
790 def trigger_commit_comment_hook(self, repo, user, action, data=None):
805 def trigger_commit_comment_hook(self, repo, user, action, data=None):
791 repo = self._get_repo(repo)
806 repo = self._get_repo(repo)
792 target_scm = repo.scm_instance()
807 target_scm = repo.scm_instance()
793 if action == 'create':
808 if action == 'create':
794 trigger_hook = hooks_utils.trigger_comment_commit_hooks
809 trigger_hook = hooks_utils.trigger_comment_commit_hooks
795 elif action == 'edit':
810 elif action == 'edit':
796 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
811 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
797 else:
812 else:
798 return
813 return
799
814
800 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
815 log.debug('Handling repo %s trigger_commit_comment_hook with action %s: %s',
801 repo, action, trigger_hook)
816 repo, action, trigger_hook)
802 trigger_hook(
817 trigger_hook(
803 username=user.username,
818 username=user.username,
804 repo_name=repo.repo_name,
819 repo_name=repo.repo_name,
805 repo_type=target_scm.alias,
820 repo_type=target_scm.alias,
806 repo=repo,
821 repo=repo,
807 data=data)
822 data=data)
808
823
809
824
810 def _parse_comment_line_number(line_no):
825 def _parse_comment_line_number(line_no):
811 """
826 """
812 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
827 Parses line numbers of the form "(o|n)\d+" and returns them in a tuple.
813 """
828 """
814 old_line = None
829 old_line = None
815 new_line = None
830 new_line = None
816 if line_no.startswith('o'):
831 if line_no.startswith('o'):
817 old_line = int(line_no[1:])
832 old_line = int(line_no[1:])
818 elif line_no.startswith('n'):
833 elif line_no.startswith('n'):
819 new_line = int(line_no[1:])
834 new_line = int(line_no[1:])
820 else:
835 else:
821 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
836 raise ValueError("Comment lines have to start with either 'o' or 'n'.")
822 return diffs.DiffLineNumber(old_line, new_line)
837 return diffs.DiffLineNumber(old_line, new_line)
823
838
824
839
825 def _diff_to_comment_line_number(diff_line):
840 def _diff_to_comment_line_number(diff_line):
826 if diff_line.new is not None:
841 if diff_line.new is not None:
827 return u'n{}'.format(diff_line.new)
842 return u'n{}'.format(diff_line.new)
828 elif diff_line.old is not None:
843 elif diff_line.old is not None:
829 return u'o{}'.format(diff_line.old)
844 return u'o{}'.format(diff_line.old)
830 return u''
845 return u''
831
846
832
847
833 def _diff_line_delta(a, b):
848 def _diff_line_delta(a, b):
834 if None not in (a.new, b.new):
849 if None not in (a.new, b.new):
835 return abs(a.new - b.new)
850 return abs(a.new - b.new)
836 elif None not in (a.old, b.old):
851 elif None not in (a.old, b.old):
837 return abs(a.old - b.old)
852 return abs(a.old - b.old)
838 else:
853 else:
839 raise ValueError(
854 raise ValueError(
840 "Cannot compute delta between {} and {}".format(a, b))
855 "Cannot compute delta between {} and {}".format(a, b))
@@ -1,5700 +1,5728 b''
1 # -*- coding: utf-8 -*-
1 # -*- coding: utf-8 -*-
2
2
3 # Copyright (C) 2010-2020 RhodeCode GmbH
3 # Copyright (C) 2010-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 Database Models for RhodeCode Enterprise
22 Database Models for RhodeCode Enterprise
23 """
23 """
24
24
25 import re
25 import re
26 import os
26 import os
27 import time
27 import time
28 import string
28 import string
29 import hashlib
29 import hashlib
30 import logging
30 import logging
31 import datetime
31 import datetime
32 import uuid
32 import uuid
33 import warnings
33 import warnings
34 import ipaddress
34 import ipaddress
35 import functools
35 import functools
36 import traceback
36 import traceback
37 import collections
37 import collections
38
38
39 from sqlalchemy import (
39 from sqlalchemy import (
40 or_, and_, not_, func, cast, TypeDecorator, event,
40 or_, and_, not_, func, cast, TypeDecorator, event,
41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
43 Text, Float, PickleType, BigInteger)
43 Text, Float, PickleType, BigInteger)
44 from sqlalchemy.sql.expression import true, false, case
44 from sqlalchemy.sql.expression import true, false, case
45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
46 from sqlalchemy.orm import (
46 from sqlalchemy.orm import (
47 relationship, joinedload, class_mapper, validates, aliased)
47 relationship, joinedload, class_mapper, validates, aliased)
48 from sqlalchemy.ext.declarative import declared_attr
48 from sqlalchemy.ext.declarative import declared_attr
49 from sqlalchemy.ext.hybrid import hybrid_property
49 from sqlalchemy.ext.hybrid import hybrid_property
50 from sqlalchemy.exc import IntegrityError # pragma: no cover
50 from sqlalchemy.exc import IntegrityError # pragma: no cover
51 from sqlalchemy.dialects.mysql import LONGTEXT
51 from sqlalchemy.dialects.mysql import LONGTEXT
52 from zope.cachedescriptors.property import Lazy as LazyProperty
52 from zope.cachedescriptors.property import Lazy as LazyProperty
53 from pyramid import compat
53 from pyramid import compat
54 from pyramid.threadlocal import get_current_request
54 from pyramid.threadlocal import get_current_request
55 from webhelpers2.text import remove_formatting
55 from webhelpers2.text import remove_formatting
56
56
57 from rhodecode.translation import _
57 from rhodecode.translation import _
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
59 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
60 from rhodecode.lib.utils2 import (
60 from rhodecode.lib.utils2 import (
61 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
61 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
62 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
62 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
63 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
63 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
64 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
64 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
65 JsonRaw
65 JsonRaw
66 from rhodecode.lib.ext_json import json
66 from rhodecode.lib.ext_json import json
67 from rhodecode.lib.caching_query import FromCache
67 from rhodecode.lib.caching_query import FromCache
68 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
68 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
69 from rhodecode.lib.encrypt2 import Encryptor
69 from rhodecode.lib.encrypt2 import Encryptor
70 from rhodecode.lib.exceptions import (
70 from rhodecode.lib.exceptions import (
71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 from rhodecode.model.meta import Base, Session
72 from rhodecode.model.meta import Base, Session
73
73
74 URL_SEP = '/'
74 URL_SEP = '/'
75 log = logging.getLogger(__name__)
75 log = logging.getLogger(__name__)
76
76
77 # =============================================================================
77 # =============================================================================
78 # BASE CLASSES
78 # BASE CLASSES
79 # =============================================================================
79 # =============================================================================
80
80
81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 # beaker.session.secret if first is not set.
82 # beaker.session.secret if first is not set.
83 # and initialized at environment.py
83 # and initialized at environment.py
84 ENCRYPTION_KEY = None
84 ENCRYPTION_KEY = None
85
85
86 # used to sort permissions by types, '#' used here is not allowed to be in
86 # used to sort permissions by types, '#' used here is not allowed to be in
87 # usernames, and it's very early in sorted string.printable table.
87 # usernames, and it's very early in sorted string.printable table.
88 PERMISSION_TYPE_SORT = {
88 PERMISSION_TYPE_SORT = {
89 'admin': '####',
89 'admin': '####',
90 'write': '###',
90 'write': '###',
91 'read': '##',
91 'read': '##',
92 'none': '#',
92 'none': '#',
93 }
93 }
94
94
95
95
96 def display_user_sort(obj):
96 def display_user_sort(obj):
97 """
97 """
98 Sort function used to sort permissions in .permissions() function of
98 Sort function used to sort permissions in .permissions() function of
99 Repository, RepoGroup, UserGroup. Also it put the default user in front
99 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 of all other resources
100 of all other resources
101 """
101 """
102
102
103 if obj.username == User.DEFAULT_USER:
103 if obj.username == User.DEFAULT_USER:
104 return '#####'
104 return '#####'
105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 extra_sort_num = '1' # default
106 extra_sort_num = '1' # default
107
107
108 # NOTE(dan): inactive duplicates goes last
108 # NOTE(dan): inactive duplicates goes last
109 if getattr(obj, 'duplicate_perm', None):
109 if getattr(obj, 'duplicate_perm', None):
110 extra_sort_num = '9'
110 extra_sort_num = '9'
111 return prefix + extra_sort_num + obj.username
111 return prefix + extra_sort_num + obj.username
112
112
113
113
114 def display_user_group_sort(obj):
114 def display_user_group_sort(obj):
115 """
115 """
116 Sort function used to sort permissions in .permissions() function of
116 Sort function used to sort permissions in .permissions() function of
117 Repository, RepoGroup, UserGroup. Also it put the default user in front
117 Repository, RepoGroup, UserGroup. Also it put the default user in front
118 of all other resources
118 of all other resources
119 """
119 """
120
120
121 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
121 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
122 return prefix + obj.users_group_name
122 return prefix + obj.users_group_name
123
123
124
124
125 def _hash_key(k):
125 def _hash_key(k):
126 return sha1_safe(k)
126 return sha1_safe(k)
127
127
128
128
129 def in_filter_generator(qry, items, limit=500):
129 def in_filter_generator(qry, items, limit=500):
130 """
130 """
131 Splits IN() into multiple with OR
131 Splits IN() into multiple with OR
132 e.g.::
132 e.g.::
133 cnt = Repository.query().filter(
133 cnt = Repository.query().filter(
134 or_(
134 or_(
135 *in_filter_generator(Repository.repo_id, range(100000))
135 *in_filter_generator(Repository.repo_id, range(100000))
136 )).count()
136 )).count()
137 """
137 """
138 if not items:
138 if not items:
139 # empty list will cause empty query which might cause security issues
139 # empty list will cause empty query which might cause security issues
140 # this can lead to hidden unpleasant results
140 # this can lead to hidden unpleasant results
141 items = [-1]
141 items = [-1]
142
142
143 parts = []
143 parts = []
144 for chunk in xrange(0, len(items), limit):
144 for chunk in xrange(0, len(items), limit):
145 parts.append(
145 parts.append(
146 qry.in_(items[chunk: chunk + limit])
146 qry.in_(items[chunk: chunk + limit])
147 )
147 )
148
148
149 return parts
149 return parts
150
150
151
151
152 base_table_args = {
152 base_table_args = {
153 'extend_existing': True,
153 'extend_existing': True,
154 'mysql_engine': 'InnoDB',
154 'mysql_engine': 'InnoDB',
155 'mysql_charset': 'utf8',
155 'mysql_charset': 'utf8',
156 'sqlite_autoincrement': True
156 'sqlite_autoincrement': True
157 }
157 }
158
158
159
159
160 class EncryptedTextValue(TypeDecorator):
160 class EncryptedTextValue(TypeDecorator):
161 """
161 """
162 Special column for encrypted long text data, use like::
162 Special column for encrypted long text data, use like::
163
163
164 value = Column("encrypted_value", EncryptedValue(), nullable=False)
164 value = Column("encrypted_value", EncryptedValue(), nullable=False)
165
165
166 This column is intelligent so if value is in unencrypted form it return
166 This column is intelligent so if value is in unencrypted form it return
167 unencrypted form, but on save it always encrypts
167 unencrypted form, but on save it always encrypts
168 """
168 """
169 impl = Text
169 impl = Text
170
170
171 def process_bind_param(self, value, dialect):
171 def process_bind_param(self, value, dialect):
172 """
172 """
173 Setter for storing value
173 Setter for storing value
174 """
174 """
175 import rhodecode
175 import rhodecode
176 if not value:
176 if not value:
177 return value
177 return value
178
178
179 # protect against double encrypting if values is already encrypted
179 # protect against double encrypting if values is already encrypted
180 if value.startswith('enc$aes$') \
180 if value.startswith('enc$aes$') \
181 or value.startswith('enc$aes_hmac$') \
181 or value.startswith('enc$aes_hmac$') \
182 or value.startswith('enc2$'):
182 or value.startswith('enc2$'):
183 raise ValueError('value needs to be in unencrypted format, '
183 raise ValueError('value needs to be in unencrypted format, '
184 'ie. not starting with enc$ or enc2$')
184 'ie. not starting with enc$ or enc2$')
185
185
186 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
186 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
187 if algo == 'aes':
187 if algo == 'aes':
188 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
188 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
189 elif algo == 'fernet':
189 elif algo == 'fernet':
190 return Encryptor(ENCRYPTION_KEY).encrypt(value)
190 return Encryptor(ENCRYPTION_KEY).encrypt(value)
191 else:
191 else:
192 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
192 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
193
193
194 def process_result_value(self, value, dialect):
194 def process_result_value(self, value, dialect):
195 """
195 """
196 Getter for retrieving value
196 Getter for retrieving value
197 """
197 """
198
198
199 import rhodecode
199 import rhodecode
200 if not value:
200 if not value:
201 return value
201 return value
202
202
203 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
203 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
204 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
204 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
205 if algo == 'aes':
205 if algo == 'aes':
206 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
206 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
207 elif algo == 'fernet':
207 elif algo == 'fernet':
208 return Encryptor(ENCRYPTION_KEY).decrypt(value)
208 return Encryptor(ENCRYPTION_KEY).decrypt(value)
209 else:
209 else:
210 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
210 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
211 return decrypted_data
211 return decrypted_data
212
212
213
213
214 class BaseModel(object):
214 class BaseModel(object):
215 """
215 """
216 Base Model for all classes
216 Base Model for all classes
217 """
217 """
218
218
219 @classmethod
219 @classmethod
220 def _get_keys(cls):
220 def _get_keys(cls):
221 """return column names for this model """
221 """return column names for this model """
222 return class_mapper(cls).c.keys()
222 return class_mapper(cls).c.keys()
223
223
224 def get_dict(self):
224 def get_dict(self):
225 """
225 """
226 return dict with keys and values corresponding
226 return dict with keys and values corresponding
227 to this model data """
227 to this model data """
228
228
229 d = {}
229 d = {}
230 for k in self._get_keys():
230 for k in self._get_keys():
231 d[k] = getattr(self, k)
231 d[k] = getattr(self, k)
232
232
233 # also use __json__() if present to get additional fields
233 # also use __json__() if present to get additional fields
234 _json_attr = getattr(self, '__json__', None)
234 _json_attr = getattr(self, '__json__', None)
235 if _json_attr:
235 if _json_attr:
236 # update with attributes from __json__
236 # update with attributes from __json__
237 if callable(_json_attr):
237 if callable(_json_attr):
238 _json_attr = _json_attr()
238 _json_attr = _json_attr()
239 for k, val in _json_attr.iteritems():
239 for k, val in _json_attr.iteritems():
240 d[k] = val
240 d[k] = val
241 return d
241 return d
242
242
243 def get_appstruct(self):
243 def get_appstruct(self):
244 """return list with keys and values tuples corresponding
244 """return list with keys and values tuples corresponding
245 to this model data """
245 to this model data """
246
246
247 lst = []
247 lst = []
248 for k in self._get_keys():
248 for k in self._get_keys():
249 lst.append((k, getattr(self, k),))
249 lst.append((k, getattr(self, k),))
250 return lst
250 return lst
251
251
252 def populate_obj(self, populate_dict):
252 def populate_obj(self, populate_dict):
253 """populate model with data from given populate_dict"""
253 """populate model with data from given populate_dict"""
254
254
255 for k in self._get_keys():
255 for k in self._get_keys():
256 if k in populate_dict:
256 if k in populate_dict:
257 setattr(self, k, populate_dict[k])
257 setattr(self, k, populate_dict[k])
258
258
259 @classmethod
259 @classmethod
260 def query(cls):
260 def query(cls):
261 return Session().query(cls)
261 return Session().query(cls)
262
262
263 @classmethod
263 @classmethod
264 def get(cls, id_):
264 def get(cls, id_):
265 if id_:
265 if id_:
266 return cls.query().get(id_)
266 return cls.query().get(id_)
267
267
268 @classmethod
268 @classmethod
269 def get_or_404(cls, id_):
269 def get_or_404(cls, id_):
270 from pyramid.httpexceptions import HTTPNotFound
270 from pyramid.httpexceptions import HTTPNotFound
271
271
272 try:
272 try:
273 id_ = int(id_)
273 id_ = int(id_)
274 except (TypeError, ValueError):
274 except (TypeError, ValueError):
275 raise HTTPNotFound()
275 raise HTTPNotFound()
276
276
277 res = cls.query().get(id_)
277 res = cls.query().get(id_)
278 if not res:
278 if not res:
279 raise HTTPNotFound()
279 raise HTTPNotFound()
280 return res
280 return res
281
281
282 @classmethod
282 @classmethod
283 def getAll(cls):
283 def getAll(cls):
284 # deprecated and left for backward compatibility
284 # deprecated and left for backward compatibility
285 return cls.get_all()
285 return cls.get_all()
286
286
287 @classmethod
287 @classmethod
288 def get_all(cls):
288 def get_all(cls):
289 return cls.query().all()
289 return cls.query().all()
290
290
291 @classmethod
291 @classmethod
292 def delete(cls, id_):
292 def delete(cls, id_):
293 obj = cls.query().get(id_)
293 obj = cls.query().get(id_)
294 Session().delete(obj)
294 Session().delete(obj)
295
295
296 @classmethod
296 @classmethod
297 def identity_cache(cls, session, attr_name, value):
297 def identity_cache(cls, session, attr_name, value):
298 exist_in_session = []
298 exist_in_session = []
299 for (item_cls, pkey), instance in session.identity_map.items():
299 for (item_cls, pkey), instance in session.identity_map.items():
300 if cls == item_cls and getattr(instance, attr_name) == value:
300 if cls == item_cls and getattr(instance, attr_name) == value:
301 exist_in_session.append(instance)
301 exist_in_session.append(instance)
302 if exist_in_session:
302 if exist_in_session:
303 if len(exist_in_session) == 1:
303 if len(exist_in_session) == 1:
304 return exist_in_session[0]
304 return exist_in_session[0]
305 log.exception(
305 log.exception(
306 'multiple objects with attr %s and '
306 'multiple objects with attr %s and '
307 'value %s found with same name: %r',
307 'value %s found with same name: %r',
308 attr_name, value, exist_in_session)
308 attr_name, value, exist_in_session)
309
309
310 def __repr__(self):
310 def __repr__(self):
311 if hasattr(self, '__unicode__'):
311 if hasattr(self, '__unicode__'):
312 # python repr needs to return str
312 # python repr needs to return str
313 try:
313 try:
314 return safe_str(self.__unicode__())
314 return safe_str(self.__unicode__())
315 except UnicodeDecodeError:
315 except UnicodeDecodeError:
316 pass
316 pass
317 return '<DB:%s>' % (self.__class__.__name__)
317 return '<DB:%s>' % (self.__class__.__name__)
318
318
319
319
320 class RhodeCodeSetting(Base, BaseModel):
320 class RhodeCodeSetting(Base, BaseModel):
321 __tablename__ = 'rhodecode_settings'
321 __tablename__ = 'rhodecode_settings'
322 __table_args__ = (
322 __table_args__ = (
323 UniqueConstraint('app_settings_name'),
323 UniqueConstraint('app_settings_name'),
324 base_table_args
324 base_table_args
325 )
325 )
326
326
327 SETTINGS_TYPES = {
327 SETTINGS_TYPES = {
328 'str': safe_str,
328 'str': safe_str,
329 'int': safe_int,
329 'int': safe_int,
330 'unicode': safe_unicode,
330 'unicode': safe_unicode,
331 'bool': str2bool,
331 'bool': str2bool,
332 'list': functools.partial(aslist, sep=',')
332 'list': functools.partial(aslist, sep=',')
333 }
333 }
334 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
334 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
335 GLOBAL_CONF_KEY = 'app_settings'
335 GLOBAL_CONF_KEY = 'app_settings'
336
336
337 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
337 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
338 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
338 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
339 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
339 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
340 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
340 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
341
341
342 def __init__(self, key='', val='', type='unicode'):
342 def __init__(self, key='', val='', type='unicode'):
343 self.app_settings_name = key
343 self.app_settings_name = key
344 self.app_settings_type = type
344 self.app_settings_type = type
345 self.app_settings_value = val
345 self.app_settings_value = val
346
346
347 @validates('_app_settings_value')
347 @validates('_app_settings_value')
348 def validate_settings_value(self, key, val):
348 def validate_settings_value(self, key, val):
349 assert type(val) == unicode
349 assert type(val) == unicode
350 return val
350 return val
351
351
352 @hybrid_property
352 @hybrid_property
353 def app_settings_value(self):
353 def app_settings_value(self):
354 v = self._app_settings_value
354 v = self._app_settings_value
355 _type = self.app_settings_type
355 _type = self.app_settings_type
356 if _type:
356 if _type:
357 _type = self.app_settings_type.split('.')[0]
357 _type = self.app_settings_type.split('.')[0]
358 # decode the encrypted value
358 # decode the encrypted value
359 if 'encrypted' in self.app_settings_type:
359 if 'encrypted' in self.app_settings_type:
360 cipher = EncryptedTextValue()
360 cipher = EncryptedTextValue()
361 v = safe_unicode(cipher.process_result_value(v, None))
361 v = safe_unicode(cipher.process_result_value(v, None))
362
362
363 converter = self.SETTINGS_TYPES.get(_type) or \
363 converter = self.SETTINGS_TYPES.get(_type) or \
364 self.SETTINGS_TYPES['unicode']
364 self.SETTINGS_TYPES['unicode']
365 return converter(v)
365 return converter(v)
366
366
367 @app_settings_value.setter
367 @app_settings_value.setter
368 def app_settings_value(self, val):
368 def app_settings_value(self, val):
369 """
369 """
370 Setter that will always make sure we use unicode in app_settings_value
370 Setter that will always make sure we use unicode in app_settings_value
371
371
372 :param val:
372 :param val:
373 """
373 """
374 val = safe_unicode(val)
374 val = safe_unicode(val)
375 # encode the encrypted value
375 # encode the encrypted value
376 if 'encrypted' in self.app_settings_type:
376 if 'encrypted' in self.app_settings_type:
377 cipher = EncryptedTextValue()
377 cipher = EncryptedTextValue()
378 val = safe_unicode(cipher.process_bind_param(val, None))
378 val = safe_unicode(cipher.process_bind_param(val, None))
379 self._app_settings_value = val
379 self._app_settings_value = val
380
380
381 @hybrid_property
381 @hybrid_property
382 def app_settings_type(self):
382 def app_settings_type(self):
383 return self._app_settings_type
383 return self._app_settings_type
384
384
385 @app_settings_type.setter
385 @app_settings_type.setter
386 def app_settings_type(self, val):
386 def app_settings_type(self, val):
387 if val.split('.')[0] not in self.SETTINGS_TYPES:
387 if val.split('.')[0] not in self.SETTINGS_TYPES:
388 raise Exception('type must be one of %s got %s'
388 raise Exception('type must be one of %s got %s'
389 % (self.SETTINGS_TYPES.keys(), val))
389 % (self.SETTINGS_TYPES.keys(), val))
390 self._app_settings_type = val
390 self._app_settings_type = val
391
391
392 @classmethod
392 @classmethod
393 def get_by_prefix(cls, prefix):
393 def get_by_prefix(cls, prefix):
394 return RhodeCodeSetting.query()\
394 return RhodeCodeSetting.query()\
395 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
395 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
396 .all()
396 .all()
397
397
398 def __unicode__(self):
398 def __unicode__(self):
399 return u"<%s('%s:%s[%s]')>" % (
399 return u"<%s('%s:%s[%s]')>" % (
400 self.__class__.__name__,
400 self.__class__.__name__,
401 self.app_settings_name, self.app_settings_value,
401 self.app_settings_name, self.app_settings_value,
402 self.app_settings_type
402 self.app_settings_type
403 )
403 )
404
404
405
405
406 class RhodeCodeUi(Base, BaseModel):
406 class RhodeCodeUi(Base, BaseModel):
407 __tablename__ = 'rhodecode_ui'
407 __tablename__ = 'rhodecode_ui'
408 __table_args__ = (
408 __table_args__ = (
409 UniqueConstraint('ui_key'),
409 UniqueConstraint('ui_key'),
410 base_table_args
410 base_table_args
411 )
411 )
412
412
413 HOOK_REPO_SIZE = 'changegroup.repo_size'
413 HOOK_REPO_SIZE = 'changegroup.repo_size'
414 # HG
414 # HG
415 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
415 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
416 HOOK_PULL = 'outgoing.pull_logger'
416 HOOK_PULL = 'outgoing.pull_logger'
417 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
417 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
418 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
418 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
419 HOOK_PUSH = 'changegroup.push_logger'
419 HOOK_PUSH = 'changegroup.push_logger'
420 HOOK_PUSH_KEY = 'pushkey.key_push'
420 HOOK_PUSH_KEY = 'pushkey.key_push'
421
421
422 HOOKS_BUILTIN = [
422 HOOKS_BUILTIN = [
423 HOOK_PRE_PULL,
423 HOOK_PRE_PULL,
424 HOOK_PULL,
424 HOOK_PULL,
425 HOOK_PRE_PUSH,
425 HOOK_PRE_PUSH,
426 HOOK_PRETX_PUSH,
426 HOOK_PRETX_PUSH,
427 HOOK_PUSH,
427 HOOK_PUSH,
428 HOOK_PUSH_KEY,
428 HOOK_PUSH_KEY,
429 ]
429 ]
430
430
431 # TODO: johbo: Unify way how hooks are configured for git and hg,
431 # TODO: johbo: Unify way how hooks are configured for git and hg,
432 # git part is currently hardcoded.
432 # git part is currently hardcoded.
433
433
434 # SVN PATTERNS
434 # SVN PATTERNS
435 SVN_BRANCH_ID = 'vcs_svn_branch'
435 SVN_BRANCH_ID = 'vcs_svn_branch'
436 SVN_TAG_ID = 'vcs_svn_tag'
436 SVN_TAG_ID = 'vcs_svn_tag'
437
437
438 ui_id = Column(
438 ui_id = Column(
439 "ui_id", Integer(), nullable=False, unique=True, default=None,
439 "ui_id", Integer(), nullable=False, unique=True, default=None,
440 primary_key=True)
440 primary_key=True)
441 ui_section = Column(
441 ui_section = Column(
442 "ui_section", String(255), nullable=True, unique=None, default=None)
442 "ui_section", String(255), nullable=True, unique=None, default=None)
443 ui_key = Column(
443 ui_key = Column(
444 "ui_key", String(255), nullable=True, unique=None, default=None)
444 "ui_key", String(255), nullable=True, unique=None, default=None)
445 ui_value = Column(
445 ui_value = Column(
446 "ui_value", String(255), nullable=True, unique=None, default=None)
446 "ui_value", String(255), nullable=True, unique=None, default=None)
447 ui_active = Column(
447 ui_active = Column(
448 "ui_active", Boolean(), nullable=True, unique=None, default=True)
448 "ui_active", Boolean(), nullable=True, unique=None, default=True)
449
449
450 def __repr__(self):
450 def __repr__(self):
451 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
451 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
452 self.ui_key, self.ui_value)
452 self.ui_key, self.ui_value)
453
453
454
454
455 class RepoRhodeCodeSetting(Base, BaseModel):
455 class RepoRhodeCodeSetting(Base, BaseModel):
456 __tablename__ = 'repo_rhodecode_settings'
456 __tablename__ = 'repo_rhodecode_settings'
457 __table_args__ = (
457 __table_args__ = (
458 UniqueConstraint(
458 UniqueConstraint(
459 'app_settings_name', 'repository_id',
459 'app_settings_name', 'repository_id',
460 name='uq_repo_rhodecode_setting_name_repo_id'),
460 name='uq_repo_rhodecode_setting_name_repo_id'),
461 base_table_args
461 base_table_args
462 )
462 )
463
463
464 repository_id = Column(
464 repository_id = Column(
465 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
465 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
466 nullable=False)
466 nullable=False)
467 app_settings_id = Column(
467 app_settings_id = Column(
468 "app_settings_id", Integer(), nullable=False, unique=True,
468 "app_settings_id", Integer(), nullable=False, unique=True,
469 default=None, primary_key=True)
469 default=None, primary_key=True)
470 app_settings_name = Column(
470 app_settings_name = Column(
471 "app_settings_name", String(255), nullable=True, unique=None,
471 "app_settings_name", String(255), nullable=True, unique=None,
472 default=None)
472 default=None)
473 _app_settings_value = Column(
473 _app_settings_value = Column(
474 "app_settings_value", String(4096), nullable=True, unique=None,
474 "app_settings_value", String(4096), nullable=True, unique=None,
475 default=None)
475 default=None)
476 _app_settings_type = Column(
476 _app_settings_type = Column(
477 "app_settings_type", String(255), nullable=True, unique=None,
477 "app_settings_type", String(255), nullable=True, unique=None,
478 default=None)
478 default=None)
479
479
480 repository = relationship('Repository')
480 repository = relationship('Repository')
481
481
482 def __init__(self, repository_id, key='', val='', type='unicode'):
482 def __init__(self, repository_id, key='', val='', type='unicode'):
483 self.repository_id = repository_id
483 self.repository_id = repository_id
484 self.app_settings_name = key
484 self.app_settings_name = key
485 self.app_settings_type = type
485 self.app_settings_type = type
486 self.app_settings_value = val
486 self.app_settings_value = val
487
487
488 @validates('_app_settings_value')
488 @validates('_app_settings_value')
489 def validate_settings_value(self, key, val):
489 def validate_settings_value(self, key, val):
490 assert type(val) == unicode
490 assert type(val) == unicode
491 return val
491 return val
492
492
493 @hybrid_property
493 @hybrid_property
494 def app_settings_value(self):
494 def app_settings_value(self):
495 v = self._app_settings_value
495 v = self._app_settings_value
496 type_ = self.app_settings_type
496 type_ = self.app_settings_type
497 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
497 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
498 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
498 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
499 return converter(v)
499 return converter(v)
500
500
501 @app_settings_value.setter
501 @app_settings_value.setter
502 def app_settings_value(self, val):
502 def app_settings_value(self, val):
503 """
503 """
504 Setter that will always make sure we use unicode in app_settings_value
504 Setter that will always make sure we use unicode in app_settings_value
505
505
506 :param val:
506 :param val:
507 """
507 """
508 self._app_settings_value = safe_unicode(val)
508 self._app_settings_value = safe_unicode(val)
509
509
510 @hybrid_property
510 @hybrid_property
511 def app_settings_type(self):
511 def app_settings_type(self):
512 return self._app_settings_type
512 return self._app_settings_type
513
513
514 @app_settings_type.setter
514 @app_settings_type.setter
515 def app_settings_type(self, val):
515 def app_settings_type(self, val):
516 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
516 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
517 if val not in SETTINGS_TYPES:
517 if val not in SETTINGS_TYPES:
518 raise Exception('type must be one of %s got %s'
518 raise Exception('type must be one of %s got %s'
519 % (SETTINGS_TYPES.keys(), val))
519 % (SETTINGS_TYPES.keys(), val))
520 self._app_settings_type = val
520 self._app_settings_type = val
521
521
522 def __unicode__(self):
522 def __unicode__(self):
523 return u"<%s('%s:%s:%s[%s]')>" % (
523 return u"<%s('%s:%s:%s[%s]')>" % (
524 self.__class__.__name__, self.repository.repo_name,
524 self.__class__.__name__, self.repository.repo_name,
525 self.app_settings_name, self.app_settings_value,
525 self.app_settings_name, self.app_settings_value,
526 self.app_settings_type
526 self.app_settings_type
527 )
527 )
528
528
529
529
530 class RepoRhodeCodeUi(Base, BaseModel):
530 class RepoRhodeCodeUi(Base, BaseModel):
531 __tablename__ = 'repo_rhodecode_ui'
531 __tablename__ = 'repo_rhodecode_ui'
532 __table_args__ = (
532 __table_args__ = (
533 UniqueConstraint(
533 UniqueConstraint(
534 'repository_id', 'ui_section', 'ui_key',
534 'repository_id', 'ui_section', 'ui_key',
535 name='uq_repo_rhodecode_ui_repository_id_section_key'),
535 name='uq_repo_rhodecode_ui_repository_id_section_key'),
536 base_table_args
536 base_table_args
537 )
537 )
538
538
539 repository_id = Column(
539 repository_id = Column(
540 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
540 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
541 nullable=False)
541 nullable=False)
542 ui_id = Column(
542 ui_id = Column(
543 "ui_id", Integer(), nullable=False, unique=True, default=None,
543 "ui_id", Integer(), nullable=False, unique=True, default=None,
544 primary_key=True)
544 primary_key=True)
545 ui_section = Column(
545 ui_section = Column(
546 "ui_section", String(255), nullable=True, unique=None, default=None)
546 "ui_section", String(255), nullable=True, unique=None, default=None)
547 ui_key = Column(
547 ui_key = Column(
548 "ui_key", String(255), nullable=True, unique=None, default=None)
548 "ui_key", String(255), nullable=True, unique=None, default=None)
549 ui_value = Column(
549 ui_value = Column(
550 "ui_value", String(255), nullable=True, unique=None, default=None)
550 "ui_value", String(255), nullable=True, unique=None, default=None)
551 ui_active = Column(
551 ui_active = Column(
552 "ui_active", Boolean(), nullable=True, unique=None, default=True)
552 "ui_active", Boolean(), nullable=True, unique=None, default=True)
553
553
554 repository = relationship('Repository')
554 repository = relationship('Repository')
555
555
556 def __repr__(self):
556 def __repr__(self):
557 return '<%s[%s:%s]%s=>%s]>' % (
557 return '<%s[%s:%s]%s=>%s]>' % (
558 self.__class__.__name__, self.repository.repo_name,
558 self.__class__.__name__, self.repository.repo_name,
559 self.ui_section, self.ui_key, self.ui_value)
559 self.ui_section, self.ui_key, self.ui_value)
560
560
561
561
562 class User(Base, BaseModel):
562 class User(Base, BaseModel):
563 __tablename__ = 'users'
563 __tablename__ = 'users'
564 __table_args__ = (
564 __table_args__ = (
565 UniqueConstraint('username'), UniqueConstraint('email'),
565 UniqueConstraint('username'), UniqueConstraint('email'),
566 Index('u_username_idx', 'username'),
566 Index('u_username_idx', 'username'),
567 Index('u_email_idx', 'email'),
567 Index('u_email_idx', 'email'),
568 base_table_args
568 base_table_args
569 )
569 )
570
570
571 DEFAULT_USER = 'default'
571 DEFAULT_USER = 'default'
572 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
572 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
573 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
573 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
574
574
575 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
575 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
576 username = Column("username", String(255), nullable=True, unique=None, default=None)
576 username = Column("username", String(255), nullable=True, unique=None, default=None)
577 password = Column("password", String(255), nullable=True, unique=None, default=None)
577 password = Column("password", String(255), nullable=True, unique=None, default=None)
578 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
578 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
579 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
579 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
580 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
580 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
581 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
581 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
582 _email = Column("email", String(255), nullable=True, unique=None, default=None)
582 _email = Column("email", String(255), nullable=True, unique=None, default=None)
583 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
583 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
584 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
584 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
585 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
585 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
586
586
587 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
587 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
588 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
588 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
589 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
589 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
590 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
590 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
591 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
591 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
592 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
592 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
593
593
594 user_log = relationship('UserLog')
594 user_log = relationship('UserLog')
595 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
595 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
596
596
597 repositories = relationship('Repository')
597 repositories = relationship('Repository')
598 repository_groups = relationship('RepoGroup')
598 repository_groups = relationship('RepoGroup')
599 user_groups = relationship('UserGroup')
599 user_groups = relationship('UserGroup')
600
600
601 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
601 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
602 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
602 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
603
603
604 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
604 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
605 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
605 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
606 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
606 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
607
607
608 group_member = relationship('UserGroupMember', cascade='all')
608 group_member = relationship('UserGroupMember', cascade='all')
609
609
610 notifications = relationship('UserNotification', cascade='all')
610 notifications = relationship('UserNotification', cascade='all')
611 # notifications assigned to this user
611 # notifications assigned to this user
612 user_created_notifications = relationship('Notification', cascade='all')
612 user_created_notifications = relationship('Notification', cascade='all')
613 # comments created by this user
613 # comments created by this user
614 user_comments = relationship('ChangesetComment', cascade='all')
614 user_comments = relationship('ChangesetComment', cascade='all')
615 # user profile extra info
615 # user profile extra info
616 user_emails = relationship('UserEmailMap', cascade='all')
616 user_emails = relationship('UserEmailMap', cascade='all')
617 user_ip_map = relationship('UserIpMap', cascade='all')
617 user_ip_map = relationship('UserIpMap', cascade='all')
618 user_auth_tokens = relationship('UserApiKeys', cascade='all')
618 user_auth_tokens = relationship('UserApiKeys', cascade='all')
619 user_ssh_keys = relationship('UserSshKeys', cascade='all')
619 user_ssh_keys = relationship('UserSshKeys', cascade='all')
620
620
621 # gists
621 # gists
622 user_gists = relationship('Gist', cascade='all')
622 user_gists = relationship('Gist', cascade='all')
623 # user pull requests
623 # user pull requests
624 user_pull_requests = relationship('PullRequest', cascade='all')
624 user_pull_requests = relationship('PullRequest', cascade='all')
625
625
626 # external identities
626 # external identities
627 external_identities = relationship(
627 external_identities = relationship(
628 'ExternalIdentity',
628 'ExternalIdentity',
629 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
629 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
630 cascade='all')
630 cascade='all')
631 # review rules
631 # review rules
632 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
632 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
633
633
634 # artifacts owned
634 # artifacts owned
635 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
635 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
636
636
637 # no cascade, set NULL
637 # no cascade, set NULL
638 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
638 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
639
639
640 def __unicode__(self):
640 def __unicode__(self):
641 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
641 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
642 self.user_id, self.username)
642 self.user_id, self.username)
643
643
644 @hybrid_property
644 @hybrid_property
645 def email(self):
645 def email(self):
646 return self._email
646 return self._email
647
647
648 @email.setter
648 @email.setter
649 def email(self, val):
649 def email(self, val):
650 self._email = val.lower() if val else None
650 self._email = val.lower() if val else None
651
651
652 @hybrid_property
652 @hybrid_property
653 def first_name(self):
653 def first_name(self):
654 from rhodecode.lib import helpers as h
654 from rhodecode.lib import helpers as h
655 if self.name:
655 if self.name:
656 return h.escape(self.name)
656 return h.escape(self.name)
657 return self.name
657 return self.name
658
658
659 @hybrid_property
659 @hybrid_property
660 def last_name(self):
660 def last_name(self):
661 from rhodecode.lib import helpers as h
661 from rhodecode.lib import helpers as h
662 if self.lastname:
662 if self.lastname:
663 return h.escape(self.lastname)
663 return h.escape(self.lastname)
664 return self.lastname
664 return self.lastname
665
665
666 @hybrid_property
666 @hybrid_property
667 def api_key(self):
667 def api_key(self):
668 """
668 """
669 Fetch if exist an auth-token with role ALL connected to this user
669 Fetch if exist an auth-token with role ALL connected to this user
670 """
670 """
671 user_auth_token = UserApiKeys.query()\
671 user_auth_token = UserApiKeys.query()\
672 .filter(UserApiKeys.user_id == self.user_id)\
672 .filter(UserApiKeys.user_id == self.user_id)\
673 .filter(or_(UserApiKeys.expires == -1,
673 .filter(or_(UserApiKeys.expires == -1,
674 UserApiKeys.expires >= time.time()))\
674 UserApiKeys.expires >= time.time()))\
675 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
675 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
676 if user_auth_token:
676 if user_auth_token:
677 user_auth_token = user_auth_token.api_key
677 user_auth_token = user_auth_token.api_key
678
678
679 return user_auth_token
679 return user_auth_token
680
680
681 @api_key.setter
681 @api_key.setter
682 def api_key(self, val):
682 def api_key(self, val):
683 # don't allow to set API key this is deprecated for now
683 # don't allow to set API key this is deprecated for now
684 self._api_key = None
684 self._api_key = None
685
685
686 @property
686 @property
687 def reviewer_pull_requests(self):
687 def reviewer_pull_requests(self):
688 return PullRequestReviewers.query() \
688 return PullRequestReviewers.query() \
689 .options(joinedload(PullRequestReviewers.pull_request)) \
689 .options(joinedload(PullRequestReviewers.pull_request)) \
690 .filter(PullRequestReviewers.user_id == self.user_id) \
690 .filter(PullRequestReviewers.user_id == self.user_id) \
691 .all()
691 .all()
692
692
693 @property
693 @property
694 def firstname(self):
694 def firstname(self):
695 # alias for future
695 # alias for future
696 return self.name
696 return self.name
697
697
698 @property
698 @property
699 def emails(self):
699 def emails(self):
700 other = UserEmailMap.query()\
700 other = UserEmailMap.query()\
701 .filter(UserEmailMap.user == self) \
701 .filter(UserEmailMap.user == self) \
702 .order_by(UserEmailMap.email_id.asc()) \
702 .order_by(UserEmailMap.email_id.asc()) \
703 .all()
703 .all()
704 return [self.email] + [x.email for x in other]
704 return [self.email] + [x.email for x in other]
705
705
706 def emails_cached(self):
706 def emails_cached(self):
707 emails = UserEmailMap.query()\
707 emails = UserEmailMap.query()\
708 .filter(UserEmailMap.user == self) \
708 .filter(UserEmailMap.user == self) \
709 .order_by(UserEmailMap.email_id.asc())
709 .order_by(UserEmailMap.email_id.asc())
710
710
711 emails = emails.options(
711 emails = emails.options(
712 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
712 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
713 )
713 )
714
714
715 return [self.email] + [x.email for x in emails]
715 return [self.email] + [x.email for x in emails]
716
716
717 @property
717 @property
718 def auth_tokens(self):
718 def auth_tokens(self):
719 auth_tokens = self.get_auth_tokens()
719 auth_tokens = self.get_auth_tokens()
720 return [x.api_key for x in auth_tokens]
720 return [x.api_key for x in auth_tokens]
721
721
722 def get_auth_tokens(self):
722 def get_auth_tokens(self):
723 return UserApiKeys.query()\
723 return UserApiKeys.query()\
724 .filter(UserApiKeys.user == self)\
724 .filter(UserApiKeys.user == self)\
725 .order_by(UserApiKeys.user_api_key_id.asc())\
725 .order_by(UserApiKeys.user_api_key_id.asc())\
726 .all()
726 .all()
727
727
728 @LazyProperty
728 @LazyProperty
729 def feed_token(self):
729 def feed_token(self):
730 return self.get_feed_token()
730 return self.get_feed_token()
731
731
732 def get_feed_token(self, cache=True):
732 def get_feed_token(self, cache=True):
733 feed_tokens = UserApiKeys.query()\
733 feed_tokens = UserApiKeys.query()\
734 .filter(UserApiKeys.user == self)\
734 .filter(UserApiKeys.user == self)\
735 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
735 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
736 if cache:
736 if cache:
737 feed_tokens = feed_tokens.options(
737 feed_tokens = feed_tokens.options(
738 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
738 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
739
739
740 feed_tokens = feed_tokens.all()
740 feed_tokens = feed_tokens.all()
741 if feed_tokens:
741 if feed_tokens:
742 return feed_tokens[0].api_key
742 return feed_tokens[0].api_key
743 return 'NO_FEED_TOKEN_AVAILABLE'
743 return 'NO_FEED_TOKEN_AVAILABLE'
744
744
745 @LazyProperty
745 @LazyProperty
746 def artifact_token(self):
746 def artifact_token(self):
747 return self.get_artifact_token()
747 return self.get_artifact_token()
748
748
749 def get_artifact_token(self, cache=True):
749 def get_artifact_token(self, cache=True):
750 artifacts_tokens = UserApiKeys.query()\
750 artifacts_tokens = UserApiKeys.query()\
751 .filter(UserApiKeys.user == self)\
751 .filter(UserApiKeys.user == self)\
752 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
752 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
753 if cache:
753 if cache:
754 artifacts_tokens = artifacts_tokens.options(
754 artifacts_tokens = artifacts_tokens.options(
755 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
755 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
756
756
757 artifacts_tokens = artifacts_tokens.all()
757 artifacts_tokens = artifacts_tokens.all()
758 if artifacts_tokens:
758 if artifacts_tokens:
759 return artifacts_tokens[0].api_key
759 return artifacts_tokens[0].api_key
760 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
760 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
761
761
762 @classmethod
762 @classmethod
763 def get(cls, user_id, cache=False):
763 def get(cls, user_id, cache=False):
764 if not user_id:
764 if not user_id:
765 return
765 return
766
766
767 user = cls.query()
767 user = cls.query()
768 if cache:
768 if cache:
769 user = user.options(
769 user = user.options(
770 FromCache("sql_cache_short", "get_users_%s" % user_id))
770 FromCache("sql_cache_short", "get_users_%s" % user_id))
771 return user.get(user_id)
771 return user.get(user_id)
772
772
773 @classmethod
773 @classmethod
774 def extra_valid_auth_tokens(cls, user, role=None):
774 def extra_valid_auth_tokens(cls, user, role=None):
775 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
775 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
776 .filter(or_(UserApiKeys.expires == -1,
776 .filter(or_(UserApiKeys.expires == -1,
777 UserApiKeys.expires >= time.time()))
777 UserApiKeys.expires >= time.time()))
778 if role:
778 if role:
779 tokens = tokens.filter(or_(UserApiKeys.role == role,
779 tokens = tokens.filter(or_(UserApiKeys.role == role,
780 UserApiKeys.role == UserApiKeys.ROLE_ALL))
780 UserApiKeys.role == UserApiKeys.ROLE_ALL))
781 return tokens.all()
781 return tokens.all()
782
782
783 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
783 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
784 from rhodecode.lib import auth
784 from rhodecode.lib import auth
785
785
786 log.debug('Trying to authenticate user: %s via auth-token, '
786 log.debug('Trying to authenticate user: %s via auth-token, '
787 'and roles: %s', self, roles)
787 'and roles: %s', self, roles)
788
788
789 if not auth_token:
789 if not auth_token:
790 return False
790 return False
791
791
792 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
792 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
793 tokens_q = UserApiKeys.query()\
793 tokens_q = UserApiKeys.query()\
794 .filter(UserApiKeys.user_id == self.user_id)\
794 .filter(UserApiKeys.user_id == self.user_id)\
795 .filter(or_(UserApiKeys.expires == -1,
795 .filter(or_(UserApiKeys.expires == -1,
796 UserApiKeys.expires >= time.time()))
796 UserApiKeys.expires >= time.time()))
797
797
798 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
798 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
799
799
800 crypto_backend = auth.crypto_backend()
800 crypto_backend = auth.crypto_backend()
801 enc_token_map = {}
801 enc_token_map = {}
802 plain_token_map = {}
802 plain_token_map = {}
803 for token in tokens_q:
803 for token in tokens_q:
804 if token.api_key.startswith(crypto_backend.ENC_PREF):
804 if token.api_key.startswith(crypto_backend.ENC_PREF):
805 enc_token_map[token.api_key] = token
805 enc_token_map[token.api_key] = token
806 else:
806 else:
807 plain_token_map[token.api_key] = token
807 plain_token_map[token.api_key] = token
808 log.debug(
808 log.debug(
809 'Found %s plain and %s encrypted tokens to check for authentication for this user',
809 'Found %s plain and %s encrypted tokens to check for authentication for this user',
810 len(plain_token_map), len(enc_token_map))
810 len(plain_token_map), len(enc_token_map))
811
811
812 # plain token match comes first
812 # plain token match comes first
813 match = plain_token_map.get(auth_token)
813 match = plain_token_map.get(auth_token)
814
814
815 # check encrypted tokens now
815 # check encrypted tokens now
816 if not match:
816 if not match:
817 for token_hash, token in enc_token_map.items():
817 for token_hash, token in enc_token_map.items():
818 # NOTE(marcink): this is expensive to calculate, but most secure
818 # NOTE(marcink): this is expensive to calculate, but most secure
819 if crypto_backend.hash_check(auth_token, token_hash):
819 if crypto_backend.hash_check(auth_token, token_hash):
820 match = token
820 match = token
821 break
821 break
822
822
823 if match:
823 if match:
824 log.debug('Found matching token %s', match)
824 log.debug('Found matching token %s', match)
825 if match.repo_id:
825 if match.repo_id:
826 log.debug('Found scope, checking for scope match of token %s', match)
826 log.debug('Found scope, checking for scope match of token %s', match)
827 if match.repo_id == scope_repo_id:
827 if match.repo_id == scope_repo_id:
828 return True
828 return True
829 else:
829 else:
830 log.debug(
830 log.debug(
831 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
831 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
832 'and calling scope is:%s, skipping further checks',
832 'and calling scope is:%s, skipping further checks',
833 match.repo, scope_repo_id)
833 match.repo, scope_repo_id)
834 return False
834 return False
835 else:
835 else:
836 return True
836 return True
837
837
838 return False
838 return False
839
839
840 @property
840 @property
841 def ip_addresses(self):
841 def ip_addresses(self):
842 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
842 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
843 return [x.ip_addr for x in ret]
843 return [x.ip_addr for x in ret]
844
844
845 @property
845 @property
846 def username_and_name(self):
846 def username_and_name(self):
847 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
847 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
848
848
849 @property
849 @property
850 def username_or_name_or_email(self):
850 def username_or_name_or_email(self):
851 full_name = self.full_name if self.full_name is not ' ' else None
851 full_name = self.full_name if self.full_name is not ' ' else None
852 return self.username or full_name or self.email
852 return self.username or full_name or self.email
853
853
854 @property
854 @property
855 def full_name(self):
855 def full_name(self):
856 return '%s %s' % (self.first_name, self.last_name)
856 return '%s %s' % (self.first_name, self.last_name)
857
857
858 @property
858 @property
859 def full_name_or_username(self):
859 def full_name_or_username(self):
860 return ('%s %s' % (self.first_name, self.last_name)
860 return ('%s %s' % (self.first_name, self.last_name)
861 if (self.first_name and self.last_name) else self.username)
861 if (self.first_name and self.last_name) else self.username)
862
862
863 @property
863 @property
864 def full_contact(self):
864 def full_contact(self):
865 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
865 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
866
866
867 @property
867 @property
868 def short_contact(self):
868 def short_contact(self):
869 return '%s %s' % (self.first_name, self.last_name)
869 return '%s %s' % (self.first_name, self.last_name)
870
870
871 @property
871 @property
872 def is_admin(self):
872 def is_admin(self):
873 return self.admin
873 return self.admin
874
874
875 @property
875 @property
876 def language(self):
876 def language(self):
877 return self.user_data.get('language')
877 return self.user_data.get('language')
878
878
879 def AuthUser(self, **kwargs):
879 def AuthUser(self, **kwargs):
880 """
880 """
881 Returns instance of AuthUser for this user
881 Returns instance of AuthUser for this user
882 """
882 """
883 from rhodecode.lib.auth import AuthUser
883 from rhodecode.lib.auth import AuthUser
884 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
884 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
885
885
886 @hybrid_property
886 @hybrid_property
887 def user_data(self):
887 def user_data(self):
888 if not self._user_data:
888 if not self._user_data:
889 return {}
889 return {}
890
890
891 try:
891 try:
892 return json.loads(self._user_data)
892 return json.loads(self._user_data)
893 except TypeError:
893 except TypeError:
894 return {}
894 return {}
895
895
896 @user_data.setter
896 @user_data.setter
897 def user_data(self, val):
897 def user_data(self, val):
898 if not isinstance(val, dict):
898 if not isinstance(val, dict):
899 raise Exception('user_data must be dict, got %s' % type(val))
899 raise Exception('user_data must be dict, got %s' % type(val))
900 try:
900 try:
901 self._user_data = json.dumps(val)
901 self._user_data = json.dumps(val)
902 except Exception:
902 except Exception:
903 log.error(traceback.format_exc())
903 log.error(traceback.format_exc())
904
904
905 @classmethod
905 @classmethod
906 def get_by_username(cls, username, case_insensitive=False,
906 def get_by_username(cls, username, case_insensitive=False,
907 cache=False, identity_cache=False):
907 cache=False, identity_cache=False):
908 session = Session()
908 session = Session()
909
909
910 if case_insensitive:
910 if case_insensitive:
911 q = cls.query().filter(
911 q = cls.query().filter(
912 func.lower(cls.username) == func.lower(username))
912 func.lower(cls.username) == func.lower(username))
913 else:
913 else:
914 q = cls.query().filter(cls.username == username)
914 q = cls.query().filter(cls.username == username)
915
915
916 if cache:
916 if cache:
917 if identity_cache:
917 if identity_cache:
918 val = cls.identity_cache(session, 'username', username)
918 val = cls.identity_cache(session, 'username', username)
919 if val:
919 if val:
920 return val
920 return val
921 else:
921 else:
922 cache_key = "get_user_by_name_%s" % _hash_key(username)
922 cache_key = "get_user_by_name_%s" % _hash_key(username)
923 q = q.options(
923 q = q.options(
924 FromCache("sql_cache_short", cache_key))
924 FromCache("sql_cache_short", cache_key))
925
925
926 return q.scalar()
926 return q.scalar()
927
927
928 @classmethod
928 @classmethod
929 def get_by_auth_token(cls, auth_token, cache=False):
929 def get_by_auth_token(cls, auth_token, cache=False):
930 q = UserApiKeys.query()\
930 q = UserApiKeys.query()\
931 .filter(UserApiKeys.api_key == auth_token)\
931 .filter(UserApiKeys.api_key == auth_token)\
932 .filter(or_(UserApiKeys.expires == -1,
932 .filter(or_(UserApiKeys.expires == -1,
933 UserApiKeys.expires >= time.time()))
933 UserApiKeys.expires >= time.time()))
934 if cache:
934 if cache:
935 q = q.options(
935 q = q.options(
936 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
936 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
937
937
938 match = q.first()
938 match = q.first()
939 if match:
939 if match:
940 return match.user
940 return match.user
941
941
942 @classmethod
942 @classmethod
943 def get_by_email(cls, email, case_insensitive=False, cache=False):
943 def get_by_email(cls, email, case_insensitive=False, cache=False):
944
944
945 if case_insensitive:
945 if case_insensitive:
946 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
946 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
947
947
948 else:
948 else:
949 q = cls.query().filter(cls.email == email)
949 q = cls.query().filter(cls.email == email)
950
950
951 email_key = _hash_key(email)
951 email_key = _hash_key(email)
952 if cache:
952 if cache:
953 q = q.options(
953 q = q.options(
954 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
954 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
955
955
956 ret = q.scalar()
956 ret = q.scalar()
957 if ret is None:
957 if ret is None:
958 q = UserEmailMap.query()
958 q = UserEmailMap.query()
959 # try fetching in alternate email map
959 # try fetching in alternate email map
960 if case_insensitive:
960 if case_insensitive:
961 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
961 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
962 else:
962 else:
963 q = q.filter(UserEmailMap.email == email)
963 q = q.filter(UserEmailMap.email == email)
964 q = q.options(joinedload(UserEmailMap.user))
964 q = q.options(joinedload(UserEmailMap.user))
965 if cache:
965 if cache:
966 q = q.options(
966 q = q.options(
967 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
967 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
968 ret = getattr(q.scalar(), 'user', None)
968 ret = getattr(q.scalar(), 'user', None)
969
969
970 return ret
970 return ret
971
971
972 @classmethod
972 @classmethod
973 def get_from_cs_author(cls, author):
973 def get_from_cs_author(cls, author):
974 """
974 """
975 Tries to get User objects out of commit author string
975 Tries to get User objects out of commit author string
976
976
977 :param author:
977 :param author:
978 """
978 """
979 from rhodecode.lib.helpers import email, author_name
979 from rhodecode.lib.helpers import email, author_name
980 # Valid email in the attribute passed, see if they're in the system
980 # Valid email in the attribute passed, see if they're in the system
981 _email = email(author)
981 _email = email(author)
982 if _email:
982 if _email:
983 user = cls.get_by_email(_email, case_insensitive=True)
983 user = cls.get_by_email(_email, case_insensitive=True)
984 if user:
984 if user:
985 return user
985 return user
986 # Maybe we can match by username?
986 # Maybe we can match by username?
987 _author = author_name(author)
987 _author = author_name(author)
988 user = cls.get_by_username(_author, case_insensitive=True)
988 user = cls.get_by_username(_author, case_insensitive=True)
989 if user:
989 if user:
990 return user
990 return user
991
991
992 def update_userdata(self, **kwargs):
992 def update_userdata(self, **kwargs):
993 usr = self
993 usr = self
994 old = usr.user_data
994 old = usr.user_data
995 old.update(**kwargs)
995 old.update(**kwargs)
996 usr.user_data = old
996 usr.user_data = old
997 Session().add(usr)
997 Session().add(usr)
998 log.debug('updated userdata with %s', kwargs)
998 log.debug('updated userdata with %s', kwargs)
999
999
1000 def update_lastlogin(self):
1000 def update_lastlogin(self):
1001 """Update user lastlogin"""
1001 """Update user lastlogin"""
1002 self.last_login = datetime.datetime.now()
1002 self.last_login = datetime.datetime.now()
1003 Session().add(self)
1003 Session().add(self)
1004 log.debug('updated user %s lastlogin', self.username)
1004 log.debug('updated user %s lastlogin', self.username)
1005
1005
1006 def update_password(self, new_password):
1006 def update_password(self, new_password):
1007 from rhodecode.lib.auth import get_crypt_password
1007 from rhodecode.lib.auth import get_crypt_password
1008
1008
1009 self.password = get_crypt_password(new_password)
1009 self.password = get_crypt_password(new_password)
1010 Session().add(self)
1010 Session().add(self)
1011
1011
1012 @classmethod
1012 @classmethod
1013 def get_first_super_admin(cls):
1013 def get_first_super_admin(cls):
1014 user = User.query()\
1014 user = User.query()\
1015 .filter(User.admin == true()) \
1015 .filter(User.admin == true()) \
1016 .order_by(User.user_id.asc()) \
1016 .order_by(User.user_id.asc()) \
1017 .first()
1017 .first()
1018
1018
1019 if user is None:
1019 if user is None:
1020 raise Exception('FATAL: Missing administrative account!')
1020 raise Exception('FATAL: Missing administrative account!')
1021 return user
1021 return user
1022
1022
1023 @classmethod
1023 @classmethod
1024 def get_all_super_admins(cls, only_active=False):
1024 def get_all_super_admins(cls, only_active=False):
1025 """
1025 """
1026 Returns all admin accounts sorted by username
1026 Returns all admin accounts sorted by username
1027 """
1027 """
1028 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1028 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1029 if only_active:
1029 if only_active:
1030 qry = qry.filter(User.active == true())
1030 qry = qry.filter(User.active == true())
1031 return qry.all()
1031 return qry.all()
1032
1032
1033 @classmethod
1033 @classmethod
1034 def get_all_user_ids(cls, only_active=True):
1034 def get_all_user_ids(cls, only_active=True):
1035 """
1035 """
1036 Returns all users IDs
1036 Returns all users IDs
1037 """
1037 """
1038 qry = Session().query(User.user_id)
1038 qry = Session().query(User.user_id)
1039
1039
1040 if only_active:
1040 if only_active:
1041 qry = qry.filter(User.active == true())
1041 qry = qry.filter(User.active == true())
1042 return [x.user_id for x in qry]
1042 return [x.user_id for x in qry]
1043
1043
1044 @classmethod
1044 @classmethod
1045 def get_default_user(cls, cache=False, refresh=False):
1045 def get_default_user(cls, cache=False, refresh=False):
1046 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1046 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1047 if user is None:
1047 if user is None:
1048 raise Exception('FATAL: Missing default account!')
1048 raise Exception('FATAL: Missing default account!')
1049 if refresh:
1049 if refresh:
1050 # The default user might be based on outdated state which
1050 # The default user might be based on outdated state which
1051 # has been loaded from the cache.
1051 # has been loaded from the cache.
1052 # A call to refresh() ensures that the
1052 # A call to refresh() ensures that the
1053 # latest state from the database is used.
1053 # latest state from the database is used.
1054 Session().refresh(user)
1054 Session().refresh(user)
1055 return user
1055 return user
1056
1056
1057 @classmethod
1057 @classmethod
1058 def get_default_user_id(cls):
1058 def get_default_user_id(cls):
1059 import rhodecode
1059 import rhodecode
1060 return rhodecode.CONFIG['default_user_id']
1060 return rhodecode.CONFIG['default_user_id']
1061
1061
1062 def _get_default_perms(self, user, suffix=''):
1062 def _get_default_perms(self, user, suffix=''):
1063 from rhodecode.model.permission import PermissionModel
1063 from rhodecode.model.permission import PermissionModel
1064 return PermissionModel().get_default_perms(user.user_perms, suffix)
1064 return PermissionModel().get_default_perms(user.user_perms, suffix)
1065
1065
1066 def get_default_perms(self, suffix=''):
1066 def get_default_perms(self, suffix=''):
1067 return self._get_default_perms(self, suffix)
1067 return self._get_default_perms(self, suffix)
1068
1068
1069 def get_api_data(self, include_secrets=False, details='full'):
1069 def get_api_data(self, include_secrets=False, details='full'):
1070 """
1070 """
1071 Common function for generating user related data for API
1071 Common function for generating user related data for API
1072
1072
1073 :param include_secrets: By default secrets in the API data will be replaced
1073 :param include_secrets: By default secrets in the API data will be replaced
1074 by a placeholder value to prevent exposing this data by accident. In case
1074 by a placeholder value to prevent exposing this data by accident. In case
1075 this data shall be exposed, set this flag to ``True``.
1075 this data shall be exposed, set this flag to ``True``.
1076
1076
1077 :param details: details can be 'basic|full' basic gives only a subset of
1077 :param details: details can be 'basic|full' basic gives only a subset of
1078 the available user information that includes user_id, name and emails.
1078 the available user information that includes user_id, name and emails.
1079 """
1079 """
1080 user = self
1080 user = self
1081 user_data = self.user_data
1081 user_data = self.user_data
1082 data = {
1082 data = {
1083 'user_id': user.user_id,
1083 'user_id': user.user_id,
1084 'username': user.username,
1084 'username': user.username,
1085 'firstname': user.name,
1085 'firstname': user.name,
1086 'lastname': user.lastname,
1086 'lastname': user.lastname,
1087 'description': user.description,
1087 'description': user.description,
1088 'email': user.email,
1088 'email': user.email,
1089 'emails': user.emails,
1089 'emails': user.emails,
1090 }
1090 }
1091 if details == 'basic':
1091 if details == 'basic':
1092 return data
1092 return data
1093
1093
1094 auth_token_length = 40
1094 auth_token_length = 40
1095 auth_token_replacement = '*' * auth_token_length
1095 auth_token_replacement = '*' * auth_token_length
1096
1096
1097 extras = {
1097 extras = {
1098 'auth_tokens': [auth_token_replacement],
1098 'auth_tokens': [auth_token_replacement],
1099 'active': user.active,
1099 'active': user.active,
1100 'admin': user.admin,
1100 'admin': user.admin,
1101 'extern_type': user.extern_type,
1101 'extern_type': user.extern_type,
1102 'extern_name': user.extern_name,
1102 'extern_name': user.extern_name,
1103 'last_login': user.last_login,
1103 'last_login': user.last_login,
1104 'last_activity': user.last_activity,
1104 'last_activity': user.last_activity,
1105 'ip_addresses': user.ip_addresses,
1105 'ip_addresses': user.ip_addresses,
1106 'language': user_data.get('language')
1106 'language': user_data.get('language')
1107 }
1107 }
1108 data.update(extras)
1108 data.update(extras)
1109
1109
1110 if include_secrets:
1110 if include_secrets:
1111 data['auth_tokens'] = user.auth_tokens
1111 data['auth_tokens'] = user.auth_tokens
1112 return data
1112 return data
1113
1113
1114 def __json__(self):
1114 def __json__(self):
1115 data = {
1115 data = {
1116 'full_name': self.full_name,
1116 'full_name': self.full_name,
1117 'full_name_or_username': self.full_name_or_username,
1117 'full_name_or_username': self.full_name_or_username,
1118 'short_contact': self.short_contact,
1118 'short_contact': self.short_contact,
1119 'full_contact': self.full_contact,
1119 'full_contact': self.full_contact,
1120 }
1120 }
1121 data.update(self.get_api_data())
1121 data.update(self.get_api_data())
1122 return data
1122 return data
1123
1123
1124
1124
1125 class UserApiKeys(Base, BaseModel):
1125 class UserApiKeys(Base, BaseModel):
1126 __tablename__ = 'user_api_keys'
1126 __tablename__ = 'user_api_keys'
1127 __table_args__ = (
1127 __table_args__ = (
1128 Index('uak_api_key_idx', 'api_key'),
1128 Index('uak_api_key_idx', 'api_key'),
1129 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1129 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1130 base_table_args
1130 base_table_args
1131 )
1131 )
1132 __mapper_args__ = {}
1132 __mapper_args__ = {}
1133
1133
1134 # ApiKey role
1134 # ApiKey role
1135 ROLE_ALL = 'token_role_all'
1135 ROLE_ALL = 'token_role_all'
1136 ROLE_VCS = 'token_role_vcs'
1136 ROLE_VCS = 'token_role_vcs'
1137 ROLE_API = 'token_role_api'
1137 ROLE_API = 'token_role_api'
1138 ROLE_HTTP = 'token_role_http'
1138 ROLE_HTTP = 'token_role_http'
1139 ROLE_FEED = 'token_role_feed'
1139 ROLE_FEED = 'token_role_feed'
1140 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1140 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1141 # The last one is ignored in the list as we only
1141 # The last one is ignored in the list as we only
1142 # use it for one action, and cannot be created by users
1142 # use it for one action, and cannot be created by users
1143 ROLE_PASSWORD_RESET = 'token_password_reset'
1143 ROLE_PASSWORD_RESET = 'token_password_reset'
1144
1144
1145 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1145 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1146
1146
1147 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1147 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1148 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1148 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1149 api_key = Column("api_key", String(255), nullable=False, unique=True)
1149 api_key = Column("api_key", String(255), nullable=False, unique=True)
1150 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1150 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1151 expires = Column('expires', Float(53), nullable=False)
1151 expires = Column('expires', Float(53), nullable=False)
1152 role = Column('role', String(255), nullable=True)
1152 role = Column('role', String(255), nullable=True)
1153 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1153 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1154
1154
1155 # scope columns
1155 # scope columns
1156 repo_id = Column(
1156 repo_id = Column(
1157 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1157 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1158 nullable=True, unique=None, default=None)
1158 nullable=True, unique=None, default=None)
1159 repo = relationship('Repository', lazy='joined')
1159 repo = relationship('Repository', lazy='joined')
1160
1160
1161 repo_group_id = Column(
1161 repo_group_id = Column(
1162 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1162 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1163 nullable=True, unique=None, default=None)
1163 nullable=True, unique=None, default=None)
1164 repo_group = relationship('RepoGroup', lazy='joined')
1164 repo_group = relationship('RepoGroup', lazy='joined')
1165
1165
1166 user = relationship('User', lazy='joined')
1166 user = relationship('User', lazy='joined')
1167
1167
1168 def __unicode__(self):
1168 def __unicode__(self):
1169 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1169 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1170
1170
1171 def __json__(self):
1171 def __json__(self):
1172 data = {
1172 data = {
1173 'auth_token': self.api_key,
1173 'auth_token': self.api_key,
1174 'role': self.role,
1174 'role': self.role,
1175 'scope': self.scope_humanized,
1175 'scope': self.scope_humanized,
1176 'expired': self.expired
1176 'expired': self.expired
1177 }
1177 }
1178 return data
1178 return data
1179
1179
1180 def get_api_data(self, include_secrets=False):
1180 def get_api_data(self, include_secrets=False):
1181 data = self.__json__()
1181 data = self.__json__()
1182 if include_secrets:
1182 if include_secrets:
1183 return data
1183 return data
1184 else:
1184 else:
1185 data['auth_token'] = self.token_obfuscated
1185 data['auth_token'] = self.token_obfuscated
1186 return data
1186 return data
1187
1187
1188 @hybrid_property
1188 @hybrid_property
1189 def description_safe(self):
1189 def description_safe(self):
1190 from rhodecode.lib import helpers as h
1190 from rhodecode.lib import helpers as h
1191 return h.escape(self.description)
1191 return h.escape(self.description)
1192
1192
1193 @property
1193 @property
1194 def expired(self):
1194 def expired(self):
1195 if self.expires == -1:
1195 if self.expires == -1:
1196 return False
1196 return False
1197 return time.time() > self.expires
1197 return time.time() > self.expires
1198
1198
1199 @classmethod
1199 @classmethod
1200 def _get_role_name(cls, role):
1200 def _get_role_name(cls, role):
1201 return {
1201 return {
1202 cls.ROLE_ALL: _('all'),
1202 cls.ROLE_ALL: _('all'),
1203 cls.ROLE_HTTP: _('http/web interface'),
1203 cls.ROLE_HTTP: _('http/web interface'),
1204 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1204 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1205 cls.ROLE_API: _('api calls'),
1205 cls.ROLE_API: _('api calls'),
1206 cls.ROLE_FEED: _('feed access'),
1206 cls.ROLE_FEED: _('feed access'),
1207 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1207 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1208 }.get(role, role)
1208 }.get(role, role)
1209
1209
1210 @classmethod
1210 @classmethod
1211 def _get_role_description(cls, role):
1211 def _get_role_description(cls, role):
1212 return {
1212 return {
1213 cls.ROLE_ALL: _('Token for all actions.'),
1213 cls.ROLE_ALL: _('Token for all actions.'),
1214 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1214 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1215 'login using `api_access_controllers_whitelist` functionality.'),
1215 'login using `api_access_controllers_whitelist` functionality.'),
1216 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1216 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1217 'Requires auth_token authentication plugin to be active. <br/>'
1217 'Requires auth_token authentication plugin to be active. <br/>'
1218 'Such Token should be used then instead of a password to '
1218 'Such Token should be used then instead of a password to '
1219 'interact with a repository, and additionally can be '
1219 'interact with a repository, and additionally can be '
1220 'limited to single repository using repo scope.'),
1220 'limited to single repository using repo scope.'),
1221 cls.ROLE_API: _('Token limited to api calls.'),
1221 cls.ROLE_API: _('Token limited to api calls.'),
1222 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1222 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1223 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1223 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1224 }.get(role, role)
1224 }.get(role, role)
1225
1225
1226 @property
1226 @property
1227 def role_humanized(self):
1227 def role_humanized(self):
1228 return self._get_role_name(self.role)
1228 return self._get_role_name(self.role)
1229
1229
1230 def _get_scope(self):
1230 def _get_scope(self):
1231 if self.repo:
1231 if self.repo:
1232 return 'Repository: {}'.format(self.repo.repo_name)
1232 return 'Repository: {}'.format(self.repo.repo_name)
1233 if self.repo_group:
1233 if self.repo_group:
1234 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1234 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1235 return 'Global'
1235 return 'Global'
1236
1236
1237 @property
1237 @property
1238 def scope_humanized(self):
1238 def scope_humanized(self):
1239 return self._get_scope()
1239 return self._get_scope()
1240
1240
1241 @property
1241 @property
1242 def token_obfuscated(self):
1242 def token_obfuscated(self):
1243 if self.api_key:
1243 if self.api_key:
1244 return self.api_key[:4] + "****"
1244 return self.api_key[:4] + "****"
1245
1245
1246
1246
1247 class UserEmailMap(Base, BaseModel):
1247 class UserEmailMap(Base, BaseModel):
1248 __tablename__ = 'user_email_map'
1248 __tablename__ = 'user_email_map'
1249 __table_args__ = (
1249 __table_args__ = (
1250 Index('uem_email_idx', 'email'),
1250 Index('uem_email_idx', 'email'),
1251 UniqueConstraint('email'),
1251 UniqueConstraint('email'),
1252 base_table_args
1252 base_table_args
1253 )
1253 )
1254 __mapper_args__ = {}
1254 __mapper_args__ = {}
1255
1255
1256 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1256 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1257 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1257 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1258 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1258 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1259 user = relationship('User', lazy='joined')
1259 user = relationship('User', lazy='joined')
1260
1260
1261 @validates('_email')
1261 @validates('_email')
1262 def validate_email(self, key, email):
1262 def validate_email(self, key, email):
1263 # check if this email is not main one
1263 # check if this email is not main one
1264 main_email = Session().query(User).filter(User.email == email).scalar()
1264 main_email = Session().query(User).filter(User.email == email).scalar()
1265 if main_email is not None:
1265 if main_email is not None:
1266 raise AttributeError('email %s is present is user table' % email)
1266 raise AttributeError('email %s is present is user table' % email)
1267 return email
1267 return email
1268
1268
1269 @hybrid_property
1269 @hybrid_property
1270 def email(self):
1270 def email(self):
1271 return self._email
1271 return self._email
1272
1272
1273 @email.setter
1273 @email.setter
1274 def email(self, val):
1274 def email(self, val):
1275 self._email = val.lower() if val else None
1275 self._email = val.lower() if val else None
1276
1276
1277
1277
1278 class UserIpMap(Base, BaseModel):
1278 class UserIpMap(Base, BaseModel):
1279 __tablename__ = 'user_ip_map'
1279 __tablename__ = 'user_ip_map'
1280 __table_args__ = (
1280 __table_args__ = (
1281 UniqueConstraint('user_id', 'ip_addr'),
1281 UniqueConstraint('user_id', 'ip_addr'),
1282 base_table_args
1282 base_table_args
1283 )
1283 )
1284 __mapper_args__ = {}
1284 __mapper_args__ = {}
1285
1285
1286 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1286 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1287 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1287 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1288 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1288 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1289 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1289 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1290 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1290 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1291 user = relationship('User', lazy='joined')
1291 user = relationship('User', lazy='joined')
1292
1292
1293 @hybrid_property
1293 @hybrid_property
1294 def description_safe(self):
1294 def description_safe(self):
1295 from rhodecode.lib import helpers as h
1295 from rhodecode.lib import helpers as h
1296 return h.escape(self.description)
1296 return h.escape(self.description)
1297
1297
1298 @classmethod
1298 @classmethod
1299 def _get_ip_range(cls, ip_addr):
1299 def _get_ip_range(cls, ip_addr):
1300 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1300 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1301 return [str(net.network_address), str(net.broadcast_address)]
1301 return [str(net.network_address), str(net.broadcast_address)]
1302
1302
1303 def __json__(self):
1303 def __json__(self):
1304 return {
1304 return {
1305 'ip_addr': self.ip_addr,
1305 'ip_addr': self.ip_addr,
1306 'ip_range': self._get_ip_range(self.ip_addr),
1306 'ip_range': self._get_ip_range(self.ip_addr),
1307 }
1307 }
1308
1308
1309 def __unicode__(self):
1309 def __unicode__(self):
1310 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1310 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1311 self.user_id, self.ip_addr)
1311 self.user_id, self.ip_addr)
1312
1312
1313
1313
1314 class UserSshKeys(Base, BaseModel):
1314 class UserSshKeys(Base, BaseModel):
1315 __tablename__ = 'user_ssh_keys'
1315 __tablename__ = 'user_ssh_keys'
1316 __table_args__ = (
1316 __table_args__ = (
1317 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1317 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1318
1318
1319 UniqueConstraint('ssh_key_fingerprint'),
1319 UniqueConstraint('ssh_key_fingerprint'),
1320
1320
1321 base_table_args
1321 base_table_args
1322 )
1322 )
1323 __mapper_args__ = {}
1323 __mapper_args__ = {}
1324
1324
1325 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1325 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1326 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1326 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1327 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1327 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1328
1328
1329 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1329 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1330
1330
1331 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1331 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1332 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1332 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1333 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1333 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1334
1334
1335 user = relationship('User', lazy='joined')
1335 user = relationship('User', lazy='joined')
1336
1336
1337 def __json__(self):
1337 def __json__(self):
1338 data = {
1338 data = {
1339 'ssh_fingerprint': self.ssh_key_fingerprint,
1339 'ssh_fingerprint': self.ssh_key_fingerprint,
1340 'description': self.description,
1340 'description': self.description,
1341 'created_on': self.created_on
1341 'created_on': self.created_on
1342 }
1342 }
1343 return data
1343 return data
1344
1344
1345 def get_api_data(self):
1345 def get_api_data(self):
1346 data = self.__json__()
1346 data = self.__json__()
1347 return data
1347 return data
1348
1348
1349
1349
1350 class UserLog(Base, BaseModel):
1350 class UserLog(Base, BaseModel):
1351 __tablename__ = 'user_logs'
1351 __tablename__ = 'user_logs'
1352 __table_args__ = (
1352 __table_args__ = (
1353 base_table_args,
1353 base_table_args,
1354 )
1354 )
1355
1355
1356 VERSION_1 = 'v1'
1356 VERSION_1 = 'v1'
1357 VERSION_2 = 'v2'
1357 VERSION_2 = 'v2'
1358 VERSIONS = [VERSION_1, VERSION_2]
1358 VERSIONS = [VERSION_1, VERSION_2]
1359
1359
1360 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1360 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1361 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1361 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1362 username = Column("username", String(255), nullable=True, unique=None, default=None)
1362 username = Column("username", String(255), nullable=True, unique=None, default=None)
1363 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1363 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1364 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1364 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1365 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1365 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1366 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1366 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1367 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1367 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1368
1368
1369 version = Column("version", String(255), nullable=True, default=VERSION_1)
1369 version = Column("version", String(255), nullable=True, default=VERSION_1)
1370 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1370 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1371 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1371 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1372
1372
1373 def __unicode__(self):
1373 def __unicode__(self):
1374 return u"<%s('id:%s:%s')>" % (
1374 return u"<%s('id:%s:%s')>" % (
1375 self.__class__.__name__, self.repository_name, self.action)
1375 self.__class__.__name__, self.repository_name, self.action)
1376
1376
1377 def __json__(self):
1377 def __json__(self):
1378 return {
1378 return {
1379 'user_id': self.user_id,
1379 'user_id': self.user_id,
1380 'username': self.username,
1380 'username': self.username,
1381 'repository_id': self.repository_id,
1381 'repository_id': self.repository_id,
1382 'repository_name': self.repository_name,
1382 'repository_name': self.repository_name,
1383 'user_ip': self.user_ip,
1383 'user_ip': self.user_ip,
1384 'action_date': self.action_date,
1384 'action_date': self.action_date,
1385 'action': self.action,
1385 'action': self.action,
1386 }
1386 }
1387
1387
1388 @hybrid_property
1388 @hybrid_property
1389 def entry_id(self):
1389 def entry_id(self):
1390 return self.user_log_id
1390 return self.user_log_id
1391
1391
1392 @property
1392 @property
1393 def action_as_day(self):
1393 def action_as_day(self):
1394 return datetime.date(*self.action_date.timetuple()[:3])
1394 return datetime.date(*self.action_date.timetuple()[:3])
1395
1395
1396 user = relationship('User')
1396 user = relationship('User')
1397 repository = relationship('Repository', cascade='')
1397 repository = relationship('Repository', cascade='')
1398
1398
1399
1399
1400 class UserGroup(Base, BaseModel):
1400 class UserGroup(Base, BaseModel):
1401 __tablename__ = 'users_groups'
1401 __tablename__ = 'users_groups'
1402 __table_args__ = (
1402 __table_args__ = (
1403 base_table_args,
1403 base_table_args,
1404 )
1404 )
1405
1405
1406 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1406 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1407 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1407 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1408 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1408 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1409 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1409 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1410 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1410 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1411 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1411 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1412 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1412 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1413 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1413 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1414
1414
1415 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1415 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1416 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1416 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1417 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1417 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1418 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1418 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1419 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1419 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1420 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1420 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1421
1421
1422 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1422 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1423 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1423 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1424
1424
1425 @classmethod
1425 @classmethod
1426 def _load_group_data(cls, column):
1426 def _load_group_data(cls, column):
1427 if not column:
1427 if not column:
1428 return {}
1428 return {}
1429
1429
1430 try:
1430 try:
1431 return json.loads(column) or {}
1431 return json.loads(column) or {}
1432 except TypeError:
1432 except TypeError:
1433 return {}
1433 return {}
1434
1434
1435 @hybrid_property
1435 @hybrid_property
1436 def description_safe(self):
1436 def description_safe(self):
1437 from rhodecode.lib import helpers as h
1437 from rhodecode.lib import helpers as h
1438 return h.escape(self.user_group_description)
1438 return h.escape(self.user_group_description)
1439
1439
1440 @hybrid_property
1440 @hybrid_property
1441 def group_data(self):
1441 def group_data(self):
1442 return self._load_group_data(self._group_data)
1442 return self._load_group_data(self._group_data)
1443
1443
1444 @group_data.expression
1444 @group_data.expression
1445 def group_data(self, **kwargs):
1445 def group_data(self, **kwargs):
1446 return self._group_data
1446 return self._group_data
1447
1447
1448 @group_data.setter
1448 @group_data.setter
1449 def group_data(self, val):
1449 def group_data(self, val):
1450 try:
1450 try:
1451 self._group_data = json.dumps(val)
1451 self._group_data = json.dumps(val)
1452 except Exception:
1452 except Exception:
1453 log.error(traceback.format_exc())
1453 log.error(traceback.format_exc())
1454
1454
1455 @classmethod
1455 @classmethod
1456 def _load_sync(cls, group_data):
1456 def _load_sync(cls, group_data):
1457 if group_data:
1457 if group_data:
1458 return group_data.get('extern_type')
1458 return group_data.get('extern_type')
1459
1459
1460 @property
1460 @property
1461 def sync(self):
1461 def sync(self):
1462 return self._load_sync(self.group_data)
1462 return self._load_sync(self.group_data)
1463
1463
1464 def __unicode__(self):
1464 def __unicode__(self):
1465 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1465 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1466 self.users_group_id,
1466 self.users_group_id,
1467 self.users_group_name)
1467 self.users_group_name)
1468
1468
1469 @classmethod
1469 @classmethod
1470 def get_by_group_name(cls, group_name, cache=False,
1470 def get_by_group_name(cls, group_name, cache=False,
1471 case_insensitive=False):
1471 case_insensitive=False):
1472 if case_insensitive:
1472 if case_insensitive:
1473 q = cls.query().filter(func.lower(cls.users_group_name) ==
1473 q = cls.query().filter(func.lower(cls.users_group_name) ==
1474 func.lower(group_name))
1474 func.lower(group_name))
1475
1475
1476 else:
1476 else:
1477 q = cls.query().filter(cls.users_group_name == group_name)
1477 q = cls.query().filter(cls.users_group_name == group_name)
1478 if cache:
1478 if cache:
1479 q = q.options(
1479 q = q.options(
1480 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1480 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1481 return q.scalar()
1481 return q.scalar()
1482
1482
1483 @classmethod
1483 @classmethod
1484 def get(cls, user_group_id, cache=False):
1484 def get(cls, user_group_id, cache=False):
1485 if not user_group_id:
1485 if not user_group_id:
1486 return
1486 return
1487
1487
1488 user_group = cls.query()
1488 user_group = cls.query()
1489 if cache:
1489 if cache:
1490 user_group = user_group.options(
1490 user_group = user_group.options(
1491 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1491 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1492 return user_group.get(user_group_id)
1492 return user_group.get(user_group_id)
1493
1493
1494 def permissions(self, with_admins=True, with_owner=True,
1494 def permissions(self, with_admins=True, with_owner=True,
1495 expand_from_user_groups=False):
1495 expand_from_user_groups=False):
1496 """
1496 """
1497 Permissions for user groups
1497 Permissions for user groups
1498 """
1498 """
1499 _admin_perm = 'usergroup.admin'
1499 _admin_perm = 'usergroup.admin'
1500
1500
1501 owner_row = []
1501 owner_row = []
1502 if with_owner:
1502 if with_owner:
1503 usr = AttributeDict(self.user.get_dict())
1503 usr = AttributeDict(self.user.get_dict())
1504 usr.owner_row = True
1504 usr.owner_row = True
1505 usr.permission = _admin_perm
1505 usr.permission = _admin_perm
1506 owner_row.append(usr)
1506 owner_row.append(usr)
1507
1507
1508 super_admin_ids = []
1508 super_admin_ids = []
1509 super_admin_rows = []
1509 super_admin_rows = []
1510 if with_admins:
1510 if with_admins:
1511 for usr in User.get_all_super_admins():
1511 for usr in User.get_all_super_admins():
1512 super_admin_ids.append(usr.user_id)
1512 super_admin_ids.append(usr.user_id)
1513 # if this admin is also owner, don't double the record
1513 # if this admin is also owner, don't double the record
1514 if usr.user_id == owner_row[0].user_id:
1514 if usr.user_id == owner_row[0].user_id:
1515 owner_row[0].admin_row = True
1515 owner_row[0].admin_row = True
1516 else:
1516 else:
1517 usr = AttributeDict(usr.get_dict())
1517 usr = AttributeDict(usr.get_dict())
1518 usr.admin_row = True
1518 usr.admin_row = True
1519 usr.permission = _admin_perm
1519 usr.permission = _admin_perm
1520 super_admin_rows.append(usr)
1520 super_admin_rows.append(usr)
1521
1521
1522 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1522 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1523 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1523 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1524 joinedload(UserUserGroupToPerm.user),
1524 joinedload(UserUserGroupToPerm.user),
1525 joinedload(UserUserGroupToPerm.permission),)
1525 joinedload(UserUserGroupToPerm.permission),)
1526
1526
1527 # get owners and admins and permissions. We do a trick of re-writing
1527 # get owners and admins and permissions. We do a trick of re-writing
1528 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1528 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1529 # has a global reference and changing one object propagates to all
1529 # has a global reference and changing one object propagates to all
1530 # others. This means if admin is also an owner admin_row that change
1530 # others. This means if admin is also an owner admin_row that change
1531 # would propagate to both objects
1531 # would propagate to both objects
1532 perm_rows = []
1532 perm_rows = []
1533 for _usr in q.all():
1533 for _usr in q.all():
1534 usr = AttributeDict(_usr.user.get_dict())
1534 usr = AttributeDict(_usr.user.get_dict())
1535 # if this user is also owner/admin, mark as duplicate record
1535 # if this user is also owner/admin, mark as duplicate record
1536 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1536 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1537 usr.duplicate_perm = True
1537 usr.duplicate_perm = True
1538 usr.permission = _usr.permission.permission_name
1538 usr.permission = _usr.permission.permission_name
1539 perm_rows.append(usr)
1539 perm_rows.append(usr)
1540
1540
1541 # filter the perm rows by 'default' first and then sort them by
1541 # filter the perm rows by 'default' first and then sort them by
1542 # admin,write,read,none permissions sorted again alphabetically in
1542 # admin,write,read,none permissions sorted again alphabetically in
1543 # each group
1543 # each group
1544 perm_rows = sorted(perm_rows, key=display_user_sort)
1544 perm_rows = sorted(perm_rows, key=display_user_sort)
1545
1545
1546 user_groups_rows = []
1546 user_groups_rows = []
1547 if expand_from_user_groups:
1547 if expand_from_user_groups:
1548 for ug in self.permission_user_groups(with_members=True):
1548 for ug in self.permission_user_groups(with_members=True):
1549 for user_data in ug.members:
1549 for user_data in ug.members:
1550 user_groups_rows.append(user_data)
1550 user_groups_rows.append(user_data)
1551
1551
1552 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1552 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1553
1553
1554 def permission_user_groups(self, with_members=False):
1554 def permission_user_groups(self, with_members=False):
1555 q = UserGroupUserGroupToPerm.query()\
1555 q = UserGroupUserGroupToPerm.query()\
1556 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1556 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1557 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1557 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1558 joinedload(UserGroupUserGroupToPerm.target_user_group),
1558 joinedload(UserGroupUserGroupToPerm.target_user_group),
1559 joinedload(UserGroupUserGroupToPerm.permission),)
1559 joinedload(UserGroupUserGroupToPerm.permission),)
1560
1560
1561 perm_rows = []
1561 perm_rows = []
1562 for _user_group in q.all():
1562 for _user_group in q.all():
1563 entry = AttributeDict(_user_group.user_group.get_dict())
1563 entry = AttributeDict(_user_group.user_group.get_dict())
1564 entry.permission = _user_group.permission.permission_name
1564 entry.permission = _user_group.permission.permission_name
1565 if with_members:
1565 if with_members:
1566 entry.members = [x.user.get_dict()
1566 entry.members = [x.user.get_dict()
1567 for x in _user_group.user_group.members]
1567 for x in _user_group.user_group.members]
1568 perm_rows.append(entry)
1568 perm_rows.append(entry)
1569
1569
1570 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1570 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1571 return perm_rows
1571 return perm_rows
1572
1572
1573 def _get_default_perms(self, user_group, suffix=''):
1573 def _get_default_perms(self, user_group, suffix=''):
1574 from rhodecode.model.permission import PermissionModel
1574 from rhodecode.model.permission import PermissionModel
1575 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1575 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1576
1576
1577 def get_default_perms(self, suffix=''):
1577 def get_default_perms(self, suffix=''):
1578 return self._get_default_perms(self, suffix)
1578 return self._get_default_perms(self, suffix)
1579
1579
1580 def get_api_data(self, with_group_members=True, include_secrets=False):
1580 def get_api_data(self, with_group_members=True, include_secrets=False):
1581 """
1581 """
1582 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1582 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1583 basically forwarded.
1583 basically forwarded.
1584
1584
1585 """
1585 """
1586 user_group = self
1586 user_group = self
1587 data = {
1587 data = {
1588 'users_group_id': user_group.users_group_id,
1588 'users_group_id': user_group.users_group_id,
1589 'group_name': user_group.users_group_name,
1589 'group_name': user_group.users_group_name,
1590 'group_description': user_group.user_group_description,
1590 'group_description': user_group.user_group_description,
1591 'active': user_group.users_group_active,
1591 'active': user_group.users_group_active,
1592 'owner': user_group.user.username,
1592 'owner': user_group.user.username,
1593 'sync': user_group.sync,
1593 'sync': user_group.sync,
1594 'owner_email': user_group.user.email,
1594 'owner_email': user_group.user.email,
1595 }
1595 }
1596
1596
1597 if with_group_members:
1597 if with_group_members:
1598 users = []
1598 users = []
1599 for user in user_group.members:
1599 for user in user_group.members:
1600 user = user.user
1600 user = user.user
1601 users.append(user.get_api_data(include_secrets=include_secrets))
1601 users.append(user.get_api_data(include_secrets=include_secrets))
1602 data['users'] = users
1602 data['users'] = users
1603
1603
1604 return data
1604 return data
1605
1605
1606
1606
1607 class UserGroupMember(Base, BaseModel):
1607 class UserGroupMember(Base, BaseModel):
1608 __tablename__ = 'users_groups_members'
1608 __tablename__ = 'users_groups_members'
1609 __table_args__ = (
1609 __table_args__ = (
1610 base_table_args,
1610 base_table_args,
1611 )
1611 )
1612
1612
1613 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1613 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1614 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1614 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1615 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1615 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1616
1616
1617 user = relationship('User', lazy='joined')
1617 user = relationship('User', lazy='joined')
1618 users_group = relationship('UserGroup')
1618 users_group = relationship('UserGroup')
1619
1619
1620 def __init__(self, gr_id='', u_id=''):
1620 def __init__(self, gr_id='', u_id=''):
1621 self.users_group_id = gr_id
1621 self.users_group_id = gr_id
1622 self.user_id = u_id
1622 self.user_id = u_id
1623
1623
1624
1624
1625 class RepositoryField(Base, BaseModel):
1625 class RepositoryField(Base, BaseModel):
1626 __tablename__ = 'repositories_fields'
1626 __tablename__ = 'repositories_fields'
1627 __table_args__ = (
1627 __table_args__ = (
1628 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1628 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1629 base_table_args,
1629 base_table_args,
1630 )
1630 )
1631
1631
1632 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1632 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1633
1633
1634 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1634 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1635 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1635 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1636 field_key = Column("field_key", String(250))
1636 field_key = Column("field_key", String(250))
1637 field_label = Column("field_label", String(1024), nullable=False)
1637 field_label = Column("field_label", String(1024), nullable=False)
1638 field_value = Column("field_value", String(10000), nullable=False)
1638 field_value = Column("field_value", String(10000), nullable=False)
1639 field_desc = Column("field_desc", String(1024), nullable=False)
1639 field_desc = Column("field_desc", String(1024), nullable=False)
1640 field_type = Column("field_type", String(255), nullable=False, unique=None)
1640 field_type = Column("field_type", String(255), nullable=False, unique=None)
1641 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1641 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1642
1642
1643 repository = relationship('Repository')
1643 repository = relationship('Repository')
1644
1644
1645 @property
1645 @property
1646 def field_key_prefixed(self):
1646 def field_key_prefixed(self):
1647 return 'ex_%s' % self.field_key
1647 return 'ex_%s' % self.field_key
1648
1648
1649 @classmethod
1649 @classmethod
1650 def un_prefix_key(cls, key):
1650 def un_prefix_key(cls, key):
1651 if key.startswith(cls.PREFIX):
1651 if key.startswith(cls.PREFIX):
1652 return key[len(cls.PREFIX):]
1652 return key[len(cls.PREFIX):]
1653 return key
1653 return key
1654
1654
1655 @classmethod
1655 @classmethod
1656 def get_by_key_name(cls, key, repo):
1656 def get_by_key_name(cls, key, repo):
1657 row = cls.query()\
1657 row = cls.query()\
1658 .filter(cls.repository == repo)\
1658 .filter(cls.repository == repo)\
1659 .filter(cls.field_key == key).scalar()
1659 .filter(cls.field_key == key).scalar()
1660 return row
1660 return row
1661
1661
1662
1662
1663 class Repository(Base, BaseModel):
1663 class Repository(Base, BaseModel):
1664 __tablename__ = 'repositories'
1664 __tablename__ = 'repositories'
1665 __table_args__ = (
1665 __table_args__ = (
1666 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1666 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1667 base_table_args,
1667 base_table_args,
1668 )
1668 )
1669 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1669 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1670 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1670 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1671 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1671 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1672
1672
1673 STATE_CREATED = 'repo_state_created'
1673 STATE_CREATED = 'repo_state_created'
1674 STATE_PENDING = 'repo_state_pending'
1674 STATE_PENDING = 'repo_state_pending'
1675 STATE_ERROR = 'repo_state_error'
1675 STATE_ERROR = 'repo_state_error'
1676
1676
1677 LOCK_AUTOMATIC = 'lock_auto'
1677 LOCK_AUTOMATIC = 'lock_auto'
1678 LOCK_API = 'lock_api'
1678 LOCK_API = 'lock_api'
1679 LOCK_WEB = 'lock_web'
1679 LOCK_WEB = 'lock_web'
1680 LOCK_PULL = 'lock_pull'
1680 LOCK_PULL = 'lock_pull'
1681
1681
1682 NAME_SEP = URL_SEP
1682 NAME_SEP = URL_SEP
1683
1683
1684 repo_id = Column(
1684 repo_id = Column(
1685 "repo_id", Integer(), nullable=False, unique=True, default=None,
1685 "repo_id", Integer(), nullable=False, unique=True, default=None,
1686 primary_key=True)
1686 primary_key=True)
1687 _repo_name = Column(
1687 _repo_name = Column(
1688 "repo_name", Text(), nullable=False, default=None)
1688 "repo_name", Text(), nullable=False, default=None)
1689 repo_name_hash = Column(
1689 repo_name_hash = Column(
1690 "repo_name_hash", String(255), nullable=False, unique=True)
1690 "repo_name_hash", String(255), nullable=False, unique=True)
1691 repo_state = Column("repo_state", String(255), nullable=True)
1691 repo_state = Column("repo_state", String(255), nullable=True)
1692
1692
1693 clone_uri = Column(
1693 clone_uri = Column(
1694 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1694 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1695 default=None)
1695 default=None)
1696 push_uri = Column(
1696 push_uri = Column(
1697 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1697 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1698 default=None)
1698 default=None)
1699 repo_type = Column(
1699 repo_type = Column(
1700 "repo_type", String(255), nullable=False, unique=False, default=None)
1700 "repo_type", String(255), nullable=False, unique=False, default=None)
1701 user_id = Column(
1701 user_id = Column(
1702 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1702 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1703 unique=False, default=None)
1703 unique=False, default=None)
1704 private = Column(
1704 private = Column(
1705 "private", Boolean(), nullable=True, unique=None, default=None)
1705 "private", Boolean(), nullable=True, unique=None, default=None)
1706 archived = Column(
1706 archived = Column(
1707 "archived", Boolean(), nullable=True, unique=None, default=None)
1707 "archived", Boolean(), nullable=True, unique=None, default=None)
1708 enable_statistics = Column(
1708 enable_statistics = Column(
1709 "statistics", Boolean(), nullable=True, unique=None, default=True)
1709 "statistics", Boolean(), nullable=True, unique=None, default=True)
1710 enable_downloads = Column(
1710 enable_downloads = Column(
1711 "downloads", Boolean(), nullable=True, unique=None, default=True)
1711 "downloads", Boolean(), nullable=True, unique=None, default=True)
1712 description = Column(
1712 description = Column(
1713 "description", String(10000), nullable=True, unique=None, default=None)
1713 "description", String(10000), nullable=True, unique=None, default=None)
1714 created_on = Column(
1714 created_on = Column(
1715 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1715 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1716 default=datetime.datetime.now)
1716 default=datetime.datetime.now)
1717 updated_on = Column(
1717 updated_on = Column(
1718 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1718 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1719 default=datetime.datetime.now)
1719 default=datetime.datetime.now)
1720 _landing_revision = Column(
1720 _landing_revision = Column(
1721 "landing_revision", String(255), nullable=False, unique=False,
1721 "landing_revision", String(255), nullable=False, unique=False,
1722 default=None)
1722 default=None)
1723 enable_locking = Column(
1723 enable_locking = Column(
1724 "enable_locking", Boolean(), nullable=False, unique=None,
1724 "enable_locking", Boolean(), nullable=False, unique=None,
1725 default=False)
1725 default=False)
1726 _locked = Column(
1726 _locked = Column(
1727 "locked", String(255), nullable=True, unique=False, default=None)
1727 "locked", String(255), nullable=True, unique=False, default=None)
1728 _changeset_cache = Column(
1728 _changeset_cache = Column(
1729 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1729 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1730
1730
1731 fork_id = Column(
1731 fork_id = Column(
1732 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1732 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1733 nullable=True, unique=False, default=None)
1733 nullable=True, unique=False, default=None)
1734 group_id = Column(
1734 group_id = Column(
1735 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1735 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1736 unique=False, default=None)
1736 unique=False, default=None)
1737
1737
1738 user = relationship('User', lazy='joined')
1738 user = relationship('User', lazy='joined')
1739 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1739 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1740 group = relationship('RepoGroup', lazy='joined')
1740 group = relationship('RepoGroup', lazy='joined')
1741 repo_to_perm = relationship(
1741 repo_to_perm = relationship(
1742 'UserRepoToPerm', cascade='all',
1742 'UserRepoToPerm', cascade='all',
1743 order_by='UserRepoToPerm.repo_to_perm_id')
1743 order_by='UserRepoToPerm.repo_to_perm_id')
1744 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1744 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1745 stats = relationship('Statistics', cascade='all', uselist=False)
1745 stats = relationship('Statistics', cascade='all', uselist=False)
1746
1746
1747 followers = relationship(
1747 followers = relationship(
1748 'UserFollowing',
1748 'UserFollowing',
1749 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1749 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1750 cascade='all')
1750 cascade='all')
1751 extra_fields = relationship(
1751 extra_fields = relationship(
1752 'RepositoryField', cascade="all, delete-orphan")
1752 'RepositoryField', cascade="all, delete-orphan")
1753 logs = relationship('UserLog')
1753 logs = relationship('UserLog')
1754 comments = relationship(
1754 comments = relationship(
1755 'ChangesetComment', cascade="all, delete-orphan")
1755 'ChangesetComment', cascade="all, delete-orphan")
1756 pull_requests_source = relationship(
1756 pull_requests_source = relationship(
1757 'PullRequest',
1757 'PullRequest',
1758 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1758 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1759 cascade="all, delete-orphan")
1759 cascade="all, delete-orphan")
1760 pull_requests_target = relationship(
1760 pull_requests_target = relationship(
1761 'PullRequest',
1761 'PullRequest',
1762 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1762 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1763 cascade="all, delete-orphan")
1763 cascade="all, delete-orphan")
1764 ui = relationship('RepoRhodeCodeUi', cascade="all")
1764 ui = relationship('RepoRhodeCodeUi', cascade="all")
1765 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1765 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1766 integrations = relationship('Integration', cascade="all, delete-orphan")
1766 integrations = relationship('Integration', cascade="all, delete-orphan")
1767
1767
1768 scoped_tokens = relationship('UserApiKeys', cascade="all")
1768 scoped_tokens = relationship('UserApiKeys', cascade="all")
1769
1769
1770 # no cascade, set NULL
1770 # no cascade, set NULL
1771 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1771 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1772
1772
1773 def __unicode__(self):
1773 def __unicode__(self):
1774 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1774 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1775 safe_unicode(self.repo_name))
1775 safe_unicode(self.repo_name))
1776
1776
1777 @hybrid_property
1777 @hybrid_property
1778 def description_safe(self):
1778 def description_safe(self):
1779 from rhodecode.lib import helpers as h
1779 from rhodecode.lib import helpers as h
1780 return h.escape(self.description)
1780 return h.escape(self.description)
1781
1781
1782 @hybrid_property
1782 @hybrid_property
1783 def landing_rev(self):
1783 def landing_rev(self):
1784 # always should return [rev_type, rev], e.g ['branch', 'master']
1784 # always should return [rev_type, rev], e.g ['branch', 'master']
1785 if self._landing_revision:
1785 if self._landing_revision:
1786 _rev_info = self._landing_revision.split(':')
1786 _rev_info = self._landing_revision.split(':')
1787 if len(_rev_info) < 2:
1787 if len(_rev_info) < 2:
1788 _rev_info.insert(0, 'rev')
1788 _rev_info.insert(0, 'rev')
1789 return [_rev_info[0], _rev_info[1]]
1789 return [_rev_info[0], _rev_info[1]]
1790 return [None, None]
1790 return [None, None]
1791
1791
1792 @property
1792 @property
1793 def landing_ref_type(self):
1793 def landing_ref_type(self):
1794 return self.landing_rev[0]
1794 return self.landing_rev[0]
1795
1795
1796 @property
1796 @property
1797 def landing_ref_name(self):
1797 def landing_ref_name(self):
1798 return self.landing_rev[1]
1798 return self.landing_rev[1]
1799
1799
1800 @landing_rev.setter
1800 @landing_rev.setter
1801 def landing_rev(self, val):
1801 def landing_rev(self, val):
1802 if ':' not in val:
1802 if ':' not in val:
1803 raise ValueError('value must be delimited with `:` and consist '
1803 raise ValueError('value must be delimited with `:` and consist '
1804 'of <rev_type>:<rev>, got %s instead' % val)
1804 'of <rev_type>:<rev>, got %s instead' % val)
1805 self._landing_revision = val
1805 self._landing_revision = val
1806
1806
1807 @hybrid_property
1807 @hybrid_property
1808 def locked(self):
1808 def locked(self):
1809 if self._locked:
1809 if self._locked:
1810 user_id, timelocked, reason = self._locked.split(':')
1810 user_id, timelocked, reason = self._locked.split(':')
1811 lock_values = int(user_id), timelocked, reason
1811 lock_values = int(user_id), timelocked, reason
1812 else:
1812 else:
1813 lock_values = [None, None, None]
1813 lock_values = [None, None, None]
1814 return lock_values
1814 return lock_values
1815
1815
1816 @locked.setter
1816 @locked.setter
1817 def locked(self, val):
1817 def locked(self, val):
1818 if val and isinstance(val, (list, tuple)):
1818 if val and isinstance(val, (list, tuple)):
1819 self._locked = ':'.join(map(str, val))
1819 self._locked = ':'.join(map(str, val))
1820 else:
1820 else:
1821 self._locked = None
1821 self._locked = None
1822
1822
1823 @classmethod
1823 @classmethod
1824 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1824 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1825 from rhodecode.lib.vcs.backends.base import EmptyCommit
1825 from rhodecode.lib.vcs.backends.base import EmptyCommit
1826 dummy = EmptyCommit().__json__()
1826 dummy = EmptyCommit().__json__()
1827 if not changeset_cache_raw:
1827 if not changeset_cache_raw:
1828 dummy['source_repo_id'] = repo_id
1828 dummy['source_repo_id'] = repo_id
1829 return json.loads(json.dumps(dummy))
1829 return json.loads(json.dumps(dummy))
1830
1830
1831 try:
1831 try:
1832 return json.loads(changeset_cache_raw)
1832 return json.loads(changeset_cache_raw)
1833 except TypeError:
1833 except TypeError:
1834 return dummy
1834 return dummy
1835 except Exception:
1835 except Exception:
1836 log.error(traceback.format_exc())
1836 log.error(traceback.format_exc())
1837 return dummy
1837 return dummy
1838
1838
1839 @hybrid_property
1839 @hybrid_property
1840 def changeset_cache(self):
1840 def changeset_cache(self):
1841 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1841 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1842
1842
1843 @changeset_cache.setter
1843 @changeset_cache.setter
1844 def changeset_cache(self, val):
1844 def changeset_cache(self, val):
1845 try:
1845 try:
1846 self._changeset_cache = json.dumps(val)
1846 self._changeset_cache = json.dumps(val)
1847 except Exception:
1847 except Exception:
1848 log.error(traceback.format_exc())
1848 log.error(traceback.format_exc())
1849
1849
1850 @hybrid_property
1850 @hybrid_property
1851 def repo_name(self):
1851 def repo_name(self):
1852 return self._repo_name
1852 return self._repo_name
1853
1853
1854 @repo_name.setter
1854 @repo_name.setter
1855 def repo_name(self, value):
1855 def repo_name(self, value):
1856 self._repo_name = value
1856 self._repo_name = value
1857 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1857 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1858
1858
1859 @classmethod
1859 @classmethod
1860 def normalize_repo_name(cls, repo_name):
1860 def normalize_repo_name(cls, repo_name):
1861 """
1861 """
1862 Normalizes os specific repo_name to the format internally stored inside
1862 Normalizes os specific repo_name to the format internally stored inside
1863 database using URL_SEP
1863 database using URL_SEP
1864
1864
1865 :param cls:
1865 :param cls:
1866 :param repo_name:
1866 :param repo_name:
1867 """
1867 """
1868 return cls.NAME_SEP.join(repo_name.split(os.sep))
1868 return cls.NAME_SEP.join(repo_name.split(os.sep))
1869
1869
1870 @classmethod
1870 @classmethod
1871 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1871 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1872 session = Session()
1872 session = Session()
1873 q = session.query(cls).filter(cls.repo_name == repo_name)
1873 q = session.query(cls).filter(cls.repo_name == repo_name)
1874
1874
1875 if cache:
1875 if cache:
1876 if identity_cache:
1876 if identity_cache:
1877 val = cls.identity_cache(session, 'repo_name', repo_name)
1877 val = cls.identity_cache(session, 'repo_name', repo_name)
1878 if val:
1878 if val:
1879 return val
1879 return val
1880 else:
1880 else:
1881 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1881 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1882 q = q.options(
1882 q = q.options(
1883 FromCache("sql_cache_short", cache_key))
1883 FromCache("sql_cache_short", cache_key))
1884
1884
1885 return q.scalar()
1885 return q.scalar()
1886
1886
1887 @classmethod
1887 @classmethod
1888 def get_by_id_or_repo_name(cls, repoid):
1888 def get_by_id_or_repo_name(cls, repoid):
1889 if isinstance(repoid, (int, long)):
1889 if isinstance(repoid, (int, long)):
1890 try:
1890 try:
1891 repo = cls.get(repoid)
1891 repo = cls.get(repoid)
1892 except ValueError:
1892 except ValueError:
1893 repo = None
1893 repo = None
1894 else:
1894 else:
1895 repo = cls.get_by_repo_name(repoid)
1895 repo = cls.get_by_repo_name(repoid)
1896 return repo
1896 return repo
1897
1897
1898 @classmethod
1898 @classmethod
1899 def get_by_full_path(cls, repo_full_path):
1899 def get_by_full_path(cls, repo_full_path):
1900 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1900 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1901 repo_name = cls.normalize_repo_name(repo_name)
1901 repo_name = cls.normalize_repo_name(repo_name)
1902 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1902 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1903
1903
1904 @classmethod
1904 @classmethod
1905 def get_repo_forks(cls, repo_id):
1905 def get_repo_forks(cls, repo_id):
1906 return cls.query().filter(Repository.fork_id == repo_id)
1906 return cls.query().filter(Repository.fork_id == repo_id)
1907
1907
1908 @classmethod
1908 @classmethod
1909 def base_path(cls):
1909 def base_path(cls):
1910 """
1910 """
1911 Returns base path when all repos are stored
1911 Returns base path when all repos are stored
1912
1912
1913 :param cls:
1913 :param cls:
1914 """
1914 """
1915 q = Session().query(RhodeCodeUi)\
1915 q = Session().query(RhodeCodeUi)\
1916 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1916 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1917 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1917 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1918 return q.one().ui_value
1918 return q.one().ui_value
1919
1919
1920 @classmethod
1920 @classmethod
1921 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1921 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1922 case_insensitive=True, archived=False):
1922 case_insensitive=True, archived=False):
1923 q = Repository.query()
1923 q = Repository.query()
1924
1924
1925 if not archived:
1925 if not archived:
1926 q = q.filter(Repository.archived.isnot(true()))
1926 q = q.filter(Repository.archived.isnot(true()))
1927
1927
1928 if not isinstance(user_id, Optional):
1928 if not isinstance(user_id, Optional):
1929 q = q.filter(Repository.user_id == user_id)
1929 q = q.filter(Repository.user_id == user_id)
1930
1930
1931 if not isinstance(group_id, Optional):
1931 if not isinstance(group_id, Optional):
1932 q = q.filter(Repository.group_id == group_id)
1932 q = q.filter(Repository.group_id == group_id)
1933
1933
1934 if case_insensitive:
1934 if case_insensitive:
1935 q = q.order_by(func.lower(Repository.repo_name))
1935 q = q.order_by(func.lower(Repository.repo_name))
1936 else:
1936 else:
1937 q = q.order_by(Repository.repo_name)
1937 q = q.order_by(Repository.repo_name)
1938
1938
1939 return q.all()
1939 return q.all()
1940
1940
1941 @property
1941 @property
1942 def repo_uid(self):
1942 def repo_uid(self):
1943 return '_{}'.format(self.repo_id)
1943 return '_{}'.format(self.repo_id)
1944
1944
1945 @property
1945 @property
1946 def forks(self):
1946 def forks(self):
1947 """
1947 """
1948 Return forks of this repo
1948 Return forks of this repo
1949 """
1949 """
1950 return Repository.get_repo_forks(self.repo_id)
1950 return Repository.get_repo_forks(self.repo_id)
1951
1951
1952 @property
1952 @property
1953 def parent(self):
1953 def parent(self):
1954 """
1954 """
1955 Returns fork parent
1955 Returns fork parent
1956 """
1956 """
1957 return self.fork
1957 return self.fork
1958
1958
1959 @property
1959 @property
1960 def just_name(self):
1960 def just_name(self):
1961 return self.repo_name.split(self.NAME_SEP)[-1]
1961 return self.repo_name.split(self.NAME_SEP)[-1]
1962
1962
1963 @property
1963 @property
1964 def groups_with_parents(self):
1964 def groups_with_parents(self):
1965 groups = []
1965 groups = []
1966 if self.group is None:
1966 if self.group is None:
1967 return groups
1967 return groups
1968
1968
1969 cur_gr = self.group
1969 cur_gr = self.group
1970 groups.insert(0, cur_gr)
1970 groups.insert(0, cur_gr)
1971 while 1:
1971 while 1:
1972 gr = getattr(cur_gr, 'parent_group', None)
1972 gr = getattr(cur_gr, 'parent_group', None)
1973 cur_gr = cur_gr.parent_group
1973 cur_gr = cur_gr.parent_group
1974 if gr is None:
1974 if gr is None:
1975 break
1975 break
1976 groups.insert(0, gr)
1976 groups.insert(0, gr)
1977
1977
1978 return groups
1978 return groups
1979
1979
1980 @property
1980 @property
1981 def groups_and_repo(self):
1981 def groups_and_repo(self):
1982 return self.groups_with_parents, self
1982 return self.groups_with_parents, self
1983
1983
1984 @LazyProperty
1984 @LazyProperty
1985 def repo_path(self):
1985 def repo_path(self):
1986 """
1986 """
1987 Returns base full path for that repository means where it actually
1987 Returns base full path for that repository means where it actually
1988 exists on a filesystem
1988 exists on a filesystem
1989 """
1989 """
1990 q = Session().query(RhodeCodeUi).filter(
1990 q = Session().query(RhodeCodeUi).filter(
1991 RhodeCodeUi.ui_key == self.NAME_SEP)
1991 RhodeCodeUi.ui_key == self.NAME_SEP)
1992 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1992 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1993 return q.one().ui_value
1993 return q.one().ui_value
1994
1994
1995 @property
1995 @property
1996 def repo_full_path(self):
1996 def repo_full_path(self):
1997 p = [self.repo_path]
1997 p = [self.repo_path]
1998 # we need to split the name by / since this is how we store the
1998 # we need to split the name by / since this is how we store the
1999 # names in the database, but that eventually needs to be converted
1999 # names in the database, but that eventually needs to be converted
2000 # into a valid system path
2000 # into a valid system path
2001 p += self.repo_name.split(self.NAME_SEP)
2001 p += self.repo_name.split(self.NAME_SEP)
2002 return os.path.join(*map(safe_unicode, p))
2002 return os.path.join(*map(safe_unicode, p))
2003
2003
2004 @property
2004 @property
2005 def cache_keys(self):
2005 def cache_keys(self):
2006 """
2006 """
2007 Returns associated cache keys for that repo
2007 Returns associated cache keys for that repo
2008 """
2008 """
2009 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2009 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2010 repo_id=self.repo_id)
2010 repo_id=self.repo_id)
2011 return CacheKey.query()\
2011 return CacheKey.query()\
2012 .filter(CacheKey.cache_args == invalidation_namespace)\
2012 .filter(CacheKey.cache_args == invalidation_namespace)\
2013 .order_by(CacheKey.cache_key)\
2013 .order_by(CacheKey.cache_key)\
2014 .all()
2014 .all()
2015
2015
2016 @property
2016 @property
2017 def cached_diffs_relative_dir(self):
2017 def cached_diffs_relative_dir(self):
2018 """
2018 """
2019 Return a relative to the repository store path of cached diffs
2019 Return a relative to the repository store path of cached diffs
2020 used for safe display for users, who shouldn't know the absolute store
2020 used for safe display for users, who shouldn't know the absolute store
2021 path
2021 path
2022 """
2022 """
2023 return os.path.join(
2023 return os.path.join(
2024 os.path.dirname(self.repo_name),
2024 os.path.dirname(self.repo_name),
2025 self.cached_diffs_dir.split(os.path.sep)[-1])
2025 self.cached_diffs_dir.split(os.path.sep)[-1])
2026
2026
2027 @property
2027 @property
2028 def cached_diffs_dir(self):
2028 def cached_diffs_dir(self):
2029 path = self.repo_full_path
2029 path = self.repo_full_path
2030 return os.path.join(
2030 return os.path.join(
2031 os.path.dirname(path),
2031 os.path.dirname(path),
2032 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
2032 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
2033
2033
2034 def cached_diffs(self):
2034 def cached_diffs(self):
2035 diff_cache_dir = self.cached_diffs_dir
2035 diff_cache_dir = self.cached_diffs_dir
2036 if os.path.isdir(diff_cache_dir):
2036 if os.path.isdir(diff_cache_dir):
2037 return os.listdir(diff_cache_dir)
2037 return os.listdir(diff_cache_dir)
2038 return []
2038 return []
2039
2039
2040 def shadow_repos(self):
2040 def shadow_repos(self):
2041 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2041 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2042 return [
2042 return [
2043 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2043 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2044 if x.startswith(shadow_repos_pattern)]
2044 if x.startswith(shadow_repos_pattern)]
2045
2045
2046 def get_new_name(self, repo_name):
2046 def get_new_name(self, repo_name):
2047 """
2047 """
2048 returns new full repository name based on assigned group and new new
2048 returns new full repository name based on assigned group and new new
2049
2049
2050 :param group_name:
2050 :param group_name:
2051 """
2051 """
2052 path_prefix = self.group.full_path_splitted if self.group else []
2052 path_prefix = self.group.full_path_splitted if self.group else []
2053 return self.NAME_SEP.join(path_prefix + [repo_name])
2053 return self.NAME_SEP.join(path_prefix + [repo_name])
2054
2054
2055 @property
2055 @property
2056 def _config(self):
2056 def _config(self):
2057 """
2057 """
2058 Returns db based config object.
2058 Returns db based config object.
2059 """
2059 """
2060 from rhodecode.lib.utils import make_db_config
2060 from rhodecode.lib.utils import make_db_config
2061 return make_db_config(clear_session=False, repo=self)
2061 return make_db_config(clear_session=False, repo=self)
2062
2062
2063 def permissions(self, with_admins=True, with_owner=True,
2063 def permissions(self, with_admins=True, with_owner=True,
2064 expand_from_user_groups=False):
2064 expand_from_user_groups=False):
2065 """
2065 """
2066 Permissions for repositories
2066 Permissions for repositories
2067 """
2067 """
2068 _admin_perm = 'repository.admin'
2068 _admin_perm = 'repository.admin'
2069
2069
2070 owner_row = []
2070 owner_row = []
2071 if with_owner:
2071 if with_owner:
2072 usr = AttributeDict(self.user.get_dict())
2072 usr = AttributeDict(self.user.get_dict())
2073 usr.owner_row = True
2073 usr.owner_row = True
2074 usr.permission = _admin_perm
2074 usr.permission = _admin_perm
2075 usr.permission_id = None
2075 usr.permission_id = None
2076 owner_row.append(usr)
2076 owner_row.append(usr)
2077
2077
2078 super_admin_ids = []
2078 super_admin_ids = []
2079 super_admin_rows = []
2079 super_admin_rows = []
2080 if with_admins:
2080 if with_admins:
2081 for usr in User.get_all_super_admins():
2081 for usr in User.get_all_super_admins():
2082 super_admin_ids.append(usr.user_id)
2082 super_admin_ids.append(usr.user_id)
2083 # if this admin is also owner, don't double the record
2083 # if this admin is also owner, don't double the record
2084 if usr.user_id == owner_row[0].user_id:
2084 if usr.user_id == owner_row[0].user_id:
2085 owner_row[0].admin_row = True
2085 owner_row[0].admin_row = True
2086 else:
2086 else:
2087 usr = AttributeDict(usr.get_dict())
2087 usr = AttributeDict(usr.get_dict())
2088 usr.admin_row = True
2088 usr.admin_row = True
2089 usr.permission = _admin_perm
2089 usr.permission = _admin_perm
2090 usr.permission_id = None
2090 usr.permission_id = None
2091 super_admin_rows.append(usr)
2091 super_admin_rows.append(usr)
2092
2092
2093 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2093 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2094 q = q.options(joinedload(UserRepoToPerm.repository),
2094 q = q.options(joinedload(UserRepoToPerm.repository),
2095 joinedload(UserRepoToPerm.user),
2095 joinedload(UserRepoToPerm.user),
2096 joinedload(UserRepoToPerm.permission),)
2096 joinedload(UserRepoToPerm.permission),)
2097
2097
2098 # get owners and admins and permissions. We do a trick of re-writing
2098 # get owners and admins and permissions. We do a trick of re-writing
2099 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2099 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2100 # has a global reference and changing one object propagates to all
2100 # has a global reference and changing one object propagates to all
2101 # others. This means if admin is also an owner admin_row that change
2101 # others. This means if admin is also an owner admin_row that change
2102 # would propagate to both objects
2102 # would propagate to both objects
2103 perm_rows = []
2103 perm_rows = []
2104 for _usr in q.all():
2104 for _usr in q.all():
2105 usr = AttributeDict(_usr.user.get_dict())
2105 usr = AttributeDict(_usr.user.get_dict())
2106 # if this user is also owner/admin, mark as duplicate record
2106 # if this user is also owner/admin, mark as duplicate record
2107 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2107 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2108 usr.duplicate_perm = True
2108 usr.duplicate_perm = True
2109 # also check if this permission is maybe used by branch_permissions
2109 # also check if this permission is maybe used by branch_permissions
2110 if _usr.branch_perm_entry:
2110 if _usr.branch_perm_entry:
2111 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2111 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2112
2112
2113 usr.permission = _usr.permission.permission_name
2113 usr.permission = _usr.permission.permission_name
2114 usr.permission_id = _usr.repo_to_perm_id
2114 usr.permission_id = _usr.repo_to_perm_id
2115 perm_rows.append(usr)
2115 perm_rows.append(usr)
2116
2116
2117 # filter the perm rows by 'default' first and then sort them by
2117 # filter the perm rows by 'default' first and then sort them by
2118 # admin,write,read,none permissions sorted again alphabetically in
2118 # admin,write,read,none permissions sorted again alphabetically in
2119 # each group
2119 # each group
2120 perm_rows = sorted(perm_rows, key=display_user_sort)
2120 perm_rows = sorted(perm_rows, key=display_user_sort)
2121
2121
2122 user_groups_rows = []
2122 user_groups_rows = []
2123 if expand_from_user_groups:
2123 if expand_from_user_groups:
2124 for ug in self.permission_user_groups(with_members=True):
2124 for ug in self.permission_user_groups(with_members=True):
2125 for user_data in ug.members:
2125 for user_data in ug.members:
2126 user_groups_rows.append(user_data)
2126 user_groups_rows.append(user_data)
2127
2127
2128 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2128 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2129
2129
2130 def permission_user_groups(self, with_members=True):
2130 def permission_user_groups(self, with_members=True):
2131 q = UserGroupRepoToPerm.query()\
2131 q = UserGroupRepoToPerm.query()\
2132 .filter(UserGroupRepoToPerm.repository == self)
2132 .filter(UserGroupRepoToPerm.repository == self)
2133 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2133 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2134 joinedload(UserGroupRepoToPerm.users_group),
2134 joinedload(UserGroupRepoToPerm.users_group),
2135 joinedload(UserGroupRepoToPerm.permission),)
2135 joinedload(UserGroupRepoToPerm.permission),)
2136
2136
2137 perm_rows = []
2137 perm_rows = []
2138 for _user_group in q.all():
2138 for _user_group in q.all():
2139 entry = AttributeDict(_user_group.users_group.get_dict())
2139 entry = AttributeDict(_user_group.users_group.get_dict())
2140 entry.permission = _user_group.permission.permission_name
2140 entry.permission = _user_group.permission.permission_name
2141 if with_members:
2141 if with_members:
2142 entry.members = [x.user.get_dict()
2142 entry.members = [x.user.get_dict()
2143 for x in _user_group.users_group.members]
2143 for x in _user_group.users_group.members]
2144 perm_rows.append(entry)
2144 perm_rows.append(entry)
2145
2145
2146 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2146 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2147 return perm_rows
2147 return perm_rows
2148
2148
2149 def get_api_data(self, include_secrets=False):
2149 def get_api_data(self, include_secrets=False):
2150 """
2150 """
2151 Common function for generating repo api data
2151 Common function for generating repo api data
2152
2152
2153 :param include_secrets: See :meth:`User.get_api_data`.
2153 :param include_secrets: See :meth:`User.get_api_data`.
2154
2154
2155 """
2155 """
2156 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2156 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2157 # move this methods on models level.
2157 # move this methods on models level.
2158 from rhodecode.model.settings import SettingsModel
2158 from rhodecode.model.settings import SettingsModel
2159 from rhodecode.model.repo import RepoModel
2159 from rhodecode.model.repo import RepoModel
2160
2160
2161 repo = self
2161 repo = self
2162 _user_id, _time, _reason = self.locked
2162 _user_id, _time, _reason = self.locked
2163
2163
2164 data = {
2164 data = {
2165 'repo_id': repo.repo_id,
2165 'repo_id': repo.repo_id,
2166 'repo_name': repo.repo_name,
2166 'repo_name': repo.repo_name,
2167 'repo_type': repo.repo_type,
2167 'repo_type': repo.repo_type,
2168 'clone_uri': repo.clone_uri or '',
2168 'clone_uri': repo.clone_uri or '',
2169 'push_uri': repo.push_uri or '',
2169 'push_uri': repo.push_uri or '',
2170 'url': RepoModel().get_url(self),
2170 'url': RepoModel().get_url(self),
2171 'private': repo.private,
2171 'private': repo.private,
2172 'created_on': repo.created_on,
2172 'created_on': repo.created_on,
2173 'description': repo.description_safe,
2173 'description': repo.description_safe,
2174 'landing_rev': repo.landing_rev,
2174 'landing_rev': repo.landing_rev,
2175 'owner': repo.user.username,
2175 'owner': repo.user.username,
2176 'fork_of': repo.fork.repo_name if repo.fork else None,
2176 'fork_of': repo.fork.repo_name if repo.fork else None,
2177 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2177 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2178 'enable_statistics': repo.enable_statistics,
2178 'enable_statistics': repo.enable_statistics,
2179 'enable_locking': repo.enable_locking,
2179 'enable_locking': repo.enable_locking,
2180 'enable_downloads': repo.enable_downloads,
2180 'enable_downloads': repo.enable_downloads,
2181 'last_changeset': repo.changeset_cache,
2181 'last_changeset': repo.changeset_cache,
2182 'locked_by': User.get(_user_id).get_api_data(
2182 'locked_by': User.get(_user_id).get_api_data(
2183 include_secrets=include_secrets) if _user_id else None,
2183 include_secrets=include_secrets) if _user_id else None,
2184 'locked_date': time_to_datetime(_time) if _time else None,
2184 'locked_date': time_to_datetime(_time) if _time else None,
2185 'lock_reason': _reason if _reason else None,
2185 'lock_reason': _reason if _reason else None,
2186 }
2186 }
2187
2187
2188 # TODO: mikhail: should be per-repo settings here
2188 # TODO: mikhail: should be per-repo settings here
2189 rc_config = SettingsModel().get_all_settings()
2189 rc_config = SettingsModel().get_all_settings()
2190 repository_fields = str2bool(
2190 repository_fields = str2bool(
2191 rc_config.get('rhodecode_repository_fields'))
2191 rc_config.get('rhodecode_repository_fields'))
2192 if repository_fields:
2192 if repository_fields:
2193 for f in self.extra_fields:
2193 for f in self.extra_fields:
2194 data[f.field_key_prefixed] = f.field_value
2194 data[f.field_key_prefixed] = f.field_value
2195
2195
2196 return data
2196 return data
2197
2197
2198 @classmethod
2198 @classmethod
2199 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2199 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2200 if not lock_time:
2200 if not lock_time:
2201 lock_time = time.time()
2201 lock_time = time.time()
2202 if not lock_reason:
2202 if not lock_reason:
2203 lock_reason = cls.LOCK_AUTOMATIC
2203 lock_reason = cls.LOCK_AUTOMATIC
2204 repo.locked = [user_id, lock_time, lock_reason]
2204 repo.locked = [user_id, lock_time, lock_reason]
2205 Session().add(repo)
2205 Session().add(repo)
2206 Session().commit()
2206 Session().commit()
2207
2207
2208 @classmethod
2208 @classmethod
2209 def unlock(cls, repo):
2209 def unlock(cls, repo):
2210 repo.locked = None
2210 repo.locked = None
2211 Session().add(repo)
2211 Session().add(repo)
2212 Session().commit()
2212 Session().commit()
2213
2213
2214 @classmethod
2214 @classmethod
2215 def getlock(cls, repo):
2215 def getlock(cls, repo):
2216 return repo.locked
2216 return repo.locked
2217
2217
2218 def is_user_lock(self, user_id):
2218 def is_user_lock(self, user_id):
2219 if self.lock[0]:
2219 if self.lock[0]:
2220 lock_user_id = safe_int(self.lock[0])
2220 lock_user_id = safe_int(self.lock[0])
2221 user_id = safe_int(user_id)
2221 user_id = safe_int(user_id)
2222 # both are ints, and they are equal
2222 # both are ints, and they are equal
2223 return all([lock_user_id, user_id]) and lock_user_id == user_id
2223 return all([lock_user_id, user_id]) and lock_user_id == user_id
2224
2224
2225 return False
2225 return False
2226
2226
2227 def get_locking_state(self, action, user_id, only_when_enabled=True):
2227 def get_locking_state(self, action, user_id, only_when_enabled=True):
2228 """
2228 """
2229 Checks locking on this repository, if locking is enabled and lock is
2229 Checks locking on this repository, if locking is enabled and lock is
2230 present returns a tuple of make_lock, locked, locked_by.
2230 present returns a tuple of make_lock, locked, locked_by.
2231 make_lock can have 3 states None (do nothing) True, make lock
2231 make_lock can have 3 states None (do nothing) True, make lock
2232 False release lock, This value is later propagated to hooks, which
2232 False release lock, This value is later propagated to hooks, which
2233 do the locking. Think about this as signals passed to hooks what to do.
2233 do the locking. Think about this as signals passed to hooks what to do.
2234
2234
2235 """
2235 """
2236 # TODO: johbo: This is part of the business logic and should be moved
2236 # TODO: johbo: This is part of the business logic and should be moved
2237 # into the RepositoryModel.
2237 # into the RepositoryModel.
2238
2238
2239 if action not in ('push', 'pull'):
2239 if action not in ('push', 'pull'):
2240 raise ValueError("Invalid action value: %s" % repr(action))
2240 raise ValueError("Invalid action value: %s" % repr(action))
2241
2241
2242 # defines if locked error should be thrown to user
2242 # defines if locked error should be thrown to user
2243 currently_locked = False
2243 currently_locked = False
2244 # defines if new lock should be made, tri-state
2244 # defines if new lock should be made, tri-state
2245 make_lock = None
2245 make_lock = None
2246 repo = self
2246 repo = self
2247 user = User.get(user_id)
2247 user = User.get(user_id)
2248
2248
2249 lock_info = repo.locked
2249 lock_info = repo.locked
2250
2250
2251 if repo and (repo.enable_locking or not only_when_enabled):
2251 if repo and (repo.enable_locking or not only_when_enabled):
2252 if action == 'push':
2252 if action == 'push':
2253 # check if it's already locked !, if it is compare users
2253 # check if it's already locked !, if it is compare users
2254 locked_by_user_id = lock_info[0]
2254 locked_by_user_id = lock_info[0]
2255 if user.user_id == locked_by_user_id:
2255 if user.user_id == locked_by_user_id:
2256 log.debug(
2256 log.debug(
2257 'Got `push` action from user %s, now unlocking', user)
2257 'Got `push` action from user %s, now unlocking', user)
2258 # unlock if we have push from user who locked
2258 # unlock if we have push from user who locked
2259 make_lock = False
2259 make_lock = False
2260 else:
2260 else:
2261 # we're not the same user who locked, ban with
2261 # we're not the same user who locked, ban with
2262 # code defined in settings (default is 423 HTTP Locked) !
2262 # code defined in settings (default is 423 HTTP Locked) !
2263 log.debug('Repo %s is currently locked by %s', repo, user)
2263 log.debug('Repo %s is currently locked by %s', repo, user)
2264 currently_locked = True
2264 currently_locked = True
2265 elif action == 'pull':
2265 elif action == 'pull':
2266 # [0] user [1] date
2266 # [0] user [1] date
2267 if lock_info[0] and lock_info[1]:
2267 if lock_info[0] and lock_info[1]:
2268 log.debug('Repo %s is currently locked by %s', repo, user)
2268 log.debug('Repo %s is currently locked by %s', repo, user)
2269 currently_locked = True
2269 currently_locked = True
2270 else:
2270 else:
2271 log.debug('Setting lock on repo %s by %s', repo, user)
2271 log.debug('Setting lock on repo %s by %s', repo, user)
2272 make_lock = True
2272 make_lock = True
2273
2273
2274 else:
2274 else:
2275 log.debug('Repository %s do not have locking enabled', repo)
2275 log.debug('Repository %s do not have locking enabled', repo)
2276
2276
2277 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2277 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2278 make_lock, currently_locked, lock_info)
2278 make_lock, currently_locked, lock_info)
2279
2279
2280 from rhodecode.lib.auth import HasRepoPermissionAny
2280 from rhodecode.lib.auth import HasRepoPermissionAny
2281 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2281 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2282 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2282 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2283 # if we don't have at least write permission we cannot make a lock
2283 # if we don't have at least write permission we cannot make a lock
2284 log.debug('lock state reset back to FALSE due to lack '
2284 log.debug('lock state reset back to FALSE due to lack '
2285 'of at least read permission')
2285 'of at least read permission')
2286 make_lock = False
2286 make_lock = False
2287
2287
2288 return make_lock, currently_locked, lock_info
2288 return make_lock, currently_locked, lock_info
2289
2289
2290 @property
2290 @property
2291 def last_commit_cache_update_diff(self):
2291 def last_commit_cache_update_diff(self):
2292 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2292 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2293
2293
2294 @classmethod
2294 @classmethod
2295 def _load_commit_change(cls, last_commit_cache):
2295 def _load_commit_change(cls, last_commit_cache):
2296 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2296 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2297 empty_date = datetime.datetime.fromtimestamp(0)
2297 empty_date = datetime.datetime.fromtimestamp(0)
2298 date_latest = last_commit_cache.get('date', empty_date)
2298 date_latest = last_commit_cache.get('date', empty_date)
2299 try:
2299 try:
2300 return parse_datetime(date_latest)
2300 return parse_datetime(date_latest)
2301 except Exception:
2301 except Exception:
2302 return empty_date
2302 return empty_date
2303
2303
2304 @property
2304 @property
2305 def last_commit_change(self):
2305 def last_commit_change(self):
2306 return self._load_commit_change(self.changeset_cache)
2306 return self._load_commit_change(self.changeset_cache)
2307
2307
2308 @property
2308 @property
2309 def last_db_change(self):
2309 def last_db_change(self):
2310 return self.updated_on
2310 return self.updated_on
2311
2311
2312 @property
2312 @property
2313 def clone_uri_hidden(self):
2313 def clone_uri_hidden(self):
2314 clone_uri = self.clone_uri
2314 clone_uri = self.clone_uri
2315 if clone_uri:
2315 if clone_uri:
2316 import urlobject
2316 import urlobject
2317 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2317 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2318 if url_obj.password:
2318 if url_obj.password:
2319 clone_uri = url_obj.with_password('*****')
2319 clone_uri = url_obj.with_password('*****')
2320 return clone_uri
2320 return clone_uri
2321
2321
2322 @property
2322 @property
2323 def push_uri_hidden(self):
2323 def push_uri_hidden(self):
2324 push_uri = self.push_uri
2324 push_uri = self.push_uri
2325 if push_uri:
2325 if push_uri:
2326 import urlobject
2326 import urlobject
2327 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2327 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2328 if url_obj.password:
2328 if url_obj.password:
2329 push_uri = url_obj.with_password('*****')
2329 push_uri = url_obj.with_password('*****')
2330 return push_uri
2330 return push_uri
2331
2331
2332 def clone_url(self, **override):
2332 def clone_url(self, **override):
2333 from rhodecode.model.settings import SettingsModel
2333 from rhodecode.model.settings import SettingsModel
2334
2334
2335 uri_tmpl = None
2335 uri_tmpl = None
2336 if 'with_id' in override:
2336 if 'with_id' in override:
2337 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2337 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2338 del override['with_id']
2338 del override['with_id']
2339
2339
2340 if 'uri_tmpl' in override:
2340 if 'uri_tmpl' in override:
2341 uri_tmpl = override['uri_tmpl']
2341 uri_tmpl = override['uri_tmpl']
2342 del override['uri_tmpl']
2342 del override['uri_tmpl']
2343
2343
2344 ssh = False
2344 ssh = False
2345 if 'ssh' in override:
2345 if 'ssh' in override:
2346 ssh = True
2346 ssh = True
2347 del override['ssh']
2347 del override['ssh']
2348
2348
2349 # we didn't override our tmpl from **overrides
2349 # we didn't override our tmpl from **overrides
2350 request = get_current_request()
2350 request = get_current_request()
2351 if not uri_tmpl:
2351 if not uri_tmpl:
2352 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2352 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2353 rc_config = request.call_context.rc_config
2353 rc_config = request.call_context.rc_config
2354 else:
2354 else:
2355 rc_config = SettingsModel().get_all_settings(cache=True)
2355 rc_config = SettingsModel().get_all_settings(cache=True)
2356
2356
2357 if ssh:
2357 if ssh:
2358 uri_tmpl = rc_config.get(
2358 uri_tmpl = rc_config.get(
2359 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2359 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2360
2360
2361 else:
2361 else:
2362 uri_tmpl = rc_config.get(
2362 uri_tmpl = rc_config.get(
2363 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2363 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2364
2364
2365 return get_clone_url(request=request,
2365 return get_clone_url(request=request,
2366 uri_tmpl=uri_tmpl,
2366 uri_tmpl=uri_tmpl,
2367 repo_name=self.repo_name,
2367 repo_name=self.repo_name,
2368 repo_id=self.repo_id,
2368 repo_id=self.repo_id,
2369 repo_type=self.repo_type,
2369 repo_type=self.repo_type,
2370 **override)
2370 **override)
2371
2371
2372 def set_state(self, state):
2372 def set_state(self, state):
2373 self.repo_state = state
2373 self.repo_state = state
2374 Session().add(self)
2374 Session().add(self)
2375 #==========================================================================
2375 #==========================================================================
2376 # SCM PROPERTIES
2376 # SCM PROPERTIES
2377 #==========================================================================
2377 #==========================================================================
2378
2378
2379 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False):
2379 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False):
2380 return get_commit_safe(
2380 return get_commit_safe(
2381 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2381 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2382 maybe_unreachable=maybe_unreachable)
2382 maybe_unreachable=maybe_unreachable)
2383
2383
2384 def get_changeset(self, rev=None, pre_load=None):
2384 def get_changeset(self, rev=None, pre_load=None):
2385 warnings.warn("Use get_commit", DeprecationWarning)
2385 warnings.warn("Use get_commit", DeprecationWarning)
2386 commit_id = None
2386 commit_id = None
2387 commit_idx = None
2387 commit_idx = None
2388 if isinstance(rev, compat.string_types):
2388 if isinstance(rev, compat.string_types):
2389 commit_id = rev
2389 commit_id = rev
2390 else:
2390 else:
2391 commit_idx = rev
2391 commit_idx = rev
2392 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2392 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2393 pre_load=pre_load)
2393 pre_load=pre_load)
2394
2394
2395 def get_landing_commit(self):
2395 def get_landing_commit(self):
2396 """
2396 """
2397 Returns landing commit, or if that doesn't exist returns the tip
2397 Returns landing commit, or if that doesn't exist returns the tip
2398 """
2398 """
2399 _rev_type, _rev = self.landing_rev
2399 _rev_type, _rev = self.landing_rev
2400 commit = self.get_commit(_rev)
2400 commit = self.get_commit(_rev)
2401 if isinstance(commit, EmptyCommit):
2401 if isinstance(commit, EmptyCommit):
2402 return self.get_commit()
2402 return self.get_commit()
2403 return commit
2403 return commit
2404
2404
2405 def flush_commit_cache(self):
2405 def flush_commit_cache(self):
2406 self.update_commit_cache(cs_cache={'raw_id':'0'})
2406 self.update_commit_cache(cs_cache={'raw_id':'0'})
2407 self.update_commit_cache()
2407 self.update_commit_cache()
2408
2408
2409 def update_commit_cache(self, cs_cache=None, config=None):
2409 def update_commit_cache(self, cs_cache=None, config=None):
2410 """
2410 """
2411 Update cache of last commit for repository
2411 Update cache of last commit for repository
2412 cache_keys should be::
2412 cache_keys should be::
2413
2413
2414 source_repo_id
2414 source_repo_id
2415 short_id
2415 short_id
2416 raw_id
2416 raw_id
2417 revision
2417 revision
2418 parents
2418 parents
2419 message
2419 message
2420 date
2420 date
2421 author
2421 author
2422 updated_on
2422 updated_on
2423
2423
2424 """
2424 """
2425 from rhodecode.lib.vcs.backends.base import BaseChangeset
2425 from rhodecode.lib.vcs.backends.base import BaseChangeset
2426 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2426 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2427 empty_date = datetime.datetime.fromtimestamp(0)
2427 empty_date = datetime.datetime.fromtimestamp(0)
2428
2428
2429 if cs_cache is None:
2429 if cs_cache is None:
2430 # use no-cache version here
2430 # use no-cache version here
2431 try:
2431 try:
2432 scm_repo = self.scm_instance(cache=False, config=config)
2432 scm_repo = self.scm_instance(cache=False, config=config)
2433 except VCSError:
2433 except VCSError:
2434 scm_repo = None
2434 scm_repo = None
2435 empty = scm_repo is None or scm_repo.is_empty()
2435 empty = scm_repo is None or scm_repo.is_empty()
2436
2436
2437 if not empty:
2437 if not empty:
2438 cs_cache = scm_repo.get_commit(
2438 cs_cache = scm_repo.get_commit(
2439 pre_load=["author", "date", "message", "parents", "branch"])
2439 pre_load=["author", "date", "message", "parents", "branch"])
2440 else:
2440 else:
2441 cs_cache = EmptyCommit()
2441 cs_cache = EmptyCommit()
2442
2442
2443 if isinstance(cs_cache, BaseChangeset):
2443 if isinstance(cs_cache, BaseChangeset):
2444 cs_cache = cs_cache.__json__()
2444 cs_cache = cs_cache.__json__()
2445
2445
2446 def is_outdated(new_cs_cache):
2446 def is_outdated(new_cs_cache):
2447 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2447 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2448 new_cs_cache['revision'] != self.changeset_cache['revision']):
2448 new_cs_cache['revision'] != self.changeset_cache['revision']):
2449 return True
2449 return True
2450 return False
2450 return False
2451
2451
2452 # check if we have maybe already latest cached revision
2452 # check if we have maybe already latest cached revision
2453 if is_outdated(cs_cache) or not self.changeset_cache:
2453 if is_outdated(cs_cache) or not self.changeset_cache:
2454 _current_datetime = datetime.datetime.utcnow()
2454 _current_datetime = datetime.datetime.utcnow()
2455 last_change = cs_cache.get('date') or _current_datetime
2455 last_change = cs_cache.get('date') or _current_datetime
2456 # we check if last update is newer than the new value
2456 # we check if last update is newer than the new value
2457 # if yes, we use the current timestamp instead. Imagine you get
2457 # if yes, we use the current timestamp instead. Imagine you get
2458 # old commit pushed 1y ago, we'd set last update 1y to ago.
2458 # old commit pushed 1y ago, we'd set last update 1y to ago.
2459 last_change_timestamp = datetime_to_time(last_change)
2459 last_change_timestamp = datetime_to_time(last_change)
2460 current_timestamp = datetime_to_time(last_change)
2460 current_timestamp = datetime_to_time(last_change)
2461 if last_change_timestamp > current_timestamp and not empty:
2461 if last_change_timestamp > current_timestamp and not empty:
2462 cs_cache['date'] = _current_datetime
2462 cs_cache['date'] = _current_datetime
2463
2463
2464 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2464 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2465 cs_cache['updated_on'] = time.time()
2465 cs_cache['updated_on'] = time.time()
2466 self.changeset_cache = cs_cache
2466 self.changeset_cache = cs_cache
2467 self.updated_on = last_change
2467 self.updated_on = last_change
2468 Session().add(self)
2468 Session().add(self)
2469 Session().commit()
2469 Session().commit()
2470
2470
2471 else:
2471 else:
2472 if empty:
2472 if empty:
2473 cs_cache = EmptyCommit().__json__()
2473 cs_cache = EmptyCommit().__json__()
2474 else:
2474 else:
2475 cs_cache = self.changeset_cache
2475 cs_cache = self.changeset_cache
2476
2476
2477 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2477 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2478
2478
2479 cs_cache['updated_on'] = time.time()
2479 cs_cache['updated_on'] = time.time()
2480 self.changeset_cache = cs_cache
2480 self.changeset_cache = cs_cache
2481 self.updated_on = _date_latest
2481 self.updated_on = _date_latest
2482 Session().add(self)
2482 Session().add(self)
2483 Session().commit()
2483 Session().commit()
2484
2484
2485 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2485 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2486 self.repo_name, cs_cache, _date_latest)
2486 self.repo_name, cs_cache, _date_latest)
2487
2487
2488 @property
2488 @property
2489 def tip(self):
2489 def tip(self):
2490 return self.get_commit('tip')
2490 return self.get_commit('tip')
2491
2491
2492 @property
2492 @property
2493 def author(self):
2493 def author(self):
2494 return self.tip.author
2494 return self.tip.author
2495
2495
2496 @property
2496 @property
2497 def last_change(self):
2497 def last_change(self):
2498 return self.scm_instance().last_change
2498 return self.scm_instance().last_change
2499
2499
2500 def get_comments(self, revisions=None):
2500 def get_comments(self, revisions=None):
2501 """
2501 """
2502 Returns comments for this repository grouped by revisions
2502 Returns comments for this repository grouped by revisions
2503
2503
2504 :param revisions: filter query by revisions only
2504 :param revisions: filter query by revisions only
2505 """
2505 """
2506 cmts = ChangesetComment.query()\
2506 cmts = ChangesetComment.query()\
2507 .filter(ChangesetComment.repo == self)
2507 .filter(ChangesetComment.repo == self)
2508 if revisions:
2508 if revisions:
2509 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2509 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2510 grouped = collections.defaultdict(list)
2510 grouped = collections.defaultdict(list)
2511 for cmt in cmts.all():
2511 for cmt in cmts.all():
2512 grouped[cmt.revision].append(cmt)
2512 grouped[cmt.revision].append(cmt)
2513 return grouped
2513 return grouped
2514
2514
2515 def statuses(self, revisions=None):
2515 def statuses(self, revisions=None):
2516 """
2516 """
2517 Returns statuses for this repository
2517 Returns statuses for this repository
2518
2518
2519 :param revisions: list of revisions to get statuses for
2519 :param revisions: list of revisions to get statuses for
2520 """
2520 """
2521 statuses = ChangesetStatus.query()\
2521 statuses = ChangesetStatus.query()\
2522 .filter(ChangesetStatus.repo == self)\
2522 .filter(ChangesetStatus.repo == self)\
2523 .filter(ChangesetStatus.version == 0)
2523 .filter(ChangesetStatus.version == 0)
2524
2524
2525 if revisions:
2525 if revisions:
2526 # Try doing the filtering in chunks to avoid hitting limits
2526 # Try doing the filtering in chunks to avoid hitting limits
2527 size = 500
2527 size = 500
2528 status_results = []
2528 status_results = []
2529 for chunk in xrange(0, len(revisions), size):
2529 for chunk in xrange(0, len(revisions), size):
2530 status_results += statuses.filter(
2530 status_results += statuses.filter(
2531 ChangesetStatus.revision.in_(
2531 ChangesetStatus.revision.in_(
2532 revisions[chunk: chunk+size])
2532 revisions[chunk: chunk+size])
2533 ).all()
2533 ).all()
2534 else:
2534 else:
2535 status_results = statuses.all()
2535 status_results = statuses.all()
2536
2536
2537 grouped = {}
2537 grouped = {}
2538
2538
2539 # maybe we have open new pullrequest without a status?
2539 # maybe we have open new pullrequest without a status?
2540 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2540 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2541 status_lbl = ChangesetStatus.get_status_lbl(stat)
2541 status_lbl = ChangesetStatus.get_status_lbl(stat)
2542 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2542 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2543 for rev in pr.revisions:
2543 for rev in pr.revisions:
2544 pr_id = pr.pull_request_id
2544 pr_id = pr.pull_request_id
2545 pr_repo = pr.target_repo.repo_name
2545 pr_repo = pr.target_repo.repo_name
2546 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2546 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2547
2547
2548 for stat in status_results:
2548 for stat in status_results:
2549 pr_id = pr_repo = None
2549 pr_id = pr_repo = None
2550 if stat.pull_request:
2550 if stat.pull_request:
2551 pr_id = stat.pull_request.pull_request_id
2551 pr_id = stat.pull_request.pull_request_id
2552 pr_repo = stat.pull_request.target_repo.repo_name
2552 pr_repo = stat.pull_request.target_repo.repo_name
2553 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2553 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2554 pr_id, pr_repo]
2554 pr_id, pr_repo]
2555 return grouped
2555 return grouped
2556
2556
2557 # ==========================================================================
2557 # ==========================================================================
2558 # SCM CACHE INSTANCE
2558 # SCM CACHE INSTANCE
2559 # ==========================================================================
2559 # ==========================================================================
2560
2560
2561 def scm_instance(self, **kwargs):
2561 def scm_instance(self, **kwargs):
2562 import rhodecode
2562 import rhodecode
2563
2563
2564 # Passing a config will not hit the cache currently only used
2564 # Passing a config will not hit the cache currently only used
2565 # for repo2dbmapper
2565 # for repo2dbmapper
2566 config = kwargs.pop('config', None)
2566 config = kwargs.pop('config', None)
2567 cache = kwargs.pop('cache', None)
2567 cache = kwargs.pop('cache', None)
2568 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2568 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2569 if vcs_full_cache is not None:
2569 if vcs_full_cache is not None:
2570 # allows override global config
2570 # allows override global config
2571 full_cache = vcs_full_cache
2571 full_cache = vcs_full_cache
2572 else:
2572 else:
2573 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2573 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2574 # if cache is NOT defined use default global, else we have a full
2574 # if cache is NOT defined use default global, else we have a full
2575 # control over cache behaviour
2575 # control over cache behaviour
2576 if cache is None and full_cache and not config:
2576 if cache is None and full_cache and not config:
2577 log.debug('Initializing pure cached instance for %s', self.repo_path)
2577 log.debug('Initializing pure cached instance for %s', self.repo_path)
2578 return self._get_instance_cached()
2578 return self._get_instance_cached()
2579
2579
2580 # cache here is sent to the "vcs server"
2580 # cache here is sent to the "vcs server"
2581 return self._get_instance(cache=bool(cache), config=config)
2581 return self._get_instance(cache=bool(cache), config=config)
2582
2582
2583 def _get_instance_cached(self):
2583 def _get_instance_cached(self):
2584 from rhodecode.lib import rc_cache
2584 from rhodecode.lib import rc_cache
2585
2585
2586 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2586 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2587 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2587 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2588 repo_id=self.repo_id)
2588 repo_id=self.repo_id)
2589 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2589 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2590
2590
2591 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2591 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2592 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2592 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2593 return self._get_instance(repo_state_uid=_cache_state_uid)
2593 return self._get_instance(repo_state_uid=_cache_state_uid)
2594
2594
2595 # we must use thread scoped cache here,
2595 # we must use thread scoped cache here,
2596 # because each thread of gevent needs it's own not shared connection and cache
2596 # because each thread of gevent needs it's own not shared connection and cache
2597 # we also alter `args` so the cache key is individual for every green thread.
2597 # we also alter `args` so the cache key is individual for every green thread.
2598 inv_context_manager = rc_cache.InvalidationContext(
2598 inv_context_manager = rc_cache.InvalidationContext(
2599 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2599 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2600 thread_scoped=True)
2600 thread_scoped=True)
2601 with inv_context_manager as invalidation_context:
2601 with inv_context_manager as invalidation_context:
2602 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2602 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2603 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2603 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2604
2604
2605 # re-compute and store cache if we get invalidate signal
2605 # re-compute and store cache if we get invalidate signal
2606 if invalidation_context.should_invalidate():
2606 if invalidation_context.should_invalidate():
2607 instance = get_instance_cached.refresh(*args)
2607 instance = get_instance_cached.refresh(*args)
2608 else:
2608 else:
2609 instance = get_instance_cached(*args)
2609 instance = get_instance_cached(*args)
2610
2610
2611 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2611 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2612 return instance
2612 return instance
2613
2613
2614 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2614 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2615 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2615 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2616 self.repo_type, self.repo_path, cache)
2616 self.repo_type, self.repo_path, cache)
2617 config = config or self._config
2617 config = config or self._config
2618 custom_wire = {
2618 custom_wire = {
2619 'cache': cache, # controls the vcs.remote cache
2619 'cache': cache, # controls the vcs.remote cache
2620 'repo_state_uid': repo_state_uid
2620 'repo_state_uid': repo_state_uid
2621 }
2621 }
2622 repo = get_vcs_instance(
2622 repo = get_vcs_instance(
2623 repo_path=safe_str(self.repo_full_path),
2623 repo_path=safe_str(self.repo_full_path),
2624 config=config,
2624 config=config,
2625 with_wire=custom_wire,
2625 with_wire=custom_wire,
2626 create=False,
2626 create=False,
2627 _vcs_alias=self.repo_type)
2627 _vcs_alias=self.repo_type)
2628 if repo is not None:
2628 if repo is not None:
2629 repo.count() # cache rebuild
2629 repo.count() # cache rebuild
2630 return repo
2630 return repo
2631
2631
2632 def get_shadow_repository_path(self, workspace_id):
2632 def get_shadow_repository_path(self, workspace_id):
2633 from rhodecode.lib.vcs.backends.base import BaseRepository
2633 from rhodecode.lib.vcs.backends.base import BaseRepository
2634 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2634 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2635 self.repo_full_path, self.repo_id, workspace_id)
2635 self.repo_full_path, self.repo_id, workspace_id)
2636 return shadow_repo_path
2636 return shadow_repo_path
2637
2637
2638 def __json__(self):
2638 def __json__(self):
2639 return {'landing_rev': self.landing_rev}
2639 return {'landing_rev': self.landing_rev}
2640
2640
2641 def get_dict(self):
2641 def get_dict(self):
2642
2642
2643 # Since we transformed `repo_name` to a hybrid property, we need to
2643 # Since we transformed `repo_name` to a hybrid property, we need to
2644 # keep compatibility with the code which uses `repo_name` field.
2644 # keep compatibility with the code which uses `repo_name` field.
2645
2645
2646 result = super(Repository, self).get_dict()
2646 result = super(Repository, self).get_dict()
2647 result['repo_name'] = result.pop('_repo_name', None)
2647 result['repo_name'] = result.pop('_repo_name', None)
2648 return result
2648 return result
2649
2649
2650
2650
2651 class RepoGroup(Base, BaseModel):
2651 class RepoGroup(Base, BaseModel):
2652 __tablename__ = 'groups'
2652 __tablename__ = 'groups'
2653 __table_args__ = (
2653 __table_args__ = (
2654 UniqueConstraint('group_name', 'group_parent_id'),
2654 UniqueConstraint('group_name', 'group_parent_id'),
2655 base_table_args,
2655 base_table_args,
2656 )
2656 )
2657 __mapper_args__ = {'order_by': 'group_name'}
2657 __mapper_args__ = {'order_by': 'group_name'}
2658
2658
2659 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2659 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2660
2660
2661 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2661 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2662 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2662 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2663 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2663 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2664 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2664 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2665 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2665 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2666 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2666 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2667 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2667 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2668 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2668 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2669 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2669 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2670 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2670 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2671 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2671 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2672
2672
2673 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2673 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2674 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2674 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2675 parent_group = relationship('RepoGroup', remote_side=group_id)
2675 parent_group = relationship('RepoGroup', remote_side=group_id)
2676 user = relationship('User')
2676 user = relationship('User')
2677 integrations = relationship('Integration', cascade="all, delete-orphan")
2677 integrations = relationship('Integration', cascade="all, delete-orphan")
2678
2678
2679 # no cascade, set NULL
2679 # no cascade, set NULL
2680 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2680 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2681
2681
2682 def __init__(self, group_name='', parent_group=None):
2682 def __init__(self, group_name='', parent_group=None):
2683 self.group_name = group_name
2683 self.group_name = group_name
2684 self.parent_group = parent_group
2684 self.parent_group = parent_group
2685
2685
2686 def __unicode__(self):
2686 def __unicode__(self):
2687 return u"<%s('id:%s:%s')>" % (
2687 return u"<%s('id:%s:%s')>" % (
2688 self.__class__.__name__, self.group_id, self.group_name)
2688 self.__class__.__name__, self.group_id, self.group_name)
2689
2689
2690 @hybrid_property
2690 @hybrid_property
2691 def group_name(self):
2691 def group_name(self):
2692 return self._group_name
2692 return self._group_name
2693
2693
2694 @group_name.setter
2694 @group_name.setter
2695 def group_name(self, value):
2695 def group_name(self, value):
2696 self._group_name = value
2696 self._group_name = value
2697 self.group_name_hash = self.hash_repo_group_name(value)
2697 self.group_name_hash = self.hash_repo_group_name(value)
2698
2698
2699 @classmethod
2699 @classmethod
2700 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2700 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2701 from rhodecode.lib.vcs.backends.base import EmptyCommit
2701 from rhodecode.lib.vcs.backends.base import EmptyCommit
2702 dummy = EmptyCommit().__json__()
2702 dummy = EmptyCommit().__json__()
2703 if not changeset_cache_raw:
2703 if not changeset_cache_raw:
2704 dummy['source_repo_id'] = repo_id
2704 dummy['source_repo_id'] = repo_id
2705 return json.loads(json.dumps(dummy))
2705 return json.loads(json.dumps(dummy))
2706
2706
2707 try:
2707 try:
2708 return json.loads(changeset_cache_raw)
2708 return json.loads(changeset_cache_raw)
2709 except TypeError:
2709 except TypeError:
2710 return dummy
2710 return dummy
2711 except Exception:
2711 except Exception:
2712 log.error(traceback.format_exc())
2712 log.error(traceback.format_exc())
2713 return dummy
2713 return dummy
2714
2714
2715 @hybrid_property
2715 @hybrid_property
2716 def changeset_cache(self):
2716 def changeset_cache(self):
2717 return self._load_changeset_cache('', self._changeset_cache)
2717 return self._load_changeset_cache('', self._changeset_cache)
2718
2718
2719 @changeset_cache.setter
2719 @changeset_cache.setter
2720 def changeset_cache(self, val):
2720 def changeset_cache(self, val):
2721 try:
2721 try:
2722 self._changeset_cache = json.dumps(val)
2722 self._changeset_cache = json.dumps(val)
2723 except Exception:
2723 except Exception:
2724 log.error(traceback.format_exc())
2724 log.error(traceback.format_exc())
2725
2725
2726 @validates('group_parent_id')
2726 @validates('group_parent_id')
2727 def validate_group_parent_id(self, key, val):
2727 def validate_group_parent_id(self, key, val):
2728 """
2728 """
2729 Check cycle references for a parent group to self
2729 Check cycle references for a parent group to self
2730 """
2730 """
2731 if self.group_id and val:
2731 if self.group_id and val:
2732 assert val != self.group_id
2732 assert val != self.group_id
2733
2733
2734 return val
2734 return val
2735
2735
2736 @hybrid_property
2736 @hybrid_property
2737 def description_safe(self):
2737 def description_safe(self):
2738 from rhodecode.lib import helpers as h
2738 from rhodecode.lib import helpers as h
2739 return h.escape(self.group_description)
2739 return h.escape(self.group_description)
2740
2740
2741 @classmethod
2741 @classmethod
2742 def hash_repo_group_name(cls, repo_group_name):
2742 def hash_repo_group_name(cls, repo_group_name):
2743 val = remove_formatting(repo_group_name)
2743 val = remove_formatting(repo_group_name)
2744 val = safe_str(val).lower()
2744 val = safe_str(val).lower()
2745 chars = []
2745 chars = []
2746 for c in val:
2746 for c in val:
2747 if c not in string.ascii_letters:
2747 if c not in string.ascii_letters:
2748 c = str(ord(c))
2748 c = str(ord(c))
2749 chars.append(c)
2749 chars.append(c)
2750
2750
2751 return ''.join(chars)
2751 return ''.join(chars)
2752
2752
2753 @classmethod
2753 @classmethod
2754 def _generate_choice(cls, repo_group):
2754 def _generate_choice(cls, repo_group):
2755 from webhelpers2.html import literal as _literal
2755 from webhelpers2.html import literal as _literal
2756 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2756 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2757 return repo_group.group_id, _name(repo_group.full_path_splitted)
2757 return repo_group.group_id, _name(repo_group.full_path_splitted)
2758
2758
2759 @classmethod
2759 @classmethod
2760 def groups_choices(cls, groups=None, show_empty_group=True):
2760 def groups_choices(cls, groups=None, show_empty_group=True):
2761 if not groups:
2761 if not groups:
2762 groups = cls.query().all()
2762 groups = cls.query().all()
2763
2763
2764 repo_groups = []
2764 repo_groups = []
2765 if show_empty_group:
2765 if show_empty_group:
2766 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2766 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2767
2767
2768 repo_groups.extend([cls._generate_choice(x) for x in groups])
2768 repo_groups.extend([cls._generate_choice(x) for x in groups])
2769
2769
2770 repo_groups = sorted(
2770 repo_groups = sorted(
2771 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2771 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2772 return repo_groups
2772 return repo_groups
2773
2773
2774 @classmethod
2774 @classmethod
2775 def url_sep(cls):
2775 def url_sep(cls):
2776 return URL_SEP
2776 return URL_SEP
2777
2777
2778 @classmethod
2778 @classmethod
2779 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2779 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2780 if case_insensitive:
2780 if case_insensitive:
2781 gr = cls.query().filter(func.lower(cls.group_name)
2781 gr = cls.query().filter(func.lower(cls.group_name)
2782 == func.lower(group_name))
2782 == func.lower(group_name))
2783 else:
2783 else:
2784 gr = cls.query().filter(cls.group_name == group_name)
2784 gr = cls.query().filter(cls.group_name == group_name)
2785 if cache:
2785 if cache:
2786 name_key = _hash_key(group_name)
2786 name_key = _hash_key(group_name)
2787 gr = gr.options(
2787 gr = gr.options(
2788 FromCache("sql_cache_short", "get_group_%s" % name_key))
2788 FromCache("sql_cache_short", "get_group_%s" % name_key))
2789 return gr.scalar()
2789 return gr.scalar()
2790
2790
2791 @classmethod
2791 @classmethod
2792 def get_user_personal_repo_group(cls, user_id):
2792 def get_user_personal_repo_group(cls, user_id):
2793 user = User.get(user_id)
2793 user = User.get(user_id)
2794 if user.username == User.DEFAULT_USER:
2794 if user.username == User.DEFAULT_USER:
2795 return None
2795 return None
2796
2796
2797 return cls.query()\
2797 return cls.query()\
2798 .filter(cls.personal == true()) \
2798 .filter(cls.personal == true()) \
2799 .filter(cls.user == user) \
2799 .filter(cls.user == user) \
2800 .order_by(cls.group_id.asc()) \
2800 .order_by(cls.group_id.asc()) \
2801 .first()
2801 .first()
2802
2802
2803 @classmethod
2803 @classmethod
2804 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2804 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2805 case_insensitive=True):
2805 case_insensitive=True):
2806 q = RepoGroup.query()
2806 q = RepoGroup.query()
2807
2807
2808 if not isinstance(user_id, Optional):
2808 if not isinstance(user_id, Optional):
2809 q = q.filter(RepoGroup.user_id == user_id)
2809 q = q.filter(RepoGroup.user_id == user_id)
2810
2810
2811 if not isinstance(group_id, Optional):
2811 if not isinstance(group_id, Optional):
2812 q = q.filter(RepoGroup.group_parent_id == group_id)
2812 q = q.filter(RepoGroup.group_parent_id == group_id)
2813
2813
2814 if case_insensitive:
2814 if case_insensitive:
2815 q = q.order_by(func.lower(RepoGroup.group_name))
2815 q = q.order_by(func.lower(RepoGroup.group_name))
2816 else:
2816 else:
2817 q = q.order_by(RepoGroup.group_name)
2817 q = q.order_by(RepoGroup.group_name)
2818 return q.all()
2818 return q.all()
2819
2819
2820 @property
2820 @property
2821 def parents(self, parents_recursion_limit=10):
2821 def parents(self, parents_recursion_limit=10):
2822 groups = []
2822 groups = []
2823 if self.parent_group is None:
2823 if self.parent_group is None:
2824 return groups
2824 return groups
2825 cur_gr = self.parent_group
2825 cur_gr = self.parent_group
2826 groups.insert(0, cur_gr)
2826 groups.insert(0, cur_gr)
2827 cnt = 0
2827 cnt = 0
2828 while 1:
2828 while 1:
2829 cnt += 1
2829 cnt += 1
2830 gr = getattr(cur_gr, 'parent_group', None)
2830 gr = getattr(cur_gr, 'parent_group', None)
2831 cur_gr = cur_gr.parent_group
2831 cur_gr = cur_gr.parent_group
2832 if gr is None:
2832 if gr is None:
2833 break
2833 break
2834 if cnt == parents_recursion_limit:
2834 if cnt == parents_recursion_limit:
2835 # this will prevent accidental infinit loops
2835 # this will prevent accidental infinit loops
2836 log.error('more than %s parents found for group %s, stopping '
2836 log.error('more than %s parents found for group %s, stopping '
2837 'recursive parent fetching', parents_recursion_limit, self)
2837 'recursive parent fetching', parents_recursion_limit, self)
2838 break
2838 break
2839
2839
2840 groups.insert(0, gr)
2840 groups.insert(0, gr)
2841 return groups
2841 return groups
2842
2842
2843 @property
2843 @property
2844 def last_commit_cache_update_diff(self):
2844 def last_commit_cache_update_diff(self):
2845 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2845 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2846
2846
2847 @classmethod
2847 @classmethod
2848 def _load_commit_change(cls, last_commit_cache):
2848 def _load_commit_change(cls, last_commit_cache):
2849 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2849 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2850 empty_date = datetime.datetime.fromtimestamp(0)
2850 empty_date = datetime.datetime.fromtimestamp(0)
2851 date_latest = last_commit_cache.get('date', empty_date)
2851 date_latest = last_commit_cache.get('date', empty_date)
2852 try:
2852 try:
2853 return parse_datetime(date_latest)
2853 return parse_datetime(date_latest)
2854 except Exception:
2854 except Exception:
2855 return empty_date
2855 return empty_date
2856
2856
2857 @property
2857 @property
2858 def last_commit_change(self):
2858 def last_commit_change(self):
2859 return self._load_commit_change(self.changeset_cache)
2859 return self._load_commit_change(self.changeset_cache)
2860
2860
2861 @property
2861 @property
2862 def last_db_change(self):
2862 def last_db_change(self):
2863 return self.updated_on
2863 return self.updated_on
2864
2864
2865 @property
2865 @property
2866 def children(self):
2866 def children(self):
2867 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2867 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2868
2868
2869 @property
2869 @property
2870 def name(self):
2870 def name(self):
2871 return self.group_name.split(RepoGroup.url_sep())[-1]
2871 return self.group_name.split(RepoGroup.url_sep())[-1]
2872
2872
2873 @property
2873 @property
2874 def full_path(self):
2874 def full_path(self):
2875 return self.group_name
2875 return self.group_name
2876
2876
2877 @property
2877 @property
2878 def full_path_splitted(self):
2878 def full_path_splitted(self):
2879 return self.group_name.split(RepoGroup.url_sep())
2879 return self.group_name.split(RepoGroup.url_sep())
2880
2880
2881 @property
2881 @property
2882 def repositories(self):
2882 def repositories(self):
2883 return Repository.query()\
2883 return Repository.query()\
2884 .filter(Repository.group == self)\
2884 .filter(Repository.group == self)\
2885 .order_by(Repository.repo_name)
2885 .order_by(Repository.repo_name)
2886
2886
2887 @property
2887 @property
2888 def repositories_recursive_count(self):
2888 def repositories_recursive_count(self):
2889 cnt = self.repositories.count()
2889 cnt = self.repositories.count()
2890
2890
2891 def children_count(group):
2891 def children_count(group):
2892 cnt = 0
2892 cnt = 0
2893 for child in group.children:
2893 for child in group.children:
2894 cnt += child.repositories.count()
2894 cnt += child.repositories.count()
2895 cnt += children_count(child)
2895 cnt += children_count(child)
2896 return cnt
2896 return cnt
2897
2897
2898 return cnt + children_count(self)
2898 return cnt + children_count(self)
2899
2899
2900 def _recursive_objects(self, include_repos=True, include_groups=True):
2900 def _recursive_objects(self, include_repos=True, include_groups=True):
2901 all_ = []
2901 all_ = []
2902
2902
2903 def _get_members(root_gr):
2903 def _get_members(root_gr):
2904 if include_repos:
2904 if include_repos:
2905 for r in root_gr.repositories:
2905 for r in root_gr.repositories:
2906 all_.append(r)
2906 all_.append(r)
2907 childs = root_gr.children.all()
2907 childs = root_gr.children.all()
2908 if childs:
2908 if childs:
2909 for gr in childs:
2909 for gr in childs:
2910 if include_groups:
2910 if include_groups:
2911 all_.append(gr)
2911 all_.append(gr)
2912 _get_members(gr)
2912 _get_members(gr)
2913
2913
2914 root_group = []
2914 root_group = []
2915 if include_groups:
2915 if include_groups:
2916 root_group = [self]
2916 root_group = [self]
2917
2917
2918 _get_members(self)
2918 _get_members(self)
2919 return root_group + all_
2919 return root_group + all_
2920
2920
2921 def recursive_groups_and_repos(self):
2921 def recursive_groups_and_repos(self):
2922 """
2922 """
2923 Recursive return all groups, with repositories in those groups
2923 Recursive return all groups, with repositories in those groups
2924 """
2924 """
2925 return self._recursive_objects()
2925 return self._recursive_objects()
2926
2926
2927 def recursive_groups(self):
2927 def recursive_groups(self):
2928 """
2928 """
2929 Returns all children groups for this group including children of children
2929 Returns all children groups for this group including children of children
2930 """
2930 """
2931 return self._recursive_objects(include_repos=False)
2931 return self._recursive_objects(include_repos=False)
2932
2932
2933 def recursive_repos(self):
2933 def recursive_repos(self):
2934 """
2934 """
2935 Returns all children repositories for this group
2935 Returns all children repositories for this group
2936 """
2936 """
2937 return self._recursive_objects(include_groups=False)
2937 return self._recursive_objects(include_groups=False)
2938
2938
2939 def get_new_name(self, group_name):
2939 def get_new_name(self, group_name):
2940 """
2940 """
2941 returns new full group name based on parent and new name
2941 returns new full group name based on parent and new name
2942
2942
2943 :param group_name:
2943 :param group_name:
2944 """
2944 """
2945 path_prefix = (self.parent_group.full_path_splitted if
2945 path_prefix = (self.parent_group.full_path_splitted if
2946 self.parent_group else [])
2946 self.parent_group else [])
2947 return RepoGroup.url_sep().join(path_prefix + [group_name])
2947 return RepoGroup.url_sep().join(path_prefix + [group_name])
2948
2948
2949 def update_commit_cache(self, config=None):
2949 def update_commit_cache(self, config=None):
2950 """
2950 """
2951 Update cache of last commit for newest repository inside this repository group.
2951 Update cache of last commit for newest repository inside this repository group.
2952 cache_keys should be::
2952 cache_keys should be::
2953
2953
2954 source_repo_id
2954 source_repo_id
2955 short_id
2955 short_id
2956 raw_id
2956 raw_id
2957 revision
2957 revision
2958 parents
2958 parents
2959 message
2959 message
2960 date
2960 date
2961 author
2961 author
2962
2962
2963 """
2963 """
2964 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2964 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2965 empty_date = datetime.datetime.fromtimestamp(0)
2965 empty_date = datetime.datetime.fromtimestamp(0)
2966
2966
2967 def repo_groups_and_repos(root_gr):
2967 def repo_groups_and_repos(root_gr):
2968 for _repo in root_gr.repositories:
2968 for _repo in root_gr.repositories:
2969 yield _repo
2969 yield _repo
2970 for child_group in root_gr.children.all():
2970 for child_group in root_gr.children.all():
2971 yield child_group
2971 yield child_group
2972
2972
2973 latest_repo_cs_cache = {}
2973 latest_repo_cs_cache = {}
2974 for obj in repo_groups_and_repos(self):
2974 for obj in repo_groups_and_repos(self):
2975 repo_cs_cache = obj.changeset_cache
2975 repo_cs_cache = obj.changeset_cache
2976 date_latest = latest_repo_cs_cache.get('date', empty_date)
2976 date_latest = latest_repo_cs_cache.get('date', empty_date)
2977 date_current = repo_cs_cache.get('date', empty_date)
2977 date_current = repo_cs_cache.get('date', empty_date)
2978 current_timestamp = datetime_to_time(parse_datetime(date_latest))
2978 current_timestamp = datetime_to_time(parse_datetime(date_latest))
2979 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
2979 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
2980 latest_repo_cs_cache = repo_cs_cache
2980 latest_repo_cs_cache = repo_cs_cache
2981 if hasattr(obj, 'repo_id'):
2981 if hasattr(obj, 'repo_id'):
2982 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
2982 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
2983 else:
2983 else:
2984 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
2984 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
2985
2985
2986 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
2986 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
2987
2987
2988 latest_repo_cs_cache['updated_on'] = time.time()
2988 latest_repo_cs_cache['updated_on'] = time.time()
2989 self.changeset_cache = latest_repo_cs_cache
2989 self.changeset_cache = latest_repo_cs_cache
2990 self.updated_on = _date_latest
2990 self.updated_on = _date_latest
2991 Session().add(self)
2991 Session().add(self)
2992 Session().commit()
2992 Session().commit()
2993
2993
2994 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
2994 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
2995 self.group_name, latest_repo_cs_cache, _date_latest)
2995 self.group_name, latest_repo_cs_cache, _date_latest)
2996
2996
2997 def permissions(self, with_admins=True, with_owner=True,
2997 def permissions(self, with_admins=True, with_owner=True,
2998 expand_from_user_groups=False):
2998 expand_from_user_groups=False):
2999 """
2999 """
3000 Permissions for repository groups
3000 Permissions for repository groups
3001 """
3001 """
3002 _admin_perm = 'group.admin'
3002 _admin_perm = 'group.admin'
3003
3003
3004 owner_row = []
3004 owner_row = []
3005 if with_owner:
3005 if with_owner:
3006 usr = AttributeDict(self.user.get_dict())
3006 usr = AttributeDict(self.user.get_dict())
3007 usr.owner_row = True
3007 usr.owner_row = True
3008 usr.permission = _admin_perm
3008 usr.permission = _admin_perm
3009 owner_row.append(usr)
3009 owner_row.append(usr)
3010
3010
3011 super_admin_ids = []
3011 super_admin_ids = []
3012 super_admin_rows = []
3012 super_admin_rows = []
3013 if with_admins:
3013 if with_admins:
3014 for usr in User.get_all_super_admins():
3014 for usr in User.get_all_super_admins():
3015 super_admin_ids.append(usr.user_id)
3015 super_admin_ids.append(usr.user_id)
3016 # if this admin is also owner, don't double the record
3016 # if this admin is also owner, don't double the record
3017 if usr.user_id == owner_row[0].user_id:
3017 if usr.user_id == owner_row[0].user_id:
3018 owner_row[0].admin_row = True
3018 owner_row[0].admin_row = True
3019 else:
3019 else:
3020 usr = AttributeDict(usr.get_dict())
3020 usr = AttributeDict(usr.get_dict())
3021 usr.admin_row = True
3021 usr.admin_row = True
3022 usr.permission = _admin_perm
3022 usr.permission = _admin_perm
3023 super_admin_rows.append(usr)
3023 super_admin_rows.append(usr)
3024
3024
3025 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3025 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
3026 q = q.options(joinedload(UserRepoGroupToPerm.group),
3026 q = q.options(joinedload(UserRepoGroupToPerm.group),
3027 joinedload(UserRepoGroupToPerm.user),
3027 joinedload(UserRepoGroupToPerm.user),
3028 joinedload(UserRepoGroupToPerm.permission),)
3028 joinedload(UserRepoGroupToPerm.permission),)
3029
3029
3030 # get owners and admins and permissions. We do a trick of re-writing
3030 # get owners and admins and permissions. We do a trick of re-writing
3031 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3031 # objects from sqlalchemy to named-tuples due to sqlalchemy session
3032 # has a global reference and changing one object propagates to all
3032 # has a global reference and changing one object propagates to all
3033 # others. This means if admin is also an owner admin_row that change
3033 # others. This means if admin is also an owner admin_row that change
3034 # would propagate to both objects
3034 # would propagate to both objects
3035 perm_rows = []
3035 perm_rows = []
3036 for _usr in q.all():
3036 for _usr in q.all():
3037 usr = AttributeDict(_usr.user.get_dict())
3037 usr = AttributeDict(_usr.user.get_dict())
3038 # if this user is also owner/admin, mark as duplicate record
3038 # if this user is also owner/admin, mark as duplicate record
3039 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3039 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3040 usr.duplicate_perm = True
3040 usr.duplicate_perm = True
3041 usr.permission = _usr.permission.permission_name
3041 usr.permission = _usr.permission.permission_name
3042 perm_rows.append(usr)
3042 perm_rows.append(usr)
3043
3043
3044 # filter the perm rows by 'default' first and then sort them by
3044 # filter the perm rows by 'default' first and then sort them by
3045 # admin,write,read,none permissions sorted again alphabetically in
3045 # admin,write,read,none permissions sorted again alphabetically in
3046 # each group
3046 # each group
3047 perm_rows = sorted(perm_rows, key=display_user_sort)
3047 perm_rows = sorted(perm_rows, key=display_user_sort)
3048
3048
3049 user_groups_rows = []
3049 user_groups_rows = []
3050 if expand_from_user_groups:
3050 if expand_from_user_groups:
3051 for ug in self.permission_user_groups(with_members=True):
3051 for ug in self.permission_user_groups(with_members=True):
3052 for user_data in ug.members:
3052 for user_data in ug.members:
3053 user_groups_rows.append(user_data)
3053 user_groups_rows.append(user_data)
3054
3054
3055 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3055 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3056
3056
3057 def permission_user_groups(self, with_members=False):
3057 def permission_user_groups(self, with_members=False):
3058 q = UserGroupRepoGroupToPerm.query()\
3058 q = UserGroupRepoGroupToPerm.query()\
3059 .filter(UserGroupRepoGroupToPerm.group == self)
3059 .filter(UserGroupRepoGroupToPerm.group == self)
3060 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3060 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3061 joinedload(UserGroupRepoGroupToPerm.users_group),
3061 joinedload(UserGroupRepoGroupToPerm.users_group),
3062 joinedload(UserGroupRepoGroupToPerm.permission),)
3062 joinedload(UserGroupRepoGroupToPerm.permission),)
3063
3063
3064 perm_rows = []
3064 perm_rows = []
3065 for _user_group in q.all():
3065 for _user_group in q.all():
3066 entry = AttributeDict(_user_group.users_group.get_dict())
3066 entry = AttributeDict(_user_group.users_group.get_dict())
3067 entry.permission = _user_group.permission.permission_name
3067 entry.permission = _user_group.permission.permission_name
3068 if with_members:
3068 if with_members:
3069 entry.members = [x.user.get_dict()
3069 entry.members = [x.user.get_dict()
3070 for x in _user_group.users_group.members]
3070 for x in _user_group.users_group.members]
3071 perm_rows.append(entry)
3071 perm_rows.append(entry)
3072
3072
3073 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3073 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3074 return perm_rows
3074 return perm_rows
3075
3075
3076 def get_api_data(self):
3076 def get_api_data(self):
3077 """
3077 """
3078 Common function for generating api data
3078 Common function for generating api data
3079
3079
3080 """
3080 """
3081 group = self
3081 group = self
3082 data = {
3082 data = {
3083 'group_id': group.group_id,
3083 'group_id': group.group_id,
3084 'group_name': group.group_name,
3084 'group_name': group.group_name,
3085 'group_description': group.description_safe,
3085 'group_description': group.description_safe,
3086 'parent_group': group.parent_group.group_name if group.parent_group else None,
3086 'parent_group': group.parent_group.group_name if group.parent_group else None,
3087 'repositories': [x.repo_name for x in group.repositories],
3087 'repositories': [x.repo_name for x in group.repositories],
3088 'owner': group.user.username,
3088 'owner': group.user.username,
3089 }
3089 }
3090 return data
3090 return data
3091
3091
3092 def get_dict(self):
3092 def get_dict(self):
3093 # Since we transformed `group_name` to a hybrid property, we need to
3093 # Since we transformed `group_name` to a hybrid property, we need to
3094 # keep compatibility with the code which uses `group_name` field.
3094 # keep compatibility with the code which uses `group_name` field.
3095 result = super(RepoGroup, self).get_dict()
3095 result = super(RepoGroup, self).get_dict()
3096 result['group_name'] = result.pop('_group_name', None)
3096 result['group_name'] = result.pop('_group_name', None)
3097 return result
3097 return result
3098
3098
3099
3099
3100 class Permission(Base, BaseModel):
3100 class Permission(Base, BaseModel):
3101 __tablename__ = 'permissions'
3101 __tablename__ = 'permissions'
3102 __table_args__ = (
3102 __table_args__ = (
3103 Index('p_perm_name_idx', 'permission_name'),
3103 Index('p_perm_name_idx', 'permission_name'),
3104 base_table_args,
3104 base_table_args,
3105 )
3105 )
3106
3106
3107 PERMS = [
3107 PERMS = [
3108 ('hg.admin', _('RhodeCode Super Administrator')),
3108 ('hg.admin', _('RhodeCode Super Administrator')),
3109
3109
3110 ('repository.none', _('Repository no access')),
3110 ('repository.none', _('Repository no access')),
3111 ('repository.read', _('Repository read access')),
3111 ('repository.read', _('Repository read access')),
3112 ('repository.write', _('Repository write access')),
3112 ('repository.write', _('Repository write access')),
3113 ('repository.admin', _('Repository admin access')),
3113 ('repository.admin', _('Repository admin access')),
3114
3114
3115 ('group.none', _('Repository group no access')),
3115 ('group.none', _('Repository group no access')),
3116 ('group.read', _('Repository group read access')),
3116 ('group.read', _('Repository group read access')),
3117 ('group.write', _('Repository group write access')),
3117 ('group.write', _('Repository group write access')),
3118 ('group.admin', _('Repository group admin access')),
3118 ('group.admin', _('Repository group admin access')),
3119
3119
3120 ('usergroup.none', _('User group no access')),
3120 ('usergroup.none', _('User group no access')),
3121 ('usergroup.read', _('User group read access')),
3121 ('usergroup.read', _('User group read access')),
3122 ('usergroup.write', _('User group write access')),
3122 ('usergroup.write', _('User group write access')),
3123 ('usergroup.admin', _('User group admin access')),
3123 ('usergroup.admin', _('User group admin access')),
3124
3124
3125 ('branch.none', _('Branch no permissions')),
3125 ('branch.none', _('Branch no permissions')),
3126 ('branch.merge', _('Branch access by web merge')),
3126 ('branch.merge', _('Branch access by web merge')),
3127 ('branch.push', _('Branch access by push')),
3127 ('branch.push', _('Branch access by push')),
3128 ('branch.push_force', _('Branch access by push with force')),
3128 ('branch.push_force', _('Branch access by push with force')),
3129
3129
3130 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3130 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3131 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3131 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3132
3132
3133 ('hg.usergroup.create.false', _('User Group creation disabled')),
3133 ('hg.usergroup.create.false', _('User Group creation disabled')),
3134 ('hg.usergroup.create.true', _('User Group creation enabled')),
3134 ('hg.usergroup.create.true', _('User Group creation enabled')),
3135
3135
3136 ('hg.create.none', _('Repository creation disabled')),
3136 ('hg.create.none', _('Repository creation disabled')),
3137 ('hg.create.repository', _('Repository creation enabled')),
3137 ('hg.create.repository', _('Repository creation enabled')),
3138 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3138 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3139 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3139 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3140
3140
3141 ('hg.fork.none', _('Repository forking disabled')),
3141 ('hg.fork.none', _('Repository forking disabled')),
3142 ('hg.fork.repository', _('Repository forking enabled')),
3142 ('hg.fork.repository', _('Repository forking enabled')),
3143
3143
3144 ('hg.register.none', _('Registration disabled')),
3144 ('hg.register.none', _('Registration disabled')),
3145 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3145 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3146 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3146 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3147
3147
3148 ('hg.password_reset.enabled', _('Password reset enabled')),
3148 ('hg.password_reset.enabled', _('Password reset enabled')),
3149 ('hg.password_reset.hidden', _('Password reset hidden')),
3149 ('hg.password_reset.hidden', _('Password reset hidden')),
3150 ('hg.password_reset.disabled', _('Password reset disabled')),
3150 ('hg.password_reset.disabled', _('Password reset disabled')),
3151
3151
3152 ('hg.extern_activate.manual', _('Manual activation of external account')),
3152 ('hg.extern_activate.manual', _('Manual activation of external account')),
3153 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3153 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3154
3154
3155 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3155 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3156 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3156 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3157 ]
3157 ]
3158
3158
3159 # definition of system default permissions for DEFAULT user, created on
3159 # definition of system default permissions for DEFAULT user, created on
3160 # system setup
3160 # system setup
3161 DEFAULT_USER_PERMISSIONS = [
3161 DEFAULT_USER_PERMISSIONS = [
3162 # object perms
3162 # object perms
3163 'repository.read',
3163 'repository.read',
3164 'group.read',
3164 'group.read',
3165 'usergroup.read',
3165 'usergroup.read',
3166 # branch, for backward compat we need same value as before so forced pushed
3166 # branch, for backward compat we need same value as before so forced pushed
3167 'branch.push_force',
3167 'branch.push_force',
3168 # global
3168 # global
3169 'hg.create.repository',
3169 'hg.create.repository',
3170 'hg.repogroup.create.false',
3170 'hg.repogroup.create.false',
3171 'hg.usergroup.create.false',
3171 'hg.usergroup.create.false',
3172 'hg.create.write_on_repogroup.true',
3172 'hg.create.write_on_repogroup.true',
3173 'hg.fork.repository',
3173 'hg.fork.repository',
3174 'hg.register.manual_activate',
3174 'hg.register.manual_activate',
3175 'hg.password_reset.enabled',
3175 'hg.password_reset.enabled',
3176 'hg.extern_activate.auto',
3176 'hg.extern_activate.auto',
3177 'hg.inherit_default_perms.true',
3177 'hg.inherit_default_perms.true',
3178 ]
3178 ]
3179
3179
3180 # defines which permissions are more important higher the more important
3180 # defines which permissions are more important higher the more important
3181 # Weight defines which permissions are more important.
3181 # Weight defines which permissions are more important.
3182 # The higher number the more important.
3182 # The higher number the more important.
3183 PERM_WEIGHTS = {
3183 PERM_WEIGHTS = {
3184 'repository.none': 0,
3184 'repository.none': 0,
3185 'repository.read': 1,
3185 'repository.read': 1,
3186 'repository.write': 3,
3186 'repository.write': 3,
3187 'repository.admin': 4,
3187 'repository.admin': 4,
3188
3188
3189 'group.none': 0,
3189 'group.none': 0,
3190 'group.read': 1,
3190 'group.read': 1,
3191 'group.write': 3,
3191 'group.write': 3,
3192 'group.admin': 4,
3192 'group.admin': 4,
3193
3193
3194 'usergroup.none': 0,
3194 'usergroup.none': 0,
3195 'usergroup.read': 1,
3195 'usergroup.read': 1,
3196 'usergroup.write': 3,
3196 'usergroup.write': 3,
3197 'usergroup.admin': 4,
3197 'usergroup.admin': 4,
3198
3198
3199 'branch.none': 0,
3199 'branch.none': 0,
3200 'branch.merge': 1,
3200 'branch.merge': 1,
3201 'branch.push': 3,
3201 'branch.push': 3,
3202 'branch.push_force': 4,
3202 'branch.push_force': 4,
3203
3203
3204 'hg.repogroup.create.false': 0,
3204 'hg.repogroup.create.false': 0,
3205 'hg.repogroup.create.true': 1,
3205 'hg.repogroup.create.true': 1,
3206
3206
3207 'hg.usergroup.create.false': 0,
3207 'hg.usergroup.create.false': 0,
3208 'hg.usergroup.create.true': 1,
3208 'hg.usergroup.create.true': 1,
3209
3209
3210 'hg.fork.none': 0,
3210 'hg.fork.none': 0,
3211 'hg.fork.repository': 1,
3211 'hg.fork.repository': 1,
3212 'hg.create.none': 0,
3212 'hg.create.none': 0,
3213 'hg.create.repository': 1
3213 'hg.create.repository': 1
3214 }
3214 }
3215
3215
3216 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3216 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3217 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3217 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3218 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3218 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3219
3219
3220 def __unicode__(self):
3220 def __unicode__(self):
3221 return u"<%s('%s:%s')>" % (
3221 return u"<%s('%s:%s')>" % (
3222 self.__class__.__name__, self.permission_id, self.permission_name
3222 self.__class__.__name__, self.permission_id, self.permission_name
3223 )
3223 )
3224
3224
3225 @classmethod
3225 @classmethod
3226 def get_by_key(cls, key):
3226 def get_by_key(cls, key):
3227 return cls.query().filter(cls.permission_name == key).scalar()
3227 return cls.query().filter(cls.permission_name == key).scalar()
3228
3228
3229 @classmethod
3229 @classmethod
3230 def get_default_repo_perms(cls, user_id, repo_id=None):
3230 def get_default_repo_perms(cls, user_id, repo_id=None):
3231 q = Session().query(UserRepoToPerm, Repository, Permission)\
3231 q = Session().query(UserRepoToPerm, Repository, Permission)\
3232 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3232 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3233 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3233 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3234 .filter(UserRepoToPerm.user_id == user_id)
3234 .filter(UserRepoToPerm.user_id == user_id)
3235 if repo_id:
3235 if repo_id:
3236 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3236 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3237 return q.all()
3237 return q.all()
3238
3238
3239 @classmethod
3239 @classmethod
3240 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3240 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3241 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3241 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3242 .join(
3242 .join(
3243 Permission,
3243 Permission,
3244 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3244 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3245 .join(
3245 .join(
3246 UserRepoToPerm,
3246 UserRepoToPerm,
3247 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3247 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3248 .filter(UserRepoToPerm.user_id == user_id)
3248 .filter(UserRepoToPerm.user_id == user_id)
3249
3249
3250 if repo_id:
3250 if repo_id:
3251 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3251 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3252 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3252 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3253
3253
3254 @classmethod
3254 @classmethod
3255 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3255 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3256 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3256 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3257 .join(
3257 .join(
3258 Permission,
3258 Permission,
3259 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3259 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3260 .join(
3260 .join(
3261 Repository,
3261 Repository,
3262 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3262 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3263 .join(
3263 .join(
3264 UserGroup,
3264 UserGroup,
3265 UserGroupRepoToPerm.users_group_id ==
3265 UserGroupRepoToPerm.users_group_id ==
3266 UserGroup.users_group_id)\
3266 UserGroup.users_group_id)\
3267 .join(
3267 .join(
3268 UserGroupMember,
3268 UserGroupMember,
3269 UserGroupRepoToPerm.users_group_id ==
3269 UserGroupRepoToPerm.users_group_id ==
3270 UserGroupMember.users_group_id)\
3270 UserGroupMember.users_group_id)\
3271 .filter(
3271 .filter(
3272 UserGroupMember.user_id == user_id,
3272 UserGroupMember.user_id == user_id,
3273 UserGroup.users_group_active == true())
3273 UserGroup.users_group_active == true())
3274 if repo_id:
3274 if repo_id:
3275 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3275 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3276 return q.all()
3276 return q.all()
3277
3277
3278 @classmethod
3278 @classmethod
3279 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3279 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3280 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3280 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3281 .join(
3281 .join(
3282 Permission,
3282 Permission,
3283 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3283 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3284 .join(
3284 .join(
3285 UserGroupRepoToPerm,
3285 UserGroupRepoToPerm,
3286 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3286 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3287 .join(
3287 .join(
3288 UserGroup,
3288 UserGroup,
3289 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3289 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3290 .join(
3290 .join(
3291 UserGroupMember,
3291 UserGroupMember,
3292 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3292 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3293 .filter(
3293 .filter(
3294 UserGroupMember.user_id == user_id,
3294 UserGroupMember.user_id == user_id,
3295 UserGroup.users_group_active == true())
3295 UserGroup.users_group_active == true())
3296
3296
3297 if repo_id:
3297 if repo_id:
3298 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3298 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3299 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3299 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3300
3300
3301 @classmethod
3301 @classmethod
3302 def get_default_group_perms(cls, user_id, repo_group_id=None):
3302 def get_default_group_perms(cls, user_id, repo_group_id=None):
3303 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3303 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3304 .join(
3304 .join(
3305 Permission,
3305 Permission,
3306 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3306 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3307 .join(
3307 .join(
3308 RepoGroup,
3308 RepoGroup,
3309 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3309 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3310 .filter(UserRepoGroupToPerm.user_id == user_id)
3310 .filter(UserRepoGroupToPerm.user_id == user_id)
3311 if repo_group_id:
3311 if repo_group_id:
3312 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3312 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3313 return q.all()
3313 return q.all()
3314
3314
3315 @classmethod
3315 @classmethod
3316 def get_default_group_perms_from_user_group(
3316 def get_default_group_perms_from_user_group(
3317 cls, user_id, repo_group_id=None):
3317 cls, user_id, repo_group_id=None):
3318 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3318 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3319 .join(
3319 .join(
3320 Permission,
3320 Permission,
3321 UserGroupRepoGroupToPerm.permission_id ==
3321 UserGroupRepoGroupToPerm.permission_id ==
3322 Permission.permission_id)\
3322 Permission.permission_id)\
3323 .join(
3323 .join(
3324 RepoGroup,
3324 RepoGroup,
3325 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3325 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3326 .join(
3326 .join(
3327 UserGroup,
3327 UserGroup,
3328 UserGroupRepoGroupToPerm.users_group_id ==
3328 UserGroupRepoGroupToPerm.users_group_id ==
3329 UserGroup.users_group_id)\
3329 UserGroup.users_group_id)\
3330 .join(
3330 .join(
3331 UserGroupMember,
3331 UserGroupMember,
3332 UserGroupRepoGroupToPerm.users_group_id ==
3332 UserGroupRepoGroupToPerm.users_group_id ==
3333 UserGroupMember.users_group_id)\
3333 UserGroupMember.users_group_id)\
3334 .filter(
3334 .filter(
3335 UserGroupMember.user_id == user_id,
3335 UserGroupMember.user_id == user_id,
3336 UserGroup.users_group_active == true())
3336 UserGroup.users_group_active == true())
3337 if repo_group_id:
3337 if repo_group_id:
3338 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3338 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3339 return q.all()
3339 return q.all()
3340
3340
3341 @classmethod
3341 @classmethod
3342 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3342 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3343 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3343 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3344 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3344 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3345 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3345 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3346 .filter(UserUserGroupToPerm.user_id == user_id)
3346 .filter(UserUserGroupToPerm.user_id == user_id)
3347 if user_group_id:
3347 if user_group_id:
3348 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3348 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3349 return q.all()
3349 return q.all()
3350
3350
3351 @classmethod
3351 @classmethod
3352 def get_default_user_group_perms_from_user_group(
3352 def get_default_user_group_perms_from_user_group(
3353 cls, user_id, user_group_id=None):
3353 cls, user_id, user_group_id=None):
3354 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3354 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3355 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3355 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3356 .join(
3356 .join(
3357 Permission,
3357 Permission,
3358 UserGroupUserGroupToPerm.permission_id ==
3358 UserGroupUserGroupToPerm.permission_id ==
3359 Permission.permission_id)\
3359 Permission.permission_id)\
3360 .join(
3360 .join(
3361 TargetUserGroup,
3361 TargetUserGroup,
3362 UserGroupUserGroupToPerm.target_user_group_id ==
3362 UserGroupUserGroupToPerm.target_user_group_id ==
3363 TargetUserGroup.users_group_id)\
3363 TargetUserGroup.users_group_id)\
3364 .join(
3364 .join(
3365 UserGroup,
3365 UserGroup,
3366 UserGroupUserGroupToPerm.user_group_id ==
3366 UserGroupUserGroupToPerm.user_group_id ==
3367 UserGroup.users_group_id)\
3367 UserGroup.users_group_id)\
3368 .join(
3368 .join(
3369 UserGroupMember,
3369 UserGroupMember,
3370 UserGroupUserGroupToPerm.user_group_id ==
3370 UserGroupUserGroupToPerm.user_group_id ==
3371 UserGroupMember.users_group_id)\
3371 UserGroupMember.users_group_id)\
3372 .filter(
3372 .filter(
3373 UserGroupMember.user_id == user_id,
3373 UserGroupMember.user_id == user_id,
3374 UserGroup.users_group_active == true())
3374 UserGroup.users_group_active == true())
3375 if user_group_id:
3375 if user_group_id:
3376 q = q.filter(
3376 q = q.filter(
3377 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3377 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3378
3378
3379 return q.all()
3379 return q.all()
3380
3380
3381
3381
3382 class UserRepoToPerm(Base, BaseModel):
3382 class UserRepoToPerm(Base, BaseModel):
3383 __tablename__ = 'repo_to_perm'
3383 __tablename__ = 'repo_to_perm'
3384 __table_args__ = (
3384 __table_args__ = (
3385 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3385 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3386 base_table_args
3386 base_table_args
3387 )
3387 )
3388
3388
3389 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3389 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3390 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3390 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3391 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3391 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3392 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3392 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3393
3393
3394 user = relationship('User')
3394 user = relationship('User')
3395 repository = relationship('Repository')
3395 repository = relationship('Repository')
3396 permission = relationship('Permission')
3396 permission = relationship('Permission')
3397
3397
3398 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3398 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3399
3399
3400 @classmethod
3400 @classmethod
3401 def create(cls, user, repository, permission):
3401 def create(cls, user, repository, permission):
3402 n = cls()
3402 n = cls()
3403 n.user = user
3403 n.user = user
3404 n.repository = repository
3404 n.repository = repository
3405 n.permission = permission
3405 n.permission = permission
3406 Session().add(n)
3406 Session().add(n)
3407 return n
3407 return n
3408
3408
3409 def __unicode__(self):
3409 def __unicode__(self):
3410 return u'<%s => %s >' % (self.user, self.repository)
3410 return u'<%s => %s >' % (self.user, self.repository)
3411
3411
3412
3412
3413 class UserUserGroupToPerm(Base, BaseModel):
3413 class UserUserGroupToPerm(Base, BaseModel):
3414 __tablename__ = 'user_user_group_to_perm'
3414 __tablename__ = 'user_user_group_to_perm'
3415 __table_args__ = (
3415 __table_args__ = (
3416 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3416 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3417 base_table_args
3417 base_table_args
3418 )
3418 )
3419
3419
3420 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3420 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3421 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3421 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3422 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3422 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3423 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3423 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3424
3424
3425 user = relationship('User')
3425 user = relationship('User')
3426 user_group = relationship('UserGroup')
3426 user_group = relationship('UserGroup')
3427 permission = relationship('Permission')
3427 permission = relationship('Permission')
3428
3428
3429 @classmethod
3429 @classmethod
3430 def create(cls, user, user_group, permission):
3430 def create(cls, user, user_group, permission):
3431 n = cls()
3431 n = cls()
3432 n.user = user
3432 n.user = user
3433 n.user_group = user_group
3433 n.user_group = user_group
3434 n.permission = permission
3434 n.permission = permission
3435 Session().add(n)
3435 Session().add(n)
3436 return n
3436 return n
3437
3437
3438 def __unicode__(self):
3438 def __unicode__(self):
3439 return u'<%s => %s >' % (self.user, self.user_group)
3439 return u'<%s => %s >' % (self.user, self.user_group)
3440
3440
3441
3441
3442 class UserToPerm(Base, BaseModel):
3442 class UserToPerm(Base, BaseModel):
3443 __tablename__ = 'user_to_perm'
3443 __tablename__ = 'user_to_perm'
3444 __table_args__ = (
3444 __table_args__ = (
3445 UniqueConstraint('user_id', 'permission_id'),
3445 UniqueConstraint('user_id', 'permission_id'),
3446 base_table_args
3446 base_table_args
3447 )
3447 )
3448
3448
3449 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3449 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3450 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3450 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3451 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3451 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3452
3452
3453 user = relationship('User')
3453 user = relationship('User')
3454 permission = relationship('Permission', lazy='joined')
3454 permission = relationship('Permission', lazy='joined')
3455
3455
3456 def __unicode__(self):
3456 def __unicode__(self):
3457 return u'<%s => %s >' % (self.user, self.permission)
3457 return u'<%s => %s >' % (self.user, self.permission)
3458
3458
3459
3459
3460 class UserGroupRepoToPerm(Base, BaseModel):
3460 class UserGroupRepoToPerm(Base, BaseModel):
3461 __tablename__ = 'users_group_repo_to_perm'
3461 __tablename__ = 'users_group_repo_to_perm'
3462 __table_args__ = (
3462 __table_args__ = (
3463 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3463 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3464 base_table_args
3464 base_table_args
3465 )
3465 )
3466
3466
3467 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3467 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3468 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3468 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3469 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3469 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3470 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3470 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3471
3471
3472 users_group = relationship('UserGroup')
3472 users_group = relationship('UserGroup')
3473 permission = relationship('Permission')
3473 permission = relationship('Permission')
3474 repository = relationship('Repository')
3474 repository = relationship('Repository')
3475 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3475 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3476
3476
3477 @classmethod
3477 @classmethod
3478 def create(cls, users_group, repository, permission):
3478 def create(cls, users_group, repository, permission):
3479 n = cls()
3479 n = cls()
3480 n.users_group = users_group
3480 n.users_group = users_group
3481 n.repository = repository
3481 n.repository = repository
3482 n.permission = permission
3482 n.permission = permission
3483 Session().add(n)
3483 Session().add(n)
3484 return n
3484 return n
3485
3485
3486 def __unicode__(self):
3486 def __unicode__(self):
3487 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3487 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3488
3488
3489
3489
3490 class UserGroupUserGroupToPerm(Base, BaseModel):
3490 class UserGroupUserGroupToPerm(Base, BaseModel):
3491 __tablename__ = 'user_group_user_group_to_perm'
3491 __tablename__ = 'user_group_user_group_to_perm'
3492 __table_args__ = (
3492 __table_args__ = (
3493 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3493 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3494 CheckConstraint('target_user_group_id != user_group_id'),
3494 CheckConstraint('target_user_group_id != user_group_id'),
3495 base_table_args
3495 base_table_args
3496 )
3496 )
3497
3497
3498 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3498 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3499 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3499 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3500 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3500 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3501 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3501 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3502
3502
3503 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3503 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3504 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3504 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3505 permission = relationship('Permission')
3505 permission = relationship('Permission')
3506
3506
3507 @classmethod
3507 @classmethod
3508 def create(cls, target_user_group, user_group, permission):
3508 def create(cls, target_user_group, user_group, permission):
3509 n = cls()
3509 n = cls()
3510 n.target_user_group = target_user_group
3510 n.target_user_group = target_user_group
3511 n.user_group = user_group
3511 n.user_group = user_group
3512 n.permission = permission
3512 n.permission = permission
3513 Session().add(n)
3513 Session().add(n)
3514 return n
3514 return n
3515
3515
3516 def __unicode__(self):
3516 def __unicode__(self):
3517 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3517 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3518
3518
3519
3519
3520 class UserGroupToPerm(Base, BaseModel):
3520 class UserGroupToPerm(Base, BaseModel):
3521 __tablename__ = 'users_group_to_perm'
3521 __tablename__ = 'users_group_to_perm'
3522 __table_args__ = (
3522 __table_args__ = (
3523 UniqueConstraint('users_group_id', 'permission_id',),
3523 UniqueConstraint('users_group_id', 'permission_id',),
3524 base_table_args
3524 base_table_args
3525 )
3525 )
3526
3526
3527 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3527 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3528 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3528 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3529 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3529 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3530
3530
3531 users_group = relationship('UserGroup')
3531 users_group = relationship('UserGroup')
3532 permission = relationship('Permission')
3532 permission = relationship('Permission')
3533
3533
3534
3534
3535 class UserRepoGroupToPerm(Base, BaseModel):
3535 class UserRepoGroupToPerm(Base, BaseModel):
3536 __tablename__ = 'user_repo_group_to_perm'
3536 __tablename__ = 'user_repo_group_to_perm'
3537 __table_args__ = (
3537 __table_args__ = (
3538 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3538 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3539 base_table_args
3539 base_table_args
3540 )
3540 )
3541
3541
3542 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3542 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3543 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3543 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3544 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3544 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3545 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3545 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3546
3546
3547 user = relationship('User')
3547 user = relationship('User')
3548 group = relationship('RepoGroup')
3548 group = relationship('RepoGroup')
3549 permission = relationship('Permission')
3549 permission = relationship('Permission')
3550
3550
3551 @classmethod
3551 @classmethod
3552 def create(cls, user, repository_group, permission):
3552 def create(cls, user, repository_group, permission):
3553 n = cls()
3553 n = cls()
3554 n.user = user
3554 n.user = user
3555 n.group = repository_group
3555 n.group = repository_group
3556 n.permission = permission
3556 n.permission = permission
3557 Session().add(n)
3557 Session().add(n)
3558 return n
3558 return n
3559
3559
3560
3560
3561 class UserGroupRepoGroupToPerm(Base, BaseModel):
3561 class UserGroupRepoGroupToPerm(Base, BaseModel):
3562 __tablename__ = 'users_group_repo_group_to_perm'
3562 __tablename__ = 'users_group_repo_group_to_perm'
3563 __table_args__ = (
3563 __table_args__ = (
3564 UniqueConstraint('users_group_id', 'group_id'),
3564 UniqueConstraint('users_group_id', 'group_id'),
3565 base_table_args
3565 base_table_args
3566 )
3566 )
3567
3567
3568 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3568 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3569 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3569 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3570 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3570 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3571 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3571 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3572
3572
3573 users_group = relationship('UserGroup')
3573 users_group = relationship('UserGroup')
3574 permission = relationship('Permission')
3574 permission = relationship('Permission')
3575 group = relationship('RepoGroup')
3575 group = relationship('RepoGroup')
3576
3576
3577 @classmethod
3577 @classmethod
3578 def create(cls, user_group, repository_group, permission):
3578 def create(cls, user_group, repository_group, permission):
3579 n = cls()
3579 n = cls()
3580 n.users_group = user_group
3580 n.users_group = user_group
3581 n.group = repository_group
3581 n.group = repository_group
3582 n.permission = permission
3582 n.permission = permission
3583 Session().add(n)
3583 Session().add(n)
3584 return n
3584 return n
3585
3585
3586 def __unicode__(self):
3586 def __unicode__(self):
3587 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3587 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3588
3588
3589
3589
3590 class Statistics(Base, BaseModel):
3590 class Statistics(Base, BaseModel):
3591 __tablename__ = 'statistics'
3591 __tablename__ = 'statistics'
3592 __table_args__ = (
3592 __table_args__ = (
3593 base_table_args
3593 base_table_args
3594 )
3594 )
3595
3595
3596 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3596 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3597 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3597 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3598 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3598 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3599 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3599 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3600 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3600 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3601 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3601 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3602
3602
3603 repository = relationship('Repository', single_parent=True)
3603 repository = relationship('Repository', single_parent=True)
3604
3604
3605
3605
3606 class UserFollowing(Base, BaseModel):
3606 class UserFollowing(Base, BaseModel):
3607 __tablename__ = 'user_followings'
3607 __tablename__ = 'user_followings'
3608 __table_args__ = (
3608 __table_args__ = (
3609 UniqueConstraint('user_id', 'follows_repository_id'),
3609 UniqueConstraint('user_id', 'follows_repository_id'),
3610 UniqueConstraint('user_id', 'follows_user_id'),
3610 UniqueConstraint('user_id', 'follows_user_id'),
3611 base_table_args
3611 base_table_args
3612 )
3612 )
3613
3613
3614 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3614 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3615 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3615 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3616 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3616 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3617 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3617 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3618 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3618 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3619
3619
3620 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3620 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3621
3621
3622 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3622 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3623 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3623 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3624
3624
3625 @classmethod
3625 @classmethod
3626 def get_repo_followers(cls, repo_id):
3626 def get_repo_followers(cls, repo_id):
3627 return cls.query().filter(cls.follows_repo_id == repo_id)
3627 return cls.query().filter(cls.follows_repo_id == repo_id)
3628
3628
3629
3629
3630 class CacheKey(Base, BaseModel):
3630 class CacheKey(Base, BaseModel):
3631 __tablename__ = 'cache_invalidation'
3631 __tablename__ = 'cache_invalidation'
3632 __table_args__ = (
3632 __table_args__ = (
3633 UniqueConstraint('cache_key'),
3633 UniqueConstraint('cache_key'),
3634 Index('key_idx', 'cache_key'),
3634 Index('key_idx', 'cache_key'),
3635 base_table_args,
3635 base_table_args,
3636 )
3636 )
3637
3637
3638 CACHE_TYPE_FEED = 'FEED'
3638 CACHE_TYPE_FEED = 'FEED'
3639
3639
3640 # namespaces used to register process/thread aware caches
3640 # namespaces used to register process/thread aware caches
3641 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3641 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3642 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3642 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3643
3643
3644 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3644 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3645 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3645 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3646 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3646 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3647 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3647 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3648 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3648 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3649
3649
3650 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3650 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3651 self.cache_key = cache_key
3651 self.cache_key = cache_key
3652 self.cache_args = cache_args
3652 self.cache_args = cache_args
3653 self.cache_active = False
3653 self.cache_active = False
3654 # first key should be same for all entries, since all workers should share it
3654 # first key should be same for all entries, since all workers should share it
3655 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3655 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3656
3656
3657 def __unicode__(self):
3657 def __unicode__(self):
3658 return u"<%s('%s:%s[%s]')>" % (
3658 return u"<%s('%s:%s[%s]')>" % (
3659 self.__class__.__name__,
3659 self.__class__.__name__,
3660 self.cache_id, self.cache_key, self.cache_active)
3660 self.cache_id, self.cache_key, self.cache_active)
3661
3661
3662 def _cache_key_partition(self):
3662 def _cache_key_partition(self):
3663 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3663 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3664 return prefix, repo_name, suffix
3664 return prefix, repo_name, suffix
3665
3665
3666 def get_prefix(self):
3666 def get_prefix(self):
3667 """
3667 """
3668 Try to extract prefix from existing cache key. The key could consist
3668 Try to extract prefix from existing cache key. The key could consist
3669 of prefix, repo_name, suffix
3669 of prefix, repo_name, suffix
3670 """
3670 """
3671 # this returns prefix, repo_name, suffix
3671 # this returns prefix, repo_name, suffix
3672 return self._cache_key_partition()[0]
3672 return self._cache_key_partition()[0]
3673
3673
3674 def get_suffix(self):
3674 def get_suffix(self):
3675 """
3675 """
3676 get suffix that might have been used in _get_cache_key to
3676 get suffix that might have been used in _get_cache_key to
3677 generate self.cache_key. Only used for informational purposes
3677 generate self.cache_key. Only used for informational purposes
3678 in repo_edit.mako.
3678 in repo_edit.mako.
3679 """
3679 """
3680 # prefix, repo_name, suffix
3680 # prefix, repo_name, suffix
3681 return self._cache_key_partition()[2]
3681 return self._cache_key_partition()[2]
3682
3682
3683 @classmethod
3683 @classmethod
3684 def generate_new_state_uid(cls, based_on=None):
3684 def generate_new_state_uid(cls, based_on=None):
3685 if based_on:
3685 if based_on:
3686 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3686 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3687 else:
3687 else:
3688 return str(uuid.uuid4())
3688 return str(uuid.uuid4())
3689
3689
3690 @classmethod
3690 @classmethod
3691 def delete_all_cache(cls):
3691 def delete_all_cache(cls):
3692 """
3692 """
3693 Delete all cache keys from database.
3693 Delete all cache keys from database.
3694 Should only be run when all instances are down and all entries
3694 Should only be run when all instances are down and all entries
3695 thus stale.
3695 thus stale.
3696 """
3696 """
3697 cls.query().delete()
3697 cls.query().delete()
3698 Session().commit()
3698 Session().commit()
3699
3699
3700 @classmethod
3700 @classmethod
3701 def set_invalidate(cls, cache_uid, delete=False):
3701 def set_invalidate(cls, cache_uid, delete=False):
3702 """
3702 """
3703 Mark all caches of a repo as invalid in the database.
3703 Mark all caches of a repo as invalid in the database.
3704 """
3704 """
3705
3705
3706 try:
3706 try:
3707 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3707 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3708 if delete:
3708 if delete:
3709 qry.delete()
3709 qry.delete()
3710 log.debug('cache objects deleted for cache args %s',
3710 log.debug('cache objects deleted for cache args %s',
3711 safe_str(cache_uid))
3711 safe_str(cache_uid))
3712 else:
3712 else:
3713 qry.update({"cache_active": False,
3713 qry.update({"cache_active": False,
3714 "cache_state_uid": cls.generate_new_state_uid()})
3714 "cache_state_uid": cls.generate_new_state_uid()})
3715 log.debug('cache objects marked as invalid for cache args %s',
3715 log.debug('cache objects marked as invalid for cache args %s',
3716 safe_str(cache_uid))
3716 safe_str(cache_uid))
3717
3717
3718 Session().commit()
3718 Session().commit()
3719 except Exception:
3719 except Exception:
3720 log.exception(
3720 log.exception(
3721 'Cache key invalidation failed for cache args %s',
3721 'Cache key invalidation failed for cache args %s',
3722 safe_str(cache_uid))
3722 safe_str(cache_uid))
3723 Session().rollback()
3723 Session().rollback()
3724
3724
3725 @classmethod
3725 @classmethod
3726 def get_active_cache(cls, cache_key):
3726 def get_active_cache(cls, cache_key):
3727 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3727 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3728 if inv_obj:
3728 if inv_obj:
3729 return inv_obj
3729 return inv_obj
3730 return None
3730 return None
3731
3731
3732 @classmethod
3732 @classmethod
3733 def get_namespace_map(cls, namespace):
3733 def get_namespace_map(cls, namespace):
3734 return {
3734 return {
3735 x.cache_key: x
3735 x.cache_key: x
3736 for x in cls.query().filter(cls.cache_args == namespace)}
3736 for x in cls.query().filter(cls.cache_args == namespace)}
3737
3737
3738
3738
3739 class ChangesetComment(Base, BaseModel):
3739 class ChangesetComment(Base, BaseModel):
3740 __tablename__ = 'changeset_comments'
3740 __tablename__ = 'changeset_comments'
3741 __table_args__ = (
3741 __table_args__ = (
3742 Index('cc_revision_idx', 'revision'),
3742 Index('cc_revision_idx', 'revision'),
3743 base_table_args,
3743 base_table_args,
3744 )
3744 )
3745
3745
3746 COMMENT_OUTDATED = u'comment_outdated'
3746 COMMENT_OUTDATED = u'comment_outdated'
3747 COMMENT_TYPE_NOTE = u'note'
3747 COMMENT_TYPE_NOTE = u'note'
3748 COMMENT_TYPE_TODO = u'todo'
3748 COMMENT_TYPE_TODO = u'todo'
3749 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3749 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3750
3750
3751 OP_IMMUTABLE = u'immutable'
3751 OP_IMMUTABLE = u'immutable'
3752 OP_CHANGEABLE = u'changeable'
3752 OP_CHANGEABLE = u'changeable'
3753
3753
3754 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3754 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3755 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3755 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3756 revision = Column('revision', String(40), nullable=True)
3756 revision = Column('revision', String(40), nullable=True)
3757 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3757 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3758 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3758 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3759 line_no = Column('line_no', Unicode(10), nullable=True)
3759 line_no = Column('line_no', Unicode(10), nullable=True)
3760 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3760 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3761 f_path = Column('f_path', Unicode(1000), nullable=True)
3761 f_path = Column('f_path', Unicode(1000), nullable=True)
3762 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3762 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3763 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3763 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3764 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3764 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3765 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3765 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3766 renderer = Column('renderer', Unicode(64), nullable=True)
3766 renderer = Column('renderer', Unicode(64), nullable=True)
3767 display_state = Column('display_state', Unicode(128), nullable=True)
3767 display_state = Column('display_state', Unicode(128), nullable=True)
3768 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3768 immutable_state = Column('immutable_state', Unicode(128), nullable=True, default=OP_CHANGEABLE)
3769
3769
3770 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3770 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3771 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3771 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3772
3772
3773 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3773 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3774 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3774 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3775
3775
3776 author = relationship('User', lazy='joined')
3776 author = relationship('User', lazy='joined')
3777 repo = relationship('Repository')
3777 repo = relationship('Repository')
3778 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
3778 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
3779 pull_request = relationship('PullRequest', lazy='joined')
3779 pull_request = relationship('PullRequest', lazy='joined')
3780 pull_request_version = relationship('PullRequestVersion')
3780 pull_request_version = relationship('PullRequestVersion')
3781 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='joined', order_by='ChangesetCommentHistory.version')
3781 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='joined', order_by='ChangesetCommentHistory.version')
3782
3782
3783 @classmethod
3783 @classmethod
3784 def get_users(cls, revision=None, pull_request_id=None):
3784 def get_users(cls, revision=None, pull_request_id=None):
3785 """
3785 """
3786 Returns user associated with this ChangesetComment. ie those
3786 Returns user associated with this ChangesetComment. ie those
3787 who actually commented
3787 who actually commented
3788
3788
3789 :param cls:
3789 :param cls:
3790 :param revision:
3790 :param revision:
3791 """
3791 """
3792 q = Session().query(User)\
3792 q = Session().query(User)\
3793 .join(ChangesetComment.author)
3793 .join(ChangesetComment.author)
3794 if revision:
3794 if revision:
3795 q = q.filter(cls.revision == revision)
3795 q = q.filter(cls.revision == revision)
3796 elif pull_request_id:
3796 elif pull_request_id:
3797 q = q.filter(cls.pull_request_id == pull_request_id)
3797 q = q.filter(cls.pull_request_id == pull_request_id)
3798 return q.all()
3798 return q.all()
3799
3799
3800 @classmethod
3800 @classmethod
3801 def get_index_from_version(cls, pr_version, versions):
3801 def get_index_from_version(cls, pr_version, versions):
3802 num_versions = [x.pull_request_version_id for x in versions]
3802 num_versions = [x.pull_request_version_id for x in versions]
3803 try:
3803 try:
3804 return num_versions.index(pr_version) + 1
3804 return num_versions.index(pr_version) + 1
3805 except (IndexError, ValueError):
3805 except (IndexError, ValueError):
3806 return
3806 return
3807
3807
3808 @property
3808 @property
3809 def outdated(self):
3809 def outdated(self):
3810 return self.display_state == self.COMMENT_OUTDATED
3810 return self.display_state == self.COMMENT_OUTDATED
3811
3811
3812 @property
3812 @property
3813 def outdated_js(self):
3813 def outdated_js(self):
3814 return json.dumps(self.display_state == self.COMMENT_OUTDATED)
3814 return json.dumps(self.display_state == self.COMMENT_OUTDATED)
3815
3815
3816 @property
3816 @property
3817 def immutable(self):
3817 def immutable(self):
3818 return self.immutable_state == self.OP_IMMUTABLE
3818 return self.immutable_state == self.OP_IMMUTABLE
3819
3819
3820 def outdated_at_version(self, version):
3820 def outdated_at_version(self, version):
3821 """
3821 """
3822 Checks if comment is outdated for given pull request version
3822 Checks if comment is outdated for given pull request version
3823 """
3823 """
3824 return self.outdated and self.pull_request_version_id != version
3824 def version_check():
3825 return self.pull_request_version_id and self.pull_request_version_id != version
3826
3827 if self.is_inline:
3828 return self.outdated and version_check()
3829 else:
3830 # general comments don't have .outdated set, also latest don't have a version
3831 return version_check()
3832
3833 def outdated_at_version_js(self, version):
3834 """
3835 Checks if comment is outdated for given pull request version
3836 """
3837 return json.dumps(self.outdated_at_version(version))
3825
3838
3826 def older_than_version(self, version):
3839 def older_than_version(self, version):
3827 """
3840 """
3828 Checks if comment is made from previous version than given
3841 Checks if comment is made from previous version than given
3829 """
3842 """
3830 if version is None:
3843 if version is None:
3831 return self.pull_request_version_id is not None
3844 return self.pull_request_version != version
3832
3845
3833 return self.pull_request_version_id < version
3846 return self.pull_request_version < version
3847
3848 def older_than_version_js(self, version):
3849 """
3850 Checks if comment is made from previous version than given
3851 """
3852 return json.dumps(self.older_than_version(version))
3834
3853
3835 @property
3854 @property
3836 def commit_id(self):
3855 def commit_id(self):
3837 """New style naming to stop using .revision"""
3856 """New style naming to stop using .revision"""
3838 return self.revision
3857 return self.revision
3839
3858
3840 @property
3859 @property
3841 def resolved(self):
3860 def resolved(self):
3842 return self.resolved_by[0] if self.resolved_by else None
3861 return self.resolved_by[0] if self.resolved_by else None
3843
3862
3844 @property
3863 @property
3845 def is_todo(self):
3864 def is_todo(self):
3846 return self.comment_type == self.COMMENT_TYPE_TODO
3865 return self.comment_type == self.COMMENT_TYPE_TODO
3847
3866
3848 @property
3867 @property
3849 def is_inline(self):
3868 def is_inline(self):
3850 return self.line_no and self.f_path
3869 return self.line_no and self.f_path
3851
3870
3852 @property
3871 @property
3853 def last_version(self):
3872 def last_version(self):
3854 version = 0
3873 version = 0
3855 if self.history:
3874 if self.history:
3856 version = self.history[-1].version
3875 version = self.history[-1].version
3857 return version
3876 return version
3858
3877
3859 def get_index_version(self, versions):
3878 def get_index_version(self, versions):
3860 return self.get_index_from_version(
3879 return self.get_index_from_version(
3861 self.pull_request_version_id, versions)
3880 self.pull_request_version_id, versions)
3862
3881
3863 def __repr__(self):
3882 def __repr__(self):
3864 if self.comment_id:
3883 if self.comment_id:
3865 return '<DB:Comment #%s>' % self.comment_id
3884 return '<DB:Comment #%s>' % self.comment_id
3866 else:
3885 else:
3867 return '<DB:Comment at %#x>' % id(self)
3886 return '<DB:Comment at %#x>' % id(self)
3868
3887
3869 def get_api_data(self):
3888 def get_api_data(self):
3870 comment = self
3889 comment = self
3871
3890
3872 data = {
3891 data = {
3873 'comment_id': comment.comment_id,
3892 'comment_id': comment.comment_id,
3874 'comment_type': comment.comment_type,
3893 'comment_type': comment.comment_type,
3875 'comment_text': comment.text,
3894 'comment_text': comment.text,
3876 'comment_status': comment.status_change,
3895 'comment_status': comment.status_change,
3877 'comment_f_path': comment.f_path,
3896 'comment_f_path': comment.f_path,
3878 'comment_lineno': comment.line_no,
3897 'comment_lineno': comment.line_no,
3879 'comment_author': comment.author,
3898 'comment_author': comment.author,
3880 'comment_created_on': comment.created_on,
3899 'comment_created_on': comment.created_on,
3881 'comment_resolved_by': self.resolved,
3900 'comment_resolved_by': self.resolved,
3882 'comment_commit_id': comment.revision,
3901 'comment_commit_id': comment.revision,
3883 'comment_pull_request_id': comment.pull_request_id,
3902 'comment_pull_request_id': comment.pull_request_id,
3884 'comment_last_version': self.last_version
3903 'comment_last_version': self.last_version
3885 }
3904 }
3886 return data
3905 return data
3887
3906
3888 def __json__(self):
3907 def __json__(self):
3889 data = dict()
3908 data = dict()
3890 data.update(self.get_api_data())
3909 data.update(self.get_api_data())
3891 return data
3910 return data
3892
3911
3893
3912
3894 class ChangesetCommentHistory(Base, BaseModel):
3913 class ChangesetCommentHistory(Base, BaseModel):
3895 __tablename__ = 'changeset_comments_history'
3914 __tablename__ = 'changeset_comments_history'
3896 __table_args__ = (
3915 __table_args__ = (
3897 Index('cch_comment_id_idx', 'comment_id'),
3916 Index('cch_comment_id_idx', 'comment_id'),
3898 base_table_args,
3917 base_table_args,
3899 )
3918 )
3900
3919
3901 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3920 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3902 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3921 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3903 version = Column("version", Integer(), nullable=False, default=0)
3922 version = Column("version", Integer(), nullable=False, default=0)
3904 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3923 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3905 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3924 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3906 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3925 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3907 deleted = Column('deleted', Boolean(), default=False)
3926 deleted = Column('deleted', Boolean(), default=False)
3908
3927
3909 author = relationship('User', lazy='joined')
3928 author = relationship('User', lazy='joined')
3910 comment = relationship('ChangesetComment', cascade="all, delete")
3929 comment = relationship('ChangesetComment', cascade="all, delete")
3911
3930
3912 @classmethod
3931 @classmethod
3913 def get_version(cls, comment_id):
3932 def get_version(cls, comment_id):
3914 q = Session().query(ChangesetCommentHistory).filter(
3933 q = Session().query(ChangesetCommentHistory).filter(
3915 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
3934 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
3916 if q.count() == 0:
3935 if q.count() == 0:
3917 return 1
3936 return 1
3918 elif q.count() >= q[0].version:
3937 elif q.count() >= q[0].version:
3919 return q.count() + 1
3938 return q.count() + 1
3920 else:
3939 else:
3921 return q[0].version + 1
3940 return q[0].version + 1
3922
3941
3923
3942
3924 class ChangesetStatus(Base, BaseModel):
3943 class ChangesetStatus(Base, BaseModel):
3925 __tablename__ = 'changeset_statuses'
3944 __tablename__ = 'changeset_statuses'
3926 __table_args__ = (
3945 __table_args__ = (
3927 Index('cs_revision_idx', 'revision'),
3946 Index('cs_revision_idx', 'revision'),
3928 Index('cs_version_idx', 'version'),
3947 Index('cs_version_idx', 'version'),
3929 UniqueConstraint('repo_id', 'revision', 'version'),
3948 UniqueConstraint('repo_id', 'revision', 'version'),
3930 base_table_args
3949 base_table_args
3931 )
3950 )
3932
3951
3933 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3952 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3934 STATUS_APPROVED = 'approved'
3953 STATUS_APPROVED = 'approved'
3935 STATUS_REJECTED = 'rejected'
3954 STATUS_REJECTED = 'rejected'
3936 STATUS_UNDER_REVIEW = 'under_review'
3955 STATUS_UNDER_REVIEW = 'under_review'
3937
3956
3938 STATUSES = [
3957 STATUSES = [
3939 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3958 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3940 (STATUS_APPROVED, _("Approved")),
3959 (STATUS_APPROVED, _("Approved")),
3941 (STATUS_REJECTED, _("Rejected")),
3960 (STATUS_REJECTED, _("Rejected")),
3942 (STATUS_UNDER_REVIEW, _("Under Review")),
3961 (STATUS_UNDER_REVIEW, _("Under Review")),
3943 ]
3962 ]
3944
3963
3945 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3964 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3946 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3965 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3947 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3966 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3948 revision = Column('revision', String(40), nullable=False)
3967 revision = Column('revision', String(40), nullable=False)
3949 status = Column('status', String(128), nullable=False, default=DEFAULT)
3968 status = Column('status', String(128), nullable=False, default=DEFAULT)
3950 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3969 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3951 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3970 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3952 version = Column('version', Integer(), nullable=False, default=0)
3971 version = Column('version', Integer(), nullable=False, default=0)
3953 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3972 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3954
3973
3955 author = relationship('User', lazy='joined')
3974 author = relationship('User', lazy='joined')
3956 repo = relationship('Repository')
3975 repo = relationship('Repository')
3957 comment = relationship('ChangesetComment', lazy='joined')
3976 comment = relationship('ChangesetComment', lazy='joined')
3958 pull_request = relationship('PullRequest', lazy='joined')
3977 pull_request = relationship('PullRequest', lazy='joined')
3959
3978
3960 def __unicode__(self):
3979 def __unicode__(self):
3961 return u"<%s('%s[v%s]:%s')>" % (
3980 return u"<%s('%s[v%s]:%s')>" % (
3962 self.__class__.__name__,
3981 self.__class__.__name__,
3963 self.status, self.version, self.author
3982 self.status, self.version, self.author
3964 )
3983 )
3965
3984
3966 @classmethod
3985 @classmethod
3967 def get_status_lbl(cls, value):
3986 def get_status_lbl(cls, value):
3968 return dict(cls.STATUSES).get(value)
3987 return dict(cls.STATUSES).get(value)
3969
3988
3970 @property
3989 @property
3971 def status_lbl(self):
3990 def status_lbl(self):
3972 return ChangesetStatus.get_status_lbl(self.status)
3991 return ChangesetStatus.get_status_lbl(self.status)
3973
3992
3974 def get_api_data(self):
3993 def get_api_data(self):
3975 status = self
3994 status = self
3976 data = {
3995 data = {
3977 'status_id': status.changeset_status_id,
3996 'status_id': status.changeset_status_id,
3978 'status': status.status,
3997 'status': status.status,
3979 }
3998 }
3980 return data
3999 return data
3981
4000
3982 def __json__(self):
4001 def __json__(self):
3983 data = dict()
4002 data = dict()
3984 data.update(self.get_api_data())
4003 data.update(self.get_api_data())
3985 return data
4004 return data
3986
4005
3987
4006
3988 class _SetState(object):
4007 class _SetState(object):
3989 """
4008 """
3990 Context processor allowing changing state for sensitive operation such as
4009 Context processor allowing changing state for sensitive operation such as
3991 pull request update or merge
4010 pull request update or merge
3992 """
4011 """
3993
4012
3994 def __init__(self, pull_request, pr_state, back_state=None):
4013 def __init__(self, pull_request, pr_state, back_state=None):
3995 self._pr = pull_request
4014 self._pr = pull_request
3996 self._org_state = back_state or pull_request.pull_request_state
4015 self._org_state = back_state or pull_request.pull_request_state
3997 self._pr_state = pr_state
4016 self._pr_state = pr_state
3998 self._current_state = None
4017 self._current_state = None
3999
4018
4000 def __enter__(self):
4019 def __enter__(self):
4001 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4020 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
4002 self._pr, self._pr_state)
4021 self._pr, self._pr_state)
4003 self.set_pr_state(self._pr_state)
4022 self.set_pr_state(self._pr_state)
4004 return self
4023 return self
4005
4024
4006 def __exit__(self, exc_type, exc_val, exc_tb):
4025 def __exit__(self, exc_type, exc_val, exc_tb):
4007 if exc_val is not None:
4026 if exc_val is not None:
4008 log.error(traceback.format_exc(exc_tb))
4027 log.error(traceback.format_exc(exc_tb))
4009 return None
4028 return None
4010
4029
4011 self.set_pr_state(self._org_state)
4030 self.set_pr_state(self._org_state)
4012 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4031 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
4013 self._pr, self._org_state)
4032 self._pr, self._org_state)
4014
4033
4015 @property
4034 @property
4016 def state(self):
4035 def state(self):
4017 return self._current_state
4036 return self._current_state
4018
4037
4019 def set_pr_state(self, pr_state):
4038 def set_pr_state(self, pr_state):
4020 try:
4039 try:
4021 self._pr.pull_request_state = pr_state
4040 self._pr.pull_request_state = pr_state
4022 Session().add(self._pr)
4041 Session().add(self._pr)
4023 Session().commit()
4042 Session().commit()
4024 self._current_state = pr_state
4043 self._current_state = pr_state
4025 except Exception:
4044 except Exception:
4026 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4045 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
4027 raise
4046 raise
4028
4047
4029
4048
4030 class _PullRequestBase(BaseModel):
4049 class _PullRequestBase(BaseModel):
4031 """
4050 """
4032 Common attributes of pull request and version entries.
4051 Common attributes of pull request and version entries.
4033 """
4052 """
4034
4053
4035 # .status values
4054 # .status values
4036 STATUS_NEW = u'new'
4055 STATUS_NEW = u'new'
4037 STATUS_OPEN = u'open'
4056 STATUS_OPEN = u'open'
4038 STATUS_CLOSED = u'closed'
4057 STATUS_CLOSED = u'closed'
4039
4058
4040 # available states
4059 # available states
4041 STATE_CREATING = u'creating'
4060 STATE_CREATING = u'creating'
4042 STATE_UPDATING = u'updating'
4061 STATE_UPDATING = u'updating'
4043 STATE_MERGING = u'merging'
4062 STATE_MERGING = u'merging'
4044 STATE_CREATED = u'created'
4063 STATE_CREATED = u'created'
4045
4064
4046 title = Column('title', Unicode(255), nullable=True)
4065 title = Column('title', Unicode(255), nullable=True)
4047 description = Column(
4066 description = Column(
4048 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4067 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
4049 nullable=True)
4068 nullable=True)
4050 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4069 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
4051
4070
4052 # new/open/closed status of pull request (not approve/reject/etc)
4071 # new/open/closed status of pull request (not approve/reject/etc)
4053 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4072 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
4054 created_on = Column(
4073 created_on = Column(
4055 'created_on', DateTime(timezone=False), nullable=False,
4074 'created_on', DateTime(timezone=False), nullable=False,
4056 default=datetime.datetime.now)
4075 default=datetime.datetime.now)
4057 updated_on = Column(
4076 updated_on = Column(
4058 'updated_on', DateTime(timezone=False), nullable=False,
4077 'updated_on', DateTime(timezone=False), nullable=False,
4059 default=datetime.datetime.now)
4078 default=datetime.datetime.now)
4060
4079
4061 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4080 pull_request_state = Column("pull_request_state", String(255), nullable=True)
4062
4081
4063 @declared_attr
4082 @declared_attr
4064 def user_id(cls):
4083 def user_id(cls):
4065 return Column(
4084 return Column(
4066 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4085 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
4067 unique=None)
4086 unique=None)
4068
4087
4069 # 500 revisions max
4088 # 500 revisions max
4070 _revisions = Column(
4089 _revisions = Column(
4071 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4090 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
4072
4091
4073 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4092 common_ancestor_id = Column('common_ancestor_id', Unicode(255), nullable=True)
4074
4093
4075 @declared_attr
4094 @declared_attr
4076 def source_repo_id(cls):
4095 def source_repo_id(cls):
4077 # TODO: dan: rename column to source_repo_id
4096 # TODO: dan: rename column to source_repo_id
4078 return Column(
4097 return Column(
4079 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4098 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4080 nullable=False)
4099 nullable=False)
4081
4100
4082 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4101 _source_ref = Column('org_ref', Unicode(255), nullable=False)
4083
4102
4084 @hybrid_property
4103 @hybrid_property
4085 def source_ref(self):
4104 def source_ref(self):
4086 return self._source_ref
4105 return self._source_ref
4087
4106
4088 @source_ref.setter
4107 @source_ref.setter
4089 def source_ref(self, val):
4108 def source_ref(self, val):
4090 parts = (val or '').split(':')
4109 parts = (val or '').split(':')
4091 if len(parts) != 3:
4110 if len(parts) != 3:
4092 raise ValueError(
4111 raise ValueError(
4093 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4112 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4094 self._source_ref = safe_unicode(val)
4113 self._source_ref = safe_unicode(val)
4095
4114
4096 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4115 _target_ref = Column('other_ref', Unicode(255), nullable=False)
4097
4116
4098 @hybrid_property
4117 @hybrid_property
4099 def target_ref(self):
4118 def target_ref(self):
4100 return self._target_ref
4119 return self._target_ref
4101
4120
4102 @target_ref.setter
4121 @target_ref.setter
4103 def target_ref(self, val):
4122 def target_ref(self, val):
4104 parts = (val or '').split(':')
4123 parts = (val or '').split(':')
4105 if len(parts) != 3:
4124 if len(parts) != 3:
4106 raise ValueError(
4125 raise ValueError(
4107 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4126 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4108 self._target_ref = safe_unicode(val)
4127 self._target_ref = safe_unicode(val)
4109
4128
4110 @declared_attr
4129 @declared_attr
4111 def target_repo_id(cls):
4130 def target_repo_id(cls):
4112 # TODO: dan: rename column to target_repo_id
4131 # TODO: dan: rename column to target_repo_id
4113 return Column(
4132 return Column(
4114 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4133 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4115 nullable=False)
4134 nullable=False)
4116
4135
4117 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4136 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4118
4137
4119 # TODO: dan: rename column to last_merge_source_rev
4138 # TODO: dan: rename column to last_merge_source_rev
4120 _last_merge_source_rev = Column(
4139 _last_merge_source_rev = Column(
4121 'last_merge_org_rev', String(40), nullable=True)
4140 'last_merge_org_rev', String(40), nullable=True)
4122 # TODO: dan: rename column to last_merge_target_rev
4141 # TODO: dan: rename column to last_merge_target_rev
4123 _last_merge_target_rev = Column(
4142 _last_merge_target_rev = Column(
4124 'last_merge_other_rev', String(40), nullable=True)
4143 'last_merge_other_rev', String(40), nullable=True)
4125 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4144 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4126 last_merge_metadata = Column(
4145 last_merge_metadata = Column(
4127 'last_merge_metadata', MutationObj.as_mutable(
4146 'last_merge_metadata', MutationObj.as_mutable(
4128 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4147 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4129
4148
4130 merge_rev = Column('merge_rev', String(40), nullable=True)
4149 merge_rev = Column('merge_rev', String(40), nullable=True)
4131
4150
4132 reviewer_data = Column(
4151 reviewer_data = Column(
4133 'reviewer_data_json', MutationObj.as_mutable(
4152 'reviewer_data_json', MutationObj.as_mutable(
4134 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4153 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4135
4154
4136 @property
4155 @property
4137 def reviewer_data_json(self):
4156 def reviewer_data_json(self):
4138 return json.dumps(self.reviewer_data)
4157 return json.dumps(self.reviewer_data)
4139
4158
4140 @property
4159 @property
4141 def last_merge_metadata_parsed(self):
4160 def last_merge_metadata_parsed(self):
4142 metadata = {}
4161 metadata = {}
4143 if not self.last_merge_metadata:
4162 if not self.last_merge_metadata:
4144 return metadata
4163 return metadata
4145
4164
4146 if hasattr(self.last_merge_metadata, 'de_coerce'):
4165 if hasattr(self.last_merge_metadata, 'de_coerce'):
4147 for k, v in self.last_merge_metadata.de_coerce().items():
4166 for k, v in self.last_merge_metadata.de_coerce().items():
4148 if k in ['target_ref', 'source_ref']:
4167 if k in ['target_ref', 'source_ref']:
4149 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4168 metadata[k] = Reference(v['type'], v['name'], v['commit_id'])
4150 else:
4169 else:
4151 if hasattr(v, 'de_coerce'):
4170 if hasattr(v, 'de_coerce'):
4152 metadata[k] = v.de_coerce()
4171 metadata[k] = v.de_coerce()
4153 else:
4172 else:
4154 metadata[k] = v
4173 metadata[k] = v
4155 return metadata
4174 return metadata
4156
4175
4157 @property
4176 @property
4158 def work_in_progress(self):
4177 def work_in_progress(self):
4159 """checks if pull request is work in progress by checking the title"""
4178 """checks if pull request is work in progress by checking the title"""
4160 title = self.title.upper()
4179 title = self.title.upper()
4161 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4180 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4162 return True
4181 return True
4163 return False
4182 return False
4164
4183
4165 @hybrid_property
4184 @hybrid_property
4166 def description_safe(self):
4185 def description_safe(self):
4167 from rhodecode.lib import helpers as h
4186 from rhodecode.lib import helpers as h
4168 return h.escape(self.description)
4187 return h.escape(self.description)
4169
4188
4170 @hybrid_property
4189 @hybrid_property
4171 def revisions(self):
4190 def revisions(self):
4172 return self._revisions.split(':') if self._revisions else []
4191 return self._revisions.split(':') if self._revisions else []
4173
4192
4174 @revisions.setter
4193 @revisions.setter
4175 def revisions(self, val):
4194 def revisions(self, val):
4176 self._revisions = u':'.join(val)
4195 self._revisions = u':'.join(val)
4177
4196
4178 @hybrid_property
4197 @hybrid_property
4179 def last_merge_status(self):
4198 def last_merge_status(self):
4180 return safe_int(self._last_merge_status)
4199 return safe_int(self._last_merge_status)
4181
4200
4182 @last_merge_status.setter
4201 @last_merge_status.setter
4183 def last_merge_status(self, val):
4202 def last_merge_status(self, val):
4184 self._last_merge_status = val
4203 self._last_merge_status = val
4185
4204
4186 @declared_attr
4205 @declared_attr
4187 def author(cls):
4206 def author(cls):
4188 return relationship('User', lazy='joined')
4207 return relationship('User', lazy='joined')
4189
4208
4190 @declared_attr
4209 @declared_attr
4191 def source_repo(cls):
4210 def source_repo(cls):
4192 return relationship(
4211 return relationship(
4193 'Repository',
4212 'Repository',
4194 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4213 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4195
4214
4196 @property
4215 @property
4197 def source_ref_parts(self):
4216 def source_ref_parts(self):
4198 return self.unicode_to_reference(self.source_ref)
4217 return self.unicode_to_reference(self.source_ref)
4199
4218
4200 @declared_attr
4219 @declared_attr
4201 def target_repo(cls):
4220 def target_repo(cls):
4202 return relationship(
4221 return relationship(
4203 'Repository',
4222 'Repository',
4204 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4223 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4205
4224
4206 @property
4225 @property
4207 def target_ref_parts(self):
4226 def target_ref_parts(self):
4208 return self.unicode_to_reference(self.target_ref)
4227 return self.unicode_to_reference(self.target_ref)
4209
4228
4210 @property
4229 @property
4211 def shadow_merge_ref(self):
4230 def shadow_merge_ref(self):
4212 return self.unicode_to_reference(self._shadow_merge_ref)
4231 return self.unicode_to_reference(self._shadow_merge_ref)
4213
4232
4214 @shadow_merge_ref.setter
4233 @shadow_merge_ref.setter
4215 def shadow_merge_ref(self, ref):
4234 def shadow_merge_ref(self, ref):
4216 self._shadow_merge_ref = self.reference_to_unicode(ref)
4235 self._shadow_merge_ref = self.reference_to_unicode(ref)
4217
4236
4218 @staticmethod
4237 @staticmethod
4219 def unicode_to_reference(raw):
4238 def unicode_to_reference(raw):
4220 """
4239 """
4221 Convert a unicode (or string) to a reference object.
4240 Convert a unicode (or string) to a reference object.
4222 If unicode evaluates to False it returns None.
4241 If unicode evaluates to False it returns None.
4223 """
4242 """
4224 if raw:
4243 if raw:
4225 refs = raw.split(':')
4244 refs = raw.split(':')
4226 return Reference(*refs)
4245 return Reference(*refs)
4227 else:
4246 else:
4228 return None
4247 return None
4229
4248
4230 @staticmethod
4249 @staticmethod
4231 def reference_to_unicode(ref):
4250 def reference_to_unicode(ref):
4232 """
4251 """
4233 Convert a reference object to unicode.
4252 Convert a reference object to unicode.
4234 If reference is None it returns None.
4253 If reference is None it returns None.
4235 """
4254 """
4236 if ref:
4255 if ref:
4237 return u':'.join(ref)
4256 return u':'.join(ref)
4238 else:
4257 else:
4239 return None
4258 return None
4240
4259
4241 def get_api_data(self, with_merge_state=True):
4260 def get_api_data(self, with_merge_state=True):
4242 from rhodecode.model.pull_request import PullRequestModel
4261 from rhodecode.model.pull_request import PullRequestModel
4243
4262
4244 pull_request = self
4263 pull_request = self
4245 if with_merge_state:
4264 if with_merge_state:
4246 merge_response, merge_status, msg = \
4265 merge_response, merge_status, msg = \
4247 PullRequestModel().merge_status(pull_request)
4266 PullRequestModel().merge_status(pull_request)
4248 merge_state = {
4267 merge_state = {
4249 'status': merge_status,
4268 'status': merge_status,
4250 'message': safe_unicode(msg),
4269 'message': safe_unicode(msg),
4251 }
4270 }
4252 else:
4271 else:
4253 merge_state = {'status': 'not_available',
4272 merge_state = {'status': 'not_available',
4254 'message': 'not_available'}
4273 'message': 'not_available'}
4255
4274
4256 merge_data = {
4275 merge_data = {
4257 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4276 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4258 'reference': (
4277 'reference': (
4259 pull_request.shadow_merge_ref._asdict()
4278 pull_request.shadow_merge_ref._asdict()
4260 if pull_request.shadow_merge_ref else None),
4279 if pull_request.shadow_merge_ref else None),
4261 }
4280 }
4262
4281
4263 data = {
4282 data = {
4264 'pull_request_id': pull_request.pull_request_id,
4283 'pull_request_id': pull_request.pull_request_id,
4265 'url': PullRequestModel().get_url(pull_request),
4284 'url': PullRequestModel().get_url(pull_request),
4266 'title': pull_request.title,
4285 'title': pull_request.title,
4267 'description': pull_request.description,
4286 'description': pull_request.description,
4268 'status': pull_request.status,
4287 'status': pull_request.status,
4269 'state': pull_request.pull_request_state,
4288 'state': pull_request.pull_request_state,
4270 'created_on': pull_request.created_on,
4289 'created_on': pull_request.created_on,
4271 'updated_on': pull_request.updated_on,
4290 'updated_on': pull_request.updated_on,
4272 'commit_ids': pull_request.revisions,
4291 'commit_ids': pull_request.revisions,
4273 'review_status': pull_request.calculated_review_status(),
4292 'review_status': pull_request.calculated_review_status(),
4274 'mergeable': merge_state,
4293 'mergeable': merge_state,
4275 'source': {
4294 'source': {
4276 'clone_url': pull_request.source_repo.clone_url(),
4295 'clone_url': pull_request.source_repo.clone_url(),
4277 'repository': pull_request.source_repo.repo_name,
4296 'repository': pull_request.source_repo.repo_name,
4278 'reference': {
4297 'reference': {
4279 'name': pull_request.source_ref_parts.name,
4298 'name': pull_request.source_ref_parts.name,
4280 'type': pull_request.source_ref_parts.type,
4299 'type': pull_request.source_ref_parts.type,
4281 'commit_id': pull_request.source_ref_parts.commit_id,
4300 'commit_id': pull_request.source_ref_parts.commit_id,
4282 },
4301 },
4283 },
4302 },
4284 'target': {
4303 'target': {
4285 'clone_url': pull_request.target_repo.clone_url(),
4304 'clone_url': pull_request.target_repo.clone_url(),
4286 'repository': pull_request.target_repo.repo_name,
4305 'repository': pull_request.target_repo.repo_name,
4287 'reference': {
4306 'reference': {
4288 'name': pull_request.target_ref_parts.name,
4307 'name': pull_request.target_ref_parts.name,
4289 'type': pull_request.target_ref_parts.type,
4308 'type': pull_request.target_ref_parts.type,
4290 'commit_id': pull_request.target_ref_parts.commit_id,
4309 'commit_id': pull_request.target_ref_parts.commit_id,
4291 },
4310 },
4292 },
4311 },
4293 'merge': merge_data,
4312 'merge': merge_data,
4294 'author': pull_request.author.get_api_data(include_secrets=False,
4313 'author': pull_request.author.get_api_data(include_secrets=False,
4295 details='basic'),
4314 details='basic'),
4296 'reviewers': [
4315 'reviewers': [
4297 {
4316 {
4298 'user': reviewer.get_api_data(include_secrets=False,
4317 'user': reviewer.get_api_data(include_secrets=False,
4299 details='basic'),
4318 details='basic'),
4300 'reasons': reasons,
4319 'reasons': reasons,
4301 'review_status': st[0][1].status if st else 'not_reviewed',
4320 'review_status': st[0][1].status if st else 'not_reviewed',
4302 }
4321 }
4303 for obj, reviewer, reasons, mandatory, st in
4322 for obj, reviewer, reasons, mandatory, st in
4304 pull_request.reviewers_statuses()
4323 pull_request.reviewers_statuses()
4305 ]
4324 ]
4306 }
4325 }
4307
4326
4308 return data
4327 return data
4309
4328
4310 def set_state(self, pull_request_state, final_state=None):
4329 def set_state(self, pull_request_state, final_state=None):
4311 """
4330 """
4312 # goes from initial state to updating to initial state.
4331 # goes from initial state to updating to initial state.
4313 # initial state can be changed by specifying back_state=
4332 # initial state can be changed by specifying back_state=
4314 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4333 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4315 pull_request.merge()
4334 pull_request.merge()
4316
4335
4317 :param pull_request_state:
4336 :param pull_request_state:
4318 :param final_state:
4337 :param final_state:
4319
4338
4320 """
4339 """
4321
4340
4322 return _SetState(self, pull_request_state, back_state=final_state)
4341 return _SetState(self, pull_request_state, back_state=final_state)
4323
4342
4324
4343
4325 class PullRequest(Base, _PullRequestBase):
4344 class PullRequest(Base, _PullRequestBase):
4326 __tablename__ = 'pull_requests'
4345 __tablename__ = 'pull_requests'
4327 __table_args__ = (
4346 __table_args__ = (
4328 base_table_args,
4347 base_table_args,
4329 )
4348 )
4349 LATEST_VER = 'latest'
4330
4350
4331 pull_request_id = Column(
4351 pull_request_id = Column(
4332 'pull_request_id', Integer(), nullable=False, primary_key=True)
4352 'pull_request_id', Integer(), nullable=False, primary_key=True)
4333
4353
4334 def __repr__(self):
4354 def __repr__(self):
4335 if self.pull_request_id:
4355 if self.pull_request_id:
4336 return '<DB:PullRequest #%s>' % self.pull_request_id
4356 return '<DB:PullRequest #%s>' % self.pull_request_id
4337 else:
4357 else:
4338 return '<DB:PullRequest at %#x>' % id(self)
4358 return '<DB:PullRequest at %#x>' % id(self)
4339
4359
4340 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4360 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4341 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4361 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4342 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4362 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4343 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4363 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4344 lazy='dynamic')
4364 lazy='dynamic')
4345
4365
4346 @classmethod
4366 @classmethod
4347 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4367 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4348 internal_methods=None):
4368 internal_methods=None):
4349
4369
4350 class PullRequestDisplay(object):
4370 class PullRequestDisplay(object):
4351 """
4371 """
4352 Special object wrapper for showing PullRequest data via Versions
4372 Special object wrapper for showing PullRequest data via Versions
4353 It mimics PR object as close as possible. This is read only object
4373 It mimics PR object as close as possible. This is read only object
4354 just for display
4374 just for display
4355 """
4375 """
4356
4376
4357 def __init__(self, attrs, internal=None):
4377 def __init__(self, attrs, internal=None):
4358 self.attrs = attrs
4378 self.attrs = attrs
4359 # internal have priority over the given ones via attrs
4379 # internal have priority over the given ones via attrs
4360 self.internal = internal or ['versions']
4380 self.internal = internal or ['versions']
4361
4381
4362 def __getattr__(self, item):
4382 def __getattr__(self, item):
4363 if item in self.internal:
4383 if item in self.internal:
4364 return getattr(self, item)
4384 return getattr(self, item)
4365 try:
4385 try:
4366 return self.attrs[item]
4386 return self.attrs[item]
4367 except KeyError:
4387 except KeyError:
4368 raise AttributeError(
4388 raise AttributeError(
4369 '%s object has no attribute %s' % (self, item))
4389 '%s object has no attribute %s' % (self, item))
4370
4390
4371 def __repr__(self):
4391 def __repr__(self):
4372 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4392 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4373
4393
4374 def versions(self):
4394 def versions(self):
4375 return pull_request_obj.versions.order_by(
4395 return pull_request_obj.versions.order_by(
4376 PullRequestVersion.pull_request_version_id).all()
4396 PullRequestVersion.pull_request_version_id).all()
4377
4397
4378 def is_closed(self):
4398 def is_closed(self):
4379 return pull_request_obj.is_closed()
4399 return pull_request_obj.is_closed()
4380
4400
4381 def is_state_changing(self):
4401 def is_state_changing(self):
4382 return pull_request_obj.is_state_changing()
4402 return pull_request_obj.is_state_changing()
4383
4403
4384 @property
4404 @property
4385 def pull_request_version_id(self):
4405 def pull_request_version_id(self):
4386 return getattr(pull_request_obj, 'pull_request_version_id', None)
4406 return getattr(pull_request_obj, 'pull_request_version_id', None)
4387
4407
4408 @property
4409 def pull_request_last_version(self):
4410 return pull_request_obj.pull_request_last_version
4411
4388 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4412 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4389
4413
4390 attrs.author = StrictAttributeDict(
4414 attrs.author = StrictAttributeDict(
4391 pull_request_obj.author.get_api_data())
4415 pull_request_obj.author.get_api_data())
4392 if pull_request_obj.target_repo:
4416 if pull_request_obj.target_repo:
4393 attrs.target_repo = StrictAttributeDict(
4417 attrs.target_repo = StrictAttributeDict(
4394 pull_request_obj.target_repo.get_api_data())
4418 pull_request_obj.target_repo.get_api_data())
4395 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4419 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4396
4420
4397 if pull_request_obj.source_repo:
4421 if pull_request_obj.source_repo:
4398 attrs.source_repo = StrictAttributeDict(
4422 attrs.source_repo = StrictAttributeDict(
4399 pull_request_obj.source_repo.get_api_data())
4423 pull_request_obj.source_repo.get_api_data())
4400 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4424 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4401
4425
4402 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4426 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4403 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4427 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4404 attrs.revisions = pull_request_obj.revisions
4428 attrs.revisions = pull_request_obj.revisions
4405 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4429 attrs.common_ancestor_id = pull_request_obj.common_ancestor_id
4406 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4430 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4407 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4431 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4408 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4432 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4409
4433
4410 return PullRequestDisplay(attrs, internal=internal_methods)
4434 return PullRequestDisplay(attrs, internal=internal_methods)
4411
4435
4412 def is_closed(self):
4436 def is_closed(self):
4413 return self.status == self.STATUS_CLOSED
4437 return self.status == self.STATUS_CLOSED
4414
4438
4415 def is_state_changing(self):
4439 def is_state_changing(self):
4416 return self.pull_request_state != PullRequest.STATE_CREATED
4440 return self.pull_request_state != PullRequest.STATE_CREATED
4417
4441
4418 def __json__(self):
4442 def __json__(self):
4419 return {
4443 return {
4420 'revisions': self.revisions,
4444 'revisions': self.revisions,
4421 'versions': self.versions_count
4445 'versions': self.versions_count
4422 }
4446 }
4423
4447
4424 def calculated_review_status(self):
4448 def calculated_review_status(self):
4425 from rhodecode.model.changeset_status import ChangesetStatusModel
4449 from rhodecode.model.changeset_status import ChangesetStatusModel
4426 return ChangesetStatusModel().calculated_review_status(self)
4450 return ChangesetStatusModel().calculated_review_status(self)
4427
4451
4428 def reviewers_statuses(self):
4452 def reviewers_statuses(self):
4429 from rhodecode.model.changeset_status import ChangesetStatusModel
4453 from rhodecode.model.changeset_status import ChangesetStatusModel
4430 return ChangesetStatusModel().reviewers_statuses(self)
4454 return ChangesetStatusModel().reviewers_statuses(self)
4431
4455
4432 @property
4456 @property
4433 def workspace_id(self):
4457 def workspace_id(self):
4434 from rhodecode.model.pull_request import PullRequestModel
4458 from rhodecode.model.pull_request import PullRequestModel
4435 return PullRequestModel()._workspace_id(self)
4459 return PullRequestModel()._workspace_id(self)
4436
4460
4437 def get_shadow_repo(self):
4461 def get_shadow_repo(self):
4438 workspace_id = self.workspace_id
4462 workspace_id = self.workspace_id
4439 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4463 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4440 if os.path.isdir(shadow_repository_path):
4464 if os.path.isdir(shadow_repository_path):
4441 vcs_obj = self.target_repo.scm_instance()
4465 vcs_obj = self.target_repo.scm_instance()
4442 return vcs_obj.get_shadow_instance(shadow_repository_path)
4466 return vcs_obj.get_shadow_instance(shadow_repository_path)
4443
4467
4444 @property
4468 @property
4445 def versions_count(self):
4469 def versions_count(self):
4446 """
4470 """
4447 return number of versions this PR have, e.g a PR that once been
4471 return number of versions this PR have, e.g a PR that once been
4448 updated will have 2 versions
4472 updated will have 2 versions
4449 """
4473 """
4450 return self.versions.count() + 1
4474 return self.versions.count() + 1
4451
4475
4476 @property
4477 def pull_request_last_version(self):
4478 return self.versions_count
4479
4452
4480
4453 class PullRequestVersion(Base, _PullRequestBase):
4481 class PullRequestVersion(Base, _PullRequestBase):
4454 __tablename__ = 'pull_request_versions'
4482 __tablename__ = 'pull_request_versions'
4455 __table_args__ = (
4483 __table_args__ = (
4456 base_table_args,
4484 base_table_args,
4457 )
4485 )
4458
4486
4459 pull_request_version_id = Column(
4487 pull_request_version_id = Column(
4460 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4488 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4461 pull_request_id = Column(
4489 pull_request_id = Column(
4462 'pull_request_id', Integer(),
4490 'pull_request_id', Integer(),
4463 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4491 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4464 pull_request = relationship('PullRequest')
4492 pull_request = relationship('PullRequest')
4465
4493
4466 def __repr__(self):
4494 def __repr__(self):
4467 if self.pull_request_version_id:
4495 if self.pull_request_version_id:
4468 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4496 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4469 else:
4497 else:
4470 return '<DB:PullRequestVersion at %#x>' % id(self)
4498 return '<DB:PullRequestVersion at %#x>' % id(self)
4471
4499
4472 @property
4500 @property
4473 def reviewers(self):
4501 def reviewers(self):
4474 return self.pull_request.reviewers
4502 return self.pull_request.reviewers
4475
4503
4476 @property
4504 @property
4477 def versions(self):
4505 def versions(self):
4478 return self.pull_request.versions
4506 return self.pull_request.versions
4479
4507
4480 def is_closed(self):
4508 def is_closed(self):
4481 # calculate from original
4509 # calculate from original
4482 return self.pull_request.status == self.STATUS_CLOSED
4510 return self.pull_request.status == self.STATUS_CLOSED
4483
4511
4484 def is_state_changing(self):
4512 def is_state_changing(self):
4485 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4513 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4486
4514
4487 def calculated_review_status(self):
4515 def calculated_review_status(self):
4488 return self.pull_request.calculated_review_status()
4516 return self.pull_request.calculated_review_status()
4489
4517
4490 def reviewers_statuses(self):
4518 def reviewers_statuses(self):
4491 return self.pull_request.reviewers_statuses()
4519 return self.pull_request.reviewers_statuses()
4492
4520
4493
4521
4494 class PullRequestReviewers(Base, BaseModel):
4522 class PullRequestReviewers(Base, BaseModel):
4495 __tablename__ = 'pull_request_reviewers'
4523 __tablename__ = 'pull_request_reviewers'
4496 __table_args__ = (
4524 __table_args__ = (
4497 base_table_args,
4525 base_table_args,
4498 )
4526 )
4499 ROLE_REVIEWER = u'reviewer'
4527 ROLE_REVIEWER = u'reviewer'
4500 ROLE_OBSERVER = u'observer'
4528 ROLE_OBSERVER = u'observer'
4501
4529
4502 @hybrid_property
4530 @hybrid_property
4503 def reasons(self):
4531 def reasons(self):
4504 if not self._reasons:
4532 if not self._reasons:
4505 return []
4533 return []
4506 return self._reasons
4534 return self._reasons
4507
4535
4508 @reasons.setter
4536 @reasons.setter
4509 def reasons(self, val):
4537 def reasons(self, val):
4510 val = val or []
4538 val = val or []
4511 if any(not isinstance(x, compat.string_types) for x in val):
4539 if any(not isinstance(x, compat.string_types) for x in val):
4512 raise Exception('invalid reasons type, must be list of strings')
4540 raise Exception('invalid reasons type, must be list of strings')
4513 self._reasons = val
4541 self._reasons = val
4514
4542
4515 pull_requests_reviewers_id = Column(
4543 pull_requests_reviewers_id = Column(
4516 'pull_requests_reviewers_id', Integer(), nullable=False,
4544 'pull_requests_reviewers_id', Integer(), nullable=False,
4517 primary_key=True)
4545 primary_key=True)
4518 pull_request_id = Column(
4546 pull_request_id = Column(
4519 "pull_request_id", Integer(),
4547 "pull_request_id", Integer(),
4520 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4548 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4521 user_id = Column(
4549 user_id = Column(
4522 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4550 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4523 _reasons = Column(
4551 _reasons = Column(
4524 'reason', MutationList.as_mutable(
4552 'reason', MutationList.as_mutable(
4525 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4553 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4526
4554
4527 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4555 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4528 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4556 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4529
4557
4530 user = relationship('User')
4558 user = relationship('User')
4531 pull_request = relationship('PullRequest')
4559 pull_request = relationship('PullRequest')
4532
4560
4533 rule_data = Column(
4561 rule_data = Column(
4534 'rule_data_json',
4562 'rule_data_json',
4535 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4563 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4536
4564
4537 def rule_user_group_data(self):
4565 def rule_user_group_data(self):
4538 """
4566 """
4539 Returns the voting user group rule data for this reviewer
4567 Returns the voting user group rule data for this reviewer
4540 """
4568 """
4541
4569
4542 if self.rule_data and 'vote_rule' in self.rule_data:
4570 if self.rule_data and 'vote_rule' in self.rule_data:
4543 user_group_data = {}
4571 user_group_data = {}
4544 if 'rule_user_group_entry_id' in self.rule_data:
4572 if 'rule_user_group_entry_id' in self.rule_data:
4545 # means a group with voting rules !
4573 # means a group with voting rules !
4546 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4574 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4547 user_group_data['name'] = self.rule_data['rule_name']
4575 user_group_data['name'] = self.rule_data['rule_name']
4548 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4576 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4549
4577
4550 return user_group_data
4578 return user_group_data
4551
4579
4552 def __unicode__(self):
4580 def __unicode__(self):
4553 return u"<%s('id:%s')>" % (self.__class__.__name__,
4581 return u"<%s('id:%s')>" % (self.__class__.__name__,
4554 self.pull_requests_reviewers_id)
4582 self.pull_requests_reviewers_id)
4555
4583
4556
4584
4557 class Notification(Base, BaseModel):
4585 class Notification(Base, BaseModel):
4558 __tablename__ = 'notifications'
4586 __tablename__ = 'notifications'
4559 __table_args__ = (
4587 __table_args__ = (
4560 Index('notification_type_idx', 'type'),
4588 Index('notification_type_idx', 'type'),
4561 base_table_args,
4589 base_table_args,
4562 )
4590 )
4563
4591
4564 TYPE_CHANGESET_COMMENT = u'cs_comment'
4592 TYPE_CHANGESET_COMMENT = u'cs_comment'
4565 TYPE_MESSAGE = u'message'
4593 TYPE_MESSAGE = u'message'
4566 TYPE_MENTION = u'mention'
4594 TYPE_MENTION = u'mention'
4567 TYPE_REGISTRATION = u'registration'
4595 TYPE_REGISTRATION = u'registration'
4568 TYPE_PULL_REQUEST = u'pull_request'
4596 TYPE_PULL_REQUEST = u'pull_request'
4569 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4597 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4570 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4598 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4571
4599
4572 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4600 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4573 subject = Column('subject', Unicode(512), nullable=True)
4601 subject = Column('subject', Unicode(512), nullable=True)
4574 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4602 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4575 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4603 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4576 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4604 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4577 type_ = Column('type', Unicode(255))
4605 type_ = Column('type', Unicode(255))
4578
4606
4579 created_by_user = relationship('User')
4607 created_by_user = relationship('User')
4580 notifications_to_users = relationship('UserNotification', lazy='joined',
4608 notifications_to_users = relationship('UserNotification', lazy='joined',
4581 cascade="all, delete-orphan")
4609 cascade="all, delete-orphan")
4582
4610
4583 @property
4611 @property
4584 def recipients(self):
4612 def recipients(self):
4585 return [x.user for x in UserNotification.query()\
4613 return [x.user for x in UserNotification.query()\
4586 .filter(UserNotification.notification == self)\
4614 .filter(UserNotification.notification == self)\
4587 .order_by(UserNotification.user_id.asc()).all()]
4615 .order_by(UserNotification.user_id.asc()).all()]
4588
4616
4589 @classmethod
4617 @classmethod
4590 def create(cls, created_by, subject, body, recipients, type_=None):
4618 def create(cls, created_by, subject, body, recipients, type_=None):
4591 if type_ is None:
4619 if type_ is None:
4592 type_ = Notification.TYPE_MESSAGE
4620 type_ = Notification.TYPE_MESSAGE
4593
4621
4594 notification = cls()
4622 notification = cls()
4595 notification.created_by_user = created_by
4623 notification.created_by_user = created_by
4596 notification.subject = subject
4624 notification.subject = subject
4597 notification.body = body
4625 notification.body = body
4598 notification.type_ = type_
4626 notification.type_ = type_
4599 notification.created_on = datetime.datetime.now()
4627 notification.created_on = datetime.datetime.now()
4600
4628
4601 # For each recipient link the created notification to his account
4629 # For each recipient link the created notification to his account
4602 for u in recipients:
4630 for u in recipients:
4603 assoc = UserNotification()
4631 assoc = UserNotification()
4604 assoc.user_id = u.user_id
4632 assoc.user_id = u.user_id
4605 assoc.notification = notification
4633 assoc.notification = notification
4606
4634
4607 # if created_by is inside recipients mark his notification
4635 # if created_by is inside recipients mark his notification
4608 # as read
4636 # as read
4609 if u.user_id == created_by.user_id:
4637 if u.user_id == created_by.user_id:
4610 assoc.read = True
4638 assoc.read = True
4611 Session().add(assoc)
4639 Session().add(assoc)
4612
4640
4613 Session().add(notification)
4641 Session().add(notification)
4614
4642
4615 return notification
4643 return notification
4616
4644
4617
4645
4618 class UserNotification(Base, BaseModel):
4646 class UserNotification(Base, BaseModel):
4619 __tablename__ = 'user_to_notification'
4647 __tablename__ = 'user_to_notification'
4620 __table_args__ = (
4648 __table_args__ = (
4621 UniqueConstraint('user_id', 'notification_id'),
4649 UniqueConstraint('user_id', 'notification_id'),
4622 base_table_args
4650 base_table_args
4623 )
4651 )
4624
4652
4625 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4653 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4626 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4654 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4627 read = Column('read', Boolean, default=False)
4655 read = Column('read', Boolean, default=False)
4628 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4656 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4629
4657
4630 user = relationship('User', lazy="joined")
4658 user = relationship('User', lazy="joined")
4631 notification = relationship('Notification', lazy="joined",
4659 notification = relationship('Notification', lazy="joined",
4632 order_by=lambda: Notification.created_on.desc(),)
4660 order_by=lambda: Notification.created_on.desc(),)
4633
4661
4634 def mark_as_read(self):
4662 def mark_as_read(self):
4635 self.read = True
4663 self.read = True
4636 Session().add(self)
4664 Session().add(self)
4637
4665
4638
4666
4639 class UserNotice(Base, BaseModel):
4667 class UserNotice(Base, BaseModel):
4640 __tablename__ = 'user_notices'
4668 __tablename__ = 'user_notices'
4641 __table_args__ = (
4669 __table_args__ = (
4642 base_table_args
4670 base_table_args
4643 )
4671 )
4644
4672
4645 NOTIFICATION_TYPE_MESSAGE = 'message'
4673 NOTIFICATION_TYPE_MESSAGE = 'message'
4646 NOTIFICATION_TYPE_NOTICE = 'notice'
4674 NOTIFICATION_TYPE_NOTICE = 'notice'
4647
4675
4648 NOTIFICATION_LEVEL_INFO = 'info'
4676 NOTIFICATION_LEVEL_INFO = 'info'
4649 NOTIFICATION_LEVEL_WARNING = 'warning'
4677 NOTIFICATION_LEVEL_WARNING = 'warning'
4650 NOTIFICATION_LEVEL_ERROR = 'error'
4678 NOTIFICATION_LEVEL_ERROR = 'error'
4651
4679
4652 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4680 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4653
4681
4654 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4682 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4655 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4683 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4656
4684
4657 notice_read = Column('notice_read', Boolean, default=False)
4685 notice_read = Column('notice_read', Boolean, default=False)
4658
4686
4659 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4687 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4660 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4688 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4661
4689
4662 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4690 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4663 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4691 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4664
4692
4665 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4693 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4666 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4694 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4667
4695
4668 @classmethod
4696 @classmethod
4669 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4697 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4670
4698
4671 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4699 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4672 cls.NOTIFICATION_LEVEL_WARNING,
4700 cls.NOTIFICATION_LEVEL_WARNING,
4673 cls.NOTIFICATION_LEVEL_INFO]:
4701 cls.NOTIFICATION_LEVEL_INFO]:
4674 return
4702 return
4675
4703
4676 from rhodecode.model.user import UserModel
4704 from rhodecode.model.user import UserModel
4677 user = UserModel().get_user(user)
4705 user = UserModel().get_user(user)
4678
4706
4679 new_notice = UserNotice()
4707 new_notice = UserNotice()
4680 if not allow_duplicate:
4708 if not allow_duplicate:
4681 existing_msg = UserNotice().query() \
4709 existing_msg = UserNotice().query() \
4682 .filter(UserNotice.user == user) \
4710 .filter(UserNotice.user == user) \
4683 .filter(UserNotice.notice_body == body) \
4711 .filter(UserNotice.notice_body == body) \
4684 .filter(UserNotice.notice_read == false()) \
4712 .filter(UserNotice.notice_read == false()) \
4685 .scalar()
4713 .scalar()
4686 if existing_msg:
4714 if existing_msg:
4687 log.warning('Ignoring duplicate notice for user %s', user)
4715 log.warning('Ignoring duplicate notice for user %s', user)
4688 return
4716 return
4689
4717
4690 new_notice.user = user
4718 new_notice.user = user
4691 new_notice.notice_subject = subject
4719 new_notice.notice_subject = subject
4692 new_notice.notice_body = body
4720 new_notice.notice_body = body
4693 new_notice.notification_level = notice_level
4721 new_notice.notification_level = notice_level
4694 Session().add(new_notice)
4722 Session().add(new_notice)
4695 Session().commit()
4723 Session().commit()
4696
4724
4697
4725
4698 class Gist(Base, BaseModel):
4726 class Gist(Base, BaseModel):
4699 __tablename__ = 'gists'
4727 __tablename__ = 'gists'
4700 __table_args__ = (
4728 __table_args__ = (
4701 Index('g_gist_access_id_idx', 'gist_access_id'),
4729 Index('g_gist_access_id_idx', 'gist_access_id'),
4702 Index('g_created_on_idx', 'created_on'),
4730 Index('g_created_on_idx', 'created_on'),
4703 base_table_args
4731 base_table_args
4704 )
4732 )
4705
4733
4706 GIST_PUBLIC = u'public'
4734 GIST_PUBLIC = u'public'
4707 GIST_PRIVATE = u'private'
4735 GIST_PRIVATE = u'private'
4708 DEFAULT_FILENAME = u'gistfile1.txt'
4736 DEFAULT_FILENAME = u'gistfile1.txt'
4709
4737
4710 ACL_LEVEL_PUBLIC = u'acl_public'
4738 ACL_LEVEL_PUBLIC = u'acl_public'
4711 ACL_LEVEL_PRIVATE = u'acl_private'
4739 ACL_LEVEL_PRIVATE = u'acl_private'
4712
4740
4713 gist_id = Column('gist_id', Integer(), primary_key=True)
4741 gist_id = Column('gist_id', Integer(), primary_key=True)
4714 gist_access_id = Column('gist_access_id', Unicode(250))
4742 gist_access_id = Column('gist_access_id', Unicode(250))
4715 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4743 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4716 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4744 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4717 gist_expires = Column('gist_expires', Float(53), nullable=False)
4745 gist_expires = Column('gist_expires', Float(53), nullable=False)
4718 gist_type = Column('gist_type', Unicode(128), nullable=False)
4746 gist_type = Column('gist_type', Unicode(128), nullable=False)
4719 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4747 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4720 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4748 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4721 acl_level = Column('acl_level', Unicode(128), nullable=True)
4749 acl_level = Column('acl_level', Unicode(128), nullable=True)
4722
4750
4723 owner = relationship('User')
4751 owner = relationship('User')
4724
4752
4725 def __repr__(self):
4753 def __repr__(self):
4726 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4754 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4727
4755
4728 @hybrid_property
4756 @hybrid_property
4729 def description_safe(self):
4757 def description_safe(self):
4730 from rhodecode.lib import helpers as h
4758 from rhodecode.lib import helpers as h
4731 return h.escape(self.gist_description)
4759 return h.escape(self.gist_description)
4732
4760
4733 @classmethod
4761 @classmethod
4734 def get_or_404(cls, id_):
4762 def get_or_404(cls, id_):
4735 from pyramid.httpexceptions import HTTPNotFound
4763 from pyramid.httpexceptions import HTTPNotFound
4736
4764
4737 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4765 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4738 if not res:
4766 if not res:
4739 raise HTTPNotFound()
4767 raise HTTPNotFound()
4740 return res
4768 return res
4741
4769
4742 @classmethod
4770 @classmethod
4743 def get_by_access_id(cls, gist_access_id):
4771 def get_by_access_id(cls, gist_access_id):
4744 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4772 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4745
4773
4746 def gist_url(self):
4774 def gist_url(self):
4747 from rhodecode.model.gist import GistModel
4775 from rhodecode.model.gist import GistModel
4748 return GistModel().get_url(self)
4776 return GistModel().get_url(self)
4749
4777
4750 @classmethod
4778 @classmethod
4751 def base_path(cls):
4779 def base_path(cls):
4752 """
4780 """
4753 Returns base path when all gists are stored
4781 Returns base path when all gists are stored
4754
4782
4755 :param cls:
4783 :param cls:
4756 """
4784 """
4757 from rhodecode.model.gist import GIST_STORE_LOC
4785 from rhodecode.model.gist import GIST_STORE_LOC
4758 q = Session().query(RhodeCodeUi)\
4786 q = Session().query(RhodeCodeUi)\
4759 .filter(RhodeCodeUi.ui_key == URL_SEP)
4787 .filter(RhodeCodeUi.ui_key == URL_SEP)
4760 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4788 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4761 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4789 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4762
4790
4763 def get_api_data(self):
4791 def get_api_data(self):
4764 """
4792 """
4765 Common function for generating gist related data for API
4793 Common function for generating gist related data for API
4766 """
4794 """
4767 gist = self
4795 gist = self
4768 data = {
4796 data = {
4769 'gist_id': gist.gist_id,
4797 'gist_id': gist.gist_id,
4770 'type': gist.gist_type,
4798 'type': gist.gist_type,
4771 'access_id': gist.gist_access_id,
4799 'access_id': gist.gist_access_id,
4772 'description': gist.gist_description,
4800 'description': gist.gist_description,
4773 'url': gist.gist_url(),
4801 'url': gist.gist_url(),
4774 'expires': gist.gist_expires,
4802 'expires': gist.gist_expires,
4775 'created_on': gist.created_on,
4803 'created_on': gist.created_on,
4776 'modified_at': gist.modified_at,
4804 'modified_at': gist.modified_at,
4777 'content': None,
4805 'content': None,
4778 'acl_level': gist.acl_level,
4806 'acl_level': gist.acl_level,
4779 }
4807 }
4780 return data
4808 return data
4781
4809
4782 def __json__(self):
4810 def __json__(self):
4783 data = dict(
4811 data = dict(
4784 )
4812 )
4785 data.update(self.get_api_data())
4813 data.update(self.get_api_data())
4786 return data
4814 return data
4787 # SCM functions
4815 # SCM functions
4788
4816
4789 def scm_instance(self, **kwargs):
4817 def scm_instance(self, **kwargs):
4790 """
4818 """
4791 Get an instance of VCS Repository
4819 Get an instance of VCS Repository
4792
4820
4793 :param kwargs:
4821 :param kwargs:
4794 """
4822 """
4795 from rhodecode.model.gist import GistModel
4823 from rhodecode.model.gist import GistModel
4796 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4824 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4797 return get_vcs_instance(
4825 return get_vcs_instance(
4798 repo_path=safe_str(full_repo_path), create=False,
4826 repo_path=safe_str(full_repo_path), create=False,
4799 _vcs_alias=GistModel.vcs_backend)
4827 _vcs_alias=GistModel.vcs_backend)
4800
4828
4801
4829
4802 class ExternalIdentity(Base, BaseModel):
4830 class ExternalIdentity(Base, BaseModel):
4803 __tablename__ = 'external_identities'
4831 __tablename__ = 'external_identities'
4804 __table_args__ = (
4832 __table_args__ = (
4805 Index('local_user_id_idx', 'local_user_id'),
4833 Index('local_user_id_idx', 'local_user_id'),
4806 Index('external_id_idx', 'external_id'),
4834 Index('external_id_idx', 'external_id'),
4807 base_table_args
4835 base_table_args
4808 )
4836 )
4809
4837
4810 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4838 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4811 external_username = Column('external_username', Unicode(1024), default=u'')
4839 external_username = Column('external_username', Unicode(1024), default=u'')
4812 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4840 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4813 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4841 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4814 access_token = Column('access_token', String(1024), default=u'')
4842 access_token = Column('access_token', String(1024), default=u'')
4815 alt_token = Column('alt_token', String(1024), default=u'')
4843 alt_token = Column('alt_token', String(1024), default=u'')
4816 token_secret = Column('token_secret', String(1024), default=u'')
4844 token_secret = Column('token_secret', String(1024), default=u'')
4817
4845
4818 @classmethod
4846 @classmethod
4819 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4847 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4820 """
4848 """
4821 Returns ExternalIdentity instance based on search params
4849 Returns ExternalIdentity instance based on search params
4822
4850
4823 :param external_id:
4851 :param external_id:
4824 :param provider_name:
4852 :param provider_name:
4825 :return: ExternalIdentity
4853 :return: ExternalIdentity
4826 """
4854 """
4827 query = cls.query()
4855 query = cls.query()
4828 query = query.filter(cls.external_id == external_id)
4856 query = query.filter(cls.external_id == external_id)
4829 query = query.filter(cls.provider_name == provider_name)
4857 query = query.filter(cls.provider_name == provider_name)
4830 if local_user_id:
4858 if local_user_id:
4831 query = query.filter(cls.local_user_id == local_user_id)
4859 query = query.filter(cls.local_user_id == local_user_id)
4832 return query.first()
4860 return query.first()
4833
4861
4834 @classmethod
4862 @classmethod
4835 def user_by_external_id_and_provider(cls, external_id, provider_name):
4863 def user_by_external_id_and_provider(cls, external_id, provider_name):
4836 """
4864 """
4837 Returns User instance based on search params
4865 Returns User instance based on search params
4838
4866
4839 :param external_id:
4867 :param external_id:
4840 :param provider_name:
4868 :param provider_name:
4841 :return: User
4869 :return: User
4842 """
4870 """
4843 query = User.query()
4871 query = User.query()
4844 query = query.filter(cls.external_id == external_id)
4872 query = query.filter(cls.external_id == external_id)
4845 query = query.filter(cls.provider_name == provider_name)
4873 query = query.filter(cls.provider_name == provider_name)
4846 query = query.filter(User.user_id == cls.local_user_id)
4874 query = query.filter(User.user_id == cls.local_user_id)
4847 return query.first()
4875 return query.first()
4848
4876
4849 @classmethod
4877 @classmethod
4850 def by_local_user_id(cls, local_user_id):
4878 def by_local_user_id(cls, local_user_id):
4851 """
4879 """
4852 Returns all tokens for user
4880 Returns all tokens for user
4853
4881
4854 :param local_user_id:
4882 :param local_user_id:
4855 :return: ExternalIdentity
4883 :return: ExternalIdentity
4856 """
4884 """
4857 query = cls.query()
4885 query = cls.query()
4858 query = query.filter(cls.local_user_id == local_user_id)
4886 query = query.filter(cls.local_user_id == local_user_id)
4859 return query
4887 return query
4860
4888
4861 @classmethod
4889 @classmethod
4862 def load_provider_plugin(cls, plugin_id):
4890 def load_provider_plugin(cls, plugin_id):
4863 from rhodecode.authentication.base import loadplugin
4891 from rhodecode.authentication.base import loadplugin
4864 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4892 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4865 auth_plugin = loadplugin(_plugin_id)
4893 auth_plugin = loadplugin(_plugin_id)
4866 return auth_plugin
4894 return auth_plugin
4867
4895
4868
4896
4869 class Integration(Base, BaseModel):
4897 class Integration(Base, BaseModel):
4870 __tablename__ = 'integrations'
4898 __tablename__ = 'integrations'
4871 __table_args__ = (
4899 __table_args__ = (
4872 base_table_args
4900 base_table_args
4873 )
4901 )
4874
4902
4875 integration_id = Column('integration_id', Integer(), primary_key=True)
4903 integration_id = Column('integration_id', Integer(), primary_key=True)
4876 integration_type = Column('integration_type', String(255))
4904 integration_type = Column('integration_type', String(255))
4877 enabled = Column('enabled', Boolean(), nullable=False)
4905 enabled = Column('enabled', Boolean(), nullable=False)
4878 name = Column('name', String(255), nullable=False)
4906 name = Column('name', String(255), nullable=False)
4879 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4907 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4880 default=False)
4908 default=False)
4881
4909
4882 settings = Column(
4910 settings = Column(
4883 'settings_json', MutationObj.as_mutable(
4911 'settings_json', MutationObj.as_mutable(
4884 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4912 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4885 repo_id = Column(
4913 repo_id = Column(
4886 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4914 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4887 nullable=True, unique=None, default=None)
4915 nullable=True, unique=None, default=None)
4888 repo = relationship('Repository', lazy='joined')
4916 repo = relationship('Repository', lazy='joined')
4889
4917
4890 repo_group_id = Column(
4918 repo_group_id = Column(
4891 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4919 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4892 nullable=True, unique=None, default=None)
4920 nullable=True, unique=None, default=None)
4893 repo_group = relationship('RepoGroup', lazy='joined')
4921 repo_group = relationship('RepoGroup', lazy='joined')
4894
4922
4895 @property
4923 @property
4896 def scope(self):
4924 def scope(self):
4897 if self.repo:
4925 if self.repo:
4898 return repr(self.repo)
4926 return repr(self.repo)
4899 if self.repo_group:
4927 if self.repo_group:
4900 if self.child_repos_only:
4928 if self.child_repos_only:
4901 return repr(self.repo_group) + ' (child repos only)'
4929 return repr(self.repo_group) + ' (child repos only)'
4902 else:
4930 else:
4903 return repr(self.repo_group) + ' (recursive)'
4931 return repr(self.repo_group) + ' (recursive)'
4904 if self.child_repos_only:
4932 if self.child_repos_only:
4905 return 'root_repos'
4933 return 'root_repos'
4906 return 'global'
4934 return 'global'
4907
4935
4908 def __repr__(self):
4936 def __repr__(self):
4909 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4937 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4910
4938
4911
4939
4912 class RepoReviewRuleUser(Base, BaseModel):
4940 class RepoReviewRuleUser(Base, BaseModel):
4913 __tablename__ = 'repo_review_rules_users'
4941 __tablename__ = 'repo_review_rules_users'
4914 __table_args__ = (
4942 __table_args__ = (
4915 base_table_args
4943 base_table_args
4916 )
4944 )
4917
4945
4918 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4946 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4919 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4947 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4920 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4948 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4921 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4949 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4922 user = relationship('User')
4950 user = relationship('User')
4923
4951
4924 def rule_data(self):
4952 def rule_data(self):
4925 return {
4953 return {
4926 'mandatory': self.mandatory
4954 'mandatory': self.mandatory
4927 }
4955 }
4928
4956
4929
4957
4930 class RepoReviewRuleUserGroup(Base, BaseModel):
4958 class RepoReviewRuleUserGroup(Base, BaseModel):
4931 __tablename__ = 'repo_review_rules_users_groups'
4959 __tablename__ = 'repo_review_rules_users_groups'
4932 __table_args__ = (
4960 __table_args__ = (
4933 base_table_args
4961 base_table_args
4934 )
4962 )
4935
4963
4936 VOTE_RULE_ALL = -1
4964 VOTE_RULE_ALL = -1
4937
4965
4938 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4966 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4939 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4967 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4940 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4968 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4941 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4969 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4942 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4970 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4943 users_group = relationship('UserGroup')
4971 users_group = relationship('UserGroup')
4944
4972
4945 def rule_data(self):
4973 def rule_data(self):
4946 return {
4974 return {
4947 'mandatory': self.mandatory,
4975 'mandatory': self.mandatory,
4948 'vote_rule': self.vote_rule
4976 'vote_rule': self.vote_rule
4949 }
4977 }
4950
4978
4951 @property
4979 @property
4952 def vote_rule_label(self):
4980 def vote_rule_label(self):
4953 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4981 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4954 return 'all must vote'
4982 return 'all must vote'
4955 else:
4983 else:
4956 return 'min. vote {}'.format(self.vote_rule)
4984 return 'min. vote {}'.format(self.vote_rule)
4957
4985
4958
4986
4959 class RepoReviewRule(Base, BaseModel):
4987 class RepoReviewRule(Base, BaseModel):
4960 __tablename__ = 'repo_review_rules'
4988 __tablename__ = 'repo_review_rules'
4961 __table_args__ = (
4989 __table_args__ = (
4962 base_table_args
4990 base_table_args
4963 )
4991 )
4964
4992
4965 repo_review_rule_id = Column(
4993 repo_review_rule_id = Column(
4966 'repo_review_rule_id', Integer(), primary_key=True)
4994 'repo_review_rule_id', Integer(), primary_key=True)
4967 repo_id = Column(
4995 repo_id = Column(
4968 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4996 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4969 repo = relationship('Repository', backref='review_rules')
4997 repo = relationship('Repository', backref='review_rules')
4970
4998
4971 review_rule_name = Column('review_rule_name', String(255))
4999 review_rule_name = Column('review_rule_name', String(255))
4972 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5000 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4973 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5001 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4974 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
5002 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4975
5003
4976 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
5004 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4977 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
5005 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4978 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
5006 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4979 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
5007 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4980
5008
4981 rule_users = relationship('RepoReviewRuleUser')
5009 rule_users = relationship('RepoReviewRuleUser')
4982 rule_user_groups = relationship('RepoReviewRuleUserGroup')
5010 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4983
5011
4984 def _validate_pattern(self, value):
5012 def _validate_pattern(self, value):
4985 re.compile('^' + glob2re(value) + '$')
5013 re.compile('^' + glob2re(value) + '$')
4986
5014
4987 @hybrid_property
5015 @hybrid_property
4988 def source_branch_pattern(self):
5016 def source_branch_pattern(self):
4989 return self._branch_pattern or '*'
5017 return self._branch_pattern or '*'
4990
5018
4991 @source_branch_pattern.setter
5019 @source_branch_pattern.setter
4992 def source_branch_pattern(self, value):
5020 def source_branch_pattern(self, value):
4993 self._validate_pattern(value)
5021 self._validate_pattern(value)
4994 self._branch_pattern = value or '*'
5022 self._branch_pattern = value or '*'
4995
5023
4996 @hybrid_property
5024 @hybrid_property
4997 def target_branch_pattern(self):
5025 def target_branch_pattern(self):
4998 return self._target_branch_pattern or '*'
5026 return self._target_branch_pattern or '*'
4999
5027
5000 @target_branch_pattern.setter
5028 @target_branch_pattern.setter
5001 def target_branch_pattern(self, value):
5029 def target_branch_pattern(self, value):
5002 self._validate_pattern(value)
5030 self._validate_pattern(value)
5003 self._target_branch_pattern = value or '*'
5031 self._target_branch_pattern = value or '*'
5004
5032
5005 @hybrid_property
5033 @hybrid_property
5006 def file_pattern(self):
5034 def file_pattern(self):
5007 return self._file_pattern or '*'
5035 return self._file_pattern or '*'
5008
5036
5009 @file_pattern.setter
5037 @file_pattern.setter
5010 def file_pattern(self, value):
5038 def file_pattern(self, value):
5011 self._validate_pattern(value)
5039 self._validate_pattern(value)
5012 self._file_pattern = value or '*'
5040 self._file_pattern = value or '*'
5013
5041
5014 def matches(self, source_branch, target_branch, files_changed):
5042 def matches(self, source_branch, target_branch, files_changed):
5015 """
5043 """
5016 Check if this review rule matches a branch/files in a pull request
5044 Check if this review rule matches a branch/files in a pull request
5017
5045
5018 :param source_branch: source branch name for the commit
5046 :param source_branch: source branch name for the commit
5019 :param target_branch: target branch name for the commit
5047 :param target_branch: target branch name for the commit
5020 :param files_changed: list of file paths changed in the pull request
5048 :param files_changed: list of file paths changed in the pull request
5021 """
5049 """
5022
5050
5023 source_branch = source_branch or ''
5051 source_branch = source_branch or ''
5024 target_branch = target_branch or ''
5052 target_branch = target_branch or ''
5025 files_changed = files_changed or []
5053 files_changed = files_changed or []
5026
5054
5027 branch_matches = True
5055 branch_matches = True
5028 if source_branch or target_branch:
5056 if source_branch or target_branch:
5029 if self.source_branch_pattern == '*':
5057 if self.source_branch_pattern == '*':
5030 source_branch_match = True
5058 source_branch_match = True
5031 else:
5059 else:
5032 if self.source_branch_pattern.startswith('re:'):
5060 if self.source_branch_pattern.startswith('re:'):
5033 source_pattern = self.source_branch_pattern[3:]
5061 source_pattern = self.source_branch_pattern[3:]
5034 else:
5062 else:
5035 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5063 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
5036 source_branch_regex = re.compile(source_pattern)
5064 source_branch_regex = re.compile(source_pattern)
5037 source_branch_match = bool(source_branch_regex.search(source_branch))
5065 source_branch_match = bool(source_branch_regex.search(source_branch))
5038 if self.target_branch_pattern == '*':
5066 if self.target_branch_pattern == '*':
5039 target_branch_match = True
5067 target_branch_match = True
5040 else:
5068 else:
5041 if self.target_branch_pattern.startswith('re:'):
5069 if self.target_branch_pattern.startswith('re:'):
5042 target_pattern = self.target_branch_pattern[3:]
5070 target_pattern = self.target_branch_pattern[3:]
5043 else:
5071 else:
5044 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5072 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
5045 target_branch_regex = re.compile(target_pattern)
5073 target_branch_regex = re.compile(target_pattern)
5046 target_branch_match = bool(target_branch_regex.search(target_branch))
5074 target_branch_match = bool(target_branch_regex.search(target_branch))
5047
5075
5048 branch_matches = source_branch_match and target_branch_match
5076 branch_matches = source_branch_match and target_branch_match
5049
5077
5050 files_matches = True
5078 files_matches = True
5051 if self.file_pattern != '*':
5079 if self.file_pattern != '*':
5052 files_matches = False
5080 files_matches = False
5053 if self.file_pattern.startswith('re:'):
5081 if self.file_pattern.startswith('re:'):
5054 file_pattern = self.file_pattern[3:]
5082 file_pattern = self.file_pattern[3:]
5055 else:
5083 else:
5056 file_pattern = glob2re(self.file_pattern)
5084 file_pattern = glob2re(self.file_pattern)
5057 file_regex = re.compile(file_pattern)
5085 file_regex = re.compile(file_pattern)
5058 for file_data in files_changed:
5086 for file_data in files_changed:
5059 filename = file_data.get('filename')
5087 filename = file_data.get('filename')
5060
5088
5061 if file_regex.search(filename):
5089 if file_regex.search(filename):
5062 files_matches = True
5090 files_matches = True
5063 break
5091 break
5064
5092
5065 return branch_matches and files_matches
5093 return branch_matches and files_matches
5066
5094
5067 @property
5095 @property
5068 def review_users(self):
5096 def review_users(self):
5069 """ Returns the users which this rule applies to """
5097 """ Returns the users which this rule applies to """
5070
5098
5071 users = collections.OrderedDict()
5099 users = collections.OrderedDict()
5072
5100
5073 for rule_user in self.rule_users:
5101 for rule_user in self.rule_users:
5074 if rule_user.user.active:
5102 if rule_user.user.active:
5075 if rule_user.user not in users:
5103 if rule_user.user not in users:
5076 users[rule_user.user.username] = {
5104 users[rule_user.user.username] = {
5077 'user': rule_user.user,
5105 'user': rule_user.user,
5078 'source': 'user',
5106 'source': 'user',
5079 'source_data': {},
5107 'source_data': {},
5080 'data': rule_user.rule_data()
5108 'data': rule_user.rule_data()
5081 }
5109 }
5082
5110
5083 for rule_user_group in self.rule_user_groups:
5111 for rule_user_group in self.rule_user_groups:
5084 source_data = {
5112 source_data = {
5085 'user_group_id': rule_user_group.users_group.users_group_id,
5113 'user_group_id': rule_user_group.users_group.users_group_id,
5086 'name': rule_user_group.users_group.users_group_name,
5114 'name': rule_user_group.users_group.users_group_name,
5087 'members': len(rule_user_group.users_group.members)
5115 'members': len(rule_user_group.users_group.members)
5088 }
5116 }
5089 for member in rule_user_group.users_group.members:
5117 for member in rule_user_group.users_group.members:
5090 if member.user.active:
5118 if member.user.active:
5091 key = member.user.username
5119 key = member.user.username
5092 if key in users:
5120 if key in users:
5093 # skip this member as we have him already
5121 # skip this member as we have him already
5094 # this prevents from override the "first" matched
5122 # this prevents from override the "first" matched
5095 # users with duplicates in multiple groups
5123 # users with duplicates in multiple groups
5096 continue
5124 continue
5097
5125
5098 users[key] = {
5126 users[key] = {
5099 'user': member.user,
5127 'user': member.user,
5100 'source': 'user_group',
5128 'source': 'user_group',
5101 'source_data': source_data,
5129 'source_data': source_data,
5102 'data': rule_user_group.rule_data()
5130 'data': rule_user_group.rule_data()
5103 }
5131 }
5104
5132
5105 return users
5133 return users
5106
5134
5107 def user_group_vote_rule(self, user_id):
5135 def user_group_vote_rule(self, user_id):
5108
5136
5109 rules = []
5137 rules = []
5110 if not self.rule_user_groups:
5138 if not self.rule_user_groups:
5111 return rules
5139 return rules
5112
5140
5113 for user_group in self.rule_user_groups:
5141 for user_group in self.rule_user_groups:
5114 user_group_members = [x.user_id for x in user_group.users_group.members]
5142 user_group_members = [x.user_id for x in user_group.users_group.members]
5115 if user_id in user_group_members:
5143 if user_id in user_group_members:
5116 rules.append(user_group)
5144 rules.append(user_group)
5117 return rules
5145 return rules
5118
5146
5119 def __repr__(self):
5147 def __repr__(self):
5120 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
5148 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
5121 self.repo_review_rule_id, self.repo)
5149 self.repo_review_rule_id, self.repo)
5122
5150
5123
5151
5124 class ScheduleEntry(Base, BaseModel):
5152 class ScheduleEntry(Base, BaseModel):
5125 __tablename__ = 'schedule_entries'
5153 __tablename__ = 'schedule_entries'
5126 __table_args__ = (
5154 __table_args__ = (
5127 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5155 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
5128 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5156 UniqueConstraint('task_uid', name='s_task_uid_idx'),
5129 base_table_args,
5157 base_table_args,
5130 )
5158 )
5131
5159
5132 schedule_types = ['crontab', 'timedelta', 'integer']
5160 schedule_types = ['crontab', 'timedelta', 'integer']
5133 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5161 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
5134
5162
5135 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5163 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
5136 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5164 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
5137 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5165 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
5138
5166
5139 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5167 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
5140 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5168 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
5141
5169
5142 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5170 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
5143 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5171 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
5144
5172
5145 # task
5173 # task
5146 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5174 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
5147 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5175 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
5148 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5176 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
5149 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5177 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5150
5178
5151 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5179 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5152 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5180 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5153
5181
5154 @hybrid_property
5182 @hybrid_property
5155 def schedule_type(self):
5183 def schedule_type(self):
5156 return self._schedule_type
5184 return self._schedule_type
5157
5185
5158 @schedule_type.setter
5186 @schedule_type.setter
5159 def schedule_type(self, val):
5187 def schedule_type(self, val):
5160 if val not in self.schedule_types:
5188 if val not in self.schedule_types:
5161 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5189 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5162 val, self.schedule_type))
5190 val, self.schedule_type))
5163
5191
5164 self._schedule_type = val
5192 self._schedule_type = val
5165
5193
5166 @classmethod
5194 @classmethod
5167 def get_uid(cls, obj):
5195 def get_uid(cls, obj):
5168 args = obj.task_args
5196 args = obj.task_args
5169 kwargs = obj.task_kwargs
5197 kwargs = obj.task_kwargs
5170 if isinstance(args, JsonRaw):
5198 if isinstance(args, JsonRaw):
5171 try:
5199 try:
5172 args = json.loads(args)
5200 args = json.loads(args)
5173 except ValueError:
5201 except ValueError:
5174 args = tuple()
5202 args = tuple()
5175
5203
5176 if isinstance(kwargs, JsonRaw):
5204 if isinstance(kwargs, JsonRaw):
5177 try:
5205 try:
5178 kwargs = json.loads(kwargs)
5206 kwargs = json.loads(kwargs)
5179 except ValueError:
5207 except ValueError:
5180 kwargs = dict()
5208 kwargs = dict()
5181
5209
5182 dot_notation = obj.task_dot_notation
5210 dot_notation = obj.task_dot_notation
5183 val = '.'.join(map(safe_str, [
5211 val = '.'.join(map(safe_str, [
5184 sorted(dot_notation), args, sorted(kwargs.items())]))
5212 sorted(dot_notation), args, sorted(kwargs.items())]))
5185 return hashlib.sha1(val).hexdigest()
5213 return hashlib.sha1(val).hexdigest()
5186
5214
5187 @classmethod
5215 @classmethod
5188 def get_by_schedule_name(cls, schedule_name):
5216 def get_by_schedule_name(cls, schedule_name):
5189 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5217 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5190
5218
5191 @classmethod
5219 @classmethod
5192 def get_by_schedule_id(cls, schedule_id):
5220 def get_by_schedule_id(cls, schedule_id):
5193 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5221 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5194
5222
5195 @property
5223 @property
5196 def task(self):
5224 def task(self):
5197 return self.task_dot_notation
5225 return self.task_dot_notation
5198
5226
5199 @property
5227 @property
5200 def schedule(self):
5228 def schedule(self):
5201 from rhodecode.lib.celerylib.utils import raw_2_schedule
5229 from rhodecode.lib.celerylib.utils import raw_2_schedule
5202 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5230 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5203 return schedule
5231 return schedule
5204
5232
5205 @property
5233 @property
5206 def args(self):
5234 def args(self):
5207 try:
5235 try:
5208 return list(self.task_args or [])
5236 return list(self.task_args or [])
5209 except ValueError:
5237 except ValueError:
5210 return list()
5238 return list()
5211
5239
5212 @property
5240 @property
5213 def kwargs(self):
5241 def kwargs(self):
5214 try:
5242 try:
5215 return dict(self.task_kwargs or {})
5243 return dict(self.task_kwargs or {})
5216 except ValueError:
5244 except ValueError:
5217 return dict()
5245 return dict()
5218
5246
5219 def _as_raw(self, val):
5247 def _as_raw(self, val):
5220 if hasattr(val, 'de_coerce'):
5248 if hasattr(val, 'de_coerce'):
5221 val = val.de_coerce()
5249 val = val.de_coerce()
5222 if val:
5250 if val:
5223 val = json.dumps(val)
5251 val = json.dumps(val)
5224
5252
5225 return val
5253 return val
5226
5254
5227 @property
5255 @property
5228 def schedule_definition_raw(self):
5256 def schedule_definition_raw(self):
5229 return self._as_raw(self.schedule_definition)
5257 return self._as_raw(self.schedule_definition)
5230
5258
5231 @property
5259 @property
5232 def args_raw(self):
5260 def args_raw(self):
5233 return self._as_raw(self.task_args)
5261 return self._as_raw(self.task_args)
5234
5262
5235 @property
5263 @property
5236 def kwargs_raw(self):
5264 def kwargs_raw(self):
5237 return self._as_raw(self.task_kwargs)
5265 return self._as_raw(self.task_kwargs)
5238
5266
5239 def __repr__(self):
5267 def __repr__(self):
5240 return '<DB:ScheduleEntry({}:{})>'.format(
5268 return '<DB:ScheduleEntry({}:{})>'.format(
5241 self.schedule_entry_id, self.schedule_name)
5269 self.schedule_entry_id, self.schedule_name)
5242
5270
5243
5271
5244 @event.listens_for(ScheduleEntry, 'before_update')
5272 @event.listens_for(ScheduleEntry, 'before_update')
5245 def update_task_uid(mapper, connection, target):
5273 def update_task_uid(mapper, connection, target):
5246 target.task_uid = ScheduleEntry.get_uid(target)
5274 target.task_uid = ScheduleEntry.get_uid(target)
5247
5275
5248
5276
5249 @event.listens_for(ScheduleEntry, 'before_insert')
5277 @event.listens_for(ScheduleEntry, 'before_insert')
5250 def set_task_uid(mapper, connection, target):
5278 def set_task_uid(mapper, connection, target):
5251 target.task_uid = ScheduleEntry.get_uid(target)
5279 target.task_uid = ScheduleEntry.get_uid(target)
5252
5280
5253
5281
5254 class _BaseBranchPerms(BaseModel):
5282 class _BaseBranchPerms(BaseModel):
5255 @classmethod
5283 @classmethod
5256 def compute_hash(cls, value):
5284 def compute_hash(cls, value):
5257 return sha1_safe(value)
5285 return sha1_safe(value)
5258
5286
5259 @hybrid_property
5287 @hybrid_property
5260 def branch_pattern(self):
5288 def branch_pattern(self):
5261 return self._branch_pattern or '*'
5289 return self._branch_pattern or '*'
5262
5290
5263 @hybrid_property
5291 @hybrid_property
5264 def branch_hash(self):
5292 def branch_hash(self):
5265 return self._branch_hash
5293 return self._branch_hash
5266
5294
5267 def _validate_glob(self, value):
5295 def _validate_glob(self, value):
5268 re.compile('^' + glob2re(value) + '$')
5296 re.compile('^' + glob2re(value) + '$')
5269
5297
5270 @branch_pattern.setter
5298 @branch_pattern.setter
5271 def branch_pattern(self, value):
5299 def branch_pattern(self, value):
5272 self._validate_glob(value)
5300 self._validate_glob(value)
5273 self._branch_pattern = value or '*'
5301 self._branch_pattern = value or '*'
5274 # set the Hash when setting the branch pattern
5302 # set the Hash when setting the branch pattern
5275 self._branch_hash = self.compute_hash(self._branch_pattern)
5303 self._branch_hash = self.compute_hash(self._branch_pattern)
5276
5304
5277 def matches(self, branch):
5305 def matches(self, branch):
5278 """
5306 """
5279 Check if this the branch matches entry
5307 Check if this the branch matches entry
5280
5308
5281 :param branch: branch name for the commit
5309 :param branch: branch name for the commit
5282 """
5310 """
5283
5311
5284 branch = branch or ''
5312 branch = branch or ''
5285
5313
5286 branch_matches = True
5314 branch_matches = True
5287 if branch:
5315 if branch:
5288 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5316 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5289 branch_matches = bool(branch_regex.search(branch))
5317 branch_matches = bool(branch_regex.search(branch))
5290
5318
5291 return branch_matches
5319 return branch_matches
5292
5320
5293
5321
5294 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5322 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5295 __tablename__ = 'user_to_repo_branch_permissions'
5323 __tablename__ = 'user_to_repo_branch_permissions'
5296 __table_args__ = (
5324 __table_args__ = (
5297 base_table_args
5325 base_table_args
5298 )
5326 )
5299
5327
5300 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5328 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5301
5329
5302 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5330 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5303 repo = relationship('Repository', backref='user_branch_perms')
5331 repo = relationship('Repository', backref='user_branch_perms')
5304
5332
5305 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5333 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5306 permission = relationship('Permission')
5334 permission = relationship('Permission')
5307
5335
5308 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5336 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5309 user_repo_to_perm = relationship('UserRepoToPerm')
5337 user_repo_to_perm = relationship('UserRepoToPerm')
5310
5338
5311 rule_order = Column('rule_order', Integer(), nullable=False)
5339 rule_order = Column('rule_order', Integer(), nullable=False)
5312 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5340 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5313 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5341 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5314
5342
5315 def __unicode__(self):
5343 def __unicode__(self):
5316 return u'<UserBranchPermission(%s => %r)>' % (
5344 return u'<UserBranchPermission(%s => %r)>' % (
5317 self.user_repo_to_perm, self.branch_pattern)
5345 self.user_repo_to_perm, self.branch_pattern)
5318
5346
5319
5347
5320 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5348 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5321 __tablename__ = 'user_group_to_repo_branch_permissions'
5349 __tablename__ = 'user_group_to_repo_branch_permissions'
5322 __table_args__ = (
5350 __table_args__ = (
5323 base_table_args
5351 base_table_args
5324 )
5352 )
5325
5353
5326 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5354 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5327
5355
5328 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5356 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5329 repo = relationship('Repository', backref='user_group_branch_perms')
5357 repo = relationship('Repository', backref='user_group_branch_perms')
5330
5358
5331 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5359 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5332 permission = relationship('Permission')
5360 permission = relationship('Permission')
5333
5361
5334 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5362 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5335 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5363 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5336
5364
5337 rule_order = Column('rule_order', Integer(), nullable=False)
5365 rule_order = Column('rule_order', Integer(), nullable=False)
5338 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5366 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5339 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5367 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5340
5368
5341 def __unicode__(self):
5369 def __unicode__(self):
5342 return u'<UserBranchPermission(%s => %r)>' % (
5370 return u'<UserBranchPermission(%s => %r)>' % (
5343 self.user_group_repo_to_perm, self.branch_pattern)
5371 self.user_group_repo_to_perm, self.branch_pattern)
5344
5372
5345
5373
5346 class UserBookmark(Base, BaseModel):
5374 class UserBookmark(Base, BaseModel):
5347 __tablename__ = 'user_bookmarks'
5375 __tablename__ = 'user_bookmarks'
5348 __table_args__ = (
5376 __table_args__ = (
5349 UniqueConstraint('user_id', 'bookmark_repo_id'),
5377 UniqueConstraint('user_id', 'bookmark_repo_id'),
5350 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5378 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5351 UniqueConstraint('user_id', 'bookmark_position'),
5379 UniqueConstraint('user_id', 'bookmark_position'),
5352 base_table_args
5380 base_table_args
5353 )
5381 )
5354
5382
5355 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5383 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5356 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5384 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5357 position = Column("bookmark_position", Integer(), nullable=False)
5385 position = Column("bookmark_position", Integer(), nullable=False)
5358 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5386 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5359 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5387 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5360 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5388 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5361
5389
5362 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5390 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5363 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5391 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5364
5392
5365 user = relationship("User")
5393 user = relationship("User")
5366
5394
5367 repository = relationship("Repository")
5395 repository = relationship("Repository")
5368 repository_group = relationship("RepoGroup")
5396 repository_group = relationship("RepoGroup")
5369
5397
5370 @classmethod
5398 @classmethod
5371 def get_by_position_for_user(cls, position, user_id):
5399 def get_by_position_for_user(cls, position, user_id):
5372 return cls.query() \
5400 return cls.query() \
5373 .filter(UserBookmark.user_id == user_id) \
5401 .filter(UserBookmark.user_id == user_id) \
5374 .filter(UserBookmark.position == position).scalar()
5402 .filter(UserBookmark.position == position).scalar()
5375
5403
5376 @classmethod
5404 @classmethod
5377 def get_bookmarks_for_user(cls, user_id, cache=True):
5405 def get_bookmarks_for_user(cls, user_id, cache=True):
5378 bookmarks = cls.query() \
5406 bookmarks = cls.query() \
5379 .filter(UserBookmark.user_id == user_id) \
5407 .filter(UserBookmark.user_id == user_id) \
5380 .options(joinedload(UserBookmark.repository)) \
5408 .options(joinedload(UserBookmark.repository)) \
5381 .options(joinedload(UserBookmark.repository_group)) \
5409 .options(joinedload(UserBookmark.repository_group)) \
5382 .order_by(UserBookmark.position.asc())
5410 .order_by(UserBookmark.position.asc())
5383
5411
5384 if cache:
5412 if cache:
5385 bookmarks = bookmarks.options(
5413 bookmarks = bookmarks.options(
5386 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5414 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5387 )
5415 )
5388
5416
5389 return bookmarks.all()
5417 return bookmarks.all()
5390
5418
5391 def __unicode__(self):
5419 def __unicode__(self):
5392 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5420 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5393
5421
5394
5422
5395 class FileStore(Base, BaseModel):
5423 class FileStore(Base, BaseModel):
5396 __tablename__ = 'file_store'
5424 __tablename__ = 'file_store'
5397 __table_args__ = (
5425 __table_args__ = (
5398 base_table_args
5426 base_table_args
5399 )
5427 )
5400
5428
5401 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5429 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5402 file_uid = Column('file_uid', String(1024), nullable=False)
5430 file_uid = Column('file_uid', String(1024), nullable=False)
5403 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5431 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5404 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5432 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5405 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5433 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5406
5434
5407 # sha256 hash
5435 # sha256 hash
5408 file_hash = Column('file_hash', String(512), nullable=False)
5436 file_hash = Column('file_hash', String(512), nullable=False)
5409 file_size = Column('file_size', BigInteger(), nullable=False)
5437 file_size = Column('file_size', BigInteger(), nullable=False)
5410
5438
5411 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5439 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5412 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5440 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5413 accessed_count = Column('accessed_count', Integer(), default=0)
5441 accessed_count = Column('accessed_count', Integer(), default=0)
5414
5442
5415 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5443 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5416
5444
5417 # if repo/repo_group reference is set, check for permissions
5445 # if repo/repo_group reference is set, check for permissions
5418 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5446 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5419
5447
5420 # hidden defines an attachment that should be hidden from showing in artifact listing
5448 # hidden defines an attachment that should be hidden from showing in artifact listing
5421 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5449 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5422
5450
5423 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5451 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5424 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5452 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5425
5453
5426 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5454 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5427
5455
5428 # scope limited to user, which requester have access to
5456 # scope limited to user, which requester have access to
5429 scope_user_id = Column(
5457 scope_user_id = Column(
5430 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5458 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5431 nullable=True, unique=None, default=None)
5459 nullable=True, unique=None, default=None)
5432 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5460 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5433
5461
5434 # scope limited to user group, which requester have access to
5462 # scope limited to user group, which requester have access to
5435 scope_user_group_id = Column(
5463 scope_user_group_id = Column(
5436 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5464 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5437 nullable=True, unique=None, default=None)
5465 nullable=True, unique=None, default=None)
5438 user_group = relationship('UserGroup', lazy='joined')
5466 user_group = relationship('UserGroup', lazy='joined')
5439
5467
5440 # scope limited to repo, which requester have access to
5468 # scope limited to repo, which requester have access to
5441 scope_repo_id = Column(
5469 scope_repo_id = Column(
5442 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5470 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5443 nullable=True, unique=None, default=None)
5471 nullable=True, unique=None, default=None)
5444 repo = relationship('Repository', lazy='joined')
5472 repo = relationship('Repository', lazy='joined')
5445
5473
5446 # scope limited to repo group, which requester have access to
5474 # scope limited to repo group, which requester have access to
5447 scope_repo_group_id = Column(
5475 scope_repo_group_id = Column(
5448 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5476 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5449 nullable=True, unique=None, default=None)
5477 nullable=True, unique=None, default=None)
5450 repo_group = relationship('RepoGroup', lazy='joined')
5478 repo_group = relationship('RepoGroup', lazy='joined')
5451
5479
5452 @classmethod
5480 @classmethod
5453 def get_by_store_uid(cls, file_store_uid, safe=False):
5481 def get_by_store_uid(cls, file_store_uid, safe=False):
5454 if safe:
5482 if safe:
5455 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5483 return FileStore.query().filter(FileStore.file_uid == file_store_uid).first()
5456 else:
5484 else:
5457 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5485 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5458
5486
5459 @classmethod
5487 @classmethod
5460 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5488 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5461 file_description='', enabled=True, hidden=False, check_acl=True,
5489 file_description='', enabled=True, hidden=False, check_acl=True,
5462 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5490 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5463
5491
5464 store_entry = FileStore()
5492 store_entry = FileStore()
5465 store_entry.file_uid = file_uid
5493 store_entry.file_uid = file_uid
5466 store_entry.file_display_name = file_display_name
5494 store_entry.file_display_name = file_display_name
5467 store_entry.file_org_name = filename
5495 store_entry.file_org_name = filename
5468 store_entry.file_size = file_size
5496 store_entry.file_size = file_size
5469 store_entry.file_hash = file_hash
5497 store_entry.file_hash = file_hash
5470 store_entry.file_description = file_description
5498 store_entry.file_description = file_description
5471
5499
5472 store_entry.check_acl = check_acl
5500 store_entry.check_acl = check_acl
5473 store_entry.enabled = enabled
5501 store_entry.enabled = enabled
5474 store_entry.hidden = hidden
5502 store_entry.hidden = hidden
5475
5503
5476 store_entry.user_id = user_id
5504 store_entry.user_id = user_id
5477 store_entry.scope_user_id = scope_user_id
5505 store_entry.scope_user_id = scope_user_id
5478 store_entry.scope_repo_id = scope_repo_id
5506 store_entry.scope_repo_id = scope_repo_id
5479 store_entry.scope_repo_group_id = scope_repo_group_id
5507 store_entry.scope_repo_group_id = scope_repo_group_id
5480
5508
5481 return store_entry
5509 return store_entry
5482
5510
5483 @classmethod
5511 @classmethod
5484 def store_metadata(cls, file_store_id, args, commit=True):
5512 def store_metadata(cls, file_store_id, args, commit=True):
5485 file_store = FileStore.get(file_store_id)
5513 file_store = FileStore.get(file_store_id)
5486 if file_store is None:
5514 if file_store is None:
5487 return
5515 return
5488
5516
5489 for section, key, value, value_type in args:
5517 for section, key, value, value_type in args:
5490 has_key = FileStoreMetadata().query() \
5518 has_key = FileStoreMetadata().query() \
5491 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5519 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5492 .filter(FileStoreMetadata.file_store_meta_section == section) \
5520 .filter(FileStoreMetadata.file_store_meta_section == section) \
5493 .filter(FileStoreMetadata.file_store_meta_key == key) \
5521 .filter(FileStoreMetadata.file_store_meta_key == key) \
5494 .scalar()
5522 .scalar()
5495 if has_key:
5523 if has_key:
5496 msg = 'key `{}` already defined under section `{}` for this file.'\
5524 msg = 'key `{}` already defined under section `{}` for this file.'\
5497 .format(key, section)
5525 .format(key, section)
5498 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5526 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5499
5527
5500 # NOTE(marcink): raises ArtifactMetadataBadValueType
5528 # NOTE(marcink): raises ArtifactMetadataBadValueType
5501 FileStoreMetadata.valid_value_type(value_type)
5529 FileStoreMetadata.valid_value_type(value_type)
5502
5530
5503 meta_entry = FileStoreMetadata()
5531 meta_entry = FileStoreMetadata()
5504 meta_entry.file_store = file_store
5532 meta_entry.file_store = file_store
5505 meta_entry.file_store_meta_section = section
5533 meta_entry.file_store_meta_section = section
5506 meta_entry.file_store_meta_key = key
5534 meta_entry.file_store_meta_key = key
5507 meta_entry.file_store_meta_value_type = value_type
5535 meta_entry.file_store_meta_value_type = value_type
5508 meta_entry.file_store_meta_value = value
5536 meta_entry.file_store_meta_value = value
5509
5537
5510 Session().add(meta_entry)
5538 Session().add(meta_entry)
5511
5539
5512 try:
5540 try:
5513 if commit:
5541 if commit:
5514 Session().commit()
5542 Session().commit()
5515 except IntegrityError:
5543 except IntegrityError:
5516 Session().rollback()
5544 Session().rollback()
5517 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5545 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5518
5546
5519 @classmethod
5547 @classmethod
5520 def bump_access_counter(cls, file_uid, commit=True):
5548 def bump_access_counter(cls, file_uid, commit=True):
5521 FileStore().query()\
5549 FileStore().query()\
5522 .filter(FileStore.file_uid == file_uid)\
5550 .filter(FileStore.file_uid == file_uid)\
5523 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5551 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5524 FileStore.accessed_on: datetime.datetime.now()})
5552 FileStore.accessed_on: datetime.datetime.now()})
5525 if commit:
5553 if commit:
5526 Session().commit()
5554 Session().commit()
5527
5555
5528 def __json__(self):
5556 def __json__(self):
5529 data = {
5557 data = {
5530 'filename': self.file_display_name,
5558 'filename': self.file_display_name,
5531 'filename_org': self.file_org_name,
5559 'filename_org': self.file_org_name,
5532 'file_uid': self.file_uid,
5560 'file_uid': self.file_uid,
5533 'description': self.file_description,
5561 'description': self.file_description,
5534 'hidden': self.hidden,
5562 'hidden': self.hidden,
5535 'size': self.file_size,
5563 'size': self.file_size,
5536 'created_on': self.created_on,
5564 'created_on': self.created_on,
5537 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5565 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5538 'downloaded_times': self.accessed_count,
5566 'downloaded_times': self.accessed_count,
5539 'sha256': self.file_hash,
5567 'sha256': self.file_hash,
5540 'metadata': self.file_metadata,
5568 'metadata': self.file_metadata,
5541 }
5569 }
5542
5570
5543 return data
5571 return data
5544
5572
5545 def __repr__(self):
5573 def __repr__(self):
5546 return '<FileStore({})>'.format(self.file_store_id)
5574 return '<FileStore({})>'.format(self.file_store_id)
5547
5575
5548
5576
5549 class FileStoreMetadata(Base, BaseModel):
5577 class FileStoreMetadata(Base, BaseModel):
5550 __tablename__ = 'file_store_metadata'
5578 __tablename__ = 'file_store_metadata'
5551 __table_args__ = (
5579 __table_args__ = (
5552 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5580 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5553 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5581 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5554 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5582 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5555 base_table_args
5583 base_table_args
5556 )
5584 )
5557 SETTINGS_TYPES = {
5585 SETTINGS_TYPES = {
5558 'str': safe_str,
5586 'str': safe_str,
5559 'int': safe_int,
5587 'int': safe_int,
5560 'unicode': safe_unicode,
5588 'unicode': safe_unicode,
5561 'bool': str2bool,
5589 'bool': str2bool,
5562 'list': functools.partial(aslist, sep=',')
5590 'list': functools.partial(aslist, sep=',')
5563 }
5591 }
5564
5592
5565 file_store_meta_id = Column(
5593 file_store_meta_id = Column(
5566 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5594 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5567 primary_key=True)
5595 primary_key=True)
5568 _file_store_meta_section = Column(
5596 _file_store_meta_section = Column(
5569 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5597 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5570 nullable=True, unique=None, default=None)
5598 nullable=True, unique=None, default=None)
5571 _file_store_meta_section_hash = Column(
5599 _file_store_meta_section_hash = Column(
5572 "file_store_meta_section_hash", String(255),
5600 "file_store_meta_section_hash", String(255),
5573 nullable=True, unique=None, default=None)
5601 nullable=True, unique=None, default=None)
5574 _file_store_meta_key = Column(
5602 _file_store_meta_key = Column(
5575 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5603 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5576 nullable=True, unique=None, default=None)
5604 nullable=True, unique=None, default=None)
5577 _file_store_meta_key_hash = Column(
5605 _file_store_meta_key_hash = Column(
5578 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5606 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5579 _file_store_meta_value = Column(
5607 _file_store_meta_value = Column(
5580 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5608 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5581 nullable=True, unique=None, default=None)
5609 nullable=True, unique=None, default=None)
5582 _file_store_meta_value_type = Column(
5610 _file_store_meta_value_type = Column(
5583 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5611 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5584 default='unicode')
5612 default='unicode')
5585
5613
5586 file_store_id = Column(
5614 file_store_id = Column(
5587 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5615 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5588 nullable=True, unique=None, default=None)
5616 nullable=True, unique=None, default=None)
5589
5617
5590 file_store = relationship('FileStore', lazy='joined')
5618 file_store = relationship('FileStore', lazy='joined')
5591
5619
5592 @classmethod
5620 @classmethod
5593 def valid_value_type(cls, value):
5621 def valid_value_type(cls, value):
5594 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5622 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5595 raise ArtifactMetadataBadValueType(
5623 raise ArtifactMetadataBadValueType(
5596 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5624 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5597
5625
5598 @hybrid_property
5626 @hybrid_property
5599 def file_store_meta_section(self):
5627 def file_store_meta_section(self):
5600 return self._file_store_meta_section
5628 return self._file_store_meta_section
5601
5629
5602 @file_store_meta_section.setter
5630 @file_store_meta_section.setter
5603 def file_store_meta_section(self, value):
5631 def file_store_meta_section(self, value):
5604 self._file_store_meta_section = value
5632 self._file_store_meta_section = value
5605 self._file_store_meta_section_hash = _hash_key(value)
5633 self._file_store_meta_section_hash = _hash_key(value)
5606
5634
5607 @hybrid_property
5635 @hybrid_property
5608 def file_store_meta_key(self):
5636 def file_store_meta_key(self):
5609 return self._file_store_meta_key
5637 return self._file_store_meta_key
5610
5638
5611 @file_store_meta_key.setter
5639 @file_store_meta_key.setter
5612 def file_store_meta_key(self, value):
5640 def file_store_meta_key(self, value):
5613 self._file_store_meta_key = value
5641 self._file_store_meta_key = value
5614 self._file_store_meta_key_hash = _hash_key(value)
5642 self._file_store_meta_key_hash = _hash_key(value)
5615
5643
5616 @hybrid_property
5644 @hybrid_property
5617 def file_store_meta_value(self):
5645 def file_store_meta_value(self):
5618 val = self._file_store_meta_value
5646 val = self._file_store_meta_value
5619
5647
5620 if self._file_store_meta_value_type:
5648 if self._file_store_meta_value_type:
5621 # e.g unicode.encrypted == unicode
5649 # e.g unicode.encrypted == unicode
5622 _type = self._file_store_meta_value_type.split('.')[0]
5650 _type = self._file_store_meta_value_type.split('.')[0]
5623 # decode the encrypted value if it's encrypted field type
5651 # decode the encrypted value if it's encrypted field type
5624 if '.encrypted' in self._file_store_meta_value_type:
5652 if '.encrypted' in self._file_store_meta_value_type:
5625 cipher = EncryptedTextValue()
5653 cipher = EncryptedTextValue()
5626 val = safe_unicode(cipher.process_result_value(val, None))
5654 val = safe_unicode(cipher.process_result_value(val, None))
5627 # do final type conversion
5655 # do final type conversion
5628 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5656 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5629 val = converter(val)
5657 val = converter(val)
5630
5658
5631 return val
5659 return val
5632
5660
5633 @file_store_meta_value.setter
5661 @file_store_meta_value.setter
5634 def file_store_meta_value(self, val):
5662 def file_store_meta_value(self, val):
5635 val = safe_unicode(val)
5663 val = safe_unicode(val)
5636 # encode the encrypted value
5664 # encode the encrypted value
5637 if '.encrypted' in self.file_store_meta_value_type:
5665 if '.encrypted' in self.file_store_meta_value_type:
5638 cipher = EncryptedTextValue()
5666 cipher = EncryptedTextValue()
5639 val = safe_unicode(cipher.process_bind_param(val, None))
5667 val = safe_unicode(cipher.process_bind_param(val, None))
5640 self._file_store_meta_value = val
5668 self._file_store_meta_value = val
5641
5669
5642 @hybrid_property
5670 @hybrid_property
5643 def file_store_meta_value_type(self):
5671 def file_store_meta_value_type(self):
5644 return self._file_store_meta_value_type
5672 return self._file_store_meta_value_type
5645
5673
5646 @file_store_meta_value_type.setter
5674 @file_store_meta_value_type.setter
5647 def file_store_meta_value_type(self, val):
5675 def file_store_meta_value_type(self, val):
5648 # e.g unicode.encrypted
5676 # e.g unicode.encrypted
5649 self.valid_value_type(val)
5677 self.valid_value_type(val)
5650 self._file_store_meta_value_type = val
5678 self._file_store_meta_value_type = val
5651
5679
5652 def __json__(self):
5680 def __json__(self):
5653 data = {
5681 data = {
5654 'artifact': self.file_store.file_uid,
5682 'artifact': self.file_store.file_uid,
5655 'section': self.file_store_meta_section,
5683 'section': self.file_store_meta_section,
5656 'key': self.file_store_meta_key,
5684 'key': self.file_store_meta_key,
5657 'value': self.file_store_meta_value,
5685 'value': self.file_store_meta_value,
5658 }
5686 }
5659
5687
5660 return data
5688 return data
5661
5689
5662 def __repr__(self):
5690 def __repr__(self):
5663 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5691 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5664 self.file_store_meta_key, self.file_store_meta_value)
5692 self.file_store_meta_key, self.file_store_meta_value)
5665
5693
5666
5694
5667 class DbMigrateVersion(Base, BaseModel):
5695 class DbMigrateVersion(Base, BaseModel):
5668 __tablename__ = 'db_migrate_version'
5696 __tablename__ = 'db_migrate_version'
5669 __table_args__ = (
5697 __table_args__ = (
5670 base_table_args,
5698 base_table_args,
5671 )
5699 )
5672
5700
5673 repository_id = Column('repository_id', String(250), primary_key=True)
5701 repository_id = Column('repository_id', String(250), primary_key=True)
5674 repository_path = Column('repository_path', Text)
5702 repository_path = Column('repository_path', Text)
5675 version = Column('version', Integer)
5703 version = Column('version', Integer)
5676
5704
5677 @classmethod
5705 @classmethod
5678 def set_version(cls, version):
5706 def set_version(cls, version):
5679 """
5707 """
5680 Helper for forcing a different version, usually for debugging purposes via ishell.
5708 Helper for forcing a different version, usually for debugging purposes via ishell.
5681 """
5709 """
5682 ver = DbMigrateVersion.query().first()
5710 ver = DbMigrateVersion.query().first()
5683 ver.version = version
5711 ver.version = version
5684 Session().commit()
5712 Session().commit()
5685
5713
5686
5714
5687 class DbSession(Base, BaseModel):
5715 class DbSession(Base, BaseModel):
5688 __tablename__ = 'db_session'
5716 __tablename__ = 'db_session'
5689 __table_args__ = (
5717 __table_args__ = (
5690 base_table_args,
5718 base_table_args,
5691 )
5719 )
5692
5720
5693 def __repr__(self):
5721 def __repr__(self):
5694 return '<DB:DbSession({})>'.format(self.id)
5722 return '<DB:DbSession({})>'.format(self.id)
5695
5723
5696 id = Column('id', Integer())
5724 id = Column('id', Integer())
5697 namespace = Column('namespace', String(255), primary_key=True)
5725 namespace = Column('namespace', String(255), primary_key=True)
5698 accessed = Column('accessed', DateTime, nullable=False)
5726 accessed = Column('accessed', DateTime, nullable=False)
5699 created = Column('created', DateTime, nullable=False)
5727 created = Column('created', DateTime, nullable=False)
5700 data = Column('data', PickleType, nullable=False)
5728 data = Column('data', PickleType, nullable=False)
@@ -1,400 +1,400 b''
1
1
2 /** MODAL **/
2 /** MODAL **/
3 .modal-open {
3 .modal-open {
4 overflow:hidden;
4 overflow:hidden;
5 }
5 }
6 body.modal-open, .modal-open .navbar-fixed-top, .modal-open .navbar-fixed-bottom {
6 body.modal-open, .modal-open .navbar-fixed-top, .modal-open .navbar-fixed-bottom {
7 margin-right:15px;
7 margin-right:15px;
8 }
8 }
9 .modal {
9 .modal {
10 position:fixed;
10 position:fixed;
11 top:0;
11 top:0;
12 right:0;
12 right:0;
13 bottom:0;
13 bottom:0;
14 left:0;
14 left:0;
15 z-index:1040;
15 z-index:1040;
16 display:none;
16 display:none;
17 overflow-y:scroll;
17 overflow-y:scroll;
18 &.fade .modal-dialog {
18 &.fade .modal-dialog {
19 -webkit-transform:translate(0,-25%);
19 -webkit-transform:translate(0,-25%);
20 -ms-transform:translate(0,-25%);
20 -ms-transform:translate(0,-25%);
21 transform:translate(0,-25%);
21 transform:translate(0,-25%);
22 -webkit-transition:-webkit-transform 0.3s ease-out;
22 -webkit-transition:-webkit-transform 0.3s ease-out;
23 -moz-transition:-moz-transform 0.3s ease-out;
23 -moz-transition:-moz-transform 0.3s ease-out;
24 -o-transition:-o-transform 0.3s ease-out;
24 -o-transition:-o-transform 0.3s ease-out;
25 transition:transform 0.3s ease-out;
25 transition:transform 0.3s ease-out;
26 }
26 }
27 &.in .modal-dialog {
27 &.in .modal-dialog {
28 -webkit-transform:translate(0,0);
28 -webkit-transform:translate(0,0);
29 -ms-transform:translate(0,0);
29 -ms-transform:translate(0,0);
30 transform:translate(0,0);
30 transform:translate(0,0);
31 }
31 }
32 }
32 }
33 .modal-dialog {
33 .modal-dialog {
34 z-index:1050;
34 z-index:1050;
35 width:auto;
35 width:auto;
36 padding:10px;
36 padding:10px;
37 margin-right:auto;
37 margin-right:auto;
38 margin-left:auto;
38 margin-left:auto;
39 }
39 }
40 .modal-content {
40 .modal-content {
41 position:relative;
41 position:relative;
42 background-color:#ffffff;
42 background-color:#ffffff;
43 border: @border-thickness solid rgba(0,0,0,0.2);
43 border: @border-thickness solid rgba(0,0,0,0.2);
44 .border-radius(@border-radius);
44 .border-radius(@border-radius);
45 outline:none;
45 outline:none;
46 -webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);
46 -webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);
47 box-shadow:0 3px 9px rgba(0,0,0,0.5);
47 box-shadow:0 3px 9px rgba(0,0,0,0.5);
48 background-clip:padding-box;
48 background-clip:padding-box;
49 }
49 }
50 .modal-backdrop {
50 .modal-backdrop {
51 position:fixed;
51 position:fixed;
52 top:0;
52 top:0;
53 right:0;
53 right:0;
54 bottom:0;
54 bottom:0;
55 left:0;
55 left:0;
56 z-index:1030;
56 z-index:1030;
57 background-color:#000000;
57 background-color:#000000;
58
58
59 &.modal-backdrop.fade {
59 &.modal-backdrop.fade {
60 opacity:0;
60 opacity:0;
61 filter:alpha(opacity=0);
61 filter:alpha(opacity=0);
62 }
62 }
63 &.in {
63 &.in {
64 opacity:0.5;
64 opacity:0.5;
65 filter:alpha(opacity=50);
65 filter:alpha(opacity=50);
66 }
66 }
67 }
67 }
68 .modal-header {
68 .modal-header {
69 min-height:16.428571429px;
69 min-height:16.428571429px;
70 padding:15px;
70 padding:15px;
71 border-bottom: @border-thickness solid @grey6;
71 border-bottom: @border-thickness solid @grey6;
72 .close {
72 .close {
73 margin-top:-2px;
73 margin-top:-2px;
74 }
74 }
75 }
75 }
76 .modal-title {
76 .modal-title {
77 margin:0;
77 margin:0;
78 line-height:1.428571429;
78 line-height:1.428571429;
79 }
79 }
80 .modal-body {
80 .modal-body {
81 position:relative;
81 position:relative;
82 padding:20px;
82 padding:20px;
83 }
83 }
84 .modal-footer {
84 .modal-footer {
85 padding:19px 20px 20px;
85 padding:19px 20px 20px;
86 margin-top:15px;
86 margin-top:15px;
87 text-align:right;
87 text-align:right;
88 border-top:1px solid #e5e5e5;
88 border-top:1px solid #e5e5e5;
89 .btn + .btn {
89 .btn + .btn {
90 margin-bottom:0;
90 margin-bottom:0;
91 margin-left:5px;
91 margin-left:5px;
92 }
92 }
93 .btn-group .btn + .btn {
93 .btn-group .btn + .btn {
94 margin-left:-1px;
94 margin-left:-1px;
95 }
95 }
96 .btn-block + .btn-block {
96 .btn-block + .btn-block {
97 margin-left:0;
97 margin-left:0;
98 }
98 }
99 &:before {
99 &:before {
100 display:table;
100 display:table;
101 content:" ";
101 content:" ";
102 }
102 }
103 &:after {
103 &:after {
104 display:table;
104 display:table;
105 content:" ";
105 content:" ";
106 clear:both;
106 clear:both;
107 }
107 }
108 }
108 }
109
109
110 /** MARKDOWN styling **/
110 /** MARKDOWN styling **/
111 div.markdown-block {
111 div.markdown-block {
112 clear: both;
112 clear: both;
113 overflow: hidden;
113 overflow: hidden;
114 margin: 0;
114 margin: 0;
115 padding: 3px 15px 3px;
115 padding: 3px 15px 3px;
116 }
116 }
117
117
118 div.markdown-block h1,
118 div.markdown-block h1,
119 div.markdown-block h2,
119 div.markdown-block h2,
120 div.markdown-block h3,
120 div.markdown-block h3,
121 div.markdown-block h4,
121 div.markdown-block h4,
122 div.markdown-block h5,
122 div.markdown-block h5,
123 div.markdown-block h6 {
123 div.markdown-block h6 {
124 border-bottom: none !important;
124 border-bottom: none !important;
125 padding: 0 !important;
125 padding: 0 !important;
126 overflow: visible !important;
126 overflow: visible !important;
127 }
127 }
128
128
129 div.markdown-block h1,
129 div.markdown-block h1,
130 div.markdown-block h2 {
130 div.markdown-block h2 {
131 border-bottom: 1px #e6e5e5 solid !important;
131 border-bottom: 1px #e6e5e5 solid !important;
132 }
132 }
133
133
134 div.markdown-block h1 {
134 div.markdown-block h1 {
135 font-size: 32px;
135 font-size: 32px;
136 margin: 15px 0 15px 0 !important;
136 margin: 15px 0 15px 0 !important;
137 }
137 }
138
138
139 div.markdown-block h2 {
139 div.markdown-block h2 {
140 font-size: 24px !important;
140 font-size: 24px !important;
141 margin: 34px 0 10px 0 !important;
141 margin: 34px 0 10px 0 !important;
142 }
142 }
143
143
144 div.markdown-block h3 {
144 div.markdown-block h3 {
145 font-size: 18px !important;
145 font-size: 18px !important;
146 margin: 30px 0 8px 0 !important;
146 margin: 30px 0 8px 0 !important;
147 padding-bottom: 2px !important;
147 padding-bottom: 2px !important;
148 }
148 }
149
149
150 div.markdown-block h4 {
150 div.markdown-block h4 {
151 font-size: 13px !important;
151 font-size: 13px !important;
152 margin: 18px 0 3px 0 !important;
152 margin: 18px 0 3px 0 !important;
153 }
153 }
154
154
155 div.markdown-block h5 {
155 div.markdown-block h5 {
156 font-size: 12px !important;
156 font-size: 12px !important;
157 margin: 15px 0 3px 0 !important;
157 margin: 15px 0 3px 0 !important;
158 }
158 }
159
159
160 div.markdown-block h6 {
160 div.markdown-block h6 {
161 font-size: 12px;
161 font-size: 12px;
162 color: #777777;
162 color: #777777;
163 margin: 15px 0 3px 0 !important;
163 margin: 15px 0 3px 0 !important;
164 }
164 }
165
165
166 div.markdown-block hr {
166 div.markdown-block hr {
167 border: 0;
167 border: 0;
168 color: #e6e5e5;
168 color: #e6e5e5;
169 background-color: #e6e5e5;
169 background-color: #e6e5e5;
170 height: 3px;
170 height: 3px;
171 margin-bottom: 13px;
171 margin-bottom: 13px;
172 }
172 }
173
173
174 div.markdown-block blockquote {
174 div.markdown-block blockquote {
175 color: #424242 !important;
175 color: #424242 !important;
176 padding: 8px 21px;
176 padding: 8px 21px;
177 margin: 12px 0;
177 margin: 12px 0;
178 border-left: 4px solid @grey6;
178 border-left: 4px solid @grey6;
179 }
179 }
180
180
181 div.markdown-block blockquote p {
181 div.markdown-block blockquote p {
182 color: #424242 !important;
182 color: #424242 !important;
183 padding: 0 !important;
183 padding: 0 !important;
184 margin: 0 !important;
184 margin: 0 !important;
185 line-height: 1.5;
185 line-height: 1.5;
186 }
186 }
187
187
188
188
189 div.markdown-block ol,
189 div.markdown-block ol,
190 div.markdown-block ul,
190 div.markdown-block ul,
191 div.markdown-block p,
191 div.markdown-block p,
192 div.markdown-block blockquote,
192 div.markdown-block blockquote,
193 div.markdown-block dl,
193 div.markdown-block dl,
194 div.markdown-block li,
194 div.markdown-block li,
195 div.markdown-block table {
195 div.markdown-block table {
196 color: #424242 !important;
196 color: #424242 !important;
197 font-size: 13px !important;
197 font-size: 13px !important;
198 font-family: @text-regular;
198 font-family: @text-regular;
199 font-weight: normal !important;
199 font-weight: normal !important;
200 overflow: visible !important;
200 overflow: visible !important;
201 }
201 }
202
202
203 div.markdown-block pre {
203 div.markdown-block pre {
204 margin: 3px 0px 13px 0px !important;
204 margin: 3px 0px 13px 0px !important;
205 padding: .5em;
205 padding: .5em;
206 color: #424242 !important;
206 color: #424242 !important;
207 font-size: 13px !important;
207 font-size: 13px !important;
208 overflow: visible !important;
208 overflow: visible !important;
209 line-height: 140% !important;
209 line-height: 140% !important;
210 background-color: @grey7;
210 background-color: @grey7;
211 }
211 }
212
212
213 div.markdown-block img {
213 div.markdown-block img {
214 border-style: none;
214 border-style: none;
215 background-color: #fff;
215 background-color: #fff;
216 padding-right: 20px;
216 padding-right: 20px;
217 max-width: 100%;
217 max-width: 100%;
218 }
218 }
219
219
220
220
221 div.markdown-block strong {
221 div.markdown-block strong {
222 font-weight: 600;
222 font-weight: 600;
223 margin: 0;
223 margin: 0;
224 }
224 }
225
225
226 div.markdown-block ul.checkbox,
226 div.markdown-block ul.checkbox,
227 div.markdown-block ol.checkbox {
227 div.markdown-block ol.checkbox {
228 padding-left: 20px !important;
228 padding-left: 20px !important;
229 margin-top: 0px !important;
229 margin-top: 0px !important;
230 margin-bottom: 18px !important;
230 margin-bottom: 18px !important;
231 }
231 }
232
232
233 div.markdown-block ul,
233 div.markdown-block ul,
234 div.markdown-block ol {
234 div.markdown-block ol {
235 padding-left: 30px !important;
235 padding-left: 30px !important;
236 margin-top: 0px !important;
236 margin-top: 0px !important;
237 margin-bottom: 18px !important;
237 margin-bottom: 18px !important;
238 }
238 }
239
239
240 div.markdown-block ul.checkbox li,
240 div.markdown-block ul.checkbox li,
241 div.markdown-block ol.checkbox li {
241 div.markdown-block ol.checkbox li {
242 list-style: none !important;
242 list-style: none !important;
243 margin: 6px !important;
243 margin: 0px !important;
244 padding: 0 !important;
244 padding: 0 !important;
245 }
245 }
246
246
247 div.markdown-block ul li,
247 div.markdown-block ul li,
248 div.markdown-block ol li {
248 div.markdown-block ol li {
249 list-style: disc !important;
249 list-style: disc !important;
250 margin: 6px !important;
250 margin: 0px !important;
251 padding: 0 !important;
251 padding: 0 !important;
252 }
252 }
253
253
254 div.markdown-block ol li {
254 div.markdown-block ol li {
255 list-style: decimal !important;
255 list-style: decimal !important;
256 }
256 }
257
257
258
258
259 div.markdown-block #message {
259 div.markdown-block #message {
260 .border-radius(@border-radius);
260 .border-radius(@border-radius);
261 border: @border-thickness solid @grey5;
261 border: @border-thickness solid @grey5;
262 display: block;
262 display: block;
263 width: 100%;
263 width: 100%;
264 height: 60px;
264 height: 60px;
265 margin: 6px 0px;
265 margin: 6px 0px;
266 }
266 }
267
267
268 div.markdown-block button,
268 div.markdown-block button,
269 div.markdown-block #ws {
269 div.markdown-block #ws {
270 font-size: @basefontsize;
270 font-size: @basefontsize;
271 padding: 4px 6px;
271 padding: 4px 6px;
272 .border-radius(@border-radius);
272 .border-radius(@border-radius);
273 border: @border-thickness solid @grey5;
273 border: @border-thickness solid @grey5;
274 background-color: @grey6;
274 background-color: @grey6;
275 }
275 }
276
276
277 div.markdown-block code,
277 div.markdown-block code,
278 div.markdown-block pre,
278 div.markdown-block pre,
279 div.markdown-block #ws,
279 div.markdown-block #ws,
280 div.markdown-block #message {
280 div.markdown-block #message {
281 font-family: @text-monospace;
281 font-family: @text-monospace;
282 font-size: 11px;
282 font-size: 11px;
283 .border-radius(@border-radius);
283 .border-radius(@border-radius);
284 background-color: white;
284 background-color: white;
285 color: @grey3;
285 color: @grey3;
286 }
286 }
287
287
288
288
289 div.markdown-block code {
289 div.markdown-block code {
290 border: @border-thickness solid @grey6;
290 border: @border-thickness solid @grey6;
291 margin: 0 2px;
291 margin: 0 2px;
292 padding: 0 5px;
292 padding: 0 5px;
293 }
293 }
294
294
295 div.markdown-block pre {
295 div.markdown-block pre {
296 border: @border-thickness solid @grey5;
296 border: @border-thickness solid @grey5;
297 overflow: auto;
297 overflow: auto;
298 padding: .5em;
298 padding: .5em;
299 background-color: @grey7;
299 background-color: @grey7;
300 }
300 }
301
301
302 div.markdown-block pre > code {
302 div.markdown-block pre > code {
303 border: 0;
303 border: 0;
304 margin: 0;
304 margin: 0;
305 padding: 0;
305 padding: 0;
306 }
306 }
307
307
308 /** RST STYLE **/
308 /** RST STYLE **/
309 div.rst-block {
309 div.rst-block {
310 clear: both;
310 clear: both;
311 overflow: hidden;
311 overflow: hidden;
312 margin: 0;
312 margin: 0;
313 padding: 3px 15px 3px;
313 padding: 3px 15px 3px;
314 }
314 }
315
315
316 div.rst-block h2 {
316 div.rst-block h2 {
317 font-weight: normal;
317 font-weight: normal;
318 }
318 }
319
319
320 div.rst-block h1,
320 div.rst-block h1,
321 div.rst-block h2,
321 div.rst-block h2,
322 div.rst-block h3,
322 div.rst-block h3,
323 div.rst-block h4,
323 div.rst-block h4,
324 div.rst-block h5,
324 div.rst-block h5,
325 div.rst-block h6 {
325 div.rst-block h6 {
326 border-bottom: 0 !important;
326 border-bottom: 0 !important;
327 margin: 0 !important;
327 margin: 0 !important;
328 padding: 0 !important;
328 padding: 0 !important;
329 line-height: 1.5em !important;
329 line-height: 1.5em !important;
330 }
330 }
331
331
332
332
333 div.rst-block h1:first-child {
333 div.rst-block h1:first-child {
334 padding-top: .25em !important;
334 padding-top: .25em !important;
335 }
335 }
336
336
337 div.rst-block h2,
337 div.rst-block h2,
338 div.rst-block h3 {
338 div.rst-block h3 {
339 margin: 1em 0 !important;
339 margin: 1em 0 !important;
340 }
340 }
341
341
342 div.rst-block h1,
342 div.rst-block h1,
343 div.rst-block h2 {
343 div.rst-block h2 {
344 border-bottom: 1px #e6e5e5 solid !important;
344 border-bottom: 1px #e6e5e5 solid !important;
345 }
345 }
346
346
347 div.rst-block h2 {
347 div.rst-block h2 {
348 margin-top: 1.5em !important;
348 margin-top: 1.5em !important;
349 padding-top: .5em !important;
349 padding-top: .5em !important;
350 }
350 }
351
351
352 div.rst-block p {
352 div.rst-block p {
353 color: black !important;
353 color: black !important;
354 margin: 1em 0 !important;
354 margin: 1em 0 !important;
355 line-height: 1.5em !important;
355 line-height: 1.5em !important;
356 }
356 }
357
357
358 div.rst-block ul {
358 div.rst-block ul {
359 list-style: disc !important;
359 list-style: disc !important;
360 margin: 1em 0 1em 2em !important;
360 margin: 1em 0 1em 2em !important;
361 clear: both;
361 clear: both;
362 }
362 }
363
363
364 div.rst-block ol {
364 div.rst-block ol {
365 list-style: decimal;
365 list-style: decimal;
366 margin: 1em 0 1em 2em !important;
366 margin: 1em 0 1em 2em !important;
367 }
367 }
368
368
369 div.rst-block pre,
369 div.rst-block pre,
370 div.rst-block code {
370 div.rst-block code {
371 font: 12px "Bitstream Vera Sans Mono","Courier",monospace;
371 font: 12px "Bitstream Vera Sans Mono","Courier",monospace;
372 }
372 }
373
373
374 div.rst-block code {
374 div.rst-block code {
375 font-size: 12px !important;
375 font-size: 12px !important;
376 background-color: ghostWhite !important;
376 background-color: ghostWhite !important;
377 color: #444 !important;
377 color: #444 !important;
378 padding: 0 .2em !important;
378 padding: 0 .2em !important;
379 border: 1px solid #dedede !important;
379 border: 1px solid #dedede !important;
380 }
380 }
381
381
382 div.rst-block pre code {
382 div.rst-block pre code {
383 padding: 0 !important;
383 padding: 0 !important;
384 font-size: 12px !important;
384 font-size: 12px !important;
385 background-color: #eee !important;
385 background-color: #eee !important;
386 border: none !important;
386 border: none !important;
387 }
387 }
388
388
389 div.rst-block pre {
389 div.rst-block pre {
390 margin: 1em 0;
390 margin: 1em 0;
391 padding: @padding;
391 padding: @padding;
392 border: 1px solid @grey6;
392 border: 1px solid @grey6;
393 .border-radius(@border-radius);
393 .border-radius(@border-radius);
394 overflow: auto;
394 overflow: auto;
395 font-size: 12px;
395 font-size: 12px;
396 color: #444;
396 color: #444;
397 background-color: @grey7;
397 background-color: @grey7;
398 }
398 }
399
399
400
400
@@ -1,3062 +1,3056 b''
1 //Primary CSS
1 //Primary CSS
2
2
3 //--- IMPORTS ------------------//
3 //--- IMPORTS ------------------//
4
4
5 @import 'helpers';
5 @import 'helpers';
6 @import 'mixins';
6 @import 'mixins';
7 @import 'rcicons';
7 @import 'rcicons';
8 @import 'variables';
8 @import 'variables';
9 @import 'bootstrap-variables';
9 @import 'bootstrap-variables';
10 @import 'form-bootstrap';
10 @import 'form-bootstrap';
11 @import 'codemirror';
11 @import 'codemirror';
12 @import 'legacy_code_styles';
12 @import 'legacy_code_styles';
13 @import 'readme-box';
13 @import 'readme-box';
14 @import 'progress-bar';
14 @import 'progress-bar';
15
15
16 @import 'type';
16 @import 'type';
17 @import 'alerts';
17 @import 'alerts';
18 @import 'buttons';
18 @import 'buttons';
19 @import 'tags';
19 @import 'tags';
20 @import 'code-block';
20 @import 'code-block';
21 @import 'examples';
21 @import 'examples';
22 @import 'login';
22 @import 'login';
23 @import 'main-content';
23 @import 'main-content';
24 @import 'select2';
24 @import 'select2';
25 @import 'comments';
25 @import 'comments';
26 @import 'panels-bootstrap';
26 @import 'panels-bootstrap';
27 @import 'panels';
27 @import 'panels';
28 @import 'deform';
28 @import 'deform';
29 @import 'tooltips';
29 @import 'tooltips';
30 @import 'sweetalert2';
30 @import 'sweetalert2';
31
31
32
32
33 //--- BASE ------------------//
33 //--- BASE ------------------//
34 .noscript-error {
34 .noscript-error {
35 top: 0;
35 top: 0;
36 left: 0;
36 left: 0;
37 width: 100%;
37 width: 100%;
38 z-index: 101;
38 z-index: 101;
39 text-align: center;
39 text-align: center;
40 font-size: 120%;
40 font-size: 120%;
41 color: white;
41 color: white;
42 background-color: @alert2;
42 background-color: @alert2;
43 padding: 5px 0 5px 0;
43 padding: 5px 0 5px 0;
44 font-weight: @text-semibold-weight;
44 font-weight: @text-semibold-weight;
45 font-family: @text-semibold;
45 font-family: @text-semibold;
46 }
46 }
47
47
48 html {
48 html {
49 display: table;
49 display: table;
50 height: 100%;
50 height: 100%;
51 width: 100%;
51 width: 100%;
52 }
52 }
53
53
54 body {
54 body {
55 display: table-cell;
55 display: table-cell;
56 width: 100%;
56 width: 100%;
57 }
57 }
58
58
59 //--- LAYOUT ------------------//
59 //--- LAYOUT ------------------//
60
60
61 .hidden{
61 .hidden{
62 display: none !important;
62 display: none !important;
63 }
63 }
64
64
65 .box{
65 .box{
66 float: left;
66 float: left;
67 width: 100%;
67 width: 100%;
68 }
68 }
69
69
70 .browser-header {
70 .browser-header {
71 clear: both;
71 clear: both;
72 }
72 }
73 .main {
73 .main {
74 clear: both;
74 clear: both;
75 padding:0 0 @pagepadding;
75 padding:0 0 @pagepadding;
76 height: auto;
76 height: auto;
77
77
78 &:after { //clearfix
78 &:after { //clearfix
79 content:"";
79 content:"";
80 clear:both;
80 clear:both;
81 width:100%;
81 width:100%;
82 display:block;
82 display:block;
83 }
83 }
84 }
84 }
85
85
86 .action-link{
86 .action-link{
87 margin-left: @padding;
87 margin-left: @padding;
88 padding-left: @padding;
88 padding-left: @padding;
89 border-left: @border-thickness solid @border-default-color;
89 border-left: @border-thickness solid @border-default-color;
90 }
90 }
91
91
92 .cursor-pointer {
92 .cursor-pointer {
93 cursor: pointer;
93 cursor: pointer;
94 }
94 }
95
95
96 input + .action-link, .action-link.first{
96 input + .action-link, .action-link.first{
97 border-left: none;
97 border-left: none;
98 }
98 }
99
99
100 .link-disabled {
100 .link-disabled {
101 color: @grey4;
101 color: @grey4;
102 cursor: default;
102 cursor: default;
103 }
103 }
104
104
105 .action-link.last{
105 .action-link.last{
106 margin-right: @padding;
106 margin-right: @padding;
107 padding-right: @padding;
107 padding-right: @padding;
108 }
108 }
109
109
110 .action-link.active,
110 .action-link.active,
111 .action-link.active a{
111 .action-link.active a{
112 color: @grey4;
112 color: @grey4;
113 }
113 }
114
114
115 .action-link.disabled {
115 .action-link.disabled {
116 color: @grey4;
116 color: @grey4;
117 cursor: inherit;
117 cursor: inherit;
118 }
118 }
119
119
120 .grey-link-action {
120 .grey-link-action {
121 cursor: pointer;
121 cursor: pointer;
122 &:hover {
122 &:hover {
123 color: @grey2;
123 color: @grey2;
124 }
124 }
125 color: @grey4;
125 color: @grey4;
126 }
126 }
127
127
128 .clipboard-action {
128 .clipboard-action {
129 cursor: pointer;
129 cursor: pointer;
130 margin-left: 5px;
130 margin-left: 5px;
131
131
132 &:not(.no-grey) {
132 &:not(.no-grey) {
133
133
134 &:hover {
134 &:hover {
135 color: @grey2;
135 color: @grey2;
136 }
136 }
137 color: @grey4;
137 color: @grey4;
138 }
138 }
139 }
139 }
140
140
141 ul.simple-list{
141 ul.simple-list{
142 list-style: none;
142 list-style: none;
143 margin: 0;
143 margin: 0;
144 padding: 0;
144 padding: 0;
145 }
145 }
146
146
147 .main-content {
147 .main-content {
148 padding-bottom: @pagepadding;
148 padding-bottom: @pagepadding;
149 }
149 }
150
150
151 .wide-mode-wrapper {
151 .wide-mode-wrapper {
152 max-width:4000px !important;
152 max-width:4000px !important;
153 }
153 }
154
154
155 .wrapper {
155 .wrapper {
156 position: relative;
156 position: relative;
157 max-width: @wrapper-maxwidth;
157 max-width: @wrapper-maxwidth;
158 margin: 0 auto;
158 margin: 0 auto;
159 }
159 }
160
160
161 #content {
161 #content {
162 clear: both;
162 clear: both;
163 padding: 0 @contentpadding;
163 padding: 0 @contentpadding;
164 }
164 }
165
165
166 .advanced-settings-fields{
166 .advanced-settings-fields{
167 input{
167 input{
168 margin-left: @textmargin;
168 margin-left: @textmargin;
169 margin-right: @padding/2;
169 margin-right: @padding/2;
170 }
170 }
171 }
171 }
172
172
173 .cs_files_title {
173 .cs_files_title {
174 margin: @pagepadding 0 0;
174 margin: @pagepadding 0 0;
175 }
175 }
176
176
177 input.inline[type="file"] {
177 input.inline[type="file"] {
178 display: inline;
178 display: inline;
179 }
179 }
180
180
181 .error_page {
181 .error_page {
182 margin: 10% auto;
182 margin: 10% auto;
183
183
184 h1 {
184 h1 {
185 color: @grey2;
185 color: @grey2;
186 }
186 }
187
187
188 .alert {
188 .alert {
189 margin: @padding 0;
189 margin: @padding 0;
190 }
190 }
191
191
192 .error-branding {
192 .error-branding {
193 color: @grey4;
193 color: @grey4;
194 font-weight: @text-semibold-weight;
194 font-weight: @text-semibold-weight;
195 font-family: @text-semibold;
195 font-family: @text-semibold;
196 }
196 }
197
197
198 .error_message {
198 .error_message {
199 font-family: @text-regular;
199 font-family: @text-regular;
200 }
200 }
201
201
202 .sidebar {
202 .sidebar {
203 min-height: 275px;
203 min-height: 275px;
204 margin: 0;
204 margin: 0;
205 padding: 0 0 @sidebarpadding @sidebarpadding;
205 padding: 0 0 @sidebarpadding @sidebarpadding;
206 border: none;
206 border: none;
207 }
207 }
208
208
209 .main-content {
209 .main-content {
210 position: relative;
210 position: relative;
211 margin: 0 @sidebarpadding @sidebarpadding;
211 margin: 0 @sidebarpadding @sidebarpadding;
212 padding: 0 0 0 @sidebarpadding;
212 padding: 0 0 0 @sidebarpadding;
213 border-left: @border-thickness solid @grey5;
213 border-left: @border-thickness solid @grey5;
214
214
215 @media (max-width:767px) {
215 @media (max-width:767px) {
216 clear: both;
216 clear: both;
217 width: 100%;
217 width: 100%;
218 margin: 0;
218 margin: 0;
219 border: none;
219 border: none;
220 }
220 }
221 }
221 }
222
222
223 .inner-column {
223 .inner-column {
224 float: left;
224 float: left;
225 width: 29.75%;
225 width: 29.75%;
226 min-height: 150px;
226 min-height: 150px;
227 margin: @sidebarpadding 2% 0 0;
227 margin: @sidebarpadding 2% 0 0;
228 padding: 0 2% 0 0;
228 padding: 0 2% 0 0;
229 border-right: @border-thickness solid @grey5;
229 border-right: @border-thickness solid @grey5;
230
230
231 @media (max-width:767px) {
231 @media (max-width:767px) {
232 clear: both;
232 clear: both;
233 width: 100%;
233 width: 100%;
234 border: none;
234 border: none;
235 }
235 }
236
236
237 ul {
237 ul {
238 padding-left: 1.25em;
238 padding-left: 1.25em;
239 }
239 }
240
240
241 &:last-child {
241 &:last-child {
242 margin: @sidebarpadding 0 0;
242 margin: @sidebarpadding 0 0;
243 border: none;
243 border: none;
244 }
244 }
245
245
246 h4 {
246 h4 {
247 margin: 0 0 @padding;
247 margin: 0 0 @padding;
248 font-weight: @text-semibold-weight;
248 font-weight: @text-semibold-weight;
249 font-family: @text-semibold;
249 font-family: @text-semibold;
250 }
250 }
251 }
251 }
252 }
252 }
253 .error-page-logo {
253 .error-page-logo {
254 width: 130px;
254 width: 130px;
255 height: 160px;
255 height: 160px;
256 }
256 }
257
257
258 // HEADER
258 // HEADER
259 .header {
259 .header {
260
260
261 // TODO: johbo: Fix login pages, so that they work without a min-height
261 // TODO: johbo: Fix login pages, so that they work without a min-height
262 // for the header and then remove the min-height. I chose a smaller value
262 // for the header and then remove the min-height. I chose a smaller value
263 // intentionally here to avoid rendering issues in the main navigation.
263 // intentionally here to avoid rendering issues in the main navigation.
264 min-height: 49px;
264 min-height: 49px;
265 min-width: 1024px;
265 min-width: 1024px;
266
266
267 position: relative;
267 position: relative;
268 vertical-align: bottom;
268 vertical-align: bottom;
269 padding: 0 @header-padding;
269 padding: 0 @header-padding;
270 background-color: @grey1;
270 background-color: @grey1;
271 color: @grey5;
271 color: @grey5;
272
272
273 .title {
273 .title {
274 overflow: visible;
274 overflow: visible;
275 }
275 }
276
276
277 &:before,
277 &:before,
278 &:after {
278 &:after {
279 content: "";
279 content: "";
280 clear: both;
280 clear: both;
281 width: 100%;
281 width: 100%;
282 }
282 }
283
283
284 // TODO: johbo: Avoids breaking "Repositories" chooser
284 // TODO: johbo: Avoids breaking "Repositories" chooser
285 .select2-container .select2-choice .select2-arrow {
285 .select2-container .select2-choice .select2-arrow {
286 display: none;
286 display: none;
287 }
287 }
288 }
288 }
289
289
290 #header-inner {
290 #header-inner {
291 &.title {
291 &.title {
292 margin: 0;
292 margin: 0;
293 }
293 }
294 &:before,
294 &:before,
295 &:after {
295 &:after {
296 content: "";
296 content: "";
297 clear: both;
297 clear: both;
298 }
298 }
299 }
299 }
300
300
301 // Gists
301 // Gists
302 #files_data {
302 #files_data {
303 clear: both; //for firefox
303 clear: both; //for firefox
304 padding-top: 10px;
304 padding-top: 10px;
305 }
305 }
306
306
307 #gistid {
307 #gistid {
308 margin-right: @padding;
308 margin-right: @padding;
309 }
309 }
310
310
311 // Global Settings Editor
311 // Global Settings Editor
312 .textarea.editor {
312 .textarea.editor {
313 float: left;
313 float: left;
314 position: relative;
314 position: relative;
315 max-width: @texteditor-width;
315 max-width: @texteditor-width;
316
316
317 select {
317 select {
318 position: absolute;
318 position: absolute;
319 top:10px;
319 top:10px;
320 right:0;
320 right:0;
321 }
321 }
322
322
323 .CodeMirror {
323 .CodeMirror {
324 margin: 0;
324 margin: 0;
325 }
325 }
326
326
327 .help-block {
327 .help-block {
328 margin: 0 0 @padding;
328 margin: 0 0 @padding;
329 padding:.5em;
329 padding:.5em;
330 background-color: @grey6;
330 background-color: @grey6;
331 &.pre-formatting {
331 &.pre-formatting {
332 white-space: pre;
332 white-space: pre;
333 }
333 }
334 }
334 }
335 }
335 }
336
336
337 ul.auth_plugins {
337 ul.auth_plugins {
338 margin: @padding 0 @padding @legend-width;
338 margin: @padding 0 @padding @legend-width;
339 padding: 0;
339 padding: 0;
340
340
341 li {
341 li {
342 margin-bottom: @padding;
342 margin-bottom: @padding;
343 line-height: 1em;
343 line-height: 1em;
344 list-style-type: none;
344 list-style-type: none;
345
345
346 .auth_buttons .btn {
346 .auth_buttons .btn {
347 margin-right: @padding;
347 margin-right: @padding;
348 }
348 }
349
349
350 }
350 }
351 }
351 }
352
352
353
353
354 // My Account PR list
354 // My Account PR list
355
355
356 #show_closed {
356 #show_closed {
357 margin: 0 1em 0 0;
357 margin: 0 1em 0 0;
358 }
358 }
359
359
360 #pull_request_list_table {
360 #pull_request_list_table {
361 .closed {
361 .closed {
362 background-color: @grey6;
362 background-color: @grey6;
363 }
363 }
364
364
365 .state-creating,
365 .state-creating,
366 .state-updating,
366 .state-updating,
367 .state-merging
367 .state-merging
368 {
368 {
369 background-color: @grey6;
369 background-color: @grey6;
370 }
370 }
371
371
372 .td-status {
372 .td-status {
373 padding-left: .5em;
373 padding-left: .5em;
374 }
374 }
375 .log-container .truncate {
375 .log-container .truncate {
376 height: 2.75em;
376 height: 2.75em;
377 white-space: pre-line;
377 white-space: pre-line;
378 }
378 }
379 table.rctable .user {
379 table.rctable .user {
380 padding-left: 0;
380 padding-left: 0;
381 }
381 }
382 table.rctable {
382 table.rctable {
383 td.td-description,
383 td.td-description,
384 .rc-user {
384 .rc-user {
385 min-width: auto;
385 min-width: auto;
386 }
386 }
387 }
387 }
388 }
388 }
389
389
390 // Pull Requests
390 // Pull Requests
391
391
392 .pullrequests_section_head {
392 .pullrequests_section_head {
393 display: block;
393 display: block;
394 clear: both;
394 clear: both;
395 margin: @padding 0;
395 margin: @padding 0;
396 font-weight: @text-bold-weight;
396 font-weight: @text-bold-weight;
397 font-family: @text-bold;
397 font-family: @text-bold;
398 }
398 }
399
399
400 .pr-commit-flow {
400 .pr-commit-flow {
401 position: relative;
401 position: relative;
402 font-weight: 600;
402 font-weight: 600;
403
403
404 .tag {
404 .tag {
405 display: inline-block;
405 display: inline-block;
406 margin: 0 1em .5em 0;
406 margin: 0 1em .5em 0;
407 }
407 }
408
408
409 .clone-url {
409 .clone-url {
410 display: inline-block;
410 display: inline-block;
411 margin: 0 0 .5em 0;
411 margin: 0 0 .5em 0;
412 padding: 0;
412 padding: 0;
413 line-height: 1.2em;
413 line-height: 1.2em;
414 }
414 }
415 }
415 }
416
416
417 .pr-mergeinfo {
417 .pr-mergeinfo {
418 min-width: 95% !important;
418 min-width: 95% !important;
419 padding: 0 !important;
419 padding: 0 !important;
420 border: 0;
420 border: 0;
421 }
421 }
422 .pr-mergeinfo-copy {
422 .pr-mergeinfo-copy {
423 padding: 0 0;
423 padding: 0 0;
424 }
424 }
425
425
426 .pr-pullinfo {
426 .pr-pullinfo {
427 min-width: 95% !important;
427 min-width: 95% !important;
428 padding: 0 !important;
428 padding: 0 !important;
429 border: 0;
429 border: 0;
430 }
430 }
431 .pr-pullinfo-copy {
431 .pr-pullinfo-copy {
432 padding: 0 0;
432 padding: 0 0;
433 }
433 }
434
434
435 .pr-title-input {
435 .pr-title-input {
436 width: 100%;
436 width: 100%;
437 font-size: 18px;
437 font-size: 18px;
438 margin: 0 0 4px 0;
438 margin: 0 0 4px 0;
439 padding: 0;
439 padding: 0;
440 line-height: 1.7em;
440 line-height: 1.7em;
441 color: @text-color;
441 color: @text-color;
442 letter-spacing: .02em;
442 letter-spacing: .02em;
443 font-weight: @text-bold-weight;
443 font-weight: @text-bold-weight;
444 font-family: @text-bold;
444 font-family: @text-bold;
445
445
446 &:hover {
446 &:hover {
447 box-shadow: none;
447 box-shadow: none;
448 }
448 }
449 }
449 }
450
450
451 #pr-title {
451 #pr-title {
452 input {
452 input {
453 border: 1px transparent;
453 border: 1px transparent;
454 color: black;
454 color: black;
455 opacity: 1;
455 opacity: 1;
456 background: #fff;
456 background: #fff;
457 font-size: 18px;
457 font-size: 18px;
458 }
458 }
459 }
459 }
460
460
461 .pr-title-closed-tag {
461 .pr-title-closed-tag {
462 font-size: 16px;
462 font-size: 16px;
463 }
463 }
464
464
465 #pr-desc {
465 #pr-desc {
466 padding: 10px 0;
466 padding: 10px 0;
467
467
468 .markdown-block {
468 .markdown-block {
469 padding: 0;
469 padding: 0;
470 margin-bottom: -30px;
470 margin-bottom: -30px;
471 }
471 }
472 }
472 }
473
473
474 #pullrequest_title {
474 #pullrequest_title {
475 width: 100%;
475 width: 100%;
476 box-sizing: border-box;
476 box-sizing: border-box;
477 }
477 }
478
478
479 #pr_open_message {
479 #pr_open_message {
480 border: @border-thickness solid #fff;
480 border: @border-thickness solid #fff;
481 border-radius: @border-radius;
481 border-radius: @border-radius;
482 text-align: left;
482 text-align: left;
483 overflow: hidden;
483 overflow: hidden;
484 white-space: pre-line;
484 white-space: pre-line;
485 }
485 }
486
486
487 .pr-details-title-author-pref {
487 .pr-details-title-author-pref {
488 padding-right: 10px
488 padding-right: 10px
489 }
489 }
490
490
491 .label-pr-detail {
491 .label-pr-detail {
492 display: table-cell;
492 display: table-cell;
493 width: 120px;
493 width: 120px;
494 padding-top: 7.5px;
494 padding-top: 7.5px;
495 padding-bottom: 7.5px;
495 padding-bottom: 7.5px;
496 padding-right: 7.5px;
496 padding-right: 7.5px;
497 }
497 }
498
498
499 .source-details ul {
499 .source-details ul {
500 padding: 10px 16px;
500 padding: 10px 16px;
501 }
501 }
502
502
503 .source-details-action {
503 .source-details-action {
504 color: @grey4;
504 color: @grey4;
505 font-size: 11px
505 font-size: 11px
506 }
506 }
507
507
508 .pr-submit-button {
508 .pr-submit-button {
509 float: right;
509 float: right;
510 margin: 0 0 0 5px;
510 margin: 0 0 0 5px;
511 }
511 }
512
512
513 .pr-spacing-container {
513 .pr-spacing-container {
514 padding: 20px;
514 padding: 20px;
515 clear: both
515 clear: both
516 }
516 }
517
517
518 #pr-description-input {
518 #pr-description-input {
519 margin-bottom: 0;
519 margin-bottom: 0;
520 }
520 }
521
521
522 .pr-description-label {
522 .pr-description-label {
523 vertical-align: top;
523 vertical-align: top;
524 }
524 }
525
525
526 #open_edit_pullrequest {
526 #open_edit_pullrequest {
527 padding: 0;
527 padding: 0;
528 }
528 }
529
529
530 #close_edit_pullrequest {
530 #close_edit_pullrequest {
531
531
532 }
532 }
533
533
534 #delete_pullrequest {
534 #delete_pullrequest {
535 clear: inherit;
535 clear: inherit;
536
536
537 form {
537 form {
538 display: inline;
538 display: inline;
539 }
539 }
540
540
541 }
541 }
542
542
543 .perms_section_head {
543 .perms_section_head {
544 min-width: 625px;
544 min-width: 625px;
545
545
546 h2 {
546 h2 {
547 margin-bottom: 0;
547 margin-bottom: 0;
548 }
548 }
549
549
550 .label-checkbox {
550 .label-checkbox {
551 float: left;
551 float: left;
552 }
552 }
553
553
554 &.field {
554 &.field {
555 margin: @space 0 @padding;
555 margin: @space 0 @padding;
556 }
556 }
557
557
558 &:first-child.field {
558 &:first-child.field {
559 margin-top: 0;
559 margin-top: 0;
560
560
561 .label {
561 .label {
562 margin-top: 0;
562 margin-top: 0;
563 padding-top: 0;
563 padding-top: 0;
564 }
564 }
565
565
566 .radios {
566 .radios {
567 padding-top: 0;
567 padding-top: 0;
568 }
568 }
569 }
569 }
570
570
571 .radios {
571 .radios {
572 position: relative;
572 position: relative;
573 width: 505px;
573 width: 505px;
574 }
574 }
575 }
575 }
576
576
577 //--- MODULES ------------------//
577 //--- MODULES ------------------//
578
578
579
579
580 // Server Announcement
580 // Server Announcement
581 #server-announcement {
581 #server-announcement {
582 width: 95%;
582 width: 95%;
583 margin: @padding auto;
583 margin: @padding auto;
584 padding: @padding;
584 padding: @padding;
585 border-width: 2px;
585 border-width: 2px;
586 border-style: solid;
586 border-style: solid;
587 .border-radius(2px);
587 .border-radius(2px);
588 font-weight: @text-bold-weight;
588 font-weight: @text-bold-weight;
589 font-family: @text-bold;
589 font-family: @text-bold;
590
590
591 &.info { border-color: @alert4; background-color: @alert4-inner; }
591 &.info { border-color: @alert4; background-color: @alert4-inner; }
592 &.warning { border-color: @alert3; background-color: @alert3-inner; }
592 &.warning { border-color: @alert3; background-color: @alert3-inner; }
593 &.error { border-color: @alert2; background-color: @alert2-inner; }
593 &.error { border-color: @alert2; background-color: @alert2-inner; }
594 &.success { border-color: @alert1; background-color: @alert1-inner; }
594 &.success { border-color: @alert1; background-color: @alert1-inner; }
595 &.neutral { border-color: @grey3; background-color: @grey6; }
595 &.neutral { border-color: @grey3; background-color: @grey6; }
596 }
596 }
597
597
598 // Fixed Sidebar Column
598 // Fixed Sidebar Column
599 .sidebar-col-wrapper {
599 .sidebar-col-wrapper {
600 padding-left: @sidebar-all-width;
600 padding-left: @sidebar-all-width;
601
601
602 .sidebar {
602 .sidebar {
603 width: @sidebar-width;
603 width: @sidebar-width;
604 margin-left: -@sidebar-all-width;
604 margin-left: -@sidebar-all-width;
605 }
605 }
606 }
606 }
607
607
608 .sidebar-col-wrapper.scw-small {
608 .sidebar-col-wrapper.scw-small {
609 padding-left: @sidebar-small-all-width;
609 padding-left: @sidebar-small-all-width;
610
610
611 .sidebar {
611 .sidebar {
612 width: @sidebar-small-width;
612 width: @sidebar-small-width;
613 margin-left: -@sidebar-small-all-width;
613 margin-left: -@sidebar-small-all-width;
614 }
614 }
615 }
615 }
616
616
617
617
618 // FOOTER
618 // FOOTER
619 #footer {
619 #footer {
620 padding: 0;
620 padding: 0;
621 text-align: center;
621 text-align: center;
622 vertical-align: middle;
622 vertical-align: middle;
623 color: @grey2;
623 color: @grey2;
624 font-size: 11px;
624 font-size: 11px;
625
625
626 p {
626 p {
627 margin: 0;
627 margin: 0;
628 padding: 1em;
628 padding: 1em;
629 line-height: 1em;
629 line-height: 1em;
630 }
630 }
631
631
632 .server-instance { //server instance
632 .server-instance { //server instance
633 display: none;
633 display: none;
634 }
634 }
635
635
636 .title {
636 .title {
637 float: none;
637 float: none;
638 margin: 0 auto;
638 margin: 0 auto;
639 }
639 }
640 }
640 }
641
641
642 button.close {
642 button.close {
643 padding: 0;
643 padding: 0;
644 cursor: pointer;
644 cursor: pointer;
645 background: transparent;
645 background: transparent;
646 border: 0;
646 border: 0;
647 .box-shadow(none);
647 .box-shadow(none);
648 -webkit-appearance: none;
648 -webkit-appearance: none;
649 }
649 }
650
650
651 .close {
651 .close {
652 float: right;
652 float: right;
653 font-size: 21px;
653 font-size: 21px;
654 font-family: @text-bootstrap;
654 font-family: @text-bootstrap;
655 line-height: 1em;
655 line-height: 1em;
656 font-weight: bold;
656 font-weight: bold;
657 color: @grey2;
657 color: @grey2;
658
658
659 &:hover,
659 &:hover,
660 &:focus {
660 &:focus {
661 color: @grey1;
661 color: @grey1;
662 text-decoration: none;
662 text-decoration: none;
663 cursor: pointer;
663 cursor: pointer;
664 }
664 }
665 }
665 }
666
666
667 // GRID
667 // GRID
668 .sorting,
668 .sorting,
669 .sorting_desc,
669 .sorting_desc,
670 .sorting_asc {
670 .sorting_asc {
671 cursor: pointer;
671 cursor: pointer;
672 }
672 }
673 .sorting_desc:after {
673 .sorting_desc:after {
674 content: "\00A0\25B2";
674 content: "\00A0\25B2";
675 font-size: .75em;
675 font-size: .75em;
676 }
676 }
677 .sorting_asc:after {
677 .sorting_asc:after {
678 content: "\00A0\25BC";
678 content: "\00A0\25BC";
679 font-size: .68em;
679 font-size: .68em;
680 }
680 }
681
681
682
682
683 .user_auth_tokens {
683 .user_auth_tokens {
684
684
685 &.truncate {
685 &.truncate {
686 white-space: nowrap;
686 white-space: nowrap;
687 overflow: hidden;
687 overflow: hidden;
688 text-overflow: ellipsis;
688 text-overflow: ellipsis;
689 }
689 }
690
690
691 .fields .field .input {
691 .fields .field .input {
692 margin: 0;
692 margin: 0;
693 }
693 }
694
694
695 input#description {
695 input#description {
696 width: 100px;
696 width: 100px;
697 margin: 0;
697 margin: 0;
698 }
698 }
699
699
700 .drop-menu {
700 .drop-menu {
701 // TODO: johbo: Remove this, should work out of the box when
701 // TODO: johbo: Remove this, should work out of the box when
702 // having multiple inputs inline
702 // having multiple inputs inline
703 margin: 0 0 0 5px;
703 margin: 0 0 0 5px;
704 }
704 }
705 }
705 }
706 #user_list_table {
706 #user_list_table {
707 .closed {
707 .closed {
708 background-color: @grey6;
708 background-color: @grey6;
709 }
709 }
710 }
710 }
711
711
712
712
713 input, textarea {
713 input, textarea {
714 &.disabled {
714 &.disabled {
715 opacity: .5;
715 opacity: .5;
716 }
716 }
717
717
718 &:hover {
718 &:hover {
719 border-color: @grey3;
719 border-color: @grey3;
720 box-shadow: @button-shadow;
720 box-shadow: @button-shadow;
721 }
721 }
722
722
723 &:focus {
723 &:focus {
724 border-color: @rcblue;
724 border-color: @rcblue;
725 box-shadow: @button-shadow;
725 box-shadow: @button-shadow;
726 }
726 }
727 }
727 }
728
728
729 // remove extra padding in firefox
729 // remove extra padding in firefox
730 input::-moz-focus-inner { border:0; padding:0 }
730 input::-moz-focus-inner { border:0; padding:0 }
731
731
732 .adjacent input {
732 .adjacent input {
733 margin-bottom: @padding;
733 margin-bottom: @padding;
734 }
734 }
735
735
736 .permissions_boxes {
736 .permissions_boxes {
737 display: block;
737 display: block;
738 }
738 }
739
739
740 //FORMS
740 //FORMS
741
741
742 .medium-inline,
742 .medium-inline,
743 input#description.medium-inline {
743 input#description.medium-inline {
744 display: inline;
744 display: inline;
745 width: @medium-inline-input-width;
745 width: @medium-inline-input-width;
746 min-width: 100px;
746 min-width: 100px;
747 }
747 }
748
748
749 select {
749 select {
750 //reset
750 //reset
751 -webkit-appearance: none;
751 -webkit-appearance: none;
752 -moz-appearance: none;
752 -moz-appearance: none;
753
753
754 display: inline-block;
754 display: inline-block;
755 height: 28px;
755 height: 28px;
756 width: auto;
756 width: auto;
757 margin: 0 @padding @padding 0;
757 margin: 0 @padding @padding 0;
758 padding: 0 18px 0 8px;
758 padding: 0 18px 0 8px;
759 line-height:1em;
759 line-height:1em;
760 font-size: @basefontsize;
760 font-size: @basefontsize;
761 border: @border-thickness solid @grey5;
761 border: @border-thickness solid @grey5;
762 border-radius: @border-radius;
762 border-radius: @border-radius;
763 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
763 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
764 color: @grey4;
764 color: @grey4;
765 box-shadow: @button-shadow;
765 box-shadow: @button-shadow;
766
766
767 &:after {
767 &:after {
768 content: "\00A0\25BE";
768 content: "\00A0\25BE";
769 }
769 }
770
770
771 &:focus, &:hover {
771 &:focus, &:hover {
772 outline: none;
772 outline: none;
773 border-color: @grey4;
773 border-color: @grey4;
774 color: @rcdarkblue;
774 color: @rcdarkblue;
775 }
775 }
776 }
776 }
777
777
778 option {
778 option {
779 &:focus {
779 &:focus {
780 outline: none;
780 outline: none;
781 }
781 }
782 }
782 }
783
783
784 input,
784 input,
785 textarea {
785 textarea {
786 padding: @input-padding;
786 padding: @input-padding;
787 border: @input-border-thickness solid @border-highlight-color;
787 border: @input-border-thickness solid @border-highlight-color;
788 .border-radius (@border-radius);
788 .border-radius (@border-radius);
789 font-family: @text-light;
789 font-family: @text-light;
790 font-size: @basefontsize;
790 font-size: @basefontsize;
791
791
792 &.input-sm {
792 &.input-sm {
793 padding: 5px;
793 padding: 5px;
794 }
794 }
795
795
796 &#description {
796 &#description {
797 min-width: @input-description-minwidth;
797 min-width: @input-description-minwidth;
798 min-height: 1em;
798 min-height: 1em;
799 padding: 10px;
799 padding: 10px;
800 }
800 }
801 }
801 }
802
802
803 .field-sm {
803 .field-sm {
804 input,
804 input,
805 textarea {
805 textarea {
806 padding: 5px;
806 padding: 5px;
807 }
807 }
808 }
808 }
809
809
810 textarea {
810 textarea {
811 display: block;
811 display: block;
812 clear: both;
812 clear: both;
813 width: 100%;
813 width: 100%;
814 min-height: 100px;
814 min-height: 100px;
815 margin-bottom: @padding;
815 margin-bottom: @padding;
816 .box-sizing(border-box);
816 .box-sizing(border-box);
817 overflow: auto;
817 overflow: auto;
818 }
818 }
819
819
820 label {
820 label {
821 font-family: @text-light;
821 font-family: @text-light;
822 }
822 }
823
823
824 // GRAVATARS
824 // GRAVATARS
825 // centers gravatar on username to the right
825 // centers gravatar on username to the right
826
826
827 .gravatar {
827 .gravatar {
828 display: inline;
828 display: inline;
829 min-width: 16px;
829 min-width: 16px;
830 min-height: 16px;
830 min-height: 16px;
831 margin: -5px 0;
831 margin: -5px 0;
832 padding: 0;
832 padding: 0;
833 line-height: 1em;
833 line-height: 1em;
834 box-sizing: content-box;
834 box-sizing: content-box;
835 border-radius: 50%;
835 border-radius: 50%;
836
836
837 &.gravatar-large {
837 &.gravatar-large {
838 margin: -0.5em .25em -0.5em 0;
838 margin: -0.5em .25em -0.5em 0;
839 }
839 }
840
840
841 & + .user {
841 & + .user {
842 display: inline;
842 display: inline;
843 margin: 0;
843 margin: 0;
844 padding: 0 0 0 .17em;
844 padding: 0 0 0 .17em;
845 line-height: 1em;
845 line-height: 1em;
846 }
846 }
847
847
848 & + .no-margin {
848 & + .no-margin {
849 margin: 0
849 margin: 0
850 }
850 }
851
851
852 }
852 }
853
853
854 .user-inline-data {
854 .user-inline-data {
855 display: inline-block;
855 display: inline-block;
856 float: left;
856 float: left;
857 padding-left: .5em;
857 padding-left: .5em;
858 line-height: 1.3em;
858 line-height: 1.3em;
859 }
859 }
860
860
861 .rc-user { // gravatar + user wrapper
861 .rc-user { // gravatar + user wrapper
862 float: left;
862 float: left;
863 position: relative;
863 position: relative;
864 min-width: 100px;
864 min-width: 100px;
865 max-width: 200px;
865 max-width: 200px;
866 min-height: (@gravatar-size + @border-thickness * 2); // account for border
866 min-height: (@gravatar-size + @border-thickness * 2); // account for border
867 display: block;
867 display: block;
868 padding: 0 0 0 (@gravatar-size + @basefontsize/4);
868 padding: 0 0 0 (@gravatar-size + @basefontsize/4);
869
869
870
870
871 .gravatar {
871 .gravatar {
872 display: block;
872 display: block;
873 position: absolute;
873 position: absolute;
874 top: 0;
874 top: 0;
875 left: 0;
875 left: 0;
876 min-width: @gravatar-size;
876 min-width: @gravatar-size;
877 min-height: @gravatar-size;
877 min-height: @gravatar-size;
878 margin: 0;
878 margin: 0;
879 }
879 }
880
880
881 .user {
881 .user {
882 display: block;
882 display: block;
883 max-width: 175px;
883 max-width: 175px;
884 padding-top: 2px;
884 padding-top: 2px;
885 overflow: hidden;
885 overflow: hidden;
886 text-overflow: ellipsis;
886 text-overflow: ellipsis;
887 }
887 }
888 }
888 }
889
889
890 .gist-gravatar,
890 .gist-gravatar,
891 .journal_container {
891 .journal_container {
892 .gravatar-large {
892 .gravatar-large {
893 margin: 0 .5em -10px 0;
893 margin: 0 .5em -10px 0;
894 }
894 }
895 }
895 }
896
896
897 .gist-type-fields {
897 .gist-type-fields {
898 line-height: 30px;
898 line-height: 30px;
899 height: 30px;
899 height: 30px;
900
900
901 .gist-type-fields-wrapper {
901 .gist-type-fields-wrapper {
902 vertical-align: middle;
902 vertical-align: middle;
903 display: inline-block;
903 display: inline-block;
904 line-height: 25px;
904 line-height: 25px;
905 }
905 }
906 }
906 }
907
907
908 // ADMIN SETTINGS
908 // ADMIN SETTINGS
909
909
910 // Tag Patterns
910 // Tag Patterns
911 .tag_patterns {
911 .tag_patterns {
912 .tag_input {
912 .tag_input {
913 margin-bottom: @padding;
913 margin-bottom: @padding;
914 }
914 }
915 }
915 }
916
916
917 .locked_input {
917 .locked_input {
918 position: relative;
918 position: relative;
919
919
920 input {
920 input {
921 display: inline;
921 display: inline;
922 margin: 3px 5px 0px 0px;
922 margin: 3px 5px 0px 0px;
923 }
923 }
924
924
925 br {
925 br {
926 display: none;
926 display: none;
927 }
927 }
928
928
929 .error-message {
929 .error-message {
930 float: left;
930 float: left;
931 width: 100%;
931 width: 100%;
932 }
932 }
933
933
934 .lock_input_button {
934 .lock_input_button {
935 display: inline;
935 display: inline;
936 }
936 }
937
937
938 .help-block {
938 .help-block {
939 clear: both;
939 clear: both;
940 }
940 }
941 }
941 }
942
942
943 // Notifications
943 // Notifications
944
944
945 .notifications_buttons {
945 .notifications_buttons {
946 margin: 0 0 @space 0;
946 margin: 0 0 @space 0;
947 padding: 0;
947 padding: 0;
948
948
949 .btn {
949 .btn {
950 display: inline-block;
950 display: inline-block;
951 }
951 }
952 }
952 }
953
953
954 .notification-list {
954 .notification-list {
955
955
956 div {
956 div {
957 vertical-align: middle;
957 vertical-align: middle;
958 }
958 }
959
959
960 .container {
960 .container {
961 display: block;
961 display: block;
962 margin: 0 0 @padding 0;
962 margin: 0 0 @padding 0;
963 }
963 }
964
964
965 .delete-notifications {
965 .delete-notifications {
966 margin-left: @padding;
966 margin-left: @padding;
967 text-align: right;
967 text-align: right;
968 cursor: pointer;
968 cursor: pointer;
969 }
969 }
970
970
971 .read-notifications {
971 .read-notifications {
972 margin-left: @padding/2;
972 margin-left: @padding/2;
973 text-align: right;
973 text-align: right;
974 width: 35px;
974 width: 35px;
975 cursor: pointer;
975 cursor: pointer;
976 }
976 }
977
977
978 .icon-minus-sign {
978 .icon-minus-sign {
979 color: @alert2;
979 color: @alert2;
980 }
980 }
981
981
982 .icon-ok-sign {
982 .icon-ok-sign {
983 color: @alert1;
983 color: @alert1;
984 }
984 }
985 }
985 }
986
986
987 .user_settings {
987 .user_settings {
988 float: left;
988 float: left;
989 clear: both;
989 clear: both;
990 display: block;
990 display: block;
991 width: 100%;
991 width: 100%;
992
992
993 .gravatar_box {
993 .gravatar_box {
994 margin-bottom: @padding;
994 margin-bottom: @padding;
995
995
996 &:after {
996 &:after {
997 content: " ";
997 content: " ";
998 clear: both;
998 clear: both;
999 width: 100%;
999 width: 100%;
1000 }
1000 }
1001 }
1001 }
1002
1002
1003 .fields .field {
1003 .fields .field {
1004 clear: both;
1004 clear: both;
1005 }
1005 }
1006 }
1006 }
1007
1007
1008 .advanced_settings {
1008 .advanced_settings {
1009 margin-bottom: @space;
1009 margin-bottom: @space;
1010
1010
1011 .help-block {
1011 .help-block {
1012 margin-left: 0;
1012 margin-left: 0;
1013 }
1013 }
1014
1014
1015 button + .help-block {
1015 button + .help-block {
1016 margin-top: @padding;
1016 margin-top: @padding;
1017 }
1017 }
1018 }
1018 }
1019
1019
1020 // admin settings radio buttons and labels
1020 // admin settings radio buttons and labels
1021 .label-2 {
1021 .label-2 {
1022 float: left;
1022 float: left;
1023 width: @label2-width;
1023 width: @label2-width;
1024
1024
1025 label {
1025 label {
1026 color: @grey1;
1026 color: @grey1;
1027 }
1027 }
1028 }
1028 }
1029 .checkboxes {
1029 .checkboxes {
1030 float: left;
1030 float: left;
1031 width: @checkboxes-width;
1031 width: @checkboxes-width;
1032 margin-bottom: @padding;
1032 margin-bottom: @padding;
1033
1033
1034 .checkbox {
1034 .checkbox {
1035 width: 100%;
1035 width: 100%;
1036
1036
1037 label {
1037 label {
1038 margin: 0;
1038 margin: 0;
1039 padding: 0;
1039 padding: 0;
1040 }
1040 }
1041 }
1041 }
1042
1042
1043 .checkbox + .checkbox {
1043 .checkbox + .checkbox {
1044 display: inline-block;
1044 display: inline-block;
1045 }
1045 }
1046
1046
1047 label {
1047 label {
1048 margin-right: 1em;
1048 margin-right: 1em;
1049 }
1049 }
1050 }
1050 }
1051
1051
1052 // CHANGELOG
1052 // CHANGELOG
1053 .container_header {
1053 .container_header {
1054 float: left;
1054 float: left;
1055 display: block;
1055 display: block;
1056 width: 100%;
1056 width: 100%;
1057 margin: @padding 0 @padding;
1057 margin: @padding 0 @padding;
1058
1058
1059 #filter_changelog {
1059 #filter_changelog {
1060 float: left;
1060 float: left;
1061 margin-right: @padding;
1061 margin-right: @padding;
1062 }
1062 }
1063
1063
1064 .breadcrumbs_light {
1064 .breadcrumbs_light {
1065 display: inline-block;
1065 display: inline-block;
1066 }
1066 }
1067 }
1067 }
1068
1068
1069 .info_box {
1069 .info_box {
1070 float: right;
1070 float: right;
1071 }
1071 }
1072
1072
1073
1073
1074
1074
1075 #graph_content{
1075 #graph_content{
1076
1076
1077 // adjust for table headers so that graph renders properly
1077 // adjust for table headers so that graph renders properly
1078 // #graph_nodes padding - table cell padding
1078 // #graph_nodes padding - table cell padding
1079 padding-top: (@space - (@basefontsize * 2.4));
1079 padding-top: (@space - (@basefontsize * 2.4));
1080
1080
1081 &.graph_full_width {
1081 &.graph_full_width {
1082 width: 100%;
1082 width: 100%;
1083 max-width: 100%;
1083 max-width: 100%;
1084 }
1084 }
1085 }
1085 }
1086
1086
1087 #graph {
1087 #graph {
1088
1088
1089 .pagination-left {
1089 .pagination-left {
1090 float: left;
1090 float: left;
1091 clear: both;
1091 clear: both;
1092 }
1092 }
1093
1093
1094 .log-container {
1094 .log-container {
1095 max-width: 345px;
1095 max-width: 345px;
1096
1096
1097 .message{
1097 .message{
1098 max-width: 340px;
1098 max-width: 340px;
1099 }
1099 }
1100 }
1100 }
1101
1101
1102 .graph-col-wrapper {
1102 .graph-col-wrapper {
1103
1103
1104 #graph_nodes {
1104 #graph_nodes {
1105 width: 100px;
1105 width: 100px;
1106 position: absolute;
1106 position: absolute;
1107 left: 70px;
1107 left: 70px;
1108 z-index: -1;
1108 z-index: -1;
1109 }
1109 }
1110 }
1110 }
1111
1111
1112 .load-more-commits {
1112 .load-more-commits {
1113 text-align: center;
1113 text-align: center;
1114 }
1114 }
1115 .load-more-commits:hover {
1115 .load-more-commits:hover {
1116 background-color: @grey7;
1116 background-color: @grey7;
1117 }
1117 }
1118 .load-more-commits {
1118 .load-more-commits {
1119 a {
1119 a {
1120 display: block;
1120 display: block;
1121 }
1121 }
1122 }
1122 }
1123 }
1123 }
1124
1124
1125 .obsolete-toggle {
1125 .obsolete-toggle {
1126 line-height: 30px;
1126 line-height: 30px;
1127 margin-left: -15px;
1127 margin-left: -15px;
1128 }
1128 }
1129
1129
1130 #rev_range_container, #rev_range_clear, #rev_range_more {
1130 #rev_range_container, #rev_range_clear, #rev_range_more {
1131 margin-top: -5px;
1131 margin-top: -5px;
1132 margin-bottom: -5px;
1132 margin-bottom: -5px;
1133 }
1133 }
1134
1134
1135 #filter_changelog {
1135 #filter_changelog {
1136 float: left;
1136 float: left;
1137 }
1137 }
1138
1138
1139
1139
1140 //--- THEME ------------------//
1140 //--- THEME ------------------//
1141
1141
1142 #logo {
1142 #logo {
1143 float: left;
1143 float: left;
1144 margin: 9px 0 0 0;
1144 margin: 9px 0 0 0;
1145
1145
1146 .header {
1146 .header {
1147 background-color: transparent;
1147 background-color: transparent;
1148 }
1148 }
1149
1149
1150 a {
1150 a {
1151 display: inline-block;
1151 display: inline-block;
1152 }
1152 }
1153
1153
1154 img {
1154 img {
1155 height:30px;
1155 height:30px;
1156 }
1156 }
1157 }
1157 }
1158
1158
1159 .logo-wrapper {
1159 .logo-wrapper {
1160 float:left;
1160 float:left;
1161 }
1161 }
1162
1162
1163 .branding {
1163 .branding {
1164 float: left;
1164 float: left;
1165 padding: 9px 2px;
1165 padding: 9px 2px;
1166 line-height: 1em;
1166 line-height: 1em;
1167 font-size: @navigation-fontsize;
1167 font-size: @navigation-fontsize;
1168
1168
1169 a {
1169 a {
1170 color: @grey5
1170 color: @grey5
1171 }
1171 }
1172 @media screen and (max-width: 1200px) {
1172 @media screen and (max-width: 1200px) {
1173 display: none;
1173 display: none;
1174 }
1174 }
1175 }
1175 }
1176
1176
1177 img {
1177 img {
1178 border: none;
1178 border: none;
1179 outline: none;
1179 outline: none;
1180 }
1180 }
1181 user-profile-header
1181 user-profile-header
1182 label {
1182 label {
1183
1183
1184 input[type="checkbox"] {
1184 input[type="checkbox"] {
1185 margin-right: 1em;
1185 margin-right: 1em;
1186 }
1186 }
1187 input[type="radio"] {
1187 input[type="radio"] {
1188 margin-right: 1em;
1188 margin-right: 1em;
1189 }
1189 }
1190 }
1190 }
1191
1191
1192 .review-status {
1192 .review-status {
1193 &.under_review {
1193 &.under_review {
1194 color: @alert3;
1194 color: @alert3;
1195 }
1195 }
1196 &.approved {
1196 &.approved {
1197 color: @alert1;
1197 color: @alert1;
1198 }
1198 }
1199 &.rejected,
1199 &.rejected,
1200 &.forced_closed{
1200 &.forced_closed{
1201 color: @alert2;
1201 color: @alert2;
1202 }
1202 }
1203 &.not_reviewed {
1203 &.not_reviewed {
1204 color: @grey5;
1204 color: @grey5;
1205 }
1205 }
1206 }
1206 }
1207
1207
1208 .review-status-under_review {
1208 .review-status-under_review {
1209 color: @alert3;
1209 color: @alert3;
1210 }
1210 }
1211 .status-tag-under_review {
1211 .status-tag-under_review {
1212 border-color: @alert3;
1212 border-color: @alert3;
1213 }
1213 }
1214
1214
1215 .review-status-approved {
1215 .review-status-approved {
1216 color: @alert1;
1216 color: @alert1;
1217 }
1217 }
1218 .status-tag-approved {
1218 .status-tag-approved {
1219 border-color: @alert1;
1219 border-color: @alert1;
1220 }
1220 }
1221
1221
1222 .review-status-rejected,
1222 .review-status-rejected,
1223 .review-status-forced_closed {
1223 .review-status-forced_closed {
1224 color: @alert2;
1224 color: @alert2;
1225 }
1225 }
1226 .status-tag-rejected,
1226 .status-tag-rejected,
1227 .status-tag-forced_closed {
1227 .status-tag-forced_closed {
1228 border-color: @alert2;
1228 border-color: @alert2;
1229 }
1229 }
1230
1230
1231 .review-status-not_reviewed {
1231 .review-status-not_reviewed {
1232 color: @grey5;
1232 color: @grey5;
1233 }
1233 }
1234 .status-tag-not_reviewed {
1234 .status-tag-not_reviewed {
1235 border-color: @grey5;
1235 border-color: @grey5;
1236 }
1236 }
1237
1237
1238 .test_pattern_preview {
1238 .test_pattern_preview {
1239 margin: @space 0;
1239 margin: @space 0;
1240
1240
1241 p {
1241 p {
1242 margin-bottom: 0;
1242 margin-bottom: 0;
1243 border-bottom: @border-thickness solid @border-default-color;
1243 border-bottom: @border-thickness solid @border-default-color;
1244 color: @grey3;
1244 color: @grey3;
1245 }
1245 }
1246
1246
1247 .btn {
1247 .btn {
1248 margin-bottom: @padding;
1248 margin-bottom: @padding;
1249 }
1249 }
1250 }
1250 }
1251 #test_pattern_result {
1251 #test_pattern_result {
1252 display: none;
1252 display: none;
1253 &:extend(pre);
1253 &:extend(pre);
1254 padding: .9em;
1254 padding: .9em;
1255 color: @grey3;
1255 color: @grey3;
1256 background-color: @grey7;
1256 background-color: @grey7;
1257 border-right: @border-thickness solid @border-default-color;
1257 border-right: @border-thickness solid @border-default-color;
1258 border-bottom: @border-thickness solid @border-default-color;
1258 border-bottom: @border-thickness solid @border-default-color;
1259 border-left: @border-thickness solid @border-default-color;
1259 border-left: @border-thickness solid @border-default-color;
1260 }
1260 }
1261
1261
1262 #repo_vcs_settings {
1262 #repo_vcs_settings {
1263 #inherit_overlay_vcs_default {
1263 #inherit_overlay_vcs_default {
1264 display: none;
1264 display: none;
1265 }
1265 }
1266 #inherit_overlay_vcs_custom {
1266 #inherit_overlay_vcs_custom {
1267 display: custom;
1267 display: custom;
1268 }
1268 }
1269 &.inherited {
1269 &.inherited {
1270 #inherit_overlay_vcs_default {
1270 #inherit_overlay_vcs_default {
1271 display: block;
1271 display: block;
1272 }
1272 }
1273 #inherit_overlay_vcs_custom {
1273 #inherit_overlay_vcs_custom {
1274 display: none;
1274 display: none;
1275 }
1275 }
1276 }
1276 }
1277 }
1277 }
1278
1278
1279 .issue-tracker-link {
1279 .issue-tracker-link {
1280 color: @rcblue;
1280 color: @rcblue;
1281 }
1281 }
1282
1282
1283 // Issue Tracker Table Show/Hide
1283 // Issue Tracker Table Show/Hide
1284 #repo_issue_tracker {
1284 #repo_issue_tracker {
1285 #inherit_overlay {
1285 #inherit_overlay {
1286 display: none;
1286 display: none;
1287 }
1287 }
1288 #custom_overlay {
1288 #custom_overlay {
1289 display: custom;
1289 display: custom;
1290 }
1290 }
1291 &.inherited {
1291 &.inherited {
1292 #inherit_overlay {
1292 #inherit_overlay {
1293 display: block;
1293 display: block;
1294 }
1294 }
1295 #custom_overlay {
1295 #custom_overlay {
1296 display: none;
1296 display: none;
1297 }
1297 }
1298 }
1298 }
1299 }
1299 }
1300 table.issuetracker {
1300 table.issuetracker {
1301 &.readonly {
1301 &.readonly {
1302 tr, td {
1302 tr, td {
1303 color: @grey3;
1303 color: @grey3;
1304 }
1304 }
1305 }
1305 }
1306 .edit {
1306 .edit {
1307 display: none;
1307 display: none;
1308 }
1308 }
1309 .editopen {
1309 .editopen {
1310 .edit {
1310 .edit {
1311 display: inline;
1311 display: inline;
1312 }
1312 }
1313 .entry {
1313 .entry {
1314 display: none;
1314 display: none;
1315 }
1315 }
1316 }
1316 }
1317 tr td.td-action {
1317 tr td.td-action {
1318 min-width: 117px;
1318 min-width: 117px;
1319 }
1319 }
1320 td input {
1320 td input {
1321 max-width: none;
1321 max-width: none;
1322 min-width: 30px;
1322 min-width: 30px;
1323 width: 80%;
1323 width: 80%;
1324 }
1324 }
1325 .issuetracker_pref input {
1325 .issuetracker_pref input {
1326 width: 40%;
1326 width: 40%;
1327 }
1327 }
1328 input.edit_issuetracker_update {
1328 input.edit_issuetracker_update {
1329 margin-right: 0;
1329 margin-right: 0;
1330 width: auto;
1330 width: auto;
1331 }
1331 }
1332 }
1332 }
1333
1333
1334 table.integrations {
1334 table.integrations {
1335 .td-icon {
1335 .td-icon {
1336 width: 20px;
1336 width: 20px;
1337 .integration-icon {
1337 .integration-icon {
1338 height: 20px;
1338 height: 20px;
1339 width: 20px;
1339 width: 20px;
1340 }
1340 }
1341 }
1341 }
1342 }
1342 }
1343
1343
1344 .integrations {
1344 .integrations {
1345 a.integration-box {
1345 a.integration-box {
1346 color: @text-color;
1346 color: @text-color;
1347 &:hover {
1347 &:hover {
1348 .panel {
1348 .panel {
1349 background: #fbfbfb;
1349 background: #fbfbfb;
1350 }
1350 }
1351 }
1351 }
1352 .integration-icon {
1352 .integration-icon {
1353 width: 30px;
1353 width: 30px;
1354 height: 30px;
1354 height: 30px;
1355 margin-right: 20px;
1355 margin-right: 20px;
1356 float: left;
1356 float: left;
1357 }
1357 }
1358
1358
1359 .panel-body {
1359 .panel-body {
1360 padding: 10px;
1360 padding: 10px;
1361 }
1361 }
1362 .panel {
1362 .panel {
1363 margin-bottom: 10px;
1363 margin-bottom: 10px;
1364 }
1364 }
1365 h2 {
1365 h2 {
1366 display: inline-block;
1366 display: inline-block;
1367 margin: 0;
1367 margin: 0;
1368 min-width: 140px;
1368 min-width: 140px;
1369 }
1369 }
1370 }
1370 }
1371 a.integration-box.dummy-integration {
1371 a.integration-box.dummy-integration {
1372 color: @grey4
1372 color: @grey4
1373 }
1373 }
1374 }
1374 }
1375
1375
1376 //Permissions Settings
1376 //Permissions Settings
1377 #add_perm {
1377 #add_perm {
1378 margin: 0 0 @padding;
1378 margin: 0 0 @padding;
1379 cursor: pointer;
1379 cursor: pointer;
1380 }
1380 }
1381
1381
1382 .perm_ac {
1382 .perm_ac {
1383 input {
1383 input {
1384 width: 95%;
1384 width: 95%;
1385 }
1385 }
1386 }
1386 }
1387
1387
1388 .autocomplete-suggestions {
1388 .autocomplete-suggestions {
1389 width: auto !important; // overrides autocomplete.js
1389 width: auto !important; // overrides autocomplete.js
1390 min-width: 278px;
1390 min-width: 278px;
1391 margin: 0;
1391 margin: 0;
1392 border: @border-thickness solid @grey5;
1392 border: @border-thickness solid @grey5;
1393 border-radius: @border-radius;
1393 border-radius: @border-radius;
1394 color: @grey2;
1394 color: @grey2;
1395 background-color: white;
1395 background-color: white;
1396 }
1396 }
1397
1397
1398 .autocomplete-qfilter-suggestions {
1398 .autocomplete-qfilter-suggestions {
1399 width: auto !important; // overrides autocomplete.js
1399 width: auto !important; // overrides autocomplete.js
1400 max-height: 100% !important;
1400 max-height: 100% !important;
1401 min-width: 376px;
1401 min-width: 376px;
1402 margin: 0;
1402 margin: 0;
1403 border: @border-thickness solid @grey5;
1403 border: @border-thickness solid @grey5;
1404 color: @grey2;
1404 color: @grey2;
1405 background-color: white;
1405 background-color: white;
1406 }
1406 }
1407
1407
1408 .autocomplete-selected {
1408 .autocomplete-selected {
1409 background: #F0F0F0;
1409 background: #F0F0F0;
1410 }
1410 }
1411
1411
1412 .ac-container-wrap {
1412 .ac-container-wrap {
1413 margin: 0;
1413 margin: 0;
1414 padding: 8px;
1414 padding: 8px;
1415 border-bottom: @border-thickness solid @grey5;
1415 border-bottom: @border-thickness solid @grey5;
1416 list-style-type: none;
1416 list-style-type: none;
1417 cursor: pointer;
1417 cursor: pointer;
1418
1418
1419 &:hover {
1419 &:hover {
1420 background-color: @grey7;
1420 background-color: @grey7;
1421 }
1421 }
1422
1422
1423 img {
1423 img {
1424 height: @gravatar-size;
1424 height: @gravatar-size;
1425 width: @gravatar-size;
1425 width: @gravatar-size;
1426 margin-right: 1em;
1426 margin-right: 1em;
1427 }
1427 }
1428
1428
1429 strong {
1429 strong {
1430 font-weight: normal;
1430 font-weight: normal;
1431 }
1431 }
1432 }
1432 }
1433
1433
1434 // Settings Dropdown
1434 // Settings Dropdown
1435 .user-menu .container {
1435 .user-menu .container {
1436 padding: 0 4px;
1436 padding: 0 4px;
1437 margin: 0;
1437 margin: 0;
1438 }
1438 }
1439
1439
1440 .user-menu .gravatar {
1440 .user-menu .gravatar {
1441 cursor: pointer;
1441 cursor: pointer;
1442 }
1442 }
1443
1443
1444 .codeblock {
1444 .codeblock {
1445 margin-bottom: @padding;
1445 margin-bottom: @padding;
1446 clear: both;
1446 clear: both;
1447
1447
1448 .stats {
1448 .stats {
1449 overflow: hidden;
1449 overflow: hidden;
1450 }
1450 }
1451
1451
1452 .message{
1452 .message{
1453 textarea{
1453 textarea{
1454 margin: 0;
1454 margin: 0;
1455 }
1455 }
1456 }
1456 }
1457
1457
1458 .code-header {
1458 .code-header {
1459 .stats {
1459 .stats {
1460 line-height: 2em;
1460 line-height: 2em;
1461
1461
1462 .revision_id {
1462 .revision_id {
1463 margin-left: 0;
1463 margin-left: 0;
1464 }
1464 }
1465 .buttons {
1465 .buttons {
1466 padding-right: 0;
1466 padding-right: 0;
1467 }
1467 }
1468 }
1468 }
1469
1469
1470 .item{
1470 .item{
1471 margin-right: 0.5em;
1471 margin-right: 0.5em;
1472 }
1472 }
1473 }
1473 }
1474
1474
1475 #editor_container {
1475 #editor_container {
1476 position: relative;
1476 position: relative;
1477 margin: @padding 10px;
1477 margin: @padding 10px;
1478 }
1478 }
1479 }
1479 }
1480
1480
1481 #file_history_container {
1481 #file_history_container {
1482 display: none;
1482 display: none;
1483 }
1483 }
1484
1484
1485 .file-history-inner {
1485 .file-history-inner {
1486 margin-bottom: 10px;
1486 margin-bottom: 10px;
1487 }
1487 }
1488
1488
1489 // Pull Requests
1489 // Pull Requests
1490 .summary-details {
1490 .summary-details {
1491 width: 100%;
1491 width: 100%;
1492 }
1492 }
1493 .pr-summary {
1493 .pr-summary {
1494 border-bottom: @border-thickness solid @grey5;
1494 border-bottom: @border-thickness solid @grey5;
1495 margin-bottom: @space;
1495 margin-bottom: @space;
1496 }
1496 }
1497
1497
1498 .reviewers {
1498 .reviewers {
1499 width: 98%;
1499 width: 98%;
1500 }
1500 }
1501
1501
1502 .reviewers ul li {
1502 .reviewers ul li {
1503 position: relative;
1503 position: relative;
1504 width: 100%;
1504 width: 100%;
1505 padding-bottom: 8px;
1505 padding-bottom: 8px;
1506 list-style-type: none;
1506 list-style-type: none;
1507 }
1507 }
1508
1508
1509 .reviewer_entry {
1509 .reviewer_entry {
1510 min-height: 55px;
1510 min-height: 55px;
1511 }
1511 }
1512
1512
1513 .reviewers_member {
1514 width: 100%;
1515 overflow: auto;
1516 }
1517 .reviewer_reason {
1513 .reviewer_reason {
1518 padding-left: 20px;
1514 padding-left: 20px;
1519 line-height: 1.5em;
1515 line-height: 1.5em;
1520 }
1516 }
1521 .reviewer_status {
1517 .reviewer_status {
1522 display: inline-block;
1518 display: inline-block;
1523 width: 25px;
1519 width: 20px;
1524 min-width: 25px;
1520 min-width: 20px;
1525 height: 1.2em;
1521 height: 1.2em;
1526 line-height: 1em;
1522 line-height: 1em;
1527 }
1523 }
1528
1524
1529 .reviewer_name {
1525 .reviewer_name {
1530 display: inline-block;
1526 display: inline-block;
1531 max-width: 83%;
1527 max-width: 83%;
1532 padding-right: 20px;
1528 padding-right: 20px;
1533 vertical-align: middle;
1529 vertical-align: middle;
1534 line-height: 1;
1530 line-height: 1;
1535
1531
1536 .rc-user {
1532 .rc-user {
1537 min-width: 0;
1533 min-width: 0;
1538 margin: -2px 1em 0 0;
1534 margin: -2px 1em 0 0;
1539 }
1535 }
1540
1536
1541 .reviewer {
1537 .reviewer {
1542 float: left;
1538 float: left;
1543 }
1539 }
1544 }
1540 }
1545
1541
1546 .reviewer_member_mandatory {
1542 .reviewer_member_mandatory {
1547 position: absolute;
1548 left: 15px;
1549 top: 8px;
1550 width: 16px;
1543 width: 16px;
1551 font-size: 11px;
1544 font-size: 11px;
1552 margin: 0;
1545 margin: 0;
1553 padding: 0;
1546 padding: 0;
1554 color: black;
1547 color: black;
1548 opacity: 0.4;
1555 }
1549 }
1556
1550
1557 .reviewer_member_mandatory_remove,
1551 .reviewer_member_mandatory_remove,
1558 .reviewer_member_remove {
1552 .reviewer_member_remove {
1559 position: absolute;
1560 right: 0;
1561 top: 0;
1562 width: 16px;
1553 width: 16px;
1563 margin-bottom: 10px;
1564 padding: 0;
1554 padding: 0;
1565 color: black;
1555 color: black;
1566 }
1556 }
1567
1557
1568 .reviewer_member_mandatory_remove {
1558 .reviewer_member_mandatory_remove {
1569 color: @grey4;
1559 color: @grey4;
1570 }
1560 }
1571
1561
1572 .reviewer_member_status {
1562 .reviewer_member_status {
1573 margin-top: 5px;
1563 margin-top: 5px;
1574 }
1564 }
1575 .pr-summary #summary{
1565 .pr-summary #summary{
1576 width: 100%;
1566 width: 100%;
1577 }
1567 }
1578 .pr-summary .action_button:hover {
1568 .pr-summary .action_button:hover {
1579 border: 0;
1569 border: 0;
1580 cursor: pointer;
1570 cursor: pointer;
1581 }
1571 }
1582 .pr-details-title {
1572 .pr-details-title {
1583 height: 20px;
1573 height: 20px;
1584 line-height: 20px;
1574 line-height: 20px;
1585
1575
1586 padding-bottom: 8px;
1576 padding-bottom: 8px;
1587 border-bottom: @border-thickness solid @grey5;
1577 border-bottom: @border-thickness solid @grey5;
1588
1578
1589 .action_button.disabled {
1579 .action_button.disabled {
1590 color: @grey4;
1580 color: @grey4;
1591 cursor: inherit;
1581 cursor: inherit;
1592 }
1582 }
1593 .action_button {
1583 .action_button {
1594 color: @rcblue;
1584 color: @rcblue;
1595 }
1585 }
1596 }
1586 }
1597 .pr-details-content {
1587 .pr-details-content {
1598 margin-top: @textmargin - 5;
1588 margin-top: @textmargin - 5;
1599 margin-bottom: @textmargin - 5;
1589 margin-bottom: @textmargin - 5;
1600 }
1590 }
1601
1591
1602 .pr-reviewer-rules {
1592 .pr-reviewer-rules {
1603 padding: 10px 0px 20px 0px;
1593 padding: 10px 0px 20px 0px;
1604 }
1594 }
1605
1595
1606 .todo-resolved {
1596 .todo-resolved {
1607 text-decoration: line-through;
1597 text-decoration: line-through;
1608 }
1598 }
1609
1599
1610 .todo-table, .comments-table {
1600 .todo-table, .comments-table {
1611 width: 100%;
1601 width: 100%;
1612
1602
1613 td {
1603 td {
1614 padding: 5px 0px;
1604 padding: 5px 0px;
1615 }
1605 }
1616
1606
1617 .td-todo-number {
1607 .td-todo-number {
1618 text-align: left;
1608 text-align: left;
1619 white-space: nowrap;
1609 white-space: nowrap;
1620 width: 15%;
1610 width: 1%;
1611 padding-right: 2px;
1621 }
1612 }
1622
1613
1623 .td-todo-gravatar {
1614 .td-todo-gravatar {
1624 width: 5%;
1615 width: 5%;
1625
1616
1626 img {
1617 img {
1627 margin: -3px 0;
1618 margin: -3px 0;
1628 }
1619 }
1629 }
1620 }
1630
1621
1631 }
1622 }
1632
1623
1633 .todo-comment-text-wrapper {
1624 .todo-comment-text-wrapper {
1634 display: inline-grid;
1625 display: inline-grid;
1635 }
1626 }
1636
1627
1637 .todo-comment-text {
1628 .todo-comment-text {
1638 margin-left: 5px;
1629 margin-left: 5px;
1639 white-space: nowrap;
1630 white-space: nowrap;
1640 overflow: hidden;
1631 overflow: hidden;
1641 text-overflow: ellipsis;
1632 text-overflow: ellipsis;
1642 }
1633 }
1643
1634
1635 table.group_members {
1636 width: 100%
1637 }
1638
1644 .group_members {
1639 .group_members {
1645 margin-top: 0;
1640 margin-top: 0;
1646 padding: 0;
1641 padding: 0;
1647 list-style: outside none none;
1648
1642
1649 img {
1643 img {
1650 height: @gravatar-size;
1644 height: @gravatar-size;
1651 width: @gravatar-size;
1645 width: @gravatar-size;
1652 margin-right: .5em;
1646 margin-right: .5em;
1653 margin-left: 3px;
1647 margin-left: 3px;
1654 }
1648 }
1655
1649
1656 .to-delete {
1650 .to-delete {
1657 .user {
1651 .user {
1658 text-decoration: line-through;
1652 text-decoration: line-through;
1659 }
1653 }
1660 }
1654 }
1661 }
1655 }
1662
1656
1663 .compare_view_commits_title {
1657 .compare_view_commits_title {
1664 .disabled {
1658 .disabled {
1665 cursor: inherit;
1659 cursor: inherit;
1666 &:hover{
1660 &:hover{
1667 background-color: inherit;
1661 background-color: inherit;
1668 color: inherit;
1662 color: inherit;
1669 }
1663 }
1670 }
1664 }
1671 }
1665 }
1672
1666
1673 .subtitle-compare {
1667 .subtitle-compare {
1674 margin: -15px 0px 0px 0px;
1668 margin: -15px 0px 0px 0px;
1675 }
1669 }
1676
1670
1677 // new entry in group_members
1671 // new entry in group_members
1678 .td-author-new-entry {
1672 .td-author-new-entry {
1679 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1673 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1680 }
1674 }
1681
1675
1682 .usergroup_member_remove {
1676 .usergroup_member_remove {
1683 width: 16px;
1677 width: 16px;
1684 margin-bottom: 10px;
1678 margin-bottom: 10px;
1685 padding: 0;
1679 padding: 0;
1686 color: black !important;
1680 color: black !important;
1687 cursor: pointer;
1681 cursor: pointer;
1688 }
1682 }
1689
1683
1690 .reviewer_ac .ac-input {
1684 .reviewer_ac .ac-input {
1691 width: 92%;
1685 width: 92%;
1692 margin-bottom: 1em;
1686 margin-bottom: 1em;
1693 }
1687 }
1694
1688
1695 .compare_view_commits tr{
1689 .compare_view_commits tr{
1696 height: 20px;
1690 height: 20px;
1697 }
1691 }
1698 .compare_view_commits td {
1692 .compare_view_commits td {
1699 vertical-align: top;
1693 vertical-align: top;
1700 padding-top: 10px;
1694 padding-top: 10px;
1701 }
1695 }
1702 .compare_view_commits .author {
1696 .compare_view_commits .author {
1703 margin-left: 5px;
1697 margin-left: 5px;
1704 }
1698 }
1705
1699
1706 .compare_view_commits {
1700 .compare_view_commits {
1707 .color-a {
1701 .color-a {
1708 color: @alert1;
1702 color: @alert1;
1709 }
1703 }
1710
1704
1711 .color-c {
1705 .color-c {
1712 color: @color3;
1706 color: @color3;
1713 }
1707 }
1714
1708
1715 .color-r {
1709 .color-r {
1716 color: @color5;
1710 color: @color5;
1717 }
1711 }
1718
1712
1719 .color-a-bg {
1713 .color-a-bg {
1720 background-color: @alert1;
1714 background-color: @alert1;
1721 }
1715 }
1722
1716
1723 .color-c-bg {
1717 .color-c-bg {
1724 background-color: @alert3;
1718 background-color: @alert3;
1725 }
1719 }
1726
1720
1727 .color-r-bg {
1721 .color-r-bg {
1728 background-color: @alert2;
1722 background-color: @alert2;
1729 }
1723 }
1730
1724
1731 .color-a-border {
1725 .color-a-border {
1732 border: 1px solid @alert1;
1726 border: 1px solid @alert1;
1733 }
1727 }
1734
1728
1735 .color-c-border {
1729 .color-c-border {
1736 border: 1px solid @alert3;
1730 border: 1px solid @alert3;
1737 }
1731 }
1738
1732
1739 .color-r-border {
1733 .color-r-border {
1740 border: 1px solid @alert2;
1734 border: 1px solid @alert2;
1741 }
1735 }
1742
1736
1743 .commit-change-indicator {
1737 .commit-change-indicator {
1744 width: 15px;
1738 width: 15px;
1745 height: 15px;
1739 height: 15px;
1746 position: relative;
1740 position: relative;
1747 left: 15px;
1741 left: 15px;
1748 }
1742 }
1749
1743
1750 .commit-change-content {
1744 .commit-change-content {
1751 text-align: center;
1745 text-align: center;
1752 vertical-align: middle;
1746 vertical-align: middle;
1753 line-height: 15px;
1747 line-height: 15px;
1754 }
1748 }
1755 }
1749 }
1756
1750
1757 .compare_view_filepath {
1751 .compare_view_filepath {
1758 color: @grey1;
1752 color: @grey1;
1759 }
1753 }
1760
1754
1761 .show_more {
1755 .show_more {
1762 display: inline-block;
1756 display: inline-block;
1763 width: 0;
1757 width: 0;
1764 height: 0;
1758 height: 0;
1765 vertical-align: middle;
1759 vertical-align: middle;
1766 content: "";
1760 content: "";
1767 border: 4px solid;
1761 border: 4px solid;
1768 border-right-color: transparent;
1762 border-right-color: transparent;
1769 border-bottom-color: transparent;
1763 border-bottom-color: transparent;
1770 border-left-color: transparent;
1764 border-left-color: transparent;
1771 font-size: 0;
1765 font-size: 0;
1772 }
1766 }
1773
1767
1774 .journal_more .show_more {
1768 .journal_more .show_more {
1775 display: inline;
1769 display: inline;
1776
1770
1777 &:after {
1771 &:after {
1778 content: none;
1772 content: none;
1779 }
1773 }
1780 }
1774 }
1781
1775
1782 .compare_view_commits .collapse_commit:after {
1776 .compare_view_commits .collapse_commit:after {
1783 cursor: pointer;
1777 cursor: pointer;
1784 content: "\00A0\25B4";
1778 content: "\00A0\25B4";
1785 margin-left: -3px;
1779 margin-left: -3px;
1786 font-size: 17px;
1780 font-size: 17px;
1787 color: @grey4;
1781 color: @grey4;
1788 }
1782 }
1789
1783
1790 .diff_links {
1784 .diff_links {
1791 margin-left: 8px;
1785 margin-left: 8px;
1792 }
1786 }
1793
1787
1794 #pull_request_overview {
1788 #pull_request_overview {
1795 div.ancestor {
1789 div.ancestor {
1796 margin: -33px 0;
1790 margin: -33px 0;
1797 }
1791 }
1798 }
1792 }
1799
1793
1800 div.ancestor {
1794 div.ancestor {
1801
1795
1802 }
1796 }
1803
1797
1804 .cs_icon_td input[type="checkbox"] {
1798 .cs_icon_td input[type="checkbox"] {
1805 display: none;
1799 display: none;
1806 }
1800 }
1807
1801
1808 .cs_icon_td .expand_file_icon:after {
1802 .cs_icon_td .expand_file_icon:after {
1809 cursor: pointer;
1803 cursor: pointer;
1810 content: "\00A0\25B6";
1804 content: "\00A0\25B6";
1811 font-size: 12px;
1805 font-size: 12px;
1812 color: @grey4;
1806 color: @grey4;
1813 }
1807 }
1814
1808
1815 .cs_icon_td .collapse_file_icon:after {
1809 .cs_icon_td .collapse_file_icon:after {
1816 cursor: pointer;
1810 cursor: pointer;
1817 content: "\00A0\25BC";
1811 content: "\00A0\25BC";
1818 font-size: 12px;
1812 font-size: 12px;
1819 color: @grey4;
1813 color: @grey4;
1820 }
1814 }
1821
1815
1822 /*new binary
1816 /*new binary
1823 NEW_FILENODE = 1
1817 NEW_FILENODE = 1
1824 DEL_FILENODE = 2
1818 DEL_FILENODE = 2
1825 MOD_FILENODE = 3
1819 MOD_FILENODE = 3
1826 RENAMED_FILENODE = 4
1820 RENAMED_FILENODE = 4
1827 COPIED_FILENODE = 5
1821 COPIED_FILENODE = 5
1828 CHMOD_FILENODE = 6
1822 CHMOD_FILENODE = 6
1829 BIN_FILENODE = 7
1823 BIN_FILENODE = 7
1830 */
1824 */
1831 .cs_files_expand {
1825 .cs_files_expand {
1832 font-size: @basefontsize + 5px;
1826 font-size: @basefontsize + 5px;
1833 line-height: 1.8em;
1827 line-height: 1.8em;
1834 float: right;
1828 float: right;
1835 }
1829 }
1836
1830
1837 .cs_files_expand span{
1831 .cs_files_expand span{
1838 color: @rcblue;
1832 color: @rcblue;
1839 cursor: pointer;
1833 cursor: pointer;
1840 }
1834 }
1841 .cs_files {
1835 .cs_files {
1842 clear: both;
1836 clear: both;
1843 padding-bottom: @padding;
1837 padding-bottom: @padding;
1844
1838
1845 .cur_cs {
1839 .cur_cs {
1846 margin: 10px 2px;
1840 margin: 10px 2px;
1847 font-weight: bold;
1841 font-weight: bold;
1848 }
1842 }
1849
1843
1850 .node {
1844 .node {
1851 float: left;
1845 float: left;
1852 }
1846 }
1853
1847
1854 .changes {
1848 .changes {
1855 float: right;
1849 float: right;
1856 color: white;
1850 color: white;
1857 font-size: @basefontsize - 4px;
1851 font-size: @basefontsize - 4px;
1858 margin-top: 4px;
1852 margin-top: 4px;
1859 opacity: 0.6;
1853 opacity: 0.6;
1860 filter: Alpha(opacity=60); /* IE8 and earlier */
1854 filter: Alpha(opacity=60); /* IE8 and earlier */
1861
1855
1862 .added {
1856 .added {
1863 background-color: @alert1;
1857 background-color: @alert1;
1864 float: left;
1858 float: left;
1865 text-align: center;
1859 text-align: center;
1866 }
1860 }
1867
1861
1868 .deleted {
1862 .deleted {
1869 background-color: @alert2;
1863 background-color: @alert2;
1870 float: left;
1864 float: left;
1871 text-align: center;
1865 text-align: center;
1872 }
1866 }
1873
1867
1874 .bin {
1868 .bin {
1875 background-color: @alert1;
1869 background-color: @alert1;
1876 text-align: center;
1870 text-align: center;
1877 }
1871 }
1878
1872
1879 /*new binary*/
1873 /*new binary*/
1880 .bin.bin1 {
1874 .bin.bin1 {
1881 background-color: @alert1;
1875 background-color: @alert1;
1882 text-align: center;
1876 text-align: center;
1883 }
1877 }
1884
1878
1885 /*deleted binary*/
1879 /*deleted binary*/
1886 .bin.bin2 {
1880 .bin.bin2 {
1887 background-color: @alert2;
1881 background-color: @alert2;
1888 text-align: center;
1882 text-align: center;
1889 }
1883 }
1890
1884
1891 /*mod binary*/
1885 /*mod binary*/
1892 .bin.bin3 {
1886 .bin.bin3 {
1893 background-color: @grey2;
1887 background-color: @grey2;
1894 text-align: center;
1888 text-align: center;
1895 }
1889 }
1896
1890
1897 /*rename file*/
1891 /*rename file*/
1898 .bin.bin4 {
1892 .bin.bin4 {
1899 background-color: @alert4;
1893 background-color: @alert4;
1900 text-align: center;
1894 text-align: center;
1901 }
1895 }
1902
1896
1903 /*copied file*/
1897 /*copied file*/
1904 .bin.bin5 {
1898 .bin.bin5 {
1905 background-color: @alert4;
1899 background-color: @alert4;
1906 text-align: center;
1900 text-align: center;
1907 }
1901 }
1908
1902
1909 /*chmod file*/
1903 /*chmod file*/
1910 .bin.bin6 {
1904 .bin.bin6 {
1911 background-color: @grey2;
1905 background-color: @grey2;
1912 text-align: center;
1906 text-align: center;
1913 }
1907 }
1914 }
1908 }
1915 }
1909 }
1916
1910
1917 .cs_files .cs_added, .cs_files .cs_A,
1911 .cs_files .cs_added, .cs_files .cs_A,
1918 .cs_files .cs_added, .cs_files .cs_M,
1912 .cs_files .cs_added, .cs_files .cs_M,
1919 .cs_files .cs_added, .cs_files .cs_D {
1913 .cs_files .cs_added, .cs_files .cs_D {
1920 height: 16px;
1914 height: 16px;
1921 padding-right: 10px;
1915 padding-right: 10px;
1922 margin-top: 7px;
1916 margin-top: 7px;
1923 text-align: left;
1917 text-align: left;
1924 }
1918 }
1925
1919
1926 .cs_icon_td {
1920 .cs_icon_td {
1927 min-width: 16px;
1921 min-width: 16px;
1928 width: 16px;
1922 width: 16px;
1929 }
1923 }
1930
1924
1931 .pull-request-merge {
1925 .pull-request-merge {
1932 border: 1px solid @grey5;
1926 border: 1px solid @grey5;
1933 padding: 10px 0px 20px;
1927 padding: 10px 0px 20px;
1934 margin-top: 10px;
1928 margin-top: 10px;
1935 margin-bottom: 20px;
1929 margin-bottom: 20px;
1936 }
1930 }
1937
1931
1938 .pull-request-merge-refresh {
1932 .pull-request-merge-refresh {
1939 margin: 2px 7px;
1933 margin: 2px 7px;
1940 a {
1934 a {
1941 color: @grey3;
1935 color: @grey3;
1942 }
1936 }
1943 }
1937 }
1944
1938
1945 .pull-request-merge ul {
1939 .pull-request-merge ul {
1946 padding: 0px 0px;
1940 padding: 0px 0px;
1947 }
1941 }
1948
1942
1949 .pull-request-merge li {
1943 .pull-request-merge li {
1950 list-style-type: none;
1944 list-style-type: none;
1951 }
1945 }
1952
1946
1953 .pull-request-merge .pull-request-wrap {
1947 .pull-request-merge .pull-request-wrap {
1954 height: auto;
1948 height: auto;
1955 padding: 0px 0px;
1949 padding: 0px 0px;
1956 text-align: right;
1950 text-align: right;
1957 }
1951 }
1958
1952
1959 .pull-request-merge span {
1953 .pull-request-merge span {
1960 margin-right: 5px;
1954 margin-right: 5px;
1961 }
1955 }
1962
1956
1963 .pull-request-merge-actions {
1957 .pull-request-merge-actions {
1964 min-height: 30px;
1958 min-height: 30px;
1965 padding: 0px 0px;
1959 padding: 0px 0px;
1966 }
1960 }
1967
1961
1968 .pull-request-merge-info {
1962 .pull-request-merge-info {
1969 padding: 0px 5px 5px 0px;
1963 padding: 0px 5px 5px 0px;
1970 }
1964 }
1971
1965
1972 .merge-status {
1966 .merge-status {
1973 margin-right: 5px;
1967 margin-right: 5px;
1974 }
1968 }
1975
1969
1976 .merge-message {
1970 .merge-message {
1977 font-size: 1.2em
1971 font-size: 1.2em
1978 }
1972 }
1979
1973
1980 .merge-message.success i,
1974 .merge-message.success i,
1981 .merge-icon.success i {
1975 .merge-icon.success i {
1982 color:@alert1;
1976 color:@alert1;
1983 }
1977 }
1984
1978
1985 .merge-message.warning i,
1979 .merge-message.warning i,
1986 .merge-icon.warning i {
1980 .merge-icon.warning i {
1987 color: @alert3;
1981 color: @alert3;
1988 }
1982 }
1989
1983
1990 .merge-message.error i,
1984 .merge-message.error i,
1991 .merge-icon.error i {
1985 .merge-icon.error i {
1992 color:@alert2;
1986 color:@alert2;
1993 }
1987 }
1994
1988
1995 .pr-versions {
1989 .pr-versions {
1996 font-size: 1.1em;
1990 font-size: 1.1em;
1997 padding: 7.5px;
1991 padding: 7.5px;
1998
1992
1999 table {
1993 table {
2000
1994
2001 }
1995 }
2002
1996
2003 td {
1997 td {
2004 line-height: 15px;
1998 line-height: 15px;
2005 }
1999 }
2006
2000
2007 .compare-radio-button {
2001 .compare-radio-button {
2008 position: relative;
2002 position: relative;
2009 top: -3px;
2003 top: -3px;
2010 }
2004 }
2011 }
2005 }
2012
2006
2013
2007
2014 #close_pull_request {
2008 #close_pull_request {
2015 margin-right: 0px;
2009 margin-right: 0px;
2016 }
2010 }
2017
2011
2018 .empty_data {
2012 .empty_data {
2019 color: @grey4;
2013 color: @grey4;
2020 }
2014 }
2021
2015
2022 #changeset_compare_view_content {
2016 #changeset_compare_view_content {
2023 clear: both;
2017 clear: both;
2024 width: 100%;
2018 width: 100%;
2025 box-sizing: border-box;
2019 box-sizing: border-box;
2026 .border-radius(@border-radius);
2020 .border-radius(@border-radius);
2027
2021
2028 .help-block {
2022 .help-block {
2029 margin: @padding 0;
2023 margin: @padding 0;
2030 color: @text-color;
2024 color: @text-color;
2031 &.pre-formatting {
2025 &.pre-formatting {
2032 white-space: pre;
2026 white-space: pre;
2033 }
2027 }
2034 }
2028 }
2035
2029
2036 .empty_data {
2030 .empty_data {
2037 margin: @padding 0;
2031 margin: @padding 0;
2038 }
2032 }
2039
2033
2040 .alert {
2034 .alert {
2041 margin-bottom: @space;
2035 margin-bottom: @space;
2042 }
2036 }
2043 }
2037 }
2044
2038
2045 .table_disp {
2039 .table_disp {
2046 .status {
2040 .status {
2047 width: auto;
2041 width: auto;
2048 }
2042 }
2049 }
2043 }
2050
2044
2051
2045
2052 .creation_in_progress {
2046 .creation_in_progress {
2053 color: @grey4
2047 color: @grey4
2054 }
2048 }
2055
2049
2056 .status_box_menu {
2050 .status_box_menu {
2057 margin: 0;
2051 margin: 0;
2058 }
2052 }
2059
2053
2060 .notification-table{
2054 .notification-table{
2061 margin-bottom: @space;
2055 margin-bottom: @space;
2062 display: table;
2056 display: table;
2063 width: 100%;
2057 width: 100%;
2064
2058
2065 .container{
2059 .container{
2066 display: table-row;
2060 display: table-row;
2067
2061
2068 .notification-header{
2062 .notification-header{
2069 border-bottom: @border-thickness solid @border-default-color;
2063 border-bottom: @border-thickness solid @border-default-color;
2070 }
2064 }
2071
2065
2072 .notification-subject{
2066 .notification-subject{
2073 display: table-cell;
2067 display: table-cell;
2074 }
2068 }
2075 }
2069 }
2076 }
2070 }
2077
2071
2078 // Notifications
2072 // Notifications
2079 .notification-header{
2073 .notification-header{
2080 display: table;
2074 display: table;
2081 width: 100%;
2075 width: 100%;
2082 padding: floor(@basefontsize/2) 0;
2076 padding: floor(@basefontsize/2) 0;
2083 line-height: 1em;
2077 line-height: 1em;
2084
2078
2085 .desc, .delete-notifications, .read-notifications{
2079 .desc, .delete-notifications, .read-notifications{
2086 display: table-cell;
2080 display: table-cell;
2087 text-align: left;
2081 text-align: left;
2088 }
2082 }
2089
2083
2090 .delete-notifications, .read-notifications{
2084 .delete-notifications, .read-notifications{
2091 width: 35px;
2085 width: 35px;
2092 min-width: 35px; //fixes when only one button is displayed
2086 min-width: 35px; //fixes when only one button is displayed
2093 }
2087 }
2094 }
2088 }
2095
2089
2096 .notification-body {
2090 .notification-body {
2097 .markdown-block,
2091 .markdown-block,
2098 .rst-block {
2092 .rst-block {
2099 padding: @padding 0;
2093 padding: @padding 0;
2100 }
2094 }
2101
2095
2102 .notification-subject {
2096 .notification-subject {
2103 padding: @textmargin 0;
2097 padding: @textmargin 0;
2104 border-bottom: @border-thickness solid @border-default-color;
2098 border-bottom: @border-thickness solid @border-default-color;
2105 }
2099 }
2106 }
2100 }
2107
2101
2108 .notice-messages {
2102 .notice-messages {
2109 .markdown-block,
2103 .markdown-block,
2110 .rst-block {
2104 .rst-block {
2111 padding: 0;
2105 padding: 0;
2112 }
2106 }
2113 }
2107 }
2114
2108
2115 .notifications_buttons{
2109 .notifications_buttons{
2116 float: right;
2110 float: right;
2117 }
2111 }
2118
2112
2119 #notification-status{
2113 #notification-status{
2120 display: inline;
2114 display: inline;
2121 }
2115 }
2122
2116
2123 // Repositories
2117 // Repositories
2124
2118
2125 #summary.fields{
2119 #summary.fields{
2126 display: table;
2120 display: table;
2127
2121
2128 .field{
2122 .field{
2129 display: table-row;
2123 display: table-row;
2130
2124
2131 .label-summary{
2125 .label-summary{
2132 display: table-cell;
2126 display: table-cell;
2133 min-width: @label-summary-minwidth;
2127 min-width: @label-summary-minwidth;
2134 padding-top: @padding/2;
2128 padding-top: @padding/2;
2135 padding-bottom: @padding/2;
2129 padding-bottom: @padding/2;
2136 padding-right: @padding/2;
2130 padding-right: @padding/2;
2137 }
2131 }
2138
2132
2139 .input{
2133 .input{
2140 display: table-cell;
2134 display: table-cell;
2141 padding: @padding/2;
2135 padding: @padding/2;
2142
2136
2143 input{
2137 input{
2144 min-width: 29em;
2138 min-width: 29em;
2145 padding: @padding/4;
2139 padding: @padding/4;
2146 }
2140 }
2147 }
2141 }
2148 .statistics, .downloads{
2142 .statistics, .downloads{
2149 .disabled{
2143 .disabled{
2150 color: @grey4;
2144 color: @grey4;
2151 }
2145 }
2152 }
2146 }
2153 }
2147 }
2154 }
2148 }
2155
2149
2156 #summary{
2150 #summary{
2157 width: 70%;
2151 width: 70%;
2158 }
2152 }
2159
2153
2160
2154
2161 // Journal
2155 // Journal
2162 .journal.title {
2156 .journal.title {
2163 h5 {
2157 h5 {
2164 float: left;
2158 float: left;
2165 margin: 0;
2159 margin: 0;
2166 width: 70%;
2160 width: 70%;
2167 }
2161 }
2168
2162
2169 ul {
2163 ul {
2170 float: right;
2164 float: right;
2171 display: inline-block;
2165 display: inline-block;
2172 margin: 0;
2166 margin: 0;
2173 width: 30%;
2167 width: 30%;
2174 text-align: right;
2168 text-align: right;
2175
2169
2176 li {
2170 li {
2177 display: inline;
2171 display: inline;
2178 font-size: @journal-fontsize;
2172 font-size: @journal-fontsize;
2179 line-height: 1em;
2173 line-height: 1em;
2180
2174
2181 list-style-type: none;
2175 list-style-type: none;
2182 }
2176 }
2183 }
2177 }
2184 }
2178 }
2185
2179
2186 .filterexample {
2180 .filterexample {
2187 position: absolute;
2181 position: absolute;
2188 top: 95px;
2182 top: 95px;
2189 left: @contentpadding;
2183 left: @contentpadding;
2190 color: @rcblue;
2184 color: @rcblue;
2191 font-size: 11px;
2185 font-size: 11px;
2192 font-family: @text-regular;
2186 font-family: @text-regular;
2193 cursor: help;
2187 cursor: help;
2194
2188
2195 &:hover {
2189 &:hover {
2196 color: @rcdarkblue;
2190 color: @rcdarkblue;
2197 }
2191 }
2198
2192
2199 @media (max-width:768px) {
2193 @media (max-width:768px) {
2200 position: relative;
2194 position: relative;
2201 top: auto;
2195 top: auto;
2202 left: auto;
2196 left: auto;
2203 display: block;
2197 display: block;
2204 }
2198 }
2205 }
2199 }
2206
2200
2207
2201
2208 #journal{
2202 #journal{
2209 margin-bottom: @space;
2203 margin-bottom: @space;
2210
2204
2211 .journal_day{
2205 .journal_day{
2212 margin-bottom: @textmargin/2;
2206 margin-bottom: @textmargin/2;
2213 padding-bottom: @textmargin/2;
2207 padding-bottom: @textmargin/2;
2214 font-size: @journal-fontsize;
2208 font-size: @journal-fontsize;
2215 border-bottom: @border-thickness solid @border-default-color;
2209 border-bottom: @border-thickness solid @border-default-color;
2216 }
2210 }
2217
2211
2218 .journal_container{
2212 .journal_container{
2219 margin-bottom: @space;
2213 margin-bottom: @space;
2220
2214
2221 .journal_user{
2215 .journal_user{
2222 display: inline-block;
2216 display: inline-block;
2223 }
2217 }
2224 .journal_action_container{
2218 .journal_action_container{
2225 display: block;
2219 display: block;
2226 margin-top: @textmargin;
2220 margin-top: @textmargin;
2227
2221
2228 div{
2222 div{
2229 display: inline;
2223 display: inline;
2230 }
2224 }
2231
2225
2232 div.journal_action_params{
2226 div.journal_action_params{
2233 display: block;
2227 display: block;
2234 }
2228 }
2235
2229
2236 div.journal_repo:after{
2230 div.journal_repo:after{
2237 content: "\A";
2231 content: "\A";
2238 white-space: pre;
2232 white-space: pre;
2239 }
2233 }
2240
2234
2241 div.date{
2235 div.date{
2242 display: block;
2236 display: block;
2243 margin-bottom: @textmargin;
2237 margin-bottom: @textmargin;
2244 }
2238 }
2245 }
2239 }
2246 }
2240 }
2247 }
2241 }
2248
2242
2249 // Files
2243 // Files
2250 .edit-file-title {
2244 .edit-file-title {
2251 font-size: 16px;
2245 font-size: 16px;
2252
2246
2253 .title-heading {
2247 .title-heading {
2254 padding: 2px;
2248 padding: 2px;
2255 }
2249 }
2256 }
2250 }
2257
2251
2258 .edit-file-fieldset {
2252 .edit-file-fieldset {
2259 margin: @sidebarpadding 0;
2253 margin: @sidebarpadding 0;
2260
2254
2261 .fieldset {
2255 .fieldset {
2262 .left-label {
2256 .left-label {
2263 width: 13%;
2257 width: 13%;
2264 }
2258 }
2265 .right-content {
2259 .right-content {
2266 width: 87%;
2260 width: 87%;
2267 max-width: 100%;
2261 max-width: 100%;
2268 }
2262 }
2269 .filename-label {
2263 .filename-label {
2270 margin-top: 13px;
2264 margin-top: 13px;
2271 }
2265 }
2272 .commit-message-label {
2266 .commit-message-label {
2273 margin-top: 4px;
2267 margin-top: 4px;
2274 }
2268 }
2275 .file-upload-input {
2269 .file-upload-input {
2276 input {
2270 input {
2277 display: none;
2271 display: none;
2278 }
2272 }
2279 margin-top: 10px;
2273 margin-top: 10px;
2280 }
2274 }
2281 .file-upload-label {
2275 .file-upload-label {
2282 margin-top: 10px;
2276 margin-top: 10px;
2283 }
2277 }
2284 p {
2278 p {
2285 margin-top: 5px;
2279 margin-top: 5px;
2286 }
2280 }
2287
2281
2288 }
2282 }
2289 .custom-path-link {
2283 .custom-path-link {
2290 margin-left: 5px;
2284 margin-left: 5px;
2291 }
2285 }
2292 #commit {
2286 #commit {
2293 resize: vertical;
2287 resize: vertical;
2294 }
2288 }
2295 }
2289 }
2296
2290
2297 .delete-file-preview {
2291 .delete-file-preview {
2298 max-height: 250px;
2292 max-height: 250px;
2299 }
2293 }
2300
2294
2301 .new-file,
2295 .new-file,
2302 #filter_activate,
2296 #filter_activate,
2303 #filter_deactivate {
2297 #filter_deactivate {
2304 float: right;
2298 float: right;
2305 margin: 0 0 0 10px;
2299 margin: 0 0 0 10px;
2306 }
2300 }
2307
2301
2308 .file-upload-transaction-wrapper {
2302 .file-upload-transaction-wrapper {
2309 margin-top: 57px;
2303 margin-top: 57px;
2310 clear: both;
2304 clear: both;
2311 }
2305 }
2312
2306
2313 .file-upload-transaction-wrapper .error {
2307 .file-upload-transaction-wrapper .error {
2314 color: @color5;
2308 color: @color5;
2315 }
2309 }
2316
2310
2317 .file-upload-transaction {
2311 .file-upload-transaction {
2318 min-height: 200px;
2312 min-height: 200px;
2319 padding: 54px;
2313 padding: 54px;
2320 border: 1px solid @grey5;
2314 border: 1px solid @grey5;
2321 text-align: center;
2315 text-align: center;
2322 clear: both;
2316 clear: both;
2323 }
2317 }
2324
2318
2325 .file-upload-transaction i {
2319 .file-upload-transaction i {
2326 font-size: 48px
2320 font-size: 48px
2327 }
2321 }
2328
2322
2329 h3.files_location{
2323 h3.files_location{
2330 line-height: 2.4em;
2324 line-height: 2.4em;
2331 }
2325 }
2332
2326
2333 .browser-nav {
2327 .browser-nav {
2334 width: 100%;
2328 width: 100%;
2335 display: table;
2329 display: table;
2336 margin-bottom: 20px;
2330 margin-bottom: 20px;
2337
2331
2338 .info_box {
2332 .info_box {
2339 float: left;
2333 float: left;
2340 display: inline-table;
2334 display: inline-table;
2341 height: 2.5em;
2335 height: 2.5em;
2342
2336
2343 .browser-cur-rev, .info_box_elem {
2337 .browser-cur-rev, .info_box_elem {
2344 display: table-cell;
2338 display: table-cell;
2345 vertical-align: middle;
2339 vertical-align: middle;
2346 }
2340 }
2347
2341
2348 .drop-menu {
2342 .drop-menu {
2349 margin: 0 10px;
2343 margin: 0 10px;
2350 }
2344 }
2351
2345
2352 .info_box_elem {
2346 .info_box_elem {
2353 border-top: @border-thickness solid @grey5;
2347 border-top: @border-thickness solid @grey5;
2354 border-bottom: @border-thickness solid @grey5;
2348 border-bottom: @border-thickness solid @grey5;
2355 box-shadow: @button-shadow;
2349 box-shadow: @button-shadow;
2356
2350
2357 #at_rev, a {
2351 #at_rev, a {
2358 padding: 0.6em 0.4em;
2352 padding: 0.6em 0.4em;
2359 margin: 0;
2353 margin: 0;
2360 .box-shadow(none);
2354 .box-shadow(none);
2361 border: 0;
2355 border: 0;
2362 height: 12px;
2356 height: 12px;
2363 color: @grey2;
2357 color: @grey2;
2364 }
2358 }
2365
2359
2366 input#at_rev {
2360 input#at_rev {
2367 max-width: 50px;
2361 max-width: 50px;
2368 text-align: center;
2362 text-align: center;
2369 }
2363 }
2370
2364
2371 &.previous {
2365 &.previous {
2372 border: @border-thickness solid @grey5;
2366 border: @border-thickness solid @grey5;
2373 border-top-left-radius: @border-radius;
2367 border-top-left-radius: @border-radius;
2374 border-bottom-left-radius: @border-radius;
2368 border-bottom-left-radius: @border-radius;
2375
2369
2376 &:hover {
2370 &:hover {
2377 border-color: @grey4;
2371 border-color: @grey4;
2378 }
2372 }
2379
2373
2380 .disabled {
2374 .disabled {
2381 color: @grey5;
2375 color: @grey5;
2382 cursor: not-allowed;
2376 cursor: not-allowed;
2383 opacity: 0.5;
2377 opacity: 0.5;
2384 }
2378 }
2385 }
2379 }
2386
2380
2387 &.next {
2381 &.next {
2388 border: @border-thickness solid @grey5;
2382 border: @border-thickness solid @grey5;
2389 border-top-right-radius: @border-radius;
2383 border-top-right-radius: @border-radius;
2390 border-bottom-right-radius: @border-radius;
2384 border-bottom-right-radius: @border-radius;
2391
2385
2392 &:hover {
2386 &:hover {
2393 border-color: @grey4;
2387 border-color: @grey4;
2394 }
2388 }
2395
2389
2396 .disabled {
2390 .disabled {
2397 color: @grey5;
2391 color: @grey5;
2398 cursor: not-allowed;
2392 cursor: not-allowed;
2399 opacity: 0.5;
2393 opacity: 0.5;
2400 }
2394 }
2401 }
2395 }
2402 }
2396 }
2403
2397
2404 .browser-cur-rev {
2398 .browser-cur-rev {
2405
2399
2406 span{
2400 span{
2407 margin: 0;
2401 margin: 0;
2408 color: @rcblue;
2402 color: @rcblue;
2409 height: 12px;
2403 height: 12px;
2410 display: inline-block;
2404 display: inline-block;
2411 padding: 0.7em 1em ;
2405 padding: 0.7em 1em ;
2412 border: @border-thickness solid @rcblue;
2406 border: @border-thickness solid @rcblue;
2413 margin-right: @padding;
2407 margin-right: @padding;
2414 }
2408 }
2415 }
2409 }
2416
2410
2417 }
2411 }
2418
2412
2419 .select-index-number {
2413 .select-index-number {
2420 margin: 0 0 0 20px;
2414 margin: 0 0 0 20px;
2421 color: @grey3;
2415 color: @grey3;
2422 }
2416 }
2423
2417
2424 .search_activate {
2418 .search_activate {
2425 display: table-cell;
2419 display: table-cell;
2426 vertical-align: middle;
2420 vertical-align: middle;
2427
2421
2428 input, label{
2422 input, label{
2429 margin: 0;
2423 margin: 0;
2430 padding: 0;
2424 padding: 0;
2431 }
2425 }
2432
2426
2433 input{
2427 input{
2434 margin-left: @textmargin;
2428 margin-left: @textmargin;
2435 }
2429 }
2436
2430
2437 }
2431 }
2438 }
2432 }
2439
2433
2440 .browser-cur-rev{
2434 .browser-cur-rev{
2441 margin-bottom: @textmargin;
2435 margin-bottom: @textmargin;
2442 }
2436 }
2443
2437
2444 #node_filter_box_loading{
2438 #node_filter_box_loading{
2445 .info_text;
2439 .info_text;
2446 }
2440 }
2447
2441
2448 .browser-search {
2442 .browser-search {
2449 margin: -25px 0px 5px 0px;
2443 margin: -25px 0px 5px 0px;
2450 }
2444 }
2451
2445
2452 .files-quick-filter {
2446 .files-quick-filter {
2453 float: right;
2447 float: right;
2454 width: 180px;
2448 width: 180px;
2455 position: relative;
2449 position: relative;
2456 }
2450 }
2457
2451
2458 .files-filter-box {
2452 .files-filter-box {
2459 display: flex;
2453 display: flex;
2460 padding: 0px;
2454 padding: 0px;
2461 border-radius: 3px;
2455 border-radius: 3px;
2462 margin-bottom: 0;
2456 margin-bottom: 0;
2463
2457
2464 a {
2458 a {
2465 border: none !important;
2459 border: none !important;
2466 }
2460 }
2467
2461
2468 li {
2462 li {
2469 list-style-type: none
2463 list-style-type: none
2470 }
2464 }
2471 }
2465 }
2472
2466
2473 .files-filter-box-path {
2467 .files-filter-box-path {
2474 line-height: 33px;
2468 line-height: 33px;
2475 padding: 0;
2469 padding: 0;
2476 width: 20px;
2470 width: 20px;
2477 position: absolute;
2471 position: absolute;
2478 z-index: 11;
2472 z-index: 11;
2479 left: 5px;
2473 left: 5px;
2480 }
2474 }
2481
2475
2482 .files-filter-box-input {
2476 .files-filter-box-input {
2483 margin-right: 0;
2477 margin-right: 0;
2484
2478
2485 input {
2479 input {
2486 border: 1px solid @white;
2480 border: 1px solid @white;
2487 padding-left: 25px;
2481 padding-left: 25px;
2488 width: 145px;
2482 width: 145px;
2489
2483
2490 &:hover {
2484 &:hover {
2491 border-color: @grey6;
2485 border-color: @grey6;
2492 }
2486 }
2493
2487
2494 &:focus {
2488 &:focus {
2495 border-color: @grey5;
2489 border-color: @grey5;
2496 }
2490 }
2497 }
2491 }
2498 }
2492 }
2499
2493
2500 .browser-result{
2494 .browser-result{
2501 td a{
2495 td a{
2502 margin-left: 0.5em;
2496 margin-left: 0.5em;
2503 display: inline-block;
2497 display: inline-block;
2504
2498
2505 em {
2499 em {
2506 font-weight: @text-bold-weight;
2500 font-weight: @text-bold-weight;
2507 font-family: @text-bold;
2501 font-family: @text-bold;
2508 }
2502 }
2509 }
2503 }
2510 }
2504 }
2511
2505
2512 .browser-highlight{
2506 .browser-highlight{
2513 background-color: @grey5-alpha;
2507 background-color: @grey5-alpha;
2514 }
2508 }
2515
2509
2516
2510
2517 .edit-file-fieldset #location,
2511 .edit-file-fieldset #location,
2518 .edit-file-fieldset #filename {
2512 .edit-file-fieldset #filename {
2519 display: flex;
2513 display: flex;
2520 width: -moz-available; /* WebKit-based browsers will ignore this. */
2514 width: -moz-available; /* WebKit-based browsers will ignore this. */
2521 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2515 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2522 width: fill-available;
2516 width: fill-available;
2523 border: 0;
2517 border: 0;
2524 }
2518 }
2525
2519
2526 .path-items {
2520 .path-items {
2527 display: flex;
2521 display: flex;
2528 padding: 0;
2522 padding: 0;
2529 border: 1px solid #eeeeee;
2523 border: 1px solid #eeeeee;
2530 width: 100%;
2524 width: 100%;
2531 float: left;
2525 float: left;
2532
2526
2533 .breadcrumb-path {
2527 .breadcrumb-path {
2534 line-height: 30px;
2528 line-height: 30px;
2535 padding: 0 4px;
2529 padding: 0 4px;
2536 white-space: nowrap;
2530 white-space: nowrap;
2537 }
2531 }
2538
2532
2539 .upload-form {
2533 .upload-form {
2540 margin-top: 46px;
2534 margin-top: 46px;
2541 }
2535 }
2542
2536
2543 .location-path {
2537 .location-path {
2544 width: -moz-available; /* WebKit-based browsers will ignore this. */
2538 width: -moz-available; /* WebKit-based browsers will ignore this. */
2545 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2539 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2546 width: fill-available;
2540 width: fill-available;
2547
2541
2548 .file-name-input {
2542 .file-name-input {
2549 padding: 0.5em 0;
2543 padding: 0.5em 0;
2550 }
2544 }
2551
2545
2552 }
2546 }
2553
2547
2554 ul {
2548 ul {
2555 display: flex;
2549 display: flex;
2556 margin: 0;
2550 margin: 0;
2557 padding: 0;
2551 padding: 0;
2558 width: 100%;
2552 width: 100%;
2559 }
2553 }
2560
2554
2561 li {
2555 li {
2562 list-style-type: none;
2556 list-style-type: none;
2563 }
2557 }
2564
2558
2565 }
2559 }
2566
2560
2567 .editor-items {
2561 .editor-items {
2568 height: 40px;
2562 height: 40px;
2569 margin: 10px 0 -17px 10px;
2563 margin: 10px 0 -17px 10px;
2570
2564
2571 .editor-action {
2565 .editor-action {
2572 cursor: pointer;
2566 cursor: pointer;
2573 }
2567 }
2574
2568
2575 .editor-action.active {
2569 .editor-action.active {
2576 border-bottom: 2px solid #5C5C5C;
2570 border-bottom: 2px solid #5C5C5C;
2577 }
2571 }
2578
2572
2579 li {
2573 li {
2580 list-style-type: none;
2574 list-style-type: none;
2581 }
2575 }
2582 }
2576 }
2583
2577
2584 .edit-file-fieldset .message textarea {
2578 .edit-file-fieldset .message textarea {
2585 border: 1px solid #eeeeee;
2579 border: 1px solid #eeeeee;
2586 }
2580 }
2587
2581
2588 #files_data .codeblock {
2582 #files_data .codeblock {
2589 background-color: #F5F5F5;
2583 background-color: #F5F5F5;
2590 }
2584 }
2591
2585
2592 #editor_preview {
2586 #editor_preview {
2593 background: white;
2587 background: white;
2594 }
2588 }
2595
2589
2596 .show-editor {
2590 .show-editor {
2597 padding: 10px;
2591 padding: 10px;
2598 background-color: white;
2592 background-color: white;
2599
2593
2600 }
2594 }
2601
2595
2602 .show-preview {
2596 .show-preview {
2603 padding: 10px;
2597 padding: 10px;
2604 background-color: white;
2598 background-color: white;
2605 border-left: 1px solid #eeeeee;
2599 border-left: 1px solid #eeeeee;
2606 }
2600 }
2607 // quick filter
2601 // quick filter
2608 .grid-quick-filter {
2602 .grid-quick-filter {
2609 float: right;
2603 float: right;
2610 position: relative;
2604 position: relative;
2611 }
2605 }
2612
2606
2613 .grid-filter-box {
2607 .grid-filter-box {
2614 display: flex;
2608 display: flex;
2615 padding: 0px;
2609 padding: 0px;
2616 border-radius: 3px;
2610 border-radius: 3px;
2617 margin-bottom: 0;
2611 margin-bottom: 0;
2618
2612
2619 a {
2613 a {
2620 border: none !important;
2614 border: none !important;
2621 }
2615 }
2622
2616
2623 li {
2617 li {
2624 list-style-type: none
2618 list-style-type: none
2625 }
2619 }
2626 }
2620 }
2627
2621
2628 .grid-filter-box-icon {
2622 .grid-filter-box-icon {
2629 line-height: 33px;
2623 line-height: 33px;
2630 padding: 0;
2624 padding: 0;
2631 width: 20px;
2625 width: 20px;
2632 position: absolute;
2626 position: absolute;
2633 z-index: 11;
2627 z-index: 11;
2634 left: 5px;
2628 left: 5px;
2635 }
2629 }
2636
2630
2637 .grid-filter-box-input {
2631 .grid-filter-box-input {
2638 margin-right: 0;
2632 margin-right: 0;
2639
2633
2640 input {
2634 input {
2641 border: 1px solid @white;
2635 border: 1px solid @white;
2642 padding-left: 25px;
2636 padding-left: 25px;
2643 width: 145px;
2637 width: 145px;
2644
2638
2645 &:hover {
2639 &:hover {
2646 border-color: @grey6;
2640 border-color: @grey6;
2647 }
2641 }
2648
2642
2649 &:focus {
2643 &:focus {
2650 border-color: @grey5;
2644 border-color: @grey5;
2651 }
2645 }
2652 }
2646 }
2653 }
2647 }
2654
2648
2655
2649
2656
2650
2657 // Search
2651 // Search
2658
2652
2659 .search-form{
2653 .search-form{
2660 #q {
2654 #q {
2661 width: @search-form-width;
2655 width: @search-form-width;
2662 }
2656 }
2663 .fields{
2657 .fields{
2664 margin: 0 0 @space;
2658 margin: 0 0 @space;
2665 }
2659 }
2666
2660
2667 label{
2661 label{
2668 display: inline-block;
2662 display: inline-block;
2669 margin-right: @textmargin;
2663 margin-right: @textmargin;
2670 padding-top: 0.25em;
2664 padding-top: 0.25em;
2671 }
2665 }
2672
2666
2673
2667
2674 .results{
2668 .results{
2675 clear: both;
2669 clear: both;
2676 margin: 0 0 @padding;
2670 margin: 0 0 @padding;
2677 }
2671 }
2678
2672
2679 .search-tags {
2673 .search-tags {
2680 padding: 5px 0;
2674 padding: 5px 0;
2681 }
2675 }
2682 }
2676 }
2683
2677
2684 div.search-feedback-items {
2678 div.search-feedback-items {
2685 display: inline-block;
2679 display: inline-block;
2686 }
2680 }
2687
2681
2688 div.search-code-body {
2682 div.search-code-body {
2689 background-color: #ffffff; padding: 5px 0 5px 10px;
2683 background-color: #ffffff; padding: 5px 0 5px 10px;
2690 pre {
2684 pre {
2691 .match { background-color: #faffa6;}
2685 .match { background-color: #faffa6;}
2692 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2686 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2693 }
2687 }
2694 }
2688 }
2695
2689
2696 .expand_commit.search {
2690 .expand_commit.search {
2697 .show_more.open {
2691 .show_more.open {
2698 height: auto;
2692 height: auto;
2699 max-height: none;
2693 max-height: none;
2700 }
2694 }
2701 }
2695 }
2702
2696
2703 .search-results {
2697 .search-results {
2704
2698
2705 h2 {
2699 h2 {
2706 margin-bottom: 0;
2700 margin-bottom: 0;
2707 }
2701 }
2708 .codeblock {
2702 .codeblock {
2709 border: none;
2703 border: none;
2710 background: transparent;
2704 background: transparent;
2711 }
2705 }
2712
2706
2713 .codeblock-header {
2707 .codeblock-header {
2714 border: none;
2708 border: none;
2715 background: transparent;
2709 background: transparent;
2716 }
2710 }
2717
2711
2718 .code-body {
2712 .code-body {
2719 border: @border-thickness solid @grey6;
2713 border: @border-thickness solid @grey6;
2720 .border-radius(@border-radius);
2714 .border-radius(@border-radius);
2721 }
2715 }
2722
2716
2723 .td-commit {
2717 .td-commit {
2724 &:extend(pre);
2718 &:extend(pre);
2725 border-bottom: @border-thickness solid @border-default-color;
2719 border-bottom: @border-thickness solid @border-default-color;
2726 }
2720 }
2727
2721
2728 .message {
2722 .message {
2729 height: auto;
2723 height: auto;
2730 max-width: 350px;
2724 max-width: 350px;
2731 white-space: normal;
2725 white-space: normal;
2732 text-overflow: initial;
2726 text-overflow: initial;
2733 overflow: visible;
2727 overflow: visible;
2734
2728
2735 .match { background-color: #faffa6;}
2729 .match { background-color: #faffa6;}
2736 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2730 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2737 }
2731 }
2738
2732
2739 .path {
2733 .path {
2740 border-bottom: none !important;
2734 border-bottom: none !important;
2741 border-left: 1px solid @grey6 !important;
2735 border-left: 1px solid @grey6 !important;
2742 border-right: 1px solid @grey6 !important;
2736 border-right: 1px solid @grey6 !important;
2743 }
2737 }
2744 }
2738 }
2745
2739
2746 table.rctable td.td-search-results div {
2740 table.rctable td.td-search-results div {
2747 max-width: 100%;
2741 max-width: 100%;
2748 }
2742 }
2749
2743
2750 #tip-box, .tip-box{
2744 #tip-box, .tip-box{
2751 padding: @menupadding/2;
2745 padding: @menupadding/2;
2752 display: block;
2746 display: block;
2753 border: @border-thickness solid @border-highlight-color;
2747 border: @border-thickness solid @border-highlight-color;
2754 .border-radius(@border-radius);
2748 .border-radius(@border-radius);
2755 background-color: white;
2749 background-color: white;
2756 z-index: 99;
2750 z-index: 99;
2757 white-space: pre-wrap;
2751 white-space: pre-wrap;
2758 }
2752 }
2759
2753
2760 #linktt {
2754 #linktt {
2761 width: 79px;
2755 width: 79px;
2762 }
2756 }
2763
2757
2764 #help_kb .modal-content{
2758 #help_kb .modal-content{
2765 max-width: 750px;
2759 max-width: 750px;
2766 margin: 10% auto;
2760 margin: 10% auto;
2767
2761
2768 table{
2762 table{
2769 td,th{
2763 td,th{
2770 border-bottom: none;
2764 border-bottom: none;
2771 line-height: 2.5em;
2765 line-height: 2.5em;
2772 }
2766 }
2773 th{
2767 th{
2774 padding-bottom: @textmargin/2;
2768 padding-bottom: @textmargin/2;
2775 }
2769 }
2776 td.keys{
2770 td.keys{
2777 text-align: center;
2771 text-align: center;
2778 }
2772 }
2779 }
2773 }
2780
2774
2781 .block-left{
2775 .block-left{
2782 width: 45%;
2776 width: 45%;
2783 margin-right: 5%;
2777 margin-right: 5%;
2784 }
2778 }
2785 .modal-footer{
2779 .modal-footer{
2786 clear: both;
2780 clear: both;
2787 }
2781 }
2788 .key.tag{
2782 .key.tag{
2789 padding: 0.5em;
2783 padding: 0.5em;
2790 background-color: @rcblue;
2784 background-color: @rcblue;
2791 color: white;
2785 color: white;
2792 border-color: @rcblue;
2786 border-color: @rcblue;
2793 .box-shadow(none);
2787 .box-shadow(none);
2794 }
2788 }
2795 }
2789 }
2796
2790
2797
2791
2798
2792
2799 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2793 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2800
2794
2801 @import 'statistics-graph';
2795 @import 'statistics-graph';
2802 @import 'tables';
2796 @import 'tables';
2803 @import 'forms';
2797 @import 'forms';
2804 @import 'diff';
2798 @import 'diff';
2805 @import 'summary';
2799 @import 'summary';
2806 @import 'navigation';
2800 @import 'navigation';
2807
2801
2808 //--- SHOW/HIDE SECTIONS --//
2802 //--- SHOW/HIDE SECTIONS --//
2809
2803
2810 .btn-collapse {
2804 .btn-collapse {
2811 float: right;
2805 float: right;
2812 text-align: right;
2806 text-align: right;
2813 font-family: @text-light;
2807 font-family: @text-light;
2814 font-size: @basefontsize;
2808 font-size: @basefontsize;
2815 cursor: pointer;
2809 cursor: pointer;
2816 border: none;
2810 border: none;
2817 color: @rcblue;
2811 color: @rcblue;
2818 }
2812 }
2819
2813
2820 table.rctable,
2814 table.rctable,
2821 table.dataTable {
2815 table.dataTable {
2822 .btn-collapse {
2816 .btn-collapse {
2823 float: right;
2817 float: right;
2824 text-align: right;
2818 text-align: right;
2825 }
2819 }
2826 }
2820 }
2827
2821
2828 table.rctable {
2822 table.rctable {
2829 &.permissions {
2823 &.permissions {
2830
2824
2831 th.td-owner {
2825 th.td-owner {
2832 padding: 0;
2826 padding: 0;
2833 }
2827 }
2834
2828
2835 th {
2829 th {
2836 font-weight: normal;
2830 font-weight: normal;
2837 padding: 0 5px;
2831 padding: 0 5px;
2838 }
2832 }
2839
2833
2840 }
2834 }
2841 }
2835 }
2842
2836
2843
2837
2844 // TODO: johbo: Fix for IE10, this avoids that we see a border
2838 // TODO: johbo: Fix for IE10, this avoids that we see a border
2845 // and padding around checkboxes and radio boxes. Move to the right place,
2839 // and padding around checkboxes and radio boxes. Move to the right place,
2846 // or better: Remove this once we did the form refactoring.
2840 // or better: Remove this once we did the form refactoring.
2847 input[type=checkbox],
2841 input[type=checkbox],
2848 input[type=radio] {
2842 input[type=radio] {
2849 padding: 0;
2843 padding: 0;
2850 border: none;
2844 border: none;
2851 }
2845 }
2852
2846
2853 .toggle-ajax-spinner{
2847 .toggle-ajax-spinner{
2854 height: 16px;
2848 height: 16px;
2855 width: 16px;
2849 width: 16px;
2856 }
2850 }
2857
2851
2858
2852
2859 .markup-form .clearfix {
2853 .markup-form .clearfix {
2860 .border-radius(@border-radius);
2854 .border-radius(@border-radius);
2861 margin: 0px;
2855 margin: 0px;
2862 }
2856 }
2863
2857
2864 .markup-form-area {
2858 .markup-form-area {
2865 padding: 8px 12px;
2859 padding: 8px 12px;
2866 border: 1px solid @grey4;
2860 border: 1px solid @grey4;
2867 .border-radius(@border-radius);
2861 .border-radius(@border-radius);
2868 }
2862 }
2869
2863
2870 .markup-form-area-header .nav-links {
2864 .markup-form-area-header .nav-links {
2871 display: flex;
2865 display: flex;
2872 flex-flow: row wrap;
2866 flex-flow: row wrap;
2873 -webkit-flex-flow: row wrap;
2867 -webkit-flex-flow: row wrap;
2874 width: 100%;
2868 width: 100%;
2875 }
2869 }
2876
2870
2877 .markup-form-area-footer {
2871 .markup-form-area-footer {
2878 display: flex;
2872 display: flex;
2879 }
2873 }
2880
2874
2881 .markup-form-area-footer .toolbar {
2875 .markup-form-area-footer .toolbar {
2882
2876
2883 }
2877 }
2884
2878
2885 // markup Form
2879 // markup Form
2886 div.markup-form {
2880 div.markup-form {
2887 margin-top: 20px;
2881 margin-top: 20px;
2888 }
2882 }
2889
2883
2890 .markup-form strong {
2884 .markup-form strong {
2891 display: block;
2885 display: block;
2892 margin-bottom: 15px;
2886 margin-bottom: 15px;
2893 }
2887 }
2894
2888
2895 .markup-form textarea {
2889 .markup-form textarea {
2896 width: 100%;
2890 width: 100%;
2897 height: 100px;
2891 height: 100px;
2898 font-family: @text-monospace;
2892 font-family: @text-monospace;
2899 }
2893 }
2900
2894
2901 form.markup-form {
2895 form.markup-form {
2902 margin-top: 10px;
2896 margin-top: 10px;
2903 margin-left: 10px;
2897 margin-left: 10px;
2904 }
2898 }
2905
2899
2906 .markup-form .comment-block-ta,
2900 .markup-form .comment-block-ta,
2907 .markup-form .preview-box {
2901 .markup-form .preview-box {
2908 .border-radius(@border-radius);
2902 .border-radius(@border-radius);
2909 .box-sizing(border-box);
2903 .box-sizing(border-box);
2910 background-color: white;
2904 background-color: white;
2911 }
2905 }
2912
2906
2913 .markup-form .preview-box.unloaded {
2907 .markup-form .preview-box.unloaded {
2914 height: 50px;
2908 height: 50px;
2915 text-align: center;
2909 text-align: center;
2916 padding: 20px;
2910 padding: 20px;
2917 background-color: white;
2911 background-color: white;
2918 }
2912 }
2919
2913
2920
2914
2921 .dropzone-wrapper {
2915 .dropzone-wrapper {
2922 border: 1px solid @grey5;
2916 border: 1px solid @grey5;
2923 padding: 20px;
2917 padding: 20px;
2924 }
2918 }
2925
2919
2926 .dropzone,
2920 .dropzone,
2927 .dropzone-pure {
2921 .dropzone-pure {
2928 border: 2px dashed @grey5;
2922 border: 2px dashed @grey5;
2929 border-radius: 5px;
2923 border-radius: 5px;
2930 background: white;
2924 background: white;
2931 min-height: 200px;
2925 min-height: 200px;
2932 padding: 54px;
2926 padding: 54px;
2933
2927
2934 .dz-message {
2928 .dz-message {
2935 font-weight: 700;
2929 font-weight: 700;
2936 text-align: center;
2930 text-align: center;
2937 margin: 2em 0;
2931 margin: 2em 0;
2938 }
2932 }
2939
2933
2940 }
2934 }
2941
2935
2942 .dz-preview {
2936 .dz-preview {
2943 margin: 10px 0 !important;
2937 margin: 10px 0 !important;
2944 position: relative;
2938 position: relative;
2945 vertical-align: top;
2939 vertical-align: top;
2946 padding: 10px;
2940 padding: 10px;
2947 border-bottom: 1px solid @grey5;
2941 border-bottom: 1px solid @grey5;
2948 }
2942 }
2949
2943
2950 .dz-filename {
2944 .dz-filename {
2951 font-weight: 700;
2945 font-weight: 700;
2952 float: left;
2946 float: left;
2953 }
2947 }
2954
2948
2955 .dz-sending {
2949 .dz-sending {
2956 float: right;
2950 float: right;
2957 }
2951 }
2958
2952
2959 .dz-response {
2953 .dz-response {
2960 clear: both
2954 clear: both
2961 }
2955 }
2962
2956
2963 .dz-filename-size {
2957 .dz-filename-size {
2964 float: right
2958 float: right
2965 }
2959 }
2966
2960
2967 .dz-error-message {
2961 .dz-error-message {
2968 color: @alert2;
2962 color: @alert2;
2969 padding-top: 10px;
2963 padding-top: 10px;
2970 clear: both;
2964 clear: both;
2971 }
2965 }
2972
2966
2973
2967
2974 .user-hovercard {
2968 .user-hovercard {
2975 padding: 5px;
2969 padding: 5px;
2976 }
2970 }
2977
2971
2978 .user-hovercard-icon {
2972 .user-hovercard-icon {
2979 display: inline;
2973 display: inline;
2980 padding: 0;
2974 padding: 0;
2981 box-sizing: content-box;
2975 box-sizing: content-box;
2982 border-radius: 50%;
2976 border-radius: 50%;
2983 float: left;
2977 float: left;
2984 }
2978 }
2985
2979
2986 .user-hovercard-name {
2980 .user-hovercard-name {
2987 float: right;
2981 float: right;
2988 vertical-align: top;
2982 vertical-align: top;
2989 padding-left: 10px;
2983 padding-left: 10px;
2990 min-width: 150px;
2984 min-width: 150px;
2991 }
2985 }
2992
2986
2993 .user-hovercard-bio {
2987 .user-hovercard-bio {
2994 clear: both;
2988 clear: both;
2995 padding-top: 10px;
2989 padding-top: 10px;
2996 }
2990 }
2997
2991
2998 .user-hovercard-header {
2992 .user-hovercard-header {
2999 clear: both;
2993 clear: both;
3000 min-height: 10px;
2994 min-height: 10px;
3001 }
2995 }
3002
2996
3003 .user-hovercard-footer {
2997 .user-hovercard-footer {
3004 clear: both;
2998 clear: both;
3005 min-height: 10px;
2999 min-height: 10px;
3006 }
3000 }
3007
3001
3008 .user-group-hovercard {
3002 .user-group-hovercard {
3009 padding: 5px;
3003 padding: 5px;
3010 }
3004 }
3011
3005
3012 .user-group-hovercard-icon {
3006 .user-group-hovercard-icon {
3013 display: inline;
3007 display: inline;
3014 padding: 0;
3008 padding: 0;
3015 box-sizing: content-box;
3009 box-sizing: content-box;
3016 border-radius: 50%;
3010 border-radius: 50%;
3017 float: left;
3011 float: left;
3018 }
3012 }
3019
3013
3020 .user-group-hovercard-name {
3014 .user-group-hovercard-name {
3021 float: left;
3015 float: left;
3022 vertical-align: top;
3016 vertical-align: top;
3023 padding-left: 10px;
3017 padding-left: 10px;
3024 min-width: 150px;
3018 min-width: 150px;
3025 }
3019 }
3026
3020
3027 .user-group-hovercard-icon i {
3021 .user-group-hovercard-icon i {
3028 border: 1px solid @grey4;
3022 border: 1px solid @grey4;
3029 border-radius: 4px;
3023 border-radius: 4px;
3030 }
3024 }
3031
3025
3032 .user-group-hovercard-bio {
3026 .user-group-hovercard-bio {
3033 clear: both;
3027 clear: both;
3034 padding-top: 10px;
3028 padding-top: 10px;
3035 line-height: 1.0em;
3029 line-height: 1.0em;
3036 }
3030 }
3037
3031
3038 .user-group-hovercard-header {
3032 .user-group-hovercard-header {
3039 clear: both;
3033 clear: both;
3040 min-height: 10px;
3034 min-height: 10px;
3041 }
3035 }
3042
3036
3043 .user-group-hovercard-footer {
3037 .user-group-hovercard-footer {
3044 clear: both;
3038 clear: both;
3045 min-height: 10px;
3039 min-height: 10px;
3046 }
3040 }
3047
3041
3048 .pr-hovercard-header {
3042 .pr-hovercard-header {
3049 clear: both;
3043 clear: both;
3050 display: block;
3044 display: block;
3051 line-height: 20px;
3045 line-height: 20px;
3052 }
3046 }
3053
3047
3054 .pr-hovercard-user {
3048 .pr-hovercard-user {
3055 display: flex;
3049 display: flex;
3056 align-items: center;
3050 align-items: center;
3057 padding-left: 5px;
3051 padding-left: 5px;
3058 }
3052 }
3059
3053
3060 .pr-hovercard-title {
3054 .pr-hovercard-title {
3061 padding-top: 5px;
3055 padding-top: 5px;
3062 } No newline at end of file
3056 }
@@ -1,400 +1,402 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']);
250 pyroutes.register('pullrequest_todos', '/%(repo_name)s/pull-request/%(pull_request_id)s/todos', ['repo_name', 'pull_request_id']);
249 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
251 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
250 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
252 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
251 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
253 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
252 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
254 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
253 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
255 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
254 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
256 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
255 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
257 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
256 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
258 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
257 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
259 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
258 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
260 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
259 pyroutes.register('edit_repo_perms_set_private', '/%(repo_name)s/settings/permissions/set_private', ['repo_name']);
261 pyroutes.register('edit_repo_perms_set_private', '/%(repo_name)s/settings/permissions/set_private', ['repo_name']);
260 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
262 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
261 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
263 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
262 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
264 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
263 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
265 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
264 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
266 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
265 pyroutes.register('repo_edit_toggle_locking', '/%(repo_name)s/settings/toggle_locking', ['repo_name']);
267 pyroutes.register('repo_edit_toggle_locking', '/%(repo_name)s/settings/toggle_locking', ['repo_name']);
266 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
268 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
267 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
269 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
268 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
270 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
269 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
271 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
270 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
272 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
271 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
273 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
272 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
274 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
273 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
275 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
274 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
276 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
275 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
277 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
276 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
278 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
277 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
279 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
278 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
280 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
279 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
281 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
280 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
282 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
281 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
283 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
282 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
284 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
283 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed-rss', ['repo_name']);
285 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed-rss', ['repo_name']);
284 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed-atom', ['repo_name']);
286 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed-atom', ['repo_name']);
285 pyroutes.register('rss_feed_home_old', '/%(repo_name)s/feed/rss', ['repo_name']);
287 pyroutes.register('rss_feed_home_old', '/%(repo_name)s/feed/rss', ['repo_name']);
286 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
288 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
287 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
289 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
288 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
290 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
289 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
291 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
290 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
292 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
291 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
293 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
292 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
294 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
293 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
295 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
294 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
296 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
295 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
297 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
296 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
298 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
297 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
299 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
298 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['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']);
299 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
301 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
300 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
302 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
301 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
303 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
302 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['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']);
303 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
305 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
304 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
306 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
305 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
307 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
306 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
308 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
307 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
309 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
308 pyroutes.register('search', '/_admin/search', []);
310 pyroutes.register('search', '/_admin/search', []);
309 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
311 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
310 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
312 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
311 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
313 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
312 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
314 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
313 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
315 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
314 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
316 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
315 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
317 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
316 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
318 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
317 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
319 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
318 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
320 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
319 pyroutes.register('my_account_auth_tokens_view', '/_admin/my_account/auth_tokens/view', []);
321 pyroutes.register('my_account_auth_tokens_view', '/_admin/my_account/auth_tokens/view', []);
320 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
322 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
321 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
323 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
322 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
324 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
323 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
325 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
324 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
326 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
325 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
327 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
326 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
328 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
327 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
329 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
328 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
330 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
329 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
331 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
330 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
332 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
331 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
333 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
332 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
334 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
333 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
335 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
334 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
336 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
335 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
337 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
336 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
338 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
337 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
339 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
338 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
340 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
339 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
341 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
340 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
342 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
341 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
343 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
342 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
344 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
343 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
345 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
344 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
346 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
345 pyroutes.register('gists_show', '/_admin/gists', []);
347 pyroutes.register('gists_show', '/_admin/gists', []);
346 pyroutes.register('gists_new', '/_admin/gists/new', []);
348 pyroutes.register('gists_new', '/_admin/gists/new', []);
347 pyroutes.register('gists_create', '/_admin/gists/create', []);
349 pyroutes.register('gists_create', '/_admin/gists/create', []);
348 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
350 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
349 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
351 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
350 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
352 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
351 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
353 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
352 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
354 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
353 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
355 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
354 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
356 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
355 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
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']);
356 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
358 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
357 pyroutes.register('debug_style_email', '/_admin/debug_style/email/%(email_id)s', ['email_id']);
359 pyroutes.register('debug_style_email', '/_admin/debug_style/email/%(email_id)s', ['email_id']);
358 pyroutes.register('debug_style_email_plain_rendered', '/_admin/debug_style/email-rendered/%(email_id)s', ['email_id']);
360 pyroutes.register('debug_style_email_plain_rendered', '/_admin/debug_style/email-rendered/%(email_id)s', ['email_id']);
359 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
361 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
360 pyroutes.register('apiv2', '/_admin/api', []);
362 pyroutes.register('apiv2', '/_admin/api', []);
361 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
363 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
362 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
364 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
363 pyroutes.register('login', '/_admin/login', []);
365 pyroutes.register('login', '/_admin/login', []);
364 pyroutes.register('register', '/_admin/register', []);
366 pyroutes.register('register', '/_admin/register', []);
365 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
367 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
366 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
368 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
367 pyroutes.register('repo_reviewers_review_rule_delete', '/%(repo_name)s/settings/review/rules/%(rule_id)s/delete', ['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']);
368 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
370 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
369 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
371 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
370 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
372 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
371 pyroutes.register('admin_settings_scheduler_show_tasks', '/_admin/settings/scheduler/_tasks', []);
373 pyroutes.register('admin_settings_scheduler_show_tasks', '/_admin/settings/scheduler/_tasks', []);
372 pyroutes.register('admin_settings_scheduler_show_all', '/_admin/settings/scheduler', []);
374 pyroutes.register('admin_settings_scheduler_show_all', '/_admin/settings/scheduler', []);
373 pyroutes.register('admin_settings_scheduler_new', '/_admin/settings/scheduler/new', []);
375 pyroutes.register('admin_settings_scheduler_new', '/_admin/settings/scheduler/new', []);
374 pyroutes.register('admin_settings_scheduler_create', '/_admin/settings/scheduler/create', []);
376 pyroutes.register('admin_settings_scheduler_create', '/_admin/settings/scheduler/create', []);
375 pyroutes.register('admin_settings_scheduler_edit', '/_admin/settings/scheduler/%(schedule_id)s', ['schedule_id']);
377 pyroutes.register('admin_settings_scheduler_edit', '/_admin/settings/scheduler/%(schedule_id)s', ['schedule_id']);
376 pyroutes.register('admin_settings_scheduler_update', '/_admin/settings/scheduler/%(schedule_id)s/update', ['schedule_id']);
378 pyroutes.register('admin_settings_scheduler_update', '/_admin/settings/scheduler/%(schedule_id)s/update', ['schedule_id']);
377 pyroutes.register('admin_settings_scheduler_delete', '/_admin/settings/scheduler/%(schedule_id)s/delete', ['schedule_id']);
379 pyroutes.register('admin_settings_scheduler_delete', '/_admin/settings/scheduler/%(schedule_id)s/delete', ['schedule_id']);
378 pyroutes.register('admin_settings_scheduler_execute', '/_admin/settings/scheduler/%(schedule_id)s/execute', ['schedule_id']);
380 pyroutes.register('admin_settings_scheduler_execute', '/_admin/settings/scheduler/%(schedule_id)s/execute', ['schedule_id']);
379 pyroutes.register('admin_settings_automation', '/_admin/settings/automation', []);
381 pyroutes.register('admin_settings_automation', '/_admin/settings/automation', []);
380 pyroutes.register('admin_settings_automation_update', '/_admin/settings/automation/%(entry_id)s/update', ['entry_id']);
382 pyroutes.register('admin_settings_automation_update', '/_admin/settings/automation/%(entry_id)s/update', ['entry_id']);
381 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
383 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
382 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
384 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
383 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
385 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
384 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
386 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
385 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
387 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
386 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
388 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
387 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
389 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
388 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
390 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
389 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
391 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
390 pyroutes.register('repo_artifacts_get', '/%(repo_name)s/artifacts/download/%(uid)s', ['repo_name', 'uid']);
392 pyroutes.register('repo_artifacts_get', '/%(repo_name)s/artifacts/download/%(uid)s', ['repo_name', 'uid']);
391 pyroutes.register('repo_artifacts_store', '/%(repo_name)s/artifacts/store', ['repo_name']);
393 pyroutes.register('repo_artifacts_store', '/%(repo_name)s/artifacts/store', ['repo_name']);
392 pyroutes.register('repo_artifacts_info', '/%(repo_name)s/artifacts/info/%(uid)s', ['repo_name', 'uid']);
394 pyroutes.register('repo_artifacts_info', '/%(repo_name)s/artifacts/info/%(uid)s', ['repo_name', 'uid']);
393 pyroutes.register('repo_artifacts_delete', '/%(repo_name)s/artifacts/delete/%(uid)s', ['repo_name', 'uid']);
395 pyroutes.register('repo_artifacts_delete', '/%(repo_name)s/artifacts/delete/%(uid)s', ['repo_name', 'uid']);
394 pyroutes.register('repo_artifacts_update', '/%(repo_name)s/artifacts/update/%(uid)s', ['repo_name', 'uid']);
396 pyroutes.register('repo_artifacts_update', '/%(repo_name)s/artifacts/update/%(uid)s', ['repo_name', 'uid']);
395 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
397 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
396 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
398 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
397 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
399 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
398 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
400 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
399 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
401 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
400 }
402 }
@@ -1,194 +1,205 b''
1 import {PolymerElement, html} from '@polymer/polymer/polymer-element.js';
1 import {PolymerElement, html} from '@polymer/polymer/polymer-element.js';
2 import '../channelstream-connection/channelstream-connection.js';
2 import '../channelstream-connection/channelstream-connection.js';
3 import '../rhodecode-toast/rhodecode-toast.js';
3 import '../rhodecode-toast/rhodecode-toast.js';
4 import '../rhodecode-favicon/rhodecode-favicon.js';
4 import '../rhodecode-favicon/rhodecode-favicon.js';
5
5
6 var ccLog = Logger.get('RhodeCodeApp');
6 var ccLog = Logger.get('RhodeCodeApp');
7 ccLog.setLevel(Logger.OFF);
7 ccLog.setLevel(Logger.OFF);
8
8
9 export class RhodecodeApp extends PolymerElement {
9 export class RhodecodeApp extends PolymerElement {
10
10
11 static get is() {
11 static get is() {
12 return 'rhodecode-app';
12 return 'rhodecode-app';
13 }
13 }
14
14
15 static get template(){
15 static get template(){
16 return html`
16 return html`
17 <channelstream-connection
17 <channelstream-connection
18 id="channelstream-connection"
18 id="channelstream-connection"
19 on-channelstream-listen-message="receivedMessage"
19 on-channelstream-listen-message="receivedMessage"
20 on-channelstream-connected="handleConnected"
20 on-channelstream-connected="handleConnected"
21 on-channelstream-subscribed="handleSubscribed">
21 on-channelstream-subscribed="handleSubscribed">
22 </channelstream-connection>
22 </channelstream-connection>
23 <rhodecode-favicon></rhodecode-favicon>
23 <rhodecode-favicon></rhodecode-favicon>
24 `
24 `
25 }
25 }
26
26
27 connectedCallback() {
27 connectedCallback() {
28 super.connectedCallback();
28 super.connectedCallback();
29 ccLog.debug('rhodeCodeApp created');
29 ccLog.debug('rhodeCodeApp created');
30 $.Topic('/notifications').subscribe(this.handleNotifications.bind(this));
30 $.Topic('/notifications').subscribe(this.handleNotifications.bind(this));
31 $.Topic('/comment').subscribe(this.handleComment.bind(this));
31 $.Topic('/favicon/update').subscribe(this.faviconUpdate.bind(this));
32 $.Topic('/favicon/update').subscribe(this.faviconUpdate.bind(this));
32 $.Topic('/connection_controller/subscribe').subscribe(
33 $.Topic('/connection_controller/subscribe').subscribe(
33 this.subscribeToChannelTopic.bind(this));
34 this.subscribeToChannelTopic.bind(this)
35 );
36
34 // this event can be used to coordinate plugins to do their
37 // this event can be used to coordinate plugins to do their
35 // initialization before channelstream is kicked off
38 // initialization before channelstream is kicked off
36 $.Topic('/__MAIN_APP__').publish({});
39 $.Topic('/__MAIN_APP__').publish({});
37
40
38 for (var i = 0; i < alertMessagePayloads.length; i++) {
41 for (var i = 0; i < alertMessagePayloads.length; i++) {
39 $.Topic('/notifications').publish(alertMessagePayloads[i]);
42 $.Topic('/notifications').publish(alertMessagePayloads[i]);
40 }
43 }
41 this.initPlugins();
44 this.initPlugins();
42 // after rest of application loads and topics get fired, launch connection
45 // after rest of application loads and topics get fired, launch connection
43 $(document).ready(function () {
46 $(document).ready(function () {
44 this.kickoffChannelstreamPlugin();
47 this.kickoffChannelstreamPlugin();
45 }.bind(this));
48 }.bind(this));
46 }
49 }
47
50
48 initPlugins() {
51 initPlugins() {
49 for (var i = 0; i < window.APPLICATION_PLUGINS.length; i++) {
52 for (var i = 0; i < window.APPLICATION_PLUGINS.length; i++) {
50 var pluginDef = window.APPLICATION_PLUGINS[i];
53 var pluginDef = window.APPLICATION_PLUGINS[i];
51 if (pluginDef.component) {
54 if (pluginDef.component) {
52 var pluginElem = document.createElement(pluginDef.component);
55 var pluginElem = document.createElement(pluginDef.component);
53 this.shadowRoot.appendChild(pluginElem);
56 this.shadowRoot.appendChild(pluginElem);
54 if (typeof pluginElem.init !== 'undefined') {
57 if (typeof pluginElem.init !== 'undefined') {
55 pluginElem.init();
58 pluginElem.init();
56 }
59 }
57 }
60 }
58 }
61 }
59 }
62 }
60
63
61 /** proxy to channelstream connection */
64 /** proxy to channelstream connection */
62 getChannelStreamConnection() {
65 getChannelStreamConnection() {
63 return this.$['channelstream-connection'];
66 return this.$['channelstream-connection'];
64 }
67 }
65
68
66 handleNotifications(data) {
69 handleNotifications(data) {
67 var elem = document.getElementById('notifications');
70 var elem = document.getElementById('notifications');
68 if (elem) {
71 if (elem) {
69 elem.handleNotification(data);
72 elem.handleNotification(data);
70 }
73 }
71
74
72 }
75 }
73
76
77 handleComment(data) {
78 if (data.message.comment_id) {
79 if (window.refreshAllComments !== undefined) {
80 refreshAllComments()
81 }
82 }
83 }
84
74 faviconUpdate(data) {
85 faviconUpdate(data) {
75 this.shadowRoot.querySelector('rhodecode-favicon').counter = data.count;
86 this.shadowRoot.querySelector('rhodecode-favicon').counter = data.count;
76 }
87 }
77
88
78 /** opens connection to ws server */
89 /** opens connection to ws server */
79 kickoffChannelstreamPlugin(data) {
90 kickoffChannelstreamPlugin(data) {
80 ccLog.debug('kickoffChannelstreamPlugin');
91 ccLog.debug('kickoffChannelstreamPlugin');
81 var channels = ['broadcast'];
92 var channels = ['broadcast'];
82 var addChannels = this.checkViewChannels();
93 var addChannels = this.checkViewChannels();
83 for (var i = 0; i < addChannels.length; i++) {
94 for (var i = 0; i < addChannels.length; i++) {
84 channels.push(addChannels[i]);
95 channels.push(addChannels[i]);
85 }
96 }
86 if (window.CHANNELSTREAM_SETTINGS && CHANNELSTREAM_SETTINGS.enabled) {
97 if (window.CHANNELSTREAM_SETTINGS && CHANNELSTREAM_SETTINGS.enabled) {
87 var channelstreamConnection = this.getChannelStreamConnection();
98 var channelstreamConnection = this.getChannelStreamConnection();
88 channelstreamConnection.connectUrl = CHANNELSTREAM_URLS.connect;
99 channelstreamConnection.connectUrl = CHANNELSTREAM_URLS.connect;
89 channelstreamConnection.subscribeUrl = CHANNELSTREAM_URLS.subscribe;
100 channelstreamConnection.subscribeUrl = CHANNELSTREAM_URLS.subscribe;
90 channelstreamConnection.websocketUrl = CHANNELSTREAM_URLS.ws + '/ws';
101 channelstreamConnection.websocketUrl = CHANNELSTREAM_URLS.ws + '/ws';
91 channelstreamConnection.longPollUrl = CHANNELSTREAM_URLS.longpoll + '/listen';
102 channelstreamConnection.longPollUrl = CHANNELSTREAM_URLS.longpoll + '/listen';
92 // some channels might already be registered by topic
103 // some channels might already be registered by topic
93 for (var i = 0; i < channels.length; i++) {
104 for (var i = 0; i < channels.length; i++) {
94 channelstreamConnection.push('channels', channels[i]);
105 channelstreamConnection.push('channels', channels[i]);
95 }
106 }
96 // append any additional channels registered in other plugins
107 // append any additional channels registered in other plugins
97 $.Topic('/connection_controller/subscribe').processPrepared();
108 $.Topic('/connection_controller/subscribe').processPrepared();
109
98 channelstreamConnection.connect();
110 channelstreamConnection.connect();
99 }
111 }
100 }
112 }
101
113
102 checkViewChannels() {
114 checkViewChannels() {
103 // subscribe to different channels data is sent.
115 // subscribe to different channels data is sent.
104
116
105 var channels = [];
117 var channels = [];
106 // subscribe to PR repo channel for PR's'
118 // subscribe to PR repo channel for PR's'
107 if (templateContext.pull_request_data.pull_request_id) {
119 if (templateContext.pull_request_data.pull_request_id) {
108 var channelName = '/repo$' + templateContext.repo_name + '$/pr/' +
120 var channelName = '/repo$' + templateContext.repo_name + '$/pr/' +
109 String(templateContext.pull_request_data.pull_request_id);
121 String(templateContext.pull_request_data.pull_request_id);
110 channels.push(channelName);
122 channels.push(channelName);
111 }
123 }
112
124
113 if (templateContext.commit_data.commit_id) {
125 if (templateContext.commit_data.commit_id) {
114 var channelName = '/repo$' + templateContext.repo_name + '$/commit/' +
126 var channelName = '/repo$' + templateContext.repo_name + '$/commit/' +
115 String(templateContext.commit_data.commit_id);
127 String(templateContext.commit_data.commit_id);
116 channels.push(channelName);
128 channels.push(channelName);
117 }
129 }
118
130
119 return channels;
131 return channels;
120 }
132 }
121
133
122 /** subscribes users from channels in channelstream */
134 /** subscribes users from channels in channelstream */
123 subscribeToChannelTopic(channels) {
135 subscribeToChannelTopic(channels) {
124 var channelstreamConnection = this.getChannelStreamConnection();
136 var channelstreamConnection = this.getChannelStreamConnection();
125 var toSubscribe = channelstreamConnection.calculateSubscribe(channels);
137 var toSubscribe = channelstreamConnection.calculateSubscribe(channels);
126 ccLog.debug('subscribeToChannelTopic', toSubscribe);
138 ccLog.debug('subscribeToChannelTopic', toSubscribe);
127 if (toSubscribe.length > 0) {
139 if (toSubscribe.length > 0) {
128 // if we are connected then subscribe
140 // if we are connected then subscribe
129 if (channelstreamConnection.connected) {
141 if (channelstreamConnection.connected) {
130 channelstreamConnection.subscribe(toSubscribe);
142 channelstreamConnection.subscribe(toSubscribe);
131 }
143 }
132 // not connected? just push channels onto the stack
144 // not connected? just push channels onto the stack
133 else {
145 else {
134 for (var i = 0; i < toSubscribe.length; i++) {
146 for (var i = 0; i < toSubscribe.length; i++) {
135 channelstreamConnection.push('channels', toSubscribe[i]);
147 channelstreamConnection.push('channels', toSubscribe[i]);
136 }
148 }
137 }
149 }
138 }
150 }
139 }
151 }
140
152
141 /** publish received messages into correct topic */
153 /** publish received messages into correct topic */
142 receivedMessage(event) {
154 receivedMessage(event) {
143 for (var i = 0; i < event.detail.length; i++) {
155 for (var i = 0; i < event.detail.length; i++) {
144 var message = event.detail[i];
156 var message = event.detail[i];
145 if (message.message.topic) {
157 if (message.message.topic) {
146 ccLog.debug('publishing', message.message.topic);
158 ccLog.debug('publishing', message.message.topic);
147 $.Topic(message.message.topic).publish(message);
159 $.Topic(message.message.topic).publish(message);
148 }
160 }
149 else if (message.type === 'presence') {
161 else if (message.type === 'presence') {
150 $.Topic('/connection_controller/presence').publish(message);
162 $.Topic('/connection_controller/presence').publish(message);
151 }
163 }
152 else {
164 else {
153 ccLog.warn('unhandled message', message);
165 ccLog.warn('unhandled message', message);
154 }
166 }
155 }
167 }
156 }
168 }
157
169
158 handleConnected(event) {
170 handleConnected(event) {
159 var channelstreamConnection = this.getChannelStreamConnection();
171 var channelstreamConnection = this.getChannelStreamConnection();
160 channelstreamConnection.set('channelsState',
172 channelstreamConnection.set('channelsState', event.detail.channels_info);
161 event.detail.channels_info);
162 channelstreamConnection.set('userState', event.detail.state);
173 channelstreamConnection.set('userState', event.detail.state);
163 channelstreamConnection.set('channels', event.detail.channels);
174 channelstreamConnection.set('channels', event.detail.channels);
164 this.propagageChannelsState();
175 this.propagageChannelsState();
165 }
176 }
166
177
167 handleSubscribed(event) {
178 handleSubscribed(event) {
168 var channelstreamConnection = this.getChannelStreamConnection();
179 var channelstreamConnection = this.getChannelStreamConnection();
169 var channelInfo = event.detail.channels_info;
180 var channelInfo = event.detail.channels_info;
170 var channelKeys = Object.keys(event.detail.channels_info);
181 var channelKeys = Object.keys(event.detail.channels_info);
171 for (var i = 0; i < channelKeys.length; i++) {
182 for (var i = 0; i < channelKeys.length; i++) {
172 var key = channelKeys[i];
183 var key = channelKeys[i];
173 channelstreamConnection.set(['channelsState', key], channelInfo[key]);
184 channelstreamConnection.set(['channelsState', key], channelInfo[key]);
174 }
185 }
175 channelstreamConnection.set('channels', event.detail.channels);
186 channelstreamConnection.set('channels', event.detail.channels);
176 this.propagageChannelsState();
187 this.propagageChannelsState();
177 }
188 }
178
189
179 /** propagates channel states on topics */
190 /** propagates channel states on topics */
180 propagageChannelsState(event) {
191 propagageChannelsState(event) {
181 var channelstreamConnection = this.getChannelStreamConnection();
192 var channelstreamConnection = this.getChannelStreamConnection();
182 var channel_data = channelstreamConnection.channelsState;
193 var channel_data = channelstreamConnection.channelsState;
183 var channels = channelstreamConnection.channels;
194 var channels = channelstreamConnection.channels;
184 for (var i = 0; i < channels.length; i++) {
195 for (var i = 0; i < channels.length; i++) {
185 var key = channels[i];
196 var key = channels[i];
186 $.Topic('/connection_controller/channel_update').publish(
197 $.Topic('/connection_controller/channel_update').publish(
187 {channel: key, state: channel_data[key]}
198 {channel: key, state: channel_data[key]}
188 );
199 );
189 }
200 }
190 }
201 }
191
202
192 }
203 }
193
204
194 customElements.define(RhodecodeApp.is, RhodecodeApp);
205 customElements.define(RhodecodeApp.is, RhodecodeApp);
@@ -1,695 +1,697 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 RhodeCode JS Files
20 RhodeCode JS Files
21 **/
21 **/
22
22
23 if (typeof console == "undefined" || typeof console.log == "undefined"){
23 if (typeof console == "undefined" || typeof console.log == "undefined"){
24 console = { log: function() {} }
24 console = { log: function() {} }
25 }
25 }
26
26
27 // TODO: move the following function to submodules
27 // TODO: move the following function to submodules
28
28
29 /**
29 /**
30 * show more
30 * show more
31 */
31 */
32 var show_more_event = function(){
32 var show_more_event = function(){
33 $('table .show_more').click(function(e) {
33 $('table .show_more').click(function(e) {
34 var cid = e.target.id.substring(1);
34 var cid = e.target.id.substring(1);
35 var button = $(this);
35 var button = $(this);
36 if (button.hasClass('open')) {
36 if (button.hasClass('open')) {
37 $('#'+cid).hide();
37 $('#'+cid).hide();
38 button.removeClass('open');
38 button.removeClass('open');
39 } else {
39 } else {
40 $('#'+cid).show();
40 $('#'+cid).show();
41 button.addClass('open one');
41 button.addClass('open one');
42 }
42 }
43 });
43 });
44 };
44 };
45
45
46 var compare_radio_buttons = function(repo_name, compare_ref_type){
46 var compare_radio_buttons = function(repo_name, compare_ref_type){
47 $('#compare_action').on('click', function(e){
47 $('#compare_action').on('click', function(e){
48 e.preventDefault();
48 e.preventDefault();
49
49
50 var source = $('input[name=compare_source]:checked').val();
50 var source = $('input[name=compare_source]:checked').val();
51 var target = $('input[name=compare_target]:checked').val();
51 var target = $('input[name=compare_target]:checked').val();
52 if(source && target){
52 if(source && target){
53 var url_data = {
53 var url_data = {
54 repo_name: repo_name,
54 repo_name: repo_name,
55 source_ref: source,
55 source_ref: source,
56 source_ref_type: compare_ref_type,
56 source_ref_type: compare_ref_type,
57 target_ref: target,
57 target_ref: target,
58 target_ref_type: compare_ref_type,
58 target_ref_type: compare_ref_type,
59 merge: 1
59 merge: 1
60 };
60 };
61 window.location = pyroutes.url('repo_compare', url_data);
61 window.location = pyroutes.url('repo_compare', url_data);
62 }
62 }
63 });
63 });
64 $('.compare-radio-button').on('click', function(e){
64 $('.compare-radio-button').on('click', function(e){
65 var source = $('input[name=compare_source]:checked').val();
65 var source = $('input[name=compare_source]:checked').val();
66 var target = $('input[name=compare_target]:checked').val();
66 var target = $('input[name=compare_target]:checked').val();
67 if(source && target){
67 if(source && target){
68 $('#compare_action').removeAttr("disabled");
68 $('#compare_action').removeAttr("disabled");
69 $('#compare_action').removeClass("disabled");
69 $('#compare_action').removeClass("disabled");
70 }
70 }
71 })
71 })
72 };
72 };
73
73
74 var showRepoSize = function(target, repo_name, commit_id, callback) {
74 var showRepoSize = function(target, repo_name, commit_id, callback) {
75 var container = $('#' + target);
75 var container = $('#' + target);
76 var url = pyroutes.url('repo_stats',
76 var url = pyroutes.url('repo_stats',
77 {"repo_name": repo_name, "commit_id": commit_id});
77 {"repo_name": repo_name, "commit_id": commit_id});
78
78
79 container.show();
79 container.show();
80 if (!container.hasClass('loaded')) {
80 if (!container.hasClass('loaded')) {
81 $.ajax({url: url})
81 $.ajax({url: url})
82 .complete(function (data) {
82 .complete(function (data) {
83 var responseJSON = data.responseJSON;
83 var responseJSON = data.responseJSON;
84 container.addClass('loaded');
84 container.addClass('loaded');
85 container.html(responseJSON.size);
85 container.html(responseJSON.size);
86 callback(responseJSON.code_stats)
86 callback(responseJSON.code_stats)
87 })
87 })
88 .fail(function (data) {
88 .fail(function (data) {
89 console.log('failed to load repo stats');
89 console.log('failed to load repo stats');
90 });
90 });
91 }
91 }
92
92
93 };
93 };
94
94
95 var showRepoStats = function(target, data){
95 var showRepoStats = function(target, data){
96 var container = $('#' + target);
96 var container = $('#' + target);
97
97
98 if (container.hasClass('loaded')) {
98 if (container.hasClass('loaded')) {
99 return
99 return
100 }
100 }
101
101
102 var total = 0;
102 var total = 0;
103 var no_data = true;
103 var no_data = true;
104 var tbl = document.createElement('table');
104 var tbl = document.createElement('table');
105 tbl.setAttribute('class', 'trending_language_tbl rctable');
105 tbl.setAttribute('class', 'trending_language_tbl rctable');
106
106
107 $.each(data, function(key, val){
107 $.each(data, function(key, val){
108 total += val.count;
108 total += val.count;
109 });
109 });
110
110
111 var sortedStats = [];
111 var sortedStats = [];
112 for (var obj in data){
112 for (var obj in data){
113 sortedStats.push([obj, data[obj]])
113 sortedStats.push([obj, data[obj]])
114 }
114 }
115 var sortedData = sortedStats.sort(function (a, b) {
115 var sortedData = sortedStats.sort(function (a, b) {
116 return b[1].count - a[1].count
116 return b[1].count - a[1].count
117 });
117 });
118 var cnt = 0;
118 var cnt = 0;
119 $.each(sortedData, function(idx, val){
119 $.each(sortedData, function(idx, val){
120 cnt += 1;
120 cnt += 1;
121 no_data = false;
121 no_data = false;
122
122
123 var tr = document.createElement('tr');
123 var tr = document.createElement('tr');
124
124
125 var key = val[0];
125 var key = val[0];
126 var obj = {"desc": val[1].desc, "count": val[1].count};
126 var obj = {"desc": val[1].desc, "count": val[1].count};
127
127
128 // meta language names
128 // meta language names
129 var td1 = document.createElement('td');
129 var td1 = document.createElement('td');
130 var trending_language_label = document.createElement('div');
130 var trending_language_label = document.createElement('div');
131 trending_language_label.innerHTML = obj.desc;
131 trending_language_label.innerHTML = obj.desc;
132 td1.appendChild(trending_language_label);
132 td1.appendChild(trending_language_label);
133
133
134 // extensions
134 // extensions
135 var td2 = document.createElement('td');
135 var td2 = document.createElement('td');
136 var extension = document.createElement('div');
136 var extension = document.createElement('div');
137 extension.innerHTML = ".{0}".format(key)
137 extension.innerHTML = ".{0}".format(key)
138 td2.appendChild(extension);
138 td2.appendChild(extension);
139
139
140 // number of files
140 // number of files
141 var td3 = document.createElement('td');
141 var td3 = document.createElement('td');
142 var file_count = document.createElement('div');
142 var file_count = document.createElement('div');
143 var percentage_num = Math.round((obj.count / total * 100), 2);
143 var percentage_num = Math.round((obj.count / total * 100), 2);
144 var label = _ngettext('file', 'files', obj.count);
144 var label = _ngettext('file', 'files', obj.count);
145 file_count.innerHTML = "{0} {1} ({2}%)".format(obj.count, label, percentage_num) ;
145 file_count.innerHTML = "{0} {1} ({2}%)".format(obj.count, label, percentage_num) ;
146 td3.appendChild(file_count);
146 td3.appendChild(file_count);
147
147
148 // percentage
148 // percentage
149 var td4 = document.createElement('td');
149 var td4 = document.createElement('td');
150 td4.setAttribute("class", 'trending_language');
150 td4.setAttribute("class", 'trending_language');
151
151
152 var percentage = document.createElement('div');
152 var percentage = document.createElement('div');
153 percentage.setAttribute('class', 'lang-bar');
153 percentage.setAttribute('class', 'lang-bar');
154 percentage.innerHTML = "&nbsp;";
154 percentage.innerHTML = "&nbsp;";
155 percentage.style.width = percentage_num + '%';
155 percentage.style.width = percentage_num + '%';
156 td4.appendChild(percentage);
156 td4.appendChild(percentage);
157
157
158 tr.appendChild(td1);
158 tr.appendChild(td1);
159 tr.appendChild(td2);
159 tr.appendChild(td2);
160 tr.appendChild(td3);
160 tr.appendChild(td3);
161 tr.appendChild(td4);
161 tr.appendChild(td4);
162 tbl.appendChild(tr);
162 tbl.appendChild(tr);
163
163
164 });
164 });
165
165
166 $(container).html(tbl);
166 $(container).html(tbl);
167 $(container).addClass('loaded');
167 $(container).addClass('loaded');
168
168
169 $('#code_stats_show_more').on('click', function (e) {
169 $('#code_stats_show_more').on('click', function (e) {
170 e.preventDefault();
170 e.preventDefault();
171 $('.stats_hidden').each(function (idx) {
171 $('.stats_hidden').each(function (idx) {
172 $(this).css("display", "");
172 $(this).css("display", "");
173 });
173 });
174 $('#code_stats_show_more').hide();
174 $('#code_stats_show_more').hide();
175 });
175 });
176
176
177 };
177 };
178
178
179 // returns a node from given html;
179 // returns a node from given html;
180 var fromHTML = function(html){
180 var fromHTML = function(html){
181 var _html = document.createElement('element');
181 var _html = document.createElement('element');
182 _html.innerHTML = html;
182 _html.innerHTML = html;
183 return _html;
183 return _html;
184 };
184 };
185
185
186 // Toggle Collapsable Content
186 // Toggle Collapsable Content
187 function collapsableContent() {
187 function collapsableContent() {
188
188
189 $('.collapsable-content').not('.no-hide').hide();
189 $('.collapsable-content').not('.no-hide').hide();
190
190
191 $('.btn-collapse').unbind(); //in case we've been here before
191 $('.btn-collapse').unbind(); //in case we've been here before
192 $('.btn-collapse').click(function() {
192 $('.btn-collapse').click(function() {
193 var button = $(this);
193 var button = $(this);
194 var togglename = $(this).data("toggle");
194 var togglename = $(this).data("toggle");
195 $('.collapsable-content[data-toggle='+togglename+']').toggle();
195 $('.collapsable-content[data-toggle='+togglename+']').toggle();
196 if ($(this).html()=="Show Less")
196 if ($(this).html()=="Show Less")
197 $(this).html("Show More");
197 $(this).html("Show More");
198 else
198 else
199 $(this).html("Show Less");
199 $(this).html("Show Less");
200 });
200 });
201 };
201 };
202
202
203 var timeagoActivate = function() {
203 var timeagoActivate = function() {
204 $("time.timeago").timeago();
204 $("time.timeago").timeago();
205 };
205 };
206
206
207
207
208 var clipboardActivate = function() {
208 var clipboardActivate = function() {
209 /*
209 /*
210 *
210 *
211 * <i class="tooltip icon-plus clipboard-action" data-clipboard-text="${commit.raw_id}" title="${_('Copy the full commit id')}"></i>
211 * <i class="tooltip icon-plus clipboard-action" data-clipboard-text="${commit.raw_id}" title="${_('Copy the full commit id')}"></i>
212 * */
212 * */
213 var clipboard = new ClipboardJS('.clipboard-action');
213 var clipboard = new ClipboardJS('.clipboard-action');
214
214
215 clipboard.on('success', function(e) {
215 clipboard.on('success', function(e) {
216 var callback = function () {
216 var callback = function () {
217 $(e.trigger).animate({'opacity': 1.00}, 200)
217 $(e.trigger).animate({'opacity': 1.00}, 200)
218 };
218 };
219 $(e.trigger).animate({'opacity': 0.15}, 200, callback);
219 $(e.trigger).animate({'opacity': 0.15}, 200, callback);
220 e.clearSelection();
220 e.clearSelection();
221 });
221 });
222 };
222 };
223
223
224 var tooltipActivate = function () {
224 var tooltipActivate = function () {
225 var delay = 50;
225 var delay = 50;
226 var animation = 'fade';
226 var animation = 'fade';
227 var theme = 'tooltipster-shadow';
227 var theme = 'tooltipster-shadow';
228 var debug = false;
228 var debug = false;
229
229
230 $('.tooltip').tooltipster({
230 $('.tooltip').tooltipster({
231 debug: debug,
231 debug: debug,
232 theme: theme,
232 theme: theme,
233 animation: animation,
233 animation: animation,
234 delay: delay,
234 delay: delay,
235 contentCloning: true,
235 contentCloning: true,
236 contentAsHTML: true,
236 contentAsHTML: true,
237
237
238 functionBefore: function (instance, helper) {
238 functionBefore: function (instance, helper) {
239 var $origin = $(helper.origin);
239 var $origin = $(helper.origin);
240 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(instance.content());
240 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(instance.content());
241 instance.content(data);
241 instance.content(data);
242 }
242 }
243 });
243 });
244 var hovercardCache = {};
244 var hovercardCache = {};
245
245
246 var loadHoverCard = function (url, altHovercard, callback) {
246 var loadHoverCard = function (url, altHovercard, callback) {
247 var id = url;
247 var id = url;
248
248
249 if (hovercardCache[id] !== undefined) {
249 if (hovercardCache[id] !== undefined) {
250 callback(hovercardCache[id]);
250 callback(hovercardCache[id]);
251 return true;
251 return true;
252 }
252 }
253
253
254 hovercardCache[id] = undefined;
254 hovercardCache[id] = undefined;
255 $.get(url, function (data) {
255 $.get(url, function (data) {
256 hovercardCache[id] = data;
256 hovercardCache[id] = data;
257 callback(hovercardCache[id]);
257 callback(hovercardCache[id]);
258 return true;
258 return true;
259 }).fail(function (data, textStatus, errorThrown) {
259 }).fail(function (data, textStatus, errorThrown) {
260
260
261 if (parseInt(data.status) === 404) {
261 if (parseInt(data.status) === 404) {
262 var msg = "<p>{0}</p>".format(altHovercard || "No Data exists for this hovercard");
262 var msg = "<p>{0}</p>".format(altHovercard || "No Data exists for this hovercard");
263 } else {
263 } else {
264 var msg = "<p class='error-message'>Error while fetching hovercard.\nError code {0} ({1}).</p>".format(data.status,data.statusText);
264 var msg = "<p class='error-message'>Error while fetching hovercard.\nError code {0} ({1}).</p>".format(data.status,data.statusText);
265 }
265 }
266 callback(msg);
266 callback(msg);
267 return false
267 return false
268 });
268 });
269 };
269 };
270
270
271 $('.tooltip-hovercard').tooltipster({
271 $('.tooltip-hovercard').tooltipster({
272 debug: debug,
272 debug: debug,
273 theme: theme,
273 theme: theme,
274 animation: animation,
274 animation: animation,
275 delay: delay,
275 delay: delay,
276 interactive: true,
276 interactive: true,
277 contentCloning: true,
277 contentCloning: true,
278
278
279 trigger: 'custom',
279 trigger: 'custom',
280 triggerOpen: {
280 triggerOpen: {
281 mouseenter: true,
281 mouseenter: true,
282 },
282 },
283 triggerClose: {
283 triggerClose: {
284 mouseleave: true,
284 mouseleave: true,
285 originClick: true,
285 originClick: true,
286 touchleave: true
286 touchleave: true
287 },
287 },
288 content: _gettext('Loading...'),
288 content: _gettext('Loading...'),
289 contentAsHTML: true,
289 contentAsHTML: true,
290 updateAnimation: null,
290 updateAnimation: null,
291
291
292 functionBefore: function (instance, helper) {
292 functionBefore: function (instance, helper) {
293
293
294 var $origin = $(helper.origin);
294 var $origin = $(helper.origin);
295
295
296 // we set a variable so the data is only loaded once via Ajax, not every time the tooltip opens
296 // we set a variable so the data is only loaded once via Ajax, not every time the tooltip opens
297 if ($origin.data('loaded') !== true) {
297 if ($origin.data('loaded') !== true) {
298 var hovercardUrl = $origin.data('hovercardUrl');
298 var hovercardUrl = $origin.data('hovercardUrl');
299 var altHovercard =$origin.data('hovercardAlt');
299 var altHovercard =$origin.data('hovercardAlt');
300
300
301 if (hovercardUrl !== undefined && hovercardUrl !== "") {
301 if (hovercardUrl !== undefined && hovercardUrl !== "") {
302 if (hovercardUrl.substr(0,12) === 'pyroutes.url'){
302 if (hovercardUrl.substr(0,12) === 'pyroutes.url'){
303 hovercardUrl = eval(hovercardUrl)
303 hovercardUrl = eval(hovercardUrl)
304 }
304 }
305
305
306 var loaded = loadHoverCard(hovercardUrl, altHovercard, function (data) {
306 var loaded = loadHoverCard(hovercardUrl, altHovercard, function (data) {
307 instance.content(data);
307 instance.content(data);
308 })
308 })
309 } else {
309 } else {
310 if ($origin.data('hovercardAltHtml')) {
310 if ($origin.data('hovercardAltHtml')) {
311 var data = atob($origin.data('hovercardAltHtml'));
311 var data = atob($origin.data('hovercardAltHtml'));
312 } else {
312 } else {
313 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(altHovercard)
313 var data = '<div style="white-space: pre-wrap">{0}</div>'.format(altHovercard)
314 }
314 }
315 var loaded = true;
315 var loaded = true;
316 instance.content(data);
316 instance.content(data);
317 }
317 }
318
318
319 // to remember that the data has been loaded
319 // to remember that the data has been loaded
320 $origin.data('loaded', loaded);
320 $origin.data('loaded', loaded);
321 }
321 }
322 }
322 }
323 })
323 })
324 };
324 };
325
325
326 // Formatting values in a Select2 dropdown of commit references
326 // Formatting values in a Select2 dropdown of commit references
327 var formatSelect2SelectionRefs = function(commit_ref){
327 var formatSelect2SelectionRefs = function(commit_ref){
328 var tmpl = '';
328 var tmpl = '';
329 if (!commit_ref.text || commit_ref.type === 'sha'){
329 if (!commit_ref.text || commit_ref.type === 'sha'){
330 return commit_ref.text;
330 return commit_ref.text;
331 }
331 }
332 if (commit_ref.type === 'branch'){
332 if (commit_ref.type === 'branch'){
333 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
333 tmpl = tmpl.concat('<i class="icon-branch"></i> ');
334 } else if (commit_ref.type === 'tag'){
334 } else if (commit_ref.type === 'tag'){
335 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
335 tmpl = tmpl.concat('<i class="icon-tag"></i> ');
336 } else if (commit_ref.type === 'book'){
336 } else if (commit_ref.type === 'book'){
337 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
337 tmpl = tmpl.concat('<i class="icon-bookmark"></i> ');
338 }
338 }
339 return tmpl.concat(escapeHtml(commit_ref.text));
339 return tmpl.concat(escapeHtml(commit_ref.text));
340 };
340 };
341
341
342 // takes a given html element and scrolls it down offset pixels
342 // takes a given html element and scrolls it down offset pixels
343 function offsetScroll(element, offset) {
343 function offsetScroll(element, offset) {
344 setTimeout(function() {
344 setTimeout(function() {
345 var location = element.offset().top;
345 var location = element.offset().top;
346 // some browsers use body, some use html
346 // some browsers use body, some use html
347 $('html, body').animate({ scrollTop: (location - offset) });
347 $('html, body').animate({ scrollTop: (location - offset) });
348 }, 100);
348 }, 100);
349 }
349 }
350
350
351 // scroll an element `percent`% from the top of page in `time` ms
351 // scroll an element `percent`% from the top of page in `time` ms
352 function scrollToElement(element, percent, time) {
352 function scrollToElement(element, percent, time) {
353 percent = (percent === undefined ? 25 : percent);
353 percent = (percent === undefined ? 25 : percent);
354 time = (time === undefined ? 100 : time);
354 time = (time === undefined ? 100 : time);
355
355
356 var $element = $(element);
356 var $element = $(element);
357 if ($element.length == 0) {
357 if ($element.length == 0) {
358 throw('Cannot scroll to {0}'.format(element))
358 throw('Cannot scroll to {0}'.format(element))
359 }
359 }
360 var elOffset = $element.offset().top;
360 var elOffset = $element.offset().top;
361 var elHeight = $element.height();
361 var elHeight = $element.height();
362 var windowHeight = $(window).height();
362 var windowHeight = $(window).height();
363 var offset = elOffset;
363 var offset = elOffset;
364 if (elHeight < windowHeight) {
364 if (elHeight < windowHeight) {
365 offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2));
365 offset = elOffset - ((windowHeight / (100 / percent)) - (elHeight / 2));
366 }
366 }
367 setTimeout(function() {
367 setTimeout(function() {
368 $('html, body').animate({ scrollTop: offset});
368 $('html, body').animate({ scrollTop: offset});
369 }, time);
369 }, time);
370 }
370 }
371
371
372 /**
372 /**
373 * global hooks after DOM is loaded
373 * global hooks after DOM is loaded
374 */
374 */
375 $(document).ready(function() {
375 $(document).ready(function() {
376 firefoxAnchorFix();
376 firefoxAnchorFix();
377
377
378 $('.navigation a.menulink').on('click', function(e){
378 $('.navigation a.menulink').on('click', function(e){
379 var menuitem = $(this).parent('li');
379 var menuitem = $(this).parent('li');
380 if (menuitem.hasClass('open')) {
380 if (menuitem.hasClass('open')) {
381 menuitem.removeClass('open');
381 menuitem.removeClass('open');
382 } else {
382 } else {
383 menuitem.addClass('open');
383 menuitem.addClass('open');
384 $(document).on('click', function(event) {
384 $(document).on('click', function(event) {
385 if (!$(event.target).closest(menuitem).length) {
385 if (!$(event.target).closest(menuitem).length) {
386 menuitem.removeClass('open');
386 menuitem.removeClass('open');
387 }
387 }
388 });
388 });
389 }
389 }
390 });
390 });
391
391
392 $('body').on('click', '.cb-lineno a', function(event) {
392 $('body').on('click', '.cb-lineno a', function(event) {
393 function sortNumber(a,b) {
393 function sortNumber(a,b) {
394 return a - b;
394 return a - b;
395 }
395 }
396
396
397 var lineNo = $(this).data('lineNo');
397 var lineNo = $(this).data('lineNo');
398 var lineName = $(this).attr('name');
398 var lineName = $(this).attr('name');
399
399
400 if (lineNo) {
400 if (lineNo) {
401 var prevLine = $('.cb-line-selected a').data('lineNo');
401 var prevLine = $('.cb-line-selected a').data('lineNo');
402
402
403 // on shift, we do a range selection, if we got previous line
403 // on shift, we do a range selection, if we got previous line
404 if (event.shiftKey && prevLine !== undefined) {
404 if (event.shiftKey && prevLine !== undefined) {
405 var prevLine = parseInt(prevLine);
405 var prevLine = parseInt(prevLine);
406 var nextLine = parseInt(lineNo);
406 var nextLine = parseInt(lineNo);
407 var pos = [prevLine, nextLine].sort(sortNumber);
407 var pos = [prevLine, nextLine].sort(sortNumber);
408 var anchor = '#L{0}-{1}'.format(pos[0], pos[1]);
408 var anchor = '#L{0}-{1}'.format(pos[0], pos[1]);
409
409
410 // single click
410 // single click
411 } else {
411 } else {
412 var nextLine = parseInt(lineNo);
412 var nextLine = parseInt(lineNo);
413 var pos = [nextLine, nextLine];
413 var pos = [nextLine, nextLine];
414 var anchor = '#L{0}'.format(pos[0]);
414 var anchor = '#L{0}'.format(pos[0]);
415
415
416 }
416 }
417 // highlight
417 // highlight
418 var range = [];
418 var range = [];
419 for (var i = pos[0]; i <= pos[1]; i++) {
419 for (var i = pos[0]; i <= pos[1]; i++) {
420 range.push(i);
420 range.push(i);
421 }
421 }
422 // clear old selected lines
422 // clear old selected lines
423 $('.cb-line-selected').removeClass('cb-line-selected');
423 $('.cb-line-selected').removeClass('cb-line-selected');
424
424
425 $.each(range, function (i, lineNo) {
425 $.each(range, function (i, lineNo) {
426 var line_td = $('td.cb-lineno#L' + lineNo);
426 var line_td = $('td.cb-lineno#L' + lineNo);
427
427
428 if (line_td.length) {
428 if (line_td.length) {
429 line_td.addClass('cb-line-selected'); // line number td
429 line_td.addClass('cb-line-selected'); // line number td
430 line_td.prev().addClass('cb-line-selected'); // line data
430 line_td.prev().addClass('cb-line-selected'); // line data
431 line_td.next().addClass('cb-line-selected'); // line content
431 line_td.next().addClass('cb-line-selected'); // line content
432 }
432 }
433 });
433 });
434
434
435 } else if (lineName !== undefined) { // lineName only occurs in diffs
435 } else if (lineName !== undefined) { // lineName only occurs in diffs
436 // clear old selected lines
436 // clear old selected lines
437 $('td.cb-line-selected').removeClass('cb-line-selected');
437 $('td.cb-line-selected').removeClass('cb-line-selected');
438 var anchor = '#{0}'.format(lineName);
438 var anchor = '#{0}'.format(lineName);
439 var diffmode = templateContext.session_attrs.diffmode || "sideside";
439 var diffmode = templateContext.session_attrs.diffmode || "sideside";
440
440
441 if (diffmode === "unified") {
441 if (diffmode === "unified") {
442 $(this).closest('tr').find('td').addClass('cb-line-selected');
442 $(this).closest('tr').find('td').addClass('cb-line-selected');
443 } else {
443 } else {
444 var activeTd = $(this).closest('td');
444 var activeTd = $(this).closest('td');
445 activeTd.addClass('cb-line-selected');
445 activeTd.addClass('cb-line-selected');
446 activeTd.next('td').addClass('cb-line-selected');
446 activeTd.next('td').addClass('cb-line-selected');
447 }
447 }
448
448
449 }
449 }
450
450
451 // Replace URL without jumping to it if browser supports.
451 // Replace URL without jumping to it if browser supports.
452 // Default otherwise
452 // Default otherwise
453 if (history.pushState && anchor !== undefined) {
453 if (history.pushState && anchor !== undefined) {
454 var new_location = location.href.rstrip('#');
454 var new_location = location.href.rstrip('#');
455 if (location.hash) {
455 if (location.hash) {
456 // location without hash
456 // location without hash
457 new_location = new_location.replace(location.hash, "");
457 new_location = new_location.replace(location.hash, "");
458 }
458 }
459
459
460 // Make new anchor url
460 // Make new anchor url
461 new_location = new_location + anchor;
461 new_location = new_location + anchor;
462 history.pushState(true, document.title, new_location);
462 history.pushState(true, document.title, new_location);
463
463
464 return false;
464 return false;
465 }
465 }
466
466
467 });
467 });
468
468
469 $('.collapse_file').on('click', function(e) {
469 $('.collapse_file').on('click', function(e) {
470 e.stopPropagation();
470 e.stopPropagation();
471 if ($(e.target).is('a')) { return; }
471 if ($(e.target).is('a')) { return; }
472 var node = $(e.delegateTarget).first();
472 var node = $(e.delegateTarget).first();
473 var icon = $($(node.children().first()).children().first());
473 var icon = $($(node.children().first()).children().first());
474 var id = node.attr('fid');
474 var id = node.attr('fid');
475 var target = $('#'+id);
475 var target = $('#'+id);
476 var tr = $('#tr_'+id);
476 var tr = $('#tr_'+id);
477 var diff = $('#diff_'+id);
477 var diff = $('#diff_'+id);
478 if(node.hasClass('expand_file')){
478 if(node.hasClass('expand_file')){
479 node.removeClass('expand_file');
479 node.removeClass('expand_file');
480 icon.removeClass('expand_file_icon');
480 icon.removeClass('expand_file_icon');
481 node.addClass('collapse_file');
481 node.addClass('collapse_file');
482 icon.addClass('collapse_file_icon');
482 icon.addClass('collapse_file_icon');
483 diff.show();
483 diff.show();
484 tr.show();
484 tr.show();
485 target.show();
485 target.show();
486 } else {
486 } else {
487 node.removeClass('collapse_file');
487 node.removeClass('collapse_file');
488 icon.removeClass('collapse_file_icon');
488 icon.removeClass('collapse_file_icon');
489 node.addClass('expand_file');
489 node.addClass('expand_file');
490 icon.addClass('expand_file_icon');
490 icon.addClass('expand_file_icon');
491 diff.hide();
491 diff.hide();
492 tr.hide();
492 tr.hide();
493 target.hide();
493 target.hide();
494 }
494 }
495 });
495 });
496
496
497 $('#expand_all_files').click(function() {
497 $('#expand_all_files').click(function() {
498 $('.expand_file').each(function() {
498 $('.expand_file').each(function() {
499 var node = $(this);
499 var node = $(this);
500 var icon = $($(node.children().first()).children().first());
500 var icon = $($(node.children().first()).children().first());
501 var id = $(this).attr('fid');
501 var id = $(this).attr('fid');
502 var target = $('#'+id);
502 var target = $('#'+id);
503 var tr = $('#tr_'+id);
503 var tr = $('#tr_'+id);
504 var diff = $('#diff_'+id);
504 var diff = $('#diff_'+id);
505 node.removeClass('expand_file');
505 node.removeClass('expand_file');
506 icon.removeClass('expand_file_icon');
506 icon.removeClass('expand_file_icon');
507 node.addClass('collapse_file');
507 node.addClass('collapse_file');
508 icon.addClass('collapse_file_icon');
508 icon.addClass('collapse_file_icon');
509 diff.show();
509 diff.show();
510 tr.show();
510 tr.show();
511 target.show();
511 target.show();
512 });
512 });
513 });
513 });
514
514
515 $('#collapse_all_files').click(function() {
515 $('#collapse_all_files').click(function() {
516 $('.collapse_file').each(function() {
516 $('.collapse_file').each(function() {
517 var node = $(this);
517 var node = $(this);
518 var icon = $($(node.children().first()).children().first());
518 var icon = $($(node.children().first()).children().first());
519 var id = $(this).attr('fid');
519 var id = $(this).attr('fid');
520 var target = $('#'+id);
520 var target = $('#'+id);
521 var tr = $('#tr_'+id);
521 var tr = $('#tr_'+id);
522 var diff = $('#diff_'+id);
522 var diff = $('#diff_'+id);
523 node.removeClass('collapse_file');
523 node.removeClass('collapse_file');
524 icon.removeClass('collapse_file_icon');
524 icon.removeClass('collapse_file_icon');
525 node.addClass('expand_file');
525 node.addClass('expand_file');
526 icon.addClass('expand_file_icon');
526 icon.addClass('expand_file_icon');
527 diff.hide();
527 diff.hide();
528 tr.hide();
528 tr.hide();
529 target.hide();
529 target.hide();
530 });
530 });
531 });
531 });
532
532
533 // Mouse over behavior for comments and line selection
533 // Mouse over behavior for comments and line selection
534
534
535 // Select the line that comes from the url anchor
535 // Select the line that comes from the url anchor
536 // At the time of development, Chrome didn't seem to support jquery's :target
536 // At the time of development, Chrome didn't seem to support jquery's :target
537 // element, so I had to scroll manually
537 // element, so I had to scroll manually
538
538
539 if (location.hash) {
539 if (location.hash) {
540 var result = splitDelimitedHash(location.hash);
540 var result = splitDelimitedHash(location.hash);
541
541
542 var loc = result.loc;
542 var loc = result.loc;
543
543
544 if (loc.length > 1) {
544 if (loc.length > 1) {
545
545
546 var highlightable_line_tds = [];
546 var highlightable_line_tds = [];
547
547
548 // source code line format
548 // source code line format
549 var page_highlights = loc.substring(loc.indexOf('#') + 1).split('L');
549 var page_highlights = loc.substring(loc.indexOf('#') + 1).split('L');
550
550
551 // multi-line HL, for files
551 // multi-line HL, for files
552 if (page_highlights.length > 1) {
552 if (page_highlights.length > 1) {
553 var highlight_ranges = page_highlights[1].split(",");
553 var highlight_ranges = page_highlights[1].split(",");
554 var h_lines = [];
554 var h_lines = [];
555 for (var pos in highlight_ranges) {
555 for (var pos in highlight_ranges) {
556 var _range = highlight_ranges[pos].split('-');
556 var _range = highlight_ranges[pos].split('-');
557 if (_range.length === 2) {
557 if (_range.length === 2) {
558 var start = parseInt(_range[0]);
558 var start = parseInt(_range[0]);
559 var end = parseInt(_range[1]);
559 var end = parseInt(_range[1]);
560 if (start < end) {
560 if (start < end) {
561 for (var i = start; i <= end; i++) {
561 for (var i = start; i <= end; i++) {
562 h_lines.push(i);
562 h_lines.push(i);
563 }
563 }
564 }
564 }
565 } else {
565 } else {
566 h_lines.push(parseInt(highlight_ranges[pos]));
566 h_lines.push(parseInt(highlight_ranges[pos]));
567 }
567 }
568 }
568 }
569 for (pos in h_lines) {
569 for (pos in h_lines) {
570 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
570 var line_td = $('td.cb-lineno#L' + h_lines[pos]);
571 if (line_td.length) {
571 if (line_td.length) {
572 highlightable_line_tds.push(line_td);
572 highlightable_line_tds.push(line_td);
573 }
573 }
574 }
574 }
575 }
575 }
576
576
577 // now check a direct id reference of line in diff / pull-request page)
577 // now check a direct id reference of line in diff / pull-request page)
578 if ($(loc).length > 0 && $(loc).hasClass('cb-lineno')) {
578 if ($(loc).length > 0 && $(loc).hasClass('cb-lineno')) {
579 highlightable_line_tds.push($(loc));
579 highlightable_line_tds.push($(loc));
580 }
580 }
581
581
582 // mark diff lines as selected
582 // mark diff lines as selected
583 $.each(highlightable_line_tds, function (i, $td) {
583 $.each(highlightable_line_tds, function (i, $td) {
584 $td.addClass('cb-line-selected'); // line number td
584 $td.addClass('cb-line-selected'); // line number td
585 $td.prev().addClass('cb-line-selected'); // line data
585 $td.prev().addClass('cb-line-selected'); // line data
586 $td.next().addClass('cb-line-selected'); // line content
586 $td.next().addClass('cb-line-selected'); // line content
587 });
587 });
588
588
589 if (highlightable_line_tds.length > 0) {
589 if (highlightable_line_tds.length > 0) {
590 var $first_line_td = highlightable_line_tds[0];
590 var $first_line_td = highlightable_line_tds[0];
591 scrollToElement($first_line_td);
591 scrollToElement($first_line_td);
592 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
592 $.Topic('/ui/plugins/code/anchor_focus').prepareOrPublish({
593 td: $first_line_td,
593 td: $first_line_td,
594 remainder: result.remainder
594 remainder: result.remainder
595 });
595 });
596 } else {
596 } else {
597 // case for direct anchor to comments
597 // case for direct anchor to comments
598 var $line = $(loc);
598 var $line = $(loc);
599
599
600 if ($line.hasClass('comment-general')) {
600 if ($line.hasClass('comment-general')) {
601 $line.show();
601 $line.show();
602 } else if ($line.hasClass('comment-inline')) {
602 } else if ($line.hasClass('comment-inline')) {
603 $line.show();
603 $line.show();
604 var $cb = $line.closest('.cb');
604 var $cb = $line.closest('.cb');
605 $cb.removeClass('cb-collapsed')
605 $cb.removeClass('cb-collapsed')
606 }
606 }
607 if ($line.length > 0) {
607 if ($line.length > 0) {
608 $line.addClass('comment-selected-hl');
608 $line.addClass('comment-selected-hl');
609 offsetScroll($line, 70);
609 offsetScroll($line, 70);
610 }
610 }
611 if (!$line.hasClass('comment-outdated') && result.remainder === '/ReplyToComment') {
611 if (!$line.hasClass('comment-outdated') && result.remainder === '/ReplyToComment') {
612 $line.nextAll('.cb-comment-add-button').trigger('click');
612 $line.nextAll('.cb-comment-add-button').trigger('click');
613 }
613 }
614 }
614 }
615
615
616 }
616 }
617 }
617 }
618 collapsableContent();
618 collapsableContent();
619 });
619 });
620
620
621 var feedLifetimeOptions = function(query, initialData){
621 var feedLifetimeOptions = function(query, initialData){
622 var data = {results: []};
622 var data = {results: []};
623 var isQuery = typeof query.term !== 'undefined';
623 var isQuery = typeof query.term !== 'undefined';
624
624
625 var section = _gettext('Lifetime');
625 var section = _gettext('Lifetime');
626 var children = [];
626 var children = [];
627
627
628 //filter results
628 //filter results
629 $.each(initialData.results, function(idx, value) {
629 $.each(initialData.results, function(idx, value) {
630
630
631 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
631 if (!isQuery || query.term.length === 0 || value.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0) {
632 children.push({
632 children.push({
633 'id': this.id,
633 'id': this.id,
634 'text': this.text
634 'text': this.text
635 })
635 })
636 }
636 }
637
637
638 });
638 });
639 data.results.push({
639 data.results.push({
640 'text': section,
640 'text': section,
641 'children': children
641 'children': children
642 });
642 });
643
643
644 if (isQuery) {
644 if (isQuery) {
645
645
646 var now = moment.utc();
646 var now = moment.utc();
647
647
648 var parseQuery = function(entry, now){
648 var parseQuery = function(entry, now){
649 var fmt = 'DD/MM/YYYY H:mm';
649 var fmt = 'DD/MM/YYYY H:mm';
650 var parsed = moment.utc(entry, fmt);
650 var parsed = moment.utc(entry, fmt);
651 var diffInMin = parsed.diff(now, 'minutes');
651 var diffInMin = parsed.diff(now, 'minutes');
652
652
653 if (diffInMin > 0){
653 if (diffInMin > 0){
654 return {
654 return {
655 id: diffInMin,
655 id: diffInMin,
656 text: parsed.format(fmt)
656 text: parsed.format(fmt)
657 }
657 }
658 } else {
658 } else {
659 return {
659 return {
660 id: undefined,
660 id: undefined,
661 text: parsed.format('DD/MM/YYYY') + ' ' + _gettext('date not in future')
661 text: parsed.format('DD/MM/YYYY') + ' ' + _gettext('date not in future')
662 }
662 }
663 }
663 }
664
664
665
665
666 };
666 };
667
667
668 data.results.push({
668 data.results.push({
669 'text': _gettext('Specified expiration date'),
669 'text': _gettext('Specified expiration date'),
670 'children': [{
670 'children': [{
671 'id': parseQuery(query.term, now).id,
671 'id': parseQuery(query.term, now).id,
672 'text': parseQuery(query.term, now).text
672 'text': parseQuery(query.term, now).text
673 }]
673 }]
674 });
674 });
675 }
675 }
676
676
677 query.callback(data);
677 query.callback(data);
678 };
678 };
679
679
680
680 /*
681 * Retrievew via templateContext.session_attrs.key
682 * */
681 var storeUserSessionAttr = function (key, val) {
683 var storeUserSessionAttr = function (key, val) {
682
684
683 var postData = {
685 var postData = {
684 'key': key,
686 'key': key,
685 'val': val,
687 'val': val,
686 'csrf_token': CSRF_TOKEN
688 'csrf_token': CSRF_TOKEN
687 };
689 };
688
690
689 var success = function(o) {
691 var success = function(o) {
690 return true
692 return true
691 };
693 };
692
694
693 ajaxPOST(pyroutes.url('store_user_session_value'), postData, success);
695 ajaxPOST(pyroutes.url('store_user_session_value'), postData, success);
694 return false;
696 return false;
695 };
697 };
@@ -1,1263 +1,1298 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 this.submitButton = $(this.submitForm).find('input[type="submit"]');
127 this.submitButton = $(this.submitForm).find('input[type="submit"]');
128 this.submitButtonText = this.submitButton.val();
128 this.submitButtonText = this.submitButton.val();
129
129
130
130
131 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
131 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
132 {'repo_name': templateContext.repo_name,
132 {'repo_name': templateContext.repo_name,
133 'commit_id': templateContext.commit_data.commit_id});
133 'commit_id': templateContext.commit_data.commit_id});
134
134
135 if (edit){
135 if (edit){
136 this.submitButtonText = _gettext('Updated Comment');
136 this.submitButtonText = _gettext('Updated Comment');
137 $(this.commentType).prop('disabled', true);
137 $(this.commentType).prop('disabled', true);
138 $(this.commentType).addClass('disabled');
138 $(this.commentType).addClass('disabled');
139 var editInfo =
139 var editInfo =
140 '';
140 '';
141 $(editInfo).insertBefore($(this.editButton).parent());
141 $(editInfo).insertBefore($(this.editButton).parent());
142 }
142 }
143
143
144 if (resolvesCommentId){
144 if (resolvesCommentId){
145 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
145 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
146 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
146 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
147 $(this.commentType).prop('disabled', true);
147 $(this.commentType).prop('disabled', true);
148 $(this.commentType).addClass('disabled');
148 $(this.commentType).addClass('disabled');
149
149
150 // disable select
150 // disable select
151 setTimeout(function() {
151 setTimeout(function() {
152 $(self.statusChange).select2('readonly', true);
152 $(self.statusChange).select2('readonly', true);
153 }, 10);
153 }, 10);
154
154
155 var resolvedInfo = (
155 var resolvedInfo = (
156 '<li class="resolve-action">' +
156 '<li class="resolve-action">' +
157 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
157 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
158 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
158 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
159 '</li>'
159 '</li>'
160 ).format(resolvesCommentId, _gettext('resolve comment'));
160 ).format(resolvesCommentId, _gettext('resolve comment'));
161 $(resolvedInfo).insertAfter($(this.commentType).parent());
161 $(resolvedInfo).insertAfter($(this.commentType).parent());
162 }
162 }
163
163
164 // based on commitId, or pullRequestId decide where do we submit
164 // based on commitId, or pullRequestId decide where do we submit
165 // out data
165 // out data
166 if (this.commitId){
166 if (this.commitId){
167 var pyurl = 'repo_commit_comment_create';
167 var pyurl = 'repo_commit_comment_create';
168 if(edit){
168 if(edit){
169 pyurl = 'repo_commit_comment_edit';
169 pyurl = 'repo_commit_comment_edit';
170 }
170 }
171 this.submitUrl = pyroutes.url(pyurl,
171 this.submitUrl = pyroutes.url(pyurl,
172 {'repo_name': templateContext.repo_name,
172 {'repo_name': templateContext.repo_name,
173 'commit_id': this.commitId,
173 'commit_id': this.commitId,
174 'comment_id': comment_id});
174 'comment_id': comment_id});
175 this.selfUrl = pyroutes.url('repo_commit',
175 this.selfUrl = pyroutes.url('repo_commit',
176 {'repo_name': templateContext.repo_name,
176 {'repo_name': templateContext.repo_name,
177 'commit_id': this.commitId});
177 'commit_id': this.commitId});
178
178
179 } else if (this.pullRequestId) {
179 } else if (this.pullRequestId) {
180 var pyurl = 'pullrequest_comment_create';
180 var pyurl = 'pullrequest_comment_create';
181 if(edit){
181 if(edit){
182 pyurl = 'pullrequest_comment_edit';
182 pyurl = 'pullrequest_comment_edit';
183 }
183 }
184 this.submitUrl = pyroutes.url(pyurl,
184 this.submitUrl = pyroutes.url(pyurl,
185 {'repo_name': templateContext.repo_name,
185 {'repo_name': templateContext.repo_name,
186 'pull_request_id': this.pullRequestId,
186 'pull_request_id': this.pullRequestId,
187 'comment_id': comment_id});
187 'comment_id': comment_id});
188 this.selfUrl = pyroutes.url('pullrequest_show',
188 this.selfUrl = pyroutes.url('pullrequest_show',
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
191
192 } else {
192 } else {
193 throw new Error(
193 throw new Error(
194 'CommentForm requires pullRequestId, or commitId to be specified.')
194 'CommentForm requires pullRequestId, or commitId to be specified.')
195 }
195 }
196
196
197 // FUNCTIONS and helpers
197 // FUNCTIONS and helpers
198 var self = this;
198 var self = this;
199
199
200 this.isInline = function(){
200 this.isInline = function(){
201 return this.lineNo && this.lineNo != 'general';
201 return this.lineNo && this.lineNo != 'general';
202 };
202 };
203
203
204 this.getCmInstance = function(){
204 this.getCmInstance = function(){
205 return this.cm
205 return this.cm
206 };
206 };
207
207
208 this.setPlaceholder = function(placeholder) {
208 this.setPlaceholder = function(placeholder) {
209 var cm = this.getCmInstance();
209 var cm = this.getCmInstance();
210 if (cm){
210 if (cm){
211 cm.setOption('placeholder', placeholder);
211 cm.setOption('placeholder', placeholder);
212 }
212 }
213 };
213 };
214
214
215 this.getCommentStatus = function() {
215 this.getCommentStatus = function() {
216 return $(this.submitForm).find(this.statusChange).val();
216 return $(this.submitForm).find(this.statusChange).val();
217 };
217 };
218 this.getCommentType = function() {
218 this.getCommentType = function() {
219 return $(this.submitForm).find(this.commentType).val();
219 return $(this.submitForm).find(this.commentType).val();
220 };
220 };
221
221
222 this.getResolvesId = function() {
222 this.getResolvesId = function() {
223 return $(this.submitForm).find(this.resolvesId).val() || null;
223 return $(this.submitForm).find(this.resolvesId).val() || null;
224 };
224 };
225
225
226 this.getClosePr = function() {
226 this.getClosePr = function() {
227 return $(this.submitForm).find(this.closesPr).val() || null;
227 return $(this.submitForm).find(this.closesPr).val() || null;
228 };
228 };
229
229
230 this.markCommentResolved = function(resolvedCommentId){
230 this.markCommentResolved = function(resolvedCommentId){
231 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
231 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
232 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
232 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
233 };
233 };
234
234
235 this.isAllowedToSubmit = function() {
235 this.isAllowedToSubmit = function() {
236 return !$(this.submitButton).prop('disabled');
236 return !$(this.submitButton).prop('disabled');
237 };
237 };
238
238
239 this.initStatusChangeSelector = function(){
239 this.initStatusChangeSelector = function(){
240 var formatChangeStatus = function(state, escapeMarkup) {
240 var formatChangeStatus = function(state, escapeMarkup) {
241 var originalOption = state.element;
241 var originalOption = state.element;
242 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
242 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
243 return tmpl
243 return tmpl
244 };
244 };
245 var formatResult = function(result, container, query, escapeMarkup) {
245 var formatResult = function(result, container, query, escapeMarkup) {
246 return formatChangeStatus(result, escapeMarkup);
246 return formatChangeStatus(result, escapeMarkup);
247 };
247 };
248
248
249 var formatSelection = function(data, container, escapeMarkup) {
249 var formatSelection = function(data, container, escapeMarkup) {
250 return formatChangeStatus(data, escapeMarkup);
250 return formatChangeStatus(data, escapeMarkup);
251 };
251 };
252
252
253 $(this.submitForm).find(this.statusChange).select2({
253 $(this.submitForm).find(this.statusChange).select2({
254 placeholder: _gettext('Status Review'),
254 placeholder: _gettext('Status Review'),
255 formatResult: formatResult,
255 formatResult: formatResult,
256 formatSelection: formatSelection,
256 formatSelection: formatSelection,
257 containerCssClass: "drop-menu status_box_menu",
257 containerCssClass: "drop-menu status_box_menu",
258 dropdownCssClass: "drop-menu-dropdown",
258 dropdownCssClass: "drop-menu-dropdown",
259 dropdownAutoWidth: true,
259 dropdownAutoWidth: true,
260 minimumResultsForSearch: -1
260 minimumResultsForSearch: -1
261 });
261 });
262 $(this.submitForm).find(this.statusChange).on('change', function() {
262 $(this.submitForm).find(this.statusChange).on('change', function() {
263 var status = self.getCommentStatus();
263 var status = self.getCommentStatus();
264
264
265 if (status && !self.isInline()) {
265 if (status && !self.isInline()) {
266 $(self.submitButton).prop('disabled', false);
266 $(self.submitButton).prop('disabled', false);
267 }
267 }
268
268
269 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
269 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
270 self.setPlaceholder(placeholderText)
270 self.setPlaceholder(placeholderText)
271 })
271 })
272 };
272 };
273
273
274 // reset the comment form into it's original state
274 // reset the comment form into it's original state
275 this.resetCommentFormState = function(content) {
275 this.resetCommentFormState = function(content) {
276 content = content || '';
276 content = content || '';
277
277
278 $(this.editContainer).show();
278 $(this.editContainer).show();
279 $(this.editButton).parent().addClass('active');
279 $(this.editButton).parent().addClass('active');
280
280
281 $(this.previewContainer).hide();
281 $(this.previewContainer).hide();
282 $(this.previewButton).parent().removeClass('active');
282 $(this.previewButton).parent().removeClass('active');
283
283
284 this.setActionButtonsDisabled(true);
284 this.setActionButtonsDisabled(true);
285 self.cm.setValue(content);
285 self.cm.setValue(content);
286 self.cm.setOption("readOnly", false);
286 self.cm.setOption("readOnly", false);
287
287
288 if (this.resolvesId) {
288 if (this.resolvesId) {
289 // destroy the resolve action
289 // destroy the resolve action
290 $(this.resolvesId).parent().remove();
290 $(this.resolvesId).parent().remove();
291 }
291 }
292 // reset closingPR flag
292 // reset closingPR flag
293 $('.close-pr-input').remove();
293 $('.close-pr-input').remove();
294
294
295 $(this.statusChange).select2('readonly', false);
295 $(this.statusChange).select2('readonly', false);
296 };
296 };
297
297
298 this.globalSubmitSuccessCallback = function(){
298 this.globalSubmitSuccessCallback = function(){
299 // default behaviour is to call GLOBAL hook, if it's registered.
299 // default behaviour is to call GLOBAL hook, if it's registered.
300 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
300 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
301 commentFormGlobalSubmitSuccessCallback();
301 commentFormGlobalSubmitSuccessCallback();
302 }
302 }
303 };
303 };
304
304
305 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
305 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
306 return _submitAjaxPOST(url, postData, successHandler, failHandler);
306 return _submitAjaxPOST(url, postData, successHandler, failHandler);
307 };
307 };
308
308
309 // overwrite a submitHandler, we need to do it for inline comments
309 // overwrite a submitHandler, we need to do it for inline comments
310 this.setHandleFormSubmit = function(callback) {
310 this.setHandleFormSubmit = function(callback) {
311 this.handleFormSubmit = callback;
311 this.handleFormSubmit = callback;
312 };
312 };
313
313
314 // overwrite a submitSuccessHandler
314 // overwrite a submitSuccessHandler
315 this.setGlobalSubmitSuccessCallback = function(callback) {
315 this.setGlobalSubmitSuccessCallback = function(callback) {
316 this.globalSubmitSuccessCallback = callback;
316 this.globalSubmitSuccessCallback = callback;
317 };
317 };
318
318
319 // default handler for for submit for main comments
319 // default handler for for submit for main comments
320 this.handleFormSubmit = function() {
320 this.handleFormSubmit = function() {
321 var text = self.cm.getValue();
321 var text = self.cm.getValue();
322 var status = self.getCommentStatus();
322 var status = self.getCommentStatus();
323 var commentType = self.getCommentType();
323 var commentType = self.getCommentType();
324 var resolvesCommentId = self.getResolvesId();
324 var resolvesCommentId = self.getResolvesId();
325 var closePullRequest = self.getClosePr();
325 var closePullRequest = self.getClosePr();
326
326
327 if (text === "" && !status) {
327 if (text === "" && !status) {
328 return;
328 return;
329 }
329 }
330
330
331 var excludeCancelBtn = false;
331 var excludeCancelBtn = false;
332 var submitEvent = true;
332 var submitEvent = true;
333 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
333 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
334 self.cm.setOption("readOnly", true);
334 self.cm.setOption("readOnly", true);
335
335
336 var postData = {
336 var postData = {
337 'text': text,
337 'text': text,
338 'changeset_status': status,
338 'changeset_status': status,
339 'comment_type': commentType,
339 'comment_type': commentType,
340 'csrf_token': CSRF_TOKEN
340 'csrf_token': CSRF_TOKEN
341 };
341 };
342
342
343 if (resolvesCommentId) {
343 if (resolvesCommentId) {
344 postData['resolves_comment_id'] = resolvesCommentId;
344 postData['resolves_comment_id'] = resolvesCommentId;
345 }
345 }
346
346
347 if (closePullRequest) {
347 if (closePullRequest) {
348 postData['close_pull_request'] = true;
348 postData['close_pull_request'] = true;
349 }
349 }
350
350
351 var submitSuccessCallback = function(o) {
351 var submitSuccessCallback = function(o) {
352 // reload page if we change status for single commit.
352 // reload page if we change status for single commit.
353 if (status && self.commitId) {
353 if (status && self.commitId) {
354 location.reload(true);
354 location.reload(true);
355 } else {
355 } else {
356 $('#injected_page_comments').append(o.rendered_text);
356 $('#injected_page_comments').append(o.rendered_text);
357 self.resetCommentFormState();
357 self.resetCommentFormState();
358 timeagoActivate();
358 timeagoActivate();
359 tooltipActivate();
359 tooltipActivate();
360
360
361 // mark visually which comment was resolved
361 // mark visually which comment was resolved
362 if (resolvesCommentId) {
362 if (resolvesCommentId) {
363 self.markCommentResolved(resolvesCommentId);
363 self.markCommentResolved(resolvesCommentId);
364 }
364 }
365 }
365 }
366
366
367 // run global callback on submit
367 // run global callback on submit
368 self.globalSubmitSuccessCallback();
368 self.globalSubmitSuccessCallback();
369
369
370 };
370 };
371 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
371 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
372 var prefix = "Error while submitting comment.\n"
372 var prefix = "Error while submitting comment.\n"
373 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
373 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
374 ajaxErrorSwal(message);
374 ajaxErrorSwal(message);
375 self.resetCommentFormState(text);
375 self.resetCommentFormState(text);
376 };
376 };
377 self.submitAjaxPOST(
377 self.submitAjaxPOST(
378 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
378 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
379 };
379 };
380
380
381 this.previewSuccessCallback = function(o) {
381 this.previewSuccessCallback = function(o) {
382 $(self.previewBoxSelector).html(o);
382 $(self.previewBoxSelector).html(o);
383 $(self.previewBoxSelector).removeClass('unloaded');
383 $(self.previewBoxSelector).removeClass('unloaded');
384
384
385 // swap buttons, making preview active
385 // swap buttons, making preview active
386 $(self.previewButton).parent().addClass('active');
386 $(self.previewButton).parent().addClass('active');
387 $(self.editButton).parent().removeClass('active');
387 $(self.editButton).parent().removeClass('active');
388
388
389 // unlock buttons
389 // unlock buttons
390 self.setActionButtonsDisabled(false);
390 self.setActionButtonsDisabled(false);
391 };
391 };
392
392
393 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
393 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
394 excludeCancelBtn = excludeCancelBtn || false;
394 excludeCancelBtn = excludeCancelBtn || false;
395 submitEvent = submitEvent || false;
395 submitEvent = submitEvent || false;
396
396
397 $(this.editButton).prop('disabled', state);
397 $(this.editButton).prop('disabled', state);
398 $(this.previewButton).prop('disabled', state);
398 $(this.previewButton).prop('disabled', state);
399
399
400 if (!excludeCancelBtn) {
400 if (!excludeCancelBtn) {
401 $(this.cancelButton).prop('disabled', state);
401 $(this.cancelButton).prop('disabled', state);
402 }
402 }
403
403
404 var submitState = state;
404 var submitState = state;
405 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
405 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
406 // if the value of commit review status is set, we allow
406 // if the value of commit review status is set, we allow
407 // submit button, but only on Main form, isInline means inline
407 // submit button, but only on Main form, isInline means inline
408 submitState = false
408 submitState = false
409 }
409 }
410
410
411 $(this.submitButton).prop('disabled', submitState);
411 $(this.submitButton).prop('disabled', submitState);
412 if (submitEvent) {
412 if (submitEvent) {
413 $(this.submitButton).val(_gettext('Submitting...'));
413 $(this.submitButton).val(_gettext('Submitting...'));
414 } else {
414 } else {
415 $(this.submitButton).val(this.submitButtonText);
415 $(this.submitButton).val(this.submitButtonText);
416 }
416 }
417
417
418 };
418 };
419
419
420 // lock preview/edit/submit buttons on load, but exclude cancel button
420 // lock preview/edit/submit buttons on load, but exclude cancel button
421 var excludeCancelBtn = true;
421 var excludeCancelBtn = true;
422 this.setActionButtonsDisabled(true, excludeCancelBtn);
422 this.setActionButtonsDisabled(true, excludeCancelBtn);
423
423
424 // anonymous users don't have access to initialized CM instance
424 // anonymous users don't have access to initialized CM instance
425 if (this.cm !== undefined){
425 if (this.cm !== undefined){
426 this.cm.on('change', function(cMirror) {
426 this.cm.on('change', function(cMirror) {
427 if (cMirror.getValue() === "") {
427 if (cMirror.getValue() === "") {
428 self.setActionButtonsDisabled(true, excludeCancelBtn)
428 self.setActionButtonsDisabled(true, excludeCancelBtn)
429 } else {
429 } else {
430 self.setActionButtonsDisabled(false, excludeCancelBtn)
430 self.setActionButtonsDisabled(false, excludeCancelBtn)
431 }
431 }
432 });
432 });
433 }
433 }
434
434
435 $(this.editButton).on('click', function(e) {
435 $(this.editButton).on('click', function(e) {
436 e.preventDefault();
436 e.preventDefault();
437
437
438 $(self.previewButton).parent().removeClass('active');
438 $(self.previewButton).parent().removeClass('active');
439 $(self.previewContainer).hide();
439 $(self.previewContainer).hide();
440
440
441 $(self.editButton).parent().addClass('active');
441 $(self.editButton).parent().addClass('active');
442 $(self.editContainer).show();
442 $(self.editContainer).show();
443
443
444 });
444 });
445
445
446 $(this.previewButton).on('click', function(e) {
446 $(this.previewButton).on('click', function(e) {
447 e.preventDefault();
447 e.preventDefault();
448 var text = self.cm.getValue();
448 var text = self.cm.getValue();
449
449
450 if (text === "") {
450 if (text === "") {
451 return;
451 return;
452 }
452 }
453
453
454 var postData = {
454 var postData = {
455 'text': text,
455 'text': text,
456 'renderer': templateContext.visual.default_renderer,
456 'renderer': templateContext.visual.default_renderer,
457 'csrf_token': CSRF_TOKEN
457 'csrf_token': CSRF_TOKEN
458 };
458 };
459
459
460 // lock ALL buttons on preview
460 // lock ALL buttons on preview
461 self.setActionButtonsDisabled(true);
461 self.setActionButtonsDisabled(true);
462
462
463 $(self.previewBoxSelector).addClass('unloaded');
463 $(self.previewBoxSelector).addClass('unloaded');
464 $(self.previewBoxSelector).html(_gettext('Loading ...'));
464 $(self.previewBoxSelector).html(_gettext('Loading ...'));
465
465
466 $(self.editContainer).hide();
466 $(self.editContainer).hide();
467 $(self.previewContainer).show();
467 $(self.previewContainer).show();
468
468
469 // by default we reset state of comment preserving the text
469 // by default we reset state of comment preserving the text
470 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
470 var previewFailCallback = function(jqXHR, textStatus, errorThrown) {
471 var prefix = "Error while preview of comment.\n"
471 var prefix = "Error while preview of comment.\n"
472 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
472 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
473 ajaxErrorSwal(message);
473 ajaxErrorSwal(message);
474
474
475 self.resetCommentFormState(text)
475 self.resetCommentFormState(text)
476 };
476 };
477 self.submitAjaxPOST(
477 self.submitAjaxPOST(
478 self.previewUrl, postData, self.previewSuccessCallback,
478 self.previewUrl, postData, self.previewSuccessCallback,
479 previewFailCallback);
479 previewFailCallback);
480
480
481 $(self.previewButton).parent().addClass('active');
481 $(self.previewButton).parent().addClass('active');
482 $(self.editButton).parent().removeClass('active');
482 $(self.editButton).parent().removeClass('active');
483 });
483 });
484
484
485 $(this.submitForm).submit(function(e) {
485 $(this.submitForm).submit(function(e) {
486 e.preventDefault();
486 e.preventDefault();
487 var allowedToSubmit = self.isAllowedToSubmit();
487 var allowedToSubmit = self.isAllowedToSubmit();
488 if (!allowedToSubmit){
488 if (!allowedToSubmit){
489 return false;
489 return false;
490 }
490 }
491 self.handleFormSubmit();
491 self.handleFormSubmit();
492 });
492 });
493
493
494 }
494 }
495
495
496 return CommentForm;
496 return CommentForm;
497 });
497 });
498
498
499 /* selector for comment versions */
499 /* selector for comment versions */
500 var initVersionSelector = function(selector, initialData) {
500 var initVersionSelector = function(selector, initialData) {
501
501
502 var formatResult = function(result, container, query, escapeMarkup) {
502 var formatResult = function(result, container, query, escapeMarkup) {
503
503
504 return renderTemplate('commentVersion', {
504 return renderTemplate('commentVersion', {
505 show_disabled: true,
505 show_disabled: true,
506 version: result.comment_version,
506 version: result.comment_version,
507 user_name: result.comment_author_username,
507 user_name: result.comment_author_username,
508 gravatar_url: result.comment_author_gravatar,
508 gravatar_url: result.comment_author_gravatar,
509 size: 16,
509 size: 16,
510 timeago_component: result.comment_created_on,
510 timeago_component: result.comment_created_on,
511 })
511 })
512 };
512 };
513
513
514 $(selector).select2({
514 $(selector).select2({
515 placeholder: "Edited",
515 placeholder: "Edited",
516 containerCssClass: "drop-menu-comment-history",
516 containerCssClass: "drop-menu-comment-history",
517 dropdownCssClass: "drop-menu-dropdown",
517 dropdownCssClass: "drop-menu-dropdown",
518 dropdownAutoWidth: true,
518 dropdownAutoWidth: true,
519 minimumResultsForSearch: -1,
519 minimumResultsForSearch: -1,
520 data: initialData,
520 data: initialData,
521 formatResult: formatResult,
521 formatResult: formatResult,
522 });
522 });
523
523
524 $(selector).on('select2-selecting', function (e) {
524 $(selector).on('select2-selecting', function (e) {
525 // hide the mast as we later do preventDefault()
525 // hide the mast as we later do preventDefault()
526 $("#select2-drop-mask").click();
526 $("#select2-drop-mask").click();
527 e.preventDefault();
527 e.preventDefault();
528 e.choice.action();
528 e.choice.action();
529 });
529 });
530
530
531 $(selector).on("select2-open", function() {
531 $(selector).on("select2-open", function() {
532 timeagoActivate();
532 timeagoActivate();
533 });
533 });
534 };
534 };
535
535
536 /* comments controller */
536 /* comments controller */
537 var CommentsController = function() {
537 var CommentsController = function() {
538 var mainComment = '#text';
538 var mainComment = '#text';
539 var self = this;
539 var self = this;
540
540
541 this.cancelComment = function (node) {
541 this.cancelComment = function (node) {
542 var $node = $(node);
542 var $node = $(node);
543 var edit = $(this).attr('edit');
543 var edit = $(this).attr('edit');
544 if (edit) {
544 if (edit) {
545 var $general_comments = null;
545 var $general_comments = null;
546 var $inline_comments = $node.closest('div.inline-comments');
546 var $inline_comments = $node.closest('div.inline-comments');
547 if (!$inline_comments.length) {
547 if (!$inline_comments.length) {
548 $general_comments = $('#comments');
548 $general_comments = $('#comments');
549 var $comment = $general_comments.parent().find('div.comment:hidden');
549 var $comment = $general_comments.parent().find('div.comment:hidden');
550 // show hidden general comment form
550 // show hidden general comment form
551 $('#cb-comment-general-form-placeholder').show();
551 $('#cb-comment-general-form-placeholder').show();
552 } else {
552 } else {
553 var $comment = $inline_comments.find('div.comment:hidden');
553 var $comment = $inline_comments.find('div.comment:hidden');
554 }
554 }
555 $comment.show();
555 $comment.show();
556 }
556 }
557 $node.closest('.comment-inline-form').remove();
557 $node.closest('.comment-inline-form').remove();
558 return false;
558 return false;
559 };
559 };
560
560
561 this.showVersion = function (comment_id, comment_history_id) {
561 this.showVersion = function (comment_id, comment_history_id) {
562
562
563 var historyViewUrl = pyroutes.url(
563 var historyViewUrl = pyroutes.url(
564 'repo_commit_comment_history_view',
564 'repo_commit_comment_history_view',
565 {
565 {
566 'repo_name': templateContext.repo_name,
566 'repo_name': templateContext.repo_name,
567 'commit_id': comment_id,
567 'commit_id': comment_id,
568 'comment_history_id': comment_history_id,
568 'comment_history_id': comment_history_id,
569 }
569 }
570 );
570 );
571 successRenderCommit = function (data) {
571 successRenderCommit = function (data) {
572 SwalNoAnimation.fire({
572 SwalNoAnimation.fire({
573 html: data,
573 html: data,
574 title: '',
574 title: '',
575 });
575 });
576 };
576 };
577 failRenderCommit = function () {
577 failRenderCommit = function () {
578 SwalNoAnimation.fire({
578 SwalNoAnimation.fire({
579 html: 'Error while loading comment history',
579 html: 'Error while loading comment history',
580 title: '',
580 title: '',
581 });
581 });
582 };
582 };
583 _submitAjaxPOST(
583 _submitAjaxPOST(
584 historyViewUrl, {'csrf_token': CSRF_TOKEN},
584 historyViewUrl, {'csrf_token': CSRF_TOKEN},
585 successRenderCommit,
585 successRenderCommit,
586 failRenderCommit
586 failRenderCommit
587 );
587 );
588 };
588 };
589
589
590 this.getLineNumber = function(node) {
590 this.getLineNumber = function(node) {
591 var $node = $(node);
591 var $node = $(node);
592 var lineNo = $node.closest('td').attr('data-line-no');
592 var lineNo = $node.closest('td').attr('data-line-no');
593 if (lineNo === undefined && $node.data('commentInline')){
593 if (lineNo === undefined && $node.data('commentInline')){
594 lineNo = $node.data('commentLineNo')
594 lineNo = $node.data('commentLineNo')
595 }
595 }
596
596
597 return lineNo
597 return lineNo
598 };
598 };
599
599
600 this.scrollToComment = function(node, offset, outdated) {
600 this.scrollToComment = function(node, offset, outdated) {
601 if (offset === undefined) {
601 if (offset === undefined) {
602 offset = 0;
602 offset = 0;
603 }
603 }
604 var outdated = outdated || false;
604 var outdated = outdated || false;
605 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
605 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
606
606
607 if (!node) {
607 if (!node) {
608 node = $('.comment-selected');
608 node = $('.comment-selected');
609 if (!node.length) {
609 if (!node.length) {
610 node = $('comment-current')
610 node = $('comment-current')
611 }
611 }
612 }
612 }
613
613
614 $wrapper = $(node).closest('div.comment');
614 $wrapper = $(node).closest('div.comment');
615
615
616 // show hidden comment when referenced.
616 // show hidden comment when referenced.
617 if (!$wrapper.is(':visible')){
617 if (!$wrapper.is(':visible')){
618 $wrapper.show();
618 $wrapper.show();
619 }
619 }
620
620
621 $comment = $(node).closest(klass);
621 $comment = $(node).closest(klass);
622 $comments = $(klass);
622 $comments = $(klass);
623
623
624 $('.comment-selected').removeClass('comment-selected');
624 $('.comment-selected').removeClass('comment-selected');
625
625
626 var nextIdx = $(klass).index($comment) + offset;
626 var nextIdx = $(klass).index($comment) + offset;
627 if (nextIdx >= $comments.length) {
627 if (nextIdx >= $comments.length) {
628 nextIdx = 0;
628 nextIdx = 0;
629 }
629 }
630 var $next = $(klass).eq(nextIdx);
630 var $next = $(klass).eq(nextIdx);
631
631
632 var $cb = $next.closest('.cb');
632 var $cb = $next.closest('.cb');
633 $cb.removeClass('cb-collapsed');
633 $cb.removeClass('cb-collapsed');
634
634
635 var $filediffCollapseState = $cb.closest('.filediff').prev();
635 var $filediffCollapseState = $cb.closest('.filediff').prev();
636 $filediffCollapseState.prop('checked', false);
636 $filediffCollapseState.prop('checked', false);
637 $next.addClass('comment-selected');
637 $next.addClass('comment-selected');
638 scrollToElement($next);
638 scrollToElement($next);
639 return false;
639 return false;
640 };
640 };
641
641
642 this.nextComment = function(node) {
642 this.nextComment = function(node) {
643 return self.scrollToComment(node, 1);
643 return self.scrollToComment(node, 1);
644 };
644 };
645
645
646 this.prevComment = function(node) {
646 this.prevComment = function(node) {
647 return self.scrollToComment(node, -1);
647 return self.scrollToComment(node, -1);
648 };
648 };
649
649
650 this.nextOutdatedComment = function(node) {
650 this.nextOutdatedComment = function(node) {
651 return self.scrollToComment(node, 1, true);
651 return self.scrollToComment(node, 1, true);
652 };
652 };
653
653
654 this.prevOutdatedComment = function(node) {
654 this.prevOutdatedComment = function(node) {
655 return self.scrollToComment(node, -1, true);
655 return self.scrollToComment(node, -1, true);
656 };
656 };
657
657
658 this._deleteComment = function(node) {
658 this._deleteComment = function(node) {
659 var $node = $(node);
659 var $node = $(node);
660 var $td = $node.closest('td');
660 var $td = $node.closest('td');
661 var $comment = $node.closest('.comment');
661 var $comment = $node.closest('.comment');
662 var comment_id = $comment.attr('data-comment-id');
662 var comment_id = $comment.attr('data-comment-id');
663 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
663 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
664 var postData = {
664 var postData = {
665 'csrf_token': CSRF_TOKEN
665 'csrf_token': CSRF_TOKEN
666 };
666 };
667
667
668 $comment.addClass('comment-deleting');
668 $comment.addClass('comment-deleting');
669 $comment.hide('fast');
669 $comment.hide('fast');
670
670
671 var success = function(response) {
671 var success = function(response) {
672 $comment.remove();
672 $comment.remove();
673
674 if (window.updateSticky !== undefined) {
675 // potentially our comments change the active window size, so we
676 // notify sticky elements
677 updateSticky()
678 }
679
680 if (window.refreshAllComments !== undefined) {
681 // if we have this handler, run it, and refresh all comments boxes
682 refreshAllComments()
683 }
673 return false;
684 return false;
674 };
685 };
686
675 var failure = function(jqXHR, textStatus, errorThrown) {
687 var failure = function(jqXHR, textStatus, errorThrown) {
676 var prefix = "Error while deleting this comment.\n"
688 var prefix = "Error while deleting this comment.\n"
677 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
689 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
678 ajaxErrorSwal(message);
690 ajaxErrorSwal(message);
679
691
680 $comment.show('fast');
692 $comment.show('fast');
681 $comment.removeClass('comment-deleting');
693 $comment.removeClass('comment-deleting');
682 return false;
694 return false;
683 };
695 };
684 ajaxPOST(url, postData, success, failure);
696 ajaxPOST(url, postData, success, failure);
697
698
699
685 }
700 }
686
701
687 this.deleteComment = function(node) {
702 this.deleteComment = function(node) {
688 var $comment = $(node).closest('.comment');
703 var $comment = $(node).closest('.comment');
689 var comment_id = $comment.attr('data-comment-id');
704 var comment_id = $comment.attr('data-comment-id');
690
705
691 SwalNoAnimation.fire({
706 SwalNoAnimation.fire({
692 title: 'Delete this comment?',
707 title: 'Delete this comment?',
693 icon: 'warning',
708 icon: 'warning',
694 showCancelButton: true,
709 showCancelButton: true,
695 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
710 confirmButtonText: _gettext('Yes, delete comment #{0}!').format(comment_id),
696
711
697 }).then(function(result) {
712 }).then(function(result) {
698 if (result.value) {
713 if (result.value) {
699 self._deleteComment(node);
714 self._deleteComment(node);
700 }
715 }
701 })
716 })
702 };
717 };
703
718
704 this.toggleWideMode = function (node) {
719 this.toggleWideMode = function (node) {
705 if ($('#content').hasClass('wrapper')) {
720 if ($('#content').hasClass('wrapper')) {
706 $('#content').removeClass("wrapper");
721 $('#content').removeClass("wrapper");
707 $('#content').addClass("wide-mode-wrapper");
722 $('#content').addClass("wide-mode-wrapper");
708 $(node).addClass('btn-success');
723 $(node).addClass('btn-success');
709 return true
724 return true
710 } else {
725 } else {
711 $('#content').removeClass("wide-mode-wrapper");
726 $('#content').removeClass("wide-mode-wrapper");
712 $('#content').addClass("wrapper");
727 $('#content').addClass("wrapper");
713 $(node).removeClass('btn-success');
728 $(node).removeClass('btn-success');
714 return false
729 return false
715 }
730 }
716
731
717 };
732 };
718
733
719 this.toggleComments = function(node, show) {
734 this.toggleComments = function(node, show) {
720 var $filediff = $(node).closest('.filediff');
735 var $filediff = $(node).closest('.filediff');
721 if (show === true) {
736 if (show === true) {
722 $filediff.removeClass('hide-comments');
737 $filediff.removeClass('hide-comments');
723 } else if (show === false) {
738 } else if (show === false) {
724 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
739 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
725 $filediff.addClass('hide-comments');
740 $filediff.addClass('hide-comments');
726 } else {
741 } else {
727 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
742 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
728 $filediff.toggleClass('hide-comments');
743 $filediff.toggleClass('hide-comments');
729 }
744 }
745
746 // since we change the height of the diff container that has anchor points for upper
747 // sticky header, we need to tell it to re-calculate those
748 if (window.updateSticky !== undefined) {
749 // potentially our comments change the active window size, so we
750 // notify sticky elements
751 updateSticky()
752 }
753
730 return false;
754 return false;
731 };
755 };
732
756
733 this.toggleLineComments = function(node) {
757 this.toggleLineComments = function(node) {
734 self.toggleComments(node, true);
758 self.toggleComments(node, true);
735 var $node = $(node);
759 var $node = $(node);
736 // mark outdated comments as visible before the toggle;
760 // mark outdated comments as visible before the toggle;
737 $(node.closest('tr')).find('.comment-outdated').show();
761 $(node.closest('tr')).find('.comment-outdated').show();
738 $node.closest('tr').toggleClass('hide-line-comments');
762 $node.closest('tr').toggleClass('hide-line-comments');
739 };
763 };
740
764
741 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
765 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
742 var pullRequestId = templateContext.pull_request_data.pull_request_id;
766 var pullRequestId = templateContext.pull_request_data.pull_request_id;
743 var commitId = templateContext.commit_data.commit_id;
767 var commitId = templateContext.commit_data.commit_id;
744
768
745 var commentForm = new CommentForm(
769 var commentForm = new CommentForm(
746 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
770 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
747 var cm = commentForm.getCmInstance();
771 var cm = commentForm.getCmInstance();
748
772
749 if (resolvesCommentId){
773 if (resolvesCommentId){
750 var placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
774 placeholderText = _gettext('Leave a resolution comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
751 }
775 }
752
776
753 setTimeout(function() {
777 setTimeout(function() {
754 // callbacks
778 // callbacks
755 if (cm !== undefined) {
779 if (cm !== undefined) {
756 commentForm.setPlaceholder(placeholderText);
780 commentForm.setPlaceholder(placeholderText);
757 if (commentForm.isInline()) {
781 if (commentForm.isInline()) {
758 cm.focus();
782 cm.focus();
759 cm.refresh();
783 cm.refresh();
760 }
784 }
761 }
785 }
762 }, 10);
786 }, 10);
763
787
764 // trigger scrolldown to the resolve comment, since it might be away
788 // trigger scrolldown to the resolve comment, since it might be away
765 // from the clicked
789 // from the clicked
766 if (resolvesCommentId){
790 if (resolvesCommentId){
767 var actionNode = $(commentForm.resolvesActionId).offset();
791 var actionNode = $(commentForm.resolvesActionId).offset();
768
792
769 setTimeout(function() {
793 setTimeout(function() {
770 if (actionNode) {
794 if (actionNode) {
771 $('body, html').animate({scrollTop: actionNode.top}, 10);
795 $('body, html').animate({scrollTop: actionNode.top}, 10);
772 }
796 }
773 }, 100);
797 }, 100);
774 }
798 }
775
799
776 // add dropzone support
800 // add dropzone support
777 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
801 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
778 var renderer = templateContext.visual.default_renderer;
802 var renderer = templateContext.visual.default_renderer;
779 if (renderer == 'rst') {
803 if (renderer == 'rst') {
780 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
804 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
781 if (isRendered){
805 if (isRendered){
782 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
806 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
783 }
807 }
784 } else if (renderer == 'markdown') {
808 } else if (renderer == 'markdown') {
785 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
809 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
786 if (isRendered){
810 if (isRendered){
787 attachmentUrl = '!' + attachmentUrl;
811 attachmentUrl = '!' + attachmentUrl;
788 }
812 }
789 } else {
813 } else {
790 var attachmentUrl = '{}'.format(attachmentStoreUrl);
814 var attachmentUrl = '{}'.format(attachmentStoreUrl);
791 }
815 }
792 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
816 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
793
817
794 return false;
818 return false;
795 };
819 };
796
820
797 //see: https://www.dropzonejs.com/#configuration
821 //see: https://www.dropzonejs.com/#configuration
798 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
822 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
799 {'repo_name': templateContext.repo_name,
823 {'repo_name': templateContext.repo_name,
800 'commit_id': templateContext.commit_data.commit_id})
824 'commit_id': templateContext.commit_data.commit_id})
801
825
802 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
826 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0);
803 if (previewTmpl !== undefined){
827 if (previewTmpl !== undefined){
804 var selectLink = $(formElement).find('.pick-attachment').get(0);
828 var selectLink = $(formElement).find('.pick-attachment').get(0);
805 $(formElement).find('.comment-attachment-uploader').dropzone({
829 $(formElement).find('.comment-attachment-uploader').dropzone({
806 url: storeUrl,
830 url: storeUrl,
807 headers: {"X-CSRF-Token": CSRF_TOKEN},
831 headers: {"X-CSRF-Token": CSRF_TOKEN},
808 paramName: function () {
832 paramName: function () {
809 return "attachment"
833 return "attachment"
810 }, // The name that will be used to transfer the file
834 }, // The name that will be used to transfer the file
811 clickable: selectLink,
835 clickable: selectLink,
812 parallelUploads: 1,
836 parallelUploads: 1,
813 maxFiles: 10,
837 maxFiles: 10,
814 maxFilesize: templateContext.attachment_store.max_file_size_mb,
838 maxFilesize: templateContext.attachment_store.max_file_size_mb,
815 uploadMultiple: false,
839 uploadMultiple: false,
816 autoProcessQueue: true, // if false queue will not be processed automatically.
840 autoProcessQueue: true, // if false queue will not be processed automatically.
817 createImageThumbnails: false,
841 createImageThumbnails: false,
818 previewTemplate: previewTmpl.innerHTML,
842 previewTemplate: previewTmpl.innerHTML,
819
843
820 accept: function (file, done) {
844 accept: function (file, done) {
821 done();
845 done();
822 },
846 },
823 init: function () {
847 init: function () {
824
848
825 this.on("sending", function (file, xhr, formData) {
849 this.on("sending", function (file, xhr, formData) {
826 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
850 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
827 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
851 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
828 });
852 });
829
853
830 this.on("success", function (file, response) {
854 this.on("success", function (file, response) {
831 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
855 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
832 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
856 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
833
857
834 var isRendered = false;
858 var isRendered = false;
835 var ext = file.name.split('.').pop();
859 var ext = file.name.split('.').pop();
836 var imageExts = templateContext.attachment_store.image_ext;
860 var imageExts = templateContext.attachment_store.image_ext;
837 if (imageExts.indexOf(ext) !== -1){
861 if (imageExts.indexOf(ext) !== -1){
838 isRendered = true;
862 isRendered = true;
839 }
863 }
840
864
841 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
865 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
842 });
866 });
843
867
844 this.on("error", function (file, errorMessage, xhr) {
868 this.on("error", function (file, errorMessage, xhr) {
845 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
869 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
846
870
847 var error = null;
871 var error = null;
848
872
849 if (xhr !== undefined){
873 if (xhr !== undefined){
850 var httpStatus = xhr.status + " " + xhr.statusText;
874 var httpStatus = xhr.status + " " + xhr.statusText;
851 if (xhr !== undefined && xhr.status >= 500) {
875 if (xhr !== undefined && xhr.status >= 500) {
852 error = httpStatus;
876 error = httpStatus;
853 }
877 }
854 }
878 }
855
879
856 if (error === null) {
880 if (error === null) {
857 error = errorMessage.error || errorMessage || httpStatus;
881 error = errorMessage.error || errorMessage || httpStatus;
858 }
882 }
859 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
883 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
860
884
861 });
885 });
862 }
886 }
863 });
887 });
864 }
888 }
865 return commentForm;
889 return commentForm;
866 };
890 };
867
891
868 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
892 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
869
893
870 var tmpl = $('#cb-comment-general-form-template').html();
894 var tmpl = $('#cb-comment-general-form-template').html();
871 tmpl = tmpl.format(null, 'general');
895 tmpl = tmpl.format(null, 'general');
872 var $form = $(tmpl);
896 var $form = $(tmpl);
873
897
874 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
898 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
875 var curForm = $formPlaceholder.find('form');
899 var curForm = $formPlaceholder.find('form');
876 if (curForm){
900 if (curForm){
877 curForm.remove();
901 curForm.remove();
878 }
902 }
879 $formPlaceholder.append($form);
903 $formPlaceholder.append($form);
880
904
881 var _form = $($form[0]);
905 var _form = $($form[0]);
882 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
906 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
883 var edit = false;
907 var edit = false;
884 var comment_id = null;
908 var comment_id = null;
885 var commentForm = this.createCommentForm(
909 var commentForm = this.createCommentForm(
886 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
910 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
887 commentForm.initStatusChangeSelector();
911 commentForm.initStatusChangeSelector();
888
912
889 return commentForm;
913 return commentForm;
890 };
914 };
891
915
892 this.editComment = function(node) {
916 this.editComment = function(node) {
893 var $node = $(node);
917 var $node = $(node);
894 var $comment = $(node).closest('.comment');
918 var $comment = $(node).closest('.comment');
895 var comment_id = $comment.attr('data-comment-id');
919 var comment_id = $comment.attr('data-comment-id');
896 var $form = null
920 var $form = null
897
921
898 var $comments = $node.closest('div.inline-comments');
922 var $comments = $node.closest('div.inline-comments');
899 var $general_comments = null;
923 var $general_comments = null;
900 var lineno = null;
924 var lineno = null;
901
925
902 if($comments.length){
926 if($comments.length){
903 // inline comments setup
927 // inline comments setup
904 $form = $comments.find('.comment-inline-form');
928 $form = $comments.find('.comment-inline-form');
905 lineno = self.getLineNumber(node)
929 lineno = self.getLineNumber(node)
906 }
930 }
907 else{
931 else{
908 // general comments setup
932 // general comments setup
909 $comments = $('#comments');
933 $comments = $('#comments');
910 $form = $comments.find('.comment-inline-form');
934 $form = $comments.find('.comment-inline-form');
911 lineno = $comment[0].id
935 lineno = $comment[0].id
912 $('#cb-comment-general-form-placeholder').hide();
936 $('#cb-comment-general-form-placeholder').hide();
913 }
937 }
914
938
915 this.edit = true;
939 this.edit = true;
916
940
917 if (!$form.length) {
941 if (!$form.length) {
918
942
919 var $filediff = $node.closest('.filediff');
943 var $filediff = $node.closest('.filediff');
920 $filediff.removeClass('hide-comments');
944 $filediff.removeClass('hide-comments');
921 var f_path = $filediff.attr('data-f-path');
945 var f_path = $filediff.attr('data-f-path');
922
946
923 // create a new HTML from template
947 // create a new HTML from template
924
948
925 var tmpl = $('#cb-comment-inline-form-template').html();
949 var tmpl = $('#cb-comment-inline-form-template').html();
926 tmpl = tmpl.format(escapeHtml(f_path), lineno);
950 tmpl = tmpl.format(escapeHtml(f_path), lineno);
927 $form = $(tmpl);
951 $form = $(tmpl);
928 $comment.after($form)
952 $comment.after($form)
929
953
930 var _form = $($form[0]).find('form');
954 var _form = $($form[0]).find('form');
931 var autocompleteActions = ['as_note',];
955 var autocompleteActions = ['as_note',];
932 var commentForm = this.createCommentForm(
956 var commentForm = this.createCommentForm(
933 _form, lineno, '', autocompleteActions, resolvesCommentId,
957 _form, lineno, '', autocompleteActions, resolvesCommentId,
934 this.edit, comment_id);
958 this.edit, comment_id);
935 var old_comment_text_binary = $comment.attr('data-comment-text');
959 var old_comment_text_binary = $comment.attr('data-comment-text');
936 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
960 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
937 commentForm.cm.setValue(old_comment_text);
961 commentForm.cm.setValue(old_comment_text);
938 $comment.hide();
962 $comment.hide();
939
963
940 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
964 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
941 form: _form,
965 form: _form,
942 parent: $comments,
966 parent: $comments,
943 lineno: lineno,
967 lineno: lineno,
944 f_path: f_path}
968 f_path: f_path}
945 );
969 );
946
970
947 // set a CUSTOM submit handler for inline comments.
971 // set a CUSTOM submit handler for inline comments.
948 commentForm.setHandleFormSubmit(function(o) {
972 commentForm.setHandleFormSubmit(function(o) {
949 var text = commentForm.cm.getValue();
973 var text = commentForm.cm.getValue();
950 var commentType = commentForm.getCommentType();
974 var commentType = commentForm.getCommentType();
951
975
952 if (text === "") {
976 if (text === "") {
953 return;
977 return;
954 }
978 }
955
979
956 if (old_comment_text == text) {
980 if (old_comment_text == text) {
957 SwalNoAnimation.fire({
981 SwalNoAnimation.fire({
958 title: 'Unable to edit comment',
982 title: 'Unable to edit comment',
959 html: _gettext('Comment body was not changed.'),
983 html: _gettext('Comment body was not changed.'),
960 });
984 });
961 return;
985 return;
962 }
986 }
963 var excludeCancelBtn = false;
987 var excludeCancelBtn = false;
964 var submitEvent = true;
988 var submitEvent = true;
965 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
989 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
966 commentForm.cm.setOption("readOnly", true);
990 commentForm.cm.setOption("readOnly", true);
967
991
968 // Read last version known
992 // Read last version known
969 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
993 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
970 var version = versionSelector.data('lastVersion');
994 var version = versionSelector.data('lastVersion');
971
995
972 if (!version) {
996 if (!version) {
973 version = 0;
997 version = 0;
974 }
998 }
975
999
976 var postData = {
1000 var postData = {
977 'text': text,
1001 'text': text,
978 'f_path': f_path,
1002 'f_path': f_path,
979 'line': lineno,
1003 'line': lineno,
980 'comment_type': commentType,
1004 'comment_type': commentType,
981 'version': version,
1005 'version': version,
982 'csrf_token': CSRF_TOKEN
1006 'csrf_token': CSRF_TOKEN
983 };
1007 };
984
1008
985 var submitSuccessCallback = function(json_data) {
1009 var submitSuccessCallback = function(json_data) {
986 $form.remove();
1010 $form.remove();
987 $comment.show();
1011 $comment.show();
988 var postData = {
1012 var postData = {
989 'text': text,
1013 'text': text,
990 'renderer': $comment.attr('data-comment-renderer'),
1014 'renderer': $comment.attr('data-comment-renderer'),
991 'csrf_token': CSRF_TOKEN
1015 'csrf_token': CSRF_TOKEN
992 };
1016 };
993
1017
994 /* Inject new edited version selector */
1018 /* Inject new edited version selector */
995 var updateCommentVersionDropDown = function () {
1019 var updateCommentVersionDropDown = function () {
996 var versionSelectId = '#comment_versions_'+comment_id;
1020 var versionSelectId = '#comment_versions_'+comment_id;
997 var preLoadVersionData = [
1021 var preLoadVersionData = [
998 {
1022 {
999 id: json_data['comment_version'],
1023 id: json_data['comment_version'],
1000 text: "v{0}".format(json_data['comment_version']),
1024 text: "v{0}".format(json_data['comment_version']),
1001 action: function () {
1025 action: function () {
1002 Rhodecode.comments.showVersion(
1026 Rhodecode.comments.showVersion(
1003 json_data['comment_id'],
1027 json_data['comment_id'],
1004 json_data['comment_history_id']
1028 json_data['comment_history_id']
1005 )
1029 )
1006 },
1030 },
1007 comment_version: json_data['comment_version'],
1031 comment_version: json_data['comment_version'],
1008 comment_author_username: json_data['comment_author_username'],
1032 comment_author_username: json_data['comment_author_username'],
1009 comment_author_gravatar: json_data['comment_author_gravatar'],
1033 comment_author_gravatar: json_data['comment_author_gravatar'],
1010 comment_created_on: json_data['comment_created_on'],
1034 comment_created_on: json_data['comment_created_on'],
1011 },
1035 },
1012 ]
1036 ]
1013
1037
1014
1038
1015 if ($(versionSelectId).data('select2')) {
1039 if ($(versionSelectId).data('select2')) {
1016 var oldData = $(versionSelectId).data('select2').opts.data.results;
1040 var oldData = $(versionSelectId).data('select2').opts.data.results;
1017 $(versionSelectId).select2("destroy");
1041 $(versionSelectId).select2("destroy");
1018 preLoadVersionData = oldData.concat(preLoadVersionData)
1042 preLoadVersionData = oldData.concat(preLoadVersionData)
1019 }
1043 }
1020
1044
1021 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1045 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1022
1046
1023 $comment.attr('data-comment-text', utf8ToB64(text));
1047 $comment.attr('data-comment-text', utf8ToB64(text));
1024
1048
1025 var versionSelector = $('#comment_versions_'+comment_id);
1049 var versionSelector = $('#comment_versions_'+comment_id);
1026
1050
1027 // set lastVersion so we know our last edit version
1051 // set lastVersion so we know our last edit version
1028 versionSelector.data('lastVersion', json_data['comment_version'])
1052 versionSelector.data('lastVersion', json_data['comment_version'])
1029 versionSelector.parent().show();
1053 versionSelector.parent().show();
1030 }
1054 }
1031 updateCommentVersionDropDown();
1055 updateCommentVersionDropDown();
1032
1056
1033 // by default we reset state of comment preserving the text
1057 // by default we reset state of comment preserving the text
1034 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1058 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1035 var prefix = "Error while editing this comment.\n"
1059 var prefix = "Error while editing this comment.\n"
1036 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1060 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1037 ajaxErrorSwal(message);
1061 ajaxErrorSwal(message);
1038 };
1062 };
1039
1063
1040 var successRenderCommit = function(o){
1064 var successRenderCommit = function(o){
1041 $comment.show();
1065 $comment.show();
1042 $comment[0].lastElementChild.innerHTML = o;
1066 $comment[0].lastElementChild.innerHTML = o;
1043 };
1067 };
1044
1068
1045 var previewUrl = pyroutes.url(
1069 var previewUrl = pyroutes.url(
1046 'repo_commit_comment_preview',
1070 'repo_commit_comment_preview',
1047 {'repo_name': templateContext.repo_name,
1071 {'repo_name': templateContext.repo_name,
1048 'commit_id': templateContext.commit_data.commit_id});
1072 'commit_id': templateContext.commit_data.commit_id});
1049
1073
1050 _submitAjaxPOST(
1074 _submitAjaxPOST(
1051 previewUrl, postData, successRenderCommit,
1075 previewUrl, postData, successRenderCommit,
1052 failRenderCommit
1076 failRenderCommit
1053 );
1077 );
1054
1078
1055 try {
1079 try {
1056 var html = json_data.rendered_text;
1080 var html = json_data.rendered_text;
1057 var lineno = json_data.line_no;
1081 var lineno = json_data.line_no;
1058 var target_id = json_data.target_id;
1082 var target_id = json_data.target_id;
1059
1083
1060 $comments.find('.cb-comment-add-button').before(html);
1084 $comments.find('.cb-comment-add-button').before(html);
1061
1085
1062 // run global callback on submit
1086 // run global callback on submit
1063 commentForm.globalSubmitSuccessCallback();
1087 commentForm.globalSubmitSuccessCallback();
1064
1088
1065 } catch (e) {
1089 } catch (e) {
1066 console.error(e);
1090 console.error(e);
1067 }
1091 }
1068
1092
1069 // re trigger the linkification of next/prev navigation
1093 // re trigger the linkification of next/prev navigation
1070 linkifyComments($('.inline-comment-injected'));
1094 linkifyComments($('.inline-comment-injected'));
1071 timeagoActivate();
1095 timeagoActivate();
1072 tooltipActivate();
1096 tooltipActivate();
1073
1097
1074 if (window.updateSticky !== undefined) {
1098 if (window.updateSticky !== undefined) {
1075 // potentially our comments change the active window size, so we
1099 // potentially our comments change the active window size, so we
1076 // notify sticky elements
1100 // notify sticky elements
1077 updateSticky()
1101 updateSticky()
1078 }
1102 }
1079
1103
1104 if (window.refreshAllComments !== undefined) {
1105 // if we have this handler, run it, and refresh all comments boxes
1106 refreshAllComments()
1107 }
1108
1080 commentForm.setActionButtonsDisabled(false);
1109 commentForm.setActionButtonsDisabled(false);
1081
1110
1082 };
1111 };
1112
1083 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1113 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1084 var prefix = "Error while editing comment.\n"
1114 var prefix = "Error while editing comment.\n"
1085 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1115 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1086 if (jqXHR.status == 409){
1116 if (jqXHR.status == 409){
1087 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1117 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1088 ajaxErrorSwal(message, 'Comment version mismatch.');
1118 ajaxErrorSwal(message, 'Comment version mismatch.');
1089 } else {
1119 } else {
1090 ajaxErrorSwal(message);
1120 ajaxErrorSwal(message);
1091 }
1121 }
1092
1122
1093 commentForm.resetCommentFormState(text)
1123 commentForm.resetCommentFormState(text)
1094 };
1124 };
1095 commentForm.submitAjaxPOST(
1125 commentForm.submitAjaxPOST(
1096 commentForm.submitUrl, postData,
1126 commentForm.submitUrl, postData,
1097 submitSuccessCallback,
1127 submitSuccessCallback,
1098 submitFailCallback);
1128 submitFailCallback);
1099 });
1129 });
1100 }
1130 }
1101
1131
1102 $form.addClass('comment-inline-form-open');
1132 $form.addClass('comment-inline-form-open');
1103 };
1133 };
1104
1134
1105 this.createComment = function(node, resolutionComment) {
1135 this.createComment = function(node, resolutionComment) {
1106 var resolvesCommentId = resolutionComment || null;
1136 var resolvesCommentId = resolutionComment || null;
1107 var $node = $(node);
1137 var $node = $(node);
1108 var $td = $node.closest('td');
1138 var $td = $node.closest('td');
1109 var $form = $td.find('.comment-inline-form');
1139 var $form = $td.find('.comment-inline-form');
1110 this.edit = false;
1140 this.edit = false;
1111
1141
1112 if (!$form.length) {
1142 if (!$form.length) {
1113
1143
1114 var $filediff = $node.closest('.filediff');
1144 var $filediff = $node.closest('.filediff');
1115 $filediff.removeClass('hide-comments');
1145 $filediff.removeClass('hide-comments');
1116 var f_path = $filediff.attr('data-f-path');
1146 var f_path = $filediff.attr('data-f-path');
1117 var lineno = self.getLineNumber(node);
1147 var lineno = self.getLineNumber(node);
1118 // create a new HTML from template
1148 // create a new HTML from template
1119 var tmpl = $('#cb-comment-inline-form-template').html();
1149 var tmpl = $('#cb-comment-inline-form-template').html();
1120 tmpl = tmpl.format(escapeHtml(f_path), lineno);
1150 tmpl = tmpl.format(escapeHtml(f_path), lineno);
1121 $form = $(tmpl);
1151 $form = $(tmpl);
1122
1152
1123 var $comments = $td.find('.inline-comments');
1153 var $comments = $td.find('.inline-comments');
1124 if (!$comments.length) {
1154 if (!$comments.length) {
1125 $comments = $(
1155 $comments = $(
1126 $('#cb-comments-inline-container-template').html());
1156 $('#cb-comments-inline-container-template').html());
1127 $td.append($comments);
1157 $td.append($comments);
1128 }
1158 }
1129
1159
1130 $td.find('.cb-comment-add-button').before($form);
1160 $td.find('.cb-comment-add-button').before($form);
1131
1161
1132 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
1162 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
1133 var _form = $($form[0]).find('form');
1163 var _form = $($form[0]).find('form');
1134 var autocompleteActions = ['as_note', 'as_todo'];
1164 var autocompleteActions = ['as_note', 'as_todo'];
1135 var comment_id=null;
1165 var comment_id=null;
1136 var commentForm = this.createCommentForm(
1166 var commentForm = this.createCommentForm(
1137 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId, this.edit, comment_id);
1167 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId, this.edit, comment_id);
1138
1168
1139 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
1169 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
1140 form: _form,
1170 form: _form,
1141 parent: $td[0],
1171 parent: $td[0],
1142 lineno: lineno,
1172 lineno: lineno,
1143 f_path: f_path}
1173 f_path: f_path}
1144 );
1174 );
1145
1175
1146 // set a CUSTOM submit handler for inline comments.
1176 // set a CUSTOM submit handler for inline comments.
1147 commentForm.setHandleFormSubmit(function(o) {
1177 commentForm.setHandleFormSubmit(function(o) {
1148 var text = commentForm.cm.getValue();
1178 var text = commentForm.cm.getValue();
1149 var commentType = commentForm.getCommentType();
1179 var commentType = commentForm.getCommentType();
1150 var resolvesCommentId = commentForm.getResolvesId();
1180 var resolvesCommentId = commentForm.getResolvesId();
1151
1181
1152 if (text === "") {
1182 if (text === "") {
1153 return;
1183 return;
1154 }
1184 }
1155
1185
1156 if (lineno === undefined) {
1186 if (lineno === undefined) {
1157 alert('missing line !');
1187 alert('missing line !');
1158 return;
1188 return;
1159 }
1189 }
1160 if (f_path === undefined) {
1190 if (f_path === undefined) {
1161 alert('missing file path !');
1191 alert('missing file path !');
1162 return;
1192 return;
1163 }
1193 }
1164
1194
1165 var excludeCancelBtn = false;
1195 var excludeCancelBtn = false;
1166 var submitEvent = true;
1196 var submitEvent = true;
1167 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1197 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
1168 commentForm.cm.setOption("readOnly", true);
1198 commentForm.cm.setOption("readOnly", true);
1169 var postData = {
1199 var postData = {
1170 'text': text,
1200 'text': text,
1171 'f_path': f_path,
1201 'f_path': f_path,
1172 'line': lineno,
1202 'line': lineno,
1173 'comment_type': commentType,
1203 'comment_type': commentType,
1174 'csrf_token': CSRF_TOKEN
1204 'csrf_token': CSRF_TOKEN
1175 };
1205 };
1176 if (resolvesCommentId){
1206 if (resolvesCommentId){
1177 postData['resolves_comment_id'] = resolvesCommentId;
1207 postData['resolves_comment_id'] = resolvesCommentId;
1178 }
1208 }
1179
1209
1180 var submitSuccessCallback = function(json_data) {
1210 var submitSuccessCallback = function(json_data) {
1181 $form.remove();
1211 $form.remove();
1182 try {
1212 try {
1183 var html = json_data.rendered_text;
1213 var html = json_data.rendered_text;
1184 var lineno = json_data.line_no;
1214 var lineno = json_data.line_no;
1185 var target_id = json_data.target_id;
1215 var target_id = json_data.target_id;
1186
1216
1187 $comments.find('.cb-comment-add-button').before(html);
1217 $comments.find('.cb-comment-add-button').before(html);
1188
1218
1189 //mark visually which comment was resolved
1219 //mark visually which comment was resolved
1190 if (resolvesCommentId) {
1220 if (resolvesCommentId) {
1191 commentForm.markCommentResolved(resolvesCommentId);
1221 commentForm.markCommentResolved(resolvesCommentId);
1192 }
1222 }
1193
1223
1194 // run global callback on submit
1224 // run global callback on submit
1195 commentForm.globalSubmitSuccessCallback();
1225 commentForm.globalSubmitSuccessCallback();
1196
1226
1197 } catch (e) {
1227 } catch (e) {
1198 console.error(e);
1228 console.error(e);
1199 }
1229 }
1200
1230
1201 // re trigger the linkification of next/prev navigation
1231 // re trigger the linkification of next/prev navigation
1202 linkifyComments($('.inline-comment-injected'));
1232 linkifyComments($('.inline-comment-injected'));
1203 timeagoActivate();
1233 timeagoActivate();
1204 tooltipActivate();
1234 tooltipActivate();
1205
1235
1206 if (window.updateSticky !== undefined) {
1236 if (window.updateSticky !== undefined) {
1207 // potentially our comments change the active window size, so we
1237 // potentially our comments change the active window size, so we
1208 // notify sticky elements
1238 // notify sticky elements
1209 updateSticky()
1239 updateSticky()
1210 }
1240 }
1211
1241
1242 if (window.refreshAllComments !== undefined) {
1243 // if we have this handler, run it, and refresh all comments boxes
1244 refreshAllComments()
1245 }
1246
1212 commentForm.setActionButtonsDisabled(false);
1247 commentForm.setActionButtonsDisabled(false);
1213
1248
1214 };
1249 };
1215 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1250 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1216 var prefix = "Error while submitting comment.\n"
1251 var prefix = "Error while submitting comment.\n"
1217 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1252 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1218 ajaxErrorSwal(message);
1253 ajaxErrorSwal(message);
1219 commentForm.resetCommentFormState(text)
1254 commentForm.resetCommentFormState(text)
1220 };
1255 };
1221 commentForm.submitAjaxPOST(
1256 commentForm.submitAjaxPOST(
1222 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1257 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
1223 });
1258 });
1224 }
1259 }
1225
1260
1226 $form.addClass('comment-inline-form-open');
1261 $form.addClass('comment-inline-form-open');
1227 };
1262 };
1228
1263
1229 this.createResolutionComment = function(commentId){
1264 this.createResolutionComment = function(commentId){
1230 // hide the trigger text
1265 // hide the trigger text
1231 $('#resolve-comment-{0}'.format(commentId)).hide();
1266 $('#resolve-comment-{0}'.format(commentId)).hide();
1232
1267
1233 var comment = $('#comment-'+commentId);
1268 var comment = $('#comment-'+commentId);
1234 var commentData = comment.data();
1269 var commentData = comment.data();
1235 if (commentData.commentInline) {
1270 if (commentData.commentInline) {
1236 this.createComment(comment, commentId)
1271 this.createComment(comment, commentId)
1237 } else {
1272 } else {
1238 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
1273 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
1239 }
1274 }
1240
1275
1241 return false;
1276 return false;
1242 };
1277 };
1243
1278
1244 this.submitResolution = function(commentId){
1279 this.submitResolution = function(commentId){
1245 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1280 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
1246 var commentForm = form.get(0).CommentForm;
1281 var commentForm = form.get(0).CommentForm;
1247
1282
1248 var cm = commentForm.getCmInstance();
1283 var cm = commentForm.getCmInstance();
1249 var renderer = templateContext.visual.default_renderer;
1284 var renderer = templateContext.visual.default_renderer;
1250 if (renderer == 'rst'){
1285 if (renderer == 'rst'){
1251 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1286 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
1252 } else if (renderer == 'markdown') {
1287 } else if (renderer == 'markdown') {
1253 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1288 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
1254 } else {
1289 } else {
1255 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1290 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
1256 }
1291 }
1257
1292
1258 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1293 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
1259 form.submit();
1294 form.submit();
1260 return false;
1295 return false;
1261 };
1296 };
1262
1297
1263 };
1298 };
@@ -1,659 +1,675 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 ReviewersController = function () {
97 ReviewersController = function () {
98 var self = this;
98 var self = this;
99 this.$reviewRulesContainer = $('#review_rules');
99 this.$reviewRulesContainer = $('#review_rules');
100 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
100 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
101 this.$userRule = $('.pr-user-rule-container');
101 this.forbidReviewUsers = undefined;
102 this.forbidReviewUsers = undefined;
102 this.$reviewMembers = $('#review_members');
103 this.$reviewMembers = $('#review_members');
103 this.currentRequest = null;
104 this.currentRequest = null;
104 this.diffData = null;
105 this.diffData = null;
106 this.enabledRules = [];
107
105 //dummy handler, we might register our own later
108 //dummy handler, we might register our own later
106 this.diffDataHandler = function(data){};
109 this.diffDataHandler = function(data){};
107
110
108 this.defaultForbidReviewUsers = function () {
111 this.defaultForbidReviewUsers = function () {
109 return [
112 return [
110 {
113 {
111 'username': 'default',
114 'username': 'default',
112 'user_id': templateContext.default_user.user_id
115 'user_id': templateContext.default_user.user_id
113 }
116 }
114 ];
117 ];
115 };
118 };
116
119
117 this.hideReviewRules = function () {
120 this.hideReviewRules = function () {
118 self.$reviewRulesContainer.hide();
121 self.$reviewRulesContainer.hide();
122 $(self.$userRule.selector).hide();
119 };
123 };
120
124
121 this.showReviewRules = function () {
125 this.showReviewRules = function () {
122 self.$reviewRulesContainer.show();
126 self.$reviewRulesContainer.show();
127 $(self.$userRule.selector).show();
123 };
128 };
124
129
125 this.addRule = function (ruleText) {
130 this.addRule = function (ruleText) {
126 self.showReviewRules();
131 self.showReviewRules();
132 self.enabledRules.push(ruleText);
127 return '<div>- {0}</div>'.format(ruleText)
133 return '<div>- {0}</div>'.format(ruleText)
128 };
134 };
129
135
130 this.loadReviewRules = function (data) {
136 this.loadReviewRules = function (data) {
131 self.diffData = data;
137 self.diffData = data;
132
138
133 // reset forbidden Users
139 // reset forbidden Users
134 this.forbidReviewUsers = self.defaultForbidReviewUsers();
140 this.forbidReviewUsers = self.defaultForbidReviewUsers();
135
141
136 // reset state of review rules
142 // reset state of review rules
137 self.$rulesList.html('');
143 self.$rulesList.html('');
138
144
139 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
145 if (!data || data.rules === undefined || $.isEmptyObject(data.rules)) {
140 // default rule, case for older repo that don't have any rules stored
146 // default rule, case for older repo that don't have any rules stored
141 self.$rulesList.append(
147 self.$rulesList.append(
142 self.addRule(
148 self.addRule(
143 _gettext('All reviewers must vote.'))
149 _gettext('All reviewers must vote.'))
144 );
150 );
145 return self.forbidReviewUsers
151 return self.forbidReviewUsers
146 }
152 }
147
153
148 if (data.rules.voting !== undefined) {
154 if (data.rules.voting !== undefined) {
149 if (data.rules.voting < 0) {
155 if (data.rules.voting < 0) {
150 self.$rulesList.append(
156 self.$rulesList.append(
151 self.addRule(
157 self.addRule(
152 _gettext('All individual reviewers must vote.'))
158 _gettext('All individual reviewers must vote.'))
153 )
159 )
154 } else if (data.rules.voting === 1) {
160 } else if (data.rules.voting === 1) {
155 self.$rulesList.append(
161 self.$rulesList.append(
156 self.addRule(
162 self.addRule(
157 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
163 _gettext('At least {0} reviewer must vote.').format(data.rules.voting))
158 )
164 )
159
165
160 } else {
166 } else {
161 self.$rulesList.append(
167 self.$rulesList.append(
162 self.addRule(
168 self.addRule(
163 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
169 _gettext('At least {0} reviewers must vote.').format(data.rules.voting))
164 )
170 )
165 }
171 }
166 }
172 }
167
173
168 if (data.rules.voting_groups !== undefined) {
174 if (data.rules.voting_groups !== undefined) {
169 $.each(data.rules.voting_groups, function (index, rule_data) {
175 $.each(data.rules.voting_groups, function (index, rule_data) {
170 self.$rulesList.append(
176 self.$rulesList.append(
171 self.addRule(rule_data.text)
177 self.addRule(rule_data.text)
172 )
178 )
173 });
179 });
174 }
180 }
175
181
176 if (data.rules.use_code_authors_for_review) {
182 if (data.rules.use_code_authors_for_review) {
177 self.$rulesList.append(
183 self.$rulesList.append(
178 self.addRule(
184 self.addRule(
179 _gettext('Reviewers picked from source code changes.'))
185 _gettext('Reviewers picked from source code changes.'))
180 )
186 )
181 }
187 }
188
182 if (data.rules.forbid_adding_reviewers) {
189 if (data.rules.forbid_adding_reviewers) {
183 $('#add_reviewer_input').remove();
190 $('#add_reviewer_input').remove();
184 self.$rulesList.append(
191 self.$rulesList.append(
185 self.addRule(
192 self.addRule(
186 _gettext('Adding new reviewers is forbidden.'))
193 _gettext('Adding new reviewers is forbidden.'))
187 )
194 )
188 }
195 }
196
189 if (data.rules.forbid_author_to_review) {
197 if (data.rules.forbid_author_to_review) {
190 self.forbidReviewUsers.push(data.rules_data.pr_author);
198 self.forbidReviewUsers.push(data.rules_data.pr_author);
191 self.$rulesList.append(
199 self.$rulesList.append(
192 self.addRule(
200 self.addRule(
193 _gettext('Author is not allowed to be a reviewer.'))
201 _gettext('Author is not allowed to be a reviewer.'))
194 )
202 )
195 }
203 }
204
196 if (data.rules.forbid_commit_author_to_review) {
205 if (data.rules.forbid_commit_author_to_review) {
197
206
198 if (data.rules_data.forbidden_users) {
207 if (data.rules_data.forbidden_users) {
199 $.each(data.rules_data.forbidden_users, function (index, member_data) {
208 $.each(data.rules_data.forbidden_users, function (index, member_data) {
200 self.forbidReviewUsers.push(member_data)
209 self.forbidReviewUsers.push(member_data)
201 });
210 });
202
211
203 }
212 }
204
213
205 self.$rulesList.append(
214 self.$rulesList.append(
206 self.addRule(
215 self.addRule(
207 _gettext('Commit Authors are not allowed to be a reviewer.'))
216 _gettext('Commit Authors are not allowed to be a reviewer.'))
208 )
217 )
209 }
218 }
210
219
220 // we don't have any rules set, so we inform users about it
221 if (self.enabledRules.length === 0) {
222 self.addRule(
223 _gettext('No review rules set.'))
224 }
225
211 return self.forbidReviewUsers
226 return self.forbidReviewUsers
212 };
227 };
213
228
214 this.loadDefaultReviewers = function (sourceRepo, sourceRef, targetRepo, targetRef) {
229 this.loadDefaultReviewers = function (sourceRepo, sourceRef, targetRepo, targetRef) {
215
230
216 if (self.currentRequest) {
231 if (self.currentRequest) {
217 // make sure we cleanup old running requests before triggering this again
232 // make sure we cleanup old running requests before triggering this again
218 self.currentRequest.abort();
233 self.currentRequest.abort();
219 }
234 }
220
235
221 $('.calculate-reviewers').show();
236 $('.calculate-reviewers').show();
222 // reset reviewer members
237 // reset reviewer members
223 self.$reviewMembers.empty();
238 self.$reviewMembers.empty();
224
239
225 prButtonLock(true, null, 'reviewers');
240 prButtonLock(true, null, 'reviewers');
226 $('#user').hide(); // hide user autocomplete before load
241 $('#user').hide(); // hide user autocomplete before load
227
242
228 // lock PR button, so we cannot send PR before it's calculated
243 // lock PR button, so we cannot send PR before it's calculated
229 prButtonLock(true, _gettext('Loading diff ...'), 'compare');
244 prButtonLock(true, _gettext('Loading diff ...'), 'compare');
230
245
231 if (sourceRef.length !== 3 || targetRef.length !== 3) {
246 if (sourceRef.length !== 3 || targetRef.length !== 3) {
232 // don't load defaults in case we're missing some refs...
247 // don't load defaults in case we're missing some refs...
233 $('.calculate-reviewers').hide();
248 $('.calculate-reviewers').hide();
234 return
249 return
235 }
250 }
236
251
237 var url = pyroutes.url('repo_default_reviewers_data',
252 var url = pyroutes.url('repo_default_reviewers_data',
238 {
253 {
239 'repo_name': templateContext.repo_name,
254 'repo_name': templateContext.repo_name,
240 'source_repo': sourceRepo,
255 'source_repo': sourceRepo,
241 'source_ref': sourceRef[2],
256 'source_ref': sourceRef[2],
242 'target_repo': targetRepo,
257 'target_repo': targetRepo,
243 'target_ref': targetRef[2]
258 'target_ref': targetRef[2]
244 });
259 });
245
260
246 self.currentRequest = $.ajax({
261 self.currentRequest = $.ajax({
247 url: url,
262 url: url,
248 headers: {'X-PARTIAL-XHR': true},
263 headers: {'X-PARTIAL-XHR': true},
249 type: 'GET',
264 type: 'GET',
250 success: function (data) {
265 success: function (data) {
251
266
252 self.currentRequest = null;
267 self.currentRequest = null;
253
268
254 // review rules
269 // review rules
255 self.loadReviewRules(data);
270 self.loadReviewRules(data);
256 self.handleDiffData(data["diff_info"]);
271 self.handleDiffData(data["diff_info"]);
257
272
258 for (var i = 0; i < data.reviewers.length; i++) {
273 for (var i = 0; i < data.reviewers.length; i++) {
259 var reviewer = data.reviewers[i];
274 var reviewer = data.reviewers[i];
260 self.addReviewMember(reviewer, reviewer.reasons, reviewer.mandatory);
275 self.addReviewMember(reviewer, reviewer.reasons, reviewer.mandatory);
261 }
276 }
262 $('.calculate-reviewers').hide();
277 $('.calculate-reviewers').hide();
263 prButtonLock(false, null, 'reviewers');
278 prButtonLock(false, null, 'reviewers');
264 $('#user').show(); // show user autocomplete after load
279 $('#user').show(); // show user autocomplete after load
265
280
266 var commitElements = data["diff_info"]['commits'];
281 var commitElements = data["diff_info"]['commits'];
267 if (commitElements.length === 0) {
282 if (commitElements.length === 0) {
268 prButtonLock(true, _gettext('no commits'), 'all');
283 prButtonLock(true, _gettext('no commits'), 'all');
269
284
270 } else {
285 } else {
271 // un-lock PR button, so we cannot send PR before it's calculated
286 // un-lock PR button, so we cannot send PR before it's calculated
272 prButtonLock(false, null, 'compare');
287 prButtonLock(false, null, 'compare');
273 }
288 }
274
289
275 },
290 },
276 error: function (jqXHR, textStatus, errorThrown) {
291 error: function (jqXHR, textStatus, errorThrown) {
277 var prefix = "Loading diff and reviewers failed\n"
292 var prefix = "Loading diff and reviewers failed\n"
278 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
293 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
279 ajaxErrorSwal(message);
294 ajaxErrorSwal(message);
280 }
295 }
281 });
296 });
282
297
283 };
298 };
284
299
285 // check those, refactor
300 // check those, refactor
286 this.removeReviewMember = function (reviewer_id, mark_delete) {
301 this.removeReviewMember = function (reviewer_id, mark_delete) {
287 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
302 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
288
303
289 if (typeof (mark_delete) === undefined) {
304 if (typeof (mark_delete) === undefined) {
290 mark_delete = false;
305 mark_delete = false;
291 }
306 }
292
307
293 if (mark_delete === true) {
308 if (mark_delete === true) {
294 if (reviewer) {
309 if (reviewer) {
295 // now delete the input
310 // now delete the input
296 $('#reviewer_{0} input'.format(reviewer_id)).remove();
311 $('#reviewer_{0} input'.format(reviewer_id)).remove();
297 // mark as to-delete
312 // mark as to-delete
298 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
313 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
299 obj.addClass('to-delete');
314 obj.addClass('to-delete');
300 obj.css({"text-decoration": "line-through", "opacity": 0.5});
315 obj.css({"text-decoration": "line-through", "opacity": 0.5});
301 }
316 }
302 } else {
317 } else {
303 $('#reviewer_{0}'.format(reviewer_id)).remove();
318 $('#reviewer_{0}'.format(reviewer_id)).remove();
304 }
319 }
305 };
320 };
306
321
307 this.reviewMemberEntry = function () {
322 this.reviewMemberEntry = function () {
308
323
309 };
324 };
310
325
311 this.addReviewMember = function (reviewer_obj, reasons, mandatory) {
326 this.addReviewMember = function (reviewer_obj, reasons, mandatory) {
312 var members = self.$reviewMembers.get(0);
327 var members = self.$reviewMembers.get(0);
313 var id = reviewer_obj.user_id;
328 var id = reviewer_obj.user_id;
314 var username = reviewer_obj.username;
329 var username = reviewer_obj.username;
315
330
316 var reasons = reasons || [];
331 var reasons = reasons || [];
317 var mandatory = mandatory || false;
332 var mandatory = mandatory || false;
318
333
319 // register IDS to check if we don't have this ID already in
334 // register IDS to check if we don't have this ID already in
320 var currentIds = [];
335 var currentIds = [];
321 var _els = self.$reviewMembers.find('li').toArray();
336 var _els = self.$reviewMembers.find('li').toArray();
322 for (el in _els) {
337 for (el in _els) {
323 currentIds.push(_els[el].id)
338 currentIds.push(_els[el].id)
324 }
339 }
325
340
326 var userAllowedReview = function (userId) {
341 var userAllowedReview = function (userId) {
327 var allowed = true;
342 var allowed = true;
328 $.each(self.forbidReviewUsers, function (index, member_data) {
343 $.each(self.forbidReviewUsers, function (index, member_data) {
329 if (parseInt(userId) === member_data['user_id']) {
344 if (parseInt(userId) === member_data['user_id']) {
330 allowed = false;
345 allowed = false;
331 return false // breaks the loop
346 return false // breaks the loop
332 }
347 }
333 });
348 });
334 return allowed
349 return allowed
335 };
350 };
336
351
337 var userAllowed = userAllowedReview(id);
352 var userAllowed = userAllowedReview(id);
338 if (!userAllowed) {
353 if (!userAllowed) {
339 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
354 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
340 } else {
355 } else {
341 // only add if it's not there
356 // only add if it's not there
342 var alreadyReviewer = currentIds.indexOf('reviewer_' + id) != -1;
357 var alreadyReviewer = currentIds.indexOf('reviewer_' + id) != -1;
343
358
344 if (alreadyReviewer) {
359 if (alreadyReviewer) {
345 alert(_gettext('User `{0}` already in reviewers').format(username));
360 alert(_gettext('User `{0}` already in reviewers').format(username));
346 } else {
361 } else {
347 members.innerHTML += renderTemplate('reviewMemberEntry', {
362 members.innerHTML += renderTemplate('reviewMemberEntry', {
348 'member': reviewer_obj,
363 'member': reviewer_obj,
349 'mandatory': mandatory,
364 'mandatory': mandatory,
365 'reasons': reasons,
350 'allowed_to_update': true,
366 'allowed_to_update': true,
351 'review_status': 'not_reviewed',
367 'review_status': 'not_reviewed',
352 'review_status_label': _gettext('Not Reviewed'),
368 'review_status_label': _gettext('Not Reviewed'),
353 'reasons': reasons,
369 'user_group': reviewer_obj.user_group,
354 'create': true
370 'create': true,
355 });
371 });
356 tooltipActivate();
372 tooltipActivate();
357 }
373 }
358 }
374 }
359
375
360 };
376 };
361
377
362 this.updateReviewers = function (repo_name, pull_request_id) {
378 this.updateReviewers = function (repo_name, pull_request_id) {
363 var postData = $('#reviewers input').serialize();
379 var postData = $('#reviewers input').serialize();
364 _updatePullRequest(repo_name, pull_request_id, postData);
380 _updatePullRequest(repo_name, pull_request_id, postData);
365 };
381 };
366
382
367 this.handleDiffData = function (data) {
383 this.handleDiffData = function (data) {
368 self.diffDataHandler(data)
384 self.diffDataHandler(data)
369 }
385 }
370 };
386 };
371
387
372
388
373 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
389 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
374 var url = pyroutes.url(
390 var url = pyroutes.url(
375 'pullrequest_update',
391 'pullrequest_update',
376 {"repo_name": repo_name, "pull_request_id": pull_request_id});
392 {"repo_name": repo_name, "pull_request_id": pull_request_id});
377 if (typeof postData === 'string' ) {
393 if (typeof postData === 'string' ) {
378 postData += '&csrf_token=' + CSRF_TOKEN;
394 postData += '&csrf_token=' + CSRF_TOKEN;
379 } else {
395 } else {
380 postData.csrf_token = CSRF_TOKEN;
396 postData.csrf_token = CSRF_TOKEN;
381 }
397 }
382
398
383 var success = function(o) {
399 var success = function(o) {
384 var redirectUrl = o['redirect_url'];
400 var redirectUrl = o['redirect_url'];
385 if (redirectUrl !== undefined && redirectUrl !== null && redirectUrl !== '') {
401 if (redirectUrl !== undefined && redirectUrl !== null && redirectUrl !== '') {
386 window.location = redirectUrl;
402 window.location = redirectUrl;
387 } else {
403 } else {
388 window.location.reload();
404 window.location.reload();
389 }
405 }
390 };
406 };
391
407
392 ajaxPOST(url, postData, success);
408 ajaxPOST(url, postData, success);
393 };
409 };
394
410
395 /**
411 /**
396 * PULL REQUEST update commits
412 * PULL REQUEST update commits
397 */
413 */
398 var updateCommits = function(repo_name, pull_request_id, force) {
414 var updateCommits = function(repo_name, pull_request_id, force) {
399 var postData = {
415 var postData = {
400 'update_commits': true
416 'update_commits': true
401 };
417 };
402 if (force !== undefined && force === true) {
418 if (force !== undefined && force === true) {
403 postData['force_refresh'] = true
419 postData['force_refresh'] = true
404 }
420 }
405 _updatePullRequest(repo_name, pull_request_id, postData);
421 _updatePullRequest(repo_name, pull_request_id, postData);
406 };
422 };
407
423
408
424
409 /**
425 /**
410 * PULL REQUEST edit info
426 * PULL REQUEST edit info
411 */
427 */
412 var editPullRequest = function(repo_name, pull_request_id, title, description, renderer) {
428 var editPullRequest = function(repo_name, pull_request_id, title, description, renderer) {
413 var url = pyroutes.url(
429 var url = pyroutes.url(
414 'pullrequest_update',
430 'pullrequest_update',
415 {"repo_name": repo_name, "pull_request_id": pull_request_id});
431 {"repo_name": repo_name, "pull_request_id": pull_request_id});
416
432
417 var postData = {
433 var postData = {
418 'title': title,
434 'title': title,
419 'description': description,
435 'description': description,
420 'description_renderer': renderer,
436 'description_renderer': renderer,
421 'edit_pull_request': true,
437 'edit_pull_request': true,
422 'csrf_token': CSRF_TOKEN
438 'csrf_token': CSRF_TOKEN
423 };
439 };
424 var success = function(o) {
440 var success = function(o) {
425 window.location.reload();
441 window.location.reload();
426 };
442 };
427 ajaxPOST(url, postData, success);
443 ajaxPOST(url, postData, success);
428 };
444 };
429
445
430
446
431 /**
447 /**
432 * Reviewer autocomplete
448 * Reviewer autocomplete
433 */
449 */
434 var ReviewerAutoComplete = function(inputId) {
450 var ReviewerAutoComplete = function(inputId) {
435 $(inputId).autocomplete({
451 $(inputId).autocomplete({
436 serviceUrl: pyroutes.url('user_autocomplete_data'),
452 serviceUrl: pyroutes.url('user_autocomplete_data'),
437 minChars:2,
453 minChars:2,
438 maxHeight:400,
454 maxHeight:400,
439 deferRequestBy: 300, //miliseconds
455 deferRequestBy: 300, //miliseconds
440 showNoSuggestionNotice: true,
456 showNoSuggestionNotice: true,
441 tabDisabled: true,
457 tabDisabled: true,
442 autoSelectFirst: true,
458 autoSelectFirst: true,
443 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
459 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
444 formatResult: autocompleteFormatResult,
460 formatResult: autocompleteFormatResult,
445 lookupFilter: autocompleteFilterResult,
461 lookupFilter: autocompleteFilterResult,
446 onSelect: function(element, data) {
462 onSelect: function(element, data) {
447 var mandatory = false;
463 var mandatory = false;
448 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
464 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
449
465
450 // add whole user groups
466 // add whole user groups
451 if (data.value_type == 'user_group') {
467 if (data.value_type == 'user_group') {
452 reasons.push(_gettext('member of "{0}"').format(data.value_display));
468 reasons.push(_gettext('member of "{0}"').format(data.value_display));
453
469
454 $.each(data.members, function(index, member_data) {
470 $.each(data.members, function(index, member_data) {
455 var reviewer = member_data;
471 var reviewer = member_data;
456 reviewer['user_id'] = member_data['id'];
472 reviewer['user_id'] = member_data['id'];
457 reviewer['gravatar_link'] = member_data['icon_link'];
473 reviewer['gravatar_link'] = member_data['icon_link'];
458 reviewer['user_link'] = member_data['profile_link'];
474 reviewer['user_link'] = member_data['profile_link'];
459 reviewer['rules'] = [];
475 reviewer['rules'] = [];
460 reviewersController.addReviewMember(reviewer, reasons, mandatory);
476 reviewersController.addReviewMember(reviewer, reasons, mandatory);
461 })
477 })
462 }
478 }
463 // add single user
479 // add single user
464 else {
480 else {
465 var reviewer = data;
481 var reviewer = data;
466 reviewer['user_id'] = data['id'];
482 reviewer['user_id'] = data['id'];
467 reviewer['gravatar_link'] = data['icon_link'];
483 reviewer['gravatar_link'] = data['icon_link'];
468 reviewer['user_link'] = data['profile_link'];
484 reviewer['user_link'] = data['profile_link'];
469 reviewer['rules'] = [];
485 reviewer['rules'] = [];
470 reviewersController.addReviewMember(reviewer, reasons, mandatory);
486 reviewersController.addReviewMember(reviewer, reasons, mandatory);
471 }
487 }
472
488
473 $(inputId).val('');
489 $(inputId).val('');
474 }
490 }
475 });
491 });
476 };
492 };
477
493
478
494
479 VersionController = function () {
495 VersionController = function () {
480 var self = this;
496 var self = this;
481 this.$verSource = $('input[name=ver_source]');
497 this.$verSource = $('input[name=ver_source]');
482 this.$verTarget = $('input[name=ver_target]');
498 this.$verTarget = $('input[name=ver_target]');
483 this.$showVersionDiff = $('#show-version-diff');
499 this.$showVersionDiff = $('#show-version-diff');
484
500
485 this.adjustRadioSelectors = function (curNode) {
501 this.adjustRadioSelectors = function (curNode) {
486 var getVal = function (item) {
502 var getVal = function (item) {
487 if (item == 'latest') {
503 if (item == 'latest') {
488 return Number.MAX_SAFE_INTEGER
504 return Number.MAX_SAFE_INTEGER
489 }
505 }
490 else {
506 else {
491 return parseInt(item)
507 return parseInt(item)
492 }
508 }
493 };
509 };
494
510
495 var curVal = getVal($(curNode).val());
511 var curVal = getVal($(curNode).val());
496 var cleared = false;
512 var cleared = false;
497
513
498 $.each(self.$verSource, function (index, value) {
514 $.each(self.$verSource, function (index, value) {
499 var elVal = getVal($(value).val());
515 var elVal = getVal($(value).val());
500
516
501 if (elVal > curVal) {
517 if (elVal > curVal) {
502 if ($(value).is(':checked')) {
518 if ($(value).is(':checked')) {
503 cleared = true;
519 cleared = true;
504 }
520 }
505 $(value).attr('disabled', 'disabled');
521 $(value).attr('disabled', 'disabled');
506 $(value).removeAttr('checked');
522 $(value).removeAttr('checked');
507 $(value).css({'opacity': 0.1});
523 $(value).css({'opacity': 0.1});
508 }
524 }
509 else {
525 else {
510 $(value).css({'opacity': 1});
526 $(value).css({'opacity': 1});
511 $(value).removeAttr('disabled');
527 $(value).removeAttr('disabled');
512 }
528 }
513 });
529 });
514
530
515 if (cleared) {
531 if (cleared) {
516 // if we unchecked an active, set the next one to same loc.
532 // if we unchecked an active, set the next one to same loc.
517 $(this.$verSource).filter('[value={0}]'.format(
533 $(this.$verSource).filter('[value={0}]'.format(
518 curVal)).attr('checked', 'checked');
534 curVal)).attr('checked', 'checked');
519 }
535 }
520
536
521 self.setLockAction(false,
537 self.setLockAction(false,
522 $(curNode).data('verPos'),
538 $(curNode).data('verPos'),
523 $(this.$verSource).filter(':checked').data('verPos')
539 $(this.$verSource).filter(':checked').data('verPos')
524 );
540 );
525 };
541 };
526
542
527
543
528 this.attachVersionListener = function () {
544 this.attachVersionListener = function () {
529 self.$verTarget.change(function (e) {
545 self.$verTarget.change(function (e) {
530 self.adjustRadioSelectors(this)
546 self.adjustRadioSelectors(this)
531 });
547 });
532 self.$verSource.change(function (e) {
548 self.$verSource.change(function (e) {
533 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
549 self.adjustRadioSelectors(self.$verTarget.filter(':checked'))
534 });
550 });
535 };
551 };
536
552
537 this.init = function () {
553 this.init = function () {
538
554
539 var curNode = self.$verTarget.filter(':checked');
555 var curNode = self.$verTarget.filter(':checked');
540 self.adjustRadioSelectors(curNode);
556 self.adjustRadioSelectors(curNode);
541 self.setLockAction(true);
557 self.setLockAction(true);
542 self.attachVersionListener();
558 self.attachVersionListener();
543
559
544 };
560 };
545
561
546 this.setLockAction = function (state, selectedVersion, otherVersion) {
562 this.setLockAction = function (state, selectedVersion, otherVersion) {
547 var $showVersionDiff = this.$showVersionDiff;
563 var $showVersionDiff = this.$showVersionDiff;
548
564
549 if (state) {
565 if (state) {
550 $showVersionDiff.attr('disabled', 'disabled');
566 $showVersionDiff.attr('disabled', 'disabled');
551 $showVersionDiff.addClass('disabled');
567 $showVersionDiff.addClass('disabled');
552 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
568 $showVersionDiff.html($showVersionDiff.data('labelTextLocked'));
553 }
569 }
554 else {
570 else {
555 $showVersionDiff.removeAttr('disabled');
571 $showVersionDiff.removeAttr('disabled');
556 $showVersionDiff.removeClass('disabled');
572 $showVersionDiff.removeClass('disabled');
557
573
558 if (selectedVersion == otherVersion) {
574 if (selectedVersion == otherVersion) {
559 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
575 $showVersionDiff.html($showVersionDiff.data('labelTextShow'));
560 } else {
576 } else {
561 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
577 $showVersionDiff.html($showVersionDiff.data('labelTextDiff'));
562 }
578 }
563 }
579 }
564
580
565 };
581 };
566
582
567 this.showVersionDiff = function () {
583 this.showVersionDiff = function () {
568 var target = self.$verTarget.filter(':checked');
584 var target = self.$verTarget.filter(':checked');
569 var source = self.$verSource.filter(':checked');
585 var source = self.$verSource.filter(':checked');
570
586
571 if (target.val() && source.val()) {
587 if (target.val() && source.val()) {
572 var params = {
588 var params = {
573 'pull_request_id': templateContext.pull_request_data.pull_request_id,
589 'pull_request_id': templateContext.pull_request_data.pull_request_id,
574 'repo_name': templateContext.repo_name,
590 'repo_name': templateContext.repo_name,
575 'version': target.val(),
591 'version': target.val(),
576 'from_version': source.val()
592 'from_version': source.val()
577 };
593 };
578 window.location = pyroutes.url('pullrequest_show', params)
594 window.location = pyroutes.url('pullrequest_show', params)
579 }
595 }
580
596
581 return false;
597 return false;
582 };
598 };
583
599
584 this.toggleVersionView = function (elem) {
600 this.toggleVersionView = function (elem) {
585
601
586 if (this.$showVersionDiff.is(':visible')) {
602 if (this.$showVersionDiff.is(':visible')) {
587 $('.version-pr').hide();
603 $('.version-pr').hide();
588 this.$showVersionDiff.hide();
604 this.$showVersionDiff.hide();
589 $(elem).html($(elem).data('toggleOn'))
605 $(elem).html($(elem).data('toggleOn'))
590 } else {
606 } else {
591 $('.version-pr').show();
607 $('.version-pr').show();
592 this.$showVersionDiff.show();
608 this.$showVersionDiff.show();
593 $(elem).html($(elem).data('toggleOff'))
609 $(elem).html($(elem).data('toggleOff'))
594 }
610 }
595
611
596 return false
612 return false
597 };
613 };
598
614
599 this.toggleElement = function (elem, target) {
615 this.toggleElement = function (elem, target) {
600 var $elem = $(elem);
616 var $elem = $(elem);
601 var $target = $(target);
617 var $target = $(target);
602
618
603 if ($target.is(':visible')) {
619 if ($target.is(':visible') || $target.length === 0) {
604 $target.hide();
620 $target.hide();
605 $elem.html($elem.data('toggleOn'))
621 $elem.html($elem.data('toggleOn'))
606 } else {
622 } else {
607 $target.show();
623 $target.show();
608 $elem.html($elem.data('toggleOff'))
624 $elem.html($elem.data('toggleOff'))
609 }
625 }
610
626
611 return false
627 return false
612 }
628 }
613
629
614 };
630 };
615
631
616
632
617 UpdatePrController = function () {
633 UpdatePrController = function () {
618 var self = this;
634 var self = this;
619 this.$updateCommits = $('#update_commits');
635 this.$updateCommits = $('#update_commits');
620 this.$updateCommitsSwitcher = $('#update_commits_switcher');
636 this.$updateCommitsSwitcher = $('#update_commits_switcher');
621
637
622 this.lockUpdateButton = function (label) {
638 this.lockUpdateButton = function (label) {
623 self.$updateCommits.attr('disabled', 'disabled');
639 self.$updateCommits.attr('disabled', 'disabled');
624 self.$updateCommitsSwitcher.attr('disabled', 'disabled');
640 self.$updateCommitsSwitcher.attr('disabled', 'disabled');
625
641
626 self.$updateCommits.addClass('disabled');
642 self.$updateCommits.addClass('disabled');
627 self.$updateCommitsSwitcher.addClass('disabled');
643 self.$updateCommitsSwitcher.addClass('disabled');
628
644
629 self.$updateCommits.removeClass('btn-primary');
645 self.$updateCommits.removeClass('btn-primary');
630 self.$updateCommitsSwitcher.removeClass('btn-primary');
646 self.$updateCommitsSwitcher.removeClass('btn-primary');
631
647
632 self.$updateCommits.text(_gettext(label));
648 self.$updateCommits.text(_gettext(label));
633 };
649 };
634
650
635 this.isUpdateLocked = function () {
651 this.isUpdateLocked = function () {
636 return self.$updateCommits.attr('disabled') !== undefined;
652 return self.$updateCommits.attr('disabled') !== undefined;
637 };
653 };
638
654
639 this.updateCommits = function (curNode) {
655 this.updateCommits = function (curNode) {
640 if (self.isUpdateLocked()) {
656 if (self.isUpdateLocked()) {
641 return
657 return
642 }
658 }
643 self.lockUpdateButton(_gettext('Updating...'));
659 self.lockUpdateButton(_gettext('Updating...'));
644 updateCommits(
660 updateCommits(
645 templateContext.repo_name,
661 templateContext.repo_name,
646 templateContext.pull_request_data.pull_request_id);
662 templateContext.pull_request_data.pull_request_id);
647 };
663 };
648
664
649 this.forceUpdateCommits = function () {
665 this.forceUpdateCommits = function () {
650 if (self.isUpdateLocked()) {
666 if (self.isUpdateLocked()) {
651 return
667 return
652 }
668 }
653 self.lockUpdateButton(_gettext('Force updating...'));
669 self.lockUpdateButton(_gettext('Force updating...'));
654 var force = true;
670 var force = true;
655 updateCommits(
671 updateCommits(
656 templateContext.repo_name,
672 templateContext.repo_name,
657 templateContext.pull_request_data.pull_request_id, force);
673 templateContext.pull_request_data.pull_request_id, force);
658 };
674 };
659 }; No newline at end of file
675 };
@@ -1,491 +1,493 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2 ## usage:
2 ## usage:
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 ## ${comment.comment_block(comment)}
4 ## ${comment.comment_block(comment)}
5 ##
5 ##
6
6
7 <%!
7 <%!
8 from rhodecode.lib import html_filters
8 from rhodecode.lib import html_filters
9 %>
9 %>
10
10
11 <%namespace name="base" file="/base/base.mako"/>
11 <%namespace name="base" file="/base/base.mako"/>
12 <%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
12 <%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
13 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
13
14 <% comment_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
14 <% latest_ver = len(getattr(c, 'versions', [])) %>
15 <% latest_ver = len(getattr(c, 'versions', [])) %>
16
15 % if inline:
17 % if inline:
16 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
18 <% outdated_at_ver = comment.outdated_at_version(c.at_version_num) %>
17 % else:
19 % else:
18 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
20 <% outdated_at_ver = comment.older_than_version(c.at_version_num) %>
19 % endif
21 % endif
20
22
21 <div class="comment
23 <div class="comment
22 ${'comment-inline' if inline else 'comment-general'}
24 ${'comment-inline' if inline else 'comment-general'}
23 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
25 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
24 id="comment-${comment.comment_id}"
26 id="comment-${comment.comment_id}"
25 line="${comment.line_no}"
27 line="${comment.line_no}"
26 data-comment-id="${comment.comment_id}"
28 data-comment-id="${comment.comment_id}"
27 data-comment-type="${comment.comment_type}"
29 data-comment-type="${comment.comment_type}"
28 data-comment-renderer="${comment.renderer}"
30 data-comment-renderer="${comment.renderer}"
29 data-comment-text="${comment.text | html_filters.base64,n}"
31 data-comment-text="${comment.text | html_filters.base64,n}"
30 data-comment-line-no="${comment.line_no}"
32 data-comment-line-no="${comment.line_no}"
31 data-comment-inline=${h.json.dumps(inline)}
33 data-comment-inline=${h.json.dumps(inline)}
32 style="${'display: none;' if outdated_at_ver else ''}">
34 style="${'display: none;' if outdated_at_ver else ''}">
33
35
34 <div class="meta">
36 <div class="meta">
35 <div class="comment-type-label">
37 <div class="comment-type-label">
36 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
38 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
37
39
38 ## TODO COMMENT
40 ## TODO COMMENT
39 % if comment.comment_type == 'todo':
41 % if comment.comment_type == 'todo':
40 % if comment.resolved:
42 % if comment.resolved:
41 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
43 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
42 <i class="icon-flag-filled"></i>
44 <i class="icon-flag-filled"></i>
43 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
45 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
44 </div>
46 </div>
45 % else:
47 % else:
46 <div class="resolved tooltip" style="display: none">
48 <div class="resolved tooltip" style="display: none">
47 <span>${comment.comment_type}</span>
49 <span>${comment.comment_type}</span>
48 </div>
50 </div>
49 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to create resolution comment.')}">
51 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to create resolution comment.')}">
50 <i class="icon-flag-filled"></i>
52 <i class="icon-flag-filled"></i>
51 ${comment.comment_type}
53 ${comment.comment_type}
52 </div>
54 </div>
53 % endif
55 % endif
54 ## NOTE COMMENT
56 ## NOTE COMMENT
55 % else:
57 % else:
56 ## RESOLVED NOTE
58 ## RESOLVED NOTE
57 % if comment.resolved_comment:
59 % if comment.resolved_comment:
58 <div class="tooltip" title="${_('This comment resolves TODO #{}').format(comment.resolved_comment.comment_id)}">
60 <div class="tooltip" title="${_('This comment resolves TODO #{}').format(comment.resolved_comment.comment_id)}">
59 fix
61 fix
60 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
62 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
61 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
63 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
62 </a>
64 </a>
63 </div>
65 </div>
64 ## STATUS CHANGE NOTE
66 ## STATUS CHANGE NOTE
65 % elif not comment.is_inline and comment.status_change:
67 % elif not comment.is_inline and comment.status_change:
66 <%
68 <%
67 if comment.pull_request:
69 if comment.pull_request:
68 status_change_title = 'Status of review for pull request !{}'.format(comment.pull_request.pull_request_id)
70 status_change_title = 'Status of review for pull request !{}'.format(comment.pull_request.pull_request_id)
69 else:
71 else:
70 status_change_title = 'Status of review for commit {}'.format(h.short_id(comment.commit_id))
72 status_change_title = 'Status of review for commit {}'.format(h.short_id(comment.commit_id))
71 %>
73 %>
72
74
73 <i class="icon-circle review-status-${comment.status_change[0].status}"></i>
75 <i class="icon-circle review-status-${comment.status_change[0].status}"></i>
74 <div class="changeset-status-lbl tooltip" title="${status_change_title}">
76 <div class="changeset-status-lbl tooltip" title="${status_change_title}">
75 ${comment.status_change[0].status_lbl}
77 ${comment.status_change[0].status_lbl}
76 </div>
78 </div>
77 % else:
79 % else:
78 <div>
80 <div>
79 <i class="icon-comment"></i>
81 <i class="icon-comment"></i>
80 ${(comment.comment_type or 'note')}
82 ${(comment.comment_type or 'note')}
81 </div>
83 </div>
82 % endif
84 % endif
83 % endif
85 % endif
84
86
85 </div>
87 </div>
86 </div>
88 </div>
87
89
88 % if 0 and comment.status_change:
90 % if 0 and comment.status_change:
89 <div class="pull-left">
91 <div class="pull-left">
90 <span class="tag authortag tooltip" title="${_('Status from pull request.')}">
92 <span class="tag authortag tooltip" title="${_('Status from pull request.')}">
91 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
93 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
92 ${'!{}'.format(comment.pull_request.pull_request_id)}
94 ${'!{}'.format(comment.pull_request.pull_request_id)}
93 </a>
95 </a>
94 </span>
96 </span>
95 </div>
97 </div>
96 % endif
98 % endif
97
99
98 <div class="author ${'author-inline' if inline else 'author-general'}">
100 <div class="author ${'author-inline' if inline else 'author-general'}">
99 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
101 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
100 </div>
102 </div>
101
103
102 <div class="date">
104 <div class="date">
103 ${h.age_component(comment.modified_at, time_is_local=True)}
105 ${h.age_component(comment.modified_at, time_is_local=True)}
104 </div>
106 </div>
105
107
106 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
108 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
107 <span class="tag authortag tooltip" title="${_('Pull request author')}">
109 <span class="tag authortag tooltip" title="${_('Pull request author')}">
108 ${_('author')}
110 ${_('author')}
109 </span>
111 </span>
110 % endif
112 % endif
111
113
112 <%
114 <%
113 comment_version_selector = 'comment_versions_{}'.format(comment.comment_id)
115 comment_version_selector = 'comment_versions_{}'.format(comment.comment_id)
114 %>
116 %>
115
117
116 % if comment.history:
118 % if comment.history:
117 <div class="date">
119 <div class="date">
118
120
119 <input id="${comment_version_selector}" name="${comment_version_selector}"
121 <input id="${comment_version_selector}" name="${comment_version_selector}"
120 type="hidden"
122 type="hidden"
121 data-last-version="${comment.history[-1].version}">
123 data-last-version="${comment.history[-1].version}">
122
124
123 <script type="text/javascript">
125 <script type="text/javascript">
124
126
125 var preLoadVersionData = [
127 var preLoadVersionData = [
126 % for comment_history in comment.history:
128 % for comment_history in comment.history:
127 {
129 {
128 id: ${comment_history.comment_history_id},
130 id: ${comment_history.comment_history_id},
129 text: 'v${comment_history.version}',
131 text: 'v${comment_history.version}',
130 action: function () {
132 action: function () {
131 Rhodecode.comments.showVersion(
133 Rhodecode.comments.showVersion(
132 "${comment.comment_id}",
134 "${comment.comment_id}",
133 "${comment_history.comment_history_id}"
135 "${comment_history.comment_history_id}"
134 )
136 )
135 },
137 },
136 comment_version: "${comment_history.version}",
138 comment_version: "${comment_history.version}",
137 comment_author_username: "${comment_history.author.username}",
139 comment_author_username: "${comment_history.author.username}",
138 comment_author_gravatar: "${h.gravatar_url(comment_history.author.email, 16)}",
140 comment_author_gravatar: "${h.gravatar_url(comment_history.author.email, 16)}",
139 comment_created_on: '${h.age_component(comment_history.created_on, time_is_local=True)}',
141 comment_created_on: '${h.age_component(comment_history.created_on, time_is_local=True)}',
140 },
142 },
141 % endfor
143 % endfor
142 ]
144 ]
143 initVersionSelector("#${comment_version_selector}", {results: preLoadVersionData});
145 initVersionSelector("#${comment_version_selector}", {results: preLoadVersionData});
144
146
145 </script>
147 </script>
146
148
147 </div>
149 </div>
148 % else:
150 % else:
149 <div class="date" style="display: none">
151 <div class="date" style="display: none">
150 <input id="${comment_version_selector}" name="${comment_version_selector}"
152 <input id="${comment_version_selector}" name="${comment_version_selector}"
151 type="hidden"
153 type="hidden"
152 data-last-version="0">
154 data-last-version="0">
153 </div>
155 </div>
154 %endif
156 %endif
155
157
156 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
158 <a class="permalink" href="#comment-${comment.comment_id}">&para; #${comment.comment_id}</a>
157
159
158 <div class="comment-links-block">
160 <div class="comment-links-block">
159
161
160 % if inline:
162 % if inline:
161 <a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
163 <a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
162 % if outdated_at_ver:
164 % if outdated_at_ver:
163 <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
165 <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">
164 outdated ${'v{}'.format(pr_index_ver)} |
166 outdated ${'v{}'.format(comment_ver)} |
165 </code>
167 </code>
166 % elif pr_index_ver:
168 % elif comment_ver:
167 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
169 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}">
168 ${'v{}'.format(pr_index_ver)} |
170 ${'v{}'.format(comment_ver)} |
169 </code>
171 </code>
170 % endif
172 % endif
171 </a>
173 </a>
172 % else:
174 % else:
173 % if pr_index_ver:
175 % if comment_ver:
174
176
175 % if comment.outdated:
177 % if comment.outdated:
176 <a class="pr-version"
178 <a class="pr-version"
177 href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}"
179 href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}"
178 >
180 >
179 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}
181 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}
180 </a> |
182 </a> |
181 % else:
183 % else:
182 <a class="tooltip pr-version"
184 <a class="tooltip pr-version"
183 title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}"
185 title="${_('Comment from pull request version v{0}, latest v{1}').format(comment_ver, latest_ver)}"
184 href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}"
186 href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}"
185 >
187 >
186 <code class="pr-version-num">
188 <code class="pr-version-num">
187 ${'v{}'.format(pr_index_ver)}
189 ${'v{}'.format(comment_ver)}
188 </code>
190 </code>
189 </a> |
191 </a> |
190 % endif
192 % endif
191
193
192 % endif
194 % endif
193 % endif
195 % endif
194
196
195 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
197 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
196 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
198 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
197 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
199 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
198 ## permissions to delete
200 ## permissions to delete
199 %if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
201 %if comment.immutable is False and (c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id):
200 <a onclick="return Rhodecode.comments.editComment(this);"
202 <a onclick="return Rhodecode.comments.editComment(this);"
201 class="edit-comment">${_('Edit')}</a>
203 class="edit-comment">${_('Edit')}</a>
202 | <a onclick="return Rhodecode.comments.deleteComment(this);"
204 | <a onclick="return Rhodecode.comments.deleteComment(this);"
203 class="delete-comment">${_('Delete')}</a>
205 class="delete-comment">${_('Delete')}</a>
204 %else:
206 %else:
205 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
207 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
206 | <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
208 | <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
207 %endif
209 %endif
208 %else:
210 %else:
209 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
211 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
210 | <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
212 | <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
211 %endif
213 %endif
212
214
213 % if outdated_at_ver:
215 % if outdated_at_ver:
214 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous outdated comment')}"> <i class="icon-angle-left"></i> </a>
216 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous outdated comment')}"> <i class="icon-angle-left"></i> </a>
215 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="tooltip next-comment" title="${_('Jump to the next outdated comment')}"> <i class="icon-angle-right"></i></a>
217 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="tooltip next-comment" title="${_('Jump to the next outdated comment')}"> <i class="icon-angle-right"></i></a>
216 % else:
218 % else:
217 | <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
219 | <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
218 | <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
220 | <a onclick="return Rhodecode.comments.nextComment(this);" class="tooltip next-comment" title="${_('Jump to the next comment')}"> <i class="icon-angle-right"></i></a>
219 % endif
221 % endif
220
222
221 </div>
223 </div>
222 </div>
224 </div>
223 <div class="text">
225 <div class="text">
224 ${h.render(comment.text, renderer=comment.renderer, mentions=True, repo_name=getattr(c, 'repo_name', None), active_pattern_entries=active_pattern_entries)}
226 ${h.render(comment.text, renderer=comment.renderer, mentions=True, repo_name=getattr(c, 'repo_name', None), active_pattern_entries=active_pattern_entries)}
225 </div>
227 </div>
226
228
227 </div>
229 </div>
228 </%def>
230 </%def>
229
231
230 ## generate main comments
232 ## generate main comments
231 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
233 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
232 <%
234 <%
233 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
235 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
234 %>
236 %>
235
237
236 <div class="general-comments" id="comments">
238 <div class="general-comments" id="comments">
237 %for comment in comments:
239 %for comment in comments:
238 <div id="comment-tr-${comment.comment_id}">
240 <div id="comment-tr-${comment.comment_id}">
239 ## only render comments that are not from pull request, or from
241 ## only render comments that are not from pull request, or from
240 ## pull request and a status change
242 ## pull request and a status change
241 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
243 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
242 ${comment_block(comment, active_pattern_entries=active_pattern_entries)}
244 ${comment_block(comment, active_pattern_entries=active_pattern_entries)}
243 %endif
245 %endif
244 </div>
246 </div>
245 %endfor
247 %endfor
246 ## to anchor ajax comments
248 ## to anchor ajax comments
247 <div id="injected_page_comments"></div>
249 <div id="injected_page_comments"></div>
248 </div>
250 </div>
249 </%def>
251 </%def>
250
252
251
253
252 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
254 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
253
255
254 <div class="comments">
256 <div class="comments">
255 <%
257 <%
256 if is_pull_request:
258 if is_pull_request:
257 placeholder = _('Leave a comment on this Pull Request.')
259 placeholder = _('Leave a comment on this Pull Request.')
258 elif is_compare:
260 elif is_compare:
259 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
261 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
260 else:
262 else:
261 placeholder = _('Leave a comment on this Commit.')
263 placeholder = _('Leave a comment on this Commit.')
262 %>
264 %>
263
265
264 % if c.rhodecode_user.username != h.DEFAULT_USER:
266 % if c.rhodecode_user.username != h.DEFAULT_USER:
265 <div class="js-template" id="cb-comment-general-form-template">
267 <div class="js-template" id="cb-comment-general-form-template">
266 ## template generated for injection
268 ## template generated for injection
267 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
269 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
268 </div>
270 </div>
269
271
270 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
272 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
271 ## inject form here
273 ## inject form here
272 </div>
274 </div>
273 <script type="text/javascript">
275 <script type="text/javascript">
274 var lineNo = 'general';
276 var lineNo = 'general';
275 var resolvesCommentId = null;
277 var resolvesCommentId = null;
276 var generalCommentForm = Rhodecode.comments.createGeneralComment(
278 var generalCommentForm = Rhodecode.comments.createGeneralComment(
277 lineNo, "${placeholder}", resolvesCommentId);
279 lineNo, "${placeholder}", resolvesCommentId);
278
280
279 // set custom success callback on rangeCommit
281 // set custom success callback on rangeCommit
280 % if is_compare:
282 % if is_compare:
281 generalCommentForm.setHandleFormSubmit(function(o) {
283 generalCommentForm.setHandleFormSubmit(function(o) {
282 var self = generalCommentForm;
284 var self = generalCommentForm;
283
285
284 var text = self.cm.getValue();
286 var text = self.cm.getValue();
285 var status = self.getCommentStatus();
287 var status = self.getCommentStatus();
286 var commentType = self.getCommentType();
288 var commentType = self.getCommentType();
287
289
288 if (text === "" && !status) {
290 if (text === "" && !status) {
289 return;
291 return;
290 }
292 }
291
293
292 // we can pick which commits we want to make the comment by
294 // we can pick which commits we want to make the comment by
293 // selecting them via click on preview pane, this will alter the hidden inputs
295 // selecting them via click on preview pane, this will alter the hidden inputs
294 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
296 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
295
297
296 var commitIds = [];
298 var commitIds = [];
297 $('#changeset_compare_view_content .compare_select').each(function(el) {
299 $('#changeset_compare_view_content .compare_select').each(function(el) {
298 var commitId = this.id.replace('row-', '');
300 var commitId = this.id.replace('row-', '');
299 if ($(this).hasClass('hl') || !cherryPicked) {
301 if ($(this).hasClass('hl') || !cherryPicked) {
300 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
302 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
301 commitIds.push(commitId);
303 commitIds.push(commitId);
302 } else {
304 } else {
303 $("input[data-commit-id='{0}']".format(commitId)).val('')
305 $("input[data-commit-id='{0}']".format(commitId)).val('')
304 }
306 }
305 });
307 });
306
308
307 self.setActionButtonsDisabled(true);
309 self.setActionButtonsDisabled(true);
308 self.cm.setOption("readOnly", true);
310 self.cm.setOption("readOnly", true);
309 var postData = {
311 var postData = {
310 'text': text,
312 'text': text,
311 'changeset_status': status,
313 'changeset_status': status,
312 'comment_type': commentType,
314 'comment_type': commentType,
313 'commit_ids': commitIds,
315 'commit_ids': commitIds,
314 'csrf_token': CSRF_TOKEN
316 'csrf_token': CSRF_TOKEN
315 };
317 };
316
318
317 var submitSuccessCallback = function(o) {
319 var submitSuccessCallback = function(o) {
318 location.reload(true);
320 location.reload(true);
319 };
321 };
320 var submitFailCallback = function(){
322 var submitFailCallback = function(){
321 self.resetCommentFormState(text)
323 self.resetCommentFormState(text)
322 };
324 };
323 self.submitAjaxPOST(
325 self.submitAjaxPOST(
324 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
326 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
325 });
327 });
326 % endif
328 % endif
327
329
328 </script>
330 </script>
329 % else:
331 % else:
330 ## form state when not logged in
332 ## form state when not logged in
331 <div class="comment-form ac">
333 <div class="comment-form ac">
332
334
333 <div class="comment-area">
335 <div class="comment-area">
334 <div class="comment-area-header">
336 <div class="comment-area-header">
335 <ul class="nav-links clearfix">
337 <ul class="nav-links clearfix">
336 <li class="active">
338 <li class="active">
337 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
339 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
338 </li>
340 </li>
339 <li class="">
341 <li class="">
340 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
342 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
341 </li>
343 </li>
342 </ul>
344 </ul>
343 </div>
345 </div>
344
346
345 <div class="comment-area-write" style="display: block;">
347 <div class="comment-area-write" style="display: block;">
346 <div id="edit-container">
348 <div id="edit-container">
347 <div style="padding: 40px 0">
349 <div style="padding: 40px 0">
348 ${_('You need to be logged in to leave comments.')}
350 ${_('You need to be logged in to leave comments.')}
349 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
351 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
350 </div>
352 </div>
351 </div>
353 </div>
352 <div id="preview-container" class="clearfix" style="display: none;">
354 <div id="preview-container" class="clearfix" style="display: none;">
353 <div id="preview-box" class="preview-box"></div>
355 <div id="preview-box" class="preview-box"></div>
354 </div>
356 </div>
355 </div>
357 </div>
356
358
357 <div class="comment-area-footer">
359 <div class="comment-area-footer">
358 <div class="toolbar">
360 <div class="toolbar">
359 <div class="toolbar-text">
361 <div class="toolbar-text">
360 </div>
362 </div>
361 </div>
363 </div>
362 </div>
364 </div>
363 </div>
365 </div>
364
366
365 <div class="comment-footer">
367 <div class="comment-footer">
366 </div>
368 </div>
367
369
368 </div>
370 </div>
369 % endif
371 % endif
370
372
371 <script type="text/javascript">
373 <script type="text/javascript">
372 bindToggleButtons();
374 bindToggleButtons();
373 </script>
375 </script>
374 </div>
376 </div>
375 </%def>
377 </%def>
376
378
377
379
378 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
380 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
379
381
380 ## comment injected based on assumption that user is logged in
382 ## comment injected based on assumption that user is logged in
381 <form ${('id="{}"'.format(form_id) if form_id else '') |n} action="#" method="GET">
383 <form ${('id="{}"'.format(form_id) if form_id else '') |n} action="#" method="GET">
382
384
383 <div class="comment-area">
385 <div class="comment-area">
384 <div class="comment-area-header">
386 <div class="comment-area-header">
385 <div class="pull-left">
387 <div class="pull-left">
386 <ul class="nav-links clearfix">
388 <ul class="nav-links clearfix">
387 <li class="active">
389 <li class="active">
388 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
390 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
389 </li>
391 </li>
390 <li class="">
392 <li class="">
391 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
393 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
392 </li>
394 </li>
393 </ul>
395 </ul>
394 </div>
396 </div>
395 <div class="pull-right">
397 <div class="pull-right">
396 <span class="comment-area-text">${_('Mark as')}:</span>
398 <span class="comment-area-text">${_('Mark as')}:</span>
397 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
399 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
398 % for val in c.visual.comment_types:
400 % for val in c.visual.comment_types:
399 <option value="${val}">${val.upper()}</option>
401 <option value="${val}">${val.upper()}</option>
400 % endfor
402 % endfor
401 </select>
403 </select>
402 </div>
404 </div>
403 </div>
405 </div>
404
406
405 <div class="comment-area-write" style="display: block;">
407 <div class="comment-area-write" style="display: block;">
406 <div id="edit-container_${lineno_id}">
408 <div id="edit-container_${lineno_id}">
407 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
409 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
408 </div>
410 </div>
409 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
411 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
410 <div id="preview-box_${lineno_id}" class="preview-box"></div>
412 <div id="preview-box_${lineno_id}" class="preview-box"></div>
411 </div>
413 </div>
412 </div>
414 </div>
413
415
414 <div class="comment-area-footer comment-attachment-uploader">
416 <div class="comment-area-footer comment-attachment-uploader">
415 <div class="toolbar">
417 <div class="toolbar">
416
418
417 <div class="comment-attachment-text">
419 <div class="comment-attachment-text">
418 <div class="dropzone-text">
420 <div class="dropzone-text">
419 ${_("Drag'n Drop files here or")} <span class="link pick-attachment">${_('Choose your files')}</span>.<br>
421 ${_("Drag'n Drop files here or")} <span class="link pick-attachment">${_('Choose your files')}</span>.<br>
420 </div>
422 </div>
421 <div class="dropzone-upload" style="display:none">
423 <div class="dropzone-upload" style="display:none">
422 <i class="icon-spin animate-spin"></i> ${_('uploading...')}
424 <i class="icon-spin animate-spin"></i> ${_('uploading...')}
423 </div>
425 </div>
424 </div>
426 </div>
425
427
426 ## comments dropzone template, empty on purpose
428 ## comments dropzone template, empty on purpose
427 <div style="display: none" class="comment-attachment-uploader-template">
429 <div style="display: none" class="comment-attachment-uploader-template">
428 <div class="dz-file-preview" style="margin: 0">
430 <div class="dz-file-preview" style="margin: 0">
429 <div class="dz-error-message"></div>
431 <div class="dz-error-message"></div>
430 </div>
432 </div>
431 </div>
433 </div>
432
434
433 </div>
435 </div>
434 </div>
436 </div>
435 </div>
437 </div>
436
438
437 <div class="comment-footer">
439 <div class="comment-footer">
438
440
439 ## inject extra inputs into the form
441 ## inject extra inputs into the form
440 % if form_extras and isinstance(form_extras, (list, tuple)):
442 % if form_extras and isinstance(form_extras, (list, tuple)):
441 <div id="comment_form_extras">
443 <div id="comment_form_extras">
442 % for form_ex_el in form_extras:
444 % for form_ex_el in form_extras:
443 ${form_ex_el|n}
445 ${form_ex_el|n}
444 % endfor
446 % endfor
445 </div>
447 </div>
446 % endif
448 % endif
447
449
448 <div class="action-buttons">
450 <div class="action-buttons">
449 % if form_type != 'inline':
451 % if form_type != 'inline':
450 <div class="action-buttons-extra"></div>
452 <div class="action-buttons-extra"></div>
451 % endif
453 % endif
452
454
453 <input class="btn btn-success comment-button-input" id="save_${lineno_id}" name="save" type="submit" value="${_('Comment')}">
455 <input class="btn btn-success comment-button-input" id="save_${lineno_id}" name="save" type="submit" value="${_('Comment')}">
454
456
455 ## inline for has a file, and line-number together with cancel hide button.
457 ## inline for has a file, and line-number together with cancel hide button.
456 % if form_type == 'inline':
458 % if form_type == 'inline':
457 <input type="hidden" name="f_path" value="{0}">
459 <input type="hidden" name="f_path" value="{0}">
458 <input type="hidden" name="line" value="${lineno_id}">
460 <input type="hidden" name="line" value="${lineno_id}">
459 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
461 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
460 ${_('Cancel')}
462 ${_('Cancel')}
461 </button>
463 </button>
462 % endif
464 % endif
463 </div>
465 </div>
464
466
465 % if review_statuses:
467 % if review_statuses:
466 <div class="status_box">
468 <div class="status_box">
467 <select id="change_status_${lineno_id}" name="changeset_status">
469 <select id="change_status_${lineno_id}" name="changeset_status">
468 <option></option> ## Placeholder
470 <option></option> ## Placeholder
469 % for status, lbl in review_statuses:
471 % for status, lbl in review_statuses:
470 <option value="${status}" data-status="${status}">${lbl}</option>
472 <option value="${status}" data-status="${status}">${lbl}</option>
471 %if is_pull_request and change_status and status in ('approved', 'rejected'):
473 %if is_pull_request and change_status and status in ('approved', 'rejected'):
472 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
474 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
473 %endif
475 %endif
474 % endfor
476 % endfor
475 </select>
477 </select>
476 </div>
478 </div>
477 % endif
479 % endif
478
480
479 <div class="toolbar-text">
481 <div class="toolbar-text">
480 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
482 <% renderer_url = '<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper()) %>
481 ${_('Comments parsed using {} syntax.').format(renderer_url)|n} <br/>
483 ${_('Comments parsed using {} syntax.').format(renderer_url)|n} <br/>
482 <span class="tooltip" title="${_('Use @username inside this text to send notification to this RhodeCode user')}">@mention</span>
484 <span class="tooltip" title="${_('Use @username inside this text to send notification to this RhodeCode user')}">@mention</span>
483 ${_('and')}
485 ${_('and')}
484 <span class="tooltip" title="${_('Start typing with / for certain actions to be triggered via text box.')}">`/` autocomplete</span>
486 <span class="tooltip" title="${_('Start typing with / for certain actions to be triggered via text box.')}">`/` autocomplete</span>
485 ${_('actions supported.')}
487 ${_('actions supported.')}
486 </div>
488 </div>
487 </div>
489 </div>
488
490
489 </form>
491 </form>
490
492
491 </%def> No newline at end of file
493 </%def>
@@ -1,79 +1,80 b''
1 ## Changesets table !
1 ## Changesets table !
2 <%namespace name="base" file="/base/base.mako"/>
2 <%namespace name="base" file="/base/base.mako"/>
3
3
4 %if c.ancestor:
4 %if c.ancestor:
5 <div class="ancestor">${_('Compare was calculated based on this common ancestor commit')}:
5 <div class="ancestor">${_('Compare was calculated based on this common ancestor commit')}:
6 <a href="${h.route_path('repo_commit', repo_name=c.repo_name, commit_id=c.ancestor)}">${h.short_id(c.ancestor)}</a>
6 <a href="${h.route_path('repo_commit', repo_name=c.repo_name, commit_id=c.ancestor)}">${h.short_id(c.ancestor)}</a>
7 <input id="common_ancestor" type="hidden" name="common_ancestor" value="${c.ancestor}">
7 <input id="common_ancestor" type="hidden" name="common_ancestor" value="${c.ancestor}">
8 </div>
8 </div>
9 %endif
9 %endif
10
10
11 <div class="container">
11 <div class="container">
12 <input type="hidden" name="__start__" value="revisions:sequence">
12 <input type="hidden" name="__start__" value="revisions:sequence">
13 <table class="rctable compare_view_commits">
13 <table class="rctable compare_view_commits">
14 <tr>
14 <tr>
15 <th>${_('Time')}</th>
15 <th>${_('Time')}</th>
16 <th>${_('Author')}</th>
16 <th>${_('Author')}</th>
17 <th>${_('Commit')}</th>
17 <th>${_('Commit')}</th>
18 <th></th>
18 <th></th>
19 <th>${_('Description')}</th>
19 <th>${_('Description')}</th>
20 </tr>
20 </tr>
21 ## to speed up lookups cache some functions before the loop
21 ## to speed up lookups cache some functions before the loop
22 <%
22 <%
23 active_patterns = h.get_active_pattern_entries(c.repo_name)
23 active_patterns = h.get_active_pattern_entries(c.repo_name)
24 urlify_commit_message = h.partial(h.urlify_commit_message, active_pattern_entries=active_patterns)
24 urlify_commit_message = h.partial(h.urlify_commit_message, active_pattern_entries=active_patterns, issues_container=getattr(c, 'referenced_commit_issues', None))
25 %>
25 %>
26
26 %for commit in c.commit_ranges:
27 %for commit in c.commit_ranges:
27 <tr id="row-${commit.raw_id}"
28 <tr id="row-${commit.raw_id}"
28 commit_id="${commit.raw_id}"
29 commit_id="${commit.raw_id}"
29 class="compare_select"
30 class="compare_select"
30 style="${'display: none' if c.collapse_all_commits else ''}"
31 style="${'display: none' if c.collapse_all_commits else ''}"
31 >
32 >
32 <td class="td-time">
33 <td class="td-time">
33 ${h.age_component(commit.date)}
34 ${h.age_component(commit.date)}
34 </td>
35 </td>
35 <td class="td-user">
36 <td class="td-user">
36 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
37 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
37 </td>
38 </td>
38 <td class="td-hash">
39 <td class="td-hash">
39 <code>
40 <code>
40 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
41 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
41 r${commit.idx}:${h.short_id(commit.raw_id)}
42 r${commit.idx}:${h.short_id(commit.raw_id)}
42 </a>
43 </a>
43 ${h.hidden('revisions',commit.raw_id)}
44 ${h.hidden('revisions',commit.raw_id)}
44 </code>
45 </code>
45 </td>
46 </td>
46 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_('Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
47 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_('Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
47 <i class="icon-expand-linked"></i>
48 <i class="icon-expand-linked"></i>
48 </td>
49 </td>
49 <td class="mid td-description">
50 <td class="mid td-description">
50 <div class="log-container truncate-wrap">
51 <div class="log-container truncate-wrap">
51 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${urlify_commit_message(commit.message, c.repo_name)}</div>
52 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${urlify_commit_message(commit.message, c.repo_name)}</div>
52 </div>
53 </div>
53 </td>
54 </td>
54 </tr>
55 </tr>
55 %endfor
56 %endfor
56 <tr class="compare_select_hidden" style="${('' if c.collapse_all_commits else 'display: none')}">
57 <tr class="compare_select_hidden" style="${('' if c.collapse_all_commits else 'display: none')}">
57 <td colspan="5">
58 <td colspan="5">
58 ${_ungettext('{} commit hidden, click expand to show them.', '{} commits hidden, click expand to show them.', len(c.commit_ranges)).format(len(c.commit_ranges))}
59 ${_ungettext('{} commit hidden, click expand to show them.', '{} commits hidden, click expand to show them.', len(c.commit_ranges)).format(len(c.commit_ranges))}
59 </td>
60 </td>
60 </tr>
61 </tr>
61 % if not c.commit_ranges:
62 % if not c.commit_ranges:
62 <tr class="compare_select">
63 <tr class="compare_select">
63 <td colspan="5">
64 <td colspan="5">
64 ${_('No commits in this compare')}
65 ${_('No commits in this compare')}
65 </td>
66 </td>
66 </tr>
67 </tr>
67 % endif
68 % endif
68 </table>
69 </table>
69 <input type="hidden" name="__end__" value="revisions:sequence">
70 <input type="hidden" name="__end__" value="revisions:sequence">
70
71
71 </div>
72 </div>
72
73
73 <script>
74 <script>
74 commitsController = new CommitsController();
75 commitsController = new CommitsController();
75 $('.compare_select').on('click',function(e){
76 $('.compare_select').on('click',function(e){
76 var cid = $(this).attr('commit_id');
77 var cid = $(this).attr('commit_id');
77 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
78 $('#row-'+cid).toggleClass('hl', !$('#row-'+cid).hasClass('hl'));
78 });
79 });
79 </script>
80 </script>
@@ -1,169 +1,185 b''
1 <%text>
1 <%text>
2 <div style="display: none">
2 <div style="display: none">
3
3
4 <script>
5 var CG = new ColorGenerator();
6 </script>
7
4 <script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
8 <script id="ejs_gravatarWithUser" type="text/template" class="ejsTemplate">
5
9
6 <%
10 <%
7 if (size > 16) {
11 if (size > 16) {
8 var gravatar_class = 'gravatar gravatar-large';
12 var gravatar_class = 'gravatar gravatar-large';
9 } else {
13 } else {
10 var gravatar_class = 'gravatar';
14 var gravatar_class = 'gravatar';
11 }
15 }
12
16
13 if (tooltip) {
17 if (tooltip) {
14 var gravatar_class = gravatar_class + ' tooltip-hovercard';
18 var gravatar_class = gravatar_class + ' tooltip-hovercard';
15 }
19 }
16
20
17 var data_hovercard_alt = username;
21 var data_hovercard_alt = username;
18
22
19 %>
23 %>
20
24
21 <%
25 <%
22 if (show_disabled) {
26 if (show_disabled) {
23 var user_cls = 'user user-disabled';
27 var user_cls = 'user user-disabled';
24 } else {
28 } else {
25 var user_cls = 'user';
29 var user_cls = 'user';
26 }
30 }
27 var data_hovercard_url = pyroutes.url('hovercard_user', {"user_id": user_id})
31 var data_hovercard_url = pyroutes.url('hovercard_user', {"user_id": user_id})
28 %>
32 %>
29
33
30 <div class="rc-user">
34 <div class="rc-user">
31 <img class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" data-hovercard-url="<%= data_hovercard_url %>" data-hovercard-alt="<%= data_hovercard_alt %>" src="<%- gravatar_url -%>">
35 <img class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" data-hovercard-url="<%= data_hovercard_url %>" data-hovercard-alt="<%= data_hovercard_alt %>" src="<%- gravatar_url -%>">
32 <span class="<%= user_cls %>"> <%- user_link -%> </span>
36 <span class="<%= user_cls %>"> <%- user_link -%> </span>
33 </div>
37 </div>
34
38
35 </script>
39 </script>
36
40
37 <script>
38 var CG = new ColorGenerator();
39 </script>
40
41 <script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
41 <script id="ejs_reviewMemberEntry" type="text/template" class="ejsTemplate">
42 <%
42 <%
43 if (create) {
43 if (create) {
44 var edit_visibility = 'visible';
44 var edit_visibility = 'visible';
45 } else {
45 } else {
46 var edit_visibility = 'hidden';
46 var edit_visibility = 'hidden';
47 }
47 }
48
48
49 if (member.user_group && member.user_group.vote_rule) {
49 if (member.user_group && member.user_group.vote_rule) {
50 var groupStyle = 'border-right: 2px solid '+CG.asRGB(CG.getColor(member.user_group.vote_rule));
50 var reviewGroup = '<i class="icon-user-group"></i>';
51 var reviewGroupColor = CG.asRGB(CG.getColor(member.user_group.vote_rule));
51 } else {
52 } else {
52 var groupStyle = 'border-right: 2px solid transparent';
53 var reviewGroup = null;
54 var reviewGroupColor = 'transparent';
53 }
55 }
54 %>
56 %>
55
57
56 <li id="reviewer_<%= member.user_id %>" class="reviewer_entry" style="<%= groupStyle%>" tooltip="Review Group">
58 <tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
57
59
58 <div class="reviewers_member">
60 <td style="width: 20px">
59 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
61 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
60 <i class="icon-circle review-status-<%= review_status %>"></i>
62 <i class="icon-circle review-status-<%= review_status %>"></i>
63 </div>
64 </td>
61
65
62 </div>
66 <td>
63 <div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
67 <div id="reviewer_<%= member.user_id %>_name" class="reviewer_name">
64 <% if (mandatory) { %>
65 <div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
66 <i class="icon-lock"></i>
67 </div>
68 <% } %>
69
70 <%-
68 <%-
71 renderTemplate('gravatarWithUser', {
69 renderTemplate('gravatarWithUser', {
72 'size': 16,
70 'size': 16,
73 'show_disabled': false,
71 'show_disabled': false,
74 'tooltip': true,
72 'tooltip': true,
75 'username': member.username,
73 'username': member.username,
76 'user_id': member.user_id,
74 'user_id': member.user_id,
77 'user_link': member.user_link,
75 'user_link': member.user_link,
78 'gravatar_url': member.gravatar_link
76 'gravatar_url': member.gravatar_link
79 })
77 })
80 %>
78 %>
79 <span class="tooltip presence-state" style="display: none" title="This users is currently at this page">
80 <i class="icon-eye" style="color: #0ac878"></i>
81 </span>
81 </div>
82 </div>
83 </td>
82
84
85 <td style="width: 10px">
86 <% if (reviewGroup !== null) { %>
87 <span class="tooltip" title="Member of review group from rule: `<%= member.user_group.name %>`" style="color: <%= reviewGroupColor %>">
88 <%- reviewGroup %>
89 </span>
90 <% } %>
91 </td>
92
93 <% if (mandatory) { %>
94 <td style="text-align: right;width: 10px;">
95 <div class="reviewer_member_mandatory tooltip" title="Mandatory reviewer">
96 <i class="icon-lock"></i>
97 </div>
98 </td>
99
100 <% } else { %>
101 <td>
102 <% if (allowed_to_update) { %>
103 <div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
104 <i class="icon-remove"></i>
105 </div>
106 <% } %>
107 </td>
108 <% } %>
109
110 </tr>
111
112 <tr>
113 <td colspan="4" style="display: none" class="pr-user-rule-container">
83 <input type="hidden" name="__start__" value="reviewer:mapping">
114 <input type="hidden" name="__start__" value="reviewer:mapping">
84
115
85
116 <%if (member.user_group && member.user_group.vote_rule) { %>
86 <%if (member.user_group && member.user_group.vote_rule) {%>
87 <div class="reviewer_reason">
117 <div class="reviewer_reason">
88
118
89 <%if (member.user_group.vote_rule == -1) {%>
119 <%if (member.user_group.vote_rule == -1) {%>
90 - group votes required: ALL
120 - group votes required: ALL
91 <%} else {%>
121 <%} else {%>
92 - group votes required: <%= member.user_group.vote_rule %>
122 - group votes required: <%= member.user_group.vote_rule %>
93 <%}%>
123 <%}%>
94 </div>
124 </div>
95 <%}%>
125 <%} %>
96
126
97 <input type="hidden" name="__start__" value="reasons:sequence">
127 <input type="hidden" name="__start__" value="reasons:sequence">
98 <% for (var i = 0; i < reasons.length; i++) { %>
128 <% for (var i = 0; i < reasons.length; i++) { %>
99 <% var reason = reasons[i] %>
129 <% var reason = reasons[i] %>
100 <div class="reviewer_reason">- <%= reason %></div>
130 <div class="reviewer_reason">- <%= reason %></div>
101 <input type="hidden" name="reason" value="<%= reason %>">
131 <input type="hidden" name="reason" value="<%= reason %>">
102 <% } %>
132 <% } %>
103 <input type="hidden" name="__end__" value="reasons:sequence">
133 <input type="hidden" name="__end__" value="reasons:sequence">
104
134
105 <input type="hidden" name="__start__" value="rules:sequence">
135 <input type="hidden" name="__start__" value="rules:sequence">
106 <% for (var i = 0; i < member.rules.length; i++) { %>
136 <% for (var i = 0; i < member.rules.length; i++) { %>
107 <% var rule = member.rules[i] %>
137 <% var rule = member.rules[i] %>
108 <input type="hidden" name="rule_id" value="<%= rule %>">
138 <input type="hidden" name="rule_id" value="<%= rule %>">
109 <% } %>
139 <% } %>
110 <input type="hidden" name="__end__" value="rules:sequence">
140 <input type="hidden" name="__end__" value="rules:sequence">
111
141
112 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
142 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
113 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
143 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
114
144
115 <input type="hidden" name="__end__" value="reviewer:mapping">
145 <input type="hidden" name="__end__" value="reviewer:mapping">
116
146 </td>
117 <% if (mandatory) { %>
147 </tr>
118 <div class="reviewer_member_mandatory_remove" style="visibility: <%= edit_visibility %>;">
119 <i class="icon-remove"></i>
120 </div>
121 <% } else { %>
122 <% if (allowed_to_update) { %>
123 <div class="reviewer_member_remove action_button" onclick="reviewersController.removeReviewMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
124 <i class="icon-remove" ></i>
125 </div>
126 <% } %>
127 <% } %>
128 </div>
129 </li>
130
148
131 </script>
149 </script>
132
150
133
134 <script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
151 <script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
135
152
136 <%
153 <%
137 if (size > 16) {
154 if (size > 16) {
138 var gravatar_class = 'gravatar gravatar-large';
155 var gravatar_class = 'gravatar gravatar-large';
139 } else {
156 } else {
140 var gravatar_class = 'gravatar';
157 var gravatar_class = 'gravatar';
141 }
158 }
142
159
143 %>
160 %>
144
161
145 <%
162 <%
146 if (show_disabled) {
163 if (show_disabled) {
147 var user_cls = 'user user-disabled';
164 var user_cls = 'user user-disabled';
148 } else {
165 } else {
149 var user_cls = 'user';
166 var user_cls = 'user';
150 }
167 }
151
168
152 %>
169 %>
153
170
154 <div style='line-height: 20px'>
171 <div style='line-height: 20px'>
155 <img style="margin: -3px 0" class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" src="<%- gravatar_url -%>">
172 <img style="margin: -3px 0" class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" src="<%- gravatar_url -%>">
156 <strong><%- user_name -%></strong>, <code>v<%- version -%></code> edited <%- timeago_component -%>
173 <strong><%- user_name -%></strong>, <code>v<%- version -%></code> edited <%- timeago_component -%>
157 </div>
174 </div>
158
175
159 </script>
176 </script>
160
177
161
162 </div>
178 </div>
163
179
164 <script>
180 <script>
165 // registers the templates into global cache
181 // registers the templates into global cache
166 registerTemplates();
182 registerTemplates();
167 </script>
183 </script>
168
184
169 </%text>
185 </%text>
@@ -1,639 +1,639 b''
1 ## -*- coding: utf-8 -*-
1 ## -*- coding: utf-8 -*-
2
2
3 ## helpers
3 ## helpers
4 <%def name="tag_button(text, tag_type=None)">
4 <%def name="tag_button(text, tag_type=None)">
5 <%
5 <%
6 color_scheme = {
6 color_scheme = {
7 'default': 'border:1px solid #979797;color:#666666;background-color:#f9f9f9',
7 'default': 'border:1px solid #979797;color:#666666;background-color:#f9f9f9',
8 'approved': 'border:1px solid #0ac878;color:#0ac878;background-color:#f9f9f9',
8 'approved': 'border:1px solid #0ac878;color:#0ac878;background-color:#f9f9f9',
9 'rejected': 'border:1px solid #e85e4d;color:#e85e4d;background-color:#f9f9f9',
9 'rejected': 'border:1px solid #e85e4d;color:#e85e4d;background-color:#f9f9f9',
10 'under_review': 'border:1px solid #ffc854;color:#ffc854;background-color:#f9f9f9',
10 'under_review': 'border:1px solid #ffc854;color:#ffc854;background-color:#f9f9f9',
11 }
11 }
12
12
13 css_style = ';'.join([
13 css_style = ';'.join([
14 'display:inline',
14 'display:inline',
15 'border-radius:2px',
15 'border-radius:2px',
16 'font-size:12px',
16 'font-size:12px',
17 'padding:.2em',
17 'padding:.2em',
18 ])
18 ])
19
19
20 %>
20 %>
21 <pre style="${css_style}; ${color_scheme.get(tag_type, color_scheme['default'])}">${text}</pre>
21 <pre style="${css_style}; ${color_scheme.get(tag_type, color_scheme['default'])}">${text}</pre>
22 </%def>
22 </%def>
23
23
24 <%def name="status_text(text, tag_type=None)">
24 <%def name="status_text(text, tag_type=None)">
25 <%
25 <%
26 color_scheme = {
26 color_scheme = {
27 'default': 'color:#666666',
27 'default': 'color:#666666',
28 'approved': 'color:#0ac878',
28 'approved': 'color:#0ac878',
29 'rejected': 'color:#e85e4d',
29 'rejected': 'color:#e85e4d',
30 'under_review': 'color:#ffc854',
30 'under_review': 'color:#ffc854',
31 }
31 }
32 %>
32 %>
33 <span style="font-weight:bold;font-size:12px;padding:.2em;${color_scheme.get(tag_type, color_scheme['default'])}">${text}</span>
33 <span style="font-weight:bold;font-size:12px;padding:.2em;${color_scheme.get(tag_type, color_scheme['default'])}">${text}</span>
34 </%def>
34 </%def>
35
35
36 <%def name="gravatar_img(email, size=16)">
36 <%def name="gravatar_img(email, size=16)">
37 <%
37 <%
38 css_style = ';'.join([
38 css_style = ';'.join([
39 'padding: 0',
39 'padding: 0',
40 'margin: -4px 0',
40 'margin: -4px 0',
41 'border-radius: 50%',
41 'border-radius: 50%',
42 'box-sizing: content-box',
42 'box-sizing: content-box',
43 'display: inline',
43 'display: inline',
44 'line-height: 1em',
44 'line-height: 1em',
45 'min-width: 16px',
45 'min-width: 16px',
46 'min-height: 16px',
46 'min-height: 16px',
47 ])
47 ])
48 %>
48 %>
49
49
50 <img alt="gravatar" style="${css_style}" src="${h.gravatar_url(email, size)}" height="${size}" width="${size}">
50 <img alt="gravatar" style="${css_style}" src="${h.gravatar_url(email, size)}" height="${size}" width="${size}">
51 </%def>
51 </%def>
52
52
53 <%def name="link_css()">\
53 <%def name="link_css()">\
54 <%
54 <%
55 css_style = ';'.join([
55 css_style = ';'.join([
56 'color:#427cc9',
56 'color:#427cc9',
57 'text-decoration:none',
57 'text-decoration:none',
58 'cursor:pointer'
58 'cursor:pointer'
59 ])
59 ])
60 %>\
60 %>\
61 ${css_style}\
61 ${css_style}\
62 </%def>
62 </%def>
63
63
64 ## Constants
64 ## Constants
65 <%
65 <%
66 text_regular = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Helvetica, sans-serif"
66 text_regular = "-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen-Sans, Ubuntu, Cantarell, 'Helvetica Neue', Helvetica, sans-serif"
67 text_monospace = "'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace"
67 text_monospace = "'Menlo', 'Liberation Mono', 'Consolas', 'DejaVu Sans Mono', 'Ubuntu Mono', 'Courier New', 'andale mono', 'lucida console', monospace"
68
68
69 %>
69 %>
70
70
71 <%def name="plaintext_footer()" filter="trim">
71 <%def name="plaintext_footer()" filter="trim">
72 ${_('This is a notification from RhodeCode.')} ${instance_url}
72 ${_('This is a notification from RhodeCode.')} ${instance_url}
73 </%def>
73 </%def>
74
74
75 <%def name="body_plaintext()" filter="n,trim">
75 <%def name="body_plaintext()" filter="n,trim">
76 ## this example is not called itself but overridden in each template
76 ## this example is not called itself but overridden in each template
77 ## the plaintext_footer should be at the bottom of both html and text emails
77 ## the plaintext_footer should be at the bottom of both html and text emails
78 ${self.plaintext_footer()}
78 ${self.plaintext_footer()}
79 </%def>
79 </%def>
80
80
81 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
81 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
82 <html xmlns="http://www.w3.org/1999/xhtml">
82 <html xmlns="http://www.w3.org/1999/xhtml">
83 <head>
83 <head>
84 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
84 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
85 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
85 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
86 <title>${self.subject()}</title>
86 <title>${self.subject()}</title>
87 <style type="text/css">
87 <style type="text/css">
88 /* Based on The MailChimp Reset INLINE: Yes. */
88 /* Based on The MailChimp Reset INLINE: Yes. */
89 #outlook a {
89 #outlook a {
90 padding: 0;
90 padding: 0;
91 }
91 }
92
92
93 /* Force Outlook to provide a "view in browser" menu link. */
93 /* Force Outlook to provide a "view in browser" menu link. */
94 body {
94 body {
95 width: 100% !important;
95 width: 100% !important;
96 -webkit-text-size-adjust: 100%;
96 -webkit-text-size-adjust: 100%;
97 -ms-text-size-adjust: 100%;
97 -ms-text-size-adjust: 100%;
98 margin: 0;
98 margin: 0;
99 padding: 0;
99 padding: 0;
100 font-family: ${text_regular|n};
100 font-family: ${text_regular|n};
101 color: #000000;
101 color: #000000;
102 }
102 }
103
103
104 /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
104 /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
105 .ExternalClass {
105 .ExternalClass {
106 width: 100%;
106 width: 100%;
107 }
107 }
108
108
109 /* Force Hotmail to display emails at full width */
109 /* Force Hotmail to display emails at full width */
110 .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {
110 .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {
111 line-height: 100%;
111 line-height: 100%;
112 }
112 }
113
113
114 /* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
114 /* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
115 #backgroundTable {
115 #backgroundTable {
116 margin: 0;
116 margin: 0;
117 padding: 0;
117 padding: 0;
118 line-height: 100% !important;
118 line-height: 100% !important;
119 }
119 }
120
120
121 /* End reset */
121 /* End reset */
122
122
123 /* defaults for images*/
123 /* defaults for images*/
124 img {
124 img {
125 outline: none;
125 outline: none;
126 text-decoration: none;
126 text-decoration: none;
127 -ms-interpolation-mode: bicubic;
127 -ms-interpolation-mode: bicubic;
128 }
128 }
129
129
130 a img {
130 a img {
131 border: none;
131 border: none;
132 }
132 }
133
133
134 .image_fix {
134 .image_fix {
135 display: block;
135 display: block;
136 }
136 }
137
137
138 body {
138 body {
139 line-height: 1.2em;
139 line-height: 1.2em;
140 }
140 }
141
141
142 p {
142 p {
143 margin: 0 0 20px;
143 margin: 0 0 20px;
144 }
144 }
145
145
146 h1, h2, h3, h4, h5, h6 {
146 h1, h2, h3, h4, h5, h6 {
147 color: #323232 !important;
147 color: #323232 !important;
148 }
148 }
149
149
150 a {
150 a {
151 color: #427cc9;
151 color: #427cc9;
152 text-decoration: none;
152 text-decoration: none;
153 outline: none;
153 outline: none;
154 cursor: pointer;
154 cursor: pointer;
155 }
155 }
156
156
157 a:focus {
157 a:focus {
158 outline: none;
158 outline: none;
159 }
159 }
160
160
161 a:hover {
161 a:hover {
162 color: #305b91;
162 color: #305b91;
163 }
163 }
164
164
165 h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
165 h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {
166 color: #427cc9 !important;
166 color: #427cc9 !important;
167 text-decoration: none !important;
167 text-decoration: none !important;
168 }
168 }
169
169
170 h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {
170 h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {
171 color: #305b91 !important;
171 color: #305b91 !important;
172 }
172 }
173
173
174 h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {
174 h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {
175 color: #305b91 !important;
175 color: #305b91 !important;
176 }
176 }
177
177
178 table {
178 table {
179 font-size: 13px;
179 font-size: 13px;
180 border-collapse: collapse;
180 border-collapse: collapse;
181 mso-table-lspace: 0pt;
181 mso-table-lspace: 0pt;
182 mso-table-rspace: 0pt;
182 mso-table-rspace: 0pt;
183 }
183 }
184
184
185 table tr {
185 table tr {
186 display: table-row;
186 display: table-row;
187 vertical-align: inherit;
187 vertical-align: inherit;
188 border-color: inherit;
188 border-color: inherit;
189 border-spacing: 0 3px;
189 border-spacing: 0 3px;
190 }
190 }
191
191
192 table td {
192 table td {
193 padding: .65em 1em .65em 0;
193 padding: .65em 1em .65em 0;
194 border-collapse: collapse;
194 border-collapse: collapse;
195 vertical-align: top;
195 vertical-align: top;
196 text-align: left;
196 text-align: left;
197 }
197 }
198
198
199 input {
199 input {
200 display: inline;
200 display: inline;
201 border-radius: 2px;
201 border-radius: 2px;
202 border: 1px solid #dbd9da;
202 border: 1px solid #dbd9da;
203 padding: .5em;
203 padding: .5em;
204 }
204 }
205
205
206 input:focus {
206 input:focus {
207 outline: 1px solid #979797
207 outline: 1px solid #979797
208 }
208 }
209
209
210 code {
210 code {
211 font-family: ${text_monospace|n};
211 font-family: ${text_monospace|n};
212 white-space: pre-line !important;
212 white-space: pre-line !important;
213 color: #000000;
213 color: #000000;
214 }
214 }
215
215
216 ul.changes-ul {
216 ul.changes-ul {
217 list-style: none;
217 list-style: none;
218 list-style-type: none;
218 list-style-type: none;
219 padding: 0;
219 padding: 0;
220 margin: 10px 0;
220 margin: 10px 0;
221 }
221 }
222 ul.changes-ul li {
222 ul.changes-ul li {
223 list-style: none;
223 list-style: none;
224 list-style-type: none;
224 list-style-type: none;
225 margin: 2px 0;
225 margin: 2px 0;
226 }
226 }
227
227
228 @media only screen and (-webkit-min-device-pixel-ratio: 2) {
228 @media only screen and (-webkit-min-device-pixel-ratio: 2) {
229 /* Put your iPhone 4g styles in here */
229 /* Put your iPhone 4g styles in here */
230 }
230 }
231
231
232 /* Android targeting */
232 /* Android targeting */
233 @media only screen and (-webkit-device-pixel-ratio:.75){
233 @media only screen and (-webkit-device-pixel-ratio:.75){
234 /* Put CSS for low density (ldpi) Android layouts in here */
234 /* Put CSS for low density (ldpi) Android layouts in here */
235 }
235 }
236 @media only screen and (-webkit-device-pixel-ratio:1){
236 @media only screen and (-webkit-device-pixel-ratio:1){
237 /* Put CSS for medium density (mdpi) Android layouts in here */
237 /* Put CSS for medium density (mdpi) Android layouts in here */
238 }
238 }
239 @media only screen and (-webkit-device-pixel-ratio:1.5){
239 @media only screen and (-webkit-device-pixel-ratio:1.5){
240 /* Put CSS for high density (hdpi) Android layouts in here */
240 /* Put CSS for high density (hdpi) Android layouts in here */
241 }
241 }
242 /* end Android targeting */
242 /* end Android targeting */
243
243
244 /** MARKDOWN styling **/
244 /** MARKDOWN styling **/
245 div.markdown-block {
245 div.markdown-block {
246 clear: both;
246 clear: both;
247 overflow: hidden;
247 overflow: hidden;
248 margin: 0;
248 margin: 0;
249 padding: 3px 5px 3px
249 padding: 3px 5px 3px
250 }
250 }
251
251
252 div.markdown-block h1,
252 div.markdown-block h1,
253 div.markdown-block h2,
253 div.markdown-block h2,
254 div.markdown-block h3,
254 div.markdown-block h3,
255 div.markdown-block h4,
255 div.markdown-block h4,
256 div.markdown-block h5,
256 div.markdown-block h5,
257 div.markdown-block h6 {
257 div.markdown-block h6 {
258 border-bottom: none !important;
258 border-bottom: none !important;
259 padding: 0 !important;
259 padding: 0 !important;
260 overflow: visible !important
260 overflow: visible !important
261 }
261 }
262
262
263 div.markdown-block h1,
263 div.markdown-block h1,
264 div.markdown-block h2 {
264 div.markdown-block h2 {
265 border-bottom: 1px #e6e5e5 solid !important
265 border-bottom: 1px #e6e5e5 solid !important
266 }
266 }
267
267
268 div.markdown-block h1 {
268 div.markdown-block h1 {
269 font-size: 32px;
269 font-size: 32px;
270 margin: 15px 0 15px 0 !important;
270 margin: 15px 0 15px 0 !important;
271 padding-bottom: 5px !important
271 padding-bottom: 5px !important
272 }
272 }
273
273
274 div.markdown-block h2 {
274 div.markdown-block h2 {
275 font-size: 24px !important;
275 font-size: 24px !important;
276 margin: 34px 0 10px 0 !important;
276 margin: 34px 0 10px 0 !important;
277 padding-top: 15px !important;
277 padding-top: 15px !important;
278 padding-bottom: 8px !important
278 padding-bottom: 8px !important
279 }
279 }
280
280
281 div.markdown-block h3 {
281 div.markdown-block h3 {
282 font-size: 18px !important;
282 font-size: 18px !important;
283 margin: 30px 0 8px 0 !important;
283 margin: 30px 0 8px 0 !important;
284 padding-bottom: 2px !important
284 padding-bottom: 2px !important
285 }
285 }
286
286
287 div.markdown-block h4 {
287 div.markdown-block h4 {
288 font-size: 13px !important;
288 font-size: 13px !important;
289 margin: 18px 0 3px 0 !important
289 margin: 18px 0 3px 0 !important
290 }
290 }
291
291
292 div.markdown-block h5 {
292 div.markdown-block h5 {
293 font-size: 12px !important;
293 font-size: 12px !important;
294 margin: 15px 0 3px 0 !important
294 margin: 15px 0 3px 0 !important
295 }
295 }
296
296
297 div.markdown-block h6 {
297 div.markdown-block h6 {
298 font-size: 12px;
298 font-size: 12px;
299 color: #777777;
299 color: #777777;
300 margin: 15px 0 3px 0 !important
300 margin: 15px 0 3px 0 !important
301 }
301 }
302
302
303 div.markdown-block hr {
303 div.markdown-block hr {
304 border: 0;
304 border: 0;
305 color: #e6e5e5;
305 color: #e6e5e5;
306 background-color: #e6e5e5;
306 background-color: #e6e5e5;
307 height: 3px;
307 height: 3px;
308 margin-bottom: 13px
308 margin-bottom: 13px
309 }
309 }
310
310
311 div.markdown-block ol,
311 div.markdown-block ol,
312 div.markdown-block ul,
312 div.markdown-block ul,
313 div.markdown-block p,
313 div.markdown-block p,
314 div.markdown-block blockquote,
314 div.markdown-block blockquote,
315 div.markdown-block dl,
315 div.markdown-block dl,
316 div.markdown-block li,
316 div.markdown-block li,
317 div.markdown-block table {
317 div.markdown-block table {
318 margin: 3px 0 13px 0 !important;
318 margin: 3px 0 13px 0 !important;
319 color: #424242 !important;
319 color: #424242 !important;
320 font-size: 13px !important;
320 font-size: 13px !important;
321 font-family: ${text_regular|n};
321 font-family: ${text_regular|n};
322 font-weight: normal !important;
322 font-weight: normal !important;
323 overflow: visible !important;
323 overflow: visible !important;
324 line-height: 140% !important
324 line-height: 140% !important
325 }
325 }
326
326
327 div.markdown-block pre {
327 div.markdown-block pre {
328 margin: 3px 0 13px 0 !important;
328 margin: 3px 0 13px 0 !important;
329 padding: .5em;
329 padding: .5em;
330 color: #424242 !important;
330 color: #424242 !important;
331 font-size: 13px !important;
331 font-size: 13px !important;
332 overflow: visible !important;
332 overflow: visible !important;
333 line-height: 140% !important;
333 line-height: 140% !important;
334 background-color: #F5F5F5
334 background-color: #F5F5F5
335 }
335 }
336
336
337 div.markdown-block img {
337 div.markdown-block img {
338 border-style: none;
338 border-style: none;
339 background-color: #fff;
339 background-color: #fff;
340 padding-right: 20px;
340 padding-right: 20px;
341 max-width: 100%
341 max-width: 100%
342 }
342 }
343
343
344 div.markdown-block strong {
344 div.markdown-block strong {
345 font-weight: 600;
345 font-weight: 600;
346 margin: 0
346 margin: 0
347 }
347 }
348
348
349 div.markdown-block ul.checkbox, div.markdown-block ol.checkbox {
349 div.markdown-block ul.checkbox, div.markdown-block ol.checkbox {
350 padding-left: 20px !important;
350 padding-left: 20px !important;
351 margin-top: 0 !important;
351 margin-top: 0 !important;
352 margin-bottom: 18px !important
352 margin-bottom: 18px !important
353 }
353 }
354
354
355 div.markdown-block ul, div.markdown-block ol {
355 div.markdown-block ul, div.markdown-block ol {
356 padding-left: 30px !important;
356 padding-left: 30px !important;
357 margin-top: 0 !important;
357 margin-top: 0 !important;
358 margin-bottom: 18px !important
358 margin-bottom: 18px !important
359 }
359 }
360
360
361 div.markdown-block ul.checkbox li, div.markdown-block ol.checkbox li {
361 div.markdown-block ul.checkbox li, div.markdown-block ol.checkbox li {
362 list-style: none !important;
362 list-style: none !important;
363 margin: 6px !important;
363 margin: 0px !important;
364 padding: 0 !important
364 padding: 0 !important
365 }
365 }
366
366
367 div.markdown-block ul li, div.markdown-block ol li {
367 div.markdown-block ul li, div.markdown-block ol li {
368 list-style: disc !important;
368 list-style: disc !important;
369 margin: 6px !important;
369 margin: 0px !important;
370 padding: 0 !important
370 padding: 0 !important
371 }
371 }
372
372
373 div.markdown-block ol li {
373 div.markdown-block ol li {
374 list-style: decimal !important
374 list-style: decimal !important
375 }
375 }
376
376
377 div.markdown-block #message {
377 div.markdown-block #message {
378 -webkit-border-radius: 2px;
378 -webkit-border-radius: 2px;
379 -moz-border-radius: 2px;
379 -moz-border-radius: 2px;
380 border-radius: 2px;
380 border-radius: 2px;
381 border: 1px solid #dbd9da;
381 border: 1px solid #dbd9da;
382 display: block;
382 display: block;
383 width: 100%;
383 width: 100%;
384 height: 60px;
384 height: 60px;
385 margin: 6px 0
385 margin: 6px 0
386 }
386 }
387
387
388 div.markdown-block button, div.markdown-block #ws {
388 div.markdown-block button, div.markdown-block #ws {
389 font-size: 13px;
389 font-size: 13px;
390 padding: 4px 6px;
390 padding: 4px 6px;
391 -webkit-border-radius: 2px;
391 -webkit-border-radius: 2px;
392 -moz-border-radius: 2px;
392 -moz-border-radius: 2px;
393 border-radius: 2px;
393 border-radius: 2px;
394 border: 1px solid #dbd9da;
394 border: 1px solid #dbd9da;
395 background-color: #eeeeee
395 background-color: #eeeeee
396 }
396 }
397
397
398 div.markdown-block code,
398 div.markdown-block code,
399 div.markdown-block pre,
399 div.markdown-block pre,
400 div.markdown-block #ws,
400 div.markdown-block #ws,
401 div.markdown-block #message {
401 div.markdown-block #message {
402 font-family: ${text_monospace|n};
402 font-family: ${text_monospace|n};
403 font-size: 11px;
403 font-size: 11px;
404 -webkit-border-radius: 2px;
404 -webkit-border-radius: 2px;
405 -moz-border-radius: 2px;
405 -moz-border-radius: 2px;
406 border-radius: 2px;
406 border-radius: 2px;
407 background-color: #FFFFFF;
407 background-color: #FFFFFF;
408 color: #7E7F7F
408 color: #7E7F7F
409 }
409 }
410
410
411 div.markdown-block code {
411 div.markdown-block code {
412 border: 1px solid #7E7F7F;
412 border: 1px solid #7E7F7F;
413 margin: 0 2px;
413 margin: 0 2px;
414 padding: 0 5px
414 padding: 0 5px
415 }
415 }
416
416
417 div.markdown-block pre {
417 div.markdown-block pre {
418 border: 1px solid #7E7F7F;
418 border: 1px solid #7E7F7F;
419 overflow: auto;
419 overflow: auto;
420 padding: .5em;
420 padding: .5em;
421 background-color: #FFFFFF;
421 background-color: #FFFFFF;
422 }
422 }
423
423
424 div.markdown-block pre > code {
424 div.markdown-block pre > code {
425 border: 0;
425 border: 0;
426 margin: 0;
426 margin: 0;
427 padding: 0
427 padding: 0
428 }
428 }
429
429
430 div.rst-block {
430 div.rst-block {
431 clear: both;
431 clear: both;
432 overflow: hidden;
432 overflow: hidden;
433 margin: 0;
433 margin: 0;
434 padding: 3px 5px 3px
434 padding: 3px 5px 3px
435 }
435 }
436
436
437 div.rst-block h2 {
437 div.rst-block h2 {
438 font-weight: normal
438 font-weight: normal
439 }
439 }
440
440
441 div.rst-block h1,
441 div.rst-block h1,
442 div.rst-block h2,
442 div.rst-block h2,
443 div.rst-block h3,
443 div.rst-block h3,
444 div.rst-block h4,
444 div.rst-block h4,
445 div.rst-block h5,
445 div.rst-block h5,
446 div.rst-block h6 {
446 div.rst-block h6 {
447 border-bottom: 0 !important;
447 border-bottom: 0 !important;
448 margin: 0 !important;
448 margin: 0 !important;
449 padding: 0 !important;
449 padding: 0 !important;
450 line-height: 1.5em !important
450 line-height: 1.5em !important
451 }
451 }
452
452
453 div.rst-block h1:first-child {
453 div.rst-block h1:first-child {
454 padding-top: .25em !important
454 padding-top: .25em !important
455 }
455 }
456
456
457 div.rst-block h2, div.rst-block h3 {
457 div.rst-block h2, div.rst-block h3 {
458 margin: 1em 0 !important
458 margin: 1em 0 !important
459 }
459 }
460
460
461 div.rst-block h1, div.rst-block h2 {
461 div.rst-block h1, div.rst-block h2 {
462 border-bottom: 1px #e6e5e5 solid !important
462 border-bottom: 1px #e6e5e5 solid !important
463 }
463 }
464
464
465 div.rst-block h2 {
465 div.rst-block h2 {
466 margin-top: 1.5em !important;
466 margin-top: 1.5em !important;
467 padding-top: .5em !important
467 padding-top: .5em !important
468 }
468 }
469
469
470 div.rst-block p {
470 div.rst-block p {
471 color: black !important;
471 color: black !important;
472 margin: 1em 0 !important;
472 margin: 1em 0 !important;
473 line-height: 1.5em !important
473 line-height: 1.5em !important
474 }
474 }
475
475
476 div.rst-block ul {
476 div.rst-block ul {
477 list-style: disc !important;
477 list-style: disc !important;
478 margin: 1em 0 1em 2em !important;
478 margin: 1em 0 1em 2em !important;
479 clear: both
479 clear: both
480 }
480 }
481
481
482 div.rst-block ol {
482 div.rst-block ol {
483 list-style: decimal;
483 list-style: decimal;
484 margin: 1em 0 1em 2em !important
484 margin: 1em 0 1em 2em !important
485 }
485 }
486
486
487 div.rst-block pre, div.rst-block code {
487 div.rst-block pre, div.rst-block code {
488 font: 12px "Bitstream Vera Sans Mono", "Courier", monospace
488 font: 12px "Bitstream Vera Sans Mono", "Courier", monospace
489 }
489 }
490
490
491 div.rst-block code {
491 div.rst-block code {
492 font-size: 12px !important;
492 font-size: 12px !important;
493 background-color: ghostWhite !important;
493 background-color: ghostWhite !important;
494 color: #444 !important;
494 color: #444 !important;
495 padding: 0 .2em !important;
495 padding: 0 .2em !important;
496 border: 1px solid #7E7F7F !important
496 border: 1px solid #7E7F7F !important
497 }
497 }
498
498
499 div.rst-block pre code {
499 div.rst-block pre code {
500 padding: 0 !important;
500 padding: 0 !important;
501 font-size: 12px !important;
501 font-size: 12px !important;
502 background-color: #eee !important;
502 background-color: #eee !important;
503 border: none !important
503 border: none !important
504 }
504 }
505
505
506 div.rst-block pre {
506 div.rst-block pre {
507 margin: 1em 0;
507 margin: 1em 0;
508 padding: 15px;
508 padding: 15px;
509 border: 1px solid #7E7F7F;
509 border: 1px solid #7E7F7F;
510 -webkit-border-radius: 2px;
510 -webkit-border-radius: 2px;
511 -moz-border-radius: 2px;
511 -moz-border-radius: 2px;
512 border-radius: 2px;
512 border-radius: 2px;
513 overflow: auto;
513 overflow: auto;
514 font-size: 12px;
514 font-size: 12px;
515 color: #444;
515 color: #444;
516 background-color: #FFFFFF;
516 background-color: #FFFFFF;
517 }
517 }
518
518
519 .clear-both {
519 .clear-both {
520 clear:both;
520 clear:both;
521 }
521 }
522
522
523 /*elasticmatch is custom rhodecode tag*/
523 /*elasticmatch is custom rhodecode tag*/
524 .codehilite .c-ElasticMatch {
524 .codehilite .c-ElasticMatch {
525 background-color: #faffa6;
525 background-color: #faffa6;
526 padding: 0.2em;
526 padding: 0.2em;
527 }
527 }
528
528
529 .codehilite .c-ElasticMatch { background-color: #faffa6; padding: 0.2em;}
529 .codehilite .c-ElasticMatch { background-color: #faffa6; padding: 0.2em;}
530 .codehilite .hll { background-color: #ffffcc }
530 .codehilite .hll { background-color: #ffffcc }
531 .codehilite .c { color: #408080; font-style: italic } /* Comment */
531 .codehilite .c { color: #408080; font-style: italic } /* Comment */
532 .codehilite .err { border: none } /* Error */
532 .codehilite .err { border: none } /* Error */
533 .codehilite .k { color: #008000; font-weight: bold } /* Keyword */
533 .codehilite .k { color: #008000; font-weight: bold } /* Keyword */
534 .codehilite .o { color: #666666 } /* Operator */
534 .codehilite .o { color: #666666 } /* Operator */
535 .codehilite .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
535 .codehilite .ch { color: #408080; font-style: italic } /* Comment.Hashbang */
536 .codehilite .cm { color: #408080; font-style: italic } /* Comment.Multiline */
536 .codehilite .cm { color: #408080; font-style: italic } /* Comment.Multiline */
537 .codehilite .cp { color: #BC7A00 } /* Comment.Preproc */
537 .codehilite .cp { color: #BC7A00 } /* Comment.Preproc */
538 .codehilite .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
538 .codehilite .cpf { color: #408080; font-style: italic } /* Comment.PreprocFile */
539 .codehilite .c1 { color: #408080; font-style: italic } /* Comment.Single */
539 .codehilite .c1 { color: #408080; font-style: italic } /* Comment.Single */
540 .codehilite .cs { color: #408080; font-style: italic } /* Comment.Special */
540 .codehilite .cs { color: #408080; font-style: italic } /* Comment.Special */
541 .codehilite .gd { color: #A00000 } /* Generic.Deleted */
541 .codehilite .gd { color: #A00000 } /* Generic.Deleted */
542 .codehilite .ge { font-style: italic } /* Generic.Emph */
542 .codehilite .ge { font-style: italic } /* Generic.Emph */
543 .codehilite .gr { color: #FF0000 } /* Generic.Error */
543 .codehilite .gr { color: #FF0000 } /* Generic.Error */
544 .codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
544 .codehilite .gh { color: #000080; font-weight: bold } /* Generic.Heading */
545 .codehilite .gi { color: #00A000 } /* Generic.Inserted */
545 .codehilite .gi { color: #00A000 } /* Generic.Inserted */
546 .codehilite .go { color: #888888 } /* Generic.Output */
546 .codehilite .go { color: #888888 } /* Generic.Output */
547 .codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
547 .codehilite .gp { color: #000080; font-weight: bold } /* Generic.Prompt */
548 .codehilite .gs { font-weight: bold } /* Generic.Strong */
548 .codehilite .gs { font-weight: bold } /* Generic.Strong */
549 .codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
549 .codehilite .gu { color: #800080; font-weight: bold } /* Generic.Subheading */
550 .codehilite .gt { color: #0044DD } /* Generic.Traceback */
550 .codehilite .gt { color: #0044DD } /* Generic.Traceback */
551 .codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
551 .codehilite .kc { color: #008000; font-weight: bold } /* Keyword.Constant */
552 .codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
552 .codehilite .kd { color: #008000; font-weight: bold } /* Keyword.Declaration */
553 .codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
553 .codehilite .kn { color: #008000; font-weight: bold } /* Keyword.Namespace */
554 .codehilite .kp { color: #008000 } /* Keyword.Pseudo */
554 .codehilite .kp { color: #008000 } /* Keyword.Pseudo */
555 .codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
555 .codehilite .kr { color: #008000; font-weight: bold } /* Keyword.Reserved */
556 .codehilite .kt { color: #B00040 } /* Keyword.Type */
556 .codehilite .kt { color: #B00040 } /* Keyword.Type */
557 .codehilite .m { color: #666666 } /* Literal.Number */
557 .codehilite .m { color: #666666 } /* Literal.Number */
558 .codehilite .s { color: #BA2121 } /* Literal.String */
558 .codehilite .s { color: #BA2121 } /* Literal.String */
559 .codehilite .na { color: #7D9029 } /* Name.Attribute */
559 .codehilite .na { color: #7D9029 } /* Name.Attribute */
560 .codehilite .nb { color: #008000 } /* Name.Builtin */
560 .codehilite .nb { color: #008000 } /* Name.Builtin */
561 .codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */
561 .codehilite .nc { color: #0000FF; font-weight: bold } /* Name.Class */
562 .codehilite .no { color: #880000 } /* Name.Constant */
562 .codehilite .no { color: #880000 } /* Name.Constant */
563 .codehilite .nd { color: #AA22FF } /* Name.Decorator */
563 .codehilite .nd { color: #AA22FF } /* Name.Decorator */
564 .codehilite .ni { color: #999999; font-weight: bold } /* Name.Entity */
564 .codehilite .ni { color: #999999; font-weight: bold } /* Name.Entity */
565 .codehilite .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
565 .codehilite .ne { color: #D2413A; font-weight: bold } /* Name.Exception */
566 .codehilite .nf { color: #0000FF } /* Name.Function */
566 .codehilite .nf { color: #0000FF } /* Name.Function */
567 .codehilite .nl { color: #A0A000 } /* Name.Label */
567 .codehilite .nl { color: #A0A000 } /* Name.Label */
568 .codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
568 .codehilite .nn { color: #0000FF; font-weight: bold } /* Name.Namespace */
569 .codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */
569 .codehilite .nt { color: #008000; font-weight: bold } /* Name.Tag */
570 .codehilite .nv { color: #19177C } /* Name.Variable */
570 .codehilite .nv { color: #19177C } /* Name.Variable */
571 .codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
571 .codehilite .ow { color: #AA22FF; font-weight: bold } /* Operator.Word */
572 .codehilite .w { color: #bbbbbb } /* Text.Whitespace */
572 .codehilite .w { color: #bbbbbb } /* Text.Whitespace */
573 .codehilite .mb { color: #666666 } /* Literal.Number.Bin */
573 .codehilite .mb { color: #666666 } /* Literal.Number.Bin */
574 .codehilite .mf { color: #666666 } /* Literal.Number.Float */
574 .codehilite .mf { color: #666666 } /* Literal.Number.Float */
575 .codehilite .mh { color: #666666 } /* Literal.Number.Hex */
575 .codehilite .mh { color: #666666 } /* Literal.Number.Hex */
576 .codehilite .mi { color: #666666 } /* Literal.Number.Integer */
576 .codehilite .mi { color: #666666 } /* Literal.Number.Integer */
577 .codehilite .mo { color: #666666 } /* Literal.Number.Oct */
577 .codehilite .mo { color: #666666 } /* Literal.Number.Oct */
578 .codehilite .sa { color: #BA2121 } /* Literal.String.Affix */
578 .codehilite .sa { color: #BA2121 } /* Literal.String.Affix */
579 .codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */
579 .codehilite .sb { color: #BA2121 } /* Literal.String.Backtick */
580 .codehilite .sc { color: #BA2121 } /* Literal.String.Char */
580 .codehilite .sc { color: #BA2121 } /* Literal.String.Char */
581 .codehilite .dl { color: #BA2121 } /* Literal.String.Delimiter */
581 .codehilite .dl { color: #BA2121 } /* Literal.String.Delimiter */
582 .codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
582 .codehilite .sd { color: #BA2121; font-style: italic } /* Literal.String.Doc */
583 .codehilite .s2 { color: #BA2121 } /* Literal.String.Double */
583 .codehilite .s2 { color: #BA2121 } /* Literal.String.Double */
584 .codehilite .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
584 .codehilite .se { color: #BB6622; font-weight: bold } /* Literal.String.Escape */
585 .codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */
585 .codehilite .sh { color: #BA2121 } /* Literal.String.Heredoc */
586 .codehilite .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
586 .codehilite .si { color: #BB6688; font-weight: bold } /* Literal.String.Interpol */
587 .codehilite .sx { color: #008000 } /* Literal.String.Other */
587 .codehilite .sx { color: #008000 } /* Literal.String.Other */
588 .codehilite .sr { color: #BB6688 } /* Literal.String.Regex */
588 .codehilite .sr { color: #BB6688 } /* Literal.String.Regex */
589 .codehilite .s1 { color: #BA2121 } /* Literal.String.Single */
589 .codehilite .s1 { color: #BA2121 } /* Literal.String.Single */
590 .codehilite .ss { color: #19177C } /* Literal.String.Symbol */
590 .codehilite .ss { color: #19177C } /* Literal.String.Symbol */
591 .codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */
591 .codehilite .bp { color: #008000 } /* Name.Builtin.Pseudo */
592 .codehilite .fm { color: #0000FF } /* Name.Function.Magic */
592 .codehilite .fm { color: #0000FF } /* Name.Function.Magic */
593 .codehilite .vc { color: #19177C } /* Name.Variable.Class */
593 .codehilite .vc { color: #19177C } /* Name.Variable.Class */
594 .codehilite .vg { color: #19177C } /* Name.Variable.Global */
594 .codehilite .vg { color: #19177C } /* Name.Variable.Global */
595 .codehilite .vi { color: #19177C } /* Name.Variable.Instance */
595 .codehilite .vi { color: #19177C } /* Name.Variable.Instance */
596 .codehilite .vm { color: #19177C } /* Name.Variable.Magic */
596 .codehilite .vm { color: #19177C } /* Name.Variable.Magic */
597 .codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */
597 .codehilite .il { color: #666666 } /* Literal.Number.Integer.Long */
598
598
599 </style>
599 </style>
600
600
601 </head>
601 </head>
602 <body>
602 <body>
603
603
604 <div>
604 <div>
605 <!-- Wrapper/Container Table: Use a wrapper table to control the width and the background color consistently of your email. Use this approach instead of setting attributes on the body tag. -->
605 <!-- Wrapper/Container Table: Use a wrapper table to control the width and the background color consistently of your email. Use this approach instead of setting attributes on the body tag. -->
606 <table cellpadding="0" cellspacing="0" border="0" id="backgroundTable" align="left" style="margin:1%;width:97%;padding:0;font-family:${text_regular|n};font-weight:100;border:1px solid #dbd9da">
606 <table cellpadding="0" cellspacing="0" border="0" id="backgroundTable" align="left" style="margin:1%;width:97%;padding:0;font-family:${text_regular|n};font-weight:100;border:1px solid #dbd9da">
607 <tr>
607 <tr>
608 <td valign="top" style="padding:0;">
608 <td valign="top" style="padding:0;">
609 <table cellpadding="0" cellspacing="0" border="0" align="left" width="100%">
609 <table cellpadding="0" cellspacing="0" border="0" align="left" width="100%">
610 <tr>
610 <tr>
611 <td style="width:100%;padding:10px 15px;background-color:#202020" valign="top">
611 <td style="width:100%;padding:10px 15px;background-color:#202020" valign="top">
612 <a style="color:#eeeeee;text-decoration:none;" href="${instance_url}">
612 <a style="color:#eeeeee;text-decoration:none;" href="${instance_url}">
613 ${_('RhodeCode')}
613 ${_('RhodeCode')}
614 % if rhodecode_instance_name:
614 % if rhodecode_instance_name:
615 - ${rhodecode_instance_name}
615 - ${rhodecode_instance_name}
616 % endif
616 % endif
617 </a>
617 </a>
618 </td>
618 </td>
619 </tr>
619 </tr>
620 <tr style="background-color: #fff">
620 <tr style="background-color: #fff">
621 <td style="padding:15px;" valign="top">${self.body()}</td>
621 <td style="padding:15px;" valign="top">${self.body()}</td>
622 </tr>
622 </tr>
623 </table>
623 </table>
624 </td>
624 </td>
625 </tr>
625 </tr>
626 </table>
626 </table>
627 <!-- End of wrapper table -->
627 <!-- End of wrapper table -->
628 </div>
628 </div>
629
629
630 <div style="width:100%; clear: both; height: 1px">&nbsp;</div>
630 <div style="width:100%; clear: both; height: 1px">&nbsp;</div>
631
631
632 <div style="margin-left:1%;font-weight:100;font-size:11px;color:#666666;text-decoration:none;font-family:${text_monospace};">
632 <div style="margin-left:1%;font-weight:100;font-size:11px;color:#666666;text-decoration:none;font-family:${text_monospace};">
633 ${_('This is a notification from RhodeCode.')}
633 ${_('This is a notification from RhodeCode.')}
634 <a style="font-weight:100;font-size:11px;color:#666666;text-decoration:none;font-family:${text_monospace};" href="${instance_url}">
634 <a style="font-weight:100;font-size:11px;color:#666666;text-decoration:none;font-family:${text_monospace};" href="${instance_url}">
635 ${instance_url}
635 ${instance_url}
636 </a>
636 </a>
637 </div>
637 </div>
638 </body>
638 </body>
639 </html>
639 </html>
This diff has been collapsed as it changes many lines, (1173 lines changed) Show them Hide them
@@ -1,1211 +1,1442 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
4
5 <%def name="title()">
5 <%def name="title()">
6 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
6 ${_('{} Pull Request !{}').format(c.repo_name, c.pull_request.pull_request_id)}
7 %if c.rhodecode_name:
7 %if c.rhodecode_name:
8 &middot; ${h.branding(c.rhodecode_name)}
8 &middot; ${h.branding(c.rhodecode_name)}
9 %endif
9 %endif
10 </%def>
10 </%def>
11
11
12 <%def name="breadcrumbs_links()">
12 <%def name="breadcrumbs_links()">
13
13
14 </%def>
14 </%def>
15
15
16 <%def name="menu_bar_nav()">
16 <%def name="menu_bar_nav()">
17 ${self.menu_items(active='repositories')}
17 ${self.menu_items(active='repositories')}
18 </%def>
18 </%def>
19
19
20 <%def name="menu_bar_subnav()">
20 <%def name="menu_bar_subnav()">
21 ${self.repo_menu(active='showpullrequest')}
21 ${self.repo_menu(active='showpullrequest')}
22 </%def>
22 </%def>
23
23
24 <%def name="comments_table(comments, counter_num, todo_comments=False)">
25 <%
26 old_comments = False
27 if todo_comments:
28 cls_ = 'todos-content-table'
29 def sorter(entry):
30 user_id = entry.author.user_id
31 resolved = '1' if entry.resolved else '0'
32 if user_id == c.rhodecode_user.user_id:
33 # own comments first
34 user_id = 0
35 return '{}'.format(str(entry.comment_id).zfill(10000))
36 else:
37 cls_ = 'comments-content-table'
38 def sorter(entry):
39 user_id = entry.author.user_id
40 return '{}'.format(str(entry.comment_id).zfill(10000))
41
42
43
44 %>
45 <table class="todo-table ${cls_}" data-total-count="${len(comments)}" data-counter="${counter_num}">
46
47 % for loop_obj, comment_obj in h.looper(reversed(sorted(comments, key=sorter))):
48 <%
49 display = ''
50 _cls = ''
51 %>
52 <% comment_ver_index = comment_obj.get_index_version(getattr(c, 'versions', [])) %>
53 <%
54 prev_comment_ver_index = 0
55 if loop_obj.previous:
56 prev_comment_ver_index = loop_obj.previous.get_index_version(getattr(c, 'versions', []))
57 %>
58 <% hidden_at_ver = comment_obj.outdated_at_version_js(c.at_version_num) %>
59 <% is_from_old_ver = comment_obj.older_than_version_js(c.at_version_num) %>
60 <%
61 if (prev_comment_ver_index > comment_ver_index) and old_comments is False:
62 old_comments = True
63 %>
64 % if todo_comments:
65 % if comment_obj.resolved:
66 <% _cls = 'resolved-todo' %>
67 <% display = 'none' %>
68 % endif
69 % else:
70 ## SKIP TODOs we display them in other area
71 % if comment_obj.is_todo:
72 <% display = 'none' %>
73 % endif
74 ## Skip outdated comments
75 % if comment_obj.outdated:
76 <% display = 'none' %>
77 <% _cls = 'hidden-comment' %>
78 % endif
79 % endif
80
81 % if not todo_comments and old_comments:
82 <tr class="old-comments-marker">
83 <td colspan="3"> <code>comments from older versions</code> </td>
84 </tr>
85 ## reset markers so we only show this marker once
86 <% old_comments = None %>
87 % endif
88
89 <tr class="${_cls}" style="display: ${display};">
90 <td class="td-todo-number">
91
92 <a class="${('todo-resolved' if comment_obj.resolved else '')} permalink"
93 href="#comment-${comment_obj.comment_id}"
94 onclick="return Rhodecode.comments.scrollToComment($('#comment-${comment_obj.comment_id}'), 0, ${hidden_at_ver})">
95
96 % if todo_comments:
97 % if comment_obj.is_inline:
98 <i class="tooltip icon-code" title="Inline TODO comment ${('made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else 'made in this version')}."></i>
99 % else:
100 <i class="tooltip icon-comment" title="General TODO comment ${('made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else 'made in this version')}."></i>
101 % endif
102 % else:
103 % if comment_obj.outdated:
104 <i class="tooltip icon-comment-toggle" title="Inline Outdated made in v${comment_ver_index}."></i>
105 % elif comment_obj.is_inline:
106 <i class="tooltip icon-code" title="Inline comment ${('made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else 'made in this version')}."></i>
107 % else:
108 <i class="tooltip icon-comment" title="General comment ${('made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else 'made in this version')}."></i>
109 % endif
110 % endif
111
112 #${comment_obj.comment_id}
113 </a>
114 </td>
115
116 <td class="td-todo-gravatar">
117 ${base.gravatar(comment_obj.author.email, 16, user=comment_obj.author, tooltip=True, extra_class=['no-margin'])}
118 </td>
119 <td class="todo-comment-text-wrapper">
120 <div class="tooltip todo-comment-text timeago" title="${h.format_date(comment_obj.created_on)}" datetime="${comment_obj.created_on}${h.get_timezone(comment_obj.created_on, time_is_local=True)}">
121 <code>${h.chop_at_smart(comment_obj.text, '\n', suffix_if_chopped='...')}</code>
122 </div>
123 </td>
124 </tr>
125 % endfor
126
127 </table>
128
129 </%def>
130
131
24 <%def name="main()">
132 <%def name="main()">
133 ## Container to gather extracted Tickets
134 <%
135 c.referenced_commit_issues = []
136 c.referenced_desc_issues = []
137 %>
25
138
26 <script type="text/javascript">
139 <script type="text/javascript">
27 // TODO: marcink switch this to pyroutes
140 // TODO: marcink switch this to pyroutes
28 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
141 AJAX_COMMENT_DELETE_URL = "${h.route_path('pullrequest_comment_delete',repo_name=c.repo_name,pull_request_id=c.pull_request.pull_request_id,comment_id='__COMMENT_ID__')}";
29 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
142 templateContext.pull_request_data.pull_request_id = ${c.pull_request.pull_request_id};
30 </script>
143 </script>
31
144
32 <div class="box">
145 <div class="box">
33
146
34 <div class="box pr-summary">
147 <div class="box pr-summary">
35
148
36 <div class="summary-details block-left">
149 <div class="summary-details block-left">
37 <div id="pr-title">
150 <div id="pr-title">
38 % if c.pull_request.is_closed():
151 % if c.pull_request.is_closed():
39 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
152 <span class="pr-title-closed-tag tag">${_('Closed')}</span>
40 % endif
153 % endif
41 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
154 <input class="pr-title-input large disabled" disabled="disabled" name="pullrequest_title" type="text" value="${c.pull_request.title}">
42 </div>
155 </div>
43 <div id="pr-title-edit" class="input" style="display: none;">
156 <div id="pr-title-edit" class="input" style="display: none;">
44 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
157 <input class="pr-title-input large" id="pr-title-input" name="pullrequest_title" type="text" value="${c.pull_request.title}">
45 </div>
158 </div>
46
159
47 <% summary = lambda n:{False:'summary-short'}.get(n) %>
160 <% summary = lambda n:{False:'summary-short'}.get(n) %>
48 <div class="pr-details-title">
161 <div class="pr-details-title">
49 <div class="pull-left">
162 <div class="pull-left">
50 <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>
163 <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>
51 ${_('Created on')}
164 ${_('Created on')}
52 <span class="tooltip" title="${_('Last updated on')} ${h.format_date(c.pull_request.updated_on)}">${h.format_date(c.pull_request.created_on)},</span>
165 <span class="tooltip" title="${_('Last updated on')} ${h.format_date(c.pull_request.updated_on)}">${h.format_date(c.pull_request.created_on)},</span>
53 <span class="pr-details-title-author-pref">${_('by')}</span>
166 <span class="pr-details-title-author-pref">${_('by')}</span>
54 </div>
167 </div>
55
168
56 <div class="pull-left">
169 <div class="pull-left">
57 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
170 ${self.gravatar_with_user(c.pull_request.author.email, 16, tooltip=True)}
58 </div>
171 </div>
59
172
60 %if c.allowed_to_update:
173 %if c.allowed_to_update:
61 <div class="pull-right">
174 <div class="pull-right">
62 <div id="edit_pull_request" class="action_button pr-save" style="display: none;">${_('Update title & description')}</div>
175 <div id="edit_pull_request" class="action_button pr-save" style="display: none;">${_('Update title & description')}</div>
63 <div id="delete_pullrequest" class="action_button pr-save ${('' if c.allowed_to_delete else 'disabled' )}" style="display: none;">
176 <div id="delete_pullrequest" class="action_button pr-save ${('' if c.allowed_to_delete else 'disabled' )}" style="display: none;">
64 % if c.allowed_to_delete:
177 % if c.allowed_to_delete:
65 ${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)}
178 ${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)}
66 <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}"
179 <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}"
67 onclick="submitConfirm(event, this, _gettext('Confirm to delete this pull request'), _gettext('Delete'), '${'!{}'.format(c.pull_request.pull_request_id)}')"
180 onclick="submitConfirm(event, this, _gettext('Confirm to delete this pull request'), _gettext('Delete'), '${'!{}'.format(c.pull_request.pull_request_id)}')"
68 type="submit" value="${_('Delete pull request')}">
181 type="submit" value="${_('Delete pull request')}">
69 ${h.end_form()}
182 ${h.end_form()}
70 % else:
183 % else:
71 <span class="tooltip" title="${_('Not allowed to delete this pull request')}">${_('Delete pull request')}</span>
184 <span class="tooltip" title="${_('Not allowed to delete this pull request')}">${_('Delete pull request')}</span>
72 % endif
185 % endif
73 </div>
186 </div>
74 <div id="open_edit_pullrequest" class="action_button">${_('Edit')}</div>
187 <div id="open_edit_pullrequest" class="action_button">${_('Edit')}</div>
75 <div id="close_edit_pullrequest" class="action_button" style="display: none;">${_('Cancel')}</div>
188 <div id="close_edit_pullrequest" class="action_button" style="display: none;">${_('Cancel')}</div>
76 </div>
189 </div>
77
190
78 %endif
191 %endif
79 </div>
192 </div>
80
193
81 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
194 <div id="pr-desc" class="input" title="${_('Rendered using {} renderer').format(c.renderer)}">
82 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name)}
195 ${h.render(c.pull_request.description, renderer=c.renderer, repo_name=c.repo_name, issues_container=c.referenced_desc_issues)}
83 </div>
196 </div>
84
197
85 <div id="pr-desc-edit" class="input textarea" style="display: none;">
198 <div id="pr-desc-edit" class="input textarea" style="display: none;">
86 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
199 <input id="pr-renderer-input" type="hidden" name="description_renderer" value="${c.visual.default_renderer}">
87 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
200 ${dt.markup_form('pr-description-input', form_text=c.pull_request.description)}
88 </div>
201 </div>
89
202
90 <div id="summary" class="fields pr-details-content">
203 <div id="summary" class="fields pr-details-content">
91
204
92 ## review
93 <div class="field">
94 <div class="label-pr-detail">
95 <label>${_('Review status')}:</label>
96 </div>
97 <div class="input">
98 %if c.pull_request_review_status:
99 <div class="tag status-tag-${c.pull_request_review_status}">
100 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
101 <span class="changeset-status-lbl">
102 %if c.pull_request.is_closed():
103 ${_('Closed')},
104 %endif
105
106 ${h.commit_status_lbl(c.pull_request_review_status)}
107
108 </span>
109 </div>
110 - ${_ungettext('calculated based on {} reviewer vote', 'calculated based on {} reviewers votes', len(c.pull_request_reviewers)).format(len(c.pull_request_reviewers))}
111 %endif
112 </div>
113 </div>
114
115 ## source
205 ## source
116 <div class="field">
206 <div class="field">
117 <div class="label-pr-detail">
207 <div class="label-pr-detail">
118 <label>${_('Commit flow')}:</label>
208 <label>${_('Commit flow')}:</label>
119 </div>
209 </div>
120 <div class="input">
210 <div class="input">
121 <div class="pr-commit-flow">
211 <div class="pr-commit-flow">
122 ## Source
212 ## Source
123 %if c.pull_request.source_ref_parts.type == 'branch':
213 %if c.pull_request.source_ref_parts.type == 'branch':
124 <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>
214 <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>
125 %else:
215 %else:
126 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
216 <code class="pr-source-info">${'{}:{}'.format(c.pull_request.source_ref_parts.type, c.pull_request.source_ref_parts.name)}</code>
127 %endif
217 %endif
128 ${_('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>
218 ${_('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>
129 &rarr;
219 &rarr;
130 ## Target
220 ## Target
131 %if c.pull_request.target_ref_parts.type == 'branch':
221 %if c.pull_request.target_ref_parts.type == 'branch':
132 <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>
222 <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>
133 %else:
223 %else:
134 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
224 <code class="pr-target-info">${'{}:{}'.format(c.pull_request.target_ref_parts.type, c.pull_request.target_ref_parts.name)}</code>
135 %endif
225 %endif
136
226
137 ${_('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>
227 ${_('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>
138
228
139 <a class="source-details-action" href="#expand-source-details" onclick="return versionController.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>'>
229 <a class="source-details-action" href="#expand-source-details" onclick="return versionController.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>'>
140 <i class="icon-angle-down">more details</i>
230 <i class="icon-angle-down">more details</i>
141 </a>
231 </a>
142
232
143 </div>
233 </div>
144
234
145 <div class="source-details" style="display: none">
235 <div class="source-details" style="display: none">
146
236
147 <ul>
237 <ul>
148
238
149 ## common ancestor
239 ## common ancestor
150 <li>
240 <li>
151 ${_('Common ancestor')}:
241 ${_('Common ancestor')}:
152 % if c.ancestor_commit:
242 % if c.ancestor_commit:
153 <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>
243 <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>
154 % else:
244 % else:
155 ${_('not available')}
245 ${_('not available')}
156 % endif
246 % endif
157 </li>
247 </li>
158
248
159 ## pull url
249 ## pull url
160 <li>
250 <li>
161 %if h.is_hg(c.pull_request.source_repo):
251 %if h.is_hg(c.pull_request.source_repo):
162 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
252 <% clone_url = 'hg pull -r {} {}'.format(h.short_id(c.source_ref), c.pull_request.source_repo.clone_url()) %>
163 %elif h.is_git(c.pull_request.source_repo):
253 %elif h.is_git(c.pull_request.source_repo):
164 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
254 <% clone_url = 'git pull {} {}'.format(c.pull_request.source_repo.clone_url(), c.pull_request.source_ref_parts.name) %>
165 %endif
255 %endif
166
256
167 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
257 <span>${_('Pull changes from source')}</span>: <input type="text" class="input-monospace pr-pullinfo" value="${clone_url}" readonly="readonly">
168 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
258 <i class="tooltip icon-clipboard clipboard-action pull-right pr-pullinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the pull url')}"></i>
169 </li>
259 </li>
170
260
171 ## Shadow repo
261 ## Shadow repo
172 <li>
262 <li>
173 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
263 % if not c.pull_request.is_closed() and c.pull_request.shadow_merge_ref:
174 %if h.is_hg(c.pull_request.target_repo):
264 %if h.is_hg(c.pull_request.target_repo):
175 <% 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) %>
265 <% 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) %>
176 %elif h.is_git(c.pull_request.target_repo):
266 %elif h.is_git(c.pull_request.target_repo):
177 <% 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) %>
267 <% 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) %>
178 %endif
268 %endif
179
269
180 <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">
270 <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">
181 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
271 <i class="tooltip icon-clipboard clipboard-action pull-right pr-mergeinfo-copy" data-clipboard-text="${clone_url}" title="${_('Copy the clone url')}"></i>
182
272
183 % else:
273 % else:
184 <div class="">
274 <div class="">
185 ${_('Shadow repository data not available')}.
275 ${_('Shadow repository data not available')}.
186 </div>
276 </div>
187 % endif
277 % endif
188 </li>
278 </li>
189
279
190 </ul>
280 </ul>
191
281
192 </div>
282 </div>
193
283
194 </div>
284 </div>
195
285
196 </div>
286 </div>
197
287
198 ## versions
288 ## versions
199 <div class="field">
289 <div class="field">
200 <div class="label-pr-detail">
290 <div class="label-pr-detail">
201 <label>${_('Versions')}:</label>
291 <label>${_('Versions')}:</label>
202 </div>
292 </div>
203
293
204 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
294 <% outdated_comm_count_ver = len(c.inline_versions[None]['outdated']) %>
205 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
295 <% general_outdated_comm_count_ver = len(c.comment_versions[None]['outdated']) %>
206
296
207 <div class="pr-versions">
297 <div class="pr-versions">
208 % if c.show_version_changes:
298 % if c.show_version_changes:
209 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
299 <% outdated_comm_count_ver = len(c.inline_versions[c.at_version_num]['outdated']) %>
210 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
300 <% general_outdated_comm_count_ver = len(c.comment_versions[c.at_version_num]['outdated']) %>
211 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
301 ${_ungettext('{} version available for this pull request, ', '{} versions available for this pull request, ', len(c.versions)).format(len(c.versions))}
212 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
302 <a id="show-pr-versions" onclick="return versionController.toggleVersionView(this)" href="#show-pr-versions"
213 data-toggle-on="${_('show versions')}."
303 data-toggle-on="${_('show versions')}."
214 data-toggle-off="${_('hide versions')}.">
304 data-toggle-off="${_('hide versions')}.">
215 ${_('show versions')}.
305 ${_('show versions')}.
216 </a>
306 </a>
217 <table>
307 <table>
218 ## SHOW ALL VERSIONS OF PR
308 ## SHOW ALL VERSIONS OF PR
219 <% ver_pr = None %>
309 <% ver_pr = None %>
220
310
221 % for data in reversed(list(enumerate(c.versions, 1))):
311 % for data in reversed(list(enumerate(c.versions, 1))):
222 <% ver_pos = data[0] %>
312 <% ver_pos = data[0] %>
223 <% ver = data[1] %>
313 <% ver = data[1] %>
224 <% ver_pr = ver.pull_request_version_id %>
314 <% ver_pr = ver.pull_request_version_id %>
225 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
315 <% display_row = '' if c.at_version and (c.at_version_num == ver_pr or c.from_version_num == ver_pr) else 'none' %>
226
316
227 <tr class="version-pr" style="display: ${display_row}">
317 <tr class="version-pr" style="display: ${display_row}">
228 <td>
318 <td>
229 <code>
319 <code>
230 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
320 <a href="${request.current_route_path(_query=dict(version=ver_pr or 'latest'))}">v${ver_pos}</a>
231 </code>
321 </code>
232 </td>
322 </td>
233 <td>
323 <td>
234 <input ${('checked="checked"' if c.from_version_num == ver_pr else '')} class="compare-radio-button" type="radio" name="ver_source" value="${ver_pr or 'latest'}" data-ver-pos="${ver_pos}"/>
324 <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}"/>
235 <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}"/>
325 <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}"/>
236 </td>
326 </td>
237 <td>
327 <td>
238 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
328 <% review_status = c.review_versions[ver_pr].status if ver_pr in c.review_versions else 'not_reviewed' %>
239 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
329 <i class="tooltip icon-circle review-status-${review_status}" title="${_('Your review status at this version')}"></i>
240
330
241 </td>
331 </td>
242 <td>
332 <td>
243 % if c.at_version_num != ver_pr:
333 % if c.at_version_num != ver_pr:
244 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
334 <i class="tooltip icon-comment" title="${_('Comments from pull request version v{0}').format(ver_pos)}"></i>
245 <code>
335 <code>
246 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
336 General:${len(c.comment_versions[ver_pr]['at'])} / Inline:${len(c.inline_versions[ver_pr]['at'])}
247 </code>
337 </code>
248 % endif
338 % endif
249 </td>
339 </td>
250 <td>
340 <td>
251 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
341 ##<code>${ver.source_ref_parts.commit_id[:6]}</code>
252 </td>
342 </td>
253 <td>
343 <td>
254 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
344 <code>${h.age_component(ver.updated_on, time_is_local=True, tooltip=False)}</code>
255 </td>
345 </td>
256 </tr>
346 </tr>
257 % endfor
347 % endfor
258
348
259 <tr>
349 <tr>
260 <td colspan="6">
350 <td colspan="6">
261 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
351 <button id="show-version-diff" onclick="return versionController.showVersionDiff()" class="btn btn-sm" style="display: none"
262 data-label-text-locked="${_('select versions to show changes')}"
352 data-label-text-locked="${_('select versions to show changes')}"
263 data-label-text-diff="${_('show changes between versions')}"
353 data-label-text-diff="${_('show changes between versions')}"
264 data-label-text-show="${_('show pull request for this version')}"
354 data-label-text-show="${_('show pull request for this version')}"
265 >
355 >
266 ${_('select versions to show changes')}
356 ${_('select versions to show changes')}
267 </button>
357 </button>
268 </td>
358 </td>
269 </tr>
359 </tr>
270 </table>
360 </table>
271 % else:
361 % else:
272 <div>
362 <div>
273 ${_('Pull request versions not available')}.
363 ${_('Pull request versions not available')}.
274 </div>
364 </div>
275 % endif
365 % endif
276 </div>
366 </div>
277 </div>
367 </div>
278
368
279 </div>
369 </div>
280
370
281 </div>
371 </div>
282
372
283
373
284
285
286 </div>
374 </div>
287
375
288 </div>
376 </div>
289
377
290 <div class="box">
378 <div class="box">
291
379
292 % if c.state_progressing:
380 % if c.state_progressing:
293
381
294 <h2 style="text-align: center">
382 <h2 style="text-align: center">
295 ${_('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>
383 ${_('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>
296
384
297 % if c.is_super_admin:
385 % if c.is_super_admin:
298 <br/>
386 <br/>
299 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.
387 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.
300 % endif
388 % endif
301 </h2>
389 </h2>
302
390
303 % else:
391 % else:
304
392
305 ## Diffs rendered here
393 ## Diffs rendered here
306 <div class="table" >
394 <div class="table" >
307 <div id="changeset_compare_view_content">
395 <div id="changeset_compare_view_content">
308 ##CS
396 ##CS
309 % if c.missing_requirements:
397 % if c.missing_requirements:
310 <div class="box">
398 <div class="box">
311 <div class="alert alert-warning">
399 <div class="alert alert-warning">
312 <div>
400 <div>
313 <strong>${_('Missing requirements:')}</strong>
401 <strong>${_('Missing requirements:')}</strong>
314 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
402 ${_('These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled.')}
315 </div>
403 </div>
316 </div>
404 </div>
317 </div>
405 </div>
318 % elif c.missing_commits:
406 % elif c.missing_commits:
319 <div class="box">
407 <div class="box">
320 <div class="alert alert-warning">
408 <div class="alert alert-warning">
321 <div>
409 <div>
322 <strong>${_('Missing commits')}:</strong>
410 <strong>${_('Missing commits')}:</strong>
323 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}<br/>
411 ${_('This pull request cannot be displayed, because one or more commits no longer exist in the source repository.')}<br/>
324 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}<br/>
412 ${_('Please update this pull request, push the commits back into the source repository, or consider closing this pull request.')}<br/>
325 ${_('Consider doing a `force update commits` in case you think this is an error.')}
413 ${_('Consider doing a `force update commits` in case you think this is an error.')}
326 </div>
414 </div>
327 </div>
415 </div>
328 </div>
416 </div>
329 % elif c.pr_merge_source_commit.changed and not c.pull_request.is_closed():
417 % elif c.pr_merge_source_commit.changed and not c.pull_request.is_closed():
330 <div class="box">
418 <div class="box">
331 <div class="alert alert-info">
419 <div class="alert alert-info">
332 <div>
420 <div>
333 <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>
421 <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>
334 </div>
422 </div>
335 </div>
423 </div>
336 </div>
424 </div>
337 % endif
425 % endif
338
426
339 <div class="compare_view_commits_title">
427 <div class="compare_view_commits_title">
340 % if not c.compare_mode:
428 % if not c.compare_mode:
341
429
342 % if c.at_version_pos:
430 % if c.at_version_index:
343 <h4>
431 <h4>
344 ${_('Showing changes at v%d, commenting is disabled.') % c.at_version_pos}
432 ${_('Showing changes at v{}, commenting is disabled.').format(c.at_version_index)}
345 </h4>
433 </h4>
346 % endif
434 % endif
347
435
348 <div class="pull-left">
436 <div class="pull-left">
349 <div class="btn-group">
437 <div class="btn-group">
350 <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)} >
438 <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)} >
351 % if c.collapse_all_commits:
439 % if c.collapse_all_commits:
352 <i class="icon-plus-squared-alt icon-no-margin"></i>
440 <i class="icon-plus-squared-alt icon-no-margin"></i>
353 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
441 ${_ungettext('Expand {} commit', 'Expand {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
354 % else:
442 % else:
355 <i class="icon-minus-squared-alt icon-no-margin"></i>
443 <i class="icon-minus-squared-alt icon-no-margin"></i>
356 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
444 ${_ungettext('Collapse {} commit', 'Collapse {} commits', len(c.commit_ranges)).format(len(c.commit_ranges))}
357 % endif
445 % endif
358 </a>
446 </a>
359 </div>
447 </div>
360 </div>
448 </div>
361
449
362 <div class="pull-right">
450 <div class="pull-right">
363 % if c.allowed_to_update and not c.pull_request.is_closed():
451 % if c.allowed_to_update and not c.pull_request.is_closed():
364
452
365 <div class="btn-group btn-group-actions">
453 <div class="btn-group btn-group-actions">
366 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
454 <a id="update_commits" class="btn btn-primary no-margin" onclick="updateController.updateCommits(this); return false">
367 ${_('Update commits')}
455 ${_('Update commits')}
368 </a>
456 </a>
369
457
370 <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')}">
458 <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')}">
371 <i class="icon-down"></i>
459 <i class="icon-down"></i>
372 </a>
460 </a>
373
461
374 <div class="btn-action-switcher-container right-align" id="update-commits-switcher">
462 <div class="btn-action-switcher-container right-align" id="update-commits-switcher">
375 <ul class="btn-action-switcher" role="menu" style="min-width: 300px;">
463 <ul class="btn-action-switcher" role="menu" style="min-width: 300px;">
376 <li>
464 <li>
377 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
465 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
378 ${_('Force update commits')}
466 ${_('Force update commits')}
379 </a>
467 </a>
380 <div class="action-help-block">
468 <div class="action-help-block">
381 ${_('Update commits and force refresh this pull request.')}
469 ${_('Update commits and force refresh this pull request.')}
382 </div>
470 </div>
383 </li>
471 </li>
384 </ul>
472 </ul>
385 </div>
473 </div>
386 </div>
474 </div>
387
475
388 % else:
476 % else:
389 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
477 <a class="tooltip btn disabled pull-right" disabled="disabled" title="${_('Update is disabled for current view')}">${_('Update commits')}</a>
390 % endif
478 % endif
391
479
392 </div>
480 </div>
393 % endif
481 % endif
394 </div>
482 </div>
395
483
396 % if not c.missing_commits:
484 % if not c.missing_commits:
485 ## COMPARE RANGE DIFF MODE
397 % if c.compare_mode:
486 % if c.compare_mode:
398 % if c.at_version:
487 % if c.at_version:
399 <h4>
488 <h4>
400 ${_('Commits and changes between v{ver_from} and {ver_to} of this pull request, commenting is disabled').format(ver_from=c.from_version_pos, ver_to=c.at_version_pos if c.at_version_pos else 'latest')}:
489 ${_('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')}:
401 </h4>
490 </h4>
402
491
403 <div class="subtitle-compare">
492 <div class="subtitle-compare">
404 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
493 ${_('commits added: {}, removed: {}').format(len(c.commit_changes_summary.added), len(c.commit_changes_summary.removed))}
405 </div>
494 </div>
406
495
407 <div class="container">
496 <div class="container">
408 <table class="rctable compare_view_commits">
497 <table class="rctable compare_view_commits">
409 <tr>
498 <tr>
410 <th></th>
499 <th></th>
411 <th>${_('Time')}</th>
500 <th>${_('Time')}</th>
412 <th>${_('Author')}</th>
501 <th>${_('Author')}</th>
413 <th>${_('Commit')}</th>
502 <th>${_('Commit')}</th>
414 <th></th>
503 <th></th>
415 <th>${_('Description')}</th>
504 <th>${_('Description')}</th>
416 </tr>
505 </tr>
417
506
418 % for c_type, commit in c.commit_changes:
507 % for c_type, commit in c.commit_changes:
419 % if c_type in ['a', 'r']:
508 % if c_type in ['a', 'r']:
420 <%
509 <%
421 if c_type == 'a':
510 if c_type == 'a':
422 cc_title = _('Commit added in displayed changes')
511 cc_title = _('Commit added in displayed changes')
423 elif c_type == 'r':
512 elif c_type == 'r':
424 cc_title = _('Commit removed in displayed changes')
513 cc_title = _('Commit removed in displayed changes')
425 else:
514 else:
426 cc_title = ''
515 cc_title = ''
427 %>
516 %>
428 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
517 <tr id="row-${commit.raw_id}" commit_id="${commit.raw_id}" class="compare_select">
429 <td>
518 <td>
430 <div class="commit-change-indicator color-${c_type}-border">
519 <div class="commit-change-indicator color-${c_type}-border">
431 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
520 <div class="commit-change-content color-${c_type} tooltip" title="${h.tooltip(cc_title)}">
432 ${c_type.upper()}
521 ${c_type.upper()}
433 </div>
522 </div>
434 </div>
523 </div>
435 </td>
524 </td>
436 <td class="td-time">
525 <td class="td-time">
437 ${h.age_component(commit.date)}
526 ${h.age_component(commit.date)}
438 </td>
527 </td>
439 <td class="td-user">
528 <td class="td-user">
440 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
529 ${base.gravatar_with_user(commit.author, 16, tooltip=True)}
441 </td>
530 </td>
442 <td class="td-hash">
531 <td class="td-hash">
443 <code>
532 <code>
444 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
533 <a href="${h.route_path('repo_commit', repo_name=c.target_repo.repo_name, commit_id=commit.raw_id)}">
445 r${commit.idx}:${h.short_id(commit.raw_id)}
534 r${commit.idx}:${h.short_id(commit.raw_id)}
446 </a>
535 </a>
447 ${h.hidden('revisions', commit.raw_id)}
536 ${h.hidden('revisions', commit.raw_id)}
448 </code>
537 </code>
449 </td>
538 </td>
450 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
539 <td class="td-message expand_commit" data-commit-id="${commit.raw_id}" title="${_( 'Expand commit message')}" onclick="commitsController.expandCommit(this); return false">
451 <i class="icon-expand-linked"></i>
540 <i class="icon-expand-linked"></i>
452 </td>
541 </td>
453 <td class="mid td-description">
542 <td class="mid td-description">
454 <div class="log-container truncate-wrap">
543 <div class="log-container truncate-wrap">
455 <div class="message truncate" id="c-${commit.raw_id}" data-message-raw="${commit.message}">${h.urlify_commit_message(commit.message, c.repo_name)}</div>
544 <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>
456 </div>
545 </div>
457 </td>
546 </td>
458 </tr>
547 </tr>
459 % endif
548 % endif
460 % endfor
549 % endfor
461 </table>
550 </table>
462 </div>
551 </div>
463
552
464 % endif
553 % endif
465
554
555 ## Regular DIFF
466 % else:
556 % else:
467 <%include file="/compare/compare_commits.mako" />
557 <%include file="/compare/compare_commits.mako" />
468 % endif
558 % endif
469
559
470 <div class="cs_files">
560 <div class="cs_files">
471 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
561 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
472 % if c.at_version:
473 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['display']) %>
474 <% c.inline_comments_flat = c.inline_versions[c.at_version_num]['display'] %>
475 <% c.comments = c.comment_versions[c.at_version_num]['display'] %>
476 % else:
477 <% c.inline_cnt = len(c.inline_versions[c.at_version_num]['until']) %>
478 <% c.inline_comments_flat = c.inline_versions[c.at_version_num]['until'] %>
479 <% c.comments = c.comment_versions[c.at_version_num]['until'] %>
480 % endif
481
562
482 <%
563 <%
483 pr_menu_data = {
564 pr_menu_data = {
484 'outdated_comm_count_ver': outdated_comm_count_ver,
565 'outdated_comm_count_ver': outdated_comm_count_ver,
485 'pull_request': c.pull_request
566 'pull_request': c.pull_request
486 }
567 }
487 %>
568 %>
488
569
489 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on, pull_request_menu=pr_menu_data)}
570 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on, pull_request_menu=pr_menu_data)}
490
571
491 % if c.range_diff_on:
572 % if c.range_diff_on:
492 % for commit in c.commit_ranges:
573 % for commit in c.commit_ranges:
493 ${cbdiffs.render_diffset(
574 ${cbdiffs.render_diffset(
494 c.changes[commit.raw_id],
575 c.changes[commit.raw_id],
495 commit=commit, use_comments=True,
576 commit=commit, use_comments=True,
496 collapse_when_files_over=5,
577 collapse_when_files_over=5,
497 disable_new_comments=True,
578 disable_new_comments=True,
498 deleted_files_comments=c.deleted_files_comments,
579 deleted_files_comments=c.deleted_files_comments,
499 inline_comments=c.inline_comments,
580 inline_comments=c.inline_comments,
500 pull_request_menu=pr_menu_data, show_todos=False)}
581 pull_request_menu=pr_menu_data, show_todos=False)}
501 % endfor
582 % endfor
502 % else:
583 % else:
503 ${cbdiffs.render_diffset(
584 ${cbdiffs.render_diffset(
504 c.diffset, use_comments=True,
585 c.diffset, use_comments=True,
505 collapse_when_files_over=30,
586 collapse_when_files_over=30,
506 disable_new_comments=not c.allowed_to_comment,
587 disable_new_comments=not c.allowed_to_comment,
507 deleted_files_comments=c.deleted_files_comments,
588 deleted_files_comments=c.deleted_files_comments,
508 inline_comments=c.inline_comments,
589 inline_comments=c.inline_comments,
509 pull_request_menu=pr_menu_data, show_todos=False)}
590 pull_request_menu=pr_menu_data, show_todos=False)}
510 % endif
591 % endif
511
592
512 </div>
593 </div>
513 % else:
594 % else:
514 ## skipping commits we need to clear the view for missing commits
595 ## skipping commits we need to clear the view for missing commits
515 <div style="clear:both;"></div>
596 <div style="clear:both;"></div>
516 % endif
597 % endif
517
598
518 </div>
599 </div>
519 </div>
600 </div>
520
601
521 ## template for inline comment form
602 ## template for inline comment form
522 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
603 <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
523
604
524 ## comments heading with count
605 ## comments heading with count
525 <div class="comments-heading">
606 <div class="comments-heading">
526 <i class="icon-comment"></i>
607 <i class="icon-comment"></i>
527 ${_('Comments')} ${len(c.comments)}
608 ${_('General Comments')} ${len(c.comments)}
528 </div>
609 </div>
529
610
530 ## render general comments
611 ## render general comments
531 <div id="comment-tr-show">
612 <div id="comment-tr-show">
532 % if general_outdated_comm_count_ver:
613 % if general_outdated_comm_count_ver:
533 <div class="info-box">
614 <div class="info-box">
534 % if general_outdated_comm_count_ver == 1:
615 % if general_outdated_comm_count_ver == 1:
535 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
616 ${_('there is {num} general comment from older versions').format(num=general_outdated_comm_count_ver)},
536 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
617 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show it')}</a>
537 % else:
618 % else:
538 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
619 ${_('there are {num} general comments from older versions').format(num=general_outdated_comm_count_ver)},
539 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
620 <a href="#show-hidden-comments" onclick="$('.comment-general.comment-outdated').show(); $(this).parent().hide(); return false;">${_('show them')}</a>
540 % endif
621 % endif
541 </div>
622 </div>
542 % endif
623 % endif
543 </div>
624 </div>
544
625
545 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
626 ${comment.generate_comments(c.comments, include_pull_request=True, is_pull_request=True)}
546
627
547 % if not c.pull_request.is_closed():
628 % if not c.pull_request.is_closed():
548 ## main comment form and it status
629 ## main comment form and it status
549 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
630 ${comment.comments(h.route_path('pullrequest_comment_create', repo_name=c.repo_name,
550 pull_request_id=c.pull_request.pull_request_id),
631 pull_request_id=c.pull_request.pull_request_id),
551 c.pull_request_review_status,
632 c.pull_request_review_status,
552 is_pull_request=True, change_status=c.allowed_to_change_status)}
633 is_pull_request=True, change_status=c.allowed_to_change_status)}
553
634
554 ## merge status, and merge action
635 ## merge status, and merge action
555 <div class="pull-request-merge">
636 <div class="pull-request-merge">
556 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
637 <%include file="/pullrequests/pullrequest_merge_checks.mako"/>
557 </div>
638 </div>
558
639
559 %endif
640 %endif
560
641
561 % endif
642 % endif
562 </div>
643 </div>
563
644
645
646 ### NAVBOG RIGHT
647 <style>
648
649 .right-sidebar {
650 position: fixed;
651 top: 0px;
652 bottom: 0;
653 right: 0;
654
655 background: #fafafa;
656 z-index: 50;
657 }
658
659 .right-sidebar {
660 border-left: 1px solid #dbdbdb;
661 }
662
663 .right-sidebar.right-sidebar-expanded {
664 width: 320px;
665 overflow: scroll;
666 }
667
668 .right-sidebar.right-sidebar-collapsed {
669 width: 50px;
670 padding: 0;
671 display: block;
672 overflow: hidden;
673 }
674
675 .sidenav {
676 float: right;
677 will-change: min-height;
678 background: #fafafa;
679 width: 100%;
680 padding-top: 50px;
681 }
682
683 .sidebar-toggle {
684 height: 30px;
685 text-align: center;
686 margin: 15px 0px 0 0;
687 }
688 .sidebar-toggle a {
689
690 }
691
692 .sidebar-content {
693 margin-left: 15px;
694 margin-right: 15px;
695 }
696
697 .sidebar-heading {
698 font-size: 1.2em;
699 font-weight: 700;
700 margin-top: 10px;
701 }
702
703 .sidebar-element {
704 margin-top: 20px;
705 }
706 .right-sidebar-collapsed-state {
707 display: flex;
708 flex-direction: column;
709 justify-content: center;
710 align-items: center;
711 padding: 0 10px;
712 cursor: pointer;
713 font-size: 1.3em;
714 margin: 0 -15px;
715 }
716
717 .right-sidebar-collapsed-state:hover {
718 background-color: #dbd9da;
719 }
720
721 .old-comments-marker {
722 text-align: center;
723 }
724
725 .old-comments-marker td {
726 padding-top: 15px;
727 border-bottom: 1px solid #dbd9da;
728 }
729
730 #add_reviewer {
731 padding-top: 10px;
732 }
733
734 </style>
735
736 <aside class="right-sidebar right-sidebar-expanded" id="pr-nav-sticky" style="display: none">
737 <div class="sidenav navbar__inner" >
738 ## TOGGLE
739 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
740 <a href="#toggleSidebar">
741
742 </a>
743 </div>
744
745 ## CONTENT
746 <div class="sidebar-content">
747
748 ## RULES SUMMARY/RULES
749 <div class="sidebar-element clear-both">
750
751 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Reviewers')}">
752 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
753 ${len(c.allowed_reviewers)}
754 </div>
755
756 ## REVIEW RULES
757 <div id="review_rules" style="display: none" class="">
758 <div class="right-sidebar-expanded-state pr-details-title">
759 <span class="sidebar-heading">
760 ${_('Reviewer rules')}
761 </span>
762
763 </div>
764 <div class="pr-reviewer-rules">
765 ## review rules will be appended here, by default reviewers logic
766 </div>
767 <input id="review_data" type="hidden" name="review_data" value="">
768 </div>
769
770 ## REVIEWERS
771 <div class="right-sidebar-expanded-state pr-details-title">
772 <span class="tooltip sidebar-heading" title="${_ungettext('Review status calculated based on {} reviewer vote', 'Review status calculated based on {} reviewers votes', len(c.allowed_reviewers)).format(len(c.allowed_reviewers))}">
773 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
774 ${_('Reviewers')}
775 </span>
776 %if c.allowed_to_update:
777 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
778 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
779 %else:
780 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Show rules')}</span>
781 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
782 %endif
783 </div>
784
785 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
786
787 ## members redering block
788 <input type="hidden" name="__start__" value="review_members:sequence">
789
790 <table id="review_members" class="group_members">
791 ## This content is loaded via JS and ReviewersPanel
792 </table>
793
794 <input type="hidden" name="__end__" value="review_members:sequence">
795 ## end members redering block
796
797 %if not c.pull_request.is_closed():
798 <div id="add_reviewer" class="ac" style="display: none;">
799 %if c.allowed_to_update:
800 % if not c.forbid_adding_reviewers:
801 <div id="add_reviewer_input" class="reviewer_ac">
802 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
803 <div id="reviewers_container"></div>
804 </div>
805 % endif
806 <div class="pull-right">
807 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
808 </div>
809 %endif
810 </div>
811 %endif
812 </div>
813 </div>
814
815 ## ## OBSERVERS
816 ## <div class="sidebar-element clear-both">
817 ## <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Observers')}">
818 ## <i class="icon-eye"></i>
819 ## 0
820 ## </div>
821 ##
822 ## <div class="right-sidebar-expanded-state pr-details-title">
823 ## <span class="sidebar-heading">
824 ## <i class="icon-eye"></i>
825 ## ${_('Observers')}
826 ## </span>
827 ## </div>
828 ## <div class="right-sidebar-expanded-state pr-details-content">
829 ## No observers
830 ## </div>
831 ## </div>
832
833 ## TODOs
834 <div class="sidebar-element clear-both">
835 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
836 <i class="icon-flag-filled"></i>
837 <span id="todos-count">${len(c.unresolved_comments)}</span>
838 </div>
839
840 <div class="right-sidebar-expanded-state pr-details-title">
841 ## Only show unresolved, that is only what matters
842 <span class="sidebar-heading noselect" onclick="refreshTODOs(); return false">
843 <i class="icon-flag-filled"></i>
844 TODOs
845 </span>
846
847 % if not c.at_version:
848 % if c.resolved_comments:
849 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return versionController.toggleElement(this, '.resolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
850 % else:
851 <span class="block-right last-item noselect">Show resolved</span>
852 % endif
853 % endif
854 </div>
855
856 <div class="right-sidebar-expanded-state pr-details-content">
857
858 % if c.at_version:
859 <table>
860 <tr>
861 <td class="unresolved-todo-text">${_('TODOs unavailable when browsing versions')}.</td>
862 </tr>
863 </table>
864 % else:
865 % if c.unresolved_comments + c.resolved_comments:
866 ${comments_table(c.unresolved_comments + c.resolved_comments, len(c.unresolved_comments), todo_comments=True)}
867 % else:
868 <table>
869 <tr>
870 <td>
871 ${_('No TODOs yet')}
872 </td>
873 </tr>
874 </table>
875 % endif
876 % endif
877 </div>
878 </div>
879
880 ## COMMENTS
881 <div class="sidebar-element clear-both">
882 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
883 <i class="icon-comment" style="color: #949494"></i>
884 <span id="comments-count">${len(c.inline_comments_flat+c.comments)}</span>
885 </div>
886
887 <div class="right-sidebar-expanded-state pr-details-title">
888 <span class="sidebar-heading noselect" onclick="refreshComments(); return false">
889 <i class="icon-comment" style="color: #949494"></i>
890 ${_('Comments')}
891
892 ## % if outdated_comm_count_ver:
893 ## <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
894 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
895 ## </a>
896 ## <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
897 ## <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
898
899 ## % else:
900 ## (${_("{} Outdated").format(outdated_comm_count_ver)})
901 ## % endif
902
903 </span>
904
905 % if outdated_comm_count_ver:
906 <span class="block-right action_button last-item noselect" onclick="return versionController.toggleElement(this, '.hidden-comment');" data-toggle-on="Show outdated" data-toggle-off="Hide outdated">Show outdated</span>
907 % else:
908 <span class="block-right last-item noselect">Show hidden</span>
909 % endif
910
911 </div>
912
913 <div class="right-sidebar-expanded-state pr-details-content">
914 % if c.inline_comments_flat + c.comments:
915 ${comments_table(c.inline_comments_flat + c.comments, len(c.inline_comments_flat+c.comments))}
916 % else:
917 <table>
918 <tr>
919 <td>
920 ${_('No Comments yet')}
921 </td>
922 </tr>
923 </table>
924 % endif
925 </div>
926
927 </div>
928
929 ## Referenced Tickets
930 <div class="sidebar-element clear-both">
931 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Referenced Tickets')}">
932 <i class="icon-info-circled"></i>
933 ${(len(c.referenced_desc_issues) + len(c.referenced_commit_issues))}
934 </div>
935
936 <div class="right-sidebar-expanded-state pr-details-title">
937 <span class="sidebar-heading">
938 <i class="icon-info-circled"></i>
939 ${_('Referenced Tickets')}
940 </span>
941 </div>
942 <div class="right-sidebar-expanded-state pr-details-content">
943 <table>
944
945 <tr><td><code>${_('Pull Request Description')}</code></td></tr>
946 % if c.referenced_desc_issues:
947 % for ticket_dict in c.referenced_desc_issues:
948 <tr>
949 <td>
950 <a href="${ticket_dict.get('url')}">
951 ${ticket_dict.get('id')}
952 </a>
953 </td>
954 </tr>
955 % endfor
956 % else:
957 <tr>
958 <td>
959 ${_('No Ticket data found.')}
960 </td>
961 </tr>
962 % endif
963
964 <tr><td style="padding-top: 10px"><code>${_('Commit Messages')}</code></td></tr>
965 % if c.referenced_commit_issues:
966 % for ticket_dict in c.referenced_commit_issues:
967 <tr>
968 <td>
969 <a href="${ticket_dict.get('url')}">
970 ${ticket_dict.get('id')}
971 </a>
972 </td>
973 </tr>
974 % endfor
975 % else:
976 <tr>
977 <td>
978 ${_('No Ticket data found.')}
979 </td>
980 </tr>
981 % endif
982 </table>
983
984 </div>
985 </div>
986
987 </div>
988
989 </div>
990 </aside>
991
992 ## This JS needs to be at the end
564 <script type="text/javascript">
993 <script type="text/javascript">
565
994
566 versionController = new VersionController();
995 versionController = new VersionController();
567 versionController.init();
996 versionController.init();
568
997
569 reviewersController = new ReviewersController();
998 reviewersController = new ReviewersController();
570 commitsController = new CommitsController();
999 commitsController = new CommitsController();
571
1000
572 updateController = new UpdatePrController();
1001 updateController = new UpdatePrController();
573
1002
1003 /** leak object to top level scope **/
1004 window.PullRequestPresenceController;
1005
1006 (function () {
1007 "use strict";
1008
1009 window.PullRequestPresenceController = function (channel) {
1010 var self = this;
1011 this.channel = channel;
1012 this.users = {};
1013
1014 this.storeUsers = function (users) {
1015 self.users = {}
1016 $.each(users, function(index, value) {
1017 var userId = value.state.id;
1018 self.users[userId] = value.state;
1019 })
1020 }
1021
1022 this.render = function () {
1023 $.each($('.reviewer_entry'), function(index, value) {
1024 var userData = $(value).data();
1025 if(self.users[userData.reviewerUserId] !== undefined){
1026 $(value).find('.presence-state').show();
1027 } else {
1028 $(value).find('.presence-state').hide();
1029 }
1030 })
1031 };
1032
1033 this.handlePresence = function (data) {
1034
1035 if (data.type == 'presence' && data.channel === self.channel) {
1036 this.storeUsers(data.users);
1037 this.render()
1038 }
1039 };
1040
1041 this.handleChannelUpdate = function (data) {
1042
1043 if (data.channel === this.channel) {
1044 this.storeUsers(data.state.users);
1045 this.render()
1046 }
1047
1048 };
1049
1050 /* subscribe our chat to topics that are interesting to it */
1051 $.Topic('/connection_controller/channel_update').subscribe(this.handleChannelUpdate.bind(this));
1052 $.Topic('/connection_controller/presence').subscribe(this.handlePresence.bind(this));
1053 };
1054
1055 })();
1056
1057
574 $(function () {
1058 $(function () {
575
1059
576 // custom code mirror
1060 // custom code mirror
577 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
1061 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
578
1062
579 var PRDetails = {
1063 var PRDetails = {
580 editButton: $('#open_edit_pullrequest'),
1064 editButton: $('#open_edit_pullrequest'),
581 closeButton: $('#close_edit_pullrequest'),
1065 closeButton: $('#close_edit_pullrequest'),
582 deleteButton: $('#delete_pullrequest'),
1066 deleteButton: $('#delete_pullrequest'),
583 viewFields: $('#pr-desc, #pr-title'),
1067 viewFields: $('#pr-desc, #pr-title'),
584 editFields: $('#pr-desc-edit, #pr-title-edit, .pr-save'),
1068 editFields: $('#pr-desc-edit, #pr-title-edit, .pr-save'),
585
1069
586 init: function () {
1070 init: function () {
587 var that = this;
1071 var that = this;
588 this.editButton.on('click', function (e) {
1072 this.editButton.on('click', function (e) {
589 that.edit();
1073 that.edit();
590 });
1074 });
591 this.closeButton.on('click', function (e) {
1075 this.closeButton.on('click', function (e) {
592 that.view();
1076 that.view();
593 });
1077 });
594 },
1078 },
595
1079
596 edit: function (event) {
1080 edit: function (event) {
597 this.viewFields.hide();
1081 this.viewFields.hide();
598 this.editButton.hide();
1082 this.editButton.hide();
599 this.deleteButton.hide();
1083 this.deleteButton.hide();
600 this.closeButton.show();
1084 this.closeButton.show();
601 this.editFields.show();
1085 this.editFields.show();
602 codeMirrorInstance.refresh();
1086 codeMirrorInstance.refresh();
603 },
1087 },
604
1088
605 view: function (event) {
1089 view: function (event) {
606 this.editButton.show();
1090 this.editButton.show();
607 this.deleteButton.show();
1091 this.deleteButton.show();
608 this.editFields.hide();
1092 this.editFields.hide();
609 this.closeButton.hide();
1093 this.closeButton.hide();
610 this.viewFields.show();
1094 this.viewFields.show();
611 }
1095 }
612 };
1096 };
613
1097
614 var ReviewersPanel = {
1098 var ReviewersPanel = {
615 editButton: $('#open_edit_reviewers'),
1099 editButton: $('#open_edit_reviewers'),
616 closeButton: $('#close_edit_reviewers'),
1100 closeButton: $('#close_edit_reviewers'),
617 addButton: $('#add_reviewer'),
1101 addButton: $('#add_reviewer'),
618 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
1102 removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove'),
1103 reviewRules: ${c.pull_request_default_reviewers_data_json | n},
1104 setReviewers: ${c.pull_request_set_reviewers_data_json | n},
619
1105
620 init: function () {
1106 init: function () {
621 var self = this;
1107 var self = this;
622 this.editButton.on('click', function (e) {
1108 this.editButton.on('click', function (e) {
623 self.edit();
1109 self.edit();
624 });
1110 });
625 this.closeButton.on('click', function (e) {
1111 this.closeButton.on('click', function (e) {
626 self.close();
1112 self.close();
1113 self.renderReviewers();
627 });
1114 });
1115
1116 self.renderReviewers();
1117
1118 },
1119
1120 renderReviewers: function () {
1121
1122 $('#review_members').html('')
1123 $.each(this.setReviewers.reviewers, function (key, val) {
1124 var member = val;
1125
1126 var entry = renderTemplate('reviewMemberEntry', {
1127 'member': member,
1128 'mandatory': member.mandatory,
1129 'reasons': member.reasons,
1130 'allowed_to_update': member.allowed_to_update,
1131 'review_status': member.review_status,
1132 'review_status_label': member.review_status_label,
1133 'user_group': member.user_group,
1134 'create': false
1135 });
1136
1137 $('#review_members').append(entry)
1138 });
1139 tooltipActivate();
1140
628 },
1141 },
629
1142
630 edit: function (event) {
1143 edit: function (event) {
631 this.editButton.hide();
1144 this.editButton.hide();
632 this.closeButton.show();
1145 this.closeButton.show();
633 this.addButton.show();
1146 this.addButton.show();
634 this.removeButtons.css('visibility', 'visible');
1147 $(this.removeButtons.selector).css('visibility', 'visible');
635 // review rules
1148 // review rules
636 reviewersController.loadReviewRules(
1149 reviewersController.loadReviewRules(this.reviewRules);
637 ${c.pull_request.reviewer_data_json | n});
638 },
1150 },
639
1151
640 close: function (event) {
1152 close: function (event) {
641 this.editButton.show();
1153 this.editButton.show();
642 this.closeButton.hide();
1154 this.closeButton.hide();
643 this.addButton.hide();
1155 this.addButton.hide();
644 this.removeButtons.css('visibility', 'hidden');
1156 $(this.removeButtons.selector).css('visibility', 'hidden');
645 // hide review rules
1157 // hide review rules
646 reviewersController.hideReviewRules()
1158 reviewersController.hideReviewRules()
647 }
1159 }
648 };
1160 };
649
1161
650 PRDetails.init();
1162 PRDetails.init();
651 ReviewersPanel.init();
1163 ReviewersPanel.init();
652
1164
653 showOutdated = function (self) {
1165 showOutdated = function (self) {
654 $('.comment-inline.comment-outdated').show();
1166 $('.comment-inline.comment-outdated').show();
655 $('.filediff-outdated').show();
1167 $('.filediff-outdated').show();
656 $('.showOutdatedComments').hide();
1168 $('.showOutdatedComments').hide();
657 $('.hideOutdatedComments').show();
1169 $('.hideOutdatedComments').show();
658 };
1170 };
659
1171
660 hideOutdated = function (self) {
1172 hideOutdated = function (self) {
661 $('.comment-inline.comment-outdated').hide();
1173 $('.comment-inline.comment-outdated').hide();
662 $('.filediff-outdated').hide();
1174 $('.filediff-outdated').hide();
663 $('.hideOutdatedComments').hide();
1175 $('.hideOutdatedComments').hide();
664 $('.showOutdatedComments').show();
1176 $('.showOutdatedComments').show();
665 };
1177 };
666
1178
667 refreshMergeChecks = function () {
1179 refreshMergeChecks = function () {
668 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
1180 var loadUrl = "${request.current_route_path(_query=dict(merge_checks=1))}";
669 $('.pull-request-merge').css('opacity', 0.3);
1181 $('.pull-request-merge').css('opacity', 0.3);
670 $('.action-buttons-extra').css('opacity', 0.3);
1182 $('.action-buttons-extra').css('opacity', 0.3);
671
1183
672 $('.pull-request-merge').load(
1184 $('.pull-request-merge').load(
673 loadUrl, function () {
1185 loadUrl, function () {
674 $('.pull-request-merge').css('opacity', 1);
1186 $('.pull-request-merge').css('opacity', 1);
675
1187
676 $('.action-buttons-extra').css('opacity', 1);
1188 $('.action-buttons-extra').css('opacity', 1);
677 }
1189 }
678 );
1190 );
679 };
1191 };
680
1192
1193 refreshComments = function () {
1194 var params = {
1195 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1196 'repo_name': templateContext.repo_name,
1197 'version': '${request.GET.get('version', '')}',
1198 };
1199 var data = {"comments[]": ["1"]};
1200 var loadUrl = pyroutes.url('pullrequest_comments', params);
1201 var $targetElem = $('.comments-content-table');
1202 $targetElem.css('opacity', 0.3);
1203 $targetElem.load(
1204 loadUrl, data, function (responseText, textStatus, jqXHR) {
1205 if (jqXHR.status !== 200) {
1206 return false;
1207 }
1208 var $counterElem = $('#comments-count');
1209 var newCount = $(responseText).data('counter');
1210 if (newCount !== undefined) {
1211 var callback = function () {
1212 $counterElem.animate({'opacity': 1.00}, 200)
1213 $counterElem.html(newCount);
1214 };
1215 $counterElem.animate({'opacity': 0.15}, 200, callback);
1216 }
1217
1218
1219 $targetElem.css('opacity', 1);
1220 tooltipActivate();
1221 }
1222 );
1223 }
1224
1225 refreshTODOs = function () {
1226 var params = {
1227 'pull_request_id': templateContext.pull_request_data.pull_request_id,
1228 'repo_name': templateContext.repo_name,
1229 'version': '${request.GET.get('version', '')}',
1230 };
1231 var data = {"comments[]": ["1"]};
1232 var loadUrl = pyroutes.url('pullrequest_todos', params);
1233 var $targetElem = $('.todos-content-table');
1234 $targetElem.css('opacity', 0.3);
1235 $targetElem.load(
1236 loadUrl, data, function (responseText, textStatus, jqXHR) {
1237 if (jqXHR.status !== 200) {
1238 return false;
1239 }
1240 var $counterElem = $('#todos-count')
1241 var newCount = $(responseText).data('counter');
1242 if (newCount !== undefined) {
1243 var callback = function () {
1244 $counterElem.animate({'opacity': 1.00}, 200)
1245 $counterElem.html(newCount);
1246 };
1247 $counterElem.animate({'opacity': 0.15}, 200, callback);
1248 }
1249
1250 $targetElem.css('opacity', 1);
1251 tooltipActivate();
1252 }
1253 );
1254
1255 }
1256
1257 refreshAllComments = function() {
1258 refreshComments();
1259 refreshTODOs();
1260 }
1261
681 closePullRequest = function (status) {
1262 closePullRequest = function (status) {
682 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
1263 if (!confirm(_gettext('Are you sure to close this pull request without merging?'))) {
683 return false;
1264 return false;
684 }
1265 }
685 // inject closing flag
1266 // inject closing flag
686 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
1267 $('.action-buttons-extra').append('<input type="hidden" class="close-pr-input" id="close_pull_request" value="1">');
687 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
1268 $(generalCommentForm.statusChange).select2("val", status).trigger('change');
688 $(generalCommentForm.submitForm).submit();
1269 $(generalCommentForm.submitForm).submit();
689 };
1270 };
690
1271
691 $('#show-outdated-comments').on('click', function (e) {
1272 $('#show-outdated-comments').on('click', function (e) {
692 var button = $(this);
1273 var button = $(this);
693 var outdated = $('.comment-outdated');
1274 var outdated = $('.comment-outdated');
694
1275
695 if (button.html() === "(Show)") {
1276 if (button.html() === "(Show)") {
696 button.html("(Hide)");
1277 button.html("(Hide)");
697 outdated.show();
1278 outdated.show();
698 } else {
1279 } else {
699 button.html("(Show)");
1280 button.html("(Show)");
700 outdated.hide();
1281 outdated.hide();
701 }
1282 }
702 });
1283 });
703
1284
704 $('.show-inline-comments').on('change', function (e) {
1285 $('.show-inline-comments').on('change', function (e) {
705 var show = 'none';
1286 var show = 'none';
706 var target = e.currentTarget;
1287 var target = e.currentTarget;
707 if (target.checked) {
1288 if (target.checked) {
708 show = ''
1289 show = ''
709 }
1290 }
710 var boxid = $(target).attr('id_for');
1291 var boxid = $(target).attr('id_for');
711 var comments = $('#{0} .inline-comments'.format(boxid));
1292 var comments = $('#{0} .inline-comments'.format(boxid));
712 var fn_display = function (idx) {
1293 var fn_display = function (idx) {
713 $(this).css('display', show);
1294 $(this).css('display', show);
714 };
1295 };
715 $(comments).each(fn_display);
1296 $(comments).each(fn_display);
716 var btns = $('#{0} .inline-comments-button'.format(boxid));
1297 var btns = $('#{0} .inline-comments-button'.format(boxid));
717 $(btns).each(fn_display);
1298 $(btns).each(fn_display);
718 });
1299 });
719
1300
720 $('#merge_pull_request_form').submit(function () {
1301 $('#merge_pull_request_form').submit(function () {
721 if (!$('#merge_pull_request').attr('disabled')) {
1302 if (!$('#merge_pull_request').attr('disabled')) {
722 $('#merge_pull_request').attr('disabled', 'disabled');
1303 $('#merge_pull_request').attr('disabled', 'disabled');
723 }
1304 }
724 return true;
1305 return true;
725 });
1306 });
726
1307
727 $('#edit_pull_request').on('click', function (e) {
1308 $('#edit_pull_request').on('click', function (e) {
728 var title = $('#pr-title-input').val();
1309 var title = $('#pr-title-input').val();
729 var description = codeMirrorInstance.getValue();
1310 var description = codeMirrorInstance.getValue();
730 var renderer = $('#pr-renderer-input').val();
1311 var renderer = $('#pr-renderer-input').val();
731 editPullRequest(
1312 editPullRequest(
732 "${c.repo_name}", "${c.pull_request.pull_request_id}",
1313 "${c.repo_name}", "${c.pull_request.pull_request_id}",
733 title, description, renderer);
1314 title, description, renderer);
734 });
1315 });
735
1316
736 $('#update_pull_request').on('click', function (e) {
1317 $('#update_pull_request').on('click', function (e) {
737 $(this).attr('disabled', 'disabled');
1318 $(this).attr('disabled', 'disabled');
738 $(this).addClass('disabled');
1319 $(this).addClass('disabled');
739 $(this).html(_gettext('Saving...'));
1320 $(this).html(_gettext('Saving...'));
740 reviewersController.updateReviewers(
1321 reviewersController.updateReviewers(
741 "${c.repo_name}", "${c.pull_request.pull_request_id}");
1322 "${c.repo_name}", "${c.pull_request.pull_request_id}");
742 });
1323 });
743
1324
744
1325
745 // fixing issue with caches on firefox
1326 // fixing issue with caches on firefox
746 $('#update_commits').removeAttr("disabled");
1327 $('#update_commits').removeAttr("disabled");
747
1328
748 $('.show-inline-comments').on('click', function (e) {
1329 $('.show-inline-comments').on('click', function (e) {
749 var boxid = $(this).attr('data-comment-id');
1330 var boxid = $(this).attr('data-comment-id');
750 var button = $(this);
1331 var button = $(this);
751
1332
752 if (button.hasClass("comments-visible")) {
1333 if (button.hasClass("comments-visible")) {
753 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
1334 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
754 $(this).hide();
1335 $(this).hide();
755 });
1336 });
756 button.removeClass("comments-visible");
1337 button.removeClass("comments-visible");
757 } else {
1338 } else {
758 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
1339 $('#{0} .inline-comments'.format(boxid)).each(function (index) {
759 $(this).show();
1340 $(this).show();
760 });
1341 });
761 button.addClass("comments-visible");
1342 button.addClass("comments-visible");
762 }
1343 }
763 });
1344 });
764
1345
765 // register submit callback on commentForm form to track TODOs
1346 // register submit callback on commentForm form to track TODOs
766 window.commentFormGlobalSubmitSuccessCallback = function () {
1347 window.commentFormGlobalSubmitSuccessCallback = function () {
767 refreshMergeChecks();
1348 refreshMergeChecks();
768 };
1349 };
769
1350
770 ReviewerAutoComplete('#user');
1351 ReviewerAutoComplete('#user');
771
1352
772 })
1353 })
773
1354
774 </script>
1355 $(document).ready(function () {
775
776
777 ### NAVBOG RIGHT
778 <style>
779
780 .right-sidebar {
781 position: fixed;
782 top: 0px;
783 bottom: 0;
784 right: 0;
785
786 background: #fafafa;
787 z-index: 200;
788 }
789
790 .right-sidebar {
791 border-left: 1px solid #dbdbdb;
792 }
793
794 .right-sidebar.right-sidebar-expanded {
795 width: 320px;
796 overflow: scroll;
797 }
798
799 .right-sidebar.right-sidebar-collapsed {
800 width: 62px;
801 padding: 0;
802 display: block;
803 overflow: hidden;
804 }
805
806 .sidenav {
807 float: right;
808 will-change: min-height;
809 background: #fafafa;
810 width: 100%;
811 }
812
813 .sidebar-toggle {
814 height: 30px;
815 text-align: center;
816 margin: 15px 0 0 0;
817 }
818 .sidebar-toggle a {
819
820 }
821
822 .sidebar-content {
823 margin-left: 5px;
824 margin-right: 5px;
825 }
826
827 .sidebar-heading {
828 font-size: 1.2em;
829 font-weight: 700;
830 margin-top: 10px;
831 }
832
833 .sidebar-element {
834 margin-top: 20px;
835 }
836 .right-sidebar-collapsed-state {
837 display: flex;
838 flex-direction: column;
839 justify-content: center;
840 align-items: center;
841 padding: 0 10px;
842 cursor: pointer;
843 font-size: 1.3em;
844 margin: 0 -10px;
845 }
846
847 .right-sidebar-collapsed-state:hover {
848 background-color: #dbd9da;
849 }
850
851 .navbar__inner {
852 height: 100%;
853 background: #fafafa;
854 position: relative;
855 }
856
857 </style>
858
859
860
861 <aside class="right-sidebar right-sidebar-expanded">
862 <div class="sidenav">
863 ## TOGGLE
864 <div class="sidebar-toggle" onclick="toggleSidebar(); return false">
865 <a href="#toggleSidebar">
866
867 </a>
868 </div>
869
870 ## CONTENT
871 <div class="sidebar-content">
872
1356
873 ## RULES SUMMARY/RULES
1357 var $sideBar = $('.right-sidebar');
874 <div class="sidebar-element clear-both">
1358 var marginExpVal = '320'
875
1359 var marginColVal = '50'
876 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Reviewers')}">
1360 var marginExpanded = {'margin': '0 {0}px 0 0'.format(marginExpVal)};
877 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
1361 var marginCollapsed = {'margin': '0 {0}px 0 0'.format(marginColVal)};
878 <br/>${len(c.pull_request_reviewers)}
1362 var marginExpandedHeader = {'margin': '0 -{0}px 0 0'.format(marginExpVal), 'z-index': 10000};
879 </div>
1363 var marginCollapsedHeader = {'margin': '0 -{0}px 0 0'.format(marginColVal), 'z-index': 10000};
880
881 ## REVIEW RULES
882 <div id="review_rules" style="display: none" class="">
883 <div class="pr-details-title">
884 <span class="sidebar-heading">
885 ${_('Reviewer rules')}
886 </span>
887
888 </div>
889 <div class="pr-reviewer-rules">
890 ## review rules will be appended here, by default reviewers logic
891 </div>
892 <input id="review_data" type="hidden" name="review_data" value="">
893 </div>
894
895 ## REVIEWERS
896 <div class="right-sidebar-expanded-state pr-details-title">
897 <span class="sidebar-heading">
898 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
899 ${_('Reviewers')} - ${len(c.pull_request_reviewers)}
900 </span>
901 %if c.allowed_to_update:
902 <span id="open_edit_reviewers" class="block-right action_button last-item">${_('Edit')}</span>
903 <span id="close_edit_reviewers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
904 %endif
905 </div>
906 <div id="reviewers" class="right-sidebar-expanded-state pr-details-content reviewers">
907
908 ## members redering block
909 <input type="hidden" name="__start__" value="review_members:sequence">
910 <ul id="review_members" class="group_members">
911
912 % for review_obj, member, reasons, mandatory, status in c.pull_request_reviewers:
913 <script>
914 var member = ${h.json.dumps(h.reviewer_as_json(member, reasons=reasons, mandatory=mandatory, user_group=review_obj.rule_user_group_data()))|n};
915 var status = "${(status[0][1].status if status else 'not_reviewed')}";
916 var status_lbl = "${h.commit_status_lbl(status[0][1].status if status else 'not_reviewed')}";
917 var allowed_to_update = ${h.json.dumps(c.allowed_to_update)};
918
919 var entry = renderTemplate('reviewMemberEntry', {
920 'member': member,
921 'mandatory': member.mandatory,
922 'reasons': member.reasons,
923 'allowed_to_update': allowed_to_update,
924 'review_status': status,
925 'review_status_label': status_lbl,
926 'user_group': member.user_group,
927 'create': false
928 });
929 $('#review_members').append(entry)
930 </script>
931
932 % endfor
933
934 </ul>
935
936 <input type="hidden" name="__end__" value="review_members:sequence">
937 ## end members redering block
938
939 %if not c.pull_request.is_closed():
940 <div id="add_reviewer" class="ac" style="display: none;">
941 %if c.allowed_to_update:
942 % if not c.forbid_adding_reviewers:
943 <div id="add_reviewer_input" class="reviewer_ac">
944 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
945 <div id="reviewers_container"></div>
946 </div>
947 % endif
948 <div class="pull-right">
949 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
950 </div>
951 %endif
952 </div>
953 %endif
954 </div>
955 </div>
956
957 ## OBSERVERS
958 <div class="sidebar-element clear-both">
959 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Observers')}">
960 <i class="icon-eye"></i>
961 <br/> 0
962 </div>
963
964 <div class="right-sidebar-expanded-state pr-details-title">
965 <span class="sidebar-heading">
966 <i class="icon-eye"></i>
967 ${_('Observers')}
968 </span>
969 </div>
970 <div class="right-sidebar-expanded-state pr-details-content">
971 No observers - 0
972 </div>
973 </div>
974
1364
975 ## TODOs
1365 var updateStickyHeader = function() {
976 <div class="sidebar-element clear-both">
1366 if (window.updateSticky !== undefined) {
977 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="TODOs">
1367 // potentially our comments change the active window size, so we
978 <i class="icon-flag-filled"></i>
1368 // notify sticky elements
979 <br/> ${len(c.unresolved_comments)}
1369 updateSticky()
980 </div>
1370 }
981
1371 }
982 ## TODOs will be listed here
983 <div class="right-sidebar-expanded-state pr-details-title">
984 ## Only show unresolved, that is only what matters
985 <span class="sidebar-heading">
986 <i class="icon-flag-filled"></i>
987 TODOs - ${len(c.unresolved_comments)}
988 ##/ ${(len(c.unresolved_comments) + len(c.resolved_comments))}
989 </span>
990
991 % if not c.at_version:
992 % if c.resolved_comments:
993 <span class="block-right action_button last-item noselect" onclick="$('.unresolved-todo-text').toggle(); return versionController.toggleElement(this, '.unresolved-todo');" data-toggle-on="Show resolved" data-toggle-off="Hide resolved">Show resolved</span>
994 % else:
995 <span class="block-right last-item noselect">Show resolved</span>
996 % endif
997 % endif
998 </div>
999
1000 <div class="right-sidebar-expanded-state pr-details-content">
1001
1002 <table class="todo-table">
1003 <%
1004 def sorter(entry):
1005 user_id = entry.author.user_id
1006 resolved = '1' if entry.resolved else '0'
1007 if user_id == c.rhodecode_user.user_id:
1008 # own comments first
1009 user_id = 0
1010 return '{}_{}_{}'.format(resolved, user_id, str(entry.comment_id).zfill(100))
1011 %>
1012
1013 % if c.at_version:
1014 <tr>
1015 <td class="unresolved-todo-text">${_('unresolved TODOs unavailable in this view')}.</td>
1016 </tr>
1017 % else:
1018 % for todo_comment in sorted(c.unresolved_comments + c.resolved_comments, key=sorter):
1019 <% resolved = todo_comment.resolved %>
1020 % if inline:
1021 <% outdated_at_ver = todo_comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
1022 % else:
1023 <% outdated_at_ver = todo_comment.older_than_version(getattr(c, 'at_version_num', None)) %>
1024 % endif
1025
1026 <tr ${('class="unresolved-todo" style="display: none"' if resolved else '') |n}>
1027
1028 <td class="td-todo-number">
1029 % if resolved:
1030 <a class="permalink todo-resolved tooltip" title="${_('Resolved by comment #{}').format(todo_comment.resolved.comment_id)}" href="#comment-${todo_comment.comment_id}" onclick="return Rhodecode.comments.scrollToComment($('#comment-${todo_comment.comment_id}'), 0, ${h.json.dumps(outdated_at_ver)})">
1031 <i class="icon-flag-filled"></i> ${todo_comment.comment_id}</a>
1032 % else:
1033 <a class="permalink" href="#comment-${todo_comment.comment_id}" onclick="return Rhodecode.comments.scrollToComment($('#comment-${todo_comment.comment_id}'), 0, ${h.json.dumps(outdated_at_ver)})">
1034 <i class="icon-flag-filled"></i> ${todo_comment.comment_id}</a>
1035 % endif
1036 </td>
1037 <td class="td-todo-gravatar">
1038 ${base.gravatar(todo_comment.author.email, 16, user=todo_comment.author, tooltip=True, extra_class=['no-margin'])}
1039 </td>
1040 <td class="todo-comment-text-wrapper">
1041 <div class="todo-comment-text">
1042 <code>${h.chop_at_smart(todo_comment.text, '\n', suffix_if_chopped='...')}</code>
1043 </div>
1044 </td>
1045
1046 </tr>
1047 % endfor
1048
1049 % if len(c.unresolved_comments) == 0:
1050 <tr>
1051 <td class="unresolved-todo-text">${_('No unresolved TODOs')}.</td>
1052 </tr>
1053 % endif
1054
1055 % endif
1056
1057 </table>
1058
1059 </div>
1060 </div>
1061
1062 ## COMMENTS
1063 <div class="sidebar-element clear-both">
1064 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Comments')}">
1065 <i class="icon-comment" style="color: #949494"></i>
1066 <br/> ${len(c.inline_comments_flat+c.comments)}
1067 </div>
1068
1069 <div class="right-sidebar-expanded-state pr-details-title">
1070 <span class="sidebar-heading">
1071 <i class="icon-comment" style="color: #949494"></i>
1072 ${_('Comments')} - ${len(c.inline_comments_flat+c.comments)}
1073 ##${_ungettext("{} General", "{} General", len(c.comments)).format(len(c.comments))} /
1074 ##${_ungettext("{} Inline", "{} Inline", c.inline_cnt).format(len(c.inline_comments_flat))}
1075
1372
1076 ## TODO check why this ins't working
1373 var expandSidebar = function() {
1077 % if pull_request_menu:
1374 var $sideBar = $('.right-sidebar');
1078 <%
1375 $('.outerwrapper').css(marginExpanded);
1079 outdated_comm_count_ver = pull_request_menu['outdated_comm_count_ver']
1376 $('.header').css(marginExpandedHeader);
1080 %>
1377 $('.sidebar-toggle a').html('<i class="icon-right" style="margin-right: -10px"></i><i class="icon-right"></i>');
1081
1378 $('.right-sidebar-collapsed-state').hide();
1082 % if outdated_comm_count_ver:
1379 $('.right-sidebar-expanded-state').show();
1083 <a href="#" onclick="showOutdated(); Rhodecode.comments.nextOutdatedComment(); return false;">
1084 (${_("{} Outdated").format(outdated_comm_count_ver)})
1085 </a>
1086 <a href="#" class="showOutdatedComments" onclick="showOutdated(this); return false;"> | ${_('show outdated')}</a>
1087 <a href="#" class="hideOutdatedComments" style="display: none" onclick="hideOutdated(this); return false;"> | ${_('hide outdated')}</a>
1088 % else:
1089 (${_("{} Outdated").format(outdated_comm_count_ver)})
1090 % endif
1091
1092 % endif
1093 </span>
1094 <span class="block-right action_button last-item noselect" onclick="return versionController.toggleElement(this, '.hidden-comment');" data-toggle-on="Show all" data-toggle-off="Hide all">Show all</span>
1095 </div>
1096
1097 <div class="right-sidebar-expanded-state pr-details-content">
1098 <table class="todo-table">
1099 <%
1100 def sorter(entry):
1101 user_id = entry.author.user_id
1102 return '{}'.format(str(entry.comment_id).zfill(100))
1103 %>
1104
1105 % for comment_obj in reversed(sorted(c.inline_comments_flat + c.comments, key=sorter)):
1106 <%
1107 display = ''
1108 _cls = ''
1109 %>
1110 ## SKIP TODOs we display them above
1111 % if comment_obj.is_todo:
1112 <% display = 'none' %>
1113 % endif
1114
1115 ## Skip outdated comments
1116 % if comment_obj.outdated:
1117 <% display = 'none' %>
1118 <% _cls = 'hidden-comment' %>
1119 % endif
1120
1380
1121 <tr class="${_cls}" style="display: ${display}">
1381 $sideBar.addClass('right-sidebar-expanded')
1122 <td class="td-todo-number">
1382 $sideBar.removeClass('right-sidebar-collapsed')
1123 <a class="permalink" href="#comment-${comment_obj.comment_id}" onclick="return Rhodecode.comments.scrollToComment($('#comment-${comment_obj.comment_id}'), 0, ${comment_obj.outdated_js})">
1383 }
1124 % if comment_obj.outdated:
1125 <i class="tooltip icon-comment-toggle" title="Outdated"></i>
1126 % elif comment_obj.is_inline:
1127 <i class="tooltip icon-code" title="Inline"></i>
1128 % else:
1129 <i class="tooltip icon-comment" title="General"></i>
1130 % endif
1131 ${comment_obj.comment_id}
1132 </a>
1133 </td>
1134 <td class="td-todo-gravatar">
1135 ${base.gravatar(comment_obj.author.email, 16, user=comment_obj.author, tooltip=True, extra_class=['no-margin'])}
1136 </td>
1137 <td class="todo-comment-text-wrapper">
1138 <div class="todo-comment-text">
1139 <code>${h.chop_at_smart(comment_obj.text, '\n', suffix_if_chopped='...')}</code>
1140 </div>
1141 </td>
1142 </tr>
1143 % endfor
1144
1145 </table>
1146 </div>
1147
1148 </div>
1149 </div>
1150
1384
1151 </div>
1385 var collapseSidebar = function() {
1152 </aside>
1386 var $sideBar = $('.right-sidebar');
1153
1154
1155 <script>
1156 var $sideBar = $('.right-sidebar');
1157 var marginExpanded = {'margin': '0 320px 0 0'};
1158 var marginCollapsed = {'margin': '0 50px 0 0'};
1159
1160 if($sideBar.hasClass('right-sidebar-expanded')) {
1161 $('.outerwrapper').css(marginExpanded);
1162 $('.sidebar-toggle a').html('<i class="icon-right" style="margin-right: -10px"></i><i class="icon-right"></i>');
1163 $('.right-sidebar-collapsed-state').hide();
1164 $('.right-sidebar-expanded-state').show();
1165 updateSticky()
1166
1167 } else {
1168 $('.outerwrapper').css(marginCollapsed);
1169 $('.sidebar-toggle a').html('<i class="icon-left" style="margin-right: -10px"></i><i class="icon-left"></i>');
1170 $('.right-sidebar-collapsed-state').hide();
1171 $('.right-sidebar-expanded-state').show();
1172 updateSticky()
1173 }
1174
1175 var toggleSidebar = function(){
1176 var $sideBar = $('.right-sidebar');
1177
1178 if($sideBar.hasClass('right-sidebar-expanded')) {
1179 // collapse now
1180 $sideBar.removeClass('right-sidebar-expanded')
1181 $sideBar.addClass('right-sidebar-collapsed')
1182 $('.outerwrapper').css(marginCollapsed);
1387 $('.outerwrapper').css(marginCollapsed);
1388 $('.header').css(marginCollapsedHeader);
1183 $('.sidebar-toggle a').html('<i class="icon-left" style="margin-right: -10px"></i><i class="icon-left"></i>');
1389 $('.sidebar-toggle a').html('<i class="icon-left" style="margin-right: -10px"></i><i class="icon-left"></i>');
1184 $('.right-sidebar-collapsed-state').show();
1390 $('.right-sidebar-collapsed-state').show();
1185 $('.right-sidebar-expanded-state').hide();
1391 $('.right-sidebar-expanded-state').hide();
1186
1392
1187 } else {
1393 $sideBar.removeClass('right-sidebar-expanded')
1188 // expand now
1394 $sideBar.addClass('right-sidebar-collapsed')
1189 $('.outerwrapper').css(marginExpanded);
1395 }
1190 $sideBar.addClass('right-sidebar-expanded')
1396
1191 $sideBar.removeClass('right-sidebar-collapsed')
1397 toggleSidebar = function () {
1192 $('.sidebar-toggle a').html('<i class="icon-right" style="margin-right: -10px"></i><i class="icon-right"></i>');
1398 var $sideBar = $('.right-sidebar');
1193 $('.right-sidebar-collapsed-state').hide();
1399
1194 $('.right-sidebar-expanded-state').show();
1400 if ($sideBar.hasClass('right-sidebar-expanded')) {
1401 // expanded -> collapsed transition
1402 collapseSidebar();
1403 var sidebarState = 'collapsed';
1404
1405 } else {
1406 // collapsed -> expanded
1407 expandSidebar();
1408 var sidebarState = 'expanded';
1409 }
1410
1411 // update our other sticky header in same context
1412 updateStickyHeader();
1413 storeUserSessionAttr('rc_user_session_attr.sidebarState', sidebarState);
1195 }
1414 }
1196
1415
1197 // update our other sticky header in same context
1416 var expanded = $sideBar.hasClass('right-sidebar-expanded');
1198 updateSticky()
1199 }
1200
1417
1201 var sidebarElement = document.getElementById('pr-nav-sticky');
1418 if (templateContext.session_attrs.sidebarState === 'expanded') {
1419 expanded = true
1420 } else if (templateContext.session_attrs.sidebarState === 'collapsed') {
1421 expanded = false
1422 }
1423
1424 // show sidebar since it's hidden on load
1425 $('.right-sidebar').show();
1202
1426
1203 ## sidebar = new StickySidebar(sidebarElement, {
1427 // init based on set initial class, or if defined user session attrs
1204 ## containerSelector: '.main',
1428 if (expanded) {
1205 ## minWidth: 62,
1429 expandSidebar();
1206 ## innerWrapperSelector: '.navbar__inner',
1430 updateStickyHeader();
1207 ## stickyClass: 'is-sticky',
1208 ## });
1209
1431
1210 </script>
1432 } else {
1433 collapseSidebar();
1434 updateStickyHeader();
1435 }
1436 var channel = '${c.pr_broadcast_channel}';
1437 new PullRequestPresenceController(channel)
1438
1439 })
1440 </script>
1441
1211 </%def>
1442 </%def>
General Comments 0
You need to be logged in to leave comments. Login now