##// END OF EJS Templates
comments: added support for adding comment attachments using the artifacts logic....
bart -
r3973:d9b26077 default
parent child Browse files
Show More

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

@@ -1,231 +1,240 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import time
23 23 import shutil
24 24 import hashlib
25 25
26 26 from rhodecode.lib.ext_json import json
27 27 from rhodecode.apps.file_store import utils
28 28 from rhodecode.apps.file_store.extensions import resolve_extensions
29 from rhodecode.apps.file_store.exceptions import FileNotAllowedException
29 from rhodecode.apps.file_store.exceptions import (
30 FileNotAllowedException, FileOverSizeException)
30 31
31 32 METADATA_VER = 'v1'
32 33
33 34
34 35 class LocalFileStorage(object):
35 36
36 37 @classmethod
37 38 def resolve_name(cls, name, directory):
38 39 """
39 40 Resolves a unique name and the correct path. If a filename
40 41 for that path already exists then a numeric prefix with values > 0 will be
41 42 added, for example test.jpg -> 1-test.jpg etc. initially file would have 0 prefix.
42 43
43 44 :param name: base name of file
44 45 :param directory: absolute directory path
45 46 """
46 47
47 48 counter = 0
48 49 while True:
49 50 name = '%d-%s' % (counter, name)
50 51
51 52 # sub_store prefix to optimize disk usage, e.g some_path/ab/final_file
52 53 sub_store = cls._sub_store_from_filename(name)
53 54 sub_store_path = os.path.join(directory, sub_store)
54 55 if not os.path.exists(sub_store_path):
55 56 os.makedirs(sub_store_path)
56 57
57 58 path = os.path.join(sub_store_path, name)
58 59 if not os.path.exists(path):
59 60 return name, path
60 61 counter += 1
61 62
62 63 @classmethod
63 64 def _sub_store_from_filename(cls, filename):
64 65 return filename[:2]
65 66
66 67 @classmethod
67 68 def calculate_path_hash(cls, file_path):
68 69 """
69 70 Efficient calculation of file_path sha256 sum
70 71
71 72 :param file_path:
72 73 :return: sha256sum
73 74 """
74 75 digest = hashlib.sha256()
75 76 with open(file_path, 'rb') as f:
76 77 for chunk in iter(lambda: f.read(1024 * 100), b""):
77 78 digest.update(chunk)
78 79
79 80 return digest.hexdigest()
80 81
81 82 def __init__(self, base_path, extension_groups=None):
82 83
83 84 """
84 85 Local file storage
85 86
86 87 :param base_path: the absolute base path where uploads are stored
87 88 :param extension_groups: extensions string
88 89 """
89 90
90 91 extension_groups = extension_groups or ['any']
91 92 self.base_path = base_path
92 93 self.extensions = resolve_extensions([], groups=extension_groups)
93 94
94 95 def __repr__(self):
95 96 return '{}@{}'.format(self.__class__, self.base_path)
96 97
97 98 def store_path(self, filename):
98 99 """
99 100 Returns absolute file path of the filename, joined to the
100 101 base_path.
101 102
102 103 :param filename: base name of file
103 104 """
104 105 sub_store = self._sub_store_from_filename(filename)
105 106 return os.path.join(self.base_path, sub_store, filename)
106 107
107 108 def delete(self, filename):
108 109 """
109 110 Deletes the filename. Filename is resolved with the
110 111 absolute path based on base_path. If file does not exist,
111 112 returns **False**, otherwise **True**
112 113
113 114 :param filename: base name of file
114 115 """
115 116 if self.exists(filename):
116 117 os.remove(self.store_path(filename))
117 118 return True
118 119 return False
119 120
120 121 def exists(self, filename):
121 122 """
122 123 Checks if file exists. Resolves filename's absolute
123 124 path based on base_path.
124 125
125 126 :param filename: base name of file
126 127 """
127 128 return os.path.exists(self.store_path(filename))
128 129
129 130 def filename_allowed(self, filename, extensions=None):
130 131 """Checks if a filename has an allowed extension
131 132
132 133 :param filename: base name of file
133 134 :param extensions: iterable of extensions (or self.extensions)
134 135 """
135 136 _, ext = os.path.splitext(filename)
136 137 return self.extension_allowed(ext, extensions)
137 138
138 139 def extension_allowed(self, ext, extensions=None):
139 140 """
140 141 Checks if an extension is permitted. Both e.g. ".jpg" and
141 142 "jpg" can be passed in. Extension lookup is case-insensitive.
142 143
143 144 :param ext: extension to check
144 145 :param extensions: iterable of extensions to validate against (or self.extensions)
145 146 """
146 147 def normalize_ext(_ext):
147 148 if _ext.startswith('.'):
148 149 _ext = _ext[1:]
149 150 return _ext.lower()
150 151
151 152 extensions = extensions or self.extensions
152 153 if not extensions:
153 154 return True
154 155
155 156 ext = normalize_ext(ext)
156 157
157 158 return ext in [normalize_ext(x) for x in extensions]
158 159
159 160 def save_file(self, file_obj, filename, directory=None, extensions=None,
160 extra_metadata=None, **kwargs):
161 extra_metadata=None, max_filesize=None, **kwargs):
161 162 """
162 163 Saves a file object to the uploads location.
163 164 Returns the resolved filename, i.e. the directory +
164 165 the (randomized/incremented) base name.
165 166
166 167 :param file_obj: **cgi.FieldStorage** object (or similar)
167 168 :param filename: original filename
168 169 :param directory: relative path of sub-directory
169 170 :param extensions: iterable of allowed extensions, if not default
171 :param max_filesize: maximum size of file that should be allowed
170 172 :param extra_metadata: extra JSON metadata to store next to the file with .meta suffix
173
171 174 """
172 175
173 176 extensions = extensions or self.extensions
174 177
175 178 if not self.filename_allowed(filename, extensions):
176 179 raise FileNotAllowedException()
177 180
178 181 if directory:
179 182 dest_directory = os.path.join(self.base_path, directory)
180 183 else:
181 184 dest_directory = self.base_path
182 185
183 186 if not os.path.exists(dest_directory):
184 187 os.makedirs(dest_directory)
185 188
186 189 filename = utils.uid_filename(filename)
187 190
188 191 # resolve also produces special sub-dir for file optimized store
189 192 filename, path = self.resolve_name(filename, dest_directory)
190 193 stored_file_dir = os.path.dirname(path)
191 194
192 195 file_obj.seek(0)
193 196
194 197 with open(path, "wb") as dest:
195 198 shutil.copyfileobj(file_obj, dest)
196 199
197 200 metadata = {}
198 201 if extra_metadata:
199 202 metadata = extra_metadata
200 203
201 204 size = os.stat(path).st_size
205
206 if max_filesize and size > max_filesize:
207 # free up the copied file, and raise exc
208 os.remove(path)
209 raise FileOverSizeException()
210
202 211 file_hash = self.calculate_path_hash(path)
203 212
204 213 metadata.update(
205 214 {"filename": filename,
206 215 "size": size,
207 216 "time": time.time(),
208 217 "sha256": file_hash,
209 218 "meta_ver": METADATA_VER})
210 219
211 220 filename_meta = filename + '.meta'
212 221 with open(os.path.join(stored_file_dir, filename_meta), "wb") as dest_meta:
213 222 dest_meta.write(json.dumps(metadata))
214 223
215 224 if directory:
216 225 filename = os.path.join(directory, filename)
217 226
218 227 return filename, metadata
219 228
220 229 def get_metadata(self, filename):
221 230 """
222 231 Reads JSON stored metadata for a file
223 232
224 233 :param filename:
225 234 :return:
226 235 """
227 236 filename = self.store_path(filename)
228 237 filename_meta = filename + '.meta'
229 238
230 239 with open(filename_meta, "rb") as source_meta:
231 240 return json.loads(source_meta.read())
@@ -1,512 +1,516 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20 from rhodecode.apps._base import add_route_with_slash
21 21
22 22
23 23 def includeme(config):
24 24
25 25 # repo creating checks, special cases that aren't repo routes
26 26 config.add_route(
27 27 name='repo_creating',
28 28 pattern='/{repo_name:.*?[^/]}/repo_creating')
29 29
30 30 config.add_route(
31 31 name='repo_creating_check',
32 32 pattern='/{repo_name:.*?[^/]}/repo_creating_check')
33 33
34 34 # Summary
35 35 # NOTE(marcink): one additional route is defined in very bottom, catch
36 36 # all pattern
37 37 config.add_route(
38 38 name='repo_summary_explicit',
39 39 pattern='/{repo_name:.*?[^/]}/summary', repo_route=True)
40 40 config.add_route(
41 41 name='repo_summary_commits',
42 42 pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
43 43
44 44 # Commits
45 45 config.add_route(
46 46 name='repo_commit',
47 47 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
48 48
49 49 config.add_route(
50 50 name='repo_commit_children',
51 51 pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
52 52
53 53 config.add_route(
54 54 name='repo_commit_parents',
55 55 pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
56 56
57 57 config.add_route(
58 58 name='repo_commit_raw',
59 59 pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
60 60
61 61 config.add_route(
62 62 name='repo_commit_patch',
63 63 pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
64 64
65 65 config.add_route(
66 66 name='repo_commit_download',
67 67 pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
68 68
69 69 config.add_route(
70 70 name='repo_commit_data',
71 71 pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
72 72
73 73 config.add_route(
74 74 name='repo_commit_comment_create',
75 75 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
76 76
77 77 config.add_route(
78 78 name='repo_commit_comment_preview',
79 79 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
80 80
81 81 config.add_route(
82 name='repo_commit_comment_attachment_upload',
83 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/attachment_upload', repo_route=True)
84
85 config.add_route(
82 86 name='repo_commit_comment_delete',
83 87 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
84 88
85 89 # still working url for backward compat.
86 90 config.add_route(
87 91 name='repo_commit_raw_deprecated',
88 92 pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
89 93
90 94 # Files
91 95 config.add_route(
92 96 name='repo_archivefile',
93 97 pattern='/{repo_name:.*?[^/]}/archive/{fname:.*}', repo_route=True)
94 98
95 99 config.add_route(
96 100 name='repo_files_diff',
97 101 pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
98 102 config.add_route( # legacy route to make old links work
99 103 name='repo_files_diff_2way_redirect',
100 104 pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
101 105
102 106 config.add_route(
103 107 name='repo_files',
104 108 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
105 109 config.add_route(
106 110 name='repo_files:default_path',
107 111 pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
108 112 config.add_route(
109 113 name='repo_files:default_commit',
110 114 pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
111 115
112 116 config.add_route(
113 117 name='repo_files:rendered',
114 118 pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
115 119
116 120 config.add_route(
117 121 name='repo_files:annotated',
118 122 pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
119 123 config.add_route(
120 124 name='repo_files:annotated_previous',
121 125 pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
122 126
123 127 config.add_route(
124 128 name='repo_nodetree_full',
125 129 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
126 130 config.add_route(
127 131 name='repo_nodetree_full:default_path',
128 132 pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
129 133
130 134 config.add_route(
131 135 name='repo_files_nodelist',
132 136 pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
133 137
134 138 config.add_route(
135 139 name='repo_file_raw',
136 140 pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
137 141
138 142 config.add_route(
139 143 name='repo_file_download',
140 144 pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
141 145 config.add_route( # backward compat to keep old links working
142 146 name='repo_file_download:legacy',
143 147 pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
144 148 repo_route=True)
145 149
146 150 config.add_route(
147 151 name='repo_file_history',
148 152 pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
149 153
150 154 config.add_route(
151 155 name='repo_file_authors',
152 156 pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
153 157
154 158 config.add_route(
155 159 name='repo_files_remove_file',
156 160 pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
157 161 repo_route=True)
158 162 config.add_route(
159 163 name='repo_files_delete_file',
160 164 pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
161 165 repo_route=True)
162 166 config.add_route(
163 167 name='repo_files_edit_file',
164 168 pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
165 169 repo_route=True)
166 170 config.add_route(
167 171 name='repo_files_update_file',
168 172 pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
169 173 repo_route=True)
170 174 config.add_route(
171 175 name='repo_files_add_file',
172 176 pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
173 177 repo_route=True)
174 178 config.add_route(
175 179 name='repo_files_upload_file',
176 180 pattern='/{repo_name:.*?[^/]}/upload_file/{commit_id}/{f_path:.*}',
177 181 repo_route=True)
178 182 config.add_route(
179 183 name='repo_files_create_file',
180 184 pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
181 185 repo_route=True)
182 186
183 187 # Refs data
184 188 config.add_route(
185 189 name='repo_refs_data',
186 190 pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
187 191
188 192 config.add_route(
189 193 name='repo_refs_changelog_data',
190 194 pattern='/{repo_name:.*?[^/]}/refs-data-changelog', repo_route=True)
191 195
192 196 config.add_route(
193 197 name='repo_stats',
194 198 pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
195 199
196 200 # Commits
197 201 config.add_route(
198 202 name='repo_commits',
199 203 pattern='/{repo_name:.*?[^/]}/commits', repo_route=True)
200 204 config.add_route(
201 205 name='repo_commits_file',
202 206 pattern='/{repo_name:.*?[^/]}/commits/{commit_id}/{f_path:.*}', repo_route=True)
203 207 config.add_route(
204 208 name='repo_commits_elements',
205 209 pattern='/{repo_name:.*?[^/]}/commits_elements', repo_route=True)
206 210 config.add_route(
207 211 name='repo_commits_elements_file',
208 212 pattern='/{repo_name:.*?[^/]}/commits_elements/{commit_id}/{f_path:.*}', repo_route=True)
209 213
210 214 # Changelog (old deprecated name for commits page)
211 215 config.add_route(
212 216 name='repo_changelog',
213 217 pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
214 218 config.add_route(
215 219 name='repo_changelog_file',
216 220 pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
217 221
218 222 # Compare
219 223 config.add_route(
220 224 name='repo_compare_select',
221 225 pattern='/{repo_name:.*?[^/]}/compare', repo_route=True)
222 226
223 227 config.add_route(
224 228 name='repo_compare',
225 229 pattern='/{repo_name:.*?[^/]}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', repo_route=True)
226 230
227 231 # Tags
228 232 config.add_route(
229 233 name='tags_home',
230 234 pattern='/{repo_name:.*?[^/]}/tags', repo_route=True)
231 235
232 236 # Branches
233 237 config.add_route(
234 238 name='branches_home',
235 239 pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
236 240
237 241 # Bookmarks
238 242 config.add_route(
239 243 name='bookmarks_home',
240 244 pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
241 245
242 246 # Forks
243 247 config.add_route(
244 248 name='repo_fork_new',
245 249 pattern='/{repo_name:.*?[^/]}/fork', repo_route=True,
246 250 repo_forbid_when_archived=True,
247 251 repo_accepted_types=['hg', 'git'])
248 252
249 253 config.add_route(
250 254 name='repo_fork_create',
251 255 pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True,
252 256 repo_forbid_when_archived=True,
253 257 repo_accepted_types=['hg', 'git'])
254 258
255 259 config.add_route(
256 260 name='repo_forks_show_all',
257 261 pattern='/{repo_name:.*?[^/]}/forks', repo_route=True,
258 262 repo_accepted_types=['hg', 'git'])
259 263 config.add_route(
260 264 name='repo_forks_data',
261 265 pattern='/{repo_name:.*?[^/]}/forks/data', repo_route=True,
262 266 repo_accepted_types=['hg', 'git'])
263 267
264 268 # Pull Requests
265 269 config.add_route(
266 270 name='pullrequest_show',
267 271 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}',
268 272 repo_route=True)
269 273
270 274 config.add_route(
271 275 name='pullrequest_show_all',
272 276 pattern='/{repo_name:.*?[^/]}/pull-request',
273 277 repo_route=True, repo_accepted_types=['hg', 'git'])
274 278
275 279 config.add_route(
276 280 name='pullrequest_show_all_data',
277 281 pattern='/{repo_name:.*?[^/]}/pull-request-data',
278 282 repo_route=True, repo_accepted_types=['hg', 'git'])
279 283
280 284 config.add_route(
281 285 name='pullrequest_repo_refs',
282 286 pattern='/{repo_name:.*?[^/]}/pull-request/refs/{target_repo_name:.*?[^/]}',
283 287 repo_route=True)
284 288
285 289 config.add_route(
286 290 name='pullrequest_repo_targets',
287 291 pattern='/{repo_name:.*?[^/]}/pull-request/repo-targets',
288 292 repo_route=True)
289 293
290 294 config.add_route(
291 295 name='pullrequest_new',
292 296 pattern='/{repo_name:.*?[^/]}/pull-request/new',
293 297 repo_route=True, repo_accepted_types=['hg', 'git'],
294 298 repo_forbid_when_archived=True)
295 299
296 300 config.add_route(
297 301 name='pullrequest_create',
298 302 pattern='/{repo_name:.*?[^/]}/pull-request/create',
299 303 repo_route=True, repo_accepted_types=['hg', 'git'],
300 304 repo_forbid_when_archived=True)
301 305
302 306 config.add_route(
303 307 name='pullrequest_update',
304 308 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
305 309 repo_route=True, repo_forbid_when_archived=True)
306 310
307 311 config.add_route(
308 312 name='pullrequest_merge',
309 313 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
310 314 repo_route=True, repo_forbid_when_archived=True)
311 315
312 316 config.add_route(
313 317 name='pullrequest_delete',
314 318 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
315 319 repo_route=True, repo_forbid_when_archived=True)
316 320
317 321 config.add_route(
318 322 name='pullrequest_comment_create',
319 323 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment',
320 324 repo_route=True)
321 325
322 326 config.add_route(
323 327 name='pullrequest_comment_delete',
324 328 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
325 329 repo_route=True, repo_accepted_types=['hg', 'git'])
326 330
327 331 # Artifacts, (EE feature)
328 332 config.add_route(
329 333 name='repo_artifacts_list',
330 334 pattern='/{repo_name:.*?[^/]}/artifacts', repo_route=True)
331 335
332 336 # Settings
333 337 config.add_route(
334 338 name='edit_repo',
335 339 pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
336 340 # update is POST on edit_repo
337 341
338 342 # Settings advanced
339 343 config.add_route(
340 344 name='edit_repo_advanced',
341 345 pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True)
342 346 config.add_route(
343 347 name='edit_repo_advanced_archive',
344 348 pattern='/{repo_name:.*?[^/]}/settings/advanced/archive', repo_route=True)
345 349 config.add_route(
346 350 name='edit_repo_advanced_delete',
347 351 pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True)
348 352 config.add_route(
349 353 name='edit_repo_advanced_locking',
350 354 pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True)
351 355 config.add_route(
352 356 name='edit_repo_advanced_journal',
353 357 pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True)
354 358 config.add_route(
355 359 name='edit_repo_advanced_fork',
356 360 pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True)
357 361
358 362 config.add_route(
359 363 name='edit_repo_advanced_hooks',
360 364 pattern='/{repo_name:.*?[^/]}/settings/advanced/hooks', repo_route=True)
361 365
362 366 # Caches
363 367 config.add_route(
364 368 name='edit_repo_caches',
365 369 pattern='/{repo_name:.*?[^/]}/settings/caches', repo_route=True)
366 370
367 371 # Permissions
368 372 config.add_route(
369 373 name='edit_repo_perms',
370 374 pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
371 375
372 376 config.add_route(
373 377 name='edit_repo_perms_set_private',
374 378 pattern='/{repo_name:.*?[^/]}/settings/permissions/set_private', repo_route=True)
375 379
376 380 # Permissions Branch (EE feature)
377 381 config.add_route(
378 382 name='edit_repo_perms_branch',
379 383 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions', repo_route=True)
380 384 config.add_route(
381 385 name='edit_repo_perms_branch_delete',
382 386 pattern='/{repo_name:.*?[^/]}/settings/branch_permissions/{rule_id}/delete',
383 387 repo_route=True)
384 388
385 389 # Maintenance
386 390 config.add_route(
387 391 name='edit_repo_maintenance',
388 392 pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
389 393
390 394 config.add_route(
391 395 name='edit_repo_maintenance_execute',
392 396 pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
393 397
394 398 # Fields
395 399 config.add_route(
396 400 name='edit_repo_fields',
397 401 pattern='/{repo_name:.*?[^/]}/settings/fields', repo_route=True)
398 402 config.add_route(
399 403 name='edit_repo_fields_create',
400 404 pattern='/{repo_name:.*?[^/]}/settings/fields/create', repo_route=True)
401 405 config.add_route(
402 406 name='edit_repo_fields_delete',
403 407 pattern='/{repo_name:.*?[^/]}/settings/fields/{field_id}/delete', repo_route=True)
404 408
405 409 # Locking
406 410 config.add_route(
407 411 name='repo_edit_toggle_locking',
408 412 pattern='/{repo_name:.*?[^/]}/settings/toggle_locking', repo_route=True)
409 413
410 414 # Remote
411 415 config.add_route(
412 416 name='edit_repo_remote',
413 417 pattern='/{repo_name:.*?[^/]}/settings/remote', repo_route=True)
414 418 config.add_route(
415 419 name='edit_repo_remote_pull',
416 420 pattern='/{repo_name:.*?[^/]}/settings/remote/pull', repo_route=True)
417 421 config.add_route(
418 422 name='edit_repo_remote_push',
419 423 pattern='/{repo_name:.*?[^/]}/settings/remote/push', repo_route=True)
420 424
421 425 # Statistics
422 426 config.add_route(
423 427 name='edit_repo_statistics',
424 428 pattern='/{repo_name:.*?[^/]}/settings/statistics', repo_route=True)
425 429 config.add_route(
426 430 name='edit_repo_statistics_reset',
427 431 pattern='/{repo_name:.*?[^/]}/settings/statistics/update', repo_route=True)
428 432
429 433 # Issue trackers
430 434 config.add_route(
431 435 name='edit_repo_issuetracker',
432 436 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers', repo_route=True)
433 437 config.add_route(
434 438 name='edit_repo_issuetracker_test',
435 439 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/test', repo_route=True)
436 440 config.add_route(
437 441 name='edit_repo_issuetracker_delete',
438 442 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/delete', repo_route=True)
439 443 config.add_route(
440 444 name='edit_repo_issuetracker_update',
441 445 pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/update', repo_route=True)
442 446
443 447 # VCS Settings
444 448 config.add_route(
445 449 name='edit_repo_vcs',
446 450 pattern='/{repo_name:.*?[^/]}/settings/vcs', repo_route=True)
447 451 config.add_route(
448 452 name='edit_repo_vcs_update',
449 453 pattern='/{repo_name:.*?[^/]}/settings/vcs/update', repo_route=True)
450 454
451 455 # svn pattern
452 456 config.add_route(
453 457 name='edit_repo_vcs_svn_pattern_delete',
454 458 pattern='/{repo_name:.*?[^/]}/settings/vcs/svn_pattern/delete', repo_route=True)
455 459
456 460 # Repo Review Rules (EE feature)
457 461 config.add_route(
458 462 name='repo_reviewers',
459 463 pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
460 464
461 465 config.add_route(
462 466 name='repo_default_reviewers_data',
463 467 pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
464 468
465 469 # Repo Automation (EE feature)
466 470 config.add_route(
467 471 name='repo_automation',
468 472 pattern='/{repo_name:.*?[^/]}/settings/automation', repo_route=True)
469 473
470 474 # Strip
471 475 config.add_route(
472 476 name='edit_repo_strip',
473 477 pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
474 478
475 479 config.add_route(
476 480 name='strip_check',
477 481 pattern='/{repo_name:.*?[^/]}/settings/strip_check', repo_route=True)
478 482
479 483 config.add_route(
480 484 name='strip_execute',
481 485 pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
482 486
483 487 # Audit logs
484 488 config.add_route(
485 489 name='edit_repo_audit_logs',
486 490 pattern='/{repo_name:.*?[^/]}/settings/audit_logs', repo_route=True)
487 491
488 492 # ATOM/RSS Feed, shouldn't contain slashes for outlook compatibility
489 493 config.add_route(
490 494 name='rss_feed_home',
491 495 pattern='/{repo_name:.*?[^/]}/feed-rss', repo_route=True)
492 496
493 497 config.add_route(
494 498 name='atom_feed_home',
495 499 pattern='/{repo_name:.*?[^/]}/feed-atom', repo_route=True)
496 500
497 501 config.add_route(
498 502 name='rss_feed_home_old',
499 503 pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
500 504
501 505 config.add_route(
502 506 name='atom_feed_home_old',
503 507 pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
504 508
505 509 # NOTE(marcink): needs to be at the end for catch-all
506 510 add_route_with_slash(
507 511 config,
508 512 name='repo_summary',
509 513 pattern='/{repo_name:.*?[^/]}', repo_route=True)
510 514
511 515 # Scan module for configuration decorators.
512 516 config.scan('.views', ignore='.tests')
@@ -1,503 +1,600 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 import logging
23 23 import collections
24 24
25 25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound
26 26 from pyramid.view import view_config
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
29 29
30 30 from rhodecode.apps._base import RepoAppView
31 from rhodecode.apps.file_store import utils as store_utils
32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
31 33
32 34 from rhodecode.lib import diffs, codeblocks
33 35 from rhodecode.lib.auth import (
34 36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
35 37
36 38 from rhodecode.lib.compat import OrderedDict
37 39 from rhodecode.lib.diffs import (
38 40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
39 41 get_diff_whitespace_flag)
40 42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 43 import rhodecode.lib.helpers as h
42 44 from rhodecode.lib.utils2 import safe_unicode, str2bool
43 45 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 46 from rhodecode.lib.vcs.exceptions import (
45 47 RepositoryError, CommitDoesNotExistError)
46 from rhodecode.model.db import ChangesetComment, ChangesetStatus
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore
47 49 from rhodecode.model.changeset_status import ChangesetStatusModel
48 50 from rhodecode.model.comment import CommentsModel
49 51 from rhodecode.model.meta import Session
50 52 from rhodecode.model.settings import VcsSettingsModel
51 53
52 54 log = logging.getLogger(__name__)
53 55
54 56
55 57 def _update_with_GET(params, request):
56 58 for k in ['diff1', 'diff2', 'diff']:
57 59 params[k] += request.GET.getall(k)
58 60
59 61
60 62 class RepoCommitsView(RepoAppView):
61 63 def load_default_context(self):
62 64 c = self._get_local_tmpl_context(include_app_defaults=True)
63 65 c.rhodecode_repo = self.rhodecode_vcs_repo
64 66
65 67 return c
66 68
67 69 def _is_diff_cache_enabled(self, target_repo):
68 70 caching_enabled = self._get_general_setting(
69 71 target_repo, 'rhodecode_diff_cache')
70 72 log.debug('Diff caching enabled: %s', caching_enabled)
71 73 return caching_enabled
72 74
73 75 def _commit(self, commit_id_range, method):
74 76 _ = self.request.translate
75 77 c = self.load_default_context()
76 78 c.fulldiff = self.request.GET.get('fulldiff')
77 79
78 80 # fetch global flags of ignore ws or context lines
79 81 diff_context = get_diff_context(self.request)
80 82 hide_whitespace_changes = get_diff_whitespace_flag(self.request)
81 83
82 84 # diff_limit will cut off the whole diff if the limit is applied
83 85 # otherwise it will just hide the big files from the front-end
84 86 diff_limit = c.visual.cut_off_limit_diff
85 87 file_limit = c.visual.cut_off_limit_file
86 88
87 89 # get ranges of commit ids if preset
88 90 commit_range = commit_id_range.split('...')[:2]
89 91
90 92 try:
91 93 pre_load = ['affected_files', 'author', 'branch', 'date',
92 94 'message', 'parents']
93 95 if self.rhodecode_vcs_repo.alias == 'hg':
94 96 pre_load += ['hidden', 'obsolete', 'phase']
95 97
96 98 if len(commit_range) == 2:
97 99 commits = self.rhodecode_vcs_repo.get_commits(
98 100 start_id=commit_range[0], end_id=commit_range[1],
99 101 pre_load=pre_load, translate_tags=False)
100 102 commits = list(commits)
101 103 else:
102 104 commits = [self.rhodecode_vcs_repo.get_commit(
103 105 commit_id=commit_id_range, pre_load=pre_load)]
104 106
105 107 c.commit_ranges = commits
106 108 if not c.commit_ranges:
107 109 raise RepositoryError('The commit range returned an empty result')
108 110 except CommitDoesNotExistError as e:
109 111 msg = _('No such commit exists. Org exception: `{}`').format(e)
110 112 h.flash(msg, category='error')
111 113 raise HTTPNotFound()
112 114 except Exception:
113 115 log.exception("General failure")
114 116 raise HTTPNotFound()
115 117
116 118 c.changes = OrderedDict()
117 119 c.lines_added = 0
118 120 c.lines_deleted = 0
119 121
120 122 # auto collapse if we have more than limit
121 123 collapse_limit = diffs.DiffProcessor._collapse_commits_over
122 124 c.collapse_all_commits = len(c.commit_ranges) > collapse_limit
123 125
124 126 c.commit_statuses = ChangesetStatus.STATUSES
125 127 c.inline_comments = []
126 128 c.files = []
127 129
128 130 c.statuses = []
129 131 c.comments = []
130 132 c.unresolved_comments = []
131 133 c.resolved_comments = []
132 134 if len(c.commit_ranges) == 1:
133 135 commit = c.commit_ranges[0]
134 136 c.comments = CommentsModel().get_comments(
135 137 self.db_repo.repo_id,
136 138 revision=commit.raw_id)
137 139 c.statuses.append(ChangesetStatusModel().get_status(
138 140 self.db_repo.repo_id, commit.raw_id))
139 141 # comments from PR
140 142 statuses = ChangesetStatusModel().get_statuses(
141 143 self.db_repo.repo_id, commit.raw_id,
142 144 with_revisions=True)
143 145 prs = set(st.pull_request for st in statuses
144 146 if st.pull_request is not None)
145 147 # from associated statuses, check the pull requests, and
146 148 # show comments from them
147 149 for pr in prs:
148 150 c.comments.extend(pr.comments)
149 151
150 152 c.unresolved_comments = CommentsModel()\
151 153 .get_commit_unresolved_todos(commit.raw_id)
152 154 c.resolved_comments = CommentsModel()\
153 155 .get_commit_resolved_todos(commit.raw_id)
154 156
155 157 diff = None
156 158 # Iterate over ranges (default commit view is always one commit)
157 159 for commit in c.commit_ranges:
158 160 c.changes[commit.raw_id] = []
159 161
160 162 commit2 = commit
161 163 commit1 = commit.first_parent
162 164
163 165 if method == 'show':
164 166 inline_comments = CommentsModel().get_inline_comments(
165 167 self.db_repo.repo_id, revision=commit.raw_id)
166 168 c.inline_cnt = CommentsModel().get_inline_comments_count(
167 169 inline_comments)
168 170 c.inline_comments = inline_comments
169 171
170 172 cache_path = self.rhodecode_vcs_repo.get_create_shadow_cache_pr_path(
171 173 self.db_repo)
172 174 cache_file_path = diff_cache_exist(
173 175 cache_path, 'diff', commit.raw_id,
174 176 hide_whitespace_changes, diff_context, c.fulldiff)
175 177
176 178 caching_enabled = self._is_diff_cache_enabled(self.db_repo)
177 179 force_recache = str2bool(self.request.GET.get('force_recache'))
178 180
179 181 cached_diff = None
180 182 if caching_enabled:
181 183 cached_diff = load_cached_diff(cache_file_path)
182 184
183 185 has_proper_diff_cache = cached_diff and cached_diff.get('diff')
184 186 if not force_recache and has_proper_diff_cache:
185 187 diffset = cached_diff['diff']
186 188 else:
187 189 vcs_diff = self.rhodecode_vcs_repo.get_diff(
188 190 commit1, commit2,
189 191 ignore_whitespace=hide_whitespace_changes,
190 192 context=diff_context)
191 193
192 194 diff_processor = diffs.DiffProcessor(
193 195 vcs_diff, format='newdiff', diff_limit=diff_limit,
194 196 file_limit=file_limit, show_full_diff=c.fulldiff)
195 197
196 198 _parsed = diff_processor.prepare()
197 199
198 200 diffset = codeblocks.DiffSet(
199 201 repo_name=self.db_repo_name,
200 202 source_node_getter=codeblocks.diffset_node_getter(commit1),
201 203 target_node_getter=codeblocks.diffset_node_getter(commit2))
202 204
203 205 diffset = self.path_filter.render_patchset_filtered(
204 206 diffset, _parsed, commit1.raw_id, commit2.raw_id)
205 207
206 208 # save cached diff
207 209 if caching_enabled:
208 210 cache_diff(cache_file_path, diffset, None)
209 211
210 212 c.limited_diff = diffset.limited_diff
211 213 c.changes[commit.raw_id] = diffset
212 214 else:
213 215 # TODO(marcink): no cache usage here...
214 216 _diff = self.rhodecode_vcs_repo.get_diff(
215 217 commit1, commit2,
216 218 ignore_whitespace=hide_whitespace_changes, context=diff_context)
217 219 diff_processor = diffs.DiffProcessor(
218 220 _diff, format='newdiff', diff_limit=diff_limit,
219 221 file_limit=file_limit, show_full_diff=c.fulldiff)
220 222 # downloads/raw we only need RAW diff nothing else
221 223 diff = self.path_filter.get_raw_patch(diff_processor)
222 224 c.changes[commit.raw_id] = [None, None, None, None, diff, None, None]
223 225
224 226 # sort comments by how they were generated
225 227 c.comments = sorted(c.comments, key=lambda x: x.comment_id)
226 228
227 229 if len(c.commit_ranges) == 1:
228 230 c.commit = c.commit_ranges[0]
229 231 c.parent_tmpl = ''.join(
230 232 '# Parent %s\n' % x.raw_id for x in c.commit.parents)
231 233
232 234 if method == 'download':
233 235 response = Response(diff)
234 236 response.content_type = 'text/plain'
235 237 response.content_disposition = (
236 238 'attachment; filename=%s.diff' % commit_id_range[:12])
237 239 return response
238 240 elif method == 'patch':
239 241 c.diff = safe_unicode(diff)
240 242 patch = render(
241 243 'rhodecode:templates/changeset/patch_changeset.mako',
242 244 self._get_template_context(c), self.request)
243 245 response = Response(patch)
244 246 response.content_type = 'text/plain'
245 247 return response
246 248 elif method == 'raw':
247 249 response = Response(diff)
248 250 response.content_type = 'text/plain'
249 251 return response
250 252 elif method == 'show':
251 253 if len(c.commit_ranges) == 1:
252 254 html = render(
253 255 'rhodecode:templates/changeset/changeset.mako',
254 256 self._get_template_context(c), self.request)
255 257 return Response(html)
256 258 else:
257 259 c.ancestor = None
258 260 c.target_repo = self.db_repo
259 261 html = render(
260 262 'rhodecode:templates/changeset/changeset_range.mako',
261 263 self._get_template_context(c), self.request)
262 264 return Response(html)
263 265
264 266 raise HTTPBadRequest()
265 267
266 268 @LoginRequired()
267 269 @HasRepoPermissionAnyDecorator(
268 270 'repository.read', 'repository.write', 'repository.admin')
269 271 @view_config(
270 272 route_name='repo_commit', request_method='GET',
271 273 renderer=None)
272 274 def repo_commit_show(self):
273 275 commit_id = self.request.matchdict['commit_id']
274 276 return self._commit(commit_id, method='show')
275 277
276 278 @LoginRequired()
277 279 @HasRepoPermissionAnyDecorator(
278 280 'repository.read', 'repository.write', 'repository.admin')
279 281 @view_config(
280 282 route_name='repo_commit_raw', request_method='GET',
281 283 renderer=None)
282 284 @view_config(
283 285 route_name='repo_commit_raw_deprecated', request_method='GET',
284 286 renderer=None)
285 287 def repo_commit_raw(self):
286 288 commit_id = self.request.matchdict['commit_id']
287 289 return self._commit(commit_id, method='raw')
288 290
289 291 @LoginRequired()
290 292 @HasRepoPermissionAnyDecorator(
291 293 'repository.read', 'repository.write', 'repository.admin')
292 294 @view_config(
293 295 route_name='repo_commit_patch', request_method='GET',
294 296 renderer=None)
295 297 def repo_commit_patch(self):
296 298 commit_id = self.request.matchdict['commit_id']
297 299 return self._commit(commit_id, method='patch')
298 300
299 301 @LoginRequired()
300 302 @HasRepoPermissionAnyDecorator(
301 303 'repository.read', 'repository.write', 'repository.admin')
302 304 @view_config(
303 305 route_name='repo_commit_download', request_method='GET',
304 306 renderer=None)
305 307 def repo_commit_download(self):
306 308 commit_id = self.request.matchdict['commit_id']
307 309 return self._commit(commit_id, method='download')
308 310
309 311 @LoginRequired()
310 312 @NotAnonymous()
311 313 @HasRepoPermissionAnyDecorator(
312 314 'repository.read', 'repository.write', 'repository.admin')
313 315 @CSRFRequired()
314 316 @view_config(
315 317 route_name='repo_commit_comment_create', request_method='POST',
316 318 renderer='json_ext')
317 319 def repo_commit_comment_create(self):
318 320 _ = self.request.translate
319 321 commit_id = self.request.matchdict['commit_id']
320 322
321 323 c = self.load_default_context()
322 324 status = self.request.POST.get('changeset_status', None)
323 325 text = self.request.POST.get('text')
324 326 comment_type = self.request.POST.get('comment_type')
325 327 resolves_comment_id = self.request.POST.get('resolves_comment_id', None)
326 328
327 329 if status:
328 330 text = text or (_('Status change %(transition_icon)s %(status)s')
329 331 % {'transition_icon': '>',
330 332 'status': ChangesetStatus.get_status_lbl(status)})
331 333
332 334 multi_commit_ids = []
333 335 for _commit_id in self.request.POST.get('commit_ids', '').split(','):
334 336 if _commit_id not in ['', None, EmptyCommit.raw_id]:
335 337 if _commit_id not in multi_commit_ids:
336 338 multi_commit_ids.append(_commit_id)
337 339
338 340 commit_ids = multi_commit_ids or [commit_id]
339 341
340 342 comment = None
341 343 for current_id in filter(None, commit_ids):
342 344 comment = CommentsModel().create(
343 345 text=text,
344 346 repo=self.db_repo.repo_id,
345 347 user=self._rhodecode_db_user.user_id,
346 348 commit_id=current_id,
347 349 f_path=self.request.POST.get('f_path'),
348 350 line_no=self.request.POST.get('line'),
349 351 status_change=(ChangesetStatus.get_status_lbl(status)
350 352 if status else None),
351 353 status_change_type=status,
352 354 comment_type=comment_type,
353 355 resolves_comment_id=resolves_comment_id,
354 356 auth_user=self._rhodecode_user
355 357 )
356 358
357 359 # get status if set !
358 360 if status:
359 361 # if latest status was from pull request and it's closed
360 362 # disallow changing status !
361 363 # dont_allow_on_closed_pull_request = True !
362 364
363 365 try:
364 366 ChangesetStatusModel().set_status(
365 367 self.db_repo.repo_id,
366 368 status,
367 369 self._rhodecode_db_user.user_id,
368 370 comment,
369 371 revision=current_id,
370 372 dont_allow_on_closed_pull_request=True
371 373 )
372 374 except StatusChangeOnClosedPullRequestError:
373 375 msg = _('Changing the status of a commit associated with '
374 376 'a closed pull request is not allowed')
375 377 log.exception(msg)
376 378 h.flash(msg, category='warning')
377 379 raise HTTPFound(h.route_path(
378 380 'repo_commit', repo_name=self.db_repo_name,
379 381 commit_id=current_id))
380 382
381 383 # finalize, commit and redirect
382 384 Session().commit()
383 385
384 386 data = {
385 387 'target_id': h.safeid(h.safe_unicode(
386 388 self.request.POST.get('f_path'))),
387 389 }
388 390 if comment:
389 391 c.co = comment
390 392 rendered_comment = render(
391 393 'rhodecode:templates/changeset/changeset_comment_block.mako',
392 394 self._get_template_context(c), self.request)
393 395
394 396 data.update(comment.get_dict())
395 397 data.update({'rendered_text': rendered_comment})
396 398
397 399 return data
398 400
399 401 @LoginRequired()
400 402 @NotAnonymous()
401 403 @HasRepoPermissionAnyDecorator(
402 404 'repository.read', 'repository.write', 'repository.admin')
403 405 @CSRFRequired()
404 406 @view_config(
405 407 route_name='repo_commit_comment_preview', request_method='POST',
406 408 renderer='string', xhr=True)
407 409 def repo_commit_comment_preview(self):
408 410 # Technically a CSRF token is not needed as no state changes with this
409 411 # call. However, as this is a POST is better to have it, so automated
410 412 # tools don't flag it as potential CSRF.
411 413 # Post is required because the payload could be bigger than the maximum
412 414 # allowed by GET.
413 415
414 416 text = self.request.POST.get('text')
415 417 renderer = self.request.POST.get('renderer') or 'rst'
416 418 if text:
417 419 return h.render(text, renderer=renderer, mentions=True)
418 420 return ''
419 421
420 422 @LoginRequired()
421 423 @NotAnonymous()
422 424 @HasRepoPermissionAnyDecorator(
423 425 'repository.read', 'repository.write', 'repository.admin')
424 426 @CSRFRequired()
425 427 @view_config(
428 route_name='repo_commit_comment_attachment_upload', request_method='POST',
429 renderer='json_ext', xhr=True)
430 def repo_commit_comment_attachment_upload(self):
431 c = self.load_default_context()
432 upload_key = 'attachment'
433
434 file_obj = self.request.POST.get(upload_key)
435
436 if file_obj is None:
437 self.request.response.status = 400
438 return {'store_fid': None,
439 'access_path': None,
440 'error': '{} data field is missing'.format(upload_key)}
441
442 if not hasattr(file_obj, 'filename'):
443 self.request.response.status = 400
444 return {'store_fid': None,
445 'access_path': None,
446 'error': 'filename cannot be read from the data field'}
447
448 filename = file_obj.filename
449 file_display_name = filename
450
451 metadata = {
452 'user_uploaded': {'username': self._rhodecode_user.username,
453 'user_id': self._rhodecode_user.user_id,
454 'ip': self._rhodecode_user.ip_addr}}
455
456 # TODO(marcink): allow .ini configuration for allowed_extensions, and file-size
457 allowed_extensions = [
458 'gif', '.jpeg', '.jpg', '.png', '.docx', '.gz', '.log', '.pdf',
459 '.pptx', '.txt', '.xlsx', '.zip']
460 max_file_size = 10 * 1024 * 1024 # 10MB, also validated via dropzone.js
461
462 try:
463 storage = store_utils.get_file_storage(self.request.registry.settings)
464 store_uid, metadata = storage.save_file(
465 file_obj.file, filename, extra_metadata=metadata,
466 extensions=allowed_extensions, max_filesize=max_file_size)
467 except FileNotAllowedException:
468 self.request.response.status = 400
469 permitted_extensions = ', '.join(allowed_extensions)
470 error_msg = 'File `{}` is not allowed. ' \
471 'Only following extensions are permitted: {}'.format(
472 filename, permitted_extensions)
473 return {'store_fid': None,
474 'access_path': None,
475 'error': error_msg}
476 except FileOverSizeException:
477 self.request.response.status = 400
478 limit_mb = h.format_byte_size_binary(max_file_size)
479 return {'store_fid': None,
480 'access_path': None,
481 'error': 'File {} is exceeding allowed limit of {}.'.format(
482 filename, limit_mb)}
483
484 try:
485 entry = FileStore.create(
486 file_uid=store_uid, filename=metadata["filename"],
487 file_hash=metadata["sha256"], file_size=metadata["size"],
488 file_display_name=file_display_name,
489 file_description=u'comment attachment `{}`'.format(safe_unicode(filename)),
490 hidden=True, check_acl=True, user_id=self._rhodecode_user.user_id,
491 scope_repo_id=self.db_repo.repo_id
492 )
493 Session().add(entry)
494 Session().commit()
495 log.debug('Stored upload in DB as %s', entry)
496 except Exception:
497 log.exception('Failed to store file %s', filename)
498 self.request.response.status = 400
499 return {'store_fid': None,
500 'access_path': None,
501 'error': 'File {} failed to store in DB.'.format(filename)}
502
503 Session().commit()
504
505 return {
506 'store_fid': store_uid,
507 'access_path': h.route_path(
508 'download_file', fid=store_uid),
509 'fqn_access_path': h.route_url(
510 'download_file', fid=store_uid),
511 'repo_access_path': h.route_path(
512 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
513 'repo_fqn_access_path': h.route_url(
514 'repo_artifacts_get', repo_name=self.db_repo_name, uid=store_uid),
515 }
516
517 @LoginRequired()
518 @NotAnonymous()
519 @HasRepoPermissionAnyDecorator(
520 'repository.read', 'repository.write', 'repository.admin')
521 @CSRFRequired()
522 @view_config(
426 523 route_name='repo_commit_comment_delete', request_method='POST',
427 524 renderer='json_ext')
428 525 def repo_commit_comment_delete(self):
429 526 commit_id = self.request.matchdict['commit_id']
430 527 comment_id = self.request.matchdict['comment_id']
431 528
432 529 comment = ChangesetComment.get_or_404(comment_id)
433 530 if not comment:
434 531 log.debug('Comment with id:%s not found, skipping', comment_id)
435 532 # comment already deleted in another call probably
436 533 return True
437 534
438 535 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
439 536 super_admin = h.HasPermissionAny('hg.admin')()
440 537 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
441 538 is_repo_comment = comment.repo.repo_name == self.db_repo_name
442 539 comment_repo_admin = is_repo_admin and is_repo_comment
443 540
444 541 if super_admin or comment_owner or comment_repo_admin:
445 542 CommentsModel().delete(comment=comment, auth_user=self._rhodecode_user)
446 543 Session().commit()
447 544 return True
448 545 else:
449 546 log.warning('No permissions for user %s to delete comment_id: %s',
450 547 self._rhodecode_db_user, comment_id)
451 548 raise HTTPNotFound()
452 549
453 550 @LoginRequired()
454 551 @HasRepoPermissionAnyDecorator(
455 552 'repository.read', 'repository.write', 'repository.admin')
456 553 @view_config(
457 554 route_name='repo_commit_data', request_method='GET',
458 555 renderer='json_ext', xhr=True)
459 556 def repo_commit_data(self):
460 557 commit_id = self.request.matchdict['commit_id']
461 558 self.load_default_context()
462 559
463 560 try:
464 561 return self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
465 562 except CommitDoesNotExistError as e:
466 563 return EmptyCommit(message=str(e))
467 564
468 565 @LoginRequired()
469 566 @HasRepoPermissionAnyDecorator(
470 567 'repository.read', 'repository.write', 'repository.admin')
471 568 @view_config(
472 569 route_name='repo_commit_children', request_method='GET',
473 570 renderer='json_ext', xhr=True)
474 571 def repo_commit_children(self):
475 572 commit_id = self.request.matchdict['commit_id']
476 573 self.load_default_context()
477 574
478 575 try:
479 576 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
480 577 children = commit.children
481 578 except CommitDoesNotExistError:
482 579 children = []
483 580
484 581 result = {"results": children}
485 582 return result
486 583
487 584 @LoginRequired()
488 585 @HasRepoPermissionAnyDecorator(
489 586 'repository.read', 'repository.write', 'repository.admin')
490 587 @view_config(
491 588 route_name='repo_commit_parents', request_method='GET',
492 589 renderer='json_ext')
493 590 def repo_commit_parents(self):
494 591 commit_id = self.request.matchdict['commit_id']
495 592 self.load_default_context()
496 593
497 594 try:
498 595 commit = self.rhodecode_vcs_repo.get_commit(commit_id=commit_id)
499 596 parents = commit.parents
500 597 except CommitDoesNotExistError:
501 598 parents = []
502 599 result = {"results": parents}
503 600 return result
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,585 +1,611 b''
1 1 // comments.less
2 2 // For use in RhodeCode applications;
3 3 // see style guide documentation for guidelines.
4 4
5 5
6 6 // Comments
7 7 @comment-outdated-opacity: 0.6;
8 8
9 9 .comments {
10 10 width: 100%;
11 11 }
12 12
13 13 .comments-heading {
14 14 margin-bottom: -1px;
15 15 background: @grey6;
16 16 display: block;
17 17 padding: 10px 0px;
18 18 font-size: 18px
19 19 }
20 20
21 21 #comment-tr-show {
22 22 padding: 5px 0;
23 23 }
24 24
25 25 tr.inline-comments div {
26 26 max-width: 100%;
27 27
28 28 p {
29 29 white-space: normal;
30 30 }
31 31
32 32 code, pre, .code, dd {
33 33 overflow-x: auto;
34 34 width: 1062px;
35 35 }
36 36
37 37 dd {
38 38 width: auto;
39 39 }
40 40 }
41 41
42 42 #injected_page_comments {
43 43 .comment-previous-link,
44 44 .comment-next-link,
45 45 .comment-links-divider {
46 46 display: none;
47 47 }
48 48 }
49 49
50 50 .add-comment {
51 51 margin-bottom: 10px;
52 52 }
53 53 .hide-comment-button .add-comment {
54 54 display: none;
55 55 }
56 56
57 57 .comment-bubble {
58 58 color: @grey4;
59 59 margin-top: 4px;
60 60 margin-right: 30px;
61 61 visibility: hidden;
62 62 }
63 63
64 64 .comment-label {
65 65 float: left;
66 66
67 67 padding: 0.4em 0.4em;
68 68 margin: 3px 5px 0px -10px;
69 69 display: inline-block;
70 70 min-height: 0;
71 71
72 72 text-align: center;
73 73 font-size: 10px;
74 74 line-height: .8em;
75 75
76 76 font-family: @text-italic;
77 77 font-style: italic;
78 78 background: #fff none;
79 79 color: @grey4;
80 80 border: 1px solid @grey4;
81 81 white-space: nowrap;
82 82
83 83 text-transform: uppercase;
84 84 min-width: 40px;
85 85
86 86 &.todo {
87 87 color: @color5;
88 88 font-style: italic;
89 89 font-weight: @text-bold-italic-weight;
90 90 font-family: @text-bold-italic;
91 91 }
92 92
93 93 .resolve {
94 94 cursor: pointer;
95 95 text-decoration: underline;
96 96 }
97 97
98 98 .resolved {
99 99 text-decoration: line-through;
100 100 color: @color1;
101 101 }
102 102 .resolved a {
103 103 text-decoration: line-through;
104 104 color: @color1;
105 105 }
106 106 .resolve-text {
107 107 color: @color1;
108 108 margin: 2px 8px;
109 109 font-family: @text-italic;
110 110 font-style: italic;
111 111 }
112 112 }
113 113
114 114 .has-spacer-after {
115 115 &:after {
116 116 content: ' | ';
117 117 color: @grey5;
118 118 }
119 119 }
120 120
121 121 .has-spacer-before {
122 122 &:before {
123 123 content: ' | ';
124 124 color: @grey5;
125 125 }
126 126 }
127 127
128 128 .comment {
129 129
130 130 &.comment-general {
131 131 border: 1px solid @grey5;
132 132 padding: 5px 5px 5px 5px;
133 133 }
134 134
135 135 margin: @padding 0;
136 136 padding: 4px 0 0 0;
137 137 line-height: 1em;
138 138
139 139 .rc-user {
140 140 min-width: 0;
141 141 margin: 0px .5em 0 0;
142 142
143 143 .user {
144 144 display: inline;
145 145 }
146 146 }
147 147
148 148 .meta {
149 149 position: relative;
150 150 width: 100%;
151 151 border-bottom: 1px solid @grey5;
152 152 margin: -5px 0px;
153 153 line-height: 24px;
154 154
155 155 &:hover .permalink {
156 156 visibility: visible;
157 157 color: @rcblue;
158 158 }
159 159 }
160 160
161 161 .author,
162 162 .date {
163 163 display: inline;
164 164
165 165 &:after {
166 166 content: ' | ';
167 167 color: @grey5;
168 168 }
169 169 }
170 170
171 171 .author-general img {
172 172 top: 3px;
173 173 }
174 174 .author-inline img {
175 175 top: 3px;
176 176 }
177 177
178 178 .status-change,
179 179 .permalink,
180 180 .changeset-status-lbl {
181 181 display: inline;
182 182 }
183 183
184 184 .permalink {
185 185 visibility: hidden;
186 186 }
187 187
188 188 .comment-links-divider {
189 189 display: inline;
190 190 }
191 191
192 192 .comment-links-block {
193 193 float:right;
194 194 text-align: right;
195 195 min-width: 85px;
196 196
197 197 [class^="icon-"]:before,
198 198 [class*=" icon-"]:before {
199 199 margin-left: 0;
200 200 margin-right: 0;
201 201 }
202 202 }
203 203
204 204 .comment-previous-link {
205 205 display: inline-block;
206 206
207 207 .arrow_comment_link{
208 208 cursor: pointer;
209 209 i {
210 210 font-size:10px;
211 211 }
212 212 }
213 213 .arrow_comment_link.disabled {
214 214 cursor: default;
215 215 color: @grey5;
216 216 }
217 217 }
218 218
219 219 .comment-next-link {
220 220 display: inline-block;
221 221
222 222 .arrow_comment_link{
223 223 cursor: pointer;
224 224 i {
225 225 font-size:10px;
226 226 }
227 227 }
228 228 .arrow_comment_link.disabled {
229 229 cursor: default;
230 230 color: @grey5;
231 231 }
232 232 }
233 233
234 234 .delete-comment {
235 235 display: inline-block;
236 236 color: @rcblue;
237 237
238 238 &:hover {
239 239 cursor: pointer;
240 240 }
241 241 }
242 242
243 243 .text {
244 244 clear: both;
245 245 .border-radius(@border-radius);
246 246 .box-sizing(border-box);
247 247
248 248 .markdown-block p,
249 249 .rst-block p {
250 250 margin: .5em 0 !important;
251 251 // TODO: lisa: This is needed because of other rst !important rules :[
252 252 }
253 253 }
254 254
255 255 .pr-version {
256 256 float: left;
257 257 margin: 0px 4px;
258 258 }
259 259 .pr-version-inline {
260 260 float: left;
261 261 margin: 0px 4px;
262 262 }
263 263 .pr-version-num {
264 264 font-size: 10px;
265 265 }
266 266 }
267 267
268 268 @comment-padding: 5px;
269 269
270 270 .general-comments {
271 271 .comment-outdated {
272 272 opacity: @comment-outdated-opacity;
273 273 }
274 274 }
275 275
276 276 .inline-comments {
277 277 border-radius: @border-radius;
278 278 .comment {
279 279 margin: 0;
280 280 border-radius: @border-radius;
281 281 }
282 282 .comment-outdated {
283 283 opacity: @comment-outdated-opacity;
284 284 }
285 285
286 286 .comment-inline {
287 287 background: white;
288 288 padding: @comment-padding @comment-padding;
289 289 border: @comment-padding solid @grey6;
290 290
291 291 .text {
292 292 border: none;
293 293 }
294 294 .meta {
295 295 border-bottom: 1px solid @grey6;
296 296 margin: -5px 0px;
297 297 line-height: 24px;
298 298 }
299 299 }
300 300 .comment-selected {
301 301 border-left: 6px solid @comment-highlight-color;
302 302 }
303 303 .comment-inline-form {
304 304 padding: @comment-padding;
305 305 display: none;
306 306 }
307 307 .cb-comment-add-button {
308 308 margin: @comment-padding;
309 309 }
310 310 /* hide add comment button when form is open */
311 311 .comment-inline-form-open ~ .cb-comment-add-button {
312 312 display: none;
313 313 }
314 314 .comment-inline-form-open {
315 315 display: block;
316 316 }
317 317 /* hide add comment button when form but no comments */
318 318 .comment-inline-form:first-child + .cb-comment-add-button {
319 319 display: none;
320 320 }
321 321 /* hide add comment button when no comments or form */
322 322 .cb-comment-add-button:first-child {
323 323 display: none;
324 324 }
325 325 /* hide add comment button when only comment is being deleted */
326 326 .comment-deleting:first-child + .cb-comment-add-button {
327 327 display: none;
328 328 }
329 329 }
330 330
331 331
332 332 .show-outdated-comments {
333 333 display: inline;
334 334 color: @rcblue;
335 335 }
336 336
337 337 // Comment Form
338 338 div.comment-form {
339 339 margin-top: 20px;
340 340 }
341 341
342 342 .comment-form strong {
343 343 display: block;
344 344 margin-bottom: 15px;
345 345 }
346 346
347 347 .comment-form textarea {
348 348 width: 100%;
349 349 height: 100px;
350 350 font-family: @text-monospace;
351 351 }
352 352
353 353 form.comment-form {
354 354 margin-top: 10px;
355 355 margin-left: 10px;
356 356 }
357 357
358 358 .comment-inline-form .comment-block-ta,
359 359 .comment-form .comment-block-ta,
360 360 .comment-form .preview-box {
361 361 .border-radius(@border-radius);
362 362 .box-sizing(border-box);
363 363 background-color: white;
364 364 }
365 365
366 366 .comment-form-submit {
367 367 margin-top: 5px;
368 368 margin-left: 525px;
369 369 }
370 370
371 371 .file-comments {
372 372 display: none;
373 373 }
374 374
375 375 .comment-form .preview-box.unloaded,
376 376 .comment-inline-form .preview-box.unloaded {
377 377 height: 50px;
378 378 text-align: center;
379 379 padding: 20px;
380 380 background-color: white;
381 381 }
382 382
383 383 .comment-footer {
384 384 position: relative;
385 385 width: 100%;
386 386 min-height: 42px;
387 387
388 388 .status_box,
389 389 .cancel-button {
390 390 float: left;
391 391 display: inline-block;
392 392 }
393 393
394 394 .action-buttons {
395 395 float: right;
396 396 display: inline-block;
397 397 }
398 398
399 399 .action-buttons-extra {
400 400 display: inline-block;
401 401 }
402 402 }
403 403
404 404 .comment-form {
405 405
406 406 .comment {
407 407 margin-left: 10px;
408 408 }
409 409
410 410 .comment-help {
411 411 color: @grey4;
412 412 padding: 5px 0 5px 0;
413 413 }
414 414
415 415 .comment-title {
416 416 padding: 5px 0 5px 0;
417 417 }
418 418
419 419 .comment-button {
420 420 display: inline-block;
421 421 }
422 422
423 423 .comment-button-input {
424 424 margin-right: 0;
425 425 }
426 426
427 427 .comment-footer {
428 428 margin-bottom: 110px;
429 429 margin-top: 10px;
430 430 }
431 431 }
432 432
433 433
434 434 .comment-form-login {
435 435 .comment-help {
436 436 padding: 0.7em; //same as the button
437 437 }
438 438
439 439 div.clearfix {
440 440 clear: both;
441 441 width: 100%;
442 442 display: block;
443 443 }
444 444 }
445 445
446 446 .comment-type {
447 447 margin: 0px;
448 448 border-radius: inherit;
449 449 border-color: @grey6;
450 450 }
451 451
452 452 .preview-box {
453 453 min-height: 105px;
454 454 margin-bottom: 15px;
455 455 background-color: white;
456 456 .border-radius(@border-radius);
457 457 .box-sizing(border-box);
458 458 }
459 459
460 460 .add-another-button {
461 461 margin-left: 10px;
462 462 margin-top: 10px;
463 463 margin-bottom: 10px;
464 464 }
465 465
466 466 .comment .buttons {
467 467 float: right;
468 468 margin: -1px 0px 0px 0px;
469 469 }
470 470
471 471 // Inline Comment Form
472 472 .injected_diff .comment-inline-form,
473 473 .comment-inline-form {
474 474 background-color: white;
475 475 margin-top: 10px;
476 476 margin-bottom: 20px;
477 477 }
478 478
479 479 .inline-form {
480 480 padding: 10px 7px;
481 481 }
482 482
483 483 .inline-form div {
484 484 max-width: 100%;
485 485 }
486 486
487 487 .overlay {
488 488 display: none;
489 489 position: absolute;
490 490 width: 100%;
491 491 text-align: center;
492 492 vertical-align: middle;
493 493 font-size: 16px;
494 494 background: none repeat scroll 0 0 white;
495 495
496 496 &.submitting {
497 497 display: block;
498 498 opacity: 0.5;
499 499 z-index: 100;
500 500 }
501 501 }
502 502 .comment-inline-form .overlay.submitting .overlay-text {
503 503 margin-top: 5%;
504 504 }
505 505
506 506 .comment-inline-form .clearfix,
507 507 .comment-form .clearfix {
508 508 .border-radius(@border-radius);
509 509 margin: 0px;
510 510 }
511 511
512 512 .comment-inline-form .comment-footer {
513 513 margin: 10px 0px 0px 0px;
514 514 }
515 515
516 516 .hide-inline-form-button {
517 517 margin-left: 5px;
518 518 }
519 519 .comment-button .hide-inline-form {
520 520 background: white;
521 521 }
522 522
523 523 .comment-area {
524 524 padding: 8px 12px;
525 525 border: 1px solid @grey5;
526 526 .border-radius(@border-radius);
527 527
528 528 .resolve-action {
529 529 padding: 1px 0px 0px 6px;
530 530 }
531 531
532 532 }
533 533
534 534 .comment-area-header .nav-links {
535 535 display: flex;
536 536 flex-flow: row wrap;
537 537 -webkit-flex-flow: row wrap;
538 538 width: 100%;
539 539 }
540 540
541 541 .comment-area-footer {
542 display: flex;
542 min-height: 30px;
543 543 }
544 544
545 545 .comment-footer .toolbar {
546 546
547 547 }
548 548
549 .comment-attachment-uploader {
550 border: 1px dashed white;
551 border-radius: @border-radius;
552 margin-top: -10px;
553
554 &.dz-drag-hover {
555 border-color: @grey3;
556 }
557
558 .dz-error-message {
559 padding-top: 0;
560 }
561 }
562
563 .comment-attachment-text {
564 clear: both;
565 font-size: 11px;
566 color: #8F8F8F;
567 width: 100%;
568 .pick-attachment {
569 color: #8F8F8F;
570 }
571 .pick-attachment:hover {
572 color: @rcblue;
573 }
574 }
575
549 576 .nav-links {
550 577 padding: 0;
551 578 margin: 0;
552 579 list-style: none;
553 580 height: auto;
554 581 border-bottom: 1px solid @grey5;
555 582 }
556 583 .nav-links li {
557 584 display: inline-block;
558 585 list-style-type: none;
559 586 }
560 587
561 588 .nav-links li a.disabled {
562 589 cursor: not-allowed;
563 590 }
564 591
565 592 .nav-links li.active a {
566 593 border-bottom: 2px solid @rcblue;
567 594 color: #000;
568 595 font-weight: 600;
569 596 }
570 597 .nav-links li a {
571 598 display: inline-block;
572 599 padding: 0px 10px 5px 10px;
573 600 margin-bottom: -1px;
574 601 font-size: 14px;
575 602 line-height: 28px;
576 603 color: #8f8f8f;
577 604 border-bottom: 2px solid transparent;
578 605 }
579 606
580 607 .toolbar-text {
581 608 float: left;
582 margin: -5px 0px 0px 0px;
583 609 font-size: 12px;
584 610 }
585 611
@@ -1,397 +1,398 b''
1 1
2 2 /** MODAL **/
3 3 .modal-open {
4 4 overflow:hidden;
5 5 }
6 6 body.modal-open, .modal-open .navbar-fixed-top, .modal-open .navbar-fixed-bottom {
7 7 margin-right:15px;
8 8 }
9 9 .modal {
10 10 position:fixed;
11 11 top:0;
12 12 right:0;
13 13 bottom:0;
14 14 left:0;
15 15 z-index:1040;
16 16 display:none;
17 17 overflow-y:scroll;
18 18 &.fade .modal-dialog {
19 19 -webkit-transform:translate(0,-25%);
20 20 -ms-transform:translate(0,-25%);
21 21 transform:translate(0,-25%);
22 22 -webkit-transition:-webkit-transform 0.3s ease-out;
23 23 -moz-transition:-moz-transform 0.3s ease-out;
24 24 -o-transition:-o-transform 0.3s ease-out;
25 25 transition:transform 0.3s ease-out;
26 26 }
27 27 &.in .modal-dialog {
28 28 -webkit-transform:translate(0,0);
29 29 -ms-transform:translate(0,0);
30 30 transform:translate(0,0);
31 31 }
32 32 }
33 33 .modal-dialog {
34 34 z-index:1050;
35 35 width:auto;
36 36 padding:10px;
37 37 margin-right:auto;
38 38 margin-left:auto;
39 39 }
40 40 .modal-content {
41 41 position:relative;
42 42 background-color:#ffffff;
43 43 border: @border-thickness solid rgba(0,0,0,0.2);
44 44 .border-radius(@border-radius);
45 45 outline:none;
46 46 -webkit-box-shadow:0 3px 9px rgba(0,0,0,0.5);
47 47 box-shadow:0 3px 9px rgba(0,0,0,0.5);
48 48 background-clip:padding-box;
49 49 }
50 50 .modal-backdrop {
51 51 position:fixed;
52 52 top:0;
53 53 right:0;
54 54 bottom:0;
55 55 left:0;
56 56 z-index:1030;
57 57 background-color:#000000;
58 58
59 59 &.modal-backdrop.fade {
60 60 opacity:0;
61 61 filter:alpha(opacity=0);
62 62 }
63 63 &.in {
64 64 opacity:0.5;
65 65 filter:alpha(opacity=50);
66 66 }
67 67 }
68 68 .modal-header {
69 69 min-height:16.428571429px;
70 70 padding:15px;
71 71 border-bottom: @border-thickness solid @grey6;
72 72 .close {
73 73 margin-top:-2px;
74 74 }
75 75 }
76 76 .modal-title {
77 77 margin:0;
78 78 line-height:1.428571429;
79 79 }
80 80 .modal-body {
81 81 position:relative;
82 82 padding:20px;
83 83 }
84 84 .modal-footer {
85 85 padding:19px 20px 20px;
86 86 margin-top:15px;
87 87 text-align:right;
88 88 border-top:1px solid #e5e5e5;
89 89 .btn + .btn {
90 90 margin-bottom:0;
91 91 margin-left:5px;
92 92 }
93 93 .btn-group .btn + .btn {
94 94 margin-left:-1px;
95 95 }
96 96 .btn-block + .btn-block {
97 97 margin-left:0;
98 98 }
99 99 &:before {
100 100 display:table;
101 101 content:" ";
102 102 }
103 103 &:after {
104 104 display:table;
105 105 content:" ";
106 106 clear:both;
107 107 }
108 108 }
109 109
110 110 /** MARKDOWN styling **/
111 111 div.markdown-block {
112 112 clear: both;
113 113 overflow: hidden;
114 114 margin: 0;
115 115 padding: 3px 15px 3px;
116 116 }
117 117
118 118 div.markdown-block h1,
119 119 div.markdown-block h2,
120 120 div.markdown-block h3,
121 121 div.markdown-block h4,
122 122 div.markdown-block h5,
123 123 div.markdown-block h6 {
124 124 border-bottom: none !important;
125 125 padding: 0 !important;
126 126 overflow: visible !important;
127 127 }
128 128
129 129 div.markdown-block h1,
130 130 div.markdown-block h2 {
131 131 border-bottom: 1px #e6e5e5 solid !important;
132 132 }
133 133
134 134 div.markdown-block h1 {
135 135 font-size: 32px;
136 136 margin: 15px 0 15px 0 !important;
137 137 padding-bottom: 5px !important;
138 138 }
139 139
140 140 div.markdown-block h2 {
141 141 font-size: 24px !important;
142 142 margin: 34px 0 10px 0 !important;
143 143 padding-top: 15px !important;
144 144 padding-bottom: 8px !important;
145 145 }
146 146
147 147 div.markdown-block h3 {
148 148 font-size: 18px !important;
149 149 margin: 30px 0 8px 0 !important;
150 150 padding-bottom: 2px !important;
151 151 }
152 152
153 153 div.markdown-block h4 {
154 154 font-size: 13px !important;
155 155 margin: 18px 0 3px 0 !important;
156 156 }
157 157
158 158 div.markdown-block h5 {
159 159 font-size: 12px !important;
160 160 margin: 15px 0 3px 0 !important;
161 161 }
162 162
163 163 div.markdown-block h6 {
164 164 font-size: 12px;
165 165 color: #777777;
166 166 margin: 15px 0 3px 0 !important;
167 167 }
168 168
169 169 div.markdown-block hr {
170 170 border: 0;
171 171 color: #e6e5e5;
172 172 background-color: #e6e5e5;
173 173 height: 3px;
174 174 margin-bottom: 13px;
175 175 }
176 176
177 177 div.markdown-block ol,
178 178 div.markdown-block ul,
179 179 div.markdown-block p,
180 180 div.markdown-block blockquote,
181 181 div.markdown-block dl,
182 182 div.markdown-block li,
183 183 div.markdown-block table {
184 184 margin: 3px 0px 13px 0px !important;
185 185 color: #424242 !important;
186 186 font-size: 13px !important;
187 187 font-family: @text-regular;
188 188 font-weight: normal !important;
189 189 overflow: visible !important;
190 190 line-height: 140% !important;
191 191 }
192 192
193 193 div.markdown-block pre {
194 194 margin: 3px 0px 13px 0px !important;
195 195 padding: .5em;
196 196 color: #424242 !important;
197 197 font-size: 13px !important;
198 198 overflow: visible !important;
199 199 line-height: 140% !important;
200 200 background-color: @grey7;
201 201 }
202 202
203 203 div.markdown-block img {
204 204 border-style: none;
205 205 background-color: #fff;
206 206 padding-right: 20px;
207 max-width: 100%;
207 208 }
208 209
209 210
210 211 div.markdown-block strong {
211 212 font-weight: 600;
212 213 margin: 0;
213 214 }
214 215
215 216 div.markdown-block ul.checkbox,
216 217 div.markdown-block ol.checkbox {
217 218 padding-left: 20px !important;
218 219 margin-top: 0px !important;
219 220 margin-bottom: 18px !important;
220 221 }
221 222
222 223 div.markdown-block ul,
223 224 div.markdown-block ol {
224 225 padding-left: 30px !important;
225 226 margin-top: 0px !important;
226 227 margin-bottom: 18px !important;
227 228 }
228 229
229 230 div.markdown-block ul.checkbox li,
230 231 div.markdown-block ol.checkbox li {
231 232 list-style: none !important;
232 233 margin: 6px !important;
233 234 padding: 0 !important;
234 235 }
235 236
236 237 div.markdown-block ul li,
237 238 div.markdown-block ol li {
238 239 list-style: disc !important;
239 240 margin: 6px !important;
240 241 padding: 0 !important;
241 242 }
242 243
243 244 div.markdown-block ol li {
244 245 list-style: decimal !important;
245 246 }
246 247
247 248 /*
248 249 div.markdown-block a,
249 250 div.markdown-block a:visited {
250 251 color: #4183C4 !important;
251 252 background-color: inherit;
252 253 text-decoration: none;
253 254 }
254 255 */
255 256
256 257 div.markdown-block #message {
257 258 .border-radius(@border-radius);
258 259 border: @border-thickness solid @grey5;
259 260 display: block;
260 261 width: 100%;
261 262 height: 60px;
262 263 margin: 6px 0px;
263 264 }
264 265
265 266 div.markdown-block button,
266 267 div.markdown-block #ws {
267 268 font-size: @basefontsize;
268 269 padding: 4px 6px;
269 270 .border-radius(@border-radius);
270 271 border: @border-thickness solid @grey5;
271 272 background-color: @grey6;
272 273 }
273 274
274 275 div.markdown-block code,
275 276 div.markdown-block pre,
276 277 div.markdown-block #ws,
277 278 div.markdown-block #message {
278 279 font-family: @text-monospace;
279 280 font-size: 11px;
280 281 .border-radius(@border-radius);
281 282 background-color: white;
282 283 color: @grey3;
283 284 }
284 285
285 286
286 287 div.markdown-block code {
287 288 border: @border-thickness solid @grey6;
288 289 margin: 0 2px;
289 290 padding: 0 5px;
290 291 }
291 292
292 293 div.markdown-block pre {
293 294 border: @border-thickness solid @grey5;
294 295 overflow: auto;
295 296 padding: .5em;
296 297 background-color: @grey7;
297 298 }
298 299
299 300 div.markdown-block pre > code {
300 301 border: 0;
301 302 margin: 0;
302 303 padding: 0;
303 304 }
304 305
305 306 /** RST STYLE **/
306 307 div.rst-block {
307 308 clear: both;
308 309 overflow: hidden;
309 310 margin: 0;
310 311 padding: 3px 15px 3px;
311 312 }
312 313
313 314 div.rst-block h2 {
314 315 font-weight: normal;
315 316 }
316 317
317 318 div.rst-block h1,
318 319 div.rst-block h2,
319 320 div.rst-block h3,
320 321 div.rst-block h4,
321 322 div.rst-block h5,
322 323 div.rst-block h6 {
323 324 border-bottom: 0 !important;
324 325 margin: 0 !important;
325 326 padding: 0 !important;
326 327 line-height: 1.5em !important;
327 328 }
328 329
329 330
330 331 div.rst-block h1:first-child {
331 332 padding-top: .25em !important;
332 333 }
333 334
334 335 div.rst-block h2,
335 336 div.rst-block h3 {
336 337 margin: 1em 0 !important;
337 338 }
338 339
339 340 div.rst-block h1,
340 341 div.rst-block h2 {
341 342 border-bottom: 1px #e6e5e5 solid !important;
342 343 }
343 344
344 345 div.rst-block h2 {
345 346 margin-top: 1.5em !important;
346 347 padding-top: .5em !important;
347 348 }
348 349
349 350 div.rst-block p {
350 351 color: black !important;
351 352 margin: 1em 0 !important;
352 353 line-height: 1.5em !important;
353 354 }
354 355
355 356 div.rst-block ul {
356 357 list-style: disc !important;
357 358 margin: 1em 0 1em 2em !important;
358 359 clear: both;
359 360 }
360 361
361 362 div.rst-block ol {
362 363 list-style: decimal;
363 364 margin: 1em 0 1em 2em !important;
364 365 }
365 366
366 367 div.rst-block pre,
367 368 div.rst-block code {
368 369 font: 12px "Bitstream Vera Sans Mono","Courier",monospace;
369 370 }
370 371
371 372 div.rst-block code {
372 373 font-size: 12px !important;
373 374 background-color: ghostWhite !important;
374 375 color: #444 !important;
375 376 padding: 0 .2em !important;
376 377 border: 1px solid #dedede !important;
377 378 }
378 379
379 380 div.rst-block pre code {
380 381 padding: 0 !important;
381 382 font-size: 12px !important;
382 383 background-color: #eee !important;
383 384 border: none !important;
384 385 }
385 386
386 387 div.rst-block pre {
387 388 margin: 1em 0;
388 389 padding: @padding;
389 390 border: 1px solid @grey6;
390 391 .border-radius(@border-radius);
391 392 overflow: auto;
392 393 font-size: 12px;
393 394 color: #444;
394 395 background-color: @grey7;
395 396 }
396 397
397 398
@@ -1,510 +1,514 b''
1 1
2 2 // tables.less
3 3 // For use in RhodeCode application tables;
4 4 // see style guide documentation for guidelines.
5 5
6 6 // TABLES
7 7
8 8 .rctable,
9 9 table.rctable,
10 10 table.dataTable {
11 11 clear:both;
12 12 width: 100%;
13 13 margin: 0 auto @padding;
14 14 padding: 0;
15 15 vertical-align: baseline;
16 16 line-height:1.5em;
17 17 border: none;
18 18 outline: none;
19 19 border-collapse: collapse;
20 20 border-spacing: 0;
21 21 color: @grey2;
22 22
23 23 b {
24 24 font-weight: normal;
25 25 }
26 26
27 27 em {
28 28 font-weight: bold;
29 29 font-style: normal;
30 30 }
31 31
32 32 th,
33 33 td {
34 34 height: auto;
35 35 max-width: 20%;
36 36 padding: .65em 0 .65em 1em;
37 37 vertical-align: middle;
38 38 border-bottom: @border-thickness solid @grey5;
39 39 white-space: normal;
40 40
41 41 &.td-radio,
42 42 &.td-checkbox {
43 43 padding-right: 0;
44 44 text-align: center;
45 45
46 46 input {
47 47 margin: 0 1em;
48 48 }
49 49 }
50 50
51 51 &.truncate-wrap {
52 52 white-space: nowrap !important;
53 53 }
54 54
55 55 pre {
56 56 margin: 0;
57 57 }
58 58
59 59 .show_more {
60 60 height: inherit;
61 61 }
62 62 }
63 63
64 64 .expired td {
65 65 background-color: @grey7;
66 66 }
67 67 .inactive td {
68 68 background-color: @grey6;
69 69 }
70 70 th {
71 71 text-align: left;
72 72 font-weight: @text-semibold-weight;
73 73 font-family: @text-semibold;
74 74 }
75 75
76 76 .hl {
77 77 td {
78 78 background-color: lighten(@alert4,25%);
79 79 }
80 80 }
81 81
82 82 // Special Data Cell Types
83 83 // See style guide for desciptions and examples.
84 84
85 85 td {
86 86
87 87 &.user {
88 88 padding-left: 1em;
89 89 }
90 90
91 91 &.td-rss {
92 92 width: 20px;
93 93 min-width: 0;
94 94 margin: 0;
95 95 }
96 96
97 97 &.quick_repo_menu {
98 98 width: 15px;
99 99 text-align: center;
100 100
101 101 &:hover {
102 102 background-color: @grey5;
103 103 }
104 104 }
105 105
106 106 &.td-icon {
107 107 min-width: 20px;
108 108 width: 20px;
109 109 }
110 110
111 111 &.td-hash {
112 112 min-width: 80px;
113 113 width: 200px;
114 114
115 115 .obsolete {
116 116 text-decoration: line-through;
117 117 color: lighten(@grey2,25%);
118 118 }
119 119 }
120 120
121 &.td-sha {
122 white-space: nowrap;
123 }
124
121 125 &.td-graphbox {
122 126 width: 100px;
123 127 max-width: 100px;
124 128 min-width: 100px;
125 129 }
126 130
127 131 &.td-time {
128 132 width: 160px;
129 133 white-space: nowrap;
130 134 }
131 135
132 136 &.annotate{
133 137 padding-right: 0;
134 138
135 139 div.annotatediv{
136 140 margin: 0 0.7em;
137 141 }
138 142 }
139 143
140 144 &.tags-col {
141 145 padding-right: 0;
142 146 }
143 147
144 148 &.td-description {
145 149 min-width: 350px;
146 150
147 151 &.truncate, .truncate-wrap {
148 152 white-space: nowrap;
149 153 overflow: hidden;
150 154 text-overflow: ellipsis;
151 155 max-width: 350px;
152 156 }
153 157 }
154 158
155 159 &.td-grid-name {
156 160 white-space: nowrap;
157 161 min-width: 300px;
158 162 }
159 163
160 164 &.td-componentname {
161 165 white-space: nowrap;
162 166 }
163 167
164 168 &.td-name {
165 169
166 170 }
167 171
168 172 &.td-journalaction {
169 173 min-width: 300px;
170 174
171 175 .journal_action_params {
172 176 // waiting for feedback
173 177 }
174 178 }
175 179
176 180 &.td-active {
177 181 padding-left: .65em;
178 182 }
179 183
180 184 &.td-url {
181 185 white-space: nowrap;
182 186 }
183 187
184 188 &.td-comments {
185 189 min-width: 3em;
186 190 }
187 191
188 192 &.td-buttons {
189 193 padding: .3em 0;
190 194 }
191 195 &.td-align-top {
192 196 vertical-align: text-top
193 197 }
194 198 &.td-action {
195 199 // this is for the remove/delete/edit buttons
196 200 padding-right: 0;
197 201 min-width: 95px;
198 202 text-transform: capitalize;
199 203
200 204 i {
201 205 display: none;
202 206 }
203 207 }
204 208
205 209 // TODO: lisa: this needs to be cleaned up with the buttons
206 210 .grid_edit,
207 211 .grid_delete {
208 212 display: inline-block;
209 213 margin: 0 @padding/3 0 0;
210 214 font-family: @text-light;
211 215
212 216 i {
213 217 display: none;
214 218 }
215 219 }
216 220
217 221 .grid_edit + .grid_delete {
218 222 border-left: @border-thickness solid @grey5;
219 223 padding-left: @padding/2;
220 224 }
221 225
222 226 &.td-compare {
223 227
224 228 input {
225 229 margin-right: 1em;
226 230 }
227 231
228 232 .compare-radio-button {
229 233 margin: 0 1em 0 0;
230 234 }
231 235
232 236
233 237 }
234 238
235 239 &.td-tags {
236 240 padding: .5em 1em .5em 0;
237 241 width: 140px;
238 242
239 243 .tag {
240 244 margin: 1px;
241 245 float: left;
242 246 }
243 247 }
244 248
245 249 .icon-svn, .icon-hg, .icon-git {
246 250 font-size: 1.4em;
247 251 }
248 252
249 253 &.collapse_commit,
250 254 &.expand_commit {
251 255 padding-right: 0;
252 256 padding-left: 1em;
253 257 cursor: pointer;
254 258 width: 20px;
255 259 }
256 260 }
257 261
258 262 .perm_admin_row {
259 263 color: @grey4;
260 264 background-color: @grey6;
261 265 }
262 266
263 267 .noborder {
264 268 border: none;
265 269
266 270 td {
267 271 border: none;
268 272 }
269 273 }
270 274 }
271 275 .rctable.audit-log {
272 276 td {
273 277 vertical-align: top;
274 278 }
275 279 }
276 280
277 281 // TRUNCATING
278 282 // TODO: lisaq: should this possibly be moved out of tables.less?
279 283 // for truncated text
280 284 // used inside of table cells and in code block headers
281 285 .truncate-wrap {
282 286 white-space: nowrap !important;
283 287
284 288 //truncated text
285 289 .truncate {
286 290 max-width: 450px;
287 291 width: 300px;
288 292 overflow: hidden;
289 293 text-overflow: ellipsis;
290 294 -o-text-overflow: ellipsis;
291 295 -ms-text-overflow: ellipsis;
292 296
293 297 &.autoexpand {
294 298 width: 120px;
295 299 margin-right: 200px;
296 300 }
297 301 }
298 302 &:hover .truncate.autoexpand {
299 303 overflow: visible;
300 304 }
301 305
302 306 .tags-truncate {
303 307 width: 150px;
304 308 height: 22px;
305 309 overflow: hidden;
306 310
307 311 .tag {
308 312 display: inline-block;
309 313 }
310 314
311 315 &.truncate {
312 316 height: 22px;
313 317 max-height:2em;
314 318 width: 140px;
315 319 }
316 320 }
317 321 }
318 322
319 323 .apikeys_wrap {
320 324 margin-bottom: @padding;
321 325
322 326 table.rctable td:first-child {
323 327 width: 340px;
324 328 }
325 329 }
326 330
327 331
328 332
329 333 // SPECIAL CASES
330 334
331 335 // Repository Followers
332 336 table.rctable.followers_data {
333 337 width: 75%;
334 338 margin: 0;
335 339 }
336 340
337 341 // Repository List
338 342 // Group Members List
339 343 table.rctable.group_members,
340 344 table#repo_list_table {
341 345 min-width: 600px;
342 346 }
343 347
344 348 // Keyboard mappings
345 349 table.keyboard-mappings {
346 350 th {
347 351 text-align: left;
348 352 font-weight: @text-semibold-weight;
349 353 font-family: @text-semibold;
350 354 }
351 355 }
352 356
353 357 // Branches, Tags, and Bookmarks
354 358 #obj_list_table.dataTable {
355 359 td.td-time {
356 360 padding-right: 1em;
357 361 }
358 362 }
359 363
360 364 // User Admin
361 365 .rctable.useremails,
362 366 .rctable.account_emails {
363 367 .tag,
364 368 .btn {
365 369 float: right;
366 370 }
367 371 .btn { //to line up with tags
368 372 margin-right: 1.65em;
369 373 }
370 374 }
371 375
372 376 // User List
373 377 #user_list_table {
374 378
375 379 td.td-user {
376 380 min-width: 100px;
377 381 }
378 382 }
379 383
380 384 // Pull Request List Table
381 385 #pull_request_list_table.dataTable {
382 386
383 387 //TODO: lisa: This needs to be removed once the description is adjusted
384 388 // for using an expand_commit button (see issue 765)
385 389 td {
386 390 vertical-align: middle;
387 391 }
388 392 }
389 393
390 394 // Settings (no border)
391 395 table.rctable.dl-settings {
392 396 td {
393 397 border: none;
394 398 vertical-align: baseline;
395 399 }
396 400 }
397 401
398 402
399 403 // Statistics
400 404 table.trending_language_tbl {
401 405 width: 100%;
402 406 line-height: 1em;
403 407
404 408 td div {
405 409 overflow: visible;
406 410 }
407 411 }
408 412
409 413 .trending_language_tbl, .trending_language_tbl td {
410 414 border: 0;
411 415 margin: 0;
412 416 padding: 0;
413 417 background: transparent;
414 418 }
415 419
416 420 .trending_language_tbl, .trending_language_tbl tr {
417 421 border-spacing: 0 3px;
418 422 }
419 423
420 424 .trending_language {
421 425 position: relative;
422 426 overflow: hidden;
423 427 color: @text-color;
424 428 width: 400px;
425 429
426 430 .lang-bar {
427 431 z-index: 1;
428 432 overflow: hidden;
429 433 background-color: @rcblue;
430 434 color: #FFF;
431 435 text-decoration: none;
432 436 }
433 437
434 438 }
435 439
436 440 // Changesets
437 441 #changesets.rctable {
438 442 th {
439 443 padding: 0 1em 0.65em 0;
440 444 }
441 445
442 446 // td must be fixed height for graph
443 447 td {
444 448 height: 32px;
445 449 padding: 0 1em 0 0;
446 450 vertical-align: middle;
447 451 white-space: nowrap;
448 452
449 453 &.td-description {
450 454 white-space: normal;
451 455 }
452 456
453 457 &.expand_commit {
454 458 padding-right: 0;
455 459 cursor: pointer;
456 460 width: 20px;
457 461 }
458 462 }
459 463 }
460 464
461 465 // Compare
462 466 table.compare_view_commits {
463 467 margin-top: @space;
464 468
465 469 td.td-time {
466 470 padding-left: .5em;
467 471 }
468 472
469 473 // special case to not show hover actions on hidden indicator
470 474 tr.compare_select_hidden:hover {
471 475 cursor: inherit;
472 476
473 477 td {
474 478 background-color: inherit;
475 479 }
476 480 }
477 481
478 482 tr:hover {
479 483 cursor: pointer;
480 484
481 485 td {
482 486 background-color: lighten(@alert4,25%);
483 487 }
484 488 }
485 489
486 490
487 491 }
488 492
489 493 .file_history {
490 494 td.td-actions {
491 495 text-align: right;
492 496 }
493 497 }
494 498
495 499
496 500 // Gist List
497 501 #gist_list_table {
498 502 td {
499 503 vertical-align: middle;
500 504
501 505 div{
502 506 display: inline-block;
503 507 vertical-align: middle;
504 508 }
505 509
506 510 img{
507 511 vertical-align: middle;
508 512 }
509 513 }
510 514 }
@@ -1,380 +1,381 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('favicon', '/favicon.ico', []);
16 16 pyroutes.register('robots', '/robots.txt', []);
17 17 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
18 18 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
19 19 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
20 20 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
21 21 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
22 22 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
23 23 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/_settings/integrations', ['repo_group_name']);
24 24 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
25 25 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
26 26 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/_settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
27 27 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/_settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
28 28 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
29 29 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
30 30 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
31 31 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
32 32 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
33 33 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
34 34 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
35 35 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
36 36 pyroutes.register('ops_ping_legacy', '/_admin/ping', []);
37 37 pyroutes.register('ops_error_test_legacy', '/_admin/error_test', []);
38 38 pyroutes.register('admin_home', '/_admin', []);
39 39 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
40 40 pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
41 41 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
42 42 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
43 43 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
44 44 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
45 45 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
46 46 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
47 47 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
48 48 pyroutes.register('admin_settings_exception_tracker', '/_admin/settings/exceptions', []);
49 49 pyroutes.register('admin_settings_exception_tracker_delete_all', '/_admin/settings/exceptions/delete', []);
50 50 pyroutes.register('admin_settings_exception_tracker_show', '/_admin/settings/exceptions/%(exception_id)s', ['exception_id']);
51 51 pyroutes.register('admin_settings_exception_tracker_delete', '/_admin/settings/exceptions/%(exception_id)s/delete', ['exception_id']);
52 52 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
53 53 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
54 54 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
55 55 pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []);
56 56 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
57 57 pyroutes.register('admin_settings_process_management_master_signal', '/_admin/settings/process_management/master_signal', []);
58 58 pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);
59 59 pyroutes.register('admin_defaults_repositories_update', '/_admin/defaults/repositories/update', []);
60 60 pyroutes.register('admin_settings', '/_admin/settings', []);
61 61 pyroutes.register('admin_settings_update', '/_admin/settings/update', []);
62 62 pyroutes.register('admin_settings_global', '/_admin/settings/global', []);
63 63 pyroutes.register('admin_settings_global_update', '/_admin/settings/global/update', []);
64 64 pyroutes.register('admin_settings_vcs', '/_admin/settings/vcs', []);
65 65 pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []);
66 66 pyroutes.register('admin_settings_vcs_svn_pattern_delete', '/_admin/settings/vcs/svn_pattern_delete', []);
67 67 pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []);
68 68 pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []);
69 69 pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []);
70 70 pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []);
71 71 pyroutes.register('admin_settings_issuetracker', '/_admin/settings/issue-tracker', []);
72 72 pyroutes.register('admin_settings_issuetracker_update', '/_admin/settings/issue-tracker/update', []);
73 73 pyroutes.register('admin_settings_issuetracker_test', '/_admin/settings/issue-tracker/test', []);
74 74 pyroutes.register('admin_settings_issuetracker_delete', '/_admin/settings/issue-tracker/delete', []);
75 75 pyroutes.register('admin_settings_email', '/_admin/settings/email', []);
76 76 pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []);
77 77 pyroutes.register('admin_settings_hooks', '/_admin/settings/hooks', []);
78 78 pyroutes.register('admin_settings_hooks_update', '/_admin/settings/hooks/update', []);
79 79 pyroutes.register('admin_settings_hooks_delete', '/_admin/settings/hooks/delete', []);
80 80 pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
81 81 pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
82 82 pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
83 83 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
84 84 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
85 85 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
86 86 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
87 87 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
88 88 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
89 89 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
90 90 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
91 91 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
92 92 pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
93 93 pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
94 94 pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
95 95 pyroutes.register('users', '/_admin/users', []);
96 96 pyroutes.register('users_data', '/_admin/users_data', []);
97 97 pyroutes.register('users_create', '/_admin/users/create', []);
98 98 pyroutes.register('users_new', '/_admin/users/new', []);
99 99 pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']);
100 100 pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']);
101 101 pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']);
102 102 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
103 103 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
104 104 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
105 105 pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
106 106 pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
107 107 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
108 108 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
109 109 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
110 110 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
111 111 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
112 112 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
113 113 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
114 114 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
115 115 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
116 116 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
117 117 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
118 118 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
119 119 pyroutes.register('edit_user_perms_summary', '/_admin/users/%(user_id)s/edit/permissions_summary', ['user_id']);
120 120 pyroutes.register('edit_user_perms_summary_json', '/_admin/users/%(user_id)s/edit/permissions_summary/json', ['user_id']);
121 121 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
122 122 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
123 123 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
124 124 pyroutes.register('edit_user_audit_logs_download', '/_admin/users/%(user_id)s/edit/audit/download', ['user_id']);
125 125 pyroutes.register('edit_user_caches', '/_admin/users/%(user_id)s/edit/caches', ['user_id']);
126 126 pyroutes.register('edit_user_caches_update', '/_admin/users/%(user_id)s/edit/caches/update', ['user_id']);
127 127 pyroutes.register('user_groups', '/_admin/user_groups', []);
128 128 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
129 129 pyroutes.register('user_groups_new', '/_admin/user_groups/new', []);
130 130 pyroutes.register('user_groups_create', '/_admin/user_groups/create', []);
131 131 pyroutes.register('repos', '/_admin/repos', []);
132 132 pyroutes.register('repo_new', '/_admin/repos/new', []);
133 133 pyroutes.register('repo_create', '/_admin/repos/create', []);
134 134 pyroutes.register('repo_groups', '/_admin/repo_groups', []);
135 135 pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []);
136 136 pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
137 137 pyroutes.register('repo_group_create', '/_admin/repo_group/create', []);
138 138 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
139 139 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
140 140 pyroutes.register('channelstream_proxy', '/_channelstream', []);
141 141 pyroutes.register('upload_file', '/_file_store/upload', []);
142 142 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
143 143 pyroutes.register('logout', '/_admin/logout', []);
144 144 pyroutes.register('reset_password', '/_admin/password_reset', []);
145 145 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
146 146 pyroutes.register('home', '/', []);
147 147 pyroutes.register('user_autocomplete_data', '/_users', []);
148 148 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
149 149 pyroutes.register('repo_list_data', '/_repos', []);
150 150 pyroutes.register('repo_group_list_data', '/_repo_groups', []);
151 151 pyroutes.register('goto_switcher_data', '/_goto_data', []);
152 152 pyroutes.register('markup_preview', '/_markup_preview', []);
153 153 pyroutes.register('file_preview', '/_file_preview', []);
154 154 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
155 155 pyroutes.register('journal', '/_admin/journal', []);
156 156 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
157 157 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
158 158 pyroutes.register('journal_public', '/_admin/public_journal', []);
159 159 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
160 160 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
161 161 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
162 162 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
163 163 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
164 164 pyroutes.register('repo_creating', '/%(repo_name)s/repo_creating', ['repo_name']);
165 165 pyroutes.register('repo_creating_check', '/%(repo_name)s/repo_creating_check', ['repo_name']);
166 166 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
167 167 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
168 168 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
169 169 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
170 170 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
171 171 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
172 172 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
173 173 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
174 174 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
175 175 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
176 176 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
177 pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
177 178 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
178 179 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
179 180 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
180 181 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
181 182 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
182 183 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
183 184 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
184 185 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
185 186 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
186 187 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
187 188 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
188 189 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
189 190 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
190 191 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
191 192 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
192 193 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
193 194 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
194 195 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
195 196 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
196 197 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
197 198 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
198 199 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
199 200 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
200 201 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
201 202 pyroutes.register('repo_files_upload_file', '/%(repo_name)s/upload_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
202 203 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
203 204 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
204 205 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
205 206 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
206 207 pyroutes.register('repo_commits', '/%(repo_name)s/commits', ['repo_name']);
207 208 pyroutes.register('repo_commits_file', '/%(repo_name)s/commits/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
208 209 pyroutes.register('repo_commits_elements', '/%(repo_name)s/commits_elements', ['repo_name']);
209 210 pyroutes.register('repo_commits_elements_file', '/%(repo_name)s/commits_elements/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
210 211 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
211 212 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
212 213 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
213 214 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']);
214 215 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
215 216 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
216 217 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
217 218 pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']);
218 219 pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']);
219 220 pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']);
220 221 pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']);
221 222 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
222 223 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
223 224 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
224 225 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
225 226 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
226 227 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
227 228 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
228 229 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
229 230 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
230 231 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
231 232 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
232 233 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']);
233 234 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
234 235 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
235 236 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
236 237 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
237 238 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
238 239 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
239 240 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
240 241 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
241 242 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
242 243 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
243 244 pyroutes.register('edit_repo_perms_set_private', '/%(repo_name)s/settings/permissions/set_private', ['repo_name']);
244 245 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
245 246 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
246 247 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
247 248 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
248 249 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
249 250 pyroutes.register('repo_edit_toggle_locking', '/%(repo_name)s/settings/toggle_locking', ['repo_name']);
250 251 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
251 252 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
252 253 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
253 254 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
254 255 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
255 256 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
256 257 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
257 258 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
258 259 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
259 260 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
260 261 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
261 262 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
262 263 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
263 264 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
264 265 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
265 266 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
266 267 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
267 268 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed-rss', ['repo_name']);
268 269 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed-atom', ['repo_name']);
269 270 pyroutes.register('rss_feed_home_old', '/%(repo_name)s/feed/rss', ['repo_name']);
270 271 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
271 272 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
272 273 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
273 274 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
274 275 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
275 276 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
276 277 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
277 278 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
278 279 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
279 280 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
280 281 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
281 282 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
282 283 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['user_group_id']);
283 284 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
284 285 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
285 286 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
286 287 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['user_group_id']);
287 288 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
288 289 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
289 290 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
290 291 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
291 292 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
292 293 pyroutes.register('search', '/_admin/search', []);
293 294 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
294 295 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
295 296 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
296 297 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
297 298 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
298 299 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
299 300 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
300 301 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
301 302 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
302 303 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
303 304 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
304 305 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
305 306 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
306 307 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
307 308 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
308 309 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
309 310 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
310 311 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
311 312 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
312 313 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
313 314 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
314 315 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
315 316 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
316 317 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
317 318 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
318 319 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
319 320 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
320 321 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
321 322 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
322 323 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
323 324 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
324 325 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
325 326 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
326 327 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
327 328 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
328 329 pyroutes.register('gists_show', '/_admin/gists', []);
329 330 pyroutes.register('gists_new', '/_admin/gists/new', []);
330 331 pyroutes.register('gists_create', '/_admin/gists/create', []);
331 332 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
332 333 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
333 334 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
334 335 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
335 336 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
336 337 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
337 338 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
338 339 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
339 340 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
340 341 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
341 342 pyroutes.register('apiv2', '/_admin/api', []);
342 343 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
343 344 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
344 345 pyroutes.register('login', '/_admin/login', []);
345 346 pyroutes.register('register', '/_admin/register', []);
346 347 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
347 348 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
348 349 pyroutes.register('repo_reviewers_review_rule_delete', '/%(repo_name)s/settings/review/rules/%(rule_id)s/delete', ['repo_name', 'rule_id']);
349 350 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
350 351 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
351 352 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
352 353 pyroutes.register('admin_settings_scheduler_show_tasks', '/_admin/settings/scheduler/_tasks', []);
353 354 pyroutes.register('admin_settings_scheduler_show_all', '/_admin/settings/scheduler', []);
354 355 pyroutes.register('admin_settings_scheduler_new', '/_admin/settings/scheduler/new', []);
355 356 pyroutes.register('admin_settings_scheduler_create', '/_admin/settings/scheduler/create', []);
356 357 pyroutes.register('admin_settings_scheduler_edit', '/_admin/settings/scheduler/%(schedule_id)s', ['schedule_id']);
357 358 pyroutes.register('admin_settings_scheduler_update', '/_admin/settings/scheduler/%(schedule_id)s/update', ['schedule_id']);
358 359 pyroutes.register('admin_settings_scheduler_delete', '/_admin/settings/scheduler/%(schedule_id)s/delete', ['schedule_id']);
359 360 pyroutes.register('admin_settings_scheduler_execute', '/_admin/settings/scheduler/%(schedule_id)s/execute', ['schedule_id']);
360 361 pyroutes.register('admin_settings_automation', '/_admin/settings/automation', []);
361 362 pyroutes.register('admin_settings_automation_update', '/_admin/settings/automation/%(entry_id)s/update', ['entry_id']);
362 363 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
363 364 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
364 365 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
365 366 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
366 367 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
367 368 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
368 369 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
369 370 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
370 371 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
371 372 pyroutes.register('repo_artifacts_get', '/%(repo_name)s/artifacts/download/%(uid)s', ['repo_name', 'uid']);
372 373 pyroutes.register('repo_artifacts_store', '/%(repo_name)s/artifacts/store', ['repo_name']);
373 374 pyroutes.register('repo_artifacts_info', '/%(repo_name)s/artifacts/info/%(uid)s', ['repo_name', 'uid']);
374 375 pyroutes.register('repo_artifacts_delete', '/%(repo_name)s/artifacts/delete/%(uid)s', ['repo_name', 'uid']);
375 376 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
376 377 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
377 378 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
378 379 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
379 380 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
380 381 }
@@ -1,838 +1,927 b''
1 1 // # Copyright (C) 2010-2019 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 var firefoxAnchorFix = function() {
20 20 // hack to make anchor links behave properly on firefox, in our inline
21 21 // comments generation when comments are injected firefox is misbehaving
22 22 // when jumping to anchor links
23 23 if (location.href.indexOf('#') > -1) {
24 24 location.href += '';
25 25 }
26 26 };
27 27
28 28 var linkifyComments = function(comments) {
29 29 var firstCommentId = null;
30 30 if (comments) {
31 31 firstCommentId = $(comments[0]).data('comment-id');
32 32 }
33 33
34 34 if (firstCommentId){
35 35 $('#inline-comments-counter').attr('href', '#comment-' + firstCommentId);
36 36 }
37 37 };
38 38
39 39 var bindToggleButtons = function() {
40 40 $('.comment-toggle').on('click', function() {
41 41 $(this).parent().nextUntil('tr.line').toggle('inline-comments');
42 42 });
43 43 };
44 44
45 45
46 46
47 47 var _submitAjaxPOST = function(url, postData, successHandler, failHandler) {
48 48 failHandler = failHandler || function() {};
49 49 postData = toQueryString(postData);
50 50 var request = $.ajax({
51 51 url: url,
52 52 type: 'POST',
53 53 data: postData,
54 54 headers: {'X-PARTIAL-XHR': true}
55 55 })
56 56 .done(function (data) {
57 57 successHandler(data);
58 58 })
59 59 .fail(function (data, textStatus, errorThrown) {
60 60 failHandler(data, textStatus, errorThrown)
61 61 });
62 62 return request;
63 63 };
64 64
65 65
66 66
67 67
68 68 /* Comment form for main and inline comments */
69 69 (function(mod) {
70 70
71 71 if (typeof exports == "object" && typeof module == "object") {
72 72 // CommonJS
73 73 module.exports = mod();
74 74 }
75 75 else {
76 76 // Plain browser env
77 77 (this || window).CommentForm = mod();
78 78 }
79 79
80 80 })(function() {
81 81 "use strict";
82 82
83 83 function CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId) {
84 84 if (!(this instanceof CommentForm)) {
85 85 return new CommentForm(formElement, commitId, pullRequestId, lineNo, initAutocompleteActions, resolvesCommentId);
86 86 }
87 87
88 88 // bind the element instance to our Form
89 89 $(formElement).get(0).CommentForm = this;
90 90
91 91 this.withLineNo = function(selector) {
92 92 var lineNo = this.lineNo;
93 93 if (lineNo === undefined) {
94 94 return selector
95 95 } else {
96 96 return selector + '_' + lineNo;
97 97 }
98 98 };
99 99
100 100 this.commitId = commitId;
101 101 this.pullRequestId = pullRequestId;
102 102 this.lineNo = lineNo;
103 103 this.initAutocompleteActions = initAutocompleteActions;
104 104
105 105 this.previewButton = this.withLineNo('#preview-btn');
106 106 this.previewContainer = this.withLineNo('#preview-container');
107 107
108 108 this.previewBoxSelector = this.withLineNo('#preview-box');
109 109
110 110 this.editButton = this.withLineNo('#edit-btn');
111 111 this.editContainer = this.withLineNo('#edit-container');
112 112 this.cancelButton = this.withLineNo('#cancel-btn');
113 113 this.commentType = this.withLineNo('#comment_type');
114 114
115 115 this.resolvesId = null;
116 116 this.resolvesActionId = null;
117 117
118 118 this.closesPr = '#close_pull_request';
119 119
120 120 this.cmBox = this.withLineNo('#text');
121 121 this.cm = initCommentBoxCodeMirror(this, this.cmBox, this.initAutocompleteActions);
122 122
123 123 this.statusChange = this.withLineNo('#change_status');
124 124
125 125 this.submitForm = formElement;
126 126 this.submitButton = $(this.submitForm).find('input[type="submit"]');
127 127 this.submitButtonText = this.submitButton.val();
128 128
129 129 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
130 130 {'repo_name': templateContext.repo_name,
131 131 'commit_id': templateContext.commit_data.commit_id});
132 132
133 133 if (resolvesCommentId){
134 134 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
135 135 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
136 136 $(this.commentType).prop('disabled', true);
137 137 $(this.commentType).addClass('disabled');
138 138
139 139 // disable select
140 140 setTimeout(function() {
141 141 $(self.statusChange).select2('readonly', true);
142 142 }, 10);
143 143
144 144 var resolvedInfo = (
145 145 '<li class="resolve-action">' +
146 146 '<input type="hidden" id="resolve_comment_{0}" name="resolve_comment_{0}" value="{0}">' +
147 147 '<button id="resolve_comment_action_{0}" class="resolve-text btn btn-sm" onclick="return Rhodecode.comments.submitResolution({0})">{1} #{0}</button>' +
148 148 '</li>'
149 149 ).format(resolvesCommentId, _gettext('resolve comment'));
150 150 $(resolvedInfo).insertAfter($(this.commentType).parent());
151 151 }
152 152
153 153 // based on commitId, or pullRequestId decide where do we submit
154 154 // out data
155 155 if (this.commitId){
156 156 this.submitUrl = pyroutes.url('repo_commit_comment_create',
157 157 {'repo_name': templateContext.repo_name,
158 158 'commit_id': this.commitId});
159 159 this.selfUrl = pyroutes.url('repo_commit',
160 160 {'repo_name': templateContext.repo_name,
161 161 'commit_id': this.commitId});
162 162
163 163 } else if (this.pullRequestId) {
164 164 this.submitUrl = pyroutes.url('pullrequest_comment_create',
165 165 {'repo_name': templateContext.repo_name,
166 166 'pull_request_id': this.pullRequestId});
167 167 this.selfUrl = pyroutes.url('pullrequest_show',
168 168 {'repo_name': templateContext.repo_name,
169 169 'pull_request_id': this.pullRequestId});
170 170
171 171 } else {
172 172 throw new Error(
173 173 'CommentForm requires pullRequestId, or commitId to be specified.')
174 174 }
175 175
176 176 // FUNCTIONS and helpers
177 177 var self = this;
178 178
179 179 this.isInline = function(){
180 180 return this.lineNo && this.lineNo != 'general';
181 181 };
182 182
183 183 this.getCmInstance = function(){
184 184 return this.cm
185 185 };
186 186
187 187 this.setPlaceholder = function(placeholder) {
188 188 var cm = this.getCmInstance();
189 189 if (cm){
190 190 cm.setOption('placeholder', placeholder);
191 191 }
192 192 };
193 193
194 194 this.getCommentStatus = function() {
195 195 return $(this.submitForm).find(this.statusChange).val();
196 196 };
197 197 this.getCommentType = function() {
198 198 return $(this.submitForm).find(this.commentType).val();
199 199 };
200 200
201 201 this.getResolvesId = function() {
202 202 return $(this.submitForm).find(this.resolvesId).val() || null;
203 203 };
204 204
205 205 this.getClosePr = function() {
206 206 return $(this.submitForm).find(this.closesPr).val() || null;
207 207 };
208 208
209 209 this.markCommentResolved = function(resolvedCommentId){
210 210 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolved').show();
211 211 $('#comment-label-{0}'.format(resolvedCommentId)).find('.resolve').hide();
212 212 };
213 213
214 214 this.isAllowedToSubmit = function() {
215 215 return !$(this.submitButton).prop('disabled');
216 216 };
217 217
218 218 this.initStatusChangeSelector = function(){
219 219 var formatChangeStatus = function(state, escapeMarkup) {
220 220 var originalOption = state.element;
221 221 var tmpl = '<i class="icon-circle review-status-{0}"></i><span>{1}</span>'.format($(originalOption).data('status'), escapeMarkup(state.text));
222 222 return tmpl
223 223 };
224 224 var formatResult = function(result, container, query, escapeMarkup) {
225 225 return formatChangeStatus(result, escapeMarkup);
226 226 };
227 227
228 228 var formatSelection = function(data, container, escapeMarkup) {
229 229 return formatChangeStatus(data, escapeMarkup);
230 230 };
231 231
232 232 $(this.submitForm).find(this.statusChange).select2({
233 233 placeholder: _gettext('Status Review'),
234 234 formatResult: formatResult,
235 235 formatSelection: formatSelection,
236 236 containerCssClass: "drop-menu status_box_menu",
237 237 dropdownCssClass: "drop-menu-dropdown",
238 238 dropdownAutoWidth: true,
239 239 minimumResultsForSearch: -1
240 240 });
241 241 $(this.submitForm).find(this.statusChange).on('change', function() {
242 242 var status = self.getCommentStatus();
243 243
244 244 if (status && !self.isInline()) {
245 245 $(self.submitButton).prop('disabled', false);
246 246 }
247 247
248 248 var placeholderText = _gettext('Comment text will be set automatically based on currently selected status ({0}) ...').format(status);
249 249 self.setPlaceholder(placeholderText)
250 250 })
251 251 };
252 252
253 253 // reset the comment form into it's original state
254 254 this.resetCommentFormState = function(content) {
255 255 content = content || '';
256 256
257 257 $(this.editContainer).show();
258 258 $(this.editButton).parent().addClass('active');
259 259
260 260 $(this.previewContainer).hide();
261 261 $(this.previewButton).parent().removeClass('active');
262 262
263 263 this.setActionButtonsDisabled(true);
264 264 self.cm.setValue(content);
265 265 self.cm.setOption("readOnly", false);
266 266
267 267 if (this.resolvesId) {
268 268 // destroy the resolve action
269 269 $(this.resolvesId).parent().remove();
270 270 }
271 271 // reset closingPR flag
272 272 $('.close-pr-input').remove();
273 273
274 274 $(this.statusChange).select2('readonly', false);
275 275 };
276 276
277 277 this.globalSubmitSuccessCallback = function(){
278 278 // default behaviour is to call GLOBAL hook, if it's registered.
279 279 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
280 280 commentFormGlobalSubmitSuccessCallback()
281 281 }
282 282 };
283 283
284 284 this.submitAjaxPOST = function(url, postData, successHandler, failHandler) {
285 285 return _submitAjaxPOST(url, postData, successHandler, failHandler);
286 286 };
287 287
288 288 // overwrite a submitHandler, we need to do it for inline comments
289 289 this.setHandleFormSubmit = function(callback) {
290 290 this.handleFormSubmit = callback;
291 291 };
292 292
293 293 // overwrite a submitSuccessHandler
294 294 this.setGlobalSubmitSuccessCallback = function(callback) {
295 295 this.globalSubmitSuccessCallback = callback;
296 296 };
297 297
298 298 // default handler for for submit for main comments
299 299 this.handleFormSubmit = function() {
300 300 var text = self.cm.getValue();
301 301 var status = self.getCommentStatus();
302 302 var commentType = self.getCommentType();
303 303 var resolvesCommentId = self.getResolvesId();
304 304 var closePullRequest = self.getClosePr();
305 305
306 306 if (text === "" && !status) {
307 307 return;
308 308 }
309 309
310 310 var excludeCancelBtn = false;
311 311 var submitEvent = true;
312 312 self.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
313 313 self.cm.setOption("readOnly", true);
314 314
315 315 var postData = {
316 316 'text': text,
317 317 'changeset_status': status,
318 318 'comment_type': commentType,
319 319 'csrf_token': CSRF_TOKEN
320 320 };
321 321
322 322 if (resolvesCommentId) {
323 323 postData['resolves_comment_id'] = resolvesCommentId;
324 324 }
325 325
326 326 if (closePullRequest) {
327 327 postData['close_pull_request'] = true;
328 328 }
329 329
330 330 var submitSuccessCallback = function(o) {
331 331 // reload page if we change status for single commit.
332 332 if (status && self.commitId) {
333 333 location.reload(true);
334 334 } else {
335 335 $('#injected_page_comments').append(o.rendered_text);
336 336 self.resetCommentFormState();
337 337 timeagoActivate();
338 338
339 339 // mark visually which comment was resolved
340 340 if (resolvesCommentId) {
341 341 self.markCommentResolved(resolvesCommentId);
342 342 }
343 343 }
344 344
345 345 // run global callback on submit
346 346 self.globalSubmitSuccessCallback();
347 347
348 348 };
349 349 var submitFailCallback = function(data) {
350 350 alert(
351 351 "Error while submitting comment.\n" +
352 352 "Error code {0} ({1}).".format(data.status, data.statusText)
353 353 );
354 354 self.resetCommentFormState(text);
355 355 };
356 356 self.submitAjaxPOST(
357 357 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
358 358 };
359 359
360 360 this.previewSuccessCallback = function(o) {
361 361 $(self.previewBoxSelector).html(o);
362 362 $(self.previewBoxSelector).removeClass('unloaded');
363 363
364 364 // swap buttons, making preview active
365 365 $(self.previewButton).parent().addClass('active');
366 366 $(self.editButton).parent().removeClass('active');
367 367
368 368 // unlock buttons
369 369 self.setActionButtonsDisabled(false);
370 370 };
371 371
372 372 this.setActionButtonsDisabled = function(state, excludeCancelBtn, submitEvent) {
373 373 excludeCancelBtn = excludeCancelBtn || false;
374 374 submitEvent = submitEvent || false;
375 375
376 376 $(this.editButton).prop('disabled', state);
377 377 $(this.previewButton).prop('disabled', state);
378 378
379 379 if (!excludeCancelBtn) {
380 380 $(this.cancelButton).prop('disabled', state);
381 381 }
382 382
383 383 var submitState = state;
384 384 if (!submitEvent && this.getCommentStatus() && !self.isInline()) {
385 385 // if the value of commit review status is set, we allow
386 386 // submit button, but only on Main form, isInline means inline
387 387 submitState = false
388 388 }
389 389
390 390 $(this.submitButton).prop('disabled', submitState);
391 391 if (submitEvent) {
392 392 $(this.submitButton).val(_gettext('Submitting...'));
393 393 } else {
394 394 $(this.submitButton).val(this.submitButtonText);
395 395 }
396 396
397 397 };
398 398
399 399 // lock preview/edit/submit buttons on load, but exclude cancel button
400 400 var excludeCancelBtn = true;
401 401 this.setActionButtonsDisabled(true, excludeCancelBtn);
402 402
403 403 // anonymous users don't have access to initialized CM instance
404 404 if (this.cm !== undefined){
405 405 this.cm.on('change', function(cMirror) {
406 406 if (cMirror.getValue() === "") {
407 407 self.setActionButtonsDisabled(true, excludeCancelBtn)
408 408 } else {
409 409 self.setActionButtonsDisabled(false, excludeCancelBtn)
410 410 }
411 411 });
412 412 }
413 413
414 414 $(this.editButton).on('click', function(e) {
415 415 e.preventDefault();
416 416
417 417 $(self.previewButton).parent().removeClass('active');
418 418 $(self.previewContainer).hide();
419 419
420 420 $(self.editButton).parent().addClass('active');
421 421 $(self.editContainer).show();
422 422
423 423 });
424 424
425 425 $(this.previewButton).on('click', function(e) {
426 426 e.preventDefault();
427 427 var text = self.cm.getValue();
428 428
429 429 if (text === "") {
430 430 return;
431 431 }
432 432
433 433 var postData = {
434 434 'text': text,
435 435 'renderer': templateContext.visual.default_renderer,
436 436 'csrf_token': CSRF_TOKEN
437 437 };
438 438
439 439 // lock ALL buttons on preview
440 440 self.setActionButtonsDisabled(true);
441 441
442 442 $(self.previewBoxSelector).addClass('unloaded');
443 443 $(self.previewBoxSelector).html(_gettext('Loading ...'));
444 444
445 445 $(self.editContainer).hide();
446 446 $(self.previewContainer).show();
447 447
448 448 // by default we reset state of comment preserving the text
449 449 var previewFailCallback = function(data){
450 450 alert(
451 451 "Error while preview of comment.\n" +
452 452 "Error code {0} ({1}).".format(data.status, data.statusText)
453 453 );
454 454 self.resetCommentFormState(text)
455 455 };
456 456 self.submitAjaxPOST(
457 457 self.previewUrl, postData, self.previewSuccessCallback,
458 458 previewFailCallback);
459 459
460 460 $(self.previewButton).parent().addClass('active');
461 461 $(self.editButton).parent().removeClass('active');
462 462 });
463 463
464 464 $(this.submitForm).submit(function(e) {
465 465 e.preventDefault();
466 466 var allowedToSubmit = self.isAllowedToSubmit();
467 467 if (!allowedToSubmit){
468 468 return false;
469 469 }
470 470 self.handleFormSubmit();
471 471 });
472 472
473 473 }
474 474
475 475 return CommentForm;
476 476 });
477 477
478 478 /* comments controller */
479 479 var CommentsController = function() {
480 480 var mainComment = '#text';
481 481 var self = this;
482 482
483 483 this.cancelComment = function(node) {
484 484 var $node = $(node);
485 485 var $td = $node.closest('td');
486 486 $node.closest('.comment-inline-form').remove();
487 487 return false;
488 488 };
489 489
490 490 this.getLineNumber = function(node) {
491 491 var $node = $(node);
492 492 var lineNo = $node.closest('td').attr('data-line-no');
493 493 if (lineNo === undefined && $node.data('commentInline')){
494 494 lineNo = $node.data('commentLineNo')
495 495 }
496 496
497 497 return lineNo
498 498 };
499 499
500 500 this.scrollToComment = function(node, offset, outdated) {
501 501 if (offset === undefined) {
502 502 offset = 0;
503 503 }
504 504 var outdated = outdated || false;
505 505 var klass = outdated ? 'div.comment-outdated' : 'div.comment-current';
506 506
507 507 if (!node) {
508 508 node = $('.comment-selected');
509 509 if (!node.length) {
510 510 node = $('comment-current')
511 511 }
512 512 }
513 513 $wrapper = $(node).closest('div.comment');
514 514 $comment = $(node).closest(klass);
515 515 $comments = $(klass);
516 516
517 517 // show hidden comment when referenced.
518 518 if (!$wrapper.is(':visible')){
519 519 $wrapper.show();
520 520 }
521 521
522 522 $('.comment-selected').removeClass('comment-selected');
523 523
524 524 var nextIdx = $(klass).index($comment) + offset;
525 525 if (nextIdx >= $comments.length) {
526 526 nextIdx = 0;
527 527 }
528 528 var $next = $(klass).eq(nextIdx);
529 529
530 530 var $cb = $next.closest('.cb');
531 531 $cb.removeClass('cb-collapsed');
532 532
533 533 var $filediffCollapseState = $cb.closest('.filediff').prev();
534 534 $filediffCollapseState.prop('checked', false);
535 535 $next.addClass('comment-selected');
536 536 scrollToElement($next);
537 537 return false;
538 538 };
539 539
540 540 this.nextComment = function(node) {
541 541 return self.scrollToComment(node, 1);
542 542 };
543 543
544 544 this.prevComment = function(node) {
545 545 return self.scrollToComment(node, -1);
546 546 };
547 547
548 548 this.nextOutdatedComment = function(node) {
549 549 return self.scrollToComment(node, 1, true);
550 550 };
551 551
552 552 this.prevOutdatedComment = function(node) {
553 553 return self.scrollToComment(node, -1, true);
554 554 };
555 555
556 556 this.deleteComment = function(node) {
557 557 if (!confirm(_gettext('Delete this comment?'))) {
558 558 return false;
559 559 }
560 560 var $node = $(node);
561 561 var $td = $node.closest('td');
562 562 var $comment = $node.closest('.comment');
563 563 var comment_id = $comment.attr('data-comment-id');
564 564 var url = AJAX_COMMENT_DELETE_URL.replace('__COMMENT_ID__', comment_id);
565 565 var postData = {
566 566 'csrf_token': CSRF_TOKEN
567 567 };
568 568
569 569 $comment.addClass('comment-deleting');
570 570 $comment.hide('fast');
571 571
572 572 var success = function(response) {
573 573 $comment.remove();
574 574 return false;
575 575 };
576 576 var failure = function(data, textStatus, xhr) {
577 577 alert("error processing request: " + textStatus);
578 578 $comment.show('fast');
579 579 $comment.removeClass('comment-deleting');
580 580 return false;
581 581 };
582 582 ajaxPOST(url, postData, success, failure);
583 583 };
584 584
585 585 this.toggleWideMode = function (node) {
586 586 if ($('#content').hasClass('wrapper')) {
587 587 $('#content').removeClass("wrapper");
588 588 $('#content').addClass("wide-mode-wrapper");
589 589 $(node).addClass('btn-success');
590 590 return true
591 591 } else {
592 592 $('#content').removeClass("wide-mode-wrapper");
593 593 $('#content').addClass("wrapper");
594 594 $(node).removeClass('btn-success');
595 595 return false
596 596 }
597 597
598 598 };
599 599
600 600 this.toggleComments = function(node, show) {
601 601 var $filediff = $(node).closest('.filediff');
602 602 if (show === true) {
603 603 $filediff.removeClass('hide-comments');
604 604 } else if (show === false) {
605 605 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
606 606 $filediff.addClass('hide-comments');
607 607 } else {
608 608 $filediff.find('.hide-line-comments').removeClass('hide-line-comments');
609 609 $filediff.toggleClass('hide-comments');
610 610 }
611 611 return false;
612 612 };
613 613
614 614 this.toggleLineComments = function(node) {
615 615 self.toggleComments(node, true);
616 616 var $node = $(node);
617 617 // mark outdated comments as visible before the toggle;
618 618 $(node.closest('tr')).find('.comment-outdated').show();
619 619 $node.closest('tr').toggleClass('hide-line-comments');
620 620 };
621 621
622 622 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
623 623 var pullRequestId = templateContext.pull_request_data.pull_request_id;
624 624 var commitId = templateContext.commit_data.commit_id;
625 625
626 626 var commentForm = new CommentForm(
627 627 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
628 628 var cm = commentForm.getCmInstance();
629 629
630 630 if (resolvesCommentId){
631 631 var placeholderText = _gettext('Leave a comment, or click resolve button to resolve TODO comment #{0}').format(resolvesCommentId);
632 632 }
633 633
634 634 setTimeout(function() {
635 635 // callbacks
636 636 if (cm !== undefined) {
637 637 commentForm.setPlaceholder(placeholderText);
638 638 if (commentForm.isInline()) {
639 639 cm.focus();
640 640 cm.refresh();
641 641 }
642 642 }
643 643 }, 10);
644 644
645 645 // trigger scrolldown to the resolve comment, since it might be away
646 646 // from the clicked
647 647 if (resolvesCommentId){
648 648 var actionNode = $(commentForm.resolvesActionId).offset();
649 649
650 650 setTimeout(function() {
651 651 if (actionNode) {
652 652 $('body, html').animate({scrollTop: actionNode.top}, 10);
653 653 }
654 654 }, 100);
655 655 }
656 656
657 // add dropzone support
658 var insertAttachmentText = function (cm, attachmentName, attachmentStoreUrl, isRendered) {
659 var renderer = templateContext.visual.default_renderer;
660 if (renderer == 'rst') {
661 var attachmentUrl = '`#{0} <{1}>`_'.format(attachmentName, attachmentStoreUrl);
662 if (isRendered){
663 attachmentUrl = '\n.. image:: {0}'.format(attachmentStoreUrl);
664 }
665 } else if (renderer == 'markdown') {
666 var attachmentUrl = '[{0}]({1})'.format(attachmentName, attachmentStoreUrl);
667 if (isRendered){
668 attachmentUrl = '!' + attachmentUrl;
669 }
670 } else {
671 var attachmentUrl = '{}'.format(attachmentStoreUrl);
672 }
673 cm.replaceRange(attachmentUrl+'\n', CodeMirror.Pos(cm.lastLine()));
674
675 return false;
676 };
677
678 //see: https://www.dropzonejs.com/#configuration
679 var storeUrl = pyroutes.url('repo_commit_comment_attachment_upload',
680 {'repo_name': templateContext.repo_name,
681 'commit_id': templateContext.commit_data.commit_id})
682
683 var previewTmpl = $(formElement).find('.comment-attachment-uploader-template').get(0).innerHTML;
684 var selectLink = $(formElement).find('.pick-attachment').get(0);
685 $(formElement).find('.comment-attachment-uploader').dropzone({
686 url: storeUrl,
687 headers: {"X-CSRF-Token": CSRF_TOKEN},
688 paramName: function () {
689 return "attachment"
690 }, // The name that will be used to transfer the file
691 clickable: selectLink,
692 parallelUploads: 1,
693 maxFiles: 10,
694 maxFilesize: templateContext.attachment_store.max_file_size_mb,
695 uploadMultiple: false,
696 autoProcessQueue: true, // if false queue will not be processed automatically.
697 createImageThumbnails: false,
698 previewTemplate: previewTmpl,
699
700 accept: function (file, done) {
701 done();
702 },
703 init: function () {
704
705 this.on("sending", function (file, xhr, formData) {
706 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').hide();
707 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').show();
708 });
709
710 this.on("success", function (file, response) {
711 $(formElement).find('.comment-attachment-uploader').find('.dropzone-text').show();
712 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
713
714 var isRendered = false;
715 var ext = file.name.split('.').pop();
716 var imageExts = templateContext.attachment_store.image_ext;
717 if (imageExts.indexOf(ext) !== -1){
718 isRendered = true;
719 }
720
721 insertAttachmentText(cm, file.name, response.repo_fqn_access_path, isRendered)
722 });
723
724 this.on("error", function (file, errorMessage, xhr) {
725 $(formElement).find('.comment-attachment-uploader').find('.dropzone-upload').hide();
726
727 var error = null;
728
729 if (xhr !== undefined){
730 var httpStatus = xhr.status + " " + xhr.statusText;
731 if (xhr.status >= 500) {
732 error = httpStatus;
733 }
734 }
735
736 if (error === null) {
737 error = errorMessage.error || errorMessage || httpStatus;
738 }
739 $(file.previewElement).find('.dz-error-message').html('ERROR: {0}'.format(error));
740
741 });
742 }
743 });
744
745
657 746 return commentForm;
658 747 };
659 748
660 749 this.createGeneralComment = function (lineNo, placeholderText, resolvesCommentId) {
661 750
662 751 var tmpl = $('#cb-comment-general-form-template').html();
663 752 tmpl = tmpl.format(null, 'general');
664 753 var $form = $(tmpl);
665 754
666 755 var $formPlaceholder = $('#cb-comment-general-form-placeholder');
667 756 var curForm = $formPlaceholder.find('form');
668 757 if (curForm){
669 758 curForm.remove();
670 759 }
671 760 $formPlaceholder.append($form);
672 761
673 762 var _form = $($form[0]);
674 763 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
675 764 var commentForm = this.createCommentForm(
676 765 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
677 766 commentForm.initStatusChangeSelector();
678 767
679 768 return commentForm;
680 769 };
681 770
682 771 this.createComment = function(node, resolutionComment) {
683 772 var resolvesCommentId = resolutionComment || null;
684 773 var $node = $(node);
685 774 var $td = $node.closest('td');
686 775 var $form = $td.find('.comment-inline-form');
687 776
688 777 if (!$form.length) {
689 778
690 779 var $filediff = $node.closest('.filediff');
691 780 $filediff.removeClass('hide-comments');
692 781 var f_path = $filediff.attr('data-f-path');
693 782 var lineno = self.getLineNumber(node);
694 783 // create a new HTML from template
695 784 var tmpl = $('#cb-comment-inline-form-template').html();
696 785 tmpl = tmpl.format(escapeHtml(f_path), lineno);
697 786 $form = $(tmpl);
698 787
699 788 var $comments = $td.find('.inline-comments');
700 789 if (!$comments.length) {
701 790 $comments = $(
702 791 $('#cb-comments-inline-container-template').html());
703 792 $td.append($comments);
704 793 }
705 794
706 795 $td.find('.cb-comment-add-button').before($form);
707 796
708 797 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
709 798 var _form = $($form[0]).find('form');
710 799 var autocompleteActions = ['as_note', 'as_todo'];
711 800 var commentForm = this.createCommentForm(
712 801 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
713 802
714 803 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
715 804 form: _form,
716 805 parent: $td[0],
717 806 lineno: lineno,
718 807 f_path: f_path}
719 808 );
720 809
721 810 // set a CUSTOM submit handler for inline comments.
722 811 commentForm.setHandleFormSubmit(function(o) {
723 812 var text = commentForm.cm.getValue();
724 813 var commentType = commentForm.getCommentType();
725 814 var resolvesCommentId = commentForm.getResolvesId();
726 815
727 816 if (text === "") {
728 817 return;
729 818 }
730 819
731 820 if (lineno === undefined) {
732 821 alert('missing line !');
733 822 return;
734 823 }
735 824 if (f_path === undefined) {
736 825 alert('missing file path !');
737 826 return;
738 827 }
739 828
740 829 var excludeCancelBtn = false;
741 830 var submitEvent = true;
742 831 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
743 832 commentForm.cm.setOption("readOnly", true);
744 833 var postData = {
745 834 'text': text,
746 835 'f_path': f_path,
747 836 'line': lineno,
748 837 'comment_type': commentType,
749 838 'csrf_token': CSRF_TOKEN
750 839 };
751 840 if (resolvesCommentId){
752 841 postData['resolves_comment_id'] = resolvesCommentId;
753 842 }
754 843
755 844 var submitSuccessCallback = function(json_data) {
756 845 $form.remove();
757 846 try {
758 847 var html = json_data.rendered_text;
759 848 var lineno = json_data.line_no;
760 849 var target_id = json_data.target_id;
761 850
762 851 $comments.find('.cb-comment-add-button').before(html);
763 852
764 853 //mark visually which comment was resolved
765 854 if (resolvesCommentId) {
766 855 commentForm.markCommentResolved(resolvesCommentId);
767 856 }
768 857
769 858 // run global callback on submit
770 859 commentForm.globalSubmitSuccessCallback();
771 860
772 861 } catch (e) {
773 862 console.error(e);
774 863 }
775 864
776 865 // re trigger the linkification of next/prev navigation
777 866 linkifyComments($('.inline-comment-injected'));
778 867 timeagoActivate();
779 868
780 869 if (window.updateSticky !== undefined) {
781 870 // potentially our comments change the active window size, so we
782 871 // notify sticky elements
783 872 updateSticky()
784 873 }
785 874
786 875 commentForm.setActionButtonsDisabled(false);
787 876
788 877 };
789 878 var submitFailCallback = function(data){
790 879 alert(
791 880 "Error while submitting comment.\n" +
792 881 "Error code {0} ({1}).".format(data.status, data.statusText)
793 882 );
794 883 commentForm.resetCommentFormState(text)
795 884 };
796 885 commentForm.submitAjaxPOST(
797 886 commentForm.submitUrl, postData, submitSuccessCallback, submitFailCallback);
798 887 });
799 888 }
800 889
801 890 $form.addClass('comment-inline-form-open');
802 891 };
803 892
804 893 this.createResolutionComment = function(commentId){
805 894 // hide the trigger text
806 895 $('#resolve-comment-{0}'.format(commentId)).hide();
807 896
808 897 var comment = $('#comment-'+commentId);
809 898 var commentData = comment.data();
810 899 if (commentData.commentInline) {
811 900 this.createComment(comment, commentId)
812 901 } else {
813 902 Rhodecode.comments.createGeneralComment('general', "$placeholder", commentId)
814 903 }
815 904
816 905 return false;
817 906 };
818 907
819 908 this.submitResolution = function(commentId){
820 909 var form = $('#resolve_comment_{0}'.format(commentId)).closest('form');
821 910 var commentForm = form.get(0).CommentForm;
822 911
823 912 var cm = commentForm.getCmInstance();
824 913 var renderer = templateContext.visual.default_renderer;
825 914 if (renderer == 'rst'){
826 915 var commentUrl = '`#{0} <{1}#comment-{0}>`_'.format(commentId, commentForm.selfUrl);
827 916 } else if (renderer == 'markdown') {
828 917 var commentUrl = '[#{0}]({1}#comment-{0})'.format(commentId, commentForm.selfUrl);
829 918 } else {
830 919 var commentUrl = '{1}#comment-{0}'.format(commentId, commentForm.selfUrl);
831 920 }
832 921
833 922 cm.setValue(_gettext('TODO from comment {0} was fixed.').format(commentUrl));
834 923 form.submit();
835 924 return false;
836 925 };
837 926
838 927 };
@@ -1,159 +1,164 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <!DOCTYPE html>
3 3
4 4 <%
5 5 c.template_context['repo_name'] = getattr(c, 'repo_name', '')
6 6 go_import_header = ''
7 7 if hasattr(c, 'rhodecode_db_repo'):
8 8 c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type
9 9 c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_rev[1]
10 10 c.template_context['repo_id'] = c.rhodecode_db_repo.repo_id
11 11 c.template_context['repo_view_type'] = h.get_repo_view_type(request)
12 12
13 13 if getattr(c, 'repo_group', None):
14 14 c.template_context['repo_group_id'] = c.repo_group.group_id
15 15 c.template_context['repo_group_name'] = c.repo_group.group_name
16 16
17 17 if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
18 18 c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username
19 19 c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email
20 20 c.template_context['rhodecode_user']['notification_status'] = c.rhodecode_user.get_instance().user_data.get('notification_status', True)
21 21 c.template_context['rhodecode_user']['first_name'] = c.rhodecode_user.first_name
22 22 c.template_context['rhodecode_user']['last_name'] = c.rhodecode_user.last_name
23 23
24 24 c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer')
25 25 c.template_context['default_user'] = {
26 26 'username': h.DEFAULT_USER,
27 27 'user_id': 1
28 28 }
29 29 c.template_context['search_context'] = {
30 30 'repo_group_id': c.template_context.get('repo_group_id'),
31 31 'repo_group_name': c.template_context.get('repo_group_name'),
32 32 'repo_id': c.template_context.get('repo_id'),
33 33 'repo_name': c.template_context.get('repo_name'),
34 34 'repo_view_type': c.template_context.get('repo_view_type'),
35 35 }
36 36
37 c.template_context['attachment_store'] = {
38 'max_file_size_mb': 10,
39 'image_ext': ["png", "jpg", "gif", "jpeg"]
40 }
41
37 42 %>
38 43 <html xmlns="http://www.w3.org/1999/xhtml">
39 44 <head>
40 45 <title>${self.title()}</title>
41 46 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
42 47
43 48 ${h.go_import_header(request, getattr(c, 'rhodecode_db_repo', None))}
44 49
45 50 % if 'safari' in (request.user_agent or '').lower():
46 51 <meta name="referrer" content="origin">
47 52 % else:
48 53 <meta name="referrer" content="origin-when-cross-origin">
49 54 % endif
50 55
51 56 <%def name="robots()">
52 57 <meta name="robots" content="index, nofollow"/>
53 58 </%def>
54 59 ${self.robots()}
55 60 <link rel="icon" href="${h.asset('images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
56 61 <script src="${h.asset('js/vendors/webcomponentsjs/custom-elements-es5-adapter.js', ver=c.rhodecode_version_hash)}"></script>
57 62 <script src="${h.asset('js/vendors/webcomponentsjs/webcomponents-bundle.js', ver=c.rhodecode_version_hash)}"></script>
58 63
59 64 ## CSS definitions
60 65 <%def name="css()">
61 66 <link rel="stylesheet" type="text/css" href="${h.asset('css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
62 67 ## EXTRA FOR CSS
63 68 ${self.css_extra()}
64 69 </%def>
65 70 ## CSS EXTRA - optionally inject some extra CSS stuff needed for specific websites
66 71 <%def name="css_extra()">
67 72 </%def>
68 73
69 74 ${self.css()}
70 75
71 76 ## JAVASCRIPT
72 77 <%def name="js()">
73 78
74 79 <script src="${h.asset('js/rhodecode/i18n/%s.js' % c.language, ver=c.rhodecode_version_hash)}"></script>
75 80 <script type="text/javascript">
76 81 // register templateContext to pass template variables to JS
77 82 var templateContext = ${h.json.dumps(c.template_context)|n};
78 83
79 84 var APPLICATION_URL = "${h.route_path('home').rstrip('/')}";
80 85 var APPLICATION_PLUGINS = [];
81 86 var ASSET_URL = "${h.asset('')}";
82 87 var DEFAULT_RENDERER = "${h.get_visual_attr(c, 'default_renderer')}";
83 88 var CSRF_TOKEN = "${getattr(c, 'csrf_token', '')}";
84 89
85 90 var APPENLIGHT = {
86 91 enabled: ${'true' if getattr(c, 'appenlight_enabled', False) else 'false'},
87 92 key: '${getattr(c, "appenlight_api_public_key", "")}',
88 93 % if getattr(c, 'appenlight_server_url', None):
89 94 serverUrl: '${getattr(c, "appenlight_server_url", "")}',
90 95 % endif
91 96 requestInfo: {
92 97 % if getattr(c, 'rhodecode_user', None):
93 98 ip: '${c.rhodecode_user.ip_addr}',
94 99 username: '${c.rhodecode_user.username}'
95 100 % endif
96 101 },
97 102 tags: {
98 103 rhodecode_version: '${c.rhodecode_version}',
99 104 rhodecode_edition: '${c.rhodecode_edition}'
100 105 }
101 106 };
102 107
103 108 </script>
104 109 <%include file="/base/plugins_base.mako"/>
105 110 <!--[if lt IE 9]>
106 111 <script language="javascript" type="text/javascript" src="${h.asset('js/src/excanvas.min.js')}"></script>
107 112 <![endif]-->
108 113 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
109 114 <script> var alertMessagePayloads = ${h.flash.json_alerts(request=request)|n}; </script>
110 115 ## avoide escaping the %N
111 116 <script language="javascript" type="text/javascript" src="${h.asset('js/scripts.js', ver=c.rhodecode_version_hash)}"></script>
112 117 <script>CodeMirror.modeURL = "${h.asset('') + 'js/mode/%N/%N.js?ver='+c.rhodecode_version_hash}";</script>
113 118
114 119
115 120 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
116 121 ${self.js_extra()}
117 122
118 123 <script type="text/javascript">
119 124 Rhodecode = (function() {
120 125 function _Rhodecode() {
121 126 this.comments = new CommentsController();
122 127 }
123 128 return new _Rhodecode();
124 129 })();
125 130
126 131 $(document).ready(function(){
127 132 show_more_event();
128 133 timeagoActivate();
129 134 clipboardActivate();
130 135 })
131 136 </script>
132 137
133 138 </%def>
134 139
135 140 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
136 141 <%def name="js_extra()"></%def>
137 142 ${self.js()}
138 143
139 144 <%def name="head_extra()"></%def>
140 145 ${self.head_extra()}
141 146 ## extra stuff
142 147 %if c.pre_code:
143 148 ${c.pre_code|n}
144 149 %endif
145 150 </head>
146 151 <body id="body">
147 152 <noscript>
148 153 <div class="noscript-error">
149 154 ${_('Please enable JavaScript to use RhodeCode Enterprise')}
150 155 </div>
151 156 </noscript>
152 157
153 158 ${next.body()}
154 159 %if c.post_code:
155 160 ${c.post_code|n}
156 161 %endif
157 162 <rhodecode-app></rhodecode-app>
158 163 </body>
159 164 </html>
@@ -1,404 +1,420 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ## usage:
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6 6 <%namespace name="base" file="/base/base.mako"/>
7 7
8 8 <%def name="comment_block(comment, inline=False)">
9 9 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 10 <% latest_ver = len(getattr(c, 'versions', [])) %>
11 11 % if inline:
12 12 <% outdated_at_ver = comment.outdated_at_version(getattr(c, 'at_version_num', None)) %>
13 13 % else:
14 14 <% outdated_at_ver = comment.older_than_version(getattr(c, 'at_version_num', None)) %>
15 15 % endif
16 16
17 17
18 18 <div class="comment
19 19 ${'comment-inline' if inline else 'comment-general'}
20 20 ${'comment-outdated' if outdated_at_ver else 'comment-current'}"
21 21 id="comment-${comment.comment_id}"
22 22 line="${comment.line_no}"
23 23 data-comment-id="${comment.comment_id}"
24 24 data-comment-type="${comment.comment_type}"
25 25 data-comment-line-no="${comment.line_no}"
26 26 data-comment-inline=${h.json.dumps(inline)}
27 27 style="${'display: none;' if outdated_at_ver else ''}">
28 28
29 29 <div class="meta">
30 30 <div class="comment-type-label">
31 31 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}" title="line: ${comment.line_no}">
32 32 % if comment.comment_type == 'todo':
33 33 % if comment.resolved:
34 34 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
35 35 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
36 36 </div>
37 37 % else:
38 38 <div class="resolved tooltip" style="display: none">
39 39 <span>${comment.comment_type}</span>
40 40 </div>
41 41 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
42 42 ${comment.comment_type}
43 43 </div>
44 44 % endif
45 45 % else:
46 46 % if comment.resolved_comment:
47 47 fix
48 48 <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)})">
49 49 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
50 50 </a>
51 51 % else:
52 52 ${comment.comment_type or 'note'}
53 53 % endif
54 54 % endif
55 55 </div>
56 56 </div>
57 57
58 58 <div class="author ${'author-inline' if inline else 'author-general'}">
59 59 ${base.gravatar_with_user(comment.author.email, 16)}
60 60 </div>
61 61 <div class="date">
62 62 ${h.age_component(comment.modified_at, time_is_local=True)}
63 63 </div>
64 64 % if inline:
65 65 <span></span>
66 66 % else:
67 67 <div class="status-change">
68 68 % if comment.pull_request:
69 69 <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)}">
70 70 % if comment.status_change:
71 71 ${_('pull request #%s') % comment.pull_request.pull_request_id}:
72 72 % else:
73 73 ${_('pull request #%s') % comment.pull_request.pull_request_id}
74 74 % endif
75 75 </a>
76 76 % else:
77 77 % if comment.status_change:
78 78 ${_('Status change on commit')}:
79 79 % endif
80 80 % endif
81 81 </div>
82 82 % endif
83 83
84 84 % if comment.status_change:
85 85 <i class="icon-circle review-status-${comment.status_change[0].status}"></i>
86 86 <div title="${_('Commit status')}" class="changeset-status-lbl">
87 87 ${comment.status_change[0].status_lbl}
88 88 </div>
89 89 % endif
90 90
91 91 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
92 92
93 93 <div class="comment-links-block">
94 94 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
95 95 <span class="tag authortag tooltip" title="${_('Pull request author')}">
96 96 ${_('author')}
97 97 </span>
98 98 |
99 99 % endif
100 100 % if inline:
101 101 <div class="pr-version-inline">
102 102 <a href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
103 103 % if outdated_at_ver:
104 104 <code class="pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
105 105 outdated ${'v{}'.format(pr_index_ver)} |
106 106 </code>
107 107 % elif pr_index_ver:
108 108 <code class="pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
109 109 ${'v{}'.format(pr_index_ver)} |
110 110 </code>
111 111 % endif
112 112 </a>
113 113 </div>
114 114 % else:
115 115 % if comment.pull_request_version_id and pr_index_ver:
116 116 |
117 117 <div class="pr-version">
118 118 % if comment.outdated:
119 119 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
120 120 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}
121 121 </a>
122 122 % else:
123 123 <div title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
124 124 <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, version=comment.pull_request_version_id)}">
125 125 <code class="pr-version-num">
126 126 ${'v{}'.format(pr_index_ver)}
127 127 </code>
128 128 </a>
129 129 </div>
130 130 % endif
131 131 </div>
132 132 % endif
133 133 % endif
134 134
135 135 ## show delete comment if it's not a PR (regular comments) or it's PR that is not closed
136 136 ## only super-admin, repo admin OR comment owner can delete, also hide delete if currently viewed comment is outdated
137 137 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
138 138 ## permissions to delete
139 139 %if c.is_super_admin or h.HasRepoPermissionAny('repository.admin')(c.repo_name) or comment.author.user_id == c.rhodecode_user.user_id:
140 140 ## TODO: dan: add edit comment here
141 141 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
142 142 %else:
143 143 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
144 144 %endif
145 145 %else:
146 146 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
147 147 %endif
148 148
149 149 % if outdated_at_ver:
150 150 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="prev-comment"> ${_('Prev')}</a>
151 151 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="next-comment"> ${_('Next')}</a>
152 152 % else:
153 153 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
154 154 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
155 155 % endif
156 156
157 157 </div>
158 158 </div>
159 159 <div class="text">
160 160 ${h.render(comment.text, renderer=comment.renderer, mentions=True)}
161 161 </div>
162 162
163 163 </div>
164 164 </%def>
165 165
166 166 ## generate main comments
167 167 <%def name="generate_comments(comments, include_pull_request=False, is_pull_request=False)">
168 168 <div class="general-comments" id="comments">
169 169 %for comment in comments:
170 170 <div id="comment-tr-${comment.comment_id}">
171 171 ## only render comments that are not from pull request, or from
172 172 ## pull request and a status change
173 173 %if not comment.pull_request or (comment.pull_request and comment.status_change) or include_pull_request:
174 174 ${comment_block(comment)}
175 175 %endif
176 176 </div>
177 177 %endfor
178 178 ## to anchor ajax comments
179 179 <div id="injected_page_comments"></div>
180 180 </div>
181 181 </%def>
182 182
183 183
184 184 <%def name="comments(post_url, cur_status, is_pull_request=False, is_compare=False, change_status=True, form_extras=None)">
185 185
186 186 <div class="comments">
187 187 <%
188 188 if is_pull_request:
189 189 placeholder = _('Leave a comment on this Pull Request.')
190 190 elif is_compare:
191 191 placeholder = _('Leave a comment on {} commits in this range.').format(len(form_extras))
192 192 else:
193 193 placeholder = _('Leave a comment on this Commit.')
194 194 %>
195 195
196 196 % if c.rhodecode_user.username != h.DEFAULT_USER:
197 197 <div class="js-template" id="cb-comment-general-form-template">
198 198 ## template generated for injection
199 199 ${comment_form(form_type='general', review_statuses=c.commit_statuses, form_extras=form_extras)}
200 200 </div>
201 201
202 202 <div id="cb-comment-general-form-placeholder" class="comment-form ac">
203 203 ## inject form here
204 204 </div>
205 205 <script type="text/javascript">
206 206 var lineNo = 'general';
207 207 var resolvesCommentId = null;
208 208 var generalCommentForm = Rhodecode.comments.createGeneralComment(
209 209 lineNo, "${placeholder}", resolvesCommentId);
210 210
211 211 // set custom success callback on rangeCommit
212 212 % if is_compare:
213 213 generalCommentForm.setHandleFormSubmit(function(o) {
214 214 var self = generalCommentForm;
215 215
216 216 var text = self.cm.getValue();
217 217 var status = self.getCommentStatus();
218 218 var commentType = self.getCommentType();
219 219
220 220 if (text === "" && !status) {
221 221 return;
222 222 }
223 223
224 224 // we can pick which commits we want to make the comment by
225 225 // selecting them via click on preview pane, this will alter the hidden inputs
226 226 var cherryPicked = $('#changeset_compare_view_content .compare_select.hl').length > 0;
227 227
228 228 var commitIds = [];
229 229 $('#changeset_compare_view_content .compare_select').each(function(el) {
230 230 var commitId = this.id.replace('row-', '');
231 231 if ($(this).hasClass('hl') || !cherryPicked) {
232 232 $("input[data-commit-id='{0}']".format(commitId)).val(commitId);
233 233 commitIds.push(commitId);
234 234 } else {
235 235 $("input[data-commit-id='{0}']".format(commitId)).val('')
236 236 }
237 237 });
238 238
239 239 self.setActionButtonsDisabled(true);
240 240 self.cm.setOption("readOnly", true);
241 241 var postData = {
242 242 'text': text,
243 243 'changeset_status': status,
244 244 'comment_type': commentType,
245 245 'commit_ids': commitIds,
246 246 'csrf_token': CSRF_TOKEN
247 247 };
248 248
249 249 var submitSuccessCallback = function(o) {
250 250 location.reload(true);
251 251 };
252 252 var submitFailCallback = function(){
253 253 self.resetCommentFormState(text)
254 254 };
255 255 self.submitAjaxPOST(
256 256 self.submitUrl, postData, submitSuccessCallback, submitFailCallback);
257 257 });
258 258 % endif
259 259
260
261 260 </script>
262 261 % else:
263 262 ## form state when not logged in
264 263 <div class="comment-form ac">
265 264
266 265 <div class="comment-area">
267 266 <div class="comment-area-header">
268 267 <ul class="nav-links clearfix">
269 268 <li class="active">
270 269 <a class="disabled" href="#edit-btn" disabled="disabled" onclick="return false">${_('Write')}</a>
271 270 </li>
272 271 <li class="">
273 272 <a class="disabled" href="#preview-btn" disabled="disabled" onclick="return false">${_('Preview')}</a>
274 273 </li>
275 274 </ul>
276 275 </div>
277 276
278 277 <div class="comment-area-write" style="display: block;">
279 278 <div id="edit-container">
280 279 <div style="padding: 40px 0">
281 280 ${_('You need to be logged in to leave comments.')}
282 281 <a href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">${_('Login now')}</a>
283 282 </div>
284 283 </div>
285 284 <div id="preview-container" class="clearfix" style="display: none;">
286 285 <div id="preview-box" class="preview-box"></div>
287 286 </div>
288 287 </div>
289 288
290 289 <div class="comment-area-footer">
291 290 <div class="toolbar">
292 291 <div class="toolbar-text">
293 292 </div>
294 293 </div>
295 294 </div>
296 295 </div>
297 296
298 297 <div class="comment-footer">
299 298 </div>
300 299
301 300 </div>
302 301 % endif
303 302
304 303 <script type="text/javascript">
305 304 bindToggleButtons();
306 305 </script>
307 306 </div>
308 307 </%def>
309 308
310 309
311 310 <%def name="comment_form(form_type, form_id='', lineno_id='{1}', review_statuses=None, form_extras=None)">
311
312 312 ## comment injected based on assumption that user is logged in
313
314 313 <form ${'id="{}"'.format(form_id) if form_id else '' |n} action="#" method="GET">
315 314
316 315 <div class="comment-area">
317 316 <div class="comment-area-header">
318 317 <ul class="nav-links clearfix">
319 318 <li class="active">
320 319 <a href="#edit-btn" tabindex="-1" id="edit-btn_${lineno_id}">${_('Write')}</a>
321 320 </li>
322 321 <li class="">
323 322 <a href="#preview-btn" tabindex="-1" id="preview-btn_${lineno_id}">${_('Preview')}</a>
324 323 </li>
325 324 <li class="pull-right">
326 325 <select class="comment-type" id="comment_type_${lineno_id}" name="comment_type">
327 326 % for val in c.visual.comment_types:
328 327 <option value="${val}">${val.upper()}</option>
329 328 % endfor
330 329 </select>
331 330 </li>
332 331 </ul>
333 332 </div>
334 333
335 334 <div class="comment-area-write" style="display: block;">
336 335 <div id="edit-container_${lineno_id}">
337 336 <textarea id="text_${lineno_id}" name="text" class="comment-block-ta ac-input"></textarea>
338 337 </div>
339 338 <div id="preview-container_${lineno_id}" class="clearfix" style="display: none;">
340 339 <div id="preview-box_${lineno_id}" class="preview-box"></div>
341 340 </div>
342 341 </div>
343 342
344 <div class="comment-area-footer">
343 <div class="comment-area-footer comment-attachment-uploader">
345 344 <div class="toolbar">
346 345 <div class="toolbar-text">
347 346 ${(_('Comments parsed using %s syntax with %s, and %s actions support.') % (
348 347 ('<a href="%s">%s</a>' % (h.route_url('%s_help' % c.visual.default_renderer), c.visual.default_renderer.upper())),
349 348 ('<span class="tooltip" title="%s">@mention</span>' % _('Use @username inside this text to send notification to this RhodeCode user')),
350 349 ('<span class="tooltip" title="%s">`/`</span>' % _('Start typing with / for certain actions to be triggered via text box.'))
351 350 )
352 351 )|n}
353 352 </div>
353
354 <div class="comment-attachment-text">
355 <div class="dropzone-text">
356 ${_("Drag'n Drop files here or")} <span class="link pick-attachment">${_('Choose your files')}</span>.<br>
357 </div>
358 <div class="dropzone-upload" style="display:none">
359 <i class="icon-spin animate-spin"></i> ${_('uploading...')}
360 </div>
361 </div>
362
363 ## comments dropzone template, empty on purpose
364 <div style="display: none" class="comment-attachment-uploader-template">
365 <div class="dz-file-preview" style="margin: 0">
366 <div class="dz-error-message"></div>
367 </div>
368 </div>
369
354 370 </div>
355 371 </div>
356 372 </div>
357 373
358 374 <div class="comment-footer">
359 375
360 376 % if review_statuses:
361 377 <div class="status_box">
362 378 <select id="change_status_${lineno_id}" name="changeset_status">
363 379 <option></option> ## Placeholder
364 380 % for status, lbl in review_statuses:
365 381 <option value="${status}" data-status="${status}">${lbl}</option>
366 382 %if is_pull_request and change_status and status in ('approved', 'rejected'):
367 383 <option value="${status}_closed" data-status="${status}">${lbl} & ${_('Closed')}</option>
368 384 %endif
369 385 % endfor
370 386 </select>
371 387 </div>
372 388 % endif
373 389
374 390 ## inject extra inputs into the form
375 391 % if form_extras and isinstance(form_extras, (list, tuple)):
376 392 <div id="comment_form_extras">
377 393 % for form_ex_el in form_extras:
378 394 ${form_ex_el|n}
379 395 % endfor
380 396 </div>
381 397 % endif
382 398
383 399 <div class="action-buttons">
384 400 ## inline for has a file, and line-number together with cancel hide button.
385 401 % if form_type == 'inline':
386 402 <input type="hidden" name="f_path" value="{0}">
387 403 <input type="hidden" name="line" value="${lineno_id}">
388 404 <button type="button" class="cb-comment-cancel" onclick="return Rhodecode.comments.cancelComment(this);">
389 405 ${_('Cancel')}
390 406 </button>
391 407 % endif
392 408
393 409 % if form_type != 'inline':
394 410 <div class="action-buttons-extra"></div>
395 411 % endif
396 412
397 413 ${h.submit('save', _('Comment'), class_='btn btn-success comment-button-input')}
398 414
399 415 </div>
400 416 </div>
401 417
402 418 </form>
403 419
404 420 </%def> No newline at end of file
General Comments 0
You need to be logged in to leave comments. Login now