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