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