##// END OF EJS Templates
reviewers: add repo review rule models and expose default...
dan -
r821:618c046d default
parent child Browse files
Show More

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

1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -0,0 +1,35 b''
1 import logging
2 import datetime
3
4 from sqlalchemy import *
5 from sqlalchemy.exc import DatabaseError
6 from sqlalchemy.orm import relation, backref, class_mapper, joinedload
7 from sqlalchemy.orm.session import Session
8 from sqlalchemy.ext.declarative import declarative_base
9
10 from rhodecode.lib.dbmigrate.migrate import *
11 from rhodecode.lib.dbmigrate.migrate.changeset import *
12 from rhodecode.lib.utils2 import str2bool
13
14 from rhodecode.model.meta import Base
15 from rhodecode.model import meta
16 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
17
18 log = logging.getLogger(__name__)
19
20
21 def upgrade(migrate_engine):
22 """
23 Upgrade operations go here.
24 Don't create your own engine; bind migrate_engine to your metadata
25 """
26 _reset_base(migrate_engine)
27 from rhodecode.lib.dbmigrate.schema import db_4_4_0_2
28
29 db_4_4_0_2.RepoReviewRule.__table__.create()
30 db_4_4_0_2.RepoReviewRuleUser.__table__.create()
31 db_4_4_0_2.RepoReviewRuleUserGroup.__table__.create()
32
33 def downgrade(migrate_engine):
34 meta = MetaData()
35 meta.bind = migrate_engine
@@ -1,63 +1,63 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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
23 23 RhodeCode, a web based repository management software
24 24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
25 25 """
26 26
27 27 import os
28 28 import sys
29 29 import platform
30 30
31 31 VERSION = tuple(open(os.path.join(
32 32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
33 33
34 34 BACKENDS = {
35 35 'hg': 'Mercurial repository',
36 36 'git': 'Git repository',
37 37 'svn': 'Subversion repository',
38 38 }
39 39
40 40 CELERY_ENABLED = False
41 41 CELERY_EAGER = False
42 42
43 43 # link to config for pylons
44 44 CONFIG = {}
45 45
46 46 # Populated with the settings dictionary from application init in
47 47 # rhodecode.conf.environment.load_pyramid_environment
48 48 PYRAMID_SETTINGS = {}
49 49
50 50 # Linked module for extensions
51 51 EXTENSIONS = {}
52 52
53 53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 58 # defines current db version for migrations
54 __dbversion__ = 59 # defines current db version for migrations
55 55 __platform__ = platform.system()
56 56 __license__ = 'AGPLv3, and Commercial License'
57 57 __author__ = 'RhodeCode GmbH'
58 58 __url__ = 'http://rhodecode.com'
59 59
60 60 is_windows = __platform__ in ['Windows']
61 61 is_unix = not is_windows
62 62 is_test = False
63 63 disable_error_handler = False
@@ -1,1160 +1,1167 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Routes configuration
23 23
24 24 The more specific and detailed routes should be defined first so they
25 25 may take precedent over the more generic routes. For more information
26 26 refer to the routes manual at http://routes.groovie.org/docs/
27 27
28 28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 29 and _route_name variable which uses some of stored naming here to do redirects.
30 30 """
31 31 import os
32 32 import re
33 33 from routes import Mapper
34 34
35 35 from rhodecode.config import routing_links
36 36
37 37 # prefix for non repository related links needs to be prefixed with `/`
38 38 ADMIN_PREFIX = '/_admin'
39 39 STATIC_FILE_PREFIX = '/_static'
40 40
41 41 # Default requirements for URL parts
42 42 URL_NAME_REQUIREMENTS = {
43 43 # group name can have a slash in them, but they must not end with a slash
44 44 'group_name': r'.*?[^/]',
45 45 'repo_group_name': r'.*?[^/]',
46 46 # repo names can have a slash in them, but they must not end with a slash
47 47 'repo_name': r'.*?[^/]',
48 48 # file path eats up everything at the end
49 49 'f_path': r'.*',
50 50 # reference types
51 51 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
52 52 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
53 53 }
54 54
55 55
56 56 def add_route_requirements(route_path, requirements):
57 57 """
58 58 Adds regex requirements to pyramid routes using a mapping dict
59 59
60 60 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
61 61 '/{action}/{id:\d+}'
62 62
63 63 """
64 64 for key, regex in requirements.items():
65 65 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
66 66 return route_path
67 67
68 68
69 69 class JSRoutesMapper(Mapper):
70 70 """
71 71 Wrapper for routes.Mapper to make pyroutes compatible url definitions
72 72 """
73 73 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
74 74 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
75 75 def __init__(self, *args, **kw):
76 76 super(JSRoutesMapper, self).__init__(*args, **kw)
77 77 self._jsroutes = []
78 78
79 79 def connect(self, *args, **kw):
80 80 """
81 81 Wrapper for connect to take an extra argument jsroute=True
82 82
83 83 :param jsroute: boolean, if True will add the route to the pyroutes list
84 84 """
85 85 if kw.pop('jsroute', False):
86 86 if not self._named_route_regex.match(args[0]):
87 87 raise Exception('only named routes can be added to pyroutes')
88 88 self._jsroutes.append(args[0])
89 89
90 90 super(JSRoutesMapper, self).connect(*args, **kw)
91 91
92 92 def _extract_route_information(self, route):
93 93 """
94 94 Convert a route into tuple(name, path, args), eg:
95 95 ('user_profile', '/profile/%(username)s', ['username'])
96 96 """
97 97 routepath = route.routepath
98 98 def replace(matchobj):
99 99 if matchobj.group(1):
100 100 return "%%(%s)s" % matchobj.group(1).split(':')[0]
101 101 else:
102 102 return "%%(%s)s" % matchobj.group(2)
103 103
104 104 routepath = self._argument_prog.sub(replace, routepath)
105 105 return (
106 106 route.name,
107 107 routepath,
108 108 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
109 109 for arg in self._argument_prog.findall(route.routepath)]
110 110 )
111 111
112 112 def jsroutes(self):
113 113 """
114 114 Return a list of pyroutes.js compatible routes
115 115 """
116 116 for route_name in self._jsroutes:
117 117 yield self._extract_route_information(self._routenames[route_name])
118 118
119 119
120 120 def make_map(config):
121 121 """Create, configure and return the routes Mapper"""
122 122 rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
123 123 always_scan=config['debug'])
124 124 rmap.minimization = False
125 125 rmap.explicit = False
126 126
127 127 from rhodecode.lib.utils2 import str2bool
128 128 from rhodecode.model import repo, repo_group
129 129
130 130 def check_repo(environ, match_dict):
131 131 """
132 132 check for valid repository for proper 404 handling
133 133
134 134 :param environ:
135 135 :param match_dict:
136 136 """
137 137 repo_name = match_dict.get('repo_name')
138 138
139 139 if match_dict.get('f_path'):
140 140 # fix for multiple initial slashes that causes errors
141 141 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
142 142 repo_model = repo.RepoModel()
143 143 by_name_match = repo_model.get_by_repo_name(repo_name)
144 144 # if we match quickly from database, short circuit the operation,
145 145 # and validate repo based on the type.
146 146 if by_name_match:
147 147 return True
148 148
149 149 by_id_match = repo_model.get_repo_by_id(repo_name)
150 150 if by_id_match:
151 151 repo_name = by_id_match.repo_name
152 152 match_dict['repo_name'] = repo_name
153 153 return True
154 154
155 155 return False
156 156
157 157 def check_group(environ, match_dict):
158 158 """
159 159 check for valid repository group path for proper 404 handling
160 160
161 161 :param environ:
162 162 :param match_dict:
163 163 """
164 164 repo_group_name = match_dict.get('group_name')
165 165 repo_group_model = repo_group.RepoGroupModel()
166 166 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
167 167 if by_name_match:
168 168 return True
169 169
170 170 return False
171 171
172 172 def check_user_group(environ, match_dict):
173 173 """
174 174 check for valid user group for proper 404 handling
175 175
176 176 :param environ:
177 177 :param match_dict:
178 178 """
179 179 return True
180 180
181 181 def check_int(environ, match_dict):
182 182 return match_dict.get('id').isdigit()
183 183
184 184
185 185 #==========================================================================
186 186 # CUSTOM ROUTES HERE
187 187 #==========================================================================
188 188
189 189 # MAIN PAGE
190 190 rmap.connect('home', '/', controller='home', action='index', jsroute=True)
191 191 rmap.connect('goto_switcher_data', '/_goto_data', controller='home',
192 192 action='goto_switcher_data')
193 193 rmap.connect('repo_list_data', '/_repos', controller='home',
194 194 action='repo_list_data')
195 195
196 196 rmap.connect('user_autocomplete_data', '/_users', controller='home',
197 197 action='user_autocomplete_data', jsroute=True)
198 198 rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
199 action='user_group_autocomplete_data')
199 action='user_group_autocomplete_data', jsroute=True)
200 200
201 201 rmap.connect(
202 202 'user_profile', '/_profiles/{username}', controller='users',
203 203 action='user_profile')
204 204
205 205 # TODO: johbo: Static links, to be replaced by our redirection mechanism
206 206 rmap.connect('rst_help',
207 207 'http://docutils.sourceforge.net/docs/user/rst/quickref.html',
208 208 _static=True)
209 209 rmap.connect('markdown_help',
210 210 'http://daringfireball.net/projects/markdown/syntax',
211 211 _static=True)
212 212 rmap.connect('rhodecode_official', 'https://rhodecode.com', _static=True)
213 213 rmap.connect('rhodecode_support', 'https://rhodecode.com/help/', _static=True)
214 214 rmap.connect('rhodecode_translations', 'https://rhodecode.com/translate/enterprise', _static=True)
215 215 # TODO: anderson - making this a static link since redirect won't play
216 216 # nice with POST requests
217 217 rmap.connect('enterprise_license_convert_from_old',
218 218 'https://rhodecode.com/u/license-upgrade',
219 219 _static=True)
220 220
221 221 routing_links.connect_redirection_links(rmap)
222 222
223 223 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
224 224 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
225 225
226 226 # ADMIN REPOSITORY ROUTES
227 227 with rmap.submapper(path_prefix=ADMIN_PREFIX,
228 228 controller='admin/repos') as m:
229 229 m.connect('repos', '/repos',
230 230 action='create', conditions={'method': ['POST']})
231 231 m.connect('repos', '/repos',
232 232 action='index', conditions={'method': ['GET']})
233 233 m.connect('new_repo', '/create_repository', jsroute=True,
234 234 action='create_repository', conditions={'method': ['GET']})
235 235 m.connect('/repos/{repo_name}',
236 236 action='update', conditions={'method': ['PUT'],
237 237 'function': check_repo},
238 238 requirements=URL_NAME_REQUIREMENTS)
239 239 m.connect('delete_repo', '/repos/{repo_name}',
240 240 action='delete', conditions={'method': ['DELETE']},
241 241 requirements=URL_NAME_REQUIREMENTS)
242 242 m.connect('repo', '/repos/{repo_name}',
243 243 action='show', conditions={'method': ['GET'],
244 244 'function': check_repo},
245 245 requirements=URL_NAME_REQUIREMENTS)
246 246
247 247 # ADMIN REPOSITORY GROUPS ROUTES
248 248 with rmap.submapper(path_prefix=ADMIN_PREFIX,
249 249 controller='admin/repo_groups') as m:
250 250 m.connect('repo_groups', '/repo_groups',
251 251 action='create', conditions={'method': ['POST']})
252 252 m.connect('repo_groups', '/repo_groups',
253 253 action='index', conditions={'method': ['GET']})
254 254 m.connect('new_repo_group', '/repo_groups/new',
255 255 action='new', conditions={'method': ['GET']})
256 256 m.connect('update_repo_group', '/repo_groups/{group_name}',
257 257 action='update', conditions={'method': ['PUT'],
258 258 'function': check_group},
259 259 requirements=URL_NAME_REQUIREMENTS)
260 260
261 261 # EXTRAS REPO GROUP ROUTES
262 262 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
263 263 action='edit',
264 264 conditions={'method': ['GET'], 'function': check_group},
265 265 requirements=URL_NAME_REQUIREMENTS)
266 266 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
267 267 action='edit',
268 268 conditions={'method': ['PUT'], 'function': check_group},
269 269 requirements=URL_NAME_REQUIREMENTS)
270 270
271 271 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
272 272 action='edit_repo_group_advanced',
273 273 conditions={'method': ['GET'], 'function': check_group},
274 274 requirements=URL_NAME_REQUIREMENTS)
275 275 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
276 276 action='edit_repo_group_advanced',
277 277 conditions={'method': ['PUT'], 'function': check_group},
278 278 requirements=URL_NAME_REQUIREMENTS)
279 279
280 280 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
281 281 action='edit_repo_group_perms',
282 282 conditions={'method': ['GET'], 'function': check_group},
283 283 requirements=URL_NAME_REQUIREMENTS)
284 284 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
285 285 action='update_perms',
286 286 conditions={'method': ['PUT'], 'function': check_group},
287 287 requirements=URL_NAME_REQUIREMENTS)
288 288
289 289 m.connect('delete_repo_group', '/repo_groups/{group_name}',
290 290 action='delete', conditions={'method': ['DELETE'],
291 291 'function': check_group},
292 292 requirements=URL_NAME_REQUIREMENTS)
293 293
294 294 # ADMIN USER ROUTES
295 295 with rmap.submapper(path_prefix=ADMIN_PREFIX,
296 296 controller='admin/users') as m:
297 297 m.connect('users', '/users',
298 298 action='create', conditions={'method': ['POST']})
299 299 m.connect('users', '/users',
300 300 action='index', conditions={'method': ['GET']})
301 301 m.connect('new_user', '/users/new',
302 302 action='new', conditions={'method': ['GET']})
303 303 m.connect('update_user', '/users/{user_id}',
304 304 action='update', conditions={'method': ['PUT']})
305 305 m.connect('delete_user', '/users/{user_id}',
306 306 action='delete', conditions={'method': ['DELETE']})
307 307 m.connect('edit_user', '/users/{user_id}/edit',
308 308 action='edit', conditions={'method': ['GET']})
309 309 m.connect('user', '/users/{user_id}',
310 310 action='show', conditions={'method': ['GET']})
311 311 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
312 312 action='reset_password', conditions={'method': ['POST']})
313 313 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
314 314 action='create_personal_repo_group', conditions={'method': ['POST']})
315 315
316 316 # EXTRAS USER ROUTES
317 317 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
318 318 action='edit_advanced', conditions={'method': ['GET']})
319 319 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
320 320 action='update_advanced', conditions={'method': ['PUT']})
321 321
322 322 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
323 323 action='edit_auth_tokens', conditions={'method': ['GET']})
324 324 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
325 325 action='add_auth_token', conditions={'method': ['PUT']})
326 326 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
327 327 action='delete_auth_token', conditions={'method': ['DELETE']})
328 328
329 329 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
330 330 action='edit_global_perms', conditions={'method': ['GET']})
331 331 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
332 332 action='update_global_perms', conditions={'method': ['PUT']})
333 333
334 334 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
335 335 action='edit_perms_summary', conditions={'method': ['GET']})
336 336
337 337 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
338 338 action='edit_emails', conditions={'method': ['GET']})
339 339 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
340 340 action='add_email', conditions={'method': ['PUT']})
341 341 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
342 342 action='delete_email', conditions={'method': ['DELETE']})
343 343
344 344 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
345 345 action='edit_ips', conditions={'method': ['GET']})
346 346 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
347 347 action='add_ip', conditions={'method': ['PUT']})
348 348 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
349 349 action='delete_ip', conditions={'method': ['DELETE']})
350 350
351 351 # ADMIN USER GROUPS REST ROUTES
352 352 with rmap.submapper(path_prefix=ADMIN_PREFIX,
353 353 controller='admin/user_groups') as m:
354 354 m.connect('users_groups', '/user_groups',
355 355 action='create', conditions={'method': ['POST']})
356 356 m.connect('users_groups', '/user_groups',
357 357 action='index', conditions={'method': ['GET']})
358 358 m.connect('new_users_group', '/user_groups/new',
359 359 action='new', conditions={'method': ['GET']})
360 360 m.connect('update_users_group', '/user_groups/{user_group_id}',
361 361 action='update', conditions={'method': ['PUT']})
362 362 m.connect('delete_users_group', '/user_groups/{user_group_id}',
363 363 action='delete', conditions={'method': ['DELETE']})
364 364 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
365 365 action='edit', conditions={'method': ['GET']},
366 366 function=check_user_group)
367 367
368 368 # EXTRAS USER GROUP ROUTES
369 369 m.connect('edit_user_group_global_perms',
370 370 '/user_groups/{user_group_id}/edit/global_permissions',
371 371 action='edit_global_perms', conditions={'method': ['GET']})
372 372 m.connect('edit_user_group_global_perms',
373 373 '/user_groups/{user_group_id}/edit/global_permissions',
374 374 action='update_global_perms', conditions={'method': ['PUT']})
375 375 m.connect('edit_user_group_perms_summary',
376 376 '/user_groups/{user_group_id}/edit/permissions_summary',
377 377 action='edit_perms_summary', conditions={'method': ['GET']})
378 378
379 379 m.connect('edit_user_group_perms',
380 380 '/user_groups/{user_group_id}/edit/permissions',
381 381 action='edit_perms', conditions={'method': ['GET']})
382 382 m.connect('edit_user_group_perms',
383 383 '/user_groups/{user_group_id}/edit/permissions',
384 384 action='update_perms', conditions={'method': ['PUT']})
385 385
386 386 m.connect('edit_user_group_advanced',
387 387 '/user_groups/{user_group_id}/edit/advanced',
388 388 action='edit_advanced', conditions={'method': ['GET']})
389 389
390 390 m.connect('edit_user_group_members',
391 391 '/user_groups/{user_group_id}/edit/members', jsroute=True,
392 392 action='edit_members', conditions={'method': ['GET']})
393 393
394 394 # ADMIN PERMISSIONS ROUTES
395 395 with rmap.submapper(path_prefix=ADMIN_PREFIX,
396 396 controller='admin/permissions') as m:
397 397 m.connect('admin_permissions_application', '/permissions/application',
398 398 action='permission_application_update', conditions={'method': ['POST']})
399 399 m.connect('admin_permissions_application', '/permissions/application',
400 400 action='permission_application', conditions={'method': ['GET']})
401 401
402 402 m.connect('admin_permissions_global', '/permissions/global',
403 403 action='permission_global_update', conditions={'method': ['POST']})
404 404 m.connect('admin_permissions_global', '/permissions/global',
405 405 action='permission_global', conditions={'method': ['GET']})
406 406
407 407 m.connect('admin_permissions_object', '/permissions/object',
408 408 action='permission_objects_update', conditions={'method': ['POST']})
409 409 m.connect('admin_permissions_object', '/permissions/object',
410 410 action='permission_objects', conditions={'method': ['GET']})
411 411
412 412 m.connect('admin_permissions_ips', '/permissions/ips',
413 413 action='permission_ips', conditions={'method': ['POST']})
414 414 m.connect('admin_permissions_ips', '/permissions/ips',
415 415 action='permission_ips', conditions={'method': ['GET']})
416 416
417 417 m.connect('admin_permissions_overview', '/permissions/overview',
418 418 action='permission_perms', conditions={'method': ['GET']})
419 419
420 420 # ADMIN DEFAULTS REST ROUTES
421 421 with rmap.submapper(path_prefix=ADMIN_PREFIX,
422 422 controller='admin/defaults') as m:
423 423 m.connect('admin_defaults_repositories', '/defaults/repositories',
424 424 action='update_repository_defaults', conditions={'method': ['POST']})
425 425 m.connect('admin_defaults_repositories', '/defaults/repositories',
426 426 action='index', conditions={'method': ['GET']})
427 427
428 428 # ADMIN DEBUG STYLE ROUTES
429 429 if str2bool(config.get('debug_style')):
430 430 with rmap.submapper(path_prefix=ADMIN_PREFIX + '/debug_style',
431 431 controller='debug_style') as m:
432 432 m.connect('debug_style_home', '',
433 433 action='index', conditions={'method': ['GET']})
434 434 m.connect('debug_style_template', '/t/{t_path}',
435 435 action='template', conditions={'method': ['GET']})
436 436
437 437 # ADMIN SETTINGS ROUTES
438 438 with rmap.submapper(path_prefix=ADMIN_PREFIX,
439 439 controller='admin/settings') as m:
440 440
441 441 # default
442 442 m.connect('admin_settings', '/settings',
443 443 action='settings_global_update',
444 444 conditions={'method': ['POST']})
445 445 m.connect('admin_settings', '/settings',
446 446 action='settings_global', conditions={'method': ['GET']})
447 447
448 448 m.connect('admin_settings_vcs', '/settings/vcs',
449 449 action='settings_vcs_update',
450 450 conditions={'method': ['POST']})
451 451 m.connect('admin_settings_vcs', '/settings/vcs',
452 452 action='settings_vcs',
453 453 conditions={'method': ['GET']})
454 454 m.connect('admin_settings_vcs', '/settings/vcs',
455 455 action='delete_svn_pattern',
456 456 conditions={'method': ['DELETE']})
457 457
458 458 m.connect('admin_settings_mapping', '/settings/mapping',
459 459 action='settings_mapping_update',
460 460 conditions={'method': ['POST']})
461 461 m.connect('admin_settings_mapping', '/settings/mapping',
462 462 action='settings_mapping', conditions={'method': ['GET']})
463 463
464 464 m.connect('admin_settings_global', '/settings/global',
465 465 action='settings_global_update',
466 466 conditions={'method': ['POST']})
467 467 m.connect('admin_settings_global', '/settings/global',
468 468 action='settings_global', conditions={'method': ['GET']})
469 469
470 470 m.connect('admin_settings_visual', '/settings/visual',
471 471 action='settings_visual_update',
472 472 conditions={'method': ['POST']})
473 473 m.connect('admin_settings_visual', '/settings/visual',
474 474 action='settings_visual', conditions={'method': ['GET']})
475 475
476 476 m.connect('admin_settings_issuetracker',
477 477 '/settings/issue-tracker', action='settings_issuetracker',
478 478 conditions={'method': ['GET']})
479 479 m.connect('admin_settings_issuetracker_save',
480 480 '/settings/issue-tracker/save',
481 481 action='settings_issuetracker_save',
482 482 conditions={'method': ['POST']})
483 483 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
484 484 action='settings_issuetracker_test',
485 485 conditions={'method': ['POST']})
486 486 m.connect('admin_issuetracker_delete',
487 487 '/settings/issue-tracker/delete',
488 488 action='settings_issuetracker_delete',
489 489 conditions={'method': ['DELETE']})
490 490
491 491 m.connect('admin_settings_email', '/settings/email',
492 492 action='settings_email_update',
493 493 conditions={'method': ['POST']})
494 494 m.connect('admin_settings_email', '/settings/email',
495 495 action='settings_email', conditions={'method': ['GET']})
496 496
497 497 m.connect('admin_settings_hooks', '/settings/hooks',
498 498 action='settings_hooks_update',
499 499 conditions={'method': ['POST', 'DELETE']})
500 500 m.connect('admin_settings_hooks', '/settings/hooks',
501 501 action='settings_hooks', conditions={'method': ['GET']})
502 502
503 503 m.connect('admin_settings_search', '/settings/search',
504 504 action='settings_search', conditions={'method': ['GET']})
505 505
506 506 m.connect('admin_settings_system', '/settings/system',
507 507 action='settings_system', conditions={'method': ['GET']})
508 508
509 509 m.connect('admin_settings_system_update', '/settings/system/updates',
510 510 action='settings_system_update', conditions={'method': ['GET']})
511 511
512 512 m.connect('admin_settings_supervisor', '/settings/supervisor',
513 513 action='settings_supervisor', conditions={'method': ['GET']})
514 514 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
515 515 action='settings_supervisor_log', conditions={'method': ['GET']})
516 516
517 517 m.connect('admin_settings_labs', '/settings/labs',
518 518 action='settings_labs_update',
519 519 conditions={'method': ['POST']})
520 520 m.connect('admin_settings_labs', '/settings/labs',
521 521 action='settings_labs', conditions={'method': ['GET']})
522 522
523 523 # ADMIN MY ACCOUNT
524 524 with rmap.submapper(path_prefix=ADMIN_PREFIX,
525 525 controller='admin/my_account') as m:
526 526
527 527 m.connect('my_account', '/my_account',
528 528 action='my_account', conditions={'method': ['GET']})
529 529 m.connect('my_account_edit', '/my_account/edit',
530 530 action='my_account_edit', conditions={'method': ['GET']})
531 531 m.connect('my_account', '/my_account',
532 532 action='my_account_update', conditions={'method': ['POST']})
533 533
534 534 m.connect('my_account_password', '/my_account/password',
535 535 action='my_account_password', conditions={'method': ['GET', 'POST']})
536 536
537 537 m.connect('my_account_repos', '/my_account/repos',
538 538 action='my_account_repos', conditions={'method': ['GET']})
539 539
540 540 m.connect('my_account_watched', '/my_account/watched',
541 541 action='my_account_watched', conditions={'method': ['GET']})
542 542
543 543 m.connect('my_account_pullrequests', '/my_account/pull_requests',
544 544 action='my_account_pullrequests', conditions={'method': ['GET']})
545 545
546 546 m.connect('my_account_perms', '/my_account/perms',
547 547 action='my_account_perms', conditions={'method': ['GET']})
548 548
549 549 m.connect('my_account_emails', '/my_account/emails',
550 550 action='my_account_emails', conditions={'method': ['GET']})
551 551 m.connect('my_account_emails', '/my_account/emails',
552 552 action='my_account_emails_add', conditions={'method': ['POST']})
553 553 m.connect('my_account_emails', '/my_account/emails',
554 554 action='my_account_emails_delete', conditions={'method': ['DELETE']})
555 555
556 556 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
557 557 action='my_account_auth_tokens', conditions={'method': ['GET']})
558 558 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
559 559 action='my_account_auth_tokens_add', conditions={'method': ['POST']})
560 560 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
561 561 action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']})
562 562 m.connect('my_account_notifications', '/my_account/notifications',
563 563 action='my_notifications',
564 564 conditions={'method': ['GET']})
565 565 m.connect('my_account_notifications_toggle_visibility',
566 566 '/my_account/toggle_visibility',
567 567 action='my_notifications_toggle_visibility',
568 568 conditions={'method': ['POST']})
569 569
570 570 # NOTIFICATION REST ROUTES
571 571 with rmap.submapper(path_prefix=ADMIN_PREFIX,
572 572 controller='admin/notifications') as m:
573 573 m.connect('notifications', '/notifications',
574 574 action='index', conditions={'method': ['GET']})
575 575 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
576 576 action='mark_all_read', conditions={'method': ['POST']})
577 577 m.connect('/notifications/{notification_id}',
578 578 action='update', conditions={'method': ['PUT']})
579 579 m.connect('/notifications/{notification_id}',
580 580 action='delete', conditions={'method': ['DELETE']})
581 581 m.connect('notification', '/notifications/{notification_id}',
582 582 action='show', conditions={'method': ['GET']})
583 583
584 584 # ADMIN GIST
585 585 with rmap.submapper(path_prefix=ADMIN_PREFIX,
586 586 controller='admin/gists') as m:
587 587 m.connect('gists', '/gists',
588 588 action='create', conditions={'method': ['POST']})
589 589 m.connect('gists', '/gists', jsroute=True,
590 590 action='index', conditions={'method': ['GET']})
591 591 m.connect('new_gist', '/gists/new', jsroute=True,
592 592 action='new', conditions={'method': ['GET']})
593 593
594 594 m.connect('/gists/{gist_id}',
595 595 action='delete', conditions={'method': ['DELETE']})
596 596 m.connect('edit_gist', '/gists/{gist_id}/edit',
597 597 action='edit_form', conditions={'method': ['GET']})
598 598 m.connect('edit_gist', '/gists/{gist_id}/edit',
599 599 action='edit', conditions={'method': ['POST']})
600 600 m.connect(
601 601 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision',
602 602 action='check_revision', conditions={'method': ['GET']})
603 603
604 604 m.connect('gist', '/gists/{gist_id}',
605 605 action='show', conditions={'method': ['GET']})
606 606 m.connect('gist_rev', '/gists/{gist_id}/{revision}',
607 607 revision='tip',
608 608 action='show', conditions={'method': ['GET']})
609 609 m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}',
610 610 revision='tip',
611 611 action='show', conditions={'method': ['GET']})
612 612 m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}',
613 613 revision='tip',
614 614 action='show', conditions={'method': ['GET']},
615 615 requirements=URL_NAME_REQUIREMENTS)
616 616
617 617 # ADMIN MAIN PAGES
618 618 with rmap.submapper(path_prefix=ADMIN_PREFIX,
619 619 controller='admin/admin') as m:
620 620 m.connect('admin_home', '', action='index')
621 621 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
622 622 action='add_repo')
623 623 m.connect(
624 624 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}',
625 625 action='pull_requests')
626 626 m.connect(
627 627 'pull_requests_global', '/pull-requests/{pull_request_id:[0-9]+}',
628 628 action='pull_requests')
629 629
630 630
631 631 # USER JOURNAL
632 632 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
633 633 controller='journal', action='index')
634 634 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
635 635 controller='journal', action='journal_rss')
636 636 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
637 637 controller='journal', action='journal_atom')
638 638
639 639 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
640 640 controller='journal', action='public_journal')
641 641
642 642 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
643 643 controller='journal', action='public_journal_rss')
644 644
645 645 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
646 646 controller='journal', action='public_journal_rss')
647 647
648 648 rmap.connect('public_journal_atom',
649 649 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
650 650 action='public_journal_atom')
651 651
652 652 rmap.connect('public_journal_atom_old',
653 653 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
654 654 action='public_journal_atom')
655 655
656 656 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
657 657 controller='journal', action='toggle_following', jsroute=True,
658 658 conditions={'method': ['POST']})
659 659
660 660 # FULL TEXT SEARCH
661 661 rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
662 662 controller='search')
663 663 rmap.connect('search_repo_home', '/{repo_name}/search',
664 664 controller='search',
665 665 action='index',
666 666 conditions={'function': check_repo},
667 667 requirements=URL_NAME_REQUIREMENTS)
668 668
669 669 # FEEDS
670 670 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
671 671 controller='feed', action='rss',
672 672 conditions={'function': check_repo},
673 673 requirements=URL_NAME_REQUIREMENTS)
674 674
675 675 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
676 676 controller='feed', action='atom',
677 677 conditions={'function': check_repo},
678 678 requirements=URL_NAME_REQUIREMENTS)
679 679
680 680 #==========================================================================
681 681 # REPOSITORY ROUTES
682 682 #==========================================================================
683 683
684 684 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
685 685 controller='admin/repos', action='repo_creating',
686 686 requirements=URL_NAME_REQUIREMENTS)
687 687 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
688 688 controller='admin/repos', action='repo_check',
689 689 requirements=URL_NAME_REQUIREMENTS)
690 690
691 691 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
692 692 controller='summary', action='repo_stats',
693 693 conditions={'function': check_repo},
694 694 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
695 695
696 696 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
697 697 controller='summary', action='repo_refs_data', jsroute=True,
698 698 requirements=URL_NAME_REQUIREMENTS)
699 699 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
700 700 controller='summary', action='repo_refs_changelog_data',
701 701 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
702 rmap.connect('repo_default_reviewers_data', '/{repo_name}/default-reviewers',
703 controller='summary', action='repo_default_reviewers_data',
704 jsroute=True, requirements=URL_NAME_REQUIREMENTS)
702 705
703 706 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
704 707 controller='changeset', revision='tip', jsroute=True,
705 708 conditions={'function': check_repo},
706 709 requirements=URL_NAME_REQUIREMENTS)
707 710 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
708 711 controller='changeset', revision='tip', action='changeset_children',
709 712 conditions={'function': check_repo},
710 713 requirements=URL_NAME_REQUIREMENTS)
711 714 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
712 715 controller='changeset', revision='tip', action='changeset_parents',
713 716 conditions={'function': check_repo},
714 717 requirements=URL_NAME_REQUIREMENTS)
715 718
716 719 # repo edit options
717 720 rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True,
718 721 controller='admin/repos', action='edit',
719 722 conditions={'method': ['GET'], 'function': check_repo},
720 723 requirements=URL_NAME_REQUIREMENTS)
721 724
722 725 rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions',
723 726 jsroute=True,
724 727 controller='admin/repos', action='edit_permissions',
725 728 conditions={'method': ['GET'], 'function': check_repo},
726 729 requirements=URL_NAME_REQUIREMENTS)
727 730 rmap.connect('edit_repo_perms_update', '/{repo_name}/settings/permissions',
728 731 controller='admin/repos', action='edit_permissions_update',
729 732 conditions={'method': ['PUT'], 'function': check_repo},
730 733 requirements=URL_NAME_REQUIREMENTS)
731 734
732 735 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
733 736 controller='admin/repos', action='edit_fields',
734 737 conditions={'method': ['GET'], 'function': check_repo},
735 738 requirements=URL_NAME_REQUIREMENTS)
736 739 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
737 740 controller='admin/repos', action='create_repo_field',
738 741 conditions={'method': ['PUT'], 'function': check_repo},
739 742 requirements=URL_NAME_REQUIREMENTS)
740 743 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
741 744 controller='admin/repos', action='delete_repo_field',
742 745 conditions={'method': ['DELETE'], 'function': check_repo},
743 746 requirements=URL_NAME_REQUIREMENTS)
744 747
745 748 rmap.connect('edit_repo_advanced', '/{repo_name}/settings/advanced',
746 749 controller='admin/repos', action='edit_advanced',
747 750 conditions={'method': ['GET'], 'function': check_repo},
748 751 requirements=URL_NAME_REQUIREMENTS)
749 752
750 753 rmap.connect('edit_repo_advanced_locking', '/{repo_name}/settings/advanced/locking',
751 754 controller='admin/repos', action='edit_advanced_locking',
752 755 conditions={'method': ['PUT'], 'function': check_repo},
753 756 requirements=URL_NAME_REQUIREMENTS)
754 757 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
755 758 controller='admin/repos', action='toggle_locking',
756 759 conditions={'method': ['GET'], 'function': check_repo},
757 760 requirements=URL_NAME_REQUIREMENTS)
758 761
759 762 rmap.connect('edit_repo_advanced_journal', '/{repo_name}/settings/advanced/journal',
760 763 controller='admin/repos', action='edit_advanced_journal',
761 764 conditions={'method': ['PUT'], 'function': check_repo},
762 765 requirements=URL_NAME_REQUIREMENTS)
763 766
764 767 rmap.connect('edit_repo_advanced_fork', '/{repo_name}/settings/advanced/fork',
765 768 controller='admin/repos', action='edit_advanced_fork',
766 769 conditions={'method': ['PUT'], 'function': check_repo},
767 770 requirements=URL_NAME_REQUIREMENTS)
768 771
769 772 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
770 773 controller='admin/repos', action='edit_caches_form',
771 774 conditions={'method': ['GET'], 'function': check_repo},
772 775 requirements=URL_NAME_REQUIREMENTS)
773 776 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
774 777 controller='admin/repos', action='edit_caches',
775 778 conditions={'method': ['PUT'], 'function': check_repo},
776 779 requirements=URL_NAME_REQUIREMENTS)
777 780
778 781 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
779 782 controller='admin/repos', action='edit_remote_form',
780 783 conditions={'method': ['GET'], 'function': check_repo},
781 784 requirements=URL_NAME_REQUIREMENTS)
782 785 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
783 786 controller='admin/repos', action='edit_remote',
784 787 conditions={'method': ['PUT'], 'function': check_repo},
785 788 requirements=URL_NAME_REQUIREMENTS)
786 789
787 790 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
788 791 controller='admin/repos', action='edit_statistics_form',
789 792 conditions={'method': ['GET'], 'function': check_repo},
790 793 requirements=URL_NAME_REQUIREMENTS)
791 794 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
792 795 controller='admin/repos', action='edit_statistics',
793 796 conditions={'method': ['PUT'], 'function': check_repo},
794 797 requirements=URL_NAME_REQUIREMENTS)
795 798 rmap.connect('repo_settings_issuetracker',
796 799 '/{repo_name}/settings/issue-tracker',
797 800 controller='admin/repos', action='repo_issuetracker',
798 801 conditions={'method': ['GET'], 'function': check_repo},
799 802 requirements=URL_NAME_REQUIREMENTS)
800 803 rmap.connect('repo_issuetracker_test',
801 804 '/{repo_name}/settings/issue-tracker/test',
802 805 controller='admin/repos', action='repo_issuetracker_test',
803 806 conditions={'method': ['POST'], 'function': check_repo},
804 807 requirements=URL_NAME_REQUIREMENTS)
805 808 rmap.connect('repo_issuetracker_delete',
806 809 '/{repo_name}/settings/issue-tracker/delete',
807 810 controller='admin/repos', action='repo_issuetracker_delete',
808 811 conditions={'method': ['DELETE'], 'function': check_repo},
809 812 requirements=URL_NAME_REQUIREMENTS)
810 813 rmap.connect('repo_issuetracker_save',
811 814 '/{repo_name}/settings/issue-tracker/save',
812 815 controller='admin/repos', action='repo_issuetracker_save',
813 816 conditions={'method': ['POST'], 'function': check_repo},
814 817 requirements=URL_NAME_REQUIREMENTS)
815 818 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
816 819 controller='admin/repos', action='repo_settings_vcs_update',
817 820 conditions={'method': ['POST'], 'function': check_repo},
818 821 requirements=URL_NAME_REQUIREMENTS)
819 822 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
820 823 controller='admin/repos', action='repo_settings_vcs',
821 824 conditions={'method': ['GET'], 'function': check_repo},
822 825 requirements=URL_NAME_REQUIREMENTS)
823 826 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
824 827 controller='admin/repos', action='repo_delete_svn_pattern',
825 828 conditions={'method': ['DELETE'], 'function': check_repo},
826 829 requirements=URL_NAME_REQUIREMENTS)
830 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
831 controller='admin/repos', action='repo_settings_pullrequest',
832 conditions={'method': ['GET', 'POST'], 'function': check_repo},
833 requirements=URL_NAME_REQUIREMENTS)
827 834
828 835 # still working url for backward compat.
829 836 rmap.connect('raw_changeset_home_depraced',
830 837 '/{repo_name}/raw-changeset/{revision}',
831 838 controller='changeset', action='changeset_raw',
832 839 revision='tip', conditions={'function': check_repo},
833 840 requirements=URL_NAME_REQUIREMENTS)
834 841
835 842 # new URLs
836 843 rmap.connect('changeset_raw_home',
837 844 '/{repo_name}/changeset-diff/{revision}',
838 845 controller='changeset', action='changeset_raw',
839 846 revision='tip', conditions={'function': check_repo},
840 847 requirements=URL_NAME_REQUIREMENTS)
841 848
842 849 rmap.connect('changeset_patch_home',
843 850 '/{repo_name}/changeset-patch/{revision}',
844 851 controller='changeset', action='changeset_patch',
845 852 revision='tip', conditions={'function': check_repo},
846 853 requirements=URL_NAME_REQUIREMENTS)
847 854
848 855 rmap.connect('changeset_download_home',
849 856 '/{repo_name}/changeset-download/{revision}',
850 857 controller='changeset', action='changeset_download',
851 858 revision='tip', conditions={'function': check_repo},
852 859 requirements=URL_NAME_REQUIREMENTS)
853 860
854 861 rmap.connect('changeset_comment',
855 862 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
856 863 controller='changeset', revision='tip', action='comment',
857 864 conditions={'function': check_repo},
858 865 requirements=URL_NAME_REQUIREMENTS)
859 866
860 867 rmap.connect('changeset_comment_preview',
861 868 '/{repo_name}/changeset/comment/preview', jsroute=True,
862 869 controller='changeset', action='preview_comment',
863 870 conditions={'function': check_repo, 'method': ['POST']},
864 871 requirements=URL_NAME_REQUIREMENTS)
865 872
866 873 rmap.connect('changeset_comment_delete',
867 874 '/{repo_name}/changeset/comment/{comment_id}/delete',
868 875 controller='changeset', action='delete_comment',
869 876 conditions={'function': check_repo, 'method': ['DELETE']},
870 877 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
871 878
872 879 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
873 880 controller='changeset', action='changeset_info',
874 881 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
875 882
876 883 rmap.connect('compare_home',
877 884 '/{repo_name}/compare',
878 885 controller='compare', action='index',
879 886 conditions={'function': check_repo},
880 887 requirements=URL_NAME_REQUIREMENTS)
881 888
882 889 rmap.connect('compare_url',
883 890 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
884 891 controller='compare', action='compare',
885 892 conditions={'function': check_repo},
886 893 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
887 894
888 895 rmap.connect('pullrequest_home',
889 896 '/{repo_name}/pull-request/new', controller='pullrequests',
890 897 action='index', conditions={'function': check_repo,
891 898 'method': ['GET']},
892 899 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
893 900
894 901 rmap.connect('pullrequest',
895 902 '/{repo_name}/pull-request/new', controller='pullrequests',
896 903 action='create', conditions={'function': check_repo,
897 904 'method': ['POST']},
898 905 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
899 906
900 907 rmap.connect('pullrequest_repo_refs',
901 908 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
902 909 controller='pullrequests',
903 910 action='get_repo_refs',
904 911 conditions={'function': check_repo, 'method': ['GET']},
905 912 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
906 913
907 914 rmap.connect('pullrequest_repo_destinations',
908 915 '/{repo_name}/pull-request/repo-destinations',
909 916 controller='pullrequests',
910 917 action='get_repo_destinations',
911 918 conditions={'function': check_repo, 'method': ['GET']},
912 919 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
913 920
914 921 rmap.connect('pullrequest_show',
915 922 '/{repo_name}/pull-request/{pull_request_id}',
916 923 controller='pullrequests',
917 924 action='show', conditions={'function': check_repo,
918 925 'method': ['GET']},
919 926 requirements=URL_NAME_REQUIREMENTS)
920 927
921 928 rmap.connect('pullrequest_update',
922 929 '/{repo_name}/pull-request/{pull_request_id}',
923 930 controller='pullrequests',
924 931 action='update', conditions={'function': check_repo,
925 932 'method': ['PUT']},
926 933 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
927 934
928 935 rmap.connect('pullrequest_merge',
929 936 '/{repo_name}/pull-request/{pull_request_id}',
930 937 controller='pullrequests',
931 938 action='merge', conditions={'function': check_repo,
932 939 'method': ['POST']},
933 940 requirements=URL_NAME_REQUIREMENTS)
934 941
935 942 rmap.connect('pullrequest_delete',
936 943 '/{repo_name}/pull-request/{pull_request_id}',
937 944 controller='pullrequests',
938 945 action='delete', conditions={'function': check_repo,
939 946 'method': ['DELETE']},
940 947 requirements=URL_NAME_REQUIREMENTS)
941 948
942 949 rmap.connect('pullrequest_show_all',
943 950 '/{repo_name}/pull-request',
944 951 controller='pullrequests',
945 952 action='show_all', conditions={'function': check_repo,
946 953 'method': ['GET']},
947 954 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
948 955
949 956 rmap.connect('pullrequest_comment',
950 957 '/{repo_name}/pull-request-comment/{pull_request_id}',
951 958 controller='pullrequests',
952 959 action='comment', conditions={'function': check_repo,
953 960 'method': ['POST']},
954 961 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
955 962
956 963 rmap.connect('pullrequest_comment_delete',
957 964 '/{repo_name}/pull-request-comment/{comment_id}/delete',
958 965 controller='pullrequests', action='delete_comment',
959 966 conditions={'function': check_repo, 'method': ['DELETE']},
960 967 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
961 968
962 969 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
963 970 controller='summary', conditions={'function': check_repo},
964 971 requirements=URL_NAME_REQUIREMENTS)
965 972
966 973 rmap.connect('branches_home', '/{repo_name}/branches',
967 974 controller='branches', conditions={'function': check_repo},
968 975 requirements=URL_NAME_REQUIREMENTS)
969 976
970 977 rmap.connect('tags_home', '/{repo_name}/tags',
971 978 controller='tags', conditions={'function': check_repo},
972 979 requirements=URL_NAME_REQUIREMENTS)
973 980
974 981 rmap.connect('bookmarks_home', '/{repo_name}/bookmarks',
975 982 controller='bookmarks', conditions={'function': check_repo},
976 983 requirements=URL_NAME_REQUIREMENTS)
977 984
978 985 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
979 986 controller='changelog', conditions={'function': check_repo},
980 987 requirements=URL_NAME_REQUIREMENTS)
981 988
982 989 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
983 990 controller='changelog', action='changelog_summary',
984 991 conditions={'function': check_repo},
985 992 requirements=URL_NAME_REQUIREMENTS)
986 993
987 994 rmap.connect('changelog_file_home',
988 995 '/{repo_name}/changelog/{revision}/{f_path}',
989 996 controller='changelog', f_path=None,
990 997 conditions={'function': check_repo},
991 998 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
992 999
993 1000 rmap.connect('changelog_details', '/{repo_name}/changelog_details/{cs}',
994 1001 controller='changelog', action='changelog_details',
995 1002 conditions={'function': check_repo},
996 1003 requirements=URL_NAME_REQUIREMENTS)
997 1004
998 1005 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
999 1006 controller='files', revision='tip', f_path='',
1000 1007 conditions={'function': check_repo},
1001 1008 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1002 1009
1003 1010 rmap.connect('files_home_simple_catchrev',
1004 1011 '/{repo_name}/files/{revision}',
1005 1012 controller='files', revision='tip', f_path='',
1006 1013 conditions={'function': check_repo},
1007 1014 requirements=URL_NAME_REQUIREMENTS)
1008 1015
1009 1016 rmap.connect('files_home_simple_catchall',
1010 1017 '/{repo_name}/files',
1011 1018 controller='files', revision='tip', f_path='',
1012 1019 conditions={'function': check_repo},
1013 1020 requirements=URL_NAME_REQUIREMENTS)
1014 1021
1015 1022 rmap.connect('files_history_home',
1016 1023 '/{repo_name}/history/{revision}/{f_path}',
1017 1024 controller='files', action='history', revision='tip', f_path='',
1018 1025 conditions={'function': check_repo},
1019 1026 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1020 1027
1021 1028 rmap.connect('files_authors_home',
1022 1029 '/{repo_name}/authors/{revision}/{f_path}',
1023 1030 controller='files', action='authors', revision='tip', f_path='',
1024 1031 conditions={'function': check_repo},
1025 1032 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1026 1033
1027 1034 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
1028 1035 controller='files', action='diff', f_path='',
1029 1036 conditions={'function': check_repo},
1030 1037 requirements=URL_NAME_REQUIREMENTS)
1031 1038
1032 1039 rmap.connect('files_diff_2way_home',
1033 1040 '/{repo_name}/diff-2way/{f_path}',
1034 1041 controller='files', action='diff_2way', f_path='',
1035 1042 conditions={'function': check_repo},
1036 1043 requirements=URL_NAME_REQUIREMENTS)
1037 1044
1038 1045 rmap.connect('files_rawfile_home',
1039 1046 '/{repo_name}/rawfile/{revision}/{f_path}',
1040 1047 controller='files', action='rawfile', revision='tip',
1041 1048 f_path='', conditions={'function': check_repo},
1042 1049 requirements=URL_NAME_REQUIREMENTS)
1043 1050
1044 1051 rmap.connect('files_raw_home',
1045 1052 '/{repo_name}/raw/{revision}/{f_path}',
1046 1053 controller='files', action='raw', revision='tip', f_path='',
1047 1054 conditions={'function': check_repo},
1048 1055 requirements=URL_NAME_REQUIREMENTS)
1049 1056
1050 1057 rmap.connect('files_render_home',
1051 1058 '/{repo_name}/render/{revision}/{f_path}',
1052 1059 controller='files', action='index', revision='tip', f_path='',
1053 1060 rendered=True, conditions={'function': check_repo},
1054 1061 requirements=URL_NAME_REQUIREMENTS)
1055 1062
1056 1063 rmap.connect('files_annotate_home',
1057 1064 '/{repo_name}/annotate/{revision}/{f_path}',
1058 1065 controller='files', action='index', revision='tip',
1059 1066 f_path='', annotate=True, conditions={'function': check_repo},
1060 1067 requirements=URL_NAME_REQUIREMENTS)
1061 1068
1062 1069 rmap.connect('files_edit',
1063 1070 '/{repo_name}/edit/{revision}/{f_path}',
1064 1071 controller='files', action='edit', revision='tip',
1065 1072 f_path='',
1066 1073 conditions={'function': check_repo, 'method': ['POST']},
1067 1074 requirements=URL_NAME_REQUIREMENTS)
1068 1075
1069 1076 rmap.connect('files_edit_home',
1070 1077 '/{repo_name}/edit/{revision}/{f_path}',
1071 1078 controller='files', action='edit_home', revision='tip',
1072 1079 f_path='', conditions={'function': check_repo},
1073 1080 requirements=URL_NAME_REQUIREMENTS)
1074 1081
1075 1082 rmap.connect('files_add',
1076 1083 '/{repo_name}/add/{revision}/{f_path}',
1077 1084 controller='files', action='add', revision='tip',
1078 1085 f_path='',
1079 1086 conditions={'function': check_repo, 'method': ['POST']},
1080 1087 requirements=URL_NAME_REQUIREMENTS)
1081 1088
1082 1089 rmap.connect('files_add_home',
1083 1090 '/{repo_name}/add/{revision}/{f_path}',
1084 1091 controller='files', action='add_home', revision='tip',
1085 1092 f_path='', conditions={'function': check_repo},
1086 1093 requirements=URL_NAME_REQUIREMENTS)
1087 1094
1088 1095 rmap.connect('files_delete',
1089 1096 '/{repo_name}/delete/{revision}/{f_path}',
1090 1097 controller='files', action='delete', revision='tip',
1091 1098 f_path='',
1092 1099 conditions={'function': check_repo, 'method': ['POST']},
1093 1100 requirements=URL_NAME_REQUIREMENTS)
1094 1101
1095 1102 rmap.connect('files_delete_home',
1096 1103 '/{repo_name}/delete/{revision}/{f_path}',
1097 1104 controller='files', action='delete_home', revision='tip',
1098 1105 f_path='', conditions={'function': check_repo},
1099 1106 requirements=URL_NAME_REQUIREMENTS)
1100 1107
1101 1108 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
1102 1109 controller='files', action='archivefile',
1103 1110 conditions={'function': check_repo},
1104 1111 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1105 1112
1106 1113 rmap.connect('files_nodelist_home',
1107 1114 '/{repo_name}/nodelist/{revision}/{f_path}',
1108 1115 controller='files', action='nodelist',
1109 1116 conditions={'function': check_repo},
1110 1117 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1111 1118
1112 1119 rmap.connect('files_nodetree_full',
1113 1120 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
1114 1121 controller='files', action='nodetree_full',
1115 1122 conditions={'function': check_repo},
1116 1123 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1117 1124
1118 1125 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
1119 1126 controller='forks', action='fork_create',
1120 1127 conditions={'function': check_repo, 'method': ['POST']},
1121 1128 requirements=URL_NAME_REQUIREMENTS)
1122 1129
1123 1130 rmap.connect('repo_fork_home', '/{repo_name}/fork',
1124 1131 controller='forks', action='fork',
1125 1132 conditions={'function': check_repo},
1126 1133 requirements=URL_NAME_REQUIREMENTS)
1127 1134
1128 1135 rmap.connect('repo_forks_home', '/{repo_name}/forks',
1129 1136 controller='forks', action='forks',
1130 1137 conditions={'function': check_repo},
1131 1138 requirements=URL_NAME_REQUIREMENTS)
1132 1139
1133 1140 rmap.connect('repo_followers_home', '/{repo_name}/followers',
1134 1141 controller='followers', action='followers',
1135 1142 conditions={'function': check_repo},
1136 1143 requirements=URL_NAME_REQUIREMENTS)
1137 1144
1138 1145 # must be here for proper group/repo catching pattern
1139 1146 _connect_with_slash(
1140 1147 rmap, 'repo_group_home', '/{group_name}',
1141 1148 controller='home', action='index_repo_group',
1142 1149 conditions={'function': check_group},
1143 1150 requirements=URL_NAME_REQUIREMENTS)
1144 1151
1145 1152 # catch all, at the end
1146 1153 _connect_with_slash(
1147 1154 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1148 1155 controller='summary', action='index',
1149 1156 conditions={'function': check_repo},
1150 1157 requirements=URL_NAME_REQUIREMENTS)
1151 1158
1152 1159 return rmap
1153 1160
1154 1161
1155 1162 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1156 1163 """
1157 1164 Connect a route with an optional trailing slash in `path`.
1158 1165 """
1159 1166 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1160 1167 mapper.connect(name, path, *args, **kwargs)
@@ -1,289 +1,288 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 Home controller for RhodeCode Enterprise
23 23 """
24 24
25 25 import logging
26 26 import time
27 27 import re
28 28
29 29 from pylons import tmpl_context as c, request, url, config
30 30 from pylons.i18n.translation import _
31 31 from sqlalchemy.sql import func
32 32
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasPermissionAllDecorator, AuthUser,
35 35 HasRepoGroupPermissionAnyDecorator, XHRRequired)
36 36 from rhodecode.lib.base import BaseController, render
37 37 from rhodecode.lib.index import searcher_from_config
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.utils import jsonify
40 40 from rhodecode.lib.utils2 import safe_unicode, str2bool
41 41 from rhodecode.model.db import Repository, RepoGroup
42 42 from rhodecode.model.repo import RepoModel
43 43 from rhodecode.model.repo_group import RepoGroupModel
44 44 from rhodecode.model.scm import RepoList, RepoGroupList
45 45
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class HomeController(BaseController):
51 51 def __before__(self):
52 52 super(HomeController, self).__before__()
53 53
54 54 def ping(self):
55 55 """
56 56 Ping, doesn't require login, good for checking out the platform
57 57 """
58 58 instance_id = getattr(c, 'rhodecode_instanceid', '')
59 59 return 'pong[%s] => %s' % (instance_id, self.ip_addr,)
60 60
61 61 @LoginRequired()
62 62 @HasPermissionAllDecorator('hg.admin')
63 63 def error_test(self):
64 64 """
65 65 Test exception handling and emails on errors
66 66 """
67 67 class TestException(Exception):
68 68 pass
69 69
70 70 msg = ('RhodeCode Enterprise %s test exception. Generation time: %s'
71 71 % (c.rhodecode_name, time.time()))
72 72 raise TestException(msg)
73 73
74 74 def _get_groups_and_repos(self, repo_group_id=None):
75 75 # repo groups groups
76 76 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
77 77 _perms = ['group.read', 'group.write', 'group.admin']
78 78 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
79 79 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
80 80 repo_group_list=repo_group_list_acl, admin=False)
81 81
82 82 # repositories
83 83 repo_list = Repository.get_all_repos(group_id=repo_group_id)
84 84 _perms = ['repository.read', 'repository.write', 'repository.admin']
85 85 repo_list_acl = RepoList(repo_list, perm_set=_perms)
86 86 repo_data = RepoModel().get_repos_as_dict(
87 87 repo_list=repo_list_acl, admin=False)
88 88
89 89 return repo_data, repo_group_data
90 90
91 91 @LoginRequired()
92 92 def index(self):
93 93 c.repo_group = None
94 94
95 95 repo_data, repo_group_data = self._get_groups_and_repos()
96 96 # json used to render the grids
97 97 c.repos_data = json.dumps(repo_data)
98 98 c.repo_groups_data = json.dumps(repo_group_data)
99 99
100 100 return render('/index.html')
101 101
102 102 @LoginRequired()
103 103 @HasRepoGroupPermissionAnyDecorator('group.read', 'group.write',
104 104 'group.admin')
105 105 def index_repo_group(self, group_name):
106 106 """GET /repo_group_name: Show a specific item"""
107 107 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
108 108 repo_data, repo_group_data = self._get_groups_and_repos(
109 109 c.repo_group.group_id)
110 110
111 111 # json used to render the grids
112 112 c.repos_data = json.dumps(repo_data)
113 113 c.repo_groups_data = json.dumps(repo_group_data)
114 114
115 115 return render('index_repo_group.html')
116 116
117 117 def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
118 118 query = Repository.query()\
119 119 .order_by(func.length(Repository.repo_name))\
120 120 .order_by(Repository.repo_name)
121 121
122 122 if repo_type:
123 123 query = query.filter(Repository.repo_type == repo_type)
124 124
125 125 if name_contains:
126 126 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
127 127 query = query.filter(
128 128 Repository.repo_name.ilike(ilike_expression))
129 129 query = query.limit(limit)
130 130
131 131 all_repos = query.all()
132 132 repo_iter = self.scm_model.get_repos(all_repos)
133 133 return [
134 134 {
135 135 'id': obj['name'],
136 136 'text': obj['name'],
137 137 'type': 'repo',
138 138 'obj': obj['dbrepo'],
139 139 'url': url('summary_home', repo_name=obj['name'])
140 140 }
141 141 for obj in repo_iter]
142 142
143 143 def _get_repo_group_list(self, name_contains=None, limit=20):
144 144 query = RepoGroup.query()\
145 145 .order_by(func.length(RepoGroup.group_name))\
146 146 .order_by(RepoGroup.group_name)
147 147
148 148 if name_contains:
149 149 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
150 150 query = query.filter(
151 151 RepoGroup.group_name.ilike(ilike_expression))
152 152 query = query.limit(limit)
153 153
154 154 all_groups = query.all()
155 155 repo_groups_iter = self.scm_model.get_repo_groups(all_groups)
156 156 return [
157 157 {
158 158 'id': obj.group_name,
159 159 'text': obj.group_name,
160 160 'type': 'group',
161 161 'obj': {},
162 162 'url': url('repo_group_home', group_name=obj.group_name)
163 163 }
164 164 for obj in repo_groups_iter]
165 165
166 166 def _get_hash_commit_list(self, hash_starts_with=None, limit=20):
167 167 if not hash_starts_with or len(hash_starts_with) < 3:
168 168 return []
169 169
170 170 commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with)
171 171
172 172 if len(commit_hashes) != 1:
173 173 return []
174 174
175 175 commit_hash_prefix = commit_hashes[0]
176 176
177 177 auth_user = AuthUser(
178 178 user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr)
179 179 searcher = searcher_from_config(config)
180 180 result = searcher.search(
181 181 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user)
182 182
183 183 return [
184 184 {
185 185 'id': entry['commit_id'],
186 186 'text': entry['commit_id'],
187 187 'type': 'commit',
188 188 'obj': {'repo': entry['repository']},
189 189 'url': url('changeset_home',
190 190 repo_name=entry['repository'], revision=entry['commit_id'])
191 191 }
192 192 for entry in result['results']]
193 193
194 194 @LoginRequired()
195 195 @XHRRequired()
196 196 @jsonify
197 197 def goto_switcher_data(self):
198 198 query = request.GET.get('query')
199 199 log.debug('generating goto switcher list, query %s', query)
200 200
201 201 res = []
202 202 repo_groups = self._get_repo_group_list(query)
203 203 if repo_groups:
204 204 res.append({
205 205 'text': _('Groups'),
206 206 'children': repo_groups
207 207 })
208 208
209 209 repos = self._get_repo_list(query)
210 210 if repos:
211 211 res.append({
212 212 'text': _('Repositories'),
213 213 'children': repos
214 214 })
215 215
216 216 commits = self._get_hash_commit_list(query)
217 217 if commits:
218 218 unique_repos = {}
219 219 for commit in commits:
220 220 unique_repos.setdefault(commit['obj']['repo'], []
221 221 ).append(commit)
222 222
223 223 for repo in unique_repos:
224 224 res.append({
225 225 'text': _('Commits in %(repo)s') % {'repo': repo},
226 226 'children': unique_repos[repo]
227 227 })
228 228
229 229 data = {
230 230 'more': False,
231 231 'results': res
232 232 }
233 233 return data
234 234
235 235 @LoginRequired()
236 236 @XHRRequired()
237 237 @jsonify
238 238 def repo_list_data(self):
239 239 query = request.GET.get('query')
240 240 repo_type = request.GET.get('repo_type')
241 241 log.debug('generating repo list, query:%s', query)
242 242
243 243 res = []
244 244 repos = self._get_repo_list(query, repo_type=repo_type)
245 245 if repos:
246 246 res.append({
247 247 'text': _('Repositories'),
248 248 'children': repos
249 249 })
250 250
251 251 data = {
252 252 'more': False,
253 253 'results': res
254 254 }
255 255 return data
256 256
257 257 @LoginRequired()
258 258 @XHRRequired()
259 259 @jsonify
260 260 def user_autocomplete_data(self):
261 261 query = request.GET.get('query')
262 262 active = str2bool(request.GET.get('active') or True)
263 263
264 264 repo_model = RepoModel()
265 265 _users = repo_model.get_users(
266 266 name_contains=query, only_active=active)
267 267
268 268 if request.GET.get('user_groups'):
269 269 # extend with user groups
270 270 _user_groups = repo_model.get_user_groups(
271 271 name_contains=query, only_active=active)
272 272 _users = _users + _user_groups
273 273
274 274 return {'suggestions': _users}
275 275
276 276 @LoginRequired()
277 277 @XHRRequired()
278 278 @jsonify
279 279 def user_group_autocomplete_data(self):
280 280 query = request.GET.get('query')
281 281 active = str2bool(request.GET.get('active') or True)
282 282
283 283 repo_model = RepoModel()
284 284 _user_groups = repo_model.get_user_groups(
285 285 name_contains=query, only_active=active)
286 286 _user_groups = _user_groups
287 287
288 288 return {'suggestions': _user_groups}
289
@@ -1,308 +1,318 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 Summary controller for RhodeCode Enterprise
23 23 """
24 24
25 25 import logging
26 26 from string import lower
27 27
28 28 from pylons import tmpl_context as c, request
29 29 from pylons.i18n.translation import _
30 30 from beaker.cache import cache_region, region_invalidate
31 31
32 32 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
33 33 from rhodecode.controllers import utils
34 34 from rhodecode.controllers.changelog import _load_changelog_summary
35 35 from rhodecode.lib import caches, helpers as h
36 36 from rhodecode.lib.utils import jsonify
37 37 from rhodecode.lib.utils2 import safe_str
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, XHRRequired)
40 40 from rhodecode.lib.base import BaseRepoController, render
41 41 from rhodecode.lib.markup_renderer import MarkupRenderer
42 42 from rhodecode.lib.ext_json import json
43 43 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 44 from rhodecode.lib.vcs.exceptions import (
45 45 CommitError, EmptyRepositoryError, NodeDoesNotExistError)
46 46 from rhodecode.model.db import Statistics, CacheKey, User
47 47 from rhodecode.model.repo import ReadmeFinder
48 48
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class SummaryController(BaseRepoController):
54 54
55 55 def __before__(self):
56 56 super(SummaryController, self).__before__()
57 57
58 58 def __get_readme_data(self, db_repo):
59 59 repo_name = db_repo.repo_name
60 60 log.debug('Looking for README file')
61 61 default_renderer = c.visual.default_renderer
62 62
63 63 @cache_region('long_term')
64 64 def _generate_readme(cache_key):
65 65 readme_data = None
66 66 readme_node = None
67 67 readme_filename = None
68 68 commit = self._get_landing_commit_or_none(db_repo)
69 69 if commit:
70 70 log.debug("Searching for a README file.")
71 71 readme_node = ReadmeFinder(default_renderer).search(commit)
72 72 if readme_node:
73 73 readme_data = self._render_readme_or_none(commit, readme_node)
74 74 readme_filename = readme_node.path
75 75 return readme_data, readme_filename
76 76
77 77 invalidator_context = CacheKey.repo_context_cache(
78 78 _generate_readme, repo_name, CacheKey.CACHE_TYPE_README)
79 79
80 80 with invalidator_context as context:
81 81 context.invalidate()
82 82 computed = context.compute()
83 83
84 84 return computed
85 85
86 86 def _get_landing_commit_or_none(self, db_repo):
87 87 log.debug("Getting the landing commit.")
88 88 try:
89 89 commit = db_repo.get_landing_commit()
90 90 if not isinstance(commit, EmptyCommit):
91 91 return commit
92 92 else:
93 93 log.debug("Repository is empty, no README to render.")
94 94 except CommitError:
95 95 log.exception(
96 96 "Problem getting commit when trying to render the README.")
97 97
98 98 def _render_readme_or_none(self, commit, readme_node):
99 99 log.debug(
100 100 'Found README file `%s` rendering...', readme_node.path)
101 101 renderer = MarkupRenderer()
102 102 try:
103 103 return renderer.render(
104 104 readme_node.content, filename=readme_node.path)
105 105 except Exception:
106 106 log.exception(
107 107 "Exception while trying to render the README")
108 108
109 109 @LoginRequired()
110 110 @HasRepoPermissionAnyDecorator(
111 111 'repository.read', 'repository.write', 'repository.admin')
112 112 def index(self, repo_name):
113 113
114 114 # Prepare the clone URL
115 115
116 116 username = ''
117 117 if c.rhodecode_user.username != User.DEFAULT_USER:
118 118 username = safe_str(c.rhodecode_user.username)
119 119
120 120 _def_clone_uri = _def_clone_uri_by_id = c.clone_uri_tmpl
121 121 if '{repo}' in _def_clone_uri:
122 122 _def_clone_uri_by_id = _def_clone_uri.replace(
123 123 '{repo}', '_{repoid}')
124 124 elif '{repoid}' in _def_clone_uri:
125 125 _def_clone_uri_by_id = _def_clone_uri.replace(
126 126 '_{repoid}', '{repo}')
127 127
128 128 c.clone_repo_url = c.rhodecode_db_repo.clone_url(
129 129 user=username, uri_tmpl=_def_clone_uri)
130 130 c.clone_repo_url_id = c.rhodecode_db_repo.clone_url(
131 131 user=username, uri_tmpl=_def_clone_uri_by_id)
132 132
133 133 # If enabled, get statistics data
134 134
135 135 c.show_stats = bool(c.rhodecode_db_repo.enable_statistics)
136 136
137 137 stats = self.sa.query(Statistics)\
138 138 .filter(Statistics.repository == c.rhodecode_db_repo)\
139 139 .scalar()
140 140
141 141 c.stats_percentage = 0
142 142
143 143 if stats and stats.languages:
144 144 c.no_data = False is c.rhodecode_db_repo.enable_statistics
145 145 lang_stats_d = json.loads(stats.languages)
146 146
147 147 # Sort first by decreasing count and second by the file extension,
148 148 # so we have a consistent output.
149 149 lang_stats_items = sorted(lang_stats_d.iteritems(),
150 150 key=lambda k: (-k[1], k[0]))[:10]
151 151 lang_stats = [(x, {"count": y,
152 152 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
153 153 for x, y in lang_stats_items]
154 154
155 155 c.trending_languages = json.dumps(lang_stats)
156 156 else:
157 157 c.no_data = True
158 158 c.trending_languages = json.dumps({})
159 159
160 160 c.enable_downloads = c.rhodecode_db_repo.enable_downloads
161 161 c.repository_followers = self.scm_model.get_followers(
162 162 c.rhodecode_db_repo)
163 163 c.repository_forks = self.scm_model.get_forks(c.rhodecode_db_repo)
164 164 c.repository_is_user_following = self.scm_model.is_following_repo(
165 165 c.repo_name, c.rhodecode_user.user_id)
166 166
167 167 if c.repository_requirements_missing:
168 168 return render('summary/missing_requirements.html')
169 169
170 170 c.readme_data, c.readme_file = \
171 171 self.__get_readme_data(c.rhodecode_db_repo)
172 172
173 173 _load_changelog_summary()
174 174
175 175 if request.is_xhr:
176 176 return render('changelog/changelog_summary_data.html')
177 177
178 178 return render('summary/summary.html')
179 179
180 180 @LoginRequired()
181 181 @XHRRequired()
182 182 @HasRepoPermissionAnyDecorator(
183 183 'repository.read', 'repository.write', 'repository.admin')
184 184 @jsonify
185 185 def repo_stats(self, repo_name, commit_id):
186 186 _namespace = caches.get_repo_namespace_key(
187 187 caches.SUMMARY_STATS, repo_name)
188 188 show_stats = bool(c.rhodecode_db_repo.enable_statistics)
189 189 cache_manager = caches.get_cache_manager('repo_cache_long', _namespace)
190 190 _cache_key = caches.compute_key_from_params(
191 191 repo_name, commit_id, show_stats)
192 192
193 193 def compute_stats():
194 194 code_stats = {}
195 195 size = 0
196 196 try:
197 197 scm_instance = c.rhodecode_db_repo.scm_instance()
198 198 commit = scm_instance.get_commit(commit_id)
199 199
200 200 for node in commit.get_filenodes_generator():
201 201 size += node.size
202 202 if not show_stats:
203 203 continue
204 204 ext = lower(node.extension)
205 205 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
206 206 if ext_info:
207 207 if ext in code_stats:
208 208 code_stats[ext]['count'] += 1
209 209 else:
210 210 code_stats[ext] = {"count": 1, "desc": ext_info}
211 211 except EmptyRepositoryError:
212 212 pass
213 213 return {'size': h.format_byte_size_binary(size),
214 214 'code_stats': code_stats}
215 215
216 216 stats = cache_manager.get(_cache_key, createfunc=compute_stats)
217 217 return stats
218 218
219 219 def _switcher_reference_data(self, repo_name, references, is_svn):
220 220 """Prepare reference data for given `references`"""
221 221 items = []
222 222 for name, commit_id in references.items():
223 223 use_commit_id = '/' in name or is_svn
224 224 items.append({
225 225 'name': name,
226 226 'commit_id': commit_id,
227 227 'files_url': h.url(
228 228 'files_home',
229 229 repo_name=repo_name,
230 230 f_path=name if is_svn else '',
231 231 revision=commit_id if use_commit_id else name,
232 232 at=name)
233 233 })
234 234 return items
235 235
236 236 @LoginRequired()
237 237 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
238 238 'repository.admin')
239 239 @jsonify
240 240 def repo_refs_data(self, repo_name):
241 241 repo = c.rhodecode_repo
242 242 refs_to_create = [
243 243 (_("Branch"), repo.branches, 'branch'),
244 244 (_("Tag"), repo.tags, 'tag'),
245 245 (_("Bookmark"), repo.bookmarks, 'book'),
246 246 ]
247 247 res = self._create_reference_data(repo, repo_name, refs_to_create)
248 248 data = {
249 249 'more': False,
250 250 'results': res
251 251 }
252 252 return data
253 253
254 @LoginRequired()
255 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
256 'repository.admin')
257 @jsonify
258 def repo_default_reviewers_data(self, repo_name):
259 return {
260 'reviewers': [utils.reviewer_as_json(
261 user=c.rhodecode_db_repo.user, reasons=None)]
262 }
263
254 264 @jsonify
255 265 def repo_refs_changelog_data(self, repo_name):
256 266 repo = c.rhodecode_repo
257 267
258 268 refs_to_create = [
259 269 (_("Branches"), repo.branches, 'branch'),
260 270 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
261 271 # TODO: enable when vcs can handle bookmarks filters
262 272 # (_("Bookmarks"), repo.bookmarks, "book"),
263 273 ]
264 274 res = self._create_reference_data(repo, repo_name, refs_to_create)
265 275 data = {
266 276 'more': False,
267 277 'results': res
268 278 }
269 279 return data
270 280
271 281 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
272 282 format_ref_id = utils.get_format_ref_id(repo)
273 283
274 284 result = []
275 285 for title, refs, ref_type in refs_to_create:
276 286 if refs:
277 287 result.append({
278 288 'text': title,
279 289 'children': self._create_reference_items(
280 290 repo, full_repo_name, refs, ref_type, format_ref_id),
281 291 })
282 292 return result
283 293
284 294 def _create_reference_items(self, repo, full_repo_name, refs, ref_type,
285 295 format_ref_id):
286 296 result = []
287 297 is_svn = h.is_svn(repo)
288 298 for ref_name, raw_id in refs.iteritems():
289 299 files_url = self._create_files_url(
290 300 repo, full_repo_name, ref_name, raw_id, is_svn)
291 301 result.append({
292 302 'text': ref_name,
293 303 'id': format_ref_id(ref_name, raw_id),
294 304 'raw_id': raw_id,
295 305 'type': ref_type,
296 306 'files_url': files_url,
297 307 })
298 308 return result
299 309
300 310 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id,
301 311 is_svn):
302 312 use_commit_id = '/' in ref_name or is_svn
303 313 return h.url(
304 314 'files_home',
305 315 repo_name=full_repo_name,
306 316 f_path=ref_name if is_svn else '',
307 317 revision=raw_id if use_commit_id else ref_name,
308 318 at=ref_name)
@@ -1,88 +1,106 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 Utilities to be shared by multiple controllers.
23 23
24 24 Should only contain utilities to be shared in the controller layer.
25 25 """
26 26
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib.vcs.exceptions import RepositoryError
29 29
30 30 def parse_path_ref(ref, default_path=None):
31 31 """
32 32 Parse out a path and reference combination and return both parts of it.
33 33
34 34 This is used to allow support of path based comparisons for Subversion
35 35 as an iterim solution in parameter handling.
36 36 """
37 37 if '@' in ref:
38 38 return ref.rsplit('@', 1)
39 39 else:
40 40 return default_path, ref
41 41
42 42
43 43 def get_format_ref_id(repo):
44 44 """Returns a `repo` specific reference formatter function"""
45 45 if h.is_svn(repo):
46 46 return _format_ref_id_svn
47 47 else:
48 48 return _format_ref_id
49 49
50 50
51 51 def _format_ref_id(name, raw_id):
52 52 """Default formatting of a given reference `name`"""
53 53 return name
54 54
55 55
56 56 def _format_ref_id_svn(name, raw_id):
57 57 """Special way of formatting a reference for Subversion including path"""
58 58 return '%s@%s' % (name, raw_id)
59 59
60 60
61 61 def get_commit_from_ref_name(repo, ref_name, ref_type=None):
62 62 """
63 63 Gets the commit for a `ref_name` taking into account `ref_type`.
64 64 Needed in case a bookmark / tag share the same name.
65 65
66 66 :param repo: the repo instance
67 67 :param ref_name: the name of the ref to get
68 68 :param ref_type: optional, used to disambiguate colliding refs
69 69 """
70 70 repo_scm = repo.scm_instance()
71 71 ref_type_mapping = {
72 72 'book': repo_scm.bookmarks,
73 73 'bookmark': repo_scm.bookmarks,
74 74 'tag': repo_scm.tags,
75 75 'branch': repo_scm.branches,
76 76 }
77 77
78 78 commit_id = ref_name
79 79 if repo_scm.alias != 'svn': # pass svn refs straight to backend until
80 80 # the branch issue with svn is fixed
81 81 if ref_type and ref_type in ref_type_mapping:
82 82 try:
83 83 commit_id = ref_type_mapping[ref_type][ref_name]
84 84 except KeyError:
85 85 raise RepositoryError(
86 86 '%s "%s" does not exist' % (ref_type, ref_name))
87 87
88 88 return repo_scm.get_commit(commit_id)
89
90
91 def reviewer_as_json(user, reasons):
92 """
93 Returns json struct of a reviewer for frontend
94
95 :param user: the reviewer
96 :param reasons: list of strings of why they are reviewers
97 """
98
99 return {
100 'user_id': user.user_id,
101 'reasons': reasons,
102 'username': user.username,
103 'firstname': user.firstname,
104 'lastname': user.lastname,
105 'gravatar_link': h.gravatar_url(user.email, 14),
106 }
@@ -1,895 +1,936 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 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 """
23 23 Some simple helper functions
24 24 """
25 25
26 26
27 27 import collections
28 28 import datetime
29 29 import dateutil.relativedelta
30 30 import hashlib
31 31 import logging
32 32 import re
33 33 import sys
34 34 import time
35 35 import threading
36 36 import urllib
37 37 import urlobject
38 38 import uuid
39 39
40 40 import pygments.lexers
41 41 import sqlalchemy
42 42 import sqlalchemy.engine.url
43 43 import webob
44 44 import routes.util
45 45
46 46 import rhodecode
47 47
48 48
49 49 def md5(s):
50 50 return hashlib.md5(s).hexdigest()
51 51
52 52
53 53 def md5_safe(s):
54 54 return md5(safe_str(s))
55 55
56 56
57 57 def __get_lem(extra_mapping=None):
58 58 """
59 59 Get language extension map based on what's inside pygments lexers
60 60 """
61 61 d = collections.defaultdict(lambda: [])
62 62
63 63 def __clean(s):
64 64 s = s.lstrip('*')
65 65 s = s.lstrip('.')
66 66
67 67 if s.find('[') != -1:
68 68 exts = []
69 69 start, stop = s.find('['), s.find(']')
70 70
71 71 for suffix in s[start + 1:stop]:
72 72 exts.append(s[:s.find('[')] + suffix)
73 73 return [e.lower() for e in exts]
74 74 else:
75 75 return [s.lower()]
76 76
77 77 for lx, t in sorted(pygments.lexers.LEXERS.items()):
78 78 m = map(__clean, t[-2])
79 79 if m:
80 80 m = reduce(lambda x, y: x + y, m)
81 81 for ext in m:
82 82 desc = lx.replace('Lexer', '')
83 83 d[ext].append(desc)
84 84
85 85 data = dict(d)
86 86
87 87 extra_mapping = extra_mapping or {}
88 88 if extra_mapping:
89 89 for k, v in extra_mapping.items():
90 90 if k not in data:
91 91 # register new mapping2lexer
92 92 data[k] = [v]
93 93
94 94 return data
95 95
96 96
97 97 def str2bool(_str):
98 98 """
99 99 returs True/False value from given string, it tries to translate the
100 100 string into boolean
101 101
102 102 :param _str: string value to translate into boolean
103 103 :rtype: boolean
104 104 :returns: boolean from given string
105 105 """
106 106 if _str is None:
107 107 return False
108 108 if _str in (True, False):
109 109 return _str
110 110 _str = str(_str).strip().lower()
111 111 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
112 112
113 113
114 114 def aslist(obj, sep=None, strip=True):
115 115 """
116 116 Returns given string separated by sep as list
117 117
118 118 :param obj:
119 119 :param sep:
120 120 :param strip:
121 121 """
122 122 if isinstance(obj, (basestring,)):
123 123 lst = obj.split(sep)
124 124 if strip:
125 125 lst = [v.strip() for v in lst]
126 126 return lst
127 127 elif isinstance(obj, (list, tuple)):
128 128 return obj
129 129 elif obj is None:
130 130 return []
131 131 else:
132 132 return [obj]
133 133
134 134
135 135 def convert_line_endings(line, mode):
136 136 """
137 137 Converts a given line "line end" accordingly to given mode
138 138
139 139 Available modes are::
140 140 0 - Unix
141 141 1 - Mac
142 142 2 - DOS
143 143
144 144 :param line: given line to convert
145 145 :param mode: mode to convert to
146 146 :rtype: str
147 147 :return: converted line according to mode
148 148 """
149 149 if mode == 0:
150 150 line = line.replace('\r\n', '\n')
151 151 line = line.replace('\r', '\n')
152 152 elif mode == 1:
153 153 line = line.replace('\r\n', '\r')
154 154 line = line.replace('\n', '\r')
155 155 elif mode == 2:
156 156 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
157 157 return line
158 158
159 159
160 160 def detect_mode(line, default):
161 161 """
162 162 Detects line break for given line, if line break couldn't be found
163 163 given default value is returned
164 164
165 165 :param line: str line
166 166 :param default: default
167 167 :rtype: int
168 168 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
169 169 """
170 170 if line.endswith('\r\n'):
171 171 return 2
172 172 elif line.endswith('\n'):
173 173 return 0
174 174 elif line.endswith('\r'):
175 175 return 1
176 176 else:
177 177 return default
178 178
179 179
180 180 def safe_int(val, default=None):
181 181 """
182 182 Returns int() of val if val is not convertable to int use default
183 183 instead
184 184
185 185 :param val:
186 186 :param default:
187 187 """
188 188
189 189 try:
190 190 val = int(val)
191 191 except (ValueError, TypeError):
192 192 val = default
193 193
194 194 return val
195 195
196 196
197 197 def safe_unicode(str_, from_encoding=None):
198 198 """
199 199 safe unicode function. Does few trick to turn str_ into unicode
200 200
201 201 In case of UnicodeDecode error, we try to return it with encoding detected
202 202 by chardet library if it fails fallback to unicode with errors replaced
203 203
204 204 :param str_: string to decode
205 205 :rtype: unicode
206 206 :returns: unicode object
207 207 """
208 208 if isinstance(str_, unicode):
209 209 return str_
210 210
211 211 if not from_encoding:
212 212 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
213 213 'utf8'), sep=',')
214 214 from_encoding = DEFAULT_ENCODINGS
215 215
216 216 if not isinstance(from_encoding, (list, tuple)):
217 217 from_encoding = [from_encoding]
218 218
219 219 try:
220 220 return unicode(str_)
221 221 except UnicodeDecodeError:
222 222 pass
223 223
224 224 for enc in from_encoding:
225 225 try:
226 226 return unicode(str_, enc)
227 227 except UnicodeDecodeError:
228 228 pass
229 229
230 230 try:
231 231 import chardet
232 232 encoding = chardet.detect(str_)['encoding']
233 233 if encoding is None:
234 234 raise Exception()
235 235 return str_.decode(encoding)
236 236 except (ImportError, UnicodeDecodeError, Exception):
237 237 return unicode(str_, from_encoding[0], 'replace')
238 238
239 239
240 240 def safe_str(unicode_, to_encoding=None):
241 241 """
242 242 safe str function. Does few trick to turn unicode_ into string
243 243
244 244 In case of UnicodeEncodeError, we try to return it with encoding detected
245 245 by chardet library if it fails fallback to string with errors replaced
246 246
247 247 :param unicode_: unicode to encode
248 248 :rtype: str
249 249 :returns: str object
250 250 """
251 251
252 252 # if it's not basestr cast to str
253 253 if not isinstance(unicode_, basestring):
254 254 return str(unicode_)
255 255
256 256 if isinstance(unicode_, str):
257 257 return unicode_
258 258
259 259 if not to_encoding:
260 260 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
261 261 'utf8'), sep=',')
262 262 to_encoding = DEFAULT_ENCODINGS
263 263
264 264 if not isinstance(to_encoding, (list, tuple)):
265 265 to_encoding = [to_encoding]
266 266
267 267 for enc in to_encoding:
268 268 try:
269 269 return unicode_.encode(enc)
270 270 except UnicodeEncodeError:
271 271 pass
272 272
273 273 try:
274 274 import chardet
275 275 encoding = chardet.detect(unicode_)['encoding']
276 276 if encoding is None:
277 277 raise UnicodeEncodeError()
278 278
279 279 return unicode_.encode(encoding)
280 280 except (ImportError, UnicodeEncodeError):
281 281 return unicode_.encode(to_encoding[0], 'replace')
282 282
283 283
284 284 def remove_suffix(s, suffix):
285 285 if s.endswith(suffix):
286 286 s = s[:-1 * len(suffix)]
287 287 return s
288 288
289 289
290 290 def remove_prefix(s, prefix):
291 291 if s.startswith(prefix):
292 292 s = s[len(prefix):]
293 293 return s
294 294
295 295
296 296 def find_calling_context(ignore_modules=None):
297 297 """
298 298 Look through the calling stack and return the frame which called
299 299 this function and is part of core module ( ie. rhodecode.* )
300 300
301 301 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
302 302 """
303 303
304 304 ignore_modules = ignore_modules or []
305 305
306 306 f = sys._getframe(2)
307 307 while f.f_back is not None:
308 308 name = f.f_globals.get('__name__')
309 309 if name and name.startswith(__name__.split('.')[0]):
310 310 if name not in ignore_modules:
311 311 return f
312 312 f = f.f_back
313 313 return None
314 314
315 315
316 316 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
317 317 """Custom engine_from_config functions."""
318 318 log = logging.getLogger('sqlalchemy.engine')
319 319 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
320 320
321 321 def color_sql(sql):
322 322 color_seq = '\033[1;33m' # This is yellow: code 33
323 323 normal = '\x1b[0m'
324 324 return ''.join([color_seq, sql, normal])
325 325
326 326 if configuration['debug']:
327 327 # attach events only for debug configuration
328 328
329 329 def before_cursor_execute(conn, cursor, statement,
330 330 parameters, context, executemany):
331 331 setattr(conn, 'query_start_time', time.time())
332 332 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
333 333 calling_context = find_calling_context(ignore_modules=[
334 334 'rhodecode.lib.caching_query',
335 335 'rhodecode.model.settings',
336 336 ])
337 337 if calling_context:
338 338 log.info(color_sql('call context %s:%s' % (
339 339 calling_context.f_code.co_filename,
340 340 calling_context.f_lineno,
341 341 )))
342 342
343 343 def after_cursor_execute(conn, cursor, statement,
344 344 parameters, context, executemany):
345 345 delattr(conn, 'query_start_time')
346 346
347 347 sqlalchemy.event.listen(engine, "before_cursor_execute",
348 348 before_cursor_execute)
349 349 sqlalchemy.event.listen(engine, "after_cursor_execute",
350 350 after_cursor_execute)
351 351
352 352 return engine
353 353
354 354
355 355 def get_encryption_key(config):
356 356 secret = config.get('rhodecode.encrypted_values.secret')
357 357 default = config['beaker.session.secret']
358 358 return secret or default
359 359
360 360
361 361 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
362 362 short_format=False):
363 363 """
364 364 Turns a datetime into an age string.
365 365 If show_short_version is True, this generates a shorter string with
366 366 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
367 367
368 368 * IMPORTANT*
369 369 Code of this function is written in special way so it's easier to
370 370 backport it to javascript. If you mean to update it, please also update
371 371 `jquery.timeago-extension.js` file
372 372
373 373 :param prevdate: datetime object
374 374 :param now: get current time, if not define we use
375 375 `datetime.datetime.now()`
376 376 :param show_short_version: if it should approximate the date and
377 377 return a shorter string
378 378 :param show_suffix:
379 379 :param short_format: show short format, eg 2D instead of 2 days
380 380 :rtype: unicode
381 381 :returns: unicode words describing age
382 382 """
383 383 from pylons.i18n.translation import _, ungettext
384 384
385 385 def _get_relative_delta(now, prevdate):
386 386 base = dateutil.relativedelta.relativedelta(now, prevdate)
387 387 return {
388 388 'year': base.years,
389 389 'month': base.months,
390 390 'day': base.days,
391 391 'hour': base.hours,
392 392 'minute': base.minutes,
393 393 'second': base.seconds,
394 394 }
395 395
396 396 def _is_leap_year(year):
397 397 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
398 398
399 399 def get_month(prevdate):
400 400 return prevdate.month
401 401
402 402 def get_year(prevdate):
403 403 return prevdate.year
404 404
405 405 now = now or datetime.datetime.now()
406 406 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
407 407 deltas = {}
408 408 future = False
409 409
410 410 if prevdate > now:
411 411 now_old = now
412 412 now = prevdate
413 413 prevdate = now_old
414 414 future = True
415 415 if future:
416 416 prevdate = prevdate.replace(microsecond=0)
417 417 # Get date parts deltas
418 418 for part in order:
419 419 rel_delta = _get_relative_delta(now, prevdate)
420 420 deltas[part] = rel_delta[part]
421 421
422 422 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
423 423 # not 1 hour, -59 minutes and -59 seconds)
424 424 offsets = [[5, 60], [4, 60], [3, 24]]
425 425 for element in offsets: # seconds, minutes, hours
426 426 num = element[0]
427 427 length = element[1]
428 428
429 429 part = order[num]
430 430 carry_part = order[num - 1]
431 431
432 432 if deltas[part] < 0:
433 433 deltas[part] += length
434 434 deltas[carry_part] -= 1
435 435
436 436 # Same thing for days except that the increment depends on the (variable)
437 437 # number of days in the month
438 438 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
439 439 if deltas['day'] < 0:
440 440 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
441 441 deltas['day'] += 29
442 442 else:
443 443 deltas['day'] += month_lengths[get_month(prevdate) - 1]
444 444
445 445 deltas['month'] -= 1
446 446
447 447 if deltas['month'] < 0:
448 448 deltas['month'] += 12
449 449 deltas['year'] -= 1
450 450
451 451 # Format the result
452 452 if short_format:
453 453 fmt_funcs = {
454 454 'year': lambda d: u'%dy' % d,
455 455 'month': lambda d: u'%dm' % d,
456 456 'day': lambda d: u'%dd' % d,
457 457 'hour': lambda d: u'%dh' % d,
458 458 'minute': lambda d: u'%dmin' % d,
459 459 'second': lambda d: u'%dsec' % d,
460 460 }
461 461 else:
462 462 fmt_funcs = {
463 463 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
464 464 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
465 465 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
466 466 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
467 467 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
468 468 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
469 469 }
470 470
471 471 i = 0
472 472 for part in order:
473 473 value = deltas[part]
474 474 if value != 0:
475 475
476 476 if i < 5:
477 477 sub_part = order[i + 1]
478 478 sub_value = deltas[sub_part]
479 479 else:
480 480 sub_value = 0
481 481
482 482 if sub_value == 0 or show_short_version:
483 483 _val = fmt_funcs[part](value)
484 484 if future:
485 485 if show_suffix:
486 486 return _(u'in %s') % _val
487 487 else:
488 488 return _val
489 489
490 490 else:
491 491 if show_suffix:
492 492 return _(u'%s ago') % _val
493 493 else:
494 494 return _val
495 495
496 496 val = fmt_funcs[part](value)
497 497 val_detail = fmt_funcs[sub_part](sub_value)
498 498
499 499 if short_format:
500 500 datetime_tmpl = u'%s, %s'
501 501 if show_suffix:
502 502 datetime_tmpl = _(u'%s, %s ago')
503 503 if future:
504 504 datetime_tmpl = _(u'in %s, %s')
505 505 else:
506 506 datetime_tmpl = _(u'%s and %s')
507 507 if show_suffix:
508 508 datetime_tmpl = _(u'%s and %s ago')
509 509 if future:
510 510 datetime_tmpl = _(u'in %s and %s')
511 511
512 512 return datetime_tmpl % (val, val_detail)
513 513 i += 1
514 514 return _(u'just now')
515 515
516 516
517 517 def uri_filter(uri):
518 518 """
519 519 Removes user:password from given url string
520 520
521 521 :param uri:
522 522 :rtype: unicode
523 523 :returns: filtered list of strings
524 524 """
525 525 if not uri:
526 526 return ''
527 527
528 528 proto = ''
529 529
530 530 for pat in ('https://', 'http://'):
531 531 if uri.startswith(pat):
532 532 uri = uri[len(pat):]
533 533 proto = pat
534 534 break
535 535
536 536 # remove passwords and username
537 537 uri = uri[uri.find('@') + 1:]
538 538
539 539 # get the port
540 540 cred_pos = uri.find(':')
541 541 if cred_pos == -1:
542 542 host, port = uri, None
543 543 else:
544 544 host, port = uri[:cred_pos], uri[cred_pos + 1:]
545 545
546 546 return filter(None, [proto, host, port])
547 547
548 548
549 549 def credentials_filter(uri):
550 550 """
551 551 Returns a url with removed credentials
552 552
553 553 :param uri:
554 554 """
555 555
556 556 uri = uri_filter(uri)
557 557 # check if we have port
558 558 if len(uri) > 2 and uri[2]:
559 559 uri[2] = ':' + uri[2]
560 560
561 561 return ''.join(uri)
562 562
563 563
564 564 def get_clone_url(uri_tmpl, qualifed_home_url, repo_name, repo_id, **override):
565 565 parsed_url = urlobject.URLObject(qualifed_home_url)
566 566 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
567 567 args = {
568 568 'scheme': parsed_url.scheme,
569 569 'user': '',
570 570 # path if we use proxy-prefix
571 571 'netloc': parsed_url.netloc+decoded_path,
572 572 'prefix': decoded_path,
573 573 'repo': repo_name,
574 574 'repoid': str(repo_id)
575 575 }
576 576 args.update(override)
577 577 args['user'] = urllib.quote(safe_str(args['user']))
578 578
579 579 for k, v in args.items():
580 580 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
581 581
582 582 # remove leading @ sign if it's present. Case of empty user
583 583 url_obj = urlobject.URLObject(uri_tmpl)
584 584 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
585 585
586 586 return safe_unicode(url)
587 587
588 588
589 589 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None):
590 590 """
591 591 Safe version of get_commit if this commit doesn't exists for a
592 592 repository it returns a Dummy one instead
593 593
594 594 :param repo: repository instance
595 595 :param commit_id: commit id as str
596 596 :param pre_load: optional list of commit attributes to load
597 597 """
598 598 # TODO(skreft): remove these circular imports
599 599 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
600 600 from rhodecode.lib.vcs.exceptions import RepositoryError
601 601 if not isinstance(repo, BaseRepository):
602 602 raise Exception('You must pass an Repository '
603 603 'object as first argument got %s', type(repo))
604 604
605 605 try:
606 606 commit = repo.get_commit(
607 607 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
608 608 except (RepositoryError, LookupError):
609 609 commit = EmptyCommit()
610 610 return commit
611 611
612 612
613 613 def datetime_to_time(dt):
614 614 if dt:
615 615 return time.mktime(dt.timetuple())
616 616
617 617
618 618 def time_to_datetime(tm):
619 619 if tm:
620 620 if isinstance(tm, basestring):
621 621 try:
622 622 tm = float(tm)
623 623 except ValueError:
624 624 return
625 625 return datetime.datetime.fromtimestamp(tm)
626 626
627 627
628 628 def time_to_utcdatetime(tm):
629 629 if tm:
630 630 if isinstance(tm, basestring):
631 631 try:
632 632 tm = float(tm)
633 633 except ValueError:
634 634 return
635 635 return datetime.datetime.utcfromtimestamp(tm)
636 636
637 637
638 638 MENTIONS_REGEX = re.compile(
639 639 # ^@ or @ without any special chars in front
640 640 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
641 641 # main body starts with letter, then can be . - _
642 642 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
643 643 re.VERBOSE | re.MULTILINE)
644 644
645 645
646 646 def extract_mentioned_users(s):
647 647 """
648 648 Returns unique usernames from given string s that have @mention
649 649
650 650 :param s: string to get mentions
651 651 """
652 652 usrs = set()
653 653 for username in MENTIONS_REGEX.findall(s):
654 654 usrs.add(username)
655 655
656 656 return sorted(list(usrs), key=lambda k: k.lower())
657 657
658 658
659 659 class AttributeDict(dict):
660 660 def __getattr__(self, attr):
661 661 return self.get(attr, None)
662 662 __setattr__ = dict.__setitem__
663 663 __delattr__ = dict.__delitem__
664 664
665 665
666 666 def fix_PATH(os_=None):
667 667 """
668 668 Get current active python path, and append it to PATH variable to fix
669 669 issues of subprocess calls and different python versions
670 670 """
671 671 if os_ is None:
672 672 import os
673 673 else:
674 674 os = os_
675 675
676 676 cur_path = os.path.split(sys.executable)[0]
677 677 if not os.environ['PATH'].startswith(cur_path):
678 678 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
679 679
680 680
681 681 def obfuscate_url_pw(engine):
682 682 _url = engine or ''
683 683 try:
684 684 _url = sqlalchemy.engine.url.make_url(engine)
685 685 if _url.password:
686 686 _url.password = 'XXXXX'
687 687 except Exception:
688 688 pass
689 689 return unicode(_url)
690 690
691 691
692 692 def get_server_url(environ):
693 693 req = webob.Request(environ)
694 694 return req.host_url + req.script_name
695 695
696 696
697 697 def unique_id(hexlen=32):
698 698 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
699 699 return suuid(truncate_to=hexlen, alphabet=alphabet)
700 700
701 701
702 702 def suuid(url=None, truncate_to=22, alphabet=None):
703 703 """
704 704 Generate and return a short URL safe UUID.
705 705
706 706 If the url parameter is provided, set the namespace to the provided
707 707 URL and generate a UUID.
708 708
709 709 :param url to get the uuid for
710 710 :truncate_to: truncate the basic 22 UUID to shorter version
711 711
712 712 The IDs won't be universally unique any longer, but the probability of
713 713 a collision will still be very low.
714 714 """
715 715 # Define our alphabet.
716 716 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
717 717
718 718 # If no URL is given, generate a random UUID.
719 719 if url is None:
720 720 unique_id = uuid.uuid4().int
721 721 else:
722 722 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
723 723
724 724 alphabet_length = len(_ALPHABET)
725 725 output = []
726 726 while unique_id > 0:
727 727 digit = unique_id % alphabet_length
728 728 output.append(_ALPHABET[digit])
729 729 unique_id = int(unique_id / alphabet_length)
730 730 return "".join(output)[:truncate_to]
731 731
732 732
733 733 def get_current_rhodecode_user():
734 734 """
735 735 Gets rhodecode user from threadlocal tmpl_context variable if it's
736 736 defined, else returns None.
737 737 """
738 738 from pylons import tmpl_context as c
739 739 if hasattr(c, 'rhodecode_user'):
740 740 return c.rhodecode_user
741 741
742 742 return None
743 743
744 744
745 745 def action_logger_generic(action, namespace=''):
746 746 """
747 747 A generic logger for actions useful to the system overview, tries to find
748 748 an acting user for the context of the call otherwise reports unknown user
749 749
750 750 :param action: logging message eg 'comment 5 deleted'
751 751 :param type: string
752 752
753 753 :param namespace: namespace of the logging message eg. 'repo.comments'
754 754 :param type: string
755 755
756 756 """
757 757
758 758 logger_name = 'rhodecode.actions'
759 759
760 760 if namespace:
761 761 logger_name += '.' + namespace
762 762
763 763 log = logging.getLogger(logger_name)
764 764
765 765 # get a user if we can
766 766 user = get_current_rhodecode_user()
767 767
768 768 logfunc = log.info
769 769
770 770 if not user:
771 771 user = '<unknown user>'
772 772 logfunc = log.warning
773 773
774 774 logfunc('Logging action by {}: {}'.format(user, action))
775 775
776 776
777 777 def escape_split(text, sep=',', maxsplit=-1):
778 778 r"""
779 779 Allows for escaping of the separator: e.g. arg='foo\, bar'
780 780
781 781 It should be noted that the way bash et. al. do command line parsing, those
782 782 single quotes are required.
783 783 """
784 784 escaped_sep = r'\%s' % sep
785 785
786 786 if escaped_sep not in text:
787 787 return text.split(sep, maxsplit)
788 788
789 789 before, _mid, after = text.partition(escaped_sep)
790 790 startlist = before.split(sep, maxsplit) # a regular split is fine here
791 791 unfinished = startlist[-1]
792 792 startlist = startlist[:-1]
793 793
794 794 # recurse because there may be more escaped separators
795 795 endlist = escape_split(after, sep, maxsplit)
796 796
797 797 # finish building the escaped value. we use endlist[0] becaue the first
798 798 # part of the string sent in recursion is the rest of the escaped value.
799 799 unfinished += sep + endlist[0]
800 800
801 801 return startlist + [unfinished] + endlist[1:] # put together all the parts
802 802
803 803
804 804 class OptionalAttr(object):
805 805 """
806 806 Special Optional Option that defines other attribute. Example::
807 807
808 808 def test(apiuser, userid=Optional(OAttr('apiuser')):
809 809 user = Optional.extract(userid)
810 810 # calls
811 811
812 812 """
813 813
814 814 def __init__(self, attr_name):
815 815 self.attr_name = attr_name
816 816
817 817 def __repr__(self):
818 818 return '<OptionalAttr:%s>' % self.attr_name
819 819
820 820 def __call__(self):
821 821 return self
822 822
823 823
824 824 # alias
825 825 OAttr = OptionalAttr
826 826
827 827
828 828 class Optional(object):
829 829 """
830 830 Defines an optional parameter::
831 831
832 832 param = param.getval() if isinstance(param, Optional) else param
833 833 param = param() if isinstance(param, Optional) else param
834 834
835 835 is equivalent of::
836 836
837 837 param = Optional.extract(param)
838 838
839 839 """
840 840
841 841 def __init__(self, type_):
842 842 self.type_ = type_
843 843
844 844 def __repr__(self):
845 845 return '<Optional:%s>' % self.type_.__repr__()
846 846
847 847 def __call__(self):
848 848 return self.getval()
849 849
850 850 def getval(self):
851 851 """
852 852 returns value from this Optional instance
853 853 """
854 854 if isinstance(self.type_, OAttr):
855 855 # use params name
856 856 return self.type_.attr_name
857 857 return self.type_
858 858
859 859 @classmethod
860 860 def extract(cls, val):
861 861 """
862 862 Extracts value from Optional() instance
863 863
864 864 :param val:
865 865 :return: original value if it's not Optional instance else
866 866 value of instance
867 867 """
868 868 if isinstance(val, cls):
869 869 return val.getval()
870 870 return val
871 871
872 872
873 873 def get_routes_generator_for_server_url(server_url):
874 874 parsed_url = urlobject.URLObject(server_url)
875 875 netloc = safe_str(parsed_url.netloc)
876 876 script_name = safe_str(parsed_url.path)
877 877
878 878 if ':' in netloc:
879 879 server_name, server_port = netloc.split(':')
880 880 else:
881 881 server_name = netloc
882 882 server_port = (parsed_url.scheme == 'https' and '443' or '80')
883 883
884 884 environ = {
885 885 'REQUEST_METHOD': 'GET',
886 886 'PATH_INFO': '/',
887 887 'SERVER_NAME': server_name,
888 888 'SERVER_PORT': server_port,
889 889 'SCRIPT_NAME': script_name,
890 890 }
891 891 if parsed_url.scheme == 'https':
892 892 environ['HTTPS'] = 'on'
893 893 environ['wsgi.url_scheme'] = 'https'
894 894
895 895 return routes.util.URLGenerator(rhodecode.CONFIG['routes.map'], environ)
896
897
898 def glob2re(pat):
899 """
900 Translate a shell PATTERN to a regular expression.
901
902 There is no way to quote meta-characters.
903 """
904
905 i, n = 0, len(pat)
906 res = ''
907 while i < n:
908 c = pat[i]
909 i = i+1
910 if c == '*':
911 #res = res + '.*'
912 res = res + '[^/]*'
913 elif c == '?':
914 #res = res + '.'
915 res = res + '[^/]'
916 elif c == '[':
917 j = i
918 if j < n and pat[j] == '!':
919 j = j+1
920 if j < n and pat[j] == ']':
921 j = j+1
922 while j < n and pat[j] != ']':
923 j = j+1
924 if j >= n:
925 res = res + '\\['
926 else:
927 stuff = pat[i:j].replace('\\','\\\\')
928 i = j+1
929 if stuff[0] == '!':
930 stuff = '^' + stuff[1:]
931 elif stuff[0] == '^':
932 stuff = '\\' + stuff
933 res = '%s[%s]' % (res, stuff)
934 else:
935 res = res + re.escape(c)
936 return res + '\Z(?ms)'
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -1,15 +1,26 b''
1 1 import os
2 import re
2 3
3 4 import ipaddress
4 5 import colander
5 6
6 7 from rhodecode.translation import _
8 from rhodecode.lib.utils2 import glob2re
7 9
8 10
9 11 def ip_addr_validator(node, value):
10 12 try:
11 13 # this raises an ValueError if address is not IpV4 or IpV6
12 14 ipaddress.ip_network(value, strict=False)
13 15 except ValueError:
14 16 msg = _(u'Please enter a valid IPv4 or IpV6 address')
15 17 raise colander.Invalid(node, msg)
18
19
20 def glob_validator(node, value):
21 try:
22 re.compile('^' + glob2re(value) + '$')
23 except Exception:
24 raise
25 msg = _(u'Invalid glob pattern')
26 raise colander.Invalid(node, msg)
@@ -1,117 +1,139 b''
1 1 .deform {
2 2
3 3 * {
4 4 box-sizing: border-box;
5 5 }
6 6
7 7 .required:after {
8 8 color: #e32;
9 9 content: '*';
10 10 display:inline;
11 11 }
12 12
13 13 .control-label {
14 14 width: 200px;
15 15 padding: 10px;
16 16 float: left;
17 17 }
18 18 .control-inputs {
19 19 width: 400px;
20 20 float: left;
21 21 }
22 22 .form-group .radio, .form-group .checkbox {
23 23 position: relative;
24 24 display: block;
25 25 /* margin-bottom: 10px; */
26 26 }
27 27
28 28 .form-group {
29 29 clear: left;
30 30 margin-bottom: 20px;
31 31
32 32 &:after { /* clear fix */
33 33 content: " ";
34 34 display: block;
35 35 clear: left;
36 36 }
37 37 }
38 38
39 39 .form-control {
40 40 width: 100%;
41 41 padding: 0.9em;
42 42 border: 1px solid #979797;
43 43 border-radius: 2px;
44 44 }
45 45 .form-control.select2-container {
46 46 padding: 0; /* padding already applied in .drop-menu a */
47 47 }
48 48
49 49 .form-control.readonly {
50 50 background: #eeeeee;
51 51 cursor: not-allowed;
52 52 }
53 53
54 54 .error-block {
55 55 color: red;
56 56 margin: 0;
57 57 }
58 58
59 59 .help-block {
60 60 margin: 0;
61 61 }
62 62
63 63 .deform-seq-container .control-inputs {
64 64 width: 100%;
65 65 }
66 66
67 67 .deform-seq-container .deform-seq-item-handle {
68 68 width: 8.3%;
69 69 float: left;
70 70 }
71 71
72 72 .deform-seq-container .deform-seq-item-group {
73 73 width: 91.6%;
74 74 float: left;
75 75 }
76 76
77 77 .form-control {
78 78 input {
79 79 height: 40px;
80 80 }
81 81 input[type=checkbox], input[type=radio] {
82 82 height: auto;
83 83 }
84 84 select {
85 85 height: 40px;
86 86 }
87 87 }
88 88
89 89 .form-control.select2-container {
90 90 height: 40px;
91 91 }
92 92
93 .deform-two-field-sequence .deform-seq-container .deform-seq-item label {
94 display: none;
95 }
96 .deform-two-field-sequence .deform-seq-container .deform-seq-item:first-child label {
97 display: block;
98 }
99 .deform-two-field-sequence .deform-seq-container .deform-seq-item .panel-heading {
100 display: none;
101 }
102 .deform-two-field-sequence .deform-seq-container .deform-seq-item.form-group {
103 margin: 0;
104 }
105 .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group .form-group {
106 width: 45%; padding: 0 2px; float: left; clear: none;
107 }
108 .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group > .panel {
109 padding: 0;
110 margin: 5px 0;
111 border: none;
112 }
113 .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group > .panel > .panel-body {
114 padding: 0;
93 .deform-full-field-sequence.control-inputs {
94 width: 100%;
115 95 }
116 96
97 .deform-table-sequence {
98 .deform-seq-container {
99 .deform-seq-item {
100 margin: 0;
101 label {
102 display: none;
103 }
104 .panel-heading {
105 display: none;
106 }
107 .deform-seq-item-group > .panel {
108 padding: 0;
109 margin: 5px 0;
110 border: none;
111 &> .panel-body {
112 padding: 0;
113 }
114 }
115 &:first-child label {
116 display: block;
117 }
118 }
119 }
120 }
121 .deform-table-2-sequence {
122 .deform-seq-container {
123 .deform-seq-item {
124 .form-group {
125 width: 45% !important; padding: 0 2px; float: left; clear: none;
126 }
127 }
128 }
129 }
130 .deform-table-3-sequence {
131 .deform-seq-container {
132 .deform-seq-item {
133 .form-group {
134 width: 30% !important; padding: 0 2px; float: left; clear: none;
135 }
136 }
137 }
138 }
117 139 }
@@ -1,51 +1,53 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('home', '/', []);
16 16 pyroutes.register('user_autocomplete_data', '/_users', []);
17 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
17 18 pyroutes.register('new_repo', '/_admin/create_repository', []);
18 19 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
19 20 pyroutes.register('gists', '/_admin/gists', []);
20 21 pyroutes.register('new_gist', '/_admin/gists/new', []);
21 22 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
22 23 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
23 24 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
24 25 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
26 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/default-reviewers', ['repo_name']);
25 27 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
26 28 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
27 29 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
28 30 pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']);
29 31 pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']);
30 32 pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
31 33 pyroutes.register('changeset_info', '/%(repo_name)s/changeset_info/%(revision)s', ['repo_name', 'revision']);
32 34 pyroutes.register('compare_url', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
33 35 pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
34 36 pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
35 37 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
36 38 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
37 39 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
38 40 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
39 41 pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
40 42 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
41 43 pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']);
42 44 pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
43 45 pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
44 46 pyroutes.register('files_history_home', '/%(repo_name)s/history/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
45 47 pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
46 48 pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
47 49 pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
48 50 pyroutes.register('files_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
49 51 pyroutes.register('summary_home_slash', '/%(repo_name)s/', ['repo_name']);
50 52 pyroutes.register('summary_home', '/%(repo_name)s', ['repo_name']);
51 53 }
@@ -1,205 +1,214 b''
1 1 // # Copyright (C) 2010-2016 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 * Pull request reviewers
21 21 */
22 22 var removeReviewMember = function(reviewer_id, mark_delete){
23 23 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
24 24
25 25 if(typeof(mark_delete) === undefined){
26 26 mark_delete = false;
27 27 }
28 28
29 29 if(mark_delete === true){
30 30 if (reviewer){
31 31 // mark as to-remove
32 32 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
33 33 obj.addClass('to-delete');
34 34 // now delete the input
35 35 $('#reviewer_{0}_input'.format(reviewer_id)).remove();
36 36 }
37 37 }
38 38 else{
39 39 $('#reviewer_{0}'.format(reviewer_id)).remove();
40 40 }
41 41 };
42 42
43 var addReviewMember = function(id,fname,lname,nname,gravatar_link){
43 var addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons) {
44 44 var members = $('#review_members').get(0);
45 var reasons_html = '';
46 if (reasons) {
47 for (var i = 0; i < reasons.length; i++) {
48 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(
49 reasons[i]
50 );
51 }
52 }
45 53 var tmpl = '<li id="reviewer_{2}">'+
46 54 '<div class="reviewer_status">'+
47 55 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
48 56 '</div>'+
49 57 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
50 58 '<span class="reviewer_name user">{1}</span>'+
59 reasons_html +
51 60 '<input type="hidden" value="{2}" name="review_members" />'+
52 61 '<div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">' +
53 62 '<i class="icon-remove-sign"></i>'+
54 63 '</div>'+
55 64 '</div>'+
56 65 '</li>' ;
57 66 var displayname = "{0} ({1} {2})".format(
58 67 nname, escapeHtml(fname), escapeHtml(lname));
59 68 var element = tmpl.format(gravatar_link,displayname,id);
60 69 // check if we don't have this ID already in
61 70 var ids = [];
62 71 var _els = $('#review_members li').toArray();
63 72 for (el in _els){
64 73 ids.push(_els[el].id)
65 74 }
66 75 if(ids.indexOf('reviewer_'+id) == -1){
67 76 // only add if it's not there
68 77 members.innerHTML += element;
69 78 }
70 79
71 80 };
72 81
73 82 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
74 83 var url = pyroutes.url(
75 84 'pullrequest_update',
76 85 {"repo_name": repo_name, "pull_request_id": pull_request_id});
77 86 postData.csrf_token = CSRF_TOKEN;
78 87 var success = function(o) {
79 88 window.location.reload();
80 89 };
81 90 ajaxPOST(url, postData, success);
82 91 };
83 92
84 93 var updateReviewers = function(reviewers_ids, repo_name, pull_request_id){
85 94 if (reviewers_ids === undefined){
86 95 var reviewers_ids = [];
87 96 var ids = $('#review_members input').toArray();
88 97 for(var i=0; i<ids.length;i++){
89 98 var id = ids[i].value
90 99 reviewers_ids.push(id);
91 100 }
92 101 }
93 102 var postData = {
94 103 '_method':'put',
95 104 'reviewers_ids': reviewers_ids};
96 105 _updatePullRequest(repo_name, pull_request_id, postData);
97 106 };
98 107
99 108 /**
100 109 * PULL REQUEST reject & close
101 110 */
102 111 var closePullRequest = function(repo_name, pull_request_id) {
103 112 var postData = {
104 113 '_method': 'put',
105 114 'close_pull_request': true};
106 115 _updatePullRequest(repo_name, pull_request_id, postData);
107 116 };
108 117
109 118 /**
110 119 * PULL REQUEST update commits
111 120 */
112 121 var updateCommits = function(repo_name, pull_request_id) {
113 122 var postData = {
114 123 '_method': 'put',
115 124 'update_commits': true};
116 125 _updatePullRequest(repo_name, pull_request_id, postData);
117 126 };
118 127
119 128
120 129 /**
121 130 * PULL REQUEST edit info
122 131 */
123 132 var editPullRequest = function(repo_name, pull_request_id, title, description) {
124 133 var url = pyroutes.url(
125 134 'pullrequest_update',
126 135 {"repo_name": repo_name, "pull_request_id": pull_request_id});
127 136
128 137 var postData = {
129 138 '_method': 'put',
130 139 'title': title,
131 140 'description': description,
132 141 'edit_pull_request': true,
133 142 'csrf_token': CSRF_TOKEN
134 143 };
135 144 var success = function(o) {
136 145 window.location.reload();
137 146 };
138 147 ajaxPOST(url, postData, success);
139 148 };
140 149
141 150 var initPullRequestsCodeMirror = function (textAreaId) {
142 151 var ta = $(textAreaId).get(0);
143 152 var initialHeight = '100px';
144 153
145 154 // default options
146 155 var codeMirrorOptions = {
147 156 mode: "text",
148 157 lineNumbers: false,
149 158 indentUnit: 4,
150 159 theme: 'rc-input'
151 160 };
152 161
153 162 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
154 163 // marker for manually set description
155 164 codeMirrorInstance._userDefinedDesc = false;
156 165 codeMirrorInstance.setSize(null, initialHeight);
157 166 codeMirrorInstance.on("change", function(instance, changeObj) {
158 167 var height = initialHeight;
159 168 var lines = instance.lineCount();
160 169 if (lines > 6 && lines < 20) {
161 170 height = "auto"
162 171 }
163 172 else if (lines >= 20) {
164 173 height = 20 * 15;
165 174 }
166 175 instance.setSize(null, height);
167 176
168 177 // detect if the change was trigger by auto desc, or user input
169 178 changeOrigin = changeObj.origin;
170 179
171 180 if (changeOrigin === "setValue") {
172 181 cmLog.debug('Change triggered by setValue');
173 182 }
174 183 else {
175 184 cmLog.debug('user triggered change !');
176 185 // set special marker to indicate user has created an input.
177 186 instance._userDefinedDesc = true;
178 187 }
179 188
180 189 });
181 190
182 191 return codeMirrorInstance
183 192 };
184 193
185 194 /**
186 195 * Reviewer autocomplete
187 196 */
188 197 var ReviewerAutoComplete = function(input_id) {
189 198 $('#'+input_id).autocomplete({
190 199 serviceUrl: pyroutes.url('user_autocomplete_data'),
191 200 minChars:2,
192 201 maxHeight:400,
193 202 deferRequestBy: 300, //miliseconds
194 203 showNoSuggestionNotice: true,
195 204 tabDisabled: true,
196 205 autoSelectFirst: true,
197 206 formatResult: autocompleteFormatResult,
198 207 lookupFilter: autocompleteFilterResult,
199 208 onSelect: function(suggestion, data){
200 209 addReviewMember(data.id, data.first_name, data.last_name,
201 210 data.username, data.icon_link);
202 211 $('#'+input_id).val('');
203 212 }
204 213 });
205 214 };
@@ -1,84 +1,100 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ##
3 3 ## See also repo_settings.html
4 4 ##
5 5 <%inherit file="/base/base.html"/>
6 6
7 7 <%def name="title()">
8 8 ${_('%s repository settings') % c.repo_info.repo_name}
9 9 %if c.rhodecode_name:
10 10 &middot; ${h.branding(c.rhodecode_name)}
11 11 %endif
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()">
15 15 ${_('Settings')}
16 16 </%def>
17 17
18 18 <%def name="menu_bar_nav()">
19 19 ${self.menu_items(active='repositories')}
20 20 </%def>
21 21
22 22 <%def name="menu_bar_subnav()">
23 23 ${self.repo_menu(active='options')}
24 24 </%def>
25 25
26 26 <%def name="main_content()">
27 27 <%include file="/admin/repos/repo_edit_${c.active}.html"/>
28 28 </%def>
29 29
30 30
31 31 <%def name="main()">
32 32 <div class="box">
33 33 <div class="title">
34 34 ${self.repo_page_title(c.rhodecode_db_repo)}
35 35 ${self.breadcrumbs()}
36 36 </div>
37 37
38 38 <div class="sidebar-col-wrapper scw-small">
39 39 ##main
40 40 <div class="sidebar">
41 41 <ul class="nav nav-pills nav-stacked">
42 42 <li class="${'active' if c.active=='settings' else ''}">
43 43 <a href="${h.url('edit_repo', repo_name=c.repo_name)}">${_('Settings')}</a>
44 44 </li>
45 45 <li class="${'active' if c.active=='permissions' else ''}">
46 46 <a href="${h.url('edit_repo_perms', repo_name=c.repo_name)}">${_('Permissions')}</a>
47 47 </li>
48 48 <li class="${'active' if c.active=='advanced' else ''}">
49 49 <a href="${h.url('edit_repo_advanced', repo_name=c.repo_name)}">${_('Advanced')}</a>
50 50 </li>
51 51 <li class="${'active' if c.active=='vcs' else ''}">
52 52 <a href="${h.url('repo_vcs_settings', repo_name=c.repo_name)}">${_('VCS')}</a>
53 53 </li>
54 54 <li class="${'active' if c.active=='fields' else ''}">
55 55 <a href="${h.url('edit_repo_fields', repo_name=c.repo_name)}">${_('Extra Fields')}</a>
56 56 </li>
57 57 <li class="${'active' if c.active=='issuetracker' else ''}">
58 58 <a href="${h.url('repo_settings_issuetracker', repo_name=c.repo_name)}">${_('Issue Tracker')}</a>
59 59 </li>
60 60 <li class="${'active' if c.active=='caches' else ''}">
61 61 <a href="${h.url('edit_repo_caches', repo_name=c.repo_name)}">${_('Caches')}</a>
62 62 </li>
63 63 %if c.repo_info.repo_type != 'svn':
64 64 <li class="${'active' if c.active=='remote' else ''}">
65 65 <a href="${h.url('edit_repo_remote', repo_name=c.repo_name)}">${_('Remote')}</a>
66 66 </li>
67 67 %endif
68 68 <li class="${'active' if c.active=='statistics' else ''}">
69 69 <a href="${h.url('edit_repo_statistics', repo_name=c.repo_name)}">${_('Statistics')}</a>
70 70 </li>
71 71 <li class="${'active' if c.active=='integrations' else ''}">
72 72 <a href="${h.route_path('repo_integrations_home', repo_name=c.repo_name)}">${_('Integrations')}</a>
73 73 </li>
74 ## TODO: dan: replace repo navigation with navlist registry like with
75 ## admin menu. First must find way to allow runtime configuration
76 ## it to account for the c.repo_info.repo_type != 'svn' call above
77 <%
78 reviewer_settings = False
79 try:
80 import rc_reviewers
81 reviewer_settings = True
82 except ImportError:
83 pass
84 %>
85 %if reviewer_settings:
86 <li class="${'active' if c.active=='reviewers' else ''}">
87 <a href="${h.route_path('repo_reviewers_home', repo_name=c.repo_name)}">${_('Reviewers')}</a>
88 </li>
89 %endif
74 90 </ul>
75 91 </div>
76 92
77 93 <div class="main-content-full-width">
78 94 ${self.main_content()}
79 95 </div>
80 96
81 97 </div>
82 98 </div>
83 99
84 100 </%def> No newline at end of file
@@ -1,541 +1,567 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('New pull request')}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${_('New pull request')}
9 9 </%def>
10 10
11 11 <%def name="menu_bar_nav()">
12 12 ${self.menu_items(active='repositories')}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_subnav()">
16 16 ${self.repo_menu(active='showpullrequest')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <div class="box">
21 21 <div class="title">
22 22 ${self.repo_page_title(c.rhodecode_db_repo)}
23 23 ${self.breadcrumbs()}
24 24 </div>
25 25
26 26 ${h.secure_form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
27 27 <div class="box pr-summary">
28 28
29 29 <div class="summary-details block-left">
30 30
31 31 <div class="form">
32 32 <!-- fields -->
33 33
34 34 <div class="fields" >
35 35
36 36 <div class="field">
37 37 <div class="label">
38 38 <label for="pullrequest_title">${_('Title')}:</label>
39 39 </div>
40 40 <div class="input">
41 41 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
42 42 </div>
43 43 </div>
44 44
45 45 <div class="field">
46 46 <div class="label label-textarea">
47 47 <label for="pullrequest_desc">${_('Description')}:</label>
48 48 </div>
49 49 <div class="textarea text-area editor">
50 50 ${h.textarea('pullrequest_desc',size=30, )}
51 51 <span class="help-block">
52 52 ${_('Write a short description on this pull request')}
53 53 </span>
54 54 </div>
55 55 </div>
56 56
57 57 <div class="field">
58 58 <div class="label label-textarea">
59 59 <label for="pullrequest_desc">${_('Commit flow')}:</label>
60 60 </div>
61 61
62 62 ## TODO: johbo: Abusing the "content" class here to get the
63 63 ## desired effect. Should be replaced by a proper solution.
64 64
65 65 ##ORG
66 66 <div class="content">
67 67 <strong>${_('Origin repository')}:</strong>
68 68 ${c.rhodecode_db_repo.description}
69 69 </div>
70 70 <div class="content">
71 71 ${h.hidden('source_repo')}
72 72 ${h.hidden('source_ref')}
73 73 </div>
74 74
75 75 ##OTHER, most Probably the PARENT OF THIS FORK
76 76 <div class="content">
77 77 ## filled with JS
78 78 <div id="target_repo_desc"></div>
79 79 </div>
80 80
81 81 <div class="content">
82 82 ${h.hidden('target_repo')}
83 83 ${h.hidden('target_ref')}
84 84 <span id="target_ref_loading" style="display: none">
85 85 ${_('Loading refs...')}
86 86 </span>
87 87 </div>
88 88 </div>
89 89
90 90 <div class="field">
91 91 <div class="label label-textarea">
92 92 <label for="pullrequest_submit"></label>
93 93 </div>
94 94 <div class="input">
95 95 <div class="pr-submit-button">
96 96 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
97 97 </div>
98 98 <div id="pr_open_message"></div>
99 99 </div>
100 100 </div>
101 101
102 102 <div class="pr-spacing-container"></div>
103 103 </div>
104 104 </div>
105 105 </div>
106 106 <div>
107 107 <div class="reviewers-title block-right">
108 108 <div class="pr-details-title">
109 109 ${_('Pull request reviewers')}
110 110 </div>
111 111 </div>
112 112 <div id="reviewers" class="block-right pr-details-content reviewers">
113 113 ## members goes here, filled via JS based on initial selection !
114 114 <ul id="review_members" class="group_members"></ul>
115 115 <div id="add_reviewer_input" class='ac'>
116 116 <div class="reviewer_ac">
117 117 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
118 118 <div id="reviewers_container"></div>
119 119 </div>
120 120 </div>
121 121 </div>
122 122 </div>
123 123 </div>
124 124 <div class="box">
125 125 <div>
126 126 ## overview pulled by ajax
127 127 <div id="pull_request_overview"></div>
128 128 </div>
129 129 </div>
130 130 ${h.end_form()}
131 131 </div>
132 132
133 133 <script type="text/javascript">
134 134 $(function(){
135 135 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
136 136 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
137 137 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
138 138 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
139 139 var targetRepoName = '${c.repo_name}';
140 140
141 141 var $pullRequestForm = $('#pull_request_form');
142 142 var $sourceRepo = $('#source_repo', $pullRequestForm);
143 143 var $targetRepo = $('#target_repo', $pullRequestForm);
144 144 var $sourceRef = $('#source_ref', $pullRequestForm);
145 145 var $targetRef = $('#target_ref', $pullRequestForm);
146 146
147 147 var calculateContainerWidth = function() {
148 148 var maxWidth = 0;
149 149 var repoSelect2Containers = ['#source_repo', '#target_repo'];
150 150 $.each(repoSelect2Containers, function(idx, value) {
151 151 $(value).select2('container').width('auto');
152 152 var curWidth = $(value).select2('container').width();
153 153 if (maxWidth <= curWidth) {
154 154 maxWidth = curWidth;
155 155 }
156 156 $.each(repoSelect2Containers, function(idx, value) {
157 157 $(value).select2('container').width(maxWidth + 10);
158 158 });
159 159 });
160 160 };
161 161
162 162 var initRefSelection = function(selectedRef) {
163 163 return function(element, callback) {
164 164 // translate our select2 id into a text, it's a mapping to show
165 165 // simple label when selecting by internal ID.
166 166 var id, refData;
167 167 if (selectedRef === undefined) {
168 168 id = element.val();
169 169 refData = element.val().split(':');
170 170 } else {
171 171 id = selectedRef;
172 172 refData = selectedRef.split(':');
173 173 }
174 174
175 175 var text = refData[1];
176 176 if (refData[0] === 'rev') {
177 177 text = text.substring(0, 12);
178 178 }
179 179
180 180 var data = {id: id, text: text};
181 181
182 182 callback(data);
183 183 };
184 184 };
185 185
186 186 var formatRefSelection = function(item) {
187 187 var prefix = '';
188 188 var refData = item.id.split(':');
189 189 if (refData[0] === 'branch') {
190 190 prefix = '<i class="icon-branch"></i>';
191 191 }
192 192 else if (refData[0] === 'book') {
193 193 prefix = '<i class="icon-bookmark"></i>';
194 194 }
195 195 else if (refData[0] === 'tag') {
196 196 prefix = '<i class="icon-tag"></i>';
197 197 }
198 198
199 199 var originalOption = item.element;
200 200 return prefix + item.text;
201 201 };
202 202
203 203 // custom code mirror
204 204 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
205 205
206 206 var queryTargetRepo = function(self, query) {
207 207 // cache ALL results if query is empty
208 208 var cacheKey = query.term || '__';
209 209 var cachedData = self.cachedDataSource[cacheKey];
210 210
211 211 if (cachedData) {
212 212 query.callback({results: cachedData.results});
213 213 } else {
214 214 $.ajax({
215 215 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': targetRepoName}),
216 216 data: {query: query.term},
217 217 dataType: 'json',
218 218 type: 'GET',
219 219 success: function(data) {
220 220 self.cachedDataSource[cacheKey] = data;
221 221 query.callback({results: data.results});
222 222 },
223 223 error: function(data, textStatus, errorThrown) {
224 224 alert(
225 225 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
226 226 }
227 227 });
228 228 }
229 229 };
230 230
231 231 var queryTargetRefs = function(initialData, query) {
232 232 var data = {results: []};
233 233 // filter initialData
234 234 $.each(initialData, function() {
235 235 var section = this.text;
236 236 var children = [];
237 237 $.each(this.children, function() {
238 238 if (query.term.length === 0 ||
239 239 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
240 240 children.push({'id': this.id, 'text': this.text})
241 241 }
242 242 });
243 243 data.results.push({'text': section, 'children': children})
244 244 });
245 245 query.callback({results: data.results});
246 246 };
247 247
248 248 var prButtonLock = function(lockEnabled, msg) {
249 249 if (lockEnabled) {
250 250 $('#save').attr('disabled', 'disabled');
251 251 }
252 252 else {
253 253 $('#save').removeAttr('disabled');
254 254 }
255 255
256 256 $('#pr_open_message').html(msg);
257 257
258 258 };
259 259
260 260 var loadRepoRefDiffPreview = function() {
261 261 var sourceRepo = $sourceRepo.eq(0).val();
262 262 var sourceRef = $sourceRef.eq(0).val().split(':');
263 263
264 264 var targetRepo = $targetRepo.eq(0).val();
265 265 var targetRef = $targetRef.eq(0).val().split(':');
266 266
267 267 var url_data = {
268 268 'repo_name': targetRepo,
269 269 'target_repo': sourceRepo,
270 270 'source_ref': targetRef[2],
271 271 'source_ref_type': 'rev',
272 272 'target_ref': sourceRef[2],
273 273 'target_ref_type': 'rev',
274 274 'merge': true,
275 275 '_': Date.now() // bypass browser caching
276 276 }; // gather the source/target ref and repo here
277 277
278 278 if (sourceRef.length !== 3 || targetRef.length !== 3) {
279 279 prButtonLock(true, "${_('Please select origin and destination')}");
280 280 return;
281 281 }
282 282 var url = pyroutes.url('compare_url', url_data);
283 283
284 284 // lock PR button, so we cannot send PR before it's calculated
285 285 prButtonLock(true, "${_('Loading compare ...')}");
286 286
287 287 if (loadRepoRefDiffPreview._currentRequest) {
288 288 loadRepoRefDiffPreview._currentRequest.abort();
289 289 }
290 290
291 291 loadRepoRefDiffPreview._currentRequest = $.get(url)
292 292 .error(function(data, textStatus, errorThrown) {
293 293 alert(
294 294 "Error while processing request.\nError code {0} ({1}).".format(
295 295 data.status, data.statusText));
296 296 })
297 297 .done(function(data) {
298 298 loadRepoRefDiffPreview._currentRequest = null;
299 299 $('#pull_request_overview').html(data);
300 300 var commitElements = $(data).find('tr[commit_id]');
301 301
302 302 var prTitleAndDesc = getTitleAndDescription(sourceRef[1],
303 303 commitElements, 5);
304 304
305 305 var title = prTitleAndDesc[0];
306 306 var proposedDescription = prTitleAndDesc[1];
307 307
308 308 var useGeneratedTitle = (
309 309 $('#pullrequest_title').hasClass('autogenerated-title') ||
310 310 $('#pullrequest_title').val() === "");
311 311
312 312 if (title && useGeneratedTitle) {
313 313 // use generated title if we haven't specified our own
314 314 $('#pullrequest_title').val(title);
315 315 $('#pullrequest_title').addClass('autogenerated-title');
316 316
317 317 }
318 318
319 319 var useGeneratedDescription = (
320 320 !codeMirrorInstance._userDefinedDesc ||
321 321 codeMirrorInstance.getValue() === "");
322 322
323 323 if (proposedDescription && useGeneratedDescription) {
324 324 // set proposed content, if we haven't defined our own,
325 325 // or we don't have description written
326 326 codeMirrorInstance._userDefinedDesc = false; // reset state
327 327 codeMirrorInstance.setValue(proposedDescription);
328 328 }
329 329
330 330 var msg = '';
331 331 if (commitElements.length === 1) {
332 332 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
333 333 } else {
334 334 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
335 335 }
336 336
337 337 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
338 338
339 339 if (commitElements.length) {
340 340 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
341 341 prButtonLock(false, msg.replace('__COMMITS__', commitsLink));
342 342 }
343 343 else {
344 344 prButtonLock(true, "${_('There are no commits to merge.')}");
345 345 }
346 346
347 347
348 348 });
349 349 };
350 350
351 351 /**
352 352 Generate Title and Description for a PullRequest.
353 353 In case of 1 commits, the title and description is that one commit
354 354 in case of multiple commits, we iterate on them with max N number of commits,
355 355 and build description in a form
356 356 - commitN
357 357 - commitN+1
358 358 ...
359 359
360 360 Title is then constructed from branch names, or other references,
361 361 replacing '-' and '_' into spaces
362 362
363 363 * @param sourceRef
364 364 * @param elements
365 365 * @param limit
366 366 * @returns {*[]}
367 367 */
368 368 var getTitleAndDescription = function(sourceRef, elements, limit) {
369 369 var title = '';
370 370 var desc = '';
371 371
372 372 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
373 373 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
374 374 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
375 375 });
376 376 // only 1 commit, use commit message as title
377 377 if (elements.length == 1) {
378 378 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
379 379 }
380 380 else {
381 381 // use reference name
382 382 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
383 383 }
384 384
385 385 return [title, desc]
386 386 };
387 387
388 388 var Select2Box = function(element, overrides) {
389 389 var globalDefaults = {
390 390 dropdownAutoWidth: true,
391 391 containerCssClass: "drop-menu",
392 392 dropdownCssClass: "drop-menu-dropdown",
393 393 };
394 394
395 395 var initSelect2 = function(defaultOptions) {
396 396 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
397 397 element.select2(options);
398 398 };
399 399
400 400 return {
401 401 initRef: function() {
402 402 var defaultOptions = {
403 403 minimumResultsForSearch: 5,
404 404 formatSelection: formatRefSelection
405 405 };
406 406
407 407 initSelect2(defaultOptions);
408 408 },
409 409
410 410 initRepo: function(defaultValue, readOnly) {
411 411 var defaultOptions = {
412 412 initSelection : function (element, callback) {
413 413 var data = {id: defaultValue, text: defaultValue};
414 414 callback(data);
415 415 }
416 416 };
417 417
418 418 initSelect2(defaultOptions);
419 419
420 420 element.select2('val', defaultSourceRepo);
421 421 if (readOnly === true) {
422 422 element.select2('readonly', true);
423 423 };
424 424 }
425 425 };
426 426 };
427 427
428 428 var initTargetRefs = function(refsData, selectedRef){
429 429 Select2Box($targetRef, {
430 430 query: function(query) {
431 431 queryTargetRefs(refsData, query);
432 432 },
433 433 initSelection : initRefSelection(selectedRef)
434 434 }).initRef();
435 435
436 436 if (!(selectedRef === undefined)) {
437 437 $targetRef.select2('val', selectedRef);
438 438 }
439 439 };
440 440
441 441 var targetRepoChanged = function(repoData) {
442 // reset && add the reviewer based on selected repo
443 $('#review_members').html('');
444 addReviewMember(
445 repoData.user.user_id, repoData.user.firstname,
446 repoData.user.lastname, repoData.user.username,
447 repoData.user.gravatar_link);
448
449 442 // generate new DESC of target repo displayed next to select
450 443 $('#target_repo_desc').html(
451 444 "<strong>${_('Destination repository')}</strong>: {0}".format(repoData['description'])
452 445 );
453 446
454 447 // generate dynamic select2 for refs.
455 448 initTargetRefs(repoData['refs']['select2_refs'],
456 449 repoData['refs']['selected_ref']);
457 450
458 451 };
459 452
460 453 var sourceRefSelect2 = Select2Box(
461 454 $sourceRef, {
462 455 placeholder: "${_('Select commit reference')}",
463 456 query: function(query) {
464 457 var initialData = defaultSourceRepoData['refs']['select2_refs'];
465 458 queryTargetRefs(initialData, query)
466 459 },
467 460 initSelection: initRefSelection()
468 461 }
469 462 );
470 463
471 464 var sourceRepoSelect2 = Select2Box($sourceRepo, {
472 465 query: function(query) {}
473 466 });
474 467
475 468 var targetRepoSelect2 = Select2Box($targetRepo, {
476 469 cachedDataSource: {},
477 470 query: $.debounce(250, function(query) {
478 471 queryTargetRepo(this, query);
479 472 }),
480 473 formatResult: formatResult
481 474 });
482 475
483 476 sourceRefSelect2.initRef();
484 477
485 478 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
486 479
487 480 targetRepoSelect2.initRepo(defaultTargetRepo, false);
488 481
489 482 $sourceRef.on('change', function(e){
490 483 loadRepoRefDiffPreview();
484 loadDefaultReviewers();
491 485 });
492 486
493 487 $targetRef.on('change', function(e){
494 488 loadRepoRefDiffPreview();
489 loadDefaultReviewers();
495 490 });
496 491
497 492 $targetRepo.on('change', function(e){
498 493 var repoName = $(this).val();
499 494 calculateContainerWidth();
500 495 $targetRef.select2('destroy');
501 496 $('#target_ref_loading').show();
502 497
503 498 $.ajax({
504 499 url: pyroutes.url('pullrequest_repo_refs',
505 500 {'repo_name': targetRepoName, 'target_repo_name':repoName}),
506 501 data: {},
507 502 dataType: 'json',
508 503 type: 'GET',
509 504 success: function(data) {
510 505 $('#target_ref_loading').hide();
511 506 targetRepoChanged(data);
512 507 loadRepoRefDiffPreview();
513 508 },
514 509 error: function(data, textStatus, errorThrown) {
515 510 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
516 511 }
517 512 })
518 513
519 514 });
520 515
516 var loadDefaultReviewers = function() {
517 if (loadDefaultReviewers._currentRequest) {
518 loadDefaultReviewers._currentRequest.abort();
519 }
520 var url = pyroutes.url('repo_default_reviewers_data', {'repo_name': targetRepoName});
521
522 var sourceRepo = $sourceRepo.eq(0).val();
523 var sourceRef = $sourceRef.eq(0).val().split(':');
524 var targetRepo = $targetRepo.eq(0).val();
525 var targetRef = $targetRef.eq(0).val().split(':');
526 url += '?source_repo=' + sourceRepo;
527 url += '&source_ref=' + sourceRef[2];
528 url += '&target_repo=' + targetRepo;
529 url += '&target_ref=' + targetRef[2];
530
531 loadDefaultReviewers._currentRequest = $.get(url)
532 .done(function(data) {
533 loadDefaultReviewers._currentRequest = null;
534
535 // reset && add the reviewer based on selected repo
536 $('#review_members').html('');
537 for (var i = 0; i < data.reviewers.length; i++) {
538 var reviewer = data.reviewers[i];
539 addReviewMember(
540 reviewer.user_id, reviewer.firstname,
541 reviewer.lastname, reviewer.username,
542 reviewer.gravatar_link, reviewer.reasons);
543 }
544 });
545 };
521 546 prButtonLock(true, "${_('Please select origin and destination')}");
522 547
523 548 // auto-load on init, the target refs select2
524 549 calculateContainerWidth();
525 550 targetRepoChanged(defaultTargetRepoData);
526 551
527 552 $('#pullrequest_title').on('keyup', function(e){
528 553 $(this).removeClass('autogenerated-title');
529 554 });
530 555
531 556 %if c.default_source_ref:
532 557 // in case we have a pre-selected value, use it now
533 558 $sourceRef.select2('val', '${c.default_source_ref}');
534 559 loadRepoRefDiffPreview();
560 loadDefaultReviewers();
535 561 %endif
536 562
537 563 ReviewerAutoComplete('user');
538 564 });
539 565 </script>
540 566
541 567 </%def>
General Comments 0
You need to be logged in to leave comments. Login now