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