##// 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
@@ -1,57 +1,57 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import os
22 22 import sys
23 23 import platform
24 24
25 25 VERSION = tuple(open(os.path.join(
26 26 os.path.dirname(__file__), 'VERSION')).read().split('.'))
27 27
28 28 BACKENDS = {
29 29 'hg': 'Mercurial repository',
30 30 'git': 'Git repository',
31 31 'svn': 'Subversion repository',
32 32 }
33 33
34 34 CELERY_ENABLED = False
35 35 CELERY_EAGER = False
36 36
37 37 # link to config for pyramid
38 38 CONFIG = {}
39 39
40 40 # Populated with the settings dictionary from application init in
41 41 # rhodecode.conf.environment.load_pyramid_environment
42 42 PYRAMID_SETTINGS = {}
43 43
44 44 # Linked module for extensions
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'
52 52 __url__ = 'https://code.rhodecode.com'
53 53
54 54 is_windows = __platform__ in ['Windows']
55 55 is_unix = not is_windows
56 56 is_test = False
57 57 disable_error_handler = False
@@ -1,457 +1,462 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 from rhodecode.apps._base import ADMIN_PREFIX
23 23
24 24
25 25 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')
33 32
34 33 config.add_route(
35 34 name='admin_audit_log_entry',
36 35 pattern='/audit_logs/{audit_log_id}')
37 36
38 37 config.add_route(
39 38 name='pull_requests_global_0', # backward compat
40 39 pattern='/pull_requests/{pull_request_id:\d+}')
41 40 config.add_route(
42 41 name='pull_requests_global_1', # backward compat
43 42 pattern='/pull-requests/{pull_request_id:\d+}')
44 43 config.add_route(
45 44 name='pull_requests_global',
46 45 pattern='/pull-request/{pull_request_id:\d+}')
47 46
48 47 config.add_route(
49 48 name='admin_settings_open_source',
50 49 pattern='/settings/open_source')
51 50 config.add_route(
52 51 name='admin_settings_vcs_svn_generate_cfg',
53 52 pattern='/settings/vcs/svn_generate_cfg')
54 53
55 54 config.add_route(
56 55 name='admin_settings_system',
57 56 pattern='/settings/system')
58 57 config.add_route(
59 58 name='admin_settings_system_update',
60 59 pattern='/settings/system/updates')
61 60
62 61 config.add_route(
63 62 name='admin_settings_exception_tracker',
64 63 pattern='/settings/exceptions')
65 64 config.add_route(
66 65 name='admin_settings_exception_tracker_delete_all',
67 66 pattern='/settings/exceptions/delete')
68 67 config.add_route(
69 68 name='admin_settings_exception_tracker_show',
70 69 pattern='/settings/exceptions/{exception_id}')
71 70 config.add_route(
72 71 name='admin_settings_exception_tracker_delete',
73 72 pattern='/settings/exceptions/{exception_id}/delete')
74 73
75 74 config.add_route(
76 75 name='admin_settings_sessions',
77 76 pattern='/settings/sessions')
78 77 config.add_route(
79 78 name='admin_settings_sessions_cleanup',
80 79 pattern='/settings/sessions/cleanup')
81 80
82 81 config.add_route(
83 82 name='admin_settings_process_management',
84 83 pattern='/settings/process_management')
85 84 config.add_route(
86 85 name='admin_settings_process_management_data',
87 86 pattern='/settings/process_management/data')
88 87 config.add_route(
89 88 name='admin_settings_process_management_signal',
90 89 pattern='/settings/process_management/signal')
91 90 config.add_route(
92 91 name='admin_settings_process_management_master_signal',
93 92 pattern='/settings/process_management/master_signal')
94 93
95 94 # default settings
96 95 config.add_route(
97 96 name='admin_defaults_repositories',
98 97 pattern='/defaults/repositories')
99 98 config.add_route(
100 99 name='admin_defaults_repositories_update',
101 100 pattern='/defaults/repositories/update')
102 101
103 102 # admin settings
104 103
105 104 config.add_route(
106 105 name='admin_settings',
107 106 pattern='/settings')
108 107 config.add_route(
109 108 name='admin_settings_update',
110 109 pattern='/settings/update')
111 110
112 111 config.add_route(
113 112 name='admin_settings_global',
114 113 pattern='/settings/global')
115 114 config.add_route(
116 115 name='admin_settings_global_update',
117 116 pattern='/settings/global/update')
118 117
119 118 config.add_route(
120 119 name='admin_settings_vcs',
121 120 pattern='/settings/vcs')
122 121 config.add_route(
123 122 name='admin_settings_vcs_update',
124 123 pattern='/settings/vcs/update')
125 124 config.add_route(
126 125 name='admin_settings_vcs_svn_pattern_delete',
127 126 pattern='/settings/vcs/svn_pattern_delete')
128 127
129 128 config.add_route(
130 129 name='admin_settings_mapping',
131 130 pattern='/settings/mapping')
132 131 config.add_route(
133 132 name='admin_settings_mapping_update',
134 133 pattern='/settings/mapping/update')
135 134
136 135 config.add_route(
137 136 name='admin_settings_visual',
138 137 pattern='/settings/visual')
139 138 config.add_route(
140 139 name='admin_settings_visual_update',
141 140 pattern='/settings/visual/update')
142 141
143 142 config.add_route(
144 143 name='admin_settings_issuetracker',
145 144 pattern='/settings/issue-tracker')
146 145 config.add_route(
147 146 name='admin_settings_issuetracker_update',
148 147 pattern='/settings/issue-tracker/update')
149 148 config.add_route(
150 149 name='admin_settings_issuetracker_test',
151 150 pattern='/settings/issue-tracker/test')
152 151 config.add_route(
153 152 name='admin_settings_issuetracker_delete',
154 153 pattern='/settings/issue-tracker/delete')
155 154
156 155 config.add_route(
157 156 name='admin_settings_email',
158 157 pattern='/settings/email')
159 158 config.add_route(
160 159 name='admin_settings_email_update',
161 160 pattern='/settings/email/update')
162 161
163 162 config.add_route(
164 163 name='admin_settings_hooks',
165 164 pattern='/settings/hooks')
166 165 config.add_route(
167 166 name='admin_settings_hooks_update',
168 167 pattern='/settings/hooks/update')
169 168 config.add_route(
170 169 name='admin_settings_hooks_delete',
171 170 pattern='/settings/hooks/delete')
172 171
173 172 config.add_route(
174 173 name='admin_settings_search',
175 174 pattern='/settings/search')
176 175
177 176 config.add_route(
178 177 name='admin_settings_labs',
179 178 pattern='/settings/labs')
180 179 config.add_route(
181 180 name='admin_settings_labs_update',
182 181 pattern='/settings/labs/update')
183 182
184 183 # Automation EE feature
185 184 config.add_route(
186 185 'admin_settings_automation',
187 186 pattern=ADMIN_PREFIX + '/settings/automation')
188 187
189 188 # global permissions
190 189
191 190 config.add_route(
192 191 name='admin_permissions_application',
193 192 pattern='/permissions/application')
194 193 config.add_route(
195 194 name='admin_permissions_application_update',
196 195 pattern='/permissions/application/update')
197 196
198 197 config.add_route(
199 198 name='admin_permissions_global',
200 199 pattern='/permissions/global')
201 200 config.add_route(
202 201 name='admin_permissions_global_update',
203 202 pattern='/permissions/global/update')
204 203
205 204 config.add_route(
206 205 name='admin_permissions_object',
207 206 pattern='/permissions/object')
208 207 config.add_route(
209 208 name='admin_permissions_object_update',
210 209 pattern='/permissions/object/update')
211 210
212 211 # Branch perms EE feature
213 212 config.add_route(
214 213 name='admin_permissions_branch',
215 214 pattern='/permissions/branch')
216 215
217 216 config.add_route(
218 217 name='admin_permissions_ips',
219 218 pattern='/permissions/ips')
220 219
221 220 config.add_route(
222 221 name='admin_permissions_overview',
223 222 pattern='/permissions/overview')
224 223
225 224 config.add_route(
226 225 name='admin_permissions_auth_token_access',
227 226 pattern='/permissions/auth_token_access')
228 227
229 228 config.add_route(
230 229 name='admin_permissions_ssh_keys',
231 230 pattern='/permissions/ssh_keys')
232 231 config.add_route(
233 232 name='admin_permissions_ssh_keys_data',
234 233 pattern='/permissions/ssh_keys/data')
235 234 config.add_route(
236 235 name='admin_permissions_ssh_keys_update',
237 236 pattern='/permissions/ssh_keys/update')
238 237
239 238 # users admin
240 239 config.add_route(
241 240 name='users',
242 241 pattern='/users')
243 242
244 243 config.add_route(
245 244 name='users_data',
246 245 pattern='/users_data')
247 246
248 247 config.add_route(
249 248 name='users_create',
250 249 pattern='/users/create')
251 250
252 251 config.add_route(
253 252 name='users_new',
254 253 pattern='/users/new')
255 254
256 255 # user management
257 256 config.add_route(
258 257 name='user_edit',
259 258 pattern='/users/{user_id:\d+}/edit',
260 259 user_route=True)
261 260 config.add_route(
262 261 name='user_edit_advanced',
263 262 pattern='/users/{user_id:\d+}/edit/advanced',
264 263 user_route=True)
265 264 config.add_route(
266 265 name='user_edit_global_perms',
267 266 pattern='/users/{user_id:\d+}/edit/global_permissions',
268 267 user_route=True)
269 268 config.add_route(
270 269 name='user_edit_global_perms_update',
271 270 pattern='/users/{user_id:\d+}/edit/global_permissions/update',
272 271 user_route=True)
273 272 config.add_route(
274 273 name='user_update',
275 274 pattern='/users/{user_id:\d+}/update',
276 275 user_route=True)
277 276 config.add_route(
278 277 name='user_delete',
279 278 pattern='/users/{user_id:\d+}/delete',
280 279 user_route=True)
281 280 config.add_route(
282 281 name='user_enable_force_password_reset',
283 282 pattern='/users/{user_id:\d+}/password_reset_enable',
284 283 user_route=True)
285 284 config.add_route(
286 285 name='user_disable_force_password_reset',
287 286 pattern='/users/{user_id:\d+}/password_reset_disable',
288 287 user_route=True)
289 288 config.add_route(
290 289 name='user_create_personal_repo_group',
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',
297 302 pattern='/users/{user_id:\d+}/edit/auth_tokens',
298 303 user_route=True)
299 304 config.add_route(
300 305 name='edit_user_auth_tokens_add',
301 306 pattern='/users/{user_id:\d+}/edit/auth_tokens/new',
302 307 user_route=True)
303 308 config.add_route(
304 309 name='edit_user_auth_tokens_delete',
305 310 pattern='/users/{user_id:\d+}/edit/auth_tokens/delete',
306 311 user_route=True)
307 312
308 313 # user ssh keys
309 314 config.add_route(
310 315 name='edit_user_ssh_keys',
311 316 pattern='/users/{user_id:\d+}/edit/ssh_keys',
312 317 user_route=True)
313 318 config.add_route(
314 319 name='edit_user_ssh_keys_generate_keypair',
315 320 pattern='/users/{user_id:\d+}/edit/ssh_keys/generate',
316 321 user_route=True)
317 322 config.add_route(
318 323 name='edit_user_ssh_keys_add',
319 324 pattern='/users/{user_id:\d+}/edit/ssh_keys/new',
320 325 user_route=True)
321 326 config.add_route(
322 327 name='edit_user_ssh_keys_delete',
323 328 pattern='/users/{user_id:\d+}/edit/ssh_keys/delete',
324 329 user_route=True)
325 330
326 331 # user emails
327 332 config.add_route(
328 333 name='edit_user_emails',
329 334 pattern='/users/{user_id:\d+}/edit/emails',
330 335 user_route=True)
331 336 config.add_route(
332 337 name='edit_user_emails_add',
333 338 pattern='/users/{user_id:\d+}/edit/emails/new',
334 339 user_route=True)
335 340 config.add_route(
336 341 name='edit_user_emails_delete',
337 342 pattern='/users/{user_id:\d+}/edit/emails/delete',
338 343 user_route=True)
339 344
340 345 # user IPs
341 346 config.add_route(
342 347 name='edit_user_ips',
343 348 pattern='/users/{user_id:\d+}/edit/ips',
344 349 user_route=True)
345 350 config.add_route(
346 351 name='edit_user_ips_add',
347 352 pattern='/users/{user_id:\d+}/edit/ips/new',
348 353 user_route_with_default=True) # enabled for default user too
349 354 config.add_route(
350 355 name='edit_user_ips_delete',
351 356 pattern='/users/{user_id:\d+}/edit/ips/delete',
352 357 user_route_with_default=True) # enabled for default user too
353 358
354 359 # user perms
355 360 config.add_route(
356 361 name='edit_user_perms_summary',
357 362 pattern='/users/{user_id:\d+}/edit/permissions_summary',
358 363 user_route=True)
359 364 config.add_route(
360 365 name='edit_user_perms_summary_json',
361 366 pattern='/users/{user_id:\d+}/edit/permissions_summary/json',
362 367 user_route=True)
363 368
364 369 # user user groups management
365 370 config.add_route(
366 371 name='edit_user_groups_management',
367 372 pattern='/users/{user_id:\d+}/edit/groups_management',
368 373 user_route=True)
369 374
370 375 config.add_route(
371 376 name='edit_user_groups_management_updates',
372 377 pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates',
373 378 user_route=True)
374 379
375 380 # user audit logs
376 381 config.add_route(
377 382 name='edit_user_audit_logs',
378 383 pattern='/users/{user_id:\d+}/edit/audit', user_route=True)
379 384
380 385 config.add_route(
381 386 name='edit_user_audit_logs_download',
382 387 pattern='/users/{user_id:\d+}/edit/audit/download', user_route=True)
383 388
384 389 # user caches
385 390 config.add_route(
386 391 name='edit_user_caches',
387 392 pattern='/users/{user_id:\d+}/edit/caches',
388 393 user_route=True)
389 394 config.add_route(
390 395 name='edit_user_caches_update',
391 396 pattern='/users/{user_id:\d+}/edit/caches/update',
392 397 user_route=True)
393 398
394 399 # user-groups admin
395 400 config.add_route(
396 401 name='user_groups',
397 402 pattern='/user_groups')
398 403
399 404 config.add_route(
400 405 name='user_groups_data',
401 406 pattern='/user_groups_data')
402 407
403 408 config.add_route(
404 409 name='user_groups_new',
405 410 pattern='/user_groups/new')
406 411
407 412 config.add_route(
408 413 name='user_groups_create',
409 414 pattern='/user_groups/create')
410 415
411 416 # repos admin
412 417 config.add_route(
413 418 name='repos',
414 419 pattern='/repos')
415 420
416 421 config.add_route(
417 422 name='repos_data',
418 423 pattern='/repos_data')
419 424
420 425 config.add_route(
421 426 name='repo_new',
422 427 pattern='/repos/new')
423 428
424 429 config.add_route(
425 430 name='repo_create',
426 431 pattern='/repos/create')
427 432
428 433 # repo groups admin
429 434 config.add_route(
430 435 name='repo_groups',
431 436 pattern='/repo_groups')
432 437
433 438 config.add_route(
434 439 name='repo_groups_data',
435 440 pattern='/repo_groups_data')
436 441
437 442 config.add_route(
438 443 name='repo_group_new',
439 444 pattern='/repo_group/new')
440 445
441 446 config.add_route(
442 447 name='repo_group_create',
443 448 pattern='/repo_group/create')
444 449
445 450
446 451 def includeme(config):
447 452 from rhodecode.apps._base.navigation import includeme as nav_includeme
448 453
449 454 # Create admin navigation registry and add it to the pyramid registry.
450 455 nav_includeme(config)
451 456
452 457 # main admin routes
453 458 config.add_route(name='admin_home', pattern=ADMIN_PREFIX)
454 459 config.include(admin_routes, route_prefix=ADMIN_PREFIX)
455 460
456 461 # Scan module for configuration decorators.
457 462 config.scan('.views', ignore='.tests')
@@ -1,1336 +1,1362 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2016-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 import logging
22 22 import datetime
23 23 import formencode
24 24 import formencode.htmlfill
25 25
26 26 from pyramid.httpexceptions import HTTPFound
27 27 from pyramid.view import view_config
28 28 from pyramid.renderers import render
29 29 from pyramid.response import Response
30 30
31 31 from rhodecode import events
32 32 from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
33 33 from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
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 (
41 41 UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
42 42 UserOwnsUserGroupsException, DefaultUserException)
43 43 from rhodecode.lib.ext_json import json
44 44 from rhodecode.lib.auth import (
45 45 LoginRequired, HasPermissionAllDecorator, CSRFRequired)
46 46 from rhodecode.lib import helpers as h
47 47 from rhodecode.lib.helpers import SqlPage
48 48 from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
49 49 from rhodecode.model.auth_token import AuthTokenModel
50 50 from rhodecode.model.forms import (
51 51 UserForm, UserIndividualPermissionsForm, UserPermissionsForm,
52 52 UserExtraEmailForm, UserExtraIpForm)
53 53 from rhodecode.model.permission import PermissionModel
54 54 from rhodecode.model.repo_group import RepoGroupModel
55 55 from rhodecode.model.ssh_key import SshKeyModel
56 56 from rhodecode.model.user import UserModel
57 57 from rhodecode.model.user_group import UserGroupModel
58 58 from rhodecode.model.db import (
59 59 or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
60 60 UserApiKeys, UserSshKeys, RepoGroup)
61 61 from rhodecode.model.meta import Session
62 62
63 63 log = logging.getLogger(__name__)
64 64
65 65
66 66 class AdminUsersView(BaseAppView, DataGridAppView):
67 67
68 68 def load_default_context(self):
69 69 c = self._get_local_tmpl_context()
70 70 return c
71 71
72 72 @LoginRequired()
73 73 @HasPermissionAllDecorator('hg.admin')
74 74 @view_config(
75 75 route_name='users', request_method='GET',
76 76 renderer='rhodecode:templates/admin/users/users.mako')
77 77 def users_list(self):
78 78 c = self.load_default_context()
79 79 return self._get_template_context(c)
80 80
81 81 @LoginRequired()
82 82 @HasPermissionAllDecorator('hg.admin')
83 83 @view_config(
84 84 # renderer defined below
85 85 route_name='users_data', request_method='GET',
86 86 renderer='json_ext', xhr=True)
87 87 def users_list_data(self):
88 88 self.load_default_context()
89 89 column_map = {
90 90 'first_name': 'name',
91 91 'last_name': 'lastname',
92 92 }
93 93 draw, start, limit = self._extract_chunk(self.request)
94 94 search_q, order_by, order_dir = self._extract_ordering(
95 95 self.request, column_map=column_map)
96 96 _render = self.request.get_partial_renderer(
97 97 'rhodecode:templates/data_table/_dt_elements.mako')
98 98
99 99 def user_actions(user_id, username):
100 100 return _render("user_actions", user_id, username)
101 101
102 102 users_data_total_count = User.query()\
103 103 .filter(User.username != User.DEFAULT_USER) \
104 104 .count()
105 105
106 106 users_data_total_inactive_count = User.query()\
107 107 .filter(User.username != User.DEFAULT_USER) \
108 108 .filter(User.active != true())\
109 109 .count()
110 110
111 111 # json generate
112 112 base_q = User.query().filter(User.username != User.DEFAULT_USER)
113 113 base_inactive_q = base_q.filter(User.active != true())
114 114
115 115 if search_q:
116 116 like_expression = u'%{}%'.format(safe_unicode(search_q))
117 117 base_q = base_q.filter(or_(
118 118 User.username.ilike(like_expression),
119 119 User._email.ilike(like_expression),
120 120 User.name.ilike(like_expression),
121 121 User.lastname.ilike(like_expression),
122 122 ))
123 123 base_inactive_q = base_q.filter(User.active != true())
124 124
125 125 users_data_total_filtered_count = base_q.count()
126 126 users_data_total_filtered_inactive_count = base_inactive_q.count()
127 127
128 128 sort_col = getattr(User, order_by, None)
129 129 if sort_col:
130 130 if order_dir == 'asc':
131 131 # handle null values properly to order by NULL last
132 132 if order_by in ['last_activity']:
133 133 sort_col = coalesce(sort_col, datetime.date.max)
134 134 sort_col = sort_col.asc()
135 135 else:
136 136 # handle null values properly to order by NULL last
137 137 if order_by in ['last_activity']:
138 138 sort_col = coalesce(sort_col, datetime.date.min)
139 139 sort_col = sort_col.desc()
140 140
141 141 base_q = base_q.order_by(sort_col)
142 142 base_q = base_q.offset(start).limit(limit)
143 143
144 144 users_list = base_q.all()
145 145
146 146 users_data = []
147 147 for user in users_list:
148 148 users_data.append({
149 149 "username": h.gravatar_with_user(self.request, user.username),
150 150 "email": user.email,
151 151 "first_name": user.first_name,
152 152 "last_name": user.last_name,
153 153 "last_login": h.format_date(user.last_login),
154 154 "last_activity": h.format_date(user.last_activity),
155 155 "active": h.bool2icon(user.active),
156 156 "active_raw": user.active,
157 157 "admin": h.bool2icon(user.admin),
158 158 "extern_type": user.extern_type,
159 159 "extern_name": user.extern_name,
160 160 "action": user_actions(user.user_id, user.username),
161 161 })
162 162 data = ({
163 163 'draw': draw,
164 164 'data': users_data,
165 165 'recordsTotal': users_data_total_count,
166 166 'recordsFiltered': users_data_total_filtered_count,
167 167 'recordsTotalInactive': users_data_total_inactive_count,
168 168 'recordsFilteredInactive': users_data_total_filtered_inactive_count
169 169 })
170 170
171 171 return data
172 172
173 173 def _set_personal_repo_group_template_vars(self, c_obj):
174 174 DummyUser = AttributeDict({
175 175 'username': '${username}',
176 176 'user_id': '${user_id}',
177 177 })
178 178 c_obj.default_create_repo_group = RepoGroupModel() \
179 179 .get_default_create_personal_repo_group()
180 180 c_obj.personal_repo_group_name = RepoGroupModel() \
181 181 .get_personal_group_name(DummyUser)
182 182
183 183 @LoginRequired()
184 184 @HasPermissionAllDecorator('hg.admin')
185 185 @view_config(
186 186 route_name='users_new', request_method='GET',
187 187 renderer='rhodecode:templates/admin/users/user_add.mako')
188 188 def users_new(self):
189 189 _ = self.request.translate
190 190 c = self.load_default_context()
191 191 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
192 192 self._set_personal_repo_group_template_vars(c)
193 193 return self._get_template_context(c)
194 194
195 195 @LoginRequired()
196 196 @HasPermissionAllDecorator('hg.admin')
197 197 @CSRFRequired()
198 198 @view_config(
199 199 route_name='users_create', request_method='POST',
200 200 renderer='rhodecode:templates/admin/users/user_add.mako')
201 201 def users_create(self):
202 202 _ = self.request.translate
203 203 c = self.load_default_context()
204 204 c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.uid
205 205 user_model = UserModel()
206 206 user_form = UserForm(self.request.translate)()
207 207 try:
208 208 form_result = user_form.to_python(dict(self.request.POST))
209 209 user = user_model.create(form_result)
210 210 Session().flush()
211 211 creation_data = user.get_api_data()
212 212 username = form_result['username']
213 213
214 214 audit_logger.store_web(
215 215 'user.create', action_data={'data': creation_data},
216 216 user=c.rhodecode_user)
217 217
218 218 user_link = h.link_to(
219 219 h.escape(username),
220 220 h.route_path('user_edit', user_id=user.user_id))
221 221 h.flash(h.literal(_('Created user %(user_link)s')
222 222 % {'user_link': user_link}), category='success')
223 223 Session().commit()
224 224 except formencode.Invalid as errors:
225 225 self._set_personal_repo_group_template_vars(c)
226 226 data = render(
227 227 'rhodecode:templates/admin/users/user_add.mako',
228 228 self._get_template_context(c), self.request)
229 229 html = formencode.htmlfill.render(
230 230 data,
231 231 defaults=errors.value,
232 232 errors=errors.error_dict or {},
233 233 prefix_error=False,
234 234 encoding="UTF-8",
235 235 force_defaults=False
236 236 )
237 237 return Response(html)
238 238 except UserCreationError as e:
239 239 h.flash(e, 'error')
240 240 except Exception:
241 241 log.exception("Exception creation of user")
242 242 h.flash(_('Error occurred during creation of user %s')
243 243 % self.request.POST.get('username'), category='error')
244 244 raise HTTPFound(h.route_path('users'))
245 245
246 246
247 247 class UsersView(UserAppView):
248 248 ALLOW_SCOPED_TOKENS = False
249 249 """
250 250 This view has alternative version inside EE, if modified please take a look
251 251 in there as well.
252 252 """
253 253
254 254 def get_auth_plugins(self):
255 255 valid_plugins = []
256 256 authn_registry = get_authn_registry(self.request.registry)
257 257 for plugin in authn_registry.get_plugins_for_authentication():
258 258 if isinstance(plugin, RhodeCodeExternalAuthPlugin):
259 259 valid_plugins.append(plugin)
260 260 elif plugin.name == 'rhodecode':
261 261 valid_plugins.append(plugin)
262 262
263 263 # extend our choices if user has set a bound plugin which isn't enabled at the
264 264 # moment
265 265 extern_type = self.db_user.extern_type
266 266 if extern_type not in [x.uid for x in valid_plugins]:
267 267 try:
268 268 plugin = authn_registry.get_plugin_by_uid(extern_type)
269 269 if plugin:
270 270 valid_plugins.append(plugin)
271 271
272 272 except Exception:
273 273 log.exception(
274 274 'Could not extend user plugins with `{}`'.format(extern_type))
275 275 return valid_plugins
276 276
277 277 def load_default_context(self):
278 278 req = self.request
279 279
280 280 c = self._get_local_tmpl_context()
281 281 c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
282 282 c.allowed_languages = [
283 283 ('en', 'English (en)'),
284 284 ('de', 'German (de)'),
285 285 ('fr', 'French (fr)'),
286 286 ('it', 'Italian (it)'),
287 287 ('ja', 'Japanese (ja)'),
288 288 ('pl', 'Polish (pl)'),
289 289 ('pt', 'Portuguese (pt)'),
290 290 ('ru', 'Russian (ru)'),
291 291 ('zh', 'Chinese (zh)'),
292 292 ]
293 293
294 294 c.allowed_extern_types = [
295 295 (x.uid, x.get_display_name()) for x in self.get_auth_plugins()
296 296 ]
297 297
298 298 c.available_permissions = req.registry.settings['available_permissions']
299 299 PermissionModel().set_global_permission_choices(
300 300 c, gettext_translator=req.translate)
301 301
302 302 return c
303 303
304 304 @LoginRequired()
305 305 @HasPermissionAllDecorator('hg.admin')
306 306 @CSRFRequired()
307 307 @view_config(
308 308 route_name='user_update', request_method='POST',
309 309 renderer='rhodecode:templates/admin/users/user_edit.mako')
310 310 def user_update(self):
311 311 _ = self.request.translate
312 312 c = self.load_default_context()
313 313
314 314 user_id = self.db_user_id
315 315 c.user = self.db_user
316 316
317 317 c.active = 'profile'
318 318 c.extern_type = c.user.extern_type
319 319 c.extern_name = c.user.extern_name
320 320 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
321 321 available_languages = [x[0] for x in c.allowed_languages]
322 322 _form = UserForm(self.request.translate, edit=True,
323 323 available_languages=available_languages,
324 324 old_data={'user_id': user_id,
325 325 'email': c.user.email})()
326 326 form_result = {}
327 327 old_values = c.user.get_api_data()
328 328 try:
329 329 form_result = _form.to_python(dict(self.request.POST))
330 330 skip_attrs = ['extern_name']
331 331 # TODO: plugin should define if username can be updated
332 332 if c.extern_type != "rhodecode":
333 333 # forbid updating username for external accounts
334 334 skip_attrs.append('username')
335 335
336 336 UserModel().update_user(
337 337 user_id, skip_attrs=skip_attrs, **form_result)
338 338
339 339 audit_logger.store_web(
340 340 'user.edit', action_data={'old_data': old_values},
341 341 user=c.rhodecode_user)
342 342
343 343 Session().commit()
344 344 h.flash(_('User updated successfully'), category='success')
345 345 except formencode.Invalid as errors:
346 346 data = render(
347 347 'rhodecode:templates/admin/users/user_edit.mako',
348 348 self._get_template_context(c), self.request)
349 349 html = formencode.htmlfill.render(
350 350 data,
351 351 defaults=errors.value,
352 352 errors=errors.error_dict or {},
353 353 prefix_error=False,
354 354 encoding="UTF-8",
355 355 force_defaults=False
356 356 )
357 357 return Response(html)
358 358 except UserCreationError as e:
359 359 h.flash(e, 'error')
360 360 except Exception:
361 361 log.exception("Exception updating user")
362 362 h.flash(_('Error occurred during update of user %s')
363 363 % form_result.get('username'), category='error')
364 364 raise HTTPFound(h.route_path('user_edit', user_id=user_id))
365 365
366 366 @LoginRequired()
367 367 @HasPermissionAllDecorator('hg.admin')
368 368 @CSRFRequired()
369 369 @view_config(
370 370 route_name='user_delete', request_method='POST',
371 371 renderer='rhodecode:templates/admin/users/user_edit.mako')
372 372 def user_delete(self):
373 373 _ = self.request.translate
374 374 c = self.load_default_context()
375 375 c.user = self.db_user
376 376
377 377 _repos = c.user.repositories
378 378 _repo_groups = c.user.repository_groups
379 379 _user_groups = c.user.user_groups
380 380 _artifacts = c.user.artifacts
381 381
382 382 handle_repos = None
383 383 handle_repo_groups = None
384 384 handle_user_groups = None
385 385 handle_artifacts = None
386 386
387 387 # calls for flash of handle based on handle case detach or delete
388 388 def set_handle_flash_repos():
389 389 handle = handle_repos
390 390 if handle == 'detach':
391 391 h.flash(_('Detached %s repositories') % len(_repos),
392 392 category='success')
393 393 elif handle == 'delete':
394 394 h.flash(_('Deleted %s repositories') % len(_repos),
395 395 category='success')
396 396
397 397 def set_handle_flash_repo_groups():
398 398 handle = handle_repo_groups
399 399 if handle == 'detach':
400 400 h.flash(_('Detached %s repository groups') % len(_repo_groups),
401 401 category='success')
402 402 elif handle == 'delete':
403 403 h.flash(_('Deleted %s repository groups') % len(_repo_groups),
404 404 category='success')
405 405
406 406 def set_handle_flash_user_groups():
407 407 handle = handle_user_groups
408 408 if handle == 'detach':
409 409 h.flash(_('Detached %s user groups') % len(_user_groups),
410 410 category='success')
411 411 elif handle == 'delete':
412 412 h.flash(_('Deleted %s user groups') % len(_user_groups),
413 413 category='success')
414 414
415 415 def set_handle_flash_artifacts():
416 416 handle = handle_artifacts
417 417 if handle == 'detach':
418 418 h.flash(_('Detached %s artifacts') % len(_artifacts),
419 419 category='success')
420 420 elif handle == 'delete':
421 421 h.flash(_('Deleted %s artifacts') % len(_artifacts),
422 422 category='success')
423 423
424 424 if _repos and self.request.POST.get('user_repos'):
425 425 handle_repos = self.request.POST['user_repos']
426 426
427 427 if _repo_groups and self.request.POST.get('user_repo_groups'):
428 428 handle_repo_groups = self.request.POST['user_repo_groups']
429 429
430 430 if _user_groups and self.request.POST.get('user_user_groups'):
431 431 handle_user_groups = self.request.POST['user_user_groups']
432 432
433 433 if _artifacts and self.request.POST.get('user_artifacts'):
434 434 handle_artifacts = self.request.POST['user_artifacts']
435 435
436 436 old_values = c.user.get_api_data()
437 437
438 438 try:
439 439 UserModel().delete(c.user, handle_repos=handle_repos,
440 440 handle_repo_groups=handle_repo_groups,
441 441 handle_user_groups=handle_user_groups,
442 442 handle_artifacts=handle_artifacts)
443 443
444 444 audit_logger.store_web(
445 445 'user.delete', action_data={'old_data': old_values},
446 446 user=c.rhodecode_user)
447 447
448 448 Session().commit()
449 449 set_handle_flash_repos()
450 450 set_handle_flash_repo_groups()
451 451 set_handle_flash_user_groups()
452 452 set_handle_flash_artifacts()
453 453 username = h.escape(old_values['username'])
454 454 h.flash(_('Successfully deleted user `{}`').format(username), category='success')
455 455 except (UserOwnsReposException, UserOwnsRepoGroupsException,
456 456 UserOwnsUserGroupsException, DefaultUserException) as e:
457 457 h.flash(e, category='warning')
458 458 except Exception:
459 459 log.exception("Exception during deletion of user")
460 460 h.flash(_('An error occurred during deletion of user'),
461 461 category='error')
462 462 raise HTTPFound(h.route_path('users'))
463 463
464 464 @LoginRequired()
465 465 @HasPermissionAllDecorator('hg.admin')
466 466 @view_config(
467 467 route_name='user_edit', request_method='GET',
468 468 renderer='rhodecode:templates/admin/users/user_edit.mako')
469 469 def user_edit(self):
470 470 _ = self.request.translate
471 471 c = self.load_default_context()
472 472 c.user = self.db_user
473 473
474 474 c.active = 'profile'
475 475 c.extern_type = c.user.extern_type
476 476 c.extern_name = c.user.extern_name
477 477 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
478 478
479 479 defaults = c.user.get_dict()
480 480 defaults.update({'language': c.user.user_data.get('language')})
481 481
482 482 data = render(
483 483 'rhodecode:templates/admin/users/user_edit.mako',
484 484 self._get_template_context(c), self.request)
485 485 html = formencode.htmlfill.render(
486 486 data,
487 487 defaults=defaults,
488 488 encoding="UTF-8",
489 489 force_defaults=False
490 490 )
491 491 return Response(html)
492 492
493 493 @LoginRequired()
494 494 @HasPermissionAllDecorator('hg.admin')
495 495 @view_config(
496 496 route_name='user_edit_advanced', request_method='GET',
497 497 renderer='rhodecode:templates/admin/users/user_edit.mako')
498 498 def user_edit_advanced(self):
499 499 _ = self.request.translate
500 500 c = self.load_default_context()
501 501
502 502 user_id = self.db_user_id
503 503 c.user = self.db_user
504 504
505 505 c.active = 'advanced'
506 506 c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
507 507 c.personal_repo_group_name = RepoGroupModel()\
508 508 .get_personal_group_name(c.user)
509 509
510 510 c.user_to_review_rules = sorted(
511 511 (x.user for x in c.user.user_review_rules),
512 512 key=lambda u: u.username.lower())
513 513
514 514 c.first_admin = User.get_first_super_admin()
515 515 defaults = c.user.get_dict()
516 516
517 517 # Interim workaround if the user participated on any pull requests as a
518 518 # reviewer.
519 519 has_review = len(c.user.reviewer_pull_requests)
520 520 c.can_delete_user = not has_review
521 521 c.can_delete_user_message = ''
522 522 inactive_link = h.link_to(
523 523 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
524 524 if has_review == 1:
525 525 c.can_delete_user_message = h.literal(_(
526 526 'The user participates as reviewer in {} pull request and '
527 527 'cannot be deleted. \nYou can set the user to '
528 528 '"{}" instead of deleting it.').format(
529 529 has_review, inactive_link))
530 530 elif has_review:
531 531 c.can_delete_user_message = h.literal(_(
532 532 'The user participates as reviewer in {} pull requests and '
533 533 'cannot be deleted. \nYou can set the user to '
534 534 '"{}" instead of deleting it.').format(
535 535 has_review, inactive_link))
536 536
537 537 data = render(
538 538 'rhodecode:templates/admin/users/user_edit.mako',
539 539 self._get_template_context(c), self.request)
540 540 html = formencode.htmlfill.render(
541 541 data,
542 542 defaults=defaults,
543 543 encoding="UTF-8",
544 544 force_defaults=False
545 545 )
546 546 return Response(html)
547 547
548 548 @LoginRequired()
549 549 @HasPermissionAllDecorator('hg.admin')
550 550 @view_config(
551 551 route_name='user_edit_global_perms', request_method='GET',
552 552 renderer='rhodecode:templates/admin/users/user_edit.mako')
553 553 def user_edit_global_perms(self):
554 554 _ = self.request.translate
555 555 c = self.load_default_context()
556 556 c.user = self.db_user
557 557
558 558 c.active = 'global_perms'
559 559
560 560 c.default_user = User.get_default_user()
561 561 defaults = c.user.get_dict()
562 562 defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
563 563 defaults.update(c.default_user.get_default_perms())
564 564 defaults.update(c.user.get_default_perms())
565 565
566 566 data = render(
567 567 'rhodecode:templates/admin/users/user_edit.mako',
568 568 self._get_template_context(c), self.request)
569 569 html = formencode.htmlfill.render(
570 570 data,
571 571 defaults=defaults,
572 572 encoding="UTF-8",
573 573 force_defaults=False
574 574 )
575 575 return Response(html)
576 576
577 577 @LoginRequired()
578 578 @HasPermissionAllDecorator('hg.admin')
579 579 @CSRFRequired()
580 580 @view_config(
581 581 route_name='user_edit_global_perms_update', request_method='POST',
582 582 renderer='rhodecode:templates/admin/users/user_edit.mako')
583 583 def user_edit_global_perms_update(self):
584 584 _ = self.request.translate
585 585 c = self.load_default_context()
586 586
587 587 user_id = self.db_user_id
588 588 c.user = self.db_user
589 589
590 590 c.active = 'global_perms'
591 591 try:
592 592 # first stage that verifies the checkbox
593 593 _form = UserIndividualPermissionsForm(self.request.translate)
594 594 form_result = _form.to_python(dict(self.request.POST))
595 595 inherit_perms = form_result['inherit_default_permissions']
596 596 c.user.inherit_default_permissions = inherit_perms
597 597 Session().add(c.user)
598 598
599 599 if not inherit_perms:
600 600 # only update the individual ones if we un check the flag
601 601 _form = UserPermissionsForm(
602 602 self.request.translate,
603 603 [x[0] for x in c.repo_create_choices],
604 604 [x[0] for x in c.repo_create_on_write_choices],
605 605 [x[0] for x in c.repo_group_create_choices],
606 606 [x[0] for x in c.user_group_create_choices],
607 607 [x[0] for x in c.fork_choices],
608 608 [x[0] for x in c.inherit_default_permission_choices])()
609 609
610 610 form_result = _form.to_python(dict(self.request.POST))
611 611 form_result.update({'perm_user_id': c.user.user_id})
612 612
613 613 PermissionModel().update_user_permissions(form_result)
614 614
615 615 # TODO(marcink): implement global permissions
616 616 # audit_log.store_web('user.edit.permissions')
617 617
618 618 Session().commit()
619 619
620 620 h.flash(_('User global permissions updated successfully'),
621 621 category='success')
622 622
623 623 except formencode.Invalid as errors:
624 624 data = render(
625 625 'rhodecode:templates/admin/users/user_edit.mako',
626 626 self._get_template_context(c), self.request)
627 627 html = formencode.htmlfill.render(
628 628 data,
629 629 defaults=errors.value,
630 630 errors=errors.error_dict or {},
631 631 prefix_error=False,
632 632 encoding="UTF-8",
633 633 force_defaults=False
634 634 )
635 635 return Response(html)
636 636 except Exception:
637 637 log.exception("Exception during permissions saving")
638 638 h.flash(_('An error occurred during permissions saving'),
639 639 category='error')
640 640
641 641 affected_user_ids = [user_id]
642 642 PermissionModel().trigger_permission_flush(affected_user_ids)
643 643 raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
644 644
645 645 @LoginRequired()
646 646 @HasPermissionAllDecorator('hg.admin')
647 647 @CSRFRequired()
648 648 @view_config(
649 649 route_name='user_enable_force_password_reset', request_method='POST',
650 650 renderer='rhodecode:templates/admin/users/user_edit.mako')
651 651 def user_enable_force_password_reset(self):
652 652 _ = self.request.translate
653 653 c = self.load_default_context()
654 654
655 655 user_id = self.db_user_id
656 656 c.user = self.db_user
657 657
658 658 try:
659 659 c.user.update_userdata(force_password_change=True)
660 660
661 661 msg = _('Force password change enabled for user')
662 662 audit_logger.store_web('user.edit.password_reset.enabled',
663 663 user=c.rhodecode_user)
664 664
665 665 Session().commit()
666 666 h.flash(msg, category='success')
667 667 except Exception:
668 668 log.exception("Exception during password reset for user")
669 669 h.flash(_('An error occurred during password reset for user'),
670 670 category='error')
671 671
672 672 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
673 673
674 674 @LoginRequired()
675 675 @HasPermissionAllDecorator('hg.admin')
676 676 @CSRFRequired()
677 677 @view_config(
678 678 route_name='user_disable_force_password_reset', request_method='POST',
679 679 renderer='rhodecode:templates/admin/users/user_edit.mako')
680 680 def user_disable_force_password_reset(self):
681 681 _ = self.request.translate
682 682 c = self.load_default_context()
683 683
684 684 user_id = self.db_user_id
685 685 c.user = self.db_user
686 686
687 687 try:
688 688 c.user.update_userdata(force_password_change=False)
689 689
690 690 msg = _('Force password change disabled for user')
691 691 audit_logger.store_web(
692 692 'user.edit.password_reset.disabled',
693 693 user=c.rhodecode_user)
694 694
695 695 Session().commit()
696 696 h.flash(msg, category='success')
697 697 except Exception:
698 698 log.exception("Exception during password reset for user")
699 699 h.flash(_('An error occurred during password reset for user'),
700 700 category='error')
701 701
702 702 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
703 703
704 704 @LoginRequired()
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):
711 737 """
712 738 Create personal repository group for this user
713 739 """
714 740 from rhodecode.model.repo_group import RepoGroupModel
715 741
716 742 _ = self.request.translate
717 743 c = self.load_default_context()
718 744
719 745 user_id = self.db_user_id
720 746 c.user = self.db_user
721 747
722 748 personal_repo_group = RepoGroup.get_user_personal_repo_group(
723 749 c.user.user_id)
724 750 if personal_repo_group:
725 751 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
726 752
727 753 personal_repo_group_name = RepoGroupModel().get_personal_group_name(c.user)
728 754 named_personal_group = RepoGroup.get_by_group_name(
729 755 personal_repo_group_name)
730 756 try:
731 757
732 758 if named_personal_group and named_personal_group.user_id == c.user.user_id:
733 759 # migrate the same named group, and mark it as personal
734 760 named_personal_group.personal = True
735 761 Session().add(named_personal_group)
736 762 Session().commit()
737 763 msg = _('Linked repository group `%s` as personal' % (
738 764 personal_repo_group_name,))
739 765 h.flash(msg, category='success')
740 766 elif not named_personal_group:
741 767 RepoGroupModel().create_personal_repo_group(c.user)
742 768
743 769 msg = _('Created repository group `%s`' % (
744 770 personal_repo_group_name,))
745 771 h.flash(msg, category='success')
746 772 else:
747 773 msg = _('Repository group `%s` is already taken' % (
748 774 personal_repo_group_name,))
749 775 h.flash(msg, category='warning')
750 776 except Exception:
751 777 log.exception("Exception during repository group creation")
752 778 msg = _(
753 779 'An error occurred during repository group creation for user')
754 780 h.flash(msg, category='error')
755 781 Session().rollback()
756 782
757 783 raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
758 784
759 785 @LoginRequired()
760 786 @HasPermissionAllDecorator('hg.admin')
761 787 @view_config(
762 788 route_name='edit_user_auth_tokens', request_method='GET',
763 789 renderer='rhodecode:templates/admin/users/user_edit.mako')
764 790 def auth_tokens(self):
765 791 _ = self.request.translate
766 792 c = self.load_default_context()
767 793 c.user = self.db_user
768 794
769 795 c.active = 'auth_tokens'
770 796
771 797 c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
772 798 c.role_values = [
773 799 (x, AuthTokenModel.cls._get_role_name(x))
774 800 for x in AuthTokenModel.cls.ROLES]
775 801 c.role_options = [(c.role_values, _("Role"))]
776 802 c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
777 803 c.user.user_id, show_expired=True)
778 804 c.role_vcs = AuthTokenModel.cls.ROLE_VCS
779 805 return self._get_template_context(c)
780 806
781 807 def maybe_attach_token_scope(self, token):
782 808 # implemented in EE edition
783 809 pass
784 810
785 811 @LoginRequired()
786 812 @HasPermissionAllDecorator('hg.admin')
787 813 @CSRFRequired()
788 814 @view_config(
789 815 route_name='edit_user_auth_tokens_add', request_method='POST')
790 816 def auth_tokens_add(self):
791 817 _ = self.request.translate
792 818 c = self.load_default_context()
793 819
794 820 user_id = self.db_user_id
795 821 c.user = self.db_user
796 822
797 823 user_data = c.user.get_api_data()
798 824 lifetime = safe_int(self.request.POST.get('lifetime'), -1)
799 825 description = self.request.POST.get('description')
800 826 role = self.request.POST.get('role')
801 827
802 828 token = UserModel().add_auth_token(
803 829 user=c.user.user_id,
804 830 lifetime_minutes=lifetime, role=role, description=description,
805 831 scope_callback=self.maybe_attach_token_scope)
806 832 token_data = token.get_api_data()
807 833
808 834 audit_logger.store_web(
809 835 'user.edit.token.add', action_data={
810 836 'data': {'token': token_data, 'user': user_data}},
811 837 user=self._rhodecode_user, )
812 838 Session().commit()
813 839
814 840 h.flash(_("Auth token successfully created"), category='success')
815 841 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
816 842
817 843 @LoginRequired()
818 844 @HasPermissionAllDecorator('hg.admin')
819 845 @CSRFRequired()
820 846 @view_config(
821 847 route_name='edit_user_auth_tokens_delete', request_method='POST')
822 848 def auth_tokens_delete(self):
823 849 _ = self.request.translate
824 850 c = self.load_default_context()
825 851
826 852 user_id = self.db_user_id
827 853 c.user = self.db_user
828 854
829 855 user_data = c.user.get_api_data()
830 856
831 857 del_auth_token = self.request.POST.get('del_auth_token')
832 858
833 859 if del_auth_token:
834 860 token = UserApiKeys.get_or_404(del_auth_token)
835 861 token_data = token.get_api_data()
836 862
837 863 AuthTokenModel().delete(del_auth_token, c.user.user_id)
838 864 audit_logger.store_web(
839 865 'user.edit.token.delete', action_data={
840 866 'data': {'token': token_data, 'user': user_data}},
841 867 user=self._rhodecode_user,)
842 868 Session().commit()
843 869 h.flash(_("Auth token successfully deleted"), category='success')
844 870
845 871 return HTTPFound(h.route_path('edit_user_auth_tokens', user_id=user_id))
846 872
847 873 @LoginRequired()
848 874 @HasPermissionAllDecorator('hg.admin')
849 875 @view_config(
850 876 route_name='edit_user_ssh_keys', request_method='GET',
851 877 renderer='rhodecode:templates/admin/users/user_edit.mako')
852 878 def ssh_keys(self):
853 879 _ = self.request.translate
854 880 c = self.load_default_context()
855 881 c.user = self.db_user
856 882
857 883 c.active = 'ssh_keys'
858 884 c.default_key = self.request.GET.get('default_key')
859 885 c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
860 886 return self._get_template_context(c)
861 887
862 888 @LoginRequired()
863 889 @HasPermissionAllDecorator('hg.admin')
864 890 @view_config(
865 891 route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
866 892 renderer='rhodecode:templates/admin/users/user_edit.mako')
867 893 def ssh_keys_generate_keypair(self):
868 894 _ = self.request.translate
869 895 c = self.load_default_context()
870 896
871 897 c.user = self.db_user
872 898
873 899 c.active = 'ssh_keys_generate'
874 900 comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
875 901 private_format = self.request.GET.get('private_format') \
876 902 or SshKeyModel.DEFAULT_PRIVATE_KEY_FORMAT
877 903 c.private, c.public = SshKeyModel().generate_keypair(
878 904 comment=comment, private_format=private_format)
879 905
880 906 return self._get_template_context(c)
881 907
882 908 @LoginRequired()
883 909 @HasPermissionAllDecorator('hg.admin')
884 910 @CSRFRequired()
885 911 @view_config(
886 912 route_name='edit_user_ssh_keys_add', request_method='POST')
887 913 def ssh_keys_add(self):
888 914 _ = self.request.translate
889 915 c = self.load_default_context()
890 916
891 917 user_id = self.db_user_id
892 918 c.user = self.db_user
893 919
894 920 user_data = c.user.get_api_data()
895 921 key_data = self.request.POST.get('key_data')
896 922 description = self.request.POST.get('description')
897 923
898 924 fingerprint = 'unknown'
899 925 try:
900 926 if not key_data:
901 927 raise ValueError('Please add a valid public key')
902 928
903 929 key = SshKeyModel().parse_key(key_data.strip())
904 930 fingerprint = key.hash_md5()
905 931
906 932 ssh_key = SshKeyModel().create(
907 933 c.user.user_id, fingerprint, key.keydata, description)
908 934 ssh_key_data = ssh_key.get_api_data()
909 935
910 936 audit_logger.store_web(
911 937 'user.edit.ssh_key.add', action_data={
912 938 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
913 939 user=self._rhodecode_user, )
914 940 Session().commit()
915 941
916 942 # Trigger an event on change of keys.
917 943 trigger(SshKeyFileChangeEvent(), self.request.registry)
918 944
919 945 h.flash(_("Ssh Key successfully created"), category='success')
920 946
921 947 except IntegrityError:
922 948 log.exception("Exception during ssh key saving")
923 949 err = 'Such key with fingerprint `{}` already exists, ' \
924 950 'please use a different one'.format(fingerprint)
925 951 h.flash(_('An error occurred during ssh key saving: {}').format(err),
926 952 category='error')
927 953 except Exception as e:
928 954 log.exception("Exception during ssh key saving")
929 955 h.flash(_('An error occurred during ssh key saving: {}').format(e),
930 956 category='error')
931 957
932 958 return HTTPFound(
933 959 h.route_path('edit_user_ssh_keys', user_id=user_id))
934 960
935 961 @LoginRequired()
936 962 @HasPermissionAllDecorator('hg.admin')
937 963 @CSRFRequired()
938 964 @view_config(
939 965 route_name='edit_user_ssh_keys_delete', request_method='POST')
940 966 def ssh_keys_delete(self):
941 967 _ = self.request.translate
942 968 c = self.load_default_context()
943 969
944 970 user_id = self.db_user_id
945 971 c.user = self.db_user
946 972
947 973 user_data = c.user.get_api_data()
948 974
949 975 del_ssh_key = self.request.POST.get('del_ssh_key')
950 976
951 977 if del_ssh_key:
952 978 ssh_key = UserSshKeys.get_or_404(del_ssh_key)
953 979 ssh_key_data = ssh_key.get_api_data()
954 980
955 981 SshKeyModel().delete(del_ssh_key, c.user.user_id)
956 982 audit_logger.store_web(
957 983 'user.edit.ssh_key.delete', action_data={
958 984 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
959 985 user=self._rhodecode_user,)
960 986 Session().commit()
961 987 # Trigger an event on change of keys.
962 988 trigger(SshKeyFileChangeEvent(), self.request.registry)
963 989 h.flash(_("Ssh key successfully deleted"), category='success')
964 990
965 991 return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
966 992
967 993 @LoginRequired()
968 994 @HasPermissionAllDecorator('hg.admin')
969 995 @view_config(
970 996 route_name='edit_user_emails', request_method='GET',
971 997 renderer='rhodecode:templates/admin/users/user_edit.mako')
972 998 def emails(self):
973 999 _ = self.request.translate
974 1000 c = self.load_default_context()
975 1001 c.user = self.db_user
976 1002
977 1003 c.active = 'emails'
978 1004 c.user_email_map = UserEmailMap.query() \
979 1005 .filter(UserEmailMap.user == c.user).all()
980 1006
981 1007 return self._get_template_context(c)
982 1008
983 1009 @LoginRequired()
984 1010 @HasPermissionAllDecorator('hg.admin')
985 1011 @CSRFRequired()
986 1012 @view_config(
987 1013 route_name='edit_user_emails_add', request_method='POST')
988 1014 def emails_add(self):
989 1015 _ = self.request.translate
990 1016 c = self.load_default_context()
991 1017
992 1018 user_id = self.db_user_id
993 1019 c.user = self.db_user
994 1020
995 1021 email = self.request.POST.get('new_email')
996 1022 user_data = c.user.get_api_data()
997 1023 try:
998 1024
999 1025 form = UserExtraEmailForm(self.request.translate)()
1000 1026 data = form.to_python({'email': email})
1001 1027 email = data['email']
1002 1028
1003 1029 UserModel().add_extra_email(c.user.user_id, email)
1004 1030 audit_logger.store_web(
1005 1031 'user.edit.email.add',
1006 1032 action_data={'email': email, 'user': user_data},
1007 1033 user=self._rhodecode_user)
1008 1034 Session().commit()
1009 1035 h.flash(_("Added new email address `%s` for user account") % email,
1010 1036 category='success')
1011 1037 except formencode.Invalid as error:
1012 1038 h.flash(h.escape(error.error_dict['email']), category='error')
1013 1039 except IntegrityError:
1014 1040 log.warning("Email %s already exists", email)
1015 1041 h.flash(_('Email `{}` is already registered for another user.').format(email),
1016 1042 category='error')
1017 1043 except Exception:
1018 1044 log.exception("Exception during email saving")
1019 1045 h.flash(_('An error occurred during email saving'),
1020 1046 category='error')
1021 1047 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1022 1048
1023 1049 @LoginRequired()
1024 1050 @HasPermissionAllDecorator('hg.admin')
1025 1051 @CSRFRequired()
1026 1052 @view_config(
1027 1053 route_name='edit_user_emails_delete', request_method='POST')
1028 1054 def emails_delete(self):
1029 1055 _ = self.request.translate
1030 1056 c = self.load_default_context()
1031 1057
1032 1058 user_id = self.db_user_id
1033 1059 c.user = self.db_user
1034 1060
1035 1061 email_id = self.request.POST.get('del_email_id')
1036 1062 user_model = UserModel()
1037 1063
1038 1064 email = UserEmailMap.query().get(email_id).email
1039 1065 user_data = c.user.get_api_data()
1040 1066 user_model.delete_extra_email(c.user.user_id, email_id)
1041 1067 audit_logger.store_web(
1042 1068 'user.edit.email.delete',
1043 1069 action_data={'email': email, 'user': user_data},
1044 1070 user=self._rhodecode_user)
1045 1071 Session().commit()
1046 1072 h.flash(_("Removed email address from user account"),
1047 1073 category='success')
1048 1074 raise HTTPFound(h.route_path('edit_user_emails', user_id=user_id))
1049 1075
1050 1076 @LoginRequired()
1051 1077 @HasPermissionAllDecorator('hg.admin')
1052 1078 @view_config(
1053 1079 route_name='edit_user_ips', request_method='GET',
1054 1080 renderer='rhodecode:templates/admin/users/user_edit.mako')
1055 1081 def ips(self):
1056 1082 _ = self.request.translate
1057 1083 c = self.load_default_context()
1058 1084 c.user = self.db_user
1059 1085
1060 1086 c.active = 'ips'
1061 1087 c.user_ip_map = UserIpMap.query() \
1062 1088 .filter(UserIpMap.user == c.user).all()
1063 1089
1064 1090 c.inherit_default_ips = c.user.inherit_default_permissions
1065 1091 c.default_user_ip_map = UserIpMap.query() \
1066 1092 .filter(UserIpMap.user == User.get_default_user()).all()
1067 1093
1068 1094 return self._get_template_context(c)
1069 1095
1070 1096 @LoginRequired()
1071 1097 @HasPermissionAllDecorator('hg.admin')
1072 1098 @CSRFRequired()
1073 1099 @view_config(
1074 1100 route_name='edit_user_ips_add', request_method='POST')
1075 1101 # NOTE(marcink): this view is allowed for default users, as we can
1076 1102 # edit their IP white list
1077 1103 def ips_add(self):
1078 1104 _ = self.request.translate
1079 1105 c = self.load_default_context()
1080 1106
1081 1107 user_id = self.db_user_id
1082 1108 c.user = self.db_user
1083 1109
1084 1110 user_model = UserModel()
1085 1111 desc = self.request.POST.get('description')
1086 1112 try:
1087 1113 ip_list = user_model.parse_ip_range(
1088 1114 self.request.POST.get('new_ip'))
1089 1115 except Exception as e:
1090 1116 ip_list = []
1091 1117 log.exception("Exception during ip saving")
1092 1118 h.flash(_('An error occurred during ip saving:%s' % (e,)),
1093 1119 category='error')
1094 1120 added = []
1095 1121 user_data = c.user.get_api_data()
1096 1122 for ip in ip_list:
1097 1123 try:
1098 1124 form = UserExtraIpForm(self.request.translate)()
1099 1125 data = form.to_python({'ip': ip})
1100 1126 ip = data['ip']
1101 1127
1102 1128 user_model.add_extra_ip(c.user.user_id, ip, desc)
1103 1129 audit_logger.store_web(
1104 1130 'user.edit.ip.add',
1105 1131 action_data={'ip': ip, 'user': user_data},
1106 1132 user=self._rhodecode_user)
1107 1133 Session().commit()
1108 1134 added.append(ip)
1109 1135 except formencode.Invalid as error:
1110 1136 msg = error.error_dict['ip']
1111 1137 h.flash(msg, category='error')
1112 1138 except Exception:
1113 1139 log.exception("Exception during ip saving")
1114 1140 h.flash(_('An error occurred during ip saving'),
1115 1141 category='error')
1116 1142 if added:
1117 1143 h.flash(
1118 1144 _("Added ips %s to user whitelist") % (', '.join(ip_list), ),
1119 1145 category='success')
1120 1146 if 'default_user' in self.request.POST:
1121 1147 # case for editing global IP list we do it for 'DEFAULT' user
1122 1148 raise HTTPFound(h.route_path('admin_permissions_ips'))
1123 1149 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1124 1150
1125 1151 @LoginRequired()
1126 1152 @HasPermissionAllDecorator('hg.admin')
1127 1153 @CSRFRequired()
1128 1154 @view_config(
1129 1155 route_name='edit_user_ips_delete', request_method='POST')
1130 1156 # NOTE(marcink): this view is allowed for default users, as we can
1131 1157 # edit their IP white list
1132 1158 def ips_delete(self):
1133 1159 _ = self.request.translate
1134 1160 c = self.load_default_context()
1135 1161
1136 1162 user_id = self.db_user_id
1137 1163 c.user = self.db_user
1138 1164
1139 1165 ip_id = self.request.POST.get('del_ip_id')
1140 1166 user_model = UserModel()
1141 1167 user_data = c.user.get_api_data()
1142 1168 ip = UserIpMap.query().get(ip_id).ip_addr
1143 1169 user_model.delete_extra_ip(c.user.user_id, ip_id)
1144 1170 audit_logger.store_web(
1145 1171 'user.edit.ip.delete', action_data={'ip': ip, 'user': user_data},
1146 1172 user=self._rhodecode_user)
1147 1173 Session().commit()
1148 1174 h.flash(_("Removed ip address from user whitelist"), category='success')
1149 1175
1150 1176 if 'default_user' in self.request.POST:
1151 1177 # case for editing global IP list we do it for 'DEFAULT' user
1152 1178 raise HTTPFound(h.route_path('admin_permissions_ips'))
1153 1179 raise HTTPFound(h.route_path('edit_user_ips', user_id=user_id))
1154 1180
1155 1181 @LoginRequired()
1156 1182 @HasPermissionAllDecorator('hg.admin')
1157 1183 @view_config(
1158 1184 route_name='edit_user_groups_management', request_method='GET',
1159 1185 renderer='rhodecode:templates/admin/users/user_edit.mako')
1160 1186 def groups_management(self):
1161 1187 c = self.load_default_context()
1162 1188 c.user = self.db_user
1163 1189 c.data = c.user.group_member
1164 1190
1165 1191 groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
1166 1192 for group in c.user.group_member]
1167 1193 c.groups = json.dumps(groups)
1168 1194 c.active = 'groups'
1169 1195
1170 1196 return self._get_template_context(c)
1171 1197
1172 1198 @LoginRequired()
1173 1199 @HasPermissionAllDecorator('hg.admin')
1174 1200 @CSRFRequired()
1175 1201 @view_config(
1176 1202 route_name='edit_user_groups_management_updates', request_method='POST')
1177 1203 def groups_management_updates(self):
1178 1204 _ = self.request.translate
1179 1205 c = self.load_default_context()
1180 1206
1181 1207 user_id = self.db_user_id
1182 1208 c.user = self.db_user
1183 1209
1184 1210 user_groups = set(self.request.POST.getall('users_group_id'))
1185 1211 user_groups_objects = []
1186 1212
1187 1213 for ugid in user_groups:
1188 1214 user_groups_objects.append(
1189 1215 UserGroupModel().get_group(safe_int(ugid)))
1190 1216 user_group_model = UserGroupModel()
1191 1217 added_to_groups, removed_from_groups = \
1192 1218 user_group_model.change_groups(c.user, user_groups_objects)
1193 1219
1194 1220 user_data = c.user.get_api_data()
1195 1221 for user_group_id in added_to_groups:
1196 1222 user_group = UserGroup.get(user_group_id)
1197 1223 old_values = user_group.get_api_data()
1198 1224 audit_logger.store_web(
1199 1225 'user_group.edit.member.add',
1200 1226 action_data={'user': user_data, 'old_data': old_values},
1201 1227 user=self._rhodecode_user)
1202 1228
1203 1229 for user_group_id in removed_from_groups:
1204 1230 user_group = UserGroup.get(user_group_id)
1205 1231 old_values = user_group.get_api_data()
1206 1232 audit_logger.store_web(
1207 1233 'user_group.edit.member.delete',
1208 1234 action_data={'user': user_data, 'old_data': old_values},
1209 1235 user=self._rhodecode_user)
1210 1236
1211 1237 Session().commit()
1212 1238 c.active = 'user_groups_management'
1213 1239 h.flash(_("Groups successfully changed"), category='success')
1214 1240
1215 1241 return HTTPFound(h.route_path(
1216 1242 'edit_user_groups_management', user_id=user_id))
1217 1243
1218 1244 @LoginRequired()
1219 1245 @HasPermissionAllDecorator('hg.admin')
1220 1246 @view_config(
1221 1247 route_name='edit_user_audit_logs', request_method='GET',
1222 1248 renderer='rhodecode:templates/admin/users/user_edit.mako')
1223 1249 def user_audit_logs(self):
1224 1250 _ = self.request.translate
1225 1251 c = self.load_default_context()
1226 1252 c.user = self.db_user
1227 1253
1228 1254 c.active = 'audit'
1229 1255
1230 1256 p = safe_int(self.request.GET.get('page', 1), 1)
1231 1257
1232 1258 filter_term = self.request.GET.get('filter')
1233 1259 user_log = UserModel().get_user_log(c.user, filter_term)
1234 1260
1235 1261 def url_generator(page_num):
1236 1262 query_params = {
1237 1263 'page': page_num
1238 1264 }
1239 1265 if filter_term:
1240 1266 query_params['filter'] = filter_term
1241 1267 return self.request.current_route_path(_query=query_params)
1242 1268
1243 1269 c.audit_logs = SqlPage(
1244 1270 user_log, page=p, items_per_page=10, url_maker=url_generator)
1245 1271 c.filter_term = filter_term
1246 1272 return self._get_template_context(c)
1247 1273
1248 1274 @LoginRequired()
1249 1275 @HasPermissionAllDecorator('hg.admin')
1250 1276 @view_config(
1251 1277 route_name='edit_user_audit_logs_download', request_method='GET',
1252 1278 renderer='string')
1253 1279 def user_audit_logs_download(self):
1254 1280 _ = self.request.translate
1255 1281 c = self.load_default_context()
1256 1282 c.user = self.db_user
1257 1283
1258 1284 user_log = UserModel().get_user_log(c.user, filter_term=None)
1259 1285
1260 1286 audit_log_data = {}
1261 1287 for entry in user_log:
1262 1288 audit_log_data[entry.user_log_id] = entry.get_dict()
1263 1289
1264 1290 response = Response(json.dumps(audit_log_data, indent=4))
1265 1291 response.content_disposition = str(
1266 1292 'attachment; filename=%s' % 'user_{}_audit_logs.json'.format(c.user.user_id))
1267 1293 response.content_type = 'application/json'
1268 1294
1269 1295 return response
1270 1296
1271 1297 @LoginRequired()
1272 1298 @HasPermissionAllDecorator('hg.admin')
1273 1299 @view_config(
1274 1300 route_name='edit_user_perms_summary', request_method='GET',
1275 1301 renderer='rhodecode:templates/admin/users/user_edit.mako')
1276 1302 def user_perms_summary(self):
1277 1303 _ = self.request.translate
1278 1304 c = self.load_default_context()
1279 1305 c.user = self.db_user
1280 1306
1281 1307 c.active = 'perms_summary'
1282 1308 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1283 1309
1284 1310 return self._get_template_context(c)
1285 1311
1286 1312 @LoginRequired()
1287 1313 @HasPermissionAllDecorator('hg.admin')
1288 1314 @view_config(
1289 1315 route_name='edit_user_perms_summary_json', request_method='GET',
1290 1316 renderer='json_ext')
1291 1317 def user_perms_summary_json(self):
1292 1318 self.load_default_context()
1293 1319 perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
1294 1320
1295 1321 return perm_user.permissions
1296 1322
1297 1323 @LoginRequired()
1298 1324 @HasPermissionAllDecorator('hg.admin')
1299 1325 @view_config(
1300 1326 route_name='edit_user_caches', request_method='GET',
1301 1327 renderer='rhodecode:templates/admin/users/user_edit.mako')
1302 1328 def user_caches(self):
1303 1329 _ = self.request.translate
1304 1330 c = self.load_default_context()
1305 1331 c.user = self.db_user
1306 1332
1307 1333 c.active = 'caches'
1308 1334 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1309 1335
1310 1336 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1311 1337 c.region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1312 1338 c.backend = c.region.backend
1313 1339 c.user_keys = sorted(c.region.backend.list_keys(prefix=cache_namespace_uid))
1314 1340
1315 1341 return self._get_template_context(c)
1316 1342
1317 1343 @LoginRequired()
1318 1344 @HasPermissionAllDecorator('hg.admin')
1319 1345 @CSRFRequired()
1320 1346 @view_config(
1321 1347 route_name='edit_user_caches_update', request_method='POST')
1322 1348 def user_caches_update(self):
1323 1349 _ = self.request.translate
1324 1350 c = self.load_default_context()
1325 1351 c.user = self.db_user
1326 1352
1327 1353 c.active = 'caches'
1328 1354 c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
1329 1355
1330 1356 cache_namespace_uid = 'cache_user_auth.{}'.format(self.db_user.user_id)
1331 1357 del_keys = rc_cache.clear_cache_namespace('cache_perms', cache_namespace_uid)
1332 1358
1333 1359 h.flash(_("Deleted {} cache keys").format(del_keys), category='success')
1334 1360
1335 1361 return HTTPFound(h.route_path(
1336 1362 'edit_user_caches', user_id=c.user.user_id))
@@ -1,2413 +1,2446 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 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
29 31 import hashlib
30 32 import itertools
31 33 import logging
32 34 import random
33 35 import traceback
34 36 from functools import wraps
35 37
36 38 import ipaddress
37 39
38 40 from pyramid.httpexceptions import HTTPForbidden, HTTPFound, HTTPNotFound
39 41 from sqlalchemy.orm.exc import ObjectDeletedError
40 42 from sqlalchemy.orm import joinedload
41 43 from zope.cachedescriptors.property import Lazy as LazyProperty
42 44
43 45 import rhodecode
44 46 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
60 61 log = logging.getLogger(__name__)
61 62
62 63 csrf_token_key = "csrf_token"
63 64
64 65
65 66 class PasswordGenerator(object):
66 67 """
67 68 This is a simple class for generating password from different sets of
68 69 characters
69 70 usage::
70 71 passwd_gen = PasswordGenerator()
71 72 #print 8-letter password containing only big and small letters
72 73 of alphabet
73 74 passwd_gen.gen_password(8, passwd_gen.ALPHABETS_BIG_SMALL)
74 75 """
75 76 ALPHABETS_NUM = r'''1234567890'''
76 77 ALPHABETS_SMALL = r'''qwertyuiopasdfghjklzxcvbnm'''
77 78 ALPHABETS_BIG = r'''QWERTYUIOPASDFGHJKLZXCVBNM'''
78 79 ALPHABETS_SPECIAL = r'''`-=[]\;',./~!@#$%^&*()_+{}|:"<>?'''
79 80 ALPHABETS_FULL = ALPHABETS_BIG + ALPHABETS_SMALL \
80 81 + ALPHABETS_NUM + ALPHABETS_SPECIAL
81 82 ALPHABETS_ALPHANUM = ALPHABETS_BIG + ALPHABETS_SMALL + ALPHABETS_NUM
82 83 ALPHABETS_BIG_SMALL = ALPHABETS_BIG + ALPHABETS_SMALL
83 84 ALPHABETS_ALPHANUM_BIG = ALPHABETS_BIG + ALPHABETS_NUM
84 85 ALPHABETS_ALPHANUM_SMALL = ALPHABETS_SMALL + ALPHABETS_NUM
85 86
86 87 def __init__(self, passwd=''):
87 88 self.passwd = passwd
88 89
89 90 def gen_password(self, length, type_=None):
90 91 if type_ is None:
91 92 type_ = self.ALPHABETS_FULL
92 93 self.passwd = ''.join([random.choice(type_) for _ in range(length)])
93 94 return self.passwd
94 95
95 96
96 97 class _RhodeCodeCryptoBase(object):
97 98 ENC_PREF = None
98 99
99 100 def hash_create(self, str_):
100 101 """
101 102 hash the string using
102 103
103 104 :param str_: password to hash
104 105 """
105 106 raise NotImplementedError
106 107
107 108 def hash_check_with_upgrade(self, password, hashed):
108 109 """
109 110 Returns tuple in which first element is boolean that states that
110 111 given password matches it's hashed version, and the second is new hash
111 112 of the password, in case this password should be migrated to new
112 113 cipher.
113 114 """
114 115 checked_hash = self.hash_check(password, hashed)
115 116 return checked_hash, None
116 117
117 118 def hash_check(self, password, hashed):
118 119 """
119 120 Checks matching password with it's hashed value.
120 121
121 122 :param password: password
122 123 :param hashed: password in hashed form
123 124 """
124 125 raise NotImplementedError
125 126
126 127 def _assert_bytes(self, value):
127 128 """
128 129 Passing in an `unicode` object can lead to hard to detect issues
129 130 if passwords contain non-ascii characters. Doing a type check
130 131 during runtime, so that such mistakes are detected early on.
131 132 """
132 133 if not isinstance(value, str):
133 134 raise TypeError(
134 135 "Bytestring required as input, got %r." % (value, ))
135 136
136 137
137 138 class _RhodeCodeCryptoBCrypt(_RhodeCodeCryptoBase):
138 139 ENC_PREF = ('$2a$10', '$2b$10')
139 140
140 141 def hash_create(self, str_):
141 142 self._assert_bytes(str_)
142 143 return bcrypt.hashpw(str_, bcrypt.gensalt(10))
143 144
144 145 def hash_check_with_upgrade(self, password, hashed):
145 146 """
146 147 Returns tuple in which first element is boolean that states that
147 148 given password matches it's hashed version, and the second is new hash
148 149 of the password, in case this password should be migrated to new
149 150 cipher.
150 151
151 152 This implements special upgrade logic which works like that:
152 153 - check if the given password == bcrypted hash, if yes then we
153 154 properly used password and it was already in bcrypt. Proceed
154 155 without any changes
155 156 - if bcrypt hash check is not working try with sha256. If hash compare
156 157 is ok, it means we using correct but old hashed password. indicate
157 158 hash change and proceed
158 159 """
159 160
160 161 new_hash = None
161 162
162 163 # regular pw check
163 164 password_match_bcrypt = self.hash_check(password, hashed)
164 165
165 166 # now we want to know if the password was maybe from sha256
166 167 # basically calling _RhodeCodeCryptoSha256().hash_check()
167 168 if not password_match_bcrypt:
168 169 if _RhodeCodeCryptoSha256().hash_check(password, hashed):
169 170 new_hash = self.hash_create(password) # make new bcrypt hash
170 171 password_match_bcrypt = True
171 172
172 173 return password_match_bcrypt, new_hash
173 174
174 175 def hash_check(self, password, hashed):
175 176 """
176 177 Checks matching password with it's hashed value.
177 178
178 179 :param password: password
179 180 :param hashed: password in hashed form
180 181 """
181 182 self._assert_bytes(password)
182 183 try:
183 184 return bcrypt.hashpw(password, hashed) == hashed
184 185 except ValueError as e:
185 186 # we're having a invalid salt here probably, we should not crash
186 187 # just return with False as it would be a wrong password.
187 188 log.debug('Failed to check password hash using bcrypt %s',
188 189 safe_str(e))
189 190
190 191 return False
191 192
192 193
193 194 class _RhodeCodeCryptoSha256(_RhodeCodeCryptoBase):
194 195 ENC_PREF = '_'
195 196
196 197 def hash_create(self, str_):
197 198 self._assert_bytes(str_)
198 199 return hashlib.sha256(str_).hexdigest()
199 200
200 201 def hash_check(self, password, hashed):
201 202 """
202 203 Checks matching password with it's hashed value.
203 204
204 205 :param password: password
205 206 :param hashed: password in hashed form
206 207 """
207 208 self._assert_bytes(password)
208 209 return hashlib.sha256(password).hexdigest() == hashed
209 210
210 211
211 212 class _RhodeCodeCryptoTest(_RhodeCodeCryptoBase):
212 213 ENC_PREF = '_'
213 214
214 215 def hash_create(self, str_):
215 216 self._assert_bytes(str_)
216 217 return sha1(str_)
217 218
218 219 def hash_check(self, password, hashed):
219 220 """
220 221 Checks matching password with it's hashed value.
221 222
222 223 :param password: password
223 224 :param hashed: password in hashed form
224 225 """
225 226 self._assert_bytes(password)
226 227 return sha1(password) == hashed
227 228
228 229
229 230 def crypto_backend():
230 231 """
231 232 Return the matching crypto backend.
232 233
233 234 Selection is based on if we run tests or not, we pick sha1-test backend to run
234 235 tests faster since BCRYPT is expensive to calculate
235 236 """
236 237 if rhodecode.is_test:
237 238 RhodeCodeCrypto = _RhodeCodeCryptoTest()
238 239 else:
239 240 RhodeCodeCrypto = _RhodeCodeCryptoBCrypt()
240 241
241 242 return RhodeCodeCrypto
242 243
243 244
244 245 def get_crypt_password(password):
245 246 """
246 247 Create the hash of `password` with the active crypto backend.
247 248
248 249 :param password: The cleartext password.
249 250 :type password: unicode
250 251 """
251 252 password = safe_str(password)
252 253 return crypto_backend().hash_create(password)
253 254
254 255
255 256 def check_password(password, hashed):
256 257 """
257 258 Check if the value in `password` matches the hash in `hashed`.
258 259
259 260 :param password: The cleartext password.
260 261 :type password: unicode
261 262
262 263 :param hashed: The expected hashed version of the password.
263 264 :type hashed: The hash has to be passed in in text representation.
264 265 """
265 266 password = safe_str(password)
266 267 return crypto_backend().hash_check(password, hashed)
267 268
268 269
269 270 def generate_auth_token(data, salt=None):
270 271 """
271 272 Generates API KEY from given string
272 273 """
273 274
274 275 if salt is None:
275 276 salt = os.urandom(16)
276 277 return hashlib.sha1(safe_str(data) + salt).hexdigest()
277 278
278 279
279 280 def get_came_from(request):
280 281 """
281 282 get query_string+path from request sanitized after removing auth_token
282 283 """
283 284 _req = request
284 285
285 286 path = _req.path
286 287 if 'auth_token' in _req.GET:
287 288 # sanitize the request and remove auth_token for redirection
288 289 _req.GET.pop('auth_token')
289 290 qs = _req.query_string
290 291 if qs:
291 292 path += '?' + qs
292 293
293 294 return path
294 295
295 296
296 297 class CookieStoreWrapper(object):
297 298
298 299 def __init__(self, cookie_store):
299 300 self.cookie_store = cookie_store
300 301
301 302 def __repr__(self):
302 303 return 'CookieStore<%s>' % (self.cookie_store)
303 304
304 305 def get(self, key, other=None):
305 306 if isinstance(self.cookie_store, dict):
306 307 return self.cookie_store.get(key, other)
307 308 elif isinstance(self.cookie_store, AuthUser):
308 309 return self.cookie_store.__dict__.get(key, other)
309 310
310 311
311 312 def _cached_perms_data(user_id, scope, user_is_admin,
312 313 user_inherit_default_permissions, explicit, algo,
313 314 calculate_super_admin):
314 315
315 316 permissions = PermissionCalculator(
316 317 user_id, scope, user_is_admin, user_inherit_default_permissions,
317 318 explicit, algo, calculate_super_admin)
318 319 return permissions.calculate()
319 320
320 321
321 322 class PermOrigin(object):
322 323 SUPER_ADMIN = 'superadmin'
323 324 ARCHIVED = 'archived'
324 325
325 326 REPO_USER = 'user:%s'
326 327 REPO_USERGROUP = 'usergroup:%s'
327 328 REPO_OWNER = 'repo.owner'
328 329 REPO_DEFAULT = 'repo.default'
329 330 REPO_DEFAULT_NO_INHERIT = 'repo.default.no.inherit'
330 331 REPO_PRIVATE = 'repo.private'
331 332
332 333 REPOGROUP_USER = 'user:%s'
333 334 REPOGROUP_USERGROUP = 'usergroup:%s'
334 335 REPOGROUP_OWNER = 'group.owner'
335 336 REPOGROUP_DEFAULT = 'group.default'
336 337 REPOGROUP_DEFAULT_NO_INHERIT = 'group.default.no.inherit'
337 338
338 339 USERGROUP_USER = 'user:%s'
339 340 USERGROUP_USERGROUP = 'usergroup:%s'
340 341 USERGROUP_OWNER = 'usergroup.owner'
341 342 USERGROUP_DEFAULT = 'usergroup.default'
342 343 USERGROUP_DEFAULT_NO_INHERIT = 'usergroup.default.no.inherit'
343 344
344 345
345 346 class PermOriginDict(dict):
346 347 """
347 348 A special dict used for tracking permissions along with their origins.
348 349
349 350 `__setitem__` has been overridden to expect a tuple(perm, origin)
350 351 `__getitem__` will return only the perm
351 352 `.perm_origin_stack` will return the stack of (perm, origin) set per key
352 353
353 354 >>> perms = PermOriginDict()
354 355 >>> perms['resource'] = 'read', 'default', 1
355 356 >>> perms['resource']
356 357 'read'
357 358 >>> perms['resource'] = 'write', 'admin', 2
358 359 >>> perms['resource']
359 360 'write'
360 361 >>> perms.perm_origin_stack
361 362 {'resource': [('read', 'default', 1), ('write', 'admin', 2)]}
362 363 """
363 364
364 365 def __init__(self, *args, **kw):
365 366 dict.__init__(self, *args, **kw)
366 367 self.perm_origin_stack = collections.OrderedDict()
367 368
368 369 def __setitem__(self, key, (perm, origin, obj_id)):
369 370 self.perm_origin_stack.setdefault(key, []).append(
370 371 (perm, origin, obj_id))
371 372 dict.__setitem__(self, key, perm)
372 373
373 374
374 375 class BranchPermOriginDict(PermOriginDict):
375 376 """
376 377 Dedicated branch permissions dict, with tracking of patterns and origins.
377 378
378 379 >>> perms = BranchPermOriginDict()
379 380 >>> perms['resource'] = '*pattern', 'read', 'default'
380 381 >>> perms['resource']
381 382 {'*pattern': 'read'}
382 383 >>> perms['resource'] = '*pattern', 'write', 'admin'
383 384 >>> perms['resource']
384 385 {'*pattern': 'write'}
385 386 >>> perms.perm_origin_stack
386 387 {'resource': {'*pattern': [('read', 'default'), ('write', 'admin')]}}
387 388 """
388 389 def __setitem__(self, key, (pattern, perm, origin)):
389 390
390 391 self.perm_origin_stack.setdefault(key, {}) \
391 392 .setdefault(pattern, []).append((perm, origin))
392 393
393 394 if key in self:
394 395 self[key].__setitem__(pattern, perm)
395 396 else:
396 397 patterns = collections.OrderedDict()
397 398 patterns[pattern] = perm
398 399 dict.__setitem__(self, key, patterns)
399 400
400 401
401 402 class PermissionCalculator(object):
402 403
403 404 def __init__(
404 405 self, user_id, scope, user_is_admin,
405 406 user_inherit_default_permissions, explicit, algo,
406 407 calculate_super_admin_as_user=False):
407 408
408 409 self.user_id = user_id
409 410 self.user_is_admin = user_is_admin
410 411 self.inherit_default_permissions = user_inherit_default_permissions
411 412 self.explicit = explicit
412 413 self.algo = algo
413 414 self.calculate_super_admin_as_user = calculate_super_admin_as_user
414 415
415 416 scope = scope or {}
416 417 self.scope_repo_id = scope.get('repo_id')
417 418 self.scope_repo_group_id = scope.get('repo_group_id')
418 419 self.scope_user_group_id = scope.get('user_group_id')
419 420
420 421 self.default_user_id = User.get_default_user(cache=True).user_id
421 422
422 423 self.permissions_repositories = PermOriginDict()
423 424 self.permissions_repository_groups = PermOriginDict()
424 425 self.permissions_user_groups = PermOriginDict()
425 426 self.permissions_repository_branches = BranchPermOriginDict()
426 427 self.permissions_global = set()
427 428
428 429 self.default_repo_perms = Permission.get_default_repo_perms(
429 430 self.default_user_id, self.scope_repo_id)
430 431 self.default_repo_groups_perms = Permission.get_default_group_perms(
431 432 self.default_user_id, self.scope_repo_group_id)
432 433 self.default_user_group_perms = \
433 434 Permission.get_default_user_group_perms(
434 435 self.default_user_id, self.scope_user_group_id)
435 436
436 437 # default branch perms
437 438 self.default_branch_repo_perms = \
438 439 Permission.get_default_repo_branch_perms(
439 440 self.default_user_id, self.scope_repo_id)
440 441
441 442 def calculate(self):
442 443 if self.user_is_admin and not self.calculate_super_admin_as_user:
443 444 return self._calculate_admin_permissions()
444 445
445 446 self._calculate_global_default_permissions()
446 447 self._calculate_global_permissions()
447 448 self._calculate_default_permissions()
448 449 self._calculate_repository_permissions()
449 450 self._calculate_repository_branch_permissions()
450 451 self._calculate_repository_group_permissions()
451 452 self._calculate_user_group_permissions()
452 453 return self._permission_structure()
453 454
454 455 def _calculate_admin_permissions(self):
455 456 """
456 457 admin user have all default rights for repositories
457 458 and groups set to admin
458 459 """
459 460 self.permissions_global.add('hg.admin')
460 461 self.permissions_global.add('hg.create.write_on_repogroup.true')
461 462
462 463 # repositories
463 464 for perm in self.default_repo_perms:
464 465 r_k = perm.UserRepoToPerm.repository.repo_name
465 466 obj_id = perm.UserRepoToPerm.repository.repo_id
466 467 archived = perm.UserRepoToPerm.repository.archived
467 468 p = 'repository.admin'
468 469 self.permissions_repositories[r_k] = p, PermOrigin.SUPER_ADMIN, obj_id
469 470 # special case for archived repositories, which we block still even for
470 471 # super admins
471 472 if archived:
472 473 p = 'repository.read'
473 474 self.permissions_repositories[r_k] = p, PermOrigin.ARCHIVED, obj_id
474 475
475 476 # repository groups
476 477 for perm in self.default_repo_groups_perms:
477 478 rg_k = perm.UserRepoGroupToPerm.group.group_name
478 479 obj_id = perm.UserRepoGroupToPerm.group.group_id
479 480 p = 'group.admin'
480 481 self.permissions_repository_groups[rg_k] = p, PermOrigin.SUPER_ADMIN, obj_id
481 482
482 483 # user groups
483 484 for perm in self.default_user_group_perms:
484 485 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
485 486 obj_id = perm.UserUserGroupToPerm.user_group.users_group_id
486 487 p = 'usergroup.admin'
487 488 self.permissions_user_groups[u_k] = p, PermOrigin.SUPER_ADMIN, obj_id
488 489
489 490 # branch permissions
490 491 # since super-admin also can have custom rule permissions
491 492 # we *always* need to calculate those inherited from default, and also explicit
492 493 self._calculate_default_permissions_repository_branches(
493 494 user_inherit_object_permissions=False)
494 495 self._calculate_repository_branch_permissions()
495 496
496 497 return self._permission_structure()
497 498
498 499 def _calculate_global_default_permissions(self):
499 500 """
500 501 global permissions taken from the default user
501 502 """
502 503 default_global_perms = UserToPerm.query()\
503 504 .filter(UserToPerm.user_id == self.default_user_id)\
504 505 .options(joinedload(UserToPerm.permission))
505 506
506 507 for perm in default_global_perms:
507 508 self.permissions_global.add(perm.permission.permission_name)
508 509
509 510 if self.user_is_admin:
510 511 self.permissions_global.add('hg.admin')
511 512 self.permissions_global.add('hg.create.write_on_repogroup.true')
512 513
513 514 def _calculate_global_permissions(self):
514 515 """
515 516 Set global system permissions with user permissions or permissions
516 517 taken from the user groups of the current user.
517 518
518 519 The permissions include repo creating, repo group creating, forking
519 520 etc.
520 521 """
521 522
522 523 # now we read the defined permissions and overwrite what we have set
523 524 # before those can be configured from groups or users explicitly.
524 525
525 526 # In case we want to extend this list we should make sure
526 527 # this is in sync with User.DEFAULT_USER_PERMISSIONS definitions
527 528 _configurable = frozenset([
528 529 'hg.fork.none', 'hg.fork.repository',
529 530 'hg.create.none', 'hg.create.repository',
530 531 'hg.usergroup.create.false', 'hg.usergroup.create.true',
531 532 'hg.repogroup.create.false', 'hg.repogroup.create.true',
532 533 'hg.create.write_on_repogroup.false', 'hg.create.write_on_repogroup.true',
533 534 'hg.inherit_default_perms.false', 'hg.inherit_default_perms.true'
534 535 ])
535 536
536 537 # USER GROUPS comes first user group global permissions
537 538 user_perms_from_users_groups = Session().query(UserGroupToPerm)\
538 539 .options(joinedload(UserGroupToPerm.permission))\
539 540 .join((UserGroupMember, UserGroupToPerm.users_group_id ==
540 541 UserGroupMember.users_group_id))\
541 542 .filter(UserGroupMember.user_id == self.user_id)\
542 543 .order_by(UserGroupToPerm.users_group_id)\
543 544 .all()
544 545
545 546 # need to group here by groups since user can be in more than
546 547 # one group, so we get all groups
547 548 _explicit_grouped_perms = [
548 549 [x, list(y)] for x, y in
549 550 itertools.groupby(user_perms_from_users_groups,
550 551 lambda _x: _x.users_group)]
551 552
552 553 for gr, perms in _explicit_grouped_perms:
553 554 # since user can be in multiple groups iterate over them and
554 555 # select the lowest permissions first (more explicit)
555 556 # TODO(marcink): do this^^
556 557
557 558 # group doesn't inherit default permissions so we actually set them
558 559 if not gr.inherit_default_permissions:
559 560 # NEED TO IGNORE all previously set configurable permissions
560 561 # and replace them with explicitly set from this user
561 562 # group permissions
562 563 self.permissions_global = self.permissions_global.difference(
563 564 _configurable)
564 565 for perm in perms:
565 566 self.permissions_global.add(perm.permission.permission_name)
566 567
567 568 # user explicit global permissions
568 569 user_perms = Session().query(UserToPerm)\
569 570 .options(joinedload(UserToPerm.permission))\
570 571 .filter(UserToPerm.user_id == self.user_id).all()
571 572
572 573 if not self.inherit_default_permissions:
573 574 # NEED TO IGNORE all configurable permissions and
574 575 # replace them with explicitly set from this user permissions
575 576 self.permissions_global = self.permissions_global.difference(
576 577 _configurable)
577 578 for perm in user_perms:
578 579 self.permissions_global.add(perm.permission.permission_name)
579 580
580 581 def _calculate_default_permissions_repositories(self, user_inherit_object_permissions):
581 582 for perm in self.default_repo_perms:
582 583 r_k = perm.UserRepoToPerm.repository.repo_name
583 584 obj_id = perm.UserRepoToPerm.repository.repo_id
584 585 archived = perm.UserRepoToPerm.repository.archived
585 586 p = perm.Permission.permission_name
586 587 o = PermOrigin.REPO_DEFAULT
587 588 self.permissions_repositories[r_k] = p, o, obj_id
588 589
589 590 # if we decide this user isn't inheriting permissions from
590 591 # default user we set him to .none so only explicit
591 592 # permissions work
592 593 if not user_inherit_object_permissions:
593 594 p = 'repository.none'
594 595 o = PermOrigin.REPO_DEFAULT_NO_INHERIT
595 596 self.permissions_repositories[r_k] = p, o, obj_id
596 597
597 598 if perm.Repository.private and not (
598 599 perm.Repository.user_id == self.user_id):
599 600 # disable defaults for private repos,
600 601 p = 'repository.none'
601 602 o = PermOrigin.REPO_PRIVATE
602 603 self.permissions_repositories[r_k] = p, o, obj_id
603 604
604 605 elif perm.Repository.user_id == self.user_id:
605 606 # set admin if owner
606 607 p = 'repository.admin'
607 608 o = PermOrigin.REPO_OWNER
608 609 self.permissions_repositories[r_k] = p, o, obj_id
609 610
610 611 if self.user_is_admin:
611 612 p = 'repository.admin'
612 613 o = PermOrigin.SUPER_ADMIN
613 614 self.permissions_repositories[r_k] = p, o, obj_id
614 615
615 616 # finally in case of archived repositories, we downgrade higher
616 617 # permissions to read
617 618 if archived:
618 619 current_perm = self.permissions_repositories[r_k]
619 620 if current_perm in ['repository.write', 'repository.admin']:
620 621 p = 'repository.read'
621 622 o = PermOrigin.ARCHIVED
622 623 self.permissions_repositories[r_k] = p, o, obj_id
623 624
624 625 def _calculate_default_permissions_repository_branches(self, user_inherit_object_permissions):
625 626 for perm in self.default_branch_repo_perms:
626 627
627 628 r_k = perm.UserRepoToPerm.repository.repo_name
628 629 p = perm.Permission.permission_name
629 630 pattern = perm.UserToRepoBranchPermission.branch_pattern
630 631 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
631 632
632 633 if not self.explicit:
633 634 cur_perm = self.permissions_repository_branches.get(r_k)
634 635 if cur_perm:
635 636 cur_perm = cur_perm[pattern]
636 637 cur_perm = cur_perm or 'branch.none'
637 638
638 639 p = self._choose_permission(p, cur_perm)
639 640
640 641 # NOTE(marcink): register all pattern/perm instances in this
641 642 # special dict that aggregates entries
642 643 self.permissions_repository_branches[r_k] = pattern, p, o
643 644
644 645 def _calculate_default_permissions_repository_groups(self, user_inherit_object_permissions):
645 646 for perm in self.default_repo_groups_perms:
646 647 rg_k = perm.UserRepoGroupToPerm.group.group_name
647 648 obj_id = perm.UserRepoGroupToPerm.group.group_id
648 649 p = perm.Permission.permission_name
649 650 o = PermOrigin.REPOGROUP_DEFAULT
650 651 self.permissions_repository_groups[rg_k] = p, o, obj_id
651 652
652 653 # if we decide this user isn't inheriting permissions from default
653 654 # user we set him to .none so only explicit permissions work
654 655 if not user_inherit_object_permissions:
655 656 p = 'group.none'
656 657 o = PermOrigin.REPOGROUP_DEFAULT_NO_INHERIT
657 658 self.permissions_repository_groups[rg_k] = p, o, obj_id
658 659
659 660 if perm.RepoGroup.user_id == self.user_id:
660 661 # set admin if owner
661 662 p = 'group.admin'
662 663 o = PermOrigin.REPOGROUP_OWNER
663 664 self.permissions_repository_groups[rg_k] = p, o, obj_id
664 665
665 666 if self.user_is_admin:
666 667 p = 'group.admin'
667 668 o = PermOrigin.SUPER_ADMIN
668 669 self.permissions_repository_groups[rg_k] = p, o, obj_id
669 670
670 671 def _calculate_default_permissions_user_groups(self, user_inherit_object_permissions):
671 672 for perm in self.default_user_group_perms:
672 673 u_k = perm.UserUserGroupToPerm.user_group.users_group_name
673 674 obj_id = perm.UserUserGroupToPerm.user_group.users_group_id
674 675 p = perm.Permission.permission_name
675 676 o = PermOrigin.USERGROUP_DEFAULT
676 677 self.permissions_user_groups[u_k] = p, o, obj_id
677 678
678 679 # if we decide this user isn't inheriting permissions from default
679 680 # user we set him to .none so only explicit permissions work
680 681 if not user_inherit_object_permissions:
681 682 p = 'usergroup.none'
682 683 o = PermOrigin.USERGROUP_DEFAULT_NO_INHERIT
683 684 self.permissions_user_groups[u_k] = p, o, obj_id
684 685
685 686 if perm.UserGroup.user_id == self.user_id:
686 687 # set admin if owner
687 688 p = 'usergroup.admin'
688 689 o = PermOrigin.USERGROUP_OWNER
689 690 self.permissions_user_groups[u_k] = p, o, obj_id
690 691
691 692 if self.user_is_admin:
692 693 p = 'usergroup.admin'
693 694 o = PermOrigin.SUPER_ADMIN
694 695 self.permissions_user_groups[u_k] = p, o, obj_id
695 696
696 697 def _calculate_default_permissions(self):
697 698 """
698 699 Set default user permissions for repositories, repository branches,
699 700 repository groups, user groups taken from the default user.
700 701
701 702 Calculate inheritance of object permissions based on what we have now
702 703 in GLOBAL permissions. We check if .false is in GLOBAL since this is
703 704 explicitly set. Inherit is the opposite of .false being there.
704 705
705 706 .. note::
706 707
707 708 the syntax is little bit odd but what we need to check here is
708 709 the opposite of .false permission being in the list so even for
709 710 inconsistent state when both .true/.false is there
710 711 .false is more important
711 712
712 713 """
713 714 user_inherit_object_permissions = not ('hg.inherit_default_perms.false'
714 715 in self.permissions_global)
715 716
716 717 # default permissions inherited from `default` user permissions
717 718 self._calculate_default_permissions_repositories(
718 719 user_inherit_object_permissions)
719 720
720 721 self._calculate_default_permissions_repository_branches(
721 722 user_inherit_object_permissions)
722 723
723 724 self._calculate_default_permissions_repository_groups(
724 725 user_inherit_object_permissions)
725 726
726 727 self._calculate_default_permissions_user_groups(
727 728 user_inherit_object_permissions)
728 729
729 730 def _calculate_repository_permissions(self):
730 731 """
731 732 Repository access permissions for the current user.
732 733
733 734 Check if the user is part of user groups for this repository and
734 735 fill in the permission from it. `_choose_permission` decides of which
735 736 permission should be selected based on selected method.
736 737 """
737 738
738 739 # user group for repositories permissions
739 740 user_repo_perms_from_user_group = Permission\
740 741 .get_default_repo_perms_from_user_group(
741 742 self.user_id, self.scope_repo_id)
742 743
743 744 multiple_counter = collections.defaultdict(int)
744 745 for perm in user_repo_perms_from_user_group:
745 746 r_k = perm.UserGroupRepoToPerm.repository.repo_name
746 747 obj_id = perm.UserGroupRepoToPerm.repository.repo_id
747 748 multiple_counter[r_k] += 1
748 749 p = perm.Permission.permission_name
749 750 o = PermOrigin.REPO_USERGROUP % perm.UserGroupRepoToPerm\
750 751 .users_group.users_group_name
751 752
752 753 if multiple_counter[r_k] > 1:
753 754 cur_perm = self.permissions_repositories[r_k]
754 755 p = self._choose_permission(p, cur_perm)
755 756
756 757 self.permissions_repositories[r_k] = p, o, obj_id
757 758
758 759 if perm.Repository.user_id == self.user_id:
759 760 # set admin if owner
760 761 p = 'repository.admin'
761 762 o = PermOrigin.REPO_OWNER
762 763 self.permissions_repositories[r_k] = p, o, obj_id
763 764
764 765 if self.user_is_admin:
765 766 p = 'repository.admin'
766 767 o = PermOrigin.SUPER_ADMIN
767 768 self.permissions_repositories[r_k] = p, o, obj_id
768 769
769 770 # user explicit permissions for repositories, overrides any specified
770 771 # by the group permission
771 772 user_repo_perms = Permission.get_default_repo_perms(
772 773 self.user_id, self.scope_repo_id)
773 774 for perm in user_repo_perms:
774 775 r_k = perm.UserRepoToPerm.repository.repo_name
775 776 obj_id = perm.UserRepoToPerm.repository.repo_id
776 777 p = perm.Permission.permission_name
777 778 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
778 779
779 780 if not self.explicit:
780 781 cur_perm = self.permissions_repositories.get(
781 782 r_k, 'repository.none')
782 783 p = self._choose_permission(p, cur_perm)
783 784
784 785 self.permissions_repositories[r_k] = p, o, obj_id
785 786
786 787 if perm.Repository.user_id == self.user_id:
787 788 # set admin if owner
788 789 p = 'repository.admin'
789 790 o = PermOrigin.REPO_OWNER
790 791 self.permissions_repositories[r_k] = p, o, obj_id
791 792
792 793 if self.user_is_admin:
793 794 p = 'repository.admin'
794 795 o = PermOrigin.SUPER_ADMIN
795 796 self.permissions_repositories[r_k] = p, o, obj_id
796 797
797 798 def _calculate_repository_branch_permissions(self):
798 799 # user group for repositories permissions
799 800 user_repo_branch_perms_from_user_group = Permission\
800 801 .get_default_repo_branch_perms_from_user_group(
801 802 self.user_id, self.scope_repo_id)
802 803
803 804 multiple_counter = collections.defaultdict(int)
804 805 for perm in user_repo_branch_perms_from_user_group:
805 806 r_k = perm.UserGroupRepoToPerm.repository.repo_name
806 807 p = perm.Permission.permission_name
807 808 pattern = perm.UserGroupToRepoBranchPermission.branch_pattern
808 809 o = PermOrigin.REPO_USERGROUP % perm.UserGroupRepoToPerm\
809 810 .users_group.users_group_name
810 811
811 812 multiple_counter[r_k] += 1
812 813 if multiple_counter[r_k] > 1:
813 814 cur_perm = self.permissions_repository_branches[r_k][pattern]
814 815 p = self._choose_permission(p, cur_perm)
815 816
816 817 self.permissions_repository_branches[r_k] = pattern, p, o
817 818
818 819 # user explicit branch permissions for repositories, overrides
819 820 # any specified by the group permission
820 821 user_repo_branch_perms = Permission.get_default_repo_branch_perms(
821 822 self.user_id, self.scope_repo_id)
822 823
823 824 for perm in user_repo_branch_perms:
824 825
825 826 r_k = perm.UserRepoToPerm.repository.repo_name
826 827 p = perm.Permission.permission_name
827 828 pattern = perm.UserToRepoBranchPermission.branch_pattern
828 829 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
829 830
830 831 if not self.explicit:
831 832 cur_perm = self.permissions_repository_branches.get(r_k)
832 833 if cur_perm:
833 834 cur_perm = cur_perm[pattern]
834 835 cur_perm = cur_perm or 'branch.none'
835 836 p = self._choose_permission(p, cur_perm)
836 837
837 838 # NOTE(marcink): register all pattern/perm instances in this
838 839 # special dict that aggregates entries
839 840 self.permissions_repository_branches[r_k] = pattern, p, o
840 841
841 842 def _calculate_repository_group_permissions(self):
842 843 """
843 844 Repository group permissions for the current user.
844 845
845 846 Check if the user is part of user groups for repository groups and
846 847 fill in the permissions from it. `_choose_permission` decides of which
847 848 permission should be selected based on selected method.
848 849 """
849 850 # user group for repo groups permissions
850 851 user_repo_group_perms_from_user_group = Permission\
851 852 .get_default_group_perms_from_user_group(
852 853 self.user_id, self.scope_repo_group_id)
853 854
854 855 multiple_counter = collections.defaultdict(int)
855 856 for perm in user_repo_group_perms_from_user_group:
856 857 rg_k = perm.UserGroupRepoGroupToPerm.group.group_name
857 858 obj_id = perm.UserGroupRepoGroupToPerm.group.group_id
858 859 multiple_counter[rg_k] += 1
859 860 o = PermOrigin.REPOGROUP_USERGROUP % perm.UserGroupRepoGroupToPerm\
860 861 .users_group.users_group_name
861 862 p = perm.Permission.permission_name
862 863
863 864 if multiple_counter[rg_k] > 1:
864 865 cur_perm = self.permissions_repository_groups[rg_k]
865 866 p = self._choose_permission(p, cur_perm)
866 867 self.permissions_repository_groups[rg_k] = p, o, obj_id
867 868
868 869 if perm.RepoGroup.user_id == self.user_id:
869 870 # set admin if owner, even for member of other user group
870 871 p = 'group.admin'
871 872 o = PermOrigin.REPOGROUP_OWNER
872 873 self.permissions_repository_groups[rg_k] = p, o, obj_id
873 874
874 875 if self.user_is_admin:
875 876 p = 'group.admin'
876 877 o = PermOrigin.SUPER_ADMIN
877 878 self.permissions_repository_groups[rg_k] = p, o, obj_id
878 879
879 880 # user explicit permissions for repository groups
880 881 user_repo_groups_perms = Permission.get_default_group_perms(
881 882 self.user_id, self.scope_repo_group_id)
882 883 for perm in user_repo_groups_perms:
883 884 rg_k = perm.UserRepoGroupToPerm.group.group_name
884 885 obj_id = perm.UserRepoGroupToPerm.group.group_id
885 886 o = PermOrigin.REPOGROUP_USER % perm.UserRepoGroupToPerm\
886 887 .user.username
887 888 p = perm.Permission.permission_name
888 889
889 890 if not self.explicit:
890 891 cur_perm = self.permissions_repository_groups.get(rg_k, 'group.none')
891 892 p = self._choose_permission(p, cur_perm)
892 893
893 894 self.permissions_repository_groups[rg_k] = p, o, obj_id
894 895
895 896 if perm.RepoGroup.user_id == self.user_id:
896 897 # set admin if owner
897 898 p = 'group.admin'
898 899 o = PermOrigin.REPOGROUP_OWNER
899 900 self.permissions_repository_groups[rg_k] = p, o, obj_id
900 901
901 902 if self.user_is_admin:
902 903 p = 'group.admin'
903 904 o = PermOrigin.SUPER_ADMIN
904 905 self.permissions_repository_groups[rg_k] = p, o, obj_id
905 906
906 907 def _calculate_user_group_permissions(self):
907 908 """
908 909 User group permissions for the current user.
909 910 """
910 911 # user group for user group permissions
911 912 user_group_from_user_group = Permission\
912 913 .get_default_user_group_perms_from_user_group(
913 914 self.user_id, self.scope_user_group_id)
914 915
915 916 multiple_counter = collections.defaultdict(int)
916 917 for perm in user_group_from_user_group:
917 918 ug_k = perm.UserGroupUserGroupToPerm.target_user_group.users_group_name
918 919 obj_id = perm.UserGroupUserGroupToPerm.target_user_group.users_group_id
919 920 multiple_counter[ug_k] += 1
920 921 o = PermOrigin.USERGROUP_USERGROUP % perm.UserGroupUserGroupToPerm\
921 922 .user_group.users_group_name
922 923 p = perm.Permission.permission_name
923 924
924 925 if multiple_counter[ug_k] > 1:
925 926 cur_perm = self.permissions_user_groups[ug_k]
926 927 p = self._choose_permission(p, cur_perm)
927 928
928 929 self.permissions_user_groups[ug_k] = p, o, obj_id
929 930
930 931 if perm.UserGroup.user_id == self.user_id:
931 932 # set admin if owner, even for member of other user group
932 933 p = 'usergroup.admin'
933 934 o = PermOrigin.USERGROUP_OWNER
934 935 self.permissions_user_groups[ug_k] = p, o, obj_id
935 936
936 937 if self.user_is_admin:
937 938 p = 'usergroup.admin'
938 939 o = PermOrigin.SUPER_ADMIN
939 940 self.permissions_user_groups[ug_k] = p, o, obj_id
940 941
941 942 # user explicit permission for user groups
942 943 user_user_groups_perms = Permission.get_default_user_group_perms(
943 944 self.user_id, self.scope_user_group_id)
944 945 for perm in user_user_groups_perms:
945 946 ug_k = perm.UserUserGroupToPerm.user_group.users_group_name
946 947 obj_id = perm.UserUserGroupToPerm.user_group.users_group_id
947 948 o = PermOrigin.USERGROUP_USER % perm.UserUserGroupToPerm\
948 949 .user.username
949 950 p = perm.Permission.permission_name
950 951
951 952 if not self.explicit:
952 953 cur_perm = self.permissions_user_groups.get(ug_k, 'usergroup.none')
953 954 p = self._choose_permission(p, cur_perm)
954 955
955 956 self.permissions_user_groups[ug_k] = p, o, obj_id
956 957
957 958 if perm.UserGroup.user_id == self.user_id:
958 959 # set admin if owner
959 960 p = 'usergroup.admin'
960 961 o = PermOrigin.USERGROUP_OWNER
961 962 self.permissions_user_groups[ug_k] = p, o, obj_id
962 963
963 964 if self.user_is_admin:
964 965 p = 'usergroup.admin'
965 966 o = PermOrigin.SUPER_ADMIN
966 967 self.permissions_user_groups[ug_k] = p, o, obj_id
967 968
968 969 def _choose_permission(self, new_perm, cur_perm):
969 970 new_perm_val = Permission.PERM_WEIGHTS[new_perm]
970 971 cur_perm_val = Permission.PERM_WEIGHTS[cur_perm]
971 972 if self.algo == 'higherwin':
972 973 if new_perm_val > cur_perm_val:
973 974 return new_perm
974 975 return cur_perm
975 976 elif self.algo == 'lowerwin':
976 977 if new_perm_val < cur_perm_val:
977 978 return new_perm
978 979 return cur_perm
979 980
980 981 def _permission_structure(self):
981 982 return {
982 983 'global': self.permissions_global,
983 984 'repositories': self.permissions_repositories,
984 985 'repository_branches': self.permissions_repository_branches,
985 986 'repositories_groups': self.permissions_repository_groups,
986 987 'user_groups': self.permissions_user_groups,
987 988 }
988 989
989 990
990 991 def allowed_auth_token_access(view_name, auth_token, whitelist=None):
991 992 """
992 993 Check if given controller_name is in whitelist of auth token access
993 994 """
994 995 if not whitelist:
995 996 from rhodecode import CONFIG
996 997 whitelist = aslist(
997 998 CONFIG.get('api_access_controllers_whitelist'), sep=',')
998 999 # backward compat translation
999 1000 compat = {
1000 1001 # old controller, new VIEW
1001 1002 'ChangesetController:*': 'RepoCommitsView:*',
1002 1003 'ChangesetController:changeset_patch': 'RepoCommitsView:repo_commit_patch',
1003 1004 'ChangesetController:changeset_raw': 'RepoCommitsView:repo_commit_raw',
1004 1005 'FilesController:raw': 'RepoCommitsView:repo_commit_raw',
1005 1006 'FilesController:archivefile': 'RepoFilesView:repo_archivefile',
1006 1007 'GistsController:*': 'GistView:*',
1007 1008 }
1008 1009
1009 1010 log.debug(
1010 1011 'Allowed views for AUTH TOKEN access: %s', whitelist)
1011 1012 auth_token_access_valid = False
1012 1013
1013 1014 for entry in whitelist:
1014 1015 token_match = True
1015 1016 if entry in compat:
1016 1017 # translate from old Controllers to Pyramid Views
1017 1018 entry = compat[entry]
1018 1019
1019 1020 if '@' in entry:
1020 1021 # specific AuthToken
1021 1022 entry, allowed_token = entry.split('@', 1)
1022 1023 token_match = auth_token == allowed_token
1023 1024
1024 1025 if fnmatch.fnmatch(view_name, entry) and token_match:
1025 1026 auth_token_access_valid = True
1026 1027 break
1027 1028
1028 1029 if auth_token_access_valid:
1029 1030 log.debug('view: `%s` matches entry in whitelist: %s',
1030 1031 view_name, whitelist)
1031 1032
1032 1033 else:
1033 1034 msg = ('view: `%s` does *NOT* match any entry in whitelist: %s'
1034 1035 % (view_name, whitelist))
1035 1036 if auth_token:
1036 1037 # if we use auth token key and don't have access it's a warning
1037 1038 log.warning(msg)
1038 1039 else:
1039 1040 log.debug(msg)
1040 1041
1041 1042 return auth_token_access_valid
1042 1043
1043 1044
1044 1045 class AuthUser(object):
1045 1046 """
1046 1047 A simple object that handles all attributes of user in RhodeCode
1047 1048
1048 1049 It does lookup based on API key,given user, or user present in session
1049 1050 Then it fills all required information for such user. It also checks if
1050 1051 anonymous access is enabled and if so, it returns default user as logged in
1051 1052 """
1052 1053 GLOBAL_PERMS = [x[0] for x in Permission.PERMS]
1053 1054 repo_read_perms = ['repository.read', 'repository.admin', 'repository.write']
1054 1055 repo_group_read_perms = ['group.read', 'group.write', 'group.admin']
1055 1056 user_group_read_perms = ['usergroup.read', 'usergroup.write', 'usergroup.admin']
1056 1057
1057 1058 def __init__(self, user_id=None, api_key=None, username=None, ip_addr=None):
1058 1059
1059 1060 self.user_id = user_id
1060 1061 self._api_key = api_key
1061 1062
1062 1063 self.api_key = None
1063 1064 self.username = username
1064 1065 self.ip_addr = ip_addr
1065 1066 self.name = ''
1066 1067 self.lastname = ''
1067 1068 self.first_name = ''
1068 1069 self.last_name = ''
1069 1070 self.email = ''
1070 1071 self.is_authenticated = False
1071 1072 self.admin = False
1072 1073 self.inherit_default_permissions = False
1073 1074 self.password = ''
1074 1075
1075 1076 self.anonymous_user = None # propagated on propagate_data
1076 1077 self.propagate_data()
1077 1078 self._instance = None
1078 1079 self._permissions_scoped_cache = {} # used to bind scoped calculation
1079 1080
1080 1081 @LazyProperty
1081 1082 def permissions(self):
1082 1083 return self.get_perms(user=self, cache=None)
1083 1084
1084 1085 @LazyProperty
1085 1086 def permissions_safe(self):
1086 1087 """
1087 1088 Filtered permissions excluding not allowed repositories
1088 1089 """
1089 1090 perms = self.get_perms(user=self, cache=None)
1090 1091
1091 1092 perms['repositories'] = {
1092 1093 k: v for k, v in perms['repositories'].items()
1093 1094 if v != 'repository.none'}
1094 1095 perms['repositories_groups'] = {
1095 1096 k: v for k, v in perms['repositories_groups'].items()
1096 1097 if v != 'group.none'}
1097 1098 perms['user_groups'] = {
1098 1099 k: v for k, v in perms['user_groups'].items()
1099 1100 if v != 'usergroup.none'}
1100 1101 perms['repository_branches'] = {
1101 1102 k: v for k, v in perms['repository_branches'].iteritems()
1102 1103 if v != 'branch.none'}
1103 1104 return perms
1104 1105
1105 1106 @LazyProperty
1106 1107 def permissions_full_details(self):
1107 1108 return self.get_perms(
1108 1109 user=self, cache=None, calculate_super_admin=True)
1109 1110
1110 1111 def permissions_with_scope(self, scope):
1111 1112 """
1112 1113 Call the get_perms function with scoped data. The scope in that function
1113 1114 narrows the SQL calls to the given ID of objects resulting in fetching
1114 1115 Just particular permission we want to obtain. If scope is an empty dict
1115 1116 then it basically narrows the scope to GLOBAL permissions only.
1116 1117
1117 1118 :param scope: dict
1118 1119 """
1119 1120 if 'repo_name' in scope:
1120 1121 obj = Repository.get_by_repo_name(scope['repo_name'])
1121 1122 if obj:
1122 1123 scope['repo_id'] = obj.repo_id
1123 1124 _scope = collections.OrderedDict()
1124 1125 _scope['repo_id'] = -1
1125 1126 _scope['user_group_id'] = -1
1126 1127 _scope['repo_group_id'] = -1
1127 1128
1128 1129 for k in sorted(scope.keys()):
1129 1130 _scope[k] = scope[k]
1130 1131
1131 1132 # store in cache to mimic how the @LazyProperty works,
1132 1133 # the difference here is that we use the unique key calculated
1133 1134 # from params and values
1134 1135 return self.get_perms(user=self, cache=None, scope=_scope)
1135 1136
1136 1137 def get_instance(self):
1137 1138 return User.get(self.user_id)
1138 1139
1139 1140 def propagate_data(self):
1140 1141 """
1141 1142 Fills in user data and propagates values to this instance. Maps fetched
1142 1143 user attributes to this class instance attributes
1143 1144 """
1144 1145 log.debug('AuthUser: starting data propagation for new potential user')
1145 1146 user_model = UserModel()
1146 1147 anon_user = self.anonymous_user = User.get_default_user(cache=True)
1147 1148 is_user_loaded = False
1148 1149
1149 1150 # lookup by userid
1150 1151 if self.user_id is not None and self.user_id != anon_user.user_id:
1151 1152 log.debug('Trying Auth User lookup by USER ID: `%s`', self.user_id)
1152 1153 is_user_loaded = user_model.fill_data(self, user_id=self.user_id)
1153 1154
1154 1155 # try go get user by api key
1155 1156 elif self._api_key and self._api_key != anon_user.api_key:
1156 1157 log.debug('Trying Auth User lookup by API KEY: `...%s`', self._api_key[-4:])
1157 1158 is_user_loaded = user_model.fill_data(self, api_key=self._api_key)
1158 1159
1159 1160 # lookup by username
1160 1161 elif self.username:
1161 1162 log.debug('Trying Auth User lookup by USER NAME: `%s`', self.username)
1162 1163 is_user_loaded = user_model.fill_data(self, username=self.username)
1163 1164 else:
1164 1165 log.debug('No data in %s that could been used to log in', self)
1165 1166
1166 1167 if not is_user_loaded:
1167 1168 log.debug(
1168 1169 'Failed to load user. Fallback to default user %s', anon_user)
1169 1170 # if we cannot authenticate user try anonymous
1170 1171 if anon_user.active:
1171 1172 log.debug('default user is active, using it as a session user')
1172 1173 user_model.fill_data(self, user_id=anon_user.user_id)
1173 1174 # then we set this user is logged in
1174 1175 self.is_authenticated = True
1175 1176 else:
1176 1177 log.debug('default user is NOT active')
1177 1178 # in case of disabled anonymous user we reset some of the
1178 1179 # parameters so such user is "corrupted", skipping the fill_data
1179 1180 for attr in ['user_id', 'username', 'admin', 'active']:
1180 1181 setattr(self, attr, None)
1181 1182 self.is_authenticated = False
1182 1183
1183 1184 if not self.username:
1184 1185 self.username = 'None'
1185 1186
1186 1187 log.debug('AuthUser: propagated user is now %s', self)
1187 1188
1188 1189 def get_perms(self, user, scope=None, explicit=True, algo='higherwin',
1189 1190 calculate_super_admin=False, cache=None):
1190 1191 """
1191 1192 Fills user permission attribute with permissions taken from database
1192 1193 works for permissions given for repositories, and for permissions that
1193 1194 are granted to groups
1194 1195
1195 1196 :param user: instance of User object from database
1196 1197 :param explicit: In case there are permissions both for user and a group
1197 1198 that user is part of, explicit flag will defiine if user will
1198 1199 explicitly override permissions from group, if it's False it will
1199 1200 make decision based on the algo
1200 1201 :param algo: algorithm to decide what permission should be choose if
1201 1202 it's multiple defined, eg user in two different groups. It also
1202 1203 decides if explicit flag is turned off how to specify the permission
1203 1204 for case when user is in a group + have defined separate permission
1204 1205 :param calculate_super_admin: calculate permissions for super-admin in the
1205 1206 same way as for regular user without speedups
1206 1207 :param cache: Use caching for calculation, None = let the cache backend decide
1207 1208 """
1208 1209 user_id = user.user_id
1209 1210 user_is_admin = user.is_admin
1210 1211
1211 1212 # inheritance of global permissions like create repo/fork repo etc
1212 1213 user_inherit_default_permissions = user.inherit_default_permissions
1213 1214
1214 1215 cache_seconds = safe_int(
1215 1216 rhodecode.CONFIG.get('rc_cache.cache_perms.expiration_time'))
1216 1217
1217 1218 if cache is None:
1218 1219 # let the backend cache decide
1219 1220 cache_on = cache_seconds > 0
1220 1221 else:
1221 1222 cache_on = cache
1222 1223
1223 1224 log.debug(
1224 1225 'Computing PERMISSION tree for user %s scope `%s` '
1225 1226 'with caching: %s[TTL: %ss]', user, scope, cache_on, cache_seconds or 0)
1226 1227
1227 1228 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
1228 1229 region = rc_cache.get_or_create_region('cache_perms', cache_namespace_uid)
1229 1230
1230 1231 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid,
1231 1232 condition=cache_on)
1232 1233 def compute_perm_tree(cache_name, cache_ver,
1233 1234 user_id, scope, user_is_admin,user_inherit_default_permissions,
1234 1235 explicit, algo, calculate_super_admin):
1235 1236 return _cached_perms_data(
1236 1237 user_id, scope, user_is_admin, user_inherit_default_permissions,
1237 1238 explicit, algo, calculate_super_admin)
1238 1239
1239 1240 start = time.time()
1240 1241 result = compute_perm_tree(
1241 1242 'permissions', 'v1', user_id, scope, user_is_admin,
1242 1243 user_inherit_default_permissions, explicit, algo,
1243 1244 calculate_super_admin)
1244 1245
1245 1246 result_repr = []
1246 1247 for k in result:
1247 1248 result_repr.append((k, len(result[k])))
1248 1249 total = time.time() - start
1249 1250 log.debug('PERMISSION tree for user %s computed in %.4fs: %s',
1250 1251 user, total, result_repr)
1251 1252
1252 1253 return result
1253 1254
1254 1255 @property
1255 1256 def is_default(self):
1256 1257 return self.username == User.DEFAULT_USER
1257 1258
1258 1259 @property
1259 1260 def is_admin(self):
1260 1261 return self.admin
1261 1262
1262 1263 @property
1263 1264 def is_user_object(self):
1264 1265 return self.user_id is not None
1265 1266
1266 1267 @property
1267 1268 def repositories_admin(self):
1268 1269 """
1269 1270 Returns list of repositories you're an admin of
1270 1271 """
1271 1272 return [
1272 1273 x[0] for x in self.permissions['repositories'].items()
1273 1274 if x[1] == 'repository.admin']
1274 1275
1275 1276 @property
1276 1277 def repository_groups_admin(self):
1277 1278 """
1278 1279 Returns list of repository groups you're an admin of
1279 1280 """
1280 1281 return [
1281 1282 x[0] for x in self.permissions['repositories_groups'].items()
1282 1283 if x[1] == 'group.admin']
1283 1284
1284 1285 @property
1285 1286 def user_groups_admin(self):
1286 1287 """
1287 1288 Returns list of user groups you're an admin of
1288 1289 """
1289 1290 return [
1290 1291 x[0] for x in self.permissions['user_groups'].items()
1291 1292 if x[1] == 'usergroup.admin']
1292 1293
1293 1294 def repo_acl_ids_from_stack(self, perms=None, prefix_filter=None, cache=False):
1294 1295 if not perms:
1295 1296 perms = AuthUser.repo_read_perms
1296 1297 allowed_ids = []
1297 1298 for k, stack_data in self.permissions['repositories'].perm_origin_stack.items():
1298 1299 perm, origin, obj_id = stack_data[-1] # last item is the current permission
1299 1300 if prefix_filter and not k.startswith(prefix_filter):
1300 1301 continue
1301 1302 if perm in perms:
1302 1303 allowed_ids.append(obj_id)
1303 1304 return allowed_ids
1304 1305
1305 1306 def repo_acl_ids(self, perms=None, name_filter=None, cache=False):
1306 1307 """
1307 1308 Returns list of repository ids that user have access to based on given
1308 1309 perms. The cache flag should be only used in cases that are used for
1309 1310 display purposes, NOT IN ANY CASE for permission checks.
1310 1311 """
1311 1312 from rhodecode.model.scm import RepoList
1312 1313 if not perms:
1313 1314 perms = AuthUser.repo_read_perms
1314 1315
1315 1316 def _cached_repo_acl(user_id, perm_def, _name_filter):
1316 1317 qry = Repository.query()
1317 1318 if _name_filter:
1318 1319 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1319 1320 qry = qry.filter(
1320 1321 Repository.repo_name.ilike(ilike_expression))
1321 1322
1322 1323 return [x.repo_id for x in
1323 1324 RepoList(qry, perm_set=perm_def, extra_kwargs={'user': self})]
1324 1325
1325 1326 return _cached_repo_acl(self.user_id, perms, name_filter)
1326 1327
1327 1328 def repo_group_acl_ids_from_stack(self, perms=None, prefix_filter=None, cache=False):
1328 1329 if not perms:
1329 1330 perms = AuthUser.repo_group_read_perms
1330 1331 allowed_ids = []
1331 1332 for k, stack_data in self.permissions['repositories_groups'].perm_origin_stack.items():
1332 1333 perm, origin, obj_id = stack_data[-1] # last item is the current permission
1333 1334 if prefix_filter and not k.startswith(prefix_filter):
1334 1335 continue
1335 1336 if perm in perms:
1336 1337 allowed_ids.append(obj_id)
1337 1338 return allowed_ids
1338 1339
1339 1340 def repo_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1340 1341 """
1341 1342 Returns list of repository group ids that user have access to based on given
1342 1343 perms. The cache flag should be only used in cases that are used for
1343 1344 display purposes, NOT IN ANY CASE for permission checks.
1344 1345 """
1345 1346 from rhodecode.model.scm import RepoGroupList
1346 1347 if not perms:
1347 1348 perms = AuthUser.repo_group_read_perms
1348 1349
1349 1350 def _cached_repo_group_acl(user_id, perm_def, _name_filter):
1350 1351 qry = RepoGroup.query()
1351 1352 if _name_filter:
1352 1353 ilike_expression = u'%{}%'.format(safe_unicode(_name_filter))
1353 1354 qry = qry.filter(
1354 1355 RepoGroup.group_name.ilike(ilike_expression))
1355 1356
1356 1357 return [x.group_id for x in
1357 1358 RepoGroupList(qry, perm_set=perm_def, extra_kwargs={'user': self})]
1358 1359
1359 1360 return _cached_repo_group_acl(self.user_id, perms, name_filter)
1360 1361
1361 1362 def user_group_acl_ids_from_stack(self, perms=None, cache=False):
1362 1363 if not perms:
1363 1364 perms = AuthUser.user_group_read_perms
1364 1365 allowed_ids = []
1365 1366 for k, stack_data in self.permissions['user_groups'].perm_origin_stack.items():
1366 1367 perm, origin, obj_id = stack_data[-1] # last item is the current permission
1367 1368 if perm in perms:
1368 1369 allowed_ids.append(obj_id)
1369 1370 return allowed_ids
1370 1371
1371 1372 def user_group_acl_ids(self, perms=None, name_filter=None, cache=False):
1372 1373 """
1373 1374 Returns list of user group ids that user have access to based on given
1374 1375 perms. The cache flag should be only used in cases that are used for
1375 1376 display purposes, NOT IN ANY CASE for permission checks.
1376 1377 """
1377 1378 from rhodecode.model.scm import UserGroupList
1378 1379 if not perms:
1379 1380 perms = AuthUser.user_group_read_perms
1380 1381
1381 1382 def _cached_user_group_acl(user_id, perm_def, name_filter):
1382 1383 qry = UserGroup.query()
1383 1384 if name_filter:
1384 1385 ilike_expression = u'%{}%'.format(safe_unicode(name_filter))
1385 1386 qry = qry.filter(
1386 1387 UserGroup.users_group_name.ilike(ilike_expression))
1387 1388
1388 1389 return [x.users_group_id for x in
1389 1390 UserGroupList(qry, perm_set=perm_def, extra_kwargs={'user': self})]
1390 1391
1391 1392 return _cached_user_group_acl(self.user_id, perms, name_filter)
1392 1393
1393 1394 @property
1394 1395 def ip_allowed(self):
1395 1396 """
1396 1397 Checks if ip_addr used in constructor is allowed from defined list of
1397 1398 allowed ip_addresses for user
1398 1399
1399 1400 :returns: boolean, True if ip is in allowed ip range
1400 1401 """
1401 1402 # check IP
1402 1403 inherit = self.inherit_default_permissions
1403 1404 return AuthUser.check_ip_allowed(self.user_id, self.ip_addr,
1404 1405 inherit_from_default=inherit)
1405 1406 @property
1406 1407 def personal_repo_group(self):
1407 1408 return RepoGroup.get_user_personal_repo_group(self.user_id)
1408 1409
1409 1410 @LazyProperty
1410 1411 def feed_token(self):
1411 1412 return self.get_instance().feed_token
1412 1413
1413 1414 @LazyProperty
1414 1415 def artifact_token(self):
1415 1416 return self.get_instance().artifact_token
1416 1417
1417 1418 @classmethod
1418 1419 def check_ip_allowed(cls, user_id, ip_addr, inherit_from_default):
1419 1420 allowed_ips = AuthUser.get_allowed_ips(
1420 1421 user_id, cache=True, inherit_from_default=inherit_from_default)
1421 1422 if check_ip_access(source_ip=ip_addr, allowed_ips=allowed_ips):
1422 1423 log.debug('IP:%s for user %s is in range of %s',
1423 1424 ip_addr, user_id, allowed_ips)
1424 1425 return True
1425 1426 else:
1426 1427 log.info('Access for IP:%s forbidden for user %s, '
1427 1428 'not in %s', ip_addr, user_id, allowed_ips)
1428 1429 return False
1429 1430
1430 1431 def get_branch_permissions(self, repo_name, perms=None):
1431 1432 perms = perms or self.permissions_with_scope({'repo_name': repo_name})
1432 1433 branch_perms = perms.get('repository_branches', {})
1433 1434 if not branch_perms:
1434 1435 return {}
1435 1436 repo_branch_perms = branch_perms.get(repo_name)
1436 1437 return repo_branch_perms or {}
1437 1438
1438 1439 def get_rule_and_branch_permission(self, repo_name, branch_name):
1439 1440 """
1440 1441 Check if this AuthUser has defined any permissions for branches. If any of
1441 1442 the rules match in order, we return the matching permissions
1442 1443 """
1443 1444
1444 1445 rule = default_perm = ''
1445 1446
1446 1447 repo_branch_perms = self.get_branch_permissions(repo_name=repo_name)
1447 1448 if not repo_branch_perms:
1448 1449 return rule, default_perm
1449 1450
1450 1451 # now calculate the permissions
1451 1452 for pattern, branch_perm in repo_branch_perms.items():
1452 1453 if fnmatch.fnmatch(branch_name, pattern):
1453 1454 rule = '`{}`=>{}'.format(pattern, branch_perm)
1454 1455 return rule, branch_perm
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)
1461 1494
1462 1495 def set_authenticated(self, authenticated=True):
1463 1496 if self.user_id != self.anonymous_user.user_id:
1464 1497 self.is_authenticated = authenticated
1465 1498
1466 1499 def get_cookie_store(self):
1467 1500 return {
1468 1501 'username': self.username,
1469 1502 'password': md5(self.password or ''),
1470 1503 'user_id': self.user_id,
1471 1504 'is_authenticated': self.is_authenticated
1472 1505 }
1473 1506
1474 1507 @classmethod
1475 1508 def from_cookie_store(cls, cookie_store):
1476 1509 """
1477 1510 Creates AuthUser from a cookie store
1478 1511
1479 1512 :param cls:
1480 1513 :param cookie_store:
1481 1514 """
1482 1515 user_id = cookie_store.get('user_id')
1483 1516 username = cookie_store.get('username')
1484 1517 api_key = cookie_store.get('api_key')
1485 1518 return AuthUser(user_id, api_key, username)
1486 1519
1487 1520 @classmethod
1488 1521 def get_allowed_ips(cls, user_id, cache=False, inherit_from_default=False):
1489 1522 _set = set()
1490 1523
1491 1524 if inherit_from_default:
1492 1525 def_user_id = User.get_default_user(cache=True).user_id
1493 1526 default_ips = UserIpMap.query().filter(UserIpMap.user_id == def_user_id)
1494 1527 if cache:
1495 1528 default_ips = default_ips.options(
1496 1529 FromCache("sql_cache_short", "get_user_ips_default"))
1497 1530
1498 1531 # populate from default user
1499 1532 for ip in default_ips:
1500 1533 try:
1501 1534 _set.add(ip.ip_addr)
1502 1535 except ObjectDeletedError:
1503 1536 # since we use heavy caching sometimes it happens that
1504 1537 # we get deleted objects here, we just skip them
1505 1538 pass
1506 1539
1507 1540 # NOTE:(marcink) we don't want to load any rules for empty
1508 1541 # user_id which is the case of access of non logged users when anonymous
1509 1542 # access is disabled
1510 1543 user_ips = []
1511 1544 if user_id:
1512 1545 user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id)
1513 1546 if cache:
1514 1547 user_ips = user_ips.options(
1515 1548 FromCache("sql_cache_short", "get_user_ips_%s" % user_id))
1516 1549
1517 1550 for ip in user_ips:
1518 1551 try:
1519 1552 _set.add(ip.ip_addr)
1520 1553 except ObjectDeletedError:
1521 1554 # since we use heavy caching sometimes it happens that we get
1522 1555 # deleted objects here, we just skip them
1523 1556 pass
1524 1557 return _set or {ip for ip in ['0.0.0.0/0', '::/0']}
1525 1558
1526 1559
1527 1560 def set_available_permissions(settings):
1528 1561 """
1529 1562 This function will propagate pyramid settings with all available defined
1530 1563 permission given in db. We don't want to check each time from db for new
1531 1564 permissions since adding a new permission also requires application restart
1532 1565 ie. to decorate new views with the newly created permission
1533 1566
1534 1567 :param settings: current pyramid registry.settings
1535 1568
1536 1569 """
1537 1570 log.debug('auth: getting information about all available permissions')
1538 1571 try:
1539 1572 sa = meta.Session
1540 1573 all_perms = sa.query(Permission).all()
1541 1574 settings.setdefault('available_permissions',
1542 1575 [x.permission_name for x in all_perms])
1543 1576 log.debug('auth: set available permissions')
1544 1577 except Exception:
1545 1578 log.exception('Failed to fetch permissions from the database.')
1546 1579 raise
1547 1580
1548 1581
1549 1582 def get_csrf_token(session, force_new=False, save_if_missing=True):
1550 1583 """
1551 1584 Return the current authentication token, creating one if one doesn't
1552 1585 already exist and the save_if_missing flag is present.
1553 1586
1554 1587 :param session: pass in the pyramid session, else we use the global ones
1555 1588 :param force_new: force to re-generate the token and store it in session
1556 1589 :param save_if_missing: save the newly generated token if it's missing in
1557 1590 session
1558 1591 """
1559 1592 # NOTE(marcink): probably should be replaced with below one from pyramid 1.9
1560 1593 # from pyramid.csrf import get_csrf_token
1561 1594
1562 1595 if (csrf_token_key not in session and save_if_missing) or force_new:
1563 1596 token = hashlib.sha1(str(random.getrandbits(128))).hexdigest()
1564 1597 session[csrf_token_key] = token
1565 1598 if hasattr(session, 'save'):
1566 1599 session.save()
1567 1600 return session.get(csrf_token_key)
1568 1601
1569 1602
1570 1603 def get_request(perm_class_instance):
1571 1604 from pyramid.threadlocal import get_current_request
1572 1605 pyramid_request = get_current_request()
1573 1606 return pyramid_request
1574 1607
1575 1608
1576 1609 # CHECK DECORATORS
1577 1610 class CSRFRequired(object):
1578 1611 """
1579 1612 Decorator for authenticating a form
1580 1613
1581 1614 This decorator uses an authorization token stored in the client's
1582 1615 session for prevention of certain Cross-site request forgery (CSRF)
1583 1616 attacks (See
1584 1617 http://en.wikipedia.org/wiki/Cross-site_request_forgery for more
1585 1618 information).
1586 1619
1587 1620 For use with the ``secure_form`` helper functions.
1588 1621
1589 1622 """
1590 1623 def __init__(self, token=csrf_token_key, header='X-CSRF-Token', except_methods=None):
1591 1624 self.token = token
1592 1625 self.header = header
1593 1626 self.except_methods = except_methods or []
1594 1627
1595 1628 def __call__(self, func):
1596 1629 return get_cython_compat_decorator(self.__wrapper, func)
1597 1630
1598 1631 def _get_csrf(self, _request):
1599 1632 return _request.POST.get(self.token, _request.headers.get(self.header))
1600 1633
1601 1634 def check_csrf(self, _request, cur_token):
1602 1635 supplied_token = self._get_csrf(_request)
1603 1636 return supplied_token and supplied_token == cur_token
1604 1637
1605 1638 def _get_request(self):
1606 1639 return get_request(self)
1607 1640
1608 1641 def __wrapper(self, func, *fargs, **fkwargs):
1609 1642 request = self._get_request()
1610 1643
1611 1644 if request.method in self.except_methods:
1612 1645 return func(*fargs, **fkwargs)
1613 1646
1614 1647 cur_token = get_csrf_token(request.session, save_if_missing=False)
1615 1648 if self.check_csrf(request, cur_token):
1616 1649 if request.POST.get(self.token):
1617 1650 del request.POST[self.token]
1618 1651 return func(*fargs, **fkwargs)
1619 1652 else:
1620 1653 reason = 'token-missing'
1621 1654 supplied_token = self._get_csrf(request)
1622 1655 if supplied_token and cur_token != supplied_token:
1623 1656 reason = 'token-mismatch [%s:%s]' % (
1624 1657 cur_token or ''[:6], supplied_token or ''[:6])
1625 1658
1626 1659 csrf_message = \
1627 1660 ("Cross-site request forgery detected, request denied. See "
1628 1661 "http://en.wikipedia.org/wiki/Cross-site_request_forgery for "
1629 1662 "more information.")
1630 1663 log.warn('Cross-site request forgery detected, request %r DENIED: %s '
1631 1664 'REMOTE_ADDR:%s, HEADERS:%s' % (
1632 1665 request, reason, request.remote_addr, request.headers))
1633 1666
1634 1667 raise HTTPForbidden(explanation=csrf_message)
1635 1668
1636 1669
1637 1670 class LoginRequired(object):
1638 1671 """
1639 1672 Must be logged in to execute this function else
1640 1673 redirect to login page
1641 1674
1642 1675 :param api_access: if enabled this checks only for valid auth token
1643 1676 and grants access based on valid token
1644 1677 """
1645 1678 def __init__(self, auth_token_access=None):
1646 1679 self.auth_token_access = auth_token_access
1647 1680 if self.auth_token_access:
1648 1681 valid_type = set(auth_token_access).intersection(set(UserApiKeys.ROLES))
1649 1682 if not valid_type:
1650 1683 raise ValueError('auth_token_access must be on of {}, got {}'.format(
1651 1684 UserApiKeys.ROLES, auth_token_access))
1652 1685
1653 1686 def __call__(self, func):
1654 1687 return get_cython_compat_decorator(self.__wrapper, func)
1655 1688
1656 1689 def _get_request(self):
1657 1690 return get_request(self)
1658 1691
1659 1692 def __wrapper(self, func, *fargs, **fkwargs):
1660 1693 from rhodecode.lib import helpers as h
1661 1694 cls = fargs[0]
1662 1695 user = cls._rhodecode_user
1663 1696 request = self._get_request()
1664 1697 _ = request.translate
1665 1698
1666 1699 loc = "%s:%s" % (cls.__class__.__name__, func.__name__)
1667 1700 log.debug('Starting login restriction checks for user: %s', user)
1668 1701 # check if our IP is allowed
1669 1702 ip_access_valid = True
1670 1703 if not user.ip_allowed:
1671 1704 h.flash(h.literal(_('IP {} not allowed'.format(user.ip_addr))),
1672 1705 category='warning')
1673 1706 ip_access_valid = False
1674 1707
1675 1708 # we used stored token that is extract from GET or URL param (if any)
1676 1709 _auth_token = request.user_auth_token
1677 1710
1678 1711 # check if we used an AUTH_TOKEN and it's a valid one
1679 1712 # defined white-list of controllers which API access will be enabled
1680 1713 whitelist = None
1681 1714 if self.auth_token_access:
1682 1715 # since this location is allowed by @LoginRequired decorator it's our
1683 1716 # only whitelist
1684 1717 whitelist = [loc]
1685 1718 auth_token_access_valid = allowed_auth_token_access(
1686 1719 loc, whitelist=whitelist, auth_token=_auth_token)
1687 1720
1688 1721 # explicit controller is enabled or API is in our whitelist
1689 1722 if auth_token_access_valid:
1690 1723 log.debug('Checking AUTH TOKEN access for %s', cls)
1691 1724 db_user = user.get_instance()
1692 1725
1693 1726 if db_user:
1694 1727 if self.auth_token_access:
1695 1728 roles = self.auth_token_access
1696 1729 else:
1697 1730 roles = [UserApiKeys.ROLE_HTTP]
1698 1731 log.debug('AUTH TOKEN: checking auth for user %s and roles %s',
1699 1732 db_user, roles)
1700 1733 token_match = db_user.authenticate_by_token(
1701 1734 _auth_token, roles=roles)
1702 1735 else:
1703 1736 log.debug('Unable to fetch db instance for auth user: %s', user)
1704 1737 token_match = False
1705 1738
1706 1739 if _auth_token and token_match:
1707 1740 auth_token_access_valid = True
1708 1741 log.debug('AUTH TOKEN ****%s is VALID', _auth_token[-4:])
1709 1742 else:
1710 1743 auth_token_access_valid = False
1711 1744 if not _auth_token:
1712 1745 log.debug("AUTH TOKEN *NOT* present in request")
1713 1746 else:
1714 1747 log.warning("AUTH TOKEN ****%s *NOT* valid", _auth_token[-4:])
1715 1748
1716 1749 log.debug('Checking if %s is authenticated @ %s', user.username, loc)
1717 1750 reason = 'RHODECODE_AUTH' if user.is_authenticated \
1718 1751 else 'AUTH_TOKEN_AUTH'
1719 1752
1720 1753 if ip_access_valid and (
1721 1754 user.is_authenticated or auth_token_access_valid):
1722 1755 log.info('user %s authenticating with:%s IS authenticated on func %s',
1723 1756 user, reason, loc)
1724 1757
1725 1758 return func(*fargs, **fkwargs)
1726 1759 else:
1727 1760 log.warning(
1728 1761 'user %s authenticating with:%s NOT authenticated on '
1729 1762 'func: %s: IP_ACCESS:%s AUTH_TOKEN_ACCESS:%s',
1730 1763 user, reason, loc, ip_access_valid, auth_token_access_valid)
1731 1764 # we preserve the get PARAM
1732 1765 came_from = get_came_from(request)
1733 1766
1734 1767 log.debug('redirecting to login page with %s', came_from)
1735 1768 raise HTTPFound(
1736 1769 h.route_path('login', _query={'came_from': came_from}))
1737 1770
1738 1771
1739 1772 class NotAnonymous(object):
1740 1773 """
1741 1774 Must be logged in to execute this function else
1742 1775 redirect to login page
1743 1776 """
1744 1777
1745 1778 def __call__(self, func):
1746 1779 return get_cython_compat_decorator(self.__wrapper, func)
1747 1780
1748 1781 def _get_request(self):
1749 1782 return get_request(self)
1750 1783
1751 1784 def __wrapper(self, func, *fargs, **fkwargs):
1752 1785 import rhodecode.lib.helpers as h
1753 1786 cls = fargs[0]
1754 1787 self.user = cls._rhodecode_user
1755 1788 request = self._get_request()
1756 1789 _ = request.translate
1757 1790 log.debug('Checking if user is not anonymous @%s', cls)
1758 1791
1759 1792 anonymous = self.user.username == User.DEFAULT_USER
1760 1793
1761 1794 if anonymous:
1762 1795 came_from = get_came_from(request)
1763 1796 h.flash(_('You need to be a registered user to '
1764 1797 'perform this action'),
1765 1798 category='warning')
1766 1799 raise HTTPFound(
1767 1800 h.route_path('login', _query={'came_from': came_from}))
1768 1801 else:
1769 1802 return func(*fargs, **fkwargs)
1770 1803
1771 1804
1772 1805 class PermsDecorator(object):
1773 1806 """
1774 1807 Base class for controller decorators, we extract the current user from
1775 1808 the class itself, which has it stored in base controllers
1776 1809 """
1777 1810
1778 1811 def __init__(self, *required_perms):
1779 1812 self.required_perms = set(required_perms)
1780 1813
1781 1814 def __call__(self, func):
1782 1815 return get_cython_compat_decorator(self.__wrapper, func)
1783 1816
1784 1817 def _get_request(self):
1785 1818 return get_request(self)
1786 1819
1787 1820 def __wrapper(self, func, *fargs, **fkwargs):
1788 1821 import rhodecode.lib.helpers as h
1789 1822 cls = fargs[0]
1790 1823 _user = cls._rhodecode_user
1791 1824 request = self._get_request()
1792 1825 _ = request.translate
1793 1826
1794 1827 log.debug('checking %s permissions %s for %s %s',
1795 1828 self.__class__.__name__, self.required_perms, cls, _user)
1796 1829
1797 1830 if self.check_permissions(_user):
1798 1831 log.debug('Permission granted for %s %s', cls, _user)
1799 1832 return func(*fargs, **fkwargs)
1800 1833
1801 1834 else:
1802 1835 log.debug('Permission denied for %s %s', cls, _user)
1803 1836 anonymous = _user.username == User.DEFAULT_USER
1804 1837
1805 1838 if anonymous:
1806 1839 came_from = get_came_from(self._get_request())
1807 1840 h.flash(_('You need to be signed in to view this page'),
1808 1841 category='warning')
1809 1842 raise HTTPFound(
1810 1843 h.route_path('login', _query={'came_from': came_from}))
1811 1844
1812 1845 else:
1813 1846 # redirect with 404 to prevent resource discovery
1814 1847 raise HTTPNotFound()
1815 1848
1816 1849 def check_permissions(self, user):
1817 1850 """Dummy function for overriding"""
1818 1851 raise NotImplementedError(
1819 1852 'You have to write this function in child class')
1820 1853
1821 1854
1822 1855 class HasPermissionAllDecorator(PermsDecorator):
1823 1856 """
1824 1857 Checks for access permission for all given predicates. All of them
1825 1858 have to be meet in order to fulfill the request
1826 1859 """
1827 1860
1828 1861 def check_permissions(self, user):
1829 1862 perms = user.permissions_with_scope({})
1830 1863 if self.required_perms.issubset(perms['global']):
1831 1864 return True
1832 1865 return False
1833 1866
1834 1867
1835 1868 class HasPermissionAnyDecorator(PermsDecorator):
1836 1869 """
1837 1870 Checks for access permission for any of given predicates. In order to
1838 1871 fulfill the request any of predicates must be meet
1839 1872 """
1840 1873
1841 1874 def check_permissions(self, user):
1842 1875 perms = user.permissions_with_scope({})
1843 1876 if self.required_perms.intersection(perms['global']):
1844 1877 return True
1845 1878 return False
1846 1879
1847 1880
1848 1881 class HasRepoPermissionAllDecorator(PermsDecorator):
1849 1882 """
1850 1883 Checks for access permission for all given predicates for specific
1851 1884 repository. All of them have to be meet in order to fulfill the request
1852 1885 """
1853 1886 def _get_repo_name(self):
1854 1887 _request = self._get_request()
1855 1888 return get_repo_slug(_request)
1856 1889
1857 1890 def check_permissions(self, user):
1858 1891 perms = user.permissions
1859 1892 repo_name = self._get_repo_name()
1860 1893
1861 1894 try:
1862 1895 user_perms = {perms['repositories'][repo_name]}
1863 1896 except KeyError:
1864 1897 log.debug('cannot locate repo with name: `%s` in permissions defs',
1865 1898 repo_name)
1866 1899 return False
1867 1900
1868 1901 log.debug('checking `%s` permissions for repo `%s`',
1869 1902 user_perms, repo_name)
1870 1903 if self.required_perms.issubset(user_perms):
1871 1904 return True
1872 1905 return False
1873 1906
1874 1907
1875 1908 class HasRepoPermissionAnyDecorator(PermsDecorator):
1876 1909 """
1877 1910 Checks for access permission for any of given predicates for specific
1878 1911 repository. In order to fulfill the request any of predicates must be meet
1879 1912 """
1880 1913 def _get_repo_name(self):
1881 1914 _request = self._get_request()
1882 1915 return get_repo_slug(_request)
1883 1916
1884 1917 def check_permissions(self, user):
1885 1918 perms = user.permissions
1886 1919 repo_name = self._get_repo_name()
1887 1920
1888 1921 try:
1889 1922 user_perms = {perms['repositories'][repo_name]}
1890 1923 except KeyError:
1891 1924 log.debug(
1892 1925 'cannot locate repo with name: `%s` in permissions defs',
1893 1926 repo_name)
1894 1927 return False
1895 1928
1896 1929 log.debug('checking `%s` permissions for repo `%s`',
1897 1930 user_perms, repo_name)
1898 1931 if self.required_perms.intersection(user_perms):
1899 1932 return True
1900 1933 return False
1901 1934
1902 1935
1903 1936 class HasRepoGroupPermissionAllDecorator(PermsDecorator):
1904 1937 """
1905 1938 Checks for access permission for all given predicates for specific
1906 1939 repository group. All of them have to be meet in order to
1907 1940 fulfill the request
1908 1941 """
1909 1942 def _get_repo_group_name(self):
1910 1943 _request = self._get_request()
1911 1944 return get_repo_group_slug(_request)
1912 1945
1913 1946 def check_permissions(self, user):
1914 1947 perms = user.permissions
1915 1948 group_name = self._get_repo_group_name()
1916 1949 try:
1917 1950 user_perms = {perms['repositories_groups'][group_name]}
1918 1951 except KeyError:
1919 1952 log.debug(
1920 1953 'cannot locate repo group with name: `%s` in permissions defs',
1921 1954 group_name)
1922 1955 return False
1923 1956
1924 1957 log.debug('checking `%s` permissions for repo group `%s`',
1925 1958 user_perms, group_name)
1926 1959 if self.required_perms.issubset(user_perms):
1927 1960 return True
1928 1961 return False
1929 1962
1930 1963
1931 1964 class HasRepoGroupPermissionAnyDecorator(PermsDecorator):
1932 1965 """
1933 1966 Checks for access permission for any of given predicates for specific
1934 1967 repository group. In order to fulfill the request any
1935 1968 of predicates must be met
1936 1969 """
1937 1970 def _get_repo_group_name(self):
1938 1971 _request = self._get_request()
1939 1972 return get_repo_group_slug(_request)
1940 1973
1941 1974 def check_permissions(self, user):
1942 1975 perms = user.permissions
1943 1976 group_name = self._get_repo_group_name()
1944 1977
1945 1978 try:
1946 1979 user_perms = {perms['repositories_groups'][group_name]}
1947 1980 except KeyError:
1948 1981 log.debug(
1949 1982 'cannot locate repo group with name: `%s` in permissions defs',
1950 1983 group_name)
1951 1984 return False
1952 1985
1953 1986 log.debug('checking `%s` permissions for repo group `%s`',
1954 1987 user_perms, group_name)
1955 1988 if self.required_perms.intersection(user_perms):
1956 1989 return True
1957 1990 return False
1958 1991
1959 1992
1960 1993 class HasUserGroupPermissionAllDecorator(PermsDecorator):
1961 1994 """
1962 1995 Checks for access permission for all given predicates for specific
1963 1996 user group. All of them have to be meet in order to fulfill the request
1964 1997 """
1965 1998 def _get_user_group_name(self):
1966 1999 _request = self._get_request()
1967 2000 return get_user_group_slug(_request)
1968 2001
1969 2002 def check_permissions(self, user):
1970 2003 perms = user.permissions
1971 2004 group_name = self._get_user_group_name()
1972 2005 try:
1973 2006 user_perms = {perms['user_groups'][group_name]}
1974 2007 except KeyError:
1975 2008 return False
1976 2009
1977 2010 if self.required_perms.issubset(user_perms):
1978 2011 return True
1979 2012 return False
1980 2013
1981 2014
1982 2015 class HasUserGroupPermissionAnyDecorator(PermsDecorator):
1983 2016 """
1984 2017 Checks for access permission for any of given predicates for specific
1985 2018 user group. In order to fulfill the request any of predicates must be meet
1986 2019 """
1987 2020 def _get_user_group_name(self):
1988 2021 _request = self._get_request()
1989 2022 return get_user_group_slug(_request)
1990 2023
1991 2024 def check_permissions(self, user):
1992 2025 perms = user.permissions
1993 2026 group_name = self._get_user_group_name()
1994 2027 try:
1995 2028 user_perms = {perms['user_groups'][group_name]}
1996 2029 except KeyError:
1997 2030 return False
1998 2031
1999 2032 if self.required_perms.intersection(user_perms):
2000 2033 return True
2001 2034 return False
2002 2035
2003 2036
2004 2037 # CHECK FUNCTIONS
2005 2038 class PermsFunction(object):
2006 2039 """Base function for other check functions"""
2007 2040
2008 2041 def __init__(self, *perms):
2009 2042 self.required_perms = set(perms)
2010 2043 self.repo_name = None
2011 2044 self.repo_group_name = None
2012 2045 self.user_group_name = None
2013 2046
2014 2047 def __bool__(self):
2015 2048 import inspect
2016 2049 frame = inspect.currentframe()
2017 2050 stack_trace = traceback.format_stack(frame)
2018 2051 log.error('Checking bool value on a class instance of perm '
2019 2052 'function is not allowed: %s', ''.join(stack_trace))
2020 2053 # rather than throwing errors, here we always return False so if by
2021 2054 # accident someone checks truth for just an instance it will always end
2022 2055 # up in returning False
2023 2056 return False
2024 2057 __nonzero__ = __bool__
2025 2058
2026 2059 def __call__(self, check_location='', user=None):
2027 2060 if not user:
2028 2061 log.debug('Using user attribute from global request')
2029 2062 request = self._get_request()
2030 2063 user = request.user
2031 2064
2032 2065 # init auth user if not already given
2033 2066 if not isinstance(user, AuthUser):
2034 2067 log.debug('Wrapping user %s into AuthUser', user)
2035 2068 user = AuthUser(user.user_id)
2036 2069
2037 2070 cls_name = self.__class__.__name__
2038 2071 check_scope = self._get_check_scope(cls_name)
2039 2072 check_location = check_location or 'unspecified location'
2040 2073
2041 2074 log.debug('checking cls:%s %s usr:%s %s @ %s', cls_name,
2042 2075 self.required_perms, user, check_scope, check_location)
2043 2076 if not user:
2044 2077 log.warning('Empty user given for permission check')
2045 2078 return False
2046 2079
2047 2080 if self.check_permissions(user):
2048 2081 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
2049 2082 check_scope, user, check_location)
2050 2083 return True
2051 2084
2052 2085 else:
2053 2086 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
2054 2087 check_scope, user, check_location)
2055 2088 return False
2056 2089
2057 2090 def _get_request(self):
2058 2091 return get_request(self)
2059 2092
2060 2093 def _get_check_scope(self, cls_name):
2061 2094 return {
2062 2095 'HasPermissionAll': 'GLOBAL',
2063 2096 'HasPermissionAny': 'GLOBAL',
2064 2097 'HasRepoPermissionAll': 'repo:%s' % self.repo_name,
2065 2098 'HasRepoPermissionAny': 'repo:%s' % self.repo_name,
2066 2099 'HasRepoGroupPermissionAll': 'repo_group:%s' % self.repo_group_name,
2067 2100 'HasRepoGroupPermissionAny': 'repo_group:%s' % self.repo_group_name,
2068 2101 'HasUserGroupPermissionAll': 'user_group:%s' % self.user_group_name,
2069 2102 'HasUserGroupPermissionAny': 'user_group:%s' % self.user_group_name,
2070 2103 }.get(cls_name, '?:%s' % cls_name)
2071 2104
2072 2105 def check_permissions(self, user):
2073 2106 """Dummy function for overriding"""
2074 2107 raise Exception('You have to write this function in child class')
2075 2108
2076 2109
2077 2110 class HasPermissionAll(PermsFunction):
2078 2111 def check_permissions(self, user):
2079 2112 perms = user.permissions_with_scope({})
2080 2113 if self.required_perms.issubset(perms.get('global')):
2081 2114 return True
2082 2115 return False
2083 2116
2084 2117
2085 2118 class HasPermissionAny(PermsFunction):
2086 2119 def check_permissions(self, user):
2087 2120 perms = user.permissions_with_scope({})
2088 2121 if self.required_perms.intersection(perms.get('global')):
2089 2122 return True
2090 2123 return False
2091 2124
2092 2125
2093 2126 class HasRepoPermissionAll(PermsFunction):
2094 2127 def __call__(self, repo_name=None, check_location='', user=None):
2095 2128 self.repo_name = repo_name
2096 2129 return super(HasRepoPermissionAll, self).__call__(check_location, user)
2097 2130
2098 2131 def _get_repo_name(self):
2099 2132 if not self.repo_name:
2100 2133 _request = self._get_request()
2101 2134 self.repo_name = get_repo_slug(_request)
2102 2135 return self.repo_name
2103 2136
2104 2137 def check_permissions(self, user):
2105 2138 self.repo_name = self._get_repo_name()
2106 2139 perms = user.permissions
2107 2140 try:
2108 2141 user_perms = {perms['repositories'][self.repo_name]}
2109 2142 except KeyError:
2110 2143 return False
2111 2144 if self.required_perms.issubset(user_perms):
2112 2145 return True
2113 2146 return False
2114 2147
2115 2148
2116 2149 class HasRepoPermissionAny(PermsFunction):
2117 2150 def __call__(self, repo_name=None, check_location='', user=None):
2118 2151 self.repo_name = repo_name
2119 2152 return super(HasRepoPermissionAny, self).__call__(check_location, user)
2120 2153
2121 2154 def _get_repo_name(self):
2122 2155 if not self.repo_name:
2123 2156 _request = self._get_request()
2124 2157 self.repo_name = get_repo_slug(_request)
2125 2158 return self.repo_name
2126 2159
2127 2160 def check_permissions(self, user):
2128 2161 self.repo_name = self._get_repo_name()
2129 2162 perms = user.permissions
2130 2163 try:
2131 2164 user_perms = {perms['repositories'][self.repo_name]}
2132 2165 except KeyError:
2133 2166 return False
2134 2167 if self.required_perms.intersection(user_perms):
2135 2168 return True
2136 2169 return False
2137 2170
2138 2171
2139 2172 class HasRepoGroupPermissionAny(PermsFunction):
2140 2173 def __call__(self, group_name=None, check_location='', user=None):
2141 2174 self.repo_group_name = group_name
2142 2175 return super(HasRepoGroupPermissionAny, self).__call__(check_location, user)
2143 2176
2144 2177 def check_permissions(self, user):
2145 2178 perms = user.permissions
2146 2179 try:
2147 2180 user_perms = {perms['repositories_groups'][self.repo_group_name]}
2148 2181 except KeyError:
2149 2182 return False
2150 2183 if self.required_perms.intersection(user_perms):
2151 2184 return True
2152 2185 return False
2153 2186
2154 2187
2155 2188 class HasRepoGroupPermissionAll(PermsFunction):
2156 2189 def __call__(self, group_name=None, check_location='', user=None):
2157 2190 self.repo_group_name = group_name
2158 2191 return super(HasRepoGroupPermissionAll, self).__call__(check_location, user)
2159 2192
2160 2193 def check_permissions(self, user):
2161 2194 perms = user.permissions
2162 2195 try:
2163 2196 user_perms = {perms['repositories_groups'][self.repo_group_name]}
2164 2197 except KeyError:
2165 2198 return False
2166 2199 if self.required_perms.issubset(user_perms):
2167 2200 return True
2168 2201 return False
2169 2202
2170 2203
2171 2204 class HasUserGroupPermissionAny(PermsFunction):
2172 2205 def __call__(self, user_group_name=None, check_location='', user=None):
2173 2206 self.user_group_name = user_group_name
2174 2207 return super(HasUserGroupPermissionAny, self).__call__(check_location, user)
2175 2208
2176 2209 def check_permissions(self, user):
2177 2210 perms = user.permissions
2178 2211 try:
2179 2212 user_perms = {perms['user_groups'][self.user_group_name]}
2180 2213 except KeyError:
2181 2214 return False
2182 2215 if self.required_perms.intersection(user_perms):
2183 2216 return True
2184 2217 return False
2185 2218
2186 2219
2187 2220 class HasUserGroupPermissionAll(PermsFunction):
2188 2221 def __call__(self, user_group_name=None, check_location='', user=None):
2189 2222 self.user_group_name = user_group_name
2190 2223 return super(HasUserGroupPermissionAll, self).__call__(check_location, user)
2191 2224
2192 2225 def check_permissions(self, user):
2193 2226 perms = user.permissions
2194 2227 try:
2195 2228 user_perms = {perms['user_groups'][self.user_group_name]}
2196 2229 except KeyError:
2197 2230 return False
2198 2231 if self.required_perms.issubset(user_perms):
2199 2232 return True
2200 2233 return False
2201 2234
2202 2235
2203 2236 # SPECIAL VERSION TO HANDLE MIDDLEWARE AUTH
2204 2237 class HasPermissionAnyMiddleware(object):
2205 2238 def __init__(self, *perms):
2206 2239 self.required_perms = set(perms)
2207 2240
2208 2241 def __call__(self, auth_user, repo_name):
2209 2242 # repo_name MUST be unicode, since we handle keys in permission
2210 2243 # dict by unicode
2211 2244 repo_name = safe_unicode(repo_name)
2212 2245 log.debug(
2213 2246 'Checking VCS protocol permissions %s for user:%s repo:`%s`',
2214 2247 self.required_perms, auth_user, repo_name)
2215 2248
2216 2249 if self.check_permissions(auth_user, repo_name):
2217 2250 log.debug('Permission to repo:`%s` GRANTED for user:%s @ %s',
2218 2251 repo_name, auth_user, 'PermissionMiddleware')
2219 2252 return True
2220 2253
2221 2254 else:
2222 2255 log.debug('Permission to repo:`%s` DENIED for user:%s @ %s',
2223 2256 repo_name, auth_user, 'PermissionMiddleware')
2224 2257 return False
2225 2258
2226 2259 def check_permissions(self, user, repo_name):
2227 2260 perms = user.permissions_with_scope({'repo_name': repo_name})
2228 2261
2229 2262 try:
2230 2263 user_perms = {perms['repositories'][repo_name]}
2231 2264 except Exception:
2232 2265 log.exception('Error while accessing user permissions')
2233 2266 return False
2234 2267
2235 2268 if self.required_perms.intersection(user_perms):
2236 2269 return True
2237 2270 return False
2238 2271
2239 2272
2240 2273 # SPECIAL VERSION TO HANDLE API AUTH
2241 2274 class _BaseApiPerm(object):
2242 2275 def __init__(self, *perms):
2243 2276 self.required_perms = set(perms)
2244 2277
2245 2278 def __call__(self, check_location=None, user=None, repo_name=None,
2246 2279 group_name=None, user_group_name=None):
2247 2280 cls_name = self.__class__.__name__
2248 2281 check_scope = 'global:%s' % (self.required_perms,)
2249 2282 if repo_name:
2250 2283 check_scope += ', repo_name:%s' % (repo_name,)
2251 2284
2252 2285 if group_name:
2253 2286 check_scope += ', repo_group_name:%s' % (group_name,)
2254 2287
2255 2288 if user_group_name:
2256 2289 check_scope += ', user_group_name:%s' % (user_group_name,)
2257 2290
2258 2291 log.debug('checking cls:%s %s %s @ %s',
2259 2292 cls_name, self.required_perms, check_scope, check_location)
2260 2293 if not user:
2261 2294 log.debug('Empty User passed into arguments')
2262 2295 return False
2263 2296
2264 2297 # process user
2265 2298 if not isinstance(user, AuthUser):
2266 2299 user = AuthUser(user.user_id)
2267 2300 if not check_location:
2268 2301 check_location = 'unspecified'
2269 2302 if self.check_permissions(user.permissions, repo_name, group_name,
2270 2303 user_group_name):
2271 2304 log.debug('Permission to repo:`%s` GRANTED for user:`%s` @ %s',
2272 2305 check_scope, user, check_location)
2273 2306 return True
2274 2307
2275 2308 else:
2276 2309 log.debug('Permission to repo:`%s` DENIED for user:`%s` @ %s',
2277 2310 check_scope, user, check_location)
2278 2311 return False
2279 2312
2280 2313 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2281 2314 user_group_name=None):
2282 2315 """
2283 2316 implement in child class should return True if permissions are ok,
2284 2317 False otherwise
2285 2318
2286 2319 :param perm_defs: dict with permission definitions
2287 2320 :param repo_name: repo name
2288 2321 """
2289 2322 raise NotImplementedError()
2290 2323
2291 2324
2292 2325 class HasPermissionAllApi(_BaseApiPerm):
2293 2326 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2294 2327 user_group_name=None):
2295 2328 if self.required_perms.issubset(perm_defs.get('global')):
2296 2329 return True
2297 2330 return False
2298 2331
2299 2332
2300 2333 class HasPermissionAnyApi(_BaseApiPerm):
2301 2334 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2302 2335 user_group_name=None):
2303 2336 if self.required_perms.intersection(perm_defs.get('global')):
2304 2337 return True
2305 2338 return False
2306 2339
2307 2340
2308 2341 class HasRepoPermissionAllApi(_BaseApiPerm):
2309 2342 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2310 2343 user_group_name=None):
2311 2344 try:
2312 2345 _user_perms = {perm_defs['repositories'][repo_name]}
2313 2346 except KeyError:
2314 2347 log.warning(traceback.format_exc())
2315 2348 return False
2316 2349 if self.required_perms.issubset(_user_perms):
2317 2350 return True
2318 2351 return False
2319 2352
2320 2353
2321 2354 class HasRepoPermissionAnyApi(_BaseApiPerm):
2322 2355 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2323 2356 user_group_name=None):
2324 2357 try:
2325 2358 _user_perms = {perm_defs['repositories'][repo_name]}
2326 2359 except KeyError:
2327 2360 log.warning(traceback.format_exc())
2328 2361 return False
2329 2362 if self.required_perms.intersection(_user_perms):
2330 2363 return True
2331 2364 return False
2332 2365
2333 2366
2334 2367 class HasRepoGroupPermissionAnyApi(_BaseApiPerm):
2335 2368 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2336 2369 user_group_name=None):
2337 2370 try:
2338 2371 _user_perms = {perm_defs['repositories_groups'][group_name]}
2339 2372 except KeyError:
2340 2373 log.warning(traceback.format_exc())
2341 2374 return False
2342 2375 if self.required_perms.intersection(_user_perms):
2343 2376 return True
2344 2377 return False
2345 2378
2346 2379
2347 2380 class HasRepoGroupPermissionAllApi(_BaseApiPerm):
2348 2381 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2349 2382 user_group_name=None):
2350 2383 try:
2351 2384 _user_perms = {perm_defs['repositories_groups'][group_name]}
2352 2385 except KeyError:
2353 2386 log.warning(traceback.format_exc())
2354 2387 return False
2355 2388 if self.required_perms.issubset(_user_perms):
2356 2389 return True
2357 2390 return False
2358 2391
2359 2392
2360 2393 class HasUserGroupPermissionAnyApi(_BaseApiPerm):
2361 2394 def check_permissions(self, perm_defs, repo_name=None, group_name=None,
2362 2395 user_group_name=None):
2363 2396 try:
2364 2397 _user_perms = {perm_defs['user_groups'][user_group_name]}
2365 2398 except KeyError:
2366 2399 log.warning(traceback.format_exc())
2367 2400 return False
2368 2401 if self.required_perms.intersection(_user_perms):
2369 2402 return True
2370 2403 return False
2371 2404
2372 2405
2373 2406 def check_ip_access(source_ip, allowed_ips=None):
2374 2407 """
2375 2408 Checks if source_ip is a subnet of any of allowed_ips.
2376 2409
2377 2410 :param source_ip:
2378 2411 :param allowed_ips: list of allowed ips together with mask
2379 2412 """
2380 2413 log.debug('checking if ip:%s is subnet of %s', source_ip, allowed_ips)
2381 2414 source_ip_address = ipaddress.ip_address(safe_unicode(source_ip))
2382 2415 if isinstance(allowed_ips, (tuple, list, set)):
2383 2416 for ip in allowed_ips:
2384 2417 ip = safe_unicode(ip)
2385 2418 try:
2386 2419 network_address = ipaddress.ip_network(ip, strict=False)
2387 2420 if source_ip_address in network_address:
2388 2421 log.debug('IP %s is network %s', source_ip_address, network_address)
2389 2422 return True
2390 2423 # for any case we cannot determine the IP, don't crash just
2391 2424 # skip it and log as error, we want to say forbidden still when
2392 2425 # sending bad IP
2393 2426 except Exception:
2394 2427 log.error(traceback.format_exc())
2395 2428 continue
2396 2429 return False
2397 2430
2398 2431
2399 2432 def get_cython_compat_decorator(wrapper, func):
2400 2433 """
2401 2434 Creates a cython compatible decorator. The previously used
2402 2435 decorator.decorator() function seems to be incompatible with cython.
2403 2436
2404 2437 :param wrapper: __wrapper method of the decorator class
2405 2438 :param func: decorated function
2406 2439 """
2407 2440 @wraps(func)
2408 2441 def local_wrapper(*args, **kwds):
2409 2442 return wrapper(func, *args, **kwds)
2410 2443 local_wrapper.__wrapped__ = func
2411 2444 return local_wrapper
2412 2445
2413 2446
@@ -1,5517 +1,5576 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2019 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 25 import re
26 26 import os
27 27 import time
28 28 import string
29 29 import hashlib
30 30 import logging
31 31 import datetime
32 32 import uuid
33 33 import warnings
34 34 import ipaddress
35 35 import functools
36 36 import traceback
37 37 import collections
38 38
39 39 from sqlalchemy import (
40 40 or_, and_, not_, func, cast, TypeDecorator, event,
41 41 Index, Sequence, UniqueConstraint, ForeignKey, CheckConstraint, Column,
42 42 Boolean, String, Unicode, UnicodeText, DateTime, Integer, LargeBinary,
43 43 Text, Float, PickleType, BigInteger)
44 44 from sqlalchemy.sql.expression import true, false, case
45 45 from sqlalchemy.sql.functions import coalesce, count # pragma: no cover
46 46 from sqlalchemy.orm import (
47 47 relationship, joinedload, class_mapper, validates, aliased)
48 48 from sqlalchemy.ext.declarative import declared_attr
49 49 from sqlalchemy.ext.hybrid import hybrid_property
50 50 from sqlalchemy.exc import IntegrityError # pragma: no cover
51 51 from sqlalchemy.dialects.mysql import LONGTEXT
52 52 from zope.cachedescriptors.property import Lazy as LazyProperty
53 53 from pyramid import compat
54 54 from pyramid.threadlocal import get_current_request
55 55 from webhelpers2.text import remove_formatting
56 56
57 57 from rhodecode.translation import _
58 58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 59 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
60 60 from rhodecode.lib.utils2 import (
61 61 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
62 62 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
63 63 glob2re, StrictAttributeDict, cleaned_uri, datetime_to_time, OrderedDefaultDict)
64 64 from rhodecode.lib.jsonalchemy import MutationObj, MutationList, JsonType, \
65 65 JsonRaw
66 66 from rhodecode.lib.ext_json import json
67 67 from rhodecode.lib.caching_query import FromCache
68 68 from rhodecode.lib.encrypt import AESCipher, validate_and_get_enc_data
69 69 from rhodecode.lib.encrypt2 import Encryptor
70 70 from rhodecode.lib.exceptions import (
71 71 ArtifactMetadataDuplicate, ArtifactMetadataBadValueType)
72 72 from rhodecode.model.meta import Base, Session
73 73
74 74 URL_SEP = '/'
75 75 log = logging.getLogger(__name__)
76 76
77 77 # =============================================================================
78 78 # BASE CLASSES
79 79 # =============================================================================
80 80
81 81 # this is propagated from .ini file rhodecode.encrypted_values.secret or
82 82 # beaker.session.secret if first is not set.
83 83 # and initialized at environment.py
84 84 ENCRYPTION_KEY = None
85 85
86 86 # used to sort permissions by types, '#' used here is not allowed to be in
87 87 # usernames, and it's very early in sorted string.printable table.
88 88 PERMISSION_TYPE_SORT = {
89 89 'admin': '####',
90 90 'write': '###',
91 91 'read': '##',
92 92 'none': '#',
93 93 }
94 94
95 95
96 96 def display_user_sort(obj):
97 97 """
98 98 Sort function used to sort permissions in .permissions() function of
99 99 Repository, RepoGroup, UserGroup. Also it put the default user in front
100 100 of all other resources
101 101 """
102 102
103 103 if obj.username == User.DEFAULT_USER:
104 104 return '#####'
105 105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 106 return prefix + obj.username
107 107
108 108
109 109 def display_user_group_sort(obj):
110 110 """
111 111 Sort function used to sort permissions in .permissions() function of
112 112 Repository, RepoGroup, UserGroup. Also it put the default user in front
113 113 of all other resources
114 114 """
115 115
116 116 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
117 117 return prefix + obj.users_group_name
118 118
119 119
120 120 def _hash_key(k):
121 121 return sha1_safe(k)
122 122
123 123
124 124 def in_filter_generator(qry, items, limit=500):
125 125 """
126 126 Splits IN() into multiple with OR
127 127 e.g.::
128 128 cnt = Repository.query().filter(
129 129 or_(
130 130 *in_filter_generator(Repository.repo_id, range(100000))
131 131 )).count()
132 132 """
133 133 if not items:
134 134 # empty list will cause empty query which might cause security issues
135 135 # this can lead to hidden unpleasant results
136 136 items = [-1]
137 137
138 138 parts = []
139 139 for chunk in xrange(0, len(items), limit):
140 140 parts.append(
141 141 qry.in_(items[chunk: chunk + limit])
142 142 )
143 143
144 144 return parts
145 145
146 146
147 147 base_table_args = {
148 148 'extend_existing': True,
149 149 'mysql_engine': 'InnoDB',
150 150 'mysql_charset': 'utf8',
151 151 'sqlite_autoincrement': True
152 152 }
153 153
154 154
155 155 class EncryptedTextValue(TypeDecorator):
156 156 """
157 157 Special column for encrypted long text data, use like::
158 158
159 159 value = Column("encrypted_value", EncryptedValue(), nullable=False)
160 160
161 161 This column is intelligent so if value is in unencrypted form it return
162 162 unencrypted form, but on save it always encrypts
163 163 """
164 164 impl = Text
165 165
166 166 def process_bind_param(self, value, dialect):
167 167 """
168 168 Setter for storing value
169 169 """
170 170 import rhodecode
171 171 if not value:
172 172 return value
173 173
174 174 # protect against double encrypting if values is already encrypted
175 175 if value.startswith('enc$aes$') \
176 176 or value.startswith('enc$aes_hmac$') \
177 177 or value.startswith('enc2$'):
178 178 raise ValueError('value needs to be in unencrypted format, '
179 179 'ie. not starting with enc$ or enc2$')
180 180
181 181 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
182 182 if algo == 'aes':
183 183 return 'enc$aes_hmac$%s' % AESCipher(ENCRYPTION_KEY, hmac=True).encrypt(value)
184 184 elif algo == 'fernet':
185 185 return Encryptor(ENCRYPTION_KEY).encrypt(value)
186 186 else:
187 187 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
188 188
189 189 def process_result_value(self, value, dialect):
190 190 """
191 191 Getter for retrieving value
192 192 """
193 193
194 194 import rhodecode
195 195 if not value:
196 196 return value
197 197
198 198 algo = rhodecode.CONFIG.get('rhodecode.encrypted_values.algorithm') or 'aes'
199 199 enc_strict_mode = str2bool(rhodecode.CONFIG.get('rhodecode.encrypted_values.strict') or True)
200 200 if algo == 'aes':
201 201 decrypted_data = validate_and_get_enc_data(value, ENCRYPTION_KEY, enc_strict_mode)
202 202 elif algo == 'fernet':
203 203 return Encryptor(ENCRYPTION_KEY).decrypt(value)
204 204 else:
205 205 ValueError('Bad encryption algorithm, should be fernet or aes, got: {}'.format(algo))
206 206 return decrypted_data
207 207
208 208
209 209 class BaseModel(object):
210 210 """
211 211 Base Model for all classes
212 212 """
213 213
214 214 @classmethod
215 215 def _get_keys(cls):
216 216 """return column names for this model """
217 217 return class_mapper(cls).c.keys()
218 218
219 219 def get_dict(self):
220 220 """
221 221 return dict with keys and values corresponding
222 222 to this model data """
223 223
224 224 d = {}
225 225 for k in self._get_keys():
226 226 d[k] = getattr(self, k)
227 227
228 228 # also use __json__() if present to get additional fields
229 229 _json_attr = getattr(self, '__json__', None)
230 230 if _json_attr:
231 231 # update with attributes from __json__
232 232 if callable(_json_attr):
233 233 _json_attr = _json_attr()
234 234 for k, val in _json_attr.iteritems():
235 235 d[k] = val
236 236 return d
237 237
238 238 def get_appstruct(self):
239 239 """return list with keys and values tuples corresponding
240 240 to this model data """
241 241
242 242 lst = []
243 243 for k in self._get_keys():
244 244 lst.append((k, getattr(self, k),))
245 245 return lst
246 246
247 247 def populate_obj(self, populate_dict):
248 248 """populate model with data from given populate_dict"""
249 249
250 250 for k in self._get_keys():
251 251 if k in populate_dict:
252 252 setattr(self, k, populate_dict[k])
253 253
254 254 @classmethod
255 255 def query(cls):
256 256 return Session().query(cls)
257 257
258 258 @classmethod
259 259 def get(cls, id_):
260 260 if id_:
261 261 return cls.query().get(id_)
262 262
263 263 @classmethod
264 264 def get_or_404(cls, id_):
265 265 from pyramid.httpexceptions import HTTPNotFound
266 266
267 267 try:
268 268 id_ = int(id_)
269 269 except (TypeError, ValueError):
270 270 raise HTTPNotFound()
271 271
272 272 res = cls.query().get(id_)
273 273 if not res:
274 274 raise HTTPNotFound()
275 275 return res
276 276
277 277 @classmethod
278 278 def getAll(cls):
279 279 # deprecated and left for backward compatibility
280 280 return cls.get_all()
281 281
282 282 @classmethod
283 283 def get_all(cls):
284 284 return cls.query().all()
285 285
286 286 @classmethod
287 287 def delete(cls, id_):
288 288 obj = cls.query().get(id_)
289 289 Session().delete(obj)
290 290
291 291 @classmethod
292 292 def identity_cache(cls, session, attr_name, value):
293 293 exist_in_session = []
294 294 for (item_cls, pkey), instance in session.identity_map.items():
295 295 if cls == item_cls and getattr(instance, attr_name) == value:
296 296 exist_in_session.append(instance)
297 297 if exist_in_session:
298 298 if len(exist_in_session) == 1:
299 299 return exist_in_session[0]
300 300 log.exception(
301 301 'multiple objects with attr %s and '
302 302 'value %s found with same name: %r',
303 303 attr_name, value, exist_in_session)
304 304
305 305 def __repr__(self):
306 306 if hasattr(self, '__unicode__'):
307 307 # python repr needs to return str
308 308 try:
309 309 return safe_str(self.__unicode__())
310 310 except UnicodeDecodeError:
311 311 pass
312 312 return '<DB:%s>' % (self.__class__.__name__)
313 313
314 314
315 315 class RhodeCodeSetting(Base, BaseModel):
316 316 __tablename__ = 'rhodecode_settings'
317 317 __table_args__ = (
318 318 UniqueConstraint('app_settings_name'),
319 319 base_table_args
320 320 )
321 321
322 322 SETTINGS_TYPES = {
323 323 'str': safe_str,
324 324 'int': safe_int,
325 325 'unicode': safe_unicode,
326 326 'bool': str2bool,
327 327 'list': functools.partial(aslist, sep=',')
328 328 }
329 329 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
330 330 GLOBAL_CONF_KEY = 'app_settings'
331 331
332 332 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
333 333 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
334 334 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
335 335 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
336 336
337 337 def __init__(self, key='', val='', type='unicode'):
338 338 self.app_settings_name = key
339 339 self.app_settings_type = type
340 340 self.app_settings_value = val
341 341
342 342 @validates('_app_settings_value')
343 343 def validate_settings_value(self, key, val):
344 344 assert type(val) == unicode
345 345 return val
346 346
347 347 @hybrid_property
348 348 def app_settings_value(self):
349 349 v = self._app_settings_value
350 350 _type = self.app_settings_type
351 351 if _type:
352 352 _type = self.app_settings_type.split('.')[0]
353 353 # decode the encrypted value
354 354 if 'encrypted' in self.app_settings_type:
355 355 cipher = EncryptedTextValue()
356 356 v = safe_unicode(cipher.process_result_value(v, None))
357 357
358 358 converter = self.SETTINGS_TYPES.get(_type) or \
359 359 self.SETTINGS_TYPES['unicode']
360 360 return converter(v)
361 361
362 362 @app_settings_value.setter
363 363 def app_settings_value(self, val):
364 364 """
365 365 Setter that will always make sure we use unicode in app_settings_value
366 366
367 367 :param val:
368 368 """
369 369 val = safe_unicode(val)
370 370 # encode the encrypted value
371 371 if 'encrypted' in self.app_settings_type:
372 372 cipher = EncryptedTextValue()
373 373 val = safe_unicode(cipher.process_bind_param(val, None))
374 374 self._app_settings_value = val
375 375
376 376 @hybrid_property
377 377 def app_settings_type(self):
378 378 return self._app_settings_type
379 379
380 380 @app_settings_type.setter
381 381 def app_settings_type(self, val):
382 382 if val.split('.')[0] not in self.SETTINGS_TYPES:
383 383 raise Exception('type must be one of %s got %s'
384 384 % (self.SETTINGS_TYPES.keys(), val))
385 385 self._app_settings_type = val
386 386
387 387 @classmethod
388 388 def get_by_prefix(cls, prefix):
389 389 return RhodeCodeSetting.query()\
390 390 .filter(RhodeCodeSetting.app_settings_name.startswith(prefix))\
391 391 .all()
392 392
393 393 def __unicode__(self):
394 394 return u"<%s('%s:%s[%s]')>" % (
395 395 self.__class__.__name__,
396 396 self.app_settings_name, self.app_settings_value,
397 397 self.app_settings_type
398 398 )
399 399
400 400
401 401 class RhodeCodeUi(Base, BaseModel):
402 402 __tablename__ = 'rhodecode_ui'
403 403 __table_args__ = (
404 404 UniqueConstraint('ui_key'),
405 405 base_table_args
406 406 )
407 407
408 408 HOOK_REPO_SIZE = 'changegroup.repo_size'
409 409 # HG
410 410 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
411 411 HOOK_PULL = 'outgoing.pull_logger'
412 412 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
413 413 HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push'
414 414 HOOK_PUSH = 'changegroup.push_logger'
415 415 HOOK_PUSH_KEY = 'pushkey.key_push'
416 416
417 417 HOOKS_BUILTIN = [
418 418 HOOK_PRE_PULL,
419 419 HOOK_PULL,
420 420 HOOK_PRE_PUSH,
421 421 HOOK_PRETX_PUSH,
422 422 HOOK_PUSH,
423 423 HOOK_PUSH_KEY,
424 424 ]
425 425
426 426 # TODO: johbo: Unify way how hooks are configured for git and hg,
427 427 # git part is currently hardcoded.
428 428
429 429 # SVN PATTERNS
430 430 SVN_BRANCH_ID = 'vcs_svn_branch'
431 431 SVN_TAG_ID = 'vcs_svn_tag'
432 432
433 433 ui_id = Column(
434 434 "ui_id", Integer(), nullable=False, unique=True, default=None,
435 435 primary_key=True)
436 436 ui_section = Column(
437 437 "ui_section", String(255), nullable=True, unique=None, default=None)
438 438 ui_key = Column(
439 439 "ui_key", String(255), nullable=True, unique=None, default=None)
440 440 ui_value = Column(
441 441 "ui_value", String(255), nullable=True, unique=None, default=None)
442 442 ui_active = Column(
443 443 "ui_active", Boolean(), nullable=True, unique=None, default=True)
444 444
445 445 def __repr__(self):
446 446 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
447 447 self.ui_key, self.ui_value)
448 448
449 449
450 450 class RepoRhodeCodeSetting(Base, BaseModel):
451 451 __tablename__ = 'repo_rhodecode_settings'
452 452 __table_args__ = (
453 453 UniqueConstraint(
454 454 'app_settings_name', 'repository_id',
455 455 name='uq_repo_rhodecode_setting_name_repo_id'),
456 456 base_table_args
457 457 )
458 458
459 459 repository_id = Column(
460 460 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
461 461 nullable=False)
462 462 app_settings_id = Column(
463 463 "app_settings_id", Integer(), nullable=False, unique=True,
464 464 default=None, primary_key=True)
465 465 app_settings_name = Column(
466 466 "app_settings_name", String(255), nullable=True, unique=None,
467 467 default=None)
468 468 _app_settings_value = Column(
469 469 "app_settings_value", String(4096), nullable=True, unique=None,
470 470 default=None)
471 471 _app_settings_type = Column(
472 472 "app_settings_type", String(255), nullable=True, unique=None,
473 473 default=None)
474 474
475 475 repository = relationship('Repository')
476 476
477 477 def __init__(self, repository_id, key='', val='', type='unicode'):
478 478 self.repository_id = repository_id
479 479 self.app_settings_name = key
480 480 self.app_settings_type = type
481 481 self.app_settings_value = val
482 482
483 483 @validates('_app_settings_value')
484 484 def validate_settings_value(self, key, val):
485 485 assert type(val) == unicode
486 486 return val
487 487
488 488 @hybrid_property
489 489 def app_settings_value(self):
490 490 v = self._app_settings_value
491 491 type_ = self.app_settings_type
492 492 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
493 493 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
494 494 return converter(v)
495 495
496 496 @app_settings_value.setter
497 497 def app_settings_value(self, val):
498 498 """
499 499 Setter that will always make sure we use unicode in app_settings_value
500 500
501 501 :param val:
502 502 """
503 503 self._app_settings_value = safe_unicode(val)
504 504
505 505 @hybrid_property
506 506 def app_settings_type(self):
507 507 return self._app_settings_type
508 508
509 509 @app_settings_type.setter
510 510 def app_settings_type(self, val):
511 511 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
512 512 if val not in SETTINGS_TYPES:
513 513 raise Exception('type must be one of %s got %s'
514 514 % (SETTINGS_TYPES.keys(), val))
515 515 self._app_settings_type = val
516 516
517 517 def __unicode__(self):
518 518 return u"<%s('%s:%s:%s[%s]')>" % (
519 519 self.__class__.__name__, self.repository.repo_name,
520 520 self.app_settings_name, self.app_settings_value,
521 521 self.app_settings_type
522 522 )
523 523
524 524
525 525 class RepoRhodeCodeUi(Base, BaseModel):
526 526 __tablename__ = 'repo_rhodecode_ui'
527 527 __table_args__ = (
528 528 UniqueConstraint(
529 529 'repository_id', 'ui_section', 'ui_key',
530 530 name='uq_repo_rhodecode_ui_repository_id_section_key'),
531 531 base_table_args
532 532 )
533 533
534 534 repository_id = Column(
535 535 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
536 536 nullable=False)
537 537 ui_id = Column(
538 538 "ui_id", Integer(), nullable=False, unique=True, default=None,
539 539 primary_key=True)
540 540 ui_section = Column(
541 541 "ui_section", String(255), nullable=True, unique=None, default=None)
542 542 ui_key = Column(
543 543 "ui_key", String(255), nullable=True, unique=None, default=None)
544 544 ui_value = Column(
545 545 "ui_value", String(255), nullable=True, unique=None, default=None)
546 546 ui_active = Column(
547 547 "ui_active", Boolean(), nullable=True, unique=None, default=True)
548 548
549 549 repository = relationship('Repository')
550 550
551 551 def __repr__(self):
552 552 return '<%s[%s:%s]%s=>%s]>' % (
553 553 self.__class__.__name__, self.repository.repo_name,
554 554 self.ui_section, self.ui_key, self.ui_value)
555 555
556 556
557 557 class User(Base, BaseModel):
558 558 __tablename__ = 'users'
559 559 __table_args__ = (
560 560 UniqueConstraint('username'), UniqueConstraint('email'),
561 561 Index('u_username_idx', 'username'),
562 562 Index('u_email_idx', 'email'),
563 563 base_table_args
564 564 )
565 565
566 566 DEFAULT_USER = 'default'
567 567 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
568 568 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
569 569
570 570 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
571 571 username = Column("username", String(255), nullable=True, unique=None, default=None)
572 572 password = Column("password", String(255), nullable=True, unique=None, default=None)
573 573 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
574 574 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
575 575 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
576 576 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
577 577 _email = Column("email", String(255), nullable=True, unique=None, default=None)
578 578 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
579 579 last_activity = Column('last_activity', DateTime(timezone=False), nullable=True, unique=None, default=None)
580 580 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
581 581
582 582 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
583 583 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
584 584 _api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
585 585 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
586 586 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
587 587 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
588 588
589 589 user_log = relationship('UserLog')
590 590 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all, delete-orphan')
591 591
592 592 repositories = relationship('Repository')
593 593 repository_groups = relationship('RepoGroup')
594 594 user_groups = relationship('UserGroup')
595 595
596 596 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
597 597 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
598 598
599 599 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all, delete-orphan')
600 600 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
601 601 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all, delete-orphan')
602 602
603 603 group_member = relationship('UserGroupMember', cascade='all')
604 604
605 605 notifications = relationship('UserNotification', cascade='all')
606 606 # notifications assigned to this user
607 607 user_created_notifications = relationship('Notification', cascade='all')
608 608 # comments created by this user
609 609 user_comments = relationship('ChangesetComment', cascade='all')
610 610 # user profile extra info
611 611 user_emails = relationship('UserEmailMap', cascade='all')
612 612 user_ip_map = relationship('UserIpMap', cascade='all')
613 613 user_auth_tokens = relationship('UserApiKeys', cascade='all')
614 614 user_ssh_keys = relationship('UserSshKeys', cascade='all')
615 615
616 616 # gists
617 617 user_gists = relationship('Gist', cascade='all')
618 618 # user pull requests
619 619 user_pull_requests = relationship('PullRequest', cascade='all')
620 620 # external identities
621 621 external_identities = relationship(
622 622 'ExternalIdentity',
623 623 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
624 624 cascade='all')
625 625 # review rules
626 626 user_review_rules = relationship('RepoReviewRuleUser', cascade='all')
627 627
628 628 # artifacts owned
629 629 artifacts = relationship('FileStore', primaryjoin='FileStore.user_id==User.user_id')
630 630
631 631 # no cascade, set NULL
632 632 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_user_id==User.user_id')
633 633
634 634 def __unicode__(self):
635 635 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
636 636 self.user_id, self.username)
637 637
638 638 @hybrid_property
639 639 def email(self):
640 640 return self._email
641 641
642 642 @email.setter
643 643 def email(self, val):
644 644 self._email = val.lower() if val else None
645 645
646 646 @hybrid_property
647 647 def first_name(self):
648 648 from rhodecode.lib import helpers as h
649 649 if self.name:
650 650 return h.escape(self.name)
651 651 return self.name
652 652
653 653 @hybrid_property
654 654 def last_name(self):
655 655 from rhodecode.lib import helpers as h
656 656 if self.lastname:
657 657 return h.escape(self.lastname)
658 658 return self.lastname
659 659
660 660 @hybrid_property
661 661 def api_key(self):
662 662 """
663 663 Fetch if exist an auth-token with role ALL connected to this user
664 664 """
665 665 user_auth_token = UserApiKeys.query()\
666 666 .filter(UserApiKeys.user_id == self.user_id)\
667 667 .filter(or_(UserApiKeys.expires == -1,
668 668 UserApiKeys.expires >= time.time()))\
669 669 .filter(UserApiKeys.role == UserApiKeys.ROLE_ALL).first()
670 670 if user_auth_token:
671 671 user_auth_token = user_auth_token.api_key
672 672
673 673 return user_auth_token
674 674
675 675 @api_key.setter
676 676 def api_key(self, val):
677 677 # don't allow to set API key this is deprecated for now
678 678 self._api_key = None
679 679
680 680 @property
681 681 def reviewer_pull_requests(self):
682 682 return PullRequestReviewers.query() \
683 683 .options(joinedload(PullRequestReviewers.pull_request)) \
684 684 .filter(PullRequestReviewers.user_id == self.user_id) \
685 685 .all()
686 686
687 687 @property
688 688 def firstname(self):
689 689 # alias for future
690 690 return self.name
691 691
692 692 @property
693 693 def emails(self):
694 694 other = UserEmailMap.query()\
695 695 .filter(UserEmailMap.user == self) \
696 696 .order_by(UserEmailMap.email_id.asc()) \
697 697 .all()
698 698 return [self.email] + [x.email for x in other]
699 699
700 700 def emails_cached(self):
701 701 emails = UserEmailMap.query()\
702 702 .filter(UserEmailMap.user == self) \
703 703 .order_by(UserEmailMap.email_id.asc())
704 704
705 705 emails = emails.options(
706 706 FromCache("sql_cache_short", "get_user_{}_emails".format(self.user_id))
707 707 )
708 708
709 709 return [self.email] + [x.email for x in emails]
710 710
711 711 @property
712 712 def auth_tokens(self):
713 713 auth_tokens = self.get_auth_tokens()
714 714 return [x.api_key for x in auth_tokens]
715 715
716 716 def get_auth_tokens(self):
717 717 return UserApiKeys.query()\
718 718 .filter(UserApiKeys.user == self)\
719 719 .order_by(UserApiKeys.user_api_key_id.asc())\
720 720 .all()
721 721
722 722 @LazyProperty
723 723 def feed_token(self):
724 724 return self.get_feed_token()
725 725
726 726 def get_feed_token(self, cache=True):
727 727 feed_tokens = UserApiKeys.query()\
728 728 .filter(UserApiKeys.user == self)\
729 729 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)
730 730 if cache:
731 731 feed_tokens = feed_tokens.options(
732 732 FromCache("sql_cache_short", "get_user_feed_token_%s" % self.user_id))
733 733
734 734 feed_tokens = feed_tokens.all()
735 735 if feed_tokens:
736 736 return feed_tokens[0].api_key
737 737 return 'NO_FEED_TOKEN_AVAILABLE'
738 738
739 739 @LazyProperty
740 740 def artifact_token(self):
741 741 return self.get_artifact_token()
742 742
743 743 def get_artifact_token(self, cache=True):
744 744 artifacts_tokens = UserApiKeys.query()\
745 745 .filter(UserApiKeys.user == self)\
746 746 .filter(UserApiKeys.role == UserApiKeys.ROLE_ARTIFACT_DOWNLOAD)
747 747 if cache:
748 748 artifacts_tokens = artifacts_tokens.options(
749 749 FromCache("sql_cache_short", "get_user_artifact_token_%s" % self.user_id))
750 750
751 751 artifacts_tokens = artifacts_tokens.all()
752 752 if artifacts_tokens:
753 753 return artifacts_tokens[0].api_key
754 754 return 'NO_ARTIFACT_TOKEN_AVAILABLE'
755 755
756 756 @classmethod
757 757 def get(cls, user_id, cache=False):
758 758 if not user_id:
759 759 return
760 760
761 761 user = cls.query()
762 762 if cache:
763 763 user = user.options(
764 764 FromCache("sql_cache_short", "get_users_%s" % user_id))
765 765 return user.get(user_id)
766 766
767 767 @classmethod
768 768 def extra_valid_auth_tokens(cls, user, role=None):
769 769 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
770 770 .filter(or_(UserApiKeys.expires == -1,
771 771 UserApiKeys.expires >= time.time()))
772 772 if role:
773 773 tokens = tokens.filter(or_(UserApiKeys.role == role,
774 774 UserApiKeys.role == UserApiKeys.ROLE_ALL))
775 775 return tokens.all()
776 776
777 777 def authenticate_by_token(self, auth_token, roles=None, scope_repo_id=None):
778 778 from rhodecode.lib import auth
779 779
780 780 log.debug('Trying to authenticate user: %s via auth-token, '
781 781 'and roles: %s', self, roles)
782 782
783 783 if not auth_token:
784 784 return False
785 785
786 786 roles = (roles or []) + [UserApiKeys.ROLE_ALL]
787 787 tokens_q = UserApiKeys.query()\
788 788 .filter(UserApiKeys.user_id == self.user_id)\
789 789 .filter(or_(UserApiKeys.expires == -1,
790 790 UserApiKeys.expires >= time.time()))
791 791
792 792 tokens_q = tokens_q.filter(UserApiKeys.role.in_(roles))
793 793
794 794 crypto_backend = auth.crypto_backend()
795 795 enc_token_map = {}
796 796 plain_token_map = {}
797 797 for token in tokens_q:
798 798 if token.api_key.startswith(crypto_backend.ENC_PREF):
799 799 enc_token_map[token.api_key] = token
800 800 else:
801 801 plain_token_map[token.api_key] = token
802 802 log.debug(
803 803 'Found %s plain and %s encrypted tokens to check for authentication for this user',
804 804 len(plain_token_map), len(enc_token_map))
805 805
806 806 # plain token match comes first
807 807 match = plain_token_map.get(auth_token)
808 808
809 809 # check encrypted tokens now
810 810 if not match:
811 811 for token_hash, token in enc_token_map.items():
812 812 # NOTE(marcink): this is expensive to calculate, but most secure
813 813 if crypto_backend.hash_check(auth_token, token_hash):
814 814 match = token
815 815 break
816 816
817 817 if match:
818 818 log.debug('Found matching token %s', match)
819 819 if match.repo_id:
820 820 log.debug('Found scope, checking for scope match of token %s', match)
821 821 if match.repo_id == scope_repo_id:
822 822 return True
823 823 else:
824 824 log.debug(
825 825 'AUTH_TOKEN: scope mismatch, token has a set repo scope: %s, '
826 826 'and calling scope is:%s, skipping further checks',
827 827 match.repo, scope_repo_id)
828 828 return False
829 829 else:
830 830 return True
831 831
832 832 return False
833 833
834 834 @property
835 835 def ip_addresses(self):
836 836 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
837 837 return [x.ip_addr for x in ret]
838 838
839 839 @property
840 840 def username_and_name(self):
841 841 return '%s (%s %s)' % (self.username, self.first_name, self.last_name)
842 842
843 843 @property
844 844 def username_or_name_or_email(self):
845 845 full_name = self.full_name if self.full_name is not ' ' else None
846 846 return self.username or full_name or self.email
847 847
848 848 @property
849 849 def full_name(self):
850 850 return '%s %s' % (self.first_name, self.last_name)
851 851
852 852 @property
853 853 def full_name_or_username(self):
854 854 return ('%s %s' % (self.first_name, self.last_name)
855 855 if (self.first_name and self.last_name) else self.username)
856 856
857 857 @property
858 858 def full_contact(self):
859 859 return '%s %s <%s>' % (self.first_name, self.last_name, self.email)
860 860
861 861 @property
862 862 def short_contact(self):
863 863 return '%s %s' % (self.first_name, self.last_name)
864 864
865 865 @property
866 866 def is_admin(self):
867 867 return self.admin
868 868
869 869 @property
870 870 def language(self):
871 871 return self.user_data.get('language')
872 872
873 873 def AuthUser(self, **kwargs):
874 874 """
875 875 Returns instance of AuthUser for this user
876 876 """
877 877 from rhodecode.lib.auth import AuthUser
878 878 return AuthUser(user_id=self.user_id, username=self.username, **kwargs)
879 879
880 880 @hybrid_property
881 881 def user_data(self):
882 882 if not self._user_data:
883 883 return {}
884 884
885 885 try:
886 886 return json.loads(self._user_data)
887 887 except TypeError:
888 888 return {}
889 889
890 890 @user_data.setter
891 891 def user_data(self, val):
892 892 if not isinstance(val, dict):
893 893 raise Exception('user_data must be dict, got %s' % type(val))
894 894 try:
895 895 self._user_data = json.dumps(val)
896 896 except Exception:
897 897 log.error(traceback.format_exc())
898 898
899 899 @classmethod
900 900 def get_by_username(cls, username, case_insensitive=False,
901 901 cache=False, identity_cache=False):
902 902 session = Session()
903 903
904 904 if case_insensitive:
905 905 q = cls.query().filter(
906 906 func.lower(cls.username) == func.lower(username))
907 907 else:
908 908 q = cls.query().filter(cls.username == username)
909 909
910 910 if cache:
911 911 if identity_cache:
912 912 val = cls.identity_cache(session, 'username', username)
913 913 if val:
914 914 return val
915 915 else:
916 916 cache_key = "get_user_by_name_%s" % _hash_key(username)
917 917 q = q.options(
918 918 FromCache("sql_cache_short", cache_key))
919 919
920 920 return q.scalar()
921 921
922 922 @classmethod
923 923 def get_by_auth_token(cls, auth_token, cache=False):
924 924 q = UserApiKeys.query()\
925 925 .filter(UserApiKeys.api_key == auth_token)\
926 926 .filter(or_(UserApiKeys.expires == -1,
927 927 UserApiKeys.expires >= time.time()))
928 928 if cache:
929 929 q = q.options(
930 930 FromCache("sql_cache_short", "get_auth_token_%s" % auth_token))
931 931
932 932 match = q.first()
933 933 if match:
934 934 return match.user
935 935
936 936 @classmethod
937 937 def get_by_email(cls, email, case_insensitive=False, cache=False):
938 938
939 939 if case_insensitive:
940 940 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
941 941
942 942 else:
943 943 q = cls.query().filter(cls.email == email)
944 944
945 945 email_key = _hash_key(email)
946 946 if cache:
947 947 q = q.options(
948 948 FromCache("sql_cache_short", "get_email_key_%s" % email_key))
949 949
950 950 ret = q.scalar()
951 951 if ret is None:
952 952 q = UserEmailMap.query()
953 953 # try fetching in alternate email map
954 954 if case_insensitive:
955 955 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
956 956 else:
957 957 q = q.filter(UserEmailMap.email == email)
958 958 q = q.options(joinedload(UserEmailMap.user))
959 959 if cache:
960 960 q = q.options(
961 961 FromCache("sql_cache_short", "get_email_map_key_%s" % email_key))
962 962 ret = getattr(q.scalar(), 'user', None)
963 963
964 964 return ret
965 965
966 966 @classmethod
967 967 def get_from_cs_author(cls, author):
968 968 """
969 969 Tries to get User objects out of commit author string
970 970
971 971 :param author:
972 972 """
973 973 from rhodecode.lib.helpers import email, author_name
974 974 # Valid email in the attribute passed, see if they're in the system
975 975 _email = email(author)
976 976 if _email:
977 977 user = cls.get_by_email(_email, case_insensitive=True)
978 978 if user:
979 979 return user
980 980 # Maybe we can match by username?
981 981 _author = author_name(author)
982 982 user = cls.get_by_username(_author, case_insensitive=True)
983 983 if user:
984 984 return user
985 985
986 986 def update_userdata(self, **kwargs):
987 987 usr = self
988 988 old = usr.user_data
989 989 old.update(**kwargs)
990 990 usr.user_data = old
991 991 Session().add(usr)
992 992 log.debug('updated userdata with %s', kwargs)
993 993
994 994 def update_lastlogin(self):
995 995 """Update user lastlogin"""
996 996 self.last_login = datetime.datetime.now()
997 997 Session().add(self)
998 998 log.debug('updated user %s lastlogin', self.username)
999 999
1000 1000 def update_password(self, new_password):
1001 1001 from rhodecode.lib.auth import get_crypt_password
1002 1002
1003 1003 self.password = get_crypt_password(new_password)
1004 1004 Session().add(self)
1005 1005
1006 1006 @classmethod
1007 1007 def get_first_super_admin(cls):
1008 1008 user = User.query()\
1009 1009 .filter(User.admin == true()) \
1010 1010 .order_by(User.user_id.asc()) \
1011 1011 .first()
1012 1012
1013 1013 if user is None:
1014 1014 raise Exception('FATAL: Missing administrative account!')
1015 1015 return user
1016 1016
1017 1017 @classmethod
1018 1018 def get_all_super_admins(cls, only_active=False):
1019 1019 """
1020 1020 Returns all admin accounts sorted by username
1021 1021 """
1022 1022 qry = User.query().filter(User.admin == true()).order_by(User.username.asc())
1023 1023 if only_active:
1024 1024 qry = qry.filter(User.active == true())
1025 1025 return qry.all()
1026 1026
1027 1027 @classmethod
1028 1028 def get_all_user_ids(cls, only_active=True):
1029 1029 """
1030 1030 Returns all users IDs
1031 1031 """
1032 1032 qry = Session().query(User.user_id)
1033 1033
1034 1034 if only_active:
1035 1035 qry = qry.filter(User.active == true())
1036 1036 return [x.user_id for x in qry]
1037 1037
1038 1038 @classmethod
1039 1039 def get_default_user(cls, cache=False, refresh=False):
1040 1040 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
1041 1041 if user is None:
1042 1042 raise Exception('FATAL: Missing default account!')
1043 1043 if refresh:
1044 1044 # The default user might be based on outdated state which
1045 1045 # has been loaded from the cache.
1046 1046 # A call to refresh() ensures that the
1047 1047 # latest state from the database is used.
1048 1048 Session().refresh(user)
1049 1049 return user
1050 1050
1051 1051 def _get_default_perms(self, user, suffix=''):
1052 1052 from rhodecode.model.permission import PermissionModel
1053 1053 return PermissionModel().get_default_perms(user.user_perms, suffix)
1054 1054
1055 1055 def get_default_perms(self, suffix=''):
1056 1056 return self._get_default_perms(self, suffix)
1057 1057
1058 1058 def get_api_data(self, include_secrets=False, details='full'):
1059 1059 """
1060 1060 Common function for generating user related data for API
1061 1061
1062 1062 :param include_secrets: By default secrets in the API data will be replaced
1063 1063 by a placeholder value to prevent exposing this data by accident. In case
1064 1064 this data shall be exposed, set this flag to ``True``.
1065 1065
1066 1066 :param details: details can be 'basic|full' basic gives only a subset of
1067 1067 the available user information that includes user_id, name and emails.
1068 1068 """
1069 1069 user = self
1070 1070 user_data = self.user_data
1071 1071 data = {
1072 1072 'user_id': user.user_id,
1073 1073 'username': user.username,
1074 1074 'firstname': user.name,
1075 1075 'lastname': user.lastname,
1076 1076 'description': user.description,
1077 1077 'email': user.email,
1078 1078 'emails': user.emails,
1079 1079 }
1080 1080 if details == 'basic':
1081 1081 return data
1082 1082
1083 1083 auth_token_length = 40
1084 1084 auth_token_replacement = '*' * auth_token_length
1085 1085
1086 1086 extras = {
1087 1087 'auth_tokens': [auth_token_replacement],
1088 1088 'active': user.active,
1089 1089 'admin': user.admin,
1090 1090 'extern_type': user.extern_type,
1091 1091 'extern_name': user.extern_name,
1092 1092 'last_login': user.last_login,
1093 1093 'last_activity': user.last_activity,
1094 1094 'ip_addresses': user.ip_addresses,
1095 1095 'language': user_data.get('language')
1096 1096 }
1097 1097 data.update(extras)
1098 1098
1099 1099 if include_secrets:
1100 1100 data['auth_tokens'] = user.auth_tokens
1101 1101 return data
1102 1102
1103 1103 def __json__(self):
1104 1104 data = {
1105 1105 'full_name': self.full_name,
1106 1106 'full_name_or_username': self.full_name_or_username,
1107 1107 'short_contact': self.short_contact,
1108 1108 'full_contact': self.full_contact,
1109 1109 }
1110 1110 data.update(self.get_api_data())
1111 1111 return data
1112 1112
1113 1113
1114 1114 class UserApiKeys(Base, BaseModel):
1115 1115 __tablename__ = 'user_api_keys'
1116 1116 __table_args__ = (
1117 1117 Index('uak_api_key_idx', 'api_key'),
1118 1118 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
1119 1119 base_table_args
1120 1120 )
1121 1121 __mapper_args__ = {}
1122 1122
1123 1123 # ApiKey role
1124 1124 ROLE_ALL = 'token_role_all'
1125 1125 ROLE_HTTP = 'token_role_http'
1126 1126 ROLE_VCS = 'token_role_vcs'
1127 1127 ROLE_API = 'token_role_api'
1128 1128 ROLE_FEED = 'token_role_feed'
1129 1129 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1130 1130 ROLE_PASSWORD_RESET = 'token_password_reset'
1131 1131
1132 1132 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1133 1133
1134 1134 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1135 1135 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1136 1136 api_key = Column("api_key", String(255), nullable=False, unique=True)
1137 1137 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1138 1138 expires = Column('expires', Float(53), nullable=False)
1139 1139 role = Column('role', String(255), nullable=True)
1140 1140 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1141 1141
1142 1142 # scope columns
1143 1143 repo_id = Column(
1144 1144 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
1145 1145 nullable=True, unique=None, default=None)
1146 1146 repo = relationship('Repository', lazy='joined')
1147 1147
1148 1148 repo_group_id = Column(
1149 1149 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
1150 1150 nullable=True, unique=None, default=None)
1151 1151 repo_group = relationship('RepoGroup', lazy='joined')
1152 1152
1153 1153 user = relationship('User', lazy='joined')
1154 1154
1155 1155 def __unicode__(self):
1156 1156 return u"<%s('%s')>" % (self.__class__.__name__, self.role)
1157 1157
1158 1158 def __json__(self):
1159 1159 data = {
1160 1160 'auth_token': self.api_key,
1161 1161 'role': self.role,
1162 1162 'scope': self.scope_humanized,
1163 1163 'expired': self.expired
1164 1164 }
1165 1165 return data
1166 1166
1167 1167 def get_api_data(self, include_secrets=False):
1168 1168 data = self.__json__()
1169 1169 if include_secrets:
1170 1170 return data
1171 1171 else:
1172 1172 data['auth_token'] = self.token_obfuscated
1173 1173 return data
1174 1174
1175 1175 @hybrid_property
1176 1176 def description_safe(self):
1177 1177 from rhodecode.lib import helpers as h
1178 1178 return h.escape(self.description)
1179 1179
1180 1180 @property
1181 1181 def expired(self):
1182 1182 if self.expires == -1:
1183 1183 return False
1184 1184 return time.time() > self.expires
1185 1185
1186 1186 @classmethod
1187 1187 def _get_role_name(cls, role):
1188 1188 return {
1189 1189 cls.ROLE_ALL: _('all'),
1190 1190 cls.ROLE_HTTP: _('http/web interface'),
1191 1191 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
1192 1192 cls.ROLE_API: _('api calls'),
1193 1193 cls.ROLE_FEED: _('feed access'),
1194 1194 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1195 1195 }.get(role, role)
1196 1196
1197 1197 @property
1198 1198 def role_humanized(self):
1199 1199 return self._get_role_name(self.role)
1200 1200
1201 1201 def _get_scope(self):
1202 1202 if self.repo:
1203 1203 return 'Repository: {}'.format(self.repo.repo_name)
1204 1204 if self.repo_group:
1205 1205 return 'RepositoryGroup: {} (recursive)'.format(self.repo_group.group_name)
1206 1206 return 'Global'
1207 1207
1208 1208 @property
1209 1209 def scope_humanized(self):
1210 1210 return self._get_scope()
1211 1211
1212 1212 @property
1213 1213 def token_obfuscated(self):
1214 1214 if self.api_key:
1215 1215 return self.api_key[:4] + "****"
1216 1216
1217 1217
1218 1218 class UserEmailMap(Base, BaseModel):
1219 1219 __tablename__ = 'user_email_map'
1220 1220 __table_args__ = (
1221 1221 Index('uem_email_idx', 'email'),
1222 1222 UniqueConstraint('email'),
1223 1223 base_table_args
1224 1224 )
1225 1225 __mapper_args__ = {}
1226 1226
1227 1227 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1228 1228 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1229 1229 _email = Column("email", String(255), nullable=True, unique=False, default=None)
1230 1230 user = relationship('User', lazy='joined')
1231 1231
1232 1232 @validates('_email')
1233 1233 def validate_email(self, key, email):
1234 1234 # check if this email is not main one
1235 1235 main_email = Session().query(User).filter(User.email == email).scalar()
1236 1236 if main_email is not None:
1237 1237 raise AttributeError('email %s is present is user table' % email)
1238 1238 return email
1239 1239
1240 1240 @hybrid_property
1241 1241 def email(self):
1242 1242 return self._email
1243 1243
1244 1244 @email.setter
1245 1245 def email(self, val):
1246 1246 self._email = val.lower() if val else None
1247 1247
1248 1248
1249 1249 class UserIpMap(Base, BaseModel):
1250 1250 __tablename__ = 'user_ip_map'
1251 1251 __table_args__ = (
1252 1252 UniqueConstraint('user_id', 'ip_addr'),
1253 1253 base_table_args
1254 1254 )
1255 1255 __mapper_args__ = {}
1256 1256
1257 1257 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1258 1258 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1259 1259 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
1260 1260 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
1261 1261 description = Column("description", String(10000), nullable=True, unique=None, default=None)
1262 1262 user = relationship('User', lazy='joined')
1263 1263
1264 1264 @hybrid_property
1265 1265 def description_safe(self):
1266 1266 from rhodecode.lib import helpers as h
1267 1267 return h.escape(self.description)
1268 1268
1269 1269 @classmethod
1270 1270 def _get_ip_range(cls, ip_addr):
1271 1271 net = ipaddress.ip_network(safe_unicode(ip_addr), strict=False)
1272 1272 return [str(net.network_address), str(net.broadcast_address)]
1273 1273
1274 1274 def __json__(self):
1275 1275 return {
1276 1276 'ip_addr': self.ip_addr,
1277 1277 'ip_range': self._get_ip_range(self.ip_addr),
1278 1278 }
1279 1279
1280 1280 def __unicode__(self):
1281 1281 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
1282 1282 self.user_id, self.ip_addr)
1283 1283
1284 1284
1285 1285 class UserSshKeys(Base, BaseModel):
1286 1286 __tablename__ = 'user_ssh_keys'
1287 1287 __table_args__ = (
1288 1288 Index('usk_ssh_key_fingerprint_idx', 'ssh_key_fingerprint'),
1289 1289
1290 1290 UniqueConstraint('ssh_key_fingerprint'),
1291 1291
1292 1292 base_table_args
1293 1293 )
1294 1294 __mapper_args__ = {}
1295 1295
1296 1296 ssh_key_id = Column('ssh_key_id', Integer(), nullable=False, unique=True, default=None, primary_key=True)
1297 1297 ssh_key_data = Column('ssh_key_data', String(10240), nullable=False, unique=None, default=None)
1298 1298 ssh_key_fingerprint = Column('ssh_key_fingerprint', String(255), nullable=False, unique=None, default=None)
1299 1299
1300 1300 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
1301 1301
1302 1302 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1303 1303 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True, default=None)
1304 1304 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1305 1305
1306 1306 user = relationship('User', lazy='joined')
1307 1307
1308 1308 def __json__(self):
1309 1309 data = {
1310 1310 'ssh_fingerprint': self.ssh_key_fingerprint,
1311 1311 'description': self.description,
1312 1312 'created_on': self.created_on
1313 1313 }
1314 1314 return data
1315 1315
1316 1316 def get_api_data(self):
1317 1317 data = self.__json__()
1318 1318 return data
1319 1319
1320 1320
1321 1321 class UserLog(Base, BaseModel):
1322 1322 __tablename__ = 'user_logs'
1323 1323 __table_args__ = (
1324 1324 base_table_args,
1325 1325 )
1326 1326
1327 1327 VERSION_1 = 'v1'
1328 1328 VERSION_2 = 'v2'
1329 1329 VERSIONS = [VERSION_1, VERSION_2]
1330 1330
1331 1331 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1332 1332 user_id = Column("user_id", Integer(), ForeignKey('users.user_id',ondelete='SET NULL'), nullable=True, unique=None, default=None)
1333 1333 username = Column("username", String(255), nullable=True, unique=None, default=None)
1334 1334 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id', ondelete='SET NULL'), nullable=True, unique=None, default=None)
1335 1335 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1336 1336 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1337 1337 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1338 1338 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1339 1339
1340 1340 version = Column("version", String(255), nullable=True, default=VERSION_1)
1341 1341 user_data = Column('user_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1342 1342 action_data = Column('action_data_json', MutationObj.as_mutable(JsonType(dialect_map=dict(mysql=LONGTEXT()))))
1343 1343
1344 1344 def __unicode__(self):
1345 1345 return u"<%s('id:%s:%s')>" % (
1346 1346 self.__class__.__name__, self.repository_name, self.action)
1347 1347
1348 1348 def __json__(self):
1349 1349 return {
1350 1350 'user_id': self.user_id,
1351 1351 'username': self.username,
1352 1352 'repository_id': self.repository_id,
1353 1353 'repository_name': self.repository_name,
1354 1354 'user_ip': self.user_ip,
1355 1355 'action_date': self.action_date,
1356 1356 'action': self.action,
1357 1357 }
1358 1358
1359 1359 @hybrid_property
1360 1360 def entry_id(self):
1361 1361 return self.user_log_id
1362 1362
1363 1363 @property
1364 1364 def action_as_day(self):
1365 1365 return datetime.date(*self.action_date.timetuple()[:3])
1366 1366
1367 1367 user = relationship('User')
1368 1368 repository = relationship('Repository', cascade='')
1369 1369
1370 1370
1371 1371 class UserGroup(Base, BaseModel):
1372 1372 __tablename__ = 'users_groups'
1373 1373 __table_args__ = (
1374 1374 base_table_args,
1375 1375 )
1376 1376
1377 1377 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1378 1378 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1379 1379 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1380 1380 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1381 1381 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1382 1382 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1383 1383 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1384 1384 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1385 1385
1386 1386 members = relationship('UserGroupMember', cascade="all, delete-orphan", lazy="joined")
1387 1387 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1388 1388 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1389 1389 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1390 1390 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1391 1391 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1392 1392
1393 1393 user_group_review_rules = relationship('RepoReviewRuleUserGroup', cascade='all')
1394 1394 user = relationship('User', primaryjoin="User.user_id==UserGroup.user_id")
1395 1395
1396 1396 @classmethod
1397 1397 def _load_group_data(cls, column):
1398 1398 if not column:
1399 1399 return {}
1400 1400
1401 1401 try:
1402 1402 return json.loads(column) or {}
1403 1403 except TypeError:
1404 1404 return {}
1405 1405
1406 1406 @hybrid_property
1407 1407 def description_safe(self):
1408 1408 from rhodecode.lib import helpers as h
1409 1409 return h.escape(self.user_group_description)
1410 1410
1411 1411 @hybrid_property
1412 1412 def group_data(self):
1413 1413 return self._load_group_data(self._group_data)
1414 1414
1415 1415 @group_data.expression
1416 1416 def group_data(self, **kwargs):
1417 1417 return self._group_data
1418 1418
1419 1419 @group_data.setter
1420 1420 def group_data(self, val):
1421 1421 try:
1422 1422 self._group_data = json.dumps(val)
1423 1423 except Exception:
1424 1424 log.error(traceback.format_exc())
1425 1425
1426 1426 @classmethod
1427 1427 def _load_sync(cls, group_data):
1428 1428 if group_data:
1429 1429 return group_data.get('extern_type')
1430 1430
1431 1431 @property
1432 1432 def sync(self):
1433 1433 return self._load_sync(self.group_data)
1434 1434
1435 1435 def __unicode__(self):
1436 1436 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1437 1437 self.users_group_id,
1438 1438 self.users_group_name)
1439 1439
1440 1440 @classmethod
1441 1441 def get_by_group_name(cls, group_name, cache=False,
1442 1442 case_insensitive=False):
1443 1443 if case_insensitive:
1444 1444 q = cls.query().filter(func.lower(cls.users_group_name) ==
1445 1445 func.lower(group_name))
1446 1446
1447 1447 else:
1448 1448 q = cls.query().filter(cls.users_group_name == group_name)
1449 1449 if cache:
1450 1450 q = q.options(
1451 1451 FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name)))
1452 1452 return q.scalar()
1453 1453
1454 1454 @classmethod
1455 1455 def get(cls, user_group_id, cache=False):
1456 1456 if not user_group_id:
1457 1457 return
1458 1458
1459 1459 user_group = cls.query()
1460 1460 if cache:
1461 1461 user_group = user_group.options(
1462 1462 FromCache("sql_cache_short", "get_users_group_%s" % user_group_id))
1463 1463 return user_group.get(user_group_id)
1464 1464
1465 1465 def permissions(self, with_admins=True, with_owner=True,
1466 1466 expand_from_user_groups=False):
1467 1467 """
1468 1468 Permissions for user groups
1469 1469 """
1470 1470 _admin_perm = 'usergroup.admin'
1471 1471
1472 1472 owner_row = []
1473 1473 if with_owner:
1474 1474 usr = AttributeDict(self.user.get_dict())
1475 1475 usr.owner_row = True
1476 1476 usr.permission = _admin_perm
1477 1477 owner_row.append(usr)
1478 1478
1479 1479 super_admin_ids = []
1480 1480 super_admin_rows = []
1481 1481 if with_admins:
1482 1482 for usr in User.get_all_super_admins():
1483 1483 super_admin_ids.append(usr.user_id)
1484 1484 # if this admin is also owner, don't double the record
1485 1485 if usr.user_id == owner_row[0].user_id:
1486 1486 owner_row[0].admin_row = True
1487 1487 else:
1488 1488 usr = AttributeDict(usr.get_dict())
1489 1489 usr.admin_row = True
1490 1490 usr.permission = _admin_perm
1491 1491 super_admin_rows.append(usr)
1492 1492
1493 1493 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1494 1494 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1495 1495 joinedload(UserUserGroupToPerm.user),
1496 1496 joinedload(UserUserGroupToPerm.permission),)
1497 1497
1498 1498 # get owners and admins and permissions. We do a trick of re-writing
1499 1499 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1500 1500 # has a global reference and changing one object propagates to all
1501 1501 # others. This means if admin is also an owner admin_row that change
1502 1502 # would propagate to both objects
1503 1503 perm_rows = []
1504 1504 for _usr in q.all():
1505 1505 usr = AttributeDict(_usr.user.get_dict())
1506 1506 # if this user is also owner/admin, mark as duplicate record
1507 1507 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
1508 1508 usr.duplicate_perm = True
1509 1509 usr.permission = _usr.permission.permission_name
1510 1510 perm_rows.append(usr)
1511 1511
1512 1512 # filter the perm rows by 'default' first and then sort them by
1513 1513 # admin,write,read,none permissions sorted again alphabetically in
1514 1514 # each group
1515 1515 perm_rows = sorted(perm_rows, key=display_user_sort)
1516 1516
1517 1517 user_groups_rows = []
1518 1518 if expand_from_user_groups:
1519 1519 for ug in self.permission_user_groups(with_members=True):
1520 1520 for user_data in ug.members:
1521 1521 user_groups_rows.append(user_data)
1522 1522
1523 1523 return super_admin_rows + owner_row + perm_rows + user_groups_rows
1524 1524
1525 1525 def permission_user_groups(self, with_members=False):
1526 1526 q = UserGroupUserGroupToPerm.query()\
1527 1527 .filter(UserGroupUserGroupToPerm.target_user_group == self)
1528 1528 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1529 1529 joinedload(UserGroupUserGroupToPerm.target_user_group),
1530 1530 joinedload(UserGroupUserGroupToPerm.permission),)
1531 1531
1532 1532 perm_rows = []
1533 1533 for _user_group in q.all():
1534 1534 entry = AttributeDict(_user_group.user_group.get_dict())
1535 1535 entry.permission = _user_group.permission.permission_name
1536 1536 if with_members:
1537 1537 entry.members = [x.user.get_dict()
1538 1538 for x in _user_group.user_group.members]
1539 1539 perm_rows.append(entry)
1540 1540
1541 1541 perm_rows = sorted(perm_rows, key=display_user_group_sort)
1542 1542 return perm_rows
1543 1543
1544 1544 def _get_default_perms(self, user_group, suffix=''):
1545 1545 from rhodecode.model.permission import PermissionModel
1546 1546 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1547 1547
1548 1548 def get_default_perms(self, suffix=''):
1549 1549 return self._get_default_perms(self, suffix)
1550 1550
1551 1551 def get_api_data(self, with_group_members=True, include_secrets=False):
1552 1552 """
1553 1553 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1554 1554 basically forwarded.
1555 1555
1556 1556 """
1557 1557 user_group = self
1558 1558 data = {
1559 1559 'users_group_id': user_group.users_group_id,
1560 1560 'group_name': user_group.users_group_name,
1561 1561 'group_description': user_group.user_group_description,
1562 1562 'active': user_group.users_group_active,
1563 1563 'owner': user_group.user.username,
1564 1564 'sync': user_group.sync,
1565 1565 'owner_email': user_group.user.email,
1566 1566 }
1567 1567
1568 1568 if with_group_members:
1569 1569 users = []
1570 1570 for user in user_group.members:
1571 1571 user = user.user
1572 1572 users.append(user.get_api_data(include_secrets=include_secrets))
1573 1573 data['users'] = users
1574 1574
1575 1575 return data
1576 1576
1577 1577
1578 1578 class UserGroupMember(Base, BaseModel):
1579 1579 __tablename__ = 'users_groups_members'
1580 1580 __table_args__ = (
1581 1581 base_table_args,
1582 1582 )
1583 1583
1584 1584 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1585 1585 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1586 1586 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1587 1587
1588 1588 user = relationship('User', lazy='joined')
1589 1589 users_group = relationship('UserGroup')
1590 1590
1591 1591 def __init__(self, gr_id='', u_id=''):
1592 1592 self.users_group_id = gr_id
1593 1593 self.user_id = u_id
1594 1594
1595 1595
1596 1596 class RepositoryField(Base, BaseModel):
1597 1597 __tablename__ = 'repositories_fields'
1598 1598 __table_args__ = (
1599 1599 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1600 1600 base_table_args,
1601 1601 )
1602 1602
1603 1603 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1604 1604
1605 1605 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1606 1606 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1607 1607 field_key = Column("field_key", String(250))
1608 1608 field_label = Column("field_label", String(1024), nullable=False)
1609 1609 field_value = Column("field_value", String(10000), nullable=False)
1610 1610 field_desc = Column("field_desc", String(1024), nullable=False)
1611 1611 field_type = Column("field_type", String(255), nullable=False, unique=None)
1612 1612 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1613 1613
1614 1614 repository = relationship('Repository')
1615 1615
1616 1616 @property
1617 1617 def field_key_prefixed(self):
1618 1618 return 'ex_%s' % self.field_key
1619 1619
1620 1620 @classmethod
1621 1621 def un_prefix_key(cls, key):
1622 1622 if key.startswith(cls.PREFIX):
1623 1623 return key[len(cls.PREFIX):]
1624 1624 return key
1625 1625
1626 1626 @classmethod
1627 1627 def get_by_key_name(cls, key, repo):
1628 1628 row = cls.query()\
1629 1629 .filter(cls.repository == repo)\
1630 1630 .filter(cls.field_key == key).scalar()
1631 1631 return row
1632 1632
1633 1633
1634 1634 class Repository(Base, BaseModel):
1635 1635 __tablename__ = 'repositories'
1636 1636 __table_args__ = (
1637 1637 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1638 1638 base_table_args,
1639 1639 )
1640 1640 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1641 1641 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1642 1642 DEFAULT_CLONE_URI_SSH = 'ssh://{sys_user}@{hostname}/{repo}'
1643 1643
1644 1644 STATE_CREATED = 'repo_state_created'
1645 1645 STATE_PENDING = 'repo_state_pending'
1646 1646 STATE_ERROR = 'repo_state_error'
1647 1647
1648 1648 LOCK_AUTOMATIC = 'lock_auto'
1649 1649 LOCK_API = 'lock_api'
1650 1650 LOCK_WEB = 'lock_web'
1651 1651 LOCK_PULL = 'lock_pull'
1652 1652
1653 1653 NAME_SEP = URL_SEP
1654 1654
1655 1655 repo_id = Column(
1656 1656 "repo_id", Integer(), nullable=False, unique=True, default=None,
1657 1657 primary_key=True)
1658 1658 _repo_name = Column(
1659 1659 "repo_name", Text(), nullable=False, default=None)
1660 1660 repo_name_hash = Column(
1661 1661 "repo_name_hash", String(255), nullable=False, unique=True)
1662 1662 repo_state = Column("repo_state", String(255), nullable=True)
1663 1663
1664 1664 clone_uri = Column(
1665 1665 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1666 1666 default=None)
1667 1667 push_uri = Column(
1668 1668 "push_uri", EncryptedTextValue(), nullable=True, unique=False,
1669 1669 default=None)
1670 1670 repo_type = Column(
1671 1671 "repo_type", String(255), nullable=False, unique=False, default=None)
1672 1672 user_id = Column(
1673 1673 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1674 1674 unique=False, default=None)
1675 1675 private = Column(
1676 1676 "private", Boolean(), nullable=True, unique=None, default=None)
1677 1677 archived = Column(
1678 1678 "archived", Boolean(), nullable=True, unique=None, default=None)
1679 1679 enable_statistics = Column(
1680 1680 "statistics", Boolean(), nullable=True, unique=None, default=True)
1681 1681 enable_downloads = Column(
1682 1682 "downloads", Boolean(), nullable=True, unique=None, default=True)
1683 1683 description = Column(
1684 1684 "description", String(10000), nullable=True, unique=None, default=None)
1685 1685 created_on = Column(
1686 1686 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1687 1687 default=datetime.datetime.now)
1688 1688 updated_on = Column(
1689 1689 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1690 1690 default=datetime.datetime.now)
1691 1691 _landing_revision = Column(
1692 1692 "landing_revision", String(255), nullable=False, unique=False,
1693 1693 default=None)
1694 1694 enable_locking = Column(
1695 1695 "enable_locking", Boolean(), nullable=False, unique=None,
1696 1696 default=False)
1697 1697 _locked = Column(
1698 1698 "locked", String(255), nullable=True, unique=False, default=None)
1699 1699 _changeset_cache = Column(
1700 1700 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1701 1701
1702 1702 fork_id = Column(
1703 1703 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1704 1704 nullable=True, unique=False, default=None)
1705 1705 group_id = Column(
1706 1706 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1707 1707 unique=False, default=None)
1708 1708
1709 1709 user = relationship('User', lazy='joined')
1710 1710 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1711 1711 group = relationship('RepoGroup', lazy='joined')
1712 1712 repo_to_perm = relationship(
1713 1713 'UserRepoToPerm', cascade='all',
1714 1714 order_by='UserRepoToPerm.repo_to_perm_id')
1715 1715 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1716 1716 stats = relationship('Statistics', cascade='all', uselist=False)
1717 1717
1718 1718 followers = relationship(
1719 1719 'UserFollowing',
1720 1720 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1721 1721 cascade='all')
1722 1722 extra_fields = relationship(
1723 1723 'RepositoryField', cascade="all, delete-orphan")
1724 1724 logs = relationship('UserLog')
1725 1725 comments = relationship(
1726 1726 'ChangesetComment', cascade="all, delete-orphan")
1727 1727 pull_requests_source = relationship(
1728 1728 'PullRequest',
1729 1729 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1730 1730 cascade="all, delete-orphan")
1731 1731 pull_requests_target = relationship(
1732 1732 'PullRequest',
1733 1733 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1734 1734 cascade="all, delete-orphan")
1735 1735 ui = relationship('RepoRhodeCodeUi', cascade="all")
1736 1736 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1737 1737 integrations = relationship('Integration', cascade="all, delete-orphan")
1738 1738
1739 1739 scoped_tokens = relationship('UserApiKeys', cascade="all")
1740 1740
1741 1741 # no cascade, set NULL
1742 1742 artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_id==Repository.repo_id')
1743 1743
1744 1744 def __unicode__(self):
1745 1745 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1746 1746 safe_unicode(self.repo_name))
1747 1747
1748 1748 @hybrid_property
1749 1749 def description_safe(self):
1750 1750 from rhodecode.lib import helpers as h
1751 1751 return h.escape(self.description)
1752 1752
1753 1753 @hybrid_property
1754 1754 def landing_rev(self):
1755 1755 # always should return [rev_type, rev]
1756 1756 if self._landing_revision:
1757 1757 _rev_info = self._landing_revision.split(':')
1758 1758 if len(_rev_info) < 2:
1759 1759 _rev_info.insert(0, 'rev')
1760 1760 return [_rev_info[0], _rev_info[1]]
1761 1761 return [None, None]
1762 1762
1763 1763 @landing_rev.setter
1764 1764 def landing_rev(self, val):
1765 1765 if ':' not in val:
1766 1766 raise ValueError('value must be delimited with `:` and consist '
1767 1767 'of <rev_type>:<rev>, got %s instead' % val)
1768 1768 self._landing_revision = val
1769 1769
1770 1770 @hybrid_property
1771 1771 def locked(self):
1772 1772 if self._locked:
1773 1773 user_id, timelocked, reason = self._locked.split(':')
1774 1774 lock_values = int(user_id), timelocked, reason
1775 1775 else:
1776 1776 lock_values = [None, None, None]
1777 1777 return lock_values
1778 1778
1779 1779 @locked.setter
1780 1780 def locked(self, val):
1781 1781 if val and isinstance(val, (list, tuple)):
1782 1782 self._locked = ':'.join(map(str, val))
1783 1783 else:
1784 1784 self._locked = None
1785 1785
1786 1786 @classmethod
1787 1787 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
1788 1788 from rhodecode.lib.vcs.backends.base import EmptyCommit
1789 1789 dummy = EmptyCommit().__json__()
1790 1790 if not changeset_cache_raw:
1791 1791 dummy['source_repo_id'] = repo_id
1792 1792 return json.loads(json.dumps(dummy))
1793 1793
1794 1794 try:
1795 1795 return json.loads(changeset_cache_raw)
1796 1796 except TypeError:
1797 1797 return dummy
1798 1798 except Exception:
1799 1799 log.error(traceback.format_exc())
1800 1800 return dummy
1801 1801
1802 1802 @hybrid_property
1803 1803 def changeset_cache(self):
1804 1804 return self._load_changeset_cache(self.repo_id, self._changeset_cache)
1805 1805
1806 1806 @changeset_cache.setter
1807 1807 def changeset_cache(self, val):
1808 1808 try:
1809 1809 self._changeset_cache = json.dumps(val)
1810 1810 except Exception:
1811 1811 log.error(traceback.format_exc())
1812 1812
1813 1813 @hybrid_property
1814 1814 def repo_name(self):
1815 1815 return self._repo_name
1816 1816
1817 1817 @repo_name.setter
1818 1818 def repo_name(self, value):
1819 1819 self._repo_name = value
1820 1820 self.repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1821 1821
1822 1822 @classmethod
1823 1823 def normalize_repo_name(cls, repo_name):
1824 1824 """
1825 1825 Normalizes os specific repo_name to the format internally stored inside
1826 1826 database using URL_SEP
1827 1827
1828 1828 :param cls:
1829 1829 :param repo_name:
1830 1830 """
1831 1831 return cls.NAME_SEP.join(repo_name.split(os.sep))
1832 1832
1833 1833 @classmethod
1834 1834 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1835 1835 session = Session()
1836 1836 q = session.query(cls).filter(cls.repo_name == repo_name)
1837 1837
1838 1838 if cache:
1839 1839 if identity_cache:
1840 1840 val = cls.identity_cache(session, 'repo_name', repo_name)
1841 1841 if val:
1842 1842 return val
1843 1843 else:
1844 1844 cache_key = "get_repo_by_name_%s" % _hash_key(repo_name)
1845 1845 q = q.options(
1846 1846 FromCache("sql_cache_short", cache_key))
1847 1847
1848 1848 return q.scalar()
1849 1849
1850 1850 @classmethod
1851 1851 def get_by_id_or_repo_name(cls, repoid):
1852 1852 if isinstance(repoid, (int, long)):
1853 1853 try:
1854 1854 repo = cls.get(repoid)
1855 1855 except ValueError:
1856 1856 repo = None
1857 1857 else:
1858 1858 repo = cls.get_by_repo_name(repoid)
1859 1859 return repo
1860 1860
1861 1861 @classmethod
1862 1862 def get_by_full_path(cls, repo_full_path):
1863 1863 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1864 1864 repo_name = cls.normalize_repo_name(repo_name)
1865 1865 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1866 1866
1867 1867 @classmethod
1868 1868 def get_repo_forks(cls, repo_id):
1869 1869 return cls.query().filter(Repository.fork_id == repo_id)
1870 1870
1871 1871 @classmethod
1872 1872 def base_path(cls):
1873 1873 """
1874 1874 Returns base path when all repos are stored
1875 1875
1876 1876 :param cls:
1877 1877 """
1878 1878 q = Session().query(RhodeCodeUi)\
1879 1879 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1880 1880 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1881 1881 return q.one().ui_value
1882 1882
1883 1883 @classmethod
1884 1884 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1885 1885 case_insensitive=True, archived=False):
1886 1886 q = Repository.query()
1887 1887
1888 1888 if not archived:
1889 1889 q = q.filter(Repository.archived.isnot(true()))
1890 1890
1891 1891 if not isinstance(user_id, Optional):
1892 1892 q = q.filter(Repository.user_id == user_id)
1893 1893
1894 1894 if not isinstance(group_id, Optional):
1895 1895 q = q.filter(Repository.group_id == group_id)
1896 1896
1897 1897 if case_insensitive:
1898 1898 q = q.order_by(func.lower(Repository.repo_name))
1899 1899 else:
1900 1900 q = q.order_by(Repository.repo_name)
1901 1901
1902 1902 return q.all()
1903 1903
1904 1904 @property
1905 1905 def repo_uid(self):
1906 1906 return '_{}'.format(self.repo_id)
1907 1907
1908 1908 @property
1909 1909 def forks(self):
1910 1910 """
1911 1911 Return forks of this repo
1912 1912 """
1913 1913 return Repository.get_repo_forks(self.repo_id)
1914 1914
1915 1915 @property
1916 1916 def parent(self):
1917 1917 """
1918 1918 Returns fork parent
1919 1919 """
1920 1920 return self.fork
1921 1921
1922 1922 @property
1923 1923 def just_name(self):
1924 1924 return self.repo_name.split(self.NAME_SEP)[-1]
1925 1925
1926 1926 @property
1927 1927 def groups_with_parents(self):
1928 1928 groups = []
1929 1929 if self.group is None:
1930 1930 return groups
1931 1931
1932 1932 cur_gr = self.group
1933 1933 groups.insert(0, cur_gr)
1934 1934 while 1:
1935 1935 gr = getattr(cur_gr, 'parent_group', None)
1936 1936 cur_gr = cur_gr.parent_group
1937 1937 if gr is None:
1938 1938 break
1939 1939 groups.insert(0, gr)
1940 1940
1941 1941 return groups
1942 1942
1943 1943 @property
1944 1944 def groups_and_repo(self):
1945 1945 return self.groups_with_parents, self
1946 1946
1947 1947 @LazyProperty
1948 1948 def repo_path(self):
1949 1949 """
1950 1950 Returns base full path for that repository means where it actually
1951 1951 exists on a filesystem
1952 1952 """
1953 1953 q = Session().query(RhodeCodeUi).filter(
1954 1954 RhodeCodeUi.ui_key == self.NAME_SEP)
1955 1955 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1956 1956 return q.one().ui_value
1957 1957
1958 1958 @property
1959 1959 def repo_full_path(self):
1960 1960 p = [self.repo_path]
1961 1961 # we need to split the name by / since this is how we store the
1962 1962 # names in the database, but that eventually needs to be converted
1963 1963 # into a valid system path
1964 1964 p += self.repo_name.split(self.NAME_SEP)
1965 1965 return os.path.join(*map(safe_unicode, p))
1966 1966
1967 1967 @property
1968 1968 def cache_keys(self):
1969 1969 """
1970 1970 Returns associated cache keys for that repo
1971 1971 """
1972 1972 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
1973 1973 repo_id=self.repo_id)
1974 1974 return CacheKey.query()\
1975 1975 .filter(CacheKey.cache_args == invalidation_namespace)\
1976 1976 .order_by(CacheKey.cache_key)\
1977 1977 .all()
1978 1978
1979 1979 @property
1980 1980 def cached_diffs_relative_dir(self):
1981 1981 """
1982 1982 Return a relative to the repository store path of cached diffs
1983 1983 used for safe display for users, who shouldn't know the absolute store
1984 1984 path
1985 1985 """
1986 1986 return os.path.join(
1987 1987 os.path.dirname(self.repo_name),
1988 1988 self.cached_diffs_dir.split(os.path.sep)[-1])
1989 1989
1990 1990 @property
1991 1991 def cached_diffs_dir(self):
1992 1992 path = self.repo_full_path
1993 1993 return os.path.join(
1994 1994 os.path.dirname(path),
1995 1995 '.__shadow_diff_cache_repo_{}'.format(self.repo_id))
1996 1996
1997 1997 def cached_diffs(self):
1998 1998 diff_cache_dir = self.cached_diffs_dir
1999 1999 if os.path.isdir(diff_cache_dir):
2000 2000 return os.listdir(diff_cache_dir)
2001 2001 return []
2002 2002
2003 2003 def shadow_repos(self):
2004 2004 shadow_repos_pattern = '.__shadow_repo_{}'.format(self.repo_id)
2005 2005 return [
2006 2006 x for x in os.listdir(os.path.dirname(self.repo_full_path))
2007 2007 if x.startswith(shadow_repos_pattern)]
2008 2008
2009 2009 def get_new_name(self, repo_name):
2010 2010 """
2011 2011 returns new full repository name based on assigned group and new new
2012 2012
2013 2013 :param group_name:
2014 2014 """
2015 2015 path_prefix = self.group.full_path_splitted if self.group else []
2016 2016 return self.NAME_SEP.join(path_prefix + [repo_name])
2017 2017
2018 2018 @property
2019 2019 def _config(self):
2020 2020 """
2021 2021 Returns db based config object.
2022 2022 """
2023 2023 from rhodecode.lib.utils import make_db_config
2024 2024 return make_db_config(clear_session=False, repo=self)
2025 2025
2026 2026 def permissions(self, with_admins=True, with_owner=True,
2027 2027 expand_from_user_groups=False):
2028 2028 """
2029 2029 Permissions for repositories
2030 2030 """
2031 2031 _admin_perm = 'repository.admin'
2032 2032
2033 2033 owner_row = []
2034 2034 if with_owner:
2035 2035 usr = AttributeDict(self.user.get_dict())
2036 2036 usr.owner_row = True
2037 2037 usr.permission = _admin_perm
2038 2038 usr.permission_id = None
2039 2039 owner_row.append(usr)
2040 2040
2041 2041 super_admin_ids = []
2042 2042 super_admin_rows = []
2043 2043 if with_admins:
2044 2044 for usr in User.get_all_super_admins():
2045 2045 super_admin_ids.append(usr.user_id)
2046 2046 # if this admin is also owner, don't double the record
2047 2047 if usr.user_id == owner_row[0].user_id:
2048 2048 owner_row[0].admin_row = True
2049 2049 else:
2050 2050 usr = AttributeDict(usr.get_dict())
2051 2051 usr.admin_row = True
2052 2052 usr.permission = _admin_perm
2053 2053 usr.permission_id = None
2054 2054 super_admin_rows.append(usr)
2055 2055
2056 2056 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
2057 2057 q = q.options(joinedload(UserRepoToPerm.repository),
2058 2058 joinedload(UserRepoToPerm.user),
2059 2059 joinedload(UserRepoToPerm.permission),)
2060 2060
2061 2061 # get owners and admins and permissions. We do a trick of re-writing
2062 2062 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2063 2063 # has a global reference and changing one object propagates to all
2064 2064 # others. This means if admin is also an owner admin_row that change
2065 2065 # would propagate to both objects
2066 2066 perm_rows = []
2067 2067 for _usr in q.all():
2068 2068 usr = AttributeDict(_usr.user.get_dict())
2069 2069 # if this user is also owner/admin, mark as duplicate record
2070 2070 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
2071 2071 usr.duplicate_perm = True
2072 2072 # also check if this permission is maybe used by branch_permissions
2073 2073 if _usr.branch_perm_entry:
2074 2074 usr.branch_rules = [x.branch_rule_id for x in _usr.branch_perm_entry]
2075 2075
2076 2076 usr.permission = _usr.permission.permission_name
2077 2077 usr.permission_id = _usr.repo_to_perm_id
2078 2078 perm_rows.append(usr)
2079 2079
2080 2080 # filter the perm rows by 'default' first and then sort them by
2081 2081 # admin,write,read,none permissions sorted again alphabetically in
2082 2082 # each group
2083 2083 perm_rows = sorted(perm_rows, key=display_user_sort)
2084 2084
2085 2085 user_groups_rows = []
2086 2086 if expand_from_user_groups:
2087 2087 for ug in self.permission_user_groups(with_members=True):
2088 2088 for user_data in ug.members:
2089 2089 user_groups_rows.append(user_data)
2090 2090
2091 2091 return super_admin_rows + owner_row + perm_rows + user_groups_rows
2092 2092
2093 2093 def permission_user_groups(self, with_members=True):
2094 2094 q = UserGroupRepoToPerm.query()\
2095 2095 .filter(UserGroupRepoToPerm.repository == self)
2096 2096 q = q.options(joinedload(UserGroupRepoToPerm.repository),
2097 2097 joinedload(UserGroupRepoToPerm.users_group),
2098 2098 joinedload(UserGroupRepoToPerm.permission),)
2099 2099
2100 2100 perm_rows = []
2101 2101 for _user_group in q.all():
2102 2102 entry = AttributeDict(_user_group.users_group.get_dict())
2103 2103 entry.permission = _user_group.permission.permission_name
2104 2104 if with_members:
2105 2105 entry.members = [x.user.get_dict()
2106 2106 for x in _user_group.users_group.members]
2107 2107 perm_rows.append(entry)
2108 2108
2109 2109 perm_rows = sorted(perm_rows, key=display_user_group_sort)
2110 2110 return perm_rows
2111 2111
2112 2112 def get_api_data(self, include_secrets=False):
2113 2113 """
2114 2114 Common function for generating repo api data
2115 2115
2116 2116 :param include_secrets: See :meth:`User.get_api_data`.
2117 2117
2118 2118 """
2119 2119 # TODO: mikhail: Here there is an anti-pattern, we probably need to
2120 2120 # move this methods on models level.
2121 2121 from rhodecode.model.settings import SettingsModel
2122 2122 from rhodecode.model.repo import RepoModel
2123 2123
2124 2124 repo = self
2125 2125 _user_id, _time, _reason = self.locked
2126 2126
2127 2127 data = {
2128 2128 'repo_id': repo.repo_id,
2129 2129 'repo_name': repo.repo_name,
2130 2130 'repo_type': repo.repo_type,
2131 2131 'clone_uri': repo.clone_uri or '',
2132 2132 'push_uri': repo.push_uri or '',
2133 2133 'url': RepoModel().get_url(self),
2134 2134 'private': repo.private,
2135 2135 'created_on': repo.created_on,
2136 2136 'description': repo.description_safe,
2137 2137 'landing_rev': repo.landing_rev,
2138 2138 'owner': repo.user.username,
2139 2139 'fork_of': repo.fork.repo_name if repo.fork else None,
2140 2140 'fork_of_id': repo.fork.repo_id if repo.fork else None,
2141 2141 'enable_statistics': repo.enable_statistics,
2142 2142 'enable_locking': repo.enable_locking,
2143 2143 'enable_downloads': repo.enable_downloads,
2144 2144 'last_changeset': repo.changeset_cache,
2145 2145 'locked_by': User.get(_user_id).get_api_data(
2146 2146 include_secrets=include_secrets) if _user_id else None,
2147 2147 'locked_date': time_to_datetime(_time) if _time else None,
2148 2148 'lock_reason': _reason if _reason else None,
2149 2149 }
2150 2150
2151 2151 # TODO: mikhail: should be per-repo settings here
2152 2152 rc_config = SettingsModel().get_all_settings()
2153 2153 repository_fields = str2bool(
2154 2154 rc_config.get('rhodecode_repository_fields'))
2155 2155 if repository_fields:
2156 2156 for f in self.extra_fields:
2157 2157 data[f.field_key_prefixed] = f.field_value
2158 2158
2159 2159 return data
2160 2160
2161 2161 @classmethod
2162 2162 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
2163 2163 if not lock_time:
2164 2164 lock_time = time.time()
2165 2165 if not lock_reason:
2166 2166 lock_reason = cls.LOCK_AUTOMATIC
2167 2167 repo.locked = [user_id, lock_time, lock_reason]
2168 2168 Session().add(repo)
2169 2169 Session().commit()
2170 2170
2171 2171 @classmethod
2172 2172 def unlock(cls, repo):
2173 2173 repo.locked = None
2174 2174 Session().add(repo)
2175 2175 Session().commit()
2176 2176
2177 2177 @classmethod
2178 2178 def getlock(cls, repo):
2179 2179 return repo.locked
2180 2180
2181 2181 def is_user_lock(self, user_id):
2182 2182 if self.lock[0]:
2183 2183 lock_user_id = safe_int(self.lock[0])
2184 2184 user_id = safe_int(user_id)
2185 2185 # both are ints, and they are equal
2186 2186 return all([lock_user_id, user_id]) and lock_user_id == user_id
2187 2187
2188 2188 return False
2189 2189
2190 2190 def get_locking_state(self, action, user_id, only_when_enabled=True):
2191 2191 """
2192 2192 Checks locking on this repository, if locking is enabled and lock is
2193 2193 present returns a tuple of make_lock, locked, locked_by.
2194 2194 make_lock can have 3 states None (do nothing) True, make lock
2195 2195 False release lock, This value is later propagated to hooks, which
2196 2196 do the locking. Think about this as signals passed to hooks what to do.
2197 2197
2198 2198 """
2199 2199 # TODO: johbo: This is part of the business logic and should be moved
2200 2200 # into the RepositoryModel.
2201 2201
2202 2202 if action not in ('push', 'pull'):
2203 2203 raise ValueError("Invalid action value: %s" % repr(action))
2204 2204
2205 2205 # defines if locked error should be thrown to user
2206 2206 currently_locked = False
2207 2207 # defines if new lock should be made, tri-state
2208 2208 make_lock = None
2209 2209 repo = self
2210 2210 user = User.get(user_id)
2211 2211
2212 2212 lock_info = repo.locked
2213 2213
2214 2214 if repo and (repo.enable_locking or not only_when_enabled):
2215 2215 if action == 'push':
2216 2216 # check if it's already locked !, if it is compare users
2217 2217 locked_by_user_id = lock_info[0]
2218 2218 if user.user_id == locked_by_user_id:
2219 2219 log.debug(
2220 2220 'Got `push` action from user %s, now unlocking', user)
2221 2221 # unlock if we have push from user who locked
2222 2222 make_lock = False
2223 2223 else:
2224 2224 # we're not the same user who locked, ban with
2225 2225 # code defined in settings (default is 423 HTTP Locked) !
2226 2226 log.debug('Repo %s is currently locked by %s', repo, user)
2227 2227 currently_locked = True
2228 2228 elif action == 'pull':
2229 2229 # [0] user [1] date
2230 2230 if lock_info[0] and lock_info[1]:
2231 2231 log.debug('Repo %s is currently locked by %s', repo, user)
2232 2232 currently_locked = True
2233 2233 else:
2234 2234 log.debug('Setting lock on repo %s by %s', repo, user)
2235 2235 make_lock = True
2236 2236
2237 2237 else:
2238 2238 log.debug('Repository %s do not have locking enabled', repo)
2239 2239
2240 2240 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
2241 2241 make_lock, currently_locked, lock_info)
2242 2242
2243 2243 from rhodecode.lib.auth import HasRepoPermissionAny
2244 2244 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
2245 2245 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
2246 2246 # if we don't have at least write permission we cannot make a lock
2247 2247 log.debug('lock state reset back to FALSE due to lack '
2248 2248 'of at least read permission')
2249 2249 make_lock = False
2250 2250
2251 2251 return make_lock, currently_locked, lock_info
2252 2252
2253 2253 @property
2254 2254 def last_commit_cache_update_diff(self):
2255 2255 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2256 2256
2257 2257 @classmethod
2258 2258 def _load_commit_change(cls, last_commit_cache):
2259 2259 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2260 2260 empty_date = datetime.datetime.fromtimestamp(0)
2261 2261 date_latest = last_commit_cache.get('date', empty_date)
2262 2262 try:
2263 2263 return parse_datetime(date_latest)
2264 2264 except Exception:
2265 2265 return empty_date
2266 2266
2267 2267 @property
2268 2268 def last_commit_change(self):
2269 2269 return self._load_commit_change(self.changeset_cache)
2270 2270
2271 2271 @property
2272 2272 def last_db_change(self):
2273 2273 return self.updated_on
2274 2274
2275 2275 @property
2276 2276 def clone_uri_hidden(self):
2277 2277 clone_uri = self.clone_uri
2278 2278 if clone_uri:
2279 2279 import urlobject
2280 2280 url_obj = urlobject.URLObject(cleaned_uri(clone_uri))
2281 2281 if url_obj.password:
2282 2282 clone_uri = url_obj.with_password('*****')
2283 2283 return clone_uri
2284 2284
2285 2285 @property
2286 2286 def push_uri_hidden(self):
2287 2287 push_uri = self.push_uri
2288 2288 if push_uri:
2289 2289 import urlobject
2290 2290 url_obj = urlobject.URLObject(cleaned_uri(push_uri))
2291 2291 if url_obj.password:
2292 2292 push_uri = url_obj.with_password('*****')
2293 2293 return push_uri
2294 2294
2295 2295 def clone_url(self, **override):
2296 2296 from rhodecode.model.settings import SettingsModel
2297 2297
2298 2298 uri_tmpl = None
2299 2299 if 'with_id' in override:
2300 2300 uri_tmpl = self.DEFAULT_CLONE_URI_ID
2301 2301 del override['with_id']
2302 2302
2303 2303 if 'uri_tmpl' in override:
2304 2304 uri_tmpl = override['uri_tmpl']
2305 2305 del override['uri_tmpl']
2306 2306
2307 2307 ssh = False
2308 2308 if 'ssh' in override:
2309 2309 ssh = True
2310 2310 del override['ssh']
2311 2311
2312 2312 # we didn't override our tmpl from **overrides
2313 2313 request = get_current_request()
2314 2314 if not uri_tmpl:
2315 2315 if hasattr(request, 'call_context') and hasattr(request.call_context, 'rc_config'):
2316 2316 rc_config = request.call_context.rc_config
2317 2317 else:
2318 2318 rc_config = SettingsModel().get_all_settings(cache=True)
2319 2319
2320 2320 if ssh:
2321 2321 uri_tmpl = rc_config.get(
2322 2322 'rhodecode_clone_uri_ssh_tmpl') or self.DEFAULT_CLONE_URI_SSH
2323 2323
2324 2324 else:
2325 2325 uri_tmpl = rc_config.get(
2326 2326 'rhodecode_clone_uri_tmpl') or self.DEFAULT_CLONE_URI
2327 2327
2328 2328 return get_clone_url(request=request,
2329 2329 uri_tmpl=uri_tmpl,
2330 2330 repo_name=self.repo_name,
2331 2331 repo_id=self.repo_id,
2332 2332 repo_type=self.repo_type,
2333 2333 **override)
2334 2334
2335 2335 def set_state(self, state):
2336 2336 self.repo_state = state
2337 2337 Session().add(self)
2338 2338 #==========================================================================
2339 2339 # SCM PROPERTIES
2340 2340 #==========================================================================
2341 2341
2342 2342 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None, maybe_unreachable=False):
2343 2343 return get_commit_safe(
2344 2344 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load,
2345 2345 maybe_unreachable=maybe_unreachable)
2346 2346
2347 2347 def get_changeset(self, rev=None, pre_load=None):
2348 2348 warnings.warn("Use get_commit", DeprecationWarning)
2349 2349 commit_id = None
2350 2350 commit_idx = None
2351 2351 if isinstance(rev, compat.string_types):
2352 2352 commit_id = rev
2353 2353 else:
2354 2354 commit_idx = rev
2355 2355 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
2356 2356 pre_load=pre_load)
2357 2357
2358 2358 def get_landing_commit(self):
2359 2359 """
2360 2360 Returns landing commit, or if that doesn't exist returns the tip
2361 2361 """
2362 2362 _rev_type, _rev = self.landing_rev
2363 2363 commit = self.get_commit(_rev)
2364 2364 if isinstance(commit, EmptyCommit):
2365 2365 return self.get_commit()
2366 2366 return commit
2367 2367
2368 2368 def flush_commit_cache(self):
2369 2369 self.update_commit_cache(cs_cache={'raw_id':'0'})
2370 2370 self.update_commit_cache()
2371 2371
2372 2372 def update_commit_cache(self, cs_cache=None, config=None):
2373 2373 """
2374 2374 Update cache of last commit for repository
2375 2375 cache_keys should be::
2376 2376
2377 2377 source_repo_id
2378 2378 short_id
2379 2379 raw_id
2380 2380 revision
2381 2381 parents
2382 2382 message
2383 2383 date
2384 2384 author
2385 2385 updated_on
2386 2386
2387 2387 """
2388 2388 from rhodecode.lib.vcs.backends.base import BaseChangeset
2389 2389 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2390 2390 empty_date = datetime.datetime.fromtimestamp(0)
2391 2391
2392 2392 if cs_cache is None:
2393 2393 # use no-cache version here
2394 2394 try:
2395 2395 scm_repo = self.scm_instance(cache=False, config=config)
2396 2396 except VCSError:
2397 2397 scm_repo = None
2398 2398 empty = scm_repo is None or scm_repo.is_empty()
2399 2399
2400 2400 if not empty:
2401 2401 cs_cache = scm_repo.get_commit(
2402 2402 pre_load=["author", "date", "message", "parents", "branch"])
2403 2403 else:
2404 2404 cs_cache = EmptyCommit()
2405 2405
2406 2406 if isinstance(cs_cache, BaseChangeset):
2407 2407 cs_cache = cs_cache.__json__()
2408 2408
2409 2409 def is_outdated(new_cs_cache):
2410 2410 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
2411 2411 new_cs_cache['revision'] != self.changeset_cache['revision']):
2412 2412 return True
2413 2413 return False
2414 2414
2415 2415 # check if we have maybe already latest cached revision
2416 2416 if is_outdated(cs_cache) or not self.changeset_cache:
2417 2417 _current_datetime = datetime.datetime.utcnow()
2418 2418 last_change = cs_cache.get('date') or _current_datetime
2419 2419 # we check if last update is newer than the new value
2420 2420 # if yes, we use the current timestamp instead. Imagine you get
2421 2421 # old commit pushed 1y ago, we'd set last update 1y to ago.
2422 2422 last_change_timestamp = datetime_to_time(last_change)
2423 2423 current_timestamp = datetime_to_time(last_change)
2424 2424 if last_change_timestamp > current_timestamp and not empty:
2425 2425 cs_cache['date'] = _current_datetime
2426 2426
2427 2427 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2428 2428 cs_cache['updated_on'] = time.time()
2429 2429 self.changeset_cache = cs_cache
2430 2430 self.updated_on = last_change
2431 2431 Session().add(self)
2432 2432 Session().commit()
2433 2433
2434 2434 else:
2435 2435 if empty:
2436 2436 cs_cache = EmptyCommit().__json__()
2437 2437 else:
2438 2438 cs_cache = self.changeset_cache
2439 2439
2440 2440 _date_latest = parse_datetime(cs_cache.get('date') or empty_date)
2441 2441
2442 2442 cs_cache['updated_on'] = time.time()
2443 2443 self.changeset_cache = cs_cache
2444 2444 self.updated_on = _date_latest
2445 2445 Session().add(self)
2446 2446 Session().commit()
2447 2447
2448 2448 log.debug('updated repo `%s` with new commit cache %s, and last update_date: %s',
2449 2449 self.repo_name, cs_cache, _date_latest)
2450 2450
2451 2451 @property
2452 2452 def tip(self):
2453 2453 return self.get_commit('tip')
2454 2454
2455 2455 @property
2456 2456 def author(self):
2457 2457 return self.tip.author
2458 2458
2459 2459 @property
2460 2460 def last_change(self):
2461 2461 return self.scm_instance().last_change
2462 2462
2463 2463 def get_comments(self, revisions=None):
2464 2464 """
2465 2465 Returns comments for this repository grouped by revisions
2466 2466
2467 2467 :param revisions: filter query by revisions only
2468 2468 """
2469 2469 cmts = ChangesetComment.query()\
2470 2470 .filter(ChangesetComment.repo == self)
2471 2471 if revisions:
2472 2472 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
2473 2473 grouped = collections.defaultdict(list)
2474 2474 for cmt in cmts.all():
2475 2475 grouped[cmt.revision].append(cmt)
2476 2476 return grouped
2477 2477
2478 2478 def statuses(self, revisions=None):
2479 2479 """
2480 2480 Returns statuses for this repository
2481 2481
2482 2482 :param revisions: list of revisions to get statuses for
2483 2483 """
2484 2484 statuses = ChangesetStatus.query()\
2485 2485 .filter(ChangesetStatus.repo == self)\
2486 2486 .filter(ChangesetStatus.version == 0)
2487 2487
2488 2488 if revisions:
2489 2489 # Try doing the filtering in chunks to avoid hitting limits
2490 2490 size = 500
2491 2491 status_results = []
2492 2492 for chunk in xrange(0, len(revisions), size):
2493 2493 status_results += statuses.filter(
2494 2494 ChangesetStatus.revision.in_(
2495 2495 revisions[chunk: chunk+size])
2496 2496 ).all()
2497 2497 else:
2498 2498 status_results = statuses.all()
2499 2499
2500 2500 grouped = {}
2501 2501
2502 2502 # maybe we have open new pullrequest without a status?
2503 2503 stat = ChangesetStatus.STATUS_UNDER_REVIEW
2504 2504 status_lbl = ChangesetStatus.get_status_lbl(stat)
2505 2505 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
2506 2506 for rev in pr.revisions:
2507 2507 pr_id = pr.pull_request_id
2508 2508 pr_repo = pr.target_repo.repo_name
2509 2509 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
2510 2510
2511 2511 for stat in status_results:
2512 2512 pr_id = pr_repo = None
2513 2513 if stat.pull_request:
2514 2514 pr_id = stat.pull_request.pull_request_id
2515 2515 pr_repo = stat.pull_request.target_repo.repo_name
2516 2516 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
2517 2517 pr_id, pr_repo]
2518 2518 return grouped
2519 2519
2520 2520 # ==========================================================================
2521 2521 # SCM CACHE INSTANCE
2522 2522 # ==========================================================================
2523 2523
2524 2524 def scm_instance(self, **kwargs):
2525 2525 import rhodecode
2526 2526
2527 2527 # Passing a config will not hit the cache currently only used
2528 2528 # for repo2dbmapper
2529 2529 config = kwargs.pop('config', None)
2530 2530 cache = kwargs.pop('cache', None)
2531 2531 vcs_full_cache = kwargs.pop('vcs_full_cache', None)
2532 2532 if vcs_full_cache is not None:
2533 2533 # allows override global config
2534 2534 full_cache = vcs_full_cache
2535 2535 else:
2536 2536 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
2537 2537 # if cache is NOT defined use default global, else we have a full
2538 2538 # control over cache behaviour
2539 2539 if cache is None and full_cache and not config:
2540 2540 log.debug('Initializing pure cached instance for %s', self.repo_path)
2541 2541 return self._get_instance_cached()
2542 2542
2543 2543 # cache here is sent to the "vcs server"
2544 2544 return self._get_instance(cache=bool(cache), config=config)
2545 2545
2546 2546 def _get_instance_cached(self):
2547 2547 from rhodecode.lib import rc_cache
2548 2548
2549 2549 cache_namespace_uid = 'cache_repo_instance.{}'.format(self.repo_id)
2550 2550 invalidation_namespace = CacheKey.REPO_INVALIDATION_NAMESPACE.format(
2551 2551 repo_id=self.repo_id)
2552 2552 region = rc_cache.get_or_create_region('cache_repo_longterm', cache_namespace_uid)
2553 2553
2554 2554 @region.conditional_cache_on_arguments(namespace=cache_namespace_uid)
2555 2555 def get_instance_cached(repo_id, context_id, _cache_state_uid):
2556 2556 return self._get_instance(repo_state_uid=_cache_state_uid)
2557 2557
2558 2558 # we must use thread scoped cache here,
2559 2559 # because each thread of gevent needs it's own not shared connection and cache
2560 2560 # we also alter `args` so the cache key is individual for every green thread.
2561 2561 inv_context_manager = rc_cache.InvalidationContext(
2562 2562 uid=cache_namespace_uid, invalidation_namespace=invalidation_namespace,
2563 2563 thread_scoped=True)
2564 2564 with inv_context_manager as invalidation_context:
2565 2565 cache_state_uid = invalidation_context.cache_data['cache_state_uid']
2566 2566 args = (self.repo_id, inv_context_manager.cache_key, cache_state_uid)
2567 2567
2568 2568 # re-compute and store cache if we get invalidate signal
2569 2569 if invalidation_context.should_invalidate():
2570 2570 instance = get_instance_cached.refresh(*args)
2571 2571 else:
2572 2572 instance = get_instance_cached(*args)
2573 2573
2574 2574 log.debug('Repo instance fetched in %.4fs', inv_context_manager.compute_time)
2575 2575 return instance
2576 2576
2577 2577 def _get_instance(self, cache=True, config=None, repo_state_uid=None):
2578 2578 log.debug('Initializing %s instance `%s` with cache flag set to: %s',
2579 2579 self.repo_type, self.repo_path, cache)
2580 2580 config = config or self._config
2581 2581 custom_wire = {
2582 2582 'cache': cache, # controls the vcs.remote cache
2583 2583 'repo_state_uid': repo_state_uid
2584 2584 }
2585 2585 repo = get_vcs_instance(
2586 2586 repo_path=safe_str(self.repo_full_path),
2587 2587 config=config,
2588 2588 with_wire=custom_wire,
2589 2589 create=False,
2590 2590 _vcs_alias=self.repo_type)
2591 2591 if repo is not None:
2592 2592 repo.count() # cache rebuild
2593 2593 return repo
2594 2594
2595 2595 def get_shadow_repository_path(self, workspace_id):
2596 2596 from rhodecode.lib.vcs.backends.base import BaseRepository
2597 2597 shadow_repo_path = BaseRepository._get_shadow_repository_path(
2598 2598 self.repo_full_path, self.repo_id, workspace_id)
2599 2599 return shadow_repo_path
2600 2600
2601 2601 def __json__(self):
2602 2602 return {'landing_rev': self.landing_rev}
2603 2603
2604 2604 def get_dict(self):
2605 2605
2606 2606 # Since we transformed `repo_name` to a hybrid property, we need to
2607 2607 # keep compatibility with the code which uses `repo_name` field.
2608 2608
2609 2609 result = super(Repository, self).get_dict()
2610 2610 result['repo_name'] = result.pop('_repo_name', None)
2611 2611 return result
2612 2612
2613 2613
2614 2614 class RepoGroup(Base, BaseModel):
2615 2615 __tablename__ = 'groups'
2616 2616 __table_args__ = (
2617 2617 UniqueConstraint('group_name', 'group_parent_id'),
2618 2618 base_table_args,
2619 2619 )
2620 2620 __mapper_args__ = {'order_by': 'group_name'}
2621 2621
2622 2622 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2623 2623
2624 2624 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2625 2625 _group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2626 2626 group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
2627 2627 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2628 2628 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2629 2629 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2630 2630 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2631 2631 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2632 2632 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2633 2633 personal = Column('personal', Boolean(), nullable=True, unique=None, default=None)
2634 2634 _changeset_cache = Column("changeset_cache", LargeBinary(), nullable=True) # JSON data
2635 2635
2636 2636 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2637 2637 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2638 2638 parent_group = relationship('RepoGroup', remote_side=group_id)
2639 2639 user = relationship('User')
2640 2640 integrations = relationship('Integration', cascade="all, delete-orphan")
2641 2641
2642 2642 # no cascade, set NULL
2643 2643 scope_artifacts = relationship('FileStore', primaryjoin='FileStore.scope_repo_group_id==RepoGroup.group_id')
2644 2644
2645 2645 def __init__(self, group_name='', parent_group=None):
2646 2646 self.group_name = group_name
2647 2647 self.parent_group = parent_group
2648 2648
2649 2649 def __unicode__(self):
2650 2650 return u"<%s('id:%s:%s')>" % (
2651 2651 self.__class__.__name__, self.group_id, self.group_name)
2652 2652
2653 2653 @hybrid_property
2654 2654 def group_name(self):
2655 2655 return self._group_name
2656 2656
2657 2657 @group_name.setter
2658 2658 def group_name(self, value):
2659 2659 self._group_name = value
2660 2660 self.group_name_hash = self.hash_repo_group_name(value)
2661 2661
2662 2662 @classmethod
2663 2663 def _load_changeset_cache(cls, repo_id, changeset_cache_raw):
2664 2664 from rhodecode.lib.vcs.backends.base import EmptyCommit
2665 2665 dummy = EmptyCommit().__json__()
2666 2666 if not changeset_cache_raw:
2667 2667 dummy['source_repo_id'] = repo_id
2668 2668 return json.loads(json.dumps(dummy))
2669 2669
2670 2670 try:
2671 2671 return json.loads(changeset_cache_raw)
2672 2672 except TypeError:
2673 2673 return dummy
2674 2674 except Exception:
2675 2675 log.error(traceback.format_exc())
2676 2676 return dummy
2677 2677
2678 2678 @hybrid_property
2679 2679 def changeset_cache(self):
2680 2680 return self._load_changeset_cache('', self._changeset_cache)
2681 2681
2682 2682 @changeset_cache.setter
2683 2683 def changeset_cache(self, val):
2684 2684 try:
2685 2685 self._changeset_cache = json.dumps(val)
2686 2686 except Exception:
2687 2687 log.error(traceback.format_exc())
2688 2688
2689 2689 @validates('group_parent_id')
2690 2690 def validate_group_parent_id(self, key, val):
2691 2691 """
2692 2692 Check cycle references for a parent group to self
2693 2693 """
2694 2694 if self.group_id and val:
2695 2695 assert val != self.group_id
2696 2696
2697 2697 return val
2698 2698
2699 2699 @hybrid_property
2700 2700 def description_safe(self):
2701 2701 from rhodecode.lib import helpers as h
2702 2702 return h.escape(self.group_description)
2703 2703
2704 2704 @classmethod
2705 2705 def hash_repo_group_name(cls, repo_group_name):
2706 2706 val = remove_formatting(repo_group_name)
2707 2707 val = safe_str(val).lower()
2708 2708 chars = []
2709 2709 for c in val:
2710 2710 if c not in string.ascii_letters:
2711 2711 c = str(ord(c))
2712 2712 chars.append(c)
2713 2713
2714 2714 return ''.join(chars)
2715 2715
2716 2716 @classmethod
2717 2717 def _generate_choice(cls, repo_group):
2718 2718 from webhelpers2.html import literal as _literal
2719 2719 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2720 2720 return repo_group.group_id, _name(repo_group.full_path_splitted)
2721 2721
2722 2722 @classmethod
2723 2723 def groups_choices(cls, groups=None, show_empty_group=True):
2724 2724 if not groups:
2725 2725 groups = cls.query().all()
2726 2726
2727 2727 repo_groups = []
2728 2728 if show_empty_group:
2729 2729 repo_groups = [(-1, u'-- %s --' % _('No parent'))]
2730 2730
2731 2731 repo_groups.extend([cls._generate_choice(x) for x in groups])
2732 2732
2733 2733 repo_groups = sorted(
2734 2734 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2735 2735 return repo_groups
2736 2736
2737 2737 @classmethod
2738 2738 def url_sep(cls):
2739 2739 return URL_SEP
2740 2740
2741 2741 @classmethod
2742 2742 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2743 2743 if case_insensitive:
2744 2744 gr = cls.query().filter(func.lower(cls.group_name)
2745 2745 == func.lower(group_name))
2746 2746 else:
2747 2747 gr = cls.query().filter(cls.group_name == group_name)
2748 2748 if cache:
2749 2749 name_key = _hash_key(group_name)
2750 2750 gr = gr.options(
2751 2751 FromCache("sql_cache_short", "get_group_%s" % name_key))
2752 2752 return gr.scalar()
2753 2753
2754 2754 @classmethod
2755 2755 def get_user_personal_repo_group(cls, user_id):
2756 2756 user = User.get(user_id)
2757 2757 if user.username == User.DEFAULT_USER:
2758 2758 return None
2759 2759
2760 2760 return cls.query()\
2761 2761 .filter(cls.personal == true()) \
2762 2762 .filter(cls.user == user) \
2763 2763 .order_by(cls.group_id.asc()) \
2764 2764 .first()
2765 2765
2766 2766 @classmethod
2767 2767 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2768 2768 case_insensitive=True):
2769 2769 q = RepoGroup.query()
2770 2770
2771 2771 if not isinstance(user_id, Optional):
2772 2772 q = q.filter(RepoGroup.user_id == user_id)
2773 2773
2774 2774 if not isinstance(group_id, Optional):
2775 2775 q = q.filter(RepoGroup.group_parent_id == group_id)
2776 2776
2777 2777 if case_insensitive:
2778 2778 q = q.order_by(func.lower(RepoGroup.group_name))
2779 2779 else:
2780 2780 q = q.order_by(RepoGroup.group_name)
2781 2781 return q.all()
2782 2782
2783 2783 @property
2784 2784 def parents(self, parents_recursion_limit=10):
2785 2785 groups = []
2786 2786 if self.parent_group is None:
2787 2787 return groups
2788 2788 cur_gr = self.parent_group
2789 2789 groups.insert(0, cur_gr)
2790 2790 cnt = 0
2791 2791 while 1:
2792 2792 cnt += 1
2793 2793 gr = getattr(cur_gr, 'parent_group', None)
2794 2794 cur_gr = cur_gr.parent_group
2795 2795 if gr is None:
2796 2796 break
2797 2797 if cnt == parents_recursion_limit:
2798 2798 # this will prevent accidental infinit loops
2799 2799 log.error('more than %s parents found for group %s, stopping '
2800 2800 'recursive parent fetching', parents_recursion_limit, self)
2801 2801 break
2802 2802
2803 2803 groups.insert(0, gr)
2804 2804 return groups
2805 2805
2806 2806 @property
2807 2807 def last_commit_cache_update_diff(self):
2808 2808 return time.time() - (safe_int(self.changeset_cache.get('updated_on')) or 0)
2809 2809
2810 2810 @classmethod
2811 2811 def _load_commit_change(cls, last_commit_cache):
2812 2812 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2813 2813 empty_date = datetime.datetime.fromtimestamp(0)
2814 2814 date_latest = last_commit_cache.get('date', empty_date)
2815 2815 try:
2816 2816 return parse_datetime(date_latest)
2817 2817 except Exception:
2818 2818 return empty_date
2819 2819
2820 2820 @property
2821 2821 def last_commit_change(self):
2822 2822 return self._load_commit_change(self.changeset_cache)
2823 2823
2824 2824 @property
2825 2825 def last_db_change(self):
2826 2826 return self.updated_on
2827 2827
2828 2828 @property
2829 2829 def children(self):
2830 2830 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2831 2831
2832 2832 @property
2833 2833 def name(self):
2834 2834 return self.group_name.split(RepoGroup.url_sep())[-1]
2835 2835
2836 2836 @property
2837 2837 def full_path(self):
2838 2838 return self.group_name
2839 2839
2840 2840 @property
2841 2841 def full_path_splitted(self):
2842 2842 return self.group_name.split(RepoGroup.url_sep())
2843 2843
2844 2844 @property
2845 2845 def repositories(self):
2846 2846 return Repository.query()\
2847 2847 .filter(Repository.group == self)\
2848 2848 .order_by(Repository.repo_name)
2849 2849
2850 2850 @property
2851 2851 def repositories_recursive_count(self):
2852 2852 cnt = self.repositories.count()
2853 2853
2854 2854 def children_count(group):
2855 2855 cnt = 0
2856 2856 for child in group.children:
2857 2857 cnt += child.repositories.count()
2858 2858 cnt += children_count(child)
2859 2859 return cnt
2860 2860
2861 2861 return cnt + children_count(self)
2862 2862
2863 2863 def _recursive_objects(self, include_repos=True, include_groups=True):
2864 2864 all_ = []
2865 2865
2866 2866 def _get_members(root_gr):
2867 2867 if include_repos:
2868 2868 for r in root_gr.repositories:
2869 2869 all_.append(r)
2870 2870 childs = root_gr.children.all()
2871 2871 if childs:
2872 2872 for gr in childs:
2873 2873 if include_groups:
2874 2874 all_.append(gr)
2875 2875 _get_members(gr)
2876 2876
2877 2877 root_group = []
2878 2878 if include_groups:
2879 2879 root_group = [self]
2880 2880
2881 2881 _get_members(self)
2882 2882 return root_group + all_
2883 2883
2884 2884 def recursive_groups_and_repos(self):
2885 2885 """
2886 2886 Recursive return all groups, with repositories in those groups
2887 2887 """
2888 2888 return self._recursive_objects()
2889 2889
2890 2890 def recursive_groups(self):
2891 2891 """
2892 2892 Returns all children groups for this group including children of children
2893 2893 """
2894 2894 return self._recursive_objects(include_repos=False)
2895 2895
2896 2896 def recursive_repos(self):
2897 2897 """
2898 2898 Returns all children repositories for this group
2899 2899 """
2900 2900 return self._recursive_objects(include_groups=False)
2901 2901
2902 2902 def get_new_name(self, group_name):
2903 2903 """
2904 2904 returns new full group name based on parent and new name
2905 2905
2906 2906 :param group_name:
2907 2907 """
2908 2908 path_prefix = (self.parent_group.full_path_splitted if
2909 2909 self.parent_group else [])
2910 2910 return RepoGroup.url_sep().join(path_prefix + [group_name])
2911 2911
2912 2912 def update_commit_cache(self, config=None):
2913 2913 """
2914 2914 Update cache of last commit for newest repository inside this repository group.
2915 2915 cache_keys should be::
2916 2916
2917 2917 source_repo_id
2918 2918 short_id
2919 2919 raw_id
2920 2920 revision
2921 2921 parents
2922 2922 message
2923 2923 date
2924 2924 author
2925 2925
2926 2926 """
2927 2927 from rhodecode.lib.vcs.utils.helpers import parse_datetime
2928 2928 empty_date = datetime.datetime.fromtimestamp(0)
2929 2929
2930 2930 def repo_groups_and_repos(root_gr):
2931 2931 for _repo in root_gr.repositories:
2932 2932 yield _repo
2933 2933 for child_group in root_gr.children.all():
2934 2934 yield child_group
2935 2935
2936 2936 latest_repo_cs_cache = {}
2937 2937 for obj in repo_groups_and_repos(self):
2938 2938 repo_cs_cache = obj.changeset_cache
2939 2939 date_latest = latest_repo_cs_cache.get('date', empty_date)
2940 2940 date_current = repo_cs_cache.get('date', empty_date)
2941 2941 current_timestamp = datetime_to_time(parse_datetime(date_latest))
2942 2942 if current_timestamp < datetime_to_time(parse_datetime(date_current)):
2943 2943 latest_repo_cs_cache = repo_cs_cache
2944 2944 if hasattr(obj, 'repo_id'):
2945 2945 latest_repo_cs_cache['source_repo_id'] = obj.repo_id
2946 2946 else:
2947 2947 latest_repo_cs_cache['source_repo_id'] = repo_cs_cache.get('source_repo_id')
2948 2948
2949 2949 _date_latest = parse_datetime(latest_repo_cs_cache.get('date') or empty_date)
2950 2950
2951 2951 latest_repo_cs_cache['updated_on'] = time.time()
2952 2952 self.changeset_cache = latest_repo_cs_cache
2953 2953 self.updated_on = _date_latest
2954 2954 Session().add(self)
2955 2955 Session().commit()
2956 2956
2957 2957 log.debug('updated repo group `%s` with new commit cache %s, and last update_date: %s',
2958 2958 self.group_name, latest_repo_cs_cache, _date_latest)
2959 2959
2960 2960 def permissions(self, with_admins=True, with_owner=True,
2961 2961 expand_from_user_groups=False):
2962 2962 """
2963 2963 Permissions for repository groups
2964 2964 """
2965 2965 _admin_perm = 'group.admin'
2966 2966
2967 2967 owner_row = []
2968 2968 if with_owner:
2969 2969 usr = AttributeDict(self.user.get_dict())
2970 2970 usr.owner_row = True
2971 2971 usr.permission = _admin_perm
2972 2972 owner_row.append(usr)
2973 2973
2974 2974 super_admin_ids = []
2975 2975 super_admin_rows = []
2976 2976 if with_admins:
2977 2977 for usr in User.get_all_super_admins():
2978 2978 super_admin_ids.append(usr.user_id)
2979 2979 # if this admin is also owner, don't double the record
2980 2980 if usr.user_id == owner_row[0].user_id:
2981 2981 owner_row[0].admin_row = True
2982 2982 else:
2983 2983 usr = AttributeDict(usr.get_dict())
2984 2984 usr.admin_row = True
2985 2985 usr.permission = _admin_perm
2986 2986 super_admin_rows.append(usr)
2987 2987
2988 2988 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2989 2989 q = q.options(joinedload(UserRepoGroupToPerm.group),
2990 2990 joinedload(UserRepoGroupToPerm.user),
2991 2991 joinedload(UserRepoGroupToPerm.permission),)
2992 2992
2993 2993 # get owners and admins and permissions. We do a trick of re-writing
2994 2994 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2995 2995 # has a global reference and changing one object propagates to all
2996 2996 # others. This means if admin is also an owner admin_row that change
2997 2997 # would propagate to both objects
2998 2998 perm_rows = []
2999 2999 for _usr in q.all():
3000 3000 usr = AttributeDict(_usr.user.get_dict())
3001 3001 # if this user is also owner/admin, mark as duplicate record
3002 3002 if usr.user_id == owner_row[0].user_id or usr.user_id in super_admin_ids:
3003 3003 usr.duplicate_perm = True
3004 3004 usr.permission = _usr.permission.permission_name
3005 3005 perm_rows.append(usr)
3006 3006
3007 3007 # filter the perm rows by 'default' first and then sort them by
3008 3008 # admin,write,read,none permissions sorted again alphabetically in
3009 3009 # each group
3010 3010 perm_rows = sorted(perm_rows, key=display_user_sort)
3011 3011
3012 3012 user_groups_rows = []
3013 3013 if expand_from_user_groups:
3014 3014 for ug in self.permission_user_groups(with_members=True):
3015 3015 for user_data in ug.members:
3016 3016 user_groups_rows.append(user_data)
3017 3017
3018 3018 return super_admin_rows + owner_row + perm_rows + user_groups_rows
3019 3019
3020 3020 def permission_user_groups(self, with_members=False):
3021 3021 q = UserGroupRepoGroupToPerm.query()\
3022 3022 .filter(UserGroupRepoGroupToPerm.group == self)
3023 3023 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
3024 3024 joinedload(UserGroupRepoGroupToPerm.users_group),
3025 3025 joinedload(UserGroupRepoGroupToPerm.permission),)
3026 3026
3027 3027 perm_rows = []
3028 3028 for _user_group in q.all():
3029 3029 entry = AttributeDict(_user_group.users_group.get_dict())
3030 3030 entry.permission = _user_group.permission.permission_name
3031 3031 if with_members:
3032 3032 entry.members = [x.user.get_dict()
3033 3033 for x in _user_group.users_group.members]
3034 3034 perm_rows.append(entry)
3035 3035
3036 3036 perm_rows = sorted(perm_rows, key=display_user_group_sort)
3037 3037 return perm_rows
3038 3038
3039 3039 def get_api_data(self):
3040 3040 """
3041 3041 Common function for generating api data
3042 3042
3043 3043 """
3044 3044 group = self
3045 3045 data = {
3046 3046 'group_id': group.group_id,
3047 3047 'group_name': group.group_name,
3048 3048 'group_description': group.description_safe,
3049 3049 'parent_group': group.parent_group.group_name if group.parent_group else None,
3050 3050 'repositories': [x.repo_name for x in group.repositories],
3051 3051 'owner': group.user.username,
3052 3052 }
3053 3053 return data
3054 3054
3055 3055 def get_dict(self):
3056 3056 # Since we transformed `group_name` to a hybrid property, we need to
3057 3057 # keep compatibility with the code which uses `group_name` field.
3058 3058 result = super(RepoGroup, self).get_dict()
3059 3059 result['group_name'] = result.pop('_group_name', None)
3060 3060 return result
3061 3061
3062 3062
3063 3063 class Permission(Base, BaseModel):
3064 3064 __tablename__ = 'permissions'
3065 3065 __table_args__ = (
3066 3066 Index('p_perm_name_idx', 'permission_name'),
3067 3067 base_table_args,
3068 3068 )
3069 3069
3070 3070 PERMS = [
3071 3071 ('hg.admin', _('RhodeCode Super Administrator')),
3072 3072
3073 3073 ('repository.none', _('Repository no access')),
3074 3074 ('repository.read', _('Repository read access')),
3075 3075 ('repository.write', _('Repository write access')),
3076 3076 ('repository.admin', _('Repository admin access')),
3077 3077
3078 3078 ('group.none', _('Repository group no access')),
3079 3079 ('group.read', _('Repository group read access')),
3080 3080 ('group.write', _('Repository group write access')),
3081 3081 ('group.admin', _('Repository group admin access')),
3082 3082
3083 3083 ('usergroup.none', _('User group no access')),
3084 3084 ('usergroup.read', _('User group read access')),
3085 3085 ('usergroup.write', _('User group write access')),
3086 3086 ('usergroup.admin', _('User group admin access')),
3087 3087
3088 3088 ('branch.none', _('Branch no permissions')),
3089 3089 ('branch.merge', _('Branch access by web merge')),
3090 3090 ('branch.push', _('Branch access by push')),
3091 3091 ('branch.push_force', _('Branch access by push with force')),
3092 3092
3093 3093 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
3094 3094 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
3095 3095
3096 3096 ('hg.usergroup.create.false', _('User Group creation disabled')),
3097 3097 ('hg.usergroup.create.true', _('User Group creation enabled')),
3098 3098
3099 3099 ('hg.create.none', _('Repository creation disabled')),
3100 3100 ('hg.create.repository', _('Repository creation enabled')),
3101 3101 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
3102 3102 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
3103 3103
3104 3104 ('hg.fork.none', _('Repository forking disabled')),
3105 3105 ('hg.fork.repository', _('Repository forking enabled')),
3106 3106
3107 3107 ('hg.register.none', _('Registration disabled')),
3108 3108 ('hg.register.manual_activate', _('User Registration with manual account activation')),
3109 3109 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
3110 3110
3111 3111 ('hg.password_reset.enabled', _('Password reset enabled')),
3112 3112 ('hg.password_reset.hidden', _('Password reset hidden')),
3113 3113 ('hg.password_reset.disabled', _('Password reset disabled')),
3114 3114
3115 3115 ('hg.extern_activate.manual', _('Manual activation of external account')),
3116 3116 ('hg.extern_activate.auto', _('Automatic activation of external account')),
3117 3117
3118 3118 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
3119 3119 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
3120 3120 ]
3121 3121
3122 3122 # definition of system default permissions for DEFAULT user, created on
3123 3123 # system setup
3124 3124 DEFAULT_USER_PERMISSIONS = [
3125 3125 # object perms
3126 3126 'repository.read',
3127 3127 'group.read',
3128 3128 'usergroup.read',
3129 3129 # branch, for backward compat we need same value as before so forced pushed
3130 3130 'branch.push_force',
3131 3131 # global
3132 3132 'hg.create.repository',
3133 3133 'hg.repogroup.create.false',
3134 3134 'hg.usergroup.create.false',
3135 3135 'hg.create.write_on_repogroup.true',
3136 3136 'hg.fork.repository',
3137 3137 'hg.register.manual_activate',
3138 3138 'hg.password_reset.enabled',
3139 3139 'hg.extern_activate.auto',
3140 3140 'hg.inherit_default_perms.true',
3141 3141 ]
3142 3142
3143 3143 # defines which permissions are more important higher the more important
3144 3144 # Weight defines which permissions are more important.
3145 3145 # The higher number the more important.
3146 3146 PERM_WEIGHTS = {
3147 3147 'repository.none': 0,
3148 3148 'repository.read': 1,
3149 3149 'repository.write': 3,
3150 3150 'repository.admin': 4,
3151 3151
3152 3152 'group.none': 0,
3153 3153 'group.read': 1,
3154 3154 'group.write': 3,
3155 3155 'group.admin': 4,
3156 3156
3157 3157 'usergroup.none': 0,
3158 3158 'usergroup.read': 1,
3159 3159 'usergroup.write': 3,
3160 3160 'usergroup.admin': 4,
3161 3161
3162 3162 'branch.none': 0,
3163 3163 'branch.merge': 1,
3164 3164 'branch.push': 3,
3165 3165 'branch.push_force': 4,
3166 3166
3167 3167 'hg.repogroup.create.false': 0,
3168 3168 'hg.repogroup.create.true': 1,
3169 3169
3170 3170 'hg.usergroup.create.false': 0,
3171 3171 'hg.usergroup.create.true': 1,
3172 3172
3173 3173 'hg.fork.none': 0,
3174 3174 'hg.fork.repository': 1,
3175 3175 'hg.create.none': 0,
3176 3176 'hg.create.repository': 1
3177 3177 }
3178 3178
3179 3179 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3180 3180 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
3181 3181 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
3182 3182
3183 3183 def __unicode__(self):
3184 3184 return u"<%s('%s:%s')>" % (
3185 3185 self.__class__.__name__, self.permission_id, self.permission_name
3186 3186 )
3187 3187
3188 3188 @classmethod
3189 3189 def get_by_key(cls, key):
3190 3190 return cls.query().filter(cls.permission_name == key).scalar()
3191 3191
3192 3192 @classmethod
3193 3193 def get_default_repo_perms(cls, user_id, repo_id=None):
3194 3194 q = Session().query(UserRepoToPerm, Repository, Permission)\
3195 3195 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
3196 3196 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
3197 3197 .filter(UserRepoToPerm.user_id == user_id)
3198 3198 if repo_id:
3199 3199 q = q.filter(UserRepoToPerm.repository_id == repo_id)
3200 3200 return q.all()
3201 3201
3202 3202 @classmethod
3203 3203 def get_default_repo_branch_perms(cls, user_id, repo_id=None):
3204 3204 q = Session().query(UserToRepoBranchPermission, UserRepoToPerm, Permission) \
3205 3205 .join(
3206 3206 Permission,
3207 3207 UserToRepoBranchPermission.permission_id == Permission.permission_id) \
3208 3208 .join(
3209 3209 UserRepoToPerm,
3210 3210 UserToRepoBranchPermission.rule_to_perm_id == UserRepoToPerm.repo_to_perm_id) \
3211 3211 .filter(UserRepoToPerm.user_id == user_id)
3212 3212
3213 3213 if repo_id:
3214 3214 q = q.filter(UserToRepoBranchPermission.repository_id == repo_id)
3215 3215 return q.order_by(UserToRepoBranchPermission.rule_order).all()
3216 3216
3217 3217 @classmethod
3218 3218 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
3219 3219 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
3220 3220 .join(
3221 3221 Permission,
3222 3222 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
3223 3223 .join(
3224 3224 Repository,
3225 3225 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
3226 3226 .join(
3227 3227 UserGroup,
3228 3228 UserGroupRepoToPerm.users_group_id ==
3229 3229 UserGroup.users_group_id)\
3230 3230 .join(
3231 3231 UserGroupMember,
3232 3232 UserGroupRepoToPerm.users_group_id ==
3233 3233 UserGroupMember.users_group_id)\
3234 3234 .filter(
3235 3235 UserGroupMember.user_id == user_id,
3236 3236 UserGroup.users_group_active == true())
3237 3237 if repo_id:
3238 3238 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
3239 3239 return q.all()
3240 3240
3241 3241 @classmethod
3242 3242 def get_default_repo_branch_perms_from_user_group(cls, user_id, repo_id=None):
3243 3243 q = Session().query(UserGroupToRepoBranchPermission, UserGroupRepoToPerm, Permission) \
3244 3244 .join(
3245 3245 Permission,
3246 3246 UserGroupToRepoBranchPermission.permission_id == Permission.permission_id) \
3247 3247 .join(
3248 3248 UserGroupRepoToPerm,
3249 3249 UserGroupToRepoBranchPermission.rule_to_perm_id == UserGroupRepoToPerm.users_group_to_perm_id) \
3250 3250 .join(
3251 3251 UserGroup,
3252 3252 UserGroupRepoToPerm.users_group_id == UserGroup.users_group_id) \
3253 3253 .join(
3254 3254 UserGroupMember,
3255 3255 UserGroupRepoToPerm.users_group_id == UserGroupMember.users_group_id) \
3256 3256 .filter(
3257 3257 UserGroupMember.user_id == user_id,
3258 3258 UserGroup.users_group_active == true())
3259 3259
3260 3260 if repo_id:
3261 3261 q = q.filter(UserGroupToRepoBranchPermission.repository_id == repo_id)
3262 3262 return q.order_by(UserGroupToRepoBranchPermission.rule_order).all()
3263 3263
3264 3264 @classmethod
3265 3265 def get_default_group_perms(cls, user_id, repo_group_id=None):
3266 3266 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
3267 3267 .join(
3268 3268 Permission,
3269 3269 UserRepoGroupToPerm.permission_id == Permission.permission_id)\
3270 3270 .join(
3271 3271 RepoGroup,
3272 3272 UserRepoGroupToPerm.group_id == RepoGroup.group_id)\
3273 3273 .filter(UserRepoGroupToPerm.user_id == user_id)
3274 3274 if repo_group_id:
3275 3275 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
3276 3276 return q.all()
3277 3277
3278 3278 @classmethod
3279 3279 def get_default_group_perms_from_user_group(
3280 3280 cls, user_id, repo_group_id=None):
3281 3281 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
3282 3282 .join(
3283 3283 Permission,
3284 3284 UserGroupRepoGroupToPerm.permission_id ==
3285 3285 Permission.permission_id)\
3286 3286 .join(
3287 3287 RepoGroup,
3288 3288 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
3289 3289 .join(
3290 3290 UserGroup,
3291 3291 UserGroupRepoGroupToPerm.users_group_id ==
3292 3292 UserGroup.users_group_id)\
3293 3293 .join(
3294 3294 UserGroupMember,
3295 3295 UserGroupRepoGroupToPerm.users_group_id ==
3296 3296 UserGroupMember.users_group_id)\
3297 3297 .filter(
3298 3298 UserGroupMember.user_id == user_id,
3299 3299 UserGroup.users_group_active == true())
3300 3300 if repo_group_id:
3301 3301 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
3302 3302 return q.all()
3303 3303
3304 3304 @classmethod
3305 3305 def get_default_user_group_perms(cls, user_id, user_group_id=None):
3306 3306 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
3307 3307 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
3308 3308 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
3309 3309 .filter(UserUserGroupToPerm.user_id == user_id)
3310 3310 if user_group_id:
3311 3311 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
3312 3312 return q.all()
3313 3313
3314 3314 @classmethod
3315 3315 def get_default_user_group_perms_from_user_group(
3316 3316 cls, user_id, user_group_id=None):
3317 3317 TargetUserGroup = aliased(UserGroup, name='target_user_group')
3318 3318 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
3319 3319 .join(
3320 3320 Permission,
3321 3321 UserGroupUserGroupToPerm.permission_id ==
3322 3322 Permission.permission_id)\
3323 3323 .join(
3324 3324 TargetUserGroup,
3325 3325 UserGroupUserGroupToPerm.target_user_group_id ==
3326 3326 TargetUserGroup.users_group_id)\
3327 3327 .join(
3328 3328 UserGroup,
3329 3329 UserGroupUserGroupToPerm.user_group_id ==
3330 3330 UserGroup.users_group_id)\
3331 3331 .join(
3332 3332 UserGroupMember,
3333 3333 UserGroupUserGroupToPerm.user_group_id ==
3334 3334 UserGroupMember.users_group_id)\
3335 3335 .filter(
3336 3336 UserGroupMember.user_id == user_id,
3337 3337 UserGroup.users_group_active == true())
3338 3338 if user_group_id:
3339 3339 q = q.filter(
3340 3340 UserGroupUserGroupToPerm.user_group_id == user_group_id)
3341 3341
3342 3342 return q.all()
3343 3343
3344 3344
3345 3345 class UserRepoToPerm(Base, BaseModel):
3346 3346 __tablename__ = 'repo_to_perm'
3347 3347 __table_args__ = (
3348 3348 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
3349 3349 base_table_args
3350 3350 )
3351 3351
3352 3352 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3353 3353 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3354 3354 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3355 3355 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3356 3356
3357 3357 user = relationship('User')
3358 3358 repository = relationship('Repository')
3359 3359 permission = relationship('Permission')
3360 3360
3361 3361 branch_perm_entry = relationship('UserToRepoBranchPermission', cascade="all, delete-orphan", lazy='joined')
3362 3362
3363 3363 @classmethod
3364 3364 def create(cls, user, repository, permission):
3365 3365 n = cls()
3366 3366 n.user = user
3367 3367 n.repository = repository
3368 3368 n.permission = permission
3369 3369 Session().add(n)
3370 3370 return n
3371 3371
3372 3372 def __unicode__(self):
3373 3373 return u'<%s => %s >' % (self.user, self.repository)
3374 3374
3375 3375
3376 3376 class UserUserGroupToPerm(Base, BaseModel):
3377 3377 __tablename__ = 'user_user_group_to_perm'
3378 3378 __table_args__ = (
3379 3379 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
3380 3380 base_table_args
3381 3381 )
3382 3382
3383 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 3384 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3385 3385 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3386 3386 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3387 3387
3388 3388 user = relationship('User')
3389 3389 user_group = relationship('UserGroup')
3390 3390 permission = relationship('Permission')
3391 3391
3392 3392 @classmethod
3393 3393 def create(cls, user, user_group, permission):
3394 3394 n = cls()
3395 3395 n.user = user
3396 3396 n.user_group = user_group
3397 3397 n.permission = permission
3398 3398 Session().add(n)
3399 3399 return n
3400 3400
3401 3401 def __unicode__(self):
3402 3402 return u'<%s => %s >' % (self.user, self.user_group)
3403 3403
3404 3404
3405 3405 class UserToPerm(Base, BaseModel):
3406 3406 __tablename__ = 'user_to_perm'
3407 3407 __table_args__ = (
3408 3408 UniqueConstraint('user_id', 'permission_id'),
3409 3409 base_table_args
3410 3410 )
3411 3411
3412 3412 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3413 3413 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3414 3414 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3415 3415
3416 3416 user = relationship('User')
3417 3417 permission = relationship('Permission', lazy='joined')
3418 3418
3419 3419 def __unicode__(self):
3420 3420 return u'<%s => %s >' % (self.user, self.permission)
3421 3421
3422 3422
3423 3423 class UserGroupRepoToPerm(Base, BaseModel):
3424 3424 __tablename__ = 'users_group_repo_to_perm'
3425 3425 __table_args__ = (
3426 3426 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
3427 3427 base_table_args
3428 3428 )
3429 3429
3430 3430 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3431 3431 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3432 3432 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3433 3433 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
3434 3434
3435 3435 users_group = relationship('UserGroup')
3436 3436 permission = relationship('Permission')
3437 3437 repository = relationship('Repository')
3438 3438 user_group_branch_perms = relationship('UserGroupToRepoBranchPermission', cascade='all')
3439 3439
3440 3440 @classmethod
3441 3441 def create(cls, users_group, repository, permission):
3442 3442 n = cls()
3443 3443 n.users_group = users_group
3444 3444 n.repository = repository
3445 3445 n.permission = permission
3446 3446 Session().add(n)
3447 3447 return n
3448 3448
3449 3449 def __unicode__(self):
3450 3450 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
3451 3451
3452 3452
3453 3453 class UserGroupUserGroupToPerm(Base, BaseModel):
3454 3454 __tablename__ = 'user_group_user_group_to_perm'
3455 3455 __table_args__ = (
3456 3456 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
3457 3457 CheckConstraint('target_user_group_id != user_group_id'),
3458 3458 base_table_args
3459 3459 )
3460 3460
3461 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 3462 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3463 3463 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3464 3464 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3465 3465
3466 3466 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
3467 3467 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
3468 3468 permission = relationship('Permission')
3469 3469
3470 3470 @classmethod
3471 3471 def create(cls, target_user_group, user_group, permission):
3472 3472 n = cls()
3473 3473 n.target_user_group = target_user_group
3474 3474 n.user_group = user_group
3475 3475 n.permission = permission
3476 3476 Session().add(n)
3477 3477 return n
3478 3478
3479 3479 def __unicode__(self):
3480 3480 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
3481 3481
3482 3482
3483 3483 class UserGroupToPerm(Base, BaseModel):
3484 3484 __tablename__ = 'users_group_to_perm'
3485 3485 __table_args__ = (
3486 3486 UniqueConstraint('users_group_id', 'permission_id',),
3487 3487 base_table_args
3488 3488 )
3489 3489
3490 3490 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3491 3491 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3492 3492 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3493 3493
3494 3494 users_group = relationship('UserGroup')
3495 3495 permission = relationship('Permission')
3496 3496
3497 3497
3498 3498 class UserRepoGroupToPerm(Base, BaseModel):
3499 3499 __tablename__ = 'user_repo_group_to_perm'
3500 3500 __table_args__ = (
3501 3501 UniqueConstraint('user_id', 'group_id', 'permission_id'),
3502 3502 base_table_args
3503 3503 )
3504 3504
3505 3505 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3506 3506 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3507 3507 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3508 3508 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3509 3509
3510 3510 user = relationship('User')
3511 3511 group = relationship('RepoGroup')
3512 3512 permission = relationship('Permission')
3513 3513
3514 3514 @classmethod
3515 3515 def create(cls, user, repository_group, permission):
3516 3516 n = cls()
3517 3517 n.user = user
3518 3518 n.group = repository_group
3519 3519 n.permission = permission
3520 3520 Session().add(n)
3521 3521 return n
3522 3522
3523 3523
3524 3524 class UserGroupRepoGroupToPerm(Base, BaseModel):
3525 3525 __tablename__ = 'users_group_repo_group_to_perm'
3526 3526 __table_args__ = (
3527 3527 UniqueConstraint('users_group_id', 'group_id'),
3528 3528 base_table_args
3529 3529 )
3530 3530
3531 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 3532 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
3533 3533 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
3534 3534 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
3535 3535
3536 3536 users_group = relationship('UserGroup')
3537 3537 permission = relationship('Permission')
3538 3538 group = relationship('RepoGroup')
3539 3539
3540 3540 @classmethod
3541 3541 def create(cls, user_group, repository_group, permission):
3542 3542 n = cls()
3543 3543 n.users_group = user_group
3544 3544 n.group = repository_group
3545 3545 n.permission = permission
3546 3546 Session().add(n)
3547 3547 return n
3548 3548
3549 3549 def __unicode__(self):
3550 3550 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
3551 3551
3552 3552
3553 3553 class Statistics(Base, BaseModel):
3554 3554 __tablename__ = 'statistics'
3555 3555 __table_args__ = (
3556 3556 base_table_args
3557 3557 )
3558 3558
3559 3559 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3560 3560 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
3561 3561 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
3562 3562 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
3563 3563 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
3564 3564 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
3565 3565
3566 3566 repository = relationship('Repository', single_parent=True)
3567 3567
3568 3568
3569 3569 class UserFollowing(Base, BaseModel):
3570 3570 __tablename__ = 'user_followings'
3571 3571 __table_args__ = (
3572 3572 UniqueConstraint('user_id', 'follows_repository_id'),
3573 3573 UniqueConstraint('user_id', 'follows_user_id'),
3574 3574 base_table_args
3575 3575 )
3576 3576
3577 3577 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3578 3578 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
3579 3579 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
3580 3580 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
3581 3581 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
3582 3582
3583 3583 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
3584 3584
3585 3585 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
3586 3586 follows_repository = relationship('Repository', order_by='Repository.repo_name')
3587 3587
3588 3588 @classmethod
3589 3589 def get_repo_followers(cls, repo_id):
3590 3590 return cls.query().filter(cls.follows_repo_id == repo_id)
3591 3591
3592 3592
3593 3593 class CacheKey(Base, BaseModel):
3594 3594 __tablename__ = 'cache_invalidation'
3595 3595 __table_args__ = (
3596 3596 UniqueConstraint('cache_key'),
3597 3597 Index('key_idx', 'cache_key'),
3598 3598 base_table_args,
3599 3599 )
3600 3600
3601 3601 CACHE_TYPE_FEED = 'FEED'
3602 3602
3603 3603 # namespaces used to register process/thread aware caches
3604 3604 REPO_INVALIDATION_NAMESPACE = 'repo_cache:{repo_id}'
3605 3605 SETTINGS_INVALIDATION_NAMESPACE = 'system_settings'
3606 3606
3607 3607 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
3608 3608 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
3609 3609 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
3610 3610 cache_state_uid = Column("cache_state_uid", String(255), nullable=True, unique=None, default=None)
3611 3611 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
3612 3612
3613 3613 def __init__(self, cache_key, cache_args='', cache_state_uid=None):
3614 3614 self.cache_key = cache_key
3615 3615 self.cache_args = cache_args
3616 3616 self.cache_active = False
3617 3617 # first key should be same for all entries, since all workers should share it
3618 3618 self.cache_state_uid = cache_state_uid or self.generate_new_state_uid()
3619 3619
3620 3620 def __unicode__(self):
3621 3621 return u"<%s('%s:%s[%s]')>" % (
3622 3622 self.__class__.__name__,
3623 3623 self.cache_id, self.cache_key, self.cache_active)
3624 3624
3625 3625 def _cache_key_partition(self):
3626 3626 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
3627 3627 return prefix, repo_name, suffix
3628 3628
3629 3629 def get_prefix(self):
3630 3630 """
3631 3631 Try to extract prefix from existing cache key. The key could consist
3632 3632 of prefix, repo_name, suffix
3633 3633 """
3634 3634 # this returns prefix, repo_name, suffix
3635 3635 return self._cache_key_partition()[0]
3636 3636
3637 3637 def get_suffix(self):
3638 3638 """
3639 3639 get suffix that might have been used in _get_cache_key to
3640 3640 generate self.cache_key. Only used for informational purposes
3641 3641 in repo_edit.mako.
3642 3642 """
3643 3643 # prefix, repo_name, suffix
3644 3644 return self._cache_key_partition()[2]
3645 3645
3646 3646 @classmethod
3647 3647 def generate_new_state_uid(cls, based_on=None):
3648 3648 if based_on:
3649 3649 return str(uuid.uuid5(uuid.NAMESPACE_URL, safe_str(based_on)))
3650 3650 else:
3651 3651 return str(uuid.uuid4())
3652 3652
3653 3653 @classmethod
3654 3654 def delete_all_cache(cls):
3655 3655 """
3656 3656 Delete all cache keys from database.
3657 3657 Should only be run when all instances are down and all entries
3658 3658 thus stale.
3659 3659 """
3660 3660 cls.query().delete()
3661 3661 Session().commit()
3662 3662
3663 3663 @classmethod
3664 3664 def set_invalidate(cls, cache_uid, delete=False):
3665 3665 """
3666 3666 Mark all caches of a repo as invalid in the database.
3667 3667 """
3668 3668
3669 3669 try:
3670 3670 qry = Session().query(cls).filter(cls.cache_args == cache_uid)
3671 3671 if delete:
3672 3672 qry.delete()
3673 3673 log.debug('cache objects deleted for cache args %s',
3674 3674 safe_str(cache_uid))
3675 3675 else:
3676 3676 qry.update({"cache_active": False,
3677 3677 "cache_state_uid": cls.generate_new_state_uid()})
3678 3678 log.debug('cache objects marked as invalid for cache args %s',
3679 3679 safe_str(cache_uid))
3680 3680
3681 3681 Session().commit()
3682 3682 except Exception:
3683 3683 log.exception(
3684 3684 'Cache key invalidation failed for cache args %s',
3685 3685 safe_str(cache_uid))
3686 3686 Session().rollback()
3687 3687
3688 3688 @classmethod
3689 3689 def get_active_cache(cls, cache_key):
3690 3690 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
3691 3691 if inv_obj:
3692 3692 return inv_obj
3693 3693 return None
3694 3694
3695 3695 @classmethod
3696 3696 def get_namespace_map(cls, namespace):
3697 3697 return {
3698 3698 x.cache_key: x
3699 3699 for x in cls.query().filter(cls.cache_args == namespace)}
3700 3700
3701 3701
3702 3702 class ChangesetComment(Base, BaseModel):
3703 3703 __tablename__ = 'changeset_comments'
3704 3704 __table_args__ = (
3705 3705 Index('cc_revision_idx', 'revision'),
3706 3706 base_table_args,
3707 3707 )
3708 3708
3709 3709 COMMENT_OUTDATED = u'comment_outdated'
3710 3710 COMMENT_TYPE_NOTE = u'note'
3711 3711 COMMENT_TYPE_TODO = u'todo'
3712 3712 COMMENT_TYPES = [COMMENT_TYPE_NOTE, COMMENT_TYPE_TODO]
3713 3713
3714 3714 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
3715 3715 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3716 3716 revision = Column('revision', String(40), nullable=True)
3717 3717 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3718 3718 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
3719 3719 line_no = Column('line_no', Unicode(10), nullable=True)
3720 3720 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
3721 3721 f_path = Column('f_path', Unicode(1000), nullable=True)
3722 3722 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3723 3723 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3724 3724 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3725 3725 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3726 3726 renderer = Column('renderer', Unicode(64), nullable=True)
3727 3727 display_state = Column('display_state', Unicode(128), nullable=True)
3728 3728
3729 3729 comment_type = Column('comment_type', Unicode(128), nullable=True, default=COMMENT_TYPE_NOTE)
3730 3730 resolved_comment_id = Column('resolved_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=True)
3731 3731
3732 3732 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3733 3733 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3734 3734
3735 3735 author = relationship('User', lazy='joined')
3736 3736 repo = relationship('Repository')
3737 3737 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
3738 3738 pull_request = relationship('PullRequest', lazy='joined')
3739 3739 pull_request_version = relationship('PullRequestVersion')
3740 3740
3741 3741 @classmethod
3742 3742 def get_users(cls, revision=None, pull_request_id=None):
3743 3743 """
3744 3744 Returns user associated with this ChangesetComment. ie those
3745 3745 who actually commented
3746 3746
3747 3747 :param cls:
3748 3748 :param revision:
3749 3749 """
3750 3750 q = Session().query(User)\
3751 3751 .join(ChangesetComment.author)
3752 3752 if revision:
3753 3753 q = q.filter(cls.revision == revision)
3754 3754 elif pull_request_id:
3755 3755 q = q.filter(cls.pull_request_id == pull_request_id)
3756 3756 return q.all()
3757 3757
3758 3758 @classmethod
3759 3759 def get_index_from_version(cls, pr_version, versions):
3760 3760 num_versions = [x.pull_request_version_id for x in versions]
3761 3761 try:
3762 3762 return num_versions.index(pr_version) +1
3763 3763 except (IndexError, ValueError):
3764 3764 return
3765 3765
3766 3766 @property
3767 3767 def outdated(self):
3768 3768 return self.display_state == self.COMMENT_OUTDATED
3769 3769
3770 3770 def outdated_at_version(self, version):
3771 3771 """
3772 3772 Checks if comment is outdated for given pull request version
3773 3773 """
3774 3774 return self.outdated and self.pull_request_version_id != version
3775 3775
3776 3776 def older_than_version(self, version):
3777 3777 """
3778 3778 Checks if comment is made from previous version than given
3779 3779 """
3780 3780 if version is None:
3781 3781 return self.pull_request_version_id is not None
3782 3782
3783 3783 return self.pull_request_version_id < version
3784 3784
3785 3785 @property
3786 3786 def resolved(self):
3787 3787 return self.resolved_by[0] if self.resolved_by else None
3788 3788
3789 3789 @property
3790 3790 def is_todo(self):
3791 3791 return self.comment_type == self.COMMENT_TYPE_TODO
3792 3792
3793 3793 @property
3794 3794 def is_inline(self):
3795 3795 return self.line_no and self.f_path
3796 3796
3797 3797 def get_index_version(self, versions):
3798 3798 return self.get_index_from_version(
3799 3799 self.pull_request_version_id, versions)
3800 3800
3801 3801 def __repr__(self):
3802 3802 if self.comment_id:
3803 3803 return '<DB:Comment #%s>' % self.comment_id
3804 3804 else:
3805 3805 return '<DB:Comment at %#x>' % id(self)
3806 3806
3807 3807 def get_api_data(self):
3808 3808 comment = self
3809 3809 data = {
3810 3810 'comment_id': comment.comment_id,
3811 3811 'comment_type': comment.comment_type,
3812 3812 'comment_text': comment.text,
3813 3813 'comment_status': comment.status_change,
3814 3814 'comment_f_path': comment.f_path,
3815 3815 'comment_lineno': comment.line_no,
3816 3816 'comment_author': comment.author,
3817 3817 'comment_created_on': comment.created_on,
3818 3818 'comment_resolved_by': self.resolved
3819 3819 }
3820 3820 return data
3821 3821
3822 3822 def __json__(self):
3823 3823 data = dict()
3824 3824 data.update(self.get_api_data())
3825 3825 return data
3826 3826
3827 3827
3828 3828 class ChangesetStatus(Base, BaseModel):
3829 3829 __tablename__ = 'changeset_statuses'
3830 3830 __table_args__ = (
3831 3831 Index('cs_revision_idx', 'revision'),
3832 3832 Index('cs_version_idx', 'version'),
3833 3833 UniqueConstraint('repo_id', 'revision', 'version'),
3834 3834 base_table_args
3835 3835 )
3836 3836
3837 3837 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
3838 3838 STATUS_APPROVED = 'approved'
3839 3839 STATUS_REJECTED = 'rejected'
3840 3840 STATUS_UNDER_REVIEW = 'under_review'
3841 3841
3842 3842 STATUSES = [
3843 3843 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
3844 3844 (STATUS_APPROVED, _("Approved")),
3845 3845 (STATUS_REJECTED, _("Rejected")),
3846 3846 (STATUS_UNDER_REVIEW, _("Under Review")),
3847 3847 ]
3848 3848
3849 3849 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
3850 3850 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
3851 3851 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
3852 3852 revision = Column('revision', String(40), nullable=False)
3853 3853 status = Column('status', String(128), nullable=False, default=DEFAULT)
3854 3854 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
3855 3855 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
3856 3856 version = Column('version', Integer(), nullable=False, default=0)
3857 3857 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3858 3858
3859 3859 author = relationship('User', lazy='joined')
3860 3860 repo = relationship('Repository')
3861 3861 comment = relationship('ChangesetComment', lazy='joined')
3862 3862 pull_request = relationship('PullRequest', lazy='joined')
3863 3863
3864 3864 def __unicode__(self):
3865 3865 return u"<%s('%s[v%s]:%s')>" % (
3866 3866 self.__class__.__name__,
3867 3867 self.status, self.version, self.author
3868 3868 )
3869 3869
3870 3870 @classmethod
3871 3871 def get_status_lbl(cls, value):
3872 3872 return dict(cls.STATUSES).get(value)
3873 3873
3874 3874 @property
3875 3875 def status_lbl(self):
3876 3876 return ChangesetStatus.get_status_lbl(self.status)
3877 3877
3878 3878 def get_api_data(self):
3879 3879 status = self
3880 3880 data = {
3881 3881 'status_id': status.changeset_status_id,
3882 3882 'status': status.status,
3883 3883 }
3884 3884 return data
3885 3885
3886 3886 def __json__(self):
3887 3887 data = dict()
3888 3888 data.update(self.get_api_data())
3889 3889 return data
3890 3890
3891 3891
3892 3892 class _SetState(object):
3893 3893 """
3894 3894 Context processor allowing changing state for sensitive operation such as
3895 3895 pull request update or merge
3896 3896 """
3897 3897
3898 3898 def __init__(self, pull_request, pr_state, back_state=None):
3899 3899 self._pr = pull_request
3900 3900 self._org_state = back_state or pull_request.pull_request_state
3901 3901 self._pr_state = pr_state
3902 3902 self._current_state = None
3903 3903
3904 3904 def __enter__(self):
3905 3905 log.debug('StateLock: entering set state context of pr %s, setting state to: `%s`',
3906 3906 self._pr, self._pr_state)
3907 3907 self.set_pr_state(self._pr_state)
3908 3908 return self
3909 3909
3910 3910 def __exit__(self, exc_type, exc_val, exc_tb):
3911 3911 if exc_val is not None:
3912 3912 log.error(traceback.format_exc(exc_tb))
3913 3913 return None
3914 3914
3915 3915 self.set_pr_state(self._org_state)
3916 3916 log.debug('StateLock: exiting set state context of pr %s, setting state to: `%s`',
3917 3917 self._pr, self._org_state)
3918 3918
3919 3919 @property
3920 3920 def state(self):
3921 3921 return self._current_state
3922 3922
3923 3923 def set_pr_state(self, pr_state):
3924 3924 try:
3925 3925 self._pr.pull_request_state = pr_state
3926 3926 Session().add(self._pr)
3927 3927 Session().commit()
3928 3928 self._current_state = pr_state
3929 3929 except Exception:
3930 3930 log.exception('Failed to set PullRequest %s state to %s', self._pr, pr_state)
3931 3931 raise
3932 3932
3933 3933
3934 3934 class _PullRequestBase(BaseModel):
3935 3935 """
3936 3936 Common attributes of pull request and version entries.
3937 3937 """
3938 3938
3939 3939 # .status values
3940 3940 STATUS_NEW = u'new'
3941 3941 STATUS_OPEN = u'open'
3942 3942 STATUS_CLOSED = u'closed'
3943 3943
3944 3944 # available states
3945 3945 STATE_CREATING = u'creating'
3946 3946 STATE_UPDATING = u'updating'
3947 3947 STATE_MERGING = u'merging'
3948 3948 STATE_CREATED = u'created'
3949 3949
3950 3950 title = Column('title', Unicode(255), nullable=True)
3951 3951 description = Column(
3952 3952 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
3953 3953 nullable=True)
3954 3954 description_renderer = Column('description_renderer', Unicode(64), nullable=True)
3955 3955
3956 3956 # new/open/closed status of pull request (not approve/reject/etc)
3957 3957 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3958 3958 created_on = Column(
3959 3959 'created_on', DateTime(timezone=False), nullable=False,
3960 3960 default=datetime.datetime.now)
3961 3961 updated_on = Column(
3962 3962 'updated_on', DateTime(timezone=False), nullable=False,
3963 3963 default=datetime.datetime.now)
3964 3964
3965 3965 pull_request_state = Column("pull_request_state", String(255), nullable=True)
3966 3966
3967 3967 @declared_attr
3968 3968 def user_id(cls):
3969 3969 return Column(
3970 3970 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3971 3971 unique=None)
3972 3972
3973 3973 # 500 revisions max
3974 3974 _revisions = Column(
3975 3975 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3976 3976
3977 3977 @declared_attr
3978 3978 def source_repo_id(cls):
3979 3979 # TODO: dan: rename column to source_repo_id
3980 3980 return Column(
3981 3981 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3982 3982 nullable=False)
3983 3983
3984 3984 _source_ref = Column('org_ref', Unicode(255), nullable=False)
3985 3985
3986 3986 @hybrid_property
3987 3987 def source_ref(self):
3988 3988 return self._source_ref
3989 3989
3990 3990 @source_ref.setter
3991 3991 def source_ref(self, val):
3992 3992 parts = (val or '').split(':')
3993 3993 if len(parts) != 3:
3994 3994 raise ValueError(
3995 3995 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
3996 3996 self._source_ref = safe_unicode(val)
3997 3997
3998 3998 _target_ref = Column('other_ref', Unicode(255), nullable=False)
3999 3999
4000 4000 @hybrid_property
4001 4001 def target_ref(self):
4002 4002 return self._target_ref
4003 4003
4004 4004 @target_ref.setter
4005 4005 def target_ref(self, val):
4006 4006 parts = (val or '').split(':')
4007 4007 if len(parts) != 3:
4008 4008 raise ValueError(
4009 4009 'Invalid reference format given: {}, expected X:Y:Z'.format(val))
4010 4010 self._target_ref = safe_unicode(val)
4011 4011
4012 4012 @declared_attr
4013 4013 def target_repo_id(cls):
4014 4014 # TODO: dan: rename column to target_repo_id
4015 4015 return Column(
4016 4016 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
4017 4017 nullable=False)
4018 4018
4019 4019 _shadow_merge_ref = Column('shadow_merge_ref', Unicode(255), nullable=True)
4020 4020
4021 4021 # TODO: dan: rename column to last_merge_source_rev
4022 4022 _last_merge_source_rev = Column(
4023 4023 'last_merge_org_rev', String(40), nullable=True)
4024 4024 # TODO: dan: rename column to last_merge_target_rev
4025 4025 _last_merge_target_rev = Column(
4026 4026 'last_merge_other_rev', String(40), nullable=True)
4027 4027 _last_merge_status = Column('merge_status', Integer(), nullable=True)
4028 4028 last_merge_metadata = Column(
4029 4029 'last_merge_metadata', MutationObj.as_mutable(
4030 4030 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4031 4031
4032 4032 merge_rev = Column('merge_rev', String(40), nullable=True)
4033 4033
4034 4034 reviewer_data = Column(
4035 4035 'reviewer_data_json', MutationObj.as_mutable(
4036 4036 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4037 4037
4038 4038 @property
4039 4039 def reviewer_data_json(self):
4040 4040 return json.dumps(self.reviewer_data)
4041 4041
4042 4042 @property
4043 4043 def work_in_progress(self):
4044 4044 """checks if pull request is work in progress by checking the title"""
4045 4045 title = self.title.upper()
4046 4046 if re.match(r'^(\[WIP\]\s*|WIP:\s*|WIP\s+)', title):
4047 4047 return True
4048 4048 return False
4049 4049
4050 4050 @hybrid_property
4051 4051 def description_safe(self):
4052 4052 from rhodecode.lib import helpers as h
4053 4053 return h.escape(self.description)
4054 4054
4055 4055 @hybrid_property
4056 4056 def revisions(self):
4057 4057 return self._revisions.split(':') if self._revisions else []
4058 4058
4059 4059 @revisions.setter
4060 4060 def revisions(self, val):
4061 4061 self._revisions = u':'.join(val)
4062 4062
4063 4063 @hybrid_property
4064 4064 def last_merge_status(self):
4065 4065 return safe_int(self._last_merge_status)
4066 4066
4067 4067 @last_merge_status.setter
4068 4068 def last_merge_status(self, val):
4069 4069 self._last_merge_status = val
4070 4070
4071 4071 @declared_attr
4072 4072 def author(cls):
4073 4073 return relationship('User', lazy='joined')
4074 4074
4075 4075 @declared_attr
4076 4076 def source_repo(cls):
4077 4077 return relationship(
4078 4078 'Repository',
4079 4079 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
4080 4080
4081 4081 @property
4082 4082 def source_ref_parts(self):
4083 4083 return self.unicode_to_reference(self.source_ref)
4084 4084
4085 4085 @declared_attr
4086 4086 def target_repo(cls):
4087 4087 return relationship(
4088 4088 'Repository',
4089 4089 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
4090 4090
4091 4091 @property
4092 4092 def target_ref_parts(self):
4093 4093 return self.unicode_to_reference(self.target_ref)
4094 4094
4095 4095 @property
4096 4096 def shadow_merge_ref(self):
4097 4097 return self.unicode_to_reference(self._shadow_merge_ref)
4098 4098
4099 4099 @shadow_merge_ref.setter
4100 4100 def shadow_merge_ref(self, ref):
4101 4101 self._shadow_merge_ref = self.reference_to_unicode(ref)
4102 4102
4103 4103 @staticmethod
4104 4104 def unicode_to_reference(raw):
4105 4105 """
4106 4106 Convert a unicode (or string) to a reference object.
4107 4107 If unicode evaluates to False it returns None.
4108 4108 """
4109 4109 if raw:
4110 4110 refs = raw.split(':')
4111 4111 return Reference(*refs)
4112 4112 else:
4113 4113 return None
4114 4114
4115 4115 @staticmethod
4116 4116 def reference_to_unicode(ref):
4117 4117 """
4118 4118 Convert a reference object to unicode.
4119 4119 If reference is None it returns None.
4120 4120 """
4121 4121 if ref:
4122 4122 return u':'.join(ref)
4123 4123 else:
4124 4124 return None
4125 4125
4126 4126 def get_api_data(self, with_merge_state=True):
4127 4127 from rhodecode.model.pull_request import PullRequestModel
4128 4128
4129 4129 pull_request = self
4130 4130 if with_merge_state:
4131 4131 merge_response, merge_status, msg = \
4132 4132 PullRequestModel().merge_status(pull_request)
4133 4133 merge_state = {
4134 4134 'status': merge_status,
4135 4135 'message': safe_unicode(msg),
4136 4136 }
4137 4137 else:
4138 4138 merge_state = {'status': 'not_available',
4139 4139 'message': 'not_available'}
4140 4140
4141 4141 merge_data = {
4142 4142 'clone_url': PullRequestModel().get_shadow_clone_url(pull_request),
4143 4143 'reference': (
4144 4144 pull_request.shadow_merge_ref._asdict()
4145 4145 if pull_request.shadow_merge_ref else None),
4146 4146 }
4147 4147
4148 4148 data = {
4149 4149 'pull_request_id': pull_request.pull_request_id,
4150 4150 'url': PullRequestModel().get_url(pull_request),
4151 4151 'title': pull_request.title,
4152 4152 'description': pull_request.description,
4153 4153 'status': pull_request.status,
4154 4154 'state': pull_request.pull_request_state,
4155 4155 'created_on': pull_request.created_on,
4156 4156 'updated_on': pull_request.updated_on,
4157 4157 'commit_ids': pull_request.revisions,
4158 4158 'review_status': pull_request.calculated_review_status(),
4159 4159 'mergeable': merge_state,
4160 4160 'source': {
4161 4161 'clone_url': pull_request.source_repo.clone_url(),
4162 4162 'repository': pull_request.source_repo.repo_name,
4163 4163 'reference': {
4164 4164 'name': pull_request.source_ref_parts.name,
4165 4165 'type': pull_request.source_ref_parts.type,
4166 4166 'commit_id': pull_request.source_ref_parts.commit_id,
4167 4167 },
4168 4168 },
4169 4169 'target': {
4170 4170 'clone_url': pull_request.target_repo.clone_url(),
4171 4171 'repository': pull_request.target_repo.repo_name,
4172 4172 'reference': {
4173 4173 'name': pull_request.target_ref_parts.name,
4174 4174 'type': pull_request.target_ref_parts.type,
4175 4175 'commit_id': pull_request.target_ref_parts.commit_id,
4176 4176 },
4177 4177 },
4178 4178 'merge': merge_data,
4179 4179 'author': pull_request.author.get_api_data(include_secrets=False,
4180 4180 details='basic'),
4181 4181 'reviewers': [
4182 4182 {
4183 4183 'user': reviewer.get_api_data(include_secrets=False,
4184 4184 details='basic'),
4185 4185 'reasons': reasons,
4186 4186 'review_status': st[0][1].status if st else 'not_reviewed',
4187 4187 }
4188 4188 for obj, reviewer, reasons, mandatory, st in
4189 4189 pull_request.reviewers_statuses()
4190 4190 ]
4191 4191 }
4192 4192
4193 4193 return data
4194 4194
4195 4195 def set_state(self, pull_request_state, final_state=None):
4196 4196 """
4197 4197 # goes from initial state to updating to initial state.
4198 4198 # initial state can be changed by specifying back_state=
4199 4199 with pull_request_obj.set_state(PullRequest.STATE_UPDATING):
4200 4200 pull_request.merge()
4201 4201
4202 4202 :param pull_request_state:
4203 4203 :param final_state:
4204 4204
4205 4205 """
4206 4206
4207 4207 return _SetState(self, pull_request_state, back_state=final_state)
4208 4208
4209 4209
4210 4210 class PullRequest(Base, _PullRequestBase):
4211 4211 __tablename__ = 'pull_requests'
4212 4212 __table_args__ = (
4213 4213 base_table_args,
4214 4214 )
4215 4215
4216 4216 pull_request_id = Column(
4217 4217 'pull_request_id', Integer(), nullable=False, primary_key=True)
4218 4218
4219 4219 def __repr__(self):
4220 4220 if self.pull_request_id:
4221 4221 return '<DB:PullRequest #%s>' % self.pull_request_id
4222 4222 else:
4223 4223 return '<DB:PullRequest at %#x>' % id(self)
4224 4224
4225 4225 reviewers = relationship('PullRequestReviewers', cascade="all, delete-orphan")
4226 4226 statuses = relationship('ChangesetStatus', cascade="all, delete-orphan")
4227 4227 comments = relationship('ChangesetComment', cascade="all, delete-orphan")
4228 4228 versions = relationship('PullRequestVersion', cascade="all, delete-orphan",
4229 4229 lazy='dynamic')
4230 4230
4231 4231 @classmethod
4232 4232 def get_pr_display_object(cls, pull_request_obj, org_pull_request_obj,
4233 4233 internal_methods=None):
4234 4234
4235 4235 class PullRequestDisplay(object):
4236 4236 """
4237 4237 Special object wrapper for showing PullRequest data via Versions
4238 4238 It mimics PR object as close as possible. This is read only object
4239 4239 just for display
4240 4240 """
4241 4241
4242 4242 def __init__(self, attrs, internal=None):
4243 4243 self.attrs = attrs
4244 4244 # internal have priority over the given ones via attrs
4245 4245 self.internal = internal or ['versions']
4246 4246
4247 4247 def __getattr__(self, item):
4248 4248 if item in self.internal:
4249 4249 return getattr(self, item)
4250 4250 try:
4251 4251 return self.attrs[item]
4252 4252 except KeyError:
4253 4253 raise AttributeError(
4254 4254 '%s object has no attribute %s' % (self, item))
4255 4255
4256 4256 def __repr__(self):
4257 4257 return '<DB:PullRequestDisplay #%s>' % self.attrs.get('pull_request_id')
4258 4258
4259 4259 def versions(self):
4260 4260 return pull_request_obj.versions.order_by(
4261 4261 PullRequestVersion.pull_request_version_id).all()
4262 4262
4263 4263 def is_closed(self):
4264 4264 return pull_request_obj.is_closed()
4265 4265
4266 4266 def is_state_changing(self):
4267 4267 return pull_request_obj.is_state_changing()
4268 4268
4269 4269 @property
4270 4270 def pull_request_version_id(self):
4271 4271 return getattr(pull_request_obj, 'pull_request_version_id', None)
4272 4272
4273 4273 attrs = StrictAttributeDict(pull_request_obj.get_api_data(with_merge_state=False))
4274 4274
4275 4275 attrs.author = StrictAttributeDict(
4276 4276 pull_request_obj.author.get_api_data())
4277 4277 if pull_request_obj.target_repo:
4278 4278 attrs.target_repo = StrictAttributeDict(
4279 4279 pull_request_obj.target_repo.get_api_data())
4280 4280 attrs.target_repo.clone_url = pull_request_obj.target_repo.clone_url
4281 4281
4282 4282 if pull_request_obj.source_repo:
4283 4283 attrs.source_repo = StrictAttributeDict(
4284 4284 pull_request_obj.source_repo.get_api_data())
4285 4285 attrs.source_repo.clone_url = pull_request_obj.source_repo.clone_url
4286 4286
4287 4287 attrs.source_ref_parts = pull_request_obj.source_ref_parts
4288 4288 attrs.target_ref_parts = pull_request_obj.target_ref_parts
4289 4289 attrs.revisions = pull_request_obj.revisions
4290 4290
4291 4291 attrs.shadow_merge_ref = org_pull_request_obj.shadow_merge_ref
4292 4292 attrs.reviewer_data = org_pull_request_obj.reviewer_data
4293 4293 attrs.reviewer_data_json = org_pull_request_obj.reviewer_data_json
4294 4294
4295 4295 return PullRequestDisplay(attrs, internal=internal_methods)
4296 4296
4297 4297 def is_closed(self):
4298 4298 return self.status == self.STATUS_CLOSED
4299 4299
4300 4300 def is_state_changing(self):
4301 4301 return self.pull_request_state != PullRequest.STATE_CREATED
4302 4302
4303 4303 def __json__(self):
4304 4304 return {
4305 4305 'revisions': self.revisions,
4306 4306 'versions': self.versions_count
4307 4307 }
4308 4308
4309 4309 def calculated_review_status(self):
4310 4310 from rhodecode.model.changeset_status import ChangesetStatusModel
4311 4311 return ChangesetStatusModel().calculated_review_status(self)
4312 4312
4313 4313 def reviewers_statuses(self):
4314 4314 from rhodecode.model.changeset_status import ChangesetStatusModel
4315 4315 return ChangesetStatusModel().reviewers_statuses(self)
4316 4316
4317 4317 @property
4318 4318 def workspace_id(self):
4319 4319 from rhodecode.model.pull_request import PullRequestModel
4320 4320 return PullRequestModel()._workspace_id(self)
4321 4321
4322 4322 def get_shadow_repo(self):
4323 4323 workspace_id = self.workspace_id
4324 4324 shadow_repository_path = self.target_repo.get_shadow_repository_path(workspace_id)
4325 4325 if os.path.isdir(shadow_repository_path):
4326 4326 vcs_obj = self.target_repo.scm_instance()
4327 4327 return vcs_obj.get_shadow_instance(shadow_repository_path)
4328 4328
4329 4329 @property
4330 4330 def versions_count(self):
4331 4331 """
4332 4332 return number of versions this PR have, e.g a PR that once been
4333 4333 updated will have 2 versions
4334 4334 """
4335 4335 return self.versions.count() + 1
4336 4336
4337 4337
4338 4338 class PullRequestVersion(Base, _PullRequestBase):
4339 4339 __tablename__ = 'pull_request_versions'
4340 4340 __table_args__ = (
4341 4341 base_table_args,
4342 4342 )
4343 4343
4344 4344 pull_request_version_id = Column(
4345 4345 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
4346 4346 pull_request_id = Column(
4347 4347 'pull_request_id', Integer(),
4348 4348 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4349 4349 pull_request = relationship('PullRequest')
4350 4350
4351 4351 def __repr__(self):
4352 4352 if self.pull_request_version_id:
4353 4353 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
4354 4354 else:
4355 4355 return '<DB:PullRequestVersion at %#x>' % id(self)
4356 4356
4357 4357 @property
4358 4358 def reviewers(self):
4359 4359 return self.pull_request.reviewers
4360 4360
4361 4361 @property
4362 4362 def versions(self):
4363 4363 return self.pull_request.versions
4364 4364
4365 4365 def is_closed(self):
4366 4366 # calculate from original
4367 4367 return self.pull_request.status == self.STATUS_CLOSED
4368 4368
4369 4369 def is_state_changing(self):
4370 4370 return self.pull_request.pull_request_state != PullRequest.STATE_CREATED
4371 4371
4372 4372 def calculated_review_status(self):
4373 4373 return self.pull_request.calculated_review_status()
4374 4374
4375 4375 def reviewers_statuses(self):
4376 4376 return self.pull_request.reviewers_statuses()
4377 4377
4378 4378
4379 4379 class PullRequestReviewers(Base, BaseModel):
4380 4380 __tablename__ = 'pull_request_reviewers'
4381 4381 __table_args__ = (
4382 4382 base_table_args,
4383 4383 )
4384 4384
4385 4385 @hybrid_property
4386 4386 def reasons(self):
4387 4387 if not self._reasons:
4388 4388 return []
4389 4389 return self._reasons
4390 4390
4391 4391 @reasons.setter
4392 4392 def reasons(self, val):
4393 4393 val = val or []
4394 4394 if any(not isinstance(x, compat.string_types) for x in val):
4395 4395 raise Exception('invalid reasons type, must be list of strings')
4396 4396 self._reasons = val
4397 4397
4398 4398 pull_requests_reviewers_id = Column(
4399 4399 'pull_requests_reviewers_id', Integer(), nullable=False,
4400 4400 primary_key=True)
4401 4401 pull_request_id = Column(
4402 4402 "pull_request_id", Integer(),
4403 4403 ForeignKey('pull_requests.pull_request_id'), nullable=False)
4404 4404 user_id = Column(
4405 4405 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
4406 4406 _reasons = Column(
4407 4407 'reason', MutationList.as_mutable(
4408 4408 JsonType('list', dialect_map=dict(mysql=UnicodeText(16384)))))
4409 4409
4410 4410 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4411 4411 user = relationship('User')
4412 4412 pull_request = relationship('PullRequest')
4413 4413
4414 4414 rule_data = Column(
4415 4415 'rule_data_json',
4416 4416 JsonType(dialect_map=dict(mysql=UnicodeText(16384))))
4417 4417
4418 4418 def rule_user_group_data(self):
4419 4419 """
4420 4420 Returns the voting user group rule data for this reviewer
4421 4421 """
4422 4422
4423 4423 if self.rule_data and 'vote_rule' in self.rule_data:
4424 4424 user_group_data = {}
4425 4425 if 'rule_user_group_entry_id' in self.rule_data:
4426 4426 # means a group with voting rules !
4427 4427 user_group_data['id'] = self.rule_data['rule_user_group_entry_id']
4428 4428 user_group_data['name'] = self.rule_data['rule_name']
4429 4429 user_group_data['vote_rule'] = self.rule_data['vote_rule']
4430 4430
4431 4431 return user_group_data
4432 4432
4433 4433 def __unicode__(self):
4434 4434 return u"<%s('id:%s')>" % (self.__class__.__name__,
4435 4435 self.pull_requests_reviewers_id)
4436 4436
4437 4437
4438 4438 class Notification(Base, BaseModel):
4439 4439 __tablename__ = 'notifications'
4440 4440 __table_args__ = (
4441 4441 Index('notification_type_idx', 'type'),
4442 4442 base_table_args,
4443 4443 )
4444 4444
4445 4445 TYPE_CHANGESET_COMMENT = u'cs_comment'
4446 4446 TYPE_MESSAGE = u'message'
4447 4447 TYPE_MENTION = u'mention'
4448 4448 TYPE_REGISTRATION = u'registration'
4449 4449 TYPE_PULL_REQUEST = u'pull_request'
4450 4450 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
4451 4451 TYPE_PULL_REQUEST_UPDATE = u'pull_request_update'
4452 4452
4453 4453 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
4454 4454 subject = Column('subject', Unicode(512), nullable=True)
4455 4455 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
4456 4456 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
4457 4457 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4458 4458 type_ = Column('type', Unicode(255))
4459 4459
4460 4460 created_by_user = relationship('User')
4461 4461 notifications_to_users = relationship('UserNotification', lazy='joined',
4462 4462 cascade="all, delete-orphan")
4463 4463
4464 4464 @property
4465 4465 def recipients(self):
4466 4466 return [x.user for x in UserNotification.query()\
4467 4467 .filter(UserNotification.notification == self)\
4468 4468 .order_by(UserNotification.user_id.asc()).all()]
4469 4469
4470 4470 @classmethod
4471 4471 def create(cls, created_by, subject, body, recipients, type_=None):
4472 4472 if type_ is None:
4473 4473 type_ = Notification.TYPE_MESSAGE
4474 4474
4475 4475 notification = cls()
4476 4476 notification.created_by_user = created_by
4477 4477 notification.subject = subject
4478 4478 notification.body = body
4479 4479 notification.type_ = type_
4480 4480 notification.created_on = datetime.datetime.now()
4481 4481
4482 4482 # For each recipient link the created notification to his account
4483 4483 for u in recipients:
4484 4484 assoc = UserNotification()
4485 4485 assoc.user_id = u.user_id
4486 4486 assoc.notification = notification
4487 4487
4488 4488 # if created_by is inside recipients mark his notification
4489 4489 # as read
4490 4490 if u.user_id == created_by.user_id:
4491 4491 assoc.read = True
4492 4492 Session().add(assoc)
4493 4493
4494 4494 Session().add(notification)
4495 4495
4496 4496 return notification
4497 4497
4498 4498
4499 4499 class UserNotification(Base, BaseModel):
4500 4500 __tablename__ = 'user_to_notification'
4501 4501 __table_args__ = (
4502 4502 UniqueConstraint('user_id', 'notification_id'),
4503 4503 base_table_args
4504 4504 )
4505 4505
4506 4506 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4507 4507 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
4508 4508 read = Column('read', Boolean, default=False)
4509 4509 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
4510 4510
4511 4511 user = relationship('User', lazy="joined")
4512 4512 notification = relationship('Notification', lazy="joined",
4513 4513 order_by=lambda: Notification.created_on.desc(),)
4514 4514
4515 4515 def mark_as_read(self):
4516 4516 self.read = True
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__ = (
4523 4582 Index('g_gist_access_id_idx', 'gist_access_id'),
4524 4583 Index('g_created_on_idx', 'created_on'),
4525 4584 base_table_args
4526 4585 )
4527 4586
4528 4587 GIST_PUBLIC = u'public'
4529 4588 GIST_PRIVATE = u'private'
4530 4589 DEFAULT_FILENAME = u'gistfile1.txt'
4531 4590
4532 4591 ACL_LEVEL_PUBLIC = u'acl_public'
4533 4592 ACL_LEVEL_PRIVATE = u'acl_private'
4534 4593
4535 4594 gist_id = Column('gist_id', Integer(), primary_key=True)
4536 4595 gist_access_id = Column('gist_access_id', Unicode(250))
4537 4596 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
4538 4597 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
4539 4598 gist_expires = Column('gist_expires', Float(53), nullable=False)
4540 4599 gist_type = Column('gist_type', Unicode(128), nullable=False)
4541 4600 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4542 4601 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4543 4602 acl_level = Column('acl_level', Unicode(128), nullable=True)
4544 4603
4545 4604 owner = relationship('User')
4546 4605
4547 4606 def __repr__(self):
4548 4607 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
4549 4608
4550 4609 @hybrid_property
4551 4610 def description_safe(self):
4552 4611 from rhodecode.lib import helpers as h
4553 4612 return h.escape(self.gist_description)
4554 4613
4555 4614 @classmethod
4556 4615 def get_or_404(cls, id_):
4557 4616 from pyramid.httpexceptions import HTTPNotFound
4558 4617
4559 4618 res = cls.query().filter(cls.gist_access_id == id_).scalar()
4560 4619 if not res:
4561 4620 raise HTTPNotFound()
4562 4621 return res
4563 4622
4564 4623 @classmethod
4565 4624 def get_by_access_id(cls, gist_access_id):
4566 4625 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
4567 4626
4568 4627 def gist_url(self):
4569 4628 from rhodecode.model.gist import GistModel
4570 4629 return GistModel().get_url(self)
4571 4630
4572 4631 @classmethod
4573 4632 def base_path(cls):
4574 4633 """
4575 4634 Returns base path when all gists are stored
4576 4635
4577 4636 :param cls:
4578 4637 """
4579 4638 from rhodecode.model.gist import GIST_STORE_LOC
4580 4639 q = Session().query(RhodeCodeUi)\
4581 4640 .filter(RhodeCodeUi.ui_key == URL_SEP)
4582 4641 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
4583 4642 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
4584 4643
4585 4644 def get_api_data(self):
4586 4645 """
4587 4646 Common function for generating gist related data for API
4588 4647 """
4589 4648 gist = self
4590 4649 data = {
4591 4650 'gist_id': gist.gist_id,
4592 4651 'type': gist.gist_type,
4593 4652 'access_id': gist.gist_access_id,
4594 4653 'description': gist.gist_description,
4595 4654 'url': gist.gist_url(),
4596 4655 'expires': gist.gist_expires,
4597 4656 'created_on': gist.created_on,
4598 4657 'modified_at': gist.modified_at,
4599 4658 'content': None,
4600 4659 'acl_level': gist.acl_level,
4601 4660 }
4602 4661 return data
4603 4662
4604 4663 def __json__(self):
4605 4664 data = dict(
4606 4665 )
4607 4666 data.update(self.get_api_data())
4608 4667 return data
4609 4668 # SCM functions
4610 4669
4611 4670 def scm_instance(self, **kwargs):
4612 4671 """
4613 4672 Get an instance of VCS Repository
4614 4673
4615 4674 :param kwargs:
4616 4675 """
4617 4676 from rhodecode.model.gist import GistModel
4618 4677 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
4619 4678 return get_vcs_instance(
4620 4679 repo_path=safe_str(full_repo_path), create=False,
4621 4680 _vcs_alias=GistModel.vcs_backend)
4622 4681
4623 4682
4624 4683 class ExternalIdentity(Base, BaseModel):
4625 4684 __tablename__ = 'external_identities'
4626 4685 __table_args__ = (
4627 4686 Index('local_user_id_idx', 'local_user_id'),
4628 4687 Index('external_id_idx', 'external_id'),
4629 4688 base_table_args
4630 4689 )
4631 4690
4632 4691 external_id = Column('external_id', Unicode(255), default=u'', primary_key=True)
4633 4692 external_username = Column('external_username', Unicode(1024), default=u'')
4634 4693 local_user_id = Column('local_user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
4635 4694 provider_name = Column('provider_name', Unicode(255), default=u'', primary_key=True)
4636 4695 access_token = Column('access_token', String(1024), default=u'')
4637 4696 alt_token = Column('alt_token', String(1024), default=u'')
4638 4697 token_secret = Column('token_secret', String(1024), default=u'')
4639 4698
4640 4699 @classmethod
4641 4700 def by_external_id_and_provider(cls, external_id, provider_name, local_user_id=None):
4642 4701 """
4643 4702 Returns ExternalIdentity instance based on search params
4644 4703
4645 4704 :param external_id:
4646 4705 :param provider_name:
4647 4706 :return: ExternalIdentity
4648 4707 """
4649 4708 query = cls.query()
4650 4709 query = query.filter(cls.external_id == external_id)
4651 4710 query = query.filter(cls.provider_name == provider_name)
4652 4711 if local_user_id:
4653 4712 query = query.filter(cls.local_user_id == local_user_id)
4654 4713 return query.first()
4655 4714
4656 4715 @classmethod
4657 4716 def user_by_external_id_and_provider(cls, external_id, provider_name):
4658 4717 """
4659 4718 Returns User instance based on search params
4660 4719
4661 4720 :param external_id:
4662 4721 :param provider_name:
4663 4722 :return: User
4664 4723 """
4665 4724 query = User.query()
4666 4725 query = query.filter(cls.external_id == external_id)
4667 4726 query = query.filter(cls.provider_name == provider_name)
4668 4727 query = query.filter(User.user_id == cls.local_user_id)
4669 4728 return query.first()
4670 4729
4671 4730 @classmethod
4672 4731 def by_local_user_id(cls, local_user_id):
4673 4732 """
4674 4733 Returns all tokens for user
4675 4734
4676 4735 :param local_user_id:
4677 4736 :return: ExternalIdentity
4678 4737 """
4679 4738 query = cls.query()
4680 4739 query = query.filter(cls.local_user_id == local_user_id)
4681 4740 return query
4682 4741
4683 4742 @classmethod
4684 4743 def load_provider_plugin(cls, plugin_id):
4685 4744 from rhodecode.authentication.base import loadplugin
4686 4745 _plugin_id = 'egg:rhodecode-enterprise-ee#{}'.format(plugin_id)
4687 4746 auth_plugin = loadplugin(_plugin_id)
4688 4747 return auth_plugin
4689 4748
4690 4749
4691 4750 class Integration(Base, BaseModel):
4692 4751 __tablename__ = 'integrations'
4693 4752 __table_args__ = (
4694 4753 base_table_args
4695 4754 )
4696 4755
4697 4756 integration_id = Column('integration_id', Integer(), primary_key=True)
4698 4757 integration_type = Column('integration_type', String(255))
4699 4758 enabled = Column('enabled', Boolean(), nullable=False)
4700 4759 name = Column('name', String(255), nullable=False)
4701 4760 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
4702 4761 default=False)
4703 4762
4704 4763 settings = Column(
4705 4764 'settings_json', MutationObj.as_mutable(
4706 4765 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
4707 4766 repo_id = Column(
4708 4767 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
4709 4768 nullable=True, unique=None, default=None)
4710 4769 repo = relationship('Repository', lazy='joined')
4711 4770
4712 4771 repo_group_id = Column(
4713 4772 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
4714 4773 nullable=True, unique=None, default=None)
4715 4774 repo_group = relationship('RepoGroup', lazy='joined')
4716 4775
4717 4776 @property
4718 4777 def scope(self):
4719 4778 if self.repo:
4720 4779 return repr(self.repo)
4721 4780 if self.repo_group:
4722 4781 if self.child_repos_only:
4723 4782 return repr(self.repo_group) + ' (child repos only)'
4724 4783 else:
4725 4784 return repr(self.repo_group) + ' (recursive)'
4726 4785 if self.child_repos_only:
4727 4786 return 'root_repos'
4728 4787 return 'global'
4729 4788
4730 4789 def __repr__(self):
4731 4790 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
4732 4791
4733 4792
4734 4793 class RepoReviewRuleUser(Base, BaseModel):
4735 4794 __tablename__ = 'repo_review_rules_users'
4736 4795 __table_args__ = (
4737 4796 base_table_args
4738 4797 )
4739 4798
4740 4799 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4741 4800 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4742 4801 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4743 4802 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4744 4803 user = relationship('User')
4745 4804
4746 4805 def rule_data(self):
4747 4806 return {
4748 4807 'mandatory': self.mandatory
4749 4808 }
4750 4809
4751 4810
4752 4811 class RepoReviewRuleUserGroup(Base, BaseModel):
4753 4812 __tablename__ = 'repo_review_rules_users_groups'
4754 4813 __table_args__ = (
4755 4814 base_table_args
4756 4815 )
4757 4816
4758 4817 VOTE_RULE_ALL = -1
4759 4818
4760 4819 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4761 4820 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4762 4821 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
4763 4822 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4764 4823 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4765 4824 users_group = relationship('UserGroup')
4766 4825
4767 4826 def rule_data(self):
4768 4827 return {
4769 4828 'mandatory': self.mandatory,
4770 4829 'vote_rule': self.vote_rule
4771 4830 }
4772 4831
4773 4832 @property
4774 4833 def vote_rule_label(self):
4775 4834 if not self.vote_rule or self.vote_rule == self.VOTE_RULE_ALL:
4776 4835 return 'all must vote'
4777 4836 else:
4778 4837 return 'min. vote {}'.format(self.vote_rule)
4779 4838
4780 4839
4781 4840 class RepoReviewRule(Base, BaseModel):
4782 4841 __tablename__ = 'repo_review_rules'
4783 4842 __table_args__ = (
4784 4843 base_table_args
4785 4844 )
4786 4845
4787 4846 repo_review_rule_id = Column(
4788 4847 'repo_review_rule_id', Integer(), primary_key=True)
4789 4848 repo_id = Column(
4790 4849 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
4791 4850 repo = relationship('Repository', backref='review_rules')
4792 4851
4793 4852 review_rule_name = Column('review_rule_name', String(255))
4794 4853 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4795 4854 _target_branch_pattern = Column("target_branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4796 4855 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'), default=u'*') # glob
4797 4856
4798 4857 use_authors_for_review = Column("use_authors_for_review", Boolean(), nullable=False, default=False)
4799 4858 forbid_author_to_review = Column("forbid_author_to_review", Boolean(), nullable=False, default=False)
4800 4859 forbid_commit_author_to_review = Column("forbid_commit_author_to_review", Boolean(), nullable=False, default=False)
4801 4860 forbid_adding_reviewers = Column("forbid_adding_reviewers", Boolean(), nullable=False, default=False)
4802 4861
4803 4862 rule_users = relationship('RepoReviewRuleUser')
4804 4863 rule_user_groups = relationship('RepoReviewRuleUserGroup')
4805 4864
4806 4865 def _validate_pattern(self, value):
4807 4866 re.compile('^' + glob2re(value) + '$')
4808 4867
4809 4868 @hybrid_property
4810 4869 def source_branch_pattern(self):
4811 4870 return self._branch_pattern or '*'
4812 4871
4813 4872 @source_branch_pattern.setter
4814 4873 def source_branch_pattern(self, value):
4815 4874 self._validate_pattern(value)
4816 4875 self._branch_pattern = value or '*'
4817 4876
4818 4877 @hybrid_property
4819 4878 def target_branch_pattern(self):
4820 4879 return self._target_branch_pattern or '*'
4821 4880
4822 4881 @target_branch_pattern.setter
4823 4882 def target_branch_pattern(self, value):
4824 4883 self._validate_pattern(value)
4825 4884 self._target_branch_pattern = value or '*'
4826 4885
4827 4886 @hybrid_property
4828 4887 def file_pattern(self):
4829 4888 return self._file_pattern or '*'
4830 4889
4831 4890 @file_pattern.setter
4832 4891 def file_pattern(self, value):
4833 4892 self._validate_pattern(value)
4834 4893 self._file_pattern = value or '*'
4835 4894
4836 4895 def matches(self, source_branch, target_branch, files_changed):
4837 4896 """
4838 4897 Check if this review rule matches a branch/files in a pull request
4839 4898
4840 4899 :param source_branch: source branch name for the commit
4841 4900 :param target_branch: target branch name for the commit
4842 4901 :param files_changed: list of file paths changed in the pull request
4843 4902 """
4844 4903
4845 4904 source_branch = source_branch or ''
4846 4905 target_branch = target_branch or ''
4847 4906 files_changed = files_changed or []
4848 4907
4849 4908 branch_matches = True
4850 4909 if source_branch or target_branch:
4851 4910 if self.source_branch_pattern == '*':
4852 4911 source_branch_match = True
4853 4912 else:
4854 4913 if self.source_branch_pattern.startswith('re:'):
4855 4914 source_pattern = self.source_branch_pattern[3:]
4856 4915 else:
4857 4916 source_pattern = '^' + glob2re(self.source_branch_pattern) + '$'
4858 4917 source_branch_regex = re.compile(source_pattern)
4859 4918 source_branch_match = bool(source_branch_regex.search(source_branch))
4860 4919 if self.target_branch_pattern == '*':
4861 4920 target_branch_match = True
4862 4921 else:
4863 4922 if self.target_branch_pattern.startswith('re:'):
4864 4923 target_pattern = self.target_branch_pattern[3:]
4865 4924 else:
4866 4925 target_pattern = '^' + glob2re(self.target_branch_pattern) + '$'
4867 4926 target_branch_regex = re.compile(target_pattern)
4868 4927 target_branch_match = bool(target_branch_regex.search(target_branch))
4869 4928
4870 4929 branch_matches = source_branch_match and target_branch_match
4871 4930
4872 4931 files_matches = True
4873 4932 if self.file_pattern != '*':
4874 4933 files_matches = False
4875 4934 if self.file_pattern.startswith('re:'):
4876 4935 file_pattern = self.file_pattern[3:]
4877 4936 else:
4878 4937 file_pattern = glob2re(self.file_pattern)
4879 4938 file_regex = re.compile(file_pattern)
4880 4939 for filename in files_changed:
4881 4940 if file_regex.search(filename):
4882 4941 files_matches = True
4883 4942 break
4884 4943
4885 4944 return branch_matches and files_matches
4886 4945
4887 4946 @property
4888 4947 def review_users(self):
4889 4948 """ Returns the users which this rule applies to """
4890 4949
4891 4950 users = collections.OrderedDict()
4892 4951
4893 4952 for rule_user in self.rule_users:
4894 4953 if rule_user.user.active:
4895 4954 if rule_user.user not in users:
4896 4955 users[rule_user.user.username] = {
4897 4956 'user': rule_user.user,
4898 4957 'source': 'user',
4899 4958 'source_data': {},
4900 4959 'data': rule_user.rule_data()
4901 4960 }
4902 4961
4903 4962 for rule_user_group in self.rule_user_groups:
4904 4963 source_data = {
4905 4964 'user_group_id': rule_user_group.users_group.users_group_id,
4906 4965 'name': rule_user_group.users_group.users_group_name,
4907 4966 'members': len(rule_user_group.users_group.members)
4908 4967 }
4909 4968 for member in rule_user_group.users_group.members:
4910 4969 if member.user.active:
4911 4970 key = member.user.username
4912 4971 if key in users:
4913 4972 # skip this member as we have him already
4914 4973 # this prevents from override the "first" matched
4915 4974 # users with duplicates in multiple groups
4916 4975 continue
4917 4976
4918 4977 users[key] = {
4919 4978 'user': member.user,
4920 4979 'source': 'user_group',
4921 4980 'source_data': source_data,
4922 4981 'data': rule_user_group.rule_data()
4923 4982 }
4924 4983
4925 4984 return users
4926 4985
4927 4986 def user_group_vote_rule(self, user_id):
4928 4987
4929 4988 rules = []
4930 4989 if not self.rule_user_groups:
4931 4990 return rules
4932 4991
4933 4992 for user_group in self.rule_user_groups:
4934 4993 user_group_members = [x.user_id for x in user_group.users_group.members]
4935 4994 if user_id in user_group_members:
4936 4995 rules.append(user_group)
4937 4996 return rules
4938 4997
4939 4998 def __repr__(self):
4940 4999 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
4941 5000 self.repo_review_rule_id, self.repo)
4942 5001
4943 5002
4944 5003 class ScheduleEntry(Base, BaseModel):
4945 5004 __tablename__ = 'schedule_entries'
4946 5005 __table_args__ = (
4947 5006 UniqueConstraint('schedule_name', name='s_schedule_name_idx'),
4948 5007 UniqueConstraint('task_uid', name='s_task_uid_idx'),
4949 5008 base_table_args,
4950 5009 )
4951 5010
4952 5011 schedule_types = ['crontab', 'timedelta', 'integer']
4953 5012 schedule_entry_id = Column('schedule_entry_id', Integer(), primary_key=True)
4954 5013
4955 5014 schedule_name = Column("schedule_name", String(255), nullable=False, unique=None, default=None)
4956 5015 schedule_description = Column("schedule_description", String(10000), nullable=True, unique=None, default=None)
4957 5016 schedule_enabled = Column("schedule_enabled", Boolean(), nullable=False, unique=None, default=True)
4958 5017
4959 5018 _schedule_type = Column("schedule_type", String(255), nullable=False, unique=None, default=None)
4960 5019 schedule_definition = Column('schedule_definition_json', MutationObj.as_mutable(JsonType(default=lambda: "", dialect_map=dict(mysql=LONGTEXT()))))
4961 5020
4962 5021 schedule_last_run = Column('schedule_last_run', DateTime(timezone=False), nullable=True, unique=None, default=None)
4963 5022 schedule_total_run_count = Column('schedule_total_run_count', Integer(), nullable=True, unique=None, default=0)
4964 5023
4965 5024 # task
4966 5025 task_uid = Column("task_uid", String(255), nullable=False, unique=None, default=None)
4967 5026 task_dot_notation = Column("task_dot_notation", String(4096), nullable=False, unique=None, default=None)
4968 5027 task_args = Column('task_args_json', MutationObj.as_mutable(JsonType(default=list, dialect_map=dict(mysql=LONGTEXT()))))
4969 5028 task_kwargs = Column('task_kwargs_json', MutationObj.as_mutable(JsonType(default=dict, dialect_map=dict(mysql=LONGTEXT()))))
4970 5029
4971 5030 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
4972 5031 updated_on = Column('updated_on', DateTime(timezone=False), nullable=True, unique=None, default=None)
4973 5032
4974 5033 @hybrid_property
4975 5034 def schedule_type(self):
4976 5035 return self._schedule_type
4977 5036
4978 5037 @schedule_type.setter
4979 5038 def schedule_type(self, val):
4980 5039 if val not in self.schedule_types:
4981 5040 raise ValueError('Value must be on of `{}` and got `{}`'.format(
4982 5041 val, self.schedule_type))
4983 5042
4984 5043 self._schedule_type = val
4985 5044
4986 5045 @classmethod
4987 5046 def get_uid(cls, obj):
4988 5047 args = obj.task_args
4989 5048 kwargs = obj.task_kwargs
4990 5049 if isinstance(args, JsonRaw):
4991 5050 try:
4992 5051 args = json.loads(args)
4993 5052 except ValueError:
4994 5053 args = tuple()
4995 5054
4996 5055 if isinstance(kwargs, JsonRaw):
4997 5056 try:
4998 5057 kwargs = json.loads(kwargs)
4999 5058 except ValueError:
5000 5059 kwargs = dict()
5001 5060
5002 5061 dot_notation = obj.task_dot_notation
5003 5062 val = '.'.join(map(safe_str, [
5004 5063 sorted(dot_notation), args, sorted(kwargs.items())]))
5005 5064 return hashlib.sha1(val).hexdigest()
5006 5065
5007 5066 @classmethod
5008 5067 def get_by_schedule_name(cls, schedule_name):
5009 5068 return cls.query().filter(cls.schedule_name == schedule_name).scalar()
5010 5069
5011 5070 @classmethod
5012 5071 def get_by_schedule_id(cls, schedule_id):
5013 5072 return cls.query().filter(cls.schedule_entry_id == schedule_id).scalar()
5014 5073
5015 5074 @property
5016 5075 def task(self):
5017 5076 return self.task_dot_notation
5018 5077
5019 5078 @property
5020 5079 def schedule(self):
5021 5080 from rhodecode.lib.celerylib.utils import raw_2_schedule
5022 5081 schedule = raw_2_schedule(self.schedule_definition, self.schedule_type)
5023 5082 return schedule
5024 5083
5025 5084 @property
5026 5085 def args(self):
5027 5086 try:
5028 5087 return list(self.task_args or [])
5029 5088 except ValueError:
5030 5089 return list()
5031 5090
5032 5091 @property
5033 5092 def kwargs(self):
5034 5093 try:
5035 5094 return dict(self.task_kwargs or {})
5036 5095 except ValueError:
5037 5096 return dict()
5038 5097
5039 5098 def _as_raw(self, val):
5040 5099 if hasattr(val, 'de_coerce'):
5041 5100 val = val.de_coerce()
5042 5101 if val:
5043 5102 val = json.dumps(val)
5044 5103
5045 5104 return val
5046 5105
5047 5106 @property
5048 5107 def schedule_definition_raw(self):
5049 5108 return self._as_raw(self.schedule_definition)
5050 5109
5051 5110 @property
5052 5111 def args_raw(self):
5053 5112 return self._as_raw(self.task_args)
5054 5113
5055 5114 @property
5056 5115 def kwargs_raw(self):
5057 5116 return self._as_raw(self.task_kwargs)
5058 5117
5059 5118 def __repr__(self):
5060 5119 return '<DB:ScheduleEntry({}:{})>'.format(
5061 5120 self.schedule_entry_id, self.schedule_name)
5062 5121
5063 5122
5064 5123 @event.listens_for(ScheduleEntry, 'before_update')
5065 5124 def update_task_uid(mapper, connection, target):
5066 5125 target.task_uid = ScheduleEntry.get_uid(target)
5067 5126
5068 5127
5069 5128 @event.listens_for(ScheduleEntry, 'before_insert')
5070 5129 def set_task_uid(mapper, connection, target):
5071 5130 target.task_uid = ScheduleEntry.get_uid(target)
5072 5131
5073 5132
5074 5133 class _BaseBranchPerms(BaseModel):
5075 5134 @classmethod
5076 5135 def compute_hash(cls, value):
5077 5136 return sha1_safe(value)
5078 5137
5079 5138 @hybrid_property
5080 5139 def branch_pattern(self):
5081 5140 return self._branch_pattern or '*'
5082 5141
5083 5142 @hybrid_property
5084 5143 def branch_hash(self):
5085 5144 return self._branch_hash
5086 5145
5087 5146 def _validate_glob(self, value):
5088 5147 re.compile('^' + glob2re(value) + '$')
5089 5148
5090 5149 @branch_pattern.setter
5091 5150 def branch_pattern(self, value):
5092 5151 self._validate_glob(value)
5093 5152 self._branch_pattern = value or '*'
5094 5153 # set the Hash when setting the branch pattern
5095 5154 self._branch_hash = self.compute_hash(self._branch_pattern)
5096 5155
5097 5156 def matches(self, branch):
5098 5157 """
5099 5158 Check if this the branch matches entry
5100 5159
5101 5160 :param branch: branch name for the commit
5102 5161 """
5103 5162
5104 5163 branch = branch or ''
5105 5164
5106 5165 branch_matches = True
5107 5166 if branch:
5108 5167 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
5109 5168 branch_matches = bool(branch_regex.search(branch))
5110 5169
5111 5170 return branch_matches
5112 5171
5113 5172
5114 5173 class UserToRepoBranchPermission(Base, _BaseBranchPerms):
5115 5174 __tablename__ = 'user_to_repo_branch_permissions'
5116 5175 __table_args__ = (
5117 5176 base_table_args
5118 5177 )
5119 5178
5120 5179 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5121 5180
5122 5181 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5123 5182 repo = relationship('Repository', backref='user_branch_perms')
5124 5183
5125 5184 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5126 5185 permission = relationship('Permission')
5127 5186
5128 5187 rule_to_perm_id = Column('rule_to_perm_id', Integer(), ForeignKey('repo_to_perm.repo_to_perm_id'), nullable=False, unique=None, default=None)
5129 5188 user_repo_to_perm = relationship('UserRepoToPerm')
5130 5189
5131 5190 rule_order = Column('rule_order', Integer(), nullable=False)
5132 5191 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5133 5192 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5134 5193
5135 5194 def __unicode__(self):
5136 5195 return u'<UserBranchPermission(%s => %r)>' % (
5137 5196 self.user_repo_to_perm, self.branch_pattern)
5138 5197
5139 5198
5140 5199 class UserGroupToRepoBranchPermission(Base, _BaseBranchPerms):
5141 5200 __tablename__ = 'user_group_to_repo_branch_permissions'
5142 5201 __table_args__ = (
5143 5202 base_table_args
5144 5203 )
5145 5204
5146 5205 branch_rule_id = Column('branch_rule_id', Integer(), primary_key=True)
5147 5206
5148 5207 repository_id = Column('repository_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
5149 5208 repo = relationship('Repository', backref='user_group_branch_perms')
5150 5209
5151 5210 permission_id = Column('permission_id', Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
5152 5211 permission = relationship('Permission')
5153 5212
5154 5213 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)
5155 5214 user_group_repo_to_perm = relationship('UserGroupRepoToPerm')
5156 5215
5157 5216 rule_order = Column('rule_order', Integer(), nullable=False)
5158 5217 _branch_pattern = Column('branch_pattern', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), default=u'*') # glob
5159 5218 _branch_hash = Column('branch_hash', UnicodeText().with_variant(UnicodeText(2048), 'mysql'))
5160 5219
5161 5220 def __unicode__(self):
5162 5221 return u'<UserBranchPermission(%s => %r)>' % (
5163 5222 self.user_group_repo_to_perm, self.branch_pattern)
5164 5223
5165 5224
5166 5225 class UserBookmark(Base, BaseModel):
5167 5226 __tablename__ = 'user_bookmarks'
5168 5227 __table_args__ = (
5169 5228 UniqueConstraint('user_id', 'bookmark_repo_id'),
5170 5229 UniqueConstraint('user_id', 'bookmark_repo_group_id'),
5171 5230 UniqueConstraint('user_id', 'bookmark_position'),
5172 5231 base_table_args
5173 5232 )
5174 5233
5175 5234 user_bookmark_id = Column("user_bookmark_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
5176 5235 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
5177 5236 position = Column("bookmark_position", Integer(), nullable=False)
5178 5237 title = Column("bookmark_title", String(255), nullable=True, unique=None, default=None)
5179 5238 redirect_url = Column("bookmark_redirect_url", String(10240), nullable=True, unique=None, default=None)
5180 5239 created_on = Column("created_on", DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5181 5240
5182 5241 bookmark_repo_id = Column("bookmark_repo_id", Integer(), ForeignKey("repositories.repo_id"), nullable=True, unique=None, default=None)
5183 5242 bookmark_repo_group_id = Column("bookmark_repo_group_id", Integer(), ForeignKey("groups.group_id"), nullable=True, unique=None, default=None)
5184 5243
5185 5244 user = relationship("User")
5186 5245
5187 5246 repository = relationship("Repository")
5188 5247 repository_group = relationship("RepoGroup")
5189 5248
5190 5249 @classmethod
5191 5250 def get_by_position_for_user(cls, position, user_id):
5192 5251 return cls.query() \
5193 5252 .filter(UserBookmark.user_id == user_id) \
5194 5253 .filter(UserBookmark.position == position).scalar()
5195 5254
5196 5255 @classmethod
5197 5256 def get_bookmarks_for_user(cls, user_id, cache=True):
5198 5257 bookmarks = cls.query() \
5199 5258 .filter(UserBookmark.user_id == user_id) \
5200 5259 .options(joinedload(UserBookmark.repository)) \
5201 5260 .options(joinedload(UserBookmark.repository_group)) \
5202 5261 .order_by(UserBookmark.position.asc())
5203 5262
5204 5263 if cache:
5205 5264 bookmarks = bookmarks.options(
5206 5265 FromCache("sql_cache_short", "get_user_{}_bookmarks".format(user_id))
5207 5266 )
5208 5267
5209 5268 return bookmarks.all()
5210 5269
5211 5270 def __unicode__(self):
5212 5271 return u'<UserBookmark(%s @ %r)>' % (self.position, self.redirect_url)
5213 5272
5214 5273
5215 5274 class FileStore(Base, BaseModel):
5216 5275 __tablename__ = 'file_store'
5217 5276 __table_args__ = (
5218 5277 base_table_args
5219 5278 )
5220 5279
5221 5280 file_store_id = Column('file_store_id', Integer(), primary_key=True)
5222 5281 file_uid = Column('file_uid', String(1024), nullable=False)
5223 5282 file_display_name = Column('file_display_name', UnicodeText().with_variant(UnicodeText(2048), 'mysql'), nullable=True)
5224 5283 file_description = Column('file_description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=True)
5225 5284 file_org_name = Column('file_org_name', UnicodeText().with_variant(UnicodeText(10240), 'mysql'), nullable=False)
5226 5285
5227 5286 # sha256 hash
5228 5287 file_hash = Column('file_hash', String(512), nullable=False)
5229 5288 file_size = Column('file_size', BigInteger(), nullable=False)
5230 5289
5231 5290 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
5232 5291 accessed_on = Column('accessed_on', DateTime(timezone=False), nullable=True)
5233 5292 accessed_count = Column('accessed_count', Integer(), default=0)
5234 5293
5235 5294 enabled = Column('enabled', Boolean(), nullable=False, default=True)
5236 5295
5237 5296 # if repo/repo_group reference is set, check for permissions
5238 5297 check_acl = Column('check_acl', Boolean(), nullable=False, default=True)
5239 5298
5240 5299 # hidden defines an attachment that should be hidden from showing in artifact listing
5241 5300 hidden = Column('hidden', Boolean(), nullable=False, default=False)
5242 5301
5243 5302 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
5244 5303 upload_user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.user_id')
5245 5304
5246 5305 file_metadata = relationship('FileStoreMetadata', lazy='joined')
5247 5306
5248 5307 # scope limited to user, which requester have access to
5249 5308 scope_user_id = Column(
5250 5309 'scope_user_id', Integer(), ForeignKey('users.user_id'),
5251 5310 nullable=True, unique=None, default=None)
5252 5311 user = relationship('User', lazy='joined', primaryjoin='User.user_id==FileStore.scope_user_id')
5253 5312
5254 5313 # scope limited to user group, which requester have access to
5255 5314 scope_user_group_id = Column(
5256 5315 'scope_user_group_id', Integer(), ForeignKey('users_groups.users_group_id'),
5257 5316 nullable=True, unique=None, default=None)
5258 5317 user_group = relationship('UserGroup', lazy='joined')
5259 5318
5260 5319 # scope limited to repo, which requester have access to
5261 5320 scope_repo_id = Column(
5262 5321 'scope_repo_id', Integer(), ForeignKey('repositories.repo_id'),
5263 5322 nullable=True, unique=None, default=None)
5264 5323 repo = relationship('Repository', lazy='joined')
5265 5324
5266 5325 # scope limited to repo group, which requester have access to
5267 5326 scope_repo_group_id = Column(
5268 5327 'scope_repo_group_id', Integer(), ForeignKey('groups.group_id'),
5269 5328 nullable=True, unique=None, default=None)
5270 5329 repo_group = relationship('RepoGroup', lazy='joined')
5271 5330
5272 5331 @classmethod
5273 5332 def get_by_store_uid(cls, file_store_uid):
5274 5333 return FileStore.query().filter(FileStore.file_uid == file_store_uid).scalar()
5275 5334
5276 5335 @classmethod
5277 5336 def create(cls, file_uid, filename, file_hash, file_size, file_display_name='',
5278 5337 file_description='', enabled=True, hidden=False, check_acl=True,
5279 5338 user_id=None, scope_user_id=None, scope_repo_id=None, scope_repo_group_id=None):
5280 5339
5281 5340 store_entry = FileStore()
5282 5341 store_entry.file_uid = file_uid
5283 5342 store_entry.file_display_name = file_display_name
5284 5343 store_entry.file_org_name = filename
5285 5344 store_entry.file_size = file_size
5286 5345 store_entry.file_hash = file_hash
5287 5346 store_entry.file_description = file_description
5288 5347
5289 5348 store_entry.check_acl = check_acl
5290 5349 store_entry.enabled = enabled
5291 5350 store_entry.hidden = hidden
5292 5351
5293 5352 store_entry.user_id = user_id
5294 5353 store_entry.scope_user_id = scope_user_id
5295 5354 store_entry.scope_repo_id = scope_repo_id
5296 5355 store_entry.scope_repo_group_id = scope_repo_group_id
5297 5356
5298 5357 return store_entry
5299 5358
5300 5359 @classmethod
5301 5360 def store_metadata(cls, file_store_id, args, commit=True):
5302 5361 file_store = FileStore.get(file_store_id)
5303 5362 if file_store is None:
5304 5363 return
5305 5364
5306 5365 for section, key, value, value_type in args:
5307 5366 has_key = FileStoreMetadata().query() \
5308 5367 .filter(FileStoreMetadata.file_store_id == file_store.file_store_id) \
5309 5368 .filter(FileStoreMetadata.file_store_meta_section == section) \
5310 5369 .filter(FileStoreMetadata.file_store_meta_key == key) \
5311 5370 .scalar()
5312 5371 if has_key:
5313 5372 msg = 'key `{}` already defined under section `{}` for this file.'\
5314 5373 .format(key, section)
5315 5374 raise ArtifactMetadataDuplicate(msg, err_section=section, err_key=key)
5316 5375
5317 5376 # NOTE(marcink): raises ArtifactMetadataBadValueType
5318 5377 FileStoreMetadata.valid_value_type(value_type)
5319 5378
5320 5379 meta_entry = FileStoreMetadata()
5321 5380 meta_entry.file_store = file_store
5322 5381 meta_entry.file_store_meta_section = section
5323 5382 meta_entry.file_store_meta_key = key
5324 5383 meta_entry.file_store_meta_value_type = value_type
5325 5384 meta_entry.file_store_meta_value = value
5326 5385
5327 5386 Session().add(meta_entry)
5328 5387
5329 5388 try:
5330 5389 if commit:
5331 5390 Session().commit()
5332 5391 except IntegrityError:
5333 5392 Session().rollback()
5334 5393 raise ArtifactMetadataDuplicate('Duplicate section/key found for this file.')
5335 5394
5336 5395 @classmethod
5337 5396 def bump_access_counter(cls, file_uid, commit=True):
5338 5397 FileStore().query()\
5339 5398 .filter(FileStore.file_uid == file_uid)\
5340 5399 .update({FileStore.accessed_count: (FileStore.accessed_count + 1),
5341 5400 FileStore.accessed_on: datetime.datetime.now()})
5342 5401 if commit:
5343 5402 Session().commit()
5344 5403
5345 5404 def __json__(self):
5346 5405 data = {
5347 5406 'filename': self.file_display_name,
5348 5407 'filename_org': self.file_org_name,
5349 5408 'file_uid': self.file_uid,
5350 5409 'description': self.file_description,
5351 5410 'hidden': self.hidden,
5352 5411 'size': self.file_size,
5353 5412 'created_on': self.created_on,
5354 5413 'uploaded_by': self.upload_user.get_api_data(details='basic'),
5355 5414 'downloaded_times': self.accessed_count,
5356 5415 'sha256': self.file_hash,
5357 5416 'metadata': self.file_metadata,
5358 5417 }
5359 5418
5360 5419 return data
5361 5420
5362 5421 def __repr__(self):
5363 5422 return '<FileStore({})>'.format(self.file_store_id)
5364 5423
5365 5424
5366 5425 class FileStoreMetadata(Base, BaseModel):
5367 5426 __tablename__ = 'file_store_metadata'
5368 5427 __table_args__ = (
5369 5428 UniqueConstraint('file_store_id', 'file_store_meta_section_hash', 'file_store_meta_key_hash'),
5370 5429 Index('file_store_meta_section_idx', 'file_store_meta_section', mysql_length=255),
5371 5430 Index('file_store_meta_key_idx', 'file_store_meta_key', mysql_length=255),
5372 5431 base_table_args
5373 5432 )
5374 5433 SETTINGS_TYPES = {
5375 5434 'str': safe_str,
5376 5435 'int': safe_int,
5377 5436 'unicode': safe_unicode,
5378 5437 'bool': str2bool,
5379 5438 'list': functools.partial(aslist, sep=',')
5380 5439 }
5381 5440
5382 5441 file_store_meta_id = Column(
5383 5442 "file_store_meta_id", Integer(), nullable=False, unique=True, default=None,
5384 5443 primary_key=True)
5385 5444 _file_store_meta_section = Column(
5386 5445 "file_store_meta_section", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5387 5446 nullable=True, unique=None, default=None)
5388 5447 _file_store_meta_section_hash = Column(
5389 5448 "file_store_meta_section_hash", String(255),
5390 5449 nullable=True, unique=None, default=None)
5391 5450 _file_store_meta_key = Column(
5392 5451 "file_store_meta_key", UnicodeText().with_variant(UnicodeText(1024), 'mysql'),
5393 5452 nullable=True, unique=None, default=None)
5394 5453 _file_store_meta_key_hash = Column(
5395 5454 "file_store_meta_key_hash", String(255), nullable=True, unique=None, default=None)
5396 5455 _file_store_meta_value = Column(
5397 5456 "file_store_meta_value", UnicodeText().with_variant(UnicodeText(20480), 'mysql'),
5398 5457 nullable=True, unique=None, default=None)
5399 5458 _file_store_meta_value_type = Column(
5400 5459 "file_store_meta_value_type", String(255), nullable=True, unique=None,
5401 5460 default='unicode')
5402 5461
5403 5462 file_store_id = Column(
5404 5463 'file_store_id', Integer(), ForeignKey('file_store.file_store_id'),
5405 5464 nullable=True, unique=None, default=None)
5406 5465
5407 5466 file_store = relationship('FileStore', lazy='joined')
5408 5467
5409 5468 @classmethod
5410 5469 def valid_value_type(cls, value):
5411 5470 if value.split('.')[0] not in cls.SETTINGS_TYPES:
5412 5471 raise ArtifactMetadataBadValueType(
5413 5472 'value_type must be one of %s got %s' % (cls.SETTINGS_TYPES.keys(), value))
5414 5473
5415 5474 @hybrid_property
5416 5475 def file_store_meta_section(self):
5417 5476 return self._file_store_meta_section
5418 5477
5419 5478 @file_store_meta_section.setter
5420 5479 def file_store_meta_section(self, value):
5421 5480 self._file_store_meta_section = value
5422 5481 self._file_store_meta_section_hash = _hash_key(value)
5423 5482
5424 5483 @hybrid_property
5425 5484 def file_store_meta_key(self):
5426 5485 return self._file_store_meta_key
5427 5486
5428 5487 @file_store_meta_key.setter
5429 5488 def file_store_meta_key(self, value):
5430 5489 self._file_store_meta_key = value
5431 5490 self._file_store_meta_key_hash = _hash_key(value)
5432 5491
5433 5492 @hybrid_property
5434 5493 def file_store_meta_value(self):
5435 5494 val = self._file_store_meta_value
5436 5495
5437 5496 if self._file_store_meta_value_type:
5438 5497 # e.g unicode.encrypted == unicode
5439 5498 _type = self._file_store_meta_value_type.split('.')[0]
5440 5499 # decode the encrypted value if it's encrypted field type
5441 5500 if '.encrypted' in self._file_store_meta_value_type:
5442 5501 cipher = EncryptedTextValue()
5443 5502 val = safe_unicode(cipher.process_result_value(val, None))
5444 5503 # do final type conversion
5445 5504 converter = self.SETTINGS_TYPES.get(_type) or self.SETTINGS_TYPES['unicode']
5446 5505 val = converter(val)
5447 5506
5448 5507 return val
5449 5508
5450 5509 @file_store_meta_value.setter
5451 5510 def file_store_meta_value(self, val):
5452 5511 val = safe_unicode(val)
5453 5512 # encode the encrypted value
5454 5513 if '.encrypted' in self.file_store_meta_value_type:
5455 5514 cipher = EncryptedTextValue()
5456 5515 val = safe_unicode(cipher.process_bind_param(val, None))
5457 5516 self._file_store_meta_value = val
5458 5517
5459 5518 @hybrid_property
5460 5519 def file_store_meta_value_type(self):
5461 5520 return self._file_store_meta_value_type
5462 5521
5463 5522 @file_store_meta_value_type.setter
5464 5523 def file_store_meta_value_type(self, val):
5465 5524 # e.g unicode.encrypted
5466 5525 self.valid_value_type(val)
5467 5526 self._file_store_meta_value_type = val
5468 5527
5469 5528 def __json__(self):
5470 5529 data = {
5471 5530 'artifact': self.file_store.file_uid,
5472 5531 'section': self.file_store_meta_section,
5473 5532 'key': self.file_store_meta_key,
5474 5533 'value': self.file_store_meta_value,
5475 5534 }
5476 5535
5477 5536 return data
5478 5537
5479 5538 def __repr__(self):
5480 5539 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.file_store_meta_section,
5481 5540 self.file_store_meta_key, self.file_store_meta_value)
5482 5541
5483 5542
5484 5543 class DbMigrateVersion(Base, BaseModel):
5485 5544 __tablename__ = 'db_migrate_version'
5486 5545 __table_args__ = (
5487 5546 base_table_args,
5488 5547 )
5489 5548
5490 5549 repository_id = Column('repository_id', String(250), primary_key=True)
5491 5550 repository_path = Column('repository_path', Text)
5492 5551 version = Column('version', Integer)
5493 5552
5494 5553 @classmethod
5495 5554 def set_version(cls, version):
5496 5555 """
5497 5556 Helper for forcing a different version, usually for debugging purposes via ishell.
5498 5557 """
5499 5558 ver = DbMigrateVersion.query().first()
5500 5559 ver.version = version
5501 5560 Session().commit()
5502 5561
5503 5562
5504 5563 class DbSession(Base, BaseModel):
5505 5564 __tablename__ = 'db_session'
5506 5565 __table_args__ = (
5507 5566 base_table_args,
5508 5567 )
5509 5568
5510 5569 def __repr__(self):
5511 5570 return '<DB:DbSession({})>'.format(self.id)
5512 5571
5513 5572 id = Column('id', Integer())
5514 5573 namespace = Column('namespace', String(255), primary_key=True)
5515 5574 accessed = Column('accessed', DateTime, nullable=False)
5516 5575 created = Column('created', DateTime, nullable=False)
5517 5576 data = Column('data', PickleType, nullable=False)
@@ -1,3048 +1,3054 b''
1 1 //Primary CSS
2 2
3 3 //--- IMPORTS ------------------//
4 4
5 5 @import 'helpers';
6 6 @import 'mixins';
7 7 @import 'rcicons';
8 8 @import 'variables';
9 9 @import 'bootstrap-variables';
10 10 @import 'form-bootstrap';
11 11 @import 'codemirror';
12 12 @import 'legacy_code_styles';
13 13 @import 'readme-box';
14 14 @import 'progress-bar';
15 15
16 16 @import 'type';
17 17 @import 'alerts';
18 18 @import 'buttons';
19 19 @import 'tags';
20 20 @import 'code-block';
21 21 @import 'examples';
22 22 @import 'login';
23 23 @import 'main-content';
24 24 @import 'select2';
25 25 @import 'comments';
26 26 @import 'panels-bootstrap';
27 27 @import 'panels';
28 28 @import 'deform';
29 29 @import 'tooltips';
30 30
31 31 //--- BASE ------------------//
32 32 .noscript-error {
33 33 top: 0;
34 34 left: 0;
35 35 width: 100%;
36 36 z-index: 101;
37 37 text-align: center;
38 38 font-size: 120%;
39 39 color: white;
40 40 background-color: @alert2;
41 41 padding: 5px 0 5px 0;
42 42 font-weight: @text-semibold-weight;
43 43 font-family: @text-semibold;
44 44 }
45 45
46 46 html {
47 47 display: table;
48 48 height: 100%;
49 49 width: 100%;
50 50 }
51 51
52 52 body {
53 53 display: table-cell;
54 54 width: 100%;
55 55 }
56 56
57 57 //--- LAYOUT ------------------//
58 58
59 59 .hidden{
60 60 display: none !important;
61 61 }
62 62
63 63 .box{
64 64 float: left;
65 65 width: 100%;
66 66 }
67 67
68 68 .browser-header {
69 69 clear: both;
70 70 }
71 71 .main {
72 72 clear: both;
73 73 padding:0 0 @pagepadding;
74 74 height: auto;
75 75
76 76 &:after { //clearfix
77 77 content:"";
78 78 clear:both;
79 79 width:100%;
80 80 display:block;
81 81 }
82 82 }
83 83
84 84 .action-link{
85 85 margin-left: @padding;
86 86 padding-left: @padding;
87 87 border-left: @border-thickness solid @border-default-color;
88 88 }
89 89
90 90 .cursor-pointer {
91 91 cursor: pointer;
92 92 }
93 93
94 94 input + .action-link, .action-link.first{
95 95 border-left: none;
96 96 }
97 97
98 98 .action-link.last{
99 99 margin-right: @padding;
100 100 padding-right: @padding;
101 101 }
102 102
103 103 .action-link.active,
104 104 .action-link.active a{
105 105 color: @grey4;
106 106 }
107 107
108 108 .action-link.disabled {
109 109 color: @grey4;
110 110 cursor: inherit;
111 111 }
112 112
113 113
114 114 .clipboard-action {
115 115 cursor: pointer;
116 116 margin-left: 5px;
117 117
118 118 &:not(.no-grey) {
119 119
120 120 &:hover {
121 121 color: @grey2;
122 122 }
123 123 color: @grey4;
124 124 }
125 125 }
126 126
127 127 ul.simple-list{
128 128 list-style: none;
129 129 margin: 0;
130 130 padding: 0;
131 131 }
132 132
133 133 .main-content {
134 134 padding-bottom: @pagepadding;
135 135 }
136 136
137 137 .wide-mode-wrapper {
138 138 max-width:4000px !important;
139 139 }
140 140
141 141 .wrapper {
142 142 position: relative;
143 143 max-width: @wrapper-maxwidth;
144 144 margin: 0 auto;
145 145 }
146 146
147 147 #content {
148 148 clear: both;
149 149 padding: 0 @contentpadding;
150 150 }
151 151
152 152 .advanced-settings-fields{
153 153 input{
154 154 margin-left: @textmargin;
155 155 margin-right: @padding/2;
156 156 }
157 157 }
158 158
159 159 .cs_files_title {
160 160 margin: @pagepadding 0 0;
161 161 }
162 162
163 163 input.inline[type="file"] {
164 164 display: inline;
165 165 }
166 166
167 167 .error_page {
168 168 margin: 10% auto;
169 169
170 170 h1 {
171 171 color: @grey2;
172 172 }
173 173
174 174 .alert {
175 175 margin: @padding 0;
176 176 }
177 177
178 178 .error-branding {
179 179 color: @grey4;
180 180 font-weight: @text-semibold-weight;
181 181 font-family: @text-semibold;
182 182 }
183 183
184 184 .error_message {
185 185 font-family: @text-regular;
186 186 }
187 187
188 188 .sidebar {
189 189 min-height: 275px;
190 190 margin: 0;
191 191 padding: 0 0 @sidebarpadding @sidebarpadding;
192 192 border: none;
193 193 }
194 194
195 195 .main-content {
196 196 position: relative;
197 197 margin: 0 @sidebarpadding @sidebarpadding;
198 198 padding: 0 0 0 @sidebarpadding;
199 199 border-left: @border-thickness solid @grey5;
200 200
201 201 @media (max-width:767px) {
202 202 clear: both;
203 203 width: 100%;
204 204 margin: 0;
205 205 border: none;
206 206 }
207 207 }
208 208
209 209 .inner-column {
210 210 float: left;
211 211 width: 29.75%;
212 212 min-height: 150px;
213 213 margin: @sidebarpadding 2% 0 0;
214 214 padding: 0 2% 0 0;
215 215 border-right: @border-thickness solid @grey5;
216 216
217 217 @media (max-width:767px) {
218 218 clear: both;
219 219 width: 100%;
220 220 border: none;
221 221 }
222 222
223 223 ul {
224 224 padding-left: 1.25em;
225 225 }
226 226
227 227 &:last-child {
228 228 margin: @sidebarpadding 0 0;
229 229 border: none;
230 230 }
231 231
232 232 h4 {
233 233 margin: 0 0 @padding;
234 234 font-weight: @text-semibold-weight;
235 235 font-family: @text-semibold;
236 236 }
237 237 }
238 238 }
239 239 .error-page-logo {
240 240 width: 130px;
241 241 height: 160px;
242 242 }
243 243
244 244 // HEADER
245 245 .header {
246 246
247 247 // TODO: johbo: Fix login pages, so that they work without a min-height
248 248 // for the header and then remove the min-height. I chose a smaller value
249 249 // intentionally here to avoid rendering issues in the main navigation.
250 250 min-height: 49px;
251 251 min-width: 1024px;
252 252
253 253 position: relative;
254 254 vertical-align: bottom;
255 255 padding: 0 @header-padding;
256 256 background-color: @grey1;
257 257 color: @grey5;
258 258
259 259 .title {
260 260 overflow: visible;
261 261 }
262 262
263 263 &:before,
264 264 &:after {
265 265 content: "";
266 266 clear: both;
267 267 width: 100%;
268 268 }
269 269
270 270 // TODO: johbo: Avoids breaking "Repositories" chooser
271 271 .select2-container .select2-choice .select2-arrow {
272 272 display: none;
273 273 }
274 274 }
275 275
276 276 #header-inner {
277 277 &.title {
278 278 margin: 0;
279 279 }
280 280 &:before,
281 281 &:after {
282 282 content: "";
283 283 clear: both;
284 284 }
285 285 }
286 286
287 287 // Gists
288 288 #files_data {
289 289 clear: both; //for firefox
290 290 padding-top: 10px;
291 291 }
292 292
293 293 #gistid {
294 294 margin-right: @padding;
295 295 }
296 296
297 297 // Global Settings Editor
298 298 .textarea.editor {
299 299 float: left;
300 300 position: relative;
301 301 max-width: @texteditor-width;
302 302
303 303 select {
304 304 position: absolute;
305 305 top:10px;
306 306 right:0;
307 307 }
308 308
309 309 .CodeMirror {
310 310 margin: 0;
311 311 }
312 312
313 313 .help-block {
314 314 margin: 0 0 @padding;
315 315 padding:.5em;
316 316 background-color: @grey6;
317 317 &.pre-formatting {
318 318 white-space: pre;
319 319 }
320 320 }
321 321 }
322 322
323 323 ul.auth_plugins {
324 324 margin: @padding 0 @padding @legend-width;
325 325 padding: 0;
326 326
327 327 li {
328 328 margin-bottom: @padding;
329 329 line-height: 1em;
330 330 list-style-type: none;
331 331
332 332 .auth_buttons .btn {
333 333 margin-right: @padding;
334 334 }
335 335
336 336 }
337 337 }
338 338
339 339
340 340 // My Account PR list
341 341
342 342 #show_closed {
343 343 margin: 0 1em 0 0;
344 344 }
345 345
346 346 #pull_request_list_table {
347 347 .closed {
348 348 background-color: @grey6;
349 349 }
350 350
351 351 .state-creating,
352 352 .state-updating,
353 353 .state-merging
354 354 {
355 355 background-color: @grey6;
356 356 }
357 357
358 358 .td-status {
359 359 padding-left: .5em;
360 360 }
361 361 .log-container .truncate {
362 362 height: 2.75em;
363 363 white-space: pre-line;
364 364 }
365 365 table.rctable .user {
366 366 padding-left: 0;
367 367 }
368 368 table.rctable {
369 369 td.td-description,
370 370 .rc-user {
371 371 min-width: auto;
372 372 }
373 373 }
374 374 }
375 375
376 376 // Pull Requests
377 377
378 378 .pullrequests_section_head {
379 379 display: block;
380 380 clear: both;
381 381 margin: @padding 0;
382 382 font-weight: @text-bold-weight;
383 383 font-family: @text-bold;
384 384 }
385 385
386 386 .pr-commit-flow {
387 387 position: relative;
388 388 font-weight: 600;
389 389
390 390 .tag {
391 391 display: inline-block;
392 392 margin: 0 1em .5em 0;
393 393 }
394 394
395 395 .clone-url {
396 396 display: inline-block;
397 397 margin: 0 0 .5em 0;
398 398 padding: 0;
399 399 line-height: 1.2em;
400 400 }
401 401 }
402 402
403 403 .pr-mergeinfo {
404 404 min-width: 95% !important;
405 405 padding: 0 !important;
406 406 border: 0;
407 407 }
408 408 .pr-mergeinfo-copy {
409 409 padding: 0 0;
410 410 }
411 411
412 412 .pr-pullinfo {
413 413 min-width: 95% !important;
414 414 padding: 0 !important;
415 415 border: 0;
416 416 }
417 417 .pr-pullinfo-copy {
418 418 padding: 0 0;
419 419 }
420 420
421 421 .pr-title-input {
422 422 width: 100%;
423 423 font-size: 18px;
424 424 margin: 0 0 4px 0;
425 425 padding: 0;
426 426 line-height: 1.7em;
427 427 color: @text-color;
428 428 letter-spacing: .02em;
429 429 font-weight: @text-bold-weight;
430 430 font-family: @text-bold;
431 431
432 432 &:hover {
433 433 box-shadow: none;
434 434 }
435 435 }
436 436
437 437 #pr-title {
438 438 input {
439 439 border: 1px transparent;
440 440 color: black;
441 441 opacity: 1;
442 442 background: #fff;
443 443 font-size: 18px;
444 444 }
445 445 }
446 446
447 447 .pr-title-closed-tag {
448 448 font-size: 16px;
449 449 }
450 450
451 451 #pr-desc {
452 452 padding: 10px 0;
453 453
454 454 .markdown-block {
455 455 padding: 0;
456 456 margin-bottom: -30px;
457 457 }
458 458 }
459 459
460 460 #pullrequest_title {
461 461 width: 100%;
462 462 box-sizing: border-box;
463 463 }
464 464
465 465 #pr_open_message {
466 466 border: @border-thickness solid #fff;
467 467 border-radius: @border-radius;
468 468 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
469 469 text-align: left;
470 470 overflow: hidden;
471 471 }
472 472
473 473 .pr-details-title {
474 474 height: 16px
475 475 }
476 476
477 477 .pr-details-title-author-pref {
478 478 padding-right: 10px
479 479 }
480 480
481 481 .label-pr-detail {
482 482 display: table-cell;
483 483 width: 120px;
484 484 padding-top: 7.5px;
485 485 padding-bottom: 7.5px;
486 486 padding-right: 7.5px;
487 487 }
488 488
489 489 .source-details ul {
490 490 padding: 10px 16px;
491 491 }
492 492
493 493 .source-details-action {
494 494 color: @grey4;
495 495 font-size: 11px
496 496 }
497 497
498 498 .pr-submit-button {
499 499 float: right;
500 500 margin: 0 0 0 5px;
501 501 }
502 502
503 503 .pr-spacing-container {
504 504 padding: 20px;
505 505 clear: both
506 506 }
507 507
508 508 #pr-description-input {
509 509 margin-bottom: 0;
510 510 }
511 511
512 512 .pr-description-label {
513 513 vertical-align: top;
514 514 }
515 515
516 516 #open_edit_pullrequest {
517 517 padding: 0;
518 518 }
519 519
520 520 #close_edit_pullrequest {
521 521
522 522 }
523 523
524 524 #delete_pullrequest {
525 525 clear: inherit;
526 526
527 527 form {
528 528 display: inline;
529 529 }
530 530
531 531 }
532 532
533 533 .perms_section_head {
534 534 min-width: 625px;
535 535
536 536 h2 {
537 537 margin-bottom: 0;
538 538 }
539 539
540 540 .label-checkbox {
541 541 float: left;
542 542 }
543 543
544 544 &.field {
545 545 margin: @space 0 @padding;
546 546 }
547 547
548 548 &:first-child.field {
549 549 margin-top: 0;
550 550
551 551 .label {
552 552 margin-top: 0;
553 553 padding-top: 0;
554 554 }
555 555
556 556 .radios {
557 557 padding-top: 0;
558 558 }
559 559 }
560 560
561 561 .radios {
562 562 position: relative;
563 563 width: 505px;
564 564 }
565 565 }
566 566
567 567 //--- MODULES ------------------//
568 568
569 569
570 570 // Server Announcement
571 571 #server-announcement {
572 572 width: 95%;
573 573 margin: @padding auto;
574 574 padding: @padding;
575 575 border-width: 2px;
576 576 border-style: solid;
577 577 .border-radius(2px);
578 578 font-weight: @text-bold-weight;
579 579 font-family: @text-bold;
580 580
581 581 &.info { border-color: @alert4; background-color: @alert4-inner; }
582 582 &.warning { border-color: @alert3; background-color: @alert3-inner; }
583 583 &.error { border-color: @alert2; background-color: @alert2-inner; }
584 584 &.success { border-color: @alert1; background-color: @alert1-inner; }
585 585 &.neutral { border-color: @grey3; background-color: @grey6; }
586 586 }
587 587
588 588 // Fixed Sidebar Column
589 589 .sidebar-col-wrapper {
590 590 padding-left: @sidebar-all-width;
591 591
592 592 .sidebar {
593 593 width: @sidebar-width;
594 594 margin-left: -@sidebar-all-width;
595 595 }
596 596 }
597 597
598 598 .sidebar-col-wrapper.scw-small {
599 599 padding-left: @sidebar-small-all-width;
600 600
601 601 .sidebar {
602 602 width: @sidebar-small-width;
603 603 margin-left: -@sidebar-small-all-width;
604 604 }
605 605 }
606 606
607 607
608 608 // FOOTER
609 609 #footer {
610 610 padding: 0;
611 611 text-align: center;
612 612 vertical-align: middle;
613 613 color: @grey2;
614 614 font-size: 11px;
615 615
616 616 p {
617 617 margin: 0;
618 618 padding: 1em;
619 619 line-height: 1em;
620 620 }
621 621
622 622 .server-instance { //server instance
623 623 display: none;
624 624 }
625 625
626 626 .title {
627 627 float: none;
628 628 margin: 0 auto;
629 629 }
630 630 }
631 631
632 632 button.close {
633 633 padding: 0;
634 634 cursor: pointer;
635 635 background: transparent;
636 636 border: 0;
637 637 .box-shadow(none);
638 638 -webkit-appearance: none;
639 639 }
640 640
641 641 .close {
642 642 float: right;
643 643 font-size: 21px;
644 644 font-family: @text-bootstrap;
645 645 line-height: 1em;
646 646 font-weight: bold;
647 647 color: @grey2;
648 648
649 649 &:hover,
650 650 &:focus {
651 651 color: @grey1;
652 652 text-decoration: none;
653 653 cursor: pointer;
654 654 }
655 655 }
656 656
657 657 // GRID
658 658 .sorting,
659 659 .sorting_desc,
660 660 .sorting_asc {
661 661 cursor: pointer;
662 662 }
663 663 .sorting_desc:after {
664 664 content: "\00A0\25B2";
665 665 font-size: .75em;
666 666 }
667 667 .sorting_asc:after {
668 668 content: "\00A0\25BC";
669 669 font-size: .68em;
670 670 }
671 671
672 672
673 673 .user_auth_tokens {
674 674
675 675 &.truncate {
676 676 white-space: nowrap;
677 677 overflow: hidden;
678 678 text-overflow: ellipsis;
679 679 }
680 680
681 681 .fields .field .input {
682 682 margin: 0;
683 683 }
684 684
685 685 input#description {
686 686 width: 100px;
687 687 margin: 0;
688 688 }
689 689
690 690 .drop-menu {
691 691 // TODO: johbo: Remove this, should work out of the box when
692 692 // having multiple inputs inline
693 693 margin: 0 0 0 5px;
694 694 }
695 695 }
696 696 #user_list_table {
697 697 .closed {
698 698 background-color: @grey6;
699 699 }
700 700 }
701 701
702 702
703 703 input, textarea {
704 704 &.disabled {
705 705 opacity: .5;
706 706 }
707 707
708 708 &:hover {
709 709 border-color: @grey3;
710 710 box-shadow: @button-shadow;
711 711 }
712 712
713 713 &:focus {
714 714 border-color: @rcblue;
715 715 box-shadow: @button-shadow;
716 716 }
717 717 }
718 718
719 719 // remove extra padding in firefox
720 720 input::-moz-focus-inner { border:0; padding:0 }
721 721
722 722 .adjacent input {
723 723 margin-bottom: @padding;
724 724 }
725 725
726 726 .permissions_boxes {
727 727 display: block;
728 728 }
729 729
730 730 //FORMS
731 731
732 732 .medium-inline,
733 733 input#description.medium-inline {
734 734 display: inline;
735 735 width: @medium-inline-input-width;
736 736 min-width: 100px;
737 737 }
738 738
739 739 select {
740 740 //reset
741 741 -webkit-appearance: none;
742 742 -moz-appearance: none;
743 743
744 744 display: inline-block;
745 745 height: 28px;
746 746 width: auto;
747 747 margin: 0 @padding @padding 0;
748 748 padding: 0 18px 0 8px;
749 749 line-height:1em;
750 750 font-size: @basefontsize;
751 751 border: @border-thickness solid @grey5;
752 752 border-radius: @border-radius;
753 753 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
754 754 color: @grey4;
755 755 box-shadow: @button-shadow;
756 756
757 757 &:after {
758 758 content: "\00A0\25BE";
759 759 }
760 760
761 761 &:focus, &:hover {
762 762 outline: none;
763 763 border-color: @grey4;
764 764 color: @rcdarkblue;
765 765 }
766 766 }
767 767
768 768 option {
769 769 &:focus {
770 770 outline: none;
771 771 }
772 772 }
773 773
774 774 input,
775 775 textarea {
776 776 padding: @input-padding;
777 777 border: @input-border-thickness solid @border-highlight-color;
778 778 .border-radius (@border-radius);
779 779 font-family: @text-light;
780 780 font-size: @basefontsize;
781 781
782 782 &.input-sm {
783 783 padding: 5px;
784 784 }
785 785
786 786 &#description {
787 787 min-width: @input-description-minwidth;
788 788 min-height: 1em;
789 789 padding: 10px;
790 790 }
791 791 }
792 792
793 793 .field-sm {
794 794 input,
795 795 textarea {
796 796 padding: 5px;
797 797 }
798 798 }
799 799
800 800 textarea {
801 801 display: block;
802 802 clear: both;
803 803 width: 100%;
804 804 min-height: 100px;
805 805 margin-bottom: @padding;
806 806 .box-sizing(border-box);
807 807 overflow: auto;
808 808 }
809 809
810 810 label {
811 811 font-family: @text-light;
812 812 }
813 813
814 814 // GRAVATARS
815 815 // centers gravatar on username to the right
816 816
817 817 .gravatar {
818 818 display: inline;
819 819 min-width: 16px;
820 820 min-height: 16px;
821 821 margin: -5px 0;
822 822 padding: 0;
823 823 line-height: 1em;
824 824 box-sizing: content-box;
825 825 border-radius: 50%;
826 826
827 827 &.gravatar-large {
828 828 margin: -0.5em .25em -0.5em 0;
829 829 }
830 830
831 831 & + .user {
832 832 display: inline;
833 833 margin: 0;
834 834 padding: 0 0 0 .17em;
835 835 line-height: 1em;
836 836 }
837 837
838 838 & + .no-margin {
839 839 margin: 0
840 840 }
841 841
842 842 }
843 843
844 844 .user-inline-data {
845 845 display: inline-block;
846 846 float: left;
847 847 padding-left: .5em;
848 848 line-height: 1.3em;
849 849 }
850 850
851 851 .rc-user { // gravatar + user wrapper
852 852 float: left;
853 853 position: relative;
854 854 min-width: 100px;
855 855 max-width: 200px;
856 856 min-height: (@gravatar-size + @border-thickness * 2); // account for border
857 857 display: block;
858 858 padding: 0 0 0 (@gravatar-size + @basefontsize/4);
859 859
860 860
861 861 .gravatar {
862 862 display: block;
863 863 position: absolute;
864 864 top: 0;
865 865 left: 0;
866 866 min-width: @gravatar-size;
867 867 min-height: @gravatar-size;
868 868 margin: 0;
869 869 }
870 870
871 871 .user {
872 872 display: block;
873 873 max-width: 175px;
874 874 padding-top: 2px;
875 875 overflow: hidden;
876 876 text-overflow: ellipsis;
877 877 }
878 878 }
879 879
880 880 .gist-gravatar,
881 881 .journal_container {
882 882 .gravatar-large {
883 883 margin: 0 .5em -10px 0;
884 884 }
885 885 }
886 886
887 887 .gist-type-fields {
888 888 line-height: 30px;
889 889 height: 30px;
890 890
891 891 .gist-type-fields-wrapper {
892 892 vertical-align: middle;
893 893 display: inline-block;
894 894 line-height: 25px;
895 895 }
896 896 }
897 897
898 898 // ADMIN SETTINGS
899 899
900 900 // Tag Patterns
901 901 .tag_patterns {
902 902 .tag_input {
903 903 margin-bottom: @padding;
904 904 }
905 905 }
906 906
907 907 .locked_input {
908 908 position: relative;
909 909
910 910 input {
911 911 display: inline;
912 912 margin: 3px 5px 0px 0px;
913 913 }
914 914
915 915 br {
916 916 display: none;
917 917 }
918 918
919 919 .error-message {
920 920 float: left;
921 921 width: 100%;
922 922 }
923 923
924 924 .lock_input_button {
925 925 display: inline;
926 926 }
927 927
928 928 .help-block {
929 929 clear: both;
930 930 }
931 931 }
932 932
933 933 // Notifications
934 934
935 935 .notifications_buttons {
936 936 margin: 0 0 @space 0;
937 937 padding: 0;
938 938
939 939 .btn {
940 940 display: inline-block;
941 941 }
942 942 }
943 943
944 944 .notification-list {
945 945
946 946 div {
947 947 vertical-align: middle;
948 948 }
949 949
950 950 .container {
951 951 display: block;
952 952 margin: 0 0 @padding 0;
953 953 }
954 954
955 955 .delete-notifications {
956 956 margin-left: @padding;
957 957 text-align: right;
958 958 cursor: pointer;
959 959 }
960 960
961 961 .read-notifications {
962 962 margin-left: @padding/2;
963 963 text-align: right;
964 964 width: 35px;
965 965 cursor: pointer;
966 966 }
967 967
968 968 .icon-minus-sign {
969 969 color: @alert2;
970 970 }
971 971
972 972 .icon-ok-sign {
973 973 color: @alert1;
974 974 }
975 975 }
976 976
977 977 .user_settings {
978 978 float: left;
979 979 clear: both;
980 980 display: block;
981 981 width: 100%;
982 982
983 983 .gravatar_box {
984 984 margin-bottom: @padding;
985 985
986 986 &:after {
987 987 content: " ";
988 988 clear: both;
989 989 width: 100%;
990 990 }
991 991 }
992 992
993 993 .fields .field {
994 994 clear: both;
995 995 }
996 996 }
997 997
998 998 .advanced_settings {
999 999 margin-bottom: @space;
1000 1000
1001 1001 .help-block {
1002 1002 margin-left: 0;
1003 1003 }
1004 1004
1005 1005 button + .help-block {
1006 1006 margin-top: @padding;
1007 1007 }
1008 1008 }
1009 1009
1010 1010 // admin settings radio buttons and labels
1011 1011 .label-2 {
1012 1012 float: left;
1013 1013 width: @label2-width;
1014 1014
1015 1015 label {
1016 1016 color: @grey1;
1017 1017 }
1018 1018 }
1019 1019 .checkboxes {
1020 1020 float: left;
1021 1021 width: @checkboxes-width;
1022 1022 margin-bottom: @padding;
1023 1023
1024 1024 .checkbox {
1025 1025 width: 100%;
1026 1026
1027 1027 label {
1028 1028 margin: 0;
1029 1029 padding: 0;
1030 1030 }
1031 1031 }
1032 1032
1033 1033 .checkbox + .checkbox {
1034 1034 display: inline-block;
1035 1035 }
1036 1036
1037 1037 label {
1038 1038 margin-right: 1em;
1039 1039 }
1040 1040 }
1041 1041
1042 1042 // CHANGELOG
1043 1043 .container_header {
1044 1044 float: left;
1045 1045 display: block;
1046 1046 width: 100%;
1047 1047 margin: @padding 0 @padding;
1048 1048
1049 1049 #filter_changelog {
1050 1050 float: left;
1051 1051 margin-right: @padding;
1052 1052 }
1053 1053
1054 1054 .breadcrumbs_light {
1055 1055 display: inline-block;
1056 1056 }
1057 1057 }
1058 1058
1059 1059 .info_box {
1060 1060 float: right;
1061 1061 }
1062 1062
1063 1063
1064 1064
1065 1065 #graph_content{
1066 1066
1067 1067 // adjust for table headers so that graph renders properly
1068 1068 // #graph_nodes padding - table cell padding
1069 1069 padding-top: (@space - (@basefontsize * 2.4));
1070 1070
1071 1071 &.graph_full_width {
1072 1072 width: 100%;
1073 1073 max-width: 100%;
1074 1074 }
1075 1075 }
1076 1076
1077 1077 #graph {
1078 1078
1079 1079 .pagination-left {
1080 1080 float: left;
1081 1081 clear: both;
1082 1082 }
1083 1083
1084 1084 .log-container {
1085 1085 max-width: 345px;
1086 1086
1087 1087 .message{
1088 1088 max-width: 340px;
1089 1089 }
1090 1090 }
1091 1091
1092 1092 .graph-col-wrapper {
1093 1093
1094 1094 #graph_nodes {
1095 1095 width: 100px;
1096 1096 position: absolute;
1097 1097 left: 70px;
1098 1098 z-index: -1;
1099 1099 }
1100 1100 }
1101 1101
1102 1102 .load-more-commits {
1103 1103 text-align: center;
1104 1104 }
1105 1105 .load-more-commits:hover {
1106 1106 background-color: @grey7;
1107 1107 }
1108 1108 .load-more-commits {
1109 1109 a {
1110 1110 display: block;
1111 1111 }
1112 1112 }
1113 1113 }
1114 1114
1115 1115 .obsolete-toggle {
1116 1116 line-height: 30px;
1117 1117 margin-left: -15px;
1118 1118 }
1119 1119
1120 1120 #rev_range_container, #rev_range_clear, #rev_range_more {
1121 1121 margin-top: -5px;
1122 1122 margin-bottom: -5px;
1123 1123 }
1124 1124
1125 1125 #filter_changelog {
1126 1126 float: left;
1127 1127 }
1128 1128
1129 1129
1130 1130 //--- THEME ------------------//
1131 1131
1132 1132 #logo {
1133 1133 float: left;
1134 1134 margin: 9px 0 0 0;
1135 1135
1136 1136 .header {
1137 1137 background-color: transparent;
1138 1138 }
1139 1139
1140 1140 a {
1141 1141 display: inline-block;
1142 1142 }
1143 1143
1144 1144 img {
1145 1145 height:30px;
1146 1146 }
1147 1147 }
1148 1148
1149 1149 .logo-wrapper {
1150 1150 float:left;
1151 1151 }
1152 1152
1153 1153 .branding {
1154 1154 float: left;
1155 1155 padding: 9px 2px;
1156 1156 line-height: 1em;
1157 1157 font-size: @navigation-fontsize;
1158 1158
1159 1159 a {
1160 1160 color: @grey5
1161 1161 }
1162 1162 @media screen and (max-width: 1200px) {
1163 1163 display: none;
1164 1164 }
1165 1165 }
1166 1166
1167 1167 img {
1168 1168 border: none;
1169 1169 outline: none;
1170 1170 }
1171 1171 user-profile-header
1172 1172 label {
1173 1173
1174 1174 input[type="checkbox"] {
1175 1175 margin-right: 1em;
1176 1176 }
1177 1177 input[type="radio"] {
1178 1178 margin-right: 1em;
1179 1179 }
1180 1180 }
1181 1181
1182 1182 .review-status {
1183 1183 &.under_review {
1184 1184 color: @alert3;
1185 1185 }
1186 1186 &.approved {
1187 1187 color: @alert1;
1188 1188 }
1189 1189 &.rejected,
1190 1190 &.forced_closed{
1191 1191 color: @alert2;
1192 1192 }
1193 1193 &.not_reviewed {
1194 1194 color: @grey5;
1195 1195 }
1196 1196 }
1197 1197
1198 1198 .review-status-under_review {
1199 1199 color: @alert3;
1200 1200 }
1201 1201 .status-tag-under_review {
1202 1202 border-color: @alert3;
1203 1203 }
1204 1204
1205 1205 .review-status-approved {
1206 1206 color: @alert1;
1207 1207 }
1208 1208 .status-tag-approved {
1209 1209 border-color: @alert1;
1210 1210 }
1211 1211
1212 1212 .review-status-rejected,
1213 1213 .review-status-forced_closed {
1214 1214 color: @alert2;
1215 1215 }
1216 1216 .status-tag-rejected,
1217 1217 .status-tag-forced_closed {
1218 1218 border-color: @alert2;
1219 1219 }
1220 1220
1221 1221 .review-status-not_reviewed {
1222 1222 color: @grey5;
1223 1223 }
1224 1224 .status-tag-not_reviewed {
1225 1225 border-color: @grey5;
1226 1226 }
1227 1227
1228 1228 .test_pattern_preview {
1229 1229 margin: @space 0;
1230 1230
1231 1231 p {
1232 1232 margin-bottom: 0;
1233 1233 border-bottom: @border-thickness solid @border-default-color;
1234 1234 color: @grey3;
1235 1235 }
1236 1236
1237 1237 .btn {
1238 1238 margin-bottom: @padding;
1239 1239 }
1240 1240 }
1241 1241 #test_pattern_result {
1242 1242 display: none;
1243 1243 &:extend(pre);
1244 1244 padding: .9em;
1245 1245 color: @grey3;
1246 1246 background-color: @grey7;
1247 1247 border-right: @border-thickness solid @border-default-color;
1248 1248 border-bottom: @border-thickness solid @border-default-color;
1249 1249 border-left: @border-thickness solid @border-default-color;
1250 1250 }
1251 1251
1252 1252 #repo_vcs_settings {
1253 1253 #inherit_overlay_vcs_default {
1254 1254 display: none;
1255 1255 }
1256 1256 #inherit_overlay_vcs_custom {
1257 1257 display: custom;
1258 1258 }
1259 1259 &.inherited {
1260 1260 #inherit_overlay_vcs_default {
1261 1261 display: block;
1262 1262 }
1263 1263 #inherit_overlay_vcs_custom {
1264 1264 display: none;
1265 1265 }
1266 1266 }
1267 1267 }
1268 1268
1269 1269 .issue-tracker-link {
1270 1270 color: @rcblue;
1271 1271 }
1272 1272
1273 1273 // Issue Tracker Table Show/Hide
1274 1274 #repo_issue_tracker {
1275 1275 #inherit_overlay {
1276 1276 display: none;
1277 1277 }
1278 1278 #custom_overlay {
1279 1279 display: custom;
1280 1280 }
1281 1281 &.inherited {
1282 1282 #inherit_overlay {
1283 1283 display: block;
1284 1284 }
1285 1285 #custom_overlay {
1286 1286 display: none;
1287 1287 }
1288 1288 }
1289 1289 }
1290 1290 table.issuetracker {
1291 1291 &.readonly {
1292 1292 tr, td {
1293 1293 color: @grey3;
1294 1294 }
1295 1295 }
1296 1296 .edit {
1297 1297 display: none;
1298 1298 }
1299 1299 .editopen {
1300 1300 .edit {
1301 1301 display: inline;
1302 1302 }
1303 1303 .entry {
1304 1304 display: none;
1305 1305 }
1306 1306 }
1307 1307 tr td.td-action {
1308 1308 min-width: 117px;
1309 1309 }
1310 1310 td input {
1311 1311 max-width: none;
1312 1312 min-width: 30px;
1313 1313 width: 80%;
1314 1314 }
1315 1315 .issuetracker_pref input {
1316 1316 width: 40%;
1317 1317 }
1318 1318 input.edit_issuetracker_update {
1319 1319 margin-right: 0;
1320 1320 width: auto;
1321 1321 }
1322 1322 }
1323 1323
1324 1324 table.integrations {
1325 1325 .td-icon {
1326 1326 width: 20px;
1327 1327 .integration-icon {
1328 1328 height: 20px;
1329 1329 width: 20px;
1330 1330 }
1331 1331 }
1332 1332 }
1333 1333
1334 1334 .integrations {
1335 1335 a.integration-box {
1336 1336 color: @text-color;
1337 1337 &:hover {
1338 1338 .panel {
1339 1339 background: #fbfbfb;
1340 1340 }
1341 1341 }
1342 1342 .integration-icon {
1343 1343 width: 30px;
1344 1344 height: 30px;
1345 1345 margin-right: 20px;
1346 1346 float: left;
1347 1347 }
1348 1348
1349 1349 .panel-body {
1350 1350 padding: 10px;
1351 1351 }
1352 1352 .panel {
1353 1353 margin-bottom: 10px;
1354 1354 }
1355 1355 h2 {
1356 1356 display: inline-block;
1357 1357 margin: 0;
1358 1358 min-width: 140px;
1359 1359 }
1360 1360 }
1361 1361 a.integration-box.dummy-integration {
1362 1362 color: @grey4
1363 1363 }
1364 1364 }
1365 1365
1366 1366 //Permissions Settings
1367 1367 #add_perm {
1368 1368 margin: 0 0 @padding;
1369 1369 cursor: pointer;
1370 1370 }
1371 1371
1372 1372 .perm_ac {
1373 1373 input {
1374 1374 width: 95%;
1375 1375 }
1376 1376 }
1377 1377
1378 1378 .autocomplete-suggestions {
1379 1379 width: auto !important; // overrides autocomplete.js
1380 1380 min-width: 278px;
1381 1381 margin: 0;
1382 1382 border: @border-thickness solid @grey5;
1383 1383 border-radius: @border-radius;
1384 1384 color: @grey2;
1385 1385 background-color: white;
1386 1386 }
1387 1387
1388 1388 .autocomplete-qfilter-suggestions {
1389 1389 width: auto !important; // overrides autocomplete.js
1390 1390 max-height: 100% !important;
1391 1391 min-width: 376px;
1392 1392 margin: 0;
1393 1393 border: @border-thickness solid @grey5;
1394 1394 color: @grey2;
1395 1395 background-color: white;
1396 1396 }
1397 1397
1398 1398 .autocomplete-selected {
1399 1399 background: #F0F0F0;
1400 1400 }
1401 1401
1402 1402 .ac-container-wrap {
1403 1403 margin: 0;
1404 1404 padding: 8px;
1405 1405 border-bottom: @border-thickness solid @grey5;
1406 1406 list-style-type: none;
1407 1407 cursor: pointer;
1408 1408
1409 1409 &:hover {
1410 1410 background-color: @grey7;
1411 1411 }
1412 1412
1413 1413 img {
1414 1414 height: @gravatar-size;
1415 1415 width: @gravatar-size;
1416 1416 margin-right: 1em;
1417 1417 }
1418 1418
1419 1419 strong {
1420 1420 font-weight: normal;
1421 1421 }
1422 1422 }
1423 1423
1424 1424 // Settings Dropdown
1425 1425 .user-menu .container {
1426 1426 padding: 0 4px;
1427 1427 margin: 0;
1428 1428 }
1429 1429
1430 1430 .user-menu .gravatar {
1431 1431 cursor: pointer;
1432 1432 }
1433 1433
1434 1434 .codeblock {
1435 1435 margin-bottom: @padding;
1436 1436 clear: both;
1437 1437
1438 1438 .stats {
1439 1439 overflow: hidden;
1440 1440 }
1441 1441
1442 1442 .message{
1443 1443 textarea{
1444 1444 margin: 0;
1445 1445 }
1446 1446 }
1447 1447
1448 1448 .code-header {
1449 1449 .stats {
1450 1450 line-height: 2em;
1451 1451
1452 1452 .revision_id {
1453 1453 margin-left: 0;
1454 1454 }
1455 1455 .buttons {
1456 1456 padding-right: 0;
1457 1457 }
1458 1458 }
1459 1459
1460 1460 .item{
1461 1461 margin-right: 0.5em;
1462 1462 }
1463 1463 }
1464 1464
1465 1465 #editor_container {
1466 1466 position: relative;
1467 1467 margin: @padding 10px;
1468 1468 }
1469 1469 }
1470 1470
1471 1471 #file_history_container {
1472 1472 display: none;
1473 1473 }
1474 1474
1475 1475 .file-history-inner {
1476 1476 margin-bottom: 10px;
1477 1477 }
1478 1478
1479 1479 // Pull Requests
1480 1480 .summary-details {
1481 1481 width: 72%;
1482 1482 }
1483 1483 .pr-summary {
1484 1484 border-bottom: @border-thickness solid @grey5;
1485 1485 margin-bottom: @space;
1486 1486 }
1487 1487
1488 1488 .reviewers-title {
1489 1489 width: 25%;
1490 1490 min-width: 200px;
1491 1491
1492 1492 &.first-panel {
1493 1493 margin-top: 34px;
1494 1494 }
1495 1495 }
1496 1496
1497 1497 .reviewers {
1498 1498 width: 25%;
1499 1499 min-width: 200px;
1500 1500 }
1501 1501 .reviewers ul li {
1502 1502 position: relative;
1503 1503 width: 100%;
1504 1504 padding-bottom: 8px;
1505 1505 list-style-type: none;
1506 1506 }
1507 1507
1508 1508 .reviewer_entry {
1509 1509 min-height: 55px;
1510 1510 }
1511 1511
1512 1512 .reviewers_member {
1513 1513 width: 100%;
1514 1514 overflow: auto;
1515 1515 }
1516 1516 .reviewer_reason {
1517 1517 padding-left: 20px;
1518 1518 line-height: 1.5em;
1519 1519 }
1520 1520 .reviewer_status {
1521 1521 display: inline-block;
1522 1522 width: 25px;
1523 1523 min-width: 25px;
1524 1524 height: 1.2em;
1525 1525 line-height: 1em;
1526 1526 }
1527 1527
1528 1528 .reviewer_name {
1529 1529 display: inline-block;
1530 1530 max-width: 83%;
1531 1531 padding-right: 20px;
1532 1532 vertical-align: middle;
1533 1533 line-height: 1;
1534 1534
1535 1535 .rc-user {
1536 1536 min-width: 0;
1537 1537 margin: -2px 1em 0 0;
1538 1538 }
1539 1539
1540 1540 .reviewer {
1541 1541 float: left;
1542 1542 }
1543 1543 }
1544 1544
1545 1545 .reviewer_member_mandatory {
1546 1546 position: absolute;
1547 1547 left: 15px;
1548 1548 top: 8px;
1549 1549 width: 16px;
1550 1550 font-size: 11px;
1551 1551 margin: 0;
1552 1552 padding: 0;
1553 1553 color: black;
1554 1554 }
1555 1555
1556 1556 .reviewer_member_mandatory_remove,
1557 1557 .reviewer_member_remove {
1558 1558 position: absolute;
1559 1559 right: 0;
1560 1560 top: 0;
1561 1561 width: 16px;
1562 1562 margin-bottom: 10px;
1563 1563 padding: 0;
1564 1564 color: black;
1565 1565 }
1566 1566
1567 1567 .reviewer_member_mandatory_remove {
1568 1568 color: @grey4;
1569 1569 }
1570 1570
1571 1571 .reviewer_member_status {
1572 1572 margin-top: 5px;
1573 1573 }
1574 1574 .pr-summary #summary{
1575 1575 width: 100%;
1576 1576 }
1577 1577 .pr-summary .action_button:hover {
1578 1578 border: 0;
1579 1579 cursor: pointer;
1580 1580 }
1581 1581 .pr-details-title {
1582 1582 padding-bottom: 8px;
1583 1583 border-bottom: @border-thickness solid @grey5;
1584 1584
1585 1585 .action_button.disabled {
1586 1586 color: @grey4;
1587 1587 cursor: inherit;
1588 1588 }
1589 1589 .action_button {
1590 1590 color: @rcblue;
1591 1591 }
1592 1592 }
1593 1593 .pr-details-content {
1594 1594 margin-top: @textmargin - 5;
1595 1595 margin-bottom: @textmargin - 5;
1596 1596 }
1597 1597
1598 1598 .pr-reviewer-rules {
1599 1599 padding: 10px 0px 20px 0px;
1600 1600 }
1601 1601
1602 1602 .todo-resolved {
1603 1603 text-decoration: line-through;
1604 1604 }
1605 1605
1606 1606 .todo-table {
1607 1607 width: 100%;
1608 1608
1609 1609 td {
1610 1610 padding: 5px 0px;
1611 1611 }
1612 1612
1613 1613 .td-todo-number {
1614 1614 text-align: left;
1615 1615 white-space: nowrap;
1616 1616 width: 15%;
1617 1617 }
1618 1618
1619 1619 .td-todo-gravatar {
1620 1620 width: 5%;
1621 1621
1622 1622 img {
1623 1623 margin: -3px 0;
1624 1624 }
1625 1625 }
1626 1626
1627 1627 }
1628 1628
1629 1629 .todo-comment-text-wrapper {
1630 1630 display: inline-grid;
1631 1631 }
1632 1632
1633 1633 .todo-comment-text {
1634 1634 margin-left: 5px;
1635 1635 white-space: nowrap;
1636 1636 overflow: hidden;
1637 1637 text-overflow: ellipsis;
1638 1638 }
1639 1639
1640 1640 .group_members {
1641 1641 margin-top: 0;
1642 1642 padding: 0;
1643 1643 list-style: outside none none;
1644 1644
1645 1645 img {
1646 1646 height: @gravatar-size;
1647 1647 width: @gravatar-size;
1648 1648 margin-right: .5em;
1649 1649 margin-left: 3px;
1650 1650 }
1651 1651
1652 1652 .to-delete {
1653 1653 .user {
1654 1654 text-decoration: line-through;
1655 1655 }
1656 1656 }
1657 1657 }
1658 1658
1659 1659 .compare_view_commits_title {
1660 1660 .disabled {
1661 1661 cursor: inherit;
1662 1662 &:hover{
1663 1663 background-color: inherit;
1664 1664 color: inherit;
1665 1665 }
1666 1666 }
1667 1667 }
1668 1668
1669 1669 .subtitle-compare {
1670 1670 margin: -15px 0px 0px 0px;
1671 1671 }
1672 1672
1673 1673 // new entry in group_members
1674 1674 .td-author-new-entry {
1675 1675 background-color: rgba(red(@alert1), green(@alert1), blue(@alert1), 0.3);
1676 1676 }
1677 1677
1678 1678 .usergroup_member_remove {
1679 1679 width: 16px;
1680 1680 margin-bottom: 10px;
1681 1681 padding: 0;
1682 1682 color: black !important;
1683 1683 cursor: pointer;
1684 1684 }
1685 1685
1686 1686 .reviewer_ac .ac-input {
1687 1687 width: 92%;
1688 1688 margin-bottom: 1em;
1689 1689 }
1690 1690
1691 1691 .compare_view_commits tr{
1692 1692 height: 20px;
1693 1693 }
1694 1694 .compare_view_commits td {
1695 1695 vertical-align: top;
1696 1696 padding-top: 10px;
1697 1697 }
1698 1698 .compare_view_commits .author {
1699 1699 margin-left: 5px;
1700 1700 }
1701 1701
1702 1702 .compare_view_commits {
1703 1703 .color-a {
1704 1704 color: @alert1;
1705 1705 }
1706 1706
1707 1707 .color-c {
1708 1708 color: @color3;
1709 1709 }
1710 1710
1711 1711 .color-r {
1712 1712 color: @color5;
1713 1713 }
1714 1714
1715 1715 .color-a-bg {
1716 1716 background-color: @alert1;
1717 1717 }
1718 1718
1719 1719 .color-c-bg {
1720 1720 background-color: @alert3;
1721 1721 }
1722 1722
1723 1723 .color-r-bg {
1724 1724 background-color: @alert2;
1725 1725 }
1726 1726
1727 1727 .color-a-border {
1728 1728 border: 1px solid @alert1;
1729 1729 }
1730 1730
1731 1731 .color-c-border {
1732 1732 border: 1px solid @alert3;
1733 1733 }
1734 1734
1735 1735 .color-r-border {
1736 1736 border: 1px solid @alert2;
1737 1737 }
1738 1738
1739 1739 .commit-change-indicator {
1740 1740 width: 15px;
1741 1741 height: 15px;
1742 1742 position: relative;
1743 1743 left: 15px;
1744 1744 }
1745 1745
1746 1746 .commit-change-content {
1747 1747 text-align: center;
1748 1748 vertical-align: middle;
1749 1749 line-height: 15px;
1750 1750 }
1751 1751 }
1752 1752
1753 1753 .compare_view_filepath {
1754 1754 color: @grey1;
1755 1755 }
1756 1756
1757 1757 .show_more {
1758 1758 display: inline-block;
1759 1759 width: 0;
1760 1760 height: 0;
1761 1761 vertical-align: middle;
1762 1762 content: "";
1763 1763 border: 4px solid;
1764 1764 border-right-color: transparent;
1765 1765 border-bottom-color: transparent;
1766 1766 border-left-color: transparent;
1767 1767 font-size: 0;
1768 1768 }
1769 1769
1770 1770 .journal_more .show_more {
1771 1771 display: inline;
1772 1772
1773 1773 &:after {
1774 1774 content: none;
1775 1775 }
1776 1776 }
1777 1777
1778 1778 .compare_view_commits .collapse_commit:after {
1779 1779 cursor: pointer;
1780 1780 content: "\00A0\25B4";
1781 1781 margin-left: -3px;
1782 1782 font-size: 17px;
1783 1783 color: @grey4;
1784 1784 }
1785 1785
1786 1786 .diff_links {
1787 1787 margin-left: 8px;
1788 1788 }
1789 1789
1790 1790 #pull_request_overview {
1791 1791 div.ancestor {
1792 1792 margin: -33px 0;
1793 1793 }
1794 1794 }
1795 1795
1796 1796 div.ancestor {
1797 1797 line-height: 33px;
1798 1798 }
1799 1799
1800 1800 .cs_icon_td input[type="checkbox"] {
1801 1801 display: none;
1802 1802 }
1803 1803
1804 1804 .cs_icon_td .expand_file_icon:after {
1805 1805 cursor: pointer;
1806 1806 content: "\00A0\25B6";
1807 1807 font-size: 12px;
1808 1808 color: @grey4;
1809 1809 }
1810 1810
1811 1811 .cs_icon_td .collapse_file_icon:after {
1812 1812 cursor: pointer;
1813 1813 content: "\00A0\25BC";
1814 1814 font-size: 12px;
1815 1815 color: @grey4;
1816 1816 }
1817 1817
1818 1818 /*new binary
1819 1819 NEW_FILENODE = 1
1820 1820 DEL_FILENODE = 2
1821 1821 MOD_FILENODE = 3
1822 1822 RENAMED_FILENODE = 4
1823 1823 COPIED_FILENODE = 5
1824 1824 CHMOD_FILENODE = 6
1825 1825 BIN_FILENODE = 7
1826 1826 */
1827 1827 .cs_files_expand {
1828 1828 font-size: @basefontsize + 5px;
1829 1829 line-height: 1.8em;
1830 1830 float: right;
1831 1831 }
1832 1832
1833 1833 .cs_files_expand span{
1834 1834 color: @rcblue;
1835 1835 cursor: pointer;
1836 1836 }
1837 1837 .cs_files {
1838 1838 clear: both;
1839 1839 padding-bottom: @padding;
1840 1840
1841 1841 .cur_cs {
1842 1842 margin: 10px 2px;
1843 1843 font-weight: bold;
1844 1844 }
1845 1845
1846 1846 .node {
1847 1847 float: left;
1848 1848 }
1849 1849
1850 1850 .changes {
1851 1851 float: right;
1852 1852 color: white;
1853 1853 font-size: @basefontsize - 4px;
1854 1854 margin-top: 4px;
1855 1855 opacity: 0.6;
1856 1856 filter: Alpha(opacity=60); /* IE8 and earlier */
1857 1857
1858 1858 .added {
1859 1859 background-color: @alert1;
1860 1860 float: left;
1861 1861 text-align: center;
1862 1862 }
1863 1863
1864 1864 .deleted {
1865 1865 background-color: @alert2;
1866 1866 float: left;
1867 1867 text-align: center;
1868 1868 }
1869 1869
1870 1870 .bin {
1871 1871 background-color: @alert1;
1872 1872 text-align: center;
1873 1873 }
1874 1874
1875 1875 /*new binary*/
1876 1876 .bin.bin1 {
1877 1877 background-color: @alert1;
1878 1878 text-align: center;
1879 1879 }
1880 1880
1881 1881 /*deleted binary*/
1882 1882 .bin.bin2 {
1883 1883 background-color: @alert2;
1884 1884 text-align: center;
1885 1885 }
1886 1886
1887 1887 /*mod binary*/
1888 1888 .bin.bin3 {
1889 1889 background-color: @grey2;
1890 1890 text-align: center;
1891 1891 }
1892 1892
1893 1893 /*rename file*/
1894 1894 .bin.bin4 {
1895 1895 background-color: @alert4;
1896 1896 text-align: center;
1897 1897 }
1898 1898
1899 1899 /*copied file*/
1900 1900 .bin.bin5 {
1901 1901 background-color: @alert4;
1902 1902 text-align: center;
1903 1903 }
1904 1904
1905 1905 /*chmod file*/
1906 1906 .bin.bin6 {
1907 1907 background-color: @grey2;
1908 1908 text-align: center;
1909 1909 }
1910 1910 }
1911 1911 }
1912 1912
1913 1913 .cs_files .cs_added, .cs_files .cs_A,
1914 1914 .cs_files .cs_added, .cs_files .cs_M,
1915 1915 .cs_files .cs_added, .cs_files .cs_D {
1916 1916 height: 16px;
1917 1917 padding-right: 10px;
1918 1918 margin-top: 7px;
1919 1919 text-align: left;
1920 1920 }
1921 1921
1922 1922 .cs_icon_td {
1923 1923 min-width: 16px;
1924 1924 width: 16px;
1925 1925 }
1926 1926
1927 1927 .pull-request-merge {
1928 1928 border: 1px solid @grey5;
1929 1929 padding: 10px 0px 20px;
1930 1930 margin-top: 10px;
1931 1931 margin-bottom: 20px;
1932 1932 }
1933 1933
1934 1934 .pull-request-merge-refresh {
1935 1935 margin: 2px 7px;
1936 1936 a {
1937 1937 color: @grey3;
1938 1938 }
1939 1939 }
1940 1940
1941 1941 .pull-request-merge ul {
1942 1942 padding: 0px 0px;
1943 1943 }
1944 1944
1945 1945 .pull-request-merge li {
1946 1946 list-style-type: none;
1947 1947 }
1948 1948
1949 1949 .pull-request-merge .pull-request-wrap {
1950 1950 height: auto;
1951 1951 padding: 0px 0px;
1952 1952 text-align: right;
1953 1953 }
1954 1954
1955 1955 .pull-request-merge span {
1956 1956 margin-right: 5px;
1957 1957 }
1958 1958
1959 1959 .pull-request-merge-actions {
1960 1960 min-height: 30px;
1961 1961 padding: 0px 0px;
1962 1962 }
1963 1963
1964 1964 .pull-request-merge-info {
1965 1965 padding: 0px 5px 5px 0px;
1966 1966 }
1967 1967
1968 1968 .merge-status {
1969 1969 margin-right: 5px;
1970 1970 }
1971 1971
1972 1972 .merge-message {
1973 1973 font-size: 1.2em
1974 1974 }
1975 1975
1976 1976 .merge-message.success i,
1977 1977 .merge-icon.success i {
1978 1978 color:@alert1;
1979 1979 }
1980 1980
1981 1981 .merge-message.warning i,
1982 1982 .merge-icon.warning i {
1983 1983 color: @alert3;
1984 1984 }
1985 1985
1986 1986 .merge-message.error i,
1987 1987 .merge-icon.error i {
1988 1988 color:@alert2;
1989 1989 }
1990 1990
1991 1991 .pr-versions {
1992 1992 font-size: 1.1em;
1993 1993 padding: 7.5px;
1994 1994
1995 1995 table {
1996 1996
1997 1997 }
1998 1998
1999 1999 td {
2000 2000 line-height: 15px;
2001 2001 }
2002 2002
2003 2003 .compare-radio-button {
2004 2004 position: relative;
2005 2005 top: -3px;
2006 2006 }
2007 2007 }
2008 2008
2009 2009
2010 2010 #close_pull_request {
2011 2011 margin-right: 0px;
2012 2012 }
2013 2013
2014 2014 .empty_data {
2015 2015 color: @grey4;
2016 2016 }
2017 2017
2018 2018 #changeset_compare_view_content {
2019 2019 clear: both;
2020 2020 width: 100%;
2021 2021 box-sizing: border-box;
2022 2022 .border-radius(@border-radius);
2023 2023
2024 2024 .help-block {
2025 2025 margin: @padding 0;
2026 2026 color: @text-color;
2027 2027 &.pre-formatting {
2028 2028 white-space: pre;
2029 2029 }
2030 2030 }
2031 2031
2032 2032 .empty_data {
2033 2033 margin: @padding 0;
2034 2034 }
2035 2035
2036 2036 .alert {
2037 2037 margin-bottom: @space;
2038 2038 }
2039 2039 }
2040 2040
2041 2041 .table_disp {
2042 2042 .status {
2043 2043 width: auto;
2044 2044 }
2045 2045 }
2046 2046
2047 2047
2048 2048 .creation_in_progress {
2049 2049 color: @grey4
2050 2050 }
2051 2051
2052 2052 .status_box_menu {
2053 2053 margin: 0;
2054 2054 }
2055 2055
2056 2056 .notification-table{
2057 2057 margin-bottom: @space;
2058 2058 display: table;
2059 2059 width: 100%;
2060 2060
2061 2061 .container{
2062 2062 display: table-row;
2063 2063
2064 2064 .notification-header{
2065 2065 border-bottom: @border-thickness solid @border-default-color;
2066 2066 }
2067 2067
2068 2068 .notification-subject{
2069 2069 display: table-cell;
2070 2070 }
2071 2071 }
2072 2072 }
2073 2073
2074 2074 // Notifications
2075 2075 .notification-header{
2076 2076 display: table;
2077 2077 width: 100%;
2078 2078 padding: floor(@basefontsize/2) 0;
2079 2079 line-height: 1em;
2080 2080
2081 2081 .desc, .delete-notifications, .read-notifications{
2082 2082 display: table-cell;
2083 2083 text-align: left;
2084 2084 }
2085 2085
2086 2086 .delete-notifications, .read-notifications{
2087 2087 width: 35px;
2088 2088 min-width: 35px; //fixes when only one button is displayed
2089 2089 }
2090 2090 }
2091 2091
2092 2092 .notification-body {
2093 2093 .markdown-block,
2094 2094 .rst-block {
2095 2095 padding: @padding 0;
2096 2096 }
2097 2097
2098 2098 .notification-subject {
2099 2099 padding: @textmargin 0;
2100 2100 border-bottom: @border-thickness solid @border-default-color;
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;
2107 2113 }
2108 2114
2109 2115 #notification-status{
2110 2116 display: inline;
2111 2117 }
2112 2118
2113 2119 // Repositories
2114 2120
2115 2121 #summary.fields{
2116 2122 display: table;
2117 2123
2118 2124 .field{
2119 2125 display: table-row;
2120 2126
2121 2127 .label-summary{
2122 2128 display: table-cell;
2123 2129 min-width: @label-summary-minwidth;
2124 2130 padding-top: @padding/2;
2125 2131 padding-bottom: @padding/2;
2126 2132 padding-right: @padding/2;
2127 2133 }
2128 2134
2129 2135 .input{
2130 2136 display: table-cell;
2131 2137 padding: @padding/2;
2132 2138
2133 2139 input{
2134 2140 min-width: 29em;
2135 2141 padding: @padding/4;
2136 2142 }
2137 2143 }
2138 2144 .statistics, .downloads{
2139 2145 .disabled{
2140 2146 color: @grey4;
2141 2147 }
2142 2148 }
2143 2149 }
2144 2150 }
2145 2151
2146 2152 #summary{
2147 2153 width: 70%;
2148 2154 }
2149 2155
2150 2156
2151 2157 // Journal
2152 2158 .journal.title {
2153 2159 h5 {
2154 2160 float: left;
2155 2161 margin: 0;
2156 2162 width: 70%;
2157 2163 }
2158 2164
2159 2165 ul {
2160 2166 float: right;
2161 2167 display: inline-block;
2162 2168 margin: 0;
2163 2169 width: 30%;
2164 2170 text-align: right;
2165 2171
2166 2172 li {
2167 2173 display: inline;
2168 2174 font-size: @journal-fontsize;
2169 2175 line-height: 1em;
2170 2176
2171 2177 list-style-type: none;
2172 2178 }
2173 2179 }
2174 2180 }
2175 2181
2176 2182 .filterexample {
2177 2183 position: absolute;
2178 2184 top: 95px;
2179 2185 left: @contentpadding;
2180 2186 color: @rcblue;
2181 2187 font-size: 11px;
2182 2188 font-family: @text-regular;
2183 2189 cursor: help;
2184 2190
2185 2191 &:hover {
2186 2192 color: @rcdarkblue;
2187 2193 }
2188 2194
2189 2195 @media (max-width:768px) {
2190 2196 position: relative;
2191 2197 top: auto;
2192 2198 left: auto;
2193 2199 display: block;
2194 2200 }
2195 2201 }
2196 2202
2197 2203
2198 2204 #journal{
2199 2205 margin-bottom: @space;
2200 2206
2201 2207 .journal_day{
2202 2208 margin-bottom: @textmargin/2;
2203 2209 padding-bottom: @textmargin/2;
2204 2210 font-size: @journal-fontsize;
2205 2211 border-bottom: @border-thickness solid @border-default-color;
2206 2212 }
2207 2213
2208 2214 .journal_container{
2209 2215 margin-bottom: @space;
2210 2216
2211 2217 .journal_user{
2212 2218 display: inline-block;
2213 2219 }
2214 2220 .journal_action_container{
2215 2221 display: block;
2216 2222 margin-top: @textmargin;
2217 2223
2218 2224 div{
2219 2225 display: inline;
2220 2226 }
2221 2227
2222 2228 div.journal_action_params{
2223 2229 display: block;
2224 2230 }
2225 2231
2226 2232 div.journal_repo:after{
2227 2233 content: "\A";
2228 2234 white-space: pre;
2229 2235 }
2230 2236
2231 2237 div.date{
2232 2238 display: block;
2233 2239 margin-bottom: @textmargin;
2234 2240 }
2235 2241 }
2236 2242 }
2237 2243 }
2238 2244
2239 2245 // Files
2240 2246 .edit-file-title {
2241 2247 font-size: 16px;
2242 2248
2243 2249 .title-heading {
2244 2250 padding: 2px;
2245 2251 }
2246 2252 }
2247 2253
2248 2254 .edit-file-fieldset {
2249 2255 margin: @sidebarpadding 0;
2250 2256
2251 2257 .fieldset {
2252 2258 .left-label {
2253 2259 width: 13%;
2254 2260 }
2255 2261 .right-content {
2256 2262 width: 87%;
2257 2263 max-width: 100%;
2258 2264 }
2259 2265 .filename-label {
2260 2266 margin-top: 13px;
2261 2267 }
2262 2268 .commit-message-label {
2263 2269 margin-top: 4px;
2264 2270 }
2265 2271 .file-upload-input {
2266 2272 input {
2267 2273 display: none;
2268 2274 }
2269 2275 margin-top: 10px;
2270 2276 }
2271 2277 .file-upload-label {
2272 2278 margin-top: 10px;
2273 2279 }
2274 2280 p {
2275 2281 margin-top: 5px;
2276 2282 }
2277 2283
2278 2284 }
2279 2285 .custom-path-link {
2280 2286 margin-left: 5px;
2281 2287 }
2282 2288 #commit {
2283 2289 resize: vertical;
2284 2290 }
2285 2291 }
2286 2292
2287 2293 .delete-file-preview {
2288 2294 max-height: 250px;
2289 2295 }
2290 2296
2291 2297 .new-file,
2292 2298 #filter_activate,
2293 2299 #filter_deactivate {
2294 2300 float: right;
2295 2301 margin: 0 0 0 10px;
2296 2302 }
2297 2303
2298 2304 .file-upload-transaction-wrapper {
2299 2305 margin-top: 57px;
2300 2306 clear: both;
2301 2307 }
2302 2308
2303 2309 .file-upload-transaction-wrapper .error {
2304 2310 color: @color5;
2305 2311 }
2306 2312
2307 2313 .file-upload-transaction {
2308 2314 min-height: 200px;
2309 2315 padding: 54px;
2310 2316 border: 1px solid @grey5;
2311 2317 text-align: center;
2312 2318 clear: both;
2313 2319 }
2314 2320
2315 2321 .file-upload-transaction i {
2316 2322 font-size: 48px
2317 2323 }
2318 2324
2319 2325 h3.files_location{
2320 2326 line-height: 2.4em;
2321 2327 }
2322 2328
2323 2329 .browser-nav {
2324 2330 width: 100%;
2325 2331 display: table;
2326 2332 margin-bottom: 20px;
2327 2333
2328 2334 .info_box {
2329 2335 float: left;
2330 2336 display: inline-table;
2331 2337 height: 2.5em;
2332 2338
2333 2339 .browser-cur-rev, .info_box_elem {
2334 2340 display: table-cell;
2335 2341 vertical-align: middle;
2336 2342 }
2337 2343
2338 2344 .drop-menu {
2339 2345 margin: 0 10px;
2340 2346 }
2341 2347
2342 2348 .info_box_elem {
2343 2349 border-top: @border-thickness solid @grey5;
2344 2350 border-bottom: @border-thickness solid @grey5;
2345 2351 box-shadow: @button-shadow;
2346 2352
2347 2353 #at_rev, a {
2348 2354 padding: 0.6em 0.4em;
2349 2355 margin: 0;
2350 2356 .box-shadow(none);
2351 2357 border: 0;
2352 2358 height: 12px;
2353 2359 color: @grey2;
2354 2360 }
2355 2361
2356 2362 input#at_rev {
2357 2363 max-width: 50px;
2358 2364 text-align: center;
2359 2365 }
2360 2366
2361 2367 &.previous {
2362 2368 border: @border-thickness solid @grey5;
2363 2369 border-top-left-radius: @border-radius;
2364 2370 border-bottom-left-radius: @border-radius;
2365 2371
2366 2372 &:hover {
2367 2373 border-color: @grey4;
2368 2374 }
2369 2375
2370 2376 .disabled {
2371 2377 color: @grey5;
2372 2378 cursor: not-allowed;
2373 2379 opacity: 0.5;
2374 2380 }
2375 2381 }
2376 2382
2377 2383 &.next {
2378 2384 border: @border-thickness solid @grey5;
2379 2385 border-top-right-radius: @border-radius;
2380 2386 border-bottom-right-radius: @border-radius;
2381 2387
2382 2388 &:hover {
2383 2389 border-color: @grey4;
2384 2390 }
2385 2391
2386 2392 .disabled {
2387 2393 color: @grey5;
2388 2394 cursor: not-allowed;
2389 2395 opacity: 0.5;
2390 2396 }
2391 2397 }
2392 2398 }
2393 2399
2394 2400 .browser-cur-rev {
2395 2401
2396 2402 span{
2397 2403 margin: 0;
2398 2404 color: @rcblue;
2399 2405 height: 12px;
2400 2406 display: inline-block;
2401 2407 padding: 0.7em 1em ;
2402 2408 border: @border-thickness solid @rcblue;
2403 2409 margin-right: @padding;
2404 2410 }
2405 2411 }
2406 2412
2407 2413 }
2408 2414
2409 2415 .select-index-number {
2410 2416 margin: 0 0 0 20px;
2411 2417 color: @grey3;
2412 2418 }
2413 2419
2414 2420 .search_activate {
2415 2421 display: table-cell;
2416 2422 vertical-align: middle;
2417 2423
2418 2424 input, label{
2419 2425 margin: 0;
2420 2426 padding: 0;
2421 2427 }
2422 2428
2423 2429 input{
2424 2430 margin-left: @textmargin;
2425 2431 }
2426 2432
2427 2433 }
2428 2434 }
2429 2435
2430 2436 .browser-cur-rev{
2431 2437 margin-bottom: @textmargin;
2432 2438 }
2433 2439
2434 2440 #node_filter_box_loading{
2435 2441 .info_text;
2436 2442 }
2437 2443
2438 2444 .browser-search {
2439 2445 margin: -25px 0px 5px 0px;
2440 2446 }
2441 2447
2442 2448 .files-quick-filter {
2443 2449 float: right;
2444 2450 width: 180px;
2445 2451 position: relative;
2446 2452 }
2447 2453
2448 2454 .files-filter-box {
2449 2455 display: flex;
2450 2456 padding: 0px;
2451 2457 border-radius: 3px;
2452 2458 margin-bottom: 0;
2453 2459
2454 2460 a {
2455 2461 border: none !important;
2456 2462 }
2457 2463
2458 2464 li {
2459 2465 list-style-type: none
2460 2466 }
2461 2467 }
2462 2468
2463 2469 .files-filter-box-path {
2464 2470 line-height: 33px;
2465 2471 padding: 0;
2466 2472 width: 20px;
2467 2473 position: absolute;
2468 2474 z-index: 11;
2469 2475 left: 5px;
2470 2476 }
2471 2477
2472 2478 .files-filter-box-input {
2473 2479 margin-right: 0;
2474 2480
2475 2481 input {
2476 2482 border: 1px solid @white;
2477 2483 padding-left: 25px;
2478 2484 width: 145px;
2479 2485
2480 2486 &:hover {
2481 2487 border-color: @grey6;
2482 2488 }
2483 2489
2484 2490 &:focus {
2485 2491 border-color: @grey5;
2486 2492 }
2487 2493 }
2488 2494 }
2489 2495
2490 2496 .browser-result{
2491 2497 td a{
2492 2498 margin-left: 0.5em;
2493 2499 display: inline-block;
2494 2500
2495 2501 em {
2496 2502 font-weight: @text-bold-weight;
2497 2503 font-family: @text-bold;
2498 2504 }
2499 2505 }
2500 2506 }
2501 2507
2502 2508 .browser-highlight{
2503 2509 background-color: @grey5-alpha;
2504 2510 }
2505 2511
2506 2512
2507 2513 .edit-file-fieldset #location,
2508 2514 .edit-file-fieldset #filename {
2509 2515 display: flex;
2510 2516 width: -moz-available; /* WebKit-based browsers will ignore this. */
2511 2517 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2512 2518 width: fill-available;
2513 2519 border: 0;
2514 2520 }
2515 2521
2516 2522 .path-items {
2517 2523 display: flex;
2518 2524 padding: 0;
2519 2525 border: 1px solid #eeeeee;
2520 2526 width: 100%;
2521 2527 float: left;
2522 2528
2523 2529 .breadcrumb-path {
2524 2530 line-height: 30px;
2525 2531 padding: 0 4px;
2526 2532 white-space: nowrap;
2527 2533 }
2528 2534
2529 2535 .location-path {
2530 2536 width: -moz-available; /* WebKit-based browsers will ignore this. */
2531 2537 width: -webkit-fill-available; /* Mozilla-based browsers will ignore this. */
2532 2538 width: fill-available;
2533 2539
2534 2540 .file-name-input {
2535 2541 padding: 0.5em 0;
2536 2542 }
2537 2543
2538 2544 }
2539 2545
2540 2546 ul {
2541 2547 display: flex;
2542 2548 margin: 0;
2543 2549 padding: 0;
2544 2550 width: 100%;
2545 2551 }
2546 2552
2547 2553 li {
2548 2554 list-style-type: none;
2549 2555 }
2550 2556
2551 2557 }
2552 2558
2553 2559 .editor-items {
2554 2560 height: 40px;
2555 2561 margin: 10px 0 -17px 10px;
2556 2562
2557 2563 .editor-action {
2558 2564 cursor: pointer;
2559 2565 }
2560 2566
2561 2567 .editor-action.active {
2562 2568 border-bottom: 2px solid #5C5C5C;
2563 2569 }
2564 2570
2565 2571 li {
2566 2572 list-style-type: none;
2567 2573 }
2568 2574 }
2569 2575
2570 2576 .edit-file-fieldset .message textarea {
2571 2577 border: 1px solid #eeeeee;
2572 2578 }
2573 2579
2574 2580 #files_data .codeblock {
2575 2581 background-color: #F5F5F5;
2576 2582 }
2577 2583
2578 2584 #editor_preview {
2579 2585 background: white;
2580 2586 }
2581 2587
2582 2588 .show-editor {
2583 2589 padding: 10px;
2584 2590 background-color: white;
2585 2591
2586 2592 }
2587 2593
2588 2594 .show-preview {
2589 2595 padding: 10px;
2590 2596 background-color: white;
2591 2597 border-left: 1px solid #eeeeee;
2592 2598 }
2593 2599 // quick filter
2594 2600 .grid-quick-filter {
2595 2601 float: right;
2596 2602 position: relative;
2597 2603 }
2598 2604
2599 2605 .grid-filter-box {
2600 2606 display: flex;
2601 2607 padding: 0px;
2602 2608 border-radius: 3px;
2603 2609 margin-bottom: 0;
2604 2610
2605 2611 a {
2606 2612 border: none !important;
2607 2613 }
2608 2614
2609 2615 li {
2610 2616 list-style-type: none
2611 2617 }
2612 2618 }
2613 2619
2614 2620 .grid-filter-box-icon {
2615 2621 line-height: 33px;
2616 2622 padding: 0;
2617 2623 width: 20px;
2618 2624 position: absolute;
2619 2625 z-index: 11;
2620 2626 left: 5px;
2621 2627 }
2622 2628
2623 2629 .grid-filter-box-input {
2624 2630 margin-right: 0;
2625 2631
2626 2632 input {
2627 2633 border: 1px solid @white;
2628 2634 padding-left: 25px;
2629 2635 width: 145px;
2630 2636
2631 2637 &:hover {
2632 2638 border-color: @grey6;
2633 2639 }
2634 2640
2635 2641 &:focus {
2636 2642 border-color: @grey5;
2637 2643 }
2638 2644 }
2639 2645 }
2640 2646
2641 2647
2642 2648
2643 2649 // Search
2644 2650
2645 2651 .search-form{
2646 2652 #q {
2647 2653 width: @search-form-width;
2648 2654 }
2649 2655 .fields{
2650 2656 margin: 0 0 @space;
2651 2657 }
2652 2658
2653 2659 label{
2654 2660 display: inline-block;
2655 2661 margin-right: @textmargin;
2656 2662 padding-top: 0.25em;
2657 2663 }
2658 2664
2659 2665
2660 2666 .results{
2661 2667 clear: both;
2662 2668 margin: 0 0 @padding;
2663 2669 }
2664 2670
2665 2671 .search-tags {
2666 2672 padding: 5px 0;
2667 2673 }
2668 2674 }
2669 2675
2670 2676 div.search-feedback-items {
2671 2677 display: inline-block;
2672 2678 }
2673 2679
2674 2680 div.search-code-body {
2675 2681 background-color: #ffffff; padding: 5px 0 5px 10px;
2676 2682 pre {
2677 2683 .match { background-color: #faffa6;}
2678 2684 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
2679 2685 }
2680 2686 }
2681 2687
2682 2688 .expand_commit.search {
2683 2689 .show_more.open {
2684 2690 height: auto;
2685 2691 max-height: none;
2686 2692 }
2687 2693 }
2688 2694
2689 2695 .search-results {
2690 2696
2691 2697 h2 {
2692 2698 margin-bottom: 0;
2693 2699 }
2694 2700 .codeblock {
2695 2701 border: none;
2696 2702 background: transparent;
2697 2703 }
2698 2704
2699 2705 .codeblock-header {
2700 2706 border: none;
2701 2707 background: transparent;
2702 2708 }
2703 2709
2704 2710 .code-body {
2705 2711 border: @border-thickness solid @grey6;
2706 2712 .border-radius(@border-radius);
2707 2713 }
2708 2714
2709 2715 .td-commit {
2710 2716 &:extend(pre);
2711 2717 border-bottom: @border-thickness solid @border-default-color;
2712 2718 }
2713 2719
2714 2720 .message {
2715 2721 height: auto;
2716 2722 max-width: 350px;
2717 2723 white-space: normal;
2718 2724 text-overflow: initial;
2719 2725 overflow: visible;
2720 2726
2721 2727 .match { background-color: #faffa6;}
2722 2728 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2723 2729 }
2724 2730
2725 2731 .path {
2726 2732 border-bottom: none !important;
2727 2733 border-left: 1px solid @grey6 !important;
2728 2734 border-right: 1px solid @grey6 !important;
2729 2735 }
2730 2736 }
2731 2737
2732 2738 table.rctable td.td-search-results div {
2733 2739 max-width: 100%;
2734 2740 }
2735 2741
2736 2742 #tip-box, .tip-box{
2737 2743 padding: @menupadding/2;
2738 2744 display: block;
2739 2745 border: @border-thickness solid @border-highlight-color;
2740 2746 .border-radius(@border-radius);
2741 2747 background-color: white;
2742 2748 z-index: 99;
2743 2749 white-space: pre-wrap;
2744 2750 }
2745 2751
2746 2752 #linktt {
2747 2753 width: 79px;
2748 2754 }
2749 2755
2750 2756 #help_kb .modal-content{
2751 2757 max-width: 750px;
2752 2758 margin: 10% auto;
2753 2759
2754 2760 table{
2755 2761 td,th{
2756 2762 border-bottom: none;
2757 2763 line-height: 2.5em;
2758 2764 }
2759 2765 th{
2760 2766 padding-bottom: @textmargin/2;
2761 2767 }
2762 2768 td.keys{
2763 2769 text-align: center;
2764 2770 }
2765 2771 }
2766 2772
2767 2773 .block-left{
2768 2774 width: 45%;
2769 2775 margin-right: 5%;
2770 2776 }
2771 2777 .modal-footer{
2772 2778 clear: both;
2773 2779 }
2774 2780 .key.tag{
2775 2781 padding: 0.5em;
2776 2782 background-color: @rcblue;
2777 2783 color: white;
2778 2784 border-color: @rcblue;
2779 2785 .box-shadow(none);
2780 2786 }
2781 2787 }
2782 2788
2783 2789
2784 2790
2785 2791 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2786 2792
2787 2793 @import 'statistics-graph';
2788 2794 @import 'tables';
2789 2795 @import 'forms';
2790 2796 @import 'diff';
2791 2797 @import 'summary';
2792 2798 @import 'navigation';
2793 2799
2794 2800 //--- SHOW/HIDE SECTIONS --//
2795 2801
2796 2802 .btn-collapse {
2797 2803 float: right;
2798 2804 text-align: right;
2799 2805 font-family: @text-light;
2800 2806 font-size: @basefontsize;
2801 2807 cursor: pointer;
2802 2808 border: none;
2803 2809 color: @rcblue;
2804 2810 }
2805 2811
2806 2812 table.rctable,
2807 2813 table.dataTable {
2808 2814 .btn-collapse {
2809 2815 float: right;
2810 2816 text-align: right;
2811 2817 }
2812 2818 }
2813 2819
2814 2820 table.rctable {
2815 2821 &.permissions {
2816 2822
2817 2823 th.td-owner {
2818 2824 padding: 0;
2819 2825 }
2820 2826
2821 2827 th {
2822 2828 font-weight: normal;
2823 2829 padding: 0 5px;
2824 2830 }
2825 2831
2826 2832 }
2827 2833 }
2828 2834
2829 2835
2830 2836 // TODO: johbo: Fix for IE10, this avoids that we see a border
2831 2837 // and padding around checkboxes and radio boxes. Move to the right place,
2832 2838 // or better: Remove this once we did the form refactoring.
2833 2839 input[type=checkbox],
2834 2840 input[type=radio] {
2835 2841 padding: 0;
2836 2842 border: none;
2837 2843 }
2838 2844
2839 2845 .toggle-ajax-spinner{
2840 2846 height: 16px;
2841 2847 width: 16px;
2842 2848 }
2843 2849
2844 2850
2845 2851 .markup-form .clearfix {
2846 2852 .border-radius(@border-radius);
2847 2853 margin: 0px;
2848 2854 }
2849 2855
2850 2856 .markup-form-area {
2851 2857 padding: 8px 12px;
2852 2858 border: 1px solid @grey4;
2853 2859 .border-radius(@border-radius);
2854 2860 }
2855 2861
2856 2862 .markup-form-area-header .nav-links {
2857 2863 display: flex;
2858 2864 flex-flow: row wrap;
2859 2865 -webkit-flex-flow: row wrap;
2860 2866 width: 100%;
2861 2867 }
2862 2868
2863 2869 .markup-form-area-footer {
2864 2870 display: flex;
2865 2871 }
2866 2872
2867 2873 .markup-form-area-footer .toolbar {
2868 2874
2869 2875 }
2870 2876
2871 2877 // markup Form
2872 2878 div.markup-form {
2873 2879 margin-top: 20px;
2874 2880 }
2875 2881
2876 2882 .markup-form strong {
2877 2883 display: block;
2878 2884 margin-bottom: 15px;
2879 2885 }
2880 2886
2881 2887 .markup-form textarea {
2882 2888 width: 100%;
2883 2889 height: 100px;
2884 2890 font-family: @text-monospace;
2885 2891 }
2886 2892
2887 2893 form.markup-form {
2888 2894 margin-top: 10px;
2889 2895 margin-left: 10px;
2890 2896 }
2891 2897
2892 2898 .markup-form .comment-block-ta,
2893 2899 .markup-form .preview-box {
2894 2900 .border-radius(@border-radius);
2895 2901 .box-sizing(border-box);
2896 2902 background-color: white;
2897 2903 }
2898 2904
2899 2905 .markup-form .preview-box.unloaded {
2900 2906 height: 50px;
2901 2907 text-align: center;
2902 2908 padding: 20px;
2903 2909 background-color: white;
2904 2910 }
2905 2911
2906 2912
2907 2913 .dropzone-wrapper {
2908 2914 border: 1px solid @grey5;
2909 2915 padding: 20px;
2910 2916 }
2911 2917
2912 2918 .dropzone,
2913 2919 .dropzone-pure {
2914 2920 border: 2px dashed @grey5;
2915 2921 border-radius: 5px;
2916 2922 background: white;
2917 2923 min-height: 200px;
2918 2924 padding: 54px;
2919 2925
2920 2926 .dz-message {
2921 2927 font-weight: 700;
2922 2928 text-align: center;
2923 2929 margin: 2em 0;
2924 2930 }
2925 2931
2926 2932 }
2927 2933
2928 2934 .dz-preview {
2929 2935 margin: 10px 0 !important;
2930 2936 position: relative;
2931 2937 vertical-align: top;
2932 2938 padding: 10px;
2933 2939 border-bottom: 1px solid @grey5;
2934 2940 }
2935 2941
2936 2942 .dz-filename {
2937 2943 font-weight: 700;
2938 2944 float: left;
2939 2945 }
2940 2946
2941 2947 .dz-sending {
2942 2948 float: right;
2943 2949 }
2944 2950
2945 2951 .dz-response {
2946 2952 clear: both
2947 2953 }
2948 2954
2949 2955 .dz-filename-size {
2950 2956 float: right
2951 2957 }
2952 2958
2953 2959 .dz-error-message {
2954 2960 color: @alert2;
2955 2961 padding-top: 10px;
2956 2962 clear: both;
2957 2963 }
2958 2964
2959 2965
2960 2966 .user-hovercard {
2961 2967 padding: 5px;
2962 2968 }
2963 2969
2964 2970 .user-hovercard-icon {
2965 2971 display: inline;
2966 2972 padding: 0;
2967 2973 box-sizing: content-box;
2968 2974 border-radius: 50%;
2969 2975 float: left;
2970 2976 }
2971 2977
2972 2978 .user-hovercard-name {
2973 2979 float: right;
2974 2980 vertical-align: top;
2975 2981 padding-left: 10px;
2976 2982 min-width: 150px;
2977 2983 }
2978 2984
2979 2985 .user-hovercard-bio {
2980 2986 clear: both;
2981 2987 padding-top: 10px;
2982 2988 }
2983 2989
2984 2990 .user-hovercard-header {
2985 2991 clear: both;
2986 2992 min-height: 10px;
2987 2993 }
2988 2994
2989 2995 .user-hovercard-footer {
2990 2996 clear: both;
2991 2997 min-height: 10px;
2992 2998 }
2993 2999
2994 3000 .user-group-hovercard {
2995 3001 padding: 5px;
2996 3002 }
2997 3003
2998 3004 .user-group-hovercard-icon {
2999 3005 display: inline;
3000 3006 padding: 0;
3001 3007 box-sizing: content-box;
3002 3008 border-radius: 50%;
3003 3009 float: left;
3004 3010 }
3005 3011
3006 3012 .user-group-hovercard-name {
3007 3013 float: left;
3008 3014 vertical-align: top;
3009 3015 padding-left: 10px;
3010 3016 min-width: 150px;
3011 3017 }
3012 3018
3013 3019 .user-group-hovercard-icon i {
3014 3020 border: 1px solid @grey4;
3015 3021 border-radius: 4px;
3016 3022 }
3017 3023
3018 3024 .user-group-hovercard-bio {
3019 3025 clear: both;
3020 3026 padding-top: 10px;
3021 3027 line-height: 1.0em;
3022 3028 }
3023 3029
3024 3030 .user-group-hovercard-header {
3025 3031 clear: both;
3026 3032 min-height: 10px;
3027 3033 }
3028 3034
3029 3035 .user-group-hovercard-footer {
3030 3036 clear: both;
3031 3037 min-height: 10px;
3032 3038 }
3033 3039
3034 3040 .pr-hovercard-header {
3035 3041 clear: both;
3036 3042 display: block;
3037 3043 line-height: 20px;
3038 3044 }
3039 3045
3040 3046 .pr-hovercard-user {
3041 3047 display: flex;
3042 3048 align-items: center;
3043 3049 padding-left: 5px;
3044 3050 }
3045 3051
3046 3052 .pr-hovercard-title {
3047 3053 padding-top: 5px;
3048 3054 } No newline at end of file
@@ -1,826 +1,872 b''
1 1 // navigation.less
2 2 // For use in RhodeCode applications;
3 3 // see style guide documentation for guidelines.
4 4
5 5 // TOP MAIN DARK NAVIGATION
6 6
7 7 .header .main_nav.horizontal-list {
8 8 float: right;
9 9 color: @grey4;
10 10 > li {
11 11 a {
12 12 color: @grey4;
13 13 }
14 14 }
15 15 }
16 16
17 17 // HEADER NAVIGATION
18 18
19 19 .horizontal-list {
20 20 display: block;
21 21 margin: 0;
22 22 padding: 0;
23 23 -webkit-padding-start: 0;
24 24 text-align: left;
25 25 font-size: @navigation-fontsize;
26 26 color: @grey6;
27 27 z-index:10;
28 28
29 29 li {
30 30 line-height: 1em;
31 31 list-style-type: none;
32 32 margin: 0 20px 0 0;
33 33
34 34 a {
35 35 padding: 0 .5em;
36 36
37 37 &.menu_link_notifications {
38 38 .pill(7px,@rcblue);
39 39 display: inline;
40 40 margin: 0 7px 0 .7em;
41 41 font-size: @basefontsize;
42 42 color: white;
43 43
44 44 &.empty {
45 45 background-color: @grey4;
46 46 }
47 47
48 48 &:hover {
49 49 background-color: @rcdarkblue;
50 50 }
51 51 }
52 52 }
53 53 .pill_container {
54 54 margin: 1.25em 0px 0px 0px;
55 55 float: right;
56 56 }
57 57
58 58 &#quick_login_li {
59 59 &:hover {
60 60 color: @grey5;
61 61 }
62 62
63 63 a.menu_link_notifications {
64 64 color: white;
65 65 }
66 66
67 67 .user {
68 68 padding-bottom: 10px;
69 69 }
70 70 }
71 71
72 72 &:before { content: none; }
73 73
74 74 &:last-child {
75 75 .menulabel {
76 76 padding-right: 0;
77 77 border-right: none;
78 78
79 79 .show_more {
80 80 padding-right: 0;
81 81 }
82 82 }
83 83
84 84 &> a {
85 85 border-bottom: none;
86 86 }
87 87 }
88 88
89 89 &.open {
90 90
91 91 a {
92 92 color: white;
93 93 }
94 94 }
95 95
96 96 &:focus {
97 97 outline: none;
98 98 }
99 99
100 100 ul li {
101 101 display: block;
102 102
103 103 &:last-child> a {
104 104 border-bottom: none;
105 105 }
106 106
107 107 ul li:last-child a {
108 108 /* we don't expect more then 3 levels of submenu and the third
109 109 level can have different html structure */
110 110 border-bottom: none;
111 111 }
112 112 }
113 113 }
114 114
115 115 > li {
116 116 float: left;
117 117 display: block;
118 118 padding: 0;
119 119
120 120 > a,
121 121 &.has_select2 a {
122 122 display: block;
123 123 padding: 10px 0;
124 124 }
125 125
126 126 .menulabel {
127 127 line-height: 1em;
128 128 // for this specifically we do not use a variable
129 129 }
130 130
131 131 .menulink-counter {
132 132 border: 1px solid @grey2;
133 133 border-radius: @border-radius;
134 134 background: @grey7;
135 135 display: inline-block;
136 136 padding: 0px 4px;
137 137 text-align: center;
138 138 font-size: 12px;
139 139 }
140 140
141 141 .pr_notifications {
142 142 padding-left: .5em;
143 143 }
144 144
145 145 .pr_notifications + .menulabel {
146 146 display:inline;
147 147 padding-left: 0;
148 148 }
149 149
150 150 &:hover,
151 151 &.open,
152 152 &.active {
153 153 a {
154 154 color: @rcblue;
155 155 }
156 156 }
157 157 }
158 158
159 159 pre {
160 160 margin: 0;
161 161 padding: 0;
162 162 }
163 163
164 164 .select2-container,
165 165 .menulink.childs {
166 166 position: relative;
167 167 }
168 168
169 169 .menulink {
170 170 &.disabled {
171 171 color: @grey3;
172 172 cursor: default;
173 173 opacity: 0.5;
174 174 }
175 175 }
176 176
177 177 #quick_login {
178 178
179 179 li a {
180 180 padding: .5em 0;
181 181 border-bottom: none;
182 182 color: @grey2;
183 183
184 184 &:hover { color: @rcblue; }
185 185 }
186 186 }
187 187
188 188 #quick_login_link {
189 189 display: inline-block;
190 190
191 191 .gravatar {
192 192 border: 1px solid @grey5;
193 193 }
194 194
195 195 .gravatar-login {
196 196 height: 20px;
197 197 width: 20px;
198 198 margin: -8px 0;
199 199 padding: 0;
200 200 }
201 201
202 202 &:hover .user {
203 203 color: @grey6;
204 204 }
205 205 }
206 206 }
207 207 .header .horizontal-list {
208 208
209 209 li {
210 210
211 211 &#quick_login_li {
212 212 padding-left: .5em;
213 213 margin-right: 0px;
214 214
215 215 &:hover #quick_login_link {
216 216 color: inherit;
217 217 }
218 218
219 219 .menu_link_user {
220 220 padding: 0 2px;
221 221 }
222 222 }
223 223 list-style-type: none;
224 224 }
225 225
226 226 > li {
227 227
228 228 a {
229 229 padding: 18px 0 12px 0;
230 230 color: @nav-grey;
231 231
232 232 &.menu_link_notifications {
233 233 padding: 1px 8px;
234 234 }
235 235 }
236 236
237 237 &:hover,
238 238 &.open,
239 239 &.active {
240 240 .pill_container a {
241 241 // don't select text for the pill container, it has it' own
242 242 // hover behaviour
243 243 color: @nav-grey;
244 244 }
245 245 }
246 246
247 247 &:hover,
248 248 &.open,
249 249 &.active {
250 250 a {
251 251 color: @grey6;
252 252 }
253 253 }
254 254
255 255 .select2-dropdown-open a {
256 256 color: @grey6;
257 257 }
258 258
259 259 .repo-switcher {
260 260 padding-left: 0;
261 261
262 262 .menulabel {
263 263 padding-left: 0;
264 264 }
265 265 }
266 266 }
267 267
268 268 li ul li {
269 269 background-color:@grey2;
270 270
271 271 a {
272 272 padding: .5em 0;
273 273 border-bottom: @border-thickness solid @border-default-color;
274 274 color: @grey6;
275 275 }
276 276
277 277 &:last-child a, &.last a{
278 278 border-bottom: none;
279 279 }
280 280
281 281 &:hover {
282 282 background-color: @grey3;
283 283 }
284 284 }
285 285
286 286 .submenu {
287 287 margin-top: 5px;
288 288 }
289 289 }
290 290
291 291 // SUBMENUS
292 292 .navigation .submenu {
293 293 display: none;
294 294 }
295 295
296 296 .navigation li.open {
297 297 .submenu {
298 298 display: block;
299 299 }
300 300 }
301 301
302 302 .navigation li:last-child .submenu {
303 303 right: auto;
304 304 left: 0;
305 305 border: 1px solid @grey5;
306 306 background: @white;
307 307 box-shadow: @dropdown-shadow;
308 308 }
309 309
310 310 .submenu {
311 311 position: absolute;
312 312 top: 100%;
313 313 left: 0;
314 314 min-width: 180px;
315 315 margin: 2px 0 0;
316 316 padding: 0;
317 317 text-align: left;
318 318 font-family: @text-light;
319 319 border-radius: @border-radius;
320 320 z-index: 20;
321 321
322 322 li {
323 323 display: block;
324 324 margin: 0;
325 325 padding: 0 .5em;
326 326 line-height: 1em;
327 327 color: @grey3;
328 328 background-color: @white;
329 329 list-style-type: none;
330 330
331 331 a {
332 332 display: block;
333 333 width: 100%;
334 334 padding: .5em 0;
335 335 border-right: none;
336 336 border-bottom: @border-thickness solid white;
337 337 color: @grey3;
338 338 }
339 339
340 340 ul {
341 341 display: none;
342 342 position: absolute;
343 343 top: 0;
344 344 right: 100%;
345 345 padding: 0;
346 346 z-index: 30;
347 347 }
348 348 &:hover {
349 349 background-color: @grey7;
350 350 -webkit-transition: background .3s;
351 351 -moz-transition: background .3s;
352 352 -o-transition: background .3s;
353 353 transition: background .3s;
354 354
355 355 ul {
356 356 display: block;
357 357 }
358 358 }
359 359 }
360 360
361 361 }
362 362
363 363
364 364
365 365
366 366 // repo dropdown
367 367 .quick_repo_menu {
368 368 width: 15px;
369 369 text-align: center;
370 370 position: relative;
371 371 cursor: pointer;
372 372
373 373 div {
374 374 overflow: visible !important;
375 375 }
376 376
377 377 &.sorting {
378 378 cursor: auto;
379 379 }
380 380
381 381 &:hover {
382 382 .menu_items_container {
383 383 position: absolute;
384 384 display: block;
385 385 }
386 386 .menu_items {
387 387 display: block;
388 388 }
389 389 }
390 390
391 391 i {
392 392 margin: 0;
393 393 color: @grey4;
394 394 }
395 395
396 396 .menu_items_container {
397 397 position: absolute;
398 398 top: 0;
399 399 left: 100%;
400 400 margin: 0;
401 401 padding: 0;
402 402 list-style: none;
403 403 background-color: @grey6;
404 404 z-index: 999;
405 405 text-align: left;
406 406
407 407 a {
408 408 color: @grey2;
409 409 }
410 410
411 411 ul.menu_items {
412 412 margin: 0;
413 413 padding: 0;
414 414 }
415 415
416 416 li {
417 417 margin: 0;
418 418 padding: 0;
419 419 line-height: 1em;
420 420 list-style-type: none;
421 421
422 422 a {
423 423 display: block;
424 424 height: 16px;
425 425 padding: 8px; //must add up to td height (28px)
426 426 width: 120px; // set width
427 427
428 428 &:hover {
429 429 background-color: @grey5;
430 430 -webkit-transition: background .3s;
431 431 -moz-transition: background .3s;
432 432 -o-transition: background .3s;
433 433 transition: background .3s;
434 434 }
435 435 }
436 436 }
437 437 }
438 438 }
439 439
440 440
441 441 // new objects main action
442 442 .action-menu {
443 443 left: auto;
444 444 right: 0;
445 445 padding: 12px;
446 446 z-index: 999;
447 447 overflow: hidden;
448 448 background-color: #fff;
449 449 border: 1px solid @grey5;
450 450 color: @grey2;
451 451 box-shadow: @dropdown-shadow;
452 452
453 453 .submenu-title {
454 454 font-weight: bold;
455 455 }
456 456
457 457 .submenu-title:not(:first-of-type) {
458 458 padding-top: 10px;
459 459 }
460 460
461 461 &.submenu {
462 462 min-width: 200px;
463 463
464 464 ol {
465 465 padding:0;
466 466 }
467 467
468 468 li {
469 469 display: block;
470 470 margin: 0;
471 471 padding: .2em .5em;
472 472 line-height: 1em;
473 473
474 474 background-color: #fff;
475 475 list-style-type: none;
476 476
477 477 a {
478 478 padding: 4px;
479 479 color: @grey4 !important;
480 480 border-bottom: none;
481 481 }
482 482 }
483 483 li:not(.submenu-title) a:hover{
484 484 color: @grey2 !important;
485 485 }
486 486 }
487 487 }
488 488
489 489
490 490 // Header Repository Switcher
491 491 // Select2 Dropdown
492 492 #select2-drop.select2-drop.repo-switcher-dropdown {
493 493 width: auto !important;
494 494 margin-top: 5px;
495 495 padding: 1em 0;
496 496 text-align: left;
497 497 .border-radius-bottom(@border-radius);
498 498 border-color: transparent;
499 499 color: @grey6;
500 500 background-color: @grey2;
501 501
502 502 input {
503 503 min-width: 90%;
504 504 }
505 505
506 506 ul.select2-result-sub {
507 507
508 508 li {
509 509 line-height: 1em;
510 510
511 511 &:hover,
512 512 &.select2-highlighted {
513 513 background-color: @grey3;
514 514 }
515 515 }
516 516
517 517 &:before { content: none; }
518 518 }
519 519
520 520 ul.select2-results {
521 521 min-width: 200px;
522 522 margin: 0;
523 523 padding: 0;
524 524 list-style-type: none;
525 525 overflow-x: visible;
526 526 overflow-y: scroll;
527 527
528 528 li {
529 529 padding: 0 8px;
530 530 line-height: 1em;
531 531 color: @grey6;
532 532
533 533 &>.select2-result-label {
534 534 padding: 8px 0;
535 535 border-bottom: @border-thickness solid @grey3;
536 536 white-space: nowrap;
537 537 color: @grey5;
538 538 cursor: pointer;
539 539 }
540 540
541 541 &.select2-result-with-children {
542 542 margin: 0;
543 543 padding: 0;
544 544 }
545 545
546 546 &.select2-result-unselectable > .select2-result-label {
547 547 margin: 0 8px;
548 548 }
549 549
550 550 }
551 551 }
552 552
553 553 ul.select2-result-sub {
554 554 margin: 0;
555 555 padding: 0;
556 556
557 557 li {
558 558 display: block;
559 559 margin: 0;
560 560 border-right: none;
561 561 line-height: 1em;
562 562 font-family: @text-light;
563 563 color: @grey2;
564 564 list-style-type: none;
565 565
566 566 &:hover {
567 567 background-color: @grey3;
568 568 }
569 569 }
570 570 }
571 571 }
572 572
573 573
574 574 #context-bar {
575 575 display: block;
576 576 margin: 0 auto 20px 0;
577 577 padding: 0 @header-padding;
578 578 background-color: @grey7;
579 579 border-bottom: 1px solid @grey5;
580 580
581 581 .clear {
582 582 clear: both;
583 583 }
584 584 }
585 585
586 586 ul#context-pages {
587 587 li {
588 588 list-style-type: none;
589 589
590 590 a {
591 591 color: @grey2;
592 592
593 593 &:hover {
594 594 color: @grey1;
595 595 }
596 596 }
597 597
598 598 &.active {
599 599 // special case, non-variable color
600 600 border-bottom: 2px solid @rcblue;
601 601
602 602 a {
603 603 color: @rcblue;
604 604 }
605 605 }
606 606 }
607 607 }
608 608
609 609 // PAGINATION
610 610
611 611 .pagination {
612 612 border: @border-thickness solid @grey5;
613 613 color: @grey2;
614 614 box-shadow: @button-shadow;
615 615
616 616 .current {
617 617 color: @grey4;
618 618 }
619 619 }
620 620
621 621 .dataTables_processing {
622 622 text-align: center;
623 623 font-size: 1.1em;
624 624 position: relative;
625 625 top: 95px;
626 626 height: 0;
627 627 }
628 628
629 629 .dataTables_paginate,
630 630 .pagination-wh {
631 631 text-align: center;
632 632 display: inline-block;
633 633 border-left: 1px solid @grey5;
634 634 float: none;
635 635 overflow: hidden;
636 636 box-shadow: @button-shadow;
637 637
638 638 .paginate_button, .pager_curpage,
639 639 .pager_link, .pg-previous, .pg-next, .pager_dotdot {
640 640 display: inline-block;
641 641 padding: @menupadding/4 @menupadding;
642 642 border: 1px solid @grey5;
643 643 margin-left: -1px;
644 644 color: @grey2;
645 645 cursor: pointer;
646 646 float: left;
647 647 font-weight: 600;
648 648 white-space: nowrap;
649 649 vertical-align: middle;
650 650 user-select: none;
651 651 min-width: 15px;
652 652
653 653 &:hover {
654 654 color: @rcdarkblue;
655 655 }
656 656 }
657 657
658 658 .paginate_button.disabled,
659 659 .disabled {
660 660 color: @grey3;
661 661 cursor: default;
662 662 opacity: 0.5;
663 663 }
664 664
665 665 .paginate_button.current, .pager_curpage {
666 666 background: @rcblue;
667 667 border-color: @rcblue;
668 668 color: @white;
669 669 }
670 670
671 671 .ellipsis {
672 672 display: inline-block;
673 673 text-align: left;
674 674 padding: @menupadding/4 @menupadding;
675 675 border: 1px solid @grey5;
676 676 border-left: 0;
677 677 float: left;
678 678 }
679 679 }
680 680
681 681 // SIDEBAR
682 682
683 683 .sidebar {
684 684 .block-left;
685 685 clear: left;
686 686 max-width: @sidebar-width;
687 687 margin-right: @sidebarpadding;
688 688 padding-right: @sidebarpadding;
689 689 font-family: @text-regular;
690 690 color: @grey1;
691 691
692 692 .nav-pills {
693 693 margin: 0;
694 694 }
695 695
696 696 .nav {
697 697 list-style: none;
698 698 padding: 0;
699 699
700 700 li {
701 701 padding-bottom: @menupadding;
702 702 line-height: 1em;
703 703 color: @grey4;
704 704 list-style-type: none;
705 705
706 706 &.active a {
707 707 color: @grey2;
708 708 }
709 709
710 710 a {
711 711 color: @grey4;
712 712 }
713 713 }
714 714
715 715 }
716 716 }
717 717
718 718 .main_filter_help_box {
719 719 padding: 7px 7px;
720 720 display: inline-block;
721 721 vertical-align: top;
722 722 background: inherit;
723 723 position: absolute;
724 724 right: 0;
725 725 top: 9px;
726 726 }
727 727
728 728 .main_filter_input_box {
729 729 display: inline-block;
730 730
731 731 .searchItems {
732 732 display:flex;
733 733 background: @black;
734 734 padding: 0px;
735 735 border-radius: 3px;
736 736 border: 1px solid @black;
737 737
738 738 a {
739 739 border: none !important;
740 740 }
741 741 }
742 742
743 743 .searchTag {
744 744 line-height: 28px;
745 745 padding: 0 5px;
746 746
747 747 .tag {
748 748 color: @grey5;
749 749 border-color: @grey2;
750 750 background: @grey1;
751 751 }
752 752 }
753 753
754 754 .searchTagFilter {
755 755 background-color: @black !important;
756 756 margin-right: 0;
757 757 }
758 758 .searchTagIcon {
759 759 margin: 0;
760 760 background: @black !important;
761 761 }
762 762 .searchTagHelp {
763 763 background-color: @grey1 !important;
764 764 margin: 0;
765 765 }
766 766 .searchTagHelp:hover {
767 767 background-color: @grey1 !important;
768 768 }
769 769 .searchTagInput {
770 770 background-color: @grey1 !important;
771 771 margin-right: 0;
772 772 }
773 773 }
774 774
775 775 .main_filter_box {
776 776 margin: 9px 0 0 0;
777 777 }
778 778
779 779 #main_filter_help {
780 780 background: @grey1;
781 781 border: 1px solid black;
782 782 position: absolute;
783 783 white-space: pre;
784 784 z-index: 9999;
785 785 color: @nav-grey;
786 786 padding: 0 10px;
787 787 }
788 788
789 789 input {
790 790
791 791 &.main_filter_input {
792 792 padding: 5px 10px;
793 793 min-width: 340px;
794 794 color: @grey7;
795 795 background: @black;
796 796 min-height: 18px;
797 797 border: 0;
798 798
799 799 &:active {
800 800 color: @grey2 !important;
801 801 background: white !important;
802 802 }
803 803 &:focus {
804 804 color: @grey2 !important;
805 805 background: white !important;
806 806 }
807 807 }
808 808 }
809 809
810 810
811 811
812 812 .main_filter_input::placeholder {
813 813 color: @nav-grey;
814 814 opacity: 1;
815 815 }
816 816
817 817 .notice-box {
818 818 display:block !important;
819 819 padding: 9px 0 !important;
820 820 }
821 821
822 822 .menulabel-notice {
823 border: 1px solid @color5;
823
824 824 padding:7px 10px;
825
826 &.notice-warning {
827 border: 1px solid @color3;
828 .notice-color-warning
829 }
830 &.notice-error {
831 border: 1px solid @color5;
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 }
@@ -1,290 +1,291 b''
1 1 @font-face {
2 2 font-family: 'rcicons';
3 3
4 4 src: url('../fonts/RCIcons/rcicons.eot?44705679');
5 5 src: url('../fonts/RCIcons/rcicons.eot?44705679#iefix') format('embedded-opentype'),
6 6 url('../fonts/RCIcons/rcicons.woff2?44705679') format('woff2'),
7 7 url('../fonts/RCIcons/rcicons.woff?44705679') format('woff'),
8 8 url('../fonts/RCIcons/rcicons.ttf?44705679') format('truetype'),
9 9 url('../fonts/RCIcons/rcicons.svg?44705679#rcicons') format('svg');
10 10
11 11 font-weight: normal;
12 12 font-style: normal;
13 13 }
14 14 /* Chrome hack: SVG is rendered more smooth in Windozze. 100% magic, uncomment if you need it. */
15 15 /* Note, that will break hinting! In other OS-es font will be not as sharp as it could be */
16 16 /*
17 17 @media screen and (-webkit-min-device-pixel-ratio:0) {
18 18 @font-face {
19 19 font-family: 'rcicons';
20 20 src: url('../fonts/RCIcons/rcicons.svg?74666722#rcicons') format('svg');
21 21 }
22 22 }
23 23 */
24 24
25 25 [class^="icon-"]:before, [class*=" icon-"]:before {
26 26 font-family: "rcicons";
27 27 font-style: normal;
28 28 font-weight: normal;
29 29 speak: none;
30 30
31 31 display: inline-block;
32 32 text-decoration: inherit;
33 33 width: 1em;
34 34 margin-right: .2em;
35 35 text-align: center;
36 36 /* opacity: .8; */
37 37
38 38 /* For safety - reset parent styles, that can break glyph codes*/
39 39 font-variant: normal;
40 40 text-transform: none;
41 41
42 42 /* fix buttons height, for twitter bootstrap */
43 43 line-height: 1em;
44 44
45 45 /* Animation center compensation - margins should be symmetric */
46 46 /* remove if not needed */
47 47 margin-left: .2em;
48 48
49 49 /* you can be more comfortable with increased icons size */
50 50 /* font-size: 120%; */
51 51
52 52 /* Font smoothing. That was taken from TWBS */
53 53 -webkit-font-smoothing: antialiased;
54 54 -moz-osx-font-smoothing: grayscale;
55 55
56 56 /* Uncomment for 3D effect */
57 57 /* text-shadow: 1px 1px 1px rgba(127, 127, 127, 0.3); */
58 58 }
59 59
60 60 .animate-spin {
61 61 -moz-animation: spin 2s infinite linear;
62 62 -o-animation: spin 2s infinite linear;
63 63 -webkit-animation: spin 2s infinite linear;
64 64 animation: spin 2s infinite linear;
65 65 display: inline-block;
66 66 }
67 67 @-moz-keyframes spin {
68 68 0% {
69 69 -moz-transform: rotate(0deg);
70 70 -o-transform: rotate(0deg);
71 71 -webkit-transform: rotate(0deg);
72 72 transform: rotate(0deg);
73 73 }
74 74
75 75 100% {
76 76 -moz-transform: rotate(359deg);
77 77 -o-transform: rotate(359deg);
78 78 -webkit-transform: rotate(359deg);
79 79 transform: rotate(359deg);
80 80 }
81 81 }
82 82 @-webkit-keyframes spin {
83 83 0% {
84 84 -moz-transform: rotate(0deg);
85 85 -o-transform: rotate(0deg);
86 86 -webkit-transform: rotate(0deg);
87 87 transform: rotate(0deg);
88 88 }
89 89
90 90 100% {
91 91 -moz-transform: rotate(359deg);
92 92 -o-transform: rotate(359deg);
93 93 -webkit-transform: rotate(359deg);
94 94 transform: rotate(359deg);
95 95 }
96 96 }
97 97 @-o-keyframes spin {
98 98 0% {
99 99 -moz-transform: rotate(0deg);
100 100 -o-transform: rotate(0deg);
101 101 -webkit-transform: rotate(0deg);
102 102 transform: rotate(0deg);
103 103 }
104 104
105 105 100% {
106 106 -moz-transform: rotate(359deg);
107 107 -o-transform: rotate(359deg);
108 108 -webkit-transform: rotate(359deg);
109 109 transform: rotate(359deg);
110 110 }
111 111 }
112 112 @-ms-keyframes spin {
113 113 0% {
114 114 -moz-transform: rotate(0deg);
115 115 -o-transform: rotate(0deg);
116 116 -webkit-transform: rotate(0deg);
117 117 transform: rotate(0deg);
118 118 }
119 119
120 120 100% {
121 121 -moz-transform: rotate(359deg);
122 122 -o-transform: rotate(359deg);
123 123 -webkit-transform: rotate(359deg);
124 124 transform: rotate(359deg);
125 125 }
126 126 }
127 127 @keyframes spin {
128 128 0% {
129 129 -moz-transform: rotate(0deg);
130 130 -o-transform: rotate(0deg);
131 131 -webkit-transform: rotate(0deg);
132 132 transform: rotate(0deg);
133 133 }
134 134
135 135 100% {
136 136 -moz-transform: rotate(359deg);
137 137 -o-transform: rotate(359deg);
138 138 -webkit-transform: rotate(359deg);
139 139 transform: rotate(359deg);
140 140 }
141 141 }
142 142
143 143
144 144
145 145 .icon-no-margin::before {
146 146 margin: 0;
147 147
148 148 }
149 149 // -- ICON CLASSES -- //
150 150 // sorter = lambda s: '\n'.join(sorted(s.splitlines()))
151 151
152 152 .icon-delete:before { content: '\e800'; } /* '' */
153 153 .icon-ok:before { content: '\e801'; } /* '' */
154 154 .icon-comment:before { content: '\e802'; } /* '' */
155 155 .icon-bookmark:before { content: '\e803'; } /* '' */
156 156 .icon-branch:before { content: '\e804'; } /* '' */
157 157 .icon-tag:before { content: '\e805'; } /* '' */
158 158 .icon-lock:before { content: '\e806'; } /* '' */
159 159 .icon-unlock:before { content: '\e807'; } /* '' */
160 160 .icon-feed:before { content: '\e808'; } /* '' */
161 161 .icon-left:before { content: '\e809'; } /* '' */
162 162 .icon-right:before { content: '\e80a'; } /* '' */
163 163 .icon-down:before { content: '\e80b'; } /* '' */
164 164 .icon-folder:before { content: '\e80c'; } /* '' */
165 165 .icon-folder-open:before { content: '\e80d'; } /* '' */
166 166 .icon-trash-empty:before { content: '\e80e'; } /* '' */
167 167 .icon-group:before { content: '\e80f'; } /* '' */
168 168 .icon-remove:before { content: '\e810'; } /* '' */
169 169 .icon-fork:before { content: '\e811'; } /* '' */
170 170 .icon-more:before { content: '\e812'; } /* '' */
171 171 .icon-search:before { content: '\e813'; } /* '' */
172 172 .icon-scissors:before { content: '\e814'; } /* '' */
173 173 .icon-download:before { content: '\e815'; } /* '' */
174 174 .icon-doc:before { content: '\e816'; } /* '' */
175 175 .icon-cog:before { content: '\e817'; } /* '' */
176 176 .icon-cog-alt:before { content: '\e818'; } /* '' */
177 177 .icon-eye:before { content: '\e819'; } /* '' */
178 178 .icon-eye-off:before { content: '\e81a'; } /* '' */
179 179 .icon-cancel-circled2:before { content: '\e81b'; } /* '' */
180 180 .icon-cancel-circled:before { content: '\e81c'; } /* '' */
181 181 .icon-plus:before { content: '\e81d'; } /* '' */
182 182 .icon-plus-circled:before { content: '\e81e'; } /* '' */
183 183 .icon-minus-circled:before { content: '\e81f'; } /* '' */
184 184 .icon-minus:before { content: '\e820'; } /* '' */
185 185 .icon-info-circled:before { content: '\e821'; } /* '' */
186 186 .icon-upload:before { content: '\e822'; } /* '' */
187 187 .icon-home:before { content: '\e823'; } /* '' */
188 188 .icon-flag-filled:before { content: '\e824'; } /* '' */
189 189 .icon-git:before { content: '\e82a'; } /* '' */
190 190 .icon-hg:before { content: '\e82d'; } /* '' */
191 191 .icon-svn:before { content: '\e82e'; } /* '' */
192 192 .icon-comment-add:before { content: '\e82f'; } /* '' */
193 193 .icon-comment-toggle:before { content: '\e830'; } /* '' */
194 194 .icon-rhodecode:before { content: '\e831'; } /* '' */
195 195 .icon-up:before { content: '\e832'; } /* '' */
196 196 .icon-merge:before { content: '\e833'; } /* '' */
197 197 .icon-spin-alt:before { content: '\e834'; } /* '' */
198 198 .icon-spin:before { content: '\e838'; } /* '' */
199 199 .icon-docs:before { content: '\f0c5'; } /* '' */
200 200 .icon-menu:before { content: '\f0c9'; } /* '' */
201 201 .icon-sort:before { content: '\f0dc'; } /* '' */
202 202 .icon-paste:before { content: '\f0ea'; } /* '' */
203 203 .icon-doc-text:before { content: '\f0f6'; } /* '' */
204 204 .icon-plus-squared:before { content: '\f0fe'; } /* '' */
205 205 .icon-angle-left:before { content: '\f104'; } /* '' */
206 206 .icon-angle-right:before { content: '\f105'; } /* '' */
207 207 .icon-angle-up:before { content: '\f106'; } /* '' */
208 208 .icon-angle-down:before { content: '\f107'; } /* '' */
209 209 .icon-circle-empty:before { content: '\f10c'; } /* '' */
210 210 .icon-circle:before { content: '\f111'; } /* '' */
211 211 .icon-folder-empty:before { content: '\f114'; } /* '' */
212 212 .icon-folder-open-empty:before { content: '\f115'; } /* '' */
213 213 .icon-code:before { content: '\f121'; } /* '' */
214 214 .icon-info:before { content: '\f129'; } /* '' */
215 215 .icon-minus-squared:before { content: '\f146'; } /* '' */
216 216 .icon-minus-squared-alt:before { content: '\f147'; } /* '' */
217 217 .icon-doc-inv:before { content: '\f15b'; } /* '' */
218 218 .icon-doc-text-inv:before { content: '\f15c'; } /* '' */
219 219 .icon-plus-squared-alt:before { content: '\f196'; } /* '' */
220 220 .icon-file-code:before { content: '\f1c9'; } /* '' */
221 221 .icon-history:before { content: '\f1da'; } /* '' */
222 222 .icon-circle-thin:before { content: '\f1db'; } /* '' */
223 223 .icon-sliders:before { content: '\f1de'; } /* '' */
224 224 .icon-trash:before { content: '\f1f8'; } /* '' */
225 225
226 226
227 227 // MERGED ICONS BASED ON CURRENT ONES
228 228 .icon-repo-group:before { &:extend(.icon-folder-open:before); }
229 229 .icon-repo-private:before { &:extend(.icon-lock:before); }
230 230 .icon-repo-lock:before { &:extend(.icon-lock:before); }
231 231 .icon-unlock-alt:before { &:extend(.icon-unlock:before); }
232 232 .icon-repo-unlock:before { &:extend(.icon-unlock:before); }
233 233 .icon-repo-public:before { &:extend(.icon-unlock:before); }
234 234 .icon-rss-sign:before { &:extend(.icon-feed:before); }
235 235 .icon-code-fork:before { &:extend(.icon-fork:before); }
236 236 .icon-arrow_up:before { &:extend(.icon-up:before); }
237 237 .icon-file:before { &:extend(.icon-file-code:before); }
238 238 .icon-file-text:before { &:extend(.icon-file-code:before); }
239 239 .icon-directory:before { &:extend(.icon-folder:before); }
240 240 .icon-more-linked:before { &:extend(.icon-more:before); }
241 241 .icon-clipboard:before { &:extend(.icon-docs:before); }
242 242 .icon-copy:before { &:extend(.icon-docs:before); }
243 243 .icon-true:before { &:extend(.icon-ok:before); }
244 244 .icon-false:before { &:extend(.icon-delete:before); }
245 245 .icon-expand-linked:before { &:extend(.icon-down:before); }
246 246 .icon-pr-merge-fail:before { &:extend(.icon-delete:before); }
247 247 .icon-wide-mode:before { &:extend(.icon-sort:before); }
248 248 .icon-flag-filled-red:before { &:extend(.icon-flag-filled:before); }
249 249 .icon-user-group-alt:before { &:extend(.icon-group:before); }
250 250
251 251 // TRANSFORM
252 252 .icon-merge:before {transform: rotate(180deg);}
253 253 .icon-wide-mode:before {transform: rotate(90deg);}
254 254
255 255 // -- END ICON CLASSES -- //
256 256
257 257
258 258 //--- ICONS STYLING ------------------//
259 259
260 260 .icon-git { color: @color4 !important; }
261 261 .icon-hg { color: @color8 !important; }
262 262 .icon-svn { color: @color1 !important; }
263 263 .icon-git-inv { color: @color4 !important; }
264 264 .icon-hg-inv { color: @color8 !important; }
265 265 .icon-svn-inv { color: @color1 !important; }
266 266 .icon-repo-lock { color: #FF0000; }
267 267 .icon-repo-unlock { color: #FF0000; }
268 268 .icon-false { color: @grey5 }
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 {
275 276 &:extend(.icon-git-transparent:before);
276 277 }
277 278 .icon-hg:before {
278 279 &:extend(.icon-hg-transparent:before);
279 280 color: @alert4;
280 281 }
281 282 .icon-svn:before {
282 283 &:extend(.icon-svn-transparent:before);
283 284 }
284 285 }
285 286
286 287 .icon-user-group:before {
287 288 &:extend(.icon-group:before);
288 289 margin: 0;
289 290 font-size: 16px;
290 291 }
@@ -1,393 +1,394 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('favicon', '/favicon.ico', []);
16 16 pyroutes.register('robots', '/robots.txt', []);
17 17 pyroutes.register('auth_home', '/_admin/auth*traverse', []);
18 18 pyroutes.register('global_integrations_new', '/_admin/integrations/new', []);
19 19 pyroutes.register('global_integrations_home', '/_admin/integrations', []);
20 20 pyroutes.register('global_integrations_list', '/_admin/integrations/%(integration)s', ['integration']);
21 21 pyroutes.register('global_integrations_create', '/_admin/integrations/%(integration)s/new', ['integration']);
22 22 pyroutes.register('global_integrations_edit', '/_admin/integrations/%(integration)s/%(integration_id)s', ['integration', 'integration_id']);
23 23 pyroutes.register('repo_group_integrations_home', '/%(repo_group_name)s/_settings/integrations', ['repo_group_name']);
24 24 pyroutes.register('repo_group_integrations_new', '/%(repo_group_name)s/_settings/integrations/new', ['repo_group_name']);
25 25 pyroutes.register('repo_group_integrations_list', '/%(repo_group_name)s/_settings/integrations/%(integration)s', ['repo_group_name', 'integration']);
26 26 pyroutes.register('repo_group_integrations_create', '/%(repo_group_name)s/_settings/integrations/%(integration)s/new', ['repo_group_name', 'integration']);
27 27 pyroutes.register('repo_group_integrations_edit', '/%(repo_group_name)s/_settings/integrations/%(integration)s/%(integration_id)s', ['repo_group_name', 'integration', 'integration_id']);
28 28 pyroutes.register('repo_integrations_home', '/%(repo_name)s/settings/integrations', ['repo_name']);
29 29 pyroutes.register('repo_integrations_new', '/%(repo_name)s/settings/integrations/new', ['repo_name']);
30 30 pyroutes.register('repo_integrations_list', '/%(repo_name)s/settings/integrations/%(integration)s', ['repo_name', 'integration']);
31 31 pyroutes.register('repo_integrations_create', '/%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']);
32 32 pyroutes.register('repo_integrations_edit', '/%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']);
33 33 pyroutes.register('hovercard_user', '/_hovercard/user/%(user_id)s', ['user_id']);
34 34 pyroutes.register('hovercard_username', '/_hovercard/username/%(username)s', ['username']);
35 35 pyroutes.register('hovercard_user_group', '/_hovercard/user_group/%(user_group_id)s', ['user_group_id']);
36 36 pyroutes.register('hovercard_pull_request', '/_hovercard/pull_request/%(pull_request_id)s', ['pull_request_id']);
37 37 pyroutes.register('hovercard_repo_commit', '/_hovercard/commit/%(repo_name)s/%(commit_id)s', ['repo_name', 'commit_id']);
38 38 pyroutes.register('ops_ping', '/_admin/ops/ping', []);
39 39 pyroutes.register('ops_error_test', '/_admin/ops/error', []);
40 40 pyroutes.register('ops_redirect_test', '/_admin/ops/redirect', []);
41 41 pyroutes.register('ops_ping_legacy', '/_admin/ping', []);
42 42 pyroutes.register('ops_error_test_legacy', '/_admin/error_test', []);
43 43 pyroutes.register('admin_home', '/_admin', []);
44 44 pyroutes.register('admin_audit_logs', '/_admin/audit_logs', []);
45 45 pyroutes.register('admin_audit_log_entry', '/_admin/audit_logs/%(audit_log_id)s', ['audit_log_id']);
46 46 pyroutes.register('pull_requests_global_0', '/_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']);
47 47 pyroutes.register('pull_requests_global_1', '/_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']);
48 48 pyroutes.register('pull_requests_global', '/_admin/pull-request/%(pull_request_id)s', ['pull_request_id']);
49 49 pyroutes.register('admin_settings_open_source', '/_admin/settings/open_source', []);
50 50 pyroutes.register('admin_settings_vcs_svn_generate_cfg', '/_admin/settings/vcs/svn_generate_cfg', []);
51 51 pyroutes.register('admin_settings_system', '/_admin/settings/system', []);
52 52 pyroutes.register('admin_settings_system_update', '/_admin/settings/system/updates', []);
53 53 pyroutes.register('admin_settings_exception_tracker', '/_admin/settings/exceptions', []);
54 54 pyroutes.register('admin_settings_exception_tracker_delete_all', '/_admin/settings/exceptions/delete', []);
55 55 pyroutes.register('admin_settings_exception_tracker_show', '/_admin/settings/exceptions/%(exception_id)s', ['exception_id']);
56 56 pyroutes.register('admin_settings_exception_tracker_delete', '/_admin/settings/exceptions/%(exception_id)s/delete', ['exception_id']);
57 57 pyroutes.register('admin_settings_sessions', '/_admin/settings/sessions', []);
58 58 pyroutes.register('admin_settings_sessions_cleanup', '/_admin/settings/sessions/cleanup', []);
59 59 pyroutes.register('admin_settings_process_management', '/_admin/settings/process_management', []);
60 60 pyroutes.register('admin_settings_process_management_data', '/_admin/settings/process_management/data', []);
61 61 pyroutes.register('admin_settings_process_management_signal', '/_admin/settings/process_management/signal', []);
62 62 pyroutes.register('admin_settings_process_management_master_signal', '/_admin/settings/process_management/master_signal', []);
63 63 pyroutes.register('admin_defaults_repositories', '/_admin/defaults/repositories', []);
64 64 pyroutes.register('admin_defaults_repositories_update', '/_admin/defaults/repositories/update', []);
65 65 pyroutes.register('admin_settings', '/_admin/settings', []);
66 66 pyroutes.register('admin_settings_update', '/_admin/settings/update', []);
67 67 pyroutes.register('admin_settings_global', '/_admin/settings/global', []);
68 68 pyroutes.register('admin_settings_global_update', '/_admin/settings/global/update', []);
69 69 pyroutes.register('admin_settings_vcs', '/_admin/settings/vcs', []);
70 70 pyroutes.register('admin_settings_vcs_update', '/_admin/settings/vcs/update', []);
71 71 pyroutes.register('admin_settings_vcs_svn_pattern_delete', '/_admin/settings/vcs/svn_pattern_delete', []);
72 72 pyroutes.register('admin_settings_mapping', '/_admin/settings/mapping', []);
73 73 pyroutes.register('admin_settings_mapping_update', '/_admin/settings/mapping/update', []);
74 74 pyroutes.register('admin_settings_visual', '/_admin/settings/visual', []);
75 75 pyroutes.register('admin_settings_visual_update', '/_admin/settings/visual/update', []);
76 76 pyroutes.register('admin_settings_issuetracker', '/_admin/settings/issue-tracker', []);
77 77 pyroutes.register('admin_settings_issuetracker_update', '/_admin/settings/issue-tracker/update', []);
78 78 pyroutes.register('admin_settings_issuetracker_test', '/_admin/settings/issue-tracker/test', []);
79 79 pyroutes.register('admin_settings_issuetracker_delete', '/_admin/settings/issue-tracker/delete', []);
80 80 pyroutes.register('admin_settings_email', '/_admin/settings/email', []);
81 81 pyroutes.register('admin_settings_email_update', '/_admin/settings/email/update', []);
82 82 pyroutes.register('admin_settings_hooks', '/_admin/settings/hooks', []);
83 83 pyroutes.register('admin_settings_hooks_update', '/_admin/settings/hooks/update', []);
84 84 pyroutes.register('admin_settings_hooks_delete', '/_admin/settings/hooks/delete', []);
85 85 pyroutes.register('admin_settings_search', '/_admin/settings/search', []);
86 86 pyroutes.register('admin_settings_labs', '/_admin/settings/labs', []);
87 87 pyroutes.register('admin_settings_labs_update', '/_admin/settings/labs/update', []);
88 88 pyroutes.register('admin_permissions_application', '/_admin/permissions/application', []);
89 89 pyroutes.register('admin_permissions_application_update', '/_admin/permissions/application/update', []);
90 90 pyroutes.register('admin_permissions_global', '/_admin/permissions/global', []);
91 91 pyroutes.register('admin_permissions_global_update', '/_admin/permissions/global/update', []);
92 92 pyroutes.register('admin_permissions_object', '/_admin/permissions/object', []);
93 93 pyroutes.register('admin_permissions_object_update', '/_admin/permissions/object/update', []);
94 94 pyroutes.register('admin_permissions_ips', '/_admin/permissions/ips', []);
95 95 pyroutes.register('admin_permissions_overview', '/_admin/permissions/overview', []);
96 96 pyroutes.register('admin_permissions_auth_token_access', '/_admin/permissions/auth_token_access', []);
97 97 pyroutes.register('admin_permissions_ssh_keys', '/_admin/permissions/ssh_keys', []);
98 98 pyroutes.register('admin_permissions_ssh_keys_data', '/_admin/permissions/ssh_keys/data', []);
99 99 pyroutes.register('admin_permissions_ssh_keys_update', '/_admin/permissions/ssh_keys/update', []);
100 100 pyroutes.register('users', '/_admin/users', []);
101 101 pyroutes.register('users_data', '/_admin/users_data', []);
102 102 pyroutes.register('users_create', '/_admin/users/create', []);
103 103 pyroutes.register('users_new', '/_admin/users/new', []);
104 104 pyroutes.register('user_edit', '/_admin/users/%(user_id)s/edit', ['user_id']);
105 105 pyroutes.register('user_edit_advanced', '/_admin/users/%(user_id)s/edit/advanced', ['user_id']);
106 106 pyroutes.register('user_edit_global_perms', '/_admin/users/%(user_id)s/edit/global_permissions', ['user_id']);
107 107 pyroutes.register('user_edit_global_perms_update', '/_admin/users/%(user_id)s/edit/global_permissions/update', ['user_id']);
108 108 pyroutes.register('user_update', '/_admin/users/%(user_id)s/update', ['user_id']);
109 109 pyroutes.register('user_delete', '/_admin/users/%(user_id)s/delete', ['user_id']);
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']);
116 117 pyroutes.register('edit_user_ssh_keys_add', '/_admin/users/%(user_id)s/edit/ssh_keys/new', ['user_id']);
117 118 pyroutes.register('edit_user_ssh_keys_delete', '/_admin/users/%(user_id)s/edit/ssh_keys/delete', ['user_id']);
118 119 pyroutes.register('edit_user_emails', '/_admin/users/%(user_id)s/edit/emails', ['user_id']);
119 120 pyroutes.register('edit_user_emails_add', '/_admin/users/%(user_id)s/edit/emails/new', ['user_id']);
120 121 pyroutes.register('edit_user_emails_delete', '/_admin/users/%(user_id)s/edit/emails/delete', ['user_id']);
121 122 pyroutes.register('edit_user_ips', '/_admin/users/%(user_id)s/edit/ips', ['user_id']);
122 123 pyroutes.register('edit_user_ips_add', '/_admin/users/%(user_id)s/edit/ips/new', ['user_id']);
123 124 pyroutes.register('edit_user_ips_delete', '/_admin/users/%(user_id)s/edit/ips/delete', ['user_id']);
124 125 pyroutes.register('edit_user_perms_summary', '/_admin/users/%(user_id)s/edit/permissions_summary', ['user_id']);
125 126 pyroutes.register('edit_user_perms_summary_json', '/_admin/users/%(user_id)s/edit/permissions_summary/json', ['user_id']);
126 127 pyroutes.register('edit_user_groups_management', '/_admin/users/%(user_id)s/edit/groups_management', ['user_id']);
127 128 pyroutes.register('edit_user_groups_management_updates', '/_admin/users/%(user_id)s/edit/edit_user_groups_management/updates', ['user_id']);
128 129 pyroutes.register('edit_user_audit_logs', '/_admin/users/%(user_id)s/edit/audit', ['user_id']);
129 130 pyroutes.register('edit_user_audit_logs_download', '/_admin/users/%(user_id)s/edit/audit/download', ['user_id']);
130 131 pyroutes.register('edit_user_caches', '/_admin/users/%(user_id)s/edit/caches', ['user_id']);
131 132 pyroutes.register('edit_user_caches_update', '/_admin/users/%(user_id)s/edit/caches/update', ['user_id']);
132 133 pyroutes.register('user_groups', '/_admin/user_groups', []);
133 134 pyroutes.register('user_groups_data', '/_admin/user_groups_data', []);
134 135 pyroutes.register('user_groups_new', '/_admin/user_groups/new', []);
135 136 pyroutes.register('user_groups_create', '/_admin/user_groups/create', []);
136 137 pyroutes.register('repos', '/_admin/repos', []);
137 138 pyroutes.register('repos_data', '/_admin/repos_data', []);
138 139 pyroutes.register('repo_new', '/_admin/repos/new', []);
139 140 pyroutes.register('repo_create', '/_admin/repos/create', []);
140 141 pyroutes.register('repo_groups', '/_admin/repo_groups', []);
141 142 pyroutes.register('repo_groups_data', '/_admin/repo_groups_data', []);
142 143 pyroutes.register('repo_group_new', '/_admin/repo_group/new', []);
143 144 pyroutes.register('repo_group_create', '/_admin/repo_group/create', []);
144 145 pyroutes.register('channelstream_connect', '/_admin/channelstream/connect', []);
145 146 pyroutes.register('channelstream_subscribe', '/_admin/channelstream/subscribe', []);
146 147 pyroutes.register('channelstream_proxy', '/_channelstream', []);
147 148 pyroutes.register('upload_file', '/_file_store/upload', []);
148 149 pyroutes.register('download_file', '/_file_store/download/%(fid)s', ['fid']);
149 150 pyroutes.register('download_file_by_token', '/_file_store/token-download/%(_auth_token)s/%(fid)s', ['_auth_token', 'fid']);
150 151 pyroutes.register('logout', '/_admin/logout', []);
151 152 pyroutes.register('reset_password', '/_admin/password_reset', []);
152 153 pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []);
153 154 pyroutes.register('home', '/', []);
154 155 pyroutes.register('main_page_repos_data', '/_home_repos', []);
155 156 pyroutes.register('main_page_repo_groups_data', '/_home_repo_groups', []);
156 157 pyroutes.register('user_autocomplete_data', '/_users', []);
157 158 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
158 159 pyroutes.register('repo_list_data', '/_repos', []);
159 160 pyroutes.register('repo_group_list_data', '/_repo_groups', []);
160 161 pyroutes.register('goto_switcher_data', '/_goto_data', []);
161 162 pyroutes.register('markup_preview', '/_markup_preview', []);
162 163 pyroutes.register('file_preview', '/_file_preview', []);
163 164 pyroutes.register('store_user_session_value', '/_store_session_attr', []);
164 165 pyroutes.register('journal', '/_admin/journal', []);
165 166 pyroutes.register('journal_rss', '/_admin/journal/rss', []);
166 167 pyroutes.register('journal_atom', '/_admin/journal/atom', []);
167 168 pyroutes.register('journal_public', '/_admin/public_journal', []);
168 169 pyroutes.register('journal_public_atom', '/_admin/public_journal/atom', []);
169 170 pyroutes.register('journal_public_atom_old', '/_admin/public_journal_atom', []);
170 171 pyroutes.register('journal_public_rss', '/_admin/public_journal/rss', []);
171 172 pyroutes.register('journal_public_rss_old', '/_admin/public_journal_rss', []);
172 173 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
173 174 pyroutes.register('repo_creating', '/%(repo_name)s/repo_creating', ['repo_name']);
174 175 pyroutes.register('repo_creating_check', '/%(repo_name)s/repo_creating_check', ['repo_name']);
175 176 pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']);
176 177 pyroutes.register('repo_summary_commits', '/%(repo_name)s/summary-commits', ['repo_name']);
177 178 pyroutes.register('repo_commit', '/%(repo_name)s/changeset/%(commit_id)s', ['repo_name', 'commit_id']);
178 179 pyroutes.register('repo_commit_children', '/%(repo_name)s/changeset_children/%(commit_id)s', ['repo_name', 'commit_id']);
179 180 pyroutes.register('repo_commit_parents', '/%(repo_name)s/changeset_parents/%(commit_id)s', ['repo_name', 'commit_id']);
180 181 pyroutes.register('repo_commit_raw', '/%(repo_name)s/changeset-diff/%(commit_id)s', ['repo_name', 'commit_id']);
181 182 pyroutes.register('repo_commit_patch', '/%(repo_name)s/changeset-patch/%(commit_id)s', ['repo_name', 'commit_id']);
182 183 pyroutes.register('repo_commit_download', '/%(repo_name)s/changeset-download/%(commit_id)s', ['repo_name', 'commit_id']);
183 184 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
184 185 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
185 186 pyroutes.register('repo_commit_comment_preview', '/%(repo_name)s/changeset/%(commit_id)s/comment/preview', ['repo_name', 'commit_id']);
186 187 pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
187 188 pyroutes.register('repo_commit_comment_delete', '/%(repo_name)s/changeset/%(commit_id)s/comment/%(comment_id)s/delete', ['repo_name', 'commit_id', 'comment_id']);
188 189 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
189 190 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
190 191 pyroutes.register('repo_files_diff', '/%(repo_name)s/diff/%(f_path)s', ['repo_name', 'f_path']);
191 192 pyroutes.register('repo_files_diff_2way_redirect', '/%(repo_name)s/diff-2way/%(f_path)s', ['repo_name', 'f_path']);
192 193 pyroutes.register('repo_files', '/%(repo_name)s/files/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
193 194 pyroutes.register('repo_files:default_path', '/%(repo_name)s/files/%(commit_id)s/', ['repo_name', 'commit_id']);
194 195 pyroutes.register('repo_files:default_commit', '/%(repo_name)s/files', ['repo_name']);
195 196 pyroutes.register('repo_files:rendered', '/%(repo_name)s/render/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
196 197 pyroutes.register('repo_files:annotated', '/%(repo_name)s/annotate/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
197 198 pyroutes.register('repo_files:annotated_previous', '/%(repo_name)s/annotate-previous/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
198 199 pyroutes.register('repo_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
199 200 pyroutes.register('repo_nodetree_full:default_path', '/%(repo_name)s/nodetree_full/%(commit_id)s/', ['repo_name', 'commit_id']);
200 201 pyroutes.register('repo_files_nodelist', '/%(repo_name)s/nodelist/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
201 202 pyroutes.register('repo_file_raw', '/%(repo_name)s/raw/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
202 203 pyroutes.register('repo_file_download', '/%(repo_name)s/download/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
203 204 pyroutes.register('repo_file_download:legacy', '/%(repo_name)s/rawfile/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
204 205 pyroutes.register('repo_file_history', '/%(repo_name)s/history/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
205 206 pyroutes.register('repo_file_authors', '/%(repo_name)s/authors/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
206 207 pyroutes.register('repo_files_remove_file', '/%(repo_name)s/remove_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
207 208 pyroutes.register('repo_files_delete_file', '/%(repo_name)s/delete_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
208 209 pyroutes.register('repo_files_edit_file', '/%(repo_name)s/edit_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
209 210 pyroutes.register('repo_files_update_file', '/%(repo_name)s/update_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
210 211 pyroutes.register('repo_files_add_file', '/%(repo_name)s/add_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
211 212 pyroutes.register('repo_files_upload_file', '/%(repo_name)s/upload_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
212 213 pyroutes.register('repo_files_create_file', '/%(repo_name)s/create_file/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
213 214 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
214 215 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
215 216 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
216 217 pyroutes.register('repo_commits', '/%(repo_name)s/commits', ['repo_name']);
217 218 pyroutes.register('repo_commits_file', '/%(repo_name)s/commits/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
218 219 pyroutes.register('repo_commits_elements', '/%(repo_name)s/commits_elements', ['repo_name']);
219 220 pyroutes.register('repo_commits_elements_file', '/%(repo_name)s/commits_elements/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
220 221 pyroutes.register('repo_changelog', '/%(repo_name)s/changelog', ['repo_name']);
221 222 pyroutes.register('repo_changelog_file', '/%(repo_name)s/changelog/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
222 223 pyroutes.register('repo_compare_select', '/%(repo_name)s/compare', ['repo_name']);
223 224 pyroutes.register('repo_compare', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
224 225 pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']);
225 226 pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']);
226 227 pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']);
227 228 pyroutes.register('repo_fork_new', '/%(repo_name)s/fork', ['repo_name']);
228 229 pyroutes.register('repo_fork_create', '/%(repo_name)s/fork/create', ['repo_name']);
229 230 pyroutes.register('repo_forks_show_all', '/%(repo_name)s/forks', ['repo_name']);
230 231 pyroutes.register('repo_forks_data', '/%(repo_name)s/forks/data', ['repo_name']);
231 232 pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
232 233 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
233 234 pyroutes.register('pullrequest_show_all_data', '/%(repo_name)s/pull-request-data', ['repo_name']);
234 235 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
235 236 pyroutes.register('pullrequest_repo_targets', '/%(repo_name)s/pull-request/repo-targets', ['repo_name']);
236 237 pyroutes.register('pullrequest_new', '/%(repo_name)s/pull-request/new', ['repo_name']);
237 238 pyroutes.register('pullrequest_create', '/%(repo_name)s/pull-request/create', ['repo_name']);
238 239 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s/update', ['repo_name', 'pull_request_id']);
239 240 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
240 241 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
241 242 pyroutes.register('pullrequest_comment_create', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment', ['repo_name', 'pull_request_id']);
242 243 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/comment/%(comment_id)s/delete', ['repo_name', 'pull_request_id', 'comment_id']);
243 244 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
244 245 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
245 246 pyroutes.register('edit_repo_advanced_archive', '/%(repo_name)s/settings/advanced/archive', ['repo_name']);
246 247 pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']);
247 248 pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']);
248 249 pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']);
249 250 pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']);
250 251 pyroutes.register('edit_repo_advanced_hooks', '/%(repo_name)s/settings/advanced/hooks', ['repo_name']);
251 252 pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']);
252 253 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
253 254 pyroutes.register('edit_repo_perms_set_private', '/%(repo_name)s/settings/permissions/set_private', ['repo_name']);
254 255 pyroutes.register('edit_repo_maintenance', '/%(repo_name)s/settings/maintenance', ['repo_name']);
255 256 pyroutes.register('edit_repo_maintenance_execute', '/%(repo_name)s/settings/maintenance/execute', ['repo_name']);
256 257 pyroutes.register('edit_repo_fields', '/%(repo_name)s/settings/fields', ['repo_name']);
257 258 pyroutes.register('edit_repo_fields_create', '/%(repo_name)s/settings/fields/create', ['repo_name']);
258 259 pyroutes.register('edit_repo_fields_delete', '/%(repo_name)s/settings/fields/%(field_id)s/delete', ['repo_name', 'field_id']);
259 260 pyroutes.register('repo_edit_toggle_locking', '/%(repo_name)s/settings/toggle_locking', ['repo_name']);
260 261 pyroutes.register('edit_repo_remote', '/%(repo_name)s/settings/remote', ['repo_name']);
261 262 pyroutes.register('edit_repo_remote_pull', '/%(repo_name)s/settings/remote/pull', ['repo_name']);
262 263 pyroutes.register('edit_repo_statistics', '/%(repo_name)s/settings/statistics', ['repo_name']);
263 264 pyroutes.register('edit_repo_statistics_reset', '/%(repo_name)s/settings/statistics/update', ['repo_name']);
264 265 pyroutes.register('edit_repo_issuetracker', '/%(repo_name)s/settings/issue_trackers', ['repo_name']);
265 266 pyroutes.register('edit_repo_issuetracker_test', '/%(repo_name)s/settings/issue_trackers/test', ['repo_name']);
266 267 pyroutes.register('edit_repo_issuetracker_delete', '/%(repo_name)s/settings/issue_trackers/delete', ['repo_name']);
267 268 pyroutes.register('edit_repo_issuetracker_update', '/%(repo_name)s/settings/issue_trackers/update', ['repo_name']);
268 269 pyroutes.register('edit_repo_vcs', '/%(repo_name)s/settings/vcs', ['repo_name']);
269 270 pyroutes.register('edit_repo_vcs_update', '/%(repo_name)s/settings/vcs/update', ['repo_name']);
270 271 pyroutes.register('edit_repo_vcs_svn_pattern_delete', '/%(repo_name)s/settings/vcs/svn_pattern/delete', ['repo_name']);
271 272 pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']);
272 273 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/settings/review/default-reviewers', ['repo_name']);
273 274 pyroutes.register('edit_repo_strip', '/%(repo_name)s/settings/strip', ['repo_name']);
274 275 pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']);
275 276 pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']);
276 277 pyroutes.register('edit_repo_audit_logs', '/%(repo_name)s/settings/audit_logs', ['repo_name']);
277 278 pyroutes.register('rss_feed_home', '/%(repo_name)s/feed-rss', ['repo_name']);
278 279 pyroutes.register('atom_feed_home', '/%(repo_name)s/feed-atom', ['repo_name']);
279 280 pyroutes.register('rss_feed_home_old', '/%(repo_name)s/feed/rss', ['repo_name']);
280 281 pyroutes.register('atom_feed_home_old', '/%(repo_name)s/feed/atom', ['repo_name']);
281 282 pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']);
282 283 pyroutes.register('repo_summary_slash', '/%(repo_name)s/', ['repo_name']);
283 284 pyroutes.register('edit_repo_group', '/%(repo_group_name)s/_edit', ['repo_group_name']);
284 285 pyroutes.register('edit_repo_group_advanced', '/%(repo_group_name)s/_settings/advanced', ['repo_group_name']);
285 286 pyroutes.register('edit_repo_group_advanced_delete', '/%(repo_group_name)s/_settings/advanced/delete', ['repo_group_name']);
286 287 pyroutes.register('edit_repo_group_perms', '/%(repo_group_name)s/_settings/permissions', ['repo_group_name']);
287 288 pyroutes.register('edit_repo_group_perms_update', '/%(repo_group_name)s/_settings/permissions/update', ['repo_group_name']);
288 289 pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']);
289 290 pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']);
290 291 pyroutes.register('user_group_members_data', '/_admin/user_groups/%(user_group_id)s/members', ['user_group_id']);
291 292 pyroutes.register('edit_user_group_perms_summary', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary', ['user_group_id']);
292 293 pyroutes.register('edit_user_group_perms_summary_json', '/_admin/user_groups/%(user_group_id)s/edit/permissions_summary/json', ['user_group_id']);
293 294 pyroutes.register('edit_user_group', '/_admin/user_groups/%(user_group_id)s/edit', ['user_group_id']);
294 295 pyroutes.register('user_groups_update', '/_admin/user_groups/%(user_group_id)s/update', ['user_group_id']);
295 296 pyroutes.register('edit_user_group_global_perms', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions', ['user_group_id']);
296 297 pyroutes.register('edit_user_group_global_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/global_permissions/update', ['user_group_id']);
297 298 pyroutes.register('edit_user_group_perms', '/_admin/user_groups/%(user_group_id)s/edit/permissions', ['user_group_id']);
298 299 pyroutes.register('edit_user_group_perms_update', '/_admin/user_groups/%(user_group_id)s/edit/permissions/update', ['user_group_id']);
299 300 pyroutes.register('edit_user_group_advanced', '/_admin/user_groups/%(user_group_id)s/edit/advanced', ['user_group_id']);
300 301 pyroutes.register('edit_user_group_advanced_sync', '/_admin/user_groups/%(user_group_id)s/edit/advanced/sync', ['user_group_id']);
301 302 pyroutes.register('user_groups_delete', '/_admin/user_groups/%(user_group_id)s/delete', ['user_group_id']);
302 303 pyroutes.register('search', '/_admin/search', []);
303 304 pyroutes.register('search_repo', '/%(repo_name)s/_search', ['repo_name']);
304 305 pyroutes.register('search_repo_alt', '/%(repo_name)s/search', ['repo_name']);
305 306 pyroutes.register('search_repo_group', '/%(repo_group_name)s/_search', ['repo_group_name']);
306 307 pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']);
307 308 pyroutes.register('user_group_profile', '/_profile_user_group/%(user_group_name)s', ['user_group_name']);
308 309 pyroutes.register('my_account_profile', '/_admin/my_account/profile', []);
309 310 pyroutes.register('my_account_edit', '/_admin/my_account/edit', []);
310 311 pyroutes.register('my_account_update', '/_admin/my_account/update', []);
311 312 pyroutes.register('my_account_password', '/_admin/my_account/password', []);
312 313 pyroutes.register('my_account_password_update', '/_admin/my_account/password/update', []);
313 314 pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []);
314 315 pyroutes.register('my_account_ssh_keys', '/_admin/my_account/ssh_keys', []);
315 316 pyroutes.register('my_account_ssh_keys_generate', '/_admin/my_account/ssh_keys/generate', []);
316 317 pyroutes.register('my_account_ssh_keys_add', '/_admin/my_account/ssh_keys/new', []);
317 318 pyroutes.register('my_account_ssh_keys_delete', '/_admin/my_account/ssh_keys/delete', []);
318 319 pyroutes.register('my_account_user_group_membership', '/_admin/my_account/user_group_membership', []);
319 320 pyroutes.register('my_account_emails', '/_admin/my_account/emails', []);
320 321 pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []);
321 322 pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []);
322 323 pyroutes.register('my_account_repos', '/_admin/my_account/repos', []);
323 324 pyroutes.register('my_account_watched', '/_admin/my_account/watched', []);
324 325 pyroutes.register('my_account_bookmarks', '/_admin/my_account/bookmarks', []);
325 326 pyroutes.register('my_account_bookmarks_update', '/_admin/my_account/bookmarks/update', []);
326 327 pyroutes.register('my_account_goto_bookmark', '/_admin/my_account/bookmark/%(bookmark_id)s', ['bookmark_id']);
327 328 pyroutes.register('my_account_perms', '/_admin/my_account/perms', []);
328 329 pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []);
329 330 pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []);
330 331 pyroutes.register('my_account_pullrequests', '/_admin/my_account/pull_requests', []);
331 332 pyroutes.register('my_account_pullrequests_data', '/_admin/my_account/pull_requests/data', []);
332 333 pyroutes.register('notifications_show_all', '/_admin/notifications', []);
333 334 pyroutes.register('notifications_mark_all_read', '/_admin/notifications/mark_all_read', []);
334 335 pyroutes.register('notifications_show', '/_admin/notifications/%(notification_id)s', ['notification_id']);
335 336 pyroutes.register('notifications_update', '/_admin/notifications/%(notification_id)s/update', ['notification_id']);
336 337 pyroutes.register('notifications_delete', '/_admin/notifications/%(notification_id)s/delete', ['notification_id']);
337 338 pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []);
338 339 pyroutes.register('gists_show', '/_admin/gists', []);
339 340 pyroutes.register('gists_new', '/_admin/gists/new', []);
340 341 pyroutes.register('gists_create', '/_admin/gists/create', []);
341 342 pyroutes.register('gist_show', '/_admin/gists/%(gist_id)s', ['gist_id']);
342 343 pyroutes.register('gist_delete', '/_admin/gists/%(gist_id)s/delete', ['gist_id']);
343 344 pyroutes.register('gist_edit', '/_admin/gists/%(gist_id)s/edit', ['gist_id']);
344 345 pyroutes.register('gist_edit_check_revision', '/_admin/gists/%(gist_id)s/edit/check_revision', ['gist_id']);
345 346 pyroutes.register('gist_update', '/_admin/gists/%(gist_id)s/update', ['gist_id']);
346 347 pyroutes.register('gist_show_rev', '/_admin/gists/%(gist_id)s/%(revision)s', ['gist_id', 'revision']);
347 348 pyroutes.register('gist_show_formatted', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s', ['gist_id', 'revision', 'format']);
348 349 pyroutes.register('gist_show_formatted_path', '/_admin/gists/%(gist_id)s/%(revision)s/%(format)s/%(f_path)s', ['gist_id', 'revision', 'format', 'f_path']);
349 350 pyroutes.register('debug_style_home', '/_admin/debug_style', []);
350 351 pyroutes.register('debug_style_email', '/_admin/debug_style/email/%(email_id)s', ['email_id']);
351 352 pyroutes.register('debug_style_email_plain_rendered', '/_admin/debug_style/email-rendered/%(email_id)s', ['email_id']);
352 353 pyroutes.register('debug_style_template', '/_admin/debug_style/t/%(t_path)s', ['t_path']);
353 354 pyroutes.register('apiv2', '/_admin/api', []);
354 355 pyroutes.register('admin_settings_license', '/_admin/settings/license', []);
355 356 pyroutes.register('admin_settings_license_unlock', '/_admin/settings/license_unlock', []);
356 357 pyroutes.register('login', '/_admin/login', []);
357 358 pyroutes.register('register', '/_admin/register', []);
358 359 pyroutes.register('repo_reviewers_review_rule_new', '/%(repo_name)s/settings/review/rules/new', ['repo_name']);
359 360 pyroutes.register('repo_reviewers_review_rule_edit', '/%(repo_name)s/settings/review/rules/%(rule_id)s', ['repo_name', 'rule_id']);
360 361 pyroutes.register('repo_reviewers_review_rule_delete', '/%(repo_name)s/settings/review/rules/%(rule_id)s/delete', ['repo_name', 'rule_id']);
361 362 pyroutes.register('plugin_admin_chat', '/_admin/plugin_admin_chat/%(action)s', ['action']);
362 363 pyroutes.register('edit_user_auth_tokens', '/_admin/users/%(user_id)s/edit/auth_tokens', ['user_id']);
363 364 pyroutes.register('edit_user_auth_tokens_add', '/_admin/users/%(user_id)s/edit/auth_tokens/new', ['user_id']);
364 365 pyroutes.register('admin_settings_scheduler_show_tasks', '/_admin/settings/scheduler/_tasks', []);
365 366 pyroutes.register('admin_settings_scheduler_show_all', '/_admin/settings/scheduler', []);
366 367 pyroutes.register('admin_settings_scheduler_new', '/_admin/settings/scheduler/new', []);
367 368 pyroutes.register('admin_settings_scheduler_create', '/_admin/settings/scheduler/create', []);
368 369 pyroutes.register('admin_settings_scheduler_edit', '/_admin/settings/scheduler/%(schedule_id)s', ['schedule_id']);
369 370 pyroutes.register('admin_settings_scheduler_update', '/_admin/settings/scheduler/%(schedule_id)s/update', ['schedule_id']);
370 371 pyroutes.register('admin_settings_scheduler_delete', '/_admin/settings/scheduler/%(schedule_id)s/delete', ['schedule_id']);
371 372 pyroutes.register('admin_settings_scheduler_execute', '/_admin/settings/scheduler/%(schedule_id)s/execute', ['schedule_id']);
372 373 pyroutes.register('admin_settings_automation', '/_admin/settings/automation', []);
373 374 pyroutes.register('admin_settings_automation_update', '/_admin/settings/automation/%(entry_id)s/update', ['entry_id']);
374 375 pyroutes.register('admin_permissions_branch', '/_admin/permissions/branch', []);
375 376 pyroutes.register('admin_permissions_branch_update', '/_admin/permissions/branch/update', []);
376 377 pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []);
377 378 pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []);
378 379 pyroutes.register('my_account_external_identity', '/_admin/my_account/external-identity', []);
379 380 pyroutes.register('my_account_external_identity_delete', '/_admin/my_account/external-identity/delete', []);
380 381 pyroutes.register('repo_artifacts_list', '/%(repo_name)s/artifacts', ['repo_name']);
381 382 pyroutes.register('repo_artifacts_data', '/%(repo_name)s/artifacts_data', ['repo_name']);
382 383 pyroutes.register('repo_artifacts_new', '/%(repo_name)s/artifacts/new', ['repo_name']);
383 384 pyroutes.register('repo_artifacts_get', '/%(repo_name)s/artifacts/download/%(uid)s', ['repo_name', 'uid']);
384 385 pyroutes.register('repo_artifacts_store', '/%(repo_name)s/artifacts/store', ['repo_name']);
385 386 pyroutes.register('repo_artifacts_info', '/%(repo_name)s/artifacts/info/%(uid)s', ['repo_name', 'uid']);
386 387 pyroutes.register('repo_artifacts_delete', '/%(repo_name)s/artifacts/delete/%(uid)s', ['repo_name', 'uid']);
387 388 pyroutes.register('repo_artifacts_update', '/%(repo_name)s/artifacts/update/%(uid)s', ['repo_name', 'uid']);
388 389 pyroutes.register('repo_automation', '/%(repo_name)s/settings/automation', ['repo_name']);
389 390 pyroutes.register('repo_automation_update', '/%(repo_name)s/settings/automation/%(entry_id)s/update', ['repo_name', 'entry_id']);
390 391 pyroutes.register('edit_repo_remote_push', '/%(repo_name)s/settings/remote/push', ['repo_name']);
391 392 pyroutes.register('edit_repo_perms_branch', '/%(repo_name)s/settings/branch_permissions', ['repo_name']);
392 393 pyroutes.register('edit_repo_perms_branch_delete', '/%(repo_name)s/settings/branch_permissions/%(rule_id)s/delete', ['repo_name', 'rule_id']);
393 394 }
@@ -1,1136 +1,1189 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%!
4 4 ## base64 filter e.g ${ example | base64 }
5 5 def base64(text):
6 6 import base64
7 7 from rhodecode.lib.helpers import safe_str
8 8 return base64.encodestring(safe_str(text))
9 9 %>
10 10
11 11 <%inherit file="root.mako"/>
12 12
13 13 <%include file="/ejs_templates/templates.html"/>
14 14
15 15 <div class="outerwrapper">
16 16 <!-- HEADER -->
17 17 <div class="header">
18 18 <div id="header-inner" class="wrapper">
19 19 <div id="logo">
20 20 <div class="logo-wrapper">
21 21 <a href="${h.route_path('home')}"><img src="${h.asset('images/rhodecode-logo-white-60x60.png')}" alt="RhodeCode"/></a>
22 22 </div>
23 23 % if c.rhodecode_name:
24 24 <div class="branding">
25 25 <a href="${h.route_path('home')}">${h.branding(c.rhodecode_name)}</a>
26 26 </div>
27 27 % endif
28 28 </div>
29 29 <!-- MENU BAR NAV -->
30 30 ${self.menu_bar_nav()}
31 31 <!-- END MENU BAR NAV -->
32 32 </div>
33 33 </div>
34 34 ${self.menu_bar_subnav()}
35 35 <!-- END HEADER -->
36 36
37 37 <!-- CONTENT -->
38 38 <div id="content" class="wrapper">
39 39
40 40 <rhodecode-toast id="notifications"></rhodecode-toast>
41 41
42 42 <div class="main">
43 43 ${next.main()}
44 44 </div>
45 45 </div>
46 46 <!-- END CONTENT -->
47 47
48 48 </div>
49 49 <!-- FOOTER -->
50 50 <div id="footer">
51 51 <div id="footer-inner" class="title wrapper">
52 52 <div>
53 53 <p class="footer-link-right">
54 54 % if c.visual.show_version:
55 55 RhodeCode Enterprise ${c.rhodecode_version} ${c.rhodecode_edition}
56 56 % endif
57 57 &copy; 2010-${h.datetime.today().year}, <a href="${h.route_url('rhodecode_official')}" target="_blank">RhodeCode GmbH</a>. All rights reserved.
58 58 % if c.visual.rhodecode_support_url:
59 59 <a href="${c.visual.rhodecode_support_url}" target="_blank">${_('Support')}</a>
60 60 % endif
61 61 </p>
62 62 <% sid = 'block' if request.GET.get('showrcid') else 'none' %>
63 63 <p class="server-instance" style="display:${sid}">
64 64 ## display hidden instance ID if specially defined
65 65 % if c.rhodecode_instanceid:
66 66 ${_('RhodeCode instance id: {}').format(c.rhodecode_instanceid)}
67 67 % endif
68 68 </p>
69 69 </div>
70 70 </div>
71 71 </div>
72 72
73 73 <!-- END FOOTER -->
74 74
75 75 ### MAKO DEFS ###
76 76
77 77 <%def name="menu_bar_subnav()">
78 78 </%def>
79 79
80 80 <%def name="breadcrumbs(class_='breadcrumbs')">
81 81 <div class="${class_}">
82 82 ${self.breadcrumbs_links()}
83 83 </div>
84 84 </%def>
85 85
86 86 <%def name="admin_menu(active=None)">
87 87
88 88 <div id="context-bar">
89 89 <div class="wrapper">
90 90 <div class="title">
91 91 <div class="title-content">
92 92 <div class="title-main">
93 93 % if c.is_super_admin:
94 94 ${_('Super-admin Panel')}
95 95 % else:
96 96 ${_('Delegated Admin Panel')}
97 97 % endif
98 98 </div>
99 99 </div>
100 100 </div>
101 101
102 102 <ul id="context-pages" class="navigation horizontal-list">
103 103
104 104 ## super-admin case
105 105 % if c.is_super_admin:
106 106 <li class="${h.is_active('audit_logs', active)}"><a href="${h.route_path('admin_audit_logs')}">${_('Admin audit logs')}</a></li>
107 107 <li class="${h.is_active('repositories', active)}"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
108 108 <li class="${h.is_active('repository_groups', active)}"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
109 109 <li class="${h.is_active('users', active)}"><a href="${h.route_path('users')}">${_('Users')}</a></li>
110 110 <li class="${h.is_active('user_groups', active)}"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
111 111 <li class="${h.is_active('permissions', active)}"><a href="${h.route_path('admin_permissions_application')}">${_('Permissions')}</a></li>
112 112 <li class="${h.is_active('authentication', active)}"><a href="${h.route_path('auth_home', traverse='')}">${_('Authentication')}</a></li>
113 113 <li class="${h.is_active('integrations', active)}"><a href="${h.route_path('global_integrations_home')}">${_('Integrations')}</a></li>
114 114 <li class="${h.is_active('defaults', active)}"><a href="${h.route_path('admin_defaults_repositories')}">${_('Defaults')}</a></li>
115 115 <li class="${h.is_active('settings', active)}"><a href="${h.route_path('admin_settings')}">${_('Settings')}</a></li>
116 116
117 117 ## delegated admin
118 118 % elif c.is_delegated_admin:
119 119 <%
120 120 repositories=c.auth_user.repositories_admin or c.can_create_repo
121 121 repository_groups=c.auth_user.repository_groups_admin or c.can_create_repo_group
122 122 user_groups=c.auth_user.user_groups_admin or c.can_create_user_group
123 123 %>
124 124
125 125 %if repositories:
126 126 <li class="${h.is_active('repositories', active)} local-admin-repos"><a href="${h.route_path('repos')}">${_('Repositories')}</a></li>
127 127 %endif
128 128 %if repository_groups:
129 129 <li class="${h.is_active('repository_groups', active)} local-admin-repo-groups"><a href="${h.route_path('repo_groups')}">${_('Repository groups')}</a></li>
130 130 %endif
131 131 %if user_groups:
132 132 <li class="${h.is_active('user_groups', active)} local-admin-user-groups"><a href="${h.route_path('user_groups')}">${_('User groups')}</a></li>
133 133 %endif
134 134 % endif
135 135 </ul>
136 136
137 137 </div>
138 138 <div class="clear"></div>
139 139 </div>
140 140 </%def>
141 141
142 142 <%def name="dt_info_panel(elements)">
143 143 <dl class="dl-horizontal">
144 144 %for dt, dd, title, show_items in elements:
145 145 <dt>${dt}:</dt>
146 146 <dd title="${h.tooltip(title)}">
147 147 %if callable(dd):
148 148 ## allow lazy evaluation of elements
149 149 ${dd()}
150 150 %else:
151 151 ${dd}
152 152 %endif
153 153 %if show_items:
154 154 <span class="btn-collapse" data-toggle="item-${h.md5_safe(dt)[:6]}-details">${_('Show More')} </span>
155 155 %endif
156 156 </dd>
157 157
158 158 %if show_items:
159 159 <div class="collapsable-content" data-toggle="item-${h.md5_safe(dt)[:6]}-details" style="display: none">
160 160 %for item in show_items:
161 161 <dt></dt>
162 162 <dd>${item}</dd>
163 163 %endfor
164 164 </div>
165 165 %endif
166 166
167 167 %endfor
168 168 </dl>
169 169 </%def>
170 170
171 171 <%def name="tr_info_entry(element)">
172 172 <% key, val, title, show_items = element %>
173 173
174 174 <tr>
175 175 <td style="vertical-align: top">${key}</td>
176 176 <td title="${h.tooltip(title)}">
177 177 %if callable(val):
178 178 ## allow lazy evaluation of elements
179 179 ${val()}
180 180 %else:
181 181 ${val}
182 182 %endif
183 183 %if show_items:
184 184 <div class="collapsable-content" data-toggle="item-${h.md5_safe(val)[:6]}-details" style="display: none">
185 185 % for item in show_items:
186 186 <dt></dt>
187 187 <dd>${item}</dd>
188 188 % endfor
189 189 </div>
190 190 %endif
191 191 </td>
192 192 <td style="vertical-align: top">
193 193 %if show_items:
194 194 <span class="btn-collapse" data-toggle="item-${h.md5_safe(val)[:6]}-details">${_('Show More')} </span>
195 195 %endif
196 196 </td>
197 197 </tr>
198 198
199 199 </%def>
200 200
201 201 <%def name="gravatar(email, size=16, tooltip=False, tooltip_alt=None, user=None, extra_class=None)">
202 202 <%
203 203 if size > 16:
204 204 gravatar_class = ['gravatar','gravatar-large']
205 205 else:
206 206 gravatar_class = ['gravatar']
207 207
208 208 data_hovercard_url = ''
209 209 data_hovercard_alt = tooltip_alt.replace('<', '&lt;').replace('>', '&gt;') if tooltip_alt else ''
210 210
211 211 if tooltip:
212 212 gravatar_class += ['tooltip-hovercard']
213 213 if extra_class:
214 214 gravatar_class += extra_class
215 215 if tooltip and user:
216 216 if user.username == h.DEFAULT_USER:
217 217 gravatar_class.pop(-1)
218 218 else:
219 219 data_hovercard_url = request.route_path('hovercard_user', user_id=getattr(user, 'user_id', ''))
220 220 gravatar_class = ' '.join(gravatar_class)
221 221
222 222 %>
223 223 <%doc>
224 224 TODO: johbo: For now we serve double size images to make it smooth
225 225 for retina. This is how it worked until now. Should be replaced
226 226 with a better solution at some point.
227 227 </%doc>
228 228
229 229 <img class="${gravatar_class}" height="${size}" width="${size}" data-hovercard-url="${data_hovercard_url}" data-hovercard-alt="${data_hovercard_alt}" src="${h.gravatar_url(email, size * 2)}" />
230 230 </%def>
231 231
232 232
233 233 <%def name="gravatar_with_user(contact, size=16, show_disabled=False, tooltip=False, _class='rc-user')">
234 234 <%
235 235 email = h.email_or_none(contact)
236 236 rc_user = h.discover_user(contact)
237 237 %>
238 238
239 239 <div class="${_class}">
240 240 ${self.gravatar(email, size, tooltip=tooltip, tooltip_alt=contact, user=rc_user)}
241 241 <span class="${('user user-disabled' if show_disabled else 'user')}"> ${h.link_to_user(rc_user or contact)}</span>
242 242 </div>
243 243 </%def>
244 244
245 245
246 246 <%def name="user_group_icon(user_group=None, size=16, tooltip=False)">
247 247 <%
248 248 if (size > 16):
249 249 gravatar_class = 'icon-user-group-alt'
250 250 else:
251 251 gravatar_class = 'icon-user-group-alt'
252 252
253 253 if tooltip:
254 254 gravatar_class += ' tooltip-hovercard'
255 255
256 256 data_hovercard_url = request.route_path('hovercard_user_group', user_group_id=user_group.users_group_id)
257 257 %>
258 258 <%doc>
259 259 TODO: johbo: For now we serve double size images to make it smooth
260 260 for retina. This is how it worked until now. Should be replaced
261 261 with a better solution at some point.
262 262 </%doc>
263 263
264 264 <i style="font-size: ${size}px" class="${gravatar_class} x-icon-size-${size}" data-hovercard-url="${data_hovercard_url}"></i>
265 265 </%def>
266 266
267 267 <%def name="repo_page_title(repo_instance)">
268 268 <div class="title-content repo-title">
269 269
270 270 <div class="title-main">
271 271 ## SVN/HG/GIT icons
272 272 %if h.is_hg(repo_instance):
273 273 <i class="icon-hg"></i>
274 274 %endif
275 275 %if h.is_git(repo_instance):
276 276 <i class="icon-git"></i>
277 277 %endif
278 278 %if h.is_svn(repo_instance):
279 279 <i class="icon-svn"></i>
280 280 %endif
281 281
282 282 ## public/private
283 283 %if repo_instance.private:
284 284 <i class="icon-repo-private"></i>
285 285 %else:
286 286 <i class="icon-repo-public"></i>
287 287 %endif
288 288
289 289 ## repo name with group name
290 290 ${h.breadcrumb_repo_link(repo_instance)}
291 291
292 292 ## Context Actions
293 293 <div class="pull-right">
294 294 %if c.rhodecode_user.username != h.DEFAULT_USER:
295 295 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid, _query=dict(auth_token=c.rhodecode_user.feed_token))}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
296 296
297 297 <a href="#WatchRepo" onclick="toggleFollowingRepo(this, templateContext.repo_id); return false" title="${_('Watch this Repository and actions on it in your personalized journal')}" class="btn btn-sm ${('watching' if c.repository_is_user_following else '')}">
298 298 % if c.repository_is_user_following:
299 299 <i class="icon-eye-off"></i>${_('Unwatch')}
300 300 % else:
301 301 <i class="icon-eye"></i>${_('Watch')}
302 302 % endif
303 303
304 304 </a>
305 305 %else:
306 306 <a href="${h.route_path('atom_feed_home', repo_name=c.rhodecode_db_repo.repo_uid)}" title="${_('RSS Feed')}" class="btn btn-sm"><i class="icon-rss-sign"></i>RSS</a>
307 307 %endif
308 308 </div>
309 309
310 310 </div>
311 311
312 312 ## FORKED
313 313 %if repo_instance.fork:
314 314 <p class="discreet">
315 315 <i class="icon-code-fork"></i> ${_('Fork of')}
316 316 ${h.link_to_if(c.has_origin_repo_read_perm,repo_instance.fork.repo_name, h.route_path('repo_summary', repo_name=repo_instance.fork.repo_name))}
317 317 </p>
318 318 %endif
319 319
320 320 ## IMPORTED FROM REMOTE
321 321 %if repo_instance.clone_uri:
322 322 <p class="discreet">
323 323 <i class="icon-code-fork"></i> ${_('Clone from')}
324 324 <a href="${h.safe_str(h.hide_credentials(repo_instance.clone_uri))}">${h.hide_credentials(repo_instance.clone_uri)}</a>
325 325 </p>
326 326 %endif
327 327
328 328 ## LOCKING STATUS
329 329 %if repo_instance.locked[0]:
330 330 <p class="locking_locked discreet">
331 331 <i class="icon-repo-lock"></i>
332 332 ${_('Repository locked by %(user)s') % {'user': h.person_by_id(repo_instance.locked[0])}}
333 333 </p>
334 334 %elif repo_instance.enable_locking:
335 335 <p class="locking_unlocked discreet">
336 336 <i class="icon-repo-unlock"></i>
337 337 ${_('Repository not locked. Pull repository to lock it.')}
338 338 </p>
339 339 %endif
340 340
341 341 </div>
342 342 </%def>
343 343
344 344 <%def name="repo_menu(active=None)">
345 345 <%
346 346 ## determine if we have "any" option available
347 347 can_lock = h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name) and c.rhodecode_db_repo.enable_locking
348 348 has_actions = can_lock
349 349
350 350 %>
351 351 % if c.rhodecode_db_repo.archived:
352 352 <div class="alert alert-warning text-center">
353 353 <strong>${_('This repository has been archived. It is now read-only.')}</strong>
354 354 </div>
355 355 % endif
356 356
357 357 <!--- REPO CONTEXT BAR -->
358 358 <div id="context-bar">
359 359 <div class="wrapper">
360 360
361 361 <div class="title">
362 362 ${self.repo_page_title(c.rhodecode_db_repo)}
363 363 </div>
364 364
365 365 <ul id="context-pages" class="navigation horizontal-list">
366 366 <li class="${h.is_active('summary', active)}"><a class="menulink" href="${h.route_path('repo_summary', repo_name=c.repo_name)}"><div class="menulabel">${_('Summary')}</div></a></li>
367 367 <li class="${h.is_active('commits', active)}"><a class="menulink" href="${h.route_path('repo_commits', repo_name=c.repo_name)}"><div class="menulabel">${_('Commits')}</div></a></li>
368 368 <li class="${h.is_active('files', active)}"><a class="menulink" href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.rhodecode_db_repo.landing_rev[1], f_path='')}"><div class="menulabel">${_('Files')}</div></a></li>
369 369 <li class="${h.is_active('compare', active)}"><a class="menulink" href="${h.route_path('repo_compare_select',repo_name=c.repo_name)}"><div class="menulabel">${_('Compare')}</div></a></li>
370 370
371 371 ## TODO: anderson: ideally it would have a function on the scm_instance "enable_pullrequest() and enable_fork()"
372 372 %if c.rhodecode_db_repo.repo_type in ['git','hg']:
373 373 <li class="${h.is_active('showpullrequest', active)}">
374 374 <a class="menulink" href="${h.route_path('pullrequest_show_all', repo_name=c.repo_name)}" title="${h.tooltip(_('Show Pull Requests for %s') % c.repo_name)}">
375 375 <div class="menulabel">
376 376 ${_('Pull Requests')} <span class="menulink-counter">${c.repository_pull_requests}</span>
377 377 </div>
378 378 </a>
379 379 </li>
380 380 %endif
381 381
382 382 <li class="${h.is_active('artifacts', active)}">
383 383 <a class="menulink" href="${h.route_path('repo_artifacts_list',repo_name=c.repo_name)}">
384 384 <div class="menulabel">
385 385 ${_('Artifacts')} <span class="menulink-counter">${c.repository_artifacts}</span>
386 386 </div>
387 387 </a>
388 388 </li>
389 389
390 390 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
391 391 <li class="${h.is_active('settings', active)}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
392 392 %endif
393 393
394 394 <li class="${h.is_active('options', active)}">
395 395 % if has_actions:
396 396 <a class="menulink dropdown">
397 397 <div class="menulabel">${_('Options')}<div class="show_more"></div></div>
398 398 </a>
399 399 <ul class="submenu">
400 400 %if can_lock:
401 401 %if c.rhodecode_db_repo.locked[0]:
402 402 <li><a class="locking_del" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Unlock Repository')}</a></li>
403 403 %else:
404 404 <li><a class="locking_add" href="${h.route_path('repo_edit_toggle_locking',repo_name=c.repo_name)}">${_('Lock Repository')}</a></li>
405 405 %endif
406 406 %endif
407 407 </ul>
408 408 % endif
409 409 </li>
410 410
411 411 </ul>
412 412 </div>
413 413 <div class="clear"></div>
414 414 </div>
415 415
416 416 <!--- REPO END CONTEXT BAR -->
417 417
418 418 </%def>
419 419
420 420 <%def name="repo_group_page_title(repo_group_instance)">
421 421 <div class="title-content">
422 422 <div class="title-main">
423 423 ## Repository Group icon
424 424 <i class="icon-repo-group"></i>
425 425
426 426 ## repo name with group name
427 427 ${h.breadcrumb_repo_group_link(repo_group_instance)}
428 428 </div>
429 429
430 430 <%namespace name="dt" file="/data_table/_dt_elements.mako"/>
431 431 <div class="repo-group-desc discreet">
432 432 ${dt.repo_group_desc(repo_group_instance.description_safe, repo_group_instance.personal, c.visual.stylify_metatags)}
433 433 </div>
434 434
435 435 </div>
436 436 </%def>
437 437
438 438
439 439 <%def name="repo_group_menu(active=None)">
440 440 <%
441 441 gr_name = c.repo_group.group_name if c.repo_group else None
442 442 # create repositories with write permission on group is set to true
443 443 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
444 444
445 445 %>
446 446
447 447
448 448 <!--- REPO GROUP CONTEXT BAR -->
449 449 <div id="context-bar">
450 450 <div class="wrapper">
451 451 <div class="title">
452 452 ${self.repo_group_page_title(c.repo_group)}
453 453 </div>
454 454
455 455 <ul id="context-pages" class="navigation horizontal-list">
456 456 <li class="${h.is_active('home', active)}">
457 457 <a class="menulink" href="${h.route_path('repo_group_home', repo_group_name=c.repo_group.group_name)}"><div class="menulabel">${_('Group Home')}</div></a>
458 458 </li>
459 459 % if c.is_super_admin or group_admin:
460 460 <li class="${h.is_active('settings', active)}">
461 461 <a class="menulink" href="${h.route_path('edit_repo_group',repo_group_name=c.repo_group.group_name)}" title="${_('You have admin right to this group, and can edit it')}"><div class="menulabel">${_('Group Settings')}</div></a>
462 462 </li>
463 463 % endif
464 464
465 465 </ul>
466 466 </div>
467 467 <div class="clear"></div>
468 468 </div>
469 469
470 470 <!--- REPO GROUP CONTEXT BAR -->
471 471
472 472 </%def>
473 473
474 474
475 475 <%def name="usermenu(active=False)">
476 476 <%
477 477 not_anonymous = c.rhodecode_user.username != h.DEFAULT_USER
478 478
479 479 gr_name = c.repo_group.group_name if (hasattr(c, 'repo_group') and c.repo_group) else None
480 480 # create repositories with write permission on group is set to true
481 481
482 482 can_fork = c.is_super_admin or h.HasPermissionAny('hg.fork.repository')()
483 483 create_on_write = h.HasPermissionAny('hg.create.write_on_repogroup.true')()
484 484 group_write = h.HasRepoGroupPermissionAny('group.write')(gr_name, 'can write into group index page')
485 485 group_admin = h.HasRepoGroupPermissionAny('group.admin')(gr_name, 'group admin index page')
486 486
487 487 can_create_repos = c.is_super_admin or c.can_create_repo
488 488 can_create_repo_groups = c.is_super_admin or c.can_create_repo_group
489 489
490 490 can_create_repos_in_group = c.is_super_admin or group_admin or (group_write and create_on_write)
491 491 can_create_repo_groups_in_group = c.is_super_admin or group_admin
492 492 %>
493 493
494 494 % if not_anonymous:
495 495 <%
496 496 default_target_group = dict()
497 497 if c.rhodecode_user.personal_repo_group:
498 498 default_target_group = dict(parent_group=c.rhodecode_user.personal_repo_group.group_id)
499 499 %>
500 500
501 501 ## create action
502 502 <li>
503 503 <a href="#create-actions" onclick="return false;" class="menulink childs">
504 504 <i class="tooltip icon-plus-circled" title="${_('Create')}"></i>
505 505 </a>
506 506
507 507 <div class="action-menu submenu">
508 508
509 509 <ol>
510 510 ## scope of within a repository
511 511 % if hasattr(c, 'rhodecode_db_repo') and c.rhodecode_db_repo:
512 512 <li class="submenu-title">${_('This Repository')}</li>
513 513 <li>
514 514 <a href="${h.route_path('pullrequest_new',repo_name=c.repo_name)}">${_('Create Pull Request')}</a>
515 515 </li>
516 516 % if can_fork:
517 517 <li>
518 518 <a href="${h.route_path('repo_fork_new',repo_name=c.repo_name,_query=default_target_group)}">${_('Fork this repository')}</a>
519 519 </li>
520 520 % endif
521 521 % endif
522 522
523 523 ## scope of within repository groups
524 524 % if hasattr(c, 'repo_group') and c.repo_group and (can_create_repos_in_group or can_create_repo_groups_in_group):
525 525 <li class="submenu-title">${_('This Repository Group')}</li>
526 526
527 527 % if can_create_repos_in_group:
528 528 <li>
529 529 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.repo_group.group_id))}">${_('New Repository')}</a>
530 530 </li>
531 531 % endif
532 532
533 533 % if can_create_repo_groups_in_group:
534 534 <li>
535 535 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.repo_group.group_id))}">${_(u'New Repository Group')}</a>
536 536 </li>
537 537 % endif
538 538 % endif
539 539
540 540 ## personal group
541 541 % if c.rhodecode_user.personal_repo_group:
542 542 <li class="submenu-title">Personal Group</li>
543 543
544 544 <li>
545 545 <a href="${h.route_path('repo_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}" >${_('New Repository')} </a>
546 546 </li>
547 547
548 548 <li>
549 549 <a href="${h.route_path('repo_group_new',_query=dict(parent_group=c.rhodecode_user.personal_repo_group.group_id))}">${_('New Repository Group')} </a>
550 550 </li>
551 551 % endif
552 552
553 553 ## Global actions
554 554 <li class="submenu-title">RhodeCode</li>
555 555 % if can_create_repos:
556 556 <li>
557 557 <a href="${h.route_path('repo_new')}" >${_('New Repository')}</a>
558 558 </li>
559 559 % endif
560 560
561 561 % if can_create_repo_groups:
562 562 <li>
563 563 <a href="${h.route_path('repo_group_new')}" >${_(u'New Repository Group')}</a>
564 564 </li>
565 565 % endif
566 566
567 567 <li>
568 568 <a href="${h.route_path('gists_new')}">${_(u'New Gist')}</a>
569 569 </li>
570 570
571 571 </ol>
572 572
573 573 </div>
574 574 </li>
575 575
576 576 ## notifications
577 577 <li>
578 578 <a class="${('empty' if c.unread_notifications == 0 else '')}" href="${h.route_path('notifications_show_all')}">
579 579 ${c.unread_notifications}
580 580 </a>
581 581 </li>
582 582 % endif
583 583
584 584 ## USER MENU
585 585 <li id="quick_login_li" class="${'active' if active else ''}">
586 586 % if c.rhodecode_user.username == h.DEFAULT_USER:
587 587 <a id="quick_login_link" class="menulink childs" href="${h.route_path('login', _query={'came_from': h.current_route_path(request)})}">
588 588 ${gravatar(c.rhodecode_user.email, 20)}
589 589 <span class="user">
590 590 <span>${_('Sign in')}</span>
591 591 </span>
592 592 </a>
593 593 % else:
594 594 ## logged in user
595 595 <a id="quick_login_link" class="menulink childs">
596 596 ${gravatar(c.rhodecode_user.email, 20)}
597 597 <span class="user">
598 598 <span class="menu_link_user">${c.rhodecode_user.username}</span>
599 599 <div class="show_more"></div>
600 600 </span>
601 601 </a>
602 602 ## subnav with menu for logged in user
603 603 <div class="user-menu submenu">
604 604 <div id="quick_login">
605 605 %if c.rhodecode_user.username != h.DEFAULT_USER:
606 606 <div class="">
607 607 <div class="big_gravatar">${gravatar(c.rhodecode_user.email, 48)}</div>
608 608 <div class="full_name">${c.rhodecode_user.full_name_or_username}</div>
609 609 <div class="email">${c.rhodecode_user.email}</div>
610 610 </div>
611 611 <div class="">
612 612 <ol class="links">
613 613 <li>${h.link_to(_(u'My account'),h.route_path('my_account_profile'))}</li>
614 614 % if c.rhodecode_user.personal_repo_group:
615 615 <li>${h.link_to(_(u'My personal group'), h.route_path('repo_group_home', repo_group_name=c.rhodecode_user.personal_repo_group.group_name))}</li>
616 616 % endif
617 617 <li>${h.link_to(_(u'Pull Requests'), h.route_path('my_account_pullrequests'))}</li>
618 618
619 619 % if c.debug_style:
620 620 <li>
621 621 <a class="menulink" title="${_('Style')}" href="${h.route_path('debug_style_home')}">
622 622 <div class="menulabel">${_('[Style]')}</div>
623 623 </a>
624 624 </li>
625 625 % endif
626 626
627 627 ## bookmark-items
628 628 <li class="bookmark-items">
629 629 ${_('Bookmarks')}
630 630 <div class="pull-right">
631 631 <a href="${h.route_path('my_account_bookmarks')}">
632 632
633 633 <i class="icon-cog"></i>
634 634 </a>
635 635 </div>
636 636 </li>
637 637 % if not c.bookmark_items:
638 638 <li>
639 639 <a href="${h.route_path('my_account_bookmarks')}">${_('No Bookmarks yet.')}</a>
640 640 </li>
641 641 % endif
642 642 % for item in c.bookmark_items:
643 643 <li>
644 644 % if item.repository:
645 645 <div>
646 646 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
647 647 <code>${item.position}</code>
648 648 % if item.repository.repo_type == 'hg':
649 649 <i class="icon-hg" title="${_('Repository')}" style="font-size: 16px"></i>
650 650 % elif item.repository.repo_type == 'git':
651 651 <i class="icon-git" title="${_('Repository')}" style="font-size: 16px"></i>
652 652 % elif item.repository.repo_type == 'svn':
653 653 <i class="icon-svn" title="${_('Repository')}" style="font-size: 16px"></i>
654 654 % endif
655 655 ${(item.title or h.shorter(item.repository.repo_name, 30))}
656 656 </a>
657 657 </div>
658 658 % elif item.repository_group:
659 659 <div>
660 660 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
661 661 <code>${item.position}</code>
662 662 <i class="icon-repo-group" title="${_('Repository group')}" style="font-size: 14px"></i>
663 663 ${(item.title or h.shorter(item.repository_group.group_name, 30))}
664 664 </a>
665 665 </div>
666 666 % else:
667 667 <a class="bookmark-item" href="${h.route_path('my_account_goto_bookmark', bookmark_id=item.position)}">
668 668 <code>${item.position}</code>
669 669 ${item.title}
670 670 </a>
671 671 % endif
672 672 </li>
673 673 % endfor
674 674
675 675 <li class="logout">
676 676 ${h.secure_form(h.route_path('logout'), request=request)}
677 677 ${h.submit('log_out', _(u'Sign Out'),class_="btn btn-primary")}
678 678 ${h.end_form()}
679 679 </li>
680 680 </ol>
681 681 </div>
682 682 %endif
683 683 </div>
684 684 </div>
685 685
686 686 % endif
687 687 </li>
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">
705 738 <div class="main_filter_input_box">
706 739 <ul class="searchItems">
707 740
708 741 <li class="searchTag searchTagIcon">
709 742 <i class="icon-search"></i>
710 743 </li>
711 744
712 745 % if c.template_context['search_context']['repo_id']:
713 746 <li class="searchTag searchTagFilter searchTagHidable" >
714 747 ##<a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">
715 748 <span class="tag">
716 749 This repo
717 750 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
718 751 </span>
719 752 ##</a>
720 753 </li>
721 754 % elif c.template_context['search_context']['repo_group_id']:
722 755 <li class="searchTag searchTagFilter searchTagHidable">
723 756 ##<a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">
724 757 <span class="tag">
725 758 This group
726 759 <a href="#removeGoToFilter" onclick="removeGoToFilter(); return false"><i class="icon-cancel-circled"></i></a>
727 760 </span>
728 761 ##</a>
729 762 </li>
730 763 % endif
731 764
732 765 <li class="searchTagInput">
733 766 <input class="main_filter_input" id="main_filter" size="25" type="text" name="main_filter" placeholder="${_('search / go to...')}" value="" />
734 767 </li>
735 768 <li class="searchTag searchTagHelp">
736 769 <a href="#showFilterHelp" onclick="showMainFilterBox(); return false">?</a>
737 770 </li>
738 771 </ul>
739 772 </div>
740 773 </div>
741 774
742 775 <div id="main_filter_help" style="display: none">
743 776 - Use '/' key to quickly access this field.
744 777
745 778 - Enter a name of repository, or repository group for quick search.
746 779
747 780 - Prefix query to allow special search:
748 781
749 782 user:admin, to search for usernames, always global
750 783
751 784 user_group:devops, to search for user groups, always global
752 785
753 786 commit:efced4, to search for commits, scoped to repositories or groups
754 787
755 788 file:models.py, to search for file paths, scoped to repositories or groups
756 789
757 790 % if c.template_context['search_context']['repo_id']:
758 791 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
759 792 % elif c.template_context['search_context']['repo_group_id']:
760 793 For advanced full text search visit: <a href="${h.route_path('search_repo_group',repo_group_name=c.template_context['search_context']['repo_group_name'])}">repository group search</a>
761 794 % else:
762 795 For advanced full text search visit: <a href="${h.route_path('search')}">global search</a>
763 796 % endif
764 797 </div>
765 798 </li>
766 799
767 800 ## ROOT MENU
768 801 <li class="${h.is_active('home', active)}">
769 802 <a class="menulink" title="${_('Home')}" href="${h.route_path('home')}">
770 803 <div class="menulabel">${_('Home')}</div>
771 804 </a>
772 805 </li>
773 806
774 807 %if c.rhodecode_user.username != h.DEFAULT_USER:
775 808 <li class="${h.is_active('journal', active)}">
776 809 <a class="menulink" title="${_('Show activity journal')}" href="${h.route_path('journal')}">
777 810 <div class="menulabel">${_('Journal')}</div>
778 811 </a>
779 812 </li>
780 813 %else:
781 814 <li class="${h.is_active('journal', active)}">
782 815 <a class="menulink" title="${_('Show Public activity journal')}" href="${h.route_path('journal_public')}">
783 816 <div class="menulabel">${_('Public journal')}</div>
784 817 </a>
785 818 </li>
786 819 %endif
787 820
788 821 <li class="${h.is_active('gists', active)}">
789 822 <a class="menulink childs" title="${_('Show Gists')}" href="${h.route_path('gists_show')}">
790 823 <div class="menulabel">${_('Gists')}</div>
791 824 </a>
792 825 </li>
793 826
794 827 % if c.is_super_admin or c.is_delegated_admin:
795 828 <li class="${h.is_active('admin', active)}">
796 829 <a class="menulink childs" title="${_('Admin settings')}" href="${h.route_path('admin_home')}">
797 830 <div class="menulabel">${_('Admin')} </div>
798 831 </a>
799 832 </li>
800 833 % endif
801 834
802 835 ## render extra user menu
803 836 ${usermenu(active=(active=='my_account'))}
804 837
805 838 </ul>
806 839
807 840 <script type="text/javascript">
808 841 var visualShowPublicIcon = "${c.visual.show_public_icon}" == "True";
809 842
810 843 var formatRepoResult = function(result, container, query, escapeMarkup) {
811 844 return function(data, escapeMarkup) {
812 845 if (!data.repo_id){
813 846 return data.text; // optgroup text Repositories
814 847 }
815 848
816 849 var tmpl = '';
817 850 var repoType = data['repo_type'];
818 851 var repoName = data['text'];
819 852
820 853 if(data && data.type == 'repo'){
821 854 if(repoType === 'hg'){
822 855 tmpl += '<i class="icon-hg"></i> ';
823 856 }
824 857 else if(repoType === 'git'){
825 858 tmpl += '<i class="icon-git"></i> ';
826 859 }
827 860 else if(repoType === 'svn'){
828 861 tmpl += '<i class="icon-svn"></i> ';
829 862 }
830 863 if(data['private']){
831 864 tmpl += '<i class="icon-lock" ></i> ';
832 865 }
833 866 else if(visualShowPublicIcon){
834 867 tmpl += '<i class="icon-unlock-alt"></i> ';
835 868 }
836 869 }
837 870 tmpl += escapeMarkup(repoName);
838 871 return tmpl;
839 872
840 873 }(result, escapeMarkup);
841 874 };
842 875
843 876 var formatRepoGroupResult = function(result, container, query, escapeMarkup) {
844 877 return function(data, escapeMarkup) {
845 878 if (!data.repo_group_id){
846 879 return data.text; // optgroup text Repositories
847 880 }
848 881
849 882 var tmpl = '';
850 883 var repoGroupName = data['text'];
851 884
852 885 if(data){
853 886
854 887 tmpl += '<i class="icon-repo-group"></i> ';
855 888
856 889 }
857 890 tmpl += escapeMarkup(repoGroupName);
858 891 return tmpl;
859 892
860 893 }(result, escapeMarkup);
861 894 };
862 895
863 896 var escapeRegExChars = function (value) {
864 897 return value.replace(/[\-\[\]\/\{\}\(\)\*\+\?\.\\\^\$\|]/g, "\\$&");
865 898 };
866 899
867 900 var getRepoIcon = function(repo_type) {
868 901 if (repo_type === 'hg') {
869 902 return '<i class="icon-hg"></i> ';
870 903 }
871 904 else if (repo_type === 'git') {
872 905 return '<i class="icon-git"></i> ';
873 906 }
874 907 else if (repo_type === 'svn') {
875 908 return '<i class="icon-svn"></i> ';
876 909 }
877 910 return ''
878 911 };
879 912
880 913 var autocompleteMainFilterFormatResult = function (data, value, org_formatter) {
881 914
882 915 if (value.split(':').length === 2) {
883 916 value = value.split(':')[1]
884 917 }
885 918
886 919 var searchType = data['type'];
887 920 var searchSubType = data['subtype'];
888 921 var valueDisplay = data['value_display'];
889 922 var valueIcon = data['value_icon'];
890 923
891 924 var pattern = '(' + escapeRegExChars(value) + ')';
892 925
893 926 valueDisplay = Select2.util.escapeMarkup(valueDisplay);
894 927
895 928 // highlight match
896 929 if (searchType != 'text') {
897 930 valueDisplay = valueDisplay.replace(new RegExp(pattern, 'gi'), '<strong>$1<\/strong>');
898 931 }
899 932
900 933 var icon = '';
901 934
902 935 if (searchType === 'hint') {
903 936 icon += '<i class="icon-repo-group"></i> ';
904 937 }
905 938 // full text search/hints
906 939 else if (searchType === 'search') {
907 940 if (valueIcon === undefined) {
908 941 icon += '<i class="icon-more"></i> ';
909 942 } else {
910 943 icon += valueIcon + ' ';
911 944 }
912 945
913 946 if (searchSubType !== undefined && searchSubType == 'repo') {
914 947 valueDisplay += '<div class="pull-right tag">repository</div>';
915 948 }
916 949 else if (searchSubType !== undefined && searchSubType == 'repo_group') {
917 950 valueDisplay += '<div class="pull-right tag">repo group</div>';
918 951 }
919 952 }
920 953 // repository
921 954 else if (searchType === 'repo') {
922 955
923 956 var repoIcon = getRepoIcon(data['repo_type']);
924 957 icon += repoIcon;
925 958
926 959 if (data['private']) {
927 960 icon += '<i class="icon-lock" ></i> ';
928 961 }
929 962 else if (visualShowPublicIcon) {
930 963 icon += '<i class="icon-unlock-alt"></i> ';
931 964 }
932 965 }
933 966 // repository groups
934 967 else if (searchType === 'repo_group') {
935 968 icon += '<i class="icon-repo-group"></i> ';
936 969 }
937 970 // user group
938 971 else if (searchType === 'user_group') {
939 972 icon += '<i class="icon-group"></i> ';
940 973 }
941 974 // user
942 975 else if (searchType === 'user') {
943 976 icon += '<img class="gravatar" src="{0}"/>'.format(data['icon_link']);
944 977 }
945 978 // commit
946 979 else if (searchType === 'commit') {
947 980 var repo_data = data['repo_data'];
948 981 var repoIcon = getRepoIcon(repo_data['repository_type']);
949 982 if (repoIcon) {
950 983 icon += repoIcon;
951 984 } else {
952 985 icon += '<i class="icon-tag"></i>';
953 986 }
954 987 }
955 988 // file
956 989 else if (searchType === 'file') {
957 990 var repo_data = data['repo_data'];
958 991 var repoIcon = getRepoIcon(repo_data['repository_type']);
959 992 if (repoIcon) {
960 993 icon += repoIcon;
961 994 } else {
962 995 icon += '<i class="icon-tag"></i>';
963 996 }
964 997 }
965 998 // generic text
966 999 else if (searchType === 'text') {
967 1000 icon = '';
968 1001 }
969 1002
970 1003 var tmpl = '<div class="ac-container-wrap">{0}{1}</div>';
971 1004 return tmpl.format(icon, valueDisplay);
972 1005 };
973 1006
974 1007 var handleSelect = function(element, suggestion) {
975 1008 if (suggestion.type === "hint") {
976 1009 // we skip action
977 1010 $('#main_filter').focus();
978 1011 }
979 1012 else if (suggestion.type === "text") {
980 1013 // we skip action
981 1014 $('#main_filter').focus();
982 1015
983 1016 } else {
984 1017 window.location = suggestion['url'];
985 1018 }
986 1019 };
987 1020
988 1021 var autocompleteMainFilterResult = function (suggestion, originalQuery, queryLowerCase) {
989 1022 if (queryLowerCase.split(':').length === 2) {
990 1023 queryLowerCase = queryLowerCase.split(':')[1]
991 1024 }
992 1025 if (suggestion.type === "text") {
993 1026 // special case we don't want to "skip" display for
994 1027 return true
995 1028 }
996 1029 return suggestion.value_display.toLowerCase().indexOf(queryLowerCase) !== -1;
997 1030 };
998 1031
999 1032 var cleanContext = {
1000 1033 repo_view_type: null,
1001 1034
1002 1035 repo_id: null,
1003 1036 repo_name: "",
1004 1037
1005 1038 repo_group_id: null,
1006 1039 repo_group_name: null
1007 1040 };
1008 1041 var removeGoToFilter = function () {
1009 1042 $('.searchTagHidable').hide();
1010 1043 $('#main_filter').autocomplete(
1011 1044 'setOptions', {params:{search_context: cleanContext}});
1012 1045 };
1013 1046
1014 1047 $('#main_filter').autocomplete({
1015 1048 serviceUrl: pyroutes.url('goto_switcher_data'),
1016 1049 params: {
1017 1050 "search_context": templateContext.search_context
1018 1051 },
1019 1052 minChars:2,
1020 1053 maxHeight:400,
1021 1054 deferRequestBy: 300, //miliseconds
1022 1055 tabDisabled: true,
1023 1056 autoSelectFirst: false,
1024 1057 containerClass: 'autocomplete-qfilter-suggestions',
1025 1058 formatResult: autocompleteMainFilterFormatResult,
1026 1059 lookupFilter: autocompleteMainFilterResult,
1027 1060 onSelect: function (element, suggestion) {
1028 1061 handleSelect(element, suggestion);
1029 1062 return false;
1030 1063 },
1031 1064 onSearchError: function (element, query, jqXHR, textStatus, errorThrown) {
1032 1065 if (jqXHR !== 'abort') {
1033 1066 alert("Error during search.\nError code: {0}".format(textStatus));
1034 1067 window.location = '';
1035 1068 }
1036 1069 },
1037 1070 onSearchStart: function (params) {
1038 1071 $('.searchTag.searchTagIcon').html('<i class="icon-spin animate-spin"></i>')
1039 1072 },
1040 1073 onSearchComplete: function (query, suggestions) {
1041 1074 $('.searchTag.searchTagIcon').html('<i class="icon-search"></i>')
1042 1075 },
1043 1076 });
1044 1077
1045 1078 showMainFilterBox = function () {
1046 1079 $('#main_filter_help').toggle();
1047 1080 };
1048 1081
1049 1082 $('#main_filter').on('keydown.autocomplete', function (e) {
1050 1083
1051 1084 var BACKSPACE = 8;
1052 1085 var el = $(e.currentTarget);
1053 1086 if(e.which === BACKSPACE){
1054 1087 var inputVal = el.val();
1055 1088 if (inputVal === ""){
1056 1089 removeGoToFilter()
1057 1090 }
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>
1064 1117
1065 1118 <div class="modal" id="help_kb" tabindex="-1" role="dialog" aria-labelledby="myModalLabel" aria-hidden="true">
1066 1119 <div class="modal-dialog">
1067 1120 <div class="modal-content">
1068 1121 <div class="modal-header">
1069 1122 <button type="button" class="close" data-dismiss="modal" aria-hidden="true">&times;</button>
1070 1123 <h4 class="modal-title" id="myModalLabel">${_('Keyboard shortcuts')}</h4>
1071 1124 </div>
1072 1125 <div class="modal-body">
1073 1126 <div class="block-left">
1074 1127 <table class="keyboard-mappings">
1075 1128 <tbody>
1076 1129 <tr>
1077 1130 <th></th>
1078 1131 <th>${_('Site-wide shortcuts')}</th>
1079 1132 </tr>
1080 1133 <%
1081 1134 elems = [
1082 1135 ('/', 'Use quick search box'),
1083 1136 ('g h', 'Goto home page'),
1084 1137 ('g g', 'Goto my private gists page'),
1085 1138 ('g G', 'Goto my public gists page'),
1086 1139 ('g 0-9', 'Goto bookmarked items from 0-9'),
1087 1140 ('n r', 'New repository page'),
1088 1141 ('n g', 'New gist page'),
1089 1142 ]
1090 1143 %>
1091 1144 %for key, desc in elems:
1092 1145 <tr>
1093 1146 <td class="keys">
1094 1147 <span class="key tag">${key}</span>
1095 1148 </td>
1096 1149 <td>${desc}</td>
1097 1150 </tr>
1098 1151 %endfor
1099 1152 </tbody>
1100 1153 </table>
1101 1154 </div>
1102 1155 <div class="block-left">
1103 1156 <table class="keyboard-mappings">
1104 1157 <tbody>
1105 1158 <tr>
1106 1159 <th></th>
1107 1160 <th>${_('Repositories')}</th>
1108 1161 </tr>
1109 1162 <%
1110 1163 elems = [
1111 1164 ('g s', 'Goto summary page'),
1112 1165 ('g c', 'Goto changelog page'),
1113 1166 ('g f', 'Goto files page'),
1114 1167 ('g F', 'Goto files page with file search activated'),
1115 1168 ('g p', 'Goto pull requests page'),
1116 1169 ('g o', 'Goto repository settings'),
1117 1170 ('g O', 'Goto repository access permissions settings'),
1118 1171 ]
1119 1172 %>
1120 1173 %for key, desc in elems:
1121 1174 <tr>
1122 1175 <td class="keys">
1123 1176 <span class="key tag">${key}</span>
1124 1177 </td>
1125 1178 <td>${desc}</td>
1126 1179 </tr>
1127 1180 %endfor
1128 1181 </tbody>
1129 1182 </table>
1130 1183 </div>
1131 1184 </div>
1132 1185 <div class="modal-footer">
1133 1186 </div>
1134 1187 </div><!-- /.modal-content -->
1135 1188 </div><!-- /.modal-dialog -->
1136 1189 </div><!-- /.modal -->
@@ -1,165 +1,166 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <!DOCTYPE html>
3 3
4 4 <%
5 5 c.template_context['repo_name'] = getattr(c, 'repo_name', '')
6 6 go_import_header = ''
7 7 if hasattr(c, 'rhodecode_db_repo'):
8 8 c.template_context['repo_type'] = c.rhodecode_db_repo.repo_type
9 9 c.template_context['repo_landing_commit'] = c.rhodecode_db_repo.landing_rev[1]
10 10 c.template_context['repo_id'] = c.rhodecode_db_repo.repo_id
11 11 c.template_context['repo_view_type'] = h.get_repo_view_type(request)
12 12
13 13 if getattr(c, 'repo_group', None):
14 14 c.template_context['repo_group_id'] = c.repo_group.group_id
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)
21 22 c.template_context['rhodecode_user']['first_name'] = c.rhodecode_user.first_name
22 23 c.template_context['rhodecode_user']['last_name'] = c.rhodecode_user.last_name
23 24
24 25 c.template_context['visual']['default_renderer'] = h.get_visual_attr(c, 'default_renderer')
25 26 c.template_context['default_user'] = {
26 27 'username': h.DEFAULT_USER,
27 28 'user_id': 1
28 29 }
29 30 c.template_context['search_context'] = {
30 31 'repo_group_id': c.template_context.get('repo_group_id'),
31 32 'repo_group_name': c.template_context.get('repo_group_name'),
32 33 'repo_id': c.template_context.get('repo_id'),
33 34 'repo_name': c.template_context.get('repo_name'),
34 35 'repo_view_type': c.template_context.get('repo_view_type'),
35 36 }
36 37
37 38 c.template_context['attachment_store'] = {
38 39 'max_file_size_mb': 10,
39 40 'image_ext': ["png", "jpg", "gif", "jpeg"]
40 41 }
41 42
42 43 %>
43 44 <html xmlns="http://www.w3.org/1999/xhtml">
44 45 <head>
45 46 <title>${self.title()}</title>
46 47 <meta http-equiv="Content-Type" content="text/html;charset=utf-8" />
47 48
48 49 ${h.go_import_header(request, getattr(c, 'rhodecode_db_repo', None))}
49 50
50 51 % if 'safari' in (request.user_agent or '').lower():
51 52 <meta name="referrer" content="origin">
52 53 % else:
53 54 <meta name="referrer" content="origin-when-cross-origin">
54 55 % endif
55 56
56 57 <%def name="robots()">
57 58 <meta name="robots" content="index, nofollow"/>
58 59 </%def>
59 60 ${self.robots()}
60 61 <link rel="icon" href="${h.asset('images/favicon.ico', ver=c.rhodecode_version_hash)}" sizes="16x16 32x32" type="image/png" />
61 62 <script src="${h.asset('js/vendors/webcomponentsjs/custom-elements-es5-adapter.js', ver=c.rhodecode_version_hash)}"></script>
62 63 <script src="${h.asset('js/vendors/webcomponentsjs/webcomponents-bundle.js', ver=c.rhodecode_version_hash)}"></script>
63 64
64 65 ## CSS definitions
65 66 <%def name="css()">
66 67 <link rel="stylesheet" type="text/css" href="${h.asset('css/style.css', ver=c.rhodecode_version_hash)}" media="screen"/>
67 68 ## EXTRA FOR CSS
68 69 ${self.css_extra()}
69 70 </%def>
70 71 ## CSS EXTRA - optionally inject some extra CSS stuff needed for specific websites
71 72 <%def name="css_extra()">
72 73 </%def>
73 74
74 75 ${self.css()}
75 76
76 77 ## JAVASCRIPT
77 78 <%def name="js()">
78 79
79 80 <script src="${h.asset('js/rhodecode/i18n/%s.js' % c.language, ver=c.rhodecode_version_hash)}"></script>
80 81 <script type="text/javascript">
81 82 // register templateContext to pass template variables to JS
82 83 var templateContext = ${h.json.dumps(c.template_context)|n};
83 84
84 85 var APPLICATION_URL = "${h.route_path('home').rstrip('/')}";
85 86 var APPLICATION_PLUGINS = [];
86 87 var ASSET_URL = "${h.asset('')}";
87 88 var DEFAULT_RENDERER = "${h.get_visual_attr(c, 'default_renderer')}";
88 89 var CSRF_TOKEN = "${getattr(c, 'csrf_token', '')}";
89 90
90 91 var APPENLIGHT = {
91 92 enabled: ${'true' if getattr(c, 'appenlight_enabled', False) else 'false'},
92 93 key: '${getattr(c, "appenlight_api_public_key", "")}',
93 94 % if getattr(c, 'appenlight_server_url', None):
94 95 serverUrl: '${getattr(c, "appenlight_server_url", "")}',
95 96 % endif
96 97 requestInfo: {
97 98 % if getattr(c, 'rhodecode_user', None):
98 99 ip: '${c.rhodecode_user.ip_addr}',
99 100 username: '${c.rhodecode_user.username}'
100 101 % endif
101 102 },
102 103 tags: {
103 104 rhodecode_version: '${c.rhodecode_version}',
104 105 rhodecode_edition: '${c.rhodecode_edition}'
105 106 }
106 107 };
107 108
108 109 </script>
109 110 <%include file="/base/plugins_base.mako"/>
110 111 <!--[if lt IE 9]>
111 112 <script language="javascript" type="text/javascript" src="${h.asset('js/src/excanvas.min.js')}"></script>
112 113 <![endif]-->
113 114 <script language="javascript" type="text/javascript" src="${h.asset('js/rhodecode/routes.js', ver=c.rhodecode_version_hash)}"></script>
114 115 <script> var alertMessagePayloads = ${h.flash.json_alerts(request=request)|n}; </script>
115 116 ## avoide escaping the %N
116 117 <script language="javascript" type="text/javascript" src="${h.asset('js/scripts.min.js', ver=c.rhodecode_version_hash)}"></script>
117 118 <script>CodeMirror.modeURL = "${h.asset('') + 'js/mode/%N/%N.js?ver='+c.rhodecode_version_hash}";</script>
118 119
119 120
120 121 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
121 122 ${self.js_extra()}
122 123
123 124 <script type="text/javascript">
124 125 Rhodecode = (function() {
125 126 function _Rhodecode() {
126 127 this.comments = new CommentsController();
127 128 }
128 129 return new _Rhodecode();
129 130 })();
130 131
131 132 $(document).ready(function(){
132 133 show_more_event();
133 134 timeagoActivate();
134 135 tooltipActivate();
135 136 clipboardActivate();
136 137 })
137 138 </script>
138 139
139 140 </%def>
140 141
141 142 ## JAVASCRIPT EXTRA - optionally inject some extra JS for specificed templates
142 143 <%def name="js_extra()"></%def>
143 144 ${self.js()}
144 145
145 146 <%def name="head_extra()"></%def>
146 147 ${self.head_extra()}
147 148 ## extra stuff
148 149 %if c.pre_code:
149 150 ${c.pre_code|n}
150 151 %endif
151 152 </head>
152 153 <body id="body">
153 154 <noscript>
154 155 <div class="noscript-error">
155 156 ${_('Please enable JavaScript to use RhodeCode Enterprise')}
156 157 </div>
157 158 </noscript>
158 159
159 160 ${next.body()}
160 161 %if c.post_code:
161 162 ${c.post_code|n}
162 163 %endif
163 164 <rhodecode-app></rhodecode-app>
164 165 </body>
165 166 </html>
General Comments 0
You need to be logged in to leave comments. Login now