##// END OF EJS Templates
users: add additional information why user with pending reviews shouldn't be deleted.
dan -
r1923:4a76cf6b default
parent child Browse files
Show More
@@ -1,493 +1,503 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 Users crud controller for pylons
23 23 """
24 24
25 25 import logging
26 26 import formencode
27 27
28 28 from formencode import htmlfill
29 29 from pylons import request, tmpl_context as c, url, config
30 30 from pylons.controllers.util import redirect
31 31 from pylons.i18n.translation import _
32 32
33 33 from rhodecode.authentication.plugins import auth_rhodecode
34 34
35 35 from rhodecode.lib import helpers as h
36 36 from rhodecode.lib import auth
37 37 from rhodecode.lib import audit_logger
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasPermissionAllDecorator, AuthUser)
40 40 from rhodecode.lib.base import BaseController, render
41 41 from rhodecode.lib.exceptions import (
42 42 DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException,
43 43 UserOwnsUserGroupsException, UserCreationError)
44 44 from rhodecode.lib.utils2 import safe_int, AttributeDict
45 45
46 46 from rhodecode.model.db import (
47 47 PullRequestReviewers, User, UserEmailMap, UserIpMap, RepoGroup)
48 48 from rhodecode.model.forms import (
49 49 UserForm, UserPermissionsForm, UserIndividualPermissionsForm)
50 50 from rhodecode.model.repo_group import RepoGroupModel
51 51 from rhodecode.model.user import UserModel
52 52 from rhodecode.model.meta import Session
53 53 from rhodecode.model.permission import PermissionModel
54 54
55 55 log = logging.getLogger(__name__)
56 56
57 57
58 58 class UsersController(BaseController):
59 59 """REST Controller styled on the Atom Publishing Protocol"""
60 60
61 61 @LoginRequired()
62 62 def __before__(self):
63 63 super(UsersController, self).__before__()
64 64 c.available_permissions = config['available_permissions']
65 65 c.allowed_languages = [
66 66 ('en', 'English (en)'),
67 67 ('de', 'German (de)'),
68 68 ('fr', 'French (fr)'),
69 69 ('it', 'Italian (it)'),
70 70 ('ja', 'Japanese (ja)'),
71 71 ('pl', 'Polish (pl)'),
72 72 ('pt', 'Portuguese (pt)'),
73 73 ('ru', 'Russian (ru)'),
74 74 ('zh', 'Chinese (zh)'),
75 75 ]
76 76 PermissionModel().set_global_permission_choices(c, gettext_translator=_)
77 77
78 78 def _get_personal_repo_group_template_vars(self):
79 79 DummyUser = AttributeDict({
80 80 'username': '${username}',
81 81 'user_id': '${user_id}',
82 82 })
83 83 c.default_create_repo_group = RepoGroupModel() \
84 84 .get_default_create_personal_repo_group()
85 85 c.personal_repo_group_name = RepoGroupModel() \
86 86 .get_personal_group_name(DummyUser)
87 87
88 88 @HasPermissionAllDecorator('hg.admin')
89 89 @auth.CSRFRequired()
90 90 def create(self):
91 91 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
92 92 user_model = UserModel()
93 93 user_form = UserForm()()
94 94 try:
95 95 form_result = user_form.to_python(dict(request.POST))
96 96 user = user_model.create(form_result)
97 97 Session().flush()
98 98 creation_data = user.get_api_data()
99 99 username = form_result['username']
100 100
101 101 audit_logger.store_web(
102 102 'user.create', action_data={'data': creation_data},
103 103 user=c.rhodecode_user)
104 104
105 105 user_link = h.link_to(h.escape(username),
106 106 url('edit_user',
107 107 user_id=user.user_id))
108 108 h.flash(h.literal(_('Created user %(user_link)s')
109 109 % {'user_link': user_link}), category='success')
110 110 Session().commit()
111 111 except formencode.Invalid as errors:
112 112 self._get_personal_repo_group_template_vars()
113 113 return htmlfill.render(
114 114 render('admin/users/user_add.mako'),
115 115 defaults=errors.value,
116 116 errors=errors.error_dict or {},
117 117 prefix_error=False,
118 118 encoding="UTF-8",
119 119 force_defaults=False)
120 120 except UserCreationError as e:
121 121 h.flash(e, 'error')
122 122 except Exception:
123 123 log.exception("Exception creation of user")
124 124 h.flash(_('Error occurred during creation of user %s')
125 125 % request.POST.get('username'), category='error')
126 126 return redirect(h.route_path('users'))
127 127
128 128 @HasPermissionAllDecorator('hg.admin')
129 129 def new(self):
130 130 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
131 131 self._get_personal_repo_group_template_vars()
132 132 return render('admin/users/user_add.mako')
133 133
134 134 @HasPermissionAllDecorator('hg.admin')
135 135 @auth.CSRFRequired()
136 136 def update(self, user_id):
137 137
138 138 user_id = safe_int(user_id)
139 139 c.user = User.get_or_404(user_id)
140 140 c.active = 'profile'
141 141 c.extern_type = c.user.extern_type
142 142 c.extern_name = c.user.extern_name
143 143 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
144 144 available_languages = [x[0] for x in c.allowed_languages]
145 145 _form = UserForm(edit=True, available_languages=available_languages,
146 146 old_data={'user_id': user_id,
147 147 'email': c.user.email})()
148 148 form_result = {}
149 149 old_values = c.user.get_api_data()
150 150 try:
151 151 form_result = _form.to_python(dict(request.POST))
152 152 skip_attrs = ['extern_type', 'extern_name']
153 153 # TODO: plugin should define if username can be updated
154 154 if c.extern_type != "rhodecode":
155 155 # forbid updating username for external accounts
156 156 skip_attrs.append('username')
157 157
158 158 UserModel().update_user(
159 159 user_id, skip_attrs=skip_attrs, **form_result)
160 160
161 161 audit_logger.store_web(
162 162 'user.edit', action_data={'old_data': old_values},
163 163 user=c.rhodecode_user)
164 164
165 165 Session().commit()
166 166 h.flash(_('User updated successfully'), category='success')
167 167 except formencode.Invalid as errors:
168 168 defaults = errors.value
169 169 e = errors.error_dict or {}
170 170
171 171 return htmlfill.render(
172 172 render('admin/users/user_edit.mako'),
173 173 defaults=defaults,
174 174 errors=e,
175 175 prefix_error=False,
176 176 encoding="UTF-8",
177 177 force_defaults=False)
178 178 except UserCreationError as e:
179 179 h.flash(e, 'error')
180 180 except Exception:
181 181 log.exception("Exception updating user")
182 182 h.flash(_('Error occurred during update of user %s')
183 183 % form_result.get('username'), category='error')
184 184 return redirect(url('edit_user', user_id=user_id))
185 185
186 186 @HasPermissionAllDecorator('hg.admin')
187 187 @auth.CSRFRequired()
188 188 def delete(self, user_id):
189 189 user_id = safe_int(user_id)
190 190 c.user = User.get_or_404(user_id)
191 191
192 192 _repos = c.user.repositories
193 193 _repo_groups = c.user.repository_groups
194 194 _user_groups = c.user.user_groups
195 195
196 196 handle_repos = None
197 197 handle_repo_groups = None
198 198 handle_user_groups = None
199 199 # dummy call for flash of handle
200 200 set_handle_flash_repos = lambda: None
201 201 set_handle_flash_repo_groups = lambda: None
202 202 set_handle_flash_user_groups = lambda: None
203 203
204 204 if _repos and request.POST.get('user_repos'):
205 205 do = request.POST['user_repos']
206 206 if do == 'detach':
207 207 handle_repos = 'detach'
208 208 set_handle_flash_repos = lambda: h.flash(
209 209 _('Detached %s repositories') % len(_repos),
210 210 category='success')
211 211 elif do == 'delete':
212 212 handle_repos = 'delete'
213 213 set_handle_flash_repos = lambda: h.flash(
214 214 _('Deleted %s repositories') % len(_repos),
215 215 category='success')
216 216
217 217 if _repo_groups and request.POST.get('user_repo_groups'):
218 218 do = request.POST['user_repo_groups']
219 219 if do == 'detach':
220 220 handle_repo_groups = 'detach'
221 221 set_handle_flash_repo_groups = lambda: h.flash(
222 222 _('Detached %s repository groups') % len(_repo_groups),
223 223 category='success')
224 224 elif do == 'delete':
225 225 handle_repo_groups = 'delete'
226 226 set_handle_flash_repo_groups = lambda: h.flash(
227 227 _('Deleted %s repository groups') % len(_repo_groups),
228 228 category='success')
229 229
230 230 if _user_groups and request.POST.get('user_user_groups'):
231 231 do = request.POST['user_user_groups']
232 232 if do == 'detach':
233 233 handle_user_groups = 'detach'
234 234 set_handle_flash_user_groups = lambda: h.flash(
235 235 _('Detached %s user groups') % len(_user_groups),
236 236 category='success')
237 237 elif do == 'delete':
238 238 handle_user_groups = 'delete'
239 239 set_handle_flash_user_groups = lambda: h.flash(
240 240 _('Deleted %s user groups') % len(_user_groups),
241 241 category='success')
242 242
243 243 old_values = c.user.get_api_data()
244 244 try:
245 245 UserModel().delete(c.user, handle_repos=handle_repos,
246 246 handle_repo_groups=handle_repo_groups,
247 247 handle_user_groups=handle_user_groups)
248 248
249 249 audit_logger.store_web(
250 250 'user.delete', action_data={'old_data': old_values},
251 251 user=c.rhodecode_user)
252 252
253 253 Session().commit()
254 254 set_handle_flash_repos()
255 255 set_handle_flash_repo_groups()
256 256 set_handle_flash_user_groups()
257 257 h.flash(_('Successfully deleted user'), category='success')
258 258 except (UserOwnsReposException, UserOwnsRepoGroupsException,
259 259 UserOwnsUserGroupsException, DefaultUserException) as e:
260 260 h.flash(e, category='warning')
261 261 except Exception:
262 262 log.exception("Exception during deletion of user")
263 263 h.flash(_('An error occurred during deletion of user'),
264 264 category='error')
265 265 return redirect(h.route_path('users'))
266 266
267 267 @HasPermissionAllDecorator('hg.admin')
268 268 @auth.CSRFRequired()
269 269 def reset_password(self, user_id):
270 270 """
271 271 toggle reset password flag for this user
272 272 """
273 273 user_id = safe_int(user_id)
274 274 c.user = User.get_or_404(user_id)
275 275 try:
276 276 old_value = c.user.user_data.get('force_password_change')
277 277 c.user.update_userdata(force_password_change=not old_value)
278 278
279 279 if old_value:
280 280 msg = _('Force password change disabled for user')
281 281 audit_logger.store_web(
282 282 'user.edit.password_reset.disabled',
283 283 user=c.rhodecode_user)
284 284 else:
285 285 msg = _('Force password change enabled for user')
286 286 audit_logger.store_web(
287 287 'user.edit.password_reset.enabled',
288 288 user=c.rhodecode_user)
289 289
290 290 Session().commit()
291 291 h.flash(msg, category='success')
292 292 except Exception:
293 293 log.exception("Exception during password reset for user")
294 294 h.flash(_('An error occurred during password reset for user'),
295 295 category='error')
296 296
297 297 return redirect(url('edit_user_advanced', user_id=user_id))
298 298
299 299 @HasPermissionAllDecorator('hg.admin')
300 300 @auth.CSRFRequired()
301 301 def create_personal_repo_group(self, user_id):
302 302 """
303 303 Create personal repository group for this user
304 304 """
305 305 from rhodecode.model.repo_group import RepoGroupModel
306 306
307 307 user_id = safe_int(user_id)
308 308 c.user = User.get_or_404(user_id)
309 309 personal_repo_group = RepoGroup.get_user_personal_repo_group(
310 310 c.user.user_id)
311 311 if personal_repo_group:
312 312 return redirect(url('edit_user_advanced', user_id=user_id))
313 313
314 314 personal_repo_group_name = RepoGroupModel().get_personal_group_name(
315 315 c.user)
316 316 named_personal_group = RepoGroup.get_by_group_name(
317 317 personal_repo_group_name)
318 318 try:
319 319
320 320 if named_personal_group and named_personal_group.user_id == c.user.user_id:
321 321 # migrate the same named group, and mark it as personal
322 322 named_personal_group.personal = True
323 323 Session().add(named_personal_group)
324 324 Session().commit()
325 325 msg = _('Linked repository group `%s` as personal' % (
326 326 personal_repo_group_name,))
327 327 h.flash(msg, category='success')
328 328 elif not named_personal_group:
329 329 RepoGroupModel().create_personal_repo_group(c.user)
330 330
331 331 msg = _('Created repository group `%s`' % (
332 332 personal_repo_group_name,))
333 333 h.flash(msg, category='success')
334 334 else:
335 335 msg = _('Repository group `%s` is already taken' % (
336 336 personal_repo_group_name,))
337 337 h.flash(msg, category='warning')
338 338 except Exception:
339 339 log.exception("Exception during repository group creation")
340 340 msg = _(
341 341 'An error occurred during repository group creation for user')
342 342 h.flash(msg, category='error')
343 343 Session().rollback()
344 344
345 345 return redirect(url('edit_user_advanced', user_id=user_id))
346 346
347 347 @HasPermissionAllDecorator('hg.admin')
348 348 def show(self, user_id):
349 349 """GET /users/user_id: Show a specific item"""
350 350 # url('user', user_id=ID)
351 351 User.get_or_404(-1)
352 352
353 353 @HasPermissionAllDecorator('hg.admin')
354 354 def edit(self, user_id):
355 355 """GET /users/user_id/edit: Form to edit an existing item"""
356 356 # url('edit_user', user_id=ID)
357 357 user_id = safe_int(user_id)
358 358 c.user = User.get_or_404(user_id)
359 359 if c.user.username == User.DEFAULT_USER:
360 360 h.flash(_("You can't edit this user"), category='warning')
361 361 return redirect(h.route_path('users'))
362 362
363 363 c.active = 'profile'
364 364 c.extern_type = c.user.extern_type
365 365 c.extern_name = c.user.extern_name
366 366 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
367 367
368 368 defaults = c.user.get_dict()
369 369 defaults.update({'language': c.user.user_data.get('language')})
370 370 return htmlfill.render(
371 371 render('admin/users/user_edit.mako'),
372 372 defaults=defaults,
373 373 encoding="UTF-8",
374 374 force_defaults=False)
375 375
376 376 @HasPermissionAllDecorator('hg.admin')
377 377 def edit_advanced(self, user_id):
378 378 user_id = safe_int(user_id)
379 379 user = c.user = User.get_or_404(user_id)
380 380 if user.username == User.DEFAULT_USER:
381 381 h.flash(_("You can't edit this user"), category='warning')
382 382 return redirect(h.route_path('users'))
383 383
384 384 c.active = 'advanced'
385 385 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
386 386 c.personal_repo_group_name = RepoGroupModel()\
387 387 .get_personal_group_name(user)
388 388 c.first_admin = User.get_first_super_admin()
389 389 defaults = user.get_dict()
390 390
391 391 # Interim workaround if the user participated on any pull requests as a
392 392 # reviewer.
393 has_review = bool(PullRequestReviewers.query().filter(
394 PullRequestReviewers.user_id == user_id).first())
393 has_review = len(user.reviewer_pull_requests)
395 394 c.can_delete_user = not has_review
396 c.can_delete_user_message = _(
397 'The user participates as reviewer in pull requests and '
398 'cannot be deleted. You can set the user to '
399 '"inactive" instead of deleting it.') if has_review else ''
395 c.can_delete_user_message = ''
396 inactive_link = h.link_to(
397 'inactive', h.url('edit_user', user_id=user_id, anchor='active'))
398 if has_review == 1:
399 c.can_delete_user_message = h.literal(_(
400 'The user participates as reviewer in {} pull request and '
401 'cannot be deleted. \nYou can set the user to '
402 '"{}" instead of deleting it.').format(
403 has_review, inactive_link))
404 elif has_review:
405 c.can_delete_user_message = h.literal(_(
406 'The user participates as reviewer in {} pull requests and '
407 'cannot be deleted. \nYou can set the user to '
408 '"{}" instead of deleting it.').format(
409 has_review, inactive_link))
400 410
401 411 return htmlfill.render(
402 412 render('admin/users/user_edit.mako'),
403 413 defaults=defaults,
404 414 encoding="UTF-8",
405 415 force_defaults=False)
406 416
407 417 @HasPermissionAllDecorator('hg.admin')
408 418 def edit_global_perms(self, user_id):
409 419 user_id = safe_int(user_id)
410 420 c.user = User.get_or_404(user_id)
411 421 if c.user.username == User.DEFAULT_USER:
412 422 h.flash(_("You can't edit this user"), category='warning')
413 423 return redirect(h.route_path('users'))
414 424
415 425 c.active = 'global_perms'
416 426
417 427 c.default_user = User.get_default_user()
418 428 defaults = c.user.get_dict()
419 429 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
420 430 defaults.update(c.default_user.get_default_perms())
421 431 defaults.update(c.user.get_default_perms())
422 432
423 433 return htmlfill.render(
424 434 render('admin/users/user_edit.mako'),
425 435 defaults=defaults,
426 436 encoding="UTF-8",
427 437 force_defaults=False)
428 438
429 439 @HasPermissionAllDecorator('hg.admin')
430 440 @auth.CSRFRequired()
431 441 def update_global_perms(self, user_id):
432 442 user_id = safe_int(user_id)
433 443 user = User.get_or_404(user_id)
434 444 c.active = 'global_perms'
435 445 try:
436 446 # first stage that verifies the checkbox
437 447 _form = UserIndividualPermissionsForm()
438 448 form_result = _form.to_python(dict(request.POST))
439 449 inherit_perms = form_result['inherit_default_permissions']
440 450 user.inherit_default_permissions = inherit_perms
441 451 Session().add(user)
442 452
443 453 if not inherit_perms:
444 454 # only update the individual ones if we un check the flag
445 455 _form = UserPermissionsForm(
446 456 [x[0] for x in c.repo_create_choices],
447 457 [x[0] for x in c.repo_create_on_write_choices],
448 458 [x[0] for x in c.repo_group_create_choices],
449 459 [x[0] for x in c.user_group_create_choices],
450 460 [x[0] for x in c.fork_choices],
451 461 [x[0] for x in c.inherit_default_permission_choices])()
452 462
453 463 form_result = _form.to_python(dict(request.POST))
454 464 form_result.update({'perm_user_id': user.user_id})
455 465
456 466 PermissionModel().update_user_permissions(form_result)
457 467
458 468 # TODO(marcink): implement global permissions
459 469 # audit_log.store_web('user.edit.permissions')
460 470
461 471 Session().commit()
462 472 h.flash(_('User global permissions updated successfully'),
463 473 category='success')
464 474
465 475 except formencode.Invalid as errors:
466 476 defaults = errors.value
467 477 c.user = user
468 478 return htmlfill.render(
469 479 render('admin/users/user_edit.mako'),
470 480 defaults=defaults,
471 481 errors=errors.error_dict or {},
472 482 prefix_error=False,
473 483 encoding="UTF-8",
474 484 force_defaults=False)
475 485 except Exception:
476 486 log.exception("Exception during permissions saving")
477 487 h.flash(_('An error occurred during permissions saving'),
478 488 category='error')
479 489 return redirect(url('edit_user_global_perms', user_id=user_id))
480 490
481 491 @HasPermissionAllDecorator('hg.admin')
482 492 def edit_perms_summary(self, user_id):
483 493 user_id = safe_int(user_id)
484 494 c.user = User.get_or_404(user_id)
485 495 if c.user.username == User.DEFAULT_USER:
486 496 h.flash(_("You can't edit this user"), category='warning')
487 497 return redirect(h.route_path('users'))
488 498
489 499 c.active = 'perms_summary'
490 500 c.perm_user = AuthUser(user_id=user_id, ip_addr=self.ip_addr)
491 501
492 502 return render('admin/users/user_edit.mako')
493 503
@@ -1,4112 +1,4119 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2017 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 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import time
28 28 import hashlib
29 29 import logging
30 30 import datetime
31 31 import warnings
32 32 import ipaddress
33 33 import functools
34 34 import traceback
35 35 import collections
36 36
37 37
38 38 from sqlalchemy import *
39 39 from sqlalchemy.ext.declarative import declared_attr
40 40 from sqlalchemy.ext.hybrid import hybrid_property
41 41 from sqlalchemy.orm import (
42 42 relationship, joinedload, class_mapper, validates, aliased)
43 43 from sqlalchemy.sql.expression import true
44 44 from beaker.cache import cache_region
45 45 from zope.cachedescriptors.property import Lazy as LazyProperty
46 46
47 47 from pyramid.threadlocal import get_current_request
48 48
49 49 from rhodecode.translation import _
50 50 from rhodecode.lib.vcs import get_vcs_instance
51 51 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
52 52 from rhodecode.lib.utils2 import (
53 53 str2bool, safe_str, get_commit_safe, safe_unicode, md5_safe,
54 54 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
55 55 glob2re, StrictAttributeDict, cleaned_uri)
56 56 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType
57 57 from rhodecode.lib.ext_json import json
58 58 from rhodecode.lib.caching_query import FromCache
59 59 from rhodecode.lib.encrypt import AESCipher
60 60
61 61 from rhodecode.model.meta import Base, Session
62 62
63 63 URL_SEP = '/'
64 64 log = logging.getLogger(__name__)
65 65
66 66 # =============================================================================
67 67 # BASE CLASSES
68 68 # =============================================================================
69 69
70 70 # this is propagated from .ini file rhodecode.encrypted_values.secret or
71 71 # beaker.session.secret if first is not set.
72 72 # and initialized at environment.py
73 73 ENCRYPTION_KEY = None
74 74
75 75 # used to sort permissions by types, '#' used here is not allowed to be in
76 76 # usernames, and it's very early in sorted string.printable table.
77 77 PERMISSION_TYPE_SORT = {
78 78 'admin': '####',
79 79 'write': '###',
80 80 'read': '##',
81 81 'none': '#',
82 82 }
83 83
84 84
85 85 def display_sort(obj):
86 86 """
87 87 Sort function used to sort permissions in .permissions() function of
88 88 Repository, RepoGroup, UserGroup. Also it put the default user in front
89 89 of all other resources
90 90 """
91 91
92 92 if obj.username == User.DEFAULT_USER:
93 93 return '#####'
94 94 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
95 95 return prefix + obj.username
96 96
97 97
98 98 def _hash_key(k):
99 99 return md5_safe(k)
100 100
101 101
102 102 class EncryptedTextValue(TypeDecorator):
103 103 """
104 104 Special column for encrypted long text data, use like::
105 105
106 106 value = Column("encrypted_value", EncryptedValue(), nullable=False)
107 107
108 108 This column is intelligent so if value is in unencrypted form it return
109 109 unencrypted form, but on save it always encrypts
110 110 """
111 111 impl = Text
112 112
113 113 def process_bind_param(self, value, dialect):
114 114 if not value:
115 115 return value
116 116 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
117 117 # protect against double encrypting if someone manually starts
118 118 # doing
119 119 raise ValueError('value needs to be in unencrypted format, ie. '
120 120 'not starting with enc$aes')
121 121 return 'enc$aes_hmac$%s' % AESCipher(
122 122 ENCRYPTION_KEY, hmac=True).encrypt(value)
123 123
124 124 def process_result_value(self, value, dialect):
125 125 import rhodecode
126 126
127 127 if not value:
128 128 return value
129 129
130 130 parts = value.split('$', 3)
131 131 if not len(parts) == 3:
132 132 # probably not encrypted values
133 133 return value
134 134 else:
135 135 if parts[0] != 'enc':
136 136 # parts ok but without our header ?
137 137 return value
138 138 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
139 139 'rhodecode.encrypted_values.strict') or True)
140 140 # at that stage we know it's our encryption
141 141 if parts[1] == 'aes':
142 142 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
143 143 elif parts[1] == 'aes_hmac':
144 144 decrypted_data = AESCipher(
145 145 ENCRYPTION_KEY, hmac=True,
146 146 strict_verification=enc_strict_mode).decrypt(parts[2])
147 147 else:
148 148 raise ValueError(
149 149 'Encryption type part is wrong, must be `aes` '
150 150 'or `aes_hmac`, got `%s` instead' % (parts[1]))
151 151 return decrypted_data
152 152
153 153
154 154 class BaseModel(object):
155 155 """
156 156 Base Model for all classes
157 157 """
158 158
159 159 @classmethod
160 160 def _get_keys(cls):
161 161 """return column names for this model """
162 162 return class_mapper(cls).c.keys()
163 163
164 164 def get_dict(self):
165 165 """
166 166 return dict with keys and values corresponding
167 167 to this model data """
168 168
169 169 d = {}
170 170 for k in self._get_keys():
171 171 d[k] = getattr(self, k)
172 172
173 173 # also use __json__() if present to get additional fields
174 174 _json_attr = getattr(self, '__json__', None)
175 175 if _json_attr:
176 176 # update with attributes from __json__
177 177 if callable(_json_attr):
178 178 _json_attr = _json_attr()
179 179 for k, val in _json_attr.iteritems():
180 180 d[k] = val
181 181 return d
182 182
183 183 def get_appstruct(self):
184 184 """return list with keys and values tuples corresponding
185 185 to this model data """
186 186
187 187 l = []
188 188 for k in self._get_keys():
189 189 l.append((k, getattr(self, k),))
190 190 return l
191 191
192 192 def populate_obj(self, populate_dict):
193 193 """populate model with data from given populate_dict"""
194 194
195 195 for k in self._get_keys():
196 196 if k in populate_dict:
197 197 setattr(self, k, populate_dict[k])
198 198
199 199 @classmethod
200 200 def query(cls):
201 201 return Session().query(cls)
202 202
203 203 @classmethod
204 204 def get(cls, id_):
205 205 if id_:
206 206 return cls.query().get(id_)
207 207
208 208 @classmethod
209 209 def get_or_404(cls, id_, pyramid_exc=False):
210 210 if pyramid_exc:
211 211 # NOTE(marcink): backward compat, once migration to pyramid
212 212 # this should only use pyramid exceptions
213 213 from pyramid.httpexceptions import HTTPNotFound
214 214 else:
215 215 from webob.exc import HTTPNotFound
216 216
217 217 try:
218 218 id_ = int(id_)
219 219 except (TypeError, ValueError):
220 220 raise HTTPNotFound
221 221
222 222 res = cls.query().get(id_)
223 223 if not res:
224 224 raise HTTPNotFound
225 225 return res
226 226
227 227 @classmethod
228 228 def getAll(cls):
229 229 # deprecated and left for backward compatibility
230 230 return cls.get_all()
231 231
232 232 @classmethod
233 233 def get_all(cls):
234 234 return cls.query().all()
235 235
236 236 @classmethod
237 237 def delete(cls, id_):
238 238 obj = cls.query().get(id_)
239 239 Session().delete(obj)
240 240
241 241 @classmethod
242 242 def identity_cache(cls, session, attr_name, value):
243 243 exist_in_session = []
244 244 for (item_cls, pkey), instance in session.identity_map.items():
245 245 if cls == item_cls and getattr(instance, attr_name) == value:
246 246 exist_in_session.append(instance)
247 247 if exist_in_session:
248 248 if len(exist_in_session) == 1:
249 249 return exist_in_session[0]
250 250 log.exception(
251 251 'multiple objects with attr %s and '
252 252 'value %s found with same name: %r',
253 253 attr_name, value, exist_in_session)
254 254
255 255 def __repr__(self):
256 256 if hasattr(self, '__unicode__'):
257 257 # python repr needs to return str
258 258 try:
259 259 return safe_str(self.__unicode__())
260 260 except UnicodeDecodeError:
261 261 pass
262 262 return '<DB:%s>' % (self.__class__.__name__)
263 263
264 264
265 265 class RhodeCodeSetting(Base, BaseModel):
266 266 __tablename__ = 'rhodecode_settings'
267 267 __table_args__ = (
268 268 UniqueConstraint('app_settings_name'),
269 269 {'extend_existing': True, 'mysql_engine': 'InnoDB',
270 270 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
271 271 )
272 272
273 273 SETTINGS_TYPES = {
274 274 'str': safe_str,
275 275 'int': safe_int,
276 276 'unicode': safe_unicode,
277 277 'bool': str2bool,
278 278 'list': functools.partial(aslist, sep=',')
279 279 }
280 280 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
281 281 GLOBAL_CONF_KEY = 'app_settings'
282 282
283 283 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
284 284 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
285 285 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
286 286 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
287 287
288 288 def __init__(self, key='', val='', type='unicode'):
289 289 self.app_settings_name = key
290 290 self.app_settings_type = type
291 291 self.app_settings_value = val
292 292
293 293 @validates('_app_settings_value')
294 294 def validate_settings_value(self, key, val):
295 295 assert type(val) == unicode
296 296 return val
297 297
298 298 @hybrid_property
299 299 def app_settings_value(self):
300 300 v = self._app_settings_value
301 301 _type = self.app_settings_type
302 302 if _type:
303 303 _type = self.app_settings_type.split('.')[0]
304 304 # decode the encrypted value
305 305 if 'encrypted' in self.app_settings_type:
306 306 cipher = EncryptedTextValue()
307 307 v = safe_unicode(cipher.process_result_value(v, None))
308 308
309 309 converter = self.SETTINGS_TYPES.get(_type) or \
310 310 self.SETTINGS_TYPES['unicode']
311 311 return converter(v)
312 312
313 313 @app_settings_value.setter
314 314 def app_settings_value(self, val):
315 315 """
316 316 Setter that will always make sure we use unicode in app_settings_value
317 317
318 318 :param val:
319 319 """
320 320 val = safe_unicode(val)
321 321 # encode the encrypted value
322 322 if 'encrypted' in self.app_settings_type:
323 323 cipher = EncryptedTextValue()
324 324 val = safe_unicode(cipher.process_bind_param(val, None))
325 325 self._app_settings_value = val
326 326
327 327 @hybrid_property
328 328 def app_settings_type(self):
329 329 return self._app_settings_type
330 330
331 331 @app_settings_type.setter
332 332 def app_settings_type(self, val):
333 333 if val.split('.')[0] not in self.SETTINGS_TYPES:
334 334 raise Exception('type must be one of %s got %s'
335 335 % (self.SETTINGS_TYPES.keys(), val))
336 336 self._app_settings_type = val
337 337
338 338 def __unicode__(self):
339 339 return u"<%s('%s:%s[%s]')>" % (
340 340 self.__class__.__name__,
341 341 self.app_settings_name, self.app_settings_value,
342 342 self.app_settings_type
343 343 )
344 344
345 345
346 346 class RhodeCodeUi(Base, BaseModel):
347 347 __tablename__ = 'rhodecode_ui'
348 348 __table_args__ = (
349 349 UniqueConstraint('ui_key'),
350 350 {'extend_existing': True, 'mysql_engine': 'InnoDB',
351 351 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
352 352 )
353 353
354 354 HOOK_REPO_SIZE = 'changegroup.repo_size'
355 355 # HG
356 356 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
357 357 HOOK_PULL = 'outgoing.pull_logger'
358 358 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
359 359 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
360 360 HOOK_PUSH = 'changegroup.push_logger'
361 361 HOOK_PUSH_KEY = 'pushkey.key_push'
362 362
363 363 # TODO: johbo: Unify way how hooks are configured for git and hg,
364 364 # git part is currently hardcoded.
365 365
366 366 # SVN PATTERNS
367 367 SVN_BRANCH_ID = 'vcs_svn_branch'
368 368 SVN_TAG_ID = 'vcs_svn_tag'
369 369
370 370 ui_id = Column(
371 371 "ui_id", Integer(), nullable=False, unique=True, default=None,
372 372 primary_key=True)
373 373 ui_section = Column(
374 374 "ui_section", String(255), nullable=True, unique=None, default=None)
375 375 ui_key = Column(
376 376 "ui_key", String(255), nullable=True, unique=None, default=None)
377 377 ui_value = Column(
378 378 "ui_value", String(255), nullable=True, unique=None, default=None)
379 379 ui_active = Column(
380 380 "ui_active", Boolean(), nullable=True, unique=None, default=True)
381 381
382 382 def __repr__(self):
383 383 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
384 384 self.ui_key, self.ui_value)
385 385
386 386
387 387 class RepoRhodeCodeSetting(Base, BaseModel):
388 388 __tablename__ = 'repo_rhodecode_settings'
389 389 __table_args__ = (
390 390 UniqueConstraint(
391 391 'app_settings_name', 'repository_id',
392 392 name='uq_repo_rhodecode_setting_name_repo_id'),
393 393 {'extend_existing': True, 'mysql_engine': 'InnoDB',
394 394 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
395 395 )
396 396
397 397 repository_id = Column(
398 398 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
399 399 nullable=False)
400 400 app_settings_id = Column(
401 401 "app_settings_id", Integer(), nullable=False, unique=True,
402 402 default=None, primary_key=True)
403 403 app_settings_name = Column(
404 404 "app_settings_name", String(255), nullable=True, unique=None,
405 405 default=None)
406 406 _app_settings_value = Column(
407 407 "app_settings_value", String(4096), nullable=True, unique=None,
408 408 default=None)
409 409 _app_settings_type = Column(
410 410 "app_settings_type", String(255), nullable=True, unique=None,
411 411 default=None)
412 412
413 413 repository = relationship('Repository')
414 414
415 415 def __init__(self, repository_id, key='', val='', type='unicode'):
416 416 self.repository_id = repository_id
417 417 self.app_settings_name = key
418 418 self.app_settings_type = type
419 419 self.app_settings_value = val
420 420
421 421 @validates('_app_settings_value')
422 422 def validate_settings_value(self, key, val):
423 423 assert type(val) == unicode
424 424 return val
425 425
426 426 @hybrid_property
427 427 def app_settings_value(self):
428 428 v = self._app_settings_value
429 429 type_ = self.app_settings_type
430 430 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
431 431 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
432 432 return converter(v)
433 433
434 434 @app_settings_value.setter
435 435 def app_settings_value(self, val):
436 436 """
437 437 Setter that will always make sure we use unicode in app_settings_value
438 438
439 439 :param val:
440 440 """
441 441 self._app_settings_value = safe_unicode(val)
442 442
443 443 @hybrid_property
444 444 def app_settings_type(self):
445 445 return self._app_settings_type
446 446
447 447 @app_settings_type.setter
448 448 def app_settings_type(self, val):
449 449 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
450 450 if val not in SETTINGS_TYPES:
451 451 raise Exception('type must be one of %s got %s'
452 452 % (SETTINGS_TYPES.keys(), val))
453 453 self._app_settings_type = val
454 454
455 455 def __unicode__(self):
456 456 return u"<%s('%s:%s:%s[%s]')>" % (
457 457 self.__class__.__name__, self.repository.repo_name,
458 458 self.app_settings_name, self.app_settings_value,
459 459 self.app_settings_type
460 460 )
461 461
462 462
463 463 class RepoRhodeCodeUi(Base, BaseModel):
464 464 __tablename__ = 'repo_rhodecode_ui'
465 465 __table_args__ = (
466 466 UniqueConstraint(
467 467 'repository_id', 'ui_section', 'ui_key',
468 468 name='uq_repo_rhodecode_ui_repository_id_section_key'),
469 469 {'extend_existing': True, 'mysql_engine': 'InnoDB',
470 470 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
471 471 )
472 472
473 473 repository_id = Column(
474 474 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
475 475 nullable=False)
476 476 ui_id = Column(
477 477 "ui_id", Integer(), nullable=False, unique=True, default=None,
478 478 primary_key=True)
479 479 ui_section = Column(
480 480 "ui_section", String(255), nullable=True, unique=None, default=None)
481 481 ui_key = Column(
482 482 "ui_key", String(255), nullable=True, unique=None, default=None)
483 483 ui_value = Column(
484 484 "ui_value", String(255), nullable=True, unique=None, default=None)
485 485 ui_active = Column(
486 486 "ui_active", Boolean(), nullable=True, unique=None, default=True)
487 487
488 488 repository = relationship('Repository')
489 489
490 490 def __repr__(self):
491 491 return '<%s[%s:%s]%s=>%s]>' % (
492 492 self.__class__.__name__, self.repository.repo_name,
493 493 self.ui_section, self.ui_key, self.ui_value)
494 494
495 495
496 496 class User(Base, BaseModel):
497 497 __tablename__ = 'users'
498 498 __table_args__ = (
499 499 UniqueConstraint('username'), UniqueConstraint('email'),
500 500 Index('u_username_idx', 'username'),
501 501 Index('u_email_idx', 'email'),
502 502 {'extend_existing': True, 'mysql_engine': 'InnoDB',
503 503 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
504 504 )
505 505 DEFAULT_USER = 'default'
506 506 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
507 507 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
508 508
509 509 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
510 510 username = Column("username", String(255), nullable=True, unique=None, default=None)
511 511 password = Column("password", String(255), nullable=True, unique=None, default=None)
512 512 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
513 513 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
514 514 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
515 515 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
516 516 _email = Column("email", String(255), nullable=True, unique=None, default=None)
517 517 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
518 518 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
519 519
520 520 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
521 521 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
522 522 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
523 523 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
524 524 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
525 525 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
526 526
527 527 user_log = relationship('UserLog')
528 528 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
529 529
530 530 repositories = relationship('Repository')
531 531 repository_groups = relationship('RepoGroup')
532 532 user_groups = relationship('UserGroup')
533 533
534 534 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
535 535 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
536 536
537 537 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
538 538 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
539 539 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
540 540
541 541 group_member = relationship('UserGroupMember', cascade='all')
542 542
543 543 notifications = relationship('UserNotification', cascade='all')
544 544 # notifications assigned to this user
545 545 user_created_notifications = relationship('Notification', cascade='all')
546 546 # comments created by this user
547 547 user_comments = relationship('ChangesetComment', cascade='all')
548 548 # user profile extra info
549 549 user_emails = relationship('UserEmailMap', cascade='all')
550 550 user_ip_map = relationship('UserIpMap', cascade='all')
551 551 user_auth_tokens = relationship('UserApiKeys', cascade='all')
552 552 # gists
553 553 user_gists = relationship('Gist', cascade='all')
554 554 # user pull requests
555 555 user_pull_requests = relationship('PullRequest', cascade='all')
556 556 # external identities
557 557 extenal_identities = relationship(
558 558 'ExternalIdentity',
559 559 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
560 560 cascade='all')
561 561
562 562 def __unicode__(self):
563 563 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
564 564 self.user_id, self.username)
565 565
566 566 @hybrid_property
567 567 def email(self):
568 568 return self._email
569 569
570 570 @email.setter
571 571 def email(self, val):
572 572 self._email = val.lower() if val else None
573 573
574 574 @hybrid_property
575 575 def first_name(self):
576 576 from rhodecode.lib import helpers as h
577 577 if self.name:
578 578 return h.escape(self.name)
579 579 return self.name
580 580
581 581 @hybrid_property
582 582 def last_name(self):
583 583 from rhodecode.lib import helpers as h
584 584 if self.lastname:
585 585 return h.escape(self.lastname)
586 586 return self.lastname
587 587
588 588 @hybrid_property
589 589 def api_key(self):
590 590 """
591 591 Fetch if exist an auth-token with role ALL connected to this user
592 592 """
593 593 user_auth_token = UserApiKeys.query()\
594 594 .filter(UserApiKeys.user_id == self.user_id)\
595 595 .filter(or_(UserApiKeys.expires == -1,
596 596 UserApiKeys.expires >= time.time()))\
597 597 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
598 598 if user_auth_token:
599 599 user_auth_token = user_auth_token.api_key
600 600
601 601 return user_auth_token
602 602
603 603 @api_key.setter
604 604 def api_key(self, val):
605 605 # don't allow to set API key this is deprecated for now
606 606 self._api_key = None
607 607
608 608 @property
609 def reviewer_pull_requests(self):
610 return PullRequestReviewers.query() \
611 .options(joinedload(PullRequestReviewers.pull_request)) \
612 .filter(PullRequestReviewers.user_id == self.user_id) \
613 .all()
614
615 @property
609 616 def firstname(self):
610 617 # alias for future
611 618 return self.name
612 619
613 620 @property
614 621 def emails(self):
615 622 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
616 623 return [self.email] + [x.email for x in other]
617 624
618 625 @property
619 626 def auth_tokens(self):
620 627 return [x.api_key for x in self.extra_auth_tokens]
621 628
622 629 @property
623 630 def extra_auth_tokens(self):
624 631 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
625 632
626 633 @property
627 634 def feed_token(self):
628 635 return self.get_feed_token()
629 636
630 637 def get_feed_token(self):
631 638 feed_tokens = UserApiKeys.query()\
632 639 .filter(UserApiKeys.user == self)\
633 640 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
634 641 .all()
635 642 if feed_tokens:
636 643 return feed_tokens[0].api_key
637 644 return 'NO_FEED_TOKEN_AVAILABLE'
638 645
639 646 @classmethod
640 647 def extra_valid_auth_tokens(cls, user, role=None):
641 648 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
642 649 .filter(or_(UserApiKeys.expires == -1,
643 650 UserApiKeys.expires >= time.time()))
644 651 if role:
645 652 tokens = tokens.filter(or_(UserApiKeys.role == role,
646 653 UserApiKeys.role == UserApiKeys.ROLE_ALL))
647 654 return tokens.all()
648 655
649 656 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
650 657 from rhodecode.lib import auth
651 658
652 659 log.debug('Trying to authenticate user: %s via auth-token, '
653 660 'and roles: %s', self, roles)
654 661
655 662 if not auth_token:
656 663 return False
657 664
658 665 crypto_backend = auth.crypto_backend()
659 666
660 667 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
661 668 tokens_q = UserApiKeys.query()\
662 669 .filter(UserApiKeys.user_id == self.user_id)\
663 670 .filter(or_(UserApiKeys.expires == -1,
664 671 UserApiKeys.expires >= time.time()))
665 672
666 673 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
667 674
668 675 plain_tokens = []
669 676 hash_tokens = []
670 677
671 678 for token in tokens_q.all():
672 679 # verify scope first
673 680 if token.repo_id:
674 681 # token has a scope, we need to verify it
675 682 if scope_repo_id != token.repo_id:
676 683 log.debug(
677 684 'Scope mismatch: token has a set repo scope: %s, '
678 685 'and calling scope is:%s, skipping further checks',
679 686 token.repo, scope_repo_id)
680 687 # token has a scope, and it doesn't match, skip token
681 688 continue
682 689
683 690 if token.api_key.startswith(crypto_backend.ENC_PREF):
684 691 hash_tokens.append(token.api_key)
685 692 else:
686 693 plain_tokens.append(token.api_key)
687 694
688 695 is_plain_match = auth_token in plain_tokens
689 696 if is_plain_match:
690 697 return True
691 698
692 699 for hashed in hash_tokens:
693 700 # TODO(marcink): this is expensive to calculate, but most secure
694 701 match = crypto_backend.hash_check(auth_token, hashed)
695 702 if match:
696 703 return True
697 704
698 705 return False
699 706
700 707 @property
701 708 def ip_addresses(self):
702 709 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
703 710 return [x.ip_addr for x in ret]
704 711
705 712 @property
706 713 def username_and_name(self):
707 714 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
708 715
709 716 @property
710 717 def username_or_name_or_email(self):
711 718 full_name = self.full_name if self.full_name is not ' ' else None
712 719 return self.username or full_name or self.email
713 720
714 721 @property
715 722 def full_name(self):
716 723 return '%s %s' % (self.first_name, self.last_name)
717 724
718 725 @property
719 726 def full_name_or_username(self):
720 727 return ('%s %s' % (self.first_name, self.last_name)
721 728 if (self.first_name and self.last_name) else self.username)
722 729
723 730 @property
724 731 def full_contact(self):
725 732 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
726 733
727 734 @property
728 735 def short_contact(self):
729 736 return '%s %s' % (self.first_name, self.last_name)
730 737
731 738 @property
732 739 def is_admin(self):
733 740 return self.admin
734 741
735 742 @property
736 743 def AuthUser(self):
737 744 """
738 745 Returns instance of AuthUser for this user
739 746 """
740 747 from rhodecode.lib.auth import AuthUser
741 748 return AuthUser(user_id=self.user_id, username=self.username)
742 749
743 750 @hybrid_property
744 751 def user_data(self):
745 752 if not self._user_data:
746 753 return {}
747 754
748 755 try:
749 756 return json.loads(self._user_data)
750 757 except TypeError:
751 758 return {}
752 759
753 760 @user_data.setter
754 761 def user_data(self, val):
755 762 if not isinstance(val, dict):
756 763 raise Exception('user_data must be dict, got %s' % type(val))
757 764 try:
758 765 self._user_data = json.dumps(val)
759 766 except Exception:
760 767 log.error(traceback.format_exc())
761 768
762 769 @classmethod
763 770 def get_by_username(cls, username, case_insensitive=False,
764 771 cache=False, identity_cache=False):
765 772 session = Session()
766 773
767 774 if case_insensitive:
768 775 q = cls.query().filter(
769 776 func.lower(cls.username) == func.lower(username))
770 777 else:
771 778 q = cls.query().filter(cls.username == username)
772 779
773 780 if cache:
774 781 if identity_cache:
775 782 val = cls.identity_cache(session, 'username', username)
776 783 if val:
777 784 return val
778 785 else:
779 786 cache_key = "get_user_by_name_%s" % _hash_key(username)
780 787 q = q.options(
781 788 FromCache("sql_cache_short", cache_key))
782 789
783 790 return q.scalar()
784 791
785 792 @classmethod
786 793 def get_by_auth_token(cls, auth_token, cache=False):
787 794 q = UserApiKeys.query()\
788 795 .filter(UserApiKeys.api_key == auth_token)\
789 796 .filter(or_(UserApiKeys.expires == -1,
790 797 UserApiKeys.expires >= time.time()))
791 798 if cache:
792 799 q = q.options(
793 800 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
794 801
795 802 match = q.first()
796 803 if match:
797 804 return match.user
798 805
799 806 @classmethod
800 807 def get_by_email(cls, email, case_insensitive=False, cache=False):
801 808
802 809 if case_insensitive:
803 810 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
804 811
805 812 else:
806 813 q = cls.query().filter(cls.email == email)
807 814
808 815 email_key = _hash_key(email)
809 816 if cache:
810 817 q = q.options(
811 818 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
812 819
813 820 ret = q.scalar()
814 821 if ret is None:
815 822 q = UserEmailMap.query()
816 823 # try fetching in alternate email map
817 824 if case_insensitive:
818 825 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
819 826 else:
820 827 q = q.filter(UserEmailMap.email == email)
821 828 q = q.options(joinedload(UserEmailMap.user))
822 829 if cache:
823 830 q = q.options(
824 831 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
825 832 ret = getattr(q.scalar(), 'user', None)
826 833
827 834 return ret
828 835
829 836 @classmethod
830 837 def get_from_cs_author(cls, author):
831 838 """
832 839 Tries to get User objects out of commit author string
833 840
834 841 :param author:
835 842 """
836 843 from rhodecode.lib.helpers import email, author_name
837 844 # Valid email in the attribute passed, see if they're in the system
838 845 _email = email(author)
839 846 if _email:
840 847 user = cls.get_by_email(_email, case_insensitive=True)
841 848 if user:
842 849 return user
843 850 # Maybe we can match by username?
844 851 _author = author_name(author)
845 852 user = cls.get_by_username(_author, case_insensitive=True)
846 853 if user:
847 854 return user
848 855
849 856 def update_userdata(self, **kwargs):
850 857 usr = self
851 858 old = usr.user_data
852 859 old.update(**kwargs)
853 860 usr.user_data = old
854 861 Session().add(usr)
855 862 log.debug('updated userdata with ', kwargs)
856 863
857 864 def update_lastlogin(self):
858 865 """Update user lastlogin"""
859 866 self.last_login = datetime.datetime.now()
860 867 Session().add(self)
861 868 log.debug('updated user %s lastlogin', self.username)
862 869
863 870 def update_lastactivity(self):
864 871 """Update user lastactivity"""
865 872 self.last_activity = datetime.datetime.now()
866 873 Session().add(self)
867 874 log.debug('updated user %s lastactivity', self.username)
868 875
869 876 def update_password(self, new_password):
870 877 from rhodecode.lib.auth import get_crypt_password
871 878
872 879 self.password = get_crypt_password(new_password)
873 880 Session().add(self)
874 881
875 882 @classmethod
876 883 def get_first_super_admin(cls):
877 884 user = User.query().filter(User.admin == true()).first()
878 885 if user is None:
879 886 raise Exception('FATAL: Missing administrative account!')
880 887 return user
881 888
882 889 @classmethod
883 890 def get_all_super_admins(cls):
884 891 """
885 892 Returns all admin accounts sorted by username
886 893 """
887 894 return User.query().filter(User.admin == true())\
888 895 .order_by(User.username.asc()).all()
889 896
890 897 @classmethod
891 898 def get_default_user(cls, cache=False, refresh=False):
892 899 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
893 900 if user is None:
894 901 raise Exception('FATAL: Missing default account!')
895 902 if refresh:
896 903 # The default user might be based on outdated state which
897 904 # has been loaded from the cache.
898 905 # A call to refresh() ensures that the
899 906 # latest state from the database is used.
900 907 Session().refresh(user)
901 908 return user
902 909
903 910 def _get_default_perms(self, user, suffix=''):
904 911 from rhodecode.model.permission import PermissionModel
905 912 return PermissionModel().get_default_perms(user.user_perms, suffix)
906 913
907 914 def get_default_perms(self, suffix=''):
908 915 return self._get_default_perms(self, suffix)
909 916
910 917 def get_api_data(self, include_secrets=False, details='full'):
911 918 """
912 919 Common function for generating user related data for API
913 920
914 921 :param include_secrets: By default secrets in the API data will be replaced
915 922 by a placeholder value to prevent exposing this data by accident. In case
916 923 this data shall be exposed, set this flag to ``True``.
917 924
918 925 :param details: details can be 'basic|full' basic gives only a subset of
919 926 the available user information that includes user_id, name and emails.
920 927 """
921 928 user = self
922 929 user_data = self.user_data
923 930 data = {
924 931 'user_id': user.user_id,
925 932 'username': user.username,
926 933 'firstname': user.name,
927 934 'lastname': user.lastname,
928 935 'email': user.email,
929 936 'emails': user.emails,
930 937 }
931 938 if details == 'basic':
932 939 return data
933 940
934 941 api_key_length = 40
935 942 api_key_replacement = '*' * api_key_length
936 943
937 944 extras = {
938 945 'api_keys': [api_key_replacement],
939 946 'auth_tokens': [api_key_replacement],
940 947 'active': user.active,
941 948 'admin': user.admin,
942 949 'extern_type': user.extern_type,
943 950 'extern_name': user.extern_name,
944 951 'last_login': user.last_login,
945 952 'last_activity': user.last_activity,
946 953 'ip_addresses': user.ip_addresses,
947 954 'language': user_data.get('language')
948 955 }
949 956 data.update(extras)
950 957
951 958 if include_secrets:
952 959 data['api_keys'] = user.auth_tokens
953 960 data['auth_tokens'] = user.extra_auth_tokens
954 961 return data
955 962
956 963 def __json__(self):
957 964 data = {
958 965 'full_name': self.full_name,
959 966 'full_name_or_username': self.full_name_or_username,
960 967 'short_contact': self.short_contact,
961 968 'full_contact': self.full_contact,
962 969 }
963 970 data.update(self.get_api_data())
964 971 return data
965 972
966 973
967 974 class UserApiKeys(Base, BaseModel):
968 975 __tablename__ = 'user_api_keys'
969 976 __table_args__ = (
970 977 Index('uak_api_key_idx', 'api_key'),
971 978 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
972 979 UniqueConstraint('api_key'),
973 980 {'extend_existing': True, 'mysql_engine': 'InnoDB',
974 981 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
975 982 )
976 983 __mapper_args__ = {}
977 984
978 985 # ApiKey role
979 986 ROLE_ALL = 'token_role_all'
980 987 ROLE_HTTP = 'token_role_http'
981 988 ROLE_VCS = 'token_role_vcs'
982 989 ROLE_API = 'token_role_api'
983 990 ROLE_FEED = 'token_role_feed'
984 991 ROLE_PASSWORD_RESET = 'token_password_reset'
985 992
986 993 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
987 994
988 995 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
989 996 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
990 997 api_key = Column("api_key", String(255), nullable=False, unique=True)
991 998 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
992 999 expires = Column('expires', Float(53), nullable=False)
993 1000 role = Column('role', String(255), nullable=True)
994 1001 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
995 1002
996 1003 # scope columns
997 1004 repo_id = Column(
998 1005 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
999 1006 nullable=True, unique=None, default=None)
1000 1007 repo = relationship('Repository', lazy='joined')
1001 1008
1002 1009 repo_group_id = Column(
1003 1010 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1004 1011 nullable=True, unique=None, default=None)
1005 1012 repo_group = relationship('RepoGroup', lazy='joined')
1006 1013
1007 1014 user = relationship('User', lazy='joined')
1008 1015
1009 1016 def __unicode__(self):
1010 1017 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1011 1018
1012 1019 def __json__(self):
1013 1020 data = {
1014 1021 'auth_token': self.api_key,
1015 1022 'role': self.role,
1016 1023 'scope': self.scope_humanized,
1017 1024 'expired': self.expired
1018 1025 }
1019 1026 return data
1020 1027
1021 1028 def get_api_data(self, include_secrets=False):
1022 1029 data = self.__json__()
1023 1030 if include_secrets:
1024 1031 return data
1025 1032 else:
1026 1033 data['auth_token'] = self.token_obfuscated
1027 1034 return data
1028 1035
1029 1036 @hybrid_property
1030 1037 def description_safe(self):
1031 1038 from rhodecode.lib import helpers as h
1032 1039 return h.escape(self.description)
1033 1040
1034 1041 @property
1035 1042 def expired(self):
1036 1043 if self.expires == -1:
1037 1044 return False
1038 1045 return time.time() > self.expires
1039 1046
1040 1047 @classmethod
1041 1048 def _get_role_name(cls, role):
1042 1049 return {
1043 1050 cls.ROLE_ALL: _('all'),
1044 1051 cls.ROLE_HTTP: _('http/web interface'),
1045 1052 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1046 1053 cls.ROLE_API: _('api calls'),
1047 1054 cls.ROLE_FEED: _('feed access'),
1048 1055 }.get(role, role)
1049 1056
1050 1057 @property
1051 1058 def role_humanized(self):
1052 1059 return self._get_role_name(self.role)
1053 1060
1054 1061 def _get_scope(self):
1055 1062 if self.repo:
1056 1063 return repr(self.repo)
1057 1064 if self.repo_group:
1058 1065 return repr(self.repo_group) + ' (recursive)'
1059 1066 return 'global'
1060 1067
1061 1068 @property
1062 1069 def scope_humanized(self):
1063 1070 return self._get_scope()
1064 1071
1065 1072 @property
1066 1073 def token_obfuscated(self):
1067 1074 if self.api_key:
1068 1075 return self.api_key[:4] + "****"
1069 1076
1070 1077
1071 1078 class UserEmailMap(Base, BaseModel):
1072 1079 __tablename__ = 'user_email_map'
1073 1080 __table_args__ = (
1074 1081 Index('uem_email_idx', 'email'),
1075 1082 UniqueConstraint('email'),
1076 1083 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1077 1084 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1078 1085 )
1079 1086 __mapper_args__ = {}
1080 1087
1081 1088 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1082 1089 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1083 1090 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1084 1091 user = relationship('User', lazy='joined')
1085 1092
1086 1093 @validates('_email')
1087 1094 def validate_email(self, key, email):
1088 1095 # check if this email is not main one
1089 1096 main_email = Session().query(User).filter(User.email == email).scalar()
1090 1097 if main_email is not None:
1091 1098 raise AttributeError('email %s is present is user table' % email)
1092 1099 return email
1093 1100
1094 1101 @hybrid_property
1095 1102 def email(self):
1096 1103 return self._email
1097 1104
1098 1105 @email.setter
1099 1106 def email(self, val):
1100 1107 self._email = val.lower() if val else None
1101 1108
1102 1109
1103 1110 class UserIpMap(Base, BaseModel):
1104 1111 __tablename__ = 'user_ip_map'
1105 1112 __table_args__ = (
1106 1113 UniqueConstraint('user_id', 'ip_addr'),
1107 1114 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1108 1115 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
1109 1116 )
1110 1117 __mapper_args__ = {}
1111 1118
1112 1119 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1113 1120 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1114 1121 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1115 1122 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1116 1123 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1117 1124 user = relationship('User', lazy='joined')
1118 1125
1119 1126 @hybrid_property
1120 1127 def description_safe(self):
1121 1128 from rhodecode.lib import helpers as h
1122 1129 return h.escape(self.description)
1123 1130
1124 1131 @classmethod
1125 1132 def _get_ip_range(cls, ip_addr):
1126 1133 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1127 1134 return [str(net.network_address), str(net.broadcast_address)]
1128 1135
1129 1136 def __json__(self):
1130 1137 return {
1131 1138 'ip_addr': self.ip_addr,
1132 1139 'ip_range': self._get_ip_range(self.ip_addr),
1133 1140 }
1134 1141
1135 1142 def __unicode__(self):
1136 1143 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1137 1144 self.user_id, self.ip_addr)
1138 1145
1139 1146
1140 1147 class UserLog(Base, BaseModel):
1141 1148 __tablename__ = 'user_logs'
1142 1149 __table_args__ = (
1143 1150 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1144 1151 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1145 1152 )
1146 1153 VERSION_1 = 'v1'
1147 1154 VERSION_2 = 'v2'
1148 1155 VERSIONS = [VERSION_1, VERSION_2]
1149 1156
1150 1157 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1151 1158 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1152 1159 username = Column("username", String(255), nullable=True, unique=None, default=None)
1153 1160 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1154 1161 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1155 1162 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1156 1163 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1157 1164 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1158 1165
1159 1166 version = Column("version", String(255), nullable=True, default=VERSION_1)
1160 1167 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1161 1168 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
1162 1169
1163 1170 def __unicode__(self):
1164 1171 return u"<%s('id:%s:%s')>" % (
1165 1172 self.__class__.__name__, self.repository_name, self.action)
1166 1173
1167 1174 def __json__(self):
1168 1175 return {
1169 1176 'user_id': self.user_id,
1170 1177 'username': self.username,
1171 1178 'repository_id': self.repository_id,
1172 1179 'repository_name': self.repository_name,
1173 1180 'user_ip': self.user_ip,
1174 1181 'action_date': self.action_date,
1175 1182 'action': self.action,
1176 1183 }
1177 1184
1178 1185 @property
1179 1186 def action_as_day(self):
1180 1187 return datetime.date(*self.action_date.timetuple()[:3])
1181 1188
1182 1189 user = relationship('User')
1183 1190 repository = relationship('Repository', cascade='')
1184 1191
1185 1192
1186 1193 class UserGroup(Base, BaseModel):
1187 1194 __tablename__ = 'users_groups'
1188 1195 __table_args__ = (
1189 1196 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1190 1197 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1191 1198 )
1192 1199
1193 1200 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1194 1201 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1195 1202 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1196 1203 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1197 1204 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1198 1205 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1199 1206 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1200 1207 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1201 1208
1202 1209 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1203 1210 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1204 1211 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1205 1212 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1206 1213 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1207 1214 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1208 1215
1209 1216 user = relationship('User')
1210 1217
1211 1218 @hybrid_property
1212 1219 def description_safe(self):
1213 1220 from rhodecode.lib import helpers as h
1214 1221 return h.escape(self.description)
1215 1222
1216 1223 @hybrid_property
1217 1224 def group_data(self):
1218 1225 if not self._group_data:
1219 1226 return {}
1220 1227
1221 1228 try:
1222 1229 return json.loads(self._group_data)
1223 1230 except TypeError:
1224 1231 return {}
1225 1232
1226 1233 @group_data.setter
1227 1234 def group_data(self, val):
1228 1235 try:
1229 1236 self._group_data = json.dumps(val)
1230 1237 except Exception:
1231 1238 log.error(traceback.format_exc())
1232 1239
1233 1240 def __unicode__(self):
1234 1241 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1235 1242 self.users_group_id,
1236 1243 self.users_group_name)
1237 1244
1238 1245 @classmethod
1239 1246 def get_by_group_name(cls, group_name, cache=False,
1240 1247 case_insensitive=False):
1241 1248 if case_insensitive:
1242 1249 q = cls.query().filter(func.lower(cls.users_group_name) ==
1243 1250 func.lower(group_name))
1244 1251
1245 1252 else:
1246 1253 q = cls.query().filter(cls.users_group_name == group_name)
1247 1254 if cache:
1248 1255 q = q.options(
1249 1256 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1250 1257 return q.scalar()
1251 1258
1252 1259 @classmethod
1253 1260 def get(cls, user_group_id, cache=False):
1254 1261 user_group = cls.query()
1255 1262 if cache:
1256 1263 user_group = user_group.options(
1257 1264 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1258 1265 return user_group.get(user_group_id)
1259 1266
1260 1267 def permissions(self, with_admins=True, with_owner=True):
1261 1268 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1262 1269 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1263 1270 joinedload(UserUserGroupToPerm.user),
1264 1271 joinedload(UserUserGroupToPerm.permission),)
1265 1272
1266 1273 # get owners and admins and permissions. We do a trick of re-writing
1267 1274 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1268 1275 # has a global reference and changing one object propagates to all
1269 1276 # others. This means if admin is also an owner admin_row that change
1270 1277 # would propagate to both objects
1271 1278 perm_rows = []
1272 1279 for _usr in q.all():
1273 1280 usr = AttributeDict(_usr.user.get_dict())
1274 1281 usr.permission = _usr.permission.permission_name
1275 1282 perm_rows.append(usr)
1276 1283
1277 1284 # filter the perm rows by 'default' first and then sort them by
1278 1285 # admin,write,read,none permissions sorted again alphabetically in
1279 1286 # each group
1280 1287 perm_rows = sorted(perm_rows, key=display_sort)
1281 1288
1282 1289 _admin_perm = 'usergroup.admin'
1283 1290 owner_row = []
1284 1291 if with_owner:
1285 1292 usr = AttributeDict(self.user.get_dict())
1286 1293 usr.owner_row = True
1287 1294 usr.permission = _admin_perm
1288 1295 owner_row.append(usr)
1289 1296
1290 1297 super_admin_rows = []
1291 1298 if with_admins:
1292 1299 for usr in User.get_all_super_admins():
1293 1300 # if this admin is also owner, don't double the record
1294 1301 if usr.user_id == owner_row[0].user_id:
1295 1302 owner_row[0].admin_row = True
1296 1303 else:
1297 1304 usr = AttributeDict(usr.get_dict())
1298 1305 usr.admin_row = True
1299 1306 usr.permission = _admin_perm
1300 1307 super_admin_rows.append(usr)
1301 1308
1302 1309 return super_admin_rows + owner_row + perm_rows
1303 1310
1304 1311 def permission_user_groups(self):
1305 1312 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1306 1313 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1307 1314 joinedload(UserGroupUserGroupToPerm.target_user_group),
1308 1315 joinedload(UserGroupUserGroupToPerm.permission),)
1309 1316
1310 1317 perm_rows = []
1311 1318 for _user_group in q.all():
1312 1319 usr = AttributeDict(_user_group.user_group.get_dict())
1313 1320 usr.permission = _user_group.permission.permission_name
1314 1321 perm_rows.append(usr)
1315 1322
1316 1323 return perm_rows
1317 1324
1318 1325 def _get_default_perms(self, user_group, suffix=''):
1319 1326 from rhodecode.model.permission import PermissionModel
1320 1327 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1321 1328
1322 1329 def get_default_perms(self, suffix=''):
1323 1330 return self._get_default_perms(self, suffix)
1324 1331
1325 1332 def get_api_data(self, with_group_members=True, include_secrets=False):
1326 1333 """
1327 1334 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1328 1335 basically forwarded.
1329 1336
1330 1337 """
1331 1338 user_group = self
1332 1339 data = {
1333 1340 'users_group_id': user_group.users_group_id,
1334 1341 'group_name': user_group.users_group_name,
1335 1342 'group_description': user_group.user_group_description,
1336 1343 'active': user_group.users_group_active,
1337 1344 'owner': user_group.user.username,
1338 1345 'owner_email': user_group.user.email,
1339 1346 }
1340 1347
1341 1348 if with_group_members:
1342 1349 users = []
1343 1350 for user in user_group.members:
1344 1351 user = user.user
1345 1352 users.append(user.get_api_data(include_secrets=include_secrets))
1346 1353 data['users'] = users
1347 1354
1348 1355 return data
1349 1356
1350 1357
1351 1358 class UserGroupMember(Base, BaseModel):
1352 1359 __tablename__ = 'users_groups_members'
1353 1360 __table_args__ = (
1354 1361 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1355 1362 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1356 1363 )
1357 1364
1358 1365 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1359 1366 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1360 1367 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1361 1368
1362 1369 user = relationship('User', lazy='joined')
1363 1370 users_group = relationship('UserGroup')
1364 1371
1365 1372 def __init__(self, gr_id='', u_id=''):
1366 1373 self.users_group_id = gr_id
1367 1374 self.user_id = u_id
1368 1375
1369 1376
1370 1377 class RepositoryField(Base, BaseModel):
1371 1378 __tablename__ = 'repositories_fields'
1372 1379 __table_args__ = (
1373 1380 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1374 1381 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1375 1382 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1376 1383 )
1377 1384 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1378 1385
1379 1386 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1380 1387 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1381 1388 field_key = Column("field_key", String(250))
1382 1389 field_label = Column("field_label", String(1024), nullable=False)
1383 1390 field_value = Column("field_value", String(10000), nullable=False)
1384 1391 field_desc = Column("field_desc", String(1024), nullable=False)
1385 1392 field_type = Column("field_type", String(255), nullable=False, unique=None)
1386 1393 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1387 1394
1388 1395 repository = relationship('Repository')
1389 1396
1390 1397 @property
1391 1398 def field_key_prefixed(self):
1392 1399 return 'ex_%s' % self.field_key
1393 1400
1394 1401 @classmethod
1395 1402 def un_prefix_key(cls, key):
1396 1403 if key.startswith(cls.PREFIX):
1397 1404 return key[len(cls.PREFIX):]
1398 1405 return key
1399 1406
1400 1407 @classmethod
1401 1408 def get_by_key_name(cls, key, repo):
1402 1409 row = cls.query()\
1403 1410 .filter(cls.repository == repo)\
1404 1411 .filter(cls.field_key == key).scalar()
1405 1412 return row
1406 1413
1407 1414
1408 1415 class Repository(Base, BaseModel):
1409 1416 __tablename__ = 'repositories'
1410 1417 __table_args__ = (
1411 1418 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1412 1419 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1413 1420 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1414 1421 )
1415 1422 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1416 1423 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1417 1424
1418 1425 STATE_CREATED = 'repo_state_created'
1419 1426 STATE_PENDING = 'repo_state_pending'
1420 1427 STATE_ERROR = 'repo_state_error'
1421 1428
1422 1429 LOCK_AUTOMATIC = 'lock_auto'
1423 1430 LOCK_API = 'lock_api'
1424 1431 LOCK_WEB = 'lock_web'
1425 1432 LOCK_PULL = 'lock_pull'
1426 1433
1427 1434 NAME_SEP = URL_SEP
1428 1435
1429 1436 repo_id = Column(
1430 1437 "repo_id", Integer(), nullable=False, unique=True, default=None,
1431 1438 primary_key=True)
1432 1439 _repo_name = Column(
1433 1440 "repo_name", Text(), nullable=False, default=None)
1434 1441 _repo_name_hash = Column(
1435 1442 "repo_name_hash", String(255), nullable=False, unique=True)
1436 1443 repo_state = Column("repo_state", String(255), nullable=True)
1437 1444
1438 1445 clone_uri = Column(
1439 1446 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1440 1447 default=None)
1441 1448 repo_type = Column(
1442 1449 "repo_type", String(255), nullable=False, unique=False, default=None)
1443 1450 user_id = Column(
1444 1451 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1445 1452 unique=False, default=None)
1446 1453 private = Column(
1447 1454 "private", Boolean(), nullable=True, unique=None, default=None)
1448 1455 enable_statistics = Column(
1449 1456 "statistics", Boolean(), nullable=True, unique=None, default=True)
1450 1457 enable_downloads = Column(
1451 1458 "downloads", Boolean(), nullable=True, unique=None, default=True)
1452 1459 description = Column(
1453 1460 "description", String(10000), nullable=True, unique=None, default=None)
1454 1461 created_on = Column(
1455 1462 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1456 1463 default=datetime.datetime.now)
1457 1464 updated_on = Column(
1458 1465 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1459 1466 default=datetime.datetime.now)
1460 1467 _landing_revision = Column(
1461 1468 "landing_revision", String(255), nullable=False, unique=False,
1462 1469 default=None)
1463 1470 enable_locking = Column(
1464 1471 "enable_locking", Boolean(), nullable=False, unique=None,
1465 1472 default=False)
1466 1473 _locked = Column(
1467 1474 "locked", String(255), nullable=True, unique=False, default=None)
1468 1475 _changeset_cache = Column(
1469 1476 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1470 1477
1471 1478 fork_id = Column(
1472 1479 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1473 1480 nullable=True, unique=False, default=None)
1474 1481 group_id = Column(
1475 1482 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1476 1483 unique=False, default=None)
1477 1484
1478 1485 user = relationship('User', lazy='joined')
1479 1486 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1480 1487 group = relationship('RepoGroup', lazy='joined')
1481 1488 repo_to_perm = relationship(
1482 1489 'UserRepoToPerm', cascade='all',
1483 1490 order_by='UserRepoToPerm.repo_to_perm_id')
1484 1491 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1485 1492 stats = relationship('Statistics', cascade='all', uselist=False)
1486 1493
1487 1494 followers = relationship(
1488 1495 'UserFollowing',
1489 1496 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1490 1497 cascade='all')
1491 1498 extra_fields = relationship(
1492 1499 'RepositoryField', cascade="all, delete, delete-orphan")
1493 1500 logs = relationship('UserLog')
1494 1501 comments = relationship(
1495 1502 'ChangesetComment', cascade="all, delete, delete-orphan")
1496 1503 pull_requests_source = relationship(
1497 1504 'PullRequest',
1498 1505 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1499 1506 cascade="all, delete, delete-orphan")
1500 1507 pull_requests_target = relationship(
1501 1508 'PullRequest',
1502 1509 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1503 1510 cascade="all, delete, delete-orphan")
1504 1511 ui = relationship('RepoRhodeCodeUi', cascade="all")
1505 1512 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1506 1513 integrations = relationship('Integration',
1507 1514 cascade="all, delete, delete-orphan")
1508 1515
1509 1516 def __unicode__(self):
1510 1517 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1511 1518 safe_unicode(self.repo_name))
1512 1519
1513 1520 @hybrid_property
1514 1521 def description_safe(self):
1515 1522 from rhodecode.lib import helpers as h
1516 1523 return h.escape(self.description)
1517 1524
1518 1525 @hybrid_property
1519 1526 def landing_rev(self):
1520 1527 # always should return [rev_type, rev]
1521 1528 if self._landing_revision:
1522 1529 _rev_info = self._landing_revision.split(':')
1523 1530 if len(_rev_info) < 2:
1524 1531 _rev_info.insert(0, 'rev')
1525 1532 return [_rev_info[0], _rev_info[1]]
1526 1533 return [None, None]
1527 1534
1528 1535 @landing_rev.setter
1529 1536 def landing_rev(self, val):
1530 1537 if ':' not in val:
1531 1538 raise ValueError('value must be delimited with `:` and consist '
1532 1539 'of <rev_type>:<rev>, got %s instead' % val)
1533 1540 self._landing_revision = val
1534 1541
1535 1542 @hybrid_property
1536 1543 def locked(self):
1537 1544 if self._locked:
1538 1545 user_id, timelocked, reason = self._locked.split(':')
1539 1546 lock_values = int(user_id), timelocked, reason
1540 1547 else:
1541 1548 lock_values = [None, None, None]
1542 1549 return lock_values
1543 1550
1544 1551 @locked.setter
1545 1552 def locked(self, val):
1546 1553 if val and isinstance(val, (list, tuple)):
1547 1554 self._locked = ':'.join(map(str, val))
1548 1555 else:
1549 1556 self._locked = None
1550 1557
1551 1558 @hybrid_property
1552 1559 def changeset_cache(self):
1553 1560 from rhodecode.lib.vcs.backends.base import EmptyCommit
1554 1561 dummy = EmptyCommit().__json__()
1555 1562 if not self._changeset_cache:
1556 1563 return dummy
1557 1564 try:
1558 1565 return json.loads(self._changeset_cache)
1559 1566 except TypeError:
1560 1567 return dummy
1561 1568 except Exception:
1562 1569 log.error(traceback.format_exc())
1563 1570 return dummy
1564 1571
1565 1572 @changeset_cache.setter
1566 1573 def changeset_cache(self, val):
1567 1574 try:
1568 1575 self._changeset_cache = json.dumps(val)
1569 1576 except Exception:
1570 1577 log.error(traceback.format_exc())
1571 1578
1572 1579 @hybrid_property
1573 1580 def repo_name(self):
1574 1581 return self._repo_name
1575 1582
1576 1583 @repo_name.setter
1577 1584 def repo_name(self, value):
1578 1585 self._repo_name = value
1579 1586 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1580 1587
1581 1588 @classmethod
1582 1589 def normalize_repo_name(cls, repo_name):
1583 1590 """
1584 1591 Normalizes os specific repo_name to the format internally stored inside
1585 1592 database using URL_SEP
1586 1593
1587 1594 :param cls:
1588 1595 :param repo_name:
1589 1596 """
1590 1597 return cls.NAME_SEP.join(repo_name.split(os.sep))
1591 1598
1592 1599 @classmethod
1593 1600 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1594 1601 session = Session()
1595 1602 q = session.query(cls).filter(cls.repo_name == repo_name)
1596 1603
1597 1604 if cache:
1598 1605 if identity_cache:
1599 1606 val = cls.identity_cache(session, 'repo_name', repo_name)
1600 1607 if val:
1601 1608 return val
1602 1609 else:
1603 1610 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1604 1611 q = q.options(
1605 1612 FromCache("sql_cache_short", cache_key))
1606 1613
1607 1614 return q.scalar()
1608 1615
1609 1616 @classmethod
1610 1617 def get_by_full_path(cls, repo_full_path):
1611 1618 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1612 1619 repo_name = cls.normalize_repo_name(repo_name)
1613 1620 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1614 1621
1615 1622 @classmethod
1616 1623 def get_repo_forks(cls, repo_id):
1617 1624 return cls.query().filter(Repository.fork_id == repo_id)
1618 1625
1619 1626 @classmethod
1620 1627 def base_path(cls):
1621 1628 """
1622 1629 Returns base path when all repos are stored
1623 1630
1624 1631 :param cls:
1625 1632 """
1626 1633 q = Session().query(RhodeCodeUi)\
1627 1634 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1628 1635 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1629 1636 return q.one().ui_value
1630 1637
1631 1638 @classmethod
1632 1639 def is_valid(cls, repo_name):
1633 1640 """
1634 1641 returns True if given repo name is a valid filesystem repository
1635 1642
1636 1643 :param cls:
1637 1644 :param repo_name:
1638 1645 """
1639 1646 from rhodecode.lib.utils import is_valid_repo
1640 1647
1641 1648 return is_valid_repo(repo_name, cls.base_path())
1642 1649
1643 1650 @classmethod
1644 1651 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1645 1652 case_insensitive=True):
1646 1653 q = Repository.query()
1647 1654
1648 1655 if not isinstance(user_id, Optional):
1649 1656 q = q.filter(Repository.user_id == user_id)
1650 1657
1651 1658 if not isinstance(group_id, Optional):
1652 1659 q = q.filter(Repository.group_id == group_id)
1653 1660
1654 1661 if case_insensitive:
1655 1662 q = q.order_by(func.lower(Repository.repo_name))
1656 1663 else:
1657 1664 q = q.order_by(Repository.repo_name)
1658 1665 return q.all()
1659 1666
1660 1667 @property
1661 1668 def forks(self):
1662 1669 """
1663 1670 Return forks of this repo
1664 1671 """
1665 1672 return Repository.get_repo_forks(self.repo_id)
1666 1673
1667 1674 @property
1668 1675 def parent(self):
1669 1676 """
1670 1677 Returns fork parent
1671 1678 """
1672 1679 return self.fork
1673 1680
1674 1681 @property
1675 1682 def just_name(self):
1676 1683 return self.repo_name.split(self.NAME_SEP)[-1]
1677 1684
1678 1685 @property
1679 1686 def groups_with_parents(self):
1680 1687 groups = []
1681 1688 if self.group is None:
1682 1689 return groups
1683 1690
1684 1691 cur_gr = self.group
1685 1692 groups.insert(0, cur_gr)
1686 1693 while 1:
1687 1694 gr = getattr(cur_gr, 'parent_group', None)
1688 1695 cur_gr = cur_gr.parent_group
1689 1696 if gr is None:
1690 1697 break
1691 1698 groups.insert(0, gr)
1692 1699
1693 1700 return groups
1694 1701
1695 1702 @property
1696 1703 def groups_and_repo(self):
1697 1704 return self.groups_with_parents, self
1698 1705
1699 1706 @LazyProperty
1700 1707 def repo_path(self):
1701 1708 """
1702 1709 Returns base full path for that repository means where it actually
1703 1710 exists on a filesystem
1704 1711 """
1705 1712 q = Session().query(RhodeCodeUi).filter(
1706 1713 RhodeCodeUi.ui_key == self.NAME_SEP)
1707 1714 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1708 1715 return q.one().ui_value
1709 1716
1710 1717 @property
1711 1718 def repo_full_path(self):
1712 1719 p = [self.repo_path]
1713 1720 # we need to split the name by / since this is how we store the
1714 1721 # names in the database, but that eventually needs to be converted
1715 1722 # into a valid system path
1716 1723 p += self.repo_name.split(self.NAME_SEP)
1717 1724 return os.path.join(*map(safe_unicode, p))
1718 1725
1719 1726 @property
1720 1727 def cache_keys(self):
1721 1728 """
1722 1729 Returns associated cache keys for that repo
1723 1730 """
1724 1731 return CacheKey.query()\
1725 1732 .filter(CacheKey.cache_args == self.repo_name)\
1726 1733 .order_by(CacheKey.cache_key)\
1727 1734 .all()
1728 1735
1729 1736 def get_new_name(self, repo_name):
1730 1737 """
1731 1738 returns new full repository name based on assigned group and new new
1732 1739
1733 1740 :param group_name:
1734 1741 """
1735 1742 path_prefix = self.group.full_path_splitted if self.group else []
1736 1743 return self.NAME_SEP.join(path_prefix + [repo_name])
1737 1744
1738 1745 @property
1739 1746 def _config(self):
1740 1747 """
1741 1748 Returns db based config object.
1742 1749 """
1743 1750 from rhodecode.lib.utils import make_db_config
1744 1751 return make_db_config(clear_session=False, repo=self)
1745 1752
1746 1753 def permissions(self, with_admins=True, with_owner=True):
1747 1754 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1748 1755 q = q.options(joinedload(UserRepoToPerm.repository),
1749 1756 joinedload(UserRepoToPerm.user),
1750 1757 joinedload(UserRepoToPerm.permission),)
1751 1758
1752 1759 # get owners and admins and permissions. We do a trick of re-writing
1753 1760 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1754 1761 # has a global reference and changing one object propagates to all
1755 1762 # others. This means if admin is also an owner admin_row that change
1756 1763 # would propagate to both objects
1757 1764 perm_rows = []
1758 1765 for _usr in q.all():
1759 1766 usr = AttributeDict(_usr.user.get_dict())
1760 1767 usr.permission = _usr.permission.permission_name
1761 1768 perm_rows.append(usr)
1762 1769
1763 1770 # filter the perm rows by 'default' first and then sort them by
1764 1771 # admin,write,read,none permissions sorted again alphabetically in
1765 1772 # each group
1766 1773 perm_rows = sorted(perm_rows, key=display_sort)
1767 1774
1768 1775 _admin_perm = 'repository.admin'
1769 1776 owner_row = []
1770 1777 if with_owner:
1771 1778 usr = AttributeDict(self.user.get_dict())
1772 1779 usr.owner_row = True
1773 1780 usr.permission = _admin_perm
1774 1781 owner_row.append(usr)
1775 1782
1776 1783 super_admin_rows = []
1777 1784 if with_admins:
1778 1785 for usr in User.get_all_super_admins():
1779 1786 # if this admin is also owner, don't double the record
1780 1787 if usr.user_id == owner_row[0].user_id:
1781 1788 owner_row[0].admin_row = True
1782 1789 else:
1783 1790 usr = AttributeDict(usr.get_dict())
1784 1791 usr.admin_row = True
1785 1792 usr.permission = _admin_perm
1786 1793 super_admin_rows.append(usr)
1787 1794
1788 1795 return super_admin_rows + owner_row + perm_rows
1789 1796
1790 1797 def permission_user_groups(self):
1791 1798 q = UserGroupRepoToPerm.query().filter(
1792 1799 UserGroupRepoToPerm.repository == self)
1793 1800 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1794 1801 joinedload(UserGroupRepoToPerm.users_group),
1795 1802 joinedload(UserGroupRepoToPerm.permission),)
1796 1803
1797 1804 perm_rows = []
1798 1805 for _user_group in q.all():
1799 1806 usr = AttributeDict(_user_group.users_group.get_dict())
1800 1807 usr.permission = _user_group.permission.permission_name
1801 1808 perm_rows.append(usr)
1802 1809
1803 1810 return perm_rows
1804 1811
1805 1812 def get_api_data(self, include_secrets=False):
1806 1813 """
1807 1814 Common function for generating repo api data
1808 1815
1809 1816 :param include_secrets: See :meth:`User.get_api_data`.
1810 1817
1811 1818 """
1812 1819 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1813 1820 # move this methods on models level.
1814 1821 from rhodecode.model.settings import SettingsModel
1815 1822 from rhodecode.model.repo import RepoModel
1816 1823
1817 1824 repo = self
1818 1825 _user_id, _time, _reason = self.locked
1819 1826
1820 1827 data = {
1821 1828 'repo_id': repo.repo_id,
1822 1829 'repo_name': repo.repo_name,
1823 1830 'repo_type': repo.repo_type,
1824 1831 'clone_uri': repo.clone_uri or '',
1825 1832 'url': RepoModel().get_url(self),
1826 1833 'private': repo.private,
1827 1834 'created_on': repo.created_on,
1828 1835 'description': repo.description_safe,
1829 1836 'landing_rev': repo.landing_rev,
1830 1837 'owner': repo.user.username,
1831 1838 'fork_of': repo.fork.repo_name if repo.fork else None,
1832 1839 'fork_of_id': repo.fork.repo_id if repo.fork else None,
1833 1840 'enable_statistics': repo.enable_statistics,
1834 1841 'enable_locking': repo.enable_locking,
1835 1842 'enable_downloads': repo.enable_downloads,
1836 1843 'last_changeset': repo.changeset_cache,
1837 1844 'locked_by': User.get(_user_id).get_api_data(
1838 1845 include_secrets=include_secrets) if _user_id else None,
1839 1846 'locked_date': time_to_datetime(_time) if _time else None,
1840 1847 'lock_reason': _reason if _reason else None,
1841 1848 }
1842 1849
1843 1850 # TODO: mikhail: should be per-repo settings here
1844 1851 rc_config = SettingsModel().get_all_settings()
1845 1852 repository_fields = str2bool(
1846 1853 rc_config.get('rhodecode_repository_fields'))
1847 1854 if repository_fields:
1848 1855 for f in self.extra_fields:
1849 1856 data[f.field_key_prefixed] = f.field_value
1850 1857
1851 1858 return data
1852 1859
1853 1860 @classmethod
1854 1861 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1855 1862 if not lock_time:
1856 1863 lock_time = time.time()
1857 1864 if not lock_reason:
1858 1865 lock_reason = cls.LOCK_AUTOMATIC
1859 1866 repo.locked = [user_id, lock_time, lock_reason]
1860 1867 Session().add(repo)
1861 1868 Session().commit()
1862 1869
1863 1870 @classmethod
1864 1871 def unlock(cls, repo):
1865 1872 repo.locked = None
1866 1873 Session().add(repo)
1867 1874 Session().commit()
1868 1875
1869 1876 @classmethod
1870 1877 def getlock(cls, repo):
1871 1878 return repo.locked
1872 1879
1873 1880 def is_user_lock(self, user_id):
1874 1881 if self.lock[0]:
1875 1882 lock_user_id = safe_int(self.lock[0])
1876 1883 user_id = safe_int(user_id)
1877 1884 # both are ints, and they are equal
1878 1885 return all([lock_user_id, user_id]) and lock_user_id == user_id
1879 1886
1880 1887 return False
1881 1888
1882 1889 def get_locking_state(self, action, user_id, only_when_enabled=True):
1883 1890 """
1884 1891 Checks locking on this repository, if locking is enabled and lock is
1885 1892 present returns a tuple of make_lock, locked, locked_by.
1886 1893 make_lock can have 3 states None (do nothing) True, make lock
1887 1894 False release lock, This value is later propagated to hooks, which
1888 1895 do the locking. Think about this as signals passed to hooks what to do.
1889 1896
1890 1897 """
1891 1898 # TODO: johbo: This is part of the business logic and should be moved
1892 1899 # into the RepositoryModel.
1893 1900
1894 1901 if action not in ('push', 'pull'):
1895 1902 raise ValueError("Invalid action value: %s" % repr(action))
1896 1903
1897 1904 # defines if locked error should be thrown to user
1898 1905 currently_locked = False
1899 1906 # defines if new lock should be made, tri-state
1900 1907 make_lock = None
1901 1908 repo = self
1902 1909 user = User.get(user_id)
1903 1910
1904 1911 lock_info = repo.locked
1905 1912
1906 1913 if repo and (repo.enable_locking or not only_when_enabled):
1907 1914 if action == 'push':
1908 1915 # check if it's already locked !, if it is compare users
1909 1916 locked_by_user_id = lock_info[0]
1910 1917 if user.user_id == locked_by_user_id:
1911 1918 log.debug(
1912 1919 'Got `push` action from user %s, now unlocking', user)
1913 1920 # unlock if we have push from user who locked
1914 1921 make_lock = False
1915 1922 else:
1916 1923 # we're not the same user who locked, ban with
1917 1924 # code defined in settings (default is 423 HTTP Locked) !
1918 1925 log.debug('Repo %s is currently locked by %s', repo, user)
1919 1926 currently_locked = True
1920 1927 elif action == 'pull':
1921 1928 # [0] user [1] date
1922 1929 if lock_info[0] and lock_info[1]:
1923 1930 log.debug('Repo %s is currently locked by %s', repo, user)
1924 1931 currently_locked = True
1925 1932 else:
1926 1933 log.debug('Setting lock on repo %s by %s', repo, user)
1927 1934 make_lock = True
1928 1935
1929 1936 else:
1930 1937 log.debug('Repository %s do not have locking enabled', repo)
1931 1938
1932 1939 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1933 1940 make_lock, currently_locked, lock_info)
1934 1941
1935 1942 from rhodecode.lib.auth import HasRepoPermissionAny
1936 1943 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1937 1944 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1938 1945 # if we don't have at least write permission we cannot make a lock
1939 1946 log.debug('lock state reset back to FALSE due to lack '
1940 1947 'of at least read permission')
1941 1948 make_lock = False
1942 1949
1943 1950 return make_lock, currently_locked, lock_info
1944 1951
1945 1952 @property
1946 1953 def last_db_change(self):
1947 1954 return self.updated_on
1948 1955
1949 1956 @property
1950 1957 def clone_uri_hidden(self):
1951 1958 clone_uri = self.clone_uri
1952 1959 if clone_uri:
1953 1960 import urlobject
1954 1961 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
1955 1962 if url_obj.password:
1956 1963 clone_uri = url_obj.with_password('*****')
1957 1964 return clone_uri
1958 1965
1959 1966 def clone_url(self, **override):
1960 1967 from rhodecode.model.settings import SettingsModel
1961 1968
1962 1969 uri_tmpl = None
1963 1970 if 'with_id' in override:
1964 1971 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1965 1972 del override['with_id']
1966 1973
1967 1974 if 'uri_tmpl' in override:
1968 1975 uri_tmpl = override['uri_tmpl']
1969 1976 del override['uri_tmpl']
1970 1977
1971 1978 # we didn't override our tmpl from **overrides
1972 1979 if not uri_tmpl:
1973 1980 rc_config = SettingsModel().get_all_settings(cache=True)
1974 1981 uri_tmpl = rc_config.get(
1975 1982 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
1976 1983
1977 1984 request = get_current_request()
1978 1985 return get_clone_url(request=request,
1979 1986 uri_tmpl=uri_tmpl,
1980 1987 repo_name=self.repo_name,
1981 1988 repo_id=self.repo_id, **override)
1982 1989
1983 1990 def set_state(self, state):
1984 1991 self.repo_state = state
1985 1992 Session().add(self)
1986 1993 #==========================================================================
1987 1994 # SCM PROPERTIES
1988 1995 #==========================================================================
1989 1996
1990 1997 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1991 1998 return get_commit_safe(
1992 1999 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1993 2000
1994 2001 def get_changeset(self, rev=None, pre_load=None):
1995 2002 warnings.warn("Use get_commit", DeprecationWarning)
1996 2003 commit_id = None
1997 2004 commit_idx = None
1998 2005 if isinstance(rev, basestring):
1999 2006 commit_id = rev
2000 2007 else:
2001 2008 commit_idx = rev
2002 2009 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2003 2010 pre_load=pre_load)
2004 2011
2005 2012 def get_landing_commit(self):
2006 2013 """
2007 2014 Returns landing commit, or if that doesn't exist returns the tip
2008 2015 """
2009 2016 _rev_type, _rev = self.landing_rev
2010 2017 commit = self.get_commit(_rev)
2011 2018 if isinstance(commit, EmptyCommit):
2012 2019 return self.get_commit()
2013 2020 return commit
2014 2021
2015 2022 def update_commit_cache(self, cs_cache=None, config=None):
2016 2023 """
2017 2024 Update cache of last changeset for repository, keys should be::
2018 2025
2019 2026 short_id
2020 2027 raw_id
2021 2028 revision
2022 2029 parents
2023 2030 message
2024 2031 date
2025 2032 author
2026 2033
2027 2034 :param cs_cache:
2028 2035 """
2029 2036 from rhodecode.lib.vcs.backends.base import BaseChangeset
2030 2037 if cs_cache is None:
2031 2038 # use no-cache version here
2032 2039 scm_repo = self.scm_instance(cache=False, config=config)
2033 2040 if scm_repo:
2034 2041 cs_cache = scm_repo.get_commit(
2035 2042 pre_load=["author", "date", "message", "parents"])
2036 2043 else:
2037 2044 cs_cache = EmptyCommit()
2038 2045
2039 2046 if isinstance(cs_cache, BaseChangeset):
2040 2047 cs_cache = cs_cache.__json__()
2041 2048
2042 2049 def is_outdated(new_cs_cache):
2043 2050 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2044 2051 new_cs_cache['revision'] != self.changeset_cache['revision']):
2045 2052 return True
2046 2053 return False
2047 2054
2048 2055 # check if we have maybe already latest cached revision
2049 2056 if is_outdated(cs_cache) or not self.changeset_cache:
2050 2057 _default = datetime.datetime.fromtimestamp(0)
2051 2058 last_change = cs_cache.get('date') or _default
2052 2059 log.debug('updated repo %s with new cs cache %s',
2053 2060 self.repo_name, cs_cache)
2054 2061 self.updated_on = last_change
2055 2062 self.changeset_cache = cs_cache
2056 2063 Session().add(self)
2057 2064 Session().commit()
2058 2065 else:
2059 2066 log.debug('Skipping update_commit_cache for repo:`%s` '
2060 2067 'commit already with latest changes', self.repo_name)
2061 2068
2062 2069 @property
2063 2070 def tip(self):
2064 2071 return self.get_commit('tip')
2065 2072
2066 2073 @property
2067 2074 def author(self):
2068 2075 return self.tip.author
2069 2076
2070 2077 @property
2071 2078 def last_change(self):
2072 2079 return self.scm_instance().last_change
2073 2080
2074 2081 def get_comments(self, revisions=None):
2075 2082 """
2076 2083 Returns comments for this repository grouped by revisions
2077 2084
2078 2085 :param revisions: filter query by revisions only
2079 2086 """
2080 2087 cmts = ChangesetComment.query()\
2081 2088 .filter(ChangesetComment.repo == self)
2082 2089 if revisions:
2083 2090 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2084 2091 grouped = collections.defaultdict(list)
2085 2092 for cmt in cmts.all():
2086 2093 grouped[cmt.revision].append(cmt)
2087 2094 return grouped
2088 2095
2089 2096 def statuses(self, revisions=None):
2090 2097 """
2091 2098 Returns statuses for this repository
2092 2099
2093 2100 :param revisions: list of revisions to get statuses for
2094 2101 """
2095 2102 statuses = ChangesetStatus.query()\
2096 2103 .filter(ChangesetStatus.repo == self)\
2097 2104 .filter(ChangesetStatus.version == 0)
2098 2105
2099 2106 if revisions:
2100 2107 # Try doing the filtering in chunks to avoid hitting limits
2101 2108 size = 500
2102 2109 status_results = []
2103 2110 for chunk in xrange(0, len(revisions), size):
2104 2111 status_results += statuses.filter(
2105 2112 ChangesetStatus.revision.in_(
2106 2113 revisions[chunk: chunk+size])
2107 2114 ).all()
2108 2115 else:
2109 2116 status_results = statuses.all()
2110 2117
2111 2118 grouped = {}
2112 2119
2113 2120 # maybe we have open new pullrequest without a status?
2114 2121 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2115 2122 status_lbl = ChangesetStatus.get_status_lbl(stat)
2116 2123 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2117 2124 for rev in pr.revisions:
2118 2125 pr_id = pr.pull_request_id
2119 2126 pr_repo = pr.target_repo.repo_name
2120 2127 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2121 2128
2122 2129 for stat in status_results:
2123 2130 pr_id = pr_repo = None
2124 2131 if stat.pull_request:
2125 2132 pr_id = stat.pull_request.pull_request_id
2126 2133 pr_repo = stat.pull_request.target_repo.repo_name
2127 2134 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2128 2135 pr_id, pr_repo]
2129 2136 return grouped
2130 2137
2131 2138 # ==========================================================================
2132 2139 # SCM CACHE INSTANCE
2133 2140 # ==========================================================================
2134 2141
2135 2142 def scm_instance(self, **kwargs):
2136 2143 import rhodecode
2137 2144
2138 2145 # Passing a config will not hit the cache currently only used
2139 2146 # for repo2dbmapper
2140 2147 config = kwargs.pop('config', None)
2141 2148 cache = kwargs.pop('cache', None)
2142 2149 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2143 2150 # if cache is NOT defined use default global, else we have a full
2144 2151 # control over cache behaviour
2145 2152 if cache is None and full_cache and not config:
2146 2153 return self._get_instance_cached()
2147 2154 return self._get_instance(cache=bool(cache), config=config)
2148 2155
2149 2156 def _get_instance_cached(self):
2150 2157 @cache_region('long_term')
2151 2158 def _get_repo(cache_key):
2152 2159 return self._get_instance()
2153 2160
2154 2161 invalidator_context = CacheKey.repo_context_cache(
2155 2162 _get_repo, self.repo_name, None, thread_scoped=True)
2156 2163
2157 2164 with invalidator_context as context:
2158 2165 context.invalidate()
2159 2166 repo = context.compute()
2160 2167
2161 2168 return repo
2162 2169
2163 2170 def _get_instance(self, cache=True, config=None):
2164 2171 config = config or self._config
2165 2172 custom_wire = {
2166 2173 'cache': cache # controls the vcs.remote cache
2167 2174 }
2168 2175 repo = get_vcs_instance(
2169 2176 repo_path=safe_str(self.repo_full_path),
2170 2177 config=config,
2171 2178 with_wire=custom_wire,
2172 2179 create=False,
2173 2180 _vcs_alias=self.repo_type)
2174 2181
2175 2182 return repo
2176 2183
2177 2184 def __json__(self):
2178 2185 return {'landing_rev': self.landing_rev}
2179 2186
2180 2187 def get_dict(self):
2181 2188
2182 2189 # Since we transformed `repo_name` to a hybrid property, we need to
2183 2190 # keep compatibility with the code which uses `repo_name` field.
2184 2191
2185 2192 result = super(Repository, self).get_dict()
2186 2193 result['repo_name'] = result.pop('_repo_name', None)
2187 2194 return result
2188 2195
2189 2196
2190 2197 class RepoGroup(Base, BaseModel):
2191 2198 __tablename__ = 'groups'
2192 2199 __table_args__ = (
2193 2200 UniqueConstraint('group_name', 'group_parent_id'),
2194 2201 CheckConstraint('group_id != group_parent_id'),
2195 2202 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2196 2203 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2197 2204 )
2198 2205 __mapper_args__ = {'order_by': 'group_name'}
2199 2206
2200 2207 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2201 2208
2202 2209 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2203 2210 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2204 2211 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2205 2212 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2206 2213 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2207 2214 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2208 2215 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2209 2216 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2210 2217
2211 2218 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2212 2219 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2213 2220 parent_group = relationship('RepoGroup', remote_side=group_id)
2214 2221 user = relationship('User')
2215 2222 integrations = relationship('Integration',
2216 2223 cascade="all, delete, delete-orphan")
2217 2224
2218 2225 def __init__(self, group_name='', parent_group=None):
2219 2226 self.group_name = group_name
2220 2227 self.parent_group = parent_group
2221 2228
2222 2229 def __unicode__(self):
2223 2230 return u"<%s('id:%s:%s')>" % (
2224 2231 self.__class__.__name__, self.group_id, self.group_name)
2225 2232
2226 2233 @hybrid_property
2227 2234 def description_safe(self):
2228 2235 from rhodecode.lib import helpers as h
2229 2236 return h.escape(self.group_description)
2230 2237
2231 2238 @classmethod
2232 2239 def _generate_choice(cls, repo_group):
2233 2240 from webhelpers.html import literal as _literal
2234 2241 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2235 2242 return repo_group.group_id, _name(repo_group.full_path_splitted)
2236 2243
2237 2244 @classmethod
2238 2245 def groups_choices(cls, groups=None, show_empty_group=True):
2239 2246 if not groups:
2240 2247 groups = cls.query().all()
2241 2248
2242 2249 repo_groups = []
2243 2250 if show_empty_group:
2244 2251 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2245 2252
2246 2253 repo_groups.extend([cls._generate_choice(x) for x in groups])
2247 2254
2248 2255 repo_groups = sorted(
2249 2256 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2250 2257 return repo_groups
2251 2258
2252 2259 @classmethod
2253 2260 def url_sep(cls):
2254 2261 return URL_SEP
2255 2262
2256 2263 @classmethod
2257 2264 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2258 2265 if case_insensitive:
2259 2266 gr = cls.query().filter(func.lower(cls.group_name)
2260 2267 == func.lower(group_name))
2261 2268 else:
2262 2269 gr = cls.query().filter(cls.group_name == group_name)
2263 2270 if cache:
2264 2271 name_key = _hash_key(group_name)
2265 2272 gr = gr.options(
2266 2273 FromCache("sql_cache_short", "get_group_%s" % name_key))
2267 2274 return gr.scalar()
2268 2275
2269 2276 @classmethod
2270 2277 def get_user_personal_repo_group(cls, user_id):
2271 2278 user = User.get(user_id)
2272 2279 if user.username == User.DEFAULT_USER:
2273 2280 return None
2274 2281
2275 2282 return cls.query()\
2276 2283 .filter(cls.personal == true()) \
2277 2284 .filter(cls.user == user).scalar()
2278 2285
2279 2286 @classmethod
2280 2287 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2281 2288 case_insensitive=True):
2282 2289 q = RepoGroup.query()
2283 2290
2284 2291 if not isinstance(user_id, Optional):
2285 2292 q = q.filter(RepoGroup.user_id == user_id)
2286 2293
2287 2294 if not isinstance(group_id, Optional):
2288 2295 q = q.filter(RepoGroup.group_parent_id == group_id)
2289 2296
2290 2297 if case_insensitive:
2291 2298 q = q.order_by(func.lower(RepoGroup.group_name))
2292 2299 else:
2293 2300 q = q.order_by(RepoGroup.group_name)
2294 2301 return q.all()
2295 2302
2296 2303 @property
2297 2304 def parents(self):
2298 2305 parents_recursion_limit = 10
2299 2306 groups = []
2300 2307 if self.parent_group is None:
2301 2308 return groups
2302 2309 cur_gr = self.parent_group
2303 2310 groups.insert(0, cur_gr)
2304 2311 cnt = 0
2305 2312 while 1:
2306 2313 cnt += 1
2307 2314 gr = getattr(cur_gr, 'parent_group', None)
2308 2315 cur_gr = cur_gr.parent_group
2309 2316 if gr is None:
2310 2317 break
2311 2318 if cnt == parents_recursion_limit:
2312 2319 # this will prevent accidental infinit loops
2313 2320 log.error(('more than %s parents found for group %s, stopping '
2314 2321 'recursive parent fetching' % (parents_recursion_limit, self)))
2315 2322 break
2316 2323
2317 2324 groups.insert(0, gr)
2318 2325 return groups
2319 2326
2320 2327 @property
2321 2328 def children(self):
2322 2329 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2323 2330
2324 2331 @property
2325 2332 def name(self):
2326 2333 return self.group_name.split(RepoGroup.url_sep())[-1]
2327 2334
2328 2335 @property
2329 2336 def full_path(self):
2330 2337 return self.group_name
2331 2338
2332 2339 @property
2333 2340 def full_path_splitted(self):
2334 2341 return self.group_name.split(RepoGroup.url_sep())
2335 2342
2336 2343 @property
2337 2344 def repositories(self):
2338 2345 return Repository.query()\
2339 2346 .filter(Repository.group == self)\
2340 2347 .order_by(Repository.repo_name)
2341 2348
2342 2349 @property
2343 2350 def repositories_recursive_count(self):
2344 2351 cnt = self.repositories.count()
2345 2352
2346 2353 def children_count(group):
2347 2354 cnt = 0
2348 2355 for child in group.children:
2349 2356 cnt += child.repositories.count()
2350 2357 cnt += children_count(child)
2351 2358 return cnt
2352 2359
2353 2360 return cnt + children_count(self)
2354 2361
2355 2362 def _recursive_objects(self, include_repos=True):
2356 2363 all_ = []
2357 2364
2358 2365 def _get_members(root_gr):
2359 2366 if include_repos:
2360 2367 for r in root_gr.repositories:
2361 2368 all_.append(r)
2362 2369 childs = root_gr.children.all()
2363 2370 if childs:
2364 2371 for gr in childs:
2365 2372 all_.append(gr)
2366 2373 _get_members(gr)
2367 2374
2368 2375 _get_members(self)
2369 2376 return [self] + all_
2370 2377
2371 2378 def recursive_groups_and_repos(self):
2372 2379 """
2373 2380 Recursive return all groups, with repositories in those groups
2374 2381 """
2375 2382 return self._recursive_objects()
2376 2383
2377 2384 def recursive_groups(self):
2378 2385 """
2379 2386 Returns all children groups for this group including children of children
2380 2387 """
2381 2388 return self._recursive_objects(include_repos=False)
2382 2389
2383 2390 def get_new_name(self, group_name):
2384 2391 """
2385 2392 returns new full group name based on parent and new name
2386 2393
2387 2394 :param group_name:
2388 2395 """
2389 2396 path_prefix = (self.parent_group.full_path_splitted if
2390 2397 self.parent_group else [])
2391 2398 return RepoGroup.url_sep().join(path_prefix + [group_name])
2392 2399
2393 2400 def permissions(self, with_admins=True, with_owner=True):
2394 2401 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2395 2402 q = q.options(joinedload(UserRepoGroupToPerm.group),
2396 2403 joinedload(UserRepoGroupToPerm.user),
2397 2404 joinedload(UserRepoGroupToPerm.permission),)
2398 2405
2399 2406 # get owners and admins and permissions. We do a trick of re-writing
2400 2407 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2401 2408 # has a global reference and changing one object propagates to all
2402 2409 # others. This means if admin is also an owner admin_row that change
2403 2410 # would propagate to both objects
2404 2411 perm_rows = []
2405 2412 for _usr in q.all():
2406 2413 usr = AttributeDict(_usr.user.get_dict())
2407 2414 usr.permission = _usr.permission.permission_name
2408 2415 perm_rows.append(usr)
2409 2416
2410 2417 # filter the perm rows by 'default' first and then sort them by
2411 2418 # admin,write,read,none permissions sorted again alphabetically in
2412 2419 # each group
2413 2420 perm_rows = sorted(perm_rows, key=display_sort)
2414 2421
2415 2422 _admin_perm = 'group.admin'
2416 2423 owner_row = []
2417 2424 if with_owner:
2418 2425 usr = AttributeDict(self.user.get_dict())
2419 2426 usr.owner_row = True
2420 2427 usr.permission = _admin_perm
2421 2428 owner_row.append(usr)
2422 2429
2423 2430 super_admin_rows = []
2424 2431 if with_admins:
2425 2432 for usr in User.get_all_super_admins():
2426 2433 # if this admin is also owner, don't double the record
2427 2434 if usr.user_id == owner_row[0].user_id:
2428 2435 owner_row[0].admin_row = True
2429 2436 else:
2430 2437 usr = AttributeDict(usr.get_dict())
2431 2438 usr.admin_row = True
2432 2439 usr.permission = _admin_perm
2433 2440 super_admin_rows.append(usr)
2434 2441
2435 2442 return super_admin_rows + owner_row + perm_rows
2436 2443
2437 2444 def permission_user_groups(self):
2438 2445 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2439 2446 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2440 2447 joinedload(UserGroupRepoGroupToPerm.users_group),
2441 2448 joinedload(UserGroupRepoGroupToPerm.permission),)
2442 2449
2443 2450 perm_rows = []
2444 2451 for _user_group in q.all():
2445 2452 usr = AttributeDict(_user_group.users_group.get_dict())
2446 2453 usr.permission = _user_group.permission.permission_name
2447 2454 perm_rows.append(usr)
2448 2455
2449 2456 return perm_rows
2450 2457
2451 2458 def get_api_data(self):
2452 2459 """
2453 2460 Common function for generating api data
2454 2461
2455 2462 """
2456 2463 group = self
2457 2464 data = {
2458 2465 'group_id': group.group_id,
2459 2466 'group_name': group.group_name,
2460 2467 'group_description': group.description_safe,
2461 2468 'parent_group': group.parent_group.group_name if group.parent_group else None,
2462 2469 'repositories': [x.repo_name for x in group.repositories],
2463 2470 'owner': group.user.username,
2464 2471 }
2465 2472 return data
2466 2473
2467 2474
2468 2475 class Permission(Base, BaseModel):
2469 2476 __tablename__ = 'permissions'
2470 2477 __table_args__ = (
2471 2478 Index('p_perm_name_idx', 'permission_name'),
2472 2479 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2473 2480 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2474 2481 )
2475 2482 PERMS = [
2476 2483 ('hg.admin', _('RhodeCode Super Administrator')),
2477 2484
2478 2485 ('repository.none', _('Repository no access')),
2479 2486 ('repository.read', _('Repository read access')),
2480 2487 ('repository.write', _('Repository write access')),
2481 2488 ('repository.admin', _('Repository admin access')),
2482 2489
2483 2490 ('group.none', _('Repository group no access')),
2484 2491 ('group.read', _('Repository group read access')),
2485 2492 ('group.write', _('Repository group write access')),
2486 2493 ('group.admin', _('Repository group admin access')),
2487 2494
2488 2495 ('usergroup.none', _('User group no access')),
2489 2496 ('usergroup.read', _('User group read access')),
2490 2497 ('usergroup.write', _('User group write access')),
2491 2498 ('usergroup.admin', _('User group admin access')),
2492 2499
2493 2500 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2494 2501 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2495 2502
2496 2503 ('hg.usergroup.create.false', _('User Group creation disabled')),
2497 2504 ('hg.usergroup.create.true', _('User Group creation enabled')),
2498 2505
2499 2506 ('hg.create.none', _('Repository creation disabled')),
2500 2507 ('hg.create.repository', _('Repository creation enabled')),
2501 2508 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2502 2509 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2503 2510
2504 2511 ('hg.fork.none', _('Repository forking disabled')),
2505 2512 ('hg.fork.repository', _('Repository forking enabled')),
2506 2513
2507 2514 ('hg.register.none', _('Registration disabled')),
2508 2515 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2509 2516 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2510 2517
2511 2518 ('hg.password_reset.enabled', _('Password reset enabled')),
2512 2519 ('hg.password_reset.hidden', _('Password reset hidden')),
2513 2520 ('hg.password_reset.disabled', _('Password reset disabled')),
2514 2521
2515 2522 ('hg.extern_activate.manual', _('Manual activation of external account')),
2516 2523 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2517 2524
2518 2525 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2519 2526 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2520 2527 ]
2521 2528
2522 2529 # definition of system default permissions for DEFAULT user
2523 2530 DEFAULT_USER_PERMISSIONS = [
2524 2531 'repository.read',
2525 2532 'group.read',
2526 2533 'usergroup.read',
2527 2534 'hg.create.repository',
2528 2535 'hg.repogroup.create.false',
2529 2536 'hg.usergroup.create.false',
2530 2537 'hg.create.write_on_repogroup.true',
2531 2538 'hg.fork.repository',
2532 2539 'hg.register.manual_activate',
2533 2540 'hg.password_reset.enabled',
2534 2541 'hg.extern_activate.auto',
2535 2542 'hg.inherit_default_perms.true',
2536 2543 ]
2537 2544
2538 2545 # defines which permissions are more important higher the more important
2539 2546 # Weight defines which permissions are more important.
2540 2547 # The higher number the more important.
2541 2548 PERM_WEIGHTS = {
2542 2549 'repository.none': 0,
2543 2550 'repository.read': 1,
2544 2551 'repository.write': 3,
2545 2552 'repository.admin': 4,
2546 2553
2547 2554 'group.none': 0,
2548 2555 'group.read': 1,
2549 2556 'group.write': 3,
2550 2557 'group.admin': 4,
2551 2558
2552 2559 'usergroup.none': 0,
2553 2560 'usergroup.read': 1,
2554 2561 'usergroup.write': 3,
2555 2562 'usergroup.admin': 4,
2556 2563
2557 2564 'hg.repogroup.create.false': 0,
2558 2565 'hg.repogroup.create.true': 1,
2559 2566
2560 2567 'hg.usergroup.create.false': 0,
2561 2568 'hg.usergroup.create.true': 1,
2562 2569
2563 2570 'hg.fork.none': 0,
2564 2571 'hg.fork.repository': 1,
2565 2572 'hg.create.none': 0,
2566 2573 'hg.create.repository': 1
2567 2574 }
2568 2575
2569 2576 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2570 2577 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2571 2578 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2572 2579
2573 2580 def __unicode__(self):
2574 2581 return u"<%s('%s:%s')>" % (
2575 2582 self.__class__.__name__, self.permission_id, self.permission_name
2576 2583 )
2577 2584
2578 2585 @classmethod
2579 2586 def get_by_key(cls, key):
2580 2587 return cls.query().filter(cls.permission_name == key).scalar()
2581 2588
2582 2589 @classmethod
2583 2590 def get_default_repo_perms(cls, user_id, repo_id=None):
2584 2591 q = Session().query(UserRepoToPerm, Repository, Permission)\
2585 2592 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2586 2593 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2587 2594 .filter(UserRepoToPerm.user_id == user_id)
2588 2595 if repo_id:
2589 2596 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2590 2597 return q.all()
2591 2598
2592 2599 @classmethod
2593 2600 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2594 2601 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2595 2602 .join(
2596 2603 Permission,
2597 2604 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2598 2605 .join(
2599 2606 Repository,
2600 2607 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2601 2608 .join(
2602 2609 UserGroup,
2603 2610 UserGroupRepoToPerm.users_group_id ==
2604 2611 UserGroup.users_group_id)\
2605 2612 .join(
2606 2613 UserGroupMember,
2607 2614 UserGroupRepoToPerm.users_group_id ==
2608 2615 UserGroupMember.users_group_id)\
2609 2616 .filter(
2610 2617 UserGroupMember.user_id == user_id,
2611 2618 UserGroup.users_group_active == true())
2612 2619 if repo_id:
2613 2620 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2614 2621 return q.all()
2615 2622
2616 2623 @classmethod
2617 2624 def get_default_group_perms(cls, user_id, repo_group_id=None):
2618 2625 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2619 2626 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2620 2627 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2621 2628 .filter(UserRepoGroupToPerm.user_id == user_id)
2622 2629 if repo_group_id:
2623 2630 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2624 2631 return q.all()
2625 2632
2626 2633 @classmethod
2627 2634 def get_default_group_perms_from_user_group(
2628 2635 cls, user_id, repo_group_id=None):
2629 2636 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2630 2637 .join(
2631 2638 Permission,
2632 2639 UserGroupRepoGroupToPerm.permission_id ==
2633 2640 Permission.permission_id)\
2634 2641 .join(
2635 2642 RepoGroup,
2636 2643 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2637 2644 .join(
2638 2645 UserGroup,
2639 2646 UserGroupRepoGroupToPerm.users_group_id ==
2640 2647 UserGroup.users_group_id)\
2641 2648 .join(
2642 2649 UserGroupMember,
2643 2650 UserGroupRepoGroupToPerm.users_group_id ==
2644 2651 UserGroupMember.users_group_id)\
2645 2652 .filter(
2646 2653 UserGroupMember.user_id == user_id,
2647 2654 UserGroup.users_group_active == true())
2648 2655 if repo_group_id:
2649 2656 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2650 2657 return q.all()
2651 2658
2652 2659 @classmethod
2653 2660 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2654 2661 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2655 2662 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2656 2663 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2657 2664 .filter(UserUserGroupToPerm.user_id == user_id)
2658 2665 if user_group_id:
2659 2666 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2660 2667 return q.all()
2661 2668
2662 2669 @classmethod
2663 2670 def get_default_user_group_perms_from_user_group(
2664 2671 cls, user_id, user_group_id=None):
2665 2672 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2666 2673 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2667 2674 .join(
2668 2675 Permission,
2669 2676 UserGroupUserGroupToPerm.permission_id ==
2670 2677 Permission.permission_id)\
2671 2678 .join(
2672 2679 TargetUserGroup,
2673 2680 UserGroupUserGroupToPerm.target_user_group_id ==
2674 2681 TargetUserGroup.users_group_id)\
2675 2682 .join(
2676 2683 UserGroup,
2677 2684 UserGroupUserGroupToPerm.user_group_id ==
2678 2685 UserGroup.users_group_id)\
2679 2686 .join(
2680 2687 UserGroupMember,
2681 2688 UserGroupUserGroupToPerm.user_group_id ==
2682 2689 UserGroupMember.users_group_id)\
2683 2690 .filter(
2684 2691 UserGroupMember.user_id == user_id,
2685 2692 UserGroup.users_group_active == true())
2686 2693 if user_group_id:
2687 2694 q = q.filter(
2688 2695 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2689 2696
2690 2697 return q.all()
2691 2698
2692 2699
2693 2700 class UserRepoToPerm(Base, BaseModel):
2694 2701 __tablename__ = 'repo_to_perm'
2695 2702 __table_args__ = (
2696 2703 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2697 2704 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2698 2705 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2699 2706 )
2700 2707 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2701 2708 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2702 2709 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2703 2710 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2704 2711
2705 2712 user = relationship('User')
2706 2713 repository = relationship('Repository')
2707 2714 permission = relationship('Permission')
2708 2715
2709 2716 @classmethod
2710 2717 def create(cls, user, repository, permission):
2711 2718 n = cls()
2712 2719 n.user = user
2713 2720 n.repository = repository
2714 2721 n.permission = permission
2715 2722 Session().add(n)
2716 2723 return n
2717 2724
2718 2725 def __unicode__(self):
2719 2726 return u'<%s => %s >' % (self.user, self.repository)
2720 2727
2721 2728
2722 2729 class UserUserGroupToPerm(Base, BaseModel):
2723 2730 __tablename__ = 'user_user_group_to_perm'
2724 2731 __table_args__ = (
2725 2732 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2726 2733 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2727 2734 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2728 2735 )
2729 2736 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2730 2737 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2731 2738 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2732 2739 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2733 2740
2734 2741 user = relationship('User')
2735 2742 user_group = relationship('UserGroup')
2736 2743 permission = relationship('Permission')
2737 2744
2738 2745 @classmethod
2739 2746 def create(cls, user, user_group, permission):
2740 2747 n = cls()
2741 2748 n.user = user
2742 2749 n.user_group = user_group
2743 2750 n.permission = permission
2744 2751 Session().add(n)
2745 2752 return n
2746 2753
2747 2754 def __unicode__(self):
2748 2755 return u'<%s => %s >' % (self.user, self.user_group)
2749 2756
2750 2757
2751 2758 class UserToPerm(Base, BaseModel):
2752 2759 __tablename__ = 'user_to_perm'
2753 2760 __table_args__ = (
2754 2761 UniqueConstraint('user_id', 'permission_id'),
2755 2762 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2756 2763 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2757 2764 )
2758 2765 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2759 2766 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2760 2767 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2761 2768
2762 2769 user = relationship('User')
2763 2770 permission = relationship('Permission', lazy='joined')
2764 2771
2765 2772 def __unicode__(self):
2766 2773 return u'<%s => %s >' % (self.user, self.permission)
2767 2774
2768 2775
2769 2776 class UserGroupRepoToPerm(Base, BaseModel):
2770 2777 __tablename__ = 'users_group_repo_to_perm'
2771 2778 __table_args__ = (
2772 2779 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2773 2780 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2774 2781 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2775 2782 )
2776 2783 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2777 2784 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2778 2785 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2779 2786 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2780 2787
2781 2788 users_group = relationship('UserGroup')
2782 2789 permission = relationship('Permission')
2783 2790 repository = relationship('Repository')
2784 2791
2785 2792 @classmethod
2786 2793 def create(cls, users_group, repository, permission):
2787 2794 n = cls()
2788 2795 n.users_group = users_group
2789 2796 n.repository = repository
2790 2797 n.permission = permission
2791 2798 Session().add(n)
2792 2799 return n
2793 2800
2794 2801 def __unicode__(self):
2795 2802 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2796 2803
2797 2804
2798 2805 class UserGroupUserGroupToPerm(Base, BaseModel):
2799 2806 __tablename__ = 'user_group_user_group_to_perm'
2800 2807 __table_args__ = (
2801 2808 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2802 2809 CheckConstraint('target_user_group_id != user_group_id'),
2803 2810 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2804 2811 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2805 2812 )
2806 2813 user_group_user_group_to_perm_id = Column("user_group_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2807 2814 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2808 2815 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2809 2816 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2810 2817
2811 2818 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2812 2819 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2813 2820 permission = relationship('Permission')
2814 2821
2815 2822 @classmethod
2816 2823 def create(cls, target_user_group, user_group, permission):
2817 2824 n = cls()
2818 2825 n.target_user_group = target_user_group
2819 2826 n.user_group = user_group
2820 2827 n.permission = permission
2821 2828 Session().add(n)
2822 2829 return n
2823 2830
2824 2831 def __unicode__(self):
2825 2832 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2826 2833
2827 2834
2828 2835 class UserGroupToPerm(Base, BaseModel):
2829 2836 __tablename__ = 'users_group_to_perm'
2830 2837 __table_args__ = (
2831 2838 UniqueConstraint('users_group_id', 'permission_id',),
2832 2839 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2833 2840 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2834 2841 )
2835 2842 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2836 2843 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2837 2844 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2838 2845
2839 2846 users_group = relationship('UserGroup')
2840 2847 permission = relationship('Permission')
2841 2848
2842 2849
2843 2850 class UserRepoGroupToPerm(Base, BaseModel):
2844 2851 __tablename__ = 'user_repo_group_to_perm'
2845 2852 __table_args__ = (
2846 2853 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2847 2854 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2848 2855 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2849 2856 )
2850 2857
2851 2858 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2852 2859 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2853 2860 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2854 2861 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2855 2862
2856 2863 user = relationship('User')
2857 2864 group = relationship('RepoGroup')
2858 2865 permission = relationship('Permission')
2859 2866
2860 2867 @classmethod
2861 2868 def create(cls, user, repository_group, permission):
2862 2869 n = cls()
2863 2870 n.user = user
2864 2871 n.group = repository_group
2865 2872 n.permission = permission
2866 2873 Session().add(n)
2867 2874 return n
2868 2875
2869 2876
2870 2877 class UserGroupRepoGroupToPerm(Base, BaseModel):
2871 2878 __tablename__ = 'users_group_repo_group_to_perm'
2872 2879 __table_args__ = (
2873 2880 UniqueConstraint('users_group_id', 'group_id'),
2874 2881 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2875 2882 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2876 2883 )
2877 2884
2878 2885 users_group_repo_group_to_perm_id = Column("users_group_repo_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2879 2886 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2880 2887 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2881 2888 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2882 2889
2883 2890 users_group = relationship('UserGroup')
2884 2891 permission = relationship('Permission')
2885 2892 group = relationship('RepoGroup')
2886 2893
2887 2894 @classmethod
2888 2895 def create(cls, user_group, repository_group, permission):
2889 2896 n = cls()
2890 2897 n.users_group = user_group
2891 2898 n.group = repository_group
2892 2899 n.permission = permission
2893 2900 Session().add(n)
2894 2901 return n
2895 2902
2896 2903 def __unicode__(self):
2897 2904 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2898 2905
2899 2906
2900 2907 class Statistics(Base, BaseModel):
2901 2908 __tablename__ = 'statistics'
2902 2909 __table_args__ = (
2903 2910 UniqueConstraint('repository_id'),
2904 2911 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2905 2912 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2906 2913 )
2907 2914 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2908 2915 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2909 2916 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2910 2917 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2911 2918 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2912 2919 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2913 2920
2914 2921 repository = relationship('Repository', single_parent=True)
2915 2922
2916 2923
2917 2924 class UserFollowing(Base, BaseModel):
2918 2925 __tablename__ = 'user_followings'
2919 2926 __table_args__ = (
2920 2927 UniqueConstraint('user_id', 'follows_repository_id'),
2921 2928 UniqueConstraint('user_id', 'follows_user_id'),
2922 2929 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2923 2930 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2924 2931 )
2925 2932
2926 2933 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2927 2934 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2928 2935 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2929 2936 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2930 2937 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2931 2938
2932 2939 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2933 2940
2934 2941 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2935 2942 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2936 2943
2937 2944 @classmethod
2938 2945 def get_repo_followers(cls, repo_id):
2939 2946 return cls.query().filter(cls.follows_repo_id == repo_id)
2940 2947
2941 2948
2942 2949 class CacheKey(Base, BaseModel):
2943 2950 __tablename__ = 'cache_invalidation'
2944 2951 __table_args__ = (
2945 2952 UniqueConstraint('cache_key'),
2946 2953 Index('key_idx', 'cache_key'),
2947 2954 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2948 2955 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2949 2956 )
2950 2957 CACHE_TYPE_ATOM = 'ATOM'
2951 2958 CACHE_TYPE_RSS = 'RSS'
2952 2959 CACHE_TYPE_README = 'README'
2953 2960
2954 2961 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2955 2962 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2956 2963 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2957 2964 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2958 2965
2959 2966 def __init__(self, cache_key, cache_args=''):
2960 2967 self.cache_key = cache_key
2961 2968 self.cache_args = cache_args
2962 2969 self.cache_active = False
2963 2970
2964 2971 def __unicode__(self):
2965 2972 return u"<%s('%s:%s[%s]')>" % (
2966 2973 self.__class__.__name__,
2967 2974 self.cache_id, self.cache_key, self.cache_active)
2968 2975
2969 2976 def _cache_key_partition(self):
2970 2977 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2971 2978 return prefix, repo_name, suffix
2972 2979
2973 2980 def get_prefix(self):
2974 2981 """
2975 2982 Try to extract prefix from existing cache key. The key could consist
2976 2983 of prefix, repo_name, suffix
2977 2984 """
2978 2985 # this returns prefix, repo_name, suffix
2979 2986 return self._cache_key_partition()[0]
2980 2987
2981 2988 def get_suffix(self):
2982 2989 """
2983 2990 get suffix that might have been used in _get_cache_key to
2984 2991 generate self.cache_key. Only used for informational purposes
2985 2992 in repo_edit.mako.
2986 2993 """
2987 2994 # prefix, repo_name, suffix
2988 2995 return self._cache_key_partition()[2]
2989 2996
2990 2997 @classmethod
2991 2998 def delete_all_cache(cls):
2992 2999 """
2993 3000 Delete all cache keys from database.
2994 3001 Should only be run when all instances are down and all entries
2995 3002 thus stale.
2996 3003 """
2997 3004 cls.query().delete()
2998 3005 Session().commit()
2999 3006
3000 3007 @classmethod
3001 3008 def get_cache_key(cls, repo_name, cache_type):
3002 3009 """
3003 3010
3004 3011 Generate a cache key for this process of RhodeCode instance.
3005 3012 Prefix most likely will be process id or maybe explicitly set
3006 3013 instance_id from .ini file.
3007 3014 """
3008 3015 import rhodecode
3009 3016 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
3010 3017
3011 3018 repo_as_unicode = safe_unicode(repo_name)
3012 3019 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
3013 3020 if cache_type else repo_as_unicode
3014 3021
3015 3022 return u'{}{}'.format(prefix, key)
3016 3023
3017 3024 @classmethod
3018 3025 def set_invalidate(cls, repo_name, delete=False):
3019 3026 """
3020 3027 Mark all caches of a repo as invalid in the database.
3021 3028 """
3022 3029
3023 3030 try:
3024 3031 qry = Session().query(cls).filter(cls.cache_args == repo_name)
3025 3032 if delete:
3026 3033 log.debug('cache objects deleted for repo %s',
3027 3034 safe_str(repo_name))
3028 3035 qry.delete()
3029 3036 else:
3030 3037 log.debug('cache objects marked as invalid for repo %s',
3031 3038 safe_str(repo_name))
3032 3039 qry.update({"cache_active": False})
3033 3040
3034 3041 Session().commit()
3035 3042 except Exception:
3036 3043 log.exception(
3037 3044 'Cache key invalidation failed for repository %s',
3038 3045 safe_str(repo_name))
3039 3046 Session().rollback()
3040 3047
3041 3048 @classmethod
3042 3049 def get_active_cache(cls, cache_key):
3043 3050 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3044 3051 if inv_obj:
3045 3052 return inv_obj
3046 3053 return None
3047 3054
3048 3055 @classmethod
3049 3056 def repo_context_cache(cls, compute_func, repo_name, cache_type,
3050 3057 thread_scoped=False):
3051 3058 """
3052 3059 @cache_region('long_term')
3053 3060 def _heavy_calculation(cache_key):
3054 3061 return 'result'
3055 3062
3056 3063 cache_context = CacheKey.repo_context_cache(
3057 3064 _heavy_calculation, repo_name, cache_type)
3058 3065
3059 3066 with cache_context as context:
3060 3067 context.invalidate()
3061 3068 computed = context.compute()
3062 3069
3063 3070 assert computed == 'result'
3064 3071 """
3065 3072 from rhodecode.lib import caches
3066 3073 return caches.InvalidationContext(
3067 3074 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
3068 3075
3069 3076
3070 3077 class ChangesetComment(Base, BaseModel):
3071 3078 __tablename__ = 'changeset_comments'
3072 3079 __table_args__ = (
3073 3080 Index('cc_revision_idx', 'revision'),
3074 3081 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3075 3082 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3076 3083 )
3077 3084
3078 3085 COMMENT_OUTDATED = u'comment_outdated'
3079 3086 COMMENT_TYPE_NOTE = u'note'
3080 3087 COMMENT_TYPE_TODO = u'todo'
3081 3088 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3082 3089
3083 3090 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3084 3091 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3085 3092 revision = Column('revision', String(40), nullable=True)
3086 3093 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3087 3094 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3088 3095 line_no = Column('line_no', Unicode(10), nullable=True)
3089 3096 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3090 3097 f_path = Column('f_path', Unicode(1000), nullable=True)
3091 3098 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3092 3099 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3093 3100 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3094 3101 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3095 3102 renderer = Column('renderer', Unicode(64), nullable=True)
3096 3103 display_state = Column('display_state', Unicode(128), nullable=True)
3097 3104
3098 3105 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3099 3106 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3100 3107 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, backref='resolved_by')
3101 3108 author = relationship('User', lazy='joined')
3102 3109 repo = relationship('Repository')
3103 3110 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan", lazy='joined')
3104 3111 pull_request = relationship('PullRequest', lazy='joined')
3105 3112 pull_request_version = relationship('PullRequestVersion')
3106 3113
3107 3114 @classmethod
3108 3115 def get_users(cls, revision=None, pull_request_id=None):
3109 3116 """
3110 3117 Returns user associated with this ChangesetComment. ie those
3111 3118 who actually commented
3112 3119
3113 3120 :param cls:
3114 3121 :param revision:
3115 3122 """
3116 3123 q = Session().query(User)\
3117 3124 .join(ChangesetComment.author)
3118 3125 if revision:
3119 3126 q = q.filter(cls.revision == revision)
3120 3127 elif pull_request_id:
3121 3128 q = q.filter(cls.pull_request_id == pull_request_id)
3122 3129 return q.all()
3123 3130
3124 3131 @classmethod
3125 3132 def get_index_from_version(cls, pr_version, versions):
3126 3133 num_versions = [x.pull_request_version_id for x in versions]
3127 3134 try:
3128 3135 return num_versions.index(pr_version) +1
3129 3136 except (IndexError, ValueError):
3130 3137 return
3131 3138
3132 3139 @property
3133 3140 def outdated(self):
3134 3141 return self.display_state == self.COMMENT_OUTDATED
3135 3142
3136 3143 def outdated_at_version(self, version):
3137 3144 """
3138 3145 Checks if comment is outdated for given pull request version
3139 3146 """
3140 3147 return self.outdated and self.pull_request_version_id != version
3141 3148
3142 3149 def older_than_version(self, version):
3143 3150 """
3144 3151 Checks if comment is made from previous version than given
3145 3152 """
3146 3153 if version is None:
3147 3154 return self.pull_request_version_id is not None
3148 3155
3149 3156 return self.pull_request_version_id < version
3150 3157
3151 3158 @property
3152 3159 def resolved(self):
3153 3160 return self.resolved_by[0] if self.resolved_by else None
3154 3161
3155 3162 @property
3156 3163 def is_todo(self):
3157 3164 return self.comment_type == self.COMMENT_TYPE_TODO
3158 3165
3159 3166 @property
3160 3167 def is_inline(self):
3161 3168 return self.line_no and self.f_path
3162 3169
3163 3170 def get_index_version(self, versions):
3164 3171 return self.get_index_from_version(
3165 3172 self.pull_request_version_id, versions)
3166 3173
3167 3174 def __repr__(self):
3168 3175 if self.comment_id:
3169 3176 return '<DB:Comment #%s>' % self.comment_id
3170 3177 else:
3171 3178 return '<DB:Comment at %#x>' % id(self)
3172 3179
3173 3180 def get_api_data(self):
3174 3181 comment = self
3175 3182 data = {
3176 3183 'comment_id': comment.comment_id,
3177 3184 'comment_type': comment.comment_type,
3178 3185 'comment_text': comment.text,
3179 3186 'comment_status': comment.status_change,
3180 3187 'comment_f_path': comment.f_path,
3181 3188 'comment_lineno': comment.line_no,
3182 3189 'comment_author': comment.author,
3183 3190 'comment_created_on': comment.created_on
3184 3191 }
3185 3192 return data
3186 3193
3187 3194 def __json__(self):
3188 3195 data = dict()
3189 3196 data.update(self.get_api_data())
3190 3197 return data
3191 3198
3192 3199
3193 3200 class ChangesetStatus(Base, BaseModel):
3194 3201 __tablename__ = 'changeset_statuses'
3195 3202 __table_args__ = (
3196 3203 Index('cs_revision_idx', 'revision'),
3197 3204 Index('cs_version_idx', 'version'),
3198 3205 UniqueConstraint('repo_id', 'revision', 'version'),
3199 3206 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3200 3207 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3201 3208 )
3202 3209 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3203 3210 STATUS_APPROVED = 'approved'
3204 3211 STATUS_REJECTED = 'rejected'
3205 3212 STATUS_UNDER_REVIEW = 'under_review'
3206 3213
3207 3214 STATUSES = [
3208 3215 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3209 3216 (STATUS_APPROVED, _("Approved")),
3210 3217 (STATUS_REJECTED, _("Rejected")),
3211 3218 (STATUS_UNDER_REVIEW, _("Under Review")),
3212 3219 ]
3213 3220
3214 3221 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3215 3222 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3216 3223 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3217 3224 revision = Column('revision', String(40), nullable=False)
3218 3225 status = Column('status', String(128), nullable=False, default=DEFAULT)
3219 3226 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3220 3227 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3221 3228 version = Column('version', Integer(), nullable=False, default=0)
3222 3229 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3223 3230
3224 3231 author = relationship('User', lazy='joined')
3225 3232 repo = relationship('Repository')
3226 3233 comment = relationship('ChangesetComment', lazy='joined')
3227 3234 pull_request = relationship('PullRequest', lazy='joined')
3228 3235
3229 3236 def __unicode__(self):
3230 3237 return u"<%s('%s[v%s]:%s')>" % (
3231 3238 self.__class__.__name__,
3232 3239 self.status, self.version, self.author
3233 3240 )
3234 3241
3235 3242 @classmethod
3236 3243 def get_status_lbl(cls, value):
3237 3244 return dict(cls.STATUSES).get(value)
3238 3245
3239 3246 @property
3240 3247 def status_lbl(self):
3241 3248 return ChangesetStatus.get_status_lbl(self.status)
3242 3249
3243 3250 def get_api_data(self):
3244 3251 status = self
3245 3252 data = {
3246 3253 'status_id': status.changeset_status_id,
3247 3254 'status': status.status,
3248 3255 }
3249 3256 return data
3250 3257
3251 3258 def __json__(self):
3252 3259 data = dict()
3253 3260 data.update(self.get_api_data())
3254 3261 return data
3255 3262
3256 3263
3257 3264 class _PullRequestBase(BaseModel):
3258 3265 """
3259 3266 Common attributes of pull request and version entries.
3260 3267 """
3261 3268
3262 3269 # .status values
3263 3270 STATUS_NEW = u'new'
3264 3271 STATUS_OPEN = u'open'
3265 3272 STATUS_CLOSED = u'closed'
3266 3273
3267 3274 title = Column('title', Unicode(255), nullable=True)
3268 3275 description = Column(
3269 3276 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3270 3277 nullable=True)
3271 3278 # new/open/closed status of pull request (not approve/reject/etc)
3272 3279 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3273 3280 created_on = Column(
3274 3281 'created_on', DateTime(timezone=False), nullable=False,
3275 3282 default=datetime.datetime.now)
3276 3283 updated_on = Column(
3277 3284 'updated_on', DateTime(timezone=False), nullable=False,
3278 3285 default=datetime.datetime.now)
3279 3286
3280 3287 @declared_attr
3281 3288 def user_id(cls):
3282 3289 return Column(
3283 3290 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3284 3291 unique=None)
3285 3292
3286 3293 # 500 revisions max
3287 3294 _revisions = Column(
3288 3295 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3289 3296
3290 3297 @declared_attr
3291 3298 def source_repo_id(cls):
3292 3299 # TODO: dan: rename column to source_repo_id
3293 3300 return Column(
3294 3301 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3295 3302 nullable=False)
3296 3303
3297 3304 source_ref = Column('org_ref', Unicode(255), nullable=False)
3298 3305
3299 3306 @declared_attr
3300 3307 def target_repo_id(cls):
3301 3308 # TODO: dan: rename column to target_repo_id
3302 3309 return Column(
3303 3310 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3304 3311 nullable=False)
3305 3312
3306 3313 target_ref = Column('other_ref', Unicode(255), nullable=False)
3307 3314 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
3308 3315
3309 3316 # TODO: dan: rename column to last_merge_source_rev
3310 3317 _last_merge_source_rev = Column(
3311 3318 'last_merge_org_rev', String(40), nullable=True)
3312 3319 # TODO: dan: rename column to last_merge_target_rev
3313 3320 _last_merge_target_rev = Column(
3314 3321 'last_merge_other_rev', String(40), nullable=True)
3315 3322 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3316 3323 merge_rev = Column('merge_rev', String(40), nullable=True)
3317 3324
3318 3325 reviewer_data = Column(
3319 3326 'reviewer_data_json', MutationObj.as_mutable(
3320 3327 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3321 3328
3322 3329 @property
3323 3330 def reviewer_data_json(self):
3324 3331 return json.dumps(self.reviewer_data)
3325 3332
3326 3333 @hybrid_property
3327 3334 def description_safe(self):
3328 3335 from rhodecode.lib import helpers as h
3329 3336 return h.escape(self.description)
3330 3337
3331 3338 @hybrid_property
3332 3339 def revisions(self):
3333 3340 return self._revisions.split(':') if self._revisions else []
3334 3341
3335 3342 @revisions.setter
3336 3343 def revisions(self, val):
3337 3344 self._revisions = ':'.join(val)
3338 3345
3339 3346 @declared_attr
3340 3347 def author(cls):
3341 3348 return relationship('User', lazy='joined')
3342 3349
3343 3350 @declared_attr
3344 3351 def source_repo(cls):
3345 3352 return relationship(
3346 3353 'Repository',
3347 3354 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3348 3355
3349 3356 @property
3350 3357 def source_ref_parts(self):
3351 3358 return self.unicode_to_reference(self.source_ref)
3352 3359
3353 3360 @declared_attr
3354 3361 def target_repo(cls):
3355 3362 return relationship(
3356 3363 'Repository',
3357 3364 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3358 3365
3359 3366 @property
3360 3367 def target_ref_parts(self):
3361 3368 return self.unicode_to_reference(self.target_ref)
3362 3369
3363 3370 @property
3364 3371 def shadow_merge_ref(self):
3365 3372 return self.unicode_to_reference(self._shadow_merge_ref)
3366 3373
3367 3374 @shadow_merge_ref.setter
3368 3375 def shadow_merge_ref(self, ref):
3369 3376 self._shadow_merge_ref = self.reference_to_unicode(ref)
3370 3377
3371 3378 def unicode_to_reference(self, raw):
3372 3379 """
3373 3380 Convert a unicode (or string) to a reference object.
3374 3381 If unicode evaluates to False it returns None.
3375 3382 """
3376 3383 if raw:
3377 3384 refs = raw.split(':')
3378 3385 return Reference(*refs)
3379 3386 else:
3380 3387 return None
3381 3388
3382 3389 def reference_to_unicode(self, ref):
3383 3390 """
3384 3391 Convert a reference object to unicode.
3385 3392 If reference is None it returns None.
3386 3393 """
3387 3394 if ref:
3388 3395 return u':'.join(ref)
3389 3396 else:
3390 3397 return None
3391 3398
3392 3399 def get_api_data(self, with_merge_state=True):
3393 3400 from rhodecode.model.pull_request import PullRequestModel
3394 3401
3395 3402 pull_request = self
3396 3403 if with_merge_state:
3397 3404 merge_status = PullRequestModel().merge_status(pull_request)
3398 3405 merge_state = {
3399 3406 'status': merge_status[0],
3400 3407 'message': safe_unicode(merge_status[1]),
3401 3408 }
3402 3409 else:
3403 3410 merge_state = {'status': 'not_available',
3404 3411 'message': 'not_available'}
3405 3412
3406 3413 merge_data = {
3407 3414 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
3408 3415 'reference': (
3409 3416 pull_request.shadow_merge_ref._asdict()
3410 3417 if pull_request.shadow_merge_ref else None),
3411 3418 }
3412 3419
3413 3420 data = {
3414 3421 'pull_request_id': pull_request.pull_request_id,
3415 3422 'url': PullRequestModel().get_url(pull_request),
3416 3423 'title': pull_request.title,
3417 3424 'description': pull_request.description,
3418 3425 'status': pull_request.status,
3419 3426 'created_on': pull_request.created_on,
3420 3427 'updated_on': pull_request.updated_on,
3421 3428 'commit_ids': pull_request.revisions,
3422 3429 'review_status': pull_request.calculated_review_status(),
3423 3430 'mergeable': merge_state,
3424 3431 'source': {
3425 3432 'clone_url': pull_request.source_repo.clone_url(),
3426 3433 'repository': pull_request.source_repo.repo_name,
3427 3434 'reference': {
3428 3435 'name': pull_request.source_ref_parts.name,
3429 3436 'type': pull_request.source_ref_parts.type,
3430 3437 'commit_id': pull_request.source_ref_parts.commit_id,
3431 3438 },
3432 3439 },
3433 3440 'target': {
3434 3441 'clone_url': pull_request.target_repo.clone_url(),
3435 3442 'repository': pull_request.target_repo.repo_name,
3436 3443 'reference': {
3437 3444 'name': pull_request.target_ref_parts.name,
3438 3445 'type': pull_request.target_ref_parts.type,
3439 3446 'commit_id': pull_request.target_ref_parts.commit_id,
3440 3447 },
3441 3448 },
3442 3449 'merge': merge_data,
3443 3450 'author': pull_request.author.get_api_data(include_secrets=False,
3444 3451 details='basic'),
3445 3452 'reviewers': [
3446 3453 {
3447 3454 'user': reviewer.get_api_data(include_secrets=False,
3448 3455 details='basic'),
3449 3456 'reasons': reasons,
3450 3457 'review_status': st[0][1].status if st else 'not_reviewed',
3451 3458 }
3452 3459 for reviewer, reasons, mandatory, st in
3453 3460 pull_request.reviewers_statuses()
3454 3461 ]
3455 3462 }
3456 3463
3457 3464 return data
3458 3465
3459 3466
3460 3467 class PullRequest(Base, _PullRequestBase):
3461 3468 __tablename__ = 'pull_requests'
3462 3469 __table_args__ = (
3463 3470 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3464 3471 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3465 3472 )
3466 3473
3467 3474 pull_request_id = Column(
3468 3475 'pull_request_id', Integer(), nullable=False, primary_key=True)
3469 3476
3470 3477 def __repr__(self):
3471 3478 if self.pull_request_id:
3472 3479 return '<DB:PullRequest #%s>' % self.pull_request_id
3473 3480 else:
3474 3481 return '<DB:PullRequest at %#x>' % id(self)
3475 3482
3476 3483 reviewers = relationship('PullRequestReviewers',
3477 3484 cascade="all, delete, delete-orphan")
3478 3485 statuses = relationship('ChangesetStatus',
3479 3486 cascade="all, delete, delete-orphan")
3480 3487 comments = relationship('ChangesetComment',
3481 3488 cascade="all, delete, delete-orphan")
3482 3489 versions = relationship('PullRequestVersion',
3483 3490 cascade="all, delete, delete-orphan",
3484 3491 lazy='dynamic')
3485 3492
3486 3493 @classmethod
3487 3494 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
3488 3495 internal_methods=None):
3489 3496
3490 3497 class PullRequestDisplay(object):
3491 3498 """
3492 3499 Special object wrapper for showing PullRequest data via Versions
3493 3500 It mimics PR object as close as possible. This is read only object
3494 3501 just for display
3495 3502 """
3496 3503
3497 3504 def __init__(self, attrs, internal=None):
3498 3505 self.attrs = attrs
3499 3506 # internal have priority over the given ones via attrs
3500 3507 self.internal = internal or ['versions']
3501 3508
3502 3509 def __getattr__(self, item):
3503 3510 if item in self.internal:
3504 3511 return getattr(self, item)
3505 3512 try:
3506 3513 return self.attrs[item]
3507 3514 except KeyError:
3508 3515 raise AttributeError(
3509 3516 '%s object has no attribute %s' % (self, item))
3510 3517
3511 3518 def __repr__(self):
3512 3519 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
3513 3520
3514 3521 def versions(self):
3515 3522 return pull_request_obj.versions.order_by(
3516 3523 PullRequestVersion.pull_request_version_id).all()
3517 3524
3518 3525 def is_closed(self):
3519 3526 return pull_request_obj.is_closed()
3520 3527
3521 3528 @property
3522 3529 def pull_request_version_id(self):
3523 3530 return getattr(pull_request_obj, 'pull_request_version_id', None)
3524 3531
3525 3532 attrs = StrictAttributeDict(pull_request_obj.get_api_data())
3526 3533
3527 3534 attrs.author = StrictAttributeDict(
3528 3535 pull_request_obj.author.get_api_data())
3529 3536 if pull_request_obj.target_repo:
3530 3537 attrs.target_repo = StrictAttributeDict(
3531 3538 pull_request_obj.target_repo.get_api_data())
3532 3539 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
3533 3540
3534 3541 if pull_request_obj.source_repo:
3535 3542 attrs.source_repo = StrictAttributeDict(
3536 3543 pull_request_obj.source_repo.get_api_data())
3537 3544 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
3538 3545
3539 3546 attrs.source_ref_parts = pull_request_obj.source_ref_parts
3540 3547 attrs.target_ref_parts = pull_request_obj.target_ref_parts
3541 3548 attrs.revisions = pull_request_obj.revisions
3542 3549
3543 3550 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
3544 3551 attrs.reviewer_data = org_pull_request_obj.reviewer_data
3545 3552 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
3546 3553
3547 3554 return PullRequestDisplay(attrs, internal=internal_methods)
3548 3555
3549 3556 def is_closed(self):
3550 3557 return self.status == self.STATUS_CLOSED
3551 3558
3552 3559 def __json__(self):
3553 3560 return {
3554 3561 'revisions': self.revisions,
3555 3562 }
3556 3563
3557 3564 def calculated_review_status(self):
3558 3565 from rhodecode.model.changeset_status import ChangesetStatusModel
3559 3566 return ChangesetStatusModel().calculated_review_status(self)
3560 3567
3561 3568 def reviewers_statuses(self):
3562 3569 from rhodecode.model.changeset_status import ChangesetStatusModel
3563 3570 return ChangesetStatusModel().reviewers_statuses(self)
3564 3571
3565 3572 @property
3566 3573 def workspace_id(self):
3567 3574 from rhodecode.model.pull_request import PullRequestModel
3568 3575 return PullRequestModel()._workspace_id(self)
3569 3576
3570 3577 def get_shadow_repo(self):
3571 3578 workspace_id = self.workspace_id
3572 3579 vcs_obj = self.target_repo.scm_instance()
3573 3580 shadow_repository_path = vcs_obj._get_shadow_repository_path(
3574 3581 workspace_id)
3575 3582 return vcs_obj._get_shadow_instance(shadow_repository_path)
3576 3583
3577 3584
3578 3585 class PullRequestVersion(Base, _PullRequestBase):
3579 3586 __tablename__ = 'pull_request_versions'
3580 3587 __table_args__ = (
3581 3588 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3582 3589 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3583 3590 )
3584 3591
3585 3592 pull_request_version_id = Column(
3586 3593 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3587 3594 pull_request_id = Column(
3588 3595 'pull_request_id', Integer(),
3589 3596 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3590 3597 pull_request = relationship('PullRequest')
3591 3598
3592 3599 def __repr__(self):
3593 3600 if self.pull_request_version_id:
3594 3601 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3595 3602 else:
3596 3603 return '<DB:PullRequestVersion at %#x>' % id(self)
3597 3604
3598 3605 @property
3599 3606 def reviewers(self):
3600 3607 return self.pull_request.reviewers
3601 3608
3602 3609 @property
3603 3610 def versions(self):
3604 3611 return self.pull_request.versions
3605 3612
3606 3613 def is_closed(self):
3607 3614 # calculate from original
3608 3615 return self.pull_request.status == self.STATUS_CLOSED
3609 3616
3610 3617 def calculated_review_status(self):
3611 3618 return self.pull_request.calculated_review_status()
3612 3619
3613 3620 def reviewers_statuses(self):
3614 3621 return self.pull_request.reviewers_statuses()
3615 3622
3616 3623
3617 3624 class PullRequestReviewers(Base, BaseModel):
3618 3625 __tablename__ = 'pull_request_reviewers'
3619 3626 __table_args__ = (
3620 3627 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3621 3628 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3622 3629 )
3623 3630
3624 3631 @hybrid_property
3625 3632 def reasons(self):
3626 3633 if not self._reasons:
3627 3634 return []
3628 3635 return self._reasons
3629 3636
3630 3637 @reasons.setter
3631 3638 def reasons(self, val):
3632 3639 val = val or []
3633 3640 if any(not isinstance(x, basestring) for x in val):
3634 3641 raise Exception('invalid reasons type, must be list of strings')
3635 3642 self._reasons = val
3636 3643
3637 3644 pull_requests_reviewers_id = Column(
3638 3645 'pull_requests_reviewers_id', Integer(), nullable=False,
3639 3646 primary_key=True)
3640 3647 pull_request_id = Column(
3641 3648 "pull_request_id", Integer(),
3642 3649 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3643 3650 user_id = Column(
3644 3651 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3645 3652 _reasons = Column(
3646 3653 'reason', MutationList.as_mutable(
3647 3654 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
3648 3655 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3649 3656 user = relationship('User')
3650 3657 pull_request = relationship('PullRequest')
3651 3658
3652 3659
3653 3660 class Notification(Base, BaseModel):
3654 3661 __tablename__ = 'notifications'
3655 3662 __table_args__ = (
3656 3663 Index('notification_type_idx', 'type'),
3657 3664 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3658 3665 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3659 3666 )
3660 3667
3661 3668 TYPE_CHANGESET_COMMENT = u'cs_comment'
3662 3669 TYPE_MESSAGE = u'message'
3663 3670 TYPE_MENTION = u'mention'
3664 3671 TYPE_REGISTRATION = u'registration'
3665 3672 TYPE_PULL_REQUEST = u'pull_request'
3666 3673 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3667 3674
3668 3675 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3669 3676 subject = Column('subject', Unicode(512), nullable=True)
3670 3677 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3671 3678 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3672 3679 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3673 3680 type_ = Column('type', Unicode(255))
3674 3681
3675 3682 created_by_user = relationship('User')
3676 3683 notifications_to_users = relationship('UserNotification', lazy='joined',
3677 3684 cascade="all, delete, delete-orphan")
3678 3685
3679 3686 @property
3680 3687 def recipients(self):
3681 3688 return [x.user for x in UserNotification.query()\
3682 3689 .filter(UserNotification.notification == self)\
3683 3690 .order_by(UserNotification.user_id.asc()).all()]
3684 3691
3685 3692 @classmethod
3686 3693 def create(cls, created_by, subject, body, recipients, type_=None):
3687 3694 if type_ is None:
3688 3695 type_ = Notification.TYPE_MESSAGE
3689 3696
3690 3697 notification = cls()
3691 3698 notification.created_by_user = created_by
3692 3699 notification.subject = subject
3693 3700 notification.body = body
3694 3701 notification.type_ = type_
3695 3702 notification.created_on = datetime.datetime.now()
3696 3703
3697 3704 for u in recipients:
3698 3705 assoc = UserNotification()
3699 3706 assoc.notification = notification
3700 3707
3701 3708 # if created_by is inside recipients mark his notification
3702 3709 # as read
3703 3710 if u.user_id == created_by.user_id:
3704 3711 assoc.read = True
3705 3712
3706 3713 u.notifications.append(assoc)
3707 3714 Session().add(notification)
3708 3715
3709 3716 return notification
3710 3717
3711 3718
3712 3719 class UserNotification(Base, BaseModel):
3713 3720 __tablename__ = 'user_to_notification'
3714 3721 __table_args__ = (
3715 3722 UniqueConstraint('user_id', 'notification_id'),
3716 3723 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3717 3724 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3718 3725 )
3719 3726 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3720 3727 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3721 3728 read = Column('read', Boolean, default=False)
3722 3729 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3723 3730
3724 3731 user = relationship('User', lazy="joined")
3725 3732 notification = relationship('Notification', lazy="joined",
3726 3733 order_by=lambda: Notification.created_on.desc(),)
3727 3734
3728 3735 def mark_as_read(self):
3729 3736 self.read = True
3730 3737 Session().add(self)
3731 3738
3732 3739
3733 3740 class Gist(Base, BaseModel):
3734 3741 __tablename__ = 'gists'
3735 3742 __table_args__ = (
3736 3743 Index('g_gist_access_id_idx', 'gist_access_id'),
3737 3744 Index('g_created_on_idx', 'created_on'),
3738 3745 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3739 3746 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3740 3747 )
3741 3748 GIST_PUBLIC = u'public'
3742 3749 GIST_PRIVATE = u'private'
3743 3750 DEFAULT_FILENAME = u'gistfile1.txt'
3744 3751
3745 3752 ACL_LEVEL_PUBLIC = u'acl_public'
3746 3753 ACL_LEVEL_PRIVATE = u'acl_private'
3747 3754
3748 3755 gist_id = Column('gist_id', Integer(), primary_key=True)
3749 3756 gist_access_id = Column('gist_access_id', Unicode(250))
3750 3757 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3751 3758 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3752 3759 gist_expires = Column('gist_expires', Float(53), nullable=False)
3753 3760 gist_type = Column('gist_type', Unicode(128), nullable=False)
3754 3761 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3755 3762 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3756 3763 acl_level = Column('acl_level', Unicode(128), nullable=True)
3757 3764
3758 3765 owner = relationship('User')
3759 3766
3760 3767 def __repr__(self):
3761 3768 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3762 3769
3763 3770 @hybrid_property
3764 3771 def description_safe(self):
3765 3772 from rhodecode.lib import helpers as h
3766 3773 return h.escape(self.gist_description)
3767 3774
3768 3775 @classmethod
3769 3776 def get_or_404(cls, id_, pyramid_exc=False):
3770 3777
3771 3778 if pyramid_exc:
3772 3779 from pyramid.httpexceptions import HTTPNotFound
3773 3780 else:
3774 3781 from webob.exc import HTTPNotFound
3775 3782
3776 3783 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3777 3784 if not res:
3778 3785 raise HTTPNotFound
3779 3786 return res
3780 3787
3781 3788 @classmethod
3782 3789 def get_by_access_id(cls, gist_access_id):
3783 3790 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3784 3791
3785 3792 def gist_url(self):
3786 3793 from rhodecode.model.gist import GistModel
3787 3794 return GistModel().get_url(self)
3788 3795
3789 3796 @classmethod
3790 3797 def base_path(cls):
3791 3798 """
3792 3799 Returns base path when all gists are stored
3793 3800
3794 3801 :param cls:
3795 3802 """
3796 3803 from rhodecode.model.gist import GIST_STORE_LOC
3797 3804 q = Session().query(RhodeCodeUi)\
3798 3805 .filter(RhodeCodeUi.ui_key == URL_SEP)
3799 3806 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3800 3807 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3801 3808
3802 3809 def get_api_data(self):
3803 3810 """
3804 3811 Common function for generating gist related data for API
3805 3812 """
3806 3813 gist = self
3807 3814 data = {
3808 3815 'gist_id': gist.gist_id,
3809 3816 'type': gist.gist_type,
3810 3817 'access_id': gist.gist_access_id,
3811 3818 'description': gist.gist_description,
3812 3819 'url': gist.gist_url(),
3813 3820 'expires': gist.gist_expires,
3814 3821 'created_on': gist.created_on,
3815 3822 'modified_at': gist.modified_at,
3816 3823 'content': None,
3817 3824 'acl_level': gist.acl_level,
3818 3825 }
3819 3826 return data
3820 3827
3821 3828 def __json__(self):
3822 3829 data = dict(
3823 3830 )
3824 3831 data.update(self.get_api_data())
3825 3832 return data
3826 3833 # SCM functions
3827 3834
3828 3835 def scm_instance(self, **kwargs):
3829 3836 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3830 3837 return get_vcs_instance(
3831 3838 repo_path=safe_str(full_repo_path), create=False)
3832 3839
3833 3840
3834 3841 class ExternalIdentity(Base, BaseModel):
3835 3842 __tablename__ = 'external_identities'
3836 3843 __table_args__ = (
3837 3844 Index('local_user_id_idx', 'local_user_id'),
3838 3845 Index('external_id_idx', 'external_id'),
3839 3846 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3840 3847 'mysql_charset': 'utf8'})
3841 3848
3842 3849 external_id = Column('external_id', Unicode(255), default=u'',
3843 3850 primary_key=True)
3844 3851 external_username = Column('external_username', Unicode(1024), default=u'')
3845 3852 local_user_id = Column('local_user_id', Integer(),
3846 3853 ForeignKey('users.user_id'), primary_key=True)
3847 3854 provider_name = Column('provider_name', Unicode(255), default=u'',
3848 3855 primary_key=True)
3849 3856 access_token = Column('access_token', String(1024), default=u'')
3850 3857 alt_token = Column('alt_token', String(1024), default=u'')
3851 3858 token_secret = Column('token_secret', String(1024), default=u'')
3852 3859
3853 3860 @classmethod
3854 3861 def by_external_id_and_provider(cls, external_id, provider_name,
3855 3862 local_user_id=None):
3856 3863 """
3857 3864 Returns ExternalIdentity instance based on search params
3858 3865
3859 3866 :param external_id:
3860 3867 :param provider_name:
3861 3868 :return: ExternalIdentity
3862 3869 """
3863 3870 query = cls.query()
3864 3871 query = query.filter(cls.external_id == external_id)
3865 3872 query = query.filter(cls.provider_name == provider_name)
3866 3873 if local_user_id:
3867 3874 query = query.filter(cls.local_user_id == local_user_id)
3868 3875 return query.first()
3869 3876
3870 3877 @classmethod
3871 3878 def user_by_external_id_and_provider(cls, external_id, provider_name):
3872 3879 """
3873 3880 Returns User instance based on search params
3874 3881
3875 3882 :param external_id:
3876 3883 :param provider_name:
3877 3884 :return: User
3878 3885 """
3879 3886 query = User.query()
3880 3887 query = query.filter(cls.external_id == external_id)
3881 3888 query = query.filter(cls.provider_name == provider_name)
3882 3889 query = query.filter(User.user_id == cls.local_user_id)
3883 3890 return query.first()
3884 3891
3885 3892 @classmethod
3886 3893 def by_local_user_id(cls, local_user_id):
3887 3894 """
3888 3895 Returns all tokens for user
3889 3896
3890 3897 :param local_user_id:
3891 3898 :return: ExternalIdentity
3892 3899 """
3893 3900 query = cls.query()
3894 3901 query = query.filter(cls.local_user_id == local_user_id)
3895 3902 return query
3896 3903
3897 3904
3898 3905 class Integration(Base, BaseModel):
3899 3906 __tablename__ = 'integrations'
3900 3907 __table_args__ = (
3901 3908 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3902 3909 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3903 3910 )
3904 3911
3905 3912 integration_id = Column('integration_id', Integer(), primary_key=True)
3906 3913 integration_type = Column('integration_type', String(255))
3907 3914 enabled = Column('enabled', Boolean(), nullable=False)
3908 3915 name = Column('name', String(255), nullable=False)
3909 3916 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3910 3917 default=False)
3911 3918
3912 3919 settings = Column(
3913 3920 'settings_json', MutationObj.as_mutable(
3914 3921 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3915 3922 repo_id = Column(
3916 3923 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3917 3924 nullable=True, unique=None, default=None)
3918 3925 repo = relationship('Repository', lazy='joined')
3919 3926
3920 3927 repo_group_id = Column(
3921 3928 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3922 3929 nullable=True, unique=None, default=None)
3923 3930 repo_group = relationship('RepoGroup', lazy='joined')
3924 3931
3925 3932 @property
3926 3933 def scope(self):
3927 3934 if self.repo:
3928 3935 return repr(self.repo)
3929 3936 if self.repo_group:
3930 3937 if self.child_repos_only:
3931 3938 return repr(self.repo_group) + ' (child repos only)'
3932 3939 else:
3933 3940 return repr(self.repo_group) + ' (recursive)'
3934 3941 if self.child_repos_only:
3935 3942 return 'root_repos'
3936 3943 return 'global'
3937 3944
3938 3945 def __repr__(self):
3939 3946 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3940 3947
3941 3948
3942 3949 class RepoReviewRuleUser(Base, BaseModel):
3943 3950 __tablename__ = 'repo_review_rules_users'
3944 3951 __table_args__ = (
3945 3952 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3946 3953 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3947 3954 )
3948 3955 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
3949 3956 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3950 3957 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
3951 3958 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3952 3959 user = relationship('User')
3953 3960
3954 3961 def rule_data(self):
3955 3962 return {
3956 3963 'mandatory': self.mandatory
3957 3964 }
3958 3965
3959 3966
3960 3967 class RepoReviewRuleUserGroup(Base, BaseModel):
3961 3968 __tablename__ = 'repo_review_rules_users_groups'
3962 3969 __table_args__ = (
3963 3970 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3964 3971 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3965 3972 )
3966 3973 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
3967 3974 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3968 3975 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
3969 3976 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
3970 3977 users_group = relationship('UserGroup')
3971 3978
3972 3979 def rule_data(self):
3973 3980 return {
3974 3981 'mandatory': self.mandatory
3975 3982 }
3976 3983
3977 3984
3978 3985 class RepoReviewRule(Base, BaseModel):
3979 3986 __tablename__ = 'repo_review_rules'
3980 3987 __table_args__ = (
3981 3988 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3982 3989 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3983 3990 )
3984 3991
3985 3992 repo_review_rule_id = Column(
3986 3993 'repo_review_rule_id', Integer(), primary_key=True)
3987 3994 repo_id = Column(
3988 3995 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3989 3996 repo = relationship('Repository', backref='review_rules')
3990 3997
3991 3998 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3992 3999 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
3993 4000
3994 4001 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
3995 4002 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
3996 4003 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
3997 4004 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
3998 4005
3999 4006 rule_users = relationship('RepoReviewRuleUser')
4000 4007 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4001 4008
4002 4009 @hybrid_property
4003 4010 def branch_pattern(self):
4004 4011 return self._branch_pattern or '*'
4005 4012
4006 4013 def _validate_glob(self, value):
4007 4014 re.compile('^' + glob2re(value) + '$')
4008 4015
4009 4016 @branch_pattern.setter
4010 4017 def branch_pattern(self, value):
4011 4018 self._validate_glob(value)
4012 4019 self._branch_pattern = value or '*'
4013 4020
4014 4021 @hybrid_property
4015 4022 def file_pattern(self):
4016 4023 return self._file_pattern or '*'
4017 4024
4018 4025 @file_pattern.setter
4019 4026 def file_pattern(self, value):
4020 4027 self._validate_glob(value)
4021 4028 self._file_pattern = value or '*'
4022 4029
4023 4030 def matches(self, branch, files_changed):
4024 4031 """
4025 4032 Check if this review rule matches a branch/files in a pull request
4026 4033
4027 4034 :param branch: branch name for the commit
4028 4035 :param files_changed: list of file paths changed in the pull request
4029 4036 """
4030 4037
4031 4038 branch = branch or ''
4032 4039 files_changed = files_changed or []
4033 4040
4034 4041 branch_matches = True
4035 4042 if branch:
4036 4043 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
4037 4044 branch_matches = bool(branch_regex.search(branch))
4038 4045
4039 4046 files_matches = True
4040 4047 if self.file_pattern != '*':
4041 4048 files_matches = False
4042 4049 file_regex = re.compile(glob2re(self.file_pattern))
4043 4050 for filename in files_changed:
4044 4051 if file_regex.search(filename):
4045 4052 files_matches = True
4046 4053 break
4047 4054
4048 4055 return branch_matches and files_matches
4049 4056
4050 4057 @property
4051 4058 def review_users(self):
4052 4059 """ Returns the users which this rule applies to """
4053 4060
4054 4061 users = collections.OrderedDict()
4055 4062
4056 4063 for rule_user in self.rule_users:
4057 4064 if rule_user.user.active:
4058 4065 if rule_user.user not in users:
4059 4066 users[rule_user.user.username] = {
4060 4067 'user': rule_user.user,
4061 4068 'source': 'user',
4062 4069 'source_data': {},
4063 4070 'data': rule_user.rule_data()
4064 4071 }
4065 4072
4066 4073 for rule_user_group in self.rule_user_groups:
4067 4074 source_data = {
4068 4075 'name': rule_user_group.users_group.users_group_name,
4069 4076 'members': len(rule_user_group.users_group.members)
4070 4077 }
4071 4078 for member in rule_user_group.users_group.members:
4072 4079 if member.user.active:
4073 4080 users[member.user.username] = {
4074 4081 'user': member.user,
4075 4082 'source': 'user_group',
4076 4083 'source_data': source_data,
4077 4084 'data': rule_user_group.rule_data()
4078 4085 }
4079 4086
4080 4087 return users
4081 4088
4082 4089 def __repr__(self):
4083 4090 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4084 4091 self.repo_review_rule_id, self.repo)
4085 4092
4086 4093
4087 4094 class DbMigrateVersion(Base, BaseModel):
4088 4095 __tablename__ = 'db_migrate_version'
4089 4096 __table_args__ = (
4090 4097 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4091 4098 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4092 4099 )
4093 4100 repository_id = Column('repository_id', String(250), primary_key=True)
4094 4101 repository_path = Column('repository_path', Text)
4095 4102 version = Column('version', Integer)
4096 4103
4097 4104
4098 4105 class DbSession(Base, BaseModel):
4099 4106 __tablename__ = 'db_session'
4100 4107 __table_args__ = (
4101 4108 {'extend_existing': True, 'mysql_engine': 'InnoDB',
4102 4109 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
4103 4110 )
4104 4111
4105 4112 def __repr__(self):
4106 4113 return '<DB:DbSession({})>'.format(self.id)
4107 4114
4108 4115 id = Column('id', Integer())
4109 4116 namespace = Column('namespace', String(255), primary_key=True)
4110 4117 accessed = Column('accessed', DateTime, nullable=False)
4111 4118 created = Column('created', DateTime, nullable=False)
4112 4119 data = Column('data', PickleType, nullable=False)
@@ -1,2385 +1,2391 b''
1 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'fonts';
9 9 @import 'variables';
10 10 @import 'bootstrap-variables';
11 11 @import 'form-bootstrap';
12 12 @import 'codemirror';
13 13 @import 'legacy_code_styles';
14 14 @import 'readme-box';
15 15 @import 'progress-bar';
16 16
17 17 @import 'type';
18 18 @import 'alerts';
19 19 @import 'buttons';
20 20 @import 'tags';
21 21 @import 'code-block';
22 22 @import 'examples';
23 23 @import 'login';
24 24 @import 'main-content';
25 25 @import 'select2';
26 26 @import 'comments';
27 27 @import 'panels-bootstrap';
28 28 @import 'panels';
29 29 @import 'deform';
30 30
31 31 //--- BASE ------------------//
32 32 .noscript-error {
33 33 top: 0;
34 34 left: 0;
35 35 width: 100%;
36 36 z-index: 101;
37 37 text-align: center;
38 38 font-family: @text-semibold;
39 39 font-size: 120%;
40 40 color: white;
41 41 background-color: @alert2;
42 42 padding: 5px 0 5px 0;
43 43 }
44 44
45 45 html {
46 46 display: table;
47 47 height: 100%;
48 48 width: 100%;
49 49 }
50 50
51 51 body {
52 52 display: table-cell;
53 53 width: 100%;
54 54 }
55 55
56 56 //--- LAYOUT ------------------//
57 57
58 58 .hidden{
59 59 display: none !important;
60 60 }
61 61
62 62 .box{
63 63 float: left;
64 64 width: 100%;
65 65 }
66 66
67 67 .browser-header {
68 68 clear: both;
69 69 }
70 70 .main {
71 71 clear: both;
72 72 padding:0 0 @pagepadding;
73 73 height: auto;
74 74
75 75 &:after { //clearfix
76 76 content:"";
77 77 clear:both;
78 78 width:100%;
79 79 display:block;
80 80 }
81 81 }
82 82
83 83 .action-link{
84 84 margin-left: @padding;
85 85 padding-left: @padding;
86 86 border-left: @border-thickness solid @border-default-color;
87 87 }
88 88
89 89 input + .action-link, .action-link.first{
90 90 border-left: none;
91 91 }
92 92
93 93 .action-link.last{
94 94 margin-right: @padding;
95 95 padding-right: @padding;
96 96 }
97 97
98 98 .action-link.active,
99 99 .action-link.active a{
100 100 color: @grey4;
101 101 }
102 102
103 103 ul.simple-list{
104 104 list-style: none;
105 105 margin: 0;
106 106 padding: 0;
107 107 }
108 108
109 109 .main-content {
110 110 padding-bottom: @pagepadding;
111 111 }
112 112
113 113 .wide-mode-wrapper {
114 114 max-width:4000px !important;
115 115 }
116 116
117 117 .wrapper {
118 118 position: relative;
119 119 max-width: @wrapper-maxwidth;
120 120 margin: 0 auto;
121 121 }
122 122
123 123 #content {
124 124 clear: both;
125 125 padding: 0 @contentpadding;
126 126 }
127 127
128 128 .advanced-settings-fields{
129 129 input{
130 130 margin-left: @textmargin;
131 131 margin-right: @padding/2;
132 132 }
133 133 }
134 134
135 135 .cs_files_title {
136 136 margin: @pagepadding 0 0;
137 137 }
138 138
139 139 input.inline[type="file"] {
140 140 display: inline;
141 141 }
142 142
143 143 .error_page {
144 144 margin: 10% auto;
145 145
146 146 h1 {
147 147 color: @grey2;
148 148 }
149 149
150 150 .alert {
151 151 margin: @padding 0;
152 152 }
153 153
154 154 .error-branding {
155 155 font-family: @text-semibold;
156 156 color: @grey4;
157 157 }
158 158
159 159 .error_message {
160 160 font-family: @text-regular;
161 161 }
162 162
163 163 .sidebar {
164 164 min-height: 275px;
165 165 margin: 0;
166 166 padding: 0 0 @sidebarpadding @sidebarpadding;
167 167 border: none;
168 168 }
169 169
170 170 .main-content {
171 171 position: relative;
172 172 margin: 0 @sidebarpadding @sidebarpadding;
173 173 padding: 0 0 0 @sidebarpadding;
174 174 border-left: @border-thickness solid @grey5;
175 175
176 176 @media (max-width:767px) {
177 177 clear: both;
178 178 width: 100%;
179 179 margin: 0;
180 180 border: none;
181 181 }
182 182 }
183 183
184 184 .inner-column {
185 185 float: left;
186 186 width: 29.75%;
187 187 min-height: 150px;
188 188 margin: @sidebarpadding 2% 0 0;
189 189 padding: 0 2% 0 0;
190 190 border-right: @border-thickness solid @grey5;
191 191
192 192 @media (max-width:767px) {
193 193 clear: both;
194 194 width: 100%;
195 195 border: none;
196 196 }
197 197
198 198 ul {
199 199 padding-left: 1.25em;
200 200 }
201 201
202 202 &:last-child {
203 203 margin: @sidebarpadding 0 0;
204 204 border: none;
205 205 }
206 206
207 207 h4 {
208 208 margin: 0 0 @padding;
209 209 font-family: @text-semibold;
210 210 }
211 211 }
212 212 }
213 213 .error-page-logo {
214 214 width: 130px;
215 215 height: 160px;
216 216 }
217 217
218 218 // HEADER
219 219 .header {
220 220
221 221 // TODO: johbo: Fix login pages, so that they work without a min-height
222 222 // for the header and then remove the min-height. I chose a smaller value
223 223 // intentionally here to avoid rendering issues in the main navigation.
224 224 min-height: 49px;
225 225
226 226 position: relative;
227 227 vertical-align: bottom;
228 228 padding: 0 @header-padding;
229 229 background-color: @grey2;
230 230 color: @grey5;
231 231
232 232 .title {
233 233 overflow: visible;
234 234 }
235 235
236 236 &:before,
237 237 &:after {
238 238 content: "";
239 239 clear: both;
240 240 width: 100%;
241 241 }
242 242
243 243 // TODO: johbo: Avoids breaking "Repositories" chooser
244 244 .select2-container .select2-choice .select2-arrow {
245 245 display: none;
246 246 }
247 247 }
248 248
249 249 #header-inner {
250 250 &.title {
251 251 margin: 0;
252 252 }
253 253 &:before,
254 254 &:after {
255 255 content: "";
256 256 clear: both;
257 257 }
258 258 }
259 259
260 260 // Gists
261 261 #files_data {
262 262 clear: both; //for firefox
263 263 }
264 264 #gistid {
265 265 margin-right: @padding;
266 266 }
267 267
268 268 // Global Settings Editor
269 269 .textarea.editor {
270 270 float: left;
271 271 position: relative;
272 272 max-width: @texteditor-width;
273 273
274 274 select {
275 275 position: absolute;
276 276 top:10px;
277 277 right:0;
278 278 }
279 279
280 280 .CodeMirror {
281 281 margin: 0;
282 282 }
283 283
284 284 .help-block {
285 285 margin: 0 0 @padding;
286 286 padding:.5em;
287 287 background-color: @grey6;
288 &.pre-formatting {
289 white-space: pre;
290 }
288 291 }
289 292 }
290 293
291 294 ul.auth_plugins {
292 295 margin: @padding 0 @padding @legend-width;
293 296 padding: 0;
294 297
295 298 li {
296 299 margin-bottom: @padding;
297 300 line-height: 1em;
298 301 list-style-type: none;
299 302
300 303 .auth_buttons .btn {
301 304 margin-right: @padding;
302 305 }
303 306
304 307 &:before { content: none; }
305 308 }
306 309 }
307 310
308 311
309 312 // My Account PR list
310 313
311 314 #show_closed {
312 315 margin: 0 1em 0 0;
313 316 }
314 317
315 318 .pullrequestlist {
316 319 .closed {
317 320 background-color: @grey6;
318 321 }
319 322 .td-status {
320 323 padding-left: .5em;
321 324 }
322 325 .log-container .truncate {
323 326 height: 2.75em;
324 327 white-space: pre-line;
325 328 }
326 329 table.rctable .user {
327 330 padding-left: 0;
328 331 }
329 332 table.rctable {
330 333 td.td-description,
331 334 .rc-user {
332 335 min-width: auto;
333 336 }
334 337 }
335 338 }
336 339
337 340 // Pull Requests
338 341
339 342 .pullrequests_section_head {
340 343 display: block;
341 344 clear: both;
342 345 margin: @padding 0;
343 346 font-family: @text-bold;
344 347 }
345 348
346 349 .pr-origininfo, .pr-targetinfo {
347 350 position: relative;
348 351
349 352 .tag {
350 353 display: inline-block;
351 354 margin: 0 1em .5em 0;
352 355 }
353 356
354 357 .clone-url {
355 358 display: inline-block;
356 359 margin: 0 0 .5em 0;
357 360 padding: 0;
358 361 line-height: 1.2em;
359 362 }
360 363 }
361 364
362 365 .pr-mergeinfo {
363 366 clear: both;
364 367 margin: .5em 0;
365 368
366 369 input {
367 370 min-width: 100% !important;
368 371 padding: 0 !important;
369 372 border: 0;
370 373 }
371 374 }
372 375
373 376 .pr-pullinfo {
374 377 clear: both;
375 378 margin: .5em 0;
376 379
377 380 input {
378 381 min-width: 100% !important;
379 382 padding: 0 !important;
380 383 border: 0;
381 384 }
382 385 }
383 386
384 387 #pr-title-input {
385 388 width: 72%;
386 389 font-size: 1em;
387 390 font-family: @text-bold;
388 391 margin: 0;
389 392 padding: 0 0 0 @padding/4;
390 393 line-height: 1.7em;
391 394 color: @text-color;
392 395 letter-spacing: .02em;
393 396 }
394 397
395 398 #pullrequest_title {
396 399 width: 100%;
397 400 box-sizing: border-box;
398 401 }
399 402
400 403 #pr_open_message {
401 404 border: @border-thickness solid #fff;
402 405 border-radius: @border-radius;
403 406 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
404 407 text-align: left;
405 408 overflow: hidden;
406 409 }
407 410
408 411 .pr-submit-button {
409 412 float: right;
410 413 margin: 0 0 0 5px;
411 414 }
412 415
413 416 .pr-spacing-container {
414 417 padding: 20px;
415 418 clear: both
416 419 }
417 420
418 421 #pr-description-input {
419 422 margin-bottom: 0;
420 423 }
421 424
422 425 .pr-description-label {
423 426 vertical-align: top;
424 427 }
425 428
426 429 .perms_section_head {
427 430 min-width: 625px;
428 431
429 432 h2 {
430 433 margin-bottom: 0;
431 434 }
432 435
433 436 .label-checkbox {
434 437 float: left;
435 438 }
436 439
437 440 &.field {
438 441 margin: @space 0 @padding;
439 442 }
440 443
441 444 &:first-child.field {
442 445 margin-top: 0;
443 446
444 447 .label {
445 448 margin-top: 0;
446 449 padding-top: 0;
447 450 }
448 451
449 452 .radios {
450 453 padding-top: 0;
451 454 }
452 455 }
453 456
454 457 .radios {
455 458 float: right;
456 459 position: relative;
457 460 width: 405px;
458 461 }
459 462 }
460 463
461 464 //--- MODULES ------------------//
462 465
463 466
464 467 // Server Announcement
465 468 #server-announcement {
466 469 width: 95%;
467 470 margin: @padding auto;
468 471 padding: @padding;
469 472 border-width: 2px;
470 473 border-style: solid;
471 474 .border-radius(2px);
472 475 font-family: @text-bold;
473 476
474 477 &.info { border-color: @alert4; background-color: @alert4-inner; }
475 478 &.warning { border-color: @alert3; background-color: @alert3-inner; }
476 479 &.error { border-color: @alert2; background-color: @alert2-inner; }
477 480 &.success { border-color: @alert1; background-color: @alert1-inner; }
478 481 &.neutral { border-color: @grey3; background-color: @grey6; }
479 482 }
480 483
481 484 // Fixed Sidebar Column
482 485 .sidebar-col-wrapper {
483 486 padding-left: @sidebar-all-width;
484 487
485 488 .sidebar {
486 489 width: @sidebar-width;
487 490 margin-left: -@sidebar-all-width;
488 491 }
489 492 }
490 493
491 494 .sidebar-col-wrapper.scw-small {
492 495 padding-left: @sidebar-small-all-width;
493 496
494 497 .sidebar {
495 498 width: @sidebar-small-width;
496 499 margin-left: -@sidebar-small-all-width;
497 500 }
498 501 }
499 502
500 503
501 504 // FOOTER
502 505 #footer {
503 506 padding: 0;
504 507 text-align: center;
505 508 vertical-align: middle;
506 509 color: @grey2;
507 510 background-color: @grey6;
508 511
509 512 p {
510 513 margin: 0;
511 514 padding: 1em;
512 515 line-height: 1em;
513 516 }
514 517
515 518 .server-instance { //server instance
516 519 display: none;
517 520 }
518 521
519 522 .title {
520 523 float: none;
521 524 margin: 0 auto;
522 525 }
523 526 }
524 527
525 528 button.close {
526 529 padding: 0;
527 530 cursor: pointer;
528 531 background: transparent;
529 532 border: 0;
530 533 .box-shadow(none);
531 534 -webkit-appearance: none;
532 535 }
533 536
534 537 .close {
535 538 float: right;
536 539 font-size: 21px;
537 540 font-family: @text-bootstrap;
538 541 line-height: 1em;
539 542 font-weight: bold;
540 543 color: @grey2;
541 544
542 545 &:hover,
543 546 &:focus {
544 547 color: @grey1;
545 548 text-decoration: none;
546 549 cursor: pointer;
547 550 }
548 551 }
549 552
550 553 // GRID
551 554 .sorting,
552 555 .sorting_desc,
553 556 .sorting_asc {
554 557 cursor: pointer;
555 558 }
556 559 .sorting_desc:after {
557 560 content: "\00A0\25B2";
558 561 font-size: .75em;
559 562 }
560 563 .sorting_asc:after {
561 564 content: "\00A0\25BC";
562 565 font-size: .68em;
563 566 }
564 567
565 568
566 569 .user_auth_tokens {
567 570
568 571 &.truncate {
569 572 white-space: nowrap;
570 573 overflow: hidden;
571 574 text-overflow: ellipsis;
572 575 }
573 576
574 577 .fields .field .input {
575 578 margin: 0;
576 579 }
577 580
578 581 input#description {
579 582 width: 100px;
580 583 margin: 0;
581 584 }
582 585
583 586 .drop-menu {
584 587 // TODO: johbo: Remove this, should work out of the box when
585 588 // having multiple inputs inline
586 589 margin: 0 0 0 5px;
587 590 }
588 591 }
589 592 #user_list_table {
590 593 .closed {
591 594 background-color: @grey6;
592 595 }
593 596 }
594 597
595 598
596 599 input {
597 600 &.disabled {
598 601 opacity: .5;
599 602 }
600 603 }
601 604
602 605 // remove extra padding in firefox
603 606 input::-moz-focus-inner { border:0; padding:0 }
604 607
605 608 .adjacent input {
606 609 margin-bottom: @padding;
607 610 }
608 611
609 612 .permissions_boxes {
610 613 display: block;
611 614 }
612 615
613 616 //TODO: lisa: this should be in tables
614 617 .show_more_col {
615 618 width: 20px;
616 619 }
617 620
618 621 //FORMS
619 622
620 623 .medium-inline,
621 624 input#description.medium-inline {
622 625 display: inline;
623 626 width: @medium-inline-input-width;
624 627 min-width: 100px;
625 628 }
626 629
627 630 select {
628 631 //reset
629 632 -webkit-appearance: none;
630 633 -moz-appearance: none;
631 634
632 635 display: inline-block;
633 636 height: 28px;
634 637 width: auto;
635 638 margin: 0 @padding @padding 0;
636 639 padding: 0 18px 0 8px;
637 640 line-height:1em;
638 641 font-size: @basefontsize;
639 642 border: @border-thickness solid @rcblue;
640 643 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
641 644 color: @rcblue;
642 645
643 646 &:after {
644 647 content: "\00A0\25BE";
645 648 }
646 649
647 650 &:focus {
648 651 outline: none;
649 652 }
650 653 }
651 654
652 655 option {
653 656 &:focus {
654 657 outline: none;
655 658 }
656 659 }
657 660
658 661 input,
659 662 textarea {
660 663 padding: @input-padding;
661 664 border: @input-border-thickness solid @border-highlight-color;
662 665 .border-radius (@border-radius);
663 666 font-family: @text-light;
664 667 font-size: @basefontsize;
665 668
666 669 &.input-sm {
667 670 padding: 5px;
668 671 }
669 672
670 673 &#description {
671 674 min-width: @input-description-minwidth;
672 675 min-height: 1em;
673 676 padding: 10px;
674 677 }
675 678 }
676 679
677 680 .field-sm {
678 681 input,
679 682 textarea {
680 683 padding: 5px;
681 684 }
682 685 }
683 686
684 687 textarea {
685 688 display: block;
686 689 clear: both;
687 690 width: 100%;
688 691 min-height: 100px;
689 692 margin-bottom: @padding;
690 693 .box-sizing(border-box);
691 694 overflow: auto;
692 695 }
693 696
694 697 label {
695 698 font-family: @text-light;
696 699 }
697 700
698 701 // GRAVATARS
699 702 // centers gravatar on username to the right
700 703
701 704 .gravatar {
702 705 display: inline;
703 706 min-width: 16px;
704 707 min-height: 16px;
705 708 margin: -5px 0;
706 709 padding: 0;
707 710 line-height: 1em;
708 711 border: 1px solid @grey4;
709 712 box-sizing: content-box;
710 713
711 714 &.gravatar-large {
712 715 margin: -0.5em .25em -0.5em 0;
713 716 }
714 717
715 718 & + .user {
716 719 display: inline;
717 720 margin: 0;
718 721 padding: 0 0 0 .17em;
719 722 line-height: 1em;
720 723 }
721 724 }
722 725
723 726 .user-inline-data {
724 727 display: inline-block;
725 728 float: left;
726 729 padding-left: .5em;
727 730 line-height: 1.3em;
728 731 }
729 732
730 733 .rc-user { // gravatar + user wrapper
731 734 float: left;
732 735 position: relative;
733 736 min-width: 100px;
734 737 max-width: 200px;
735 738 min-height: (@gravatar-size + @border-thickness * 2); // account for border
736 739 display: block;
737 740 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
738 741
739 742
740 743 .gravatar {
741 744 display: block;
742 745 position: absolute;
743 746 top: 0;
744 747 left: 0;
745 748 min-width: @gravatar-size;
746 749 min-height: @gravatar-size;
747 750 margin: 0;
748 751 }
749 752
750 753 .user {
751 754 display: block;
752 755 max-width: 175px;
753 756 padding-top: 2px;
754 757 overflow: hidden;
755 758 text-overflow: ellipsis;
756 759 }
757 760 }
758 761
759 762 .gist-gravatar,
760 763 .journal_container {
761 764 .gravatar-large {
762 765 margin: 0 .5em -10px 0;
763 766 }
764 767 }
765 768
766 769
767 770 // ADMIN SETTINGS
768 771
769 772 // Tag Patterns
770 773 .tag_patterns {
771 774 .tag_input {
772 775 margin-bottom: @padding;
773 776 }
774 777 }
775 778
776 779 .locked_input {
777 780 position: relative;
778 781
779 782 input {
780 783 display: inline;
781 784 margin: 3px 5px 0px 0px;
782 785 }
783 786
784 787 br {
785 788 display: none;
786 789 }
787 790
788 791 .error-message {
789 792 float: left;
790 793 width: 100%;
791 794 }
792 795
793 796 .lock_input_button {
794 797 display: inline;
795 798 }
796 799
797 800 .help-block {
798 801 clear: both;
799 802 }
800 803 }
801 804
802 805 // Notifications
803 806
804 807 .notifications_buttons {
805 808 margin: 0 0 @space 0;
806 809 padding: 0;
807 810
808 811 .btn {
809 812 display: inline-block;
810 813 }
811 814 }
812 815
813 816 .notification-list {
814 817
815 818 div {
816 819 display: inline-block;
817 820 vertical-align: middle;
818 821 }
819 822
820 823 .container {
821 824 display: block;
822 825 margin: 0 0 @padding 0;
823 826 }
824 827
825 828 .delete-notifications {
826 829 margin-left: @padding;
827 830 text-align: right;
828 831 cursor: pointer;
829 832 }
830 833
831 834 .read-notifications {
832 835 margin-left: @padding/2;
833 836 text-align: right;
834 837 width: 35px;
835 838 cursor: pointer;
836 839 }
837 840
838 841 .icon-minus-sign {
839 842 color: @alert2;
840 843 }
841 844
842 845 .icon-ok-sign {
843 846 color: @alert1;
844 847 }
845 848 }
846 849
847 850 .user_settings {
848 851 float: left;
849 852 clear: both;
850 853 display: block;
851 854 width: 100%;
852 855
853 856 .gravatar_box {
854 857 margin-bottom: @padding;
855 858
856 859 &:after {
857 860 content: " ";
858 861 clear: both;
859 862 width: 100%;
860 863 }
861 864 }
862 865
863 866 .fields .field {
864 867 clear: both;
865 868 }
866 869 }
867 870
868 871 .advanced_settings {
869 872 margin-bottom: @space;
870 873
871 874 .help-block {
872 875 margin-left: 0;
873 876 }
874 877
875 878 button + .help-block {
876 879 margin-top: @padding;
877 880 }
878 881 }
879 882
880 883 // admin settings radio buttons and labels
881 884 .label-2 {
882 885 float: left;
883 886 width: @label2-width;
884 887
885 888 label {
886 889 color: @grey1;
887 890 }
888 891 }
889 892 .checkboxes {
890 893 float: left;
891 894 width: @checkboxes-width;
892 895 margin-bottom: @padding;
893 896
894 897 .checkbox {
895 898 width: 100%;
896 899
897 900 label {
898 901 margin: 0;
899 902 padding: 0;
900 903 }
901 904 }
902 905
903 906 .checkbox + .checkbox {
904 907 display: inline-block;
905 908 }
906 909
907 910 label {
908 911 margin-right: 1em;
909 912 }
910 913 }
911 914
912 915 // CHANGELOG
913 916 .container_header {
914 917 float: left;
915 918 display: block;
916 919 width: 100%;
917 920 margin: @padding 0 @padding;
918 921
919 922 #filter_changelog {
920 923 float: left;
921 924 margin-right: @padding;
922 925 }
923 926
924 927 .breadcrumbs_light {
925 928 display: inline-block;
926 929 }
927 930 }
928 931
929 932 .info_box {
930 933 float: right;
931 934 }
932 935
933 936
934 937 #graph_nodes {
935 938 padding-top: 43px;
936 939 }
937 940
938 941 #graph_content{
939 942
940 943 // adjust for table headers so that graph renders properly
941 944 // #graph_nodes padding - table cell padding
942 945 padding-top: (@space - (@basefontsize * 2.4));
943 946
944 947 &.graph_full_width {
945 948 width: 100%;
946 949 max-width: 100%;
947 950 }
948 951 }
949 952
950 953 #graph {
951 954 .flag_status {
952 955 margin: 0;
953 956 }
954 957
955 958 .pagination-left {
956 959 float: left;
957 960 clear: both;
958 961 }
959 962
960 963 .log-container {
961 964 max-width: 345px;
962 965
963 966 .message{
964 967 max-width: 340px;
965 968 }
966 969 }
967 970
968 971 .graph-col-wrapper {
969 972 padding-left: 110px;
970 973
971 974 #graph_nodes {
972 975 width: 100px;
973 976 margin-left: -110px;
974 977 float: left;
975 978 clear: left;
976 979 }
977 980 }
978 981
979 982 .load-more-commits {
980 983 text-align: center;
981 984 }
982 985 .load-more-commits:hover {
983 986 background-color: @grey7;
984 987 }
985 988 .load-more-commits {
986 989 a {
987 990 display: block;
988 991 }
989 992 }
990 993 }
991 994
992 995 #filter_changelog {
993 996 float: left;
994 997 }
995 998
996 999
997 1000 //--- THEME ------------------//
998 1001
999 1002 #logo {
1000 1003 float: left;
1001 1004 margin: 9px 0 0 0;
1002 1005
1003 1006 .header {
1004 1007 background-color: transparent;
1005 1008 }
1006 1009
1007 1010 a {
1008 1011 display: inline-block;
1009 1012 }
1010 1013
1011 1014 img {
1012 1015 height:30px;
1013 1016 }
1014 1017 }
1015 1018
1016 1019 .logo-wrapper {
1017 1020 float:left;
1018 1021 }
1019 1022
1020 1023 .branding{
1021 1024 float: left;
1022 1025 padding: 9px 2px;
1023 1026 line-height: 1em;
1024 1027 font-size: @navigation-fontsize;
1025 1028 }
1026 1029
1027 1030 img {
1028 1031 border: none;
1029 1032 outline: none;
1030 1033 }
1031 1034 user-profile-header
1032 1035 label {
1033 1036
1034 1037 input[type="checkbox"] {
1035 1038 margin-right: 1em;
1036 1039 }
1037 1040 input[type="radio"] {
1038 1041 margin-right: 1em;
1039 1042 }
1040 1043 }
1041 1044
1042 1045 .flag_status {
1043 1046 margin: 2px 8px 6px 2px;
1044 1047 &.under_review {
1045 1048 .circle(5px, @alert3);
1046 1049 }
1047 1050 &.approved {
1048 1051 .circle(5px, @alert1);
1049 1052 }
1050 1053 &.rejected,
1051 1054 &.forced_closed{
1052 1055 .circle(5px, @alert2);
1053 1056 }
1054 1057 &.not_reviewed {
1055 1058 .circle(5px, @grey5);
1056 1059 }
1057 1060 }
1058 1061
1059 1062 .flag_status_comment_box {
1060 1063 margin: 5px 6px 0px 2px;
1061 1064 }
1062 1065 .test_pattern_preview {
1063 1066 margin: @space 0;
1064 1067
1065 1068 p {
1066 1069 margin-bottom: 0;
1067 1070 border-bottom: @border-thickness solid @border-default-color;
1068 1071 color: @grey3;
1069 1072 }
1070 1073
1071 1074 .btn {
1072 1075 margin-bottom: @padding;
1073 1076 }
1074 1077 }
1075 1078 #test_pattern_result {
1076 1079 display: none;
1077 1080 &:extend(pre);
1078 1081 padding: .9em;
1079 1082 color: @grey3;
1080 1083 background-color: @grey7;
1081 1084 border-right: @border-thickness solid @border-default-color;
1082 1085 border-bottom: @border-thickness solid @border-default-color;
1083 1086 border-left: @border-thickness solid @border-default-color;
1084 1087 }
1085 1088
1086 1089 #repo_vcs_settings {
1087 1090 #inherit_overlay_vcs_default {
1088 1091 display: none;
1089 1092 }
1090 1093 #inherit_overlay_vcs_custom {
1091 1094 display: custom;
1092 1095 }
1093 1096 &.inherited {
1094 1097 #inherit_overlay_vcs_default {
1095 1098 display: block;
1096 1099 }
1097 1100 #inherit_overlay_vcs_custom {
1098 1101 display: none;
1099 1102 }
1100 1103 }
1101 1104 }
1102 1105
1103 1106 .issue-tracker-link {
1104 1107 color: @rcblue;
1105 1108 }
1106 1109
1107 1110 // Issue Tracker Table Show/Hide
1108 1111 #repo_issue_tracker {
1109 1112 #inherit_overlay {
1110 1113 display: none;
1111 1114 }
1112 1115 #custom_overlay {
1113 1116 display: custom;
1114 1117 }
1115 1118 &.inherited {
1116 1119 #inherit_overlay {
1117 1120 display: block;
1118 1121 }
1119 1122 #custom_overlay {
1120 1123 display: none;
1121 1124 }
1122 1125 }
1123 1126 }
1124 1127 table.issuetracker {
1125 1128 &.readonly {
1126 1129 tr, td {
1127 1130 color: @grey3;
1128 1131 }
1129 1132 }
1130 1133 .edit {
1131 1134 display: none;
1132 1135 }
1133 1136 .editopen {
1134 1137 .edit {
1135 1138 display: inline;
1136 1139 }
1137 1140 .entry {
1138 1141 display: none;
1139 1142 }
1140 1143 }
1141 1144 tr td.td-action {
1142 1145 min-width: 117px;
1143 1146 }
1144 1147 td input {
1145 1148 max-width: none;
1146 1149 min-width: 30px;
1147 1150 width: 80%;
1148 1151 }
1149 1152 .issuetracker_pref input {
1150 1153 width: 40%;
1151 1154 }
1152 1155 input.edit_issuetracker_update {
1153 1156 margin-right: 0;
1154 1157 width: auto;
1155 1158 }
1156 1159 }
1157 1160
1158 1161 table.integrations {
1159 1162 .td-icon {
1160 1163 width: 20px;
1161 1164 .integration-icon {
1162 1165 height: 20px;
1163 1166 width: 20px;
1164 1167 }
1165 1168 }
1166 1169 }
1167 1170
1168 1171 .integrations {
1169 1172 a.integration-box {
1170 1173 color: @text-color;
1171 1174 &:hover {
1172 1175 .panel {
1173 1176 background: #fbfbfb;
1174 1177 }
1175 1178 }
1176 1179 .integration-icon {
1177 1180 width: 30px;
1178 1181 height: 30px;
1179 1182 margin-right: 20px;
1180 1183 float: left;
1181 1184 }
1182 1185
1183 1186 .panel-body {
1184 1187 padding: 10px;
1185 1188 }
1186 1189 .panel {
1187 1190 margin-bottom: 10px;
1188 1191 }
1189 1192 h2 {
1190 1193 display: inline-block;
1191 1194 margin: 0;
1192 1195 min-width: 140px;
1193 1196 }
1194 1197 }
1195 1198 }
1196 1199
1197 1200 //Permissions Settings
1198 1201 #add_perm {
1199 1202 margin: 0 0 @padding;
1200 1203 cursor: pointer;
1201 1204 }
1202 1205
1203 1206 .perm_ac {
1204 1207 input {
1205 1208 width: 95%;
1206 1209 }
1207 1210 }
1208 1211
1209 1212 .autocomplete-suggestions {
1210 1213 width: auto !important; // overrides autocomplete.js
1211 1214 margin: 0;
1212 1215 border: @border-thickness solid @rcblue;
1213 1216 border-radius: @border-radius;
1214 1217 color: @rcblue;
1215 1218 background-color: white;
1216 1219 }
1217 1220 .autocomplete-selected {
1218 1221 background: #F0F0F0;
1219 1222 }
1220 1223 .ac-container-wrap {
1221 1224 margin: 0;
1222 1225 padding: 8px;
1223 1226 border-bottom: @border-thickness solid @rclightblue;
1224 1227 list-style-type: none;
1225 1228 cursor: pointer;
1226 1229
1227 1230 &:hover {
1228 1231 background-color: @rclightblue;
1229 1232 }
1230 1233
1231 1234 img {
1232 1235 height: @gravatar-size;
1233 1236 width: @gravatar-size;
1234 1237 margin-right: 1em;
1235 1238 }
1236 1239
1237 1240 strong {
1238 1241 font-weight: normal;
1239 1242 }
1240 1243 }
1241 1244
1242 1245 // Settings Dropdown
1243 1246 .user-menu .container {
1244 1247 padding: 0 4px;
1245 1248 margin: 0;
1246 1249 }
1247 1250
1248 1251 .user-menu .gravatar {
1249 1252 cursor: pointer;
1250 1253 }
1251 1254
1252 1255 .codeblock {
1253 1256 margin-bottom: @padding;
1254 1257 clear: both;
1255 1258
1256 1259 .stats{
1257 1260 overflow: hidden;
1258 1261 }
1259 1262
1260 1263 .message{
1261 1264 textarea{
1262 1265 margin: 0;
1263 1266 }
1264 1267 }
1265 1268
1266 1269 .code-header {
1267 1270 .stats {
1268 1271 line-height: 2em;
1269 1272
1270 1273 .revision_id {
1271 1274 margin-left: 0;
1272 1275 }
1273 1276 .buttons {
1274 1277 padding-right: 0;
1275 1278 }
1276 1279 }
1277 1280
1278 1281 .item{
1279 1282 margin-right: 0.5em;
1280 1283 }
1281 1284 }
1282 1285
1283 1286 #editor_container{
1284 1287 position: relative;
1285 1288 margin: @padding;
1286 1289 }
1287 1290 }
1288 1291
1289 1292 #file_history_container {
1290 1293 display: none;
1291 1294 }
1292 1295
1293 1296 .file-history-inner {
1294 1297 margin-bottom: 10px;
1295 1298 }
1296 1299
1297 1300 // Pull Requests
1298 1301 .summary-details {
1299 1302 width: 72%;
1300 1303 }
1301 1304 .pr-summary {
1302 1305 border-bottom: @border-thickness solid @grey5;
1303 1306 margin-bottom: @space;
1304 1307 }
1305 1308 .reviewers-title {
1306 1309 width: 25%;
1307 1310 min-width: 200px;
1308 1311 }
1309 1312 .reviewers {
1310 1313 width: 25%;
1311 1314 min-width: 200px;
1312 1315 }
1313 1316 .reviewers ul li {
1314 1317 position: relative;
1315 1318 width: 100%;
1316 1319 margin-bottom: 8px;
1317 1320 }
1318 1321
1319 1322 .reviewer_entry {
1320 1323 min-height: 55px;
1321 1324 }
1322 1325
1323 1326 .reviewers_member {
1324 1327 width: 100%;
1325 1328 overflow: auto;
1326 1329 }
1327 1330 .reviewer_reason {
1328 1331 padding-left: 20px;
1329 1332 }
1330 1333 .reviewer_status {
1331 1334 display: inline-block;
1332 1335 vertical-align: top;
1333 1336 width: 7%;
1334 1337 min-width: 20px;
1335 1338 height: 1.2em;
1336 1339 margin-top: 3px;
1337 1340 line-height: 1em;
1338 1341 }
1339 1342
1340 1343 .reviewer_name {
1341 1344 display: inline-block;
1342 1345 max-width: 83%;
1343 1346 padding-right: 20px;
1344 1347 vertical-align: middle;
1345 1348 line-height: 1;
1346 1349
1347 1350 .rc-user {
1348 1351 min-width: 0;
1349 1352 margin: -2px 1em 0 0;
1350 1353 }
1351 1354
1352 1355 .reviewer {
1353 1356 float: left;
1354 1357 }
1355 1358 }
1356 1359
1357 1360 .reviewer_member_mandatory,
1358 1361 .reviewer_member_mandatory_remove,
1359 1362 .reviewer_member_remove {
1360 1363 position: absolute;
1361 1364 right: 0;
1362 1365 top: 0;
1363 1366 width: 16px;
1364 1367 margin-bottom: 10px;
1365 1368 padding: 0;
1366 1369 color: black;
1367 1370 }
1368 1371
1369 1372 .reviewer_member_mandatory_remove {
1370 1373 color: @grey4;
1371 1374 }
1372 1375
1373 1376 .reviewer_member_mandatory {
1374 1377 padding-top:20px;
1375 1378 }
1376 1379
1377 1380 .reviewer_member_status {
1378 1381 margin-top: 5px;
1379 1382 }
1380 1383 .pr-summary #summary{
1381 1384 width: 100%;
1382 1385 }
1383 1386 .pr-summary .action_button:hover {
1384 1387 border: 0;
1385 1388 cursor: pointer;
1386 1389 }
1387 1390 .pr-details-title {
1388 1391 padding-bottom: 8px;
1389 1392 border-bottom: @border-thickness solid @grey5;
1390 1393
1391 1394 .action_button.disabled {
1392 1395 color: @grey4;
1393 1396 cursor: inherit;
1394 1397 }
1395 1398 .action_button {
1396 1399 color: @rcblue;
1397 1400 }
1398 1401 }
1399 1402 .pr-details-content {
1400 1403 margin-top: @textmargin;
1401 1404 margin-bottom: @textmargin;
1402 1405 }
1403 1406 .pr-description {
1404 1407 white-space:pre-wrap;
1405 1408 }
1406 1409
1407 1410 .pr-reviewer-rules {
1408 1411 padding: 10px 0px 20px 0px;
1409 1412 }
1410 1413
1411 1414 .group_members {
1412 1415 margin-top: 0;
1413 1416 padding: 0;
1414 1417 list-style: outside none none;
1415 1418
1416 1419 img {
1417 1420 height: @gravatar-size;
1418 1421 width: @gravatar-size;
1419 1422 margin-right: .5em;
1420 1423 margin-left: 3px;
1421 1424 }
1422 1425
1423 1426 .to-delete {
1424 1427 .user {
1425 1428 text-decoration: line-through;
1426 1429 }
1427 1430 }
1428 1431 }
1429 1432
1430 1433 .compare_view_commits_title {
1431 1434 .disabled {
1432 1435 cursor: inherit;
1433 1436 &:hover{
1434 1437 background-color: inherit;
1435 1438 color: inherit;
1436 1439 }
1437 1440 }
1438 1441 }
1439 1442
1440 1443 .subtitle-compare {
1441 1444 margin: -15px 0px 0px 0px;
1442 1445 }
1443 1446
1444 1447 .comments-summary-td {
1445 1448 border-top: 1px dashed @grey5;
1446 1449 }
1447 1450
1448 1451 // new entry in group_members
1449 1452 .td-author-new-entry {
1450 1453 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1451 1454 }
1452 1455
1453 1456 .usergroup_member_remove {
1454 1457 width: 16px;
1455 1458 margin-bottom: 10px;
1456 1459 padding: 0;
1457 1460 color: black !important;
1458 1461 cursor: pointer;
1459 1462 }
1460 1463
1461 1464 .reviewer_ac .ac-input {
1462 1465 width: 92%;
1463 1466 margin-bottom: 1em;
1464 1467 }
1465 1468
1466 1469 .compare_view_commits tr{
1467 1470 height: 20px;
1468 1471 }
1469 1472 .compare_view_commits td {
1470 1473 vertical-align: top;
1471 1474 padding-top: 10px;
1472 1475 }
1473 1476 .compare_view_commits .author {
1474 1477 margin-left: 5px;
1475 1478 }
1476 1479
1477 1480 .compare_view_commits {
1478 1481 .color-a {
1479 1482 color: @alert1;
1480 1483 }
1481 1484
1482 1485 .color-c {
1483 1486 color: @color3;
1484 1487 }
1485 1488
1486 1489 .color-r {
1487 1490 color: @color5;
1488 1491 }
1489 1492
1490 1493 .color-a-bg {
1491 1494 background-color: @alert1;
1492 1495 }
1493 1496
1494 1497 .color-c-bg {
1495 1498 background-color: @alert3;
1496 1499 }
1497 1500
1498 1501 .color-r-bg {
1499 1502 background-color: @alert2;
1500 1503 }
1501 1504
1502 1505 .color-a-border {
1503 1506 border: 1px solid @alert1;
1504 1507 }
1505 1508
1506 1509 .color-c-border {
1507 1510 border: 1px solid @alert3;
1508 1511 }
1509 1512
1510 1513 .color-r-border {
1511 1514 border: 1px solid @alert2;
1512 1515 }
1513 1516
1514 1517 .commit-change-indicator {
1515 1518 width: 15px;
1516 1519 height: 15px;
1517 1520 position: relative;
1518 1521 left: 15px;
1519 1522 }
1520 1523
1521 1524 .commit-change-content {
1522 1525 text-align: center;
1523 1526 vertical-align: middle;
1524 1527 line-height: 15px;
1525 1528 }
1526 1529 }
1527 1530
1528 1531 .compare_view_files {
1529 1532 width: 100%;
1530 1533
1531 1534 td {
1532 1535 vertical-align: middle;
1533 1536 }
1534 1537 }
1535 1538
1536 1539 .compare_view_filepath {
1537 1540 color: @grey1;
1538 1541 }
1539 1542
1540 1543 .show_more {
1541 1544 display: inline-block;
1542 1545 position: relative;
1543 1546 vertical-align: middle;
1544 1547 width: 4px;
1545 1548 height: @basefontsize;
1546 1549
1547 1550 &:after {
1548 1551 content: "\00A0\25BE";
1549 1552 display: inline-block;
1550 1553 width:10px;
1551 1554 line-height: 5px;
1552 1555 font-size: 12px;
1553 1556 cursor: pointer;
1554 1557 }
1555 1558 }
1556 1559
1557 1560 .journal_more .show_more {
1558 1561 display: inline;
1559 1562
1560 1563 &:after {
1561 1564 content: none;
1562 1565 }
1563 1566 }
1564 1567
1565 1568 .open .show_more:after,
1566 1569 .select2-dropdown-open .show_more:after {
1567 1570 .rotate(180deg);
1568 1571 margin-left: 4px;
1569 1572 }
1570 1573
1571 1574
1572 1575 .compare_view_commits .collapse_commit:after {
1573 1576 cursor: pointer;
1574 1577 content: "\00A0\25B4";
1575 1578 margin-left: -3px;
1576 1579 font-size: 17px;
1577 1580 color: @grey4;
1578 1581 }
1579 1582
1580 1583 .diff_links {
1581 1584 margin-left: 8px;
1582 1585 }
1583 1586
1584 1587 div.ancestor {
1585 1588 margin: -30px 0px;
1586 1589 }
1587 1590
1588 1591 .cs_icon_td input[type="checkbox"] {
1589 1592 display: none;
1590 1593 }
1591 1594
1592 1595 .cs_icon_td .expand_file_icon:after {
1593 1596 cursor: pointer;
1594 1597 content: "\00A0\25B6";
1595 1598 font-size: 12px;
1596 1599 color: @grey4;
1597 1600 }
1598 1601
1599 1602 .cs_icon_td .collapse_file_icon:after {
1600 1603 cursor: pointer;
1601 1604 content: "\00A0\25BC";
1602 1605 font-size: 12px;
1603 1606 color: @grey4;
1604 1607 }
1605 1608
1606 1609 /*new binary
1607 1610 NEW_FILENODE = 1
1608 1611 DEL_FILENODE = 2
1609 1612 MOD_FILENODE = 3
1610 1613 RENAMED_FILENODE = 4
1611 1614 COPIED_FILENODE = 5
1612 1615 CHMOD_FILENODE = 6
1613 1616 BIN_FILENODE = 7
1614 1617 */
1615 1618 .cs_files_expand {
1616 1619 font-size: @basefontsize + 5px;
1617 1620 line-height: 1.8em;
1618 1621 float: right;
1619 1622 }
1620 1623
1621 1624 .cs_files_expand span{
1622 1625 color: @rcblue;
1623 1626 cursor: pointer;
1624 1627 }
1625 1628 .cs_files {
1626 1629 clear: both;
1627 1630 padding-bottom: @padding;
1628 1631
1629 1632 .cur_cs {
1630 1633 margin: 10px 2px;
1631 1634 font-weight: bold;
1632 1635 }
1633 1636
1634 1637 .node {
1635 1638 float: left;
1636 1639 }
1637 1640
1638 1641 .changes {
1639 1642 float: right;
1640 1643 color: white;
1641 1644 font-size: @basefontsize - 4px;
1642 1645 margin-top: 4px;
1643 1646 opacity: 0.6;
1644 1647 filter: Alpha(opacity=60); /* IE8 and earlier */
1645 1648
1646 1649 .added {
1647 1650 background-color: @alert1;
1648 1651 float: left;
1649 1652 text-align: center;
1650 1653 }
1651 1654
1652 1655 .deleted {
1653 1656 background-color: @alert2;
1654 1657 float: left;
1655 1658 text-align: center;
1656 1659 }
1657 1660
1658 1661 .bin {
1659 1662 background-color: @alert1;
1660 1663 text-align: center;
1661 1664 }
1662 1665
1663 1666 /*new binary*/
1664 1667 .bin.bin1 {
1665 1668 background-color: @alert1;
1666 1669 text-align: center;
1667 1670 }
1668 1671
1669 1672 /*deleted binary*/
1670 1673 .bin.bin2 {
1671 1674 background-color: @alert2;
1672 1675 text-align: center;
1673 1676 }
1674 1677
1675 1678 /*mod binary*/
1676 1679 .bin.bin3 {
1677 1680 background-color: @grey2;
1678 1681 text-align: center;
1679 1682 }
1680 1683
1681 1684 /*rename file*/
1682 1685 .bin.bin4 {
1683 1686 background-color: @alert4;
1684 1687 text-align: center;
1685 1688 }
1686 1689
1687 1690 /*copied file*/
1688 1691 .bin.bin5 {
1689 1692 background-color: @alert4;
1690 1693 text-align: center;
1691 1694 }
1692 1695
1693 1696 /*chmod file*/
1694 1697 .bin.bin6 {
1695 1698 background-color: @grey2;
1696 1699 text-align: center;
1697 1700 }
1698 1701 }
1699 1702 }
1700 1703
1701 1704 .cs_files .cs_added, .cs_files .cs_A,
1702 1705 .cs_files .cs_added, .cs_files .cs_M,
1703 1706 .cs_files .cs_added, .cs_files .cs_D {
1704 1707 height: 16px;
1705 1708 padding-right: 10px;
1706 1709 margin-top: 7px;
1707 1710 text-align: left;
1708 1711 }
1709 1712
1710 1713 .cs_icon_td {
1711 1714 min-width: 16px;
1712 1715 width: 16px;
1713 1716 }
1714 1717
1715 1718 .pull-request-merge {
1716 1719 border: 1px solid @grey5;
1717 1720 padding: 10px 0px 20px;
1718 1721 margin-top: 10px;
1719 1722 margin-bottom: 20px;
1720 1723 }
1721 1724
1722 1725 .pull-request-merge ul {
1723 1726 padding: 0px 0px;
1724 1727 }
1725 1728
1726 1729 .pull-request-merge li:before{
1727 1730 content:none;
1728 1731 }
1729 1732
1730 1733 .pull-request-merge .pull-request-wrap {
1731 1734 height: auto;
1732 1735 padding: 0px 0px;
1733 1736 text-align: right;
1734 1737 }
1735 1738
1736 1739 .pull-request-merge span {
1737 1740 margin-right: 5px;
1738 1741 }
1739 1742
1740 1743 .pull-request-merge-actions {
1741 1744 height: 30px;
1742 1745 padding: 0px 0px;
1743 1746 }
1744 1747
1745 1748 .merge-status {
1746 1749 margin-right: 5px;
1747 1750 }
1748 1751
1749 1752 .merge-message {
1750 1753 font-size: 1.2em
1751 1754 }
1752 1755
1753 1756 .merge-message.success i,
1754 1757 .merge-icon.success i {
1755 1758 color:@alert1;
1756 1759 }
1757 1760
1758 1761 .merge-message.warning i,
1759 1762 .merge-icon.warning i {
1760 1763 color: @alert3;
1761 1764 }
1762 1765
1763 1766 .merge-message.error i,
1764 1767 .merge-icon.error i {
1765 1768 color:@alert2;
1766 1769 }
1767 1770
1768 1771 .pr-versions {
1769 1772 font-size: 1.1em;
1770 1773
1771 1774 table {
1772 1775 padding: 0px 5px;
1773 1776 }
1774 1777
1775 1778 td {
1776 1779 line-height: 15px;
1777 1780 }
1778 1781
1779 1782 .flag_status {
1780 1783 margin: 0;
1781 1784 }
1782 1785
1783 1786 .compare-radio-button {
1784 1787 position: relative;
1785 1788 top: -3px;
1786 1789 }
1787 1790 }
1788 1791
1789 1792
1790 1793 #close_pull_request {
1791 1794 margin-right: 0px;
1792 1795 }
1793 1796
1794 1797 .empty_data {
1795 1798 color: @grey4;
1796 1799 }
1797 1800
1798 1801 #changeset_compare_view_content {
1799 1802 margin-bottom: @space;
1800 1803 clear: both;
1801 1804 width: 100%;
1802 1805 box-sizing: border-box;
1803 1806 .border-radius(@border-radius);
1804 1807
1805 1808 .help-block {
1806 1809 margin: @padding 0;
1807 1810 color: @text-color;
1811 &.pre-formatting {
1812 white-space: pre;
1813 }
1808 1814 }
1809 1815
1810 1816 .empty_data {
1811 1817 margin: @padding 0;
1812 1818 }
1813 1819
1814 1820 .alert {
1815 1821 margin-bottom: @space;
1816 1822 }
1817 1823 }
1818 1824
1819 1825 .table_disp {
1820 1826 .status {
1821 1827 width: auto;
1822 1828
1823 1829 .flag_status {
1824 1830 float: left;
1825 1831 }
1826 1832 }
1827 1833 }
1828 1834
1829 1835 .status_box_menu {
1830 1836 margin: 0;
1831 1837 }
1832 1838
1833 1839 .notification-table{
1834 1840 margin-bottom: @space;
1835 1841 display: table;
1836 1842 width: 100%;
1837 1843
1838 1844 .container{
1839 1845 display: table-row;
1840 1846
1841 1847 .notification-header{
1842 1848 border-bottom: @border-thickness solid @border-default-color;
1843 1849 }
1844 1850
1845 1851 .notification-subject{
1846 1852 display: table-cell;
1847 1853 }
1848 1854 }
1849 1855 }
1850 1856
1851 1857 // Notifications
1852 1858 .notification-header{
1853 1859 display: table;
1854 1860 width: 100%;
1855 1861 padding: floor(@basefontsize/2) 0;
1856 1862 line-height: 1em;
1857 1863
1858 1864 .desc, .delete-notifications, .read-notifications{
1859 1865 display: table-cell;
1860 1866 text-align: left;
1861 1867 }
1862 1868
1863 1869 .desc{
1864 1870 width: 1163px;
1865 1871 }
1866 1872
1867 1873 .delete-notifications, .read-notifications{
1868 1874 width: 35px;
1869 1875 min-width: 35px; //fixes when only one button is displayed
1870 1876 }
1871 1877 }
1872 1878
1873 1879 .notification-body {
1874 1880 .markdown-block,
1875 1881 .rst-block {
1876 1882 padding: @padding 0;
1877 1883 }
1878 1884
1879 1885 .notification-subject {
1880 1886 padding: @textmargin 0;
1881 1887 border-bottom: @border-thickness solid @border-default-color;
1882 1888 }
1883 1889 }
1884 1890
1885 1891
1886 1892 .notifications_buttons{
1887 1893 float: right;
1888 1894 }
1889 1895
1890 1896 #notification-status{
1891 1897 display: inline;
1892 1898 }
1893 1899
1894 1900 // Repositories
1895 1901
1896 1902 #summary.fields{
1897 1903 display: table;
1898 1904
1899 1905 .field{
1900 1906 display: table-row;
1901 1907
1902 1908 .label-summary{
1903 1909 display: table-cell;
1904 1910 min-width: @label-summary-minwidth;
1905 1911 padding-top: @padding/2;
1906 1912 padding-bottom: @padding/2;
1907 1913 padding-right: @padding/2;
1908 1914 }
1909 1915
1910 1916 .input{
1911 1917 display: table-cell;
1912 1918 padding: @padding/2;
1913 1919
1914 1920 input{
1915 1921 min-width: 29em;
1916 1922 padding: @padding/4;
1917 1923 }
1918 1924 }
1919 1925 .statistics, .downloads{
1920 1926 .disabled{
1921 1927 color: @grey4;
1922 1928 }
1923 1929 }
1924 1930 }
1925 1931 }
1926 1932
1927 1933 #summary{
1928 1934 width: 70%;
1929 1935 }
1930 1936
1931 1937
1932 1938 // Journal
1933 1939 .journal.title {
1934 1940 h5 {
1935 1941 float: left;
1936 1942 margin: 0;
1937 1943 width: 70%;
1938 1944 }
1939 1945
1940 1946 ul {
1941 1947 float: right;
1942 1948 display: inline-block;
1943 1949 margin: 0;
1944 1950 width: 30%;
1945 1951 text-align: right;
1946 1952
1947 1953 li {
1948 1954 display: inline;
1949 1955 font-size: @journal-fontsize;
1950 1956 line-height: 1em;
1951 1957
1952 1958 &:before { content: none; }
1953 1959 }
1954 1960 }
1955 1961 }
1956 1962
1957 1963 .filterexample {
1958 1964 position: absolute;
1959 1965 top: 95px;
1960 1966 left: @contentpadding;
1961 1967 color: @rcblue;
1962 1968 font-size: 11px;
1963 1969 font-family: @text-regular;
1964 1970 cursor: help;
1965 1971
1966 1972 &:hover {
1967 1973 color: @rcdarkblue;
1968 1974 }
1969 1975
1970 1976 @media (max-width:768px) {
1971 1977 position: relative;
1972 1978 top: auto;
1973 1979 left: auto;
1974 1980 display: block;
1975 1981 }
1976 1982 }
1977 1983
1978 1984
1979 1985 #journal{
1980 1986 margin-bottom: @space;
1981 1987
1982 1988 .journal_day{
1983 1989 margin-bottom: @textmargin/2;
1984 1990 padding-bottom: @textmargin/2;
1985 1991 font-size: @journal-fontsize;
1986 1992 border-bottom: @border-thickness solid @border-default-color;
1987 1993 }
1988 1994
1989 1995 .journal_container{
1990 1996 margin-bottom: @space;
1991 1997
1992 1998 .journal_user{
1993 1999 display: inline-block;
1994 2000 }
1995 2001 .journal_action_container{
1996 2002 display: block;
1997 2003 margin-top: @textmargin;
1998 2004
1999 2005 div{
2000 2006 display: inline;
2001 2007 }
2002 2008
2003 2009 div.journal_action_params{
2004 2010 display: block;
2005 2011 }
2006 2012
2007 2013 div.journal_repo:after{
2008 2014 content: "\A";
2009 2015 white-space: pre;
2010 2016 }
2011 2017
2012 2018 div.date{
2013 2019 display: block;
2014 2020 margin-bottom: @textmargin;
2015 2021 }
2016 2022 }
2017 2023 }
2018 2024 }
2019 2025
2020 2026 // Files
2021 2027 .edit-file-title {
2022 2028 border-bottom: @border-thickness solid @border-default-color;
2023 2029
2024 2030 .breadcrumbs {
2025 2031 margin-bottom: 0;
2026 2032 }
2027 2033 }
2028 2034
2029 2035 .edit-file-fieldset {
2030 2036 margin-top: @sidebarpadding;
2031 2037
2032 2038 .fieldset {
2033 2039 .left-label {
2034 2040 width: 13%;
2035 2041 }
2036 2042 .right-content {
2037 2043 width: 87%;
2038 2044 max-width: 100%;
2039 2045 }
2040 2046 .filename-label {
2041 2047 margin-top: 13px;
2042 2048 }
2043 2049 .commit-message-label {
2044 2050 margin-top: 4px;
2045 2051 }
2046 2052 .file-upload-input {
2047 2053 input {
2048 2054 display: none;
2049 2055 }
2050 2056 margin-top: 10px;
2051 2057 }
2052 2058 .file-upload-label {
2053 2059 margin-top: 10px;
2054 2060 }
2055 2061 p {
2056 2062 margin-top: 5px;
2057 2063 }
2058 2064
2059 2065 }
2060 2066 .custom-path-link {
2061 2067 margin-left: 5px;
2062 2068 }
2063 2069 #commit {
2064 2070 resize: vertical;
2065 2071 }
2066 2072 }
2067 2073
2068 2074 .delete-file-preview {
2069 2075 max-height: 250px;
2070 2076 }
2071 2077
2072 2078 .new-file,
2073 2079 #filter_activate,
2074 2080 #filter_deactivate {
2075 2081 float: left;
2076 2082 margin: 0 0 0 15px;
2077 2083 }
2078 2084
2079 2085 h3.files_location{
2080 2086 line-height: 2.4em;
2081 2087 }
2082 2088
2083 2089 .browser-nav {
2084 2090 display: table;
2085 2091 margin-bottom: @space;
2086 2092
2087 2093
2088 2094 .info_box {
2089 2095 display: inline-table;
2090 2096 height: 2.5em;
2091 2097
2092 2098 .browser-cur-rev, .info_box_elem {
2093 2099 display: table-cell;
2094 2100 vertical-align: middle;
2095 2101 }
2096 2102
2097 2103 .info_box_elem {
2098 2104 border-top: @border-thickness solid @rcblue;
2099 2105 border-bottom: @border-thickness solid @rcblue;
2100 2106
2101 2107 #at_rev, a {
2102 2108 padding: 0.6em 0.9em;
2103 2109 margin: 0;
2104 2110 .box-shadow(none);
2105 2111 border: 0;
2106 2112 height: 12px;
2107 2113 }
2108 2114
2109 2115 input#at_rev {
2110 2116 max-width: 50px;
2111 2117 text-align: right;
2112 2118 }
2113 2119
2114 2120 &.previous {
2115 2121 border: @border-thickness solid @rcblue;
2116 2122 .disabled {
2117 2123 color: @grey4;
2118 2124 cursor: not-allowed;
2119 2125 }
2120 2126 }
2121 2127
2122 2128 &.next {
2123 2129 border: @border-thickness solid @rcblue;
2124 2130 .disabled {
2125 2131 color: @grey4;
2126 2132 cursor: not-allowed;
2127 2133 }
2128 2134 }
2129 2135 }
2130 2136
2131 2137 .browser-cur-rev {
2132 2138
2133 2139 span{
2134 2140 margin: 0;
2135 2141 color: @rcblue;
2136 2142 height: 12px;
2137 2143 display: inline-block;
2138 2144 padding: 0.7em 1em ;
2139 2145 border: @border-thickness solid @rcblue;
2140 2146 margin-right: @padding;
2141 2147 }
2142 2148 }
2143 2149 }
2144 2150
2145 2151 .search_activate {
2146 2152 display: table-cell;
2147 2153 vertical-align: middle;
2148 2154
2149 2155 input, label{
2150 2156 margin: 0;
2151 2157 padding: 0;
2152 2158 }
2153 2159
2154 2160 input{
2155 2161 margin-left: @textmargin;
2156 2162 }
2157 2163
2158 2164 }
2159 2165 }
2160 2166
2161 2167 .browser-cur-rev{
2162 2168 margin-bottom: @textmargin;
2163 2169 }
2164 2170
2165 2171 #node_filter_box_loading{
2166 2172 .info_text;
2167 2173 }
2168 2174
2169 2175 .browser-search {
2170 2176 margin: -25px 0px 5px 0px;
2171 2177 }
2172 2178
2173 2179 .node-filter {
2174 2180 font-size: @repo-title-fontsize;
2175 2181 padding: 4px 0px 0px 0px;
2176 2182
2177 2183 .node-filter-path {
2178 2184 float: left;
2179 2185 color: @grey4;
2180 2186 }
2181 2187 .node-filter-input {
2182 2188 float: left;
2183 2189 margin: -2px 0px 0px 2px;
2184 2190 input {
2185 2191 padding: 2px;
2186 2192 border: none;
2187 2193 font-size: @repo-title-fontsize;
2188 2194 }
2189 2195 }
2190 2196 }
2191 2197
2192 2198
2193 2199 .browser-result{
2194 2200 td a{
2195 2201 margin-left: 0.5em;
2196 2202 display: inline-block;
2197 2203
2198 2204 em{
2199 2205 font-family: @text-bold;
2200 2206 }
2201 2207 }
2202 2208 }
2203 2209
2204 2210 .browser-highlight{
2205 2211 background-color: @grey5-alpha;
2206 2212 }
2207 2213
2208 2214
2209 2215 // Search
2210 2216
2211 2217 .search-form{
2212 2218 #q {
2213 2219 width: @search-form-width;
2214 2220 }
2215 2221 .fields{
2216 2222 margin: 0 0 @space;
2217 2223 }
2218 2224
2219 2225 label{
2220 2226 display: inline-block;
2221 2227 margin-right: @textmargin;
2222 2228 padding-top: 0.25em;
2223 2229 }
2224 2230
2225 2231
2226 2232 .results{
2227 2233 clear: both;
2228 2234 margin: 0 0 @padding;
2229 2235 }
2230 2236 }
2231 2237
2232 2238 div.search-feedback-items {
2233 2239 display: inline-block;
2234 2240 padding:0px 0px 0px 96px;
2235 2241 }
2236 2242
2237 2243 div.search-code-body {
2238 2244 background-color: #ffffff; padding: 5px 0 5px 10px;
2239 2245 pre {
2240 2246 .match { background-color: #faffa6;}
2241 2247 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2242 2248 }
2243 2249 }
2244 2250
2245 2251 .expand_commit.search {
2246 2252 .show_more.open {
2247 2253 height: auto;
2248 2254 max-height: none;
2249 2255 }
2250 2256 }
2251 2257
2252 2258 .search-results {
2253 2259
2254 2260 h2 {
2255 2261 margin-bottom: 0;
2256 2262 }
2257 2263 .codeblock {
2258 2264 border: none;
2259 2265 background: transparent;
2260 2266 }
2261 2267
2262 2268 .codeblock-header {
2263 2269 border: none;
2264 2270 background: transparent;
2265 2271 }
2266 2272
2267 2273 .code-body {
2268 2274 border: @border-thickness solid @border-default-color;
2269 2275 .border-radius(@border-radius);
2270 2276 }
2271 2277
2272 2278 .td-commit {
2273 2279 &:extend(pre);
2274 2280 border-bottom: @border-thickness solid @border-default-color;
2275 2281 }
2276 2282
2277 2283 .message {
2278 2284 height: auto;
2279 2285 max-width: 350px;
2280 2286 white-space: normal;
2281 2287 text-overflow: initial;
2282 2288 overflow: visible;
2283 2289
2284 2290 .match { background-color: #faffa6;}
2285 2291 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2286 2292 }
2287 2293
2288 2294 }
2289 2295
2290 2296 table.rctable td.td-search-results div {
2291 2297 max-width: 100%;
2292 2298 }
2293 2299
2294 2300 #tip-box, .tip-box{
2295 2301 padding: @menupadding/2;
2296 2302 display: block;
2297 2303 border: @border-thickness solid @border-highlight-color;
2298 2304 .border-radius(@border-radius);
2299 2305 background-color: white;
2300 2306 z-index: 99;
2301 2307 white-space: pre-wrap;
2302 2308 }
2303 2309
2304 2310 #linktt {
2305 2311 width: 79px;
2306 2312 }
2307 2313
2308 2314 #help_kb .modal-content{
2309 2315 max-width: 750px;
2310 2316 margin: 10% auto;
2311 2317
2312 2318 table{
2313 2319 td,th{
2314 2320 border-bottom: none;
2315 2321 line-height: 2.5em;
2316 2322 }
2317 2323 th{
2318 2324 padding-bottom: @textmargin/2;
2319 2325 }
2320 2326 td.keys{
2321 2327 text-align: center;
2322 2328 }
2323 2329 }
2324 2330
2325 2331 .block-left{
2326 2332 width: 45%;
2327 2333 margin-right: 5%;
2328 2334 }
2329 2335 .modal-footer{
2330 2336 clear: both;
2331 2337 }
2332 2338 .key.tag{
2333 2339 padding: 0.5em;
2334 2340 background-color: @rcblue;
2335 2341 color: white;
2336 2342 border-color: @rcblue;
2337 2343 .box-shadow(none);
2338 2344 }
2339 2345 }
2340 2346
2341 2347
2342 2348
2343 2349 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2344 2350
2345 2351 @import 'statistics-graph';
2346 2352 @import 'tables';
2347 2353 @import 'forms';
2348 2354 @import 'diff';
2349 2355 @import 'summary';
2350 2356 @import 'navigation';
2351 2357
2352 2358 //--- SHOW/HIDE SECTIONS --//
2353 2359
2354 2360 .btn-collapse {
2355 2361 float: right;
2356 2362 text-align: right;
2357 2363 font-family: @text-light;
2358 2364 font-size: @basefontsize;
2359 2365 cursor: pointer;
2360 2366 border: none;
2361 2367 color: @rcblue;
2362 2368 }
2363 2369
2364 2370 table.rctable,
2365 2371 table.dataTable {
2366 2372 .btn-collapse {
2367 2373 float: right;
2368 2374 text-align: right;
2369 2375 }
2370 2376 }
2371 2377
2372 2378
2373 2379 // TODO: johbo: Fix for IE10, this avoids that we see a border
2374 2380 // and padding around checkboxes and radio boxes. Move to the right place,
2375 2381 // or better: Remove this once we did the form refactoring.
2376 2382 input[type=checkbox],
2377 2383 input[type=radio] {
2378 2384 padding: 0;
2379 2385 border: none;
2380 2386 }
2381 2387
2382 2388 .toggle-ajax-spinner{
2383 2389 height: 16px;
2384 2390 width: 16px;
2385 2391 }
@@ -1,542 +1,545 b''
1 1 //
2 2 // Typography
3 3 // modified from Bootstrap
4 4 // --------------------------------------------------
5 5
6 6 // Base
7 7 body {
8 8 font-size: @basefontsize;
9 9 font-family: @text-light;
10 10 letter-spacing: .02em;
11 11 color: @grey2;
12 12 }
13 13
14 14 #content, label{
15 15 font-size: @basefontsize;
16 16 }
17 17
18 18 label {
19 19 color: @grey2;
20 20 }
21 21
22 22 ::selection { background: @rchighlightblue; }
23 23
24 24 // Headings
25 25 // -------------------------
26 26
27 27 h1, h2, h3, h4, h5, h6,
28 28 .h1, .h2, .h3, .h4, .h5, .h6 {
29 29 margin: 0 0 @textmargin 0;
30 30 padding: 0;
31 31 line-height: 1.8em;
32 32 color: @text-color;
33 33 a {
34 34 color: @rcblue;
35 35 }
36 36 }
37 37
38 38 h1, .h1 { font-size: 1.54em; font-family: @text-bold; }
39 39 h2, .h2 { font-size: 1.23em; font-family: @text-semibold; }
40 40 h3, .h3 { font-size: 1.23em; font-family: @text-regular; }
41 41 h4, .h4 { font-size: 1em; font-family: @text-bold; }
42 42 h5, .h5 { font-size: 1em; font-family: @text-bold-italic; }
43 43 h6, .h6 { font-size: 1em; font-family: @text-bold-italic; }
44 44
45 45 // Breadcrumbs
46 46 .breadcrumbs {
47 47 &:extend(h1);
48 48 margin: 0;
49 49 }
50 50
51 51 .breadcrumbs_light {
52 52 float:left;
53 53 font-size: 1.3em;
54 54 line-height: 38px;
55 55 }
56 56
57 57 // Body text
58 58 // -------------------------
59 59
60 60 p {
61 61 margin: 0 0 @textmargin 0;
62 62 padding: 0;
63 63 line-height: 2em;
64 64 }
65 65
66 66 .lead {
67 67 margin-bottom: @textmargin;
68 68 font-weight: 300;
69 69 line-height: 1.4;
70 70
71 71 @media (min-width: @screen-sm-min) {
72 72 font-size: (@basefontsize * 1.5);
73 73 }
74 74 }
75 75
76 76 a,
77 77 .link {
78 78 color: @rcblue;
79 79 text-decoration: none;
80 80 outline: none;
81 81 cursor: pointer;
82 82
83 83 &:focus {
84 84 outline: none;
85 85 }
86 86
87 87 &:hover {
88 88 color: @rcdarkblue;
89 89 }
90 90 }
91 91
92 92 img {
93 93 border: none;
94 94 outline: none;
95 95 }
96 96
97 97 strong {
98 98 font-family: @text-bold;
99 99 }
100 100
101 101 em {
102 102 font-family: @text-italic;
103 103 }
104 104
105 105 strong em,
106 106 em strong {
107 107 font-family: @text-bold-italic;
108 108 }
109 109
110 110 //TODO: lisa: b and i are depreciated, but we are still using them in places.
111 111 // Should probably make some decision whether to keep or lose these.
112 112 b {
113 113
114 114 }
115 115
116 116 i {
117 117 font-style: normal;
118 118 }
119 119
120 120 label {
121 121 color: @text-color;
122 122
123 123 input[type="checkbox"] {
124 124 margin-right: 1em;
125 125 }
126 126 input[type="radio"] {
127 127 margin-right: 1em;
128 128 }
129 129 }
130 130
131 131 code,
132 132 .code {
133 133 font-size: .95em;
134 134 font-family: "Lucida Console", Monaco, monospace;
135 135 color: @grey3;
136 136
137 137 a {
138 138 color: lighten(@rcblue,10%)
139 139 }
140 140 }
141 141
142 142 pre {
143 143 margin: 0;
144 144 padding: 0;
145 145 border: 0;
146 146 outline: 0;
147 147 font-size: @basefontsize*.95;
148 148 line-height: 1.4em;
149 149 font-family: "Lucida Console", Monaco, monospace;
150 150 color: @grey3;
151 151 }
152 152
153 153 // Emphasis & misc
154 154 // -------------------------
155 155
156 156 small,
157 157 .small {
158 158 font-size: 75%;
159 159 font-weight: normal;
160 160 line-height: 1em;
161 161 }
162 162
163 163 mark,
164 164 .mark {
165 165 background-color: @rclightblue;
166 166 padding: .2em;
167 167 }
168 168
169 169 // Alignment
170 170 .text-left { text-align: left; }
171 171 .text-right { text-align: right; }
172 172 .text-center { text-align: center; }
173 173 .text-justify { text-align: justify; }
174 174 .text-nowrap { white-space: nowrap; }
175 175
176 176 // Transformation
177 177 .text-lowercase { text-transform: lowercase; }
178 178 .text-uppercase { text-transform: uppercase; }
179 179 .text-capitalize { text-transform: capitalize; }
180 180
181 181 // Contextual colors
182 182 .text-muted {
183 183 color: @grey4;
184 184 }
185 185 .text-primary {
186 186 color: @rcblue;
187 187 }
188 188 .text-success {
189 189 color: @alert1;
190 190 }
191 191 .text-info {
192 192 color: @alert4;
193 193 }
194 194 .text-warning {
195 195 color: @alert3;
196 196 }
197 197 .text-danger {
198 198 color: @alert2;
199 199 }
200 200
201 201 // Contextual backgrounds
202 202 .bg-primary {
203 203 background-color: white;
204 204 }
205 205 .bg-success {
206 206 background-color: @alert1;
207 207 }
208 208 .bg-info {
209 209 background-color: @alert4;
210 210 }
211 211 .bg-warning {
212 212 background-color: @alert3;
213 213 }
214 214 .bg-danger {
215 215 background-color: @alert2;
216 216 }
217 217
218 218
219 219 // Page header
220 220 // -------------------------
221 221
222 222 .page-header {
223 223 margin: @pagepadding 0 @textmargin;
224 224 border-bottom: @border-thickness solid @grey5;
225 225 }
226 226
227 227 .title {
228 228 clear: both;
229 229 float: left;
230 230 width: 100%;
231 231 margin: @pagepadding 0 @pagepadding;
232 232
233 233 .breadcrumbs{
234 234 float: left;
235 235 clear: both;
236 236 width: 700px;
237 237 margin: 0;
238 238
239 239 .q_filter_box {
240 240 margin-right: @padding;
241 241 }
242 242 }
243 243
244 244 h1 a {
245 245 color: @rcblue;
246 246 }
247 247
248 248 input{
249 249 margin-right: @padding;
250 250 }
251 251
252 252 h5, .h5 {
253 253 color: @grey1;
254 254 margin-bottom: @space;
255 255
256 256 span {
257 257 display: inline-block;
258 258 }
259 259 }
260 260
261 261 p {
262 262 margin-bottom: 0;
263 263 }
264 264
265 265 .links {
266 266 float: right;
267 267 display: inline;
268 268 margin: 0;
269 269 padding-left: 0;
270 270 list-style: none;
271 271 text-align: right;
272 272
273 273 li:before { content: none; }
274 274 li { float: right; }
275 275 a {
276 276 display: inline-block;
277 277 margin-left: @textmargin/2;
278 278 }
279 279 }
280 280
281 281 .title-content {
282 282 float: left;
283 283 margin: 0;
284 284 padding: 0;
285 285
286 286 & + .breadcrumbs {
287 287 margin-top: @padding;
288 288 }
289 289
290 290 & + .links {
291 291 margin-top: -@button-padding;
292 292
293 293 & + .breadcrumbs {
294 294 margin-top: @padding;
295 295 }
296 296 }
297 297 }
298 298
299 299 .title-main {
300 300 font-size: @repo-title-fontsize;
301 301 }
302 302
303 303 .title-description {
304 304 margin-top: .5em;
305 305 }
306 306
307 307 .q_filter_box {
308 308 width: 200px;
309 309 }
310 310
311 311 }
312 312
313 313 #readme .title {
314 314 text-transform: none;
315 315 }
316 316
317 317 // Lists
318 318 // -------------------------
319 319
320 320 // Unordered and Ordered lists
321 321 ul,
322 322 ol {
323 323 margin-top: 0;
324 324 margin-bottom: @textmargin;
325 325 ul,
326 326 ol {
327 327 margin-bottom: 0;
328 328 }
329 329 }
330 330
331 331 li {
332 332 line-height: 2em;
333 333 }
334 334
335 335 ul li {
336 336 position: relative;
337 337 display: block;
338 338 list-style-type: none;
339 339
340 340 &:before {
341 341 content: "\2014\00A0";
342 342 position: absolute;
343 343 top: 0;
344 344 left: -1.25em;
345 345 }
346 346
347 347 p:first-child {
348 348 display:inline;
349 349 }
350 350 }
351 351
352 352 // List options
353 353
354 354 // Unstyled keeps list items block level, just removes default browser padding and list-style
355 355 .list-unstyled {
356 356 padding-left: 0;
357 357 list-style: none;
358 358 li:before { content: none; }
359 359 }
360 360
361 361 // Inline turns list items into inline-block
362 362 .list-inline {
363 363 .list-unstyled();
364 364 margin-left: -5px;
365 365
366 366 > li {
367 367 display: inline-block;
368 368 padding-left: 5px;
369 369 padding-right: 5px;
370 370 }
371 371 }
372 372
373 373 // Description Lists
374 374
375 375 dl {
376 376 margin-top: 0; // Remove browser default
377 377 margin-bottom: @textmargin;
378 378 }
379 379
380 380 dt,
381 381 dd {
382 382 line-height: 1.4em;
383 383 }
384 384
385 385 dt {
386 386 margin: @textmargin 0 0 0;
387 387 font-family: @text-bold;
388 388 }
389 389
390 390 dd {
391 391 margin-left: 0; // Undo browser default
392 392 }
393 393
394 394 // Horizontal description lists
395 395 // Defaults to being stacked without any of the below styles applied, until the
396 396 // grid breakpoint is reached (default of ~768px).
397 397 // These are used in forms as well; see style guide.
398 398 // TODO: lisa: These should really not be used in forms.
399 399
400 400 .dl-horizontal {
401 401
402 402 overflow: hidden;
403 403 margin-bottom: @space;
404 404
405 405 dt, dd {
406 406 float: left;
407 407 margin: 5px 0 5px 0;
408 408 }
409 409
410 410 dt {
411 411 clear: left;
412 412 width: @label-width - @form-vertical-margin;
413 413 }
414 414
415 415 dd {
416 416 &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present
417 417 margin-left: @form-vertical-margin;
418 418 max-width: @form-max-width - (@label-width - @form-vertical-margin) - @form-vertical-margin;
419 419 }
420 420
421 421 pre {
422 422 margin: 0;
423 423 }
424 424
425 425 &.settings {
426 426 dt {
427 427 text-align: left;
428 428 }
429 429 }
430 430
431 431 @media (min-width: 768px) {
432 432 dt {
433 433 float: left;
434 434 width: 180px;
435 435 clear: left;
436 436 text-align: right;
437 437 }
438 438 dd {
439 439 margin-left: 20px;
440 440 }
441 441 }
442 442 }
443 443
444 444
445 445 // Misc
446 446 // -------------------------
447 447
448 448 // Abbreviations and acronyms
449 449 abbr[title],
450 450 abbr[data-original-title] {
451 451 cursor: help;
452 452 border-bottom: @border-thickness dotted @grey4;
453 453 }
454 454 .initialism {
455 455 font-size: 90%;
456 456 text-transform: uppercase;
457 457 }
458 458
459 459 // Blockquotes
460 460 blockquote {
461 461 padding: 1em 2em;
462 462 margin: 0 0 2em;
463 463 font-size: @basefontsize;
464 464 border-left: 2px solid @grey6;
465 465
466 466 p,
467 467 ul,
468 468 ol {
469 469 &:last-child {
470 470 margin-bottom: 0;
471 471 }
472 472 }
473 473
474 474 footer,
475 475 small,
476 476 .small {
477 477 display: block;
478 478 font-size: 80%;
479 479
480 480 &:before {
481 481 content: '\2014 \00A0'; // em dash, nbsp
482 482 }
483 483 }
484 484 }
485 485
486 486 // Opposite alignment of blockquote
487 487 //
488 488 .blockquote-reverse,
489 489 blockquote.pull-right {
490 490 padding-right: 15px;
491 491 padding-left: 0;
492 492 border-right: 5px solid @grey6;
493 493 border-left: 0;
494 494 text-align: right;
495 495
496 496 // Account for citation
497 497 footer,
498 498 small,
499 499 .small {
500 500 &:before { content: ''; }
501 501 &:after {
502 502 content: '\00A0 \2014'; // nbsp, em dash
503 503 }
504 504 }
505 505 }
506 506
507 507 // Addresses
508 508 address {
509 509 margin-bottom: 2em;
510 510 font-style: normal;
511 511 line-height: 1.8em;
512 512 }
513 513
514 514 .error-message {
515 515 display: block;
516 516 margin: @padding/3 0;
517 517 color: @alert2;
518 518 }
519 519
520 520 .issue-tracker-link {
521 521 color: @rcblue;
522 522 }
523 523
524 524 .info_text{
525 525 font-size: @basefontsize;
526 526 color: @grey4;
527 527 font-family: @text-regular;
528 528 }
529 529
530 530 // help block text
531 531 .help-block {
532 532 display: block;
533 533 margin: 0 0 @padding;
534 534 color: @grey4;
535 535 font-family: @text-light;
536 &.pre-formatting {
537 white-space: pre;
538 }
536 539 }
537 540
538 541 .error-message {
539 542 display: block;
540 543 margin: @padding/3 0;
541 544 color: @alert2;
542 545 }
@@ -1,158 +1,159 b''
1 1 <%namespace name="base" file="/base/base.mako"/>
2 2
3 3 <%
4 4 elems = [
5 5 (_('Created on'), h.format_date(c.user.created_on), '', ''),
6 6 (_('Source of Record'), c.user.extern_type, '', ''),
7 7
8 8 (_('Last login'), c.user.last_login or '-', '', ''),
9 9 (_('Last activity'), c.user.last_activity, '', ''),
10 10
11 11 (_('Repositories'), len(c.user.repositories), '', [x.repo_name for x in c.user.repositories]),
12 12 (_('Repository groups'), len(c.user.repository_groups), '', [x.group_name for x in c.user.repository_groups]),
13 13 (_('User groups'), len(c.user.user_groups), '', [x.users_group_name for x in c.user.user_groups]),
14 14
15 (_('Reviewer of pull requests'), len(c.user.reviewer_pull_requests), '', ['Pull Request #{}'.format(x.pull_request.pull_request_id) for x in c.user.reviewer_pull_requests]),
15 16 (_('Member of User groups'), len(c.user.group_member), '', [x.users_group.users_group_name for x in c.user.group_member]),
16 17 (_('Force password change'), c.user.user_data.get('force_password_change', 'False'), '', ''),
17 18 ]
18 19 %>
19 20
20 21 <div class="panel panel-default">
21 22 <div class="panel-heading">
22 23 <h3 class="panel-title">${_('User: %s') % c.user.username}</h3>
23 24 </div>
24 25 <div class="panel-body">
25 26 ${base.dt_info_panel(elems)}
26 27 </div>
27 28 </div>
28 29
29 30 <div class="panel panel-default">
30 31 <div class="panel-heading">
31 32 <h3 class="panel-title">${_('Force Password Reset')}</h3>
32 33 </div>
33 34 <div class="panel-body">
34 35 ${h.secure_form(h.url('force_password_reset_user', user_id=c.user.user_id), method='post')}
35 36 <div class="field">
36 37 <button class="btn btn-default" type="submit">
37 38 <i class="icon-lock"></i>
38 39 %if c.user.user_data.get('force_password_change'):
39 40 ${_('Disable forced password reset')}
40 41 %else:
41 42 ${_('Enable forced password reset')}
42 43 %endif
43 44 </button>
44 45 </div>
45 46 <div class="field">
46 47 <span class="help-block">
47 48 ${_("When this is enabled user will have to change they password when they next use RhodeCode system. This will also forbid vcs operations until someone makes a password change in the web interface")}
48 49 </span>
49 50 </div>
50 51 ${h.end_form()}
51 52 </div>
52 53 </div>
53 54
54 55 <div class="panel panel-default">
55 56 <div class="panel-heading">
56 57 <h3 class="panel-title">${_('Personal Repository Group')}</h3>
57 58 </div>
58 59 <div class="panel-body">
59 60 ${h.secure_form(h.url('create_personal_repo_group', user_id=c.user.user_id), method='post')}
60 61
61 62 %if c.personal_repo_group:
62 63 <div class="panel-body-title-text">${_('Users personal repository group')} : ${h.link_to(c.personal_repo_group.group_name, h.route_path('repo_group_home', repo_group_name=c.personal_repo_group.group_name))}</div>
63 64 %else:
64 65 <div class="panel-body-title-text">
65 66 ${_('This user currently does not have a personal repository group')}
66 67 <br/>
67 68 ${_('New group will be created at: `/%(path)s`') % {'path': c.personal_repo_group_name}}
68 69 </div>
69 70 %endif
70 71 <button class="btn btn-default" type="submit" ${'disabled="disabled"' if c.personal_repo_group else ''}>
71 72 <i class="icon-folder-close"></i>
72 73 ${_('Create personal repository group')}
73 74 </button>
74 75 ${h.end_form()}
75 76 </div>
76 77 </div>
77 78
78 79
79 80 <div class="panel panel-danger">
80 81 <div class="panel-heading">
81 82 <h3 class="panel-title">${_('Delete User')}</h3>
82 83 </div>
83 84 <div class="panel-body">
84 85 ${h.secure_form(h.url('delete_user', user_id=c.user.user_id), method='delete')}
85 86
86 87 <table class="display">
87 88 <tr>
88 89 <td>
89 90 ${ungettext('This user owns %s repository.', 'This user owns %s repositories.', len(c.user.repositories)) % len(c.user.repositories)}
90 91 </td>
91 92 <td>
92 93 %if len(c.user.repositories) > 0:
93 94 <input type="radio" id="user_repos_1" name="user_repos" value="detach" checked="checked"/> <label for="user_repos_1">${_('Detach repositories')}</label>
94 95 %endif
95 96 </td>
96 97 <td>
97 98 %if len(c.user.repositories) > 0:
98 99 <input type="radio" id="user_repos_2" name="user_repos" value="delete" /> <label for="user_repos_2">${_('Delete repositories')}</label>
99 100 %endif
100 101 </td>
101 102 </tr>
102 103
103 104 <tr>
104 105 <td>
105 106 ${ungettext('This user owns %s repository group.', 'This user owns %s repository groups.', len(c.user.repository_groups)) % len(c.user.repository_groups)}
106 107 </td>
107 108 <td>
108 109 %if len(c.user.repository_groups) > 0:
109 110 <input type="radio" id="user_repo_groups_1" name="user_repo_groups" value="detach" checked="checked"/> <label for="user_repo_groups_1">${_('Detach repository groups')}</label>
110 111 %endif
111 112 </td>
112 113 <td>
113 114 %if len(c.user.repository_groups) > 0:
114 115 <input type="radio" id="user_repo_groups_2" name="user_repo_groups" value="delete" /> <label for="user_repo_groups_2">${_('Delete repositories')}</label>
115 116 %endif
116 117 </td>
117 118 </tr>
118 119
119 120 <tr>
120 121 <td>
121 122 ${ungettext('This user owns %s user group.', 'This user owns %s user groups.', len(c.user.user_groups)) % len(c.user.user_groups)}
122 123 </td>
123 124 <td>
124 125 %if len(c.user.user_groups) > 0:
125 126 <input type="radio" id="user_user_groups_1" name="user_user_groups" value="detach" checked="checked"/> <label for="user_user_groups_1">${_('Detach user groups')}</label>
126 127 %endif
127 128 </td>
128 129 <td>
129 130 %if len(c.user.user_groups) > 0:
130 131 <input type="radio" id="user_user_groups_2" name="user_user_groups" value="delete" /> <label for="user_user_groups_2">${_('Delete repositories')}</label>
131 132 %endif
132 133 </td>
133 134 </tr>
134 135 </table>
135 136 <div style="margin: 0 0 20px 0" class="fake-space"></div>
136 137
137 138 <div class="field">
138 139 <button class="btn btn-small btn-danger" type="submit"
139 140 onclick="return confirm('${_('Confirm to delete this user: %s') % c.user.username}');"
140 141 ${"disabled" if not c.can_delete_user else ""}>
141 142 ${_('Delete this user')}
142 143 </button>
143 144 </div>
144 145 % if c.can_delete_user_message:
145 <p class="help-block">${c.can_delete_user_message}</p>
146 <p class="help-block pre-formatting">${c.can_delete_user_message}</p>
146 147 % endif
147 148
148 149 <div class="field">
149 150 <span class="help-block">
150 151 %if len(c.user.repositories) > 0 or len(c.user.repository_groups) > 0 or len(c.user.user_groups) > 0:
151 152 <p class="help-block">${_("When selecting the detach option, the depending objects owned by this user will be assigned to the `%s` super admin in the system. The delete option will delete the user's repositories!") % (c.first_admin.full_name)}</p>
152 153 %endif
153 154 </span>
154 155 </div>
155 156
156 157 ${h.end_form()}
157 158 </div>
158 159 </div>
General Comments 0
You need to be logged in to leave comments. Login now