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