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