##// END OF EJS Templates
repositories: preserve order of defined backends, and switched repo type selector to radios.
marcink -
r4321:711124f9 default
parent child Browse files
Show More
@@ -1,57 +1,60 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 from collections import OrderedDict
23
22 24 import sys
23 25 import platform
24 26
25 27 VERSION = tuple(open(os.path.join(
26 28 os.path.dirname(__file__), 'VERSION')).read().split('.'))
27 29
28 BACKENDS = {
29 'hg': 'Mercurial repository',
30 'git': 'Git repository',
31 'svn': 'Subversion repository',
32 }
30 BACKENDS = OrderedDict()
31
32 BACKENDS['hg'] = 'Mercurial repository'
33 BACKENDS['git'] = 'Git repository'
34 BACKENDS['svn'] = 'Subversion repository'
35
33 36
34 37 CELERY_ENABLED = False
35 38 CELERY_EAGER = False
36 39
37 40 # link to config for pyramid
38 41 CONFIG = {}
39 42
40 43 # Populated with the settings dictionary from application init in
41 44 # rhodecode.conf.environment.load_pyramid_environment
42 45 PYRAMID_SETTINGS = {}
43 46
44 47 # Linked module for extensions
45 48 EXTENSIONS = {}
46 49
47 50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
48 51 __dbversion__ = 105 # defines current db version for migrations
49 52 __platform__ = platform.system()
50 53 __license__ = 'AGPLv3, and Commercial License'
51 54 __author__ = 'RhodeCode GmbH'
52 55 __url__ = 'https://code.rhodecode.com'
53 56
54 57 is_windows = __platform__ in ['Windows']
55 58 is_unix = not is_windows
56 59 is_test = False
57 60 disable_error_handler = False
@@ -1,81 +1,86 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import logging
23 23 import rhodecode
24 24
25 25 from rhodecode.config import utils
26 26
27 27 from rhodecode.lib.utils import load_rcextensions
28 28 from rhodecode.lib.utils2 import str2bool
29 29 from rhodecode.lib.vcs import connect_vcs
30 30
31 31 log = logging.getLogger(__name__)
32 32
33 33
34 34 def load_pyramid_environment(global_config, settings):
35 35 # Some parts of the code expect a merge of global and app settings.
36 36 settings_merged = global_config.copy()
37 37 settings_merged.update(settings)
38 38
39 39 # TODO(marcink): probably not required anymore
40 40 # configure channelstream,
41 41 settings_merged['channelstream_config'] = {
42 42 'enabled': str2bool(settings_merged.get('channelstream.enabled', False)),
43 43 'server': settings_merged.get('channelstream.server'),
44 44 'secret': settings_merged.get('channelstream.secret')
45 45 }
46 46
47 47 # If this is a test run we prepare the test environment like
48 48 # creating a test database, test search index and test repositories.
49 49 # This has to be done before the database connection is initialized.
50 50 if settings['is_test']:
51 51 rhodecode.is_test = True
52 52 rhodecode.disable_error_handler = True
53 53 from rhodecode import authentication
54 54 authentication.plugin_default_auth_ttl = 0
55 55
56 56 utils.initialize_test_environment(settings_merged)
57 57
58 58 # Initialize the database connection.
59 59 utils.initialize_database(settings_merged)
60 60
61 61 load_rcextensions(root_path=settings_merged['here'])
62 62
63 # Limit backends to `vcs.backends` from configuration
63 # Limit backends to `vcs.backends` from configuration, and preserve the order
64 64 for alias in rhodecode.BACKENDS.keys():
65 65 if alias not in settings['vcs.backends']:
66 66 del rhodecode.BACKENDS[alias]
67
68 def sorter(item):
69 return settings['vcs.backends'].index(item[0])
70 rhodecode.BACKENDS = rhodecode.OrderedDict(sorted(rhodecode.BACKENDS.items(), key=sorter))
71
67 72 log.info('Enabled VCS backends: %s', rhodecode.BACKENDS.keys())
68 73
69 74 # initialize vcs client and optionally run the server if enabled
70 75 vcs_server_uri = settings['vcs.server']
71 76 vcs_server_enabled = settings['vcs.server.enable']
72 77
73 78 utils.configure_vcs(settings)
74 79
75 80 # Store the settings to make them available to other modules.
76 81
77 82 rhodecode.PYRAMID_SETTINGS = settings_merged
78 83 rhodecode.CONFIG = settings_merged
79 84
80 85 if vcs_server_enabled:
81 86 connect_vcs(vcs_server_uri, utils.get_vcs_server_protocol(settings))
@@ -1,617 +1,617 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2020 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 The base Controller API
23 23 Provides the BaseController class for subclassing. And usage in different
24 24 controllers
25 25 """
26 26
27 27 import logging
28 28 import socket
29 29
30 30 import markupsafe
31 31 import ipaddress
32 32
33 33 from paste.auth.basic import AuthBasicAuthenticator
34 34 from paste.httpexceptions import HTTPUnauthorized, HTTPForbidden, get_exception
35 35 from paste.httpheaders import WWW_AUTHENTICATE, AUTHORIZATION
36 36
37 37 import rhodecode
38 38 from rhodecode.apps._base import TemplateArgs
39 39 from rhodecode.authentication.base import VCS_TYPE
40 40 from rhodecode.lib import auth, utils2
41 41 from rhodecode.lib import helpers as h
42 42 from rhodecode.lib.auth import AuthUser, CookieStoreWrapper
43 43 from rhodecode.lib.exceptions import UserCreationError
44 44 from rhodecode.lib.utils import (password_changed, get_enabled_hook_classes)
45 45 from rhodecode.lib.utils2 import (
46 46 str2bool, safe_unicode, AttributeDict, safe_int, sha1, aslist, safe_str)
47 47 from rhodecode.model.db import Repository, User, ChangesetComment, UserBookmark
48 48 from rhodecode.model.notification import NotificationModel
49 49 from rhodecode.model.settings import VcsSettingsModel, SettingsModel
50 50
51 51 log = logging.getLogger(__name__)
52 52
53 53
54 54 def _filter_proxy(ip):
55 55 """
56 56 Passed in IP addresses in HEADERS can be in a special format of multiple
57 57 ips. Those comma separated IPs are passed from various proxies in the
58 58 chain of request processing. The left-most being the original client.
59 59 We only care about the first IP which came from the org. client.
60 60
61 61 :param ip: ip string from headers
62 62 """
63 63 if ',' in ip:
64 64 _ips = ip.split(',')
65 65 _first_ip = _ips[0].strip()
66 66 log.debug('Got multiple IPs %s, using %s', ','.join(_ips), _first_ip)
67 67 return _first_ip
68 68 return ip
69 69
70 70
71 71 def _filter_port(ip):
72 72 """
73 73 Removes a port from ip, there are 4 main cases to handle here.
74 74 - ipv4 eg. 127.0.0.1
75 75 - ipv6 eg. ::1
76 76 - ipv4+port eg. 127.0.0.1:8080
77 77 - ipv6+port eg. [::1]:8080
78 78
79 79 :param ip:
80 80 """
81 81 def is_ipv6(ip_addr):
82 82 if hasattr(socket, 'inet_pton'):
83 83 try:
84 84 socket.inet_pton(socket.AF_INET6, ip_addr)
85 85 except socket.error:
86 86 return False
87 87 else:
88 88 # fallback to ipaddress
89 89 try:
90 90 ipaddress.IPv6Address(safe_unicode(ip_addr))
91 91 except Exception:
92 92 return False
93 93 return True
94 94
95 95 if ':' not in ip: # must be ipv4 pure ip
96 96 return ip
97 97
98 98 if '[' in ip and ']' in ip: # ipv6 with port
99 99 return ip.split(']')[0][1:].lower()
100 100
101 101 # must be ipv6 or ipv4 with port
102 102 if is_ipv6(ip):
103 103 return ip
104 104 else:
105 105 ip, _port = ip.split(':')[:2] # means ipv4+port
106 106 return ip
107 107
108 108
109 109 def get_ip_addr(environ):
110 110 proxy_key = 'HTTP_X_REAL_IP'
111 111 proxy_key2 = 'HTTP_X_FORWARDED_FOR'
112 112 def_key = 'REMOTE_ADDR'
113 113 _filters = lambda x: _filter_port(_filter_proxy(x))
114 114
115 115 ip = environ.get(proxy_key)
116 116 if ip:
117 117 return _filters(ip)
118 118
119 119 ip = environ.get(proxy_key2)
120 120 if ip:
121 121 return _filters(ip)
122 122
123 123 ip = environ.get(def_key, '0.0.0.0')
124 124 return _filters(ip)
125 125
126 126
127 127 def get_server_ip_addr(environ, log_errors=True):
128 128 hostname = environ.get('SERVER_NAME')
129 129 try:
130 130 return socket.gethostbyname(hostname)
131 131 except Exception as e:
132 132 if log_errors:
133 133 # in some cases this lookup is not possible, and we don't want to
134 134 # make it an exception in logs
135 135 log.exception('Could not retrieve server ip address: %s', e)
136 136 return hostname
137 137
138 138
139 139 def get_server_port(environ):
140 140 return environ.get('SERVER_PORT')
141 141
142 142
143 143 def get_access_path(environ):
144 144 path = environ.get('PATH_INFO')
145 145 org_req = environ.get('pylons.original_request')
146 146 if org_req:
147 147 path = org_req.environ.get('PATH_INFO')
148 148 return path
149 149
150 150
151 151 def get_user_agent(environ):
152 152 return environ.get('HTTP_USER_AGENT')
153 153
154 154
155 155 def vcs_operation_context(
156 156 environ, repo_name, username, action, scm, check_locking=True,
157 157 is_shadow_repo=False, check_branch_perms=False, detect_force_push=False):
158 158 """
159 159 Generate the context for a vcs operation, e.g. push or pull.
160 160
161 161 This context is passed over the layers so that hooks triggered by the
162 162 vcs operation know details like the user, the user's IP address etc.
163 163
164 164 :param check_locking: Allows to switch of the computation of the locking
165 165 data. This serves mainly the need of the simplevcs middleware to be
166 166 able to disable this for certain operations.
167 167
168 168 """
169 169 # Tri-state value: False: unlock, None: nothing, True: lock
170 170 make_lock = None
171 171 locked_by = [None, None, None]
172 172 is_anonymous = username == User.DEFAULT_USER
173 173 user = User.get_by_username(username)
174 174 if not is_anonymous and check_locking:
175 175 log.debug('Checking locking on repository "%s"', repo_name)
176 176 repo = Repository.get_by_repo_name(repo_name)
177 177 make_lock, __, locked_by = repo.get_locking_state(
178 178 action, user.user_id)
179 179 user_id = user.user_id
180 180 settings_model = VcsSettingsModel(repo=repo_name)
181 181 ui_settings = settings_model.get_ui_settings()
182 182
183 183 # NOTE(marcink): This should be also in sync with
184 184 # rhodecode/apps/ssh_support/lib/backends/base.py:update_environment scm_data
185 185 store = [x for x in ui_settings if x.key == '/']
186 186 repo_store = ''
187 187 if store:
188 188 repo_store = store[0].value
189 189
190 190 scm_data = {
191 191 'ip': get_ip_addr(environ),
192 192 'username': username,
193 193 'user_id': user_id,
194 194 'action': action,
195 195 'repository': repo_name,
196 196 'scm': scm,
197 197 'config': rhodecode.CONFIG['__file__'],
198 198 'repo_store': repo_store,
199 199 'make_lock': make_lock,
200 200 'locked_by': locked_by,
201 201 'server_url': utils2.get_server_url(environ),
202 202 'user_agent': get_user_agent(environ),
203 203 'hooks': get_enabled_hook_classes(ui_settings),
204 204 'is_shadow_repo': is_shadow_repo,
205 205 'detect_force_push': detect_force_push,
206 206 'check_branch_perms': check_branch_perms,
207 207 }
208 208 return scm_data
209 209
210 210
211 211 class BasicAuth(AuthBasicAuthenticator):
212 212
213 213 def __init__(self, realm, authfunc, registry, auth_http_code=None,
214 214 initial_call_detection=False, acl_repo_name=None, rc_realm=''):
215 215 self.realm = realm
216 216 self.rc_realm = rc_realm
217 217 self.initial_call = initial_call_detection
218 218 self.authfunc = authfunc
219 219 self.registry = registry
220 220 self.acl_repo_name = acl_repo_name
221 221 self._rc_auth_http_code = auth_http_code
222 222
223 223 def _get_response_from_code(self, http_code):
224 224 try:
225 225 return get_exception(safe_int(http_code))
226 226 except Exception:
227 227 log.exception('Failed to fetch response for code %s', http_code)
228 228 return HTTPForbidden
229 229
230 230 def get_rc_realm(self):
231 231 return safe_str(self.rc_realm)
232 232
233 233 def build_authentication(self):
234 234 head = WWW_AUTHENTICATE.tuples('Basic realm="%s"' % self.realm)
235 235 if self._rc_auth_http_code and not self.initial_call:
236 236 # return alternative HTTP code if alternative http return code
237 237 # is specified in RhodeCode config, but ONLY if it's not the
238 238 # FIRST call
239 239 custom_response_klass = self._get_response_from_code(
240 240 self._rc_auth_http_code)
241 241 return custom_response_klass(headers=head)
242 242 return HTTPUnauthorized(headers=head)
243 243
244 244 def authenticate(self, environ):
245 245 authorization = AUTHORIZATION(environ)
246 246 if not authorization:
247 247 return self.build_authentication()
248 248 (authmeth, auth) = authorization.split(' ', 1)
249 249 if 'basic' != authmeth.lower():
250 250 return self.build_authentication()
251 251 auth = auth.strip().decode('base64')
252 252 _parts = auth.split(':', 1)
253 253 if len(_parts) == 2:
254 254 username, password = _parts
255 255 auth_data = self.authfunc(
256 256 username, password, environ, VCS_TYPE,
257 257 registry=self.registry, acl_repo_name=self.acl_repo_name)
258 258 if auth_data:
259 259 return {'username': username, 'auth_data': auth_data}
260 260 if username and password:
261 261 # we mark that we actually executed authentication once, at
262 262 # that point we can use the alternative auth code
263 263 self.initial_call = False
264 264
265 265 return self.build_authentication()
266 266
267 267 __call__ = authenticate
268 268
269 269
270 270 def calculate_version_hash(config):
271 271 return sha1(
272 272 config.get('beaker.session.secret', '') +
273 273 rhodecode.__version__)[:8]
274 274
275 275
276 276 def get_current_lang(request):
277 277 # NOTE(marcink): remove after pyramid move
278 278 try:
279 279 return translation.get_lang()[0]
280 280 except:
281 281 pass
282 282
283 283 return getattr(request, '_LOCALE_', request.locale_name)
284 284
285 285
286 286 def attach_context_attributes(context, request, user_id=None, is_api=None):
287 287 """
288 288 Attach variables into template context called `c`.
289 289 """
290 290 config = request.registry.settings
291 291
292 292 rc_config = SettingsModel().get_all_settings(cache=True, from_request=False)
293 293 context.rc_config = rc_config
294 294 context.rhodecode_version = rhodecode.__version__
295 295 context.rhodecode_edition = config.get('rhodecode.edition')
296 296 # unique secret + version does not leak the version but keep consistency
297 297 context.rhodecode_version_hash = calculate_version_hash(config)
298 298
299 299 # Default language set for the incoming request
300 300 context.language = get_current_lang(request)
301 301
302 302 # Visual options
303 303 context.visual = AttributeDict({})
304 304
305 305 # DB stored Visual Items
306 306 context.visual.show_public_icon = str2bool(
307 307 rc_config.get('rhodecode_show_public_icon'))
308 308 context.visual.show_private_icon = str2bool(
309 309 rc_config.get('rhodecode_show_private_icon'))
310 310 context.visual.stylify_metatags = str2bool(
311 311 rc_config.get('rhodecode_stylify_metatags'))
312 312 context.visual.dashboard_items = safe_int(
313 313 rc_config.get('rhodecode_dashboard_items', 100))
314 314 context.visual.admin_grid_items = safe_int(
315 315 rc_config.get('rhodecode_admin_grid_items', 100))
316 316 context.visual.show_revision_number = str2bool(
317 317 rc_config.get('rhodecode_show_revision_number', True))
318 318 context.visual.show_sha_length = safe_int(
319 319 rc_config.get('rhodecode_show_sha_length', 100))
320 320 context.visual.repository_fields = str2bool(
321 321 rc_config.get('rhodecode_repository_fields'))
322 322 context.visual.show_version = str2bool(
323 323 rc_config.get('rhodecode_show_version'))
324 324 context.visual.use_gravatar = str2bool(
325 325 rc_config.get('rhodecode_use_gravatar'))
326 326 context.visual.gravatar_url = rc_config.get('rhodecode_gravatar_url')
327 327 context.visual.default_renderer = rc_config.get(
328 328 'rhodecode_markup_renderer', 'rst')
329 329 context.visual.comment_types = ChangesetComment.COMMENT_TYPES
330 330 context.visual.rhodecode_support_url = \
331 331 rc_config.get('rhodecode_support_url') or h.route_url('rhodecode_support')
332 332
333 333 context.visual.affected_files_cut_off = 60
334 334
335 335 context.pre_code = rc_config.get('rhodecode_pre_code')
336 336 context.post_code = rc_config.get('rhodecode_post_code')
337 337 context.rhodecode_name = rc_config.get('rhodecode_title')
338 338 context.default_encodings = aslist(config.get('default_encoding'), sep=',')
339 339 # if we have specified default_encoding in the request, it has more
340 340 # priority
341 341 if request.GET.get('default_encoding'):
342 342 context.default_encodings.insert(0, request.GET.get('default_encoding'))
343 343 context.clone_uri_tmpl = rc_config.get('rhodecode_clone_uri_tmpl')
344 344 context.clone_uri_ssh_tmpl = rc_config.get('rhodecode_clone_uri_ssh_tmpl')
345 345
346 346 # INI stored
347 347 context.labs_active = str2bool(
348 348 config.get('labs_settings_active', 'false'))
349 349 context.ssh_enabled = str2bool(
350 350 config.get('ssh.generate_authorized_keyfile', 'false'))
351 351 context.ssh_key_generator_enabled = str2bool(
352 352 config.get('ssh.enable_ui_key_generator', 'true'))
353 353
354 354 context.visual.allow_repo_location_change = str2bool(
355 355 config.get('allow_repo_location_change', True))
356 356 context.visual.allow_custom_hooks_settings = str2bool(
357 357 config.get('allow_custom_hooks_settings', True))
358 358 context.debug_style = str2bool(config.get('debug_style', False))
359 359
360 360 context.rhodecode_instanceid = config.get('instance_id')
361 361
362 362 context.visual.cut_off_limit_diff = safe_int(
363 363 config.get('cut_off_limit_diff'))
364 364 context.visual.cut_off_limit_file = safe_int(
365 365 config.get('cut_off_limit_file'))
366 366
367 367 context.license = AttributeDict({})
368 368 context.license.hide_license_info = str2bool(
369 369 config.get('license.hide_license_info', False))
370 370
371 371 # AppEnlight
372 372 context.appenlight_enabled = str2bool(config.get('appenlight', 'false'))
373 373 context.appenlight_api_public_key = config.get(
374 374 'appenlight.api_public_key', '')
375 375 context.appenlight_server_url = config.get('appenlight.server_url', '')
376 376
377 377 diffmode = {
378 378 "unified": "unified",
379 379 "sideside": "sideside"
380 380 }.get(request.GET.get('diffmode'))
381 381
382 382 if is_api is not None:
383 383 is_api = hasattr(request, 'rpc_user')
384 384 session_attrs = {
385 385 # defaults
386 386 "clone_url_format": "http",
387 387 "diffmode": "sideside"
388 388 }
389 389
390 390 if not is_api:
391 391 # don't access pyramid session for API calls
392 392 if diffmode and diffmode != request.session.get('rc_user_session_attr.diffmode'):
393 393 request.session['rc_user_session_attr.diffmode'] = diffmode
394 394
395 395 # session settings per user
396 396
397 397 for k, v in request.session.items():
398 398 pref = 'rc_user_session_attr.'
399 399 if k and k.startswith(pref):
400 400 k = k[len(pref):]
401 401 session_attrs[k] = v
402 402
403 403 context.user_session_attrs = session_attrs
404 404
405 405 # JS template context
406 406 context.template_context = {
407 407 'repo_name': None,
408 408 'repo_type': None,
409 409 'repo_landing_commit': None,
410 410 'rhodecode_user': {
411 411 'username': None,
412 412 'email': None,
413 413 'notification_status': False
414 414 },
415 415 'session_attrs': session_attrs,
416 416 'visual': {
417 417 'default_renderer': None
418 418 },
419 419 'commit_data': {
420 420 'commit_id': None
421 421 },
422 422 'pull_request_data': {'pull_request_id': None},
423 423 'timeago': {
424 424 'refresh_time': 120 * 1000,
425 425 'cutoff_limit': 1000 * 60 * 60 * 24 * 7
426 426 },
427 427 'pyramid_dispatch': {
428 428
429 429 },
430 430 'extra': {'plugins': {}}
431 431 }
432 432 # END CONFIG VARS
433 433 if is_api:
434 434 csrf_token = None
435 435 else:
436 436 csrf_token = auth.get_csrf_token(session=request.session)
437 437
438 438 context.csrf_token = csrf_token
439 439 context.backends = rhodecode.BACKENDS.keys()
440 context.backends.sort()
440
441 441 unread_count = 0
442 442 user_bookmark_list = []
443 443 if user_id:
444 444 unread_count = NotificationModel().get_unread_cnt_for_user(user_id)
445 445 user_bookmark_list = UserBookmark.get_bookmarks_for_user(user_id)
446 446 context.unread_notifications = unread_count
447 447 context.bookmark_items = user_bookmark_list
448 448
449 449 # web case
450 450 if hasattr(request, 'user'):
451 451 context.auth_user = request.user
452 452 context.rhodecode_user = request.user
453 453
454 454 # api case
455 455 if hasattr(request, 'rpc_user'):
456 456 context.auth_user = request.rpc_user
457 457 context.rhodecode_user = request.rpc_user
458 458
459 459 # attach the whole call context to the request
460 460 request.call_context = context
461 461
462 462
463 463 def get_auth_user(request):
464 464 environ = request.environ
465 465 session = request.session
466 466
467 467 ip_addr = get_ip_addr(environ)
468 468
469 469 # make sure that we update permissions each time we call controller
470 470 _auth_token = (request.GET.get('auth_token', '') or request.GET.get('api_key', ''))
471 471 if not _auth_token and request.matchdict:
472 472 url_auth_token = request.matchdict.get('_auth_token')
473 473 _auth_token = url_auth_token
474 474 if _auth_token:
475 475 log.debug('Using URL extracted auth token `...%s`', _auth_token[-4:])
476 476
477 477 if _auth_token:
478 478 # when using API_KEY we assume user exists, and
479 479 # doesn't need auth based on cookies.
480 480 auth_user = AuthUser(api_key=_auth_token, ip_addr=ip_addr)
481 481 authenticated = False
482 482 else:
483 483 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
484 484 try:
485 485 auth_user = AuthUser(user_id=cookie_store.get('user_id', None),
486 486 ip_addr=ip_addr)
487 487 except UserCreationError as e:
488 488 h.flash(e, 'error')
489 489 # container auth or other auth functions that create users
490 490 # on the fly can throw this exception signaling that there's
491 491 # issue with user creation, explanation should be provided
492 492 # in Exception itself. We then create a simple blank
493 493 # AuthUser
494 494 auth_user = AuthUser(ip_addr=ip_addr)
495 495
496 496 # in case someone changes a password for user it triggers session
497 497 # flush and forces a re-login
498 498 if password_changed(auth_user, session):
499 499 session.invalidate()
500 500 cookie_store = CookieStoreWrapper(session.get('rhodecode_user'))
501 501 auth_user = AuthUser(ip_addr=ip_addr)
502 502
503 503 authenticated = cookie_store.get('is_authenticated')
504 504
505 505 if not auth_user.is_authenticated and auth_user.is_user_object:
506 506 # user is not authenticated and not empty
507 507 auth_user.set_authenticated(authenticated)
508 508
509 509 return auth_user, _auth_token
510 510
511 511
512 512 def h_filter(s):
513 513 """
514 514 Custom filter for Mako templates. Mako by standard uses `markupsafe.escape`
515 515 we wrap this with additional functionality that converts None to empty
516 516 strings
517 517 """
518 518 if s is None:
519 519 return markupsafe.Markup()
520 520 return markupsafe.escape(s)
521 521
522 522
523 523 def add_events_routes(config):
524 524 """
525 525 Adds routing that can be used in events. Because some events are triggered
526 526 outside of pyramid context, we need to bootstrap request with some
527 527 routing registered
528 528 """
529 529
530 530 from rhodecode.apps._base import ADMIN_PREFIX
531 531
532 532 config.add_route(name='home', pattern='/')
533 533 config.add_route(name='main_page_repos_data', pattern='/_home_repos')
534 534 config.add_route(name='main_page_repo_groups_data', pattern='/_home_repo_groups')
535 535
536 536 config.add_route(name='login', pattern=ADMIN_PREFIX + '/login')
537 537 config.add_route(name='logout', pattern=ADMIN_PREFIX + '/logout')
538 538 config.add_route(name='repo_summary', pattern='/{repo_name}')
539 539 config.add_route(name='repo_summary_explicit', pattern='/{repo_name}/summary')
540 540 config.add_route(name='repo_group_home', pattern='/{repo_group_name}')
541 541
542 542 config.add_route(name='pullrequest_show',
543 543 pattern='/{repo_name}/pull-request/{pull_request_id}')
544 544 config.add_route(name='pull_requests_global',
545 545 pattern='/pull-request/{pull_request_id}')
546 546
547 547 config.add_route(name='repo_commit',
548 548 pattern='/{repo_name}/changeset/{commit_id}')
549 549 config.add_route(name='repo_files',
550 550 pattern='/{repo_name}/files/{commit_id}/{f_path}')
551 551
552 552 config.add_route(name='hovercard_user',
553 553 pattern='/_hovercard/user/{user_id}')
554 554
555 555 config.add_route(name='hovercard_user_group',
556 556 pattern='/_hovercard/user_group/{user_group_id}')
557 557
558 558 config.add_route(name='hovercard_pull_request',
559 559 pattern='/_hovercard/pull_request/{pull_request_id}')
560 560
561 561 config.add_route(name='hovercard_repo_commit',
562 562 pattern='/_hovercard/commit/{repo_name}/{commit_id}')
563 563
564 564
565 565 def bootstrap_config(request):
566 566 import pyramid.testing
567 567 registry = pyramid.testing.Registry('RcTestRegistry')
568 568
569 569 config = pyramid.testing.setUp(registry=registry, request=request)
570 570
571 571 # allow pyramid lookup in testing
572 572 config.include('pyramid_mako')
573 573 config.include('rhodecode.lib.rc_beaker')
574 574 config.include('rhodecode.lib.rc_cache')
575 575
576 576 add_events_routes(config)
577 577
578 578 return config
579 579
580 580
581 581 def bootstrap_request(**kwargs):
582 582 import pyramid.testing
583 583
584 584 class TestRequest(pyramid.testing.DummyRequest):
585 585 application_url = kwargs.pop('application_url', 'http://example.com')
586 586 host = kwargs.pop('host', 'example.com:80')
587 587 domain = kwargs.pop('domain', 'example.com')
588 588
589 589 def translate(self, msg):
590 590 return msg
591 591
592 592 def plularize(self, singular, plural, n):
593 593 return singular
594 594
595 595 def get_partial_renderer(self, tmpl_name):
596 596
597 597 from rhodecode.lib.partial_renderer import get_partial_renderer
598 598 return get_partial_renderer(request=self, tmpl_name=tmpl_name)
599 599
600 600 _call_context = TemplateArgs()
601 601 _call_context.visual = TemplateArgs()
602 602 _call_context.visual.show_sha_length = 12
603 603 _call_context.visual.show_revision_number = True
604 604
605 605 @property
606 606 def call_context(self):
607 607 return self._call_context
608 608
609 609 class TestDummySession(pyramid.testing.DummySession):
610 610 def save(*arg, **kw):
611 611 pass
612 612
613 613 request = TestRequest(**kwargs)
614 614 request.session = TestDummySession()
615 615
616 616 return request
617 617
@@ -1,404 +1,411 b''
1 1 // forms.less
2 2 // For use in RhodeCode applications;
3 3 // see style guide documentation for guidelines.
4 4
5 5 form.rcform {
6 6
7 7 // reset for ie
8 8 // using :not(#ie) prevents older browsers from applying these rules
9 9 input[type="radio"],
10 10 input[type="checkbox"] {
11 11 padding: 0;
12 12 border: none;
13 13 }
14 14 label { display: inline; border:none; padding:0; }
15 15 .label { display: none; }
16 16
17 17 max-width: 940px;
18 18 line-height: normal;
19 19 white-space: normal;
20 20 font-size: @basefontsize;
21 21 font-family: @text-light;
22 22 color: @form-textcolor;
23 23
24 24 fieldset,
25 25 .buttons {
26 26 clear: both;
27 27 position: relative;
28 28 display:block;
29 29 width: 100%;
30 30 min-height: 3em;
31 31 margin-bottom: @form-vertical-margin;
32 32 line-height: 1.2em;
33 33
34 34 &:after { //clearfix
35 35 content: "";
36 36 clear: both;
37 37 width: 100%;
38 38 height: 1em;
39 39 }
40 40
41 41 .label:not(#ie) {
42 42 display: inline;
43 43 margin: 0 1em 0 .5em;
44 44 line-height: 1em;
45 45 }
46 46 }
47 47
48 48 legend {
49 49 float: left;
50 50 display: block;
51 51 width: @legend-width;
52 52 margin: 0;
53 53 padding: 0 @padding 0 0;
54 54 }
55 55
56 56 .fields {
57 57 float: left;
58 58 display: block;
59 59 width: 100%;
60 60 max-width: 500px;
61 61 margin: 0 0 @padding -@legend-width;
62 62 padding: 0 0 0 @legend-width;
63 63
64 64 .btn {
65 65 display: inline-block;
66 66 margin: 0 1em @padding 0;
67 67 }
68 68 }
69 69
70 70 input,
71 71 textarea {
72 72 float: left;
73 73 .box-sizing(content-box);
74 74 padding: @input-padding;
75 75 border: @border-thickness-inputs solid @grey4;
76 76 }
77 77
78 78 input {
79 79 float: left;
80 80 margin: 0 @input-padding 0 0;
81 81 line-height: 1em;
82 82 }
83 83
84 84 input[type="text"],
85 85 input[type="password"],
86 86 textarea {
87 87 float: left;
88 88 min-width: 200px;
89 89 margin: 0 1em @padding 0;
90 90 color: @form-textcolor;
91 91 }
92 92
93 93 input[type="text"],
94 94 input[type="password"] {
95 95 height: 1em;
96 96 }
97 97
98 98 textarea {
99 99 width: 100%;
100 100 margin-top: -1em; //so it lines up with legend
101 101 overflow: auto;
102 102 }
103 103
104 104 label:not(#ie) {
105 105 cursor: pointer;
106 106 display: inline-block;
107 107 position: relative;
108 108 background: white;
109 109 border-radius: 4px;
110 110 box-shadow: none;
111 111
112 112 &:hover::after {
113 113 opacity: 0.5;
114 114 }
115 115 }
116 116
117 117 input[type="radio"]:not(#ie),
118 118 input[type="checkbox"]:not(#ie) {
119 119 // Hide the input, but have it still be clickable
120 120 opacity: 0;
121 121 float: left;
122 122 height: 0;
123 123 width: 0;
124 124 margin: 0;
125 125 padding: 0;
126 126 }
127 127 input[type='radio'] + label:not(#ie),
128 128 input[type='checkbox'] + label:not(#ie) {
129 129 margin: 0;
130 130 clear: none;
131 131 }
132 132
133 133 input[type='radio'] + label:not(#ie) {
134 134 .circle (@form-radio-width,white);
135 135 float: left;
136 136 display: inline-block;
137 137 height: @form-radio-width;
138 138 width: @form-radio-width;
139 139 margin: 2px 6px 2px 0;
140 140 border: 1px solid @grey4;
141 141 background-color: white;
142 142 box-shadow: none;
143 143 text-indent: -9999px;
144 144 transition: none;
145 145
146 146 & + .label {
147 147 float: left;
148 148 margin-top: 7px
149 149 }
150 150 }
151 151
152 152 input[type='radio']:checked + label:not(#ie) {
153 153 margin: 0 4px 0 -2px;
154 154 padding: 3px;
155 155 border-style: double;
156 156 border-color: white;
157 157 border-width: thick;
158 158 background-color: @rcblue;
159 159 box-shadow: none;
160 160 }
161 161
162 162 input[type='checkbox'] + label:not(#ie) {
163 163 float: left;
164 164 width: @form-check-width;
165 165 height: @form-check-width;
166 166 margin: 0 5px 1em 0;
167 167 border: 1px solid @grey3;
168 168 .border-radius(@border-radius);
169 169 background-color: white;
170 170 box-shadow: none;
171 171 text-indent: -9999px;
172 172 transition: none;
173 173
174 174 &:after {
175 175 content: '';
176 176 width: 9px;
177 177 height: 5px;
178 178 position: absolute;
179 179 top: 4px;
180 180 left: 4px;
181 181 border: 3px solid @grey3;
182 182 border-top: none;
183 183 border-right: none;
184 184 background: transparent;
185 185 opacity: 0;
186 186 transform: rotate(-45deg);
187 187 filter: progid:DXImageTransform.Microsoft.Matrix(sizingMethod='auto expand', M11=0.7071067811865476, M12=-0.7071067811865475, M21=0.7071067811865475, M22=0.7071067811865476); /* IE6,IE7 */
188 188
189 189 -ms-filter: "progid:DXImageTransform.Microsoft.Matrix(SizingMethod='auto expand', M11=0.7071067811865476, M12=-0.7071067811865475, M21=0.7071067811865475, M22=0.7071067811865476)"; /* IE8 */ }
190 190
191 191 & + .label {
192 192 float: left;
193 193 margin-top: 5px
194 194 }
195 195 }
196 196
197 197 input[type=checkbox]:not(#ie) {
198 198 visibility: hidden;
199 199 &:checked + label:after {
200 200 opacity: 1;
201 201 }
202 202 }
203 203
204 204 // center checkbox and label on a drop-down
205 205 .drop-menu + select + input[type='checkbox'] + label:not(#ie) {
206 206 margin-top:10px;
207 207
208 208 & + .label {
209 209 margin-top: 15px;
210 210 }
211 211 }
212 212
213 213 .formlist {
214 214 position: relative;
215 215 float: left;
216 216 margin: 0;
217 217 padding: 0;
218 218
219 219 li {
220 220 list-style-type: none;
221 221
222 222 &:after {
223 223 content: "";
224 224 float: left;
225 225 display: block;
226 226 height: @padding;
227 227 width: 100%;
228 228 }
229 229 }
230 230 }
231 231
232 232 .drop-menu {
233 233 float: left;
234 234
235 235 & + .last-item {
236 236 margin: 0;
237 237 }
238 238
239 239 margin: 0 @input-padding 0 0;
240 240 }
241 241
242 242 .help-block,
243 243 .error-message {
244 244 display: block;
245 245 clear: both;
246 246 margin: @textmargin 0;
247 247 }
248 248
249 249 .error-message {
250 250 margin-top: 5px;
251 251 }
252 252
253 253 input[type=submit] {
254 254 &:extend(.btn-primary);
255 255
256 256 &:hover {
257 257 &:extend(.btn-primary:hover);
258 258 }
259 259 }
260 260
261 261 input[type=reset] {
262 262 &:extend(.btn-default);
263 263
264 264 &:hover {
265 265 &:extend(.btn-default:hover);
266 266 }
267 267 }
268 268
269 269 select,
270 270 option:checked {
271 271 background-color: @rclightblue;
272 272 }
273 273
274 274 }
275 275
276 276 .rcform-element {
277 277
278 278 label { display: inline; border:none; padding:0; }
279 279 .label { display: none; }
280 280
281 281 label:not(#ie) {
282 282 cursor: pointer;
283 283 display: inline-block;
284 284 position: relative;
285 285 background: white;
286 286 border-radius: 4px;
287 287 box-shadow: none;
288 288
289 289 &:hover::after {
290 290 opacity: 0.5;
291 291 }
292 292 }
293 293
294 294 input[type="radio"],
295 295 input[type="checkbox"] {
296 296 padding: 0;
297 297 border: none;
298 298 }
299 299
300 300 input[type="radio"]:not(#ie),
301 301 input[type="checkbox"]:not(#ie) {
302 302 // Hide the input, but have it still be clickable
303 303 opacity: 0;
304 304 float: left;
305 305 height: 0;
306 306 width: 0;
307 307 margin: 0;
308 308 padding: 0;
309 309 }
310 310 input[type='radio'] + label:not(#ie),
311 311 input[type='checkbox'] + label:not(#ie) {
312 312 margin: 0;
313 313 clear: none;
314 314 }
315 315
316 316 input[type='radio'] + label:not(#ie) {
317 317 .circle (@form-radio-width,white);
318 318 float: left;
319 319 display: inline-block;
320 320 height: @form-radio-width;
321 321 width: @form-radio-width;
322 322 margin: 2px 2px 2px 0;
323 323 border: 1px solid @grey4;
324 324 background-color: white;
325 325 box-shadow: none;
326 326 text-indent: -9999px;
327 327 transition: none;
328 328
329 329 & + .label {
330 330 float: left;
331 331 margin-top: 7px
332 332 }
333 333 }
334 334
335 335 input[type='radio']:checked + label:not(#ie) {
336 336 margin: 0 0px 0 -2px;
337 337 padding: 3px;
338 338 border-style: double;
339 339 border-color: white;
340 340 border-width: thick;
341 341 background-color: @rcblue;
342 342 box-shadow: none;
343 343 }
344 344
345 345 fieldset {
346 346 .label:not(#ie) {
347 347 display: inline;
348 348 margin: 0 1em 0 .5em;
349 349 line-height: 1em;
350 350 }
351 351 }
352 352
353 353 }
354 354
355 355 .badged-field {
356 356 .user-badge {
357 357 line-height: 25px;
358 358 padding: .4em;
359 359 border-radius: @border-radius;
360 360 border-top: 1px solid @grey4;
361 361 border-left: 1px solid @grey4;
362 362 border-bottom: 1px solid @grey4;
363 363 font-size: 14px;
364 364 font-style: normal;
365 365 color: @text-light;
366 366 background: @grey7;
367 367 display: inline-block;
368 368 vertical-align: top;
369 369 cursor: default;
370 370 margin-right: -2px;
371 371 }
372 372 .badge-input-container {
373 373 display: flex;
374 374 position: relative;
375 375 }
376 376 .user-disabled {
377 377 text-decoration: line-through;
378 378 }
379 379 .badge-input-wrap {
380 380 display: inline-block;
381 381 }
382 382 }
383 383
384 384 // for situations where we wish to display the form value but not the form input
385 385 input.input-valuedisplay {
386 386 border: none;
387 387 }
388 388
389 389 // for forms which only display information
390 390 .infoform {
391 391 .fields {
392 392 .field {
393 393 label,
394 394 .label,
395 395 input,
396 396 .input {
397 397 margin-top: 0;
398 398 margin-bottom: 0;
399 399 padding-top: 0;
400 400 padding-bottom: 0;
401 401 }
402 402 }
403 403 }
404 404 }
405
406 .repo-type-radio input {
407 margin: 0 0 0 0
408 }
409 .repo-type-radio label {
410 padding-right: 15px;
411 }
@@ -1,158 +1,175 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 ${h.secure_form(h.route_path('repo_create'), request=request)}
4 4 <div class="form">
5 5 <!-- fields -->
6 6 <div class="fields">
7 7 <div class="field">
8 8 <div class="label">
9 9 <label for="repo_name">${_('Repository name')}:</label>
10 10 </div>
11 11 <div class="input">
12 12 ${h.text('repo_name', class_="medium")}
13 13 <div class="info-block">
14 14 <a id="remote_clone_toggle" href="#">${_('Import Existing Repository ?')}</a>
15 15 </div>
16 16 %if not c.rhodecode_user.is_admin:
17 17 ${h.hidden('user_created',True)}
18 18 %endif
19 19 </div>
20 20 </div>
21 21 <div id="remote_clone" class="field" style="display: none;">
22 22 <div class="label">
23 23 <label for="clone_uri">${_('Clone from')}:</label>
24 24 </div>
25 25 <div class="input">
26 26 ${h.text('clone_uri', class_="medium")}
27 27 <span class="help-block">
28 28 <pre>
29 29 - The repository must be accessible over http:// or https://
30 30 - For Git projects it's recommended appending .git to the end of clone url.
31 31 - Make sure to select proper repository type from the below selector before importing it.
32 32 - If your HTTP[S] repository is not publicly accessible,
33 33 add authentication information to the URL: https://username:password@server.company.com/repo-name.
34 34 - The Git LFS/Mercurial Largefiles objects will not be imported.
35 35 - For very large repositories, it's recommended to manually copy them into the
36 36 RhodeCode <a href="${h.route_path('admin_settings_vcs', _anchor='vcs-storage-options')}">storage location</a> and run <a href="${h.route_path('admin_settings_mapping')}">Remap and Rescan</a>.
37 37 </pre>
38 38 </span>
39 39 </div>
40 40 </div>
41 41 <div class="field">
42 42 <div class="label">
43 43 <label for="repo_group">${_('Repository group')}:</label>
44 44 </div>
45 45 <div class="select">
46 46 ${h.select('repo_group',request.GET.get('parent_group'),c.repo_groups,class_="medium")}
47 47 % if c.personal_repo_group:
48 48 <a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}">
49 49 ${_('Select my personal group (%(repo_group_name)s)') % {'repo_group_name': c.personal_repo_group.group_name}}
50 50 </a>
51 51 % endif
52 52 <span class="help-block">${_('Optionally select a group to put this repository into.')}</span>
53 53 </div>
54 54 </div>
55
55 56 <div class="field">
56 57 <div class="label">
57 58 <label for="repo_type">${_('Type')}:</label>
58 59 </div>
59 <div class="select">
60 ${h.select('repo_type','hg',c.backends)}
60 <div class="fields repo-type-radio">
61
62
63 % for backend in c.backends:
64 % if loop.index == 0:
65 <input id="repo_type_${backend}" name="repo_type" type="radio" value="${backend}" checked="checked"/>
66 % else:
67 <input id="repo_type_${backend}" name="repo_type" type="radio" value="${backend}" />
68 % endif
69
70 <label for="repo_type_${backend}">
71 <i class="icon-${backend}" style="font-size: 16px"></i>
72 ${backend.upper()}
73 </label>
74
75 % endfor
76
77
61 78 <span class="help-block">${_('Set the type of repository to create.')}</span>
62 79 </div>
63 80 </div>
64 81 <div class="field">
65 82 <div class="label">
66 83 <label for="repo_description">${_('Description')}:</label>
67 84 </div>
68 85 <div class="textarea editor">
69 86 ${h.textarea('repo_description',cols=23,rows=5,class_="medium")}
70 87 <% metatags_url = h.literal('''<a href="#metatagsShow" onclick="$('#meta-tags-desc').toggle();return false">meta-tags</a>''') %>
71 88 <span class="help-block">
72 89 % if c.visual.stylify_metatags:
73 90 ${_('Plain text format with {metatags} support.').format(metatags=metatags_url)|n}
74 91 % else:
75 92 ${_('Plain text format.')}
76 93 % endif
77 94 ${_('Add a README file for longer descriptions')}
78 95 </span>
79 96 <span id="meta-tags-desc" style="display: none">
80 97 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
81 98 ${dt.metatags_help()}
82 99 </span>
83 100 </div>
84 101 </div>
85 102 <div id="copy_perms" class="field">
86 103 <div class="label label-checkbox">
87 104 <label for="repo_copy_permissions">${_('Copy Parent Group Permissions')}:</label>
88 105 </div>
89 106 <div class="checkboxes">
90 107 ${h.checkbox('repo_copy_permissions', value="True", checked="checked")}
91 108 <span class="help-block">${_('Copy permissions from parent repository group.')}</span>
92 109 </div>
93 110 </div>
94 111 <div class="field">
95 112 <div class="label label-checkbox">
96 113 <label for="repo_private">${_('Private Repository')}:</label>
97 114 </div>
98 115 <div class="checkboxes">
99 116 ${h.checkbox('repo_private',value="True")}
100 117 <span class="help-block">${_('Private repositories are only visible to people explicitly added as collaborators.')}</span>
101 118 </div>
102 119 </div>
103 120 <div class="buttons">
104 121 ${h.submit('save',_('Create Repository'),class_="btn")}
105 122 </div>
106 123 </div>
107 124 </div>
108 125 <script>
109 126 $(document).ready(function(){
110 127 var setCopyPermsOption = function(group_val){
111 128 if(group_val != "-1"){
112 129 $('#copy_perms').show()
113 130 }
114 131 else{
115 132 $('#copy_perms').hide();
116 133 }
117 134 };
118 135
119 136 $('#remote_clone_toggle').on('click', function(e){
120 137 $('#remote_clone').show();
121 138 e.preventDefault();
122 139 });
123 140
124 141 if($('#remote_clone input').hasClass('error')){
125 142 $('#remote_clone').show();
126 143 }
127 144 if($('#remote_clone input').val()){
128 145 $('#remote_clone').show();
129 146 }
130 147
131 148 $("#repo_group").select2({
132 149 'containerCssClass': "drop-menu",
133 150 'dropdownCssClass': "drop-menu-dropdown",
134 151 'dropdownAutoWidth': true,
135 152 'width': "resolve"
136 153 });
137 154
138 155 setCopyPermsOption($('#repo_group').val());
139 156 $("#repo_group").on("change", function(e) {
140 157 setCopyPermsOption(e.val)
141 158 });
142 159
143 160 $("#repo_type").select2({
144 161 'containerCssClass': "drop-menu",
145 162 'dropdownCssClass': "drop-menu-dropdown",
146 163 'minimumResultsForSearch': -1,
147 164 });
148 165
149 166 $('#repo_name').focus();
150 167
151 168 $('#select_my_group').on('click', function(e){
152 169 e.preventDefault();
153 170 $("#repo_group").val($(this).data('personalGroupId')).trigger("change");
154 171 })
155 172
156 173 })
157 174 </script>
158 175 ${h.end_form()}
General Comments 0
You need to be logged in to leave comments. Login now