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