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