##// END OF EJS Templates
core: added user-notice logic to push notice messages....
ergo -
r4300:8f93504d default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (5547 lines changed) Show them Hide them
@@ -0,0 +1,5547 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2019 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 string
29 import hashlib
30 import logging
31 import datetime
32 import uuid
33 import warnings
34 import ipaddress
35 import functools
36 import traceback
37 import collections
38
39 from sqlalchemy import (
40 or_, and_, not_, func, cast, TypeDecorator, event,
41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
43 Text, Float, PickleType, BigInteger)
44 from sqlalchemy.sql.expression import true, false, case
45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
46 from sqlalchemy.orm import (
47 relationship, joinedload, class_mapper, validates, aliased)
48 from sqlalchemy.ext.declarative import declared_attr
49 from sqlalchemy.ext.hybrid import hybrid_property
50 from sqlalchemy.exc import IntegrityError # pragma: no cover
51 from sqlalchemy.dialects.mysql import LONGTEXT
52 from zope.cachedescriptors.property import Lazy as LazyProperty
53 from pyramid import compat
54 from pyramid.threadlocal import get_current_request
55 from webhelpers2.text import remove_formatting
56
57 from rhodecode.translation import _
58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
60 from rhodecode.lib.utils2 import (
61 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
62 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
63 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
64 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
65 JsonRaw
66 from rhodecode.lib.ext_json import json
67 from rhodecode.lib.caching_query import FromCache
68 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
69 from rhodecode.lib.encrypt2 import Encryptor
70 from rhodecode.lib.exceptions import (
71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 from rhodecode.model.meta import Base, Session
73
74 URL_SEP = '/'
75 log = logging.getLogger(__name__)
76
77 # =============================================================================
78 # BASE CLASSES
79 # =============================================================================
80
81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 # beaker.session.secret if first is not set.
83 # and initialized at environment.py
84 ENCRYPTION_KEY = None
85
86 # used to sort permissions by types, '#' used here is not allowed to be in
87 # usernames, and it's very early in sorted string.printable table.
88 PERMISSION_TYPE_SORT = {
89 'admin': '####',
90 'write': '###',
91 'read': '##',
92 'none': '#',
93 }
94
95
96 def display_user_sort(obj):
97 """
98 Sort function used to sort permissions in .permissions() function of
99 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 of all other resources
101 """
102
103 if obj.username == User.DEFAULT_USER:
104 return '#####'
105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 return prefix + obj.username
107
108
109 def display_user_group_sort(obj):
110 """
111 Sort function used to sort permissions in .permissions() function of
112 Repository, RepoGroup, UserGroup. Also it put the default user in front
113 of all other resources
114 """
115
116 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
117 return prefix + obj.users_group_name
118
119
120 def _hash_key(k):
121 return sha1_safe(k)
122
123
124 def in_filter_generator(qry, items, limit=500):
125 """
126 Splits IN() into multiple with OR
127 e.g.::
128 cnt = Repository.query().filter(
129 or_(
130 *in_filter_generator(Repository.repo_id, range(100000))
131 )).count()
132 """
133 if not items:
134 # empty list will cause empty query which might cause security issues
135 # this can lead to hidden unpleasant results
136 items = [-1]
137
138 parts = []
139 for chunk in xrange(0, len(items), limit):
140 parts.append(
141 qry.in_(items[chunk: chunk + limit])
142 )
143
144 return parts
145
146
147 base_table_args = {
148 'extend_existing': True,
149 'mysql_engine': 'InnoDB',
150 'mysql_charset': 'utf8',
151 'sqlite_autoincrement': True
152 }
153
154
155 class EncryptedTextValue(TypeDecorator):
156 """
157 Special column for encrypted long text data, use like::
158
159 value = Column("encrypted_value", EncryptedValue(), nullable=False)
160
161 This column is intelligent so if value is in unencrypted form it return
162 unencrypted form, but on save it always encrypts
163 """
164 impl = Text
165
166 def process_bind_param(self, value, dialect):
167 """
168 Setter for storing value
169 """
170 import rhodecode
171 if not value:
172 return value
173
174 # protect against double encrypting if values is already encrypted
175 if value.startswith('enc$aes$') \
176 or value.startswith('enc$aes_hmac$') \
177 or value.startswith('enc2$'):
178 raise ValueError('value needs to be in unencrypted format, '
179 'ie. not starting with enc$ or enc2$')
180
181 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
182 if algo == 'aes':
183 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
184 elif algo == 'fernet':
185 return Encryptor(ENCRYPTION_KEY).encrypt(value)
186 else:
187 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
188
189 def process_result_value(self, value, dialect):
190 """
191 Getter for retrieving value
192 """
193
194 import rhodecode
195 if not value:
196 return value
197
198 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
199 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
200 if algo == 'aes':
201 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
202 elif algo == 'fernet':
203 return Encryptor(ENCRYPTION_KEY).decrypt(value)
204 else:
205 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
206 return decrypted_data
207
208
209 class BaseModel(object):
210 """
211 Base Model for all classes
212 """
213
214 @classmethod
215 def _get_keys(cls):
216 """return column names for this model """
217 return class_mapper(cls).c.keys()
218
219 def get_dict(self):
220 """
221 return dict with keys and values corresponding
222 to this model data """
223
224 d = {}
225 for k in self._get_keys():
226 d[k] = getattr(self, k)
227
228 # also use __json__() if present to get additional fields
229 _json_attr = getattr(self, '__json__', None)
230 if _json_attr:
231 # update with attributes from __json__
232 if callable(_json_attr):
233 _json_attr = _json_attr()
234 for k, val in _json_attr.iteritems():
235 d[k] = val
236 return d
237
238 def get_appstruct(self):
239 """return list with keys and values tuples corresponding
240 to this model data """
241
242 lst = []
243 for k in self._get_keys():
244 lst.append((k, getattr(self, k),))
245 return lst
246
247 def populate_obj(self, populate_dict):
248 """populate model with data from given populate_dict"""
249
250 for k in self._get_keys():
251 if k in populate_dict:
252 setattr(self, k, populate_dict[k])
253
254 @classmethod
255 def query(cls):
256 return Session().query(cls)
257
258 @classmethod
259 def get(cls, id_):
260 if id_:
261 return cls.query().get(id_)
262
263 @classmethod
264 def get_or_404(cls, id_):
265 from pyramid.httpexceptions import HTTPNotFound
266
267 try:
268 id_ = int(id_)
269 except (TypeError, ValueError):
270 raise HTTPNotFound()
271
272 res = cls.query().get(id_)
273 if not res:
274 raise HTTPNotFound()
275 return res
276
277 @classmethod
278 def getAll(cls):
279 # deprecated and left for backward compatibility
280 return cls.get_all()
281
282 @classmethod
283 def get_all(cls):
284 return cls.query().all()
285
286 @classmethod
287 def delete(cls, id_):
288 obj = cls.query().get(id_)
289 Session().delete(obj)
290
291 @classmethod
292 def identity_cache(cls, session, attr_name, value):
293 exist_in_session = []
294 for (item_cls, pkey), instance in session.identity_map.items():
295 if cls == item_cls and getattr(instance, attr_name) == value:
296 exist_in_session.append(instance)
297 if exist_in_session:
298 if len(exist_in_session) == 1:
299 return exist_in_session[0]
300 log.exception(
301 'multiple objects with attr %s and '
302 'value %s found with same name: %r',
303 attr_name, value, exist_in_session)
304
305 def __repr__(self):
306 if hasattr(self, '__unicode__'):
307 # python repr needs to return str
308 try:
309 return safe_str(self.__unicode__())
310 except UnicodeDecodeError:
311 pass
312 return '<DB:%s>' % (self.__class__.__name__)
313
314
315 class RhodeCodeSetting(Base, BaseModel):
316 __tablename__ = 'rhodecode_settings'
317 __table_args__ = (
318 UniqueConstraint('app_settings_name'),
319 base_table_args
320 )
321
322 SETTINGS_TYPES = {
323 'str': safe_str,
324 'int': safe_int,
325 'unicode': safe_unicode,
326 'bool': str2bool,
327 'list': functools.partial(aslist, sep=',')
328 }
329 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
330 GLOBAL_CONF_KEY = 'app_settings'
331
332 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
333 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
334 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
335 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
336
337 def __init__(self, key='', val='', type='unicode'):
338 self.app_settings_name = key
339 self.app_settings_type = type
340 self.app_settings_value = val
341
342 @validates('_app_settings_value')
343 def validate_settings_value(self, key, val):
344 assert type(val) == unicode
345 return val
346
347 @hybrid_property
348 def app_settings_value(self):
349 v = self._app_settings_value
350 _type = self.app_settings_type
351 if _type:
352 _type = self.app_settings_type.split('.')[0]
353 # decode the encrypted value
354 if 'encrypted' in self.app_settings_type:
355 cipher = EncryptedTextValue()
356 v = safe_unicode(cipher.process_result_value(v, None))
357
358 converter = self.SETTINGS_TYPES.get(_type) or \
359 self.SETTINGS_TYPES['unicode']
360 return converter(v)
361
362 @app_settings_value.setter
363 def app_settings_value(self, val):
364 """
365 Setter that will always make sure we use unicode in app_settings_value
366
367 :param val:
368 """
369 val = safe_unicode(val)
370 # encode the encrypted value
371 if 'encrypted' in self.app_settings_type:
372 cipher = EncryptedTextValue()
373 val = safe_unicode(cipher.process_bind_param(val, None))
374 self._app_settings_value = val
375
376 @hybrid_property
377 def app_settings_type(self):
378 return self._app_settings_type
379
380 @app_settings_type.setter
381 def app_settings_type(self, val):
382 if val.split('.')[0] not in self.SETTINGS_TYPES:
383 raise Exception('type must be one of %s got %s'
384 % (self.SETTINGS_TYPES.keys(), val))
385 self._app_settings_type = val
386
387 @classmethod
388 def get_by_prefix(cls, prefix):
389 return RhodeCodeSetting.query()\
390 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
391 .all()
392
393 def __unicode__(self):
394 return u"<%s('%s:%s[%s]')>" % (
395 self.__class__.__name__,
396 self.app_settings_name, self.app_settings_value,
397 self.app_settings_type
398 )
399
400
401 class RhodeCodeUi(Base, BaseModel):
402 __tablename__ = 'rhodecode_ui'
403 __table_args__ = (
404 UniqueConstraint('ui_key'),
405 base_table_args
406 )
407
408 HOOK_REPO_SIZE = 'changegroup.repo_size'
409 # HG
410 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
411 HOOK_PULL = 'outgoing.pull_logger'
412 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
413 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
414 HOOK_PUSH = 'changegroup.push_logger'
415 HOOK_PUSH_KEY = 'pushkey.key_push'
416
417 HOOKS_BUILTIN = [
418 HOOK_PRE_PULL,
419 HOOK_PULL,
420 HOOK_PRE_PUSH,
421 HOOK_PRETX_PUSH,
422 HOOK_PUSH,
423 HOOK_PUSH_KEY,
424 ]
425
426 # TODO: johbo: Unify way how hooks are configured for git and hg,
427 # git part is currently hardcoded.
428
429 # SVN PATTERNS
430 SVN_BRANCH_ID = 'vcs_svn_branch'
431 SVN_TAG_ID = 'vcs_svn_tag'
432
433 ui_id = Column(
434 "ui_id", Integer(), nullable=False, unique=True, default=None,
435 primary_key=True)
436 ui_section = Column(
437 "ui_section", String(255), nullable=True, unique=None, default=None)
438 ui_key = Column(
439 "ui_key", String(255), nullable=True, unique=None, default=None)
440 ui_value = Column(
441 "ui_value", String(255), nullable=True, unique=None, default=None)
442 ui_active = Column(
443 "ui_active", Boolean(), nullable=True, unique=None, default=True)
444
445 def __repr__(self):
446 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
447 self.ui_key, self.ui_value)
448
449
450 class RepoRhodeCodeSetting(Base, BaseModel):
451 __tablename__ = 'repo_rhodecode_settings'
452 __table_args__ = (
453 UniqueConstraint(
454 'app_settings_name', 'repository_id',
455 name='uq_repo_rhodecode_setting_name_repo_id'),
456 base_table_args
457 )
458
459 repository_id = Column(
460 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
461 nullable=False)
462 app_settings_id = Column(
463 "app_settings_id", Integer(), nullable=False, unique=True,
464 default=None, primary_key=True)
465 app_settings_name = Column(
466 "app_settings_name", String(255), nullable=True, unique=None,
467 default=None)
468 _app_settings_value = Column(
469 "app_settings_value", String(4096), nullable=True, unique=None,
470 default=None)
471 _app_settings_type = Column(
472 "app_settings_type", String(255), nullable=True, unique=None,
473 default=None)
474
475 repository = relationship('Repository')
476
477 def __init__(self, repository_id, key='', val='', type='unicode'):
478 self.repository_id = repository_id
479 self.app_settings_name = key
480 self.app_settings_type = type
481 self.app_settings_value = val
482
483 @validates('_app_settings_value')
484 def validate_settings_value(self, key, val):
485 assert type(val) == unicode
486 return val
487
488 @hybrid_property
489 def app_settings_value(self):
490 v = self._app_settings_value
491 type_ = self.app_settings_type
492 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
493 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
494 return converter(v)
495
496 @app_settings_value.setter
497 def app_settings_value(self, val):
498 """
499 Setter that will always make sure we use unicode in app_settings_value
500
501 :param val:
502 """
503 self._app_settings_value = safe_unicode(val)
504
505 @hybrid_property
506 def app_settings_type(self):
507 return self._app_settings_type
508
509 @app_settings_type.setter
510 def app_settings_type(self, val):
511 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
512 if val not in SETTINGS_TYPES:
513 raise Exception('type must be one of %s got %s'
514 % (SETTINGS_TYPES.keys(), val))
515 self._app_settings_type = val
516
517 def __unicode__(self):
518 return u"<%s('%s:%s:%s[%s]')>" % (
519 self.__class__.__name__, self.repository.repo_name,
520 self.app_settings_name, self.app_settings_value,
521 self.app_settings_type
522 )
523
524
525 class RepoRhodeCodeUi(Base, BaseModel):
526 __tablename__ = 'repo_rhodecode_ui'
527 __table_args__ = (
528 UniqueConstraint(
529 'repository_id', 'ui_section', 'ui_key',
530 name='uq_repo_rhodecode_ui_repository_id_section_key'),
531 base_table_args
532 )
533
534 repository_id = Column(
535 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
536 nullable=False)
537 ui_id = Column(
538 "ui_id", Integer(), nullable=False, unique=True, default=None,
539 primary_key=True)
540 ui_section = Column(
541 "ui_section", String(255), nullable=True, unique=None, default=None)
542 ui_key = Column(
543 "ui_key", String(255), nullable=True, unique=None, default=None)
544 ui_value = Column(
545 "ui_value", String(255), nullable=True, unique=None, default=None)
546 ui_active = Column(
547 "ui_active", Boolean(), nullable=True, unique=None, default=True)
548
549 repository = relationship('Repository')
550
551 def __repr__(self):
552 return '<%s[%s:%s]%s=>%s]>' % (
553 self.__class__.__name__, self.repository.repo_name,
554 self.ui_section, self.ui_key, self.ui_value)
555
556
557 class User(Base, BaseModel):
558 __tablename__ = 'users'
559 __table_args__ = (
560 UniqueConstraint('username'), UniqueConstraint('email'),
561 Index('u_username_idx', 'username'),
562 Index('u_email_idx', 'email'),
563 base_table_args
564 )
565
566 DEFAULT_USER = 'default'
567 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
568 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
569
570 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
571 username = Column("username", String(255), nullable=True, unique=None, default=None)
572 password = Column("password", String(255), nullable=True, unique=None, default=None)
573 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
574 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
575 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
576 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
577 _email = Column("email", String(255), nullable=True, unique=None, default=None)
578 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
579 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
580 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
581
582 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
583 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
584 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
585 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
586 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
587 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
588
589 user_log = relationship('UserLog')
590 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
591
592 repositories = relationship('Repository')
593 repository_groups = relationship('RepoGroup')
594 user_groups = relationship('UserGroup')
595
596 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
597 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
598
599 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
600 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
601 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
602
603 group_member = relationship('UserGroupMember', cascade='all')
604
605 notifications = relationship('UserNotification', cascade='all')
606 # notifications assigned to this user
607 user_created_notifications = relationship('Notification', cascade='all')
608 # comments created by this user
609 user_comments = relationship('ChangesetComment', cascade='all')
610 # user profile extra info
611 user_emails = relationship('UserEmailMap', cascade='all')
612 user_ip_map = relationship('UserIpMap', cascade='all')
613 user_auth_tokens = relationship('UserApiKeys', cascade='all')
614 user_ssh_keys = relationship('UserSshKeys', cascade='all')
615
616 # gists
617 user_gists = relationship('Gist', cascade='all')
618 # user pull requests
619 user_pull_requests = relationship('PullRequest', cascade='all')
620 # external identities
621 external_identities = relationship(
622 'ExternalIdentity',
623 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
624 cascade='all')
625 # review rules
626 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
627
628 # artifacts owned
629 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
630
631 # no cascade, set NULL
632 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
633
634 def __unicode__(self):
635 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
636 self.user_id, self.username)
637
638 @hybrid_property
639 def email(self):
640 return self._email
641
642 @email.setter
643 def email(self, val):
644 self._email = val.lower() if val else None
645
646 @hybrid_property
647 def first_name(self):
648 from rhodecode.lib import helpers as h
649 if self.name:
650 return h.escape(self.name)
651 return self.name
652
653 @hybrid_property
654 def last_name(self):
655 from rhodecode.lib import helpers as h
656 if self.lastname:
657 return h.escape(self.lastname)
658 return self.lastname
659
660 @hybrid_property
661 def api_key(self):
662 """
663 Fetch if exist an auth-token with role ALL connected to this user
664 """
665 user_auth_token = UserApiKeys.query()\
666 .filter(UserApiKeys.user_id == self.user_id)\
667 .filter(or_(UserApiKeys.expires == -1,
668 UserApiKeys.expires >= time.time()))\
669 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
670 if user_auth_token:
671 user_auth_token = user_auth_token.api_key
672
673 return user_auth_token
674
675 @api_key.setter
676 def api_key(self, val):
677 # don't allow to set API key this is deprecated for now
678 self._api_key = None
679
680 @property
681 def reviewer_pull_requests(self):
682 return PullRequestReviewers.query() \
683 .options(joinedload(PullRequestReviewers.pull_request)) \
684 .filter(PullRequestReviewers.user_id == self.user_id) \
685 .all()
686
687 @property
688 def firstname(self):
689 # alias for future
690 return self.name
691
692 @property
693 def emails(self):
694 other = UserEmailMap.query()\
695 .filter(UserEmailMap.user == self) \
696 .order_by(UserEmailMap.email_id.asc()) \
697 .all()
698 return [self.email] + [x.email for x in other]
699
700 def emails_cached(self):
701 emails = UserEmailMap.query()\
702 .filter(UserEmailMap.user == self) \
703 .order_by(UserEmailMap.email_id.asc())
704
705 emails = emails.options(
706 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
707 )
708
709 return [self.email] + [x.email for x in emails]
710
711 @property
712 def auth_tokens(self):
713 auth_tokens = self.get_auth_tokens()
714 return [x.api_key for x in auth_tokens]
715
716 def get_auth_tokens(self):
717 return UserApiKeys.query()\
718 .filter(UserApiKeys.user == self)\
719 .order_by(UserApiKeys.user_api_key_id.asc())\
720 .all()
721
722 @LazyProperty
723 def feed_token(self):
724 return self.get_feed_token()
725
726 def get_feed_token(self, cache=True):
727 feed_tokens = UserApiKeys.query()\
728 .filter(UserApiKeys.user == self)\
729 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
730 if cache:
731 feed_tokens = feed_tokens.options(
732 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
733
734 feed_tokens = feed_tokens.all()
735 if feed_tokens:
736 return feed_tokens[0].api_key
737 return 'NO_FEED_TOKEN_AVAILABLE'
738
739 @LazyProperty
740 def artifact_token(self):
741 return self.get_artifact_token()
742
743 def get_artifact_token(self, cache=True):
744 artifacts_tokens = UserApiKeys.query()\
745 .filter(UserApiKeys.user == self)\
746 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
747 if cache:
748 artifacts_tokens = artifacts_tokens.options(
749 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
750
751 artifacts_tokens = artifacts_tokens.all()
752 if artifacts_tokens:
753 return artifacts_tokens[0].api_key
754 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
755
756 @classmethod
757 def get(cls, user_id, cache=False):
758 if not user_id:
759 return
760
761 user = cls.query()
762 if cache:
763 user = user.options(
764 FromCache("sql_cache_short", "get_users_%s" % user_id))
765 return user.get(user_id)
766
767 @classmethod
768 def extra_valid_auth_tokens(cls, user, role=None):
769 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
770 .filter(or_(UserApiKeys.expires == -1,
771 UserApiKeys.expires >= time.time()))
772 if role:
773 tokens = tokens.filter(or_(UserApiKeys.role == role,
774 UserApiKeys.role == UserApiKeys.ROLE_ALL))
775 return tokens.all()
776
777 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
778 from rhodecode.lib import auth
779
780 log.debug('Trying to authenticate user: %s via auth-token, '
781 'and roles: %s', self, roles)
782
783 if not auth_token:
784 return False
785
786 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
787 tokens_q = UserApiKeys.query()\
788 .filter(UserApiKeys.user_id == self.user_id)\
789 .filter(or_(UserApiKeys.expires == -1,
790 UserApiKeys.expires >= time.time()))
791
792 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
793
794 crypto_backend = auth.crypto_backend()
795 enc_token_map = {}
796 plain_token_map = {}
797 for token in tokens_q:
798 if token.api_key.startswith(crypto_backend.ENC_PREF):
799 enc_token_map[token.api_key] = token
800 else:
801 plain_token_map[token.api_key] = token
802 log.debug(
803 'Found %s plain and %s encrypted tokens to check for authentication for this user',
804 len(plain_token_map), len(enc_token_map))
805
806 # plain token match comes first
807 match = plain_token_map.get(auth_token)
808
809 # check encrypted tokens now
810 if not match:
811 for token_hash, token in enc_token_map.items():
812 # NOTE(marcink): this is expensive to calculate, but most secure
813 if crypto_backend.hash_check(auth_token, token_hash):
814 match = token
815 break
816
817 if match:
818 log.debug('Found matching token %s', match)
819 if match.repo_id:
820 log.debug('Found scope, checking for scope match of token %s', match)
821 if match.repo_id == scope_repo_id:
822 return True
823 else:
824 log.debug(
825 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
826 'and calling scope is:%s, skipping further checks',
827 match.repo, scope_repo_id)
828 return False
829 else:
830 return True
831
832 return False
833
834 @property
835 def ip_addresses(self):
836 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
837 return [x.ip_addr for x in ret]
838
839 @property
840 def username_and_name(self):
841 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
842
843 @property
844 def username_or_name_or_email(self):
845 full_name = self.full_name if self.full_name is not ' ' else None
846 return self.username or full_name or self.email
847
848 @property
849 def full_name(self):
850 return '%s %s' % (self.first_name, self.last_name)
851
852 @property
853 def full_name_or_username(self):
854 return ('%s %s' % (self.first_name, self.last_name)
855 if (self.first_name and self.last_name) else self.username)
856
857 @property
858 def full_contact(self):
859 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
860
861 @property
862 def short_contact(self):
863 return '%s %s' % (self.first_name, self.last_name)
864
865 @property
866 def is_admin(self):
867 return self.admin
868
869 @property
870 def language(self):
871 return self.user_data.get('language')
872
873 def AuthUser(self, **kwargs):
874 """
875 Returns instance of AuthUser for this user
876 """
877 from rhodecode.lib.auth import AuthUser
878 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
879
880 @hybrid_property
881 def user_data(self):
882 if not self._user_data:
883 return {}
884
885 try:
886 return json.loads(self._user_data)
887 except TypeError:
888 return {}
889
890 @user_data.setter
891 def user_data(self, val):
892 if not isinstance(val, dict):
893 raise Exception('user_data must be dict, got %s' % type(val))
894 try:
895 self._user_data = json.dumps(val)
896 except Exception:
897 log.error(traceback.format_exc())
898
899 @classmethod
900 def get_by_username(cls, username, case_insensitive=False,
901 cache=False, identity_cache=False):
902 session = Session()
903
904 if case_insensitive:
905 q = cls.query().filter(
906 func.lower(cls.username) == func.lower(username))
907 else:
908 q = cls.query().filter(cls.username == username)
909
910 if cache:
911 if identity_cache:
912 val = cls.identity_cache(session, 'username', username)
913 if val:
914 return val
915 else:
916 cache_key = "get_user_by_name_%s" % _hash_key(username)
917 q = q.options(
918 FromCache("sql_cache_short", cache_key))
919
920 return q.scalar()
921
922 @classmethod
923 def get_by_auth_token(cls, auth_token, cache=False):
924 q = UserApiKeys.query()\
925 .filter(UserApiKeys.api_key == auth_token)\
926 .filter(or_(UserApiKeys.expires == -1,
927 UserApiKeys.expires >= time.time()))
928 if cache:
929 q = q.options(
930 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
931
932 match = q.first()
933 if match:
934 return match.user
935
936 @classmethod
937 def get_by_email(cls, email, case_insensitive=False, cache=False):
938
939 if case_insensitive:
940 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
941
942 else:
943 q = cls.query().filter(cls.email == email)
944
945 email_key = _hash_key(email)
946 if cache:
947 q = q.options(
948 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
949
950 ret = q.scalar()
951 if ret is None:
952 q = UserEmailMap.query()
953 # try fetching in alternate email map
954 if case_insensitive:
955 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
956 else:
957 q = q.filter(UserEmailMap.email == email)
958 q = q.options(joinedload(UserEmailMap.user))
959 if cache:
960 q = q.options(
961 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
962 ret = getattr(q.scalar(), 'user', None)
963
964 return ret
965
966 @classmethod
967 def get_from_cs_author(cls, author):
968 """
969 Tries to get User objects out of commit author string
970
971 :param author:
972 """
973 from rhodecode.lib.helpers import email, author_name
974 # Valid email in the attribute passed, see if they're in the system
975 _email = email(author)
976 if _email:
977 user = cls.get_by_email(_email, case_insensitive=True)
978 if user:
979 return user
980 # Maybe we can match by username?
981 _author = author_name(author)
982 user = cls.get_by_username(_author, case_insensitive=True)
983 if user:
984 return user
985
986 def update_userdata(self, **kwargs):
987 usr = self
988 old = usr.user_data
989 old.update(**kwargs)
990 usr.user_data = old
991 Session().add(usr)
992 log.debug('updated userdata with %s', kwargs)
993
994 def update_lastlogin(self):
995 """Update user lastlogin"""
996 self.last_login = datetime.datetime.now()
997 Session().add(self)
998 log.debug('updated user %s lastlogin', self.username)
999
1000 def update_password(self, new_password):
1001 from rhodecode.lib.auth import get_crypt_password
1002
1003 self.password = get_crypt_password(new_password)
1004 Session().add(self)
1005
1006 @classmethod
1007 def get_first_super_admin(cls):
1008 user = User.query()\
1009 .filter(User.admin == true()) \
1010 .order_by(User.user_id.asc()) \
1011 .first()
1012
1013 if user is None:
1014 raise Exception('FATAL: Missing administrative account!')
1015 return user
1016
1017 @classmethod
1018 def get_all_super_admins(cls, only_active=False):
1019 """
1020 Returns all admin accounts sorted by username
1021 """
1022 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1023 if only_active:
1024 qry = qry.filter(User.active == true())
1025 return qry.all()
1026
1027 @classmethod
1028 def get_all_user_ids(cls, only_active=True):
1029 """
1030 Returns all users IDs
1031 """
1032 qry = Session().query(User.user_id)
1033
1034 if only_active:
1035 qry = qry.filter(User.active == true())
1036 return [x.user_id for x in qry]
1037
1038 @classmethod
1039 def get_default_user(cls, cache=False, refresh=False):
1040 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1041 if user is None:
1042 raise Exception('FATAL: Missing default account!')
1043 if refresh:
1044 # The default user might be based on outdated state which
1045 # has been loaded from the cache.
1046 # A call to refresh() ensures that the
1047 # latest state from the database is used.
1048 Session().refresh(user)
1049 return user
1050
1051 def _get_default_perms(self, user, suffix=''):
1052 from rhodecode.model.permission import PermissionModel
1053 return PermissionModel().get_default_perms(user.user_perms, suffix)
1054
1055 def get_default_perms(self, suffix=''):
1056 return self._get_default_perms(self, suffix)
1057
1058 def get_api_data(self, include_secrets=False, details='full'):
1059 """
1060 Common function for generating user related data for API
1061
1062 :param include_secrets: By default secrets in the API data will be replaced
1063 by a placeholder value to prevent exposing this data by accident. In case
1064 this data shall be exposed, set this flag to ``True``.
1065
1066 :param details: details can be 'basic|full' basic gives only a subset of
1067 the available user information that includes user_id, name and emails.
1068 """
1069 user = self
1070 user_data = self.user_data
1071 data = {
1072 'user_id': user.user_id,
1073 'username': user.username,
1074 'firstname': user.name,
1075 'lastname': user.lastname,
1076 'description': user.description,
1077 'email': user.email,
1078 'emails': user.emails,
1079 }
1080 if details == 'basic':
1081 return data
1082
1083 auth_token_length = 40
1084 auth_token_replacement = '*' * auth_token_length
1085
1086 extras = {
1087 'auth_tokens': [auth_token_replacement],
1088 'active': user.active,
1089 'admin': user.admin,
1090 'extern_type': user.extern_type,
1091 'extern_name': user.extern_name,
1092 'last_login': user.last_login,
1093 'last_activity': user.last_activity,
1094 'ip_addresses': user.ip_addresses,
1095 'language': user_data.get('language')
1096 }
1097 data.update(extras)
1098
1099 if include_secrets:
1100 data['auth_tokens'] = user.auth_tokens
1101 return data
1102
1103 def __json__(self):
1104 data = {
1105 'full_name': self.full_name,
1106 'full_name_or_username': self.full_name_or_username,
1107 'short_contact': self.short_contact,
1108 'full_contact': self.full_contact,
1109 }
1110 data.update(self.get_api_data())
1111 return data
1112
1113
1114 class UserApiKeys(Base, BaseModel):
1115 __tablename__ = 'user_api_keys'
1116 __table_args__ = (
1117 Index('uak_api_key_idx', 'api_key'),
1118 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1119 base_table_args
1120 )
1121 __mapper_args__ = {}
1122
1123 # ApiKey role
1124 ROLE_ALL = 'token_role_all'
1125 ROLE_HTTP = 'token_role_http'
1126 ROLE_VCS = 'token_role_vcs'
1127 ROLE_API = 'token_role_api'
1128 ROLE_FEED = 'token_role_feed'
1129 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1130 ROLE_PASSWORD_RESET = 'token_password_reset'
1131
1132 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1133
1134 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1135 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1136 api_key = Column("api_key", String(255), nullable=False, unique=True)
1137 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1138 expires = Column('expires', Float(53), nullable=False)
1139 role = Column('role', String(255), nullable=True)
1140 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1141
1142 # scope columns
1143 repo_id = Column(
1144 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1145 nullable=True, unique=None, default=None)
1146 repo = relationship('Repository', lazy='joined')
1147
1148 repo_group_id = Column(
1149 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1150 nullable=True, unique=None, default=None)
1151 repo_group = relationship('RepoGroup', lazy='joined')
1152
1153 user = relationship('User', lazy='joined')
1154
1155 def __unicode__(self):
1156 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1157
1158 def __json__(self):
1159 data = {
1160 'auth_token': self.api_key,
1161 'role': self.role,
1162 'scope': self.scope_humanized,
1163 'expired': self.expired
1164 }
1165 return data
1166
1167 def get_api_data(self, include_secrets=False):
1168 data = self.__json__()
1169 if include_secrets:
1170 return data
1171 else:
1172 data['auth_token'] = self.token_obfuscated
1173 return data
1174
1175 @hybrid_property
1176 def description_safe(self):
1177 from rhodecode.lib import helpers as h
1178 return h.escape(self.description)
1179
1180 @property
1181 def expired(self):
1182 if self.expires == -1:
1183 return False
1184 return time.time() > self.expires
1185
1186 @classmethod
1187 def _get_role_name(cls, role):
1188 return {
1189 cls.ROLE_ALL: _('all'),
1190 cls.ROLE_HTTP: _('http/web interface'),
1191 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1192 cls.ROLE_API: _('api calls'),
1193 cls.ROLE_FEED: _('feed access'),
1194 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1195 }.get(role, role)
1196
1197 @property
1198 def role_humanized(self):
1199 return self._get_role_name(self.role)
1200
1201 def _get_scope(self):
1202 if self.repo:
1203 return 'Repository: {}'.format(self.repo.repo_name)
1204 if self.repo_group:
1205 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1206 return 'Global'
1207
1208 @property
1209 def scope_humanized(self):
1210 return self._get_scope()
1211
1212 @property
1213 def token_obfuscated(self):
1214 if self.api_key:
1215 return self.api_key[:4] + "****"
1216
1217
1218 class UserEmailMap(Base, BaseModel):
1219 __tablename__ = 'user_email_map'
1220 __table_args__ = (
1221 Index('uem_email_idx', 'email'),
1222 UniqueConstraint('email'),
1223 base_table_args
1224 )
1225 __mapper_args__ = {}
1226
1227 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1228 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1229 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1230 user = relationship('User', lazy='joined')
1231
1232 @validates('_email')
1233 def validate_email(self, key, email):
1234 # check if this email is not main one
1235 main_email = Session().query(User).filter(User.email == email).scalar()
1236 if main_email is not None:
1237 raise AttributeError('email %s is present is user table' % email)
1238 return email
1239
1240 @hybrid_property
1241 def email(self):
1242 return self._email
1243
1244 @email.setter
1245 def email(self, val):
1246 self._email = val.lower() if val else None
1247
1248
1249 class UserIpMap(Base, BaseModel):
1250 __tablename__ = 'user_ip_map'
1251 __table_args__ = (
1252 UniqueConstraint('user_id', 'ip_addr'),
1253 base_table_args
1254 )
1255 __mapper_args__ = {}
1256
1257 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1258 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1259 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1260 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1261 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1262 user = relationship('User', lazy='joined')
1263
1264 @hybrid_property
1265 def description_safe(self):
1266 from rhodecode.lib import helpers as h
1267 return h.escape(self.description)
1268
1269 @classmethod
1270 def _get_ip_range(cls, ip_addr):
1271 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1272 return [str(net.network_address), str(net.broadcast_address)]
1273
1274 def __json__(self):
1275 return {
1276 'ip_addr': self.ip_addr,
1277 'ip_range': self._get_ip_range(self.ip_addr),
1278 }
1279
1280 def __unicode__(self):
1281 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1282 self.user_id, self.ip_addr)
1283
1284
1285 class UserSshKeys(Base, BaseModel):
1286 __tablename__ = 'user_ssh_keys'
1287 __table_args__ = (
1288 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1289
1290 UniqueConstraint('ssh_key_fingerprint'),
1291
1292 base_table_args
1293 )
1294 __mapper_args__ = {}
1295
1296 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1297 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1298 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1299
1300 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1301
1302 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1303 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1304 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1305
1306 user = relationship('User', lazy='joined')
1307
1308 def __json__(self):
1309 data = {
1310 'ssh_fingerprint': self.ssh_key_fingerprint,
1311 'description': self.description,
1312 'created_on': self.created_on
1313 }
1314 return data
1315
1316 def get_api_data(self):
1317 data = self.__json__()
1318 return data
1319
1320
1321 class UserLog(Base, BaseModel):
1322 __tablename__ = 'user_logs'
1323 __table_args__ = (
1324 base_table_args,
1325 )
1326
1327 VERSION_1 = 'v1'
1328 VERSION_2 = 'v2'
1329 VERSIONS = [VERSION_1, VERSION_2]
1330
1331 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1332 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1333 username = Column("username", String(255), nullable=True, unique=None, default=None)
1334 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1335 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1336 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1337 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1338 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1339
1340 version = Column("version", String(255), nullable=True, default=VERSION_1)
1341 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1342 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1343
1344 def __unicode__(self):
1345 return u"<%s('id:%s:%s')>" % (
1346 self.__class__.__name__, self.repository_name, self.action)
1347
1348 def __json__(self):
1349 return {
1350 'user_id': self.user_id,
1351 'username': self.username,
1352 'repository_id': self.repository_id,
1353 'repository_name': self.repository_name,
1354 'user_ip': self.user_ip,
1355 'action_date': self.action_date,
1356 'action': self.action,
1357 }
1358
1359 @hybrid_property
1360 def entry_id(self):
1361 return self.user_log_id
1362
1363 @property
1364 def action_as_day(self):
1365 return datetime.date(*self.action_date.timetuple()[:3])
1366
1367 user = relationship('User')
1368 repository = relationship('Repository', cascade='')
1369
1370
1371 class UserGroup(Base, BaseModel):
1372 __tablename__ = 'users_groups'
1373 __table_args__ = (
1374 base_table_args,
1375 )
1376
1377 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1378 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1379 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1380 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1381 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1382 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1383 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1384 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1385
1386 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1387 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1388 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1389 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1390 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1391 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1392
1393 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1394 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1395
1396 @classmethod
1397 def _load_group_data(cls, column):
1398 if not column:
1399 return {}
1400
1401 try:
1402 return json.loads(column) or {}
1403 except TypeError:
1404 return {}
1405
1406 @hybrid_property
1407 def description_safe(self):
1408 from rhodecode.lib import helpers as h
1409 return h.escape(self.user_group_description)
1410
1411 @hybrid_property
1412 def group_data(self):
1413 return self._load_group_data(self._group_data)
1414
1415 @group_data.expression
1416 def group_data(self, **kwargs):
1417 return self._group_data
1418
1419 @group_data.setter
1420 def group_data(self, val):
1421 try:
1422 self._group_data = json.dumps(val)
1423 except Exception:
1424 log.error(traceback.format_exc())
1425
1426 @classmethod
1427 def _load_sync(cls, group_data):
1428 if group_data:
1429 return group_data.get('extern_type')
1430
1431 @property
1432 def sync(self):
1433 return self._load_sync(self.group_data)
1434
1435 def __unicode__(self):
1436 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1437 self.users_group_id,
1438 self.users_group_name)
1439
1440 @classmethod
1441 def get_by_group_name(cls, group_name, cache=False,
1442 case_insensitive=False):
1443 if case_insensitive:
1444 q = cls.query().filter(func.lower(cls.users_group_name) ==
1445 func.lower(group_name))
1446
1447 else:
1448 q = cls.query().filter(cls.users_group_name == group_name)
1449 if cache:
1450 q = q.options(
1451 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1452 return q.scalar()
1453
1454 @classmethod
1455 def get(cls, user_group_id, cache=False):
1456 if not user_group_id:
1457 return
1458
1459 user_group = cls.query()
1460 if cache:
1461 user_group = user_group.options(
1462 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1463 return user_group.get(user_group_id)
1464
1465 def permissions(self, with_admins=True, with_owner=True,
1466 expand_from_user_groups=False):
1467 """
1468 Permissions for user groups
1469 """
1470 _admin_perm = 'usergroup.admin'
1471
1472 owner_row = []
1473 if with_owner:
1474 usr = AttributeDict(self.user.get_dict())
1475 usr.owner_row = True
1476 usr.permission = _admin_perm
1477 owner_row.append(usr)
1478
1479 super_admin_ids = []
1480 super_admin_rows = []
1481 if with_admins:
1482 for usr in User.get_all_super_admins():
1483 super_admin_ids.append(usr.user_id)
1484 # if this admin is also owner, don't double the record
1485 if usr.user_id == owner_row[0].user_id:
1486 owner_row[0].admin_row = True
1487 else:
1488 usr = AttributeDict(usr.get_dict())
1489 usr.admin_row = True
1490 usr.permission = _admin_perm
1491 super_admin_rows.append(usr)
1492
1493 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1494 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1495 joinedload(UserUserGroupToPerm.user),
1496 joinedload(UserUserGroupToPerm.permission),)
1497
1498 # get owners and admins and permissions. We do a trick of re-writing
1499 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1500 # has a global reference and changing one object propagates to all
1501 # others. This means if admin is also an owner admin_row that change
1502 # would propagate to both objects
1503 perm_rows = []
1504 for _usr in q.all():
1505 usr = AttributeDict(_usr.user.get_dict())
1506 # if this user is also owner/admin, mark as duplicate record
1507 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1508 usr.duplicate_perm = True
1509 usr.permission = _usr.permission.permission_name
1510 perm_rows.append(usr)
1511
1512 # filter the perm rows by 'default' first and then sort them by
1513 # admin,write,read,none permissions sorted again alphabetically in
1514 # each group
1515 perm_rows = sorted(perm_rows, key=display_user_sort)
1516
1517 user_groups_rows = []
1518 if expand_from_user_groups:
1519 for ug in self.permission_user_groups(with_members=True):
1520 for user_data in ug.members:
1521 user_groups_rows.append(user_data)
1522
1523 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1524
1525 def permission_user_groups(self, with_members=False):
1526 q = UserGroupUserGroupToPerm.query()\
1527 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1528 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1529 joinedload(UserGroupUserGroupToPerm.target_user_group),
1530 joinedload(UserGroupUserGroupToPerm.permission),)
1531
1532 perm_rows = []
1533 for _user_group in q.all():
1534 entry = AttributeDict(_user_group.user_group.get_dict())
1535 entry.permission = _user_group.permission.permission_name
1536 if with_members:
1537 entry.members = [x.user.get_dict()
1538 for x in _user_group.user_group.members]
1539 perm_rows.append(entry)
1540
1541 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1542 return perm_rows
1543
1544 def _get_default_perms(self, user_group, suffix=''):
1545 from rhodecode.model.permission import PermissionModel
1546 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1547
1548 def get_default_perms(self, suffix=''):
1549 return self._get_default_perms(self, suffix)
1550
1551 def get_api_data(self, with_group_members=True, include_secrets=False):
1552 """
1553 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1554 basically forwarded.
1555
1556 """
1557 user_group = self
1558 data = {
1559 'users_group_id': user_group.users_group_id,
1560 'group_name': user_group.users_group_name,
1561 'group_description': user_group.user_group_description,
1562 'active': user_group.users_group_active,
1563 'owner': user_group.user.username,
1564 'sync': user_group.sync,
1565 'owner_email': user_group.user.email,
1566 }
1567
1568 if with_group_members:
1569 users = []
1570 for user in user_group.members:
1571 user = user.user
1572 users.append(user.get_api_data(include_secrets=include_secrets))
1573 data['users'] = users
1574
1575 return data
1576
1577
1578 class UserGroupMember(Base, BaseModel):
1579 __tablename__ = 'users_groups_members'
1580 __table_args__ = (
1581 base_table_args,
1582 )
1583
1584 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1585 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1586 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1587
1588 user = relationship('User', lazy='joined')
1589 users_group = relationship('UserGroup')
1590
1591 def __init__(self, gr_id='', u_id=''):
1592 self.users_group_id = gr_id
1593 self.user_id = u_id
1594
1595
1596 class RepositoryField(Base, BaseModel):
1597 __tablename__ = 'repositories_fields'
1598 __table_args__ = (
1599 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1600 base_table_args,
1601 )
1602
1603 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1604
1605 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1606 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1607 field_key = Column("field_key", String(250))
1608 field_label = Column("field_label", String(1024), nullable=False)
1609 field_value = Column("field_value", String(10000), nullable=False)
1610 field_desc = Column("field_desc", String(1024), nullable=False)
1611 field_type = Column("field_type", String(255), nullable=False, unique=None)
1612 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1613
1614 repository = relationship('Repository')
1615
1616 @property
1617 def field_key_prefixed(self):
1618 return 'ex_%s' % self.field_key
1619
1620 @classmethod
1621 def un_prefix_key(cls, key):
1622 if key.startswith(cls.PREFIX):
1623 return key[len(cls.PREFIX):]
1624 return key
1625
1626 @classmethod
1627 def get_by_key_name(cls, key, repo):
1628 row = cls.query()\
1629 .filter(cls.repository == repo)\
1630 .filter(cls.field_key == key).scalar()
1631 return row
1632
1633
1634 class Repository(Base, BaseModel):
1635 __tablename__ = 'repositories'
1636 __table_args__ = (
1637 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1638 base_table_args,
1639 )
1640 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1641 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1642 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1643
1644 STATE_CREATED = 'repo_state_created'
1645 STATE_PENDING = 'repo_state_pending'
1646 STATE_ERROR = 'repo_state_error'
1647
1648 LOCK_AUTOMATIC = 'lock_auto'
1649 LOCK_API = 'lock_api'
1650 LOCK_WEB = 'lock_web'
1651 LOCK_PULL = 'lock_pull'
1652
1653 NAME_SEP = URL_SEP
1654
1655 repo_id = Column(
1656 "repo_id", Integer(), nullable=False, unique=True, default=None,
1657 primary_key=True)
1658 _repo_name = Column(
1659 "repo_name", Text(), nullable=False, default=None)
1660 repo_name_hash = Column(
1661 "repo_name_hash", String(255), nullable=False, unique=True)
1662 repo_state = Column("repo_state", String(255), nullable=True)
1663
1664 clone_uri = Column(
1665 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1666 default=None)
1667 push_uri = Column(
1668 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1669 default=None)
1670 repo_type = Column(
1671 "repo_type", String(255), nullable=False, unique=False, default=None)
1672 user_id = Column(
1673 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1674 unique=False, default=None)
1675 private = Column(
1676 "private", Boolean(), nullable=True, unique=None, default=None)
1677 archived = Column(
1678 "archived", Boolean(), nullable=True, unique=None, default=None)
1679 enable_statistics = Column(
1680 "statistics", Boolean(), nullable=True, unique=None, default=True)
1681 enable_downloads = Column(
1682 "downloads", Boolean(), nullable=True, unique=None, default=True)
1683 description = Column(
1684 "description", String(10000), nullable=True, unique=None, default=None)
1685 created_on = Column(
1686 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1687 default=datetime.datetime.now)
1688 updated_on = Column(
1689 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1690 default=datetime.datetime.now)
1691 _landing_revision = Column(
1692 "landing_revision", String(255), nullable=False, unique=False,
1693 default=None)
1694 enable_locking = Column(
1695 "enable_locking", Boolean(), nullable=False, unique=None,
1696 default=False)
1697 _locked = Column(
1698 "locked", String(255), nullable=True, unique=False, default=None)
1699 _changeset_cache = Column(
1700 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1701
1702 fork_id = Column(
1703 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1704 nullable=True, unique=False, default=None)
1705 group_id = Column(
1706 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1707 unique=False, default=None)
1708
1709 user = relationship('User', lazy='joined')
1710 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1711 group = relationship('RepoGroup', lazy='joined')
1712 repo_to_perm = relationship(
1713 'UserRepoToPerm', cascade='all',
1714 order_by='UserRepoToPerm.repo_to_perm_id')
1715 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1716 stats = relationship('Statistics', cascade='all', uselist=False)
1717
1718 followers = relationship(
1719 'UserFollowing',
1720 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1721 cascade='all')
1722 extra_fields = relationship(
1723 'RepositoryField', cascade="all, delete-orphan")
1724 logs = relationship('UserLog')
1725 comments = relationship(
1726 'ChangesetComment', cascade="all, delete-orphan")
1727 pull_requests_source = relationship(
1728 'PullRequest',
1729 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1730 cascade="all, delete-orphan")
1731 pull_requests_target = relationship(
1732 'PullRequest',
1733 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1734 cascade="all, delete-orphan")
1735 ui = relationship('RepoRhodeCodeUi', cascade="all")
1736 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1737 integrations = relationship('Integration', cascade="all, delete-orphan")
1738
1739 scoped_tokens = relationship('UserApiKeys', cascade="all")
1740
1741 # no cascade, set NULL
1742 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1743
1744 def __unicode__(self):
1745 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1746 safe_unicode(self.repo_name))
1747
1748 @hybrid_property
1749 def description_safe(self):
1750 from rhodecode.lib import helpers as h
1751 return h.escape(self.description)
1752
1753 @hybrid_property
1754 def landing_rev(self):
1755 # always should return [rev_type, rev]
1756 if self._landing_revision:
1757 _rev_info = self._landing_revision.split(':')
1758 if len(_rev_info) < 2:
1759 _rev_info.insert(0, 'rev')
1760 return [_rev_info[0], _rev_info[1]]
1761 return [None, None]
1762
1763 @landing_rev.setter
1764 def landing_rev(self, val):
1765 if ':' not in val:
1766 raise ValueError('value must be delimited with `:` and consist '
1767 'of <rev_type>:<rev>, got %s instead' % val)
1768 self._landing_revision = val
1769
1770 @hybrid_property
1771 def locked(self):
1772 if self._locked:
1773 user_id, timelocked, reason = self._locked.split(':')
1774 lock_values = int(user_id), timelocked, reason
1775 else:
1776 lock_values = [None, None, None]
1777 return lock_values
1778
1779 @locked.setter
1780 def locked(self, val):
1781 if val and isinstance(val, (list, tuple)):
1782 self._locked = ':'.join(map(str, val))
1783 else:
1784 self._locked = None
1785
1786 @classmethod
1787 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1788 from rhodecode.lib.vcs.backends.base import EmptyCommit
1789 dummy = EmptyCommit().__json__()
1790 if not changeset_cache_raw:
1791 dummy['source_repo_id'] = repo_id
1792 return json.loads(json.dumps(dummy))
1793
1794 try:
1795 return json.loads(changeset_cache_raw)
1796 except TypeError:
1797 return dummy
1798 except Exception:
1799 log.error(traceback.format_exc())
1800 return dummy
1801
1802 @hybrid_property
1803 def changeset_cache(self):
1804 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1805
1806 @changeset_cache.setter
1807 def changeset_cache(self, val):
1808 try:
1809 self._changeset_cache = json.dumps(val)
1810 except Exception:
1811 log.error(traceback.format_exc())
1812
1813 @hybrid_property
1814 def repo_name(self):
1815 return self._repo_name
1816
1817 @repo_name.setter
1818 def repo_name(self, value):
1819 self._repo_name = value
1820 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1821
1822 @classmethod
1823 def normalize_repo_name(cls, repo_name):
1824 """
1825 Normalizes os specific repo_name to the format internally stored inside
1826 database using URL_SEP
1827
1828 :param cls:
1829 :param repo_name:
1830 """
1831 return cls.NAME_SEP.join(repo_name.split(os.sep))
1832
1833 @classmethod
1834 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1835 session = Session()
1836 q = session.query(cls).filter(cls.repo_name == repo_name)
1837
1838 if cache:
1839 if identity_cache:
1840 val = cls.identity_cache(session, 'repo_name', repo_name)
1841 if val:
1842 return val
1843 else:
1844 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1845 q = q.options(
1846 FromCache("sql_cache_short", cache_key))
1847
1848 return q.scalar()
1849
1850 @classmethod
1851 def get_by_id_or_repo_name(cls, repoid):
1852 if isinstance(repoid, (int, long)):
1853 try:
1854 repo = cls.get(repoid)
1855 except ValueError:
1856 repo = None
1857 else:
1858 repo = cls.get_by_repo_name(repoid)
1859 return repo
1860
1861 @classmethod
1862 def get_by_full_path(cls, repo_full_path):
1863 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1864 repo_name = cls.normalize_repo_name(repo_name)
1865 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1866
1867 @classmethod
1868 def get_repo_forks(cls, repo_id):
1869 return cls.query().filter(Repository.fork_id == repo_id)
1870
1871 @classmethod
1872 def base_path(cls):
1873 """
1874 Returns base path when all repos are stored
1875
1876 :param cls:
1877 """
1878 q = Session().query(RhodeCodeUi)\
1879 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1880 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1881 return q.one().ui_value
1882
1883 @classmethod
1884 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1885 case_insensitive=True, archived=False):
1886 q = Repository.query()
1887
1888 if not archived:
1889 q = q.filter(Repository.archived.isnot(true()))
1890
1891 if not isinstance(user_id, Optional):
1892 q = q.filter(Repository.user_id == user_id)
1893
1894 if not isinstance(group_id, Optional):
1895 q = q.filter(Repository.group_id == group_id)
1896
1897 if case_insensitive:
1898 q = q.order_by(func.lower(Repository.repo_name))
1899 else:
1900 q = q.order_by(Repository.repo_name)
1901
1902 return q.all()
1903
1904 @property
1905 def repo_uid(self):
1906 return '_{}'.format(self.repo_id)
1907
1908 @property
1909 def forks(self):
1910 """
1911 Return forks of this repo
1912 """
1913 return Repository.get_repo_forks(self.repo_id)
1914
1915 @property
1916 def parent(self):
1917 """
1918 Returns fork parent
1919 """
1920 return self.fork
1921
1922 @property
1923 def just_name(self):
1924 return self.repo_name.split(self.NAME_SEP)[-1]
1925
1926 @property
1927 def groups_with_parents(self):
1928 groups = []
1929 if self.group is None:
1930 return groups
1931
1932 cur_gr = self.group
1933 groups.insert(0, cur_gr)
1934 while 1:
1935 gr = getattr(cur_gr, 'parent_group', None)
1936 cur_gr = cur_gr.parent_group
1937 if gr is None:
1938 break
1939 groups.insert(0, gr)
1940
1941 return groups
1942
1943 @property
1944 def groups_and_repo(self):
1945 return self.groups_with_parents, self
1946
1947 @LazyProperty
1948 def repo_path(self):
1949 """
1950 Returns base full path for that repository means where it actually
1951 exists on a filesystem
1952 """
1953 q = Session().query(RhodeCodeUi).filter(
1954 RhodeCodeUi.ui_key == self.NAME_SEP)
1955 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1956 return q.one().ui_value
1957
1958 @property
1959 def repo_full_path(self):
1960 p = [self.repo_path]
1961 # we need to split the name by / since this is how we store the
1962 # names in the database, but that eventually needs to be converted
1963 # into a valid system path
1964 p += self.repo_name.split(self.NAME_SEP)
1965 return os.path.join(*map(safe_unicode, p))
1966
1967 @property
1968 def cache_keys(self):
1969 """
1970 Returns associated cache keys for that repo
1971 """
1972 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
1973 repo_id=self.repo_id)
1974 return CacheKey.query()\
1975 .filter(CacheKey.cache_args == invalidation_namespace)\
1976 .order_by(CacheKey.cache_key)\
1977 .all()
1978
1979 @property
1980 def cached_diffs_relative_dir(self):
1981 """
1982 Return a relative to the repository store path of cached diffs
1983 used for safe display for users, who shouldn't know the absolute store
1984 path
1985 """
1986 return os.path.join(
1987 os.path.dirname(self.repo_name),
1988 self.cached_diffs_dir.split(os.path.sep)[-1])
1989
1990 @property
1991 def cached_diffs_dir(self):
1992 path = self.repo_full_path
1993 return os.path.join(
1994 os.path.dirname(path),
1995 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
1996
1997 def cached_diffs(self):
1998 diff_cache_dir = self.cached_diffs_dir
1999 if os.path.isdir(diff_cache_dir):
2000 return os.listdir(diff_cache_dir)
2001 return []
2002
2003 def shadow_repos(self):
2004 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2005 return [
2006 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2007 if x.startswith(shadow_repos_pattern)]
2008
2009 def get_new_name(self, repo_name):
2010 """
2011 returns new full repository name based on assigned group and new new
2012
2013 :param group_name:
2014 """
2015 path_prefix = self.group.full_path_splitted if self.group else []
2016 return self.NAME_SEP.join(path_prefix + [repo_name])
2017
2018 @property
2019 def _config(self):
2020 """
2021 Returns db based config object.
2022 """
2023 from rhodecode.lib.utils import make_db_config
2024 return make_db_config(clear_session=False, repo=self)
2025
2026 def permissions(self, with_admins=True, with_owner=True,
2027 expand_from_user_groups=False):
2028 """
2029 Permissions for repositories
2030 """
2031 _admin_perm = 'repository.admin'
2032
2033 owner_row = []
2034 if with_owner:
2035 usr = AttributeDict(self.user.get_dict())
2036 usr.owner_row = True
2037 usr.permission = _admin_perm
2038 usr.permission_id = None
2039 owner_row.append(usr)
2040
2041 super_admin_ids = []
2042 super_admin_rows = []
2043 if with_admins:
2044 for usr in User.get_all_super_admins():
2045 super_admin_ids.append(usr.user_id)
2046 # if this admin is also owner, don't double the record
2047 if usr.user_id == owner_row[0].user_id:
2048 owner_row[0].admin_row = True
2049 else:
2050 usr = AttributeDict(usr.get_dict())
2051 usr.admin_row = True
2052 usr.permission = _admin_perm
2053 usr.permission_id = None
2054 super_admin_rows.append(usr)
2055
2056 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2057 q = q.options(joinedload(UserRepoToPerm.repository),
2058 joinedload(UserRepoToPerm.user),
2059 joinedload(UserRepoToPerm.permission),)
2060
2061 # get owners and admins and permissions. We do a trick of re-writing
2062 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2063 # has a global reference and changing one object propagates to all
2064 # others. This means if admin is also an owner admin_row that change
2065 # would propagate to both objects
2066 perm_rows = []
2067 for _usr in q.all():
2068 usr = AttributeDict(_usr.user.get_dict())
2069 # if this user is also owner/admin, mark as duplicate record
2070 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2071 usr.duplicate_perm = True
2072 # also check if this permission is maybe used by branch_permissions
2073 if _usr.branch_perm_entry:
2074 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2075
2076 usr.permission = _usr.permission.permission_name
2077 usr.permission_id = _usr.repo_to_perm_id
2078 perm_rows.append(usr)
2079
2080 # filter the perm rows by 'default' first and then sort them by
2081 # admin,write,read,none permissions sorted again alphabetically in
2082 # each group
2083 perm_rows = sorted(perm_rows, key=display_user_sort)
2084
2085 user_groups_rows = []
2086 if expand_from_user_groups:
2087 for ug in self.permission_user_groups(with_members=True):
2088 for user_data in ug.members:
2089 user_groups_rows.append(user_data)
2090
2091 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2092
2093 def permission_user_groups(self, with_members=True):
2094 q = UserGroupRepoToPerm.query()\
2095 .filter(UserGroupRepoToPerm.repository == self)
2096 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2097 joinedload(UserGroupRepoToPerm.users_group),
2098 joinedload(UserGroupRepoToPerm.permission),)
2099
2100 perm_rows = []
2101 for _user_group in q.all():
2102 entry = AttributeDict(_user_group.users_group.get_dict())
2103 entry.permission = _user_group.permission.permission_name
2104 if with_members:
2105 entry.members = [x.user.get_dict()
2106 for x in _user_group.users_group.members]
2107 perm_rows.append(entry)
2108
2109 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2110 return perm_rows
2111
2112 def get_api_data(self, include_secrets=False):
2113 """
2114 Common function for generating repo api data
2115
2116 :param include_secrets: See :meth:`User.get_api_data`.
2117
2118 """
2119 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2120 # move this methods on models level.
2121 from rhodecode.model.settings import SettingsModel
2122 from rhodecode.model.repo import RepoModel
2123
2124 repo = self
2125 _user_id, _time, _reason = self.locked
2126
2127 data = {
2128 'repo_id': repo.repo_id,
2129 'repo_name': repo.repo_name,
2130 'repo_type': repo.repo_type,
2131 'clone_uri': repo.clone_uri or '',
2132 'push_uri': repo.push_uri or '',
2133 'url': RepoModel().get_url(self),
2134 'private': repo.private,
2135 'created_on': repo.created_on,
2136 'description': repo.description_safe,
2137 'landing_rev': repo.landing_rev,
2138 'owner': repo.user.username,
2139 'fork_of': repo.fork.repo_name if repo.fork else None,
2140 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2141 'enable_statistics': repo.enable_statistics,
2142 'enable_locking': repo.enable_locking,
2143 'enable_downloads': repo.enable_downloads,
2144 'last_changeset': repo.changeset_cache,
2145 'locked_by': User.get(_user_id).get_api_data(
2146 include_secrets=include_secrets) if _user_id else None,
2147 'locked_date': time_to_datetime(_time) if _time else None,
2148 'lock_reason': _reason if _reason else None,
2149 }
2150
2151 # TODO: mikhail: should be per-repo settings here
2152 rc_config = SettingsModel().get_all_settings()
2153 repository_fields = str2bool(
2154 rc_config.get('rhodecode_repository_fields'))
2155 if repository_fields:
2156 for f in self.extra_fields:
2157 data[f.field_key_prefixed] = f.field_value
2158
2159 return data
2160
2161 @classmethod
2162 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2163 if not lock_time:
2164 lock_time = time.time()
2165 if not lock_reason:
2166 lock_reason = cls.LOCK_AUTOMATIC
2167 repo.locked = [user_id, lock_time, lock_reason]
2168 Session().add(repo)
2169 Session().commit()
2170
2171 @classmethod
2172 def unlock(cls, repo):
2173 repo.locked = None
2174 Session().add(repo)
2175 Session().commit()
2176
2177 @classmethod
2178 def getlock(cls, repo):
2179 return repo.locked
2180
2181 def is_user_lock(self, user_id):
2182 if self.lock[0]:
2183 lock_user_id = safe_int(self.lock[0])
2184 user_id = safe_int(user_id)
2185 # both are ints, and they are equal
2186 return all([lock_user_id, user_id]) and lock_user_id == user_id
2187
2188 return False
2189
2190 def get_locking_state(self, action, user_id, only_when_enabled=True):
2191 """
2192 Checks locking on this repository, if locking is enabled and lock is
2193 present returns a tuple of make_lock, locked, locked_by.
2194 make_lock can have 3 states None (do nothing) True, make lock
2195 False release lock, This value is later propagated to hooks, which
2196 do the locking. Think about this as signals passed to hooks what to do.
2197
2198 """
2199 # TODO: johbo: This is part of the business logic and should be moved
2200 # into the RepositoryModel.
2201
2202 if action not in ('push', 'pull'):
2203 raise ValueError("Invalid action value: %s" % repr(action))
2204
2205 # defines if locked error should be thrown to user
2206 currently_locked = False
2207 # defines if new lock should be made, tri-state
2208 make_lock = None
2209 repo = self
2210 user = User.get(user_id)
2211
2212 lock_info = repo.locked
2213
2214 if repo and (repo.enable_locking or not only_when_enabled):
2215 if action == 'push':
2216 # check if it's already locked !, if it is compare users
2217 locked_by_user_id = lock_info[0]
2218 if user.user_id == locked_by_user_id:
2219 log.debug(
2220 'Got `push` action from user %s, now unlocking', user)
2221 # unlock if we have push from user who locked
2222 make_lock = False
2223 else:
2224 # we're not the same user who locked, ban with
2225 # code defined in settings (default is 423 HTTP Locked) !
2226 log.debug('Repo %s is currently locked by %s', repo, user)
2227 currently_locked = True
2228 elif action == 'pull':
2229 # [0] user [1] date
2230 if lock_info[0] and lock_info[1]:
2231 log.debug('Repo %s is currently locked by %s', repo, user)
2232 currently_locked = True
2233 else:
2234 log.debug('Setting lock on repo %s by %s', repo, user)
2235 make_lock = True
2236
2237 else:
2238 log.debug('Repository %s do not have locking enabled', repo)
2239
2240 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2241 make_lock, currently_locked, lock_info)
2242
2243 from rhodecode.lib.auth import HasRepoPermissionAny
2244 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2245 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2246 # if we don't have at least write permission we cannot make a lock
2247 log.debug('lock state reset back to FALSE due to lack '
2248 'of at least read permission')
2249 make_lock = False
2250
2251 return make_lock, currently_locked, lock_info
2252
2253 @property
2254 def last_commit_cache_update_diff(self):
2255 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2256
2257 @classmethod
2258 def _load_commit_change(cls, last_commit_cache):
2259 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2260 empty_date = datetime.datetime.fromtimestamp(0)
2261 date_latest = last_commit_cache.get('date', empty_date)
2262 try:
2263 return parse_datetime(date_latest)
2264 except Exception:
2265 return empty_date
2266
2267 @property
2268 def last_commit_change(self):
2269 return self._load_commit_change(self.changeset_cache)
2270
2271 @property
2272 def last_db_change(self):
2273 return self.updated_on
2274
2275 @property
2276 def clone_uri_hidden(self):
2277 clone_uri = self.clone_uri
2278 if clone_uri:
2279 import urlobject
2280 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2281 if url_obj.password:
2282 clone_uri = url_obj.with_password('*****')
2283 return clone_uri
2284
2285 @property
2286 def push_uri_hidden(self):
2287 push_uri = self.push_uri
2288 if push_uri:
2289 import urlobject
2290 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2291 if url_obj.password:
2292 push_uri = url_obj.with_password('*****')
2293 return push_uri
2294
2295 def clone_url(self, **override):
2296 from rhodecode.model.settings import SettingsModel
2297
2298 uri_tmpl = None
2299 if 'with_id' in override:
2300 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2301 del override['with_id']
2302
2303 if 'uri_tmpl' in override:
2304 uri_tmpl = override['uri_tmpl']
2305 del override['uri_tmpl']
2306
2307 ssh = False
2308 if 'ssh' in override:
2309 ssh = True
2310 del override['ssh']
2311
2312 # we didn't override our tmpl from **overrides
2313 request = get_current_request()
2314 if not uri_tmpl:
2315 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2316 rc_config = request.call_context.rc_config
2317 else:
2318 rc_config = SettingsModel().get_all_settings(cache=True)
2319
2320 if ssh:
2321 uri_tmpl = rc_config.get(
2322 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2323
2324 else:
2325 uri_tmpl = rc_config.get(
2326 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2327
2328 return get_clone_url(request=request,
2329 uri_tmpl=uri_tmpl,
2330 repo_name=self.repo_name,
2331 repo_id=self.repo_id,
2332 repo_type=self.repo_type,
2333 **override)
2334
2335 def set_state(self, state):
2336 self.repo_state = state
2337 Session().add(self)
2338 #==========================================================================
2339 # SCM PROPERTIES
2340 #==========================================================================
2341
2342 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False):
2343 return get_commit_safe(
2344 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2345 maybe_unreachable=maybe_unreachable)
2346
2347 def get_changeset(self, rev=None, pre_load=None):
2348 warnings.warn("Use get_commit", DeprecationWarning)
2349 commit_id = None
2350 commit_idx = None
2351 if isinstance(rev, compat.string_types):
2352 commit_id = rev
2353 else:
2354 commit_idx = rev
2355 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2356 pre_load=pre_load)
2357
2358 def get_landing_commit(self):
2359 """
2360 Returns landing commit, or if that doesn't exist returns the tip
2361 """
2362 _rev_type, _rev = self.landing_rev
2363 commit = self.get_commit(_rev)
2364 if isinstance(commit, EmptyCommit):
2365 return self.get_commit()
2366 return commit
2367
2368 def flush_commit_cache(self):
2369 self.update_commit_cache(cs_cache={'raw_id':'0'})
2370 self.update_commit_cache()
2371
2372 def update_commit_cache(self, cs_cache=None, config=None):
2373 """
2374 Update cache of last commit for repository
2375 cache_keys should be::
2376
2377 source_repo_id
2378 short_id
2379 raw_id
2380 revision
2381 parents
2382 message
2383 date
2384 author
2385 updated_on
2386
2387 """
2388 from rhodecode.lib.vcs.backends.base import BaseChangeset
2389 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2390 empty_date = datetime.datetime.fromtimestamp(0)
2391
2392 if cs_cache is None:
2393 # use no-cache version here
2394 try:
2395 scm_repo = self.scm_instance(cache=False, config=config)
2396 except VCSError:
2397 scm_repo = None
2398 empty = scm_repo is None or scm_repo.is_empty()
2399
2400 if not empty:
2401 cs_cache = scm_repo.get_commit(
2402 pre_load=["author", "date", "message", "parents", "branch"])
2403 else:
2404 cs_cache = EmptyCommit()
2405
2406 if isinstance(cs_cache, BaseChangeset):
2407 cs_cache = cs_cache.__json__()
2408
2409 def is_outdated(new_cs_cache):
2410 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2411 new_cs_cache['revision'] != self.changeset_cache['revision']):
2412 return True
2413 return False
2414
2415 # check if we have maybe already latest cached revision
2416 if is_outdated(cs_cache) or not self.changeset_cache:
2417 _current_datetime = datetime.datetime.utcnow()
2418 last_change = cs_cache.get('date') or _current_datetime
2419 # we check if last update is newer than the new value
2420 # if yes, we use the current timestamp instead. Imagine you get
2421 # old commit pushed 1y ago, we'd set last update 1y to ago.
2422 last_change_timestamp = datetime_to_time(last_change)
2423 current_timestamp = datetime_to_time(last_change)
2424 if last_change_timestamp > current_timestamp and not empty:
2425 cs_cache['date'] = _current_datetime
2426
2427 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2428 cs_cache['updated_on'] = time.time()
2429 self.changeset_cache = cs_cache
2430 self.updated_on = last_change
2431 Session().add(self)
2432 Session().commit()
2433
2434 else:
2435 if empty:
2436 cs_cache = EmptyCommit().__json__()
2437 else:
2438 cs_cache = self.changeset_cache
2439
2440 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2441
2442 cs_cache['updated_on'] = time.time()
2443 self.changeset_cache = cs_cache
2444 self.updated_on = _date_latest
2445 Session().add(self)
2446 Session().commit()
2447
2448 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2449 self.repo_name, cs_cache, _date_latest)
2450
2451 @property
2452 def tip(self):
2453 return self.get_commit('tip')
2454
2455 @property
2456 def author(self):
2457 return self.tip.author
2458
2459 @property
2460 def last_change(self):
2461 return self.scm_instance().last_change
2462
2463 def get_comments(self, revisions=None):
2464 """
2465 Returns comments for this repository grouped by revisions
2466
2467 :param revisions: filter query by revisions only
2468 """
2469 cmts = ChangesetComment.query()\
2470 .filter(ChangesetComment.repo == self)
2471 if revisions:
2472 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2473 grouped = collections.defaultdict(list)
2474 for cmt in cmts.all():
2475 grouped[cmt.revision].append(cmt)
2476 return grouped
2477
2478 def statuses(self, revisions=None):
2479 """
2480 Returns statuses for this repository
2481
2482 :param revisions: list of revisions to get statuses for
2483 """
2484 statuses = ChangesetStatus.query()\
2485 .filter(ChangesetStatus.repo == self)\
2486 .filter(ChangesetStatus.version == 0)
2487
2488 if revisions:
2489 # Try doing the filtering in chunks to avoid hitting limits
2490 size = 500
2491 status_results = []
2492 for chunk in xrange(0, len(revisions), size):
2493 status_results += statuses.filter(
2494 ChangesetStatus.revision.in_(
2495 revisions[chunk: chunk+size])
2496 ).all()
2497 else:
2498 status_results = statuses.all()
2499
2500 grouped = {}
2501
2502 # maybe we have open new pullrequest without a status?
2503 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2504 status_lbl = ChangesetStatus.get_status_lbl(stat)
2505 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2506 for rev in pr.revisions:
2507 pr_id = pr.pull_request_id
2508 pr_repo = pr.target_repo.repo_name
2509 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2510
2511 for stat in status_results:
2512 pr_id = pr_repo = None
2513 if stat.pull_request:
2514 pr_id = stat.pull_request.pull_request_id
2515 pr_repo = stat.pull_request.target_repo.repo_name
2516 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2517 pr_id, pr_repo]
2518 return grouped
2519
2520 # ==========================================================================
2521 # SCM CACHE INSTANCE
2522 # ==========================================================================
2523
2524 def scm_instance(self, **kwargs):
2525 import rhodecode
2526
2527 # Passing a config will not hit the cache currently only used
2528 # for repo2dbmapper
2529 config = kwargs.pop('config', None)
2530 cache = kwargs.pop('cache', None)
2531 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2532 if vcs_full_cache is not None:
2533 # allows override global config
2534 full_cache = vcs_full_cache
2535 else:
2536 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2537 # if cache is NOT defined use default global, else we have a full
2538 # control over cache behaviour
2539 if cache is None and full_cache and not config:
2540 log.debug('Initializing pure cached instance for %s', self.repo_path)
2541 return self._get_instance_cached()
2542
2543 # cache here is sent to the "vcs server"
2544 return self._get_instance(cache=bool(cache), config=config)
2545
2546 def _get_instance_cached(self):
2547 from rhodecode.lib import rc_cache
2548
2549 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2550 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2551 repo_id=self.repo_id)
2552 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2553
2554 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2555 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2556 return self._get_instance(repo_state_uid=_cache_state_uid)
2557
2558 # we must use thread scoped cache here,
2559 # because each thread of gevent needs it's own not shared connection and cache
2560 # we also alter `args` so the cache key is individual for every green thread.
2561 inv_context_manager = rc_cache.InvalidationContext(
2562 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2563 thread_scoped=True)
2564 with inv_context_manager as invalidation_context:
2565 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2566 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2567
2568 # re-compute and store cache if we get invalidate signal
2569 if invalidation_context.should_invalidate():
2570 instance = get_instance_cached.refresh(*args)
2571 else:
2572 instance = get_instance_cached(*args)
2573
2574 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2575 return instance
2576
2577 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2578 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2579 self.repo_type, self.repo_path, cache)
2580 config = config or self._config
2581 custom_wire = {
2582 'cache': cache, # controls the vcs.remote cache
2583 'repo_state_uid': repo_state_uid
2584 }
2585 repo = get_vcs_instance(
2586 repo_path=safe_str(self.repo_full_path),
2587 config=config,
2588 with_wire=custom_wire,
2589 create=False,
2590 _vcs_alias=self.repo_type)
2591 if repo is not None:
2592 repo.count() # cache rebuild
2593 return repo
2594
2595 def get_shadow_repository_path(self, workspace_id):
2596 from rhodecode.lib.vcs.backends.base import BaseRepository
2597 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2598 self.repo_full_path, self.repo_id, workspace_id)
2599 return shadow_repo_path
2600
2601 def __json__(self):
2602 return {'landing_rev': self.landing_rev}
2603
2604 def get_dict(self):
2605
2606 # Since we transformed `repo_name` to a hybrid property, we need to
2607 # keep compatibility with the code which uses `repo_name` field.
2608
2609 result = super(Repository, self).get_dict()
2610 result['repo_name'] = result.pop('_repo_name', None)
2611 return result
2612
2613
2614 class RepoGroup(Base, BaseModel):
2615 __tablename__ = 'groups'
2616 __table_args__ = (
2617 UniqueConstraint('group_name', 'group_parent_id'),
2618 base_table_args,
2619 )
2620 __mapper_args__ = {'order_by': 'group_name'}
2621
2622 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2623
2624 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2625 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2626 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2627 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2628 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2629 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2630 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2631 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2632 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2633 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2634 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2635
2636 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2637 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2638 parent_group = relationship('RepoGroup', remote_side=group_id)
2639 user = relationship('User')
2640 integrations = relationship('Integration', cascade="all, delete-orphan")
2641
2642 # no cascade, set NULL
2643 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2644
2645 def __init__(self, group_name='', parent_group=None):
2646 self.group_name = group_name
2647 self.parent_group = parent_group
2648
2649 def __unicode__(self):
2650 return u"<%s('id:%s:%s')>" % (
2651 self.__class__.__name__, self.group_id, self.group_name)
2652
2653 @hybrid_property
2654 def group_name(self):
2655 return self._group_name
2656
2657 @group_name.setter
2658 def group_name(self, value):
2659 self._group_name = value
2660 self.group_name_hash = self.hash_repo_group_name(value)
2661
2662 @classmethod
2663 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2664 from rhodecode.lib.vcs.backends.base import EmptyCommit
2665 dummy = EmptyCommit().__json__()
2666 if not changeset_cache_raw:
2667 dummy['source_repo_id'] = repo_id
2668 return json.loads(json.dumps(dummy))
2669
2670 try:
2671 return json.loads(changeset_cache_raw)
2672 except TypeError:
2673 return dummy
2674 except Exception:
2675 log.error(traceback.format_exc())
2676 return dummy
2677
2678 @hybrid_property
2679 def changeset_cache(self):
2680 return self._load_changeset_cache('', self._changeset_cache)
2681
2682 @changeset_cache.setter
2683 def changeset_cache(self, val):
2684 try:
2685 self._changeset_cache = json.dumps(val)
2686 except Exception:
2687 log.error(traceback.format_exc())
2688
2689 @validates('group_parent_id')
2690 def validate_group_parent_id(self, key, val):
2691 """
2692 Check cycle references for a parent group to self
2693 """
2694 if self.group_id and val:
2695 assert val != self.group_id
2696
2697 return val
2698
2699 @hybrid_property
2700 def description_safe(self):
2701 from rhodecode.lib import helpers as h
2702 return h.escape(self.group_description)
2703
2704 @classmethod
2705 def hash_repo_group_name(cls, repo_group_name):
2706 val = remove_formatting(repo_group_name)
2707 val = safe_str(val).lower()
2708 chars = []
2709 for c in val:
2710 if c not in string.ascii_letters:
2711 c = str(ord(c))
2712 chars.append(c)
2713
2714 return ''.join(chars)
2715
2716 @classmethod
2717 def _generate_choice(cls, repo_group):
2718 from webhelpers2.html import literal as _literal
2719 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2720 return repo_group.group_id, _name(repo_group.full_path_splitted)
2721
2722 @classmethod
2723 def groups_choices(cls, groups=None, show_empty_group=True):
2724 if not groups:
2725 groups = cls.query().all()
2726
2727 repo_groups = []
2728 if show_empty_group:
2729 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2730
2731 repo_groups.extend([cls._generate_choice(x) for x in groups])
2732
2733 repo_groups = sorted(
2734 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2735 return repo_groups
2736
2737 @classmethod
2738 def url_sep(cls):
2739 return URL_SEP
2740
2741 @classmethod
2742 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2743 if case_insensitive:
2744 gr = cls.query().filter(func.lower(cls.group_name)
2745 == func.lower(group_name))
2746 else:
2747 gr = cls.query().filter(cls.group_name == group_name)
2748 if cache:
2749 name_key = _hash_key(group_name)
2750 gr = gr.options(
2751 FromCache("sql_cache_short", "get_group_%s" % name_key))
2752 return gr.scalar()
2753
2754 @classmethod
2755 def get_user_personal_repo_group(cls, user_id):
2756 user = User.get(user_id)
2757 if user.username == User.DEFAULT_USER:
2758 return None
2759
2760 return cls.query()\
2761 .filter(cls.personal == true()) \
2762 .filter(cls.user == user) \
2763 .order_by(cls.group_id.asc()) \
2764 .first()
2765
2766 @classmethod
2767 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2768 case_insensitive=True):
2769 q = RepoGroup.query()
2770
2771 if not isinstance(user_id, Optional):
2772 q = q.filter(RepoGroup.user_id == user_id)
2773
2774 if not isinstance(group_id, Optional):
2775 q = q.filter(RepoGroup.group_parent_id == group_id)
2776
2777 if case_insensitive:
2778 q = q.order_by(func.lower(RepoGroup.group_name))
2779 else:
2780 q = q.order_by(RepoGroup.group_name)
2781 return q.all()
2782
2783 @property
2784 def parents(self, parents_recursion_limit=10):
2785 groups = []
2786 if self.parent_group is None:
2787 return groups
2788 cur_gr = self.parent_group
2789 groups.insert(0, cur_gr)
2790 cnt = 0
2791 while 1:
2792 cnt += 1
2793 gr = getattr(cur_gr, 'parent_group', None)
2794 cur_gr = cur_gr.parent_group
2795 if gr is None:
2796 break
2797 if cnt == parents_recursion_limit:
2798 # this will prevent accidental infinit loops
2799 log.error('more than %s parents found for group %s, stopping '
2800 'recursive parent fetching', parents_recursion_limit, self)
2801 break
2802
2803 groups.insert(0, gr)
2804 return groups
2805
2806 @property
2807 def last_commit_cache_update_diff(self):
2808 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2809
2810 @classmethod
2811 def _load_commit_change(cls, last_commit_cache):
2812 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2813 empty_date = datetime.datetime.fromtimestamp(0)
2814 date_latest = last_commit_cache.get('date', empty_date)
2815 try:
2816 return parse_datetime(date_latest)
2817 except Exception:
2818 return empty_date
2819
2820 @property
2821 def last_commit_change(self):
2822 return self._load_commit_change(self.changeset_cache)
2823
2824 @property
2825 def last_db_change(self):
2826 return self.updated_on
2827
2828 @property
2829 def children(self):
2830 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2831
2832 @property
2833 def name(self):
2834 return self.group_name.split(RepoGroup.url_sep())[-1]
2835
2836 @property
2837 def full_path(self):
2838 return self.group_name
2839
2840 @property
2841 def full_path_splitted(self):
2842 return self.group_name.split(RepoGroup.url_sep())
2843
2844 @property
2845 def repositories(self):
2846 return Repository.query()\
2847 .filter(Repository.group == self)\
2848 .order_by(Repository.repo_name)
2849
2850 @property
2851 def repositories_recursive_count(self):
2852 cnt = self.repositories.count()
2853
2854 def children_count(group):
2855 cnt = 0
2856 for child in group.children:
2857 cnt += child.repositories.count()
2858 cnt += children_count(child)
2859 return cnt
2860
2861 return cnt + children_count(self)
2862
2863 def _recursive_objects(self, include_repos=True, include_groups=True):
2864 all_ = []
2865
2866 def _get_members(root_gr):
2867 if include_repos:
2868 for r in root_gr.repositories:
2869 all_.append(r)
2870 childs = root_gr.children.all()
2871 if childs:
2872 for gr in childs:
2873 if include_groups:
2874 all_.append(gr)
2875 _get_members(gr)
2876
2877 root_group = []
2878 if include_groups:
2879 root_group = [self]
2880
2881 _get_members(self)
2882 return root_group + all_
2883
2884 def recursive_groups_and_repos(self):
2885 """
2886 Recursive return all groups, with repositories in those groups
2887 """
2888 return self._recursive_objects()
2889
2890 def recursive_groups(self):
2891 """
2892 Returns all children groups for this group including children of children
2893 """
2894 return self._recursive_objects(include_repos=False)
2895
2896 def recursive_repos(self):
2897 """
2898 Returns all children repositories for this group
2899 """
2900 return self._recursive_objects(include_groups=False)
2901
2902 def get_new_name(self, group_name):
2903 """
2904 returns new full group name based on parent and new name
2905
2906 :param group_name:
2907 """
2908 path_prefix = (self.parent_group.full_path_splitted if
2909 self.parent_group else [])
2910 return RepoGroup.url_sep().join(path_prefix + [group_name])
2911
2912 def update_commit_cache(self, config=None):
2913 """
2914 Update cache of last commit for newest repository inside this repository group.
2915 cache_keys should be::
2916
2917 source_repo_id
2918 short_id
2919 raw_id
2920 revision
2921 parents
2922 message
2923 date
2924 author
2925
2926 """
2927 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2928 empty_date = datetime.datetime.fromtimestamp(0)
2929
2930 def repo_groups_and_repos(root_gr):
2931 for _repo in root_gr.repositories:
2932 yield _repo
2933 for child_group in root_gr.children.all():
2934 yield child_group
2935
2936 latest_repo_cs_cache = {}
2937 for obj in repo_groups_and_repos(self):
2938 repo_cs_cache = obj.changeset_cache
2939 date_latest = latest_repo_cs_cache.get('date', empty_date)
2940 date_current = repo_cs_cache.get('date', empty_date)
2941 current_timestamp = datetime_to_time(parse_datetime(date_latest))
2942 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
2943 latest_repo_cs_cache = repo_cs_cache
2944 if hasattr(obj, 'repo_id'):
2945 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
2946 else:
2947 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
2948
2949 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
2950
2951 latest_repo_cs_cache['updated_on'] = time.time()
2952 self.changeset_cache = latest_repo_cs_cache
2953 self.updated_on = _date_latest
2954 Session().add(self)
2955 Session().commit()
2956
2957 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
2958 self.group_name, latest_repo_cs_cache, _date_latest)
2959
2960 def permissions(self, with_admins=True, with_owner=True,
2961 expand_from_user_groups=False):
2962 """
2963 Permissions for repository groups
2964 """
2965 _admin_perm = 'group.admin'
2966
2967 owner_row = []
2968 if with_owner:
2969 usr = AttributeDict(self.user.get_dict())
2970 usr.owner_row = True
2971 usr.permission = _admin_perm
2972 owner_row.append(usr)
2973
2974 super_admin_ids = []
2975 super_admin_rows = []
2976 if with_admins:
2977 for usr in User.get_all_super_admins():
2978 super_admin_ids.append(usr.user_id)
2979 # if this admin is also owner, don't double the record
2980 if usr.user_id == owner_row[0].user_id:
2981 owner_row[0].admin_row = True
2982 else:
2983 usr = AttributeDict(usr.get_dict())
2984 usr.admin_row = True
2985 usr.permission = _admin_perm
2986 super_admin_rows.append(usr)
2987
2988 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2989 q = q.options(joinedload(UserRepoGroupToPerm.group),
2990 joinedload(UserRepoGroupToPerm.user),
2991 joinedload(UserRepoGroupToPerm.permission),)
2992
2993 # get owners and admins and permissions. We do a trick of re-writing
2994 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2995 # has a global reference and changing one object propagates to all
2996 # others. This means if admin is also an owner admin_row that change
2997 # would propagate to both objects
2998 perm_rows = []
2999 for _usr in q.all():
3000 usr = AttributeDict(_usr.user.get_dict())
3001 # if this user is also owner/admin, mark as duplicate record
3002 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3003 usr.duplicate_perm = True
3004 usr.permission = _usr.permission.permission_name
3005 perm_rows.append(usr)
3006
3007 # filter the perm rows by 'default' first and then sort them by
3008 # admin,write,read,none permissions sorted again alphabetically in
3009 # each group
3010 perm_rows = sorted(perm_rows, key=display_user_sort)
3011
3012 user_groups_rows = []
3013 if expand_from_user_groups:
3014 for ug in self.permission_user_groups(with_members=True):
3015 for user_data in ug.members:
3016 user_groups_rows.append(user_data)
3017
3018 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3019
3020 def permission_user_groups(self, with_members=False):
3021 q = UserGroupRepoGroupToPerm.query()\
3022 .filter(UserGroupRepoGroupToPerm.group == self)
3023 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3024 joinedload(UserGroupRepoGroupToPerm.users_group),
3025 joinedload(UserGroupRepoGroupToPerm.permission),)
3026
3027 perm_rows = []
3028 for _user_group in q.all():
3029 entry = AttributeDict(_user_group.users_group.get_dict())
3030 entry.permission = _user_group.permission.permission_name
3031 if with_members:
3032 entry.members = [x.user.get_dict()
3033 for x in _user_group.users_group.members]
3034 perm_rows.append(entry)
3035
3036 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3037 return perm_rows
3038
3039 def get_api_data(self):
3040 """
3041 Common function for generating api data
3042
3043 """
3044 group = self
3045 data = {
3046 'group_id': group.group_id,
3047 'group_name': group.group_name,
3048 'group_description': group.description_safe,
3049 'parent_group': group.parent_group.group_name if group.parent_group else None,
3050 'repositories': [x.repo_name for x in group.repositories],
3051 'owner': group.user.username,
3052 }
3053 return data
3054
3055 def get_dict(self):
3056 # Since we transformed `group_name` to a hybrid property, we need to
3057 # keep compatibility with the code which uses `group_name` field.
3058 result = super(RepoGroup, self).get_dict()
3059 result['group_name'] = result.pop('_group_name', None)
3060 return result
3061
3062
3063 class Permission(Base, BaseModel):
3064 __tablename__ = 'permissions'
3065 __table_args__ = (
3066 Index('p_perm_name_idx', 'permission_name'),
3067 base_table_args,
3068 )
3069
3070 PERMS = [
3071 ('hg.admin', _('RhodeCode Super Administrator')),
3072
3073 ('repository.none', _('Repository no access')),
3074 ('repository.read', _('Repository read access')),
3075 ('repository.write', _('Repository write access')),
3076 ('repository.admin', _('Repository admin access')),
3077
3078 ('group.none', _('Repository group no access')),
3079 ('group.read', _('Repository group read access')),
3080 ('group.write', _('Repository group write access')),
3081 ('group.admin', _('Repository group admin access')),
3082
3083 ('usergroup.none', _('User group no access')),
3084 ('usergroup.read', _('User group read access')),
3085 ('usergroup.write', _('User group write access')),
3086 ('usergroup.admin', _('User group admin access')),
3087
3088 ('branch.none', _('Branch no permissions')),
3089 ('branch.merge', _('Branch access by web merge')),
3090 ('branch.push', _('Branch access by push')),
3091 ('branch.push_force', _('Branch access by push with force')),
3092
3093 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3094 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3095
3096 ('hg.usergroup.create.false', _('User Group creation disabled')),
3097 ('hg.usergroup.create.true', _('User Group creation enabled')),
3098
3099 ('hg.create.none', _('Repository creation disabled')),
3100 ('hg.create.repository', _('Repository creation enabled')),
3101 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3102 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3103
3104 ('hg.fork.none', _('Repository forking disabled')),
3105 ('hg.fork.repository', _('Repository forking enabled')),
3106
3107 ('hg.register.none', _('Registration disabled')),
3108 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3109 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3110
3111 ('hg.password_reset.enabled', _('Password reset enabled')),
3112 ('hg.password_reset.hidden', _('Password reset hidden')),
3113 ('hg.password_reset.disabled', _('Password reset disabled')),
3114
3115 ('hg.extern_activate.manual', _('Manual activation of external account')),
3116 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3117
3118 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3119 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3120 ]
3121
3122 # definition of system default permissions for DEFAULT user, created on
3123 # system setup
3124 DEFAULT_USER_PERMISSIONS = [
3125 # object perms
3126 'repository.read',
3127 'group.read',
3128 'usergroup.read',
3129 # branch, for backward compat we need same value as before so forced pushed
3130 'branch.push_force',
3131 # global
3132 'hg.create.repository',
3133 'hg.repogroup.create.false',
3134 'hg.usergroup.create.false',
3135 'hg.create.write_on_repogroup.true',
3136 'hg.fork.repository',
3137 'hg.register.manual_activate',
3138 'hg.password_reset.enabled',
3139 'hg.extern_activate.auto',
3140 'hg.inherit_default_perms.true',
3141 ]
3142
3143 # defines which permissions are more important higher the more important
3144 # Weight defines which permissions are more important.
3145 # The higher number the more important.
3146 PERM_WEIGHTS = {
3147 'repository.none': 0,
3148 'repository.read': 1,
3149 'repository.write': 3,
3150 'repository.admin': 4,
3151
3152 'group.none': 0,
3153 'group.read': 1,
3154 'group.write': 3,
3155 'group.admin': 4,
3156
3157 'usergroup.none': 0,
3158 'usergroup.read': 1,
3159 'usergroup.write': 3,
3160 'usergroup.admin': 4,
3161
3162 'branch.none': 0,
3163 'branch.merge': 1,
3164 'branch.push': 3,
3165 'branch.push_force': 4,
3166
3167 'hg.repogroup.create.false': 0,
3168 'hg.repogroup.create.true': 1,
3169
3170 'hg.usergroup.create.false': 0,
3171 'hg.usergroup.create.true': 1,
3172
3173 'hg.fork.none': 0,
3174 'hg.fork.repository': 1,
3175 'hg.create.none': 0,
3176 'hg.create.repository': 1
3177 }
3178
3179 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3180 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3181 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3182
3183 def __unicode__(self):
3184 return u"<%s('%s:%s')>" % (
3185 self.__class__.__name__, self.permission_id, self.permission_name
3186 )
3187
3188 @classmethod
3189 def get_by_key(cls, key):
3190 return cls.query().filter(cls.permission_name == key).scalar()
3191
3192 @classmethod
3193 def get_default_repo_perms(cls, user_id, repo_id=None):
3194 q = Session().query(UserRepoToPerm, Repository, Permission)\
3195 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3196 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3197 .filter(UserRepoToPerm.user_id == user_id)
3198 if repo_id:
3199 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3200 return q.all()
3201
3202 @classmethod
3203 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3204 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3205 .join(
3206 Permission,
3207 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3208 .join(
3209 UserRepoToPerm,
3210 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3211 .filter(UserRepoToPerm.user_id == user_id)
3212
3213 if repo_id:
3214 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3215 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3216
3217 @classmethod
3218 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3219 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3220 .join(
3221 Permission,
3222 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3223 .join(
3224 Repository,
3225 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3226 .join(
3227 UserGroup,
3228 UserGroupRepoToPerm.users_group_id ==
3229 UserGroup.users_group_id)\
3230 .join(
3231 UserGroupMember,
3232 UserGroupRepoToPerm.users_group_id ==
3233 UserGroupMember.users_group_id)\
3234 .filter(
3235 UserGroupMember.user_id == user_id,
3236 UserGroup.users_group_active == true())
3237 if repo_id:
3238 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3239 return q.all()
3240
3241 @classmethod
3242 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3243 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3244 .join(
3245 Permission,
3246 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3247 .join(
3248 UserGroupRepoToPerm,
3249 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3250 .join(
3251 UserGroup,
3252 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3253 .join(
3254 UserGroupMember,
3255 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3256 .filter(
3257 UserGroupMember.user_id == user_id,
3258 UserGroup.users_group_active == true())
3259
3260 if repo_id:
3261 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3262 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3263
3264 @classmethod
3265 def get_default_group_perms(cls, user_id, repo_group_id=None):
3266 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3267 .join(
3268 Permission,
3269 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3270 .join(
3271 RepoGroup,
3272 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3273 .filter(UserRepoGroupToPerm.user_id == user_id)
3274 if repo_group_id:
3275 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3276 return q.all()
3277
3278 @classmethod
3279 def get_default_group_perms_from_user_group(
3280 cls, user_id, repo_group_id=None):
3281 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3282 .join(
3283 Permission,
3284 UserGroupRepoGroupToPerm.permission_id ==
3285 Permission.permission_id)\
3286 .join(
3287 RepoGroup,
3288 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3289 .join(
3290 UserGroup,
3291 UserGroupRepoGroupToPerm.users_group_id ==
3292 UserGroup.users_group_id)\
3293 .join(
3294 UserGroupMember,
3295 UserGroupRepoGroupToPerm.users_group_id ==
3296 UserGroupMember.users_group_id)\
3297 .filter(
3298 UserGroupMember.user_id == user_id,
3299 UserGroup.users_group_active == true())
3300 if repo_group_id:
3301 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3302 return q.all()
3303
3304 @classmethod
3305 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3306 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3307 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3308 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3309 .filter(UserUserGroupToPerm.user_id == user_id)
3310 if user_group_id:
3311 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3312 return q.all()
3313
3314 @classmethod
3315 def get_default_user_group_perms_from_user_group(
3316 cls, user_id, user_group_id=None):
3317 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3318 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3319 .join(
3320 Permission,
3321 UserGroupUserGroupToPerm.permission_id ==
3322 Permission.permission_id)\
3323 .join(
3324 TargetUserGroup,
3325 UserGroupUserGroupToPerm.target_user_group_id ==
3326 TargetUserGroup.users_group_id)\
3327 .join(
3328 UserGroup,
3329 UserGroupUserGroupToPerm.user_group_id ==
3330 UserGroup.users_group_id)\
3331 .join(
3332 UserGroupMember,
3333 UserGroupUserGroupToPerm.user_group_id ==
3334 UserGroupMember.users_group_id)\
3335 .filter(
3336 UserGroupMember.user_id == user_id,
3337 UserGroup.users_group_active == true())
3338 if user_group_id:
3339 q = q.filter(
3340 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3341
3342 return q.all()
3343
3344
3345 class UserRepoToPerm(Base, BaseModel):
3346 __tablename__ = 'repo_to_perm'
3347 __table_args__ = (
3348 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3349 base_table_args
3350 )
3351
3352 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3353 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3354 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3355 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3356
3357 user = relationship('User')
3358 repository = relationship('Repository')
3359 permission = relationship('Permission')
3360
3361 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3362
3363 @classmethod
3364 def create(cls, user, repository, permission):
3365 n = cls()
3366 n.user = user
3367 n.repository = repository
3368 n.permission = permission
3369 Session().add(n)
3370 return n
3371
3372 def __unicode__(self):
3373 return u'<%s => %s >' % (self.user, self.repository)
3374
3375
3376 class UserUserGroupToPerm(Base, BaseModel):
3377 __tablename__ = 'user_user_group_to_perm'
3378 __table_args__ = (
3379 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3380 base_table_args
3381 )
3382
3383 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3384 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3385 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3386 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3387
3388 user = relationship('User')
3389 user_group = relationship('UserGroup')
3390 permission = relationship('Permission')
3391
3392 @classmethod
3393 def create(cls, user, user_group, permission):
3394 n = cls()
3395 n.user = user
3396 n.user_group = user_group
3397 n.permission = permission
3398 Session().add(n)
3399 return n
3400
3401 def __unicode__(self):
3402 return u'<%s => %s >' % (self.user, self.user_group)
3403
3404
3405 class UserToPerm(Base, BaseModel):
3406 __tablename__ = 'user_to_perm'
3407 __table_args__ = (
3408 UniqueConstraint('user_id', 'permission_id'),
3409 base_table_args
3410 )
3411
3412 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3413 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3414 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3415
3416 user = relationship('User')
3417 permission = relationship('Permission', lazy='joined')
3418
3419 def __unicode__(self):
3420 return u'<%s => %s >' % (self.user, self.permission)
3421
3422
3423 class UserGroupRepoToPerm(Base, BaseModel):
3424 __tablename__ = 'users_group_repo_to_perm'
3425 __table_args__ = (
3426 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3427 base_table_args
3428 )
3429
3430 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3431 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3432 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3433 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3434
3435 users_group = relationship('UserGroup')
3436 permission = relationship('Permission')
3437 repository = relationship('Repository')
3438 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3439
3440 @classmethod
3441 def create(cls, users_group, repository, permission):
3442 n = cls()
3443 n.users_group = users_group
3444 n.repository = repository
3445 n.permission = permission
3446 Session().add(n)
3447 return n
3448
3449 def __unicode__(self):
3450 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3451
3452
3453 class UserGroupUserGroupToPerm(Base, BaseModel):
3454 __tablename__ = 'user_group_user_group_to_perm'
3455 __table_args__ = (
3456 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3457 CheckConstraint('target_user_group_id != user_group_id'),
3458 base_table_args
3459 )
3460
3461 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)
3462 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3463 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3464 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3465
3466 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3467 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3468 permission = relationship('Permission')
3469
3470 @classmethod
3471 def create(cls, target_user_group, user_group, permission):
3472 n = cls()
3473 n.target_user_group = target_user_group
3474 n.user_group = user_group
3475 n.permission = permission
3476 Session().add(n)
3477 return n
3478
3479 def __unicode__(self):
3480 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3481
3482
3483 class UserGroupToPerm(Base, BaseModel):
3484 __tablename__ = 'users_group_to_perm'
3485 __table_args__ = (
3486 UniqueConstraint('users_group_id', 'permission_id',),
3487 base_table_args
3488 )
3489
3490 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3491 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3492 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3493
3494 users_group = relationship('UserGroup')
3495 permission = relationship('Permission')
3496
3497
3498 class UserRepoGroupToPerm(Base, BaseModel):
3499 __tablename__ = 'user_repo_group_to_perm'
3500 __table_args__ = (
3501 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3502 base_table_args
3503 )
3504
3505 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3506 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3507 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3508 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3509
3510 user = relationship('User')
3511 group = relationship('RepoGroup')
3512 permission = relationship('Permission')
3513
3514 @classmethod
3515 def create(cls, user, repository_group, permission):
3516 n = cls()
3517 n.user = user
3518 n.group = repository_group
3519 n.permission = permission
3520 Session().add(n)
3521 return n
3522
3523
3524 class UserGroupRepoGroupToPerm(Base, BaseModel):
3525 __tablename__ = 'users_group_repo_group_to_perm'
3526 __table_args__ = (
3527 UniqueConstraint('users_group_id', 'group_id'),
3528 base_table_args
3529 )
3530
3531 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)
3532 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3533 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3534 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3535
3536 users_group = relationship('UserGroup')
3537 permission = relationship('Permission')
3538 group = relationship('RepoGroup')
3539
3540 @classmethod
3541 def create(cls, user_group, repository_group, permission):
3542 n = cls()
3543 n.users_group = user_group
3544 n.group = repository_group
3545 n.permission = permission
3546 Session().add(n)
3547 return n
3548
3549 def __unicode__(self):
3550 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3551
3552
3553 class Statistics(Base, BaseModel):
3554 __tablename__ = 'statistics'
3555 __table_args__ = (
3556 base_table_args
3557 )
3558
3559 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3560 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3561 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3562 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3563 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3564 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3565
3566 repository = relationship('Repository', single_parent=True)
3567
3568
3569 class UserFollowing(Base, BaseModel):
3570 __tablename__ = 'user_followings'
3571 __table_args__ = (
3572 UniqueConstraint('user_id', 'follows_repository_id'),
3573 UniqueConstraint('user_id', 'follows_user_id'),
3574 base_table_args
3575 )
3576
3577 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3578 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3579 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3580 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3581 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3582
3583 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3584
3585 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3586 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3587
3588 @classmethod
3589 def get_repo_followers(cls, repo_id):
3590 return cls.query().filter(cls.follows_repo_id == repo_id)
3591
3592
3593 class CacheKey(Base, BaseModel):
3594 __tablename__ = 'cache_invalidation'
3595 __table_args__ = (
3596 UniqueConstraint('cache_key'),
3597 Index('key_idx', 'cache_key'),
3598 base_table_args,
3599 )
3600
3601 CACHE_TYPE_FEED = 'FEED'
3602
3603 # namespaces used to register process/thread aware caches
3604 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3605 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3606
3607 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3608 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3609 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3610 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3611 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3612
3613 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3614 self.cache_key = cache_key
3615 self.cache_args = cache_args
3616 self.cache_active = False
3617 # first key should be same for all entries, since all workers should share it
3618 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3619
3620 def __unicode__(self):
3621 return u"<%s('%s:%s[%s]')>" % (
3622 self.__class__.__name__,
3623 self.cache_id, self.cache_key, self.cache_active)
3624
3625 def _cache_key_partition(self):
3626 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3627 return prefix, repo_name, suffix
3628
3629 def get_prefix(self):
3630 """
3631 Try to extract prefix from existing cache key. The key could consist
3632 of prefix, repo_name, suffix
3633 """
3634 # this returns prefix, repo_name, suffix
3635 return self._cache_key_partition()[0]
3636
3637 def get_suffix(self):
3638 """
3639 get suffix that might have been used in _get_cache_key to
3640 generate self.cache_key. Only used for informational purposes
3641 in repo_edit.mako.
3642 """
3643 # prefix, repo_name, suffix
3644 return self._cache_key_partition()[2]
3645
3646 @classmethod
3647 def generate_new_state_uid(cls, based_on=None):
3648 if based_on:
3649 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3650 else:
3651 return str(uuid.uuid4())
3652
3653 @classmethod
3654 def delete_all_cache(cls):
3655 """
3656 Delete all cache keys from database.
3657 Should only be run when all instances are down and all entries
3658 thus stale.
3659 """
3660 cls.query().delete()
3661 Session().commit()
3662
3663 @classmethod
3664 def set_invalidate(cls, cache_uid, delete=False):
3665 """
3666 Mark all caches of a repo as invalid in the database.
3667 """
3668
3669 try:
3670 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3671 if delete:
3672 qry.delete()
3673 log.debug('cache objects deleted for cache args %s',
3674 safe_str(cache_uid))
3675 else:
3676 qry.update({"cache_active": False,
3677 "cache_state_uid": cls.generate_new_state_uid()})
3678 log.debug('cache objects marked as invalid for cache args %s',
3679 safe_str(cache_uid))
3680
3681 Session().commit()
3682 except Exception:
3683 log.exception(
3684 'Cache key invalidation failed for cache args %s',
3685 safe_str(cache_uid))
3686 Session().rollback()
3687
3688 @classmethod
3689 def get_active_cache(cls, cache_key):
3690 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3691 if inv_obj:
3692 return inv_obj
3693 return None
3694
3695 @classmethod
3696 def get_namespace_map(cls, namespace):
3697 return {
3698 x.cache_key: x
3699 for x in cls.query().filter(cls.cache_args == namespace)}
3700
3701
3702 class ChangesetComment(Base, BaseModel):
3703 __tablename__ = 'changeset_comments'
3704 __table_args__ = (
3705 Index('cc_revision_idx', 'revision'),
3706 base_table_args,
3707 )
3708
3709 COMMENT_OUTDATED = u'comment_outdated'
3710 COMMENT_TYPE_NOTE = u'note'
3711 COMMENT_TYPE_TODO = u'todo'
3712 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3713
3714 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3715 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3716 revision = Column('revision', String(40), nullable=True)
3717 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3718 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3719 line_no = Column('line_no', Unicode(10), nullable=True)
3720 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3721 f_path = Column('f_path', Unicode(1000), nullable=True)
3722 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3723 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3724 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3725 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3726 renderer = Column('renderer', Unicode(64), nullable=True)
3727 display_state = Column('display_state', Unicode(128), nullable=True)
3728
3729 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3730 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3731
3732 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3733 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3734
3735 author = relationship('User', lazy='joined')
3736 repo = relationship('Repository')
3737 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
3738 pull_request = relationship('PullRequest', lazy='joined')
3739 pull_request_version = relationship('PullRequestVersion')
3740
3741 @classmethod
3742 def get_users(cls, revision=None, pull_request_id=None):
3743 """
3744 Returns user associated with this ChangesetComment. ie those
3745 who actually commented
3746
3747 :param cls:
3748 :param revision:
3749 """
3750 q = Session().query(User)\
3751 .join(ChangesetComment.author)
3752 if revision:
3753 q = q.filter(cls.revision == revision)
3754 elif pull_request_id:
3755 q = q.filter(cls.pull_request_id == pull_request_id)
3756 return q.all()
3757
3758 @classmethod
3759 def get_index_from_version(cls, pr_version, versions):
3760 num_versions = [x.pull_request_version_id for x in versions]
3761 try:
3762 return num_versions.index(pr_version) +1
3763 except (IndexError, ValueError):
3764 return
3765
3766 @property
3767 def outdated(self):
3768 return self.display_state == self.COMMENT_OUTDATED
3769
3770 def outdated_at_version(self, version):
3771 """
3772 Checks if comment is outdated for given pull request version
3773 """
3774 return self.outdated and self.pull_request_version_id != version
3775
3776 def older_than_version(self, version):
3777 """
3778 Checks if comment is made from previous version than given
3779 """
3780 if version is None:
3781 return self.pull_request_version_id is not None
3782
3783 return self.pull_request_version_id < version
3784
3785 @property
3786 def resolved(self):
3787 return self.resolved_by[0] if self.resolved_by else None
3788
3789 @property
3790 def is_todo(self):
3791 return self.comment_type == self.COMMENT_TYPE_TODO
3792
3793 @property
3794 def is_inline(self):
3795 return self.line_no and self.f_path
3796
3797 def get_index_version(self, versions):
3798 return self.get_index_from_version(
3799 self.pull_request_version_id, versions)
3800
3801 def __repr__(self):
3802 if self.comment_id:
3803 return '<DB:Comment #%s>' % self.comment_id
3804 else:
3805 return '<DB:Comment at %#x>' % id(self)
3806
3807 def get_api_data(self):
3808 comment = self
3809 data = {
3810 'comment_id': comment.comment_id,
3811 'comment_type': comment.comment_type,
3812 'comment_text': comment.text,
3813 'comment_status': comment.status_change,
3814 'comment_f_path': comment.f_path,
3815 'comment_lineno': comment.line_no,
3816 'comment_author': comment.author,
3817 'comment_created_on': comment.created_on,
3818 'comment_resolved_by': self.resolved
3819 }
3820 return data
3821
3822 def __json__(self):
3823 data = dict()
3824 data.update(self.get_api_data())
3825 return data
3826
3827
3828 class ChangesetStatus(Base, BaseModel):
3829 __tablename__ = 'changeset_statuses'
3830 __table_args__ = (
3831 Index('cs_revision_idx', 'revision'),
3832 Index('cs_version_idx', 'version'),
3833 UniqueConstraint('repo_id', 'revision', 'version'),
3834 base_table_args
3835 )
3836
3837 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3838 STATUS_APPROVED = 'approved'
3839 STATUS_REJECTED = 'rejected'
3840 STATUS_UNDER_REVIEW = 'under_review'
3841
3842 STATUSES = [
3843 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3844 (STATUS_APPROVED, _("Approved")),
3845 (STATUS_REJECTED, _("Rejected")),
3846 (STATUS_UNDER_REVIEW, _("Under Review")),
3847 ]
3848
3849 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3850 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3851 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3852 revision = Column('revision', String(40), nullable=False)
3853 status = Column('status', String(128), nullable=False, default=DEFAULT)
3854 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3855 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3856 version = Column('version', Integer(), nullable=False, default=0)
3857 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3858
3859 author = relationship('User', lazy='joined')
3860 repo = relationship('Repository')
3861 comment = relationship('ChangesetComment', lazy='joined')
3862 pull_request = relationship('PullRequest', lazy='joined')
3863
3864 def __unicode__(self):
3865 return u"<%s('%s[v%s]:%s')>" % (
3866 self.__class__.__name__,
3867 self.status, self.version, self.author
3868 )
3869
3870 @classmethod
3871 def get_status_lbl(cls, value):
3872 return dict(cls.STATUSES).get(value)
3873
3874 @property
3875 def status_lbl(self):
3876 return ChangesetStatus.get_status_lbl(self.status)
3877
3878 def get_api_data(self):
3879 status = self
3880 data = {
3881 'status_id': status.changeset_status_id,
3882 'status': status.status,
3883 }
3884 return data
3885
3886 def __json__(self):
3887 data = dict()
3888 data.update(self.get_api_data())
3889 return data
3890
3891
3892 class _SetState(object):
3893 """
3894 Context processor allowing changing state for sensitive operation such as
3895 pull request update or merge
3896 """
3897
3898 def __init__(self, pull_request, pr_state, back_state=None):
3899 self._pr = pull_request
3900 self._org_state = back_state or pull_request.pull_request_state
3901 self._pr_state = pr_state
3902 self._current_state = None
3903
3904 def __enter__(self):
3905 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
3906 self._pr, self._pr_state)
3907 self.set_pr_state(self._pr_state)
3908 return self
3909
3910 def __exit__(self, exc_type, exc_val, exc_tb):
3911 if exc_val is not None:
3912 log.error(traceback.format_exc(exc_tb))
3913 return None
3914
3915 self.set_pr_state(self._org_state)
3916 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
3917 self._pr, self._org_state)
3918
3919 @property
3920 def state(self):
3921 return self._current_state
3922
3923 def set_pr_state(self, pr_state):
3924 try:
3925 self._pr.pull_request_state = pr_state
3926 Session().add(self._pr)
3927 Session().commit()
3928 self._current_state = pr_state
3929 except Exception:
3930 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
3931 raise
3932
3933
3934 class _PullRequestBase(BaseModel):
3935 """
3936 Common attributes of pull request and version entries.
3937 """
3938
3939 # .status values
3940 STATUS_NEW = u'new'
3941 STATUS_OPEN = u'open'
3942 STATUS_CLOSED = u'closed'
3943
3944 # available states
3945 STATE_CREATING = u'creating'
3946 STATE_UPDATING = u'updating'
3947 STATE_MERGING = u'merging'
3948 STATE_CREATED = u'created'
3949
3950 title = Column('title', Unicode(255), nullable=True)
3951 description = Column(
3952 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3953 nullable=True)
3954 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
3955
3956 # new/open/closed status of pull request (not approve/reject/etc)
3957 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3958 created_on = Column(
3959 'created_on', DateTime(timezone=False), nullable=False,
3960 default=datetime.datetime.now)
3961 updated_on = Column(
3962 'updated_on', DateTime(timezone=False), nullable=False,
3963 default=datetime.datetime.now)
3964
3965 pull_request_state = Column("pull_request_state", String(255), nullable=True)
3966
3967 @declared_attr
3968 def user_id(cls):
3969 return Column(
3970 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3971 unique=None)
3972
3973 # 500 revisions max
3974 _revisions = Column(
3975 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3976
3977 @declared_attr
3978 def source_repo_id(cls):
3979 # TODO: dan: rename column to source_repo_id
3980 return Column(
3981 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3982 nullable=False)
3983
3984 _source_ref = Column('org_ref', Unicode(255), nullable=False)
3985
3986 @hybrid_property
3987 def source_ref(self):
3988 return self._source_ref
3989
3990 @source_ref.setter
3991 def source_ref(self, val):
3992 parts = (val or '').split(':')
3993 if len(parts) != 3:
3994 raise ValueError(
3995 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
3996 self._source_ref = safe_unicode(val)
3997
3998 _target_ref = Column('other_ref', Unicode(255), nullable=False)
3999
4000 @hybrid_property
4001 def target_ref(self):
4002 return self._target_ref
4003
4004 @target_ref.setter
4005 def target_ref(self, val):
4006 parts = (val or '').split(':')
4007 if len(parts) != 3:
4008 raise ValueError(
4009 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4010 self._target_ref = safe_unicode(val)
4011
4012 @declared_attr
4013 def target_repo_id(cls):
4014 # TODO: dan: rename column to target_repo_id
4015 return Column(
4016 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4017 nullable=False)
4018
4019 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4020
4021 # TODO: dan: rename column to last_merge_source_rev
4022 _last_merge_source_rev = Column(
4023 'last_merge_org_rev', String(40), nullable=True)
4024 # TODO: dan: rename column to last_merge_target_rev
4025 _last_merge_target_rev = Column(
4026 'last_merge_other_rev', String(40), nullable=True)
4027 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4028 last_merge_metadata = Column(
4029 'last_merge_metadata', MutationObj.as_mutable(
4030 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4031
4032 merge_rev = Column('merge_rev', String(40), nullable=True)
4033
4034 reviewer_data = Column(
4035 'reviewer_data_json', MutationObj.as_mutable(
4036 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4037
4038 @property
4039 def reviewer_data_json(self):
4040 return json.dumps(self.reviewer_data)
4041
4042 @property
4043 def work_in_progress(self):
4044 """checks if pull request is work in progress by checking the title"""
4045 title = self.title.upper()
4046 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4047 return True
4048 return False
4049
4050 @hybrid_property
4051 def description_safe(self):
4052 from rhodecode.lib import helpers as h
4053 return h.escape(self.description)
4054
4055 @hybrid_property
4056 def revisions(self):
4057 return self._revisions.split(':') if self._revisions else []
4058
4059 @revisions.setter
4060 def revisions(self, val):
4061 self._revisions = u':'.join(val)
4062
4063 @hybrid_property
4064 def last_merge_status(self):
4065 return safe_int(self._last_merge_status)
4066
4067 @last_merge_status.setter
4068 def last_merge_status(self, val):
4069 self._last_merge_status = val
4070
4071 @declared_attr
4072 def author(cls):
4073 return relationship('User', lazy='joined')
4074
4075 @declared_attr
4076 def source_repo(cls):
4077 return relationship(
4078 'Repository',
4079 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4080
4081 @property
4082 def source_ref_parts(self):
4083 return self.unicode_to_reference(self.source_ref)
4084
4085 @declared_attr
4086 def target_repo(cls):
4087 return relationship(
4088 'Repository',
4089 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4090
4091 @property
4092 def target_ref_parts(self):
4093 return self.unicode_to_reference(self.target_ref)
4094
4095 @property
4096 def shadow_merge_ref(self):
4097 return self.unicode_to_reference(self._shadow_merge_ref)
4098
4099 @shadow_merge_ref.setter
4100 def shadow_merge_ref(self, ref):
4101 self._shadow_merge_ref = self.reference_to_unicode(ref)
4102
4103 @staticmethod
4104 def unicode_to_reference(raw):
4105 """
4106 Convert a unicode (or string) to a reference object.
4107 If unicode evaluates to False it returns None.
4108 """
4109 if raw:
4110 refs = raw.split(':')
4111 return Reference(*refs)
4112 else:
4113 return None
4114
4115 @staticmethod
4116 def reference_to_unicode(ref):
4117 """
4118 Convert a reference object to unicode.
4119 If reference is None it returns None.
4120 """
4121 if ref:
4122 return u':'.join(ref)
4123 else:
4124 return None
4125
4126 def get_api_data(self, with_merge_state=True):
4127 from rhodecode.model.pull_request import PullRequestModel
4128
4129 pull_request = self
4130 if with_merge_state:
4131 merge_response, merge_status, msg = \
4132 PullRequestModel().merge_status(pull_request)
4133 merge_state = {
4134 'status': merge_status,
4135 'message': safe_unicode(msg),
4136 }
4137 else:
4138 merge_state = {'status': 'not_available',
4139 'message': 'not_available'}
4140
4141 merge_data = {
4142 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4143 'reference': (
4144 pull_request.shadow_merge_ref._asdict()
4145 if pull_request.shadow_merge_ref else None),
4146 }
4147
4148 data = {
4149 'pull_request_id': pull_request.pull_request_id,
4150 'url': PullRequestModel().get_url(pull_request),
4151 'title': pull_request.title,
4152 'description': pull_request.description,
4153 'status': pull_request.status,
4154 'state': pull_request.pull_request_state,
4155 'created_on': pull_request.created_on,
4156 'updated_on': pull_request.updated_on,
4157 'commit_ids': pull_request.revisions,
4158 'review_status': pull_request.calculated_review_status(),
4159 'mergeable': merge_state,
4160 'source': {
4161 'clone_url': pull_request.source_repo.clone_url(),
4162 'repository': pull_request.source_repo.repo_name,
4163 'reference': {
4164 'name': pull_request.source_ref_parts.name,
4165 'type': pull_request.source_ref_parts.type,
4166 'commit_id': pull_request.source_ref_parts.commit_id,
4167 },
4168 },
4169 'target': {
4170 'clone_url': pull_request.target_repo.clone_url(),
4171 'repository': pull_request.target_repo.repo_name,
4172 'reference': {
4173 'name': pull_request.target_ref_parts.name,
4174 'type': pull_request.target_ref_parts.type,
4175 'commit_id': pull_request.target_ref_parts.commit_id,
4176 },
4177 },
4178 'merge': merge_data,
4179 'author': pull_request.author.get_api_data(include_secrets=False,
4180 details='basic'),
4181 'reviewers': [
4182 {
4183 'user': reviewer.get_api_data(include_secrets=False,
4184 details='basic'),
4185 'reasons': reasons,
4186 'review_status': st[0][1].status if st else 'not_reviewed',
4187 }
4188 for obj, reviewer, reasons, mandatory, st in
4189 pull_request.reviewers_statuses()
4190 ]
4191 }
4192
4193 return data
4194
4195 def set_state(self, pull_request_state, final_state=None):
4196 """
4197 # goes from initial state to updating to initial state.
4198 # initial state can be changed by specifying back_state=
4199 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4200 pull_request.merge()
4201
4202 :param pull_request_state:
4203 :param final_state:
4204
4205 """
4206
4207 return _SetState(self, pull_request_state, back_state=final_state)
4208
4209
4210 class PullRequest(Base, _PullRequestBase):
4211 __tablename__ = 'pull_requests'
4212 __table_args__ = (
4213 base_table_args,
4214 )
4215
4216 pull_request_id = Column(
4217 'pull_request_id', Integer(), nullable=False, primary_key=True)
4218
4219 def __repr__(self):
4220 if self.pull_request_id:
4221 return '<DB:PullRequest #%s>' % self.pull_request_id
4222 else:
4223 return '<DB:PullRequest at %#x>' % id(self)
4224
4225 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4226 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4227 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4228 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4229 lazy='dynamic')
4230
4231 @classmethod
4232 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4233 internal_methods=None):
4234
4235 class PullRequestDisplay(object):
4236 """
4237 Special object wrapper for showing PullRequest data via Versions
4238 It mimics PR object as close as possible. This is read only object
4239 just for display
4240 """
4241
4242 def __init__(self, attrs, internal=None):
4243 self.attrs = attrs
4244 # internal have priority over the given ones via attrs
4245 self.internal = internal or ['versions']
4246
4247 def __getattr__(self, item):
4248 if item in self.internal:
4249 return getattr(self, item)
4250 try:
4251 return self.attrs[item]
4252 except KeyError:
4253 raise AttributeError(
4254 '%s object has no attribute %s' % (self, item))
4255
4256 def __repr__(self):
4257 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4258
4259 def versions(self):
4260 return pull_request_obj.versions.order_by(
4261 PullRequestVersion.pull_request_version_id).all()
4262
4263 def is_closed(self):
4264 return pull_request_obj.is_closed()
4265
4266 def is_state_changing(self):
4267 return pull_request_obj.is_state_changing()
4268
4269 @property
4270 def pull_request_version_id(self):
4271 return getattr(pull_request_obj, 'pull_request_version_id', None)
4272
4273 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4274
4275 attrs.author = StrictAttributeDict(
4276 pull_request_obj.author.get_api_data())
4277 if pull_request_obj.target_repo:
4278 attrs.target_repo = StrictAttributeDict(
4279 pull_request_obj.target_repo.get_api_data())
4280 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4281
4282 if pull_request_obj.source_repo:
4283 attrs.source_repo = StrictAttributeDict(
4284 pull_request_obj.source_repo.get_api_data())
4285 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4286
4287 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4288 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4289 attrs.revisions = pull_request_obj.revisions
4290
4291 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4292 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4293 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4294
4295 return PullRequestDisplay(attrs, internal=internal_methods)
4296
4297 def is_closed(self):
4298 return self.status == self.STATUS_CLOSED
4299
4300 def is_state_changing(self):
4301 return self.pull_request_state != PullRequest.STATE_CREATED
4302
4303 def __json__(self):
4304 return {
4305 'revisions': self.revisions,
4306 'versions': self.versions_count
4307 }
4308
4309 def calculated_review_status(self):
4310 from rhodecode.model.changeset_status import ChangesetStatusModel
4311 return ChangesetStatusModel().calculated_review_status(self)
4312
4313 def reviewers_statuses(self):
4314 from rhodecode.model.changeset_status import ChangesetStatusModel
4315 return ChangesetStatusModel().reviewers_statuses(self)
4316
4317 @property
4318 def workspace_id(self):
4319 from rhodecode.model.pull_request import PullRequestModel
4320 return PullRequestModel()._workspace_id(self)
4321
4322 def get_shadow_repo(self):
4323 workspace_id = self.workspace_id
4324 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4325 if os.path.isdir(shadow_repository_path):
4326 vcs_obj = self.target_repo.scm_instance()
4327 return vcs_obj.get_shadow_instance(shadow_repository_path)
4328
4329 @property
4330 def versions_count(self):
4331 """
4332 return number of versions this PR have, e.g a PR that once been
4333 updated will have 2 versions
4334 """
4335 return self.versions.count() + 1
4336
4337
4338 class PullRequestVersion(Base, _PullRequestBase):
4339 __tablename__ = 'pull_request_versions'
4340 __table_args__ = (
4341 base_table_args,
4342 )
4343
4344 pull_request_version_id = Column(
4345 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4346 pull_request_id = Column(
4347 'pull_request_id', Integer(),
4348 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4349 pull_request = relationship('PullRequest')
4350
4351 def __repr__(self):
4352 if self.pull_request_version_id:
4353 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4354 else:
4355 return '<DB:PullRequestVersion at %#x>' % id(self)
4356
4357 @property
4358 def reviewers(self):
4359 return self.pull_request.reviewers
4360
4361 @property
4362 def versions(self):
4363 return self.pull_request.versions
4364
4365 def is_closed(self):
4366 # calculate from original
4367 return self.pull_request.status == self.STATUS_CLOSED
4368
4369 def is_state_changing(self):
4370 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4371
4372 def calculated_review_status(self):
4373 return self.pull_request.calculated_review_status()
4374
4375 def reviewers_statuses(self):
4376 return self.pull_request.reviewers_statuses()
4377
4378
4379 class PullRequestReviewers(Base, BaseModel):
4380 __tablename__ = 'pull_request_reviewers'
4381 __table_args__ = (
4382 base_table_args,
4383 )
4384
4385 @hybrid_property
4386 def reasons(self):
4387 if not self._reasons:
4388 return []
4389 return self._reasons
4390
4391 @reasons.setter
4392 def reasons(self, val):
4393 val = val or []
4394 if any(not isinstance(x, compat.string_types) for x in val):
4395 raise Exception('invalid reasons type, must be list of strings')
4396 self._reasons = val
4397
4398 pull_requests_reviewers_id = Column(
4399 'pull_requests_reviewers_id', Integer(), nullable=False,
4400 primary_key=True)
4401 pull_request_id = Column(
4402 "pull_request_id", Integer(),
4403 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4404 user_id = Column(
4405 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4406 _reasons = Column(
4407 'reason', MutationList.as_mutable(
4408 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4409
4410 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4411 user = relationship('User')
4412 pull_request = relationship('PullRequest')
4413
4414 rule_data = Column(
4415 'rule_data_json',
4416 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4417
4418 def rule_user_group_data(self):
4419 """
4420 Returns the voting user group rule data for this reviewer
4421 """
4422
4423 if self.rule_data and 'vote_rule' in self.rule_data:
4424 user_group_data = {}
4425 if 'rule_user_group_entry_id' in self.rule_data:
4426 # means a group with voting rules !
4427 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4428 user_group_data['name'] = self.rule_data['rule_name']
4429 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4430
4431 return user_group_data
4432
4433 def __unicode__(self):
4434 return u"<%s('id:%s')>" % (self.__class__.__name__,
4435 self.pull_requests_reviewers_id)
4436
4437
4438 class Notification(Base, BaseModel):
4439 __tablename__ = 'notifications'
4440 __table_args__ = (
4441 Index('notification_type_idx', 'type'),
4442 base_table_args,
4443 )
4444
4445 TYPE_CHANGESET_COMMENT = u'cs_comment'
4446 TYPE_MESSAGE = u'message'
4447 TYPE_MENTION = u'mention'
4448 TYPE_REGISTRATION = u'registration'
4449 TYPE_PULL_REQUEST = u'pull_request'
4450 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4451 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4452
4453 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4454 subject = Column('subject', Unicode(512), nullable=True)
4455 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4456 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4457 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4458 type_ = Column('type', Unicode(255))
4459
4460 created_by_user = relationship('User')
4461 notifications_to_users = relationship('UserNotification', lazy='joined',
4462 cascade="all, delete-orphan")
4463
4464 @property
4465 def recipients(self):
4466 return [x.user for x in UserNotification.query()\
4467 .filter(UserNotification.notification == self)\
4468 .order_by(UserNotification.user_id.asc()).all()]
4469
4470 @classmethod
4471 def create(cls, created_by, subject, body, recipients, type_=None):
4472 if type_ is None:
4473 type_ = Notification.TYPE_MESSAGE
4474
4475 notification = cls()
4476 notification.created_by_user = created_by
4477 notification.subject = subject
4478 notification.body = body
4479 notification.type_ = type_
4480 notification.created_on = datetime.datetime.now()
4481
4482 # For each recipient link the created notification to his account
4483 for u in recipients:
4484 assoc = UserNotification()
4485 assoc.user_id = u.user_id
4486 assoc.notification = notification
4487
4488 # if created_by is inside recipients mark his notification
4489 # as read
4490 if u.user_id == created_by.user_id:
4491 assoc.read = True
4492 Session().add(assoc)
4493
4494 Session().add(notification)
4495
4496 return notification
4497
4498
4499 class UserNotification(Base, BaseModel):
4500 __tablename__ = 'user_to_notification'
4501 __table_args__ = (
4502 UniqueConstraint('user_id', 'notification_id'),
4503 base_table_args
4504 )
4505
4506 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4507 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4508 read = Column('read', Boolean, default=False)
4509 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4510
4511 user = relationship('User', lazy="joined")
4512 notification = relationship('Notification', lazy="joined",
4513 order_by=lambda: Notification.created_on.desc(),)
4514
4515 def mark_as_read(self):
4516 self.read = True
4517 Session().add(self)
4518
4519
4520 class UserNotice(Base, BaseModel):
4521 __tablename__ = 'user_notices'
4522 __table_args__ = (
4523 base_table_args
4524 )
4525
4526 NOTIFICATION_TYPE_MESSAGE = 'message'
4527 NOTIFICATION_TYPE_NOTICE = 'notice'
4528
4529 NOTIFICATION_LEVEL_INFO = 'info'
4530 NOTIFICATION_LEVEL_WARNING = 'warning'
4531 NOTIFICATION_LEVEL_ERROR = 'error'
4532
4533 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4534
4535 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4536 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4537
4538 notice_read = Column('notice_read', Boolean, default=False)
4539
4540 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4541 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4542
4543 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4544 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4545
4546 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4547 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4548
4549
4550 class Gist(Base, BaseModel):
4551 __tablename__ = 'gists'
4552 __table_args__ = (
4553 Index('g_gist_access_id_idx', 'gist_access_id'),
4554 Index('g_created_on_idx', 'created_on'),
4555 base_table_args
4556 )
4557
4558 GIST_PUBLIC = u'public'
4559 GIST_PRIVATE = u'private'
4560 DEFAULT_FILENAME = u'gistfile1.txt'
4561
4562 ACL_LEVEL_PUBLIC = u'acl_public'
4563 ACL_LEVEL_PRIVATE = u'acl_private'
4564
4565 gist_id = Column('gist_id', Integer(), primary_key=True)
4566 gist_access_id = Column('gist_access_id', Unicode(250))
4567 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4568 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4569 gist_expires = Column('gist_expires', Float(53), nullable=False)
4570 gist_type = Column('gist_type', Unicode(128), nullable=False)
4571 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4572 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4573 acl_level = Column('acl_level', Unicode(128), nullable=True)
4574
4575 owner = relationship('User')
4576
4577 def __repr__(self):
4578 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4579
4580 @hybrid_property
4581 def description_safe(self):
4582 from rhodecode.lib import helpers as h
4583 return h.escape(self.gist_description)
4584
4585 @classmethod
4586 def get_or_404(cls, id_):
4587 from pyramid.httpexceptions import HTTPNotFound
4588
4589 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4590 if not res:
4591 raise HTTPNotFound()
4592 return res
4593
4594 @classmethod
4595 def get_by_access_id(cls, gist_access_id):
4596 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4597
4598 def gist_url(self):
4599 from rhodecode.model.gist import GistModel
4600 return GistModel().get_url(self)
4601
4602 @classmethod
4603 def base_path(cls):
4604 """
4605 Returns base path when all gists are stored
4606
4607 :param cls:
4608 """
4609 from rhodecode.model.gist import GIST_STORE_LOC
4610 q = Session().query(RhodeCodeUi)\
4611 .filter(RhodeCodeUi.ui_key == URL_SEP)
4612 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4613 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4614
4615 def get_api_data(self):
4616 """
4617 Common function for generating gist related data for API
4618 """
4619 gist = self
4620 data = {
4621 'gist_id': gist.gist_id,
4622 'type': gist.gist_type,
4623 'access_id': gist.gist_access_id,
4624 'description': gist.gist_description,
4625 'url': gist.gist_url(),
4626 'expires': gist.gist_expires,
4627 'created_on': gist.created_on,
4628 'modified_at': gist.modified_at,
4629 'content': None,
4630 'acl_level': gist.acl_level,
4631 }
4632 return data
4633
4634 def __json__(self):
4635 data = dict(
4636 )
4637 data.update(self.get_api_data())
4638 return data
4639 # SCM functions
4640
4641 def scm_instance(self, **kwargs):
4642 """
4643 Get an instance of VCS Repository
4644
4645 :param kwargs:
4646 """
4647 from rhodecode.model.gist import GistModel
4648 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4649 return get_vcs_instance(
4650 repo_path=safe_str(full_repo_path), create=False,
4651 _vcs_alias=GistModel.vcs_backend)
4652
4653
4654 class ExternalIdentity(Base, BaseModel):
4655 __tablename__ = 'external_identities'
4656 __table_args__ = (
4657 Index('local_user_id_idx', 'local_user_id'),
4658 Index('external_id_idx', 'external_id'),
4659 base_table_args
4660 )
4661
4662 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4663 external_username = Column('external_username', Unicode(1024), default=u'')
4664 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4665 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4666 access_token = Column('access_token', String(1024), default=u'')
4667 alt_token = Column('alt_token', String(1024), default=u'')
4668 token_secret = Column('token_secret', String(1024), default=u'')
4669
4670 @classmethod
4671 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4672 """
4673 Returns ExternalIdentity instance based on search params
4674
4675 :param external_id:
4676 :param provider_name:
4677 :return: ExternalIdentity
4678 """
4679 query = cls.query()
4680 query = query.filter(cls.external_id == external_id)
4681 query = query.filter(cls.provider_name == provider_name)
4682 if local_user_id:
4683 query = query.filter(cls.local_user_id == local_user_id)
4684 return query.first()
4685
4686 @classmethod
4687 def user_by_external_id_and_provider(cls, external_id, provider_name):
4688 """
4689 Returns User instance based on search params
4690
4691 :param external_id:
4692 :param provider_name:
4693 :return: User
4694 """
4695 query = User.query()
4696 query = query.filter(cls.external_id == external_id)
4697 query = query.filter(cls.provider_name == provider_name)
4698 query = query.filter(User.user_id == cls.local_user_id)
4699 return query.first()
4700
4701 @classmethod
4702 def by_local_user_id(cls, local_user_id):
4703 """
4704 Returns all tokens for user
4705
4706 :param local_user_id:
4707 :return: ExternalIdentity
4708 """
4709 query = cls.query()
4710 query = query.filter(cls.local_user_id == local_user_id)
4711 return query
4712
4713 @classmethod
4714 def load_provider_plugin(cls, plugin_id):
4715 from rhodecode.authentication.base import loadplugin
4716 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4717 auth_plugin = loadplugin(_plugin_id)
4718 return auth_plugin
4719
4720
4721 class Integration(Base, BaseModel):
4722 __tablename__ = 'integrations'
4723 __table_args__ = (
4724 base_table_args
4725 )
4726
4727 integration_id = Column('integration_id', Integer(), primary_key=True)
4728 integration_type = Column('integration_type', String(255))
4729 enabled = Column('enabled', Boolean(), nullable=False)
4730 name = Column('name', String(255), nullable=False)
4731 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4732 default=False)
4733
4734 settings = Column(
4735 'settings_json', MutationObj.as_mutable(
4736 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4737 repo_id = Column(
4738 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4739 nullable=True, unique=None, default=None)
4740 repo = relationship('Repository', lazy='joined')
4741
4742 repo_group_id = Column(
4743 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4744 nullable=True, unique=None, default=None)
4745 repo_group = relationship('RepoGroup', lazy='joined')
4746
4747 @property
4748 def scope(self):
4749 if self.repo:
4750 return repr(self.repo)
4751 if self.repo_group:
4752 if self.child_repos_only:
4753 return repr(self.repo_group) + ' (child repos only)'
4754 else:
4755 return repr(self.repo_group) + ' (recursive)'
4756 if self.child_repos_only:
4757 return 'root_repos'
4758 return 'global'
4759
4760 def __repr__(self):
4761 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4762
4763
4764 class RepoReviewRuleUser(Base, BaseModel):
4765 __tablename__ = 'repo_review_rules_users'
4766 __table_args__ = (
4767 base_table_args
4768 )
4769
4770 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4771 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4772 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4773 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4774 user = relationship('User')
4775
4776 def rule_data(self):
4777 return {
4778 'mandatory': self.mandatory
4779 }
4780
4781
4782 class RepoReviewRuleUserGroup(Base, BaseModel):
4783 __tablename__ = 'repo_review_rules_users_groups'
4784 __table_args__ = (
4785 base_table_args
4786 )
4787
4788 VOTE_RULE_ALL = -1
4789
4790 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4791 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4792 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4793 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4794 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4795 users_group = relationship('UserGroup')
4796
4797 def rule_data(self):
4798 return {
4799 'mandatory': self.mandatory,
4800 'vote_rule': self.vote_rule
4801 }
4802
4803 @property
4804 def vote_rule_label(self):
4805 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4806 return 'all must vote'
4807 else:
4808 return 'min. vote {}'.format(self.vote_rule)
4809
4810
4811 class RepoReviewRule(Base, BaseModel):
4812 __tablename__ = 'repo_review_rules'
4813 __table_args__ = (
4814 base_table_args
4815 )
4816
4817 repo_review_rule_id = Column(
4818 'repo_review_rule_id', Integer(), primary_key=True)
4819 repo_id = Column(
4820 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4821 repo = relationship('Repository', backref='review_rules')
4822
4823 review_rule_name = Column('review_rule_name', String(255))
4824 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4825 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4826 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4827
4828 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4829 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4830 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4831 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4832
4833 rule_users = relationship('RepoReviewRuleUser')
4834 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4835
4836 def _validate_pattern(self, value):
4837 re.compile('^' + glob2re(value) + '$')
4838
4839 @hybrid_property
4840 def source_branch_pattern(self):
4841 return self._branch_pattern or '*'
4842
4843 @source_branch_pattern.setter
4844 def source_branch_pattern(self, value):
4845 self._validate_pattern(value)
4846 self._branch_pattern = value or '*'
4847
4848 @hybrid_property
4849 def target_branch_pattern(self):
4850 return self._target_branch_pattern or '*'
4851
4852 @target_branch_pattern.setter
4853 def target_branch_pattern(self, value):
4854 self._validate_pattern(value)
4855 self._target_branch_pattern = value or '*'
4856
4857 @hybrid_property
4858 def file_pattern(self):
4859 return self._file_pattern or '*'
4860
4861 @file_pattern.setter
4862 def file_pattern(self, value):
4863 self._validate_pattern(value)
4864 self._file_pattern = value or '*'
4865
4866 def matches(self, source_branch, target_branch, files_changed):
4867 """
4868 Check if this review rule matches a branch/files in a pull request
4869
4870 :param source_branch: source branch name for the commit
4871 :param target_branch: target branch name for the commit
4872 :param files_changed: list of file paths changed in the pull request
4873 """
4874
4875 source_branch = source_branch or ''
4876 target_branch = target_branch or ''
4877 files_changed = files_changed or []
4878
4879 branch_matches = True
4880 if source_branch or target_branch:
4881 if self.source_branch_pattern == '*':
4882 source_branch_match = True
4883 else:
4884 if self.source_branch_pattern.startswith('re:'):
4885 source_pattern = self.source_branch_pattern[3:]
4886 else:
4887 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
4888 source_branch_regex = re.compile(source_pattern)
4889 source_branch_match = bool(source_branch_regex.search(source_branch))
4890 if self.target_branch_pattern == '*':
4891 target_branch_match = True
4892 else:
4893 if self.target_branch_pattern.startswith('re:'):
4894 target_pattern = self.target_branch_pattern[3:]
4895 else:
4896 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
4897 target_branch_regex = re.compile(target_pattern)
4898 target_branch_match = bool(target_branch_regex.search(target_branch))
4899
4900 branch_matches = source_branch_match and target_branch_match
4901
4902 files_matches = True
4903 if self.file_pattern != '*':
4904 files_matches = False
4905 if self.file_pattern.startswith('re:'):
4906 file_pattern = self.file_pattern[3:]
4907 else:
4908 file_pattern = glob2re(self.file_pattern)
4909 file_regex = re.compile(file_pattern)
4910 for filename in files_changed:
4911 if file_regex.search(filename):
4912 files_matches = True
4913 break
4914
4915 return branch_matches and files_matches
4916
4917 @property
4918 def review_users(self):
4919 """ Returns the users which this rule applies to """
4920
4921 users = collections.OrderedDict()
4922
4923 for rule_user in self.rule_users:
4924 if rule_user.user.active:
4925 if rule_user.user not in users:
4926 users[rule_user.user.username] = {
4927 'user': rule_user.user,
4928 'source': 'user',
4929 'source_data': {},
4930 'data': rule_user.rule_data()
4931 }
4932
4933 for rule_user_group in self.rule_user_groups:
4934 source_data = {
4935 'user_group_id': rule_user_group.users_group.users_group_id,
4936 'name': rule_user_group.users_group.users_group_name,
4937 'members': len(rule_user_group.users_group.members)
4938 }
4939 for member in rule_user_group.users_group.members:
4940 if member.user.active:
4941 key = member.user.username
4942 if key in users:
4943 # skip this member as we have him already
4944 # this prevents from override the "first" matched
4945 # users with duplicates in multiple groups
4946 continue
4947
4948 users[key] = {
4949 'user': member.user,
4950 'source': 'user_group',
4951 'source_data': source_data,
4952 'data': rule_user_group.rule_data()
4953 }
4954
4955 return users
4956
4957 def user_group_vote_rule(self, user_id):
4958
4959 rules = []
4960 if not self.rule_user_groups:
4961 return rules
4962
4963 for user_group in self.rule_user_groups:
4964 user_group_members = [x.user_id for x in user_group.users_group.members]
4965 if user_id in user_group_members:
4966 rules.append(user_group)
4967 return rules
4968
4969 def __repr__(self):
4970 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4971 self.repo_review_rule_id, self.repo)
4972
4973
4974 class ScheduleEntry(Base, BaseModel):
4975 __tablename__ = 'schedule_entries'
4976 __table_args__ = (
4977 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
4978 UniqueConstraint('task_uid', name='s_task_uid_idx'),
4979 base_table_args,
4980 )
4981
4982 schedule_types = ['crontab', 'timedelta', 'integer']
4983 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
4984
4985 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
4986 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
4987 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
4988
4989 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
4990 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
4991
4992 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
4993 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
4994
4995 # task
4996 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
4997 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
4998 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
4999 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
5000
5001 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5002 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
5003
5004 @hybrid_property
5005 def schedule_type(self):
5006 return self._schedule_type
5007
5008 @schedule_type.setter
5009 def schedule_type(self, val):
5010 if val not in self.schedule_types:
5011 raise ValueError('Value must be on of `{}` and got `{}`'.format(
5012 val, self.schedule_type))
5013
5014 self._schedule_type = val
5015
5016 @classmethod
5017 def get_uid(cls, obj):
5018 args = obj.task_args
5019 kwargs = obj.task_kwargs
5020 if isinstance(args, JsonRaw):
5021 try:
5022 args = json.loads(args)
5023 except ValueError:
5024 args = tuple()
5025
5026 if isinstance(kwargs, JsonRaw):
5027 try:
5028 kwargs = json.loads(kwargs)
5029 except ValueError:
5030 kwargs = dict()
5031
5032 dot_notation = obj.task_dot_notation
5033 val = '.'.join(map(safe_str, [
5034 sorted(dot_notation), args, sorted(kwargs.items())]))
5035 return hashlib.sha1(val).hexdigest()
5036
5037 @classmethod
5038 def get_by_schedule_name(cls, schedule_name):
5039 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5040
5041 @classmethod
5042 def get_by_schedule_id(cls, schedule_id):
5043 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5044
5045 @property
5046 def task(self):
5047 return self.task_dot_notation
5048
5049 @property
5050 def schedule(self):
5051 from rhodecode.lib.celerylib.utils import raw_2_schedule
5052 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5053 return schedule
5054
5055 @property
5056 def args(self):
5057 try:
5058 return list(self.task_args or [])
5059 except ValueError:
5060 return list()
5061
5062 @property
5063 def kwargs(self):
5064 try:
5065 return dict(self.task_kwargs or {})
5066 except ValueError:
5067 return dict()
5068
5069 def _as_raw(self, val):
5070 if hasattr(val, 'de_coerce'):
5071 val = val.de_coerce()
5072 if val:
5073 val = json.dumps(val)
5074
5075 return val
5076
5077 @property
5078 def schedule_definition_raw(self):
5079 return self._as_raw(self.schedule_definition)
5080
5081 @property
5082 def args_raw(self):
5083 return self._as_raw(self.task_args)
5084
5085 @property
5086 def kwargs_raw(self):
5087 return self._as_raw(self.task_kwargs)
5088
5089 def __repr__(self):
5090 return '<DB:ScheduleEntry({}:{})>'.format(
5091 self.schedule_entry_id, self.schedule_name)
5092
5093
5094 @event.listens_for(ScheduleEntry, 'before_update')
5095 def update_task_uid(mapper, connection, target):
5096 target.task_uid = ScheduleEntry.get_uid(target)
5097
5098
5099 @event.listens_for(ScheduleEntry, 'before_insert')
5100 def set_task_uid(mapper, connection, target):
5101 target.task_uid = ScheduleEntry.get_uid(target)
5102
5103
5104 class _BaseBranchPerms(BaseModel):
5105 @classmethod
5106 def compute_hash(cls, value):
5107 return sha1_safe(value)
5108
5109 @hybrid_property
5110 def branch_pattern(self):
5111 return self._branch_pattern or '*'
5112
5113 @hybrid_property
5114 def branch_hash(self):
5115 return self._branch_hash
5116
5117 def _validate_glob(self, value):
5118 re.compile('^' + glob2re(value) + '$')
5119
5120 @branch_pattern.setter
5121 def branch_pattern(self, value):
5122 self._validate_glob(value)
5123 self._branch_pattern = value or '*'
5124 # set the Hash when setting the branch pattern
5125 self._branch_hash = self.compute_hash(self._branch_pattern)
5126
5127 def matches(self, branch):
5128 """
5129 Check if this the branch matches entry
5130
5131 :param branch: branch name for the commit
5132 """
5133
5134 branch = branch or ''
5135
5136 branch_matches = True
5137 if branch:
5138 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5139 branch_matches = bool(branch_regex.search(branch))
5140
5141 return branch_matches
5142
5143
5144 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5145 __tablename__ = 'user_to_repo_branch_permissions'
5146 __table_args__ = (
5147 base_table_args
5148 )
5149
5150 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5151
5152 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5153 repo = relationship('Repository', backref='user_branch_perms')
5154
5155 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5156 permission = relationship('Permission')
5157
5158 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5159 user_repo_to_perm = relationship('UserRepoToPerm')
5160
5161 rule_order = Column('rule_order', Integer(), nullable=False)
5162 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5163 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5164
5165 def __unicode__(self):
5166 return u'<UserBranchPermission(%s => %r)>' % (
5167 self.user_repo_to_perm, self.branch_pattern)
5168
5169
5170 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5171 __tablename__ = 'user_group_to_repo_branch_permissions'
5172 __table_args__ = (
5173 base_table_args
5174 )
5175
5176 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5177
5178 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5179 repo = relationship('Repository', backref='user_group_branch_perms')
5180
5181 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5182 permission = relationship('Permission')
5183
5184 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('users_group_repo_to_perm.users_group_to_perm_id'), nullable=False, unique=None, default=None)
5185 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5186
5187 rule_order = Column('rule_order', Integer(), nullable=False)
5188 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5189 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5190
5191 def __unicode__(self):
5192 return u'<UserBranchPermission(%s => %r)>' % (
5193 self.user_group_repo_to_perm, self.branch_pattern)
5194
5195
5196 class UserBookmark(Base, BaseModel):
5197 __tablename__ = 'user_bookmarks'
5198 __table_args__ = (
5199 UniqueConstraint('user_id', 'bookmark_repo_id'),
5200 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5201 UniqueConstraint('user_id', 'bookmark_position'),
5202 base_table_args
5203 )
5204
5205 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5206 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5207 position = Column("bookmark_position", Integer(), nullable=False)
5208 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5209 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5210 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5211
5212 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5213 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5214
5215 user = relationship("User")
5216
5217 repository = relationship("Repository")
5218 repository_group = relationship("RepoGroup")
5219
5220 @classmethod
5221 def get_by_position_for_user(cls, position, user_id):
5222 return cls.query() \
5223 .filter(UserBookmark.user_id == user_id) \
5224 .filter(UserBookmark.position == position).scalar()
5225
5226 @classmethod
5227 def get_bookmarks_for_user(cls, user_id, cache=True):
5228 bookmarks = cls.query() \
5229 .filter(UserBookmark.user_id == user_id) \
5230 .options(joinedload(UserBookmark.repository)) \
5231 .options(joinedload(UserBookmark.repository_group)) \
5232 .order_by(UserBookmark.position.asc())
5233
5234 if cache:
5235 bookmarks = bookmarks.options(
5236 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5237 )
5238
5239 return bookmarks.all()
5240
5241 def __unicode__(self):
5242 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5243
5244
5245 class FileStore(Base, BaseModel):
5246 __tablename__ = 'file_store'
5247 __table_args__ = (
5248 base_table_args
5249 )
5250
5251 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5252 file_uid = Column('file_uid', String(1024), nullable=False)
5253 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5254 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5255 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5256
5257 # sha256 hash
5258 file_hash = Column('file_hash', String(512), nullable=False)
5259 file_size = Column('file_size', BigInteger(), nullable=False)
5260
5261 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5262 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5263 accessed_count = Column('accessed_count', Integer(), default=0)
5264
5265 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5266
5267 # if repo/repo_group reference is set, check for permissions
5268 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5269
5270 # hidden defines an attachment that should be hidden from showing in artifact listing
5271 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5272
5273 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5274 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5275
5276 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5277
5278 # scope limited to user, which requester have access to
5279 scope_user_id = Column(
5280 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5281 nullable=True, unique=None, default=None)
5282 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5283
5284 # scope limited to user group, which requester have access to
5285 scope_user_group_id = Column(
5286 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5287 nullable=True, unique=None, default=None)
5288 user_group = relationship('UserGroup', lazy='joined')
5289
5290 # scope limited to repo, which requester have access to
5291 scope_repo_id = Column(
5292 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5293 nullable=True, unique=None, default=None)
5294 repo = relationship('Repository', lazy='joined')
5295
5296 # scope limited to repo group, which requester have access to
5297 scope_repo_group_id = Column(
5298 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5299 nullable=True, unique=None, default=None)
5300 repo_group = relationship('RepoGroup', lazy='joined')
5301
5302 @classmethod
5303 def get_by_store_uid(cls, file_store_uid):
5304 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5305
5306 @classmethod
5307 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5308 file_description='', enabled=True, hidden=False, check_acl=True,
5309 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5310
5311 store_entry = FileStore()
5312 store_entry.file_uid = file_uid
5313 store_entry.file_display_name = file_display_name
5314 store_entry.file_org_name = filename
5315 store_entry.file_size = file_size
5316 store_entry.file_hash = file_hash
5317 store_entry.file_description = file_description
5318
5319 store_entry.check_acl = check_acl
5320 store_entry.enabled = enabled
5321 store_entry.hidden = hidden
5322
5323 store_entry.user_id = user_id
5324 store_entry.scope_user_id = scope_user_id
5325 store_entry.scope_repo_id = scope_repo_id
5326 store_entry.scope_repo_group_id = scope_repo_group_id
5327
5328 return store_entry
5329
5330 @classmethod
5331 def store_metadata(cls, file_store_id, args, commit=True):
5332 file_store = FileStore.get(file_store_id)
5333 if file_store is None:
5334 return
5335
5336 for section, key, value, value_type in args:
5337 has_key = FileStoreMetadata().query() \
5338 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5339 .filter(FileStoreMetadata.file_store_meta_section == section) \
5340 .filter(FileStoreMetadata.file_store_meta_key == key) \
5341 .scalar()
5342 if has_key:
5343 msg = 'key `{}` already defined under section `{}` for this file.'\
5344 .format(key, section)
5345 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5346
5347 # NOTE(marcink): raises ArtifactMetadataBadValueType
5348 FileStoreMetadata.valid_value_type(value_type)
5349
5350 meta_entry = FileStoreMetadata()
5351 meta_entry.file_store = file_store
5352 meta_entry.file_store_meta_section = section
5353 meta_entry.file_store_meta_key = key
5354 meta_entry.file_store_meta_value_type = value_type
5355 meta_entry.file_store_meta_value = value
5356
5357 Session().add(meta_entry)
5358
5359 try:
5360 if commit:
5361 Session().commit()
5362 except IntegrityError:
5363 Session().rollback()
5364 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5365
5366 @classmethod
5367 def bump_access_counter(cls, file_uid, commit=True):
5368 FileStore().query()\
5369 .filter(FileStore.file_uid == file_uid)\
5370 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5371 FileStore.accessed_on: datetime.datetime.now()})
5372 if commit:
5373 Session().commit()
5374
5375 def __json__(self):
5376 data = {
5377 'filename': self.file_display_name,
5378 'filename_org': self.file_org_name,
5379 'file_uid': self.file_uid,
5380 'description': self.file_description,
5381 'hidden': self.hidden,
5382 'size': self.file_size,
5383 'created_on': self.created_on,
5384 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5385 'downloaded_times': self.accessed_count,
5386 'sha256': self.file_hash,
5387 'metadata': self.file_metadata,
5388 }
5389
5390 return data
5391
5392 def __repr__(self):
5393 return '<FileStore({})>'.format(self.file_store_id)
5394
5395
5396 class FileStoreMetadata(Base, BaseModel):
5397 __tablename__ = 'file_store_metadata'
5398 __table_args__ = (
5399 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5400 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5401 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5402 base_table_args
5403 )
5404 SETTINGS_TYPES = {
5405 'str': safe_str,
5406 'int': safe_int,
5407 'unicode': safe_unicode,
5408 'bool': str2bool,
5409 'list': functools.partial(aslist, sep=',')
5410 }
5411
5412 file_store_meta_id = Column(
5413 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5414 primary_key=True)
5415 _file_store_meta_section = Column(
5416 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5417 nullable=True, unique=None, default=None)
5418 _file_store_meta_section_hash = Column(
5419 "file_store_meta_section_hash", String(255),
5420 nullable=True, unique=None, default=None)
5421 _file_store_meta_key = Column(
5422 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5423 nullable=True, unique=None, default=None)
5424 _file_store_meta_key_hash = Column(
5425 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5426 _file_store_meta_value = Column(
5427 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5428 nullable=True, unique=None, default=None)
5429 _file_store_meta_value_type = Column(
5430 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5431 default='unicode')
5432
5433 file_store_id = Column(
5434 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5435 nullable=True, unique=None, default=None)
5436
5437 file_store = relationship('FileStore', lazy='joined')
5438
5439 @classmethod
5440 def valid_value_type(cls, value):
5441 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5442 raise ArtifactMetadataBadValueType(
5443 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5444
5445 @hybrid_property
5446 def file_store_meta_section(self):
5447 return self._file_store_meta_section
5448
5449 @file_store_meta_section.setter
5450 def file_store_meta_section(self, value):
5451 self._file_store_meta_section = value
5452 self._file_store_meta_section_hash = _hash_key(value)
5453
5454 @hybrid_property
5455 def file_store_meta_key(self):
5456 return self._file_store_meta_key
5457
5458 @file_store_meta_key.setter
5459 def file_store_meta_key(self, value):
5460 self._file_store_meta_key = value
5461 self._file_store_meta_key_hash = _hash_key(value)
5462
5463 @hybrid_property
5464 def file_store_meta_value(self):
5465 val = self._file_store_meta_value
5466
5467 if self._file_store_meta_value_type:
5468 # e.g unicode.encrypted == unicode
5469 _type = self._file_store_meta_value_type.split('.')[0]
5470 # decode the encrypted value if it's encrypted field type
5471 if '.encrypted' in self._file_store_meta_value_type:
5472 cipher = EncryptedTextValue()
5473 val = safe_unicode(cipher.process_result_value(val, None))
5474 # do final type conversion
5475 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5476 val = converter(val)
5477
5478 return val
5479
5480 @file_store_meta_value.setter
5481 def file_store_meta_value(self, val):
5482 val = safe_unicode(val)
5483 # encode the encrypted value
5484 if '.encrypted' in self.file_store_meta_value_type:
5485 cipher = EncryptedTextValue()
5486 val = safe_unicode(cipher.process_bind_param(val, None))
5487 self._file_store_meta_value = val
5488
5489 @hybrid_property
5490 def file_store_meta_value_type(self):
5491 return self._file_store_meta_value_type
5492
5493 @file_store_meta_value_type.setter
5494 def file_store_meta_value_type(self, val):
5495 # e.g unicode.encrypted
5496 self.valid_value_type(val)
5497 self._file_store_meta_value_type = val
5498
5499 def __json__(self):
5500 data = {
5501 'artifact': self.file_store.file_uid,
5502 'section': self.file_store_meta_section,
5503 'key': self.file_store_meta_key,
5504 'value': self.file_store_meta_value,
5505 }
5506
5507 return data
5508
5509 def __repr__(self):
5510 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5511 self.file_store_meta_key, self.file_store_meta_value)
5512
5513
5514 class DbMigrateVersion(Base, BaseModel):
5515 __tablename__ = 'db_migrate_version'
5516 __table_args__ = (
5517 base_table_args,
5518 )
5519
5520 repository_id = Column('repository_id', String(250), primary_key=True)
5521 repository_path = Column('repository_path', Text)
5522 version = Column('version', Integer)
5523
5524 @classmethod
5525 def set_version(cls, version):
5526 """
5527 Helper for forcing a different version, usually for debugging purposes via ishell.
5528 """
5529 ver = DbMigrateVersion.query().first()
5530 ver.version = version
5531 Session().commit()
5532
5533
5534 class DbSession(Base, BaseModel):
5535 __tablename__ = 'db_session'
5536 __table_args__ = (
5537 base_table_args,
5538 )
5539
5540 def __repr__(self):
5541 return '<DB:DbSession({})>'.format(self.id)
5542
5543 id = Column('id', Integer())
5544 namespace = Column('namespace', String(255), primary_key=True)
5545 accessed = Column('accessed', DateTime, nullable=False)
5546 created = Column('created', DateTime, nullable=False)
5547 data = Column('data', PickleType, nullable=False)
@@ -0,0 +1,35 b''
1 # -*- coding: utf-8 -*-
2
3 import logging
4 from sqlalchemy import *
5
6 from alembic.migration import MigrationContext
7 from alembic.operations import Operations
8 from sqlalchemy import BigInteger
9
10 from rhodecode.lib.dbmigrate.versions import _reset_base
11 from rhodecode.model import init_model_encryption
12
13
14 log = logging.getLogger(__name__)
15
16
17 def upgrade(migrate_engine):
18 """
19 Upgrade operations go here.
20 Don't create your own engine; bind migrate_engine to your metadata
21 """
22 _reset_base(migrate_engine)
23 from rhodecode.lib.dbmigrate.schema import db_4_19_0_0 as db
24
25 init_model_encryption(db)
26 db.UserNotice().__table__.create()
27
28
29 def downgrade(migrate_engine):
30 meta = MetaData()
31 meta.bind = migrate_engine
32
33
34 def fixups(models, _SESSION):
35 pass
@@ -45,7 +45,7 b' PYRAMID_SETTINGS = {}'
45 45 EXTENSIONS = {}
46 46
47 47 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
48 __dbversion__ = 104 # defines current db version for migrations
48 __dbversion__ = 105 # defines current db version for migrations
49 49 __platform__ = platform.system()
50 50 __license__ = 'AGPLv3, and Commercial License'
51 51 __author__ = 'RhodeCode GmbH'
@@ -26,7 +26,6 b' def admin_routes(config):'
26 26 """
27 27 Admin prefixed routes
28 28 """
29
30 29 config.add_route(
31 30 name='admin_audit_logs',
32 31 pattern='/audit_logs')
@@ -291,6 +290,12 b' def admin_routes(config):'
291 290 pattern='/users/{user_id:\d+}/create_repo_group',
292 291 user_route=True)
293 292
293 # user notice
294 config.add_route(
295 name='user_notice_dismiss',
296 pattern='/users/{user_id:\d+}/notice_dismiss',
297 user_route=True)
298
294 299 # user auth tokens
295 300 config.add_route(
296 301 name='edit_user_auth_tokens',
@@ -34,7 +34,7 b' from rhodecode.apps.ssh_support import S'
34 34 from rhodecode.authentication.base import get_authn_registry, RhodeCodeExternalAuthPlugin
35 35 from rhodecode.authentication.plugins import auth_rhodecode
36 36 from rhodecode.events import trigger
37 from rhodecode.model.db import true
37 from rhodecode.model.db import true, UserNotice
38 38
39 39 from rhodecode.lib import audit_logger, rc_cache
40 40 from rhodecode.lib.exceptions import (
@@ -705,6 +705,32 b' class UsersView(UserAppView):'
705 705 @HasPermissionAllDecorator('hg.admin')
706 706 @CSRFRequired()
707 707 @view_config(
708 route_name='user_notice_dismiss', request_method='POST',
709 renderer='json_ext', xhr=True)
710 def user_notice_dismiss(self):
711 _ = self.request.translate
712 c = self.load_default_context()
713
714 user_id = self.db_user_id
715 c.user = self.db_user
716 user_notice_id = safe_int(self.request.POST.get('notice_id'))
717 notice = UserNotice().query()\
718 .filter(UserNotice.user_id == user_id)\
719 .filter(UserNotice.user_notice_id == user_notice_id)\
720 .scalar()
721 read = False
722 if notice:
723 notice.notice_read = True
724 Session().add(notice)
725 Session().commit()
726 read = True
727
728 return {'notice': user_notice_id, 'read': read}
729
730 @LoginRequired()
731 @HasPermissionAllDecorator('hg.admin')
732 @CSRFRequired()
733 @view_config(
708 734 route_name='user_create_personal_repo_group', request_method='POST',
709 735 renderer='rhodecode:templates/admin/users/user_edit.mako')
710 736 def user_create_personal_repo_group(self):
@@ -23,6 +23,8 b' authentication and permission libraries'
23 23 """
24 24
25 25 import os
26
27 import colander
26 28 import time
27 29 import collections
28 30 import fnmatch
@@ -45,15 +47,14 b' from rhodecode.model import meta'
45 47 from rhodecode.model.meta import Session
46 48 from rhodecode.model.user import UserModel
47 49 from rhodecode.model.db import (
48 User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
49 UserIpMap, UserApiKeys, RepoGroup, UserGroup)
50 false, User, Repository, Permission, UserToPerm, UserGroupToPerm, UserGroupMember,
51 UserIpMap, UserApiKeys, RepoGroup, UserGroup, UserNotice)
50 52 from rhodecode.lib import rc_cache
51 53 from rhodecode.lib.utils2 import safe_unicode, aslist, safe_str, md5, safe_int, sha1
52 54 from rhodecode.lib.utils import (
53 55 get_repo_slug, get_repo_group_slug, get_user_group_slug)
54 56 from rhodecode.lib.caching_query import FromCache
55 57
56
57 58 if rhodecode.is_unix:
58 59 import bcrypt
59 60
@@ -1455,6 +1456,38 b' class AuthUser(object):'
1455 1456
1456 1457 return rule, default_perm
1457 1458
1459 def get_notice_messages(self):
1460
1461 notice_level = 'notice-error'
1462 notice_messages = []
1463 if self.is_default:
1464 return [], notice_level
1465
1466 notices = UserNotice.query()\
1467 .filter(UserNotice.user_id == self.user_id)\
1468 .filter(UserNotice.notice_read == false())\
1469 .all()
1470
1471 try:
1472 for entry in notices:
1473
1474 msg = {
1475 'msg_id': entry.user_notice_id,
1476 'level': entry.notification_level,
1477 'subject': entry.notice_subject,
1478 'body': entry.notice_body,
1479 }
1480 notice_messages.append(msg)
1481
1482 log.debug('Got user %s %s messages', self, len(notice_messages))
1483
1484 levels = [x['level'] for x in notice_messages]
1485 notice_level = 'notice-error' if 'error' in levels else 'notice-warning'
1486 except Exception:
1487 pass
1488
1489 return notice_messages, notice_level
1490
1458 1491 def __repr__(self):
1459 1492 return "<AuthUser('id:%s[%s] ip:%s auth:%s')>"\
1460 1493 % (self.user_id, self.username, self.ip_addr, self.is_authenticated)
@@ -4517,6 +4517,65 b' class UserNotification(Base, BaseModel):'
4517 4517 Session().add(self)
4518 4518
4519 4519
4520 class UserNotice(Base, BaseModel):
4521 __tablename__ = 'user_notices'
4522 __table_args__ = (
4523 base_table_args
4524 )
4525
4526 NOTIFICATION_TYPE_MESSAGE = 'message'
4527 NOTIFICATION_TYPE_NOTICE = 'notice'
4528
4529 NOTIFICATION_LEVEL_INFO = 'info'
4530 NOTIFICATION_LEVEL_WARNING = 'warning'
4531 NOTIFICATION_LEVEL_ERROR = 'error'
4532
4533 user_notice_id = Column('gist_id', Integer(), primary_key=True)
4534
4535 notice_subject = Column('notice_subject', Unicode(512), nullable=True)
4536 notice_body = Column('notice_body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4537
4538 notice_read = Column('notice_read', Boolean, default=False)
4539
4540 notification_level = Column('notification_level', String(1024), default=NOTIFICATION_LEVEL_INFO)
4541 notification_type = Column('notification_type', String(1024), default=NOTIFICATION_TYPE_NOTICE)
4542
4543 notice_created_by = Column('notice_created_by', Integer(), ForeignKey('users.user_id'), nullable=True)
4544 notice_created_on = Column('notice_created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4545
4546 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'))
4547 user = relationship('User', lazy="joined", primaryjoin='User.user_id==UserNotice.user_id')
4548
4549 @classmethod
4550 def create_for_user(cls, user, subject, body, notice_level=NOTIFICATION_LEVEL_INFO, allow_duplicate=False):
4551
4552 if notice_level not in [cls.NOTIFICATION_LEVEL_ERROR,
4553 cls.NOTIFICATION_LEVEL_WARNING,
4554 cls.NOTIFICATION_LEVEL_INFO]:
4555 return
4556
4557 from rhodecode.model.user import UserModel
4558 user = UserModel().get_user(user)
4559
4560 new_notice = UserNotice()
4561 if not allow_duplicate:
4562 existing_msg = UserNotice().query() \
4563 .filter(UserNotice.user == user) \
4564 .filter(UserNotice.notice_body == body) \
4565 .filter(UserNotice.notice_read == false()) \
4566 .scalar()
4567 if existing_msg:
4568 log.warning('Ignoring duplicate notice for user %s', user)
4569 return
4570
4571 new_notice.user = user
4572 new_notice.notice_subject = subject
4573 new_notice.notice_body = body
4574 new_notice.notification_level = notice_level
4575 Session().add(new_notice)
4576 Session().commit()
4577
4578
4520 4579 class Gist(Base, BaseModel):
4521 4580 __tablename__ = 'gists'
4522 4581 __table_args__ = (
@@ -2101,6 +2101,12 b' BIN_FILENODE = 7'
2101 2101 }
2102 2102 }
2103 2103
2104 .notice-messages {
2105 .markdown-block,
2106 .rst-block {
2107 padding: 0;
2108 }
2109 }
2104 2110
2105 2111 .notifications_buttons{
2106 2112 float: right;
@@ -820,7 +820,53 b' input {'
820 820 }
821 821
822 822 .menulabel-notice {
823
824 padding:7px 10px;
825
826 &.notice-warning {
827 border: 1px solid @color3;
828 .notice-color-warning
829 }
830 &.notice-error {
823 831 border: 1px solid @color5;
824 padding:7px 10px;
832 .notice-color-error
833 }
834 &.notice-info {
835 border: 1px solid @color1;
836 .notice-color-info
837 }
838 }
839
840 .notice-messages-container {
841 position: absolute;
842 top: 45px;
843 }
844
845 .notice-messages {
846 display: block;
847 position: relative;
848 z-index: 300;
849 min-width: 500px;
850 max-width: 500px;
851 min-height: 100px;
852 margin-top: 4px;
853 margin-bottom: 24px;
854 font-size: 14px;
855 font-weight: 400;
856 padding: 8px 0;
857 background-color: #fff;
858 border: 1px solid @grey4;
859 box-shadow: 0 1px 2px 0 rgba(0, 0, 0, 0.07);
860 }
861
862 .notice-color-warning {
863 color: @color3;
864 }
865
866 .notice-color-error {
825 867 color: @color5;
826 868 }
869
870 .notice-color-info {
871 color: @color1;
872 }
@@ -269,6 +269,7 b''
269 269 .icon-expand-linked { cursor: pointer; color: @grey3; font-size: 14px }
270 270 .icon-more-linked { cursor: pointer; color: @grey3 }
271 271 .icon-flag-filled-red { color: @color5 !important; }
272 .icon-filled-red { color: @color5 !important; }
272 273
273 274 .repo-switcher-dropdown .select2-result-label {
274 275 .icon-git:before {
@@ -110,6 +110,7 b' function registerRCRoutes() {'
110 110 pyroutes.register('user_enable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_enable', ['user_id']);
111 111 pyroutes.register('user_disable_force_password_reset', '/_admin/users/%(user_id)s/password_reset_disable', ['user_id']);
112 112 pyroutes.register('user_create_personal_repo_group', '/_admin/users/%(user_id)s/create_repo_group', ['user_id']);
113 pyroutes.register('user_notice_dismiss', '/_admin/users/%(user_id)s/notice_dismiss', ['user_id']);
113 114 pyroutes.register('edit_user_auth_tokens_delete', '/_admin/users/%(user_id)s/edit/auth_tokens/delete', ['user_id']);
114 115 pyroutes.register('edit_user_ssh_keys', '/_admin/users/%(user_id)s/edit/ssh_keys', ['user_id']);
115 116 pyroutes.register('edit_user_ssh_keys_generate_keypair', '/_admin/users/%(user_id)s/edit/ssh_keys/generate', ['user_id']);
@@ -688,17 +688,50 b''
688 688 </%def>
689 689
690 690 <%def name="menu_items(active=None)">
691 <%
692 notice_messages, notice_level = c.rhodecode_user.get_notice_messages()
693 notice_display = 'none' if len(notice_messages) == 0 else ''
694 %>
695 <style>
696
697 </style>
691 698
692 699 <ul id="quick" class="main_nav navigation horizontal-list">
693 700 ## notice box for important system messages
694 <li style="display: none">
695 <a class="notice-box" href="#openNotice" onclick="return false">
696 <div class="menulabel-notice" >
697 0
701 <li style="display: ${notice_display}">
702 <a class="notice-box" href="#openNotice" onclick="$('.notice-messages-container').toggle(); return false">
703 <div class="menulabel-notice ${notice_level}" >
704 ${len(notice_messages)}
698 705 </div>
699 706 </a>
700 707 </li>
708 <div class="notice-messages-container" style="display: none">
709 <div class="notice-messages">
710 <table class="rctable">
711 % for notice in notice_messages:
712 <tr id="notice-message-${notice['msg_id']}" class="notice-message-${notice['level']}">
713 <td style="vertical-align: text-top; width: 20px">
714 <i class="tooltip icon-info notice-color-${notice['level']}" title="${notice['level']}"></i>
715 </td>
716 <td>
717 <span><i class="icon-plus-squared cursor-pointer" onclick="$('#notice-${notice['msg_id']}').toggle()"></i> </span>
718 ${notice['subject']}
701 719
720 <div id="notice-${notice['msg_id']}" style="display: none">
721 ${h.render(notice['body'], renderer='markdown')}
722 </div>
723 </td>
724 <td style="vertical-align: text-top; width: 35px;">
725 <a class="tooltip" title="${_('dismiss')}" href="#dismiss" onclick="dismissNotice(${notice['msg_id']});return false">
726 <i class="icon-remove icon-filled-red"></i>
727 </a>
728 </td>
729 </tr>
730
731 % endfor
732 </table>
733 </div>
734 </div>
702 735 ## Main filter
703 736 <li>
704 737 <div class="menulabel main_filter_box">
@@ -1058,6 +1091,26 b''
1058 1091 }
1059 1092 });
1060 1093
1094 var dismissNotice = function(noticeId) {
1095
1096 var url = pyroutes.url('user_notice_dismiss',
1097 {"user_id": templateContext.rhodecode_user.user_id});
1098
1099 var postData = {
1100 'csrf_token': CSRF_TOKEN,
1101 'notice_id': noticeId,
1102 };
1103
1104 var success = function(response) {
1105 $('#notice-message-' + noticeId).remove();
1106 return false;
1107 };
1108 var failure = function(data, textStatus, xhr) {
1109 alert("error processing request: " + textStatus);
1110 return false;
1111 };
1112 ajaxPOST(url, postData, success, failure);
1113 }
1061 1114 </script>
1062 1115 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
1063 1116 </%def>
@@ -15,6 +15,7 b" if getattr(c, 'repo_group', None):"
15 15 c.template_context['repo_group_name'] = c.repo_group.group_name
16 16
17 17 if getattr(c, 'rhodecode_user', None) and c.rhodecode_user.user_id:
18 c.template_context['rhodecode_user']['user_id'] = c.rhodecode_user.user_id
18 19 c.template_context['rhodecode_user']['username'] = c.rhodecode_user.username
19 20 c.template_context['rhodecode_user']['email'] = c.rhodecode_user.email
20 21 c.template_context['rhodecode_user']['notification_status'] = c.rhodecode_user.get_instance().user_data.get('notification_status', True)
General Comments 0
You need to be logged in to leave comments. Login now