##// END OF EJS Templates
integrations: refactor/cleanup + features, fixes #4181...
dan -
r731:7a6d3636 default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (3529 lines changed) Show them Hide them
@@ -0,0 +1,3529 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 """
22 Database Models for RhodeCode Enterprise
23 """
24
25 import os
26 import sys
27 import time
28 import hashlib
29 import logging
30 import datetime
31 import warnings
32 import ipaddress
33 import functools
34 import traceback
35 import collections
36
37
38 from sqlalchemy import *
39 from sqlalchemy.exc import IntegrityError
40 from sqlalchemy.ext.declarative import declared_attr
41 from sqlalchemy.ext.hybrid import hybrid_property
42 from sqlalchemy.orm import (
43 relationship, joinedload, class_mapper, validates, aliased)
44 from sqlalchemy.sql.expression import true
45 from beaker.cache import cache_region, region_invalidate
46 from webob.exc import HTTPNotFound
47 from zope.cachedescriptors.property import Lazy as LazyProperty
48
49 from pylons import url
50 from pylons.i18n.translation import lazy_ugettext as _
51
52 from rhodecode.lib.vcs import get_backend, get_vcs_instance
53 from rhodecode.lib.vcs.utils.helpers import get_scm
54 from rhodecode.lib.vcs.exceptions import VCSError
55 from rhodecode.lib.vcs.backends.base import (
56 EmptyCommit, Reference, MergeFailureReason)
57 from rhodecode.lib.utils2 import (
58 str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
59 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict)
60 from rhodecode.lib.jsonalchemy import MutationObj, JsonType, JSONDict
61 from rhodecode.lib.ext_json import json
62 from rhodecode.lib.caching_query import FromCache
63 from rhodecode.lib.encrypt import AESCipher
64
65 from rhodecode.model.meta import Base, Session
66
67 URL_SEP = '/'
68 log = logging.getLogger(__name__)
69
70 # =============================================================================
71 # BASE CLASSES
72 # =============================================================================
73
74 # this is propagated from .ini file rhodecode.encrypted_values.secret or
75 # beaker.session.secret if first is not set.
76 # and initialized at environment.py
77 ENCRYPTION_KEY = None
78
79 # used to sort permissions by types, '#' used here is not allowed to be in
80 # usernames, and it's very early in sorted string.printable table.
81 PERMISSION_TYPE_SORT = {
82 'admin': '####',
83 'write': '###',
84 'read': '##',
85 'none': '#',
86 }
87
88
89 def display_sort(obj):
90 """
91 Sort function used to sort permissions in .permissions() function of
92 Repository, RepoGroup, UserGroup. Also it put the default user in front
93 of all other resources
94 """
95
96 if obj.username == User.DEFAULT_USER:
97 return '#####'
98 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
99 return prefix + obj.username
100
101
102 def _hash_key(k):
103 return md5_safe(k)
104
105
106 class EncryptedTextValue(TypeDecorator):
107 """
108 Special column for encrypted long text data, use like::
109
110 value = Column("encrypted_value", EncryptedValue(), nullable=False)
111
112 This column is intelligent so if value is in unencrypted form it return
113 unencrypted form, but on save it always encrypts
114 """
115 impl = Text
116
117 def process_bind_param(self, value, dialect):
118 if not value:
119 return value
120 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
121 # protect against double encrypting if someone manually starts
122 # doing
123 raise ValueError('value needs to be in unencrypted format, ie. '
124 'not starting with enc$aes')
125 return 'enc$aes_hmac$%s' % AESCipher(
126 ENCRYPTION_KEY, hmac=True).encrypt(value)
127
128 def process_result_value(self, value, dialect):
129 import rhodecode
130
131 if not value:
132 return value
133
134 parts = value.split('$', 3)
135 if not len(parts) == 3:
136 # probably not encrypted values
137 return value
138 else:
139 if parts[0] != 'enc':
140 # parts ok but without our header ?
141 return value
142 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
143 'rhodecode.encrypted_values.strict') or True)
144 # at that stage we know it's our encryption
145 if parts[1] == 'aes':
146 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
147 elif parts[1] == 'aes_hmac':
148 decrypted_data = AESCipher(
149 ENCRYPTION_KEY, hmac=True,
150 strict_verification=enc_strict_mode).decrypt(parts[2])
151 else:
152 raise ValueError(
153 'Encryption type part is wrong, must be `aes` '
154 'or `aes_hmac`, got `%s` instead' % (parts[1]))
155 return decrypted_data
156
157
158 class BaseModel(object):
159 """
160 Base Model for all classes
161 """
162
163 @classmethod
164 def _get_keys(cls):
165 """return column names for this model """
166 return class_mapper(cls).c.keys()
167
168 def get_dict(self):
169 """
170 return dict with keys and values corresponding
171 to this model data """
172
173 d = {}
174 for k in self._get_keys():
175 d[k] = getattr(self, k)
176
177 # also use __json__() if present to get additional fields
178 _json_attr = getattr(self, '__json__', None)
179 if _json_attr:
180 # update with attributes from __json__
181 if callable(_json_attr):
182 _json_attr = _json_attr()
183 for k, val in _json_attr.iteritems():
184 d[k] = val
185 return d
186
187 def get_appstruct(self):
188 """return list with keys and values tuples corresponding
189 to this model data """
190
191 l = []
192 for k in self._get_keys():
193 l.append((k, getattr(self, k),))
194 return l
195
196 def populate_obj(self, populate_dict):
197 """populate model with data from given populate_dict"""
198
199 for k in self._get_keys():
200 if k in populate_dict:
201 setattr(self, k, populate_dict[k])
202
203 @classmethod
204 def query(cls):
205 return Session().query(cls)
206
207 @classmethod
208 def get(cls, id_):
209 if id_:
210 return cls.query().get(id_)
211
212 @classmethod
213 def get_or_404(cls, id_):
214 try:
215 id_ = int(id_)
216 except (TypeError, ValueError):
217 raise HTTPNotFound
218
219 res = cls.query().get(id_)
220 if not res:
221 raise HTTPNotFound
222 return res
223
224 @classmethod
225 def getAll(cls):
226 # deprecated and left for backward compatibility
227 return cls.get_all()
228
229 @classmethod
230 def get_all(cls):
231 return cls.query().all()
232
233 @classmethod
234 def delete(cls, id_):
235 obj = cls.query().get(id_)
236 Session().delete(obj)
237
238 @classmethod
239 def identity_cache(cls, session, attr_name, value):
240 exist_in_session = []
241 for (item_cls, pkey), instance in session.identity_map.items():
242 if cls == item_cls and getattr(instance, attr_name) == value:
243 exist_in_session.append(instance)
244 if exist_in_session:
245 if len(exist_in_session) == 1:
246 return exist_in_session[0]
247 log.exception(
248 'multiple objects with attr %s and '
249 'value %s found with same name: %r',
250 attr_name, value, exist_in_session)
251
252 def __repr__(self):
253 if hasattr(self, '__unicode__'):
254 # python repr needs to return str
255 try:
256 return safe_str(self.__unicode__())
257 except UnicodeDecodeError:
258 pass
259 return '<DB:%s>' % (self.__class__.__name__)
260
261
262 class RhodeCodeSetting(Base, BaseModel):
263 __tablename__ = 'rhodecode_settings'
264 __table_args__ = (
265 UniqueConstraint('app_settings_name'),
266 {'extend_existing': True, 'mysql_engine': 'InnoDB',
267 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
268 )
269
270 SETTINGS_TYPES = {
271 'str': safe_str,
272 'int': safe_int,
273 'unicode': safe_unicode,
274 'bool': str2bool,
275 'list': functools.partial(aslist, sep=',')
276 }
277 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
278 GLOBAL_CONF_KEY = 'app_settings'
279
280 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
281 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
282 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
283 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
284
285 def __init__(self, key='', val='', type='unicode'):
286 self.app_settings_name = key
287 self.app_settings_type = type
288 self.app_settings_value = val
289
290 @validates('_app_settings_value')
291 def validate_settings_value(self, key, val):
292 assert type(val) == unicode
293 return val
294
295 @hybrid_property
296 def app_settings_value(self):
297 v = self._app_settings_value
298 _type = self.app_settings_type
299 if _type:
300 _type = self.app_settings_type.split('.')[0]
301 # decode the encrypted value
302 if 'encrypted' in self.app_settings_type:
303 cipher = EncryptedTextValue()
304 v = safe_unicode(cipher.process_result_value(v, None))
305
306 converter = self.SETTINGS_TYPES.get(_type) or \
307 self.SETTINGS_TYPES['unicode']
308 return converter(v)
309
310 @app_settings_value.setter
311 def app_settings_value(self, val):
312 """
313 Setter that will always make sure we use unicode in app_settings_value
314
315 :param val:
316 """
317 val = safe_unicode(val)
318 # encode the encrypted value
319 if 'encrypted' in self.app_settings_type:
320 cipher = EncryptedTextValue()
321 val = safe_unicode(cipher.process_bind_param(val, None))
322 self._app_settings_value = val
323
324 @hybrid_property
325 def app_settings_type(self):
326 return self._app_settings_type
327
328 @app_settings_type.setter
329 def app_settings_type(self, val):
330 if val.split('.')[0] not in self.SETTINGS_TYPES:
331 raise Exception('type must be one of %s got %s'
332 % (self.SETTINGS_TYPES.keys(), val))
333 self._app_settings_type = val
334
335 def __unicode__(self):
336 return u"<%s('%s:%s[%s]')>" % (
337 self.__class__.__name__,
338 self.app_settings_name, self.app_settings_value,
339 self.app_settings_type
340 )
341
342
343 class RhodeCodeUi(Base, BaseModel):
344 __tablename__ = 'rhodecode_ui'
345 __table_args__ = (
346 UniqueConstraint('ui_key'),
347 {'extend_existing': True, 'mysql_engine': 'InnoDB',
348 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
349 )
350
351 HOOK_REPO_SIZE = 'changegroup.repo_size'
352 # HG
353 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
354 HOOK_PULL = 'outgoing.pull_logger'
355 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
356 HOOK_PUSH = 'changegroup.push_logger'
357
358 # TODO: johbo: Unify way how hooks are configured for git and hg,
359 # git part is currently hardcoded.
360
361 # SVN PATTERNS
362 SVN_BRANCH_ID = 'vcs_svn_branch'
363 SVN_TAG_ID = 'vcs_svn_tag'
364
365 ui_id = Column(
366 "ui_id", Integer(), nullable=False, unique=True, default=None,
367 primary_key=True)
368 ui_section = Column(
369 "ui_section", String(255), nullable=True, unique=None, default=None)
370 ui_key = Column(
371 "ui_key", String(255), nullable=True, unique=None, default=None)
372 ui_value = Column(
373 "ui_value", String(255), nullable=True, unique=None, default=None)
374 ui_active = Column(
375 "ui_active", Boolean(), nullable=True, unique=None, default=True)
376
377 def __repr__(self):
378 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
379 self.ui_key, self.ui_value)
380
381
382 class RepoRhodeCodeSetting(Base, BaseModel):
383 __tablename__ = 'repo_rhodecode_settings'
384 __table_args__ = (
385 UniqueConstraint(
386 'app_settings_name', 'repository_id',
387 name='uq_repo_rhodecode_setting_name_repo_id'),
388 {'extend_existing': True, 'mysql_engine': 'InnoDB',
389 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
390 )
391
392 repository_id = Column(
393 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
394 nullable=False)
395 app_settings_id = Column(
396 "app_settings_id", Integer(), nullable=False, unique=True,
397 default=None, primary_key=True)
398 app_settings_name = Column(
399 "app_settings_name", String(255), nullable=True, unique=None,
400 default=None)
401 _app_settings_value = Column(
402 "app_settings_value", String(4096), nullable=True, unique=None,
403 default=None)
404 _app_settings_type = Column(
405 "app_settings_type", String(255), nullable=True, unique=None,
406 default=None)
407
408 repository = relationship('Repository')
409
410 def __init__(self, repository_id, key='', val='', type='unicode'):
411 self.repository_id = repository_id
412 self.app_settings_name = key
413 self.app_settings_type = type
414 self.app_settings_value = val
415
416 @validates('_app_settings_value')
417 def validate_settings_value(self, key, val):
418 assert type(val) == unicode
419 return val
420
421 @hybrid_property
422 def app_settings_value(self):
423 v = self._app_settings_value
424 type_ = self.app_settings_type
425 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
426 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
427 return converter(v)
428
429 @app_settings_value.setter
430 def app_settings_value(self, val):
431 """
432 Setter that will always make sure we use unicode in app_settings_value
433
434 :param val:
435 """
436 self._app_settings_value = safe_unicode(val)
437
438 @hybrid_property
439 def app_settings_type(self):
440 return self._app_settings_type
441
442 @app_settings_type.setter
443 def app_settings_type(self, val):
444 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
445 if val not in SETTINGS_TYPES:
446 raise Exception('type must be one of %s got %s'
447 % (SETTINGS_TYPES.keys(), val))
448 self._app_settings_type = val
449
450 def __unicode__(self):
451 return u"<%s('%s:%s:%s[%s]')>" % (
452 self.__class__.__name__, self.repository.repo_name,
453 self.app_settings_name, self.app_settings_value,
454 self.app_settings_type
455 )
456
457
458 class RepoRhodeCodeUi(Base, BaseModel):
459 __tablename__ = 'repo_rhodecode_ui'
460 __table_args__ = (
461 UniqueConstraint(
462 'repository_id', 'ui_section', 'ui_key',
463 name='uq_repo_rhodecode_ui_repository_id_section_key'),
464 {'extend_existing': True, 'mysql_engine': 'InnoDB',
465 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
466 )
467
468 repository_id = Column(
469 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
470 nullable=False)
471 ui_id = Column(
472 "ui_id", Integer(), nullable=False, unique=True, default=None,
473 primary_key=True)
474 ui_section = Column(
475 "ui_section", String(255), nullable=True, unique=None, default=None)
476 ui_key = Column(
477 "ui_key", String(255), nullable=True, unique=None, default=None)
478 ui_value = Column(
479 "ui_value", String(255), nullable=True, unique=None, default=None)
480 ui_active = Column(
481 "ui_active", Boolean(), nullable=True, unique=None, default=True)
482
483 repository = relationship('Repository')
484
485 def __repr__(self):
486 return '<%s[%s:%s]%s=>%s]>' % (
487 self.__class__.__name__, self.repository.repo_name,
488 self.ui_section, self.ui_key, self.ui_value)
489
490
491 class User(Base, BaseModel):
492 __tablename__ = 'users'
493 __table_args__ = (
494 UniqueConstraint('username'), UniqueConstraint('email'),
495 Index('u_username_idx', 'username'),
496 Index('u_email_idx', 'email'),
497 {'extend_existing': True, 'mysql_engine': 'InnoDB',
498 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
499 )
500 DEFAULT_USER = 'default'
501 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
502 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
503
504 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
505 username = Column("username", String(255), nullable=True, unique=None, default=None)
506 password = Column("password", String(255), nullable=True, unique=None, default=None)
507 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
508 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
509 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
510 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
511 _email = Column("email", String(255), nullable=True, unique=None, default=None)
512 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
513 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
514 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
515 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
516 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
517 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
518 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
519
520 user_log = relationship('UserLog')
521 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
522
523 repositories = relationship('Repository')
524 repository_groups = relationship('RepoGroup')
525 user_groups = relationship('UserGroup')
526
527 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
528 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
529
530 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
531 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
532 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
533
534 group_member = relationship('UserGroupMember', cascade='all')
535
536 notifications = relationship('UserNotification', cascade='all')
537 # notifications assigned to this user
538 user_created_notifications = relationship('Notification', cascade='all')
539 # comments created by this user
540 user_comments = relationship('ChangesetComment', cascade='all')
541 # user profile extra info
542 user_emails = relationship('UserEmailMap', cascade='all')
543 user_ip_map = relationship('UserIpMap', cascade='all')
544 user_auth_tokens = relationship('UserApiKeys', cascade='all')
545 # gists
546 user_gists = relationship('Gist', cascade='all')
547 # user pull requests
548 user_pull_requests = relationship('PullRequest', cascade='all')
549 # external identities
550 extenal_identities = relationship(
551 'ExternalIdentity',
552 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
553 cascade='all')
554
555 def __unicode__(self):
556 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
557 self.user_id, self.username)
558
559 @hybrid_property
560 def email(self):
561 return self._email
562
563 @email.setter
564 def email(self, val):
565 self._email = val.lower() if val else None
566
567 @property
568 def firstname(self):
569 # alias for future
570 return self.name
571
572 @property
573 def emails(self):
574 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
575 return [self.email] + [x.email for x in other]
576
577 @property
578 def auth_tokens(self):
579 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
580
581 @property
582 def extra_auth_tokens(self):
583 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
584
585 @property
586 def feed_token(self):
587 feed_tokens = UserApiKeys.query()\
588 .filter(UserApiKeys.user == self)\
589 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
590 .all()
591 if feed_tokens:
592 return feed_tokens[0].api_key
593 else:
594 # use the main token so we don't end up with nothing...
595 return self.api_key
596
597 @classmethod
598 def extra_valid_auth_tokens(cls, user, role=None):
599 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
600 .filter(or_(UserApiKeys.expires == -1,
601 UserApiKeys.expires >= time.time()))
602 if role:
603 tokens = tokens.filter(or_(UserApiKeys.role == role,
604 UserApiKeys.role == UserApiKeys.ROLE_ALL))
605 return tokens.all()
606
607 @property
608 def ip_addresses(self):
609 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
610 return [x.ip_addr for x in ret]
611
612 @property
613 def username_and_name(self):
614 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
615
616 @property
617 def username_or_name_or_email(self):
618 full_name = self.full_name if self.full_name is not ' ' else None
619 return self.username or full_name or self.email
620
621 @property
622 def full_name(self):
623 return '%s %s' % (self.firstname, self.lastname)
624
625 @property
626 def full_name_or_username(self):
627 return ('%s %s' % (self.firstname, self.lastname)
628 if (self.firstname and self.lastname) else self.username)
629
630 @property
631 def full_contact(self):
632 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
633
634 @property
635 def short_contact(self):
636 return '%s %s' % (self.firstname, self.lastname)
637
638 @property
639 def is_admin(self):
640 return self.admin
641
642 @property
643 def AuthUser(self):
644 """
645 Returns instance of AuthUser for this user
646 """
647 from rhodecode.lib.auth import AuthUser
648 return AuthUser(user_id=self.user_id, api_key=self.api_key,
649 username=self.username)
650
651 @hybrid_property
652 def user_data(self):
653 if not self._user_data:
654 return {}
655
656 try:
657 return json.loads(self._user_data)
658 except TypeError:
659 return {}
660
661 @user_data.setter
662 def user_data(self, val):
663 if not isinstance(val, dict):
664 raise Exception('user_data must be dict, got %s' % type(val))
665 try:
666 self._user_data = json.dumps(val)
667 except Exception:
668 log.error(traceback.format_exc())
669
670 @classmethod
671 def get_by_username(cls, username, case_insensitive=False,
672 cache=False, identity_cache=False):
673 session = Session()
674
675 if case_insensitive:
676 q = cls.query().filter(
677 func.lower(cls.username) == func.lower(username))
678 else:
679 q = cls.query().filter(cls.username == username)
680
681 if cache:
682 if identity_cache:
683 val = cls.identity_cache(session, 'username', username)
684 if val:
685 return val
686 else:
687 q = q.options(
688 FromCache("sql_cache_short",
689 "get_user_by_name_%s" % _hash_key(username)))
690
691 return q.scalar()
692
693 @classmethod
694 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
695 q = cls.query().filter(cls.api_key == auth_token)
696
697 if cache:
698 q = q.options(FromCache("sql_cache_short",
699 "get_auth_token_%s" % auth_token))
700 res = q.scalar()
701
702 if fallback and not res:
703 #fallback to additional keys
704 _res = UserApiKeys.query()\
705 .filter(UserApiKeys.api_key == auth_token)\
706 .filter(or_(UserApiKeys.expires == -1,
707 UserApiKeys.expires >= time.time()))\
708 .first()
709 if _res:
710 res = _res.user
711 return res
712
713 @classmethod
714 def get_by_email(cls, email, case_insensitive=False, cache=False):
715
716 if case_insensitive:
717 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
718
719 else:
720 q = cls.query().filter(cls.email == email)
721
722 if cache:
723 q = q.options(FromCache("sql_cache_short",
724 "get_email_key_%s" % _hash_key(email)))
725
726 ret = q.scalar()
727 if ret is None:
728 q = UserEmailMap.query()
729 # try fetching in alternate email map
730 if case_insensitive:
731 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
732 else:
733 q = q.filter(UserEmailMap.email == email)
734 q = q.options(joinedload(UserEmailMap.user))
735 if cache:
736 q = q.options(FromCache("sql_cache_short",
737 "get_email_map_key_%s" % email))
738 ret = getattr(q.scalar(), 'user', None)
739
740 return ret
741
742 @classmethod
743 def get_from_cs_author(cls, author):
744 """
745 Tries to get User objects out of commit author string
746
747 :param author:
748 """
749 from rhodecode.lib.helpers import email, author_name
750 # Valid email in the attribute passed, see if they're in the system
751 _email = email(author)
752 if _email:
753 user = cls.get_by_email(_email, case_insensitive=True)
754 if user:
755 return user
756 # Maybe we can match by username?
757 _author = author_name(author)
758 user = cls.get_by_username(_author, case_insensitive=True)
759 if user:
760 return user
761
762 def update_userdata(self, **kwargs):
763 usr = self
764 old = usr.user_data
765 old.update(**kwargs)
766 usr.user_data = old
767 Session().add(usr)
768 log.debug('updated userdata with ', kwargs)
769
770 def update_lastlogin(self):
771 """Update user lastlogin"""
772 self.last_login = datetime.datetime.now()
773 Session().add(self)
774 log.debug('updated user %s lastlogin', self.username)
775
776 def update_lastactivity(self):
777 """Update user lastactivity"""
778 usr = self
779 old = usr.user_data
780 old.update({'last_activity': time.time()})
781 usr.user_data = old
782 Session().add(usr)
783 log.debug('updated user %s lastactivity', usr.username)
784
785 def update_password(self, new_password, change_api_key=False):
786 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
787
788 self.password = get_crypt_password(new_password)
789 if change_api_key:
790 self.api_key = generate_auth_token(self.username)
791 Session().add(self)
792
793 @classmethod
794 def get_first_super_admin(cls):
795 user = User.query().filter(User.admin == true()).first()
796 if user is None:
797 raise Exception('FATAL: Missing administrative account!')
798 return user
799
800 @classmethod
801 def get_all_super_admins(cls):
802 """
803 Returns all admin accounts sorted by username
804 """
805 return User.query().filter(User.admin == true())\
806 .order_by(User.username.asc()).all()
807
808 @classmethod
809 def get_default_user(cls, cache=False):
810 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
811 if user is None:
812 raise Exception('FATAL: Missing default account!')
813 return user
814
815 def _get_default_perms(self, user, suffix=''):
816 from rhodecode.model.permission import PermissionModel
817 return PermissionModel().get_default_perms(user.user_perms, suffix)
818
819 def get_default_perms(self, suffix=''):
820 return self._get_default_perms(self, suffix)
821
822 def get_api_data(self, include_secrets=False, details='full'):
823 """
824 Common function for generating user related data for API
825
826 :param include_secrets: By default secrets in the API data will be replaced
827 by a placeholder value to prevent exposing this data by accident. In case
828 this data shall be exposed, set this flag to ``True``.
829
830 :param details: details can be 'basic|full' basic gives only a subset of
831 the available user information that includes user_id, name and emails.
832 """
833 user = self
834 user_data = self.user_data
835 data = {
836 'user_id': user.user_id,
837 'username': user.username,
838 'firstname': user.name,
839 'lastname': user.lastname,
840 'email': user.email,
841 'emails': user.emails,
842 }
843 if details == 'basic':
844 return data
845
846 api_key_length = 40
847 api_key_replacement = '*' * api_key_length
848
849 extras = {
850 'api_key': api_key_replacement,
851 'api_keys': [api_key_replacement],
852 'active': user.active,
853 'admin': user.admin,
854 'extern_type': user.extern_type,
855 'extern_name': user.extern_name,
856 'last_login': user.last_login,
857 'ip_addresses': user.ip_addresses,
858 'language': user_data.get('language')
859 }
860 data.update(extras)
861
862 if include_secrets:
863 data['api_key'] = user.api_key
864 data['api_keys'] = user.auth_tokens
865 return data
866
867 def __json__(self):
868 data = {
869 'full_name': self.full_name,
870 'full_name_or_username': self.full_name_or_username,
871 'short_contact': self.short_contact,
872 'full_contact': self.full_contact,
873 }
874 data.update(self.get_api_data())
875 return data
876
877
878 class UserApiKeys(Base, BaseModel):
879 __tablename__ = 'user_api_keys'
880 __table_args__ = (
881 Index('uak_api_key_idx', 'api_key'),
882 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
883 UniqueConstraint('api_key'),
884 {'extend_existing': True, 'mysql_engine': 'InnoDB',
885 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
886 )
887 __mapper_args__ = {}
888
889 # ApiKey role
890 ROLE_ALL = 'token_role_all'
891 ROLE_HTTP = 'token_role_http'
892 ROLE_VCS = 'token_role_vcs'
893 ROLE_API = 'token_role_api'
894 ROLE_FEED = 'token_role_feed'
895 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
896
897 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
898 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
899 api_key = Column("api_key", String(255), nullable=False, unique=True)
900 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
901 expires = Column('expires', Float(53), nullable=False)
902 role = Column('role', String(255), nullable=True)
903 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
904
905 user = relationship('User', lazy='joined')
906
907 @classmethod
908 def _get_role_name(cls, role):
909 return {
910 cls.ROLE_ALL: _('all'),
911 cls.ROLE_HTTP: _('http/web interface'),
912 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
913 cls.ROLE_API: _('api calls'),
914 cls.ROLE_FEED: _('feed access'),
915 }.get(role, role)
916
917 @property
918 def expired(self):
919 if self.expires == -1:
920 return False
921 return time.time() > self.expires
922
923 @property
924 def role_humanized(self):
925 return self._get_role_name(self.role)
926
927
928 class UserEmailMap(Base, BaseModel):
929 __tablename__ = 'user_email_map'
930 __table_args__ = (
931 Index('uem_email_idx', 'email'),
932 UniqueConstraint('email'),
933 {'extend_existing': True, 'mysql_engine': 'InnoDB',
934 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
935 )
936 __mapper_args__ = {}
937
938 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
939 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
940 _email = Column("email", String(255), nullable=True, unique=False, default=None)
941 user = relationship('User', lazy='joined')
942
943 @validates('_email')
944 def validate_email(self, key, email):
945 # check if this email is not main one
946 main_email = Session().query(User).filter(User.email == email).scalar()
947 if main_email is not None:
948 raise AttributeError('email %s is present is user table' % email)
949 return email
950
951 @hybrid_property
952 def email(self):
953 return self._email
954
955 @email.setter
956 def email(self, val):
957 self._email = val.lower() if val else None
958
959
960 class UserIpMap(Base, BaseModel):
961 __tablename__ = 'user_ip_map'
962 __table_args__ = (
963 UniqueConstraint('user_id', 'ip_addr'),
964 {'extend_existing': True, 'mysql_engine': 'InnoDB',
965 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
966 )
967 __mapper_args__ = {}
968
969 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
970 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
971 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
972 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
973 description = Column("description", String(10000), nullable=True, unique=None, default=None)
974 user = relationship('User', lazy='joined')
975
976 @classmethod
977 def _get_ip_range(cls, ip_addr):
978 net = ipaddress.ip_network(ip_addr, strict=False)
979 return [str(net.network_address), str(net.broadcast_address)]
980
981 def __json__(self):
982 return {
983 'ip_addr': self.ip_addr,
984 'ip_range': self._get_ip_range(self.ip_addr),
985 }
986
987 def __unicode__(self):
988 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
989 self.user_id, self.ip_addr)
990
991 class UserLog(Base, BaseModel):
992 __tablename__ = 'user_logs'
993 __table_args__ = (
994 {'extend_existing': True, 'mysql_engine': 'InnoDB',
995 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
996 )
997 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
998 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
999 username = Column("username", String(255), nullable=True, unique=None, default=None)
1000 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1001 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1002 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1003 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1004 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1005
1006 def __unicode__(self):
1007 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1008 self.repository_name,
1009 self.action)
1010
1011 @property
1012 def action_as_day(self):
1013 return datetime.date(*self.action_date.timetuple()[:3])
1014
1015 user = relationship('User')
1016 repository = relationship('Repository', cascade='')
1017
1018
1019 class UserGroup(Base, BaseModel):
1020 __tablename__ = 'users_groups'
1021 __table_args__ = (
1022 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1023 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1024 )
1025
1026 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1027 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1028 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1029 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1030 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1031 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1032 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1033 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1034
1035 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1036 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1037 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1038 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1039 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1040 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1041
1042 user = relationship('User')
1043
1044 @hybrid_property
1045 def group_data(self):
1046 if not self._group_data:
1047 return {}
1048
1049 try:
1050 return json.loads(self._group_data)
1051 except TypeError:
1052 return {}
1053
1054 @group_data.setter
1055 def group_data(self, val):
1056 try:
1057 self._group_data = json.dumps(val)
1058 except Exception:
1059 log.error(traceback.format_exc())
1060
1061 def __unicode__(self):
1062 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1063 self.users_group_id,
1064 self.users_group_name)
1065
1066 @classmethod
1067 def get_by_group_name(cls, group_name, cache=False,
1068 case_insensitive=False):
1069 if case_insensitive:
1070 q = cls.query().filter(func.lower(cls.users_group_name) ==
1071 func.lower(group_name))
1072
1073 else:
1074 q = cls.query().filter(cls.users_group_name == group_name)
1075 if cache:
1076 q = q.options(FromCache(
1077 "sql_cache_short",
1078 "get_group_%s" % _hash_key(group_name)))
1079 return q.scalar()
1080
1081 @classmethod
1082 def get(cls, user_group_id, cache=False):
1083 user_group = cls.query()
1084 if cache:
1085 user_group = user_group.options(FromCache("sql_cache_short",
1086 "get_users_group_%s" % user_group_id))
1087 return user_group.get(user_group_id)
1088
1089 def permissions(self, with_admins=True, with_owner=True):
1090 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1091 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1092 joinedload(UserUserGroupToPerm.user),
1093 joinedload(UserUserGroupToPerm.permission),)
1094
1095 # get owners and admins and permissions. We do a trick of re-writing
1096 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1097 # has a global reference and changing one object propagates to all
1098 # others. This means if admin is also an owner admin_row that change
1099 # would propagate to both objects
1100 perm_rows = []
1101 for _usr in q.all():
1102 usr = AttributeDict(_usr.user.get_dict())
1103 usr.permission = _usr.permission.permission_name
1104 perm_rows.append(usr)
1105
1106 # filter the perm rows by 'default' first and then sort them by
1107 # admin,write,read,none permissions sorted again alphabetically in
1108 # each group
1109 perm_rows = sorted(perm_rows, key=display_sort)
1110
1111 _admin_perm = 'usergroup.admin'
1112 owner_row = []
1113 if with_owner:
1114 usr = AttributeDict(self.user.get_dict())
1115 usr.owner_row = True
1116 usr.permission = _admin_perm
1117 owner_row.append(usr)
1118
1119 super_admin_rows = []
1120 if with_admins:
1121 for usr in User.get_all_super_admins():
1122 # if this admin is also owner, don't double the record
1123 if usr.user_id == owner_row[0].user_id:
1124 owner_row[0].admin_row = True
1125 else:
1126 usr = AttributeDict(usr.get_dict())
1127 usr.admin_row = True
1128 usr.permission = _admin_perm
1129 super_admin_rows.append(usr)
1130
1131 return super_admin_rows + owner_row + perm_rows
1132
1133 def permission_user_groups(self):
1134 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1135 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1136 joinedload(UserGroupUserGroupToPerm.target_user_group),
1137 joinedload(UserGroupUserGroupToPerm.permission),)
1138
1139 perm_rows = []
1140 for _user_group in q.all():
1141 usr = AttributeDict(_user_group.user_group.get_dict())
1142 usr.permission = _user_group.permission.permission_name
1143 perm_rows.append(usr)
1144
1145 return perm_rows
1146
1147 def _get_default_perms(self, user_group, suffix=''):
1148 from rhodecode.model.permission import PermissionModel
1149 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1150
1151 def get_default_perms(self, suffix=''):
1152 return self._get_default_perms(self, suffix)
1153
1154 def get_api_data(self, with_group_members=True, include_secrets=False):
1155 """
1156 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1157 basically forwarded.
1158
1159 """
1160 user_group = self
1161
1162 data = {
1163 'users_group_id': user_group.users_group_id,
1164 'group_name': user_group.users_group_name,
1165 'group_description': user_group.user_group_description,
1166 'active': user_group.users_group_active,
1167 'owner': user_group.user.username,
1168 }
1169 if with_group_members:
1170 users = []
1171 for user in user_group.members:
1172 user = user.user
1173 users.append(user.get_api_data(include_secrets=include_secrets))
1174 data['users'] = users
1175
1176 return data
1177
1178
1179 class UserGroupMember(Base, BaseModel):
1180 __tablename__ = 'users_groups_members'
1181 __table_args__ = (
1182 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1183 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1184 )
1185
1186 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1187 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1188 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1189
1190 user = relationship('User', lazy='joined')
1191 users_group = relationship('UserGroup')
1192
1193 def __init__(self, gr_id='', u_id=''):
1194 self.users_group_id = gr_id
1195 self.user_id = u_id
1196
1197
1198 class RepositoryField(Base, BaseModel):
1199 __tablename__ = 'repositories_fields'
1200 __table_args__ = (
1201 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1202 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1203 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1204 )
1205 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1206
1207 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1208 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1209 field_key = Column("field_key", String(250))
1210 field_label = Column("field_label", String(1024), nullable=False)
1211 field_value = Column("field_value", String(10000), nullable=False)
1212 field_desc = Column("field_desc", String(1024), nullable=False)
1213 field_type = Column("field_type", String(255), nullable=False, unique=None)
1214 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1215
1216 repository = relationship('Repository')
1217
1218 @property
1219 def field_key_prefixed(self):
1220 return 'ex_%s' % self.field_key
1221
1222 @classmethod
1223 def un_prefix_key(cls, key):
1224 if key.startswith(cls.PREFIX):
1225 return key[len(cls.PREFIX):]
1226 return key
1227
1228 @classmethod
1229 def get_by_key_name(cls, key, repo):
1230 row = cls.query()\
1231 .filter(cls.repository == repo)\
1232 .filter(cls.field_key == key).scalar()
1233 return row
1234
1235
1236 class Repository(Base, BaseModel):
1237 __tablename__ = 'repositories'
1238 __table_args__ = (
1239 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1240 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1241 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1242 )
1243 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1244 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1245
1246 STATE_CREATED = 'repo_state_created'
1247 STATE_PENDING = 'repo_state_pending'
1248 STATE_ERROR = 'repo_state_error'
1249
1250 LOCK_AUTOMATIC = 'lock_auto'
1251 LOCK_API = 'lock_api'
1252 LOCK_WEB = 'lock_web'
1253 LOCK_PULL = 'lock_pull'
1254
1255 NAME_SEP = URL_SEP
1256
1257 repo_id = Column(
1258 "repo_id", Integer(), nullable=False, unique=True, default=None,
1259 primary_key=True)
1260 _repo_name = Column(
1261 "repo_name", Text(), nullable=False, default=None)
1262 _repo_name_hash = Column(
1263 "repo_name_hash", String(255), nullable=False, unique=True)
1264 repo_state = Column("repo_state", String(255), nullable=True)
1265
1266 clone_uri = Column(
1267 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1268 default=None)
1269 repo_type = Column(
1270 "repo_type", String(255), nullable=False, unique=False, default=None)
1271 user_id = Column(
1272 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1273 unique=False, default=None)
1274 private = Column(
1275 "private", Boolean(), nullable=True, unique=None, default=None)
1276 enable_statistics = Column(
1277 "statistics", Boolean(), nullable=True, unique=None, default=True)
1278 enable_downloads = Column(
1279 "downloads", Boolean(), nullable=True, unique=None, default=True)
1280 description = Column(
1281 "description", String(10000), nullable=True, unique=None, default=None)
1282 created_on = Column(
1283 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1284 default=datetime.datetime.now)
1285 updated_on = Column(
1286 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1287 default=datetime.datetime.now)
1288 _landing_revision = Column(
1289 "landing_revision", String(255), nullable=False, unique=False,
1290 default=None)
1291 enable_locking = Column(
1292 "enable_locking", Boolean(), nullable=False, unique=None,
1293 default=False)
1294 _locked = Column(
1295 "locked", String(255), nullable=True, unique=False, default=None)
1296 _changeset_cache = Column(
1297 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1298
1299 fork_id = Column(
1300 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1301 nullable=True, unique=False, default=None)
1302 group_id = Column(
1303 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1304 unique=False, default=None)
1305
1306 user = relationship('User', lazy='joined')
1307 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1308 group = relationship('RepoGroup', lazy='joined')
1309 repo_to_perm = relationship(
1310 'UserRepoToPerm', cascade='all',
1311 order_by='UserRepoToPerm.repo_to_perm_id')
1312 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1313 stats = relationship('Statistics', cascade='all', uselist=False)
1314
1315 followers = relationship(
1316 'UserFollowing',
1317 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1318 cascade='all')
1319 extra_fields = relationship(
1320 'RepositoryField', cascade="all, delete, delete-orphan")
1321 logs = relationship('UserLog')
1322 comments = relationship(
1323 'ChangesetComment', cascade="all, delete, delete-orphan")
1324 pull_requests_source = relationship(
1325 'PullRequest',
1326 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1327 cascade="all, delete, delete-orphan")
1328 pull_requests_target = relationship(
1329 'PullRequest',
1330 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1331 cascade="all, delete, delete-orphan")
1332 ui = relationship('RepoRhodeCodeUi', cascade="all")
1333 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1334 integrations = relationship('Integration',
1335 cascade="all, delete, delete-orphan")
1336
1337 def __unicode__(self):
1338 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1339 safe_unicode(self.repo_name))
1340
1341 @hybrid_property
1342 def landing_rev(self):
1343 # always should return [rev_type, rev]
1344 if self._landing_revision:
1345 _rev_info = self._landing_revision.split(':')
1346 if len(_rev_info) < 2:
1347 _rev_info.insert(0, 'rev')
1348 return [_rev_info[0], _rev_info[1]]
1349 return [None, None]
1350
1351 @landing_rev.setter
1352 def landing_rev(self, val):
1353 if ':' not in val:
1354 raise ValueError('value must be delimited with `:` and consist '
1355 'of <rev_type>:<rev>, got %s instead' % val)
1356 self._landing_revision = val
1357
1358 @hybrid_property
1359 def locked(self):
1360 if self._locked:
1361 user_id, timelocked, reason = self._locked.split(':')
1362 lock_values = int(user_id), timelocked, reason
1363 else:
1364 lock_values = [None, None, None]
1365 return lock_values
1366
1367 @locked.setter
1368 def locked(self, val):
1369 if val and isinstance(val, (list, tuple)):
1370 self._locked = ':'.join(map(str, val))
1371 else:
1372 self._locked = None
1373
1374 @hybrid_property
1375 def changeset_cache(self):
1376 from rhodecode.lib.vcs.backends.base import EmptyCommit
1377 dummy = EmptyCommit().__json__()
1378 if not self._changeset_cache:
1379 return dummy
1380 try:
1381 return json.loads(self._changeset_cache)
1382 except TypeError:
1383 return dummy
1384 except Exception:
1385 log.error(traceback.format_exc())
1386 return dummy
1387
1388 @changeset_cache.setter
1389 def changeset_cache(self, val):
1390 try:
1391 self._changeset_cache = json.dumps(val)
1392 except Exception:
1393 log.error(traceback.format_exc())
1394
1395 @hybrid_property
1396 def repo_name(self):
1397 return self._repo_name
1398
1399 @repo_name.setter
1400 def repo_name(self, value):
1401 self._repo_name = value
1402 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1403
1404 @classmethod
1405 def normalize_repo_name(cls, repo_name):
1406 """
1407 Normalizes os specific repo_name to the format internally stored inside
1408 database using URL_SEP
1409
1410 :param cls:
1411 :param repo_name:
1412 """
1413 return cls.NAME_SEP.join(repo_name.split(os.sep))
1414
1415 @classmethod
1416 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1417 session = Session()
1418 q = session.query(cls).filter(cls.repo_name == repo_name)
1419
1420 if cache:
1421 if identity_cache:
1422 val = cls.identity_cache(session, 'repo_name', repo_name)
1423 if val:
1424 return val
1425 else:
1426 q = q.options(
1427 FromCache("sql_cache_short",
1428 "get_repo_by_name_%s" % _hash_key(repo_name)))
1429
1430 return q.scalar()
1431
1432 @classmethod
1433 def get_by_full_path(cls, repo_full_path):
1434 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1435 repo_name = cls.normalize_repo_name(repo_name)
1436 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1437
1438 @classmethod
1439 def get_repo_forks(cls, repo_id):
1440 return cls.query().filter(Repository.fork_id == repo_id)
1441
1442 @classmethod
1443 def base_path(cls):
1444 """
1445 Returns base path when all repos are stored
1446
1447 :param cls:
1448 """
1449 q = Session().query(RhodeCodeUi)\
1450 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1451 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1452 return q.one().ui_value
1453
1454 @classmethod
1455 def is_valid(cls, repo_name):
1456 """
1457 returns True if given repo name is a valid filesystem repository
1458
1459 :param cls:
1460 :param repo_name:
1461 """
1462 from rhodecode.lib.utils import is_valid_repo
1463
1464 return is_valid_repo(repo_name, cls.base_path())
1465
1466 @classmethod
1467 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1468 case_insensitive=True):
1469 q = Repository.query()
1470
1471 if not isinstance(user_id, Optional):
1472 q = q.filter(Repository.user_id == user_id)
1473
1474 if not isinstance(group_id, Optional):
1475 q = q.filter(Repository.group_id == group_id)
1476
1477 if case_insensitive:
1478 q = q.order_by(func.lower(Repository.repo_name))
1479 else:
1480 q = q.order_by(Repository.repo_name)
1481 return q.all()
1482
1483 @property
1484 def forks(self):
1485 """
1486 Return forks of this repo
1487 """
1488 return Repository.get_repo_forks(self.repo_id)
1489
1490 @property
1491 def parent(self):
1492 """
1493 Returns fork parent
1494 """
1495 return self.fork
1496
1497 @property
1498 def just_name(self):
1499 return self.repo_name.split(self.NAME_SEP)[-1]
1500
1501 @property
1502 def groups_with_parents(self):
1503 groups = []
1504 if self.group is None:
1505 return groups
1506
1507 cur_gr = self.group
1508 groups.insert(0, cur_gr)
1509 while 1:
1510 gr = getattr(cur_gr, 'parent_group', None)
1511 cur_gr = cur_gr.parent_group
1512 if gr is None:
1513 break
1514 groups.insert(0, gr)
1515
1516 return groups
1517
1518 @property
1519 def groups_and_repo(self):
1520 return self.groups_with_parents, self
1521
1522 @LazyProperty
1523 def repo_path(self):
1524 """
1525 Returns base full path for that repository means where it actually
1526 exists on a filesystem
1527 """
1528 q = Session().query(RhodeCodeUi).filter(
1529 RhodeCodeUi.ui_key == self.NAME_SEP)
1530 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1531 return q.one().ui_value
1532
1533 @property
1534 def repo_full_path(self):
1535 p = [self.repo_path]
1536 # we need to split the name by / since this is how we store the
1537 # names in the database, but that eventually needs to be converted
1538 # into a valid system path
1539 p += self.repo_name.split(self.NAME_SEP)
1540 return os.path.join(*map(safe_unicode, p))
1541
1542 @property
1543 def cache_keys(self):
1544 """
1545 Returns associated cache keys for that repo
1546 """
1547 return CacheKey.query()\
1548 .filter(CacheKey.cache_args == self.repo_name)\
1549 .order_by(CacheKey.cache_key)\
1550 .all()
1551
1552 def get_new_name(self, repo_name):
1553 """
1554 returns new full repository name based on assigned group and new new
1555
1556 :param group_name:
1557 """
1558 path_prefix = self.group.full_path_splitted if self.group else []
1559 return self.NAME_SEP.join(path_prefix + [repo_name])
1560
1561 @property
1562 def _config(self):
1563 """
1564 Returns db based config object.
1565 """
1566 from rhodecode.lib.utils import make_db_config
1567 return make_db_config(clear_session=False, repo=self)
1568
1569 def permissions(self, with_admins=True, with_owner=True):
1570 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1571 q = q.options(joinedload(UserRepoToPerm.repository),
1572 joinedload(UserRepoToPerm.user),
1573 joinedload(UserRepoToPerm.permission),)
1574
1575 # get owners and admins and permissions. We do a trick of re-writing
1576 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1577 # has a global reference and changing one object propagates to all
1578 # others. This means if admin is also an owner admin_row that change
1579 # would propagate to both objects
1580 perm_rows = []
1581 for _usr in q.all():
1582 usr = AttributeDict(_usr.user.get_dict())
1583 usr.permission = _usr.permission.permission_name
1584 perm_rows.append(usr)
1585
1586 # filter the perm rows by 'default' first and then sort them by
1587 # admin,write,read,none permissions sorted again alphabetically in
1588 # each group
1589 perm_rows = sorted(perm_rows, key=display_sort)
1590
1591 _admin_perm = 'repository.admin'
1592 owner_row = []
1593 if with_owner:
1594 usr = AttributeDict(self.user.get_dict())
1595 usr.owner_row = True
1596 usr.permission = _admin_perm
1597 owner_row.append(usr)
1598
1599 super_admin_rows = []
1600 if with_admins:
1601 for usr in User.get_all_super_admins():
1602 # if this admin is also owner, don't double the record
1603 if usr.user_id == owner_row[0].user_id:
1604 owner_row[0].admin_row = True
1605 else:
1606 usr = AttributeDict(usr.get_dict())
1607 usr.admin_row = True
1608 usr.permission = _admin_perm
1609 super_admin_rows.append(usr)
1610
1611 return super_admin_rows + owner_row + perm_rows
1612
1613 def permission_user_groups(self):
1614 q = UserGroupRepoToPerm.query().filter(
1615 UserGroupRepoToPerm.repository == self)
1616 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1617 joinedload(UserGroupRepoToPerm.users_group),
1618 joinedload(UserGroupRepoToPerm.permission),)
1619
1620 perm_rows = []
1621 for _user_group in q.all():
1622 usr = AttributeDict(_user_group.users_group.get_dict())
1623 usr.permission = _user_group.permission.permission_name
1624 perm_rows.append(usr)
1625
1626 return perm_rows
1627
1628 def get_api_data(self, include_secrets=False):
1629 """
1630 Common function for generating repo api data
1631
1632 :param include_secrets: See :meth:`User.get_api_data`.
1633
1634 """
1635 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1636 # move this methods on models level.
1637 from rhodecode.model.settings import SettingsModel
1638
1639 repo = self
1640 _user_id, _time, _reason = self.locked
1641
1642 data = {
1643 'repo_id': repo.repo_id,
1644 'repo_name': repo.repo_name,
1645 'repo_type': repo.repo_type,
1646 'clone_uri': repo.clone_uri or '',
1647 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1648 'private': repo.private,
1649 'created_on': repo.created_on,
1650 'description': repo.description,
1651 'landing_rev': repo.landing_rev,
1652 'owner': repo.user.username,
1653 'fork_of': repo.fork.repo_name if repo.fork else None,
1654 'enable_statistics': repo.enable_statistics,
1655 'enable_locking': repo.enable_locking,
1656 'enable_downloads': repo.enable_downloads,
1657 'last_changeset': repo.changeset_cache,
1658 'locked_by': User.get(_user_id).get_api_data(
1659 include_secrets=include_secrets) if _user_id else None,
1660 'locked_date': time_to_datetime(_time) if _time else None,
1661 'lock_reason': _reason if _reason else None,
1662 }
1663
1664 # TODO: mikhail: should be per-repo settings here
1665 rc_config = SettingsModel().get_all_settings()
1666 repository_fields = str2bool(
1667 rc_config.get('rhodecode_repository_fields'))
1668 if repository_fields:
1669 for f in self.extra_fields:
1670 data[f.field_key_prefixed] = f.field_value
1671
1672 return data
1673
1674 @classmethod
1675 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1676 if not lock_time:
1677 lock_time = time.time()
1678 if not lock_reason:
1679 lock_reason = cls.LOCK_AUTOMATIC
1680 repo.locked = [user_id, lock_time, lock_reason]
1681 Session().add(repo)
1682 Session().commit()
1683
1684 @classmethod
1685 def unlock(cls, repo):
1686 repo.locked = None
1687 Session().add(repo)
1688 Session().commit()
1689
1690 @classmethod
1691 def getlock(cls, repo):
1692 return repo.locked
1693
1694 def is_user_lock(self, user_id):
1695 if self.lock[0]:
1696 lock_user_id = safe_int(self.lock[0])
1697 user_id = safe_int(user_id)
1698 # both are ints, and they are equal
1699 return all([lock_user_id, user_id]) and lock_user_id == user_id
1700
1701 return False
1702
1703 def get_locking_state(self, action, user_id, only_when_enabled=True):
1704 """
1705 Checks locking on this repository, if locking is enabled and lock is
1706 present returns a tuple of make_lock, locked, locked_by.
1707 make_lock can have 3 states None (do nothing) True, make lock
1708 False release lock, This value is later propagated to hooks, which
1709 do the locking. Think about this as signals passed to hooks what to do.
1710
1711 """
1712 # TODO: johbo: This is part of the business logic and should be moved
1713 # into the RepositoryModel.
1714
1715 if action not in ('push', 'pull'):
1716 raise ValueError("Invalid action value: %s" % repr(action))
1717
1718 # defines if locked error should be thrown to user
1719 currently_locked = False
1720 # defines if new lock should be made, tri-state
1721 make_lock = None
1722 repo = self
1723 user = User.get(user_id)
1724
1725 lock_info = repo.locked
1726
1727 if repo and (repo.enable_locking or not only_when_enabled):
1728 if action == 'push':
1729 # check if it's already locked !, if it is compare users
1730 locked_by_user_id = lock_info[0]
1731 if user.user_id == locked_by_user_id:
1732 log.debug(
1733 'Got `push` action from user %s, now unlocking', user)
1734 # unlock if we have push from user who locked
1735 make_lock = False
1736 else:
1737 # we're not the same user who locked, ban with
1738 # code defined in settings (default is 423 HTTP Locked) !
1739 log.debug('Repo %s is currently locked by %s', repo, user)
1740 currently_locked = True
1741 elif action == 'pull':
1742 # [0] user [1] date
1743 if lock_info[0] and lock_info[1]:
1744 log.debug('Repo %s is currently locked by %s', repo, user)
1745 currently_locked = True
1746 else:
1747 log.debug('Setting lock on repo %s by %s', repo, user)
1748 make_lock = True
1749
1750 else:
1751 log.debug('Repository %s do not have locking enabled', repo)
1752
1753 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1754 make_lock, currently_locked, lock_info)
1755
1756 from rhodecode.lib.auth import HasRepoPermissionAny
1757 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1758 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1759 # if we don't have at least write permission we cannot make a lock
1760 log.debug('lock state reset back to FALSE due to lack '
1761 'of at least read permission')
1762 make_lock = False
1763
1764 return make_lock, currently_locked, lock_info
1765
1766 @property
1767 def last_db_change(self):
1768 return self.updated_on
1769
1770 @property
1771 def clone_uri_hidden(self):
1772 clone_uri = self.clone_uri
1773 if clone_uri:
1774 import urlobject
1775 url_obj = urlobject.URLObject(clone_uri)
1776 if url_obj.password:
1777 clone_uri = url_obj.with_password('*****')
1778 return clone_uri
1779
1780 def clone_url(self, **override):
1781 qualified_home_url = url('home', qualified=True)
1782
1783 uri_tmpl = None
1784 if 'with_id' in override:
1785 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1786 del override['with_id']
1787
1788 if 'uri_tmpl' in override:
1789 uri_tmpl = override['uri_tmpl']
1790 del override['uri_tmpl']
1791
1792 # we didn't override our tmpl from **overrides
1793 if not uri_tmpl:
1794 uri_tmpl = self.DEFAULT_CLONE_URI
1795 try:
1796 from pylons import tmpl_context as c
1797 uri_tmpl = c.clone_uri_tmpl
1798 except Exception:
1799 # in any case if we call this outside of request context,
1800 # ie, not having tmpl_context set up
1801 pass
1802
1803 return get_clone_url(uri_tmpl=uri_tmpl,
1804 qualifed_home_url=qualified_home_url,
1805 repo_name=self.repo_name,
1806 repo_id=self.repo_id, **override)
1807
1808 def set_state(self, state):
1809 self.repo_state = state
1810 Session().add(self)
1811 #==========================================================================
1812 # SCM PROPERTIES
1813 #==========================================================================
1814
1815 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1816 return get_commit_safe(
1817 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1818
1819 def get_changeset(self, rev=None, pre_load=None):
1820 warnings.warn("Use get_commit", DeprecationWarning)
1821 commit_id = None
1822 commit_idx = None
1823 if isinstance(rev, basestring):
1824 commit_id = rev
1825 else:
1826 commit_idx = rev
1827 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1828 pre_load=pre_load)
1829
1830 def get_landing_commit(self):
1831 """
1832 Returns landing commit, or if that doesn't exist returns the tip
1833 """
1834 _rev_type, _rev = self.landing_rev
1835 commit = self.get_commit(_rev)
1836 if isinstance(commit, EmptyCommit):
1837 return self.get_commit()
1838 return commit
1839
1840 def update_commit_cache(self, cs_cache=None, config=None):
1841 """
1842 Update cache of last changeset for repository, keys should be::
1843
1844 short_id
1845 raw_id
1846 revision
1847 parents
1848 message
1849 date
1850 author
1851
1852 :param cs_cache:
1853 """
1854 from rhodecode.lib.vcs.backends.base import BaseChangeset
1855 if cs_cache is None:
1856 # use no-cache version here
1857 scm_repo = self.scm_instance(cache=False, config=config)
1858 if scm_repo:
1859 cs_cache = scm_repo.get_commit(
1860 pre_load=["author", "date", "message", "parents"])
1861 else:
1862 cs_cache = EmptyCommit()
1863
1864 if isinstance(cs_cache, BaseChangeset):
1865 cs_cache = cs_cache.__json__()
1866
1867 def is_outdated(new_cs_cache):
1868 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1869 new_cs_cache['revision'] != self.changeset_cache['revision']):
1870 return True
1871 return False
1872
1873 # check if we have maybe already latest cached revision
1874 if is_outdated(cs_cache) or not self.changeset_cache:
1875 _default = datetime.datetime.fromtimestamp(0)
1876 last_change = cs_cache.get('date') or _default
1877 log.debug('updated repo %s with new cs cache %s',
1878 self.repo_name, cs_cache)
1879 self.updated_on = last_change
1880 self.changeset_cache = cs_cache
1881 Session().add(self)
1882 Session().commit()
1883 else:
1884 log.debug('Skipping update_commit_cache for repo:`%s` '
1885 'commit already with latest changes', self.repo_name)
1886
1887 @property
1888 def tip(self):
1889 return self.get_commit('tip')
1890
1891 @property
1892 def author(self):
1893 return self.tip.author
1894
1895 @property
1896 def last_change(self):
1897 return self.scm_instance().last_change
1898
1899 def get_comments(self, revisions=None):
1900 """
1901 Returns comments for this repository grouped by revisions
1902
1903 :param revisions: filter query by revisions only
1904 """
1905 cmts = ChangesetComment.query()\
1906 .filter(ChangesetComment.repo == self)
1907 if revisions:
1908 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1909 grouped = collections.defaultdict(list)
1910 for cmt in cmts.all():
1911 grouped[cmt.revision].append(cmt)
1912 return grouped
1913
1914 def statuses(self, revisions=None):
1915 """
1916 Returns statuses for this repository
1917
1918 :param revisions: list of revisions to get statuses for
1919 """
1920 statuses = ChangesetStatus.query()\
1921 .filter(ChangesetStatus.repo == self)\
1922 .filter(ChangesetStatus.version == 0)
1923
1924 if revisions:
1925 # Try doing the filtering in chunks to avoid hitting limits
1926 size = 500
1927 status_results = []
1928 for chunk in xrange(0, len(revisions), size):
1929 status_results += statuses.filter(
1930 ChangesetStatus.revision.in_(
1931 revisions[chunk: chunk+size])
1932 ).all()
1933 else:
1934 status_results = statuses.all()
1935
1936 grouped = {}
1937
1938 # maybe we have open new pullrequest without a status?
1939 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1940 status_lbl = ChangesetStatus.get_status_lbl(stat)
1941 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1942 for rev in pr.revisions:
1943 pr_id = pr.pull_request_id
1944 pr_repo = pr.target_repo.repo_name
1945 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1946
1947 for stat in status_results:
1948 pr_id = pr_repo = None
1949 if stat.pull_request:
1950 pr_id = stat.pull_request.pull_request_id
1951 pr_repo = stat.pull_request.target_repo.repo_name
1952 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1953 pr_id, pr_repo]
1954 return grouped
1955
1956 # ==========================================================================
1957 # SCM CACHE INSTANCE
1958 # ==========================================================================
1959
1960 def scm_instance(self, **kwargs):
1961 import rhodecode
1962
1963 # Passing a config will not hit the cache currently only used
1964 # for repo2dbmapper
1965 config = kwargs.pop('config', None)
1966 cache = kwargs.pop('cache', None)
1967 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1968 # if cache is NOT defined use default global, else we have a full
1969 # control over cache behaviour
1970 if cache is None and full_cache and not config:
1971 return self._get_instance_cached()
1972 return self._get_instance(cache=bool(cache), config=config)
1973
1974 def _get_instance_cached(self):
1975 @cache_region('long_term')
1976 def _get_repo(cache_key):
1977 return self._get_instance()
1978
1979 invalidator_context = CacheKey.repo_context_cache(
1980 _get_repo, self.repo_name, None, thread_scoped=True)
1981
1982 with invalidator_context as context:
1983 context.invalidate()
1984 repo = context.compute()
1985
1986 return repo
1987
1988 def _get_instance(self, cache=True, config=None):
1989 config = config or self._config
1990 custom_wire = {
1991 'cache': cache # controls the vcs.remote cache
1992 }
1993
1994 repo = get_vcs_instance(
1995 repo_path=safe_str(self.repo_full_path),
1996 config=config,
1997 with_wire=custom_wire,
1998 create=False)
1999
2000 return repo
2001
2002 def __json__(self):
2003 return {'landing_rev': self.landing_rev}
2004
2005 def get_dict(self):
2006
2007 # Since we transformed `repo_name` to a hybrid property, we need to
2008 # keep compatibility with the code which uses `repo_name` field.
2009
2010 result = super(Repository, self).get_dict()
2011 result['repo_name'] = result.pop('_repo_name', None)
2012 return result
2013
2014
2015 class RepoGroup(Base, BaseModel):
2016 __tablename__ = 'groups'
2017 __table_args__ = (
2018 UniqueConstraint('group_name', 'group_parent_id'),
2019 CheckConstraint('group_id != group_parent_id'),
2020 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2021 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2022 )
2023 __mapper_args__ = {'order_by': 'group_name'}
2024
2025 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2026
2027 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2028 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2029 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2030 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2031 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2032 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2033 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2034
2035 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2036 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2037 parent_group = relationship('RepoGroup', remote_side=group_id)
2038 user = relationship('User')
2039
2040 def __init__(self, group_name='', parent_group=None):
2041 self.group_name = group_name
2042 self.parent_group = parent_group
2043
2044 def __unicode__(self):
2045 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2046 self.group_name)
2047
2048 @classmethod
2049 def _generate_choice(cls, repo_group):
2050 from webhelpers.html import literal as _literal
2051 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2052 return repo_group.group_id, _name(repo_group.full_path_splitted)
2053
2054 @classmethod
2055 def groups_choices(cls, groups=None, show_empty_group=True):
2056 if not groups:
2057 groups = cls.query().all()
2058
2059 repo_groups = []
2060 if show_empty_group:
2061 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2062
2063 repo_groups.extend([cls._generate_choice(x) for x in groups])
2064
2065 repo_groups = sorted(
2066 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2067 return repo_groups
2068
2069 @classmethod
2070 def url_sep(cls):
2071 return URL_SEP
2072
2073 @classmethod
2074 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2075 if case_insensitive:
2076 gr = cls.query().filter(func.lower(cls.group_name)
2077 == func.lower(group_name))
2078 else:
2079 gr = cls.query().filter(cls.group_name == group_name)
2080 if cache:
2081 gr = gr.options(FromCache(
2082 "sql_cache_short",
2083 "get_group_%s" % _hash_key(group_name)))
2084 return gr.scalar()
2085
2086 @classmethod
2087 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2088 case_insensitive=True):
2089 q = RepoGroup.query()
2090
2091 if not isinstance(user_id, Optional):
2092 q = q.filter(RepoGroup.user_id == user_id)
2093
2094 if not isinstance(group_id, Optional):
2095 q = q.filter(RepoGroup.group_parent_id == group_id)
2096
2097 if case_insensitive:
2098 q = q.order_by(func.lower(RepoGroup.group_name))
2099 else:
2100 q = q.order_by(RepoGroup.group_name)
2101 return q.all()
2102
2103 @property
2104 def parents(self):
2105 parents_recursion_limit = 10
2106 groups = []
2107 if self.parent_group is None:
2108 return groups
2109 cur_gr = self.parent_group
2110 groups.insert(0, cur_gr)
2111 cnt = 0
2112 while 1:
2113 cnt += 1
2114 gr = getattr(cur_gr, 'parent_group', None)
2115 cur_gr = cur_gr.parent_group
2116 if gr is None:
2117 break
2118 if cnt == parents_recursion_limit:
2119 # this will prevent accidental infinit loops
2120 log.error(('more than %s parents found for group %s, stopping '
2121 'recursive parent fetching' % (parents_recursion_limit, self)))
2122 break
2123
2124 groups.insert(0, gr)
2125 return groups
2126
2127 @property
2128 def children(self):
2129 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2130
2131 @property
2132 def name(self):
2133 return self.group_name.split(RepoGroup.url_sep())[-1]
2134
2135 @property
2136 def full_path(self):
2137 return self.group_name
2138
2139 @property
2140 def full_path_splitted(self):
2141 return self.group_name.split(RepoGroup.url_sep())
2142
2143 @property
2144 def repositories(self):
2145 return Repository.query()\
2146 .filter(Repository.group == self)\
2147 .order_by(Repository.repo_name)
2148
2149 @property
2150 def repositories_recursive_count(self):
2151 cnt = self.repositories.count()
2152
2153 def children_count(group):
2154 cnt = 0
2155 for child in group.children:
2156 cnt += child.repositories.count()
2157 cnt += children_count(child)
2158 return cnt
2159
2160 return cnt + children_count(self)
2161
2162 def _recursive_objects(self, include_repos=True):
2163 all_ = []
2164
2165 def _get_members(root_gr):
2166 if include_repos:
2167 for r in root_gr.repositories:
2168 all_.append(r)
2169 childs = root_gr.children.all()
2170 if childs:
2171 for gr in childs:
2172 all_.append(gr)
2173 _get_members(gr)
2174
2175 _get_members(self)
2176 return [self] + all_
2177
2178 def recursive_groups_and_repos(self):
2179 """
2180 Recursive return all groups, with repositories in those groups
2181 """
2182 return self._recursive_objects()
2183
2184 def recursive_groups(self):
2185 """
2186 Returns all children groups for this group including children of children
2187 """
2188 return self._recursive_objects(include_repos=False)
2189
2190 def get_new_name(self, group_name):
2191 """
2192 returns new full group name based on parent and new name
2193
2194 :param group_name:
2195 """
2196 path_prefix = (self.parent_group.full_path_splitted if
2197 self.parent_group else [])
2198 return RepoGroup.url_sep().join(path_prefix + [group_name])
2199
2200 def permissions(self, with_admins=True, with_owner=True):
2201 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2202 q = q.options(joinedload(UserRepoGroupToPerm.group),
2203 joinedload(UserRepoGroupToPerm.user),
2204 joinedload(UserRepoGroupToPerm.permission),)
2205
2206 # get owners and admins and permissions. We do a trick of re-writing
2207 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2208 # has a global reference and changing one object propagates to all
2209 # others. This means if admin is also an owner admin_row that change
2210 # would propagate to both objects
2211 perm_rows = []
2212 for _usr in q.all():
2213 usr = AttributeDict(_usr.user.get_dict())
2214 usr.permission = _usr.permission.permission_name
2215 perm_rows.append(usr)
2216
2217 # filter the perm rows by 'default' first and then sort them by
2218 # admin,write,read,none permissions sorted again alphabetically in
2219 # each group
2220 perm_rows = sorted(perm_rows, key=display_sort)
2221
2222 _admin_perm = 'group.admin'
2223 owner_row = []
2224 if with_owner:
2225 usr = AttributeDict(self.user.get_dict())
2226 usr.owner_row = True
2227 usr.permission = _admin_perm
2228 owner_row.append(usr)
2229
2230 super_admin_rows = []
2231 if with_admins:
2232 for usr in User.get_all_super_admins():
2233 # if this admin is also owner, don't double the record
2234 if usr.user_id == owner_row[0].user_id:
2235 owner_row[0].admin_row = True
2236 else:
2237 usr = AttributeDict(usr.get_dict())
2238 usr.admin_row = True
2239 usr.permission = _admin_perm
2240 super_admin_rows.append(usr)
2241
2242 return super_admin_rows + owner_row + perm_rows
2243
2244 def permission_user_groups(self):
2245 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2246 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2247 joinedload(UserGroupRepoGroupToPerm.users_group),
2248 joinedload(UserGroupRepoGroupToPerm.permission),)
2249
2250 perm_rows = []
2251 for _user_group in q.all():
2252 usr = AttributeDict(_user_group.users_group.get_dict())
2253 usr.permission = _user_group.permission.permission_name
2254 perm_rows.append(usr)
2255
2256 return perm_rows
2257
2258 def get_api_data(self):
2259 """
2260 Common function for generating api data
2261
2262 """
2263 group = self
2264 data = {
2265 'group_id': group.group_id,
2266 'group_name': group.group_name,
2267 'group_description': group.group_description,
2268 'parent_group': group.parent_group.group_name if group.parent_group else None,
2269 'repositories': [x.repo_name for x in group.repositories],
2270 'owner': group.user.username,
2271 }
2272 return data
2273
2274
2275 class Permission(Base, BaseModel):
2276 __tablename__ = 'permissions'
2277 __table_args__ = (
2278 Index('p_perm_name_idx', 'permission_name'),
2279 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2280 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2281 )
2282 PERMS = [
2283 ('hg.admin', _('RhodeCode Super Administrator')),
2284
2285 ('repository.none', _('Repository no access')),
2286 ('repository.read', _('Repository read access')),
2287 ('repository.write', _('Repository write access')),
2288 ('repository.admin', _('Repository admin access')),
2289
2290 ('group.none', _('Repository group no access')),
2291 ('group.read', _('Repository group read access')),
2292 ('group.write', _('Repository group write access')),
2293 ('group.admin', _('Repository group admin access')),
2294
2295 ('usergroup.none', _('User group no access')),
2296 ('usergroup.read', _('User group read access')),
2297 ('usergroup.write', _('User group write access')),
2298 ('usergroup.admin', _('User group admin access')),
2299
2300 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2301 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2302
2303 ('hg.usergroup.create.false', _('User Group creation disabled')),
2304 ('hg.usergroup.create.true', _('User Group creation enabled')),
2305
2306 ('hg.create.none', _('Repository creation disabled')),
2307 ('hg.create.repository', _('Repository creation enabled')),
2308 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2309 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2310
2311 ('hg.fork.none', _('Repository forking disabled')),
2312 ('hg.fork.repository', _('Repository forking enabled')),
2313
2314 ('hg.register.none', _('Registration disabled')),
2315 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2316 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2317
2318 ('hg.extern_activate.manual', _('Manual activation of external account')),
2319 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2320
2321 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2322 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2323 ]
2324
2325 # definition of system default permissions for DEFAULT user
2326 DEFAULT_USER_PERMISSIONS = [
2327 'repository.read',
2328 'group.read',
2329 'usergroup.read',
2330 'hg.create.repository',
2331 'hg.repogroup.create.false',
2332 'hg.usergroup.create.false',
2333 'hg.create.write_on_repogroup.true',
2334 'hg.fork.repository',
2335 'hg.register.manual_activate',
2336 'hg.extern_activate.auto',
2337 'hg.inherit_default_perms.true',
2338 ]
2339
2340 # defines which permissions are more important higher the more important
2341 # Weight defines which permissions are more important.
2342 # The higher number the more important.
2343 PERM_WEIGHTS = {
2344 'repository.none': 0,
2345 'repository.read': 1,
2346 'repository.write': 3,
2347 'repository.admin': 4,
2348
2349 'group.none': 0,
2350 'group.read': 1,
2351 'group.write': 3,
2352 'group.admin': 4,
2353
2354 'usergroup.none': 0,
2355 'usergroup.read': 1,
2356 'usergroup.write': 3,
2357 'usergroup.admin': 4,
2358
2359 'hg.repogroup.create.false': 0,
2360 'hg.repogroup.create.true': 1,
2361
2362 'hg.usergroup.create.false': 0,
2363 'hg.usergroup.create.true': 1,
2364
2365 'hg.fork.none': 0,
2366 'hg.fork.repository': 1,
2367 'hg.create.none': 0,
2368 'hg.create.repository': 1
2369 }
2370
2371 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2372 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2373 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2374
2375 def __unicode__(self):
2376 return u"<%s('%s:%s')>" % (
2377 self.__class__.__name__, self.permission_id, self.permission_name
2378 )
2379
2380 @classmethod
2381 def get_by_key(cls, key):
2382 return cls.query().filter(cls.permission_name == key).scalar()
2383
2384 @classmethod
2385 def get_default_repo_perms(cls, user_id, repo_id=None):
2386 q = Session().query(UserRepoToPerm, Repository, Permission)\
2387 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2388 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2389 .filter(UserRepoToPerm.user_id == user_id)
2390 if repo_id:
2391 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2392 return q.all()
2393
2394 @classmethod
2395 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2396 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2397 .join(
2398 Permission,
2399 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2400 .join(
2401 Repository,
2402 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2403 .join(
2404 UserGroup,
2405 UserGroupRepoToPerm.users_group_id ==
2406 UserGroup.users_group_id)\
2407 .join(
2408 UserGroupMember,
2409 UserGroupRepoToPerm.users_group_id ==
2410 UserGroupMember.users_group_id)\
2411 .filter(
2412 UserGroupMember.user_id == user_id,
2413 UserGroup.users_group_active == true())
2414 if repo_id:
2415 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2416 return q.all()
2417
2418 @classmethod
2419 def get_default_group_perms(cls, user_id, repo_group_id=None):
2420 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2421 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2422 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2423 .filter(UserRepoGroupToPerm.user_id == user_id)
2424 if repo_group_id:
2425 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2426 return q.all()
2427
2428 @classmethod
2429 def get_default_group_perms_from_user_group(
2430 cls, user_id, repo_group_id=None):
2431 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2432 .join(
2433 Permission,
2434 UserGroupRepoGroupToPerm.permission_id ==
2435 Permission.permission_id)\
2436 .join(
2437 RepoGroup,
2438 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2439 .join(
2440 UserGroup,
2441 UserGroupRepoGroupToPerm.users_group_id ==
2442 UserGroup.users_group_id)\
2443 .join(
2444 UserGroupMember,
2445 UserGroupRepoGroupToPerm.users_group_id ==
2446 UserGroupMember.users_group_id)\
2447 .filter(
2448 UserGroupMember.user_id == user_id,
2449 UserGroup.users_group_active == true())
2450 if repo_group_id:
2451 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2452 return q.all()
2453
2454 @classmethod
2455 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2456 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2457 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2458 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2459 .filter(UserUserGroupToPerm.user_id == user_id)
2460 if user_group_id:
2461 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2462 return q.all()
2463
2464 @classmethod
2465 def get_default_user_group_perms_from_user_group(
2466 cls, user_id, user_group_id=None):
2467 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2468 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2469 .join(
2470 Permission,
2471 UserGroupUserGroupToPerm.permission_id ==
2472 Permission.permission_id)\
2473 .join(
2474 TargetUserGroup,
2475 UserGroupUserGroupToPerm.target_user_group_id ==
2476 TargetUserGroup.users_group_id)\
2477 .join(
2478 UserGroup,
2479 UserGroupUserGroupToPerm.user_group_id ==
2480 UserGroup.users_group_id)\
2481 .join(
2482 UserGroupMember,
2483 UserGroupUserGroupToPerm.user_group_id ==
2484 UserGroupMember.users_group_id)\
2485 .filter(
2486 UserGroupMember.user_id == user_id,
2487 UserGroup.users_group_active == true())
2488 if user_group_id:
2489 q = q.filter(
2490 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2491
2492 return q.all()
2493
2494
2495 class UserRepoToPerm(Base, BaseModel):
2496 __tablename__ = 'repo_to_perm'
2497 __table_args__ = (
2498 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2499 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2500 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2501 )
2502 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2503 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2504 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2505 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2506
2507 user = relationship('User')
2508 repository = relationship('Repository')
2509 permission = relationship('Permission')
2510
2511 @classmethod
2512 def create(cls, user, repository, permission):
2513 n = cls()
2514 n.user = user
2515 n.repository = repository
2516 n.permission = permission
2517 Session().add(n)
2518 return n
2519
2520 def __unicode__(self):
2521 return u'<%s => %s >' % (self.user, self.repository)
2522
2523
2524 class UserUserGroupToPerm(Base, BaseModel):
2525 __tablename__ = 'user_user_group_to_perm'
2526 __table_args__ = (
2527 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2528 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2529 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2530 )
2531 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2532 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2533 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2534 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2535
2536 user = relationship('User')
2537 user_group = relationship('UserGroup')
2538 permission = relationship('Permission')
2539
2540 @classmethod
2541 def create(cls, user, user_group, permission):
2542 n = cls()
2543 n.user = user
2544 n.user_group = user_group
2545 n.permission = permission
2546 Session().add(n)
2547 return n
2548
2549 def __unicode__(self):
2550 return u'<%s => %s >' % (self.user, self.user_group)
2551
2552
2553 class UserToPerm(Base, BaseModel):
2554 __tablename__ = 'user_to_perm'
2555 __table_args__ = (
2556 UniqueConstraint('user_id', 'permission_id'),
2557 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2558 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2559 )
2560 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2561 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2562 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2563
2564 user = relationship('User')
2565 permission = relationship('Permission', lazy='joined')
2566
2567 def __unicode__(self):
2568 return u'<%s => %s >' % (self.user, self.permission)
2569
2570
2571 class UserGroupRepoToPerm(Base, BaseModel):
2572 __tablename__ = 'users_group_repo_to_perm'
2573 __table_args__ = (
2574 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2575 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2576 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2577 )
2578 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2579 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2580 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2581 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2582
2583 users_group = relationship('UserGroup')
2584 permission = relationship('Permission')
2585 repository = relationship('Repository')
2586
2587 @classmethod
2588 def create(cls, users_group, repository, permission):
2589 n = cls()
2590 n.users_group = users_group
2591 n.repository = repository
2592 n.permission = permission
2593 Session().add(n)
2594 return n
2595
2596 def __unicode__(self):
2597 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2598
2599
2600 class UserGroupUserGroupToPerm(Base, BaseModel):
2601 __tablename__ = 'user_group_user_group_to_perm'
2602 __table_args__ = (
2603 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2604 CheckConstraint('target_user_group_id != user_group_id'),
2605 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2606 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2607 )
2608 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)
2609 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2610 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2611 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2612
2613 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2614 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2615 permission = relationship('Permission')
2616
2617 @classmethod
2618 def create(cls, target_user_group, user_group, permission):
2619 n = cls()
2620 n.target_user_group = target_user_group
2621 n.user_group = user_group
2622 n.permission = permission
2623 Session().add(n)
2624 return n
2625
2626 def __unicode__(self):
2627 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2628
2629
2630 class UserGroupToPerm(Base, BaseModel):
2631 __tablename__ = 'users_group_to_perm'
2632 __table_args__ = (
2633 UniqueConstraint('users_group_id', 'permission_id',),
2634 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2635 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2636 )
2637 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2638 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2639 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2640
2641 users_group = relationship('UserGroup')
2642 permission = relationship('Permission')
2643
2644
2645 class UserRepoGroupToPerm(Base, BaseModel):
2646 __tablename__ = 'user_repo_group_to_perm'
2647 __table_args__ = (
2648 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2649 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2650 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2651 )
2652
2653 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2654 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2655 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2656 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2657
2658 user = relationship('User')
2659 group = relationship('RepoGroup')
2660 permission = relationship('Permission')
2661
2662 @classmethod
2663 def create(cls, user, repository_group, permission):
2664 n = cls()
2665 n.user = user
2666 n.group = repository_group
2667 n.permission = permission
2668 Session().add(n)
2669 return n
2670
2671
2672 class UserGroupRepoGroupToPerm(Base, BaseModel):
2673 __tablename__ = 'users_group_repo_group_to_perm'
2674 __table_args__ = (
2675 UniqueConstraint('users_group_id', 'group_id'),
2676 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2677 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2678 )
2679
2680 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)
2681 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2682 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2683 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2684
2685 users_group = relationship('UserGroup')
2686 permission = relationship('Permission')
2687 group = relationship('RepoGroup')
2688
2689 @classmethod
2690 def create(cls, user_group, repository_group, permission):
2691 n = cls()
2692 n.users_group = user_group
2693 n.group = repository_group
2694 n.permission = permission
2695 Session().add(n)
2696 return n
2697
2698 def __unicode__(self):
2699 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2700
2701
2702 class Statistics(Base, BaseModel):
2703 __tablename__ = 'statistics'
2704 __table_args__ = (
2705 UniqueConstraint('repository_id'),
2706 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2707 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2708 )
2709 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2710 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2711 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2712 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2713 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2714 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2715
2716 repository = relationship('Repository', single_parent=True)
2717
2718
2719 class UserFollowing(Base, BaseModel):
2720 __tablename__ = 'user_followings'
2721 __table_args__ = (
2722 UniqueConstraint('user_id', 'follows_repository_id'),
2723 UniqueConstraint('user_id', 'follows_user_id'),
2724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2725 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2726 )
2727
2728 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2729 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2730 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2731 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2732 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2733
2734 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2735
2736 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2737 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2738
2739 @classmethod
2740 def get_repo_followers(cls, repo_id):
2741 return cls.query().filter(cls.follows_repo_id == repo_id)
2742
2743
2744 class CacheKey(Base, BaseModel):
2745 __tablename__ = 'cache_invalidation'
2746 __table_args__ = (
2747 UniqueConstraint('cache_key'),
2748 Index('key_idx', 'cache_key'),
2749 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2750 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2751 )
2752 CACHE_TYPE_ATOM = 'ATOM'
2753 CACHE_TYPE_RSS = 'RSS'
2754 CACHE_TYPE_README = 'README'
2755
2756 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2757 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2758 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2759 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2760
2761 def __init__(self, cache_key, cache_args=''):
2762 self.cache_key = cache_key
2763 self.cache_args = cache_args
2764 self.cache_active = False
2765
2766 def __unicode__(self):
2767 return u"<%s('%s:%s[%s]')>" % (
2768 self.__class__.__name__,
2769 self.cache_id, self.cache_key, self.cache_active)
2770
2771 def _cache_key_partition(self):
2772 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2773 return prefix, repo_name, suffix
2774
2775 def get_prefix(self):
2776 """
2777 Try to extract prefix from existing cache key. The key could consist
2778 of prefix, repo_name, suffix
2779 """
2780 # this returns prefix, repo_name, suffix
2781 return self._cache_key_partition()[0]
2782
2783 def get_suffix(self):
2784 """
2785 get suffix that might have been used in _get_cache_key to
2786 generate self.cache_key. Only used for informational purposes
2787 in repo_edit.html.
2788 """
2789 # prefix, repo_name, suffix
2790 return self._cache_key_partition()[2]
2791
2792 @classmethod
2793 def delete_all_cache(cls):
2794 """
2795 Delete all cache keys from database.
2796 Should only be run when all instances are down and all entries
2797 thus stale.
2798 """
2799 cls.query().delete()
2800 Session().commit()
2801
2802 @classmethod
2803 def get_cache_key(cls, repo_name, cache_type):
2804 """
2805
2806 Generate a cache key for this process of RhodeCode instance.
2807 Prefix most likely will be process id or maybe explicitly set
2808 instance_id from .ini file.
2809 """
2810 import rhodecode
2811 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2812
2813 repo_as_unicode = safe_unicode(repo_name)
2814 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2815 if cache_type else repo_as_unicode
2816
2817 return u'{}{}'.format(prefix, key)
2818
2819 @classmethod
2820 def set_invalidate(cls, repo_name, delete=False):
2821 """
2822 Mark all caches of a repo as invalid in the database.
2823 """
2824
2825 try:
2826 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2827 if delete:
2828 log.debug('cache objects deleted for repo %s',
2829 safe_str(repo_name))
2830 qry.delete()
2831 else:
2832 log.debug('cache objects marked as invalid for repo %s',
2833 safe_str(repo_name))
2834 qry.update({"cache_active": False})
2835
2836 Session().commit()
2837 except Exception:
2838 log.exception(
2839 'Cache key invalidation failed for repository %s',
2840 safe_str(repo_name))
2841 Session().rollback()
2842
2843 @classmethod
2844 def get_active_cache(cls, cache_key):
2845 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2846 if inv_obj:
2847 return inv_obj
2848 return None
2849
2850 @classmethod
2851 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2852 thread_scoped=False):
2853 """
2854 @cache_region('long_term')
2855 def _heavy_calculation(cache_key):
2856 return 'result'
2857
2858 cache_context = CacheKey.repo_context_cache(
2859 _heavy_calculation, repo_name, cache_type)
2860
2861 with cache_context as context:
2862 context.invalidate()
2863 computed = context.compute()
2864
2865 assert computed == 'result'
2866 """
2867 from rhodecode.lib import caches
2868 return caches.InvalidationContext(
2869 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2870
2871
2872 class ChangesetComment(Base, BaseModel):
2873 __tablename__ = 'changeset_comments'
2874 __table_args__ = (
2875 Index('cc_revision_idx', 'revision'),
2876 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2877 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2878 )
2879
2880 COMMENT_OUTDATED = u'comment_outdated'
2881
2882 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2883 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2884 revision = Column('revision', String(40), nullable=True)
2885 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2886 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2887 line_no = Column('line_no', Unicode(10), nullable=True)
2888 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2889 f_path = Column('f_path', Unicode(1000), nullable=True)
2890 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2891 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2892 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2893 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2894 renderer = Column('renderer', Unicode(64), nullable=True)
2895 display_state = Column('display_state', Unicode(128), nullable=True)
2896
2897 author = relationship('User', lazy='joined')
2898 repo = relationship('Repository')
2899 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2900 pull_request = relationship('PullRequest', lazy='joined')
2901 pull_request_version = relationship('PullRequestVersion')
2902
2903 @classmethod
2904 def get_users(cls, revision=None, pull_request_id=None):
2905 """
2906 Returns user associated with this ChangesetComment. ie those
2907 who actually commented
2908
2909 :param cls:
2910 :param revision:
2911 """
2912 q = Session().query(User)\
2913 .join(ChangesetComment.author)
2914 if revision:
2915 q = q.filter(cls.revision == revision)
2916 elif pull_request_id:
2917 q = q.filter(cls.pull_request_id == pull_request_id)
2918 return q.all()
2919
2920 def render(self, mentions=False):
2921 from rhodecode.lib import helpers as h
2922 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2923
2924 def __repr__(self):
2925 if self.comment_id:
2926 return '<DB:ChangesetComment #%s>' % self.comment_id
2927 else:
2928 return '<DB:ChangesetComment at %#x>' % id(self)
2929
2930
2931 class ChangesetStatus(Base, BaseModel):
2932 __tablename__ = 'changeset_statuses'
2933 __table_args__ = (
2934 Index('cs_revision_idx', 'revision'),
2935 Index('cs_version_idx', 'version'),
2936 UniqueConstraint('repo_id', 'revision', 'version'),
2937 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2938 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2939 )
2940 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2941 STATUS_APPROVED = 'approved'
2942 STATUS_REJECTED = 'rejected'
2943 STATUS_UNDER_REVIEW = 'under_review'
2944
2945 STATUSES = [
2946 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2947 (STATUS_APPROVED, _("Approved")),
2948 (STATUS_REJECTED, _("Rejected")),
2949 (STATUS_UNDER_REVIEW, _("Under Review")),
2950 ]
2951
2952 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2953 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2954 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2955 revision = Column('revision', String(40), nullable=False)
2956 status = Column('status', String(128), nullable=False, default=DEFAULT)
2957 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2958 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2959 version = Column('version', Integer(), nullable=False, default=0)
2960 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2961
2962 author = relationship('User', lazy='joined')
2963 repo = relationship('Repository')
2964 comment = relationship('ChangesetComment', lazy='joined')
2965 pull_request = relationship('PullRequest', lazy='joined')
2966
2967 def __unicode__(self):
2968 return u"<%s('%s[%s]:%s')>" % (
2969 self.__class__.__name__,
2970 self.status, self.version, self.author
2971 )
2972
2973 @classmethod
2974 def get_status_lbl(cls, value):
2975 return dict(cls.STATUSES).get(value)
2976
2977 @property
2978 def status_lbl(self):
2979 return ChangesetStatus.get_status_lbl(self.status)
2980
2981
2982 class _PullRequestBase(BaseModel):
2983 """
2984 Common attributes of pull request and version entries.
2985 """
2986
2987 # .status values
2988 STATUS_NEW = u'new'
2989 STATUS_OPEN = u'open'
2990 STATUS_CLOSED = u'closed'
2991
2992 title = Column('title', Unicode(255), nullable=True)
2993 description = Column(
2994 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2995 nullable=True)
2996 # new/open/closed status of pull request (not approve/reject/etc)
2997 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
2998 created_on = Column(
2999 'created_on', DateTime(timezone=False), nullable=False,
3000 default=datetime.datetime.now)
3001 updated_on = Column(
3002 'updated_on', DateTime(timezone=False), nullable=False,
3003 default=datetime.datetime.now)
3004
3005 @declared_attr
3006 def user_id(cls):
3007 return Column(
3008 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3009 unique=None)
3010
3011 # 500 revisions max
3012 _revisions = Column(
3013 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3014
3015 @declared_attr
3016 def source_repo_id(cls):
3017 # TODO: dan: rename column to source_repo_id
3018 return Column(
3019 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3020 nullable=False)
3021
3022 source_ref = Column('org_ref', Unicode(255), nullable=False)
3023
3024 @declared_attr
3025 def target_repo_id(cls):
3026 # TODO: dan: rename column to target_repo_id
3027 return Column(
3028 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3029 nullable=False)
3030
3031 target_ref = Column('other_ref', Unicode(255), nullable=False)
3032
3033 # TODO: dan: rename column to last_merge_source_rev
3034 _last_merge_source_rev = Column(
3035 'last_merge_org_rev', String(40), nullable=True)
3036 # TODO: dan: rename column to last_merge_target_rev
3037 _last_merge_target_rev = Column(
3038 'last_merge_other_rev', String(40), nullable=True)
3039 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3040 merge_rev = Column('merge_rev', String(40), nullable=True)
3041
3042 @hybrid_property
3043 def revisions(self):
3044 return self._revisions.split(':') if self._revisions else []
3045
3046 @revisions.setter
3047 def revisions(self, val):
3048 self._revisions = ':'.join(val)
3049
3050 @declared_attr
3051 def author(cls):
3052 return relationship('User', lazy='joined')
3053
3054 @declared_attr
3055 def source_repo(cls):
3056 return relationship(
3057 'Repository',
3058 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3059
3060 @property
3061 def source_ref_parts(self):
3062 refs = self.source_ref.split(':')
3063 return Reference(refs[0], refs[1], refs[2])
3064
3065 @declared_attr
3066 def target_repo(cls):
3067 return relationship(
3068 'Repository',
3069 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3070
3071 @property
3072 def target_ref_parts(self):
3073 refs = self.target_ref.split(':')
3074 return Reference(refs[0], refs[1], refs[2])
3075
3076
3077 class PullRequest(Base, _PullRequestBase):
3078 __tablename__ = 'pull_requests'
3079 __table_args__ = (
3080 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3081 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3082 )
3083
3084 pull_request_id = Column(
3085 'pull_request_id', Integer(), nullable=False, primary_key=True)
3086
3087 def __repr__(self):
3088 if self.pull_request_id:
3089 return '<DB:PullRequest #%s>' % self.pull_request_id
3090 else:
3091 return '<DB:PullRequest at %#x>' % id(self)
3092
3093 reviewers = relationship('PullRequestReviewers',
3094 cascade="all, delete, delete-orphan")
3095 statuses = relationship('ChangesetStatus')
3096 comments = relationship('ChangesetComment',
3097 cascade="all, delete, delete-orphan")
3098 versions = relationship('PullRequestVersion',
3099 cascade="all, delete, delete-orphan")
3100
3101 def is_closed(self):
3102 return self.status == self.STATUS_CLOSED
3103
3104 def get_api_data(self):
3105 from rhodecode.model.pull_request import PullRequestModel
3106 pull_request = self
3107 merge_status = PullRequestModel().merge_status(pull_request)
3108 data = {
3109 'pull_request_id': pull_request.pull_request_id,
3110 'url': url('pullrequest_show', repo_name=self.target_repo.repo_name,
3111 pull_request_id=self.pull_request_id,
3112 qualified=True),
3113 'title': pull_request.title,
3114 'description': pull_request.description,
3115 'status': pull_request.status,
3116 'created_on': pull_request.created_on,
3117 'updated_on': pull_request.updated_on,
3118 'commit_ids': pull_request.revisions,
3119 'review_status': pull_request.calculated_review_status(),
3120 'mergeable': {
3121 'status': merge_status[0],
3122 'message': unicode(merge_status[1]),
3123 },
3124 'source': {
3125 'clone_url': pull_request.source_repo.clone_url(),
3126 'repository': pull_request.source_repo.repo_name,
3127 'reference': {
3128 'name': pull_request.source_ref_parts.name,
3129 'type': pull_request.source_ref_parts.type,
3130 'commit_id': pull_request.source_ref_parts.commit_id,
3131 },
3132 },
3133 'target': {
3134 'clone_url': pull_request.target_repo.clone_url(),
3135 'repository': pull_request.target_repo.repo_name,
3136 'reference': {
3137 'name': pull_request.target_ref_parts.name,
3138 'type': pull_request.target_ref_parts.type,
3139 'commit_id': pull_request.target_ref_parts.commit_id,
3140 },
3141 },
3142 'author': pull_request.author.get_api_data(include_secrets=False,
3143 details='basic'),
3144 'reviewers': [
3145 {
3146 'user': reviewer.get_api_data(include_secrets=False,
3147 details='basic'),
3148 'review_status': st[0][1].status if st else 'not_reviewed',
3149 }
3150 for reviewer, st in pull_request.reviewers_statuses()
3151 ]
3152 }
3153
3154 return data
3155
3156 def __json__(self):
3157 return {
3158 'revisions': self.revisions,
3159 }
3160
3161 def calculated_review_status(self):
3162 # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html
3163 # because it's tricky on how to use ChangesetStatusModel from there
3164 warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning)
3165 from rhodecode.model.changeset_status import ChangesetStatusModel
3166 return ChangesetStatusModel().calculated_review_status(self)
3167
3168 def reviewers_statuses(self):
3169 warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
3170 from rhodecode.model.changeset_status import ChangesetStatusModel
3171 return ChangesetStatusModel().reviewers_statuses(self)
3172
3173
3174 class PullRequestVersion(Base, _PullRequestBase):
3175 __tablename__ = 'pull_request_versions'
3176 __table_args__ = (
3177 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3178 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3179 )
3180
3181 pull_request_version_id = Column(
3182 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3183 pull_request_id = Column(
3184 'pull_request_id', Integer(),
3185 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3186 pull_request = relationship('PullRequest')
3187
3188 def __repr__(self):
3189 if self.pull_request_version_id:
3190 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3191 else:
3192 return '<DB:PullRequestVersion at %#x>' % id(self)
3193
3194
3195 class PullRequestReviewers(Base, BaseModel):
3196 __tablename__ = 'pull_request_reviewers'
3197 __table_args__ = (
3198 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3199 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3200 )
3201
3202 def __init__(self, user=None, pull_request=None):
3203 self.user = user
3204 self.pull_request = pull_request
3205
3206 pull_requests_reviewers_id = Column(
3207 'pull_requests_reviewers_id', Integer(), nullable=False,
3208 primary_key=True)
3209 pull_request_id = Column(
3210 "pull_request_id", Integer(),
3211 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3212 user_id = Column(
3213 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3214
3215 user = relationship('User')
3216 pull_request = relationship('PullRequest')
3217
3218
3219 class Notification(Base, BaseModel):
3220 __tablename__ = 'notifications'
3221 __table_args__ = (
3222 Index('notification_type_idx', 'type'),
3223 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3224 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3225 )
3226
3227 TYPE_CHANGESET_COMMENT = u'cs_comment'
3228 TYPE_MESSAGE = u'message'
3229 TYPE_MENTION = u'mention'
3230 TYPE_REGISTRATION = u'registration'
3231 TYPE_PULL_REQUEST = u'pull_request'
3232 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3233
3234 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3235 subject = Column('subject', Unicode(512), nullable=True)
3236 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3237 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3238 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3239 type_ = Column('type', Unicode(255))
3240
3241 created_by_user = relationship('User')
3242 notifications_to_users = relationship('UserNotification', lazy='joined',
3243 cascade="all, delete, delete-orphan")
3244
3245 @property
3246 def recipients(self):
3247 return [x.user for x in UserNotification.query()\
3248 .filter(UserNotification.notification == self)\
3249 .order_by(UserNotification.user_id.asc()).all()]
3250
3251 @classmethod
3252 def create(cls, created_by, subject, body, recipients, type_=None):
3253 if type_ is None:
3254 type_ = Notification.TYPE_MESSAGE
3255
3256 notification = cls()
3257 notification.created_by_user = created_by
3258 notification.subject = subject
3259 notification.body = body
3260 notification.type_ = type_
3261 notification.created_on = datetime.datetime.now()
3262
3263 for u in recipients:
3264 assoc = UserNotification()
3265 assoc.notification = notification
3266
3267 # if created_by is inside recipients mark his notification
3268 # as read
3269 if u.user_id == created_by.user_id:
3270 assoc.read = True
3271
3272 u.notifications.append(assoc)
3273 Session().add(notification)
3274
3275 return notification
3276
3277 @property
3278 def description(self):
3279 from rhodecode.model.notification import NotificationModel
3280 return NotificationModel().make_description(self)
3281
3282
3283 class UserNotification(Base, BaseModel):
3284 __tablename__ = 'user_to_notification'
3285 __table_args__ = (
3286 UniqueConstraint('user_id', 'notification_id'),
3287 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3288 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3289 )
3290 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3291 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3292 read = Column('read', Boolean, default=False)
3293 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3294
3295 user = relationship('User', lazy="joined")
3296 notification = relationship('Notification', lazy="joined",
3297 order_by=lambda: Notification.created_on.desc(),)
3298
3299 def mark_as_read(self):
3300 self.read = True
3301 Session().add(self)
3302
3303
3304 class Gist(Base, BaseModel):
3305 __tablename__ = 'gists'
3306 __table_args__ = (
3307 Index('g_gist_access_id_idx', 'gist_access_id'),
3308 Index('g_created_on_idx', 'created_on'),
3309 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3310 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3311 )
3312 GIST_PUBLIC = u'public'
3313 GIST_PRIVATE = u'private'
3314 DEFAULT_FILENAME = u'gistfile1.txt'
3315
3316 ACL_LEVEL_PUBLIC = u'acl_public'
3317 ACL_LEVEL_PRIVATE = u'acl_private'
3318
3319 gist_id = Column('gist_id', Integer(), primary_key=True)
3320 gist_access_id = Column('gist_access_id', Unicode(250))
3321 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3322 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3323 gist_expires = Column('gist_expires', Float(53), nullable=False)
3324 gist_type = Column('gist_type', Unicode(128), nullable=False)
3325 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3326 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3327 acl_level = Column('acl_level', Unicode(128), nullable=True)
3328
3329 owner = relationship('User')
3330
3331 def __repr__(self):
3332 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3333
3334 @classmethod
3335 def get_or_404(cls, id_):
3336 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3337 if not res:
3338 raise HTTPNotFound
3339 return res
3340
3341 @classmethod
3342 def get_by_access_id(cls, gist_access_id):
3343 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3344
3345 def gist_url(self):
3346 import rhodecode
3347 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3348 if alias_url:
3349 return alias_url.replace('{gistid}', self.gist_access_id)
3350
3351 return url('gist', gist_id=self.gist_access_id, qualified=True)
3352
3353 @classmethod
3354 def base_path(cls):
3355 """
3356 Returns base path when all gists are stored
3357
3358 :param cls:
3359 """
3360 from rhodecode.model.gist import GIST_STORE_LOC
3361 q = Session().query(RhodeCodeUi)\
3362 .filter(RhodeCodeUi.ui_key == URL_SEP)
3363 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3364 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3365
3366 def get_api_data(self):
3367 """
3368 Common function for generating gist related data for API
3369 """
3370 gist = self
3371 data = {
3372 'gist_id': gist.gist_id,
3373 'type': gist.gist_type,
3374 'access_id': gist.gist_access_id,
3375 'description': gist.gist_description,
3376 'url': gist.gist_url(),
3377 'expires': gist.gist_expires,
3378 'created_on': gist.created_on,
3379 'modified_at': gist.modified_at,
3380 'content': None,
3381 'acl_level': gist.acl_level,
3382 }
3383 return data
3384
3385 def __json__(self):
3386 data = dict(
3387 )
3388 data.update(self.get_api_data())
3389 return data
3390 # SCM functions
3391
3392 def scm_instance(self, **kwargs):
3393 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3394 return get_vcs_instance(
3395 repo_path=safe_str(full_repo_path), create=False)
3396
3397
3398 class DbMigrateVersion(Base, BaseModel):
3399 __tablename__ = 'db_migrate_version'
3400 __table_args__ = (
3401 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3402 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3403 )
3404 repository_id = Column('repository_id', String(250), primary_key=True)
3405 repository_path = Column('repository_path', Text)
3406 version = Column('version', Integer)
3407
3408
3409 class ExternalIdentity(Base, BaseModel):
3410 __tablename__ = 'external_identities'
3411 __table_args__ = (
3412 Index('local_user_id_idx', 'local_user_id'),
3413 Index('external_id_idx', 'external_id'),
3414 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3415 'mysql_charset': 'utf8'})
3416
3417 external_id = Column('external_id', Unicode(255), default=u'',
3418 primary_key=True)
3419 external_username = Column('external_username', Unicode(1024), default=u'')
3420 local_user_id = Column('local_user_id', Integer(),
3421 ForeignKey('users.user_id'), primary_key=True)
3422 provider_name = Column('provider_name', Unicode(255), default=u'',
3423 primary_key=True)
3424 access_token = Column('access_token', String(1024), default=u'')
3425 alt_token = Column('alt_token', String(1024), default=u'')
3426 token_secret = Column('token_secret', String(1024), default=u'')
3427
3428 @classmethod
3429 def by_external_id_and_provider(cls, external_id, provider_name,
3430 local_user_id=None):
3431 """
3432 Returns ExternalIdentity instance based on search params
3433
3434 :param external_id:
3435 :param provider_name:
3436 :return: ExternalIdentity
3437 """
3438 query = cls.query()
3439 query = query.filter(cls.external_id == external_id)
3440 query = query.filter(cls.provider_name == provider_name)
3441 if local_user_id:
3442 query = query.filter(cls.local_user_id == local_user_id)
3443 return query.first()
3444
3445 @classmethod
3446 def user_by_external_id_and_provider(cls, external_id, provider_name):
3447 """
3448 Returns User instance based on search params
3449
3450 :param external_id:
3451 :param provider_name:
3452 :return: User
3453 """
3454 query = User.query()
3455 query = query.filter(cls.external_id == external_id)
3456 query = query.filter(cls.provider_name == provider_name)
3457 query = query.filter(User.user_id == cls.local_user_id)
3458 return query.first()
3459
3460 @classmethod
3461 def by_local_user_id(cls, local_user_id):
3462 """
3463 Returns all tokens for user
3464
3465 :param local_user_id:
3466 :return: ExternalIdentity
3467 """
3468 query = cls.query()
3469 query = query.filter(cls.local_user_id == local_user_id)
3470 return query
3471
3472
3473 class Integration(Base, BaseModel):
3474 __tablename__ = 'integrations'
3475 __table_args__ = (
3476 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3477 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3478 )
3479
3480 integration_id = Column('integration_id', Integer(), primary_key=True)
3481 integration_type = Column('integration_type', String(255))
3482 enabled = Column('enabled', Boolean(), nullable=False)
3483 name = Column('name', String(255), nullable=False)
3484 child_repos_only = Column('child_repos_only', Boolean(), nullable=True)
3485
3486 settings = Column(
3487 'settings_json', MutationObj.as_mutable(
3488 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3489 repo_id = Column(
3490 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3491 nullable=True, unique=None, default=None)
3492 repo = relationship('Repository', lazy='joined')
3493
3494 repo_group_id = Column(
3495 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3496 nullable=True, unique=None, default=None)
3497 repo_group = relationship('RepoGroup', lazy='joined')
3498
3499 @hybrid_property
3500 def scope(self):
3501 if self.repo:
3502 return self.repo
3503 if self.repo_group:
3504 return self.repo_group
3505 if self.child_repos_only:
3506 return 'root_repos'
3507 return 'global'
3508
3509 @scope.setter
3510 def scope(self, value):
3511 self.repo = None
3512 self.repo_id = None
3513 self.repo_group_id = None
3514 self.repo_group = None
3515 self.child_repos_only = None
3516 if isinstance(value, Repository):
3517 self.repo = value
3518 elif isinstance(value, RepoGroup):
3519 self.repo_group = value
3520 elif value == 'root_repos':
3521 self.child_repos_only = True
3522 elif value == 'global':
3523 pass
3524 else:
3525 raise Exception("invalid scope: %s, must be one of "
3526 "['global', 'root_repos', <RepoGroup>. <Repository>]" % value)
3527
3528 def __repr__(self):
3529 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
@@ -0,0 +1,35 b''
1 import logging
2 import datetime
3
4 from sqlalchemy import *
5 from sqlalchemy.exc import DatabaseError
6 from sqlalchemy.orm import relation, backref, class_mapper, joinedload
7 from sqlalchemy.orm.session import Session
8 from sqlalchemy.ext.declarative import declarative_base
9
10 from rhodecode.lib.dbmigrate.migrate import *
11 from rhodecode.lib.dbmigrate.migrate.changeset import *
12 from rhodecode.lib.utils2 import str2bool
13
14 from rhodecode.model.meta import Base
15 from rhodecode.model import meta
16 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
17
18 log = logging.getLogger(__name__)
19
20
21 def upgrade(migrate_engine):
22 """
23 Upgrade operations go here.
24 Don't create your own engine; bind migrate_engine to your metadata
25 """
26 _reset_base(migrate_engine)
27 from rhodecode.lib.dbmigrate.schema import db_4_4_0_1
28
29 tbl = db_4_4_0_1.Integration.__table__
30 child_repos_only = db_4_4_0_1.Integration.child_repos_only
31 child_repos_only.create(table=tbl)
32
33 def downgrade(migrate_engine):
34 meta = MetaData()
35 meta.bind = migrate_engine
@@ -0,0 +1,187 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import os
22
23 import deform
24 import colander
25
26 from rhodecode.translation import _
27 from rhodecode.model.db import Repository, RepoGroup
28 from rhodecode.model.validation_schema import validators, preparers
29
30
31 def integration_scope_choices(permissions):
32 """
33 Return list of (value, label) choices for integration scopes depending on
34 the permissions
35 """
36 result = [('', _('Pick a scope:'))]
37 if 'hg.admin' in permissions['global']:
38 result.extend([
39 ('global', _('Global (all repositories)')),
40 ('root_repos', _('Top level repositories only')),
41 ])
42
43 repo_choices = [
44 ('repo:%s' % repo_name, '/' + repo_name)
45 for repo_name, repo_perm
46 in permissions['repositories'].items()
47 if repo_perm == 'repository.admin'
48 ]
49 repogroup_choices = [
50 ('repogroup:%s' % repo_group_name, '/' + repo_group_name + ' (group)')
51 for repo_group_name, repo_group_perm
52 in permissions['repositories_groups'].items()
53 if repo_group_perm == 'group.admin'
54 ]
55 result.extend(
56 sorted(repogroup_choices + repo_choices,
57 key=lambda (choice, label): choice.split(':', 1)[1]
58 )
59 )
60 return result
61
62
63 @colander.deferred
64 def deferred_integration_scopes_validator(node, kw):
65 perms = kw.get('permissions')
66 def _scope_validator(_node, scope):
67 is_super_admin = 'hg.admin' in perms['global']
68
69 if scope in ('global', 'root_repos'):
70 if is_super_admin:
71 return True
72 msg = _('Only superadmins can create global integrations')
73 raise colander.Invalid(_node, msg)
74 elif isinstance(scope, Repository):
75 if (is_super_admin or perms['repositories'].get(
76 scope.repo_name) == 'repository.admin'):
77 return True
78 msg = _('Only repo admins can create integrations')
79 raise colander.Invalid(_node, msg)
80 elif isinstance(scope, RepoGroup):
81 if (is_super_admin or perms['repositories_groups'].get(
82 scope.group_name) == 'group.admin'):
83 return True
84
85 msg = _('Only repogroup admins can create integrations')
86 raise colander.Invalid(_node, msg)
87
88 msg = _('Invalid integration scope: %s' % scope)
89 raise colander.Invalid(node, msg)
90
91 return _scope_validator
92
93
94 @colander.deferred
95 def deferred_integration_scopes_widget(node, kw):
96 if kw.get('no_scope'):
97 return deform.widget.TextInputWidget(readonly=True)
98
99 choices = integration_scope_choices(kw.get('permissions'))
100 widget = deform.widget.Select2Widget(values=choices)
101 return widget
102
103 class IntegrationScope(colander.SchemaType):
104 def serialize(self, node, appstruct):
105 if appstruct is colander.null:
106 return colander.null
107
108 if isinstance(appstruct, Repository):
109 return 'repo:%s' % appstruct.repo_name
110 elif isinstance(appstruct, RepoGroup):
111 return 'repogroup:%s' % appstruct.group_name
112 elif appstruct in ('global', 'root_repos'):
113 return appstruct
114 raise colander.Invalid(node, '%r is not a valid scope' % appstruct)
115
116 def deserialize(self, node, cstruct):
117 if cstruct is colander.null:
118 return colander.null
119
120 if cstruct.startswith('repo:'):
121 repo = Repository.get_by_repo_name(cstruct.split(':')[1])
122 if repo:
123 return repo
124 elif cstruct.startswith('repogroup:'):
125 repo_group = RepoGroup.get_by_group_name(cstruct.split(':')[1])
126 if repo_group:
127 return repo_group
128 elif cstruct in ('global', 'root_repos'):
129 return cstruct
130
131 raise colander.Invalid(node, '%r is not a valid scope' % cstruct)
132
133 class IntegrationOptionsSchemaBase(colander.MappingSchema):
134
135 name = colander.SchemaNode(
136 colander.String(),
137 description=_('Short name for this integration.'),
138 missing=colander.required,
139 title=_('Integration name'),
140 )
141
142 scope = colander.SchemaNode(
143 IntegrationScope(),
144 description=_(
145 'Scope of the integration. Group scope means the integration '
146 ' runs on all child repos of that group.'),
147 title=_('Integration scope'),
148 validator=deferred_integration_scopes_validator,
149 widget=deferred_integration_scopes_widget,
150 missing=colander.required,
151 )
152
153 enabled = colander.SchemaNode(
154 colander.Bool(),
155 default=True,
156 description=_('Enable or disable this integration.'),
157 missing=False,
158 title=_('Enabled'),
159 )
160
161
162
163 def make_integration_schema(IntegrationType, settings=None):
164 """
165 Return a colander schema for an integration type
166
167 :param IntegrationType: the integration type class
168 :param settings: existing integration settings dict (optional)
169 """
170
171 settings = settings or {}
172 settings_schema = IntegrationType(settings=settings).settings_schema()
173
174 class IntegrationSchema(colander.Schema):
175 options = IntegrationOptionsSchemaBase()
176
177 schema = IntegrationSchema()
178 schema['options'].title = _('General integration options')
179
180 settings_schema.name = 'settings'
181 settings_schema.title = _('{integration_type} settings').format(
182 integration_type=IntegrationType.display_name)
183 schema.add(settings_schema)
184
185 return schema
186
187
@@ -0,0 +1,66 b''
1 ## -*- coding: utf-8 -*-
2 <%inherit file="base.html"/>
3 <%namespace name="widgets" file="/widgets.html"/>
4
5 <%def name="breadcrumbs_links()">
6 %if c.repo:
7 ${h.link_to('Settings',h.url('edit_repo', repo_name=c.repo.repo_name))}
8 &raquo;
9 ${h.link_to(_('Integrations'),request.route_url(route_name='repo_integrations_home', repo_name=c.repo.repo_name))}
10 %elif c.repo_group:
11 ${h.link_to(_('Admin'),h.url('admin_home'))}
12 &raquo;
13 ${h.link_to(_('Repository Groups'),h.url('repo_groups'))}
14 &raquo;
15 ${h.link_to(c.repo_group.group_name,h.url('edit_repo_group', group_name=c.repo_group.group_name))}
16 &raquo;
17 ${h.link_to(_('Integrations'),request.route_url(route_name='repo_group_integrations_home', repo_group_name=c.repo_group.group_name))}
18 %else:
19 ${h.link_to(_('Admin'),h.url('admin_home'))}
20 &raquo;
21 ${h.link_to(_('Settings'),h.url('admin_settings'))}
22 &raquo;
23 ${h.link_to(_('Integrations'),request.route_url(route_name='global_integrations_home'))}
24 %endif
25 &raquo;
26 ${_('Create new integration')}
27 </%def>
28 <%widgets:panel class_='integrations'>
29 <%def name="title()">
30 %if c.repo:
31 ${_('Create New Integration for repository: {repo_name}').format(repo_name=c.repo.repo_name)}
32 %elif c.repo_group:
33 ${_('Create New Integration for repository group: {repo_group_name}').format(repo_group_name=c.repo_group.group_name)}
34 %else:
35 ${_('Create New Global Integration')}
36 %endif
37 </%def>
38
39 %for integration, IntegrationType in available_integrations.items():
40 <%
41 if c.repo:
42 create_url = request.route_path('repo_integrations_create',
43 repo_name=c.repo.repo_name,
44 integration=integration)
45 elif c.repo_group:
46 create_url = request.route_path('repo_group_integrations_create',
47 repo_group_name=c.repo_group.group_name,
48 integration=integration)
49 else:
50 create_url = request.route_path('global_integrations_create',
51 integration=integration)
52 %>
53 <a href="${create_url}" class="integration-box">
54 <%widgets:panel>
55 <h2>
56 <div class="integration-icon">
57 ${IntegrationType.icon|n}
58 </div>
59 ${IntegrationType.display_name}
60 </h2>
61 ${IntegrationType.description or _('No description available')}
62 </%widgets:panel>
63 </a>
64 %endfor
65 <div style="clear:both"></div>
66 </%widgets:panel>
@@ -0,0 +1,4 b''
1 <div class="form-control readonly"
2 id="${oid|field.oid}">
3 ${cstruct}
4 </div>
@@ -0,0 +1,262 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import mock
22 import pytest
23 from webob.exc import HTTPNotFound
24
25 import rhodecode
26 from rhodecode.model.db import Integration
27 from rhodecode.model.meta import Session
28 from rhodecode.tests import assert_session_flash, url, TEST_USER_ADMIN_LOGIN
29 from rhodecode.tests.utils import AssertResponse
30 from rhodecode.integrations import integration_type_registry
31 from rhodecode.config.routing import ADMIN_PREFIX
32
33
34 @pytest.mark.usefixtures('app', 'autologin_user')
35 class TestIntegrationsView(object):
36 pass
37
38
39 class TestGlobalIntegrationsView(TestIntegrationsView):
40 def test_index_no_integrations(self, app):
41 url = ADMIN_PREFIX + '/integrations'
42 response = app.get(url)
43
44 assert response.status_code == 200
45 assert 'exist yet' in response.body
46
47 def test_index_with_integrations(self, app, global_integration_stub):
48 url = ADMIN_PREFIX + '/integrations'
49 response = app.get(url)
50
51 assert response.status_code == 200
52 assert 'exist yet' not in response.body
53 assert global_integration_stub.name in response.body
54
55 def test_new_integration_page(self, app):
56 url = ADMIN_PREFIX + '/integrations/new'
57
58 response = app.get(url)
59
60 assert response.status_code == 200
61
62 for integration_key in integration_type_registry:
63 nurl = (ADMIN_PREFIX + '/integrations/{integration}/new').format(
64 integration=integration_key)
65 assert nurl in response.body
66
67 @pytest.mark.parametrize(
68 'IntegrationType', integration_type_registry.values())
69 def test_get_create_integration_page(self, app, IntegrationType):
70 url = ADMIN_PREFIX + '/integrations/{integration_key}/new'.format(
71 integration_key=IntegrationType.key)
72
73 response = app.get(url)
74
75 assert response.status_code == 200
76 assert IntegrationType.display_name in response.body
77
78 def test_post_integration_page(self, app, StubIntegrationType, csrf_token,
79 test_repo_group, backend_random):
80 url = ADMIN_PREFIX + '/integrations/{integration_key}/new'.format(
81 integration_key=StubIntegrationType.key)
82
83 _post_integration_test_helper(app, url, csrf_token, admin_view=True,
84 repo=backend_random.repo, repo_group=test_repo_group)
85
86
87 class TestRepoGroupIntegrationsView(TestIntegrationsView):
88 def test_index_no_integrations(self, app, test_repo_group):
89 url = '/{repo_group_name}/settings/integrations'.format(
90 repo_group_name=test_repo_group.group_name)
91 response = app.get(url)
92
93 assert response.status_code == 200
94 assert 'exist yet' in response.body
95
96 def test_index_with_integrations(self, app, test_repo_group,
97 repogroup_integration_stub):
98 url = '/{repo_group_name}/settings/integrations'.format(
99 repo_group_name=test_repo_group.group_name)
100
101 stub_name = repogroup_integration_stub.name
102 response = app.get(url)
103
104 assert response.status_code == 200
105 assert 'exist yet' not in response.body
106 assert stub_name in response.body
107
108 def test_new_integration_page(self, app, test_repo_group):
109 repo_group_name = test_repo_group.group_name
110 url = '/{repo_group_name}/settings/integrations/new'.format(
111 repo_group_name=test_repo_group.group_name)
112
113 response = app.get(url)
114
115 assert response.status_code == 200
116
117 for integration_key in integration_type_registry:
118 nurl = ('/{repo_group_name}/settings/integrations'
119 '/{integration}/new').format(
120 repo_group_name=repo_group_name,
121 integration=integration_key)
122
123 assert nurl in response.body
124
125 @pytest.mark.parametrize(
126 'IntegrationType', integration_type_registry.values())
127 def test_get_create_integration_page(self, app, test_repo_group,
128 IntegrationType):
129 repo_group_name = test_repo_group.group_name
130 url = ('/{repo_group_name}/settings/integrations/{integration_key}/new'
131 ).format(repo_group_name=repo_group_name,
132 integration_key=IntegrationType.key)
133
134 response = app.get(url)
135
136 assert response.status_code == 200
137 assert IntegrationType.display_name in response.body
138
139 def test_post_integration_page(self, app, test_repo_group, backend_random,
140 StubIntegrationType, csrf_token):
141 repo_group_name = test_repo_group.group_name
142 url = ('/{repo_group_name}/settings/integrations/{integration_key}/new'
143 ).format(repo_group_name=repo_group_name,
144 integration_key=StubIntegrationType.key)
145
146 _post_integration_test_helper(app, url, csrf_token, admin_view=False,
147 repo=backend_random.repo, repo_group=test_repo_group)
148
149
150 class TestRepoIntegrationsView(TestIntegrationsView):
151 def test_index_no_integrations(self, app, backend_random):
152 url = '/{repo_name}/settings/integrations'.format(
153 repo_name=backend_random.repo.repo_name)
154 response = app.get(url)
155
156 assert response.status_code == 200
157 assert 'exist yet' in response.body
158
159 def test_index_with_integrations(self, app, repo_integration_stub):
160 url = '/{repo_name}/settings/integrations'.format(
161 repo_name=repo_integration_stub.repo.repo_name)
162 stub_name = repo_integration_stub.name
163
164 response = app.get(url)
165
166 assert response.status_code == 200
167 assert stub_name in response.body
168 assert 'exist yet' not in response.body
169
170 def test_new_integration_page(self, app, backend_random):
171 repo_name = backend_random.repo.repo_name
172 url = '/{repo_name}/settings/integrations/new'.format(
173 repo_name=repo_name)
174
175 response = app.get(url)
176
177 assert response.status_code == 200
178
179 for integration_key in integration_type_registry:
180 nurl = ('/{repo_name}/settings/integrations'
181 '/{integration}/new').format(
182 repo_name=repo_name,
183 integration=integration_key)
184
185 assert nurl in response.body
186
187 @pytest.mark.parametrize(
188 'IntegrationType', integration_type_registry.values())
189 def test_get_create_integration_page(self, app, backend_random,
190 IntegrationType):
191 repo_name = backend_random.repo.repo_name
192 url = '/{repo_name}/settings/integrations/{integration_key}/new'.format(
193 repo_name=repo_name, integration_key=IntegrationType.key)
194
195 response = app.get(url)
196
197 assert response.status_code == 200
198 assert IntegrationType.display_name in response.body
199
200 def test_post_integration_page(self, app, backend_random, test_repo_group,
201 StubIntegrationType, csrf_token):
202 repo_name = backend_random.repo.repo_name
203 url = '/{repo_name}/settings/integrations/{integration_key}/new'.format(
204 repo_name=repo_name, integration_key=StubIntegrationType.key)
205
206 _post_integration_test_helper(app, url, csrf_token, admin_view=False,
207 repo=backend_random.repo, repo_group=test_repo_group)
208
209
210 def _post_integration_test_helper(app, url, csrf_token, repo, repo_group,
211 admin_view):
212 """
213 Posts form data to create integration at the url given then deletes it and
214 checks if the redirect url is correct.
215 """
216
217 app.post(url, params={}, status=403) # missing csrf check
218 response = app.post(url, params={'csrf_token': csrf_token})
219 assert response.status_code == 200
220 assert 'Errors exist' in response.body
221
222 scopes_destinations = [
223 ('global',
224 ADMIN_PREFIX + '/integrations'),
225 ('root_repos',
226 ADMIN_PREFIX + '/integrations'),
227 ('repo:%s' % repo.repo_name,
228 '/%s/settings/integrations' % repo.repo_name),
229 ('repogroup:%s' % repo_group.group_name,
230 '/%s/settings/integrations' % repo_group.group_name),
231 ]
232
233 for scope, destination in scopes_destinations:
234 if admin_view:
235 destination = ADMIN_PREFIX + '/integrations'
236
237 form_data = [
238 ('csrf_token', csrf_token),
239 ('__start__', 'options:mapping'),
240 ('name', 'test integration'),
241 ('scope', scope),
242 ('enabled', 'true'),
243 ('__end__', 'options:mapping'),
244 ('__start__', 'settings:mapping'),
245 ('test_int_field', '34'),
246 ('test_string_field', ''), # empty value on purpose as it's required
247 ('__end__', 'settings:mapping'),
248 ]
249 errors_response = app.post(url, form_data)
250 assert 'Errors exist' in errors_response.body
251
252 form_data[-2] = ('test_string_field', 'data!')
253 assert Session().query(Integration).count() == 0
254 created_response = app.post(url, form_data)
255 assert Session().query(Integration).count() == 1
256
257 delete_response = app.post(
258 created_response.location,
259 params={'csrf_token': csrf_token, 'delete': 'delete'})
260
261 assert Session().query(Integration).count() == 0
262 assert delete_response.location.endswith(destination)
@@ -0,0 +1,120 b''
1 # -*- coding: utf-8 -*-
2
3 # Copyright (C) 2016-2016 RhodeCode GmbH
4 #
5 # This program is free software: you can redistribute it and/or modify
6 # it under the terms of the GNU Affero General Public License, version 3
7 # (only), as published by the Free Software Foundation.
8 #
9 # This program is distributed in the hope that it will be useful,
10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 # GNU General Public License for more details.
13 #
14 # You should have received a copy of the GNU Affero General Public License
15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 #
17 # This program is dual-licensed. If you wish to learn more about the
18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20
21 import colander
22 import pytest
23
24 from rhodecode.model import validation_schema
25
26 from rhodecode.integrations import integration_type_registry
27 from rhodecode.integrations.types.base import IntegrationTypeBase
28 from rhodecode.model.validation_schema.schemas.integration_schema import (
29 make_integration_schema
30 )
31
32
33 @pytest.mark.usefixtures('app', 'autologin_user')
34 class TestIntegrationSchema(object):
35
36 def test_deserialize_integration_schema_perms(self, backend_random,
37 test_repo_group,
38 StubIntegrationType):
39
40 repo = backend_random.repo
41 repo_group = test_repo_group
42
43
44 empty_perms_dict = {
45 'global': [],
46 'repositories': {},
47 'repositories_groups': {},
48 }
49
50 perms_tests = {
51 ('repo:%s' % repo.repo_name, repo): [
52 ({}, False),
53 ({'global': ['hg.admin']}, True),
54 ({'global': []}, False),
55 ({'repositories': {repo.repo_name: 'repository.admin'}}, True),
56 ({'repositories': {repo.repo_name: 'repository.read'}}, False),
57 ({'repositories': {repo.repo_name: 'repository.write'}}, False),
58 ({'repositories': {repo.repo_name: 'repository.none'}}, False),
59 ],
60 ('repogroup:%s' % repo_group.group_name, repo_group): [
61 ({}, False),
62 ({'global': ['hg.admin']}, True),
63 ({'global': []}, False),
64 ({'repositories_groups':
65 {repo_group.group_name: 'group.admin'}}, True),
66 ({'repositories_groups':
67 {repo_group.group_name: 'group.read'}}, False),
68 ({'repositories_groups':
69 {repo_group.group_name: 'group.write'}}, False),
70 ({'repositories_groups':
71 {repo_group.group_name: 'group.none'}}, False),
72 ],
73 ('global', 'global'): [
74 ({}, False),
75 ({'global': ['hg.admin']}, True),
76 ({'global': []}, False),
77 ],
78 ('root_repos', 'root_repos'): [
79 ({}, False),
80 ({'global': ['hg.admin']}, True),
81 ({'global': []}, False),
82 ],
83 }
84
85 for (scope_input, scope_output), perms_allowed in perms_tests.items():
86 for perms_update, allowed in perms_allowed:
87 perms = dict(empty_perms_dict, **perms_update)
88
89 schema = make_integration_schema(
90 IntegrationType=StubIntegrationType
91 ).bind(permissions=perms)
92
93 input_data = {
94 'options': {
95 'enabled': 'true',
96 'scope': scope_input,
97 'name': 'test integration',
98 },
99 'settings': {
100 'test_string_field': 'stringy',
101 'test_int_field': '100',
102 }
103 }
104
105 if not allowed:
106 with pytest.raises(colander.Invalid):
107 schema.deserialize(input_data)
108 else:
109 assert schema.deserialize(input_data) == {
110 'options': {
111 'enabled': True,
112 'scope': scope_output,
113 'name': 'test integration',
114 },
115 'settings': {
116 'test_string_field': 'stringy',
117 'test_int_field': 100,
118 }
119 }
120
@@ -1,62 +1,62 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21 """
22 22
23 23 RhodeCode, a web based repository management software
24 24 versioning implementation: http://www.python.org/dev/peps/pep-0386/
25 25 """
26 26
27 27 import os
28 28 import sys
29 29 import platform
30 30
31 31 VERSION = tuple(open(os.path.join(
32 32 os.path.dirname(__file__), 'VERSION')).read().split('.'))
33 33
34 34 BACKENDS = {
35 35 'hg': 'Mercurial repository',
36 36 'git': 'Git repository',
37 37 'svn': 'Subversion repository',
38 38 }
39 39
40 40 CELERY_ENABLED = False
41 41 CELERY_EAGER = False
42 42
43 43 # link to config for pylons
44 44 CONFIG = {}
45 45
46 46 # Populated with the settings dictionary from application init in
47 47 # rhodecode.conf.environment.load_pyramid_environment
48 48 PYRAMID_SETTINGS = {}
49 49
50 50 # Linked module for extensions
51 51 EXTENSIONS = {}
52 52
53 53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 56 # defines current db version for migrations
54 __dbversion__ = 57 # defines current db version for migrations
55 55 __platform__ = platform.system()
56 56 __license__ = 'AGPLv3, and Commercial License'
57 57 __author__ = 'RhodeCode GmbH'
58 58 __url__ = 'http://rhodecode.com'
59 59
60 60 is_windows = __platform__ in ['Windows']
61 61 is_unix = not is_windows
62 62 is_test = False
@@ -1,1159 +1,1160 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 Routes configuration
23 23
24 24 The more specific and detailed routes should be defined first so they
25 25 may take precedent over the more generic routes. For more information
26 26 refer to the routes manual at http://routes.groovie.org/docs/
27 27
28 28 IMPORTANT: if you change any routing here, make sure to take a look at lib/base.py
29 29 and _route_name variable which uses some of stored naming here to do redirects.
30 30 """
31 31 import os
32 32 import re
33 33 from routes import Mapper
34 34
35 35 from rhodecode.config import routing_links
36 36
37 37 # prefix for non repository related links needs to be prefixed with `/`
38 38 ADMIN_PREFIX = '/_admin'
39 39 STATIC_FILE_PREFIX = '/_static'
40 40
41 41 # Default requirements for URL parts
42 42 URL_NAME_REQUIREMENTS = {
43 43 # group name can have a slash in them, but they must not end with a slash
44 44 'group_name': r'.*?[^/]',
45 'repo_group_name': r'.*?[^/]',
45 46 # repo names can have a slash in them, but they must not end with a slash
46 47 'repo_name': r'.*?[^/]',
47 48 # file path eats up everything at the end
48 49 'f_path': r'.*',
49 50 # reference types
50 51 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
51 52 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
52 53 }
53 54
54 55
55 56 def add_route_requirements(route_path, requirements):
56 57 """
57 58 Adds regex requirements to pyramid routes using a mapping dict
58 59
59 60 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
60 61 '/{action}/{id:\d+}'
61 62
62 63 """
63 64 for key, regex in requirements.items():
64 65 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
65 66 return route_path
66 67
67 68
68 69 class JSRoutesMapper(Mapper):
69 70 """
70 71 Wrapper for routes.Mapper to make pyroutes compatible url definitions
71 72 """
72 73 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
73 74 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
74 75 def __init__(self, *args, **kw):
75 76 super(JSRoutesMapper, self).__init__(*args, **kw)
76 77 self._jsroutes = []
77 78
78 79 def connect(self, *args, **kw):
79 80 """
80 81 Wrapper for connect to take an extra argument jsroute=True
81 82
82 83 :param jsroute: boolean, if True will add the route to the pyroutes list
83 84 """
84 85 if kw.pop('jsroute', False):
85 86 if not self._named_route_regex.match(args[0]):
86 87 raise Exception('only named routes can be added to pyroutes')
87 88 self._jsroutes.append(args[0])
88 89
89 90 super(JSRoutesMapper, self).connect(*args, **kw)
90 91
91 92 def _extract_route_information(self, route):
92 93 """
93 94 Convert a route into tuple(name, path, args), eg:
94 95 ('user_profile', '/profile/%(username)s', ['username'])
95 96 """
96 97 routepath = route.routepath
97 98 def replace(matchobj):
98 99 if matchobj.group(1):
99 100 return "%%(%s)s" % matchobj.group(1).split(':')[0]
100 101 else:
101 102 return "%%(%s)s" % matchobj.group(2)
102 103
103 104 routepath = self._argument_prog.sub(replace, routepath)
104 105 return (
105 106 route.name,
106 107 routepath,
107 108 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
108 109 for arg in self._argument_prog.findall(route.routepath)]
109 110 )
110 111
111 112 def jsroutes(self):
112 113 """
113 114 Return a list of pyroutes.js compatible routes
114 115 """
115 116 for route_name in self._jsroutes:
116 117 yield self._extract_route_information(self._routenames[route_name])
117 118
118 119
119 120 def make_map(config):
120 121 """Create, configure and return the routes Mapper"""
121 122 rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
122 123 always_scan=config['debug'])
123 124 rmap.minimization = False
124 125 rmap.explicit = False
125 126
126 127 from rhodecode.lib.utils2 import str2bool
127 128 from rhodecode.model import repo, repo_group
128 129
129 130 def check_repo(environ, match_dict):
130 131 """
131 132 check for valid repository for proper 404 handling
132 133
133 134 :param environ:
134 135 :param match_dict:
135 136 """
136 137 repo_name = match_dict.get('repo_name')
137 138
138 139 if match_dict.get('f_path'):
139 140 # fix for multiple initial slashes that causes errors
140 141 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
141 142 repo_model = repo.RepoModel()
142 143 by_name_match = repo_model.get_by_repo_name(repo_name)
143 144 # if we match quickly from database, short circuit the operation,
144 145 # and validate repo based on the type.
145 146 if by_name_match:
146 147 return True
147 148
148 149 by_id_match = repo_model.get_repo_by_id(repo_name)
149 150 if by_id_match:
150 151 repo_name = by_id_match.repo_name
151 152 match_dict['repo_name'] = repo_name
152 153 return True
153 154
154 155 return False
155 156
156 157 def check_group(environ, match_dict):
157 158 """
158 159 check for valid repository group path for proper 404 handling
159 160
160 161 :param environ:
161 162 :param match_dict:
162 163 """
163 164 repo_group_name = match_dict.get('group_name')
164 165 repo_group_model = repo_group.RepoGroupModel()
165 166 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
166 167 if by_name_match:
167 168 return True
168 169
169 170 return False
170 171
171 172 def check_user_group(environ, match_dict):
172 173 """
173 174 check for valid user group for proper 404 handling
174 175
175 176 :param environ:
176 177 :param match_dict:
177 178 """
178 179 return True
179 180
180 181 def check_int(environ, match_dict):
181 182 return match_dict.get('id').isdigit()
182 183
183 184
184 185 #==========================================================================
185 186 # CUSTOM ROUTES HERE
186 187 #==========================================================================
187 188
188 189 # MAIN PAGE
189 190 rmap.connect('home', '/', controller='home', action='index', jsroute=True)
190 191 rmap.connect('goto_switcher_data', '/_goto_data', controller='home',
191 192 action='goto_switcher_data')
192 193 rmap.connect('repo_list_data', '/_repos', controller='home',
193 194 action='repo_list_data')
194 195
195 196 rmap.connect('user_autocomplete_data', '/_users', controller='home',
196 197 action='user_autocomplete_data', jsroute=True)
197 198 rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
198 199 action='user_group_autocomplete_data')
199 200
200 201 rmap.connect(
201 202 'user_profile', '/_profiles/{username}', controller='users',
202 203 action='user_profile')
203 204
204 205 # TODO: johbo: Static links, to be replaced by our redirection mechanism
205 206 rmap.connect('rst_help',
206 207 'http://docutils.sourceforge.net/docs/user/rst/quickref.html',
207 208 _static=True)
208 209 rmap.connect('markdown_help',
209 210 'http://daringfireball.net/projects/markdown/syntax',
210 211 _static=True)
211 212 rmap.connect('rhodecode_official', 'https://rhodecode.com', _static=True)
212 213 rmap.connect('rhodecode_support', 'https://rhodecode.com/help/', _static=True)
213 214 rmap.connect('rhodecode_translations', 'https://rhodecode.com/translate/enterprise', _static=True)
214 215 # TODO: anderson - making this a static link since redirect won't play
215 216 # nice with POST requests
216 217 rmap.connect('enterprise_license_convert_from_old',
217 218 'https://rhodecode.com/u/license-upgrade',
218 219 _static=True)
219 220
220 221 routing_links.connect_redirection_links(rmap)
221 222
222 223 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
223 224 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
224 225
225 226 # ADMIN REPOSITORY ROUTES
226 227 with rmap.submapper(path_prefix=ADMIN_PREFIX,
227 228 controller='admin/repos') as m:
228 229 m.connect('repos', '/repos',
229 230 action='create', conditions={'method': ['POST']})
230 231 m.connect('repos', '/repos',
231 232 action='index', conditions={'method': ['GET']})
232 233 m.connect('new_repo', '/create_repository', jsroute=True,
233 234 action='create_repository', conditions={'method': ['GET']})
234 235 m.connect('/repos/{repo_name}',
235 236 action='update', conditions={'method': ['PUT'],
236 237 'function': check_repo},
237 238 requirements=URL_NAME_REQUIREMENTS)
238 239 m.connect('delete_repo', '/repos/{repo_name}',
239 240 action='delete', conditions={'method': ['DELETE']},
240 241 requirements=URL_NAME_REQUIREMENTS)
241 242 m.connect('repo', '/repos/{repo_name}',
242 243 action='show', conditions={'method': ['GET'],
243 244 'function': check_repo},
244 245 requirements=URL_NAME_REQUIREMENTS)
245 246
246 247 # ADMIN REPOSITORY GROUPS ROUTES
247 248 with rmap.submapper(path_prefix=ADMIN_PREFIX,
248 249 controller='admin/repo_groups') as m:
249 250 m.connect('repo_groups', '/repo_groups',
250 251 action='create', conditions={'method': ['POST']})
251 252 m.connect('repo_groups', '/repo_groups',
252 253 action='index', conditions={'method': ['GET']})
253 254 m.connect('new_repo_group', '/repo_groups/new',
254 255 action='new', conditions={'method': ['GET']})
255 256 m.connect('update_repo_group', '/repo_groups/{group_name}',
256 257 action='update', conditions={'method': ['PUT'],
257 258 'function': check_group},
258 259 requirements=URL_NAME_REQUIREMENTS)
259 260
260 261 # EXTRAS REPO GROUP ROUTES
261 262 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
262 263 action='edit',
263 264 conditions={'method': ['GET'], 'function': check_group},
264 265 requirements=URL_NAME_REQUIREMENTS)
265 266 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
266 267 action='edit',
267 268 conditions={'method': ['PUT'], 'function': check_group},
268 269 requirements=URL_NAME_REQUIREMENTS)
269 270
270 271 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
271 272 action='edit_repo_group_advanced',
272 273 conditions={'method': ['GET'], 'function': check_group},
273 274 requirements=URL_NAME_REQUIREMENTS)
274 275 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
275 276 action='edit_repo_group_advanced',
276 277 conditions={'method': ['PUT'], 'function': check_group},
277 278 requirements=URL_NAME_REQUIREMENTS)
278 279
279 280 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
280 281 action='edit_repo_group_perms',
281 282 conditions={'method': ['GET'], 'function': check_group},
282 283 requirements=URL_NAME_REQUIREMENTS)
283 284 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
284 285 action='update_perms',
285 286 conditions={'method': ['PUT'], 'function': check_group},
286 287 requirements=URL_NAME_REQUIREMENTS)
287 288
288 289 m.connect('delete_repo_group', '/repo_groups/{group_name}',
289 290 action='delete', conditions={'method': ['DELETE'],
290 291 'function': check_group},
291 292 requirements=URL_NAME_REQUIREMENTS)
292 293
293 294 # ADMIN USER ROUTES
294 295 with rmap.submapper(path_prefix=ADMIN_PREFIX,
295 296 controller='admin/users') as m:
296 297 m.connect('users', '/users',
297 298 action='create', conditions={'method': ['POST']})
298 299 m.connect('users', '/users',
299 300 action='index', conditions={'method': ['GET']})
300 301 m.connect('new_user', '/users/new',
301 302 action='new', conditions={'method': ['GET']})
302 303 m.connect('update_user', '/users/{user_id}',
303 304 action='update', conditions={'method': ['PUT']})
304 305 m.connect('delete_user', '/users/{user_id}',
305 306 action='delete', conditions={'method': ['DELETE']})
306 307 m.connect('edit_user', '/users/{user_id}/edit',
307 308 action='edit', conditions={'method': ['GET']})
308 309 m.connect('user', '/users/{user_id}',
309 310 action='show', conditions={'method': ['GET']})
310 311 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
311 312 action='reset_password', conditions={'method': ['POST']})
312 313 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
313 314 action='create_personal_repo_group', conditions={'method': ['POST']})
314 315
315 316 # EXTRAS USER ROUTES
316 317 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
317 318 action='edit_advanced', conditions={'method': ['GET']})
318 319 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
319 320 action='update_advanced', conditions={'method': ['PUT']})
320 321
321 322 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
322 323 action='edit_auth_tokens', conditions={'method': ['GET']})
323 324 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
324 325 action='add_auth_token', conditions={'method': ['PUT']})
325 326 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
326 327 action='delete_auth_token', conditions={'method': ['DELETE']})
327 328
328 329 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
329 330 action='edit_global_perms', conditions={'method': ['GET']})
330 331 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
331 332 action='update_global_perms', conditions={'method': ['PUT']})
332 333
333 334 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
334 335 action='edit_perms_summary', conditions={'method': ['GET']})
335 336
336 337 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
337 338 action='edit_emails', conditions={'method': ['GET']})
338 339 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
339 340 action='add_email', conditions={'method': ['PUT']})
340 341 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
341 342 action='delete_email', conditions={'method': ['DELETE']})
342 343
343 344 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
344 345 action='edit_ips', conditions={'method': ['GET']})
345 346 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
346 347 action='add_ip', conditions={'method': ['PUT']})
347 348 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
348 349 action='delete_ip', conditions={'method': ['DELETE']})
349 350
350 351 # ADMIN USER GROUPS REST ROUTES
351 352 with rmap.submapper(path_prefix=ADMIN_PREFIX,
352 353 controller='admin/user_groups') as m:
353 354 m.connect('users_groups', '/user_groups',
354 355 action='create', conditions={'method': ['POST']})
355 356 m.connect('users_groups', '/user_groups',
356 357 action='index', conditions={'method': ['GET']})
357 358 m.connect('new_users_group', '/user_groups/new',
358 359 action='new', conditions={'method': ['GET']})
359 360 m.connect('update_users_group', '/user_groups/{user_group_id}',
360 361 action='update', conditions={'method': ['PUT']})
361 362 m.connect('delete_users_group', '/user_groups/{user_group_id}',
362 363 action='delete', conditions={'method': ['DELETE']})
363 364 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
364 365 action='edit', conditions={'method': ['GET']},
365 366 function=check_user_group)
366 367
367 368 # EXTRAS USER GROUP ROUTES
368 369 m.connect('edit_user_group_global_perms',
369 370 '/user_groups/{user_group_id}/edit/global_permissions',
370 371 action='edit_global_perms', conditions={'method': ['GET']})
371 372 m.connect('edit_user_group_global_perms',
372 373 '/user_groups/{user_group_id}/edit/global_permissions',
373 374 action='update_global_perms', conditions={'method': ['PUT']})
374 375 m.connect('edit_user_group_perms_summary',
375 376 '/user_groups/{user_group_id}/edit/permissions_summary',
376 377 action='edit_perms_summary', conditions={'method': ['GET']})
377 378
378 379 m.connect('edit_user_group_perms',
379 380 '/user_groups/{user_group_id}/edit/permissions',
380 381 action='edit_perms', conditions={'method': ['GET']})
381 382 m.connect('edit_user_group_perms',
382 383 '/user_groups/{user_group_id}/edit/permissions',
383 384 action='update_perms', conditions={'method': ['PUT']})
384 385
385 386 m.connect('edit_user_group_advanced',
386 387 '/user_groups/{user_group_id}/edit/advanced',
387 388 action='edit_advanced', conditions={'method': ['GET']})
388 389
389 390 m.connect('edit_user_group_members',
390 391 '/user_groups/{user_group_id}/edit/members', jsroute=True,
391 392 action='edit_members', conditions={'method': ['GET']})
392 393
393 394 # ADMIN PERMISSIONS ROUTES
394 395 with rmap.submapper(path_prefix=ADMIN_PREFIX,
395 396 controller='admin/permissions') as m:
396 397 m.connect('admin_permissions_application', '/permissions/application',
397 398 action='permission_application_update', conditions={'method': ['POST']})
398 399 m.connect('admin_permissions_application', '/permissions/application',
399 400 action='permission_application', conditions={'method': ['GET']})
400 401
401 402 m.connect('admin_permissions_global', '/permissions/global',
402 403 action='permission_global_update', conditions={'method': ['POST']})
403 404 m.connect('admin_permissions_global', '/permissions/global',
404 405 action='permission_global', conditions={'method': ['GET']})
405 406
406 407 m.connect('admin_permissions_object', '/permissions/object',
407 408 action='permission_objects_update', conditions={'method': ['POST']})
408 409 m.connect('admin_permissions_object', '/permissions/object',
409 410 action='permission_objects', conditions={'method': ['GET']})
410 411
411 412 m.connect('admin_permissions_ips', '/permissions/ips',
412 413 action='permission_ips', conditions={'method': ['POST']})
413 414 m.connect('admin_permissions_ips', '/permissions/ips',
414 415 action='permission_ips', conditions={'method': ['GET']})
415 416
416 417 m.connect('admin_permissions_overview', '/permissions/overview',
417 418 action='permission_perms', conditions={'method': ['GET']})
418 419
419 420 # ADMIN DEFAULTS REST ROUTES
420 421 with rmap.submapper(path_prefix=ADMIN_PREFIX,
421 422 controller='admin/defaults') as m:
422 423 m.connect('admin_defaults_repositories', '/defaults/repositories',
423 424 action='update_repository_defaults', conditions={'method': ['POST']})
424 425 m.connect('admin_defaults_repositories', '/defaults/repositories',
425 426 action='index', conditions={'method': ['GET']})
426 427
427 428 # ADMIN DEBUG STYLE ROUTES
428 429 if str2bool(config.get('debug_style')):
429 430 with rmap.submapper(path_prefix=ADMIN_PREFIX + '/debug_style',
430 431 controller='debug_style') as m:
431 432 m.connect('debug_style_home', '',
432 433 action='index', conditions={'method': ['GET']})
433 434 m.connect('debug_style_template', '/t/{t_path}',
434 435 action='template', conditions={'method': ['GET']})
435 436
436 437 # ADMIN SETTINGS ROUTES
437 438 with rmap.submapper(path_prefix=ADMIN_PREFIX,
438 439 controller='admin/settings') as m:
439 440
440 441 # default
441 442 m.connect('admin_settings', '/settings',
442 443 action='settings_global_update',
443 444 conditions={'method': ['POST']})
444 445 m.connect('admin_settings', '/settings',
445 446 action='settings_global', conditions={'method': ['GET']})
446 447
447 448 m.connect('admin_settings_vcs', '/settings/vcs',
448 449 action='settings_vcs_update',
449 450 conditions={'method': ['POST']})
450 451 m.connect('admin_settings_vcs', '/settings/vcs',
451 452 action='settings_vcs',
452 453 conditions={'method': ['GET']})
453 454 m.connect('admin_settings_vcs', '/settings/vcs',
454 455 action='delete_svn_pattern',
455 456 conditions={'method': ['DELETE']})
456 457
457 458 m.connect('admin_settings_mapping', '/settings/mapping',
458 459 action='settings_mapping_update',
459 460 conditions={'method': ['POST']})
460 461 m.connect('admin_settings_mapping', '/settings/mapping',
461 462 action='settings_mapping', conditions={'method': ['GET']})
462 463
463 464 m.connect('admin_settings_global', '/settings/global',
464 465 action='settings_global_update',
465 466 conditions={'method': ['POST']})
466 467 m.connect('admin_settings_global', '/settings/global',
467 468 action='settings_global', conditions={'method': ['GET']})
468 469
469 470 m.connect('admin_settings_visual', '/settings/visual',
470 471 action='settings_visual_update',
471 472 conditions={'method': ['POST']})
472 473 m.connect('admin_settings_visual', '/settings/visual',
473 474 action='settings_visual', conditions={'method': ['GET']})
474 475
475 476 m.connect('admin_settings_issuetracker',
476 477 '/settings/issue-tracker', action='settings_issuetracker',
477 478 conditions={'method': ['GET']})
478 479 m.connect('admin_settings_issuetracker_save',
479 480 '/settings/issue-tracker/save',
480 481 action='settings_issuetracker_save',
481 482 conditions={'method': ['POST']})
482 483 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
483 484 action='settings_issuetracker_test',
484 485 conditions={'method': ['POST']})
485 486 m.connect('admin_issuetracker_delete',
486 487 '/settings/issue-tracker/delete',
487 488 action='settings_issuetracker_delete',
488 489 conditions={'method': ['DELETE']})
489 490
490 491 m.connect('admin_settings_email', '/settings/email',
491 492 action='settings_email_update',
492 493 conditions={'method': ['POST']})
493 494 m.connect('admin_settings_email', '/settings/email',
494 495 action='settings_email', conditions={'method': ['GET']})
495 496
496 497 m.connect('admin_settings_hooks', '/settings/hooks',
497 498 action='settings_hooks_update',
498 499 conditions={'method': ['POST', 'DELETE']})
499 500 m.connect('admin_settings_hooks', '/settings/hooks',
500 501 action='settings_hooks', conditions={'method': ['GET']})
501 502
502 503 m.connect('admin_settings_search', '/settings/search',
503 504 action='settings_search', conditions={'method': ['GET']})
504 505
505 506 m.connect('admin_settings_system', '/settings/system',
506 507 action='settings_system', conditions={'method': ['GET']})
507 508
508 509 m.connect('admin_settings_system_update', '/settings/system/updates',
509 510 action='settings_system_update', conditions={'method': ['GET']})
510 511
511 512 m.connect('admin_settings_supervisor', '/settings/supervisor',
512 513 action='settings_supervisor', conditions={'method': ['GET']})
513 514 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
514 515 action='settings_supervisor_log', conditions={'method': ['GET']})
515 516
516 517 m.connect('admin_settings_labs', '/settings/labs',
517 518 action='settings_labs_update',
518 519 conditions={'method': ['POST']})
519 520 m.connect('admin_settings_labs', '/settings/labs',
520 521 action='settings_labs', conditions={'method': ['GET']})
521 522
522 523 # ADMIN MY ACCOUNT
523 524 with rmap.submapper(path_prefix=ADMIN_PREFIX,
524 525 controller='admin/my_account') as m:
525 526
526 527 m.connect('my_account', '/my_account',
527 528 action='my_account', conditions={'method': ['GET']})
528 529 m.connect('my_account_edit', '/my_account/edit',
529 530 action='my_account_edit', conditions={'method': ['GET']})
530 531 m.connect('my_account', '/my_account',
531 532 action='my_account_update', conditions={'method': ['POST']})
532 533
533 534 m.connect('my_account_password', '/my_account/password',
534 535 action='my_account_password', conditions={'method': ['GET', 'POST']})
535 536
536 537 m.connect('my_account_repos', '/my_account/repos',
537 538 action='my_account_repos', conditions={'method': ['GET']})
538 539
539 540 m.connect('my_account_watched', '/my_account/watched',
540 541 action='my_account_watched', conditions={'method': ['GET']})
541 542
542 543 m.connect('my_account_pullrequests', '/my_account/pull_requests',
543 544 action='my_account_pullrequests', conditions={'method': ['GET']})
544 545
545 546 m.connect('my_account_perms', '/my_account/perms',
546 547 action='my_account_perms', conditions={'method': ['GET']})
547 548
548 549 m.connect('my_account_emails', '/my_account/emails',
549 550 action='my_account_emails', conditions={'method': ['GET']})
550 551 m.connect('my_account_emails', '/my_account/emails',
551 552 action='my_account_emails_add', conditions={'method': ['POST']})
552 553 m.connect('my_account_emails', '/my_account/emails',
553 554 action='my_account_emails_delete', conditions={'method': ['DELETE']})
554 555
555 556 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
556 557 action='my_account_auth_tokens', conditions={'method': ['GET']})
557 558 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
558 559 action='my_account_auth_tokens_add', conditions={'method': ['POST']})
559 560 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
560 561 action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']})
561 562 m.connect('my_account_notifications', '/my_account/notifications',
562 563 action='my_notifications',
563 564 conditions={'method': ['GET']})
564 565 m.connect('my_account_notifications_toggle_visibility',
565 566 '/my_account/toggle_visibility',
566 567 action='my_notifications_toggle_visibility',
567 568 conditions={'method': ['POST']})
568 569
569 570 # NOTIFICATION REST ROUTES
570 571 with rmap.submapper(path_prefix=ADMIN_PREFIX,
571 572 controller='admin/notifications') as m:
572 573 m.connect('notifications', '/notifications',
573 574 action='index', conditions={'method': ['GET']})
574 575 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
575 576 action='mark_all_read', conditions={'method': ['POST']})
576 577 m.connect('/notifications/{notification_id}',
577 578 action='update', conditions={'method': ['PUT']})
578 579 m.connect('/notifications/{notification_id}',
579 580 action='delete', conditions={'method': ['DELETE']})
580 581 m.connect('notification', '/notifications/{notification_id}',
581 582 action='show', conditions={'method': ['GET']})
582 583
583 584 # ADMIN GIST
584 585 with rmap.submapper(path_prefix=ADMIN_PREFIX,
585 586 controller='admin/gists') as m:
586 587 m.connect('gists', '/gists',
587 588 action='create', conditions={'method': ['POST']})
588 589 m.connect('gists', '/gists', jsroute=True,
589 590 action='index', conditions={'method': ['GET']})
590 591 m.connect('new_gist', '/gists/new', jsroute=True,
591 592 action='new', conditions={'method': ['GET']})
592 593
593 594 m.connect('/gists/{gist_id}',
594 595 action='delete', conditions={'method': ['DELETE']})
595 596 m.connect('edit_gist', '/gists/{gist_id}/edit',
596 597 action='edit_form', conditions={'method': ['GET']})
597 598 m.connect('edit_gist', '/gists/{gist_id}/edit',
598 599 action='edit', conditions={'method': ['POST']})
599 600 m.connect(
600 601 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision',
601 602 action='check_revision', conditions={'method': ['GET']})
602 603
603 604 m.connect('gist', '/gists/{gist_id}',
604 605 action='show', conditions={'method': ['GET']})
605 606 m.connect('gist_rev', '/gists/{gist_id}/{revision}',
606 607 revision='tip',
607 608 action='show', conditions={'method': ['GET']})
608 609 m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}',
609 610 revision='tip',
610 611 action='show', conditions={'method': ['GET']})
611 612 m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}',
612 613 revision='tip',
613 614 action='show', conditions={'method': ['GET']},
614 615 requirements=URL_NAME_REQUIREMENTS)
615 616
616 617 # ADMIN MAIN PAGES
617 618 with rmap.submapper(path_prefix=ADMIN_PREFIX,
618 619 controller='admin/admin') as m:
619 620 m.connect('admin_home', '', action='index')
620 621 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
621 622 action='add_repo')
622 623 m.connect(
623 624 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}',
624 625 action='pull_requests')
625 626 m.connect(
626 627 'pull_requests_global', '/pull-requests/{pull_request_id:[0-9]+}',
627 628 action='pull_requests')
628 629
629 630
630 631 # USER JOURNAL
631 632 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
632 633 controller='journal', action='index')
633 634 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
634 635 controller='journal', action='journal_rss')
635 636 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
636 637 controller='journal', action='journal_atom')
637 638
638 639 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
639 640 controller='journal', action='public_journal')
640 641
641 642 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
642 643 controller='journal', action='public_journal_rss')
643 644
644 645 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
645 646 controller='journal', action='public_journal_rss')
646 647
647 648 rmap.connect('public_journal_atom',
648 649 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
649 650 action='public_journal_atom')
650 651
651 652 rmap.connect('public_journal_atom_old',
652 653 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
653 654 action='public_journal_atom')
654 655
655 656 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
656 657 controller='journal', action='toggle_following', jsroute=True,
657 658 conditions={'method': ['POST']})
658 659
659 660 # FULL TEXT SEARCH
660 661 rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
661 662 controller='search')
662 663 rmap.connect('search_repo_home', '/{repo_name}/search',
663 664 controller='search',
664 665 action='index',
665 666 conditions={'function': check_repo},
666 667 requirements=URL_NAME_REQUIREMENTS)
667 668
668 669 # FEEDS
669 670 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
670 671 controller='feed', action='rss',
671 672 conditions={'function': check_repo},
672 673 requirements=URL_NAME_REQUIREMENTS)
673 674
674 675 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
675 676 controller='feed', action='atom',
676 677 conditions={'function': check_repo},
677 678 requirements=URL_NAME_REQUIREMENTS)
678 679
679 680 #==========================================================================
680 681 # REPOSITORY ROUTES
681 682 #==========================================================================
682 683
683 684 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
684 685 controller='admin/repos', action='repo_creating',
685 686 requirements=URL_NAME_REQUIREMENTS)
686 687 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
687 688 controller='admin/repos', action='repo_check',
688 689 requirements=URL_NAME_REQUIREMENTS)
689 690
690 691 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
691 692 controller='summary', action='repo_stats',
692 693 conditions={'function': check_repo},
693 694 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
694 695
695 696 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
696 697 controller='summary', action='repo_refs_data', jsroute=True,
697 698 requirements=URL_NAME_REQUIREMENTS)
698 699 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
699 700 controller='summary', action='repo_refs_changelog_data',
700 701 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
701 702
702 703 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
703 704 controller='changeset', revision='tip', jsroute=True,
704 705 conditions={'function': check_repo},
705 706 requirements=URL_NAME_REQUIREMENTS)
706 707 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
707 708 controller='changeset', revision='tip', action='changeset_children',
708 709 conditions={'function': check_repo},
709 710 requirements=URL_NAME_REQUIREMENTS)
710 711 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
711 712 controller='changeset', revision='tip', action='changeset_parents',
712 713 conditions={'function': check_repo},
713 714 requirements=URL_NAME_REQUIREMENTS)
714 715
715 716 # repo edit options
716 717 rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True,
717 718 controller='admin/repos', action='edit',
718 719 conditions={'method': ['GET'], 'function': check_repo},
719 720 requirements=URL_NAME_REQUIREMENTS)
720 721
721 722 rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions',
722 723 jsroute=True,
723 724 controller='admin/repos', action='edit_permissions',
724 725 conditions={'method': ['GET'], 'function': check_repo},
725 726 requirements=URL_NAME_REQUIREMENTS)
726 727 rmap.connect('edit_repo_perms_update', '/{repo_name}/settings/permissions',
727 728 controller='admin/repos', action='edit_permissions_update',
728 729 conditions={'method': ['PUT'], 'function': check_repo},
729 730 requirements=URL_NAME_REQUIREMENTS)
730 731
731 732 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
732 733 controller='admin/repos', action='edit_fields',
733 734 conditions={'method': ['GET'], 'function': check_repo},
734 735 requirements=URL_NAME_REQUIREMENTS)
735 736 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
736 737 controller='admin/repos', action='create_repo_field',
737 738 conditions={'method': ['PUT'], 'function': check_repo},
738 739 requirements=URL_NAME_REQUIREMENTS)
739 740 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
740 741 controller='admin/repos', action='delete_repo_field',
741 742 conditions={'method': ['DELETE'], 'function': check_repo},
742 743 requirements=URL_NAME_REQUIREMENTS)
743 744
744 745 rmap.connect('edit_repo_advanced', '/{repo_name}/settings/advanced',
745 746 controller='admin/repos', action='edit_advanced',
746 747 conditions={'method': ['GET'], 'function': check_repo},
747 748 requirements=URL_NAME_REQUIREMENTS)
748 749
749 750 rmap.connect('edit_repo_advanced_locking', '/{repo_name}/settings/advanced/locking',
750 751 controller='admin/repos', action='edit_advanced_locking',
751 752 conditions={'method': ['PUT'], 'function': check_repo},
752 753 requirements=URL_NAME_REQUIREMENTS)
753 754 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
754 755 controller='admin/repos', action='toggle_locking',
755 756 conditions={'method': ['GET'], 'function': check_repo},
756 757 requirements=URL_NAME_REQUIREMENTS)
757 758
758 759 rmap.connect('edit_repo_advanced_journal', '/{repo_name}/settings/advanced/journal',
759 760 controller='admin/repos', action='edit_advanced_journal',
760 761 conditions={'method': ['PUT'], 'function': check_repo},
761 762 requirements=URL_NAME_REQUIREMENTS)
762 763
763 764 rmap.connect('edit_repo_advanced_fork', '/{repo_name}/settings/advanced/fork',
764 765 controller='admin/repos', action='edit_advanced_fork',
765 766 conditions={'method': ['PUT'], 'function': check_repo},
766 767 requirements=URL_NAME_REQUIREMENTS)
767 768
768 769 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
769 770 controller='admin/repos', action='edit_caches_form',
770 771 conditions={'method': ['GET'], 'function': check_repo},
771 772 requirements=URL_NAME_REQUIREMENTS)
772 773 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
773 774 controller='admin/repos', action='edit_caches',
774 775 conditions={'method': ['PUT'], 'function': check_repo},
775 776 requirements=URL_NAME_REQUIREMENTS)
776 777
777 778 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
778 779 controller='admin/repos', action='edit_remote_form',
779 780 conditions={'method': ['GET'], 'function': check_repo},
780 781 requirements=URL_NAME_REQUIREMENTS)
781 782 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
782 783 controller='admin/repos', action='edit_remote',
783 784 conditions={'method': ['PUT'], 'function': check_repo},
784 785 requirements=URL_NAME_REQUIREMENTS)
785 786
786 787 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
787 788 controller='admin/repos', action='edit_statistics_form',
788 789 conditions={'method': ['GET'], 'function': check_repo},
789 790 requirements=URL_NAME_REQUIREMENTS)
790 791 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
791 792 controller='admin/repos', action='edit_statistics',
792 793 conditions={'method': ['PUT'], 'function': check_repo},
793 794 requirements=URL_NAME_REQUIREMENTS)
794 795 rmap.connect('repo_settings_issuetracker',
795 796 '/{repo_name}/settings/issue-tracker',
796 797 controller='admin/repos', action='repo_issuetracker',
797 798 conditions={'method': ['GET'], 'function': check_repo},
798 799 requirements=URL_NAME_REQUIREMENTS)
799 800 rmap.connect('repo_issuetracker_test',
800 801 '/{repo_name}/settings/issue-tracker/test',
801 802 controller='admin/repos', action='repo_issuetracker_test',
802 803 conditions={'method': ['POST'], 'function': check_repo},
803 804 requirements=URL_NAME_REQUIREMENTS)
804 805 rmap.connect('repo_issuetracker_delete',
805 806 '/{repo_name}/settings/issue-tracker/delete',
806 807 controller='admin/repos', action='repo_issuetracker_delete',
807 808 conditions={'method': ['DELETE'], 'function': check_repo},
808 809 requirements=URL_NAME_REQUIREMENTS)
809 810 rmap.connect('repo_issuetracker_save',
810 811 '/{repo_name}/settings/issue-tracker/save',
811 812 controller='admin/repos', action='repo_issuetracker_save',
812 813 conditions={'method': ['POST'], 'function': check_repo},
813 814 requirements=URL_NAME_REQUIREMENTS)
814 815 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
815 816 controller='admin/repos', action='repo_settings_vcs_update',
816 817 conditions={'method': ['POST'], 'function': check_repo},
817 818 requirements=URL_NAME_REQUIREMENTS)
818 819 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
819 820 controller='admin/repos', action='repo_settings_vcs',
820 821 conditions={'method': ['GET'], 'function': check_repo},
821 822 requirements=URL_NAME_REQUIREMENTS)
822 823 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
823 824 controller='admin/repos', action='repo_delete_svn_pattern',
824 825 conditions={'method': ['DELETE'], 'function': check_repo},
825 826 requirements=URL_NAME_REQUIREMENTS)
826 827
827 828 # still working url for backward compat.
828 829 rmap.connect('raw_changeset_home_depraced',
829 830 '/{repo_name}/raw-changeset/{revision}',
830 831 controller='changeset', action='changeset_raw',
831 832 revision='tip', conditions={'function': check_repo},
832 833 requirements=URL_NAME_REQUIREMENTS)
833 834
834 835 # new URLs
835 836 rmap.connect('changeset_raw_home',
836 837 '/{repo_name}/changeset-diff/{revision}',
837 838 controller='changeset', action='changeset_raw',
838 839 revision='tip', conditions={'function': check_repo},
839 840 requirements=URL_NAME_REQUIREMENTS)
840 841
841 842 rmap.connect('changeset_patch_home',
842 843 '/{repo_name}/changeset-patch/{revision}',
843 844 controller='changeset', action='changeset_patch',
844 845 revision='tip', conditions={'function': check_repo},
845 846 requirements=URL_NAME_REQUIREMENTS)
846 847
847 848 rmap.connect('changeset_download_home',
848 849 '/{repo_name}/changeset-download/{revision}',
849 850 controller='changeset', action='changeset_download',
850 851 revision='tip', conditions={'function': check_repo},
851 852 requirements=URL_NAME_REQUIREMENTS)
852 853
853 854 rmap.connect('changeset_comment',
854 855 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
855 856 controller='changeset', revision='tip', action='comment',
856 857 conditions={'function': check_repo},
857 858 requirements=URL_NAME_REQUIREMENTS)
858 859
859 860 rmap.connect('changeset_comment_preview',
860 861 '/{repo_name}/changeset/comment/preview', jsroute=True,
861 862 controller='changeset', action='preview_comment',
862 863 conditions={'function': check_repo, 'method': ['POST']},
863 864 requirements=URL_NAME_REQUIREMENTS)
864 865
865 866 rmap.connect('changeset_comment_delete',
866 867 '/{repo_name}/changeset/comment/{comment_id}/delete',
867 868 controller='changeset', action='delete_comment',
868 869 conditions={'function': check_repo, 'method': ['DELETE']},
869 870 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
870 871
871 872 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
872 873 controller='changeset', action='changeset_info',
873 874 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
874 875
875 876 rmap.connect('compare_home',
876 877 '/{repo_name}/compare',
877 878 controller='compare', action='index',
878 879 conditions={'function': check_repo},
879 880 requirements=URL_NAME_REQUIREMENTS)
880 881
881 882 rmap.connect('compare_url',
882 883 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
883 884 controller='compare', action='compare',
884 885 conditions={'function': check_repo},
885 886 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
886 887
887 888 rmap.connect('pullrequest_home',
888 889 '/{repo_name}/pull-request/new', controller='pullrequests',
889 890 action='index', conditions={'function': check_repo,
890 891 'method': ['GET']},
891 892 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
892 893
893 894 rmap.connect('pullrequest',
894 895 '/{repo_name}/pull-request/new', controller='pullrequests',
895 896 action='create', conditions={'function': check_repo,
896 897 'method': ['POST']},
897 898 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
898 899
899 900 rmap.connect('pullrequest_repo_refs',
900 901 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
901 902 controller='pullrequests',
902 903 action='get_repo_refs',
903 904 conditions={'function': check_repo, 'method': ['GET']},
904 905 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
905 906
906 907 rmap.connect('pullrequest_repo_destinations',
907 908 '/{repo_name}/pull-request/repo-destinations',
908 909 controller='pullrequests',
909 910 action='get_repo_destinations',
910 911 conditions={'function': check_repo, 'method': ['GET']},
911 912 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
912 913
913 914 rmap.connect('pullrequest_show',
914 915 '/{repo_name}/pull-request/{pull_request_id}',
915 916 controller='pullrequests',
916 917 action='show', conditions={'function': check_repo,
917 918 'method': ['GET']},
918 919 requirements=URL_NAME_REQUIREMENTS)
919 920
920 921 rmap.connect('pullrequest_update',
921 922 '/{repo_name}/pull-request/{pull_request_id}',
922 923 controller='pullrequests',
923 924 action='update', conditions={'function': check_repo,
924 925 'method': ['PUT']},
925 926 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
926 927
927 928 rmap.connect('pullrequest_merge',
928 929 '/{repo_name}/pull-request/{pull_request_id}',
929 930 controller='pullrequests',
930 931 action='merge', conditions={'function': check_repo,
931 932 'method': ['POST']},
932 933 requirements=URL_NAME_REQUIREMENTS)
933 934
934 935 rmap.connect('pullrequest_delete',
935 936 '/{repo_name}/pull-request/{pull_request_id}',
936 937 controller='pullrequests',
937 938 action='delete', conditions={'function': check_repo,
938 939 'method': ['DELETE']},
939 940 requirements=URL_NAME_REQUIREMENTS)
940 941
941 942 rmap.connect('pullrequest_show_all',
942 943 '/{repo_name}/pull-request',
943 944 controller='pullrequests',
944 945 action='show_all', conditions={'function': check_repo,
945 946 'method': ['GET']},
946 947 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
947 948
948 949 rmap.connect('pullrequest_comment',
949 950 '/{repo_name}/pull-request-comment/{pull_request_id}',
950 951 controller='pullrequests',
951 952 action='comment', conditions={'function': check_repo,
952 953 'method': ['POST']},
953 954 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
954 955
955 956 rmap.connect('pullrequest_comment_delete',
956 957 '/{repo_name}/pull-request-comment/{comment_id}/delete',
957 958 controller='pullrequests', action='delete_comment',
958 959 conditions={'function': check_repo, 'method': ['DELETE']},
959 960 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
960 961
961 962 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
962 963 controller='summary', conditions={'function': check_repo},
963 964 requirements=URL_NAME_REQUIREMENTS)
964 965
965 966 rmap.connect('branches_home', '/{repo_name}/branches',
966 967 controller='branches', conditions={'function': check_repo},
967 968 requirements=URL_NAME_REQUIREMENTS)
968 969
969 970 rmap.connect('tags_home', '/{repo_name}/tags',
970 971 controller='tags', conditions={'function': check_repo},
971 972 requirements=URL_NAME_REQUIREMENTS)
972 973
973 974 rmap.connect('bookmarks_home', '/{repo_name}/bookmarks',
974 975 controller='bookmarks', conditions={'function': check_repo},
975 976 requirements=URL_NAME_REQUIREMENTS)
976 977
977 978 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
978 979 controller='changelog', conditions={'function': check_repo},
979 980 requirements=URL_NAME_REQUIREMENTS)
980 981
981 982 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
982 983 controller='changelog', action='changelog_summary',
983 984 conditions={'function': check_repo},
984 985 requirements=URL_NAME_REQUIREMENTS)
985 986
986 987 rmap.connect('changelog_file_home',
987 988 '/{repo_name}/changelog/{revision}/{f_path}',
988 989 controller='changelog', f_path=None,
989 990 conditions={'function': check_repo},
990 991 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
991 992
992 993 rmap.connect('changelog_details', '/{repo_name}/changelog_details/{cs}',
993 994 controller='changelog', action='changelog_details',
994 995 conditions={'function': check_repo},
995 996 requirements=URL_NAME_REQUIREMENTS)
996 997
997 998 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
998 999 controller='files', revision='tip', f_path='',
999 1000 conditions={'function': check_repo},
1000 1001 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1001 1002
1002 1003 rmap.connect('files_home_simple_catchrev',
1003 1004 '/{repo_name}/files/{revision}',
1004 1005 controller='files', revision='tip', f_path='',
1005 1006 conditions={'function': check_repo},
1006 1007 requirements=URL_NAME_REQUIREMENTS)
1007 1008
1008 1009 rmap.connect('files_home_simple_catchall',
1009 1010 '/{repo_name}/files',
1010 1011 controller='files', revision='tip', f_path='',
1011 1012 conditions={'function': check_repo},
1012 1013 requirements=URL_NAME_REQUIREMENTS)
1013 1014
1014 1015 rmap.connect('files_history_home',
1015 1016 '/{repo_name}/history/{revision}/{f_path}',
1016 1017 controller='files', action='history', revision='tip', f_path='',
1017 1018 conditions={'function': check_repo},
1018 1019 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1019 1020
1020 1021 rmap.connect('files_authors_home',
1021 1022 '/{repo_name}/authors/{revision}/{f_path}',
1022 1023 controller='files', action='authors', revision='tip', f_path='',
1023 1024 conditions={'function': check_repo},
1024 1025 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1025 1026
1026 1027 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
1027 1028 controller='files', action='diff', f_path='',
1028 1029 conditions={'function': check_repo},
1029 1030 requirements=URL_NAME_REQUIREMENTS)
1030 1031
1031 1032 rmap.connect('files_diff_2way_home',
1032 1033 '/{repo_name}/diff-2way/{f_path}',
1033 1034 controller='files', action='diff_2way', f_path='',
1034 1035 conditions={'function': check_repo},
1035 1036 requirements=URL_NAME_REQUIREMENTS)
1036 1037
1037 1038 rmap.connect('files_rawfile_home',
1038 1039 '/{repo_name}/rawfile/{revision}/{f_path}',
1039 1040 controller='files', action='rawfile', revision='tip',
1040 1041 f_path='', conditions={'function': check_repo},
1041 1042 requirements=URL_NAME_REQUIREMENTS)
1042 1043
1043 1044 rmap.connect('files_raw_home',
1044 1045 '/{repo_name}/raw/{revision}/{f_path}',
1045 1046 controller='files', action='raw', revision='tip', f_path='',
1046 1047 conditions={'function': check_repo},
1047 1048 requirements=URL_NAME_REQUIREMENTS)
1048 1049
1049 1050 rmap.connect('files_render_home',
1050 1051 '/{repo_name}/render/{revision}/{f_path}',
1051 1052 controller='files', action='index', revision='tip', f_path='',
1052 1053 rendered=True, conditions={'function': check_repo},
1053 1054 requirements=URL_NAME_REQUIREMENTS)
1054 1055
1055 1056 rmap.connect('files_annotate_home',
1056 1057 '/{repo_name}/annotate/{revision}/{f_path}',
1057 1058 controller='files', action='index', revision='tip',
1058 1059 f_path='', annotate=True, conditions={'function': check_repo},
1059 1060 requirements=URL_NAME_REQUIREMENTS)
1060 1061
1061 1062 rmap.connect('files_edit',
1062 1063 '/{repo_name}/edit/{revision}/{f_path}',
1063 1064 controller='files', action='edit', revision='tip',
1064 1065 f_path='',
1065 1066 conditions={'function': check_repo, 'method': ['POST']},
1066 1067 requirements=URL_NAME_REQUIREMENTS)
1067 1068
1068 1069 rmap.connect('files_edit_home',
1069 1070 '/{repo_name}/edit/{revision}/{f_path}',
1070 1071 controller='files', action='edit_home', revision='tip',
1071 1072 f_path='', conditions={'function': check_repo},
1072 1073 requirements=URL_NAME_REQUIREMENTS)
1073 1074
1074 1075 rmap.connect('files_add',
1075 1076 '/{repo_name}/add/{revision}/{f_path}',
1076 1077 controller='files', action='add', revision='tip',
1077 1078 f_path='',
1078 1079 conditions={'function': check_repo, 'method': ['POST']},
1079 1080 requirements=URL_NAME_REQUIREMENTS)
1080 1081
1081 1082 rmap.connect('files_add_home',
1082 1083 '/{repo_name}/add/{revision}/{f_path}',
1083 1084 controller='files', action='add_home', revision='tip',
1084 1085 f_path='', conditions={'function': check_repo},
1085 1086 requirements=URL_NAME_REQUIREMENTS)
1086 1087
1087 1088 rmap.connect('files_delete',
1088 1089 '/{repo_name}/delete/{revision}/{f_path}',
1089 1090 controller='files', action='delete', revision='tip',
1090 1091 f_path='',
1091 1092 conditions={'function': check_repo, 'method': ['POST']},
1092 1093 requirements=URL_NAME_REQUIREMENTS)
1093 1094
1094 1095 rmap.connect('files_delete_home',
1095 1096 '/{repo_name}/delete/{revision}/{f_path}',
1096 1097 controller='files', action='delete_home', revision='tip',
1097 1098 f_path='', conditions={'function': check_repo},
1098 1099 requirements=URL_NAME_REQUIREMENTS)
1099 1100
1100 1101 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
1101 1102 controller='files', action='archivefile',
1102 1103 conditions={'function': check_repo},
1103 1104 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1104 1105
1105 1106 rmap.connect('files_nodelist_home',
1106 1107 '/{repo_name}/nodelist/{revision}/{f_path}',
1107 1108 controller='files', action='nodelist',
1108 1109 conditions={'function': check_repo},
1109 1110 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1110 1111
1111 1112 rmap.connect('files_nodetree_full',
1112 1113 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
1113 1114 controller='files', action='nodetree_full',
1114 1115 conditions={'function': check_repo},
1115 1116 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1116 1117
1117 1118 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
1118 1119 controller='forks', action='fork_create',
1119 1120 conditions={'function': check_repo, 'method': ['POST']},
1120 1121 requirements=URL_NAME_REQUIREMENTS)
1121 1122
1122 1123 rmap.connect('repo_fork_home', '/{repo_name}/fork',
1123 1124 controller='forks', action='fork',
1124 1125 conditions={'function': check_repo},
1125 1126 requirements=URL_NAME_REQUIREMENTS)
1126 1127
1127 1128 rmap.connect('repo_forks_home', '/{repo_name}/forks',
1128 1129 controller='forks', action='forks',
1129 1130 conditions={'function': check_repo},
1130 1131 requirements=URL_NAME_REQUIREMENTS)
1131 1132
1132 1133 rmap.connect('repo_followers_home', '/{repo_name}/followers',
1133 1134 controller='followers', action='followers',
1134 1135 conditions={'function': check_repo},
1135 1136 requirements=URL_NAME_REQUIREMENTS)
1136 1137
1137 1138 # must be here for proper group/repo catching pattern
1138 1139 _connect_with_slash(
1139 1140 rmap, 'repo_group_home', '/{group_name}',
1140 1141 controller='home', action='index_repo_group',
1141 1142 conditions={'function': check_group},
1142 1143 requirements=URL_NAME_REQUIREMENTS)
1143 1144
1144 1145 # catch all, at the end
1145 1146 _connect_with_slash(
1146 1147 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1147 1148 controller='summary', action='index',
1148 1149 conditions={'function': check_repo},
1149 1150 requirements=URL_NAME_REQUIREMENTS)
1150 1151
1151 1152 return rmap
1152 1153
1153 1154
1154 1155 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1155 1156 """
1156 1157 Connect a route with an optional trailing slash in `path`.
1157 1158 """
1158 1159 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1159 1160 mapper.connect(name, path, *args, **kwargs)
@@ -1,200 +1,238 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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
23 23 from rhodecode.model.db import Repository, Integration, RepoGroup
24 24 from rhodecode.config.routing import (
25 25 ADMIN_PREFIX, add_route_requirements, URL_NAME_REQUIREMENTS)
26 26 from rhodecode.integrations import integration_type_registry
27 27
28 28 log = logging.getLogger(__name__)
29 29
30 30
31 31 def includeme(config):
32 32
33 33 # global integrations
34
35 config.add_route('global_integrations_new',
36 ADMIN_PREFIX + '/integrations/new')
37 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
38 attr='new_integration',
39 renderer='rhodecode:templates/admin/integrations/new.html',
40 request_method='GET',
41 route_name='global_integrations_new')
42
34 43 config.add_route('global_integrations_home',
35 44 ADMIN_PREFIX + '/integrations')
36 45 config.add_route('global_integrations_list',
37 46 ADMIN_PREFIX + '/integrations/{integration}')
38 47 for route_name in ['global_integrations_home', 'global_integrations_list']:
39 48 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
40 49 attr='index',
41 50 renderer='rhodecode:templates/admin/integrations/list.html',
42 51 request_method='GET',
43 52 route_name=route_name)
44 53
45 54 config.add_route('global_integrations_create',
46 55 ADMIN_PREFIX + '/integrations/{integration}/new',
47 56 custom_predicates=(valid_integration,))
48 57 config.add_route('global_integrations_edit',
49 58 ADMIN_PREFIX + '/integrations/{integration}/{integration_id}',
50 59 custom_predicates=(valid_integration,))
60
61
51 62 for route_name in ['global_integrations_create', 'global_integrations_edit']:
52 63 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
53 64 attr='settings_get',
54 renderer='rhodecode:templates/admin/integrations/edit.html',
65 renderer='rhodecode:templates/admin/integrations/form.html',
55 66 request_method='GET',
56 67 route_name=route_name)
57 68 config.add_view('rhodecode.integrations.views.GlobalIntegrationsView',
58 69 attr='settings_post',
59 renderer='rhodecode:templates/admin/integrations/edit.html',
70 renderer='rhodecode:templates/admin/integrations/form.html',
71 request_method='POST',
72 route_name=route_name)
73
74
75 # repo group integrations
76 config.add_route('repo_group_integrations_home',
77 add_route_requirements(
78 '{repo_group_name}/settings/integrations',
79 URL_NAME_REQUIREMENTS
80 ),
81 custom_predicates=(valid_repo_group,)
82 )
83 config.add_route('repo_group_integrations_list',
84 add_route_requirements(
85 '{repo_group_name}/settings/integrations/{integration}',
86 URL_NAME_REQUIREMENTS
87 ),
88 custom_predicates=(valid_repo_group, valid_integration))
89 for route_name in ['repo_group_integrations_home', 'repo_group_integrations_list']:
90 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
91 attr='index',
92 renderer='rhodecode:templates/admin/integrations/list.html',
93 request_method='GET',
94 route_name=route_name)
95
96 config.add_route('repo_group_integrations_new',
97 add_route_requirements(
98 '{repo_group_name}/settings/integrations/new',
99 URL_NAME_REQUIREMENTS
100 ),
101 custom_predicates=(valid_repo_group,))
102 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
103 attr='new_integration',
104 renderer='rhodecode:templates/admin/integrations/new.html',
105 request_method='GET',
106 route_name='repo_group_integrations_new')
107
108 config.add_route('repo_group_integrations_create',
109 add_route_requirements(
110 '{repo_group_name}/settings/integrations/{integration}/new',
111 URL_NAME_REQUIREMENTS
112 ),
113 custom_predicates=(valid_repo_group, valid_integration))
114 config.add_route('repo_group_integrations_edit',
115 add_route_requirements(
116 '{repo_group_name}/settings/integrations/{integration}/{integration_id}',
117 URL_NAME_REQUIREMENTS
118 ),
119 custom_predicates=(valid_repo_group, valid_integration))
120 for route_name in ['repo_group_integrations_edit', 'repo_group_integrations_create']:
121 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
122 attr='settings_get',
123 renderer='rhodecode:templates/admin/integrations/form.html',
124 request_method='GET',
125 route_name=route_name)
126 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
127 attr='settings_post',
128 renderer='rhodecode:templates/admin/integrations/form.html',
60 129 request_method='POST',
61 130 route_name=route_name)
62 131
63 132
64 133 # repo integrations
65 134 config.add_route('repo_integrations_home',
66 135 add_route_requirements(
67 136 '{repo_name}/settings/integrations',
68 137 URL_NAME_REQUIREMENTS
69 138 ),
70 139 custom_predicates=(valid_repo,))
71 140 config.add_route('repo_integrations_list',
72 141 add_route_requirements(
73 142 '{repo_name}/settings/integrations/{integration}',
74 143 URL_NAME_REQUIREMENTS
75 144 ),
76 145 custom_predicates=(valid_repo, valid_integration))
77 146 for route_name in ['repo_integrations_home', 'repo_integrations_list']:
78 147 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
79 148 attr='index',
80 149 request_method='GET',
150 renderer='rhodecode:templates/admin/integrations/list.html',
81 151 route_name=route_name)
82 152
153 config.add_route('repo_integrations_new',
154 add_route_requirements(
155 '{repo_name}/settings/integrations/new',
156 URL_NAME_REQUIREMENTS
157 ),
158 custom_predicates=(valid_repo,))
159 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
160 attr='new_integration',
161 renderer='rhodecode:templates/admin/integrations/new.html',
162 request_method='GET',
163 route_name='repo_integrations_new')
164
83 165 config.add_route('repo_integrations_create',
84 166 add_route_requirements(
85 167 '{repo_name}/settings/integrations/{integration}/new',
86 168 URL_NAME_REQUIREMENTS
87 169 ),
88 170 custom_predicates=(valid_repo, valid_integration))
89 171 config.add_route('repo_integrations_edit',
90 172 add_route_requirements(
91 173 '{repo_name}/settings/integrations/{integration}/{integration_id}',
92 174 URL_NAME_REQUIREMENTS
93 175 ),
94 176 custom_predicates=(valid_repo, valid_integration))
95 177 for route_name in ['repo_integrations_edit', 'repo_integrations_create']:
96 178 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
97 179 attr='settings_get',
98 renderer='rhodecode:templates/admin/integrations/edit.html',
180 renderer='rhodecode:templates/admin/integrations/form.html',
99 181 request_method='GET',
100 182 route_name=route_name)
101 183 config.add_view('rhodecode.integrations.views.RepoIntegrationsView',
102 184 attr='settings_post',
103 renderer='rhodecode:templates/admin/integrations/edit.html',
104 request_method='POST',
105 route_name=route_name)
106
107
108 # repo group integrations
109 config.add_route('repo_group_integrations_home',
110 add_route_requirements(
111 '{repo_group_name}/settings/integrations',
112 URL_NAME_REQUIREMENTS
113 ),
114 custom_predicates=(valid_repo_group,))
115 config.add_route('repo_group_integrations_list',
116 add_route_requirements(
117 '{repo_group_name}/settings/integrations/{integration}',
118 URL_NAME_REQUIREMENTS
119 ),
120 custom_predicates=(valid_repo_group, valid_integration))
121 for route_name in ['repo_group_integrations_home', 'repo_group_integrations_list']:
122 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
123 attr='index',
124 request_method='GET',
125 route_name=route_name)
126
127 config.add_route('repo_group_integrations_create',
128 add_route_requirements(
129 '{repo_group_name}/settings/integrations/{integration}/new',
130 URL_NAME_REQUIREMENTS
131 ),
132 custom_predicates=(valid_repo_group, valid_integration))
133 config.add_route('repo_group_integrations_edit',
134 add_route_requirements(
135 '{repo_group_name}/settings/integrations/{integration}/{integration_id}',
136 URL_NAME_REQUIREMENTS
137 ),
138 custom_predicates=(valid_repo_group, valid_integration))
139 for route_name in ['repo_group_integrations_edit', 'repo_group_integrations_create']:
140 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
141 attr='settings_get',
142 renderer='rhodecode:templates/admin/integrations/edit.html',
143 request_method='GET',
144 route_name=route_name)
145 config.add_view('rhodecode.integrations.views.RepoGroupIntegrationsView',
146 attr='settings_post',
147 renderer='rhodecode:templates/admin/integrations/edit.html',
185 renderer='rhodecode:templates/admin/integrations/form.html',
148 186 request_method='POST',
149 187 route_name=route_name)
150 188
151 189
152 190 def valid_repo(info, request):
153 191 repo = Repository.get_by_repo_name(info['match']['repo_name'])
154 192 if repo:
155 193 return True
156 194
157 195
158 196 def valid_repo_group(info, request):
159 197 repo_group = RepoGroup.get_by_group_name(info['match']['repo_group_name'])
160 198 if repo_group:
161 199 return True
162 200 return False
163 201
164 202
165 203 def valid_integration(info, request):
166 204 integration_type = info['match']['integration']
167 205 integration_id = info['match'].get('integration_id')
168 206 repo_name = info['match'].get('repo_name')
169 207 repo_group_name = info['match'].get('repo_group_name')
170 208
171 209 if integration_type not in integration_type_registry:
172 210 return False
173 211
174 212 repo, repo_group = None, None
175 213 if repo_name:
176 214 repo = Repository.get_by_repo_name(repo_name)
177 215 if not repo:
178 216 return False
179 217
180 218 if repo_group_name:
181 219 repo_group = RepoGroup.get_by_group_name(repo_group_name)
182 220 if not repo_group:
183 221 return False
184 222
185 223 if repo_name and repo_group:
186 224 raise Exception('Either repo or repo_group can be set, not both')
187 225
188 226
189 227 if integration_id:
190 228 integration = Integration.get(integration_id)
191 229 if not integration:
192 230 return False
193 231 if integration.integration_type != integration_type:
194 232 return False
195 233 if repo and repo.repo_id != integration.repo_id:
196 234 return False
197 if repo_group and repo_group.repo_group_id != integration.repo_group_id:
235 if repo_group and repo_group.group_id != integration.repo_group_id:
198 236 return False
199 237
200 238 return True
@@ -1,45 +1,71 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 colander
22 22
23 from rhodecode.translation import lazy_ugettext
23 from rhodecode.translation import _
24 24
25 25
26 class IntegrationSettingsSchemaBase(colander.MappingSchema):
27 """
28 This base schema is intended for use in integrations.
29 It adds a few default settings (e.g., "enabled"), so that integration
30 authors don't have to maintain a bunch of boilerplate.
31 """
26 class IntegrationOptionsSchemaBase(colander.MappingSchema):
32 27 enabled = colander.SchemaNode(
33 28 colander.Bool(),
34 29 default=True,
35 description=lazy_ugettext('Enable or disable this integration.'),
30 description=_('Enable or disable this integration.'),
36 31 missing=False,
37 title=lazy_ugettext('Enabled'),
32 title=_('Enabled'),
38 33 )
39 34
40 35 name = colander.SchemaNode(
41 36 colander.String(),
42 description=lazy_ugettext('Short name for this integration.'),
37 description=_('Short name for this integration.'),
43 38 missing=colander.required,
44 title=lazy_ugettext('Integration name'),
39 title=_('Integration name'),
45 40 )
41
42
43 class RepoIntegrationOptionsSchema(IntegrationOptionsSchemaBase):
44 pass
45
46
47 class RepoGroupIntegrationOptionsSchema(IntegrationOptionsSchemaBase):
48 child_repos_only = colander.SchemaNode(
49 colander.Bool(),
50 default=True,
51 description=_(
52 'Limit integrations to to work only on the direct children '
53 'repositories of this repository group (no subgroups)'),
54 missing=False,
55 title=_('Limit to childen repos only'),
56 )
57
58
59 class GlobalIntegrationOptionsSchema(IntegrationOptionsSchemaBase):
60 child_repos_only = colander.SchemaNode(
61 colander.Bool(),
62 default=False,
63 description=_(
64 'Limit integrations to to work only on root level repositories'),
65 missing=False,
66 title=_('Root repositories only'),
67 )
68
69
70 class IntegrationSettingsSchemaBase(colander.MappingSchema):
71 pass
@@ -1,42 +1,101 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
21 import colander
22 from rhodecode.translation import _
22 23
23 24
24 25 class IntegrationTypeBase(object):
25 26 """ Base class for IntegrationType plugins """
26 27
28 description = ''
29 icon = '''
30 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
31 <svg
32 xmlns:dc="http://purl.org/dc/elements/1.1/"
33 xmlns:cc="http://creativecommons.org/ns#"
34 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
35 xmlns:svg="http://www.w3.org/2000/svg"
36 xmlns="http://www.w3.org/2000/svg"
37 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
38 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
39 viewBox="0 -256 1792 1792"
40 id="svg3025"
41 version="1.1"
42 inkscape:version="0.48.3.1 r9886"
43 width="100%"
44 height="100%"
45 sodipodi:docname="cog_font_awesome.svg">
46 <metadata
47 id="metadata3035">
48 <rdf:RDF>
49 <cc:Work
50 rdf:about="">
51 <dc:format>image/svg+xml</dc:format>
52 <dc:type
53 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
54 </cc:Work>
55 </rdf:RDF>
56 </metadata>
57 <defs
58 id="defs3033" />
59 <sodipodi:namedview
60 pagecolor="#ffffff"
61 bordercolor="#666666"
62 borderopacity="1"
63 objecttolerance="10"
64 gridtolerance="10"
65 guidetolerance="10"
66 inkscape:pageopacity="0"
67 inkscape:pageshadow="2"
68 inkscape:window-width="640"
69 inkscape:window-height="480"
70 id="namedview3031"
71 showgrid="false"
72 inkscape:zoom="0.13169643"
73 inkscape:cx="896"
74 inkscape:cy="896"
75 inkscape:window-x="0"
76 inkscape:window-y="25"
77 inkscape:window-maximized="0"
78 inkscape:current-layer="svg3025" />
79 <g
80 transform="matrix(1,0,0,-1,121.49153,1285.4237)"
81 id="g3027">
82 <path
83 d="m 1024,640 q 0,106 -75,181 -75,75 -181,75 -106,0 -181,-75 -75,-75 -75,-181 0,-106 75,-181 75,-75 181,-75 106,0 181,75 75,75 75,181 z m 512,109 V 527 q 0,-12 -8,-23 -8,-11 -20,-13 l -185,-28 q -19,-54 -39,-91 35,-50 107,-138 10,-12 10,-25 0,-13 -9,-23 -27,-37 -99,-108 -72,-71 -94,-71 -12,0 -26,9 l -138,108 q -44,-23 -91,-38 -16,-136 -29,-186 -7,-28 -36,-28 H 657 q -14,0 -24.5,8.5 Q 622,-111 621,-98 L 593,86 q -49,16 -90,37 L 362,16 Q 352,7 337,7 323,7 312,18 186,132 147,186 q -7,10 -7,23 0,12 8,23 15,21 51,66.5 36,45.5 54,70.5 -27,50 -41,99 L 29,495 Q 16,497 8,507.5 0,518 0,531 v 222 q 0,12 8,23 8,11 19,13 l 186,28 q 14,46 39,92 -40,57 -107,138 -10,12 -10,24 0,10 9,23 26,36 98.5,107.5 72.5,71.5 94.5,71.5 13,0 26,-10 l 138,-107 q 44,23 91,38 16,136 29,186 7,28 36,28 h 222 q 14,0 24.5,-8.5 Q 914,1391 915,1378 l 28,-184 q 49,-16 90,-37 l 142,107 q 9,9 24,9 13,0 25,-10 129,-119 165,-170 7,-8 7,-22 0,-12 -8,-23 -15,-21 -51,-66.5 -36,-45.5 -54,-70.5 26,-50 41,-98 l 183,-28 q 13,-2 21,-12.5 8,-10.5 8,-23.5 z"
84 id="path3029"
85 inkscape:connector-curvature="0"
86 style="fill:currentColor" />
87 </g>
88 </svg>
89 '''
90
27 91 def __init__(self, settings):
28 92 """
29 93 :param settings: dict of settings to be used for the integration
30 94 """
31 95 self.settings = settings
32 96
33
34 97 def settings_schema(self):
35 98 """
36 99 A colander schema of settings for the integration type
37
38 Subclasses can return their own schema but should always
39 inherit from IntegrationSettingsSchemaBase
40 100 """
41 return IntegrationSettingsSchemaBase()
42
101 return colander.Schema()
@@ -1,222 +1,283 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 from __future__ import unicode_literals
22 22 import deform
23 23 import logging
24 24 import colander
25 25
26 26 from mako.template import Template
27 27
28 28 from rhodecode import events
29 from rhodecode.translation import _, lazy_ugettext
29 from rhodecode.translation import _
30 30 from rhodecode.lib.celerylib import run_task
31 31 from rhodecode.lib.celerylib import tasks
32 32 from rhodecode.integrations.types.base import IntegrationTypeBase
33 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
34 33
35 34
36 35 log = logging.getLogger(__name__)
37 36
38 37 repo_push_template_plaintext = Template('''
39 38 Commits:
40 39
41 40 % for commit in data['push']['commits']:
42 41 ${commit['url']} by ${commit['author']} at ${commit['date']}
43 42 ${commit['message']}
44 43 ----
45 44
46 45 % endfor
47 46 ''')
48 47
49 48 ## TODO (marcink): think about putting this into a file, or use base.mako email template
50 49
51 50 repo_push_template_html = Template('''
52 51 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
53 52 <html xmlns="http://www.w3.org/1999/xhtml">
54 53 <head>
55 54 <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
56 55 <meta name="viewport" content="width=device-width, initial-scale=1.0"/>
57 56 <title>${subject}</title>
58 57 <style type="text/css">
59 58 /* Based on The MailChimp Reset INLINE: Yes. */
60 59 #outlook a {padding:0;} /* Force Outlook to provide a "view in browser" menu link. */
61 60 body{width:100% !important; -webkit-text-size-adjust:100%; -ms-text-size-adjust:100%; margin:0; padding:0;}
62 61 /* Prevent Webkit and Windows Mobile platforms from changing default font sizes.*/
63 62 .ExternalClass {width:100%;} /* Force Hotmail to display emails at full width */
64 63 .ExternalClass, .ExternalClass p, .ExternalClass span, .ExternalClass font, .ExternalClass td, .ExternalClass div {line-height: 100%;}
65 64 /* Forces Hotmail to display normal line spacing. More on that: http://www.emailonacid.com/forum/viewthread/43/ */
66 65 #backgroundTable {margin:0; padding:0; line-height: 100% !important;}
67 66 /* End reset */
68 67
69 68 /* defaults for images*/
70 69 img {outline:none; text-decoration:none; -ms-interpolation-mode: bicubic;}
71 70 a img {border:none;}
72 71 .image_fix {display:block;}
73 72
74 73 body {line-height:1.2em;}
75 74 p {margin: 0 0 20px;}
76 75 h1, h2, h3, h4, h5, h6 {color:#323232!important;}
77 76 a {color:#427cc9;text-decoration:none;outline:none;cursor:pointer;}
78 77 a:focus {outline:none;}
79 78 a:hover {color: #305b91;}
80 79 h1 a, h2 a, h3 a, h4 a, h5 a, h6 a {color:#427cc9!important;text-decoration:none!important;}
81 80 h1 a:active, h2 a:active, h3 a:active, h4 a:active, h5 a:active, h6 a:active {color: #305b91!important;}
82 81 h1 a:visited, h2 a:visited, h3 a:visited, h4 a:visited, h5 a:visited, h6 a:visited {color: #305b91!important;}
83 82 table {font-size:13px;border-collapse:collapse;mso-table-lspace:0pt;mso-table-rspace:0pt;}
84 83 table td {padding:.65em 1em .65em 0;border-collapse:collapse;vertical-align:top;text-align:left;}
85 84 input {display:inline;border-radius:2px;border-style:solid;border: 1px solid #dbd9da;padding:.5em;}
86 85 input:focus {outline: 1px solid #979797}
87 86 @media only screen and (-webkit-min-device-pixel-ratio: 2) {
88 87 /* Put your iPhone 4g styles in here */
89 88 }
90 89
91 90 /* Android targeting */
92 91 @media only screen and (-webkit-device-pixel-ratio:.75){
93 92 /* Put CSS for low density (ldpi) Android layouts in here */
94 93 }
95 94 @media only screen and (-webkit-device-pixel-ratio:1){
96 95 /* Put CSS for medium density (mdpi) Android layouts in here */
97 96 }
98 97 @media only screen and (-webkit-device-pixel-ratio:1.5){
99 98 /* Put CSS for high density (hdpi) Android layouts in here */
100 99 }
101 100 /* end Android targeting */
102 101
103 102 </style>
104 103
105 104 <!-- Targeting Windows Mobile -->
106 105 <!--[if IEMobile 7]>
107 106 <style type="text/css">
108 107
109 108 </style>
110 109 <![endif]-->
111 110
112 111 <!--[if gte mso 9]>
113 112 <style>
114 113 /* Target Outlook 2007 and 2010 */
115 114 </style>
116 115 <![endif]-->
117 116 </head>
118 117 <body>
119 118 <!-- Wrapper/Container Table: Use a wrapper table to control the width and the background color consistently of your email. Use this approach instead of setting attributes on the body tag. -->
120 119 <table cellpadding="0" cellspacing="0" border="0" id="backgroundTable" align="left" style="margin:1%;width:97%;padding:0;font-family:sans-serif;font-weight:100;border:1px solid #dbd9da">
121 120 <tr>
122 121 <td valign="top" style="padding:0;">
123 122 <table cellpadding="0" cellspacing="0" border="0" align="left" width="100%">
124 123 <tr><td style="width:100%;padding:7px;background-color:#202020" valign="top">
125 124 <a style="color:#eeeeee;text-decoration:none;" href="${instance_url}">
126 125 ${'RhodeCode'}
127 126 </a>
128 127 </td></tr>
129 128 <tr>
130 129 <td style="padding:15px;" valign="top">
131 130 % for commit in data['push']['commits']:
132 131 <a href="${commit['url']}">${commit['short_id']}</a> by ${commit['author']} at ${commit['date']} <br/>
133 132 ${commit['message_html']} <br/>
134 133 <br/>
135 134 % endfor
136 135 </td>
137 136 </tr>
138 137 </table>
139 138 </td>
140 139 </tr>
141 140 </table>
142 141 <!-- End of wrapper table -->
143 142 <p><a style="margin-top:15px;margin-left:1%;font-family:sans-serif;font-weight:100;font-size:11px;color:#666666;text-decoration:none;" href="${instance_url}">
144 143 ${'This is a notification from RhodeCode. %(instance_url)s' % {'instance_url': instance_url}}
145 144 </a></p>
146 145 </body>
147 146 </html>
148 147 ''')
149 148
149 email_icon = '''
150 <?xml version="1.0" encoding="UTF-8" standalone="no"?>
151 <svg
152 xmlns:dc="http://purl.org/dc/elements/1.1/"
153 xmlns:cc="http://creativecommons.org/ns#"
154 xmlns:rdf="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
155 xmlns:svg="http://www.w3.org/2000/svg"
156 xmlns="http://www.w3.org/2000/svg"
157 xmlns:sodipodi="http://sodipodi.sourceforge.net/DTD/sodipodi-0.dtd"
158 xmlns:inkscape="http://www.inkscape.org/namespaces/inkscape"
159 viewBox="0 -256 1850 1850"
160 id="svg2989"
161 version="1.1"
162 inkscape:version="0.48.3.1 r9886"
163 width="100%"
164 height="100%"
165 sodipodi:docname="envelope_font_awesome.svg">
166 <metadata
167 id="metadata2999">
168 <rdf:RDF>
169 <cc:Work
170 rdf:about="">
171 <dc:format>image/svg+xml</dc:format>
172 <dc:type
173 rdf:resource="http://purl.org/dc/dcmitype/StillImage" />
174 </cc:Work>
175 </rdf:RDF>
176 </metadata>
177 <defs
178 id="defs2997" />
179 <sodipodi:namedview
180 pagecolor="#ffffff"
181 bordercolor="#666666"
182 borderopacity="1"
183 objecttolerance="10"
184 gridtolerance="10"
185 guidetolerance="10"
186 inkscape:pageopacity="0"
187 inkscape:pageshadow="2"
188 inkscape:window-width="640"
189 inkscape:window-height="480"
190 id="namedview2995"
191 showgrid="false"
192 inkscape:zoom="0.13169643"
193 inkscape:cx="896"
194 inkscape:cy="896"
195 inkscape:window-x="0"
196 inkscape:window-y="25"
197 inkscape:window-maximized="0"
198 inkscape:current-layer="svg2989" />
199 <g
200 transform="matrix(1,0,0,-1,37.966102,1282.678)"
201 id="g2991">
202 <path
203 d="m 1664,32 v 768 q -32,-36 -69,-66 -268,-206 -426,-338 -51,-43 -83,-67 -32,-24 -86.5,-48.5 Q 945,256 897,256 h -1 -1 Q 847,256 792.5,280.5 738,305 706,329 674,353 623,396 465,528 197,734 160,764 128,800 V 32 Q 128,19 137.5,9.5 147,0 160,0 h 1472 q 13,0 22.5,9.5 9.5,9.5 9.5,22.5 z m 0,1051 v 11 13.5 q 0,0 -0.5,13 -0.5,13 -3,12.5 -2.5,-0.5 -5.5,9 -3,9.5 -9,7.5 -6,-2 -14,2.5 H 160 q -13,0 -22.5,-9.5 Q 128,1133 128,1120 128,952 275,836 468,684 676,519 682,514 711,489.5 740,465 757,452 774,439 801.5,420.5 829,402 852,393 q 23,-9 43,-9 h 1 1 q 20,0 43,9 23,9 50.5,27.5 27.5,18.5 44.5,31.5 17,13 46,37.5 29,24.5 35,29.5 208,165 401,317 54,43 100.5,115.5 46.5,72.5 46.5,131.5 z m 128,37 V 32 q 0,-66 -47,-113 -47,-47 -113,-47 H 160 Q 94,-128 47,-81 0,-34 0,32 v 1088 q 0,66 47,113 47,47 113,47 h 1472 q 66,0 113,-47 47,-47 47,-113 z"
204 id="path2993"
205 inkscape:connector-curvature="0"
206 style="fill:currentColor" />
207 </g>
208 </svg>
209 '''
150 210
151 class EmailSettingsSchema(IntegrationSettingsSchemaBase):
211 class EmailSettingsSchema(colander.Schema):
152 212 @colander.instantiate(validator=colander.Length(min=1))
153 213 class recipients(colander.SequenceSchema):
154 title = lazy_ugettext('Recipients')
155 description = lazy_ugettext('Email addresses to send push events to')
214 title = _('Recipients')
215 description = _('Email addresses to send push events to')
156 216 widget = deform.widget.SequenceWidget(min_len=1)
157 217
158 218 recipient = colander.SchemaNode(
159 219 colander.String(),
160 title=lazy_ugettext('Email address'),
161 description=lazy_ugettext('Email address'),
220 title=_('Email address'),
221 description=_('Email address'),
162 222 default='',
163 223 validator=colander.Email(),
164 224 widget=deform.widget.TextInputWidget(
165 225 placeholder='user@domain.com',
166 226 ),
167 227 )
168 228
169 229
170 230 class EmailIntegrationType(IntegrationTypeBase):
171 231 key = 'email'
172 display_name = lazy_ugettext('Email')
173 SettingsSchema = EmailSettingsSchema
232 display_name = _('Email')
233 description = _('Send repo push summaries to a list of recipients via email')
234 icon = email_icon
174 235
175 236 def settings_schema(self):
176 237 schema = EmailSettingsSchema()
177 238 return schema
178 239
179 240 def send_event(self, event):
180 241 data = event.as_dict()
181 242 log.debug('got event: %r', event)
182 243
183 244 if isinstance(event, events.RepoPushEvent):
184 245 repo_push_handler(data, self.settings)
185 246 else:
186 247 log.debug('ignoring event: %r', event)
187 248
188 249
189 250 def repo_push_handler(data, settings):
190 251 commit_num = len(data['push']['commits'])
191 252 server_url = data['server_url']
192 253
193 254 if commit_num == 0:
194 255 subject = '[{repo_name}] {author} pushed {commit_num} commit on branches: {branches}'.format(
195 256 author=data['actor']['username'],
196 257 repo_name=data['repo']['repo_name'],
197 258 commit_num=commit_num,
198 259 branches=', '.join(
199 260 branch['name'] for branch in data['push']['branches'])
200 261 )
201 262 else:
202 263 subject = '[{repo_name}] {author} pushed {commit_num} commits on branches: {branches}'.format(
203 264 author=data['actor']['username'],
204 265 repo_name=data['repo']['repo_name'],
205 266 commit_num=commit_num,
206 267 branches=', '.join(
207 268 branch['name'] for branch in data['push']['branches']))
208 269
209 270 email_body_plaintext = repo_push_template_plaintext.render(
210 271 data=data,
211 272 subject=subject,
212 273 instance_url=server_url)
213 274
214 275 email_body_html = repo_push_template_html.render(
215 276 data=data,
216 277 subject=subject,
217 278 instance_url=server_url)
218 279
219 280 for email_address in settings['recipients']:
220 281 run_task(
221 282 tasks.send_email, email_address, subject,
222 283 email_body_plaintext, email_body_html)
@@ -1,242 +1,243 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 from __future__ import unicode_literals
22 22 import deform
23 23 import re
24 24 import logging
25 25 import requests
26 26 import colander
27 27 import textwrap
28 28 from celery.task import task
29 29 from mako.template import Template
30 30
31 31 from rhodecode import events
32 from rhodecode.translation import lazy_ugettext
32 from rhodecode.translation import _
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib.celerylib import run_task
35 35 from rhodecode.lib.colander_utils import strip_whitespace
36 36 from rhodecode.integrations.types.base import IntegrationTypeBase
37 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
38 37
39 38 log = logging.getLogger(__name__)
40 39
41 40
42 class HipchatSettingsSchema(IntegrationSettingsSchemaBase):
41 class HipchatSettingsSchema(colander.Schema):
43 42 color_choices = [
44 ('yellow', lazy_ugettext('Yellow')),
45 ('red', lazy_ugettext('Red')),
46 ('green', lazy_ugettext('Green')),
47 ('purple', lazy_ugettext('Purple')),
48 ('gray', lazy_ugettext('Gray')),
43 ('yellow', _('Yellow')),
44 ('red', _('Red')),
45 ('green', _('Green')),
46 ('purple', _('Purple')),
47 ('gray', _('Gray')),
49 48 ]
50 49
51 50 server_url = colander.SchemaNode(
52 51 colander.String(),
53 title=lazy_ugettext('Hipchat server URL'),
54 description=lazy_ugettext('Hipchat integration url.'),
52 title=_('Hipchat server URL'),
53 description=_('Hipchat integration url.'),
55 54 default='',
56 55 preparer=strip_whitespace,
57 56 validator=colander.url,
58 57 widget=deform.widget.TextInputWidget(
59 58 placeholder='https://?.hipchat.com/v2/room/?/notification?auth_token=?',
60 59 ),
61 60 )
62 61 notify = colander.SchemaNode(
63 62 colander.Bool(),
64 title=lazy_ugettext('Notify'),
65 description=lazy_ugettext('Make a notification to the users in room.'),
63 title=_('Notify'),
64 description=_('Make a notification to the users in room.'),
66 65 missing=False,
67 66 default=False,
68 67 )
69 68 color = colander.SchemaNode(
70 69 colander.String(),
71 title=lazy_ugettext('Color'),
72 description=lazy_ugettext('Background color of message.'),
70 title=_('Color'),
71 description=_('Background color of message.'),
73 72 missing='',
74 73 validator=colander.OneOf([x[0] for x in color_choices]),
75 74 widget=deform.widget.Select2Widget(
76 75 values=color_choices,
77 76 ),
78 77 )
79 78
80 79
81 80 repo_push_template = Template('''
82 81 <b>${data['actor']['username']}</b> pushed to
83 82 %if data['push']['branches']:
84 83 ${len(data['push']['branches']) > 1 and 'branches' or 'branch'}
85 84 ${', '.join('<a href="%s">%s</a>' % (branch['url'], branch['name']) for branch in data['push']['branches'])}
86 85 %else:
87 86 unknown branch
88 87 %endif
89 88 in <a href="${data['repo']['url']}">${data['repo']['repo_name']}</a>
90 89 <br>
91 90 <ul>
92 91 %for commit in data['push']['commits']:
93 92 <li>
94 93 <a href="${commit['url']}">${commit['short_id']}</a> - ${commit['message_html']}
95 94 </li>
96 95 %endfor
97 96 </ul>
98 97 ''')
99 98
100 99
101
102 100 class HipchatIntegrationType(IntegrationTypeBase):
103 101 key = 'hipchat'
104 display_name = lazy_ugettext('Hipchat')
102 display_name = _('Hipchat')
103 description = _('Send events such as repo pushes and pull requests to '
104 'your hipchat channel.')
105 icon = '''<?xml version="1.0" encoding="utf-8"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px" viewBox="0 0 1000 1000" enable-background="new 0 0 1000 1000" xml:space="preserve"><g><g transform="translate(0.000000,511.000000) scale(0.100000,-0.100000)"><path fill="#205281" d="M4197.1,4662.4c-1661.5-260.4-3018-1171.6-3682.6-2473.3C219.9,1613.6,100,1120.3,100,462.6c0-1014,376.8-1918.4,1127-2699.4C2326.7-3377.6,3878.5-3898.3,5701-3730.5l486.5,44.5l208.9-123.3c637.2-373.4,1551.8-640.6,2240.4-650.9c304.9-6.9,335.7,0,417.9,75.4c185,174.7,147.3,411.1-89.1,548.1c-315.2,181.6-620,544.7-733.1,870.1l-51.4,157.6l472.7,472.7c349.4,349.4,520.7,551.5,657.7,774.2c784.5,1281.2,784.5,2788.5,0,4052.6c-236.4,376.8-794.8,966-1178.4,1236.7c-572.1,407.7-1264.1,709.1-1993.7,870.1c-267.2,58.2-479.6,75.4-1038,82.2C4714.4,4686.4,4310.2,4679.6,4197.1,4662.4z M5947.6,3740.9c1856.7-380.3,3127.6-1709.4,3127.6-3275c0-1000.3-534.4-1949.2-1466.2-2600.1c-188.4-133.6-287.8-226.1-301.5-284.4c-41.1-157.6,263.8-938.6,397.4-1020.8c20.5-10.3,34.3-44.5,34.3-75.4c0-167.8-811.9,195.3-1363.4,609.8l-181.6,137l-332.3-58.2c-445.3-78.8-1281.2-78.8-1702.6,0C2796-2569.2,1734.1-1832.6,1220.2-801.5C983.8-318.5,905,51.5,929,613.3c27.4,640.6,243.2,1192.1,685.1,1740.3c620,770.8,1661.5,1305.2,2822.8,1452.5C4806.9,3854,5553.7,3819.7,5947.6,3740.9z"/><path fill="#205281" d="M2381.5-345.9c-75.4-106.2-68.5-167.8,34.3-322c332.3-500.2,1010.6-928.4,1760.8-1120.2c417.9-106.2,1226.4-106.2,1644.3,0c712.5,181.6,1270.9,517.3,1685.4,1014C7681-561.7,7715.3-424.7,7616-325.4c-89.1,89.1-167.9,65.1-431.7-133.6c-835.8-630.3-2028-856.4-3086.5-585.8C3683.3-938.6,3142-685,2830.3-448.7C2576.8-253.4,2463.7-229.4,2381.5-345.9z"/></g></g><!-- Svg Vector Icons : http://www.onlinewebfonts.com/icon --></svg>'''
105 106 valid_events = [
106 107 events.PullRequestCloseEvent,
107 108 events.PullRequestMergeEvent,
108 109 events.PullRequestUpdateEvent,
109 110 events.PullRequestCommentEvent,
110 111 events.PullRequestReviewEvent,
111 112 events.PullRequestCreateEvent,
112 113 events.RepoPushEvent,
113 114 events.RepoCreateEvent,
114 115 ]
115 116
116 117 def send_event(self, event):
117 118 if event.__class__ not in self.valid_events:
118 119 log.debug('event not valid: %r' % event)
119 120 return
120 121
121 122 if event.name not in self.settings['events']:
122 123 log.debug('event ignored: %r' % event)
123 124 return
124 125
125 126 data = event.as_dict()
126 127
127 128 text = '<b>%s<b> caused a <b>%s</b> event' % (
128 129 data['actor']['username'], event.name)
129 130
130 131 log.debug('handling hipchat event for %s' % event.name)
131 132
132 133 if isinstance(event, events.PullRequestCommentEvent):
133 134 text = self.format_pull_request_comment_event(event, data)
134 135 elif isinstance(event, events.PullRequestReviewEvent):
135 136 text = self.format_pull_request_review_event(event, data)
136 137 elif isinstance(event, events.PullRequestEvent):
137 138 text = self.format_pull_request_event(event, data)
138 139 elif isinstance(event, events.RepoPushEvent):
139 140 text = self.format_repo_push_event(data)
140 141 elif isinstance(event, events.RepoCreateEvent):
141 142 text = self.format_repo_create_event(data)
142 143 else:
143 144 log.error('unhandled event type: %r' % event)
144 145
145 146 run_task(post_text_to_hipchat, self.settings, text)
146 147
147 148 def settings_schema(self):
148 149 schema = HipchatSettingsSchema()
149 150 schema.add(colander.SchemaNode(
150 151 colander.Set(),
151 152 widget=deform.widget.CheckboxChoiceWidget(
152 153 values=sorted(
153 154 [(e.name, e.display_name) for e in self.valid_events]
154 155 )
155 156 ),
156 157 description="Events activated for this integration",
157 158 name='events'
158 159 ))
159 160
160 161 return schema
161 162
162 163 def format_pull_request_comment_event(self, event, data):
163 164 comment_text = data['comment']['text']
164 165 if len(comment_text) > 200:
165 166 comment_text = '{comment_text}<a href="{comment_url}">...<a/>'.format(
166 167 comment_text=comment_text[:200],
167 168 comment_url=data['comment']['url'],
168 169 )
169 170
170 171 comment_status = ''
171 172 if data['comment']['status']:
172 173 comment_status = '[{}]: '.format(data['comment']['status'])
173 174
174 175 return (textwrap.dedent(
175 176 '''
176 177 {user} commented on pull request <a href="{pr_url}">{number}</a> - {pr_title}:
177 178 >>> {comment_status}{comment_text}
178 179 ''').format(
179 180 comment_status=comment_status,
180 181 user=data['actor']['username'],
181 182 number=data['pullrequest']['pull_request_id'],
182 183 pr_url=data['pullrequest']['url'],
183 184 pr_status=data['pullrequest']['status'],
184 185 pr_title=data['pullrequest']['title'],
185 186 comment_text=comment_text
186 187 )
187 188 )
188 189
189 190 def format_pull_request_review_event(self, event, data):
190 191 return (textwrap.dedent(
191 192 '''
192 193 Status changed to {pr_status} for pull request <a href="{pr_url}">#{number}</a> - {pr_title}
193 194 ''').format(
194 195 user=data['actor']['username'],
195 196 number=data['pullrequest']['pull_request_id'],
196 197 pr_url=data['pullrequest']['url'],
197 198 pr_status=data['pullrequest']['status'],
198 199 pr_title=data['pullrequest']['title'],
199 200 )
200 201 )
201 202
202 203 def format_pull_request_event(self, event, data):
203 204 action = {
204 205 events.PullRequestCloseEvent: 'closed',
205 206 events.PullRequestMergeEvent: 'merged',
206 207 events.PullRequestUpdateEvent: 'updated',
207 208 events.PullRequestCreateEvent: 'created',
208 209 }.get(event.__class__, str(event.__class__))
209 210
210 211 return ('Pull request <a href="{url}">#{number}</a> - {title} '
211 212 '{action} by {user}').format(
212 213 user=data['actor']['username'],
213 214 number=data['pullrequest']['pull_request_id'],
214 215 url=data['pullrequest']['url'],
215 216 title=data['pullrequest']['title'],
216 217 action=action
217 218 )
218 219
219 220 def format_repo_push_event(self, data):
220 221 result = repo_push_template.render(
221 222 data=data,
222 223 )
223 224 return result
224 225
225 226 def format_repo_create_event(self, data):
226 227 return '<a href="{}">{}</a> ({}) repository created by <b>{}</b>'.format(
227 228 data['repo']['url'],
228 229 data['repo']['repo_name'],
229 230 data['repo']['repo_type'],
230 231 data['actor']['username'],
231 232 )
232 233
233 234
234 235 @task(ignore_result=True)
235 236 def post_text_to_hipchat(settings, text):
236 237 log.debug('sending %s to hipchat %s' % (text, settings['server_url']))
237 238 resp = requests.post(settings['server_url'], json={
238 239 "message": text,
239 240 "color": settings.get('color', 'yellow'),
240 241 "notify": settings.get('notify', False),
241 242 })
242 243 resp.raise_for_status() # raise exception on a failed request
@@ -1,253 +1,256 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 from __future__ import unicode_literals
22 22 import deform
23 23 import re
24 24 import logging
25 25 import requests
26 26 import colander
27 27 import textwrap
28 28 from celery.task import task
29 29 from mako.template import Template
30 30
31 31 from rhodecode import events
32 from rhodecode.translation import lazy_ugettext
32 from rhodecode.translation import _
33 33 from rhodecode.lib import helpers as h
34 34 from rhodecode.lib.celerylib import run_task
35 35 from rhodecode.lib.colander_utils import strip_whitespace
36 36 from rhodecode.integrations.types.base import IntegrationTypeBase
37 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
38 37
39 38 log = logging.getLogger(__name__)
40 39
41 40
42 class SlackSettingsSchema(IntegrationSettingsSchemaBase):
41 class SlackSettingsSchema(colander.Schema):
43 42 service = colander.SchemaNode(
44 43 colander.String(),
45 title=lazy_ugettext('Slack service URL'),
46 description=h.literal(lazy_ugettext(
44 title=_('Slack service URL'),
45 description=h.literal(_(
47 46 'This can be setup at the '
48 47 '<a href="https://my.slack.com/services/new/incoming-webhook/">'
49 48 'slack app manager</a>')),
50 49 default='',
51 50 preparer=strip_whitespace,
52 51 validator=colander.url,
53 52 widget=deform.widget.TextInputWidget(
54 53 placeholder='https://hooks.slack.com/services/...',
55 54 ),
56 55 )
57 56 username = colander.SchemaNode(
58 57 colander.String(),
59 title=lazy_ugettext('Username'),
60 description=lazy_ugettext('Username to show notifications coming from.'),
58 title=_('Username'),
59 description=_('Username to show notifications coming from.'),
61 60 missing='Rhodecode',
62 61 preparer=strip_whitespace,
63 62 widget=deform.widget.TextInputWidget(
64 63 placeholder='Rhodecode'
65 64 ),
66 65 )
67 66 channel = colander.SchemaNode(
68 67 colander.String(),
69 title=lazy_ugettext('Channel'),
70 description=lazy_ugettext('Channel to send notifications to.'),
68 title=_('Channel'),
69 description=_('Channel to send notifications to.'),
71 70 missing='',
72 71 preparer=strip_whitespace,
73 72 widget=deform.widget.TextInputWidget(
74 73 placeholder='#general'
75 74 ),
76 75 )
77 76 icon_emoji = colander.SchemaNode(
78 77 colander.String(),
79 title=lazy_ugettext('Emoji'),
80 description=lazy_ugettext('Emoji to use eg. :studio_microphone:'),
78 title=_('Emoji'),
79 description=_('Emoji to use eg. :studio_microphone:'),
81 80 missing='',
82 81 preparer=strip_whitespace,
83 82 widget=deform.widget.TextInputWidget(
84 83 placeholder=':studio_microphone:'
85 84 ),
86 85 )
87 86
88 87
89 88 repo_push_template = Template(r'''
90 89 *${data['actor']['username']}* pushed to \
91 90 %if data['push']['branches']:
92 91 ${len(data['push']['branches']) > 1 and 'branches' or 'branch'} \
93 92 ${', '.join('<%s|%s>' % (branch['url'], branch['name']) for branch in data['push']['branches'])} \
94 93 %else:
95 94 unknown branch \
96 95 %endif
97 96 in <${data['repo']['url']}|${data['repo']['repo_name']}>
98 97 >>>
99 98 %for commit in data['push']['commits']:
100 99 <${commit['url']}|${commit['short_id']}> - ${commit['message_html']|html_to_slack_links}
101 100 %endfor
102 101 ''')
103 102
104 103
104
105
105 106 class SlackIntegrationType(IntegrationTypeBase):
106 107 key = 'slack'
107 display_name = lazy_ugettext('Slack')
108 SettingsSchema = SlackSettingsSchema
108 display_name = _('Slack')
109 description = _('Send events such as repo pushes and pull requests to '
110 'your slack channel.')
111 icon = '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 256" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M165.963541,15.8384262 C162.07318,3.86308197 149.212328,-2.69009836 137.239082,1.20236066 C125.263738,5.09272131 118.710557,17.9535738 122.603016,29.9268197 L181.550164,211.292328 C185.597902,222.478689 197.682361,228.765377 209.282098,225.426885 C221.381246,221.943607 228.756984,209.093246 224.896,197.21023 C224.749115,196.756984 165.963541,15.8384262 165.963541,15.8384262" fill="#DFA22F"></path><path d="M74.6260984,45.515541 C70.7336393,33.5422951 57.8727869,26.9891148 45.899541,30.8794754 C33.9241967,34.7698361 27.3710164,47.6306885 31.2634754,59.6060328 L90.210623,240.971541 C94.2583607,252.157902 106.34282,258.44459 117.942557,255.104 C130.041705,251.62282 137.417443,238.772459 133.556459,226.887344 C133.409574,226.436197 74.6260984,45.515541 74.6260984,45.515541" fill="#3CB187"></path><path d="M240.161574,166.045377 C252.136918,162.155016 258.688,149.294164 254.797639,137.31882 C250.907279,125.345574 238.046426,118.792393 226.07318,122.682754 L44.7076721,181.632 C33.5213115,185.677639 27.234623,197.762098 30.5731148,209.361836 C34.0563934,221.460984 46.9067541,228.836721 58.7897705,224.975738 C59.2430164,224.828852 240.161574,166.045377 240.161574,166.045377" fill="#CE1E5B"></path><path d="M82.507541,217.270557 C94.312918,213.434754 109.528131,208.491016 125.855475,203.186361 C122.019672,191.380984 117.075934,176.163672 111.76918,159.83423 L68.4191475,173.924721 L82.507541,217.270557" fill="#392538"></path><path d="M173.847082,187.591344 C190.235279,182.267803 205.467279,177.31777 217.195016,173.507148 C213.359213,161.70177 208.413377,146.480262 203.106623,130.146623 L159.75659,144.237115 L173.847082,187.591344" fill="#BB242A"></path><path d="M210.484459,74.7058361 C222.457705,70.8154754 229.010885,57.954623 225.120525,45.9792787 C221.230164,34.0060328 208.369311,27.4528525 196.393967,31.3432131 L15.028459,90.292459 C3.84209836,94.3380984 -2.44459016,106.422557 0.896,118.022295 C4.37718033,130.121443 17.227541,137.49718 29.1126557,133.636197 C29.5638033,133.489311 210.484459,74.7058361 210.484459,74.7058361" fill="#72C5CD"></path><path d="M52.8220328,125.933115 C64.6274098,122.097311 79.8468197,117.151475 96.1762623,111.84682 C90.8527213,95.4565246 85.9026885,80.2245246 82.0920656,68.4946885 L38.731541,82.5872787 L52.8220328,125.933115" fill="#248C73"></path><path d="M144.159475,96.256 C160.551869,90.9303607 175.785967,85.9803279 187.515803,82.1676066 C182.190164,65.7752131 177.240131,50.5390164 173.42741,38.807082 L130.068984,52.8996721 L144.159475,96.256" fill="#62803A"></path></g></svg>'''
109 112 valid_events = [
110 113 events.PullRequestCloseEvent,
111 114 events.PullRequestMergeEvent,
112 115 events.PullRequestUpdateEvent,
113 116 events.PullRequestCommentEvent,
114 117 events.PullRequestReviewEvent,
115 118 events.PullRequestCreateEvent,
116 119 events.RepoPushEvent,
117 120 events.RepoCreateEvent,
118 121 ]
119 122
120 123 def send_event(self, event):
121 124 if event.__class__ not in self.valid_events:
122 125 log.debug('event not valid: %r' % event)
123 126 return
124 127
125 128 if event.name not in self.settings['events']:
126 129 log.debug('event ignored: %r' % event)
127 130 return
128 131
129 132 data = event.as_dict()
130 133
131 134 text = '*%s* caused a *%s* event' % (
132 135 data['actor']['username'], event.name)
133 136
134 137 log.debug('handling slack event for %s' % event.name)
135 138
136 139 if isinstance(event, events.PullRequestCommentEvent):
137 140 text = self.format_pull_request_comment_event(event, data)
138 141 elif isinstance(event, events.PullRequestReviewEvent):
139 142 text = self.format_pull_request_review_event(event, data)
140 143 elif isinstance(event, events.PullRequestEvent):
141 144 text = self.format_pull_request_event(event, data)
142 145 elif isinstance(event, events.RepoPushEvent):
143 146 text = self.format_repo_push_event(data)
144 147 elif isinstance(event, events.RepoCreateEvent):
145 148 text = self.format_repo_create_event(data)
146 149 else:
147 150 log.error('unhandled event type: %r' % event)
148 151
149 152 run_task(post_text_to_slack, self.settings, text)
150 153
151 154 def settings_schema(self):
152 155 schema = SlackSettingsSchema()
153 156 schema.add(colander.SchemaNode(
154 157 colander.Set(),
155 158 widget=deform.widget.CheckboxChoiceWidget(
156 159 values=sorted(
157 160 [(e.name, e.display_name) for e in self.valid_events]
158 161 )
159 162 ),
160 163 description="Events activated for this integration",
161 164 name='events'
162 165 ))
163 166
164 167 return schema
165 168
166 169 def format_pull_request_comment_event(self, event, data):
167 170 comment_text = data['comment']['text']
168 171 if len(comment_text) > 200:
169 172 comment_text = '<{comment_url}|{comment_text}...>'.format(
170 173 comment_text=comment_text[:200],
171 174 comment_url=data['comment']['url'],
172 175 )
173 176
174 177 comment_status = ''
175 178 if data['comment']['status']:
176 179 comment_status = '[{}]: '.format(data['comment']['status'])
177 180
178 181 return (textwrap.dedent(
179 182 '''
180 183 {user} commented on pull request <{pr_url}|#{number}> - {pr_title}:
181 184 >>> {comment_status}{comment_text}
182 185 ''').format(
183 186 comment_status=comment_status,
184 187 user=data['actor']['username'],
185 188 number=data['pullrequest']['pull_request_id'],
186 189 pr_url=data['pullrequest']['url'],
187 190 pr_status=data['pullrequest']['status'],
188 191 pr_title=data['pullrequest']['title'],
189 192 comment_text=comment_text
190 193 )
191 194 )
192 195
193 196 def format_pull_request_review_event(self, event, data):
194 197 return (textwrap.dedent(
195 198 '''
196 199 Status changed to {pr_status} for pull request <{pr_url}|#{number}> - {pr_title}
197 200 ''').format(
198 201 user=data['actor']['username'],
199 202 number=data['pullrequest']['pull_request_id'],
200 203 pr_url=data['pullrequest']['url'],
201 204 pr_status=data['pullrequest']['status'],
202 205 pr_title=data['pullrequest']['title'],
203 206 )
204 207 )
205 208
206 209 def format_pull_request_event(self, event, data):
207 210 action = {
208 211 events.PullRequestCloseEvent: 'closed',
209 212 events.PullRequestMergeEvent: 'merged',
210 213 events.PullRequestUpdateEvent: 'updated',
211 214 events.PullRequestCreateEvent: 'created',
212 215 }.get(event.__class__, str(event.__class__))
213 216
214 217 return ('Pull request <{url}|#{number}> - {title} '
215 218 '{action} by {user}').format(
216 219 user=data['actor']['username'],
217 220 number=data['pullrequest']['pull_request_id'],
218 221 url=data['pullrequest']['url'],
219 222 title=data['pullrequest']['title'],
220 223 action=action
221 224 )
222 225
223 226 def format_repo_push_event(self, data):
224 227 result = repo_push_template.render(
225 228 data=data,
226 229 html_to_slack_links=html_to_slack_links,
227 230 )
228 231 return result
229 232
230 233 def format_repo_create_event(self, data):
231 234 return '<{}|{}> ({}) repository created by *{}*'.format(
232 235 data['repo']['url'],
233 236 data['repo']['repo_name'],
234 237 data['repo']['repo_type'],
235 238 data['actor']['username'],
236 239 )
237 240
238 241
239 242 def html_to_slack_links(message):
240 243 return re.compile(r'<a .*?href=["\'](.+?)".*?>(.+?)</a>').sub(
241 244 r'<\1|\2>', message)
242 245
243 246
244 247 @task(ignore_result=True)
245 248 def post_text_to_slack(settings, text):
246 249 log.debug('sending %s to slack %s' % (text, settings['service']))
247 250 resp = requests.post(settings['service'], json={
248 251 "channel": settings.get('channel', ''),
249 252 "username": settings.get('username', 'Rhodecode'),
250 253 "text": text,
251 254 "icon_emoji": settings.get('icon_emoji', ':studio_microphone:')
252 255 })
253 256 resp.raise_for_status() # raise exception on a failed request
@@ -1,111 +1,117 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 from __future__ import unicode_literals
22 22
23 23 import deform
24 24 import logging
25 25 import requests
26 26 import colander
27 27 from celery.task import task
28 28 from mako.template import Template
29 29
30 30 from rhodecode import events
31 from rhodecode.translation import lazy_ugettext
31 from rhodecode.translation import _
32 32 from rhodecode.integrations.types.base import IntegrationTypeBase
33 from rhodecode.integrations.schema import IntegrationSettingsSchemaBase
34 33
35 34 log = logging.getLogger(__name__)
36 35
37 36
38 class WebhookSettingsSchema(IntegrationSettingsSchemaBase):
37 class WebhookSettingsSchema(colander.Schema):
39 38 url = colander.SchemaNode(
40 39 colander.String(),
41 title=lazy_ugettext('Webhook URL'),
42 description=lazy_ugettext('URL of the webhook to receive POST event.'),
43 default='',
40 title=_('Webhook URL'),
41 description=_('URL of the webhook to receive POST event.'),
42 missing=colander.required,
43 required=True,
44 44 validator=colander.url,
45 45 widget=deform.widget.TextInputWidget(
46 46 placeholder='https://www.example.com/webhook'
47 47 ),
48 48 )
49 49 secret_token = colander.SchemaNode(
50 50 colander.String(),
51 title=lazy_ugettext('Secret Token'),
52 description=lazy_ugettext('String used to validate received payloads.'),
51 title=_('Secret Token'),
52 description=_('String used to validate received payloads.'),
53 53 default='',
54 missing='',
54 55 widget=deform.widget.TextInputWidget(
55 56 placeholder='secret_token'
56 57 ),
57 58 )
58 59
59 60
61
62
60 63 class WebhookIntegrationType(IntegrationTypeBase):
61 64 key = 'webhook'
62 display_name = lazy_ugettext('Webhook')
65 display_name = _('Webhook')
66 description = _('Post json events to a webhook endpoint')
67 icon = '''<?xml version="1.0" encoding="UTF-8" standalone="no"?><svg viewBox="0 0 256 239" version="1.1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" preserveAspectRatio="xMidYMid"><g><path d="M119.540432,100.502743 C108.930124,118.338815 98.7646301,135.611455 88.3876025,152.753617 C85.7226696,157.154315 84.4040417,160.738531 86.5332204,166.333309 C92.4107024,181.787152 84.1193605,196.825836 68.5350381,200.908244 C53.8383677,204.759349 39.5192953,195.099955 36.6032893,179.365384 C34.0194114,165.437749 44.8274148,151.78491 60.1824106,149.608284 C61.4694072,149.424428 62.7821041,149.402681 64.944891,149.240571 C72.469175,136.623655 80.1773157,123.700312 88.3025935,110.073173 C73.611854,95.4654658 64.8677898,78.3885437 66.803227,57.2292132 C68.1712787,42.2715849 74.0527146,29.3462646 84.8033863,18.7517722 C105.393354,-1.53572199 136.805164,-4.82141828 161.048542,10.7510424 C184.333097,25.7086706 194.996783,54.8450075 185.906752,79.7822957 C179.052655,77.9239597 172.151111,76.049808 164.563565,73.9917997 C167.418285,60.1274266 165.306899,47.6765751 155.95591,37.0109123 C149.777932,29.9690049 141.850349,26.2780332 132.835442,24.9178894 C114.764113,22.1877169 97.0209573,33.7983633 91.7563309,51.5355878 C85.7800012,71.6669027 94.8245623,88.1111998 119.540432,100.502743 L119.540432,100.502743 Z" fill="#C73A63"></path><path d="M149.841194,79.4106285 C157.316054,92.5969067 164.905578,105.982857 172.427885,119.246236 C210.44865,107.483365 239.114472,128.530009 249.398582,151.063322 C261.81978,178.282014 253.328765,210.520191 228.933162,227.312431 C203.893073,244.551464 172.226236,241.605803 150.040866,219.46195 C155.694953,214.729124 161.376716,209.974552 167.44794,204.895759 C189.360489,219.088306 208.525074,218.420096 222.753207,201.614016 C234.885769,187.277151 234.622834,165.900356 222.138374,151.863988 C207.730339,135.66681 188.431321,135.172572 165.103273,150.721309 C155.426087,133.553447 145.58086,116.521995 136.210101,99.2295848 C133.05093,93.4015266 129.561608,90.0209366 122.440622,88.7873178 C110.547271,86.7253555 102.868785,76.5124151 102.408155,65.0698097 C101.955433,53.7537294 108.621719,43.5249733 119.04224,39.5394355 C129.363912,35.5914599 141.476705,38.7783085 148.419765,47.554004 C154.093621,54.7244134 155.896602,62.7943365 152.911402,71.6372484 C152.081082,74.1025091 151.00562,76.4886916 149.841194,79.4106285 L149.841194,79.4106285 Z" fill="#4B4B4B"></path><path d="M167.706921,187.209935 L121.936499,187.209935 C117.54964,205.253587 108.074103,219.821756 91.7464461,229.085759 C79.0544063,236.285822 65.3738898,238.72736 50.8136292,236.376762 C24.0061432,232.053165 2.08568567,207.920497 0.156179306,180.745298 C-2.02835403,149.962159 19.1309765,122.599149 47.3341915,116.452801 C49.2814904,123.524363 51.2485589,130.663141 53.1958579,137.716911 C27.3195169,150.919004 18.3639187,167.553089 25.6054984,188.352614 C31.9811726,206.657224 50.0900643,216.690262 69.7528413,212.809503 C89.8327554,208.847688 99.9567329,192.160226 98.7211371,165.37844 C117.75722,165.37844 136.809118,165.180745 155.847178,165.475311 C163.280522,165.591951 169.019617,164.820939 174.620326,158.267339 C183.840836,147.48306 200.811003,148.455721 210.741239,158.640984 C220.88894,169.049642 220.402609,185.79839 209.663799,195.768166 C199.302587,205.38802 182.933414,204.874012 173.240413,194.508846 C171.247644,192.37176 169.677943,189.835329 167.706921,187.209935 L167.706921,187.209935 Z" fill="#4A4A4A"></path></g></svg>'''
68
63 69 valid_events = [
64 70 events.PullRequestCloseEvent,
65 71 events.PullRequestMergeEvent,
66 72 events.PullRequestUpdateEvent,
67 73 events.PullRequestCommentEvent,
68 74 events.PullRequestReviewEvent,
69 75 events.PullRequestCreateEvent,
70 76 events.RepoPushEvent,
71 77 events.RepoCreateEvent,
72 78 ]
73 79
74 80 def settings_schema(self):
75 81 schema = WebhookSettingsSchema()
76 82 schema.add(colander.SchemaNode(
77 83 colander.Set(),
78 84 widget=deform.widget.CheckboxChoiceWidget(
79 85 values=sorted(
80 86 [(e.name, e.display_name) for e in self.valid_events]
81 87 )
82 88 ),
83 89 description="Events activated for this integration",
84 90 name='events'
85 91 ))
86 92 return schema
87 93
88 94 def send_event(self, event):
89 95 log.debug('handling event %s with webhook integration %s',
90 96 event.name, self)
91 97
92 98 if event.__class__ not in self.valid_events:
93 99 log.debug('event not valid: %r' % event)
94 100 return
95 101
96 102 if event.name not in self.settings['events']:
97 103 log.debug('event ignored: %r' % event)
98 104 return
99 105
100 106 data = event.as_dict()
101 107 post_to_webhook(data, self.settings)
102 108
103 109
104 110 @task(ignore_result=True)
105 111 def post_to_webhook(data, settings):
106 112 log.debug('sending event:%s to webhook %s', data['name'], settings['url'])
107 113 resp = requests.post(settings['url'], json={
108 114 'token': settings['secret_token'],
109 115 'event': data
110 116 })
111 117 resp.raise_for_status() # raise exception on a failed request
@@ -1,299 +1,385 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2012-2016 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 import colander
22 import logging
23 21 import pylons
24 22 import deform
23 import logging
24 import colander
25 import peppercorn
26 import webhelpers.paginate
25 27
26 from pyramid.httpexceptions import HTTPFound, HTTPForbidden
28 from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest
27 29 from pyramid.renderers import render
28 30 from pyramid.response import Response
29 31
30 32 from rhodecode.lib import auth
31 33 from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
34 from rhodecode.lib.utils2 import safe_int
35 from rhodecode.lib.helpers import Page
32 36 from rhodecode.model.db import Repository, RepoGroup, Session, Integration
33 37 from rhodecode.model.scm import ScmModel
34 38 from rhodecode.model.integration import IntegrationModel
35 39 from rhodecode.admin.navigation import navigation_list
36 40 from rhodecode.translation import _
37 41 from rhodecode.integrations import integration_type_registry
42 from rhodecode.model.validation_schema.schemas.integration_schema import (
43 make_integration_schema)
38 44
39 45 log = logging.getLogger(__name__)
40 46
41 47
42 48 class IntegrationSettingsViewBase(object):
43 49 """ Base Integration settings view used by both repo / global settings """
44 50
45 51 def __init__(self, context, request):
46 52 self.context = context
47 53 self.request = request
48 54 self._load_general_context()
49 55
50 56 if not self.perm_check(request.user):
51 57 raise HTTPForbidden()
52 58
53 59 def _load_general_context(self):
54 60 """
55 61 This avoids boilerplate for repo/global+list/edit+views/templates
56 62 by doing all possible contexts at the same time however it should
57 63 be split up into separate functions once more "contexts" exist
58 64 """
59 65
60 66 self.IntegrationType = None
61 67 self.repo = None
62 68 self.repo_group = None
63 69 self.integration = None
64 70 self.integrations = {}
65 71
66 72 request = self.request
67 73
68 if 'repo_name' in request.matchdict: # we're in a repo context
74 if 'repo_name' in request.matchdict: # in repo settings context
69 75 repo_name = request.matchdict['repo_name']
70 76 self.repo = Repository.get_by_repo_name(repo_name)
71 77
72 if 'repo_group_name' in request.matchdict: # we're in repo_group context
78 if 'repo_group_name' in request.matchdict: # in group settings context
73 79 repo_group_name = request.matchdict['repo_group_name']
74 80 self.repo_group = RepoGroup.get_by_group_name(repo_group_name)
75 81
76 if 'integration' in request.matchdict: # we're in integration context
82
83 if 'integration' in request.matchdict: # integration type context
77 84 integration_type = request.matchdict['integration']
78 85 self.IntegrationType = integration_type_registry[integration_type]
79 86
80 87 if 'integration_id' in request.matchdict: # single integration context
81 88 integration_id = request.matchdict['integration_id']
82 89 self.integration = Integration.get(integration_id)
83 else: # list integrations context
84 integrations = IntegrationModel().get_integrations(
85 repo=self.repo, repo_group=self.repo_group)
86 90
87 for integration in integrations:
88 self.integrations.setdefault(integration.integration_type, []
89 ).append(integration)
91 # extra perms check just in case
92 if not self._has_perms_for_integration(self.integration):
93 raise HTTPForbidden()
90 94
91 95 self.settings = self.integration and self.integration.settings or {}
96 self.admin_view = not (self.repo or self.repo_group)
97
98 def _has_perms_for_integration(self, integration):
99 perms = self.request.user.permissions
100
101 if 'hg.admin' in perms['global']:
102 return True
103
104 if integration.repo:
105 return perms['repositories'].get(
106 integration.repo.repo_name) == 'repository.admin'
107
108 if integration.repo_group:
109 return perms['repositories_groups'].get(
110 integration.repo_group.group_name) == 'group.admin'
111
112 return False
92 113
93 114 def _template_c_context(self):
94 115 # TODO: dan: this is a stopgap in order to inherit from current pylons
95 116 # based admin/repo settings templates - this should be removed entirely
96 117 # after port to pyramid
97 118
98 119 c = pylons.tmpl_context
99 120 c.active = 'integrations'
100 121 c.rhodecode_user = self.request.user
101 122 c.repo = self.repo
102 123 c.repo_group = self.repo_group
103 124 c.repo_name = self.repo and self.repo.repo_name or None
104 125 c.repo_group_name = self.repo_group and self.repo_group.group_name or None
126
105 127 if self.repo:
106 128 c.repo_info = self.repo
107 129 c.rhodecode_db_repo = self.repo
108 130 c.repository_pull_requests = ScmModel().get_pull_requests(self.repo)
109 131 else:
110 132 c.navlist = navigation_list(self.request)
111 133
112 134 return c
113 135
114 136 def _form_schema(self):
115 if self.integration:
116 settings = self.integration.settings
117 else:
118 settings = {}
119 return self.IntegrationType(settings=settings).settings_schema()
137 schema = make_integration_schema(IntegrationType=self.IntegrationType,
138 settings=self.settings)
120 139
121 def settings_get(self, defaults=None, errors=None, form=None):
122 """
123 View that displays the plugin settings as a form.
124 """
125 defaults = defaults or {}
126 errors = errors or {}
140 # returns a clone, important if mutating the schema later
141 return schema.bind(
142 permissions=self.request.user.permissions,
143 no_scope=not self.admin_view)
144
145
146 def _form_defaults(self):
147 defaults = {}
127 148
128 149 if self.integration:
129 defaults = self.integration.settings or {}
130 defaults['name'] = self.integration.name
131 defaults['enabled'] = self.integration.enabled
150 defaults['settings'] = self.integration.settings or {}
151 defaults['options'] = {
152 'name': self.integration.name,
153 'enabled': self.integration.enabled,
154 'scope': self.integration.scope,
155 }
132 156 else:
133 157 if self.repo:
134 158 scope = _('{repo_name} repository').format(
135 159 repo_name=self.repo.repo_name)
136 160 elif self.repo_group:
137 161 scope = _('{repo_group_name} repo group').format(
138 162 repo_group_name=self.repo_group.group_name)
139 163 else:
140 164 scope = _('Global')
141 165
142 defaults['name'] = '{} {} integration'.format(scope,
143 self.IntegrationType.display_name)
144 defaults['enabled'] = True
166 defaults['options'] = {
167 'enabled': True,
168 'name': _('{name} integration').format(
169 name=self.IntegrationType.display_name),
170 }
171 if self.repo:
172 defaults['options']['scope'] = self.repo
173 elif self.repo_group:
174 defaults['options']['scope'] = self.repo_group
175
176 return defaults
145 177
146 schema = self._form_schema().bind(request=self.request)
178 def _delete_integration(self, integration):
179 Session().delete(self.integration)
180 Session().commit()
181 self.request.session.flash(
182 _('Integration {integration_name} deleted successfully.').format(
183 integration_name=self.integration.name),
184 queue='success')
185
186 if self.repo:
187 redirect_to = self.request.route_url(
188 'repo_integrations_home', repo_name=self.repo.repo_name)
189 elif self.repo_group:
190 redirect_to = self.request.route_url(
191 'repo_group_integrations_home',
192 repo_group_name=self.repo_group.group_name)
193 else:
194 redirect_to = self.request.route_url('global_integrations_home')
195 raise HTTPFound(redirect_to)
196
197 def settings_get(self, defaults=None, form=None):
198 """
199 View that displays the integration settings as a form.
200 """
201
202 defaults = defaults or self._form_defaults()
203 schema = self._form_schema()
147 204
148 205 if self.integration:
149 206 buttons = ('submit', 'delete')
150 207 else:
151 208 buttons = ('submit',)
152 209
153 210 form = form or deform.Form(schema, appstruct=defaults, buttons=buttons)
154 211
155 for node in schema:
156 setting = self.settings.get(node.name)
157 if setting is not None:
158 defaults.setdefault(node.name, setting)
159 else:
160 if node.default:
161 defaults.setdefault(node.name, node.default)
162
163 212 template_context = {
164 213 'form': form,
165 'defaults': defaults,
166 'errors': errors,
167 'schema': schema,
168 214 'current_IntegrationType': self.IntegrationType,
169 215 'integration': self.integration,
170 'settings': self.settings,
171 'resource': self.context,
172 216 'c': self._template_c_context(),
173 217 }
174 218
175 219 return template_context
176 220
177 221 @auth.CSRFRequired()
178 222 def settings_post(self):
179 223 """
180 View that validates and stores the plugin settings.
224 View that validates and stores the integration settings.
181 225 """
182 if self.request.params.get('delete'):
183 Session().delete(self.integration)
184 Session().commit()
185 self.request.session.flash(
186 _('Integration {integration_name} deleted successfully.').format(
187 integration_name=self.integration.name),
188 queue='success')
189 if self.repo:
190 redirect_to = self.request.route_url(
191 'repo_integrations_home', repo_name=self.repo.repo_name)
192 else:
193 redirect_to = self.request.route_url('global_integrations_home')
194 raise HTTPFound(redirect_to)
226 controls = self.request.POST.items()
227 pstruct = peppercorn.parse(controls)
228
229 if self.integration and pstruct.get('delete'):
230 return self._delete_integration(self.integration)
231
232 schema = self._form_schema()
233
234 skip_settings_validation = False
235 if self.integration and 'enabled' not in pstruct.get('options', {}):
236 skip_settings_validation = True
237 schema['settings'].validator = None
238 for field in schema['settings'].children:
239 field.validator = None
240 field.missing = ''
195 241
196 schema = self._form_schema().bind(request=self.request)
242 if self.integration:
243 buttons = ('submit', 'delete')
244 else:
245 buttons = ('submit',)
197 246
198 form = deform.Form(schema, buttons=('submit', 'delete'))
247 form = deform.Form(schema, buttons=buttons)
199 248
200 params = {}
201 for node in schema.children:
202 if type(node.typ) in (colander.Set, colander.List):
203 val = self.request.params.getall(node.name)
204 else:
205 val = self.request.params.get(node.name)
206 if val:
207 params[node.name] = val
249 if not self.admin_view:
250 # scope is read only field in these cases, and has to be added
251 options = pstruct.setdefault('options', {})
252 if 'scope' not in options:
253 if self.repo:
254 options['scope'] = 'repo:{}'.format(self.repo.repo_name)
255 elif self.repo_group:
256 options['scope'] = 'repogroup:{}'.format(
257 self.repo_group.group_name)
208 258
209 controls = self.request.POST.items()
210 259 try:
211 valid_data = form.validate(controls)
260 valid_data = form.validate_pstruct(pstruct)
212 261 except deform.ValidationFailure as e:
213 262 self.request.session.flash(
214 263 _('Errors exist when saving integration settings. '
215 264 'Please check the form inputs.'),
216 265 queue='error')
217 return self.settings_get(errors={}, defaults=params, form=e)
266 return self.settings_get(form=e)
218 267
219 268 if not self.integration:
220 269 self.integration = Integration()
221 270 self.integration.integration_type = self.IntegrationType.key
222 if self.repo:
223 self.integration.repo = self.repo
224 elif self.repo_group:
225 self.integration.repo_group = self.repo_group
226 271 Session().add(self.integration)
227 272
228 self.integration.enabled = valid_data.pop('enabled', False)
229 self.integration.name = valid_data.pop('name')
230 self.integration.settings = valid_data
273 scope = valid_data['options']['scope']
231 274
275 IntegrationModel().update_integration(self.integration,
276 name=valid_data['options']['name'],
277 enabled=valid_data['options']['enabled'],
278 settings=valid_data['settings'],
279 scope=scope)
280
281 self.integration.settings = valid_data['settings']
232 282 Session().commit()
233
234 283 # Display success message and redirect.
235 284 self.request.session.flash(
236 285 _('Integration {integration_name} updated successfully.').format(
237 286 integration_name=self.IntegrationType.display_name),
238 287 queue='success')
239 288
240 if self.repo:
241 redirect_to = self.request.route_url(
242 'repo_integrations_edit', repo_name=self.repo.repo_name,
289
290 # if integration scope changes, we must redirect to the right place
291 # keeping in mind if the original view was for /repo/ or /_admin/
292 admin_view = not (self.repo or self.repo_group)
293
294 if isinstance(self.integration.scope, Repository) and not admin_view:
295 redirect_to = self.request.route_path(
296 'repo_integrations_edit',
297 repo_name=self.integration.scope.repo_name,
243 298 integration=self.integration.integration_type,
244 299 integration_id=self.integration.integration_id)
245 elif self.repo:
246 redirect_to = self.request.route_url(
300 elif isinstance(self.integration.scope, RepoGroup) and not admin_view:
301 redirect_to = self.request.route_path(
247 302 'repo_group_integrations_edit',
248 repo_group_name=self.repo_group.group_name,
303 repo_group_name=self.integration.scope.group_name,
249 304 integration=self.integration.integration_type,
250 305 integration_id=self.integration.integration_id)
251 306 else:
252 redirect_to = self.request.route_url(
307 redirect_to = self.request.route_path(
253 308 'global_integrations_edit',
254 309 integration=self.integration.integration_type,
255 310 integration_id=self.integration.integration_id)
256 311
257 312 return HTTPFound(redirect_to)
258 313
259 314 def index(self):
260 current_integrations = self.integrations
261 if self.IntegrationType:
262 current_integrations = {
263 self.IntegrationType.key: self.integrations.get(
264 self.IntegrationType.key, [])
265 }
315 """ List integrations """
316 if self.repo:
317 scope = self.repo
318 elif self.repo_group:
319 scope = self.repo_group
320 else:
321 scope = 'all'
322
323 integrations = []
324
325 for integration in IntegrationModel().get_integrations(
326 scope=scope, IntegrationType=self.IntegrationType):
327
328 # extra permissions check *just in case*
329 if not self._has_perms_for_integration(integration):
330 continue
331 integrations.append(integration)
332
333 sort_arg = self.request.GET.get('sort', 'name:asc')
334 if ':' in sort_arg:
335 sort_field, sort_dir = sort_arg.split(':')
336 else:
337 sort_field = sort_arg, 'asc'
338
339 assert sort_field in ('name', 'integration_type', 'enabled', 'scope')
340
341 integrations.sort(
342 key=lambda x: getattr(x[1], sort_field), reverse=(sort_dir=='desc'))
343
344
345 page_url = webhelpers.paginate.PageURL(
346 self.request.path, self.request.GET)
347 page = safe_int(self.request.GET.get('page', 1), 1)
348
349 integrations = Page(integrations, page=page, items_per_page=10,
350 url=page_url)
266 351
267 352 template_context = {
353 'sort_field': sort_field,
354 'rev_sort_dir': sort_dir != 'desc' and 'desc' or 'asc',
268 355 'current_IntegrationType': self.IntegrationType,
269 'current_integrations': current_integrations,
356 'integrations_list': integrations,
270 357 'available_integrations': integration_type_registry,
271 'c': self._template_c_context()
358 'c': self._template_c_context(),
359 'request': self.request,
272 360 }
361 return template_context
273 362
274 if self.repo:
275 html = render('rhodecode:templates/admin/integrations/list.html',
276 template_context,
277 request=self.request)
278 else:
279 html = render('rhodecode:templates/admin/integrations/list.html',
280 template_context,
281 request=self.request)
282
283 return Response(html)
284
363 def new_integration(self):
364 template_context = {
365 'available_integrations': integration_type_registry,
366 'c': self._template_c_context(),
367 }
368 return template_context
285 369
286 370 class GlobalIntegrationsView(IntegrationSettingsViewBase):
287 371 def perm_check(self, user):
288 372 return auth.HasPermissionAll('hg.admin').check_permissions(user=user)
289 373
290 374
291 375 class RepoIntegrationsView(IntegrationSettingsViewBase):
292 376 def perm_check(self, user):
293 377 return auth.HasRepoPermissionAll('repository.admin'
294 378 )(repo_name=self.repo.repo_name, user=user)
295 379
380
296 381 class RepoGroupIntegrationsView(IntegrationSettingsViewBase):
297 382 def perm_check(self, user):
298 383 return auth.HasRepoGroupPermissionAll('group.admin'
299 384 )(group_name=self.repo_group.group_name, user=user)
385
@@ -1,3506 +1,3505 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 os
26 26 import sys
27 27 import time
28 28 import hashlib
29 29 import logging
30 30 import datetime
31 31 import warnings
32 32 import ipaddress
33 33 import functools
34 34 import traceback
35 35 import collections
36 36
37 37
38 38 from sqlalchemy import *
39 39 from sqlalchemy.exc import IntegrityError
40 40 from sqlalchemy.ext.declarative import declared_attr
41 41 from sqlalchemy.ext.hybrid import hybrid_property
42 42 from sqlalchemy.orm import (
43 43 relationship, joinedload, class_mapper, validates, aliased)
44 44 from sqlalchemy.sql.expression import true
45 45 from beaker.cache import cache_region, region_invalidate
46 46 from webob.exc import HTTPNotFound
47 47 from zope.cachedescriptors.property import Lazy as LazyProperty
48 48
49 49 from pylons import url
50 50 from pylons.i18n.translation import lazy_ugettext as _
51 51
52 52 from rhodecode.lib.vcs import get_backend, get_vcs_instance
53 53 from rhodecode.lib.vcs.utils.helpers import get_scm
54 54 from rhodecode.lib.vcs.exceptions import VCSError
55 55 from rhodecode.lib.vcs.backends.base import (
56 56 EmptyCommit, Reference, MergeFailureReason)
57 57 from rhodecode.lib.utils2 import (
58 58 str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
59 59 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict)
60 60 from rhodecode.lib.jsonalchemy import MutationObj, JsonType, JSONDict
61 61 from rhodecode.lib.ext_json import json
62 62 from rhodecode.lib.caching_query import FromCache
63 63 from rhodecode.lib.encrypt import AESCipher
64 64
65 65 from rhodecode.model.meta import Base, Session
66 66
67 67 URL_SEP = '/'
68 68 log = logging.getLogger(__name__)
69 69
70 70 # =============================================================================
71 71 # BASE CLASSES
72 72 # =============================================================================
73 73
74 74 # this is propagated from .ini file rhodecode.encrypted_values.secret or
75 75 # beaker.session.secret if first is not set.
76 76 # and initialized at environment.py
77 77 ENCRYPTION_KEY = None
78 78
79 79 # used to sort permissions by types, '#' used here is not allowed to be in
80 80 # usernames, and it's very early in sorted string.printable table.
81 81 PERMISSION_TYPE_SORT = {
82 82 'admin': '####',
83 83 'write': '###',
84 84 'read': '##',
85 85 'none': '#',
86 86 }
87 87
88 88
89 89 def display_sort(obj):
90 90 """
91 91 Sort function used to sort permissions in .permissions() function of
92 92 Repository, RepoGroup, UserGroup. Also it put the default user in front
93 93 of all other resources
94 94 """
95 95
96 96 if obj.username == User.DEFAULT_USER:
97 97 return '#####'
98 98 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
99 99 return prefix + obj.username
100 100
101 101
102 102 def _hash_key(k):
103 103 return md5_safe(k)
104 104
105 105
106 106 class EncryptedTextValue(TypeDecorator):
107 107 """
108 108 Special column for encrypted long text data, use like::
109 109
110 110 value = Column("encrypted_value", EncryptedValue(), nullable=False)
111 111
112 112 This column is intelligent so if value is in unencrypted form it return
113 113 unencrypted form, but on save it always encrypts
114 114 """
115 115 impl = Text
116 116
117 117 def process_bind_param(self, value, dialect):
118 118 if not value:
119 119 return value
120 120 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
121 121 # protect against double encrypting if someone manually starts
122 122 # doing
123 123 raise ValueError('value needs to be in unencrypted format, ie. '
124 124 'not starting with enc$aes')
125 125 return 'enc$aes_hmac$%s' % AESCipher(
126 126 ENCRYPTION_KEY, hmac=True).encrypt(value)
127 127
128 128 def process_result_value(self, value, dialect):
129 129 import rhodecode
130 130
131 131 if not value:
132 132 return value
133 133
134 134 parts = value.split('$', 3)
135 135 if not len(parts) == 3:
136 136 # probably not encrypted values
137 137 return value
138 138 else:
139 139 if parts[0] != 'enc':
140 140 # parts ok but without our header ?
141 141 return value
142 142 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
143 143 'rhodecode.encrypted_values.strict') or True)
144 144 # at that stage we know it's our encryption
145 145 if parts[1] == 'aes':
146 146 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
147 147 elif parts[1] == 'aes_hmac':
148 148 decrypted_data = AESCipher(
149 149 ENCRYPTION_KEY, hmac=True,
150 150 strict_verification=enc_strict_mode).decrypt(parts[2])
151 151 else:
152 152 raise ValueError(
153 153 'Encryption type part is wrong, must be `aes` '
154 154 'or `aes_hmac`, got `%s` instead' % (parts[1]))
155 155 return decrypted_data
156 156
157 157
158 158 class BaseModel(object):
159 159 """
160 160 Base Model for all classes
161 161 """
162 162
163 163 @classmethod
164 164 def _get_keys(cls):
165 165 """return column names for this model """
166 166 return class_mapper(cls).c.keys()
167 167
168 168 def get_dict(self):
169 169 """
170 170 return dict with keys and values corresponding
171 171 to this model data """
172 172
173 173 d = {}
174 174 for k in self._get_keys():
175 175 d[k] = getattr(self, k)
176 176
177 177 # also use __json__() if present to get additional fields
178 178 _json_attr = getattr(self, '__json__', None)
179 179 if _json_attr:
180 180 # update with attributes from __json__
181 181 if callable(_json_attr):
182 182 _json_attr = _json_attr()
183 183 for k, val in _json_attr.iteritems():
184 184 d[k] = val
185 185 return d
186 186
187 187 def get_appstruct(self):
188 188 """return list with keys and values tuples corresponding
189 189 to this model data """
190 190
191 191 l = []
192 192 for k in self._get_keys():
193 193 l.append((k, getattr(self, k),))
194 194 return l
195 195
196 196 def populate_obj(self, populate_dict):
197 197 """populate model with data from given populate_dict"""
198 198
199 199 for k in self._get_keys():
200 200 if k in populate_dict:
201 201 setattr(self, k, populate_dict[k])
202 202
203 203 @classmethod
204 204 def query(cls):
205 205 return Session().query(cls)
206 206
207 207 @classmethod
208 208 def get(cls, id_):
209 209 if id_:
210 210 return cls.query().get(id_)
211 211
212 212 @classmethod
213 213 def get_or_404(cls, id_):
214 214 try:
215 215 id_ = int(id_)
216 216 except (TypeError, ValueError):
217 217 raise HTTPNotFound
218 218
219 219 res = cls.query().get(id_)
220 220 if not res:
221 221 raise HTTPNotFound
222 222 return res
223 223
224 224 @classmethod
225 225 def getAll(cls):
226 226 # deprecated and left for backward compatibility
227 227 return cls.get_all()
228 228
229 229 @classmethod
230 230 def get_all(cls):
231 231 return cls.query().all()
232 232
233 233 @classmethod
234 234 def delete(cls, id_):
235 235 obj = cls.query().get(id_)
236 236 Session().delete(obj)
237 237
238 238 @classmethod
239 239 def identity_cache(cls, session, attr_name, value):
240 240 exist_in_session = []
241 241 for (item_cls, pkey), instance in session.identity_map.items():
242 242 if cls == item_cls and getattr(instance, attr_name) == value:
243 243 exist_in_session.append(instance)
244 244 if exist_in_session:
245 245 if len(exist_in_session) == 1:
246 246 return exist_in_session[0]
247 247 log.exception(
248 248 'multiple objects with attr %s and '
249 249 'value %s found with same name: %r',
250 250 attr_name, value, exist_in_session)
251 251
252 252 def __repr__(self):
253 253 if hasattr(self, '__unicode__'):
254 254 # python repr needs to return str
255 255 try:
256 256 return safe_str(self.__unicode__())
257 257 except UnicodeDecodeError:
258 258 pass
259 259 return '<DB:%s>' % (self.__class__.__name__)
260 260
261 261
262 262 class RhodeCodeSetting(Base, BaseModel):
263 263 __tablename__ = 'rhodecode_settings'
264 264 __table_args__ = (
265 265 UniqueConstraint('app_settings_name'),
266 266 {'extend_existing': True, 'mysql_engine': 'InnoDB',
267 267 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
268 268 )
269 269
270 270 SETTINGS_TYPES = {
271 271 'str': safe_str,
272 272 'int': safe_int,
273 273 'unicode': safe_unicode,
274 274 'bool': str2bool,
275 275 'list': functools.partial(aslist, sep=',')
276 276 }
277 277 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
278 278 GLOBAL_CONF_KEY = 'app_settings'
279 279
280 280 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
281 281 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
282 282 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
283 283 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
284 284
285 285 def __init__(self, key='', val='', type='unicode'):
286 286 self.app_settings_name = key
287 287 self.app_settings_type = type
288 288 self.app_settings_value = val
289 289
290 290 @validates('_app_settings_value')
291 291 def validate_settings_value(self, key, val):
292 292 assert type(val) == unicode
293 293 return val
294 294
295 295 @hybrid_property
296 296 def app_settings_value(self):
297 297 v = self._app_settings_value
298 298 _type = self.app_settings_type
299 299 if _type:
300 300 _type = self.app_settings_type.split('.')[0]
301 301 # decode the encrypted value
302 302 if 'encrypted' in self.app_settings_type:
303 303 cipher = EncryptedTextValue()
304 304 v = safe_unicode(cipher.process_result_value(v, None))
305 305
306 306 converter = self.SETTINGS_TYPES.get(_type) or \
307 307 self.SETTINGS_TYPES['unicode']
308 308 return converter(v)
309 309
310 310 @app_settings_value.setter
311 311 def app_settings_value(self, val):
312 312 """
313 313 Setter that will always make sure we use unicode in app_settings_value
314 314
315 315 :param val:
316 316 """
317 317 val = safe_unicode(val)
318 318 # encode the encrypted value
319 319 if 'encrypted' in self.app_settings_type:
320 320 cipher = EncryptedTextValue()
321 321 val = safe_unicode(cipher.process_bind_param(val, None))
322 322 self._app_settings_value = val
323 323
324 324 @hybrid_property
325 325 def app_settings_type(self):
326 326 return self._app_settings_type
327 327
328 328 @app_settings_type.setter
329 329 def app_settings_type(self, val):
330 330 if val.split('.')[0] not in self.SETTINGS_TYPES:
331 331 raise Exception('type must be one of %s got %s'
332 332 % (self.SETTINGS_TYPES.keys(), val))
333 333 self._app_settings_type = val
334 334
335 335 def __unicode__(self):
336 336 return u"<%s('%s:%s[%s]')>" % (
337 337 self.__class__.__name__,
338 338 self.app_settings_name, self.app_settings_value,
339 339 self.app_settings_type
340 340 )
341 341
342 342
343 343 class RhodeCodeUi(Base, BaseModel):
344 344 __tablename__ = 'rhodecode_ui'
345 345 __table_args__ = (
346 346 UniqueConstraint('ui_key'),
347 347 {'extend_existing': True, 'mysql_engine': 'InnoDB',
348 348 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
349 349 )
350 350
351 351 HOOK_REPO_SIZE = 'changegroup.repo_size'
352 352 # HG
353 353 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
354 354 HOOK_PULL = 'outgoing.pull_logger'
355 355 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
356 356 HOOK_PUSH = 'changegroup.push_logger'
357 357
358 358 # TODO: johbo: Unify way how hooks are configured for git and hg,
359 359 # git part is currently hardcoded.
360 360
361 361 # SVN PATTERNS
362 362 SVN_BRANCH_ID = 'vcs_svn_branch'
363 363 SVN_TAG_ID = 'vcs_svn_tag'
364 364
365 365 ui_id = Column(
366 366 "ui_id", Integer(), nullable=False, unique=True, default=None,
367 367 primary_key=True)
368 368 ui_section = Column(
369 369 "ui_section", String(255), nullable=True, unique=None, default=None)
370 370 ui_key = Column(
371 371 "ui_key", String(255), nullable=True, unique=None, default=None)
372 372 ui_value = Column(
373 373 "ui_value", String(255), nullable=True, unique=None, default=None)
374 374 ui_active = Column(
375 375 "ui_active", Boolean(), nullable=True, unique=None, default=True)
376 376
377 377 def __repr__(self):
378 378 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
379 379 self.ui_key, self.ui_value)
380 380
381 381
382 382 class RepoRhodeCodeSetting(Base, BaseModel):
383 383 __tablename__ = 'repo_rhodecode_settings'
384 384 __table_args__ = (
385 385 UniqueConstraint(
386 386 'app_settings_name', 'repository_id',
387 387 name='uq_repo_rhodecode_setting_name_repo_id'),
388 388 {'extend_existing': True, 'mysql_engine': 'InnoDB',
389 389 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
390 390 )
391 391
392 392 repository_id = Column(
393 393 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
394 394 nullable=False)
395 395 app_settings_id = Column(
396 396 "app_settings_id", Integer(), nullable=False, unique=True,
397 397 default=None, primary_key=True)
398 398 app_settings_name = Column(
399 399 "app_settings_name", String(255), nullable=True, unique=None,
400 400 default=None)
401 401 _app_settings_value = Column(
402 402 "app_settings_value", String(4096), nullable=True, unique=None,
403 403 default=None)
404 404 _app_settings_type = Column(
405 405 "app_settings_type", String(255), nullable=True, unique=None,
406 406 default=None)
407 407
408 408 repository = relationship('Repository')
409 409
410 410 def __init__(self, repository_id, key='', val='', type='unicode'):
411 411 self.repository_id = repository_id
412 412 self.app_settings_name = key
413 413 self.app_settings_type = type
414 414 self.app_settings_value = val
415 415
416 416 @validates('_app_settings_value')
417 417 def validate_settings_value(self, key, val):
418 418 assert type(val) == unicode
419 419 return val
420 420
421 421 @hybrid_property
422 422 def app_settings_value(self):
423 423 v = self._app_settings_value
424 424 type_ = self.app_settings_type
425 425 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
426 426 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
427 427 return converter(v)
428 428
429 429 @app_settings_value.setter
430 430 def app_settings_value(self, val):
431 431 """
432 432 Setter that will always make sure we use unicode in app_settings_value
433 433
434 434 :param val:
435 435 """
436 436 self._app_settings_value = safe_unicode(val)
437 437
438 438 @hybrid_property
439 439 def app_settings_type(self):
440 440 return self._app_settings_type
441 441
442 442 @app_settings_type.setter
443 443 def app_settings_type(self, val):
444 444 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
445 445 if val not in SETTINGS_TYPES:
446 446 raise Exception('type must be one of %s got %s'
447 447 % (SETTINGS_TYPES.keys(), val))
448 448 self._app_settings_type = val
449 449
450 450 def __unicode__(self):
451 451 return u"<%s('%s:%s:%s[%s]')>" % (
452 452 self.__class__.__name__, self.repository.repo_name,
453 453 self.app_settings_name, self.app_settings_value,
454 454 self.app_settings_type
455 455 )
456 456
457 457
458 458 class RepoRhodeCodeUi(Base, BaseModel):
459 459 __tablename__ = 'repo_rhodecode_ui'
460 460 __table_args__ = (
461 461 UniqueConstraint(
462 462 'repository_id', 'ui_section', 'ui_key',
463 463 name='uq_repo_rhodecode_ui_repository_id_section_key'),
464 464 {'extend_existing': True, 'mysql_engine': 'InnoDB',
465 465 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
466 466 )
467 467
468 468 repository_id = Column(
469 469 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
470 470 nullable=False)
471 471 ui_id = Column(
472 472 "ui_id", Integer(), nullable=False, unique=True, default=None,
473 473 primary_key=True)
474 474 ui_section = Column(
475 475 "ui_section", String(255), nullable=True, unique=None, default=None)
476 476 ui_key = Column(
477 477 "ui_key", String(255), nullable=True, unique=None, default=None)
478 478 ui_value = Column(
479 479 "ui_value", String(255), nullable=True, unique=None, default=None)
480 480 ui_active = Column(
481 481 "ui_active", Boolean(), nullable=True, unique=None, default=True)
482 482
483 483 repository = relationship('Repository')
484 484
485 485 def __repr__(self):
486 486 return '<%s[%s:%s]%s=>%s]>' % (
487 487 self.__class__.__name__, self.repository.repo_name,
488 488 self.ui_section, self.ui_key, self.ui_value)
489 489
490 490
491 491 class User(Base, BaseModel):
492 492 __tablename__ = 'users'
493 493 __table_args__ = (
494 494 UniqueConstraint('username'), UniqueConstraint('email'),
495 495 Index('u_username_idx', 'username'),
496 496 Index('u_email_idx', 'email'),
497 497 {'extend_existing': True, 'mysql_engine': 'InnoDB',
498 498 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
499 499 )
500 500 DEFAULT_USER = 'default'
501 501 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
502 502 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
503 503
504 504 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
505 505 username = Column("username", String(255), nullable=True, unique=None, default=None)
506 506 password = Column("password", String(255), nullable=True, unique=None, default=None)
507 507 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
508 508 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
509 509 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
510 510 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
511 511 _email = Column("email", String(255), nullable=True, unique=None, default=None)
512 512 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
513 513 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
514 514 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
515 515 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
516 516 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
517 517 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
518 518 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
519 519
520 520 user_log = relationship('UserLog')
521 521 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
522 522
523 523 repositories = relationship('Repository')
524 524 repository_groups = relationship('RepoGroup')
525 525 user_groups = relationship('UserGroup')
526 526
527 527 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
528 528 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
529 529
530 530 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
531 531 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
532 532 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
533 533
534 534 group_member = relationship('UserGroupMember', cascade='all')
535 535
536 536 notifications = relationship('UserNotification', cascade='all')
537 537 # notifications assigned to this user
538 538 user_created_notifications = relationship('Notification', cascade='all')
539 539 # comments created by this user
540 540 user_comments = relationship('ChangesetComment', cascade='all')
541 541 # user profile extra info
542 542 user_emails = relationship('UserEmailMap', cascade='all')
543 543 user_ip_map = relationship('UserIpMap', cascade='all')
544 544 user_auth_tokens = relationship('UserApiKeys', cascade='all')
545 545 # gists
546 546 user_gists = relationship('Gist', cascade='all')
547 547 # user pull requests
548 548 user_pull_requests = relationship('PullRequest', cascade='all')
549 549 # external identities
550 550 extenal_identities = relationship(
551 551 'ExternalIdentity',
552 552 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
553 553 cascade='all')
554 554
555 555 def __unicode__(self):
556 556 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
557 557 self.user_id, self.username)
558 558
559 559 @hybrid_property
560 560 def email(self):
561 561 return self._email
562 562
563 563 @email.setter
564 564 def email(self, val):
565 565 self._email = val.lower() if val else None
566 566
567 567 @property
568 568 def firstname(self):
569 569 # alias for future
570 570 return self.name
571 571
572 572 @property
573 573 def emails(self):
574 574 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
575 575 return [self.email] + [x.email for x in other]
576 576
577 577 @property
578 578 def auth_tokens(self):
579 579 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
580 580
581 581 @property
582 582 def extra_auth_tokens(self):
583 583 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
584 584
585 585 @property
586 586 def feed_token(self):
587 587 feed_tokens = UserApiKeys.query()\
588 588 .filter(UserApiKeys.user == self)\
589 589 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
590 590 .all()
591 591 if feed_tokens:
592 592 return feed_tokens[0].api_key
593 593 else:
594 594 # use the main token so we don't end up with nothing...
595 595 return self.api_key
596 596
597 597 @classmethod
598 598 def extra_valid_auth_tokens(cls, user, role=None):
599 599 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
600 600 .filter(or_(UserApiKeys.expires == -1,
601 601 UserApiKeys.expires >= time.time()))
602 602 if role:
603 603 tokens = tokens.filter(or_(UserApiKeys.role == role,
604 604 UserApiKeys.role == UserApiKeys.ROLE_ALL))
605 605 return tokens.all()
606 606
607 607 @property
608 608 def ip_addresses(self):
609 609 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
610 610 return [x.ip_addr for x in ret]
611 611
612 612 @property
613 613 def username_and_name(self):
614 614 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
615 615
616 616 @property
617 617 def username_or_name_or_email(self):
618 618 full_name = self.full_name if self.full_name is not ' ' else None
619 619 return self.username or full_name or self.email
620 620
621 621 @property
622 622 def full_name(self):
623 623 return '%s %s' % (self.firstname, self.lastname)
624 624
625 625 @property
626 626 def full_name_or_username(self):
627 627 return ('%s %s' % (self.firstname, self.lastname)
628 628 if (self.firstname and self.lastname) else self.username)
629 629
630 630 @property
631 631 def full_contact(self):
632 632 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
633 633
634 634 @property
635 635 def short_contact(self):
636 636 return '%s %s' % (self.firstname, self.lastname)
637 637
638 638 @property
639 639 def is_admin(self):
640 640 return self.admin
641 641
642 642 @property
643 643 def AuthUser(self):
644 644 """
645 645 Returns instance of AuthUser for this user
646 646 """
647 647 from rhodecode.lib.auth import AuthUser
648 648 return AuthUser(user_id=self.user_id, api_key=self.api_key,
649 649 username=self.username)
650 650
651 651 @hybrid_property
652 652 def user_data(self):
653 653 if not self._user_data:
654 654 return {}
655 655
656 656 try:
657 657 return json.loads(self._user_data)
658 658 except TypeError:
659 659 return {}
660 660
661 661 @user_data.setter
662 662 def user_data(self, val):
663 663 if not isinstance(val, dict):
664 664 raise Exception('user_data must be dict, got %s' % type(val))
665 665 try:
666 666 self._user_data = json.dumps(val)
667 667 except Exception:
668 668 log.error(traceback.format_exc())
669 669
670 670 @classmethod
671 671 def get_by_username(cls, username, case_insensitive=False,
672 672 cache=False, identity_cache=False):
673 673 session = Session()
674 674
675 675 if case_insensitive:
676 676 q = cls.query().filter(
677 677 func.lower(cls.username) == func.lower(username))
678 678 else:
679 679 q = cls.query().filter(cls.username == username)
680 680
681 681 if cache:
682 682 if identity_cache:
683 683 val = cls.identity_cache(session, 'username', username)
684 684 if val:
685 685 return val
686 686 else:
687 687 q = q.options(
688 688 FromCache("sql_cache_short",
689 689 "get_user_by_name_%s" % _hash_key(username)))
690 690
691 691 return q.scalar()
692 692
693 693 @classmethod
694 694 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
695 695 q = cls.query().filter(cls.api_key == auth_token)
696 696
697 697 if cache:
698 698 q = q.options(FromCache("sql_cache_short",
699 699 "get_auth_token_%s" % auth_token))
700 700 res = q.scalar()
701 701
702 702 if fallback and not res:
703 703 #fallback to additional keys
704 704 _res = UserApiKeys.query()\
705 705 .filter(UserApiKeys.api_key == auth_token)\
706 706 .filter(or_(UserApiKeys.expires == -1,
707 707 UserApiKeys.expires >= time.time()))\
708 708 .first()
709 709 if _res:
710 710 res = _res.user
711 711 return res
712 712
713 713 @classmethod
714 714 def get_by_email(cls, email, case_insensitive=False, cache=False):
715 715
716 716 if case_insensitive:
717 717 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
718 718
719 719 else:
720 720 q = cls.query().filter(cls.email == email)
721 721
722 722 if cache:
723 723 q = q.options(FromCache("sql_cache_short",
724 724 "get_email_key_%s" % _hash_key(email)))
725 725
726 726 ret = q.scalar()
727 727 if ret is None:
728 728 q = UserEmailMap.query()
729 729 # try fetching in alternate email map
730 730 if case_insensitive:
731 731 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
732 732 else:
733 733 q = q.filter(UserEmailMap.email == email)
734 734 q = q.options(joinedload(UserEmailMap.user))
735 735 if cache:
736 736 q = q.options(FromCache("sql_cache_short",
737 737 "get_email_map_key_%s" % email))
738 738 ret = getattr(q.scalar(), 'user', None)
739 739
740 740 return ret
741 741
742 742 @classmethod
743 743 def get_from_cs_author(cls, author):
744 744 """
745 745 Tries to get User objects out of commit author string
746 746
747 747 :param author:
748 748 """
749 749 from rhodecode.lib.helpers import email, author_name
750 750 # Valid email in the attribute passed, see if they're in the system
751 751 _email = email(author)
752 752 if _email:
753 753 user = cls.get_by_email(_email, case_insensitive=True)
754 754 if user:
755 755 return user
756 756 # Maybe we can match by username?
757 757 _author = author_name(author)
758 758 user = cls.get_by_username(_author, case_insensitive=True)
759 759 if user:
760 760 return user
761 761
762 762 def update_userdata(self, **kwargs):
763 763 usr = self
764 764 old = usr.user_data
765 765 old.update(**kwargs)
766 766 usr.user_data = old
767 767 Session().add(usr)
768 768 log.debug('updated userdata with ', kwargs)
769 769
770 770 def update_lastlogin(self):
771 771 """Update user lastlogin"""
772 772 self.last_login = datetime.datetime.now()
773 773 Session().add(self)
774 774 log.debug('updated user %s lastlogin', self.username)
775 775
776 776 def update_lastactivity(self):
777 777 """Update user lastactivity"""
778 778 usr = self
779 779 old = usr.user_data
780 780 old.update({'last_activity': time.time()})
781 781 usr.user_data = old
782 782 Session().add(usr)
783 783 log.debug('updated user %s lastactivity', usr.username)
784 784
785 785 def update_password(self, new_password, change_api_key=False):
786 786 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
787 787
788 788 self.password = get_crypt_password(new_password)
789 789 if change_api_key:
790 790 self.api_key = generate_auth_token(self.username)
791 791 Session().add(self)
792 792
793 793 @classmethod
794 794 def get_first_super_admin(cls):
795 795 user = User.query().filter(User.admin == true()).first()
796 796 if user is None:
797 797 raise Exception('FATAL: Missing administrative account!')
798 798 return user
799 799
800 800 @classmethod
801 801 def get_all_super_admins(cls):
802 802 """
803 803 Returns all admin accounts sorted by username
804 804 """
805 805 return User.query().filter(User.admin == true())\
806 806 .order_by(User.username.asc()).all()
807 807
808 808 @classmethod
809 809 def get_default_user(cls, cache=False):
810 810 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
811 811 if user is None:
812 812 raise Exception('FATAL: Missing default account!')
813 813 return user
814 814
815 815 def _get_default_perms(self, user, suffix=''):
816 816 from rhodecode.model.permission import PermissionModel
817 817 return PermissionModel().get_default_perms(user.user_perms, suffix)
818 818
819 819 def get_default_perms(self, suffix=''):
820 820 return self._get_default_perms(self, suffix)
821 821
822 822 def get_api_data(self, include_secrets=False, details='full'):
823 823 """
824 824 Common function for generating user related data for API
825 825
826 826 :param include_secrets: By default secrets in the API data will be replaced
827 827 by a placeholder value to prevent exposing this data by accident. In case
828 828 this data shall be exposed, set this flag to ``True``.
829 829
830 830 :param details: details can be 'basic|full' basic gives only a subset of
831 831 the available user information that includes user_id, name and emails.
832 832 """
833 833 user = self
834 834 user_data = self.user_data
835 835 data = {
836 836 'user_id': user.user_id,
837 837 'username': user.username,
838 838 'firstname': user.name,
839 839 'lastname': user.lastname,
840 840 'email': user.email,
841 841 'emails': user.emails,
842 842 }
843 843 if details == 'basic':
844 844 return data
845 845
846 846 api_key_length = 40
847 847 api_key_replacement = '*' * api_key_length
848 848
849 849 extras = {
850 850 'api_key': api_key_replacement,
851 851 'api_keys': [api_key_replacement],
852 852 'active': user.active,
853 853 'admin': user.admin,
854 854 'extern_type': user.extern_type,
855 855 'extern_name': user.extern_name,
856 856 'last_login': user.last_login,
857 857 'ip_addresses': user.ip_addresses,
858 858 'language': user_data.get('language')
859 859 }
860 860 data.update(extras)
861 861
862 862 if include_secrets:
863 863 data['api_key'] = user.api_key
864 864 data['api_keys'] = user.auth_tokens
865 865 return data
866 866
867 867 def __json__(self):
868 868 data = {
869 869 'full_name': self.full_name,
870 870 'full_name_or_username': self.full_name_or_username,
871 871 'short_contact': self.short_contact,
872 872 'full_contact': self.full_contact,
873 873 }
874 874 data.update(self.get_api_data())
875 875 return data
876 876
877 877
878 878 class UserApiKeys(Base, BaseModel):
879 879 __tablename__ = 'user_api_keys'
880 880 __table_args__ = (
881 881 Index('uak_api_key_idx', 'api_key'),
882 882 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
883 883 UniqueConstraint('api_key'),
884 884 {'extend_existing': True, 'mysql_engine': 'InnoDB',
885 885 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
886 886 )
887 887 __mapper_args__ = {}
888 888
889 889 # ApiKey role
890 890 ROLE_ALL = 'token_role_all'
891 891 ROLE_HTTP = 'token_role_http'
892 892 ROLE_VCS = 'token_role_vcs'
893 893 ROLE_API = 'token_role_api'
894 894 ROLE_FEED = 'token_role_feed'
895 895 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
896 896
897 897 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
898 898 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
899 899 api_key = Column("api_key", String(255), nullable=False, unique=True)
900 900 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
901 901 expires = Column('expires', Float(53), nullable=False)
902 902 role = Column('role', String(255), nullable=True)
903 903 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
904 904
905 905 user = relationship('User', lazy='joined')
906 906
907 907 @classmethod
908 908 def _get_role_name(cls, role):
909 909 return {
910 910 cls.ROLE_ALL: _('all'),
911 911 cls.ROLE_HTTP: _('http/web interface'),
912 912 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
913 913 cls.ROLE_API: _('api calls'),
914 914 cls.ROLE_FEED: _('feed access'),
915 915 }.get(role, role)
916 916
917 917 @property
918 918 def expired(self):
919 919 if self.expires == -1:
920 920 return False
921 921 return time.time() > self.expires
922 922
923 923 @property
924 924 def role_humanized(self):
925 925 return self._get_role_name(self.role)
926 926
927 927
928 928 class UserEmailMap(Base, BaseModel):
929 929 __tablename__ = 'user_email_map'
930 930 __table_args__ = (
931 931 Index('uem_email_idx', 'email'),
932 932 UniqueConstraint('email'),
933 933 {'extend_existing': True, 'mysql_engine': 'InnoDB',
934 934 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
935 935 )
936 936 __mapper_args__ = {}
937 937
938 938 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
939 939 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
940 940 _email = Column("email", String(255), nullable=True, unique=False, default=None)
941 941 user = relationship('User', lazy='joined')
942 942
943 943 @validates('_email')
944 944 def validate_email(self, key, email):
945 945 # check if this email is not main one
946 946 main_email = Session().query(User).filter(User.email == email).scalar()
947 947 if main_email is not None:
948 948 raise AttributeError('email %s is present is user table' % email)
949 949 return email
950 950
951 951 @hybrid_property
952 952 def email(self):
953 953 return self._email
954 954
955 955 @email.setter
956 956 def email(self, val):
957 957 self._email = val.lower() if val else None
958 958
959 959
960 960 class UserIpMap(Base, BaseModel):
961 961 __tablename__ = 'user_ip_map'
962 962 __table_args__ = (
963 963 UniqueConstraint('user_id', 'ip_addr'),
964 964 {'extend_existing': True, 'mysql_engine': 'InnoDB',
965 965 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
966 966 )
967 967 __mapper_args__ = {}
968 968
969 969 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
970 970 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
971 971 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
972 972 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
973 973 description = Column("description", String(10000), nullable=True, unique=None, default=None)
974 974 user = relationship('User', lazy='joined')
975 975
976 976 @classmethod
977 977 def _get_ip_range(cls, ip_addr):
978 978 net = ipaddress.ip_network(ip_addr, strict=False)
979 979 return [str(net.network_address), str(net.broadcast_address)]
980 980
981 981 def __json__(self):
982 982 return {
983 983 'ip_addr': self.ip_addr,
984 984 'ip_range': self._get_ip_range(self.ip_addr),
985 985 }
986 986
987 987 def __unicode__(self):
988 988 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
989 989 self.user_id, self.ip_addr)
990 990
991 991 class UserLog(Base, BaseModel):
992 992 __tablename__ = 'user_logs'
993 993 __table_args__ = (
994 994 {'extend_existing': True, 'mysql_engine': 'InnoDB',
995 995 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
996 996 )
997 997 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
998 998 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
999 999 username = Column("username", String(255), nullable=True, unique=None, default=None)
1000 1000 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1001 1001 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1002 1002 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1003 1003 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1004 1004 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1005 1005
1006 1006 def __unicode__(self):
1007 1007 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1008 1008 self.repository_name,
1009 1009 self.action)
1010 1010
1011 1011 @property
1012 1012 def action_as_day(self):
1013 1013 return datetime.date(*self.action_date.timetuple()[:3])
1014 1014
1015 1015 user = relationship('User')
1016 1016 repository = relationship('Repository', cascade='')
1017 1017
1018 1018
1019 1019 class UserGroup(Base, BaseModel):
1020 1020 __tablename__ = 'users_groups'
1021 1021 __table_args__ = (
1022 1022 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1023 1023 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1024 1024 )
1025 1025
1026 1026 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1027 1027 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1028 1028 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1029 1029 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1030 1030 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1031 1031 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1032 1032 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1033 1033 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1034 1034
1035 1035 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1036 1036 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1037 1037 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1038 1038 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1039 1039 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1040 1040 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1041 1041
1042 1042 user = relationship('User')
1043 1043
1044 1044 @hybrid_property
1045 1045 def group_data(self):
1046 1046 if not self._group_data:
1047 1047 return {}
1048 1048
1049 1049 try:
1050 1050 return json.loads(self._group_data)
1051 1051 except TypeError:
1052 1052 return {}
1053 1053
1054 1054 @group_data.setter
1055 1055 def group_data(self, val):
1056 1056 try:
1057 1057 self._group_data = json.dumps(val)
1058 1058 except Exception:
1059 1059 log.error(traceback.format_exc())
1060 1060
1061 1061 def __unicode__(self):
1062 1062 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1063 1063 self.users_group_id,
1064 1064 self.users_group_name)
1065 1065
1066 1066 @classmethod
1067 1067 def get_by_group_name(cls, group_name, cache=False,
1068 1068 case_insensitive=False):
1069 1069 if case_insensitive:
1070 1070 q = cls.query().filter(func.lower(cls.users_group_name) ==
1071 1071 func.lower(group_name))
1072 1072
1073 1073 else:
1074 1074 q = cls.query().filter(cls.users_group_name == group_name)
1075 1075 if cache:
1076 1076 q = q.options(FromCache(
1077 1077 "sql_cache_short",
1078 1078 "get_group_%s" % _hash_key(group_name)))
1079 1079 return q.scalar()
1080 1080
1081 1081 @classmethod
1082 1082 def get(cls, user_group_id, cache=False):
1083 1083 user_group = cls.query()
1084 1084 if cache:
1085 1085 user_group = user_group.options(FromCache("sql_cache_short",
1086 1086 "get_users_group_%s" % user_group_id))
1087 1087 return user_group.get(user_group_id)
1088 1088
1089 1089 def permissions(self, with_admins=True, with_owner=True):
1090 1090 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1091 1091 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1092 1092 joinedload(UserUserGroupToPerm.user),
1093 1093 joinedload(UserUserGroupToPerm.permission),)
1094 1094
1095 1095 # get owners and admins and permissions. We do a trick of re-writing
1096 1096 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1097 1097 # has a global reference and changing one object propagates to all
1098 1098 # others. This means if admin is also an owner admin_row that change
1099 1099 # would propagate to both objects
1100 1100 perm_rows = []
1101 1101 for _usr in q.all():
1102 1102 usr = AttributeDict(_usr.user.get_dict())
1103 1103 usr.permission = _usr.permission.permission_name
1104 1104 perm_rows.append(usr)
1105 1105
1106 1106 # filter the perm rows by 'default' first and then sort them by
1107 1107 # admin,write,read,none permissions sorted again alphabetically in
1108 1108 # each group
1109 1109 perm_rows = sorted(perm_rows, key=display_sort)
1110 1110
1111 1111 _admin_perm = 'usergroup.admin'
1112 1112 owner_row = []
1113 1113 if with_owner:
1114 1114 usr = AttributeDict(self.user.get_dict())
1115 1115 usr.owner_row = True
1116 1116 usr.permission = _admin_perm
1117 1117 owner_row.append(usr)
1118 1118
1119 1119 super_admin_rows = []
1120 1120 if with_admins:
1121 1121 for usr in User.get_all_super_admins():
1122 1122 # if this admin is also owner, don't double the record
1123 1123 if usr.user_id == owner_row[0].user_id:
1124 1124 owner_row[0].admin_row = True
1125 1125 else:
1126 1126 usr = AttributeDict(usr.get_dict())
1127 1127 usr.admin_row = True
1128 1128 usr.permission = _admin_perm
1129 1129 super_admin_rows.append(usr)
1130 1130
1131 1131 return super_admin_rows + owner_row + perm_rows
1132 1132
1133 1133 def permission_user_groups(self):
1134 1134 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1135 1135 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1136 1136 joinedload(UserGroupUserGroupToPerm.target_user_group),
1137 1137 joinedload(UserGroupUserGroupToPerm.permission),)
1138 1138
1139 1139 perm_rows = []
1140 1140 for _user_group in q.all():
1141 1141 usr = AttributeDict(_user_group.user_group.get_dict())
1142 1142 usr.permission = _user_group.permission.permission_name
1143 1143 perm_rows.append(usr)
1144 1144
1145 1145 return perm_rows
1146 1146
1147 1147 def _get_default_perms(self, user_group, suffix=''):
1148 1148 from rhodecode.model.permission import PermissionModel
1149 1149 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1150 1150
1151 1151 def get_default_perms(self, suffix=''):
1152 1152 return self._get_default_perms(self, suffix)
1153 1153
1154 1154 def get_api_data(self, with_group_members=True, include_secrets=False):
1155 1155 """
1156 1156 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1157 1157 basically forwarded.
1158 1158
1159 1159 """
1160 1160 user_group = self
1161 1161
1162 1162 data = {
1163 1163 'users_group_id': user_group.users_group_id,
1164 1164 'group_name': user_group.users_group_name,
1165 1165 'group_description': user_group.user_group_description,
1166 1166 'active': user_group.users_group_active,
1167 1167 'owner': user_group.user.username,
1168 1168 }
1169 1169 if with_group_members:
1170 1170 users = []
1171 1171 for user in user_group.members:
1172 1172 user = user.user
1173 1173 users.append(user.get_api_data(include_secrets=include_secrets))
1174 1174 data['users'] = users
1175 1175
1176 1176 return data
1177 1177
1178 1178
1179 1179 class UserGroupMember(Base, BaseModel):
1180 1180 __tablename__ = 'users_groups_members'
1181 1181 __table_args__ = (
1182 1182 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1183 1183 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1184 1184 )
1185 1185
1186 1186 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1187 1187 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1188 1188 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1189 1189
1190 1190 user = relationship('User', lazy='joined')
1191 1191 users_group = relationship('UserGroup')
1192 1192
1193 1193 def __init__(self, gr_id='', u_id=''):
1194 1194 self.users_group_id = gr_id
1195 1195 self.user_id = u_id
1196 1196
1197 1197
1198 1198 class RepositoryField(Base, BaseModel):
1199 1199 __tablename__ = 'repositories_fields'
1200 1200 __table_args__ = (
1201 1201 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1202 1202 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1203 1203 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1204 1204 )
1205 1205 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1206 1206
1207 1207 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1208 1208 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1209 1209 field_key = Column("field_key", String(250))
1210 1210 field_label = Column("field_label", String(1024), nullable=False)
1211 1211 field_value = Column("field_value", String(10000), nullable=False)
1212 1212 field_desc = Column("field_desc", String(1024), nullable=False)
1213 1213 field_type = Column("field_type", String(255), nullable=False, unique=None)
1214 1214 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1215 1215
1216 1216 repository = relationship('Repository')
1217 1217
1218 1218 @property
1219 1219 def field_key_prefixed(self):
1220 1220 return 'ex_%s' % self.field_key
1221 1221
1222 1222 @classmethod
1223 1223 def un_prefix_key(cls, key):
1224 1224 if key.startswith(cls.PREFIX):
1225 1225 return key[len(cls.PREFIX):]
1226 1226 return key
1227 1227
1228 1228 @classmethod
1229 1229 def get_by_key_name(cls, key, repo):
1230 1230 row = cls.query()\
1231 1231 .filter(cls.repository == repo)\
1232 1232 .filter(cls.field_key == key).scalar()
1233 1233 return row
1234 1234
1235 1235
1236 1236 class Repository(Base, BaseModel):
1237 1237 __tablename__ = 'repositories'
1238 1238 __table_args__ = (
1239 1239 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1240 1240 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1241 1241 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1242 1242 )
1243 1243 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1244 1244 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1245 1245
1246 1246 STATE_CREATED = 'repo_state_created'
1247 1247 STATE_PENDING = 'repo_state_pending'
1248 1248 STATE_ERROR = 'repo_state_error'
1249 1249
1250 1250 LOCK_AUTOMATIC = 'lock_auto'
1251 1251 LOCK_API = 'lock_api'
1252 1252 LOCK_WEB = 'lock_web'
1253 1253 LOCK_PULL = 'lock_pull'
1254 1254
1255 1255 NAME_SEP = URL_SEP
1256 1256
1257 1257 repo_id = Column(
1258 1258 "repo_id", Integer(), nullable=False, unique=True, default=None,
1259 1259 primary_key=True)
1260 1260 _repo_name = Column(
1261 1261 "repo_name", Text(), nullable=False, default=None)
1262 1262 _repo_name_hash = Column(
1263 1263 "repo_name_hash", String(255), nullable=False, unique=True)
1264 1264 repo_state = Column("repo_state", String(255), nullable=True)
1265 1265
1266 1266 clone_uri = Column(
1267 1267 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1268 1268 default=None)
1269 1269 repo_type = Column(
1270 1270 "repo_type", String(255), nullable=False, unique=False, default=None)
1271 1271 user_id = Column(
1272 1272 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1273 1273 unique=False, default=None)
1274 1274 private = Column(
1275 1275 "private", Boolean(), nullable=True, unique=None, default=None)
1276 1276 enable_statistics = Column(
1277 1277 "statistics", Boolean(), nullable=True, unique=None, default=True)
1278 1278 enable_downloads = Column(
1279 1279 "downloads", Boolean(), nullable=True, unique=None, default=True)
1280 1280 description = Column(
1281 1281 "description", String(10000), nullable=True, unique=None, default=None)
1282 1282 created_on = Column(
1283 1283 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1284 1284 default=datetime.datetime.now)
1285 1285 updated_on = Column(
1286 1286 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1287 1287 default=datetime.datetime.now)
1288 1288 _landing_revision = Column(
1289 1289 "landing_revision", String(255), nullable=False, unique=False,
1290 1290 default=None)
1291 1291 enable_locking = Column(
1292 1292 "enable_locking", Boolean(), nullable=False, unique=None,
1293 1293 default=False)
1294 1294 _locked = Column(
1295 1295 "locked", String(255), nullable=True, unique=False, default=None)
1296 1296 _changeset_cache = Column(
1297 1297 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1298 1298
1299 1299 fork_id = Column(
1300 1300 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1301 1301 nullable=True, unique=False, default=None)
1302 1302 group_id = Column(
1303 1303 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1304 1304 unique=False, default=None)
1305 1305
1306 1306 user = relationship('User', lazy='joined')
1307 1307 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1308 1308 group = relationship('RepoGroup', lazy='joined')
1309 1309 repo_to_perm = relationship(
1310 1310 'UserRepoToPerm', cascade='all',
1311 1311 order_by='UserRepoToPerm.repo_to_perm_id')
1312 1312 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1313 1313 stats = relationship('Statistics', cascade='all', uselist=False)
1314 1314
1315 1315 followers = relationship(
1316 1316 'UserFollowing',
1317 1317 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1318 1318 cascade='all')
1319 1319 extra_fields = relationship(
1320 1320 'RepositoryField', cascade="all, delete, delete-orphan")
1321 1321 logs = relationship('UserLog')
1322 1322 comments = relationship(
1323 1323 'ChangesetComment', cascade="all, delete, delete-orphan")
1324 1324 pull_requests_source = relationship(
1325 1325 'PullRequest',
1326 1326 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1327 1327 cascade="all, delete, delete-orphan")
1328 1328 pull_requests_target = relationship(
1329 1329 'PullRequest',
1330 1330 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1331 1331 cascade="all, delete, delete-orphan")
1332 1332 ui = relationship('RepoRhodeCodeUi', cascade="all")
1333 1333 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1334 1334 integrations = relationship('Integration',
1335 1335 cascade="all, delete, delete-orphan")
1336 1336
1337 1337 def __unicode__(self):
1338 1338 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1339 1339 safe_unicode(self.repo_name))
1340 1340
1341 1341 @hybrid_property
1342 1342 def landing_rev(self):
1343 1343 # always should return [rev_type, rev]
1344 1344 if self._landing_revision:
1345 1345 _rev_info = self._landing_revision.split(':')
1346 1346 if len(_rev_info) < 2:
1347 1347 _rev_info.insert(0, 'rev')
1348 1348 return [_rev_info[0], _rev_info[1]]
1349 1349 return [None, None]
1350 1350
1351 1351 @landing_rev.setter
1352 1352 def landing_rev(self, val):
1353 1353 if ':' not in val:
1354 1354 raise ValueError('value must be delimited with `:` and consist '
1355 1355 'of <rev_type>:<rev>, got %s instead' % val)
1356 1356 self._landing_revision = val
1357 1357
1358 1358 @hybrid_property
1359 1359 def locked(self):
1360 1360 if self._locked:
1361 1361 user_id, timelocked, reason = self._locked.split(':')
1362 1362 lock_values = int(user_id), timelocked, reason
1363 1363 else:
1364 1364 lock_values = [None, None, None]
1365 1365 return lock_values
1366 1366
1367 1367 @locked.setter
1368 1368 def locked(self, val):
1369 1369 if val and isinstance(val, (list, tuple)):
1370 1370 self._locked = ':'.join(map(str, val))
1371 1371 else:
1372 1372 self._locked = None
1373 1373
1374 1374 @hybrid_property
1375 1375 def changeset_cache(self):
1376 1376 from rhodecode.lib.vcs.backends.base import EmptyCommit
1377 1377 dummy = EmptyCommit().__json__()
1378 1378 if not self._changeset_cache:
1379 1379 return dummy
1380 1380 try:
1381 1381 return json.loads(self._changeset_cache)
1382 1382 except TypeError:
1383 1383 return dummy
1384 1384 except Exception:
1385 1385 log.error(traceback.format_exc())
1386 1386 return dummy
1387 1387
1388 1388 @changeset_cache.setter
1389 1389 def changeset_cache(self, val):
1390 1390 try:
1391 1391 self._changeset_cache = json.dumps(val)
1392 1392 except Exception:
1393 1393 log.error(traceback.format_exc())
1394 1394
1395 1395 @hybrid_property
1396 1396 def repo_name(self):
1397 1397 return self._repo_name
1398 1398
1399 1399 @repo_name.setter
1400 1400 def repo_name(self, value):
1401 1401 self._repo_name = value
1402 1402 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1403 1403
1404 1404 @classmethod
1405 1405 def normalize_repo_name(cls, repo_name):
1406 1406 """
1407 1407 Normalizes os specific repo_name to the format internally stored inside
1408 1408 database using URL_SEP
1409 1409
1410 1410 :param cls:
1411 1411 :param repo_name:
1412 1412 """
1413 1413 return cls.NAME_SEP.join(repo_name.split(os.sep))
1414 1414
1415 1415 @classmethod
1416 1416 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1417 1417 session = Session()
1418 1418 q = session.query(cls).filter(cls.repo_name == repo_name)
1419 1419
1420 1420 if cache:
1421 1421 if identity_cache:
1422 1422 val = cls.identity_cache(session, 'repo_name', repo_name)
1423 1423 if val:
1424 1424 return val
1425 1425 else:
1426 1426 q = q.options(
1427 1427 FromCache("sql_cache_short",
1428 1428 "get_repo_by_name_%s" % _hash_key(repo_name)))
1429 1429
1430 1430 return q.scalar()
1431 1431
1432 1432 @classmethod
1433 1433 def get_by_full_path(cls, repo_full_path):
1434 1434 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1435 1435 repo_name = cls.normalize_repo_name(repo_name)
1436 1436 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1437 1437
1438 1438 @classmethod
1439 1439 def get_repo_forks(cls, repo_id):
1440 1440 return cls.query().filter(Repository.fork_id == repo_id)
1441 1441
1442 1442 @classmethod
1443 1443 def base_path(cls):
1444 1444 """
1445 1445 Returns base path when all repos are stored
1446 1446
1447 1447 :param cls:
1448 1448 """
1449 1449 q = Session().query(RhodeCodeUi)\
1450 1450 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1451 1451 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1452 1452 return q.one().ui_value
1453 1453
1454 1454 @classmethod
1455 1455 def is_valid(cls, repo_name):
1456 1456 """
1457 1457 returns True if given repo name is a valid filesystem repository
1458 1458
1459 1459 :param cls:
1460 1460 :param repo_name:
1461 1461 """
1462 1462 from rhodecode.lib.utils import is_valid_repo
1463 1463
1464 1464 return is_valid_repo(repo_name, cls.base_path())
1465 1465
1466 1466 @classmethod
1467 1467 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1468 1468 case_insensitive=True):
1469 1469 q = Repository.query()
1470 1470
1471 1471 if not isinstance(user_id, Optional):
1472 1472 q = q.filter(Repository.user_id == user_id)
1473 1473
1474 1474 if not isinstance(group_id, Optional):
1475 1475 q = q.filter(Repository.group_id == group_id)
1476 1476
1477 1477 if case_insensitive:
1478 1478 q = q.order_by(func.lower(Repository.repo_name))
1479 1479 else:
1480 1480 q = q.order_by(Repository.repo_name)
1481 1481 return q.all()
1482 1482
1483 1483 @property
1484 1484 def forks(self):
1485 1485 """
1486 1486 Return forks of this repo
1487 1487 """
1488 1488 return Repository.get_repo_forks(self.repo_id)
1489 1489
1490 1490 @property
1491 1491 def parent(self):
1492 1492 """
1493 1493 Returns fork parent
1494 1494 """
1495 1495 return self.fork
1496 1496
1497 1497 @property
1498 1498 def just_name(self):
1499 1499 return self.repo_name.split(self.NAME_SEP)[-1]
1500 1500
1501 1501 @property
1502 1502 def groups_with_parents(self):
1503 1503 groups = []
1504 1504 if self.group is None:
1505 1505 return groups
1506 1506
1507 1507 cur_gr = self.group
1508 1508 groups.insert(0, cur_gr)
1509 1509 while 1:
1510 1510 gr = getattr(cur_gr, 'parent_group', None)
1511 1511 cur_gr = cur_gr.parent_group
1512 1512 if gr is None:
1513 1513 break
1514 1514 groups.insert(0, gr)
1515 1515
1516 1516 return groups
1517 1517
1518 1518 @property
1519 1519 def groups_and_repo(self):
1520 1520 return self.groups_with_parents, self
1521 1521
1522 1522 @LazyProperty
1523 1523 def repo_path(self):
1524 1524 """
1525 1525 Returns base full path for that repository means where it actually
1526 1526 exists on a filesystem
1527 1527 """
1528 1528 q = Session().query(RhodeCodeUi).filter(
1529 1529 RhodeCodeUi.ui_key == self.NAME_SEP)
1530 1530 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1531 1531 return q.one().ui_value
1532 1532
1533 1533 @property
1534 1534 def repo_full_path(self):
1535 1535 p = [self.repo_path]
1536 1536 # we need to split the name by / since this is how we store the
1537 1537 # names in the database, but that eventually needs to be converted
1538 1538 # into a valid system path
1539 1539 p += self.repo_name.split(self.NAME_SEP)
1540 1540 return os.path.join(*map(safe_unicode, p))
1541 1541
1542 1542 @property
1543 1543 def cache_keys(self):
1544 1544 """
1545 1545 Returns associated cache keys for that repo
1546 1546 """
1547 1547 return CacheKey.query()\
1548 1548 .filter(CacheKey.cache_args == self.repo_name)\
1549 1549 .order_by(CacheKey.cache_key)\
1550 1550 .all()
1551 1551
1552 1552 def get_new_name(self, repo_name):
1553 1553 """
1554 1554 returns new full repository name based on assigned group and new new
1555 1555
1556 1556 :param group_name:
1557 1557 """
1558 1558 path_prefix = self.group.full_path_splitted if self.group else []
1559 1559 return self.NAME_SEP.join(path_prefix + [repo_name])
1560 1560
1561 1561 @property
1562 1562 def _config(self):
1563 1563 """
1564 1564 Returns db based config object.
1565 1565 """
1566 1566 from rhodecode.lib.utils import make_db_config
1567 1567 return make_db_config(clear_session=False, repo=self)
1568 1568
1569 1569 def permissions(self, with_admins=True, with_owner=True):
1570 1570 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1571 1571 q = q.options(joinedload(UserRepoToPerm.repository),
1572 1572 joinedload(UserRepoToPerm.user),
1573 1573 joinedload(UserRepoToPerm.permission),)
1574 1574
1575 1575 # get owners and admins and permissions. We do a trick of re-writing
1576 1576 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1577 1577 # has a global reference and changing one object propagates to all
1578 1578 # others. This means if admin is also an owner admin_row that change
1579 1579 # would propagate to both objects
1580 1580 perm_rows = []
1581 1581 for _usr in q.all():
1582 1582 usr = AttributeDict(_usr.user.get_dict())
1583 1583 usr.permission = _usr.permission.permission_name
1584 1584 perm_rows.append(usr)
1585 1585
1586 1586 # filter the perm rows by 'default' first and then sort them by
1587 1587 # admin,write,read,none permissions sorted again alphabetically in
1588 1588 # each group
1589 1589 perm_rows = sorted(perm_rows, key=display_sort)
1590 1590
1591 1591 _admin_perm = 'repository.admin'
1592 1592 owner_row = []
1593 1593 if with_owner:
1594 1594 usr = AttributeDict(self.user.get_dict())
1595 1595 usr.owner_row = True
1596 1596 usr.permission = _admin_perm
1597 1597 owner_row.append(usr)
1598 1598
1599 1599 super_admin_rows = []
1600 1600 if with_admins:
1601 1601 for usr in User.get_all_super_admins():
1602 1602 # if this admin is also owner, don't double the record
1603 1603 if usr.user_id == owner_row[0].user_id:
1604 1604 owner_row[0].admin_row = True
1605 1605 else:
1606 1606 usr = AttributeDict(usr.get_dict())
1607 1607 usr.admin_row = True
1608 1608 usr.permission = _admin_perm
1609 1609 super_admin_rows.append(usr)
1610 1610
1611 1611 return super_admin_rows + owner_row + perm_rows
1612 1612
1613 1613 def permission_user_groups(self):
1614 1614 q = UserGroupRepoToPerm.query().filter(
1615 1615 UserGroupRepoToPerm.repository == self)
1616 1616 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1617 1617 joinedload(UserGroupRepoToPerm.users_group),
1618 1618 joinedload(UserGroupRepoToPerm.permission),)
1619 1619
1620 1620 perm_rows = []
1621 1621 for _user_group in q.all():
1622 1622 usr = AttributeDict(_user_group.users_group.get_dict())
1623 1623 usr.permission = _user_group.permission.permission_name
1624 1624 perm_rows.append(usr)
1625 1625
1626 1626 return perm_rows
1627 1627
1628 1628 def get_api_data(self, include_secrets=False):
1629 1629 """
1630 1630 Common function for generating repo api data
1631 1631
1632 1632 :param include_secrets: See :meth:`User.get_api_data`.
1633 1633
1634 1634 """
1635 1635 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1636 1636 # move this methods on models level.
1637 1637 from rhodecode.model.settings import SettingsModel
1638 1638
1639 1639 repo = self
1640 1640 _user_id, _time, _reason = self.locked
1641 1641
1642 1642 data = {
1643 1643 'repo_id': repo.repo_id,
1644 1644 'repo_name': repo.repo_name,
1645 1645 'repo_type': repo.repo_type,
1646 1646 'clone_uri': repo.clone_uri or '',
1647 1647 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1648 1648 'private': repo.private,
1649 1649 'created_on': repo.created_on,
1650 1650 'description': repo.description,
1651 1651 'landing_rev': repo.landing_rev,
1652 1652 'owner': repo.user.username,
1653 1653 'fork_of': repo.fork.repo_name if repo.fork else None,
1654 1654 'enable_statistics': repo.enable_statistics,
1655 1655 'enable_locking': repo.enable_locking,
1656 1656 'enable_downloads': repo.enable_downloads,
1657 1657 'last_changeset': repo.changeset_cache,
1658 1658 'locked_by': User.get(_user_id).get_api_data(
1659 1659 include_secrets=include_secrets) if _user_id else None,
1660 1660 'locked_date': time_to_datetime(_time) if _time else None,
1661 1661 'lock_reason': _reason if _reason else None,
1662 1662 }
1663 1663
1664 1664 # TODO: mikhail: should be per-repo settings here
1665 1665 rc_config = SettingsModel().get_all_settings()
1666 1666 repository_fields = str2bool(
1667 1667 rc_config.get('rhodecode_repository_fields'))
1668 1668 if repository_fields:
1669 1669 for f in self.extra_fields:
1670 1670 data[f.field_key_prefixed] = f.field_value
1671 1671
1672 1672 return data
1673 1673
1674 1674 @classmethod
1675 1675 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1676 1676 if not lock_time:
1677 1677 lock_time = time.time()
1678 1678 if not lock_reason:
1679 1679 lock_reason = cls.LOCK_AUTOMATIC
1680 1680 repo.locked = [user_id, lock_time, lock_reason]
1681 1681 Session().add(repo)
1682 1682 Session().commit()
1683 1683
1684 1684 @classmethod
1685 1685 def unlock(cls, repo):
1686 1686 repo.locked = None
1687 1687 Session().add(repo)
1688 1688 Session().commit()
1689 1689
1690 1690 @classmethod
1691 1691 def getlock(cls, repo):
1692 1692 return repo.locked
1693 1693
1694 1694 def is_user_lock(self, user_id):
1695 1695 if self.lock[0]:
1696 1696 lock_user_id = safe_int(self.lock[0])
1697 1697 user_id = safe_int(user_id)
1698 1698 # both are ints, and they are equal
1699 1699 return all([lock_user_id, user_id]) and lock_user_id == user_id
1700 1700
1701 1701 return False
1702 1702
1703 1703 def get_locking_state(self, action, user_id, only_when_enabled=True):
1704 1704 """
1705 1705 Checks locking on this repository, if locking is enabled and lock is
1706 1706 present returns a tuple of make_lock, locked, locked_by.
1707 1707 make_lock can have 3 states None (do nothing) True, make lock
1708 1708 False release lock, This value is later propagated to hooks, which
1709 1709 do the locking. Think about this as signals passed to hooks what to do.
1710 1710
1711 1711 """
1712 1712 # TODO: johbo: This is part of the business logic and should be moved
1713 1713 # into the RepositoryModel.
1714 1714
1715 1715 if action not in ('push', 'pull'):
1716 1716 raise ValueError("Invalid action value: %s" % repr(action))
1717 1717
1718 1718 # defines if locked error should be thrown to user
1719 1719 currently_locked = False
1720 1720 # defines if new lock should be made, tri-state
1721 1721 make_lock = None
1722 1722 repo = self
1723 1723 user = User.get(user_id)
1724 1724
1725 1725 lock_info = repo.locked
1726 1726
1727 1727 if repo and (repo.enable_locking or not only_when_enabled):
1728 1728 if action == 'push':
1729 1729 # check if it's already locked !, if it is compare users
1730 1730 locked_by_user_id = lock_info[0]
1731 1731 if user.user_id == locked_by_user_id:
1732 1732 log.debug(
1733 1733 'Got `push` action from user %s, now unlocking', user)
1734 1734 # unlock if we have push from user who locked
1735 1735 make_lock = False
1736 1736 else:
1737 1737 # we're not the same user who locked, ban with
1738 1738 # code defined in settings (default is 423 HTTP Locked) !
1739 1739 log.debug('Repo %s is currently locked by %s', repo, user)
1740 1740 currently_locked = True
1741 1741 elif action == 'pull':
1742 1742 # [0] user [1] date
1743 1743 if lock_info[0] and lock_info[1]:
1744 1744 log.debug('Repo %s is currently locked by %s', repo, user)
1745 1745 currently_locked = True
1746 1746 else:
1747 1747 log.debug('Setting lock on repo %s by %s', repo, user)
1748 1748 make_lock = True
1749 1749
1750 1750 else:
1751 1751 log.debug('Repository %s do not have locking enabled', repo)
1752 1752
1753 1753 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1754 1754 make_lock, currently_locked, lock_info)
1755 1755
1756 1756 from rhodecode.lib.auth import HasRepoPermissionAny
1757 1757 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1758 1758 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1759 1759 # if we don't have at least write permission we cannot make a lock
1760 1760 log.debug('lock state reset back to FALSE due to lack '
1761 1761 'of at least read permission')
1762 1762 make_lock = False
1763 1763
1764 1764 return make_lock, currently_locked, lock_info
1765 1765
1766 1766 @property
1767 1767 def last_db_change(self):
1768 1768 return self.updated_on
1769 1769
1770 1770 @property
1771 1771 def clone_uri_hidden(self):
1772 1772 clone_uri = self.clone_uri
1773 1773 if clone_uri:
1774 1774 import urlobject
1775 1775 url_obj = urlobject.URLObject(clone_uri)
1776 1776 if url_obj.password:
1777 1777 clone_uri = url_obj.with_password('*****')
1778 1778 return clone_uri
1779 1779
1780 1780 def clone_url(self, **override):
1781 1781 qualified_home_url = url('home', qualified=True)
1782 1782
1783 1783 uri_tmpl = None
1784 1784 if 'with_id' in override:
1785 1785 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1786 1786 del override['with_id']
1787 1787
1788 1788 if 'uri_tmpl' in override:
1789 1789 uri_tmpl = override['uri_tmpl']
1790 1790 del override['uri_tmpl']
1791 1791
1792 1792 # we didn't override our tmpl from **overrides
1793 1793 if not uri_tmpl:
1794 1794 uri_tmpl = self.DEFAULT_CLONE_URI
1795 1795 try:
1796 1796 from pylons import tmpl_context as c
1797 1797 uri_tmpl = c.clone_uri_tmpl
1798 1798 except Exception:
1799 1799 # in any case if we call this outside of request context,
1800 1800 # ie, not having tmpl_context set up
1801 1801 pass
1802 1802
1803 1803 return get_clone_url(uri_tmpl=uri_tmpl,
1804 1804 qualifed_home_url=qualified_home_url,
1805 1805 repo_name=self.repo_name,
1806 1806 repo_id=self.repo_id, **override)
1807 1807
1808 1808 def set_state(self, state):
1809 1809 self.repo_state = state
1810 1810 Session().add(self)
1811 1811 #==========================================================================
1812 1812 # SCM PROPERTIES
1813 1813 #==========================================================================
1814 1814
1815 1815 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1816 1816 return get_commit_safe(
1817 1817 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1818 1818
1819 1819 def get_changeset(self, rev=None, pre_load=None):
1820 1820 warnings.warn("Use get_commit", DeprecationWarning)
1821 1821 commit_id = None
1822 1822 commit_idx = None
1823 1823 if isinstance(rev, basestring):
1824 1824 commit_id = rev
1825 1825 else:
1826 1826 commit_idx = rev
1827 1827 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1828 1828 pre_load=pre_load)
1829 1829
1830 1830 def get_landing_commit(self):
1831 1831 """
1832 1832 Returns landing commit, or if that doesn't exist returns the tip
1833 1833 """
1834 1834 _rev_type, _rev = self.landing_rev
1835 1835 commit = self.get_commit(_rev)
1836 1836 if isinstance(commit, EmptyCommit):
1837 1837 return self.get_commit()
1838 1838 return commit
1839 1839
1840 1840 def update_commit_cache(self, cs_cache=None, config=None):
1841 1841 """
1842 1842 Update cache of last changeset for repository, keys should be::
1843 1843
1844 1844 short_id
1845 1845 raw_id
1846 1846 revision
1847 1847 parents
1848 1848 message
1849 1849 date
1850 1850 author
1851 1851
1852 1852 :param cs_cache:
1853 1853 """
1854 1854 from rhodecode.lib.vcs.backends.base import BaseChangeset
1855 1855 if cs_cache is None:
1856 1856 # use no-cache version here
1857 1857 scm_repo = self.scm_instance(cache=False, config=config)
1858 1858 if scm_repo:
1859 1859 cs_cache = scm_repo.get_commit(
1860 1860 pre_load=["author", "date", "message", "parents"])
1861 1861 else:
1862 1862 cs_cache = EmptyCommit()
1863 1863
1864 1864 if isinstance(cs_cache, BaseChangeset):
1865 1865 cs_cache = cs_cache.__json__()
1866 1866
1867 1867 def is_outdated(new_cs_cache):
1868 1868 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1869 1869 new_cs_cache['revision'] != self.changeset_cache['revision']):
1870 1870 return True
1871 1871 return False
1872 1872
1873 1873 # check if we have maybe already latest cached revision
1874 1874 if is_outdated(cs_cache) or not self.changeset_cache:
1875 1875 _default = datetime.datetime.fromtimestamp(0)
1876 1876 last_change = cs_cache.get('date') or _default
1877 1877 log.debug('updated repo %s with new cs cache %s',
1878 1878 self.repo_name, cs_cache)
1879 1879 self.updated_on = last_change
1880 1880 self.changeset_cache = cs_cache
1881 1881 Session().add(self)
1882 1882 Session().commit()
1883 1883 else:
1884 1884 log.debug('Skipping update_commit_cache for repo:`%s` '
1885 1885 'commit already with latest changes', self.repo_name)
1886 1886
1887 1887 @property
1888 1888 def tip(self):
1889 1889 return self.get_commit('tip')
1890 1890
1891 1891 @property
1892 1892 def author(self):
1893 1893 return self.tip.author
1894 1894
1895 1895 @property
1896 1896 def last_change(self):
1897 1897 return self.scm_instance().last_change
1898 1898
1899 1899 def get_comments(self, revisions=None):
1900 1900 """
1901 1901 Returns comments for this repository grouped by revisions
1902 1902
1903 1903 :param revisions: filter query by revisions only
1904 1904 """
1905 1905 cmts = ChangesetComment.query()\
1906 1906 .filter(ChangesetComment.repo == self)
1907 1907 if revisions:
1908 1908 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1909 1909 grouped = collections.defaultdict(list)
1910 1910 for cmt in cmts.all():
1911 1911 grouped[cmt.revision].append(cmt)
1912 1912 return grouped
1913 1913
1914 1914 def statuses(self, revisions=None):
1915 1915 """
1916 1916 Returns statuses for this repository
1917 1917
1918 1918 :param revisions: list of revisions to get statuses for
1919 1919 """
1920 1920 statuses = ChangesetStatus.query()\
1921 1921 .filter(ChangesetStatus.repo == self)\
1922 1922 .filter(ChangesetStatus.version == 0)
1923 1923
1924 1924 if revisions:
1925 1925 # Try doing the filtering in chunks to avoid hitting limits
1926 1926 size = 500
1927 1927 status_results = []
1928 1928 for chunk in xrange(0, len(revisions), size):
1929 1929 status_results += statuses.filter(
1930 1930 ChangesetStatus.revision.in_(
1931 1931 revisions[chunk: chunk+size])
1932 1932 ).all()
1933 1933 else:
1934 1934 status_results = statuses.all()
1935 1935
1936 1936 grouped = {}
1937 1937
1938 1938 # maybe we have open new pullrequest without a status?
1939 1939 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1940 1940 status_lbl = ChangesetStatus.get_status_lbl(stat)
1941 1941 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1942 1942 for rev in pr.revisions:
1943 1943 pr_id = pr.pull_request_id
1944 1944 pr_repo = pr.target_repo.repo_name
1945 1945 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1946 1946
1947 1947 for stat in status_results:
1948 1948 pr_id = pr_repo = None
1949 1949 if stat.pull_request:
1950 1950 pr_id = stat.pull_request.pull_request_id
1951 1951 pr_repo = stat.pull_request.target_repo.repo_name
1952 1952 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1953 1953 pr_id, pr_repo]
1954 1954 return grouped
1955 1955
1956 1956 # ==========================================================================
1957 1957 # SCM CACHE INSTANCE
1958 1958 # ==========================================================================
1959 1959
1960 1960 def scm_instance(self, **kwargs):
1961 1961 import rhodecode
1962 1962
1963 1963 # Passing a config will not hit the cache currently only used
1964 1964 # for repo2dbmapper
1965 1965 config = kwargs.pop('config', None)
1966 1966 cache = kwargs.pop('cache', None)
1967 1967 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1968 1968 # if cache is NOT defined use default global, else we have a full
1969 1969 # control over cache behaviour
1970 1970 if cache is None and full_cache and not config:
1971 1971 return self._get_instance_cached()
1972 1972 return self._get_instance(cache=bool(cache), config=config)
1973 1973
1974 1974 def _get_instance_cached(self):
1975 1975 @cache_region('long_term')
1976 1976 def _get_repo(cache_key):
1977 1977 return self._get_instance()
1978 1978
1979 1979 invalidator_context = CacheKey.repo_context_cache(
1980 1980 _get_repo, self.repo_name, None, thread_scoped=True)
1981 1981
1982 1982 with invalidator_context as context:
1983 1983 context.invalidate()
1984 1984 repo = context.compute()
1985 1985
1986 1986 return repo
1987 1987
1988 1988 def _get_instance(self, cache=True, config=None):
1989 1989 config = config or self._config
1990 1990 custom_wire = {
1991 1991 'cache': cache # controls the vcs.remote cache
1992 1992 }
1993 1993
1994 1994 repo = get_vcs_instance(
1995 1995 repo_path=safe_str(self.repo_full_path),
1996 1996 config=config,
1997 1997 with_wire=custom_wire,
1998 1998 create=False)
1999 1999
2000 2000 return repo
2001 2001
2002 2002 def __json__(self):
2003 2003 return {'landing_rev': self.landing_rev}
2004 2004
2005 2005 def get_dict(self):
2006 2006
2007 2007 # Since we transformed `repo_name` to a hybrid property, we need to
2008 2008 # keep compatibility with the code which uses `repo_name` field.
2009 2009
2010 2010 result = super(Repository, self).get_dict()
2011 2011 result['repo_name'] = result.pop('_repo_name', None)
2012 2012 return result
2013 2013
2014 2014
2015 2015 class RepoGroup(Base, BaseModel):
2016 2016 __tablename__ = 'groups'
2017 2017 __table_args__ = (
2018 2018 UniqueConstraint('group_name', 'group_parent_id'),
2019 2019 CheckConstraint('group_id != group_parent_id'),
2020 2020 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2021 2021 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2022 2022 )
2023 2023 __mapper_args__ = {'order_by': 'group_name'}
2024 2024
2025 2025 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2026 2026
2027 2027 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2028 2028 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2029 2029 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2030 2030 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2031 2031 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2032 2032 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2033 2033 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2034 2034
2035 2035 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2036 2036 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2037 2037 parent_group = relationship('RepoGroup', remote_side=group_id)
2038 2038 user = relationship('User')
2039 2039
2040 2040 def __init__(self, group_name='', parent_group=None):
2041 2041 self.group_name = group_name
2042 2042 self.parent_group = parent_group
2043 2043
2044 2044 def __unicode__(self):
2045 2045 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2046 2046 self.group_name)
2047 2047
2048 2048 @classmethod
2049 2049 def _generate_choice(cls, repo_group):
2050 2050 from webhelpers.html import literal as _literal
2051 2051 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2052 2052 return repo_group.group_id, _name(repo_group.full_path_splitted)
2053 2053
2054 2054 @classmethod
2055 2055 def groups_choices(cls, groups=None, show_empty_group=True):
2056 2056 if not groups:
2057 2057 groups = cls.query().all()
2058 2058
2059 2059 repo_groups = []
2060 2060 if show_empty_group:
2061 2061 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2062 2062
2063 2063 repo_groups.extend([cls._generate_choice(x) for x in groups])
2064 2064
2065 2065 repo_groups = sorted(
2066 2066 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2067 2067 return repo_groups
2068 2068
2069 2069 @classmethod
2070 2070 def url_sep(cls):
2071 2071 return URL_SEP
2072 2072
2073 2073 @classmethod
2074 2074 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2075 2075 if case_insensitive:
2076 2076 gr = cls.query().filter(func.lower(cls.group_name)
2077 2077 == func.lower(group_name))
2078 2078 else:
2079 2079 gr = cls.query().filter(cls.group_name == group_name)
2080 2080 if cache:
2081 2081 gr = gr.options(FromCache(
2082 2082 "sql_cache_short",
2083 2083 "get_group_%s" % _hash_key(group_name)))
2084 2084 return gr.scalar()
2085 2085
2086 2086 @classmethod
2087 2087 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2088 2088 case_insensitive=True):
2089 2089 q = RepoGroup.query()
2090 2090
2091 2091 if not isinstance(user_id, Optional):
2092 2092 q = q.filter(RepoGroup.user_id == user_id)
2093 2093
2094 2094 if not isinstance(group_id, Optional):
2095 2095 q = q.filter(RepoGroup.group_parent_id == group_id)
2096 2096
2097 2097 if case_insensitive:
2098 2098 q = q.order_by(func.lower(RepoGroup.group_name))
2099 2099 else:
2100 2100 q = q.order_by(RepoGroup.group_name)
2101 2101 return q.all()
2102 2102
2103 2103 @property
2104 2104 def parents(self):
2105 2105 parents_recursion_limit = 10
2106 2106 groups = []
2107 2107 if self.parent_group is None:
2108 2108 return groups
2109 2109 cur_gr = self.parent_group
2110 2110 groups.insert(0, cur_gr)
2111 2111 cnt = 0
2112 2112 while 1:
2113 2113 cnt += 1
2114 2114 gr = getattr(cur_gr, 'parent_group', None)
2115 2115 cur_gr = cur_gr.parent_group
2116 2116 if gr is None:
2117 2117 break
2118 2118 if cnt == parents_recursion_limit:
2119 2119 # this will prevent accidental infinit loops
2120 2120 log.error(('more than %s parents found for group %s, stopping '
2121 2121 'recursive parent fetching' % (parents_recursion_limit, self)))
2122 2122 break
2123 2123
2124 2124 groups.insert(0, gr)
2125 2125 return groups
2126 2126
2127 2127 @property
2128 2128 def children(self):
2129 2129 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2130 2130
2131 2131 @property
2132 2132 def name(self):
2133 2133 return self.group_name.split(RepoGroup.url_sep())[-1]
2134 2134
2135 2135 @property
2136 2136 def full_path(self):
2137 2137 return self.group_name
2138 2138
2139 2139 @property
2140 2140 def full_path_splitted(self):
2141 2141 return self.group_name.split(RepoGroup.url_sep())
2142 2142
2143 2143 @property
2144 2144 def repositories(self):
2145 2145 return Repository.query()\
2146 2146 .filter(Repository.group == self)\
2147 2147 .order_by(Repository.repo_name)
2148 2148
2149 2149 @property
2150 2150 def repositories_recursive_count(self):
2151 2151 cnt = self.repositories.count()
2152 2152
2153 2153 def children_count(group):
2154 2154 cnt = 0
2155 2155 for child in group.children:
2156 2156 cnt += child.repositories.count()
2157 2157 cnt += children_count(child)
2158 2158 return cnt
2159 2159
2160 2160 return cnt + children_count(self)
2161 2161
2162 2162 def _recursive_objects(self, include_repos=True):
2163 2163 all_ = []
2164 2164
2165 2165 def _get_members(root_gr):
2166 2166 if include_repos:
2167 2167 for r in root_gr.repositories:
2168 2168 all_.append(r)
2169 2169 childs = root_gr.children.all()
2170 2170 if childs:
2171 2171 for gr in childs:
2172 2172 all_.append(gr)
2173 2173 _get_members(gr)
2174 2174
2175 2175 _get_members(self)
2176 2176 return [self] + all_
2177 2177
2178 2178 def recursive_groups_and_repos(self):
2179 2179 """
2180 2180 Recursive return all groups, with repositories in those groups
2181 2181 """
2182 2182 return self._recursive_objects()
2183 2183
2184 2184 def recursive_groups(self):
2185 2185 """
2186 2186 Returns all children groups for this group including children of children
2187 2187 """
2188 2188 return self._recursive_objects(include_repos=False)
2189 2189
2190 2190 def get_new_name(self, group_name):
2191 2191 """
2192 2192 returns new full group name based on parent and new name
2193 2193
2194 2194 :param group_name:
2195 2195 """
2196 2196 path_prefix = (self.parent_group.full_path_splitted if
2197 2197 self.parent_group else [])
2198 2198 return RepoGroup.url_sep().join(path_prefix + [group_name])
2199 2199
2200 2200 def permissions(self, with_admins=True, with_owner=True):
2201 2201 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2202 2202 q = q.options(joinedload(UserRepoGroupToPerm.group),
2203 2203 joinedload(UserRepoGroupToPerm.user),
2204 2204 joinedload(UserRepoGroupToPerm.permission),)
2205 2205
2206 2206 # get owners and admins and permissions. We do a trick of re-writing
2207 2207 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2208 2208 # has a global reference and changing one object propagates to all
2209 2209 # others. This means if admin is also an owner admin_row that change
2210 2210 # would propagate to both objects
2211 2211 perm_rows = []
2212 2212 for _usr in q.all():
2213 2213 usr = AttributeDict(_usr.user.get_dict())
2214 2214 usr.permission = _usr.permission.permission_name
2215 2215 perm_rows.append(usr)
2216 2216
2217 2217 # filter the perm rows by 'default' first and then sort them by
2218 2218 # admin,write,read,none permissions sorted again alphabetically in
2219 2219 # each group
2220 2220 perm_rows = sorted(perm_rows, key=display_sort)
2221 2221
2222 2222 _admin_perm = 'group.admin'
2223 2223 owner_row = []
2224 2224 if with_owner:
2225 2225 usr = AttributeDict(self.user.get_dict())
2226 2226 usr.owner_row = True
2227 2227 usr.permission = _admin_perm
2228 2228 owner_row.append(usr)
2229 2229
2230 2230 super_admin_rows = []
2231 2231 if with_admins:
2232 2232 for usr in User.get_all_super_admins():
2233 2233 # if this admin is also owner, don't double the record
2234 2234 if usr.user_id == owner_row[0].user_id:
2235 2235 owner_row[0].admin_row = True
2236 2236 else:
2237 2237 usr = AttributeDict(usr.get_dict())
2238 2238 usr.admin_row = True
2239 2239 usr.permission = _admin_perm
2240 2240 super_admin_rows.append(usr)
2241 2241
2242 2242 return super_admin_rows + owner_row + perm_rows
2243 2243
2244 2244 def permission_user_groups(self):
2245 2245 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2246 2246 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2247 2247 joinedload(UserGroupRepoGroupToPerm.users_group),
2248 2248 joinedload(UserGroupRepoGroupToPerm.permission),)
2249 2249
2250 2250 perm_rows = []
2251 2251 for _user_group in q.all():
2252 2252 usr = AttributeDict(_user_group.users_group.get_dict())
2253 2253 usr.permission = _user_group.permission.permission_name
2254 2254 perm_rows.append(usr)
2255 2255
2256 2256 return perm_rows
2257 2257
2258 2258 def get_api_data(self):
2259 2259 """
2260 2260 Common function for generating api data
2261 2261
2262 2262 """
2263 2263 group = self
2264 2264 data = {
2265 2265 'group_id': group.group_id,
2266 2266 'group_name': group.group_name,
2267 2267 'group_description': group.group_description,
2268 2268 'parent_group': group.parent_group.group_name if group.parent_group else None,
2269 2269 'repositories': [x.repo_name for x in group.repositories],
2270 2270 'owner': group.user.username,
2271 2271 }
2272 2272 return data
2273 2273
2274 2274
2275 2275 class Permission(Base, BaseModel):
2276 2276 __tablename__ = 'permissions'
2277 2277 __table_args__ = (
2278 2278 Index('p_perm_name_idx', 'permission_name'),
2279 2279 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2280 2280 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2281 2281 )
2282 2282 PERMS = [
2283 2283 ('hg.admin', _('RhodeCode Super Administrator')),
2284 2284
2285 2285 ('repository.none', _('Repository no access')),
2286 2286 ('repository.read', _('Repository read access')),
2287 2287 ('repository.write', _('Repository write access')),
2288 2288 ('repository.admin', _('Repository admin access')),
2289 2289
2290 2290 ('group.none', _('Repository group no access')),
2291 2291 ('group.read', _('Repository group read access')),
2292 2292 ('group.write', _('Repository group write access')),
2293 2293 ('group.admin', _('Repository group admin access')),
2294 2294
2295 2295 ('usergroup.none', _('User group no access')),
2296 2296 ('usergroup.read', _('User group read access')),
2297 2297 ('usergroup.write', _('User group write access')),
2298 2298 ('usergroup.admin', _('User group admin access')),
2299 2299
2300 2300 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2301 2301 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2302 2302
2303 2303 ('hg.usergroup.create.false', _('User Group creation disabled')),
2304 2304 ('hg.usergroup.create.true', _('User Group creation enabled')),
2305 2305
2306 2306 ('hg.create.none', _('Repository creation disabled')),
2307 2307 ('hg.create.repository', _('Repository creation enabled')),
2308 2308 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2309 2309 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2310 2310
2311 2311 ('hg.fork.none', _('Repository forking disabled')),
2312 2312 ('hg.fork.repository', _('Repository forking enabled')),
2313 2313
2314 2314 ('hg.register.none', _('Registration disabled')),
2315 2315 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2316 2316 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2317 2317
2318 2318 ('hg.extern_activate.manual', _('Manual activation of external account')),
2319 2319 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2320 2320
2321 2321 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2322 2322 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2323 2323 ]
2324 2324
2325 2325 # definition of system default permissions for DEFAULT user
2326 2326 DEFAULT_USER_PERMISSIONS = [
2327 2327 'repository.read',
2328 2328 'group.read',
2329 2329 'usergroup.read',
2330 2330 'hg.create.repository',
2331 2331 'hg.repogroup.create.false',
2332 2332 'hg.usergroup.create.false',
2333 2333 'hg.create.write_on_repogroup.true',
2334 2334 'hg.fork.repository',
2335 2335 'hg.register.manual_activate',
2336 2336 'hg.extern_activate.auto',
2337 2337 'hg.inherit_default_perms.true',
2338 2338 ]
2339 2339
2340 2340 # defines which permissions are more important higher the more important
2341 2341 # Weight defines which permissions are more important.
2342 2342 # The higher number the more important.
2343 2343 PERM_WEIGHTS = {
2344 2344 'repository.none': 0,
2345 2345 'repository.read': 1,
2346 2346 'repository.write': 3,
2347 2347 'repository.admin': 4,
2348 2348
2349 2349 'group.none': 0,
2350 2350 'group.read': 1,
2351 2351 'group.write': 3,
2352 2352 'group.admin': 4,
2353 2353
2354 2354 'usergroup.none': 0,
2355 2355 'usergroup.read': 1,
2356 2356 'usergroup.write': 3,
2357 2357 'usergroup.admin': 4,
2358 2358
2359 2359 'hg.repogroup.create.false': 0,
2360 2360 'hg.repogroup.create.true': 1,
2361 2361
2362 2362 'hg.usergroup.create.false': 0,
2363 2363 'hg.usergroup.create.true': 1,
2364 2364
2365 2365 'hg.fork.none': 0,
2366 2366 'hg.fork.repository': 1,
2367 2367 'hg.create.none': 0,
2368 2368 'hg.create.repository': 1
2369 2369 }
2370 2370
2371 2371 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2372 2372 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2373 2373 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2374 2374
2375 2375 def __unicode__(self):
2376 2376 return u"<%s('%s:%s')>" % (
2377 2377 self.__class__.__name__, self.permission_id, self.permission_name
2378 2378 )
2379 2379
2380 2380 @classmethod
2381 2381 def get_by_key(cls, key):
2382 2382 return cls.query().filter(cls.permission_name == key).scalar()
2383 2383
2384 2384 @classmethod
2385 2385 def get_default_repo_perms(cls, user_id, repo_id=None):
2386 2386 q = Session().query(UserRepoToPerm, Repository, Permission)\
2387 2387 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2388 2388 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2389 2389 .filter(UserRepoToPerm.user_id == user_id)
2390 2390 if repo_id:
2391 2391 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2392 2392 return q.all()
2393 2393
2394 2394 @classmethod
2395 2395 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2396 2396 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2397 2397 .join(
2398 2398 Permission,
2399 2399 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2400 2400 .join(
2401 2401 Repository,
2402 2402 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2403 2403 .join(
2404 2404 UserGroup,
2405 2405 UserGroupRepoToPerm.users_group_id ==
2406 2406 UserGroup.users_group_id)\
2407 2407 .join(
2408 2408 UserGroupMember,
2409 2409 UserGroupRepoToPerm.users_group_id ==
2410 2410 UserGroupMember.users_group_id)\
2411 2411 .filter(
2412 2412 UserGroupMember.user_id == user_id,
2413 2413 UserGroup.users_group_active == true())
2414 2414 if repo_id:
2415 2415 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2416 2416 return q.all()
2417 2417
2418 2418 @classmethod
2419 2419 def get_default_group_perms(cls, user_id, repo_group_id=None):
2420 2420 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2421 2421 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2422 2422 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2423 2423 .filter(UserRepoGroupToPerm.user_id == user_id)
2424 2424 if repo_group_id:
2425 2425 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2426 2426 return q.all()
2427 2427
2428 2428 @classmethod
2429 2429 def get_default_group_perms_from_user_group(
2430 2430 cls, user_id, repo_group_id=None):
2431 2431 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2432 2432 .join(
2433 2433 Permission,
2434 2434 UserGroupRepoGroupToPerm.permission_id ==
2435 2435 Permission.permission_id)\
2436 2436 .join(
2437 2437 RepoGroup,
2438 2438 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2439 2439 .join(
2440 2440 UserGroup,
2441 2441 UserGroupRepoGroupToPerm.users_group_id ==
2442 2442 UserGroup.users_group_id)\
2443 2443 .join(
2444 2444 UserGroupMember,
2445 2445 UserGroupRepoGroupToPerm.users_group_id ==
2446 2446 UserGroupMember.users_group_id)\
2447 2447 .filter(
2448 2448 UserGroupMember.user_id == user_id,
2449 2449 UserGroup.users_group_active == true())
2450 2450 if repo_group_id:
2451 2451 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2452 2452 return q.all()
2453 2453
2454 2454 @classmethod
2455 2455 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2456 2456 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2457 2457 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2458 2458 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2459 2459 .filter(UserUserGroupToPerm.user_id == user_id)
2460 2460 if user_group_id:
2461 2461 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2462 2462 return q.all()
2463 2463
2464 2464 @classmethod
2465 2465 def get_default_user_group_perms_from_user_group(
2466 2466 cls, user_id, user_group_id=None):
2467 2467 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2468 2468 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2469 2469 .join(
2470 2470 Permission,
2471 2471 UserGroupUserGroupToPerm.permission_id ==
2472 2472 Permission.permission_id)\
2473 2473 .join(
2474 2474 TargetUserGroup,
2475 2475 UserGroupUserGroupToPerm.target_user_group_id ==
2476 2476 TargetUserGroup.users_group_id)\
2477 2477 .join(
2478 2478 UserGroup,
2479 2479 UserGroupUserGroupToPerm.user_group_id ==
2480 2480 UserGroup.users_group_id)\
2481 2481 .join(
2482 2482 UserGroupMember,
2483 2483 UserGroupUserGroupToPerm.user_group_id ==
2484 2484 UserGroupMember.users_group_id)\
2485 2485 .filter(
2486 2486 UserGroupMember.user_id == user_id,
2487 2487 UserGroup.users_group_active == true())
2488 2488 if user_group_id:
2489 2489 q = q.filter(
2490 2490 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2491 2491
2492 2492 return q.all()
2493 2493
2494 2494
2495 2495 class UserRepoToPerm(Base, BaseModel):
2496 2496 __tablename__ = 'repo_to_perm'
2497 2497 __table_args__ = (
2498 2498 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2499 2499 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2500 2500 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2501 2501 )
2502 2502 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2503 2503 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2504 2504 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2505 2505 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2506 2506
2507 2507 user = relationship('User')
2508 2508 repository = relationship('Repository')
2509 2509 permission = relationship('Permission')
2510 2510
2511 2511 @classmethod
2512 2512 def create(cls, user, repository, permission):
2513 2513 n = cls()
2514 2514 n.user = user
2515 2515 n.repository = repository
2516 2516 n.permission = permission
2517 2517 Session().add(n)
2518 2518 return n
2519 2519
2520 2520 def __unicode__(self):
2521 2521 return u'<%s => %s >' % (self.user, self.repository)
2522 2522
2523 2523
2524 2524 class UserUserGroupToPerm(Base, BaseModel):
2525 2525 __tablename__ = 'user_user_group_to_perm'
2526 2526 __table_args__ = (
2527 2527 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2528 2528 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2529 2529 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2530 2530 )
2531 2531 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2532 2532 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2533 2533 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2534 2534 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2535 2535
2536 2536 user = relationship('User')
2537 2537 user_group = relationship('UserGroup')
2538 2538 permission = relationship('Permission')
2539 2539
2540 2540 @classmethod
2541 2541 def create(cls, user, user_group, permission):
2542 2542 n = cls()
2543 2543 n.user = user
2544 2544 n.user_group = user_group
2545 2545 n.permission = permission
2546 2546 Session().add(n)
2547 2547 return n
2548 2548
2549 2549 def __unicode__(self):
2550 2550 return u'<%s => %s >' % (self.user, self.user_group)
2551 2551
2552 2552
2553 2553 class UserToPerm(Base, BaseModel):
2554 2554 __tablename__ = 'user_to_perm'
2555 2555 __table_args__ = (
2556 2556 UniqueConstraint('user_id', 'permission_id'),
2557 2557 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2558 2558 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2559 2559 )
2560 2560 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2561 2561 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2562 2562 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2563 2563
2564 2564 user = relationship('User')
2565 2565 permission = relationship('Permission', lazy='joined')
2566 2566
2567 2567 def __unicode__(self):
2568 2568 return u'<%s => %s >' % (self.user, self.permission)
2569 2569
2570 2570
2571 2571 class UserGroupRepoToPerm(Base, BaseModel):
2572 2572 __tablename__ = 'users_group_repo_to_perm'
2573 2573 __table_args__ = (
2574 2574 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2575 2575 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2576 2576 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2577 2577 )
2578 2578 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2579 2579 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2580 2580 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2581 2581 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2582 2582
2583 2583 users_group = relationship('UserGroup')
2584 2584 permission = relationship('Permission')
2585 2585 repository = relationship('Repository')
2586 2586
2587 2587 @classmethod
2588 2588 def create(cls, users_group, repository, permission):
2589 2589 n = cls()
2590 2590 n.users_group = users_group
2591 2591 n.repository = repository
2592 2592 n.permission = permission
2593 2593 Session().add(n)
2594 2594 return n
2595 2595
2596 2596 def __unicode__(self):
2597 2597 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2598 2598
2599 2599
2600 2600 class UserGroupUserGroupToPerm(Base, BaseModel):
2601 2601 __tablename__ = 'user_group_user_group_to_perm'
2602 2602 __table_args__ = (
2603 2603 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2604 2604 CheckConstraint('target_user_group_id != user_group_id'),
2605 2605 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2606 2606 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2607 2607 )
2608 2608 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)
2609 2609 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2610 2610 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2611 2611 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2612 2612
2613 2613 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2614 2614 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2615 2615 permission = relationship('Permission')
2616 2616
2617 2617 @classmethod
2618 2618 def create(cls, target_user_group, user_group, permission):
2619 2619 n = cls()
2620 2620 n.target_user_group = target_user_group
2621 2621 n.user_group = user_group
2622 2622 n.permission = permission
2623 2623 Session().add(n)
2624 2624 return n
2625 2625
2626 2626 def __unicode__(self):
2627 2627 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2628 2628
2629 2629
2630 2630 class UserGroupToPerm(Base, BaseModel):
2631 2631 __tablename__ = 'users_group_to_perm'
2632 2632 __table_args__ = (
2633 2633 UniqueConstraint('users_group_id', 'permission_id',),
2634 2634 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2635 2635 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2636 2636 )
2637 2637 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2638 2638 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2639 2639 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2640 2640
2641 2641 users_group = relationship('UserGroup')
2642 2642 permission = relationship('Permission')
2643 2643
2644 2644
2645 2645 class UserRepoGroupToPerm(Base, BaseModel):
2646 2646 __tablename__ = 'user_repo_group_to_perm'
2647 2647 __table_args__ = (
2648 2648 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2649 2649 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2650 2650 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2651 2651 )
2652 2652
2653 2653 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2654 2654 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2655 2655 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2656 2656 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2657 2657
2658 2658 user = relationship('User')
2659 2659 group = relationship('RepoGroup')
2660 2660 permission = relationship('Permission')
2661 2661
2662 2662 @classmethod
2663 2663 def create(cls, user, repository_group, permission):
2664 2664 n = cls()
2665 2665 n.user = user
2666 2666 n.group = repository_group
2667 2667 n.permission = permission
2668 2668 Session().add(n)
2669 2669 return n
2670 2670
2671 2671
2672 2672 class UserGroupRepoGroupToPerm(Base, BaseModel):
2673 2673 __tablename__ = 'users_group_repo_group_to_perm'
2674 2674 __table_args__ = (
2675 2675 UniqueConstraint('users_group_id', 'group_id'),
2676 2676 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2677 2677 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2678 2678 )
2679 2679
2680 2680 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)
2681 2681 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2682 2682 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2683 2683 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2684 2684
2685 2685 users_group = relationship('UserGroup')
2686 2686 permission = relationship('Permission')
2687 2687 group = relationship('RepoGroup')
2688 2688
2689 2689 @classmethod
2690 2690 def create(cls, user_group, repository_group, permission):
2691 2691 n = cls()
2692 2692 n.users_group = user_group
2693 2693 n.group = repository_group
2694 2694 n.permission = permission
2695 2695 Session().add(n)
2696 2696 return n
2697 2697
2698 2698 def __unicode__(self):
2699 2699 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2700 2700
2701 2701
2702 2702 class Statistics(Base, BaseModel):
2703 2703 __tablename__ = 'statistics'
2704 2704 __table_args__ = (
2705 2705 UniqueConstraint('repository_id'),
2706 2706 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2707 2707 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2708 2708 )
2709 2709 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2710 2710 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2711 2711 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2712 2712 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2713 2713 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2714 2714 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2715 2715
2716 2716 repository = relationship('Repository', single_parent=True)
2717 2717
2718 2718
2719 2719 class UserFollowing(Base, BaseModel):
2720 2720 __tablename__ = 'user_followings'
2721 2721 __table_args__ = (
2722 2722 UniqueConstraint('user_id', 'follows_repository_id'),
2723 2723 UniqueConstraint('user_id', 'follows_user_id'),
2724 2724 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2725 2725 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2726 2726 )
2727 2727
2728 2728 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2729 2729 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2730 2730 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2731 2731 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2732 2732 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2733 2733
2734 2734 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2735 2735
2736 2736 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2737 2737 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2738 2738
2739 2739 @classmethod
2740 2740 def get_repo_followers(cls, repo_id):
2741 2741 return cls.query().filter(cls.follows_repo_id == repo_id)
2742 2742
2743 2743
2744 2744 class CacheKey(Base, BaseModel):
2745 2745 __tablename__ = 'cache_invalidation'
2746 2746 __table_args__ = (
2747 2747 UniqueConstraint('cache_key'),
2748 2748 Index('key_idx', 'cache_key'),
2749 2749 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2750 2750 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2751 2751 )
2752 2752 CACHE_TYPE_ATOM = 'ATOM'
2753 2753 CACHE_TYPE_RSS = 'RSS'
2754 2754 CACHE_TYPE_README = 'README'
2755 2755
2756 2756 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2757 2757 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2758 2758 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2759 2759 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2760 2760
2761 2761 def __init__(self, cache_key, cache_args=''):
2762 2762 self.cache_key = cache_key
2763 2763 self.cache_args = cache_args
2764 2764 self.cache_active = False
2765 2765
2766 2766 def __unicode__(self):
2767 2767 return u"<%s('%s:%s[%s]')>" % (
2768 2768 self.__class__.__name__,
2769 2769 self.cache_id, self.cache_key, self.cache_active)
2770 2770
2771 2771 def _cache_key_partition(self):
2772 2772 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2773 2773 return prefix, repo_name, suffix
2774 2774
2775 2775 def get_prefix(self):
2776 2776 """
2777 2777 Try to extract prefix from existing cache key. The key could consist
2778 2778 of prefix, repo_name, suffix
2779 2779 """
2780 2780 # this returns prefix, repo_name, suffix
2781 2781 return self._cache_key_partition()[0]
2782 2782
2783 2783 def get_suffix(self):
2784 2784 """
2785 2785 get suffix that might have been used in _get_cache_key to
2786 2786 generate self.cache_key. Only used for informational purposes
2787 2787 in repo_edit.html.
2788 2788 """
2789 2789 # prefix, repo_name, suffix
2790 2790 return self._cache_key_partition()[2]
2791 2791
2792 2792 @classmethod
2793 2793 def delete_all_cache(cls):
2794 2794 """
2795 2795 Delete all cache keys from database.
2796 2796 Should only be run when all instances are down and all entries
2797 2797 thus stale.
2798 2798 """
2799 2799 cls.query().delete()
2800 2800 Session().commit()
2801 2801
2802 2802 @classmethod
2803 2803 def get_cache_key(cls, repo_name, cache_type):
2804 2804 """
2805 2805
2806 2806 Generate a cache key for this process of RhodeCode instance.
2807 2807 Prefix most likely will be process id or maybe explicitly set
2808 2808 instance_id from .ini file.
2809 2809 """
2810 2810 import rhodecode
2811 2811 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2812 2812
2813 2813 repo_as_unicode = safe_unicode(repo_name)
2814 2814 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2815 2815 if cache_type else repo_as_unicode
2816 2816
2817 2817 return u'{}{}'.format(prefix, key)
2818 2818
2819 2819 @classmethod
2820 2820 def set_invalidate(cls, repo_name, delete=False):
2821 2821 """
2822 2822 Mark all caches of a repo as invalid in the database.
2823 2823 """
2824 2824
2825 2825 try:
2826 2826 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2827 2827 if delete:
2828 2828 log.debug('cache objects deleted for repo %s',
2829 2829 safe_str(repo_name))
2830 2830 qry.delete()
2831 2831 else:
2832 2832 log.debug('cache objects marked as invalid for repo %s',
2833 2833 safe_str(repo_name))
2834 2834 qry.update({"cache_active": False})
2835 2835
2836 2836 Session().commit()
2837 2837 except Exception:
2838 2838 log.exception(
2839 2839 'Cache key invalidation failed for repository %s',
2840 2840 safe_str(repo_name))
2841 2841 Session().rollback()
2842 2842
2843 2843 @classmethod
2844 2844 def get_active_cache(cls, cache_key):
2845 2845 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2846 2846 if inv_obj:
2847 2847 return inv_obj
2848 2848 return None
2849 2849
2850 2850 @classmethod
2851 2851 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2852 2852 thread_scoped=False):
2853 2853 """
2854 2854 @cache_region('long_term')
2855 2855 def _heavy_calculation(cache_key):
2856 2856 return 'result'
2857 2857
2858 2858 cache_context = CacheKey.repo_context_cache(
2859 2859 _heavy_calculation, repo_name, cache_type)
2860 2860
2861 2861 with cache_context as context:
2862 2862 context.invalidate()
2863 2863 computed = context.compute()
2864 2864
2865 2865 assert computed == 'result'
2866 2866 """
2867 2867 from rhodecode.lib import caches
2868 2868 return caches.InvalidationContext(
2869 2869 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2870 2870
2871 2871
2872 2872 class ChangesetComment(Base, BaseModel):
2873 2873 __tablename__ = 'changeset_comments'
2874 2874 __table_args__ = (
2875 2875 Index('cc_revision_idx', 'revision'),
2876 2876 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2877 2877 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2878 2878 )
2879 2879
2880 2880 COMMENT_OUTDATED = u'comment_outdated'
2881 2881
2882 2882 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2883 2883 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2884 2884 revision = Column('revision', String(40), nullable=True)
2885 2885 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2886 2886 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2887 2887 line_no = Column('line_no', Unicode(10), nullable=True)
2888 2888 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2889 2889 f_path = Column('f_path', Unicode(1000), nullable=True)
2890 2890 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2891 2891 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2892 2892 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2893 2893 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2894 2894 renderer = Column('renderer', Unicode(64), nullable=True)
2895 2895 display_state = Column('display_state', Unicode(128), nullable=True)
2896 2896
2897 2897 author = relationship('User', lazy='joined')
2898 2898 repo = relationship('Repository')
2899 2899 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2900 2900 pull_request = relationship('PullRequest', lazy='joined')
2901 2901 pull_request_version = relationship('PullRequestVersion')
2902 2902
2903 2903 @classmethod
2904 2904 def get_users(cls, revision=None, pull_request_id=None):
2905 2905 """
2906 2906 Returns user associated with this ChangesetComment. ie those
2907 2907 who actually commented
2908 2908
2909 2909 :param cls:
2910 2910 :param revision:
2911 2911 """
2912 2912 q = Session().query(User)\
2913 2913 .join(ChangesetComment.author)
2914 2914 if revision:
2915 2915 q = q.filter(cls.revision == revision)
2916 2916 elif pull_request_id:
2917 2917 q = q.filter(cls.pull_request_id == pull_request_id)
2918 2918 return q.all()
2919 2919
2920 2920 def render(self, mentions=False):
2921 2921 from rhodecode.lib import helpers as h
2922 2922 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2923 2923
2924 2924 def __repr__(self):
2925 2925 if self.comment_id:
2926 2926 return '<DB:ChangesetComment #%s>' % self.comment_id
2927 2927 else:
2928 2928 return '<DB:ChangesetComment at %#x>' % id(self)
2929 2929
2930 2930
2931 2931 class ChangesetStatus(Base, BaseModel):
2932 2932 __tablename__ = 'changeset_statuses'
2933 2933 __table_args__ = (
2934 2934 Index('cs_revision_idx', 'revision'),
2935 2935 Index('cs_version_idx', 'version'),
2936 2936 UniqueConstraint('repo_id', 'revision', 'version'),
2937 2937 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2938 2938 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2939 2939 )
2940 2940 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2941 2941 STATUS_APPROVED = 'approved'
2942 2942 STATUS_REJECTED = 'rejected'
2943 2943 STATUS_UNDER_REVIEW = 'under_review'
2944 2944
2945 2945 STATUSES = [
2946 2946 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2947 2947 (STATUS_APPROVED, _("Approved")),
2948 2948 (STATUS_REJECTED, _("Rejected")),
2949 2949 (STATUS_UNDER_REVIEW, _("Under Review")),
2950 2950 ]
2951 2951
2952 2952 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2953 2953 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2954 2954 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2955 2955 revision = Column('revision', String(40), nullable=False)
2956 2956 status = Column('status', String(128), nullable=False, default=DEFAULT)
2957 2957 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2958 2958 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2959 2959 version = Column('version', Integer(), nullable=False, default=0)
2960 2960 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2961 2961
2962 2962 author = relationship('User', lazy='joined')
2963 2963 repo = relationship('Repository')
2964 2964 comment = relationship('ChangesetComment', lazy='joined')
2965 2965 pull_request = relationship('PullRequest', lazy='joined')
2966 2966
2967 2967 def __unicode__(self):
2968 2968 return u"<%s('%s[%s]:%s')>" % (
2969 2969 self.__class__.__name__,
2970 2970 self.status, self.version, self.author
2971 2971 )
2972 2972
2973 2973 @classmethod
2974 2974 def get_status_lbl(cls, value):
2975 2975 return dict(cls.STATUSES).get(value)
2976 2976
2977 2977 @property
2978 2978 def status_lbl(self):
2979 2979 return ChangesetStatus.get_status_lbl(self.status)
2980 2980
2981 2981
2982 2982 class _PullRequestBase(BaseModel):
2983 2983 """
2984 2984 Common attributes of pull request and version entries.
2985 2985 """
2986 2986
2987 2987 # .status values
2988 2988 STATUS_NEW = u'new'
2989 2989 STATUS_OPEN = u'open'
2990 2990 STATUS_CLOSED = u'closed'
2991 2991
2992 2992 title = Column('title', Unicode(255), nullable=True)
2993 2993 description = Column(
2994 2994 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2995 2995 nullable=True)
2996 2996 # new/open/closed status of pull request (not approve/reject/etc)
2997 2997 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
2998 2998 created_on = Column(
2999 2999 'created_on', DateTime(timezone=False), nullable=False,
3000 3000 default=datetime.datetime.now)
3001 3001 updated_on = Column(
3002 3002 'updated_on', DateTime(timezone=False), nullable=False,
3003 3003 default=datetime.datetime.now)
3004 3004
3005 3005 @declared_attr
3006 3006 def user_id(cls):
3007 3007 return Column(
3008 3008 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3009 3009 unique=None)
3010 3010
3011 3011 # 500 revisions max
3012 3012 _revisions = Column(
3013 3013 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3014 3014
3015 3015 @declared_attr
3016 3016 def source_repo_id(cls):
3017 3017 # TODO: dan: rename column to source_repo_id
3018 3018 return Column(
3019 3019 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3020 3020 nullable=False)
3021 3021
3022 3022 source_ref = Column('org_ref', Unicode(255), nullable=False)
3023 3023
3024 3024 @declared_attr
3025 3025 def target_repo_id(cls):
3026 3026 # TODO: dan: rename column to target_repo_id
3027 3027 return Column(
3028 3028 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3029 3029 nullable=False)
3030 3030
3031 3031 target_ref = Column('other_ref', Unicode(255), nullable=False)
3032 3032
3033 3033 # TODO: dan: rename column to last_merge_source_rev
3034 3034 _last_merge_source_rev = Column(
3035 3035 'last_merge_org_rev', String(40), nullable=True)
3036 3036 # TODO: dan: rename column to last_merge_target_rev
3037 3037 _last_merge_target_rev = Column(
3038 3038 'last_merge_other_rev', String(40), nullable=True)
3039 3039 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3040 3040 merge_rev = Column('merge_rev', String(40), nullable=True)
3041 3041
3042 3042 @hybrid_property
3043 3043 def revisions(self):
3044 3044 return self._revisions.split(':') if self._revisions else []
3045 3045
3046 3046 @revisions.setter
3047 3047 def revisions(self, val):
3048 3048 self._revisions = ':'.join(val)
3049 3049
3050 3050 @declared_attr
3051 3051 def author(cls):
3052 3052 return relationship('User', lazy='joined')
3053 3053
3054 3054 @declared_attr
3055 3055 def source_repo(cls):
3056 3056 return relationship(
3057 3057 'Repository',
3058 3058 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3059 3059
3060 3060 @property
3061 3061 def source_ref_parts(self):
3062 3062 refs = self.source_ref.split(':')
3063 3063 return Reference(refs[0], refs[1], refs[2])
3064 3064
3065 3065 @declared_attr
3066 3066 def target_repo(cls):
3067 3067 return relationship(
3068 3068 'Repository',
3069 3069 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3070 3070
3071 3071 @property
3072 3072 def target_ref_parts(self):
3073 3073 refs = self.target_ref.split(':')
3074 3074 return Reference(refs[0], refs[1], refs[2])
3075 3075
3076 3076
3077 3077 class PullRequest(Base, _PullRequestBase):
3078 3078 __tablename__ = 'pull_requests'
3079 3079 __table_args__ = (
3080 3080 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3081 3081 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3082 3082 )
3083 3083
3084 3084 pull_request_id = Column(
3085 3085 'pull_request_id', Integer(), nullable=False, primary_key=True)
3086 3086
3087 3087 def __repr__(self):
3088 3088 if self.pull_request_id:
3089 3089 return '<DB:PullRequest #%s>' % self.pull_request_id
3090 3090 else:
3091 3091 return '<DB:PullRequest at %#x>' % id(self)
3092 3092
3093 3093 reviewers = relationship('PullRequestReviewers',
3094 3094 cascade="all, delete, delete-orphan")
3095 3095 statuses = relationship('ChangesetStatus')
3096 3096 comments = relationship('ChangesetComment',
3097 3097 cascade="all, delete, delete-orphan")
3098 3098 versions = relationship('PullRequestVersion',
3099 3099 cascade="all, delete, delete-orphan")
3100 3100
3101 3101 def is_closed(self):
3102 3102 return self.status == self.STATUS_CLOSED
3103 3103
3104 3104 def get_api_data(self):
3105 3105 from rhodecode.model.pull_request import PullRequestModel
3106 3106 pull_request = self
3107 3107 merge_status = PullRequestModel().merge_status(pull_request)
3108 3108 data = {
3109 3109 'pull_request_id': pull_request.pull_request_id,
3110 3110 'url': url('pullrequest_show', repo_name=self.target_repo.repo_name,
3111 3111 pull_request_id=self.pull_request_id,
3112 3112 qualified=True),
3113 3113 'title': pull_request.title,
3114 3114 'description': pull_request.description,
3115 3115 'status': pull_request.status,
3116 3116 'created_on': pull_request.created_on,
3117 3117 'updated_on': pull_request.updated_on,
3118 3118 'commit_ids': pull_request.revisions,
3119 3119 'review_status': pull_request.calculated_review_status(),
3120 3120 'mergeable': {
3121 3121 'status': merge_status[0],
3122 3122 'message': unicode(merge_status[1]),
3123 3123 },
3124 3124 'source': {
3125 3125 'clone_url': pull_request.source_repo.clone_url(),
3126 3126 'repository': pull_request.source_repo.repo_name,
3127 3127 'reference': {
3128 3128 'name': pull_request.source_ref_parts.name,
3129 3129 'type': pull_request.source_ref_parts.type,
3130 3130 'commit_id': pull_request.source_ref_parts.commit_id,
3131 3131 },
3132 3132 },
3133 3133 'target': {
3134 3134 'clone_url': pull_request.target_repo.clone_url(),
3135 3135 'repository': pull_request.target_repo.repo_name,
3136 3136 'reference': {
3137 3137 'name': pull_request.target_ref_parts.name,
3138 3138 'type': pull_request.target_ref_parts.type,
3139 3139 'commit_id': pull_request.target_ref_parts.commit_id,
3140 3140 },
3141 3141 },
3142 3142 'author': pull_request.author.get_api_data(include_secrets=False,
3143 3143 details='basic'),
3144 3144 'reviewers': [
3145 3145 {
3146 3146 'user': reviewer.get_api_data(include_secrets=False,
3147 3147 details='basic'),
3148 3148 'review_status': st[0][1].status if st else 'not_reviewed',
3149 3149 }
3150 3150 for reviewer, st in pull_request.reviewers_statuses()
3151 3151 ]
3152 3152 }
3153 3153
3154 3154 return data
3155 3155
3156 3156 def __json__(self):
3157 3157 return {
3158 3158 'revisions': self.revisions,
3159 3159 }
3160 3160
3161 3161 def calculated_review_status(self):
3162 3162 # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html
3163 3163 # because it's tricky on how to use ChangesetStatusModel from there
3164 3164 warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning)
3165 3165 from rhodecode.model.changeset_status import ChangesetStatusModel
3166 3166 return ChangesetStatusModel().calculated_review_status(self)
3167 3167
3168 3168 def reviewers_statuses(self):
3169 3169 warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
3170 3170 from rhodecode.model.changeset_status import ChangesetStatusModel
3171 3171 return ChangesetStatusModel().reviewers_statuses(self)
3172 3172
3173 3173
3174 3174 class PullRequestVersion(Base, _PullRequestBase):
3175 3175 __tablename__ = 'pull_request_versions'
3176 3176 __table_args__ = (
3177 3177 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3178 3178 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3179 3179 )
3180 3180
3181 3181 pull_request_version_id = Column(
3182 3182 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3183 3183 pull_request_id = Column(
3184 3184 'pull_request_id', Integer(),
3185 3185 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3186 3186 pull_request = relationship('PullRequest')
3187 3187
3188 3188 def __repr__(self):
3189 3189 if self.pull_request_version_id:
3190 3190 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3191 3191 else:
3192 3192 return '<DB:PullRequestVersion at %#x>' % id(self)
3193 3193
3194 3194
3195 3195 class PullRequestReviewers(Base, BaseModel):
3196 3196 __tablename__ = 'pull_request_reviewers'
3197 3197 __table_args__ = (
3198 3198 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3199 3199 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3200 3200 )
3201 3201
3202 3202 def __init__(self, user=None, pull_request=None):
3203 3203 self.user = user
3204 3204 self.pull_request = pull_request
3205 3205
3206 3206 pull_requests_reviewers_id = Column(
3207 3207 'pull_requests_reviewers_id', Integer(), nullable=False,
3208 3208 primary_key=True)
3209 3209 pull_request_id = Column(
3210 3210 "pull_request_id", Integer(),
3211 3211 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3212 3212 user_id = Column(
3213 3213 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3214 3214
3215 3215 user = relationship('User')
3216 3216 pull_request = relationship('PullRequest')
3217 3217
3218 3218
3219 3219 class Notification(Base, BaseModel):
3220 3220 __tablename__ = 'notifications'
3221 3221 __table_args__ = (
3222 3222 Index('notification_type_idx', 'type'),
3223 3223 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3224 3224 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3225 3225 )
3226 3226
3227 3227 TYPE_CHANGESET_COMMENT = u'cs_comment'
3228 3228 TYPE_MESSAGE = u'message'
3229 3229 TYPE_MENTION = u'mention'
3230 3230 TYPE_REGISTRATION = u'registration'
3231 3231 TYPE_PULL_REQUEST = u'pull_request'
3232 3232 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3233 3233
3234 3234 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3235 3235 subject = Column('subject', Unicode(512), nullable=True)
3236 3236 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3237 3237 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3238 3238 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3239 3239 type_ = Column('type', Unicode(255))
3240 3240
3241 3241 created_by_user = relationship('User')
3242 3242 notifications_to_users = relationship('UserNotification', lazy='joined',
3243 3243 cascade="all, delete, delete-orphan")
3244 3244
3245 3245 @property
3246 3246 def recipients(self):
3247 3247 return [x.user for x in UserNotification.query()\
3248 3248 .filter(UserNotification.notification == self)\
3249 3249 .order_by(UserNotification.user_id.asc()).all()]
3250 3250
3251 3251 @classmethod
3252 3252 def create(cls, created_by, subject, body, recipients, type_=None):
3253 3253 if type_ is None:
3254 3254 type_ = Notification.TYPE_MESSAGE
3255 3255
3256 3256 notification = cls()
3257 3257 notification.created_by_user = created_by
3258 3258 notification.subject = subject
3259 3259 notification.body = body
3260 3260 notification.type_ = type_
3261 3261 notification.created_on = datetime.datetime.now()
3262 3262
3263 3263 for u in recipients:
3264 3264 assoc = UserNotification()
3265 3265 assoc.notification = notification
3266 3266
3267 3267 # if created_by is inside recipients mark his notification
3268 3268 # as read
3269 3269 if u.user_id == created_by.user_id:
3270 3270 assoc.read = True
3271 3271
3272 3272 u.notifications.append(assoc)
3273 3273 Session().add(notification)
3274 3274
3275 3275 return notification
3276 3276
3277 3277 @property
3278 3278 def description(self):
3279 3279 from rhodecode.model.notification import NotificationModel
3280 3280 return NotificationModel().make_description(self)
3281 3281
3282 3282
3283 3283 class UserNotification(Base, BaseModel):
3284 3284 __tablename__ = 'user_to_notification'
3285 3285 __table_args__ = (
3286 3286 UniqueConstraint('user_id', 'notification_id'),
3287 3287 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3288 3288 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3289 3289 )
3290 3290 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3291 3291 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3292 3292 read = Column('read', Boolean, default=False)
3293 3293 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3294 3294
3295 3295 user = relationship('User', lazy="joined")
3296 3296 notification = relationship('Notification', lazy="joined",
3297 3297 order_by=lambda: Notification.created_on.desc(),)
3298 3298
3299 3299 def mark_as_read(self):
3300 3300 self.read = True
3301 3301 Session().add(self)
3302 3302
3303 3303
3304 3304 class Gist(Base, BaseModel):
3305 3305 __tablename__ = 'gists'
3306 3306 __table_args__ = (
3307 3307 Index('g_gist_access_id_idx', 'gist_access_id'),
3308 3308 Index('g_created_on_idx', 'created_on'),
3309 3309 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3310 3310 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3311 3311 )
3312 3312 GIST_PUBLIC = u'public'
3313 3313 GIST_PRIVATE = u'private'
3314 3314 DEFAULT_FILENAME = u'gistfile1.txt'
3315 3315
3316 3316 ACL_LEVEL_PUBLIC = u'acl_public'
3317 3317 ACL_LEVEL_PRIVATE = u'acl_private'
3318 3318
3319 3319 gist_id = Column('gist_id', Integer(), primary_key=True)
3320 3320 gist_access_id = Column('gist_access_id', Unicode(250))
3321 3321 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3322 3322 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3323 3323 gist_expires = Column('gist_expires', Float(53), nullable=False)
3324 3324 gist_type = Column('gist_type', Unicode(128), nullable=False)
3325 3325 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3326 3326 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3327 3327 acl_level = Column('acl_level', Unicode(128), nullable=True)
3328 3328
3329 3329 owner = relationship('User')
3330 3330
3331 3331 def __repr__(self):
3332 3332 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3333 3333
3334 3334 @classmethod
3335 3335 def get_or_404(cls, id_):
3336 3336 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3337 3337 if not res:
3338 3338 raise HTTPNotFound
3339 3339 return res
3340 3340
3341 3341 @classmethod
3342 3342 def get_by_access_id(cls, gist_access_id):
3343 3343 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3344 3344
3345 3345 def gist_url(self):
3346 3346 import rhodecode
3347 3347 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3348 3348 if alias_url:
3349 3349 return alias_url.replace('{gistid}', self.gist_access_id)
3350 3350
3351 3351 return url('gist', gist_id=self.gist_access_id, qualified=True)
3352 3352
3353 3353 @classmethod
3354 3354 def base_path(cls):
3355 3355 """
3356 3356 Returns base path when all gists are stored
3357 3357
3358 3358 :param cls:
3359 3359 """
3360 3360 from rhodecode.model.gist import GIST_STORE_LOC
3361 3361 q = Session().query(RhodeCodeUi)\
3362 3362 .filter(RhodeCodeUi.ui_key == URL_SEP)
3363 3363 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3364 3364 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3365 3365
3366 3366 def get_api_data(self):
3367 3367 """
3368 3368 Common function for generating gist related data for API
3369 3369 """
3370 3370 gist = self
3371 3371 data = {
3372 3372 'gist_id': gist.gist_id,
3373 3373 'type': gist.gist_type,
3374 3374 'access_id': gist.gist_access_id,
3375 3375 'description': gist.gist_description,
3376 3376 'url': gist.gist_url(),
3377 3377 'expires': gist.gist_expires,
3378 3378 'created_on': gist.created_on,
3379 3379 'modified_at': gist.modified_at,
3380 3380 'content': None,
3381 3381 'acl_level': gist.acl_level,
3382 3382 }
3383 3383 return data
3384 3384
3385 3385 def __json__(self):
3386 3386 data = dict(
3387 3387 )
3388 3388 data.update(self.get_api_data())
3389 3389 return data
3390 3390 # SCM functions
3391 3391
3392 3392 def scm_instance(self, **kwargs):
3393 3393 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3394 3394 return get_vcs_instance(
3395 3395 repo_path=safe_str(full_repo_path), create=False)
3396 3396
3397 3397
3398 3398 class DbMigrateVersion(Base, BaseModel):
3399 3399 __tablename__ = 'db_migrate_version'
3400 3400 __table_args__ = (
3401 3401 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3402 3402 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3403 3403 )
3404 3404 repository_id = Column('repository_id', String(250), primary_key=True)
3405 3405 repository_path = Column('repository_path', Text)
3406 3406 version = Column('version', Integer)
3407 3407
3408 3408
3409 3409 class ExternalIdentity(Base, BaseModel):
3410 3410 __tablename__ = 'external_identities'
3411 3411 __table_args__ = (
3412 3412 Index('local_user_id_idx', 'local_user_id'),
3413 3413 Index('external_id_idx', 'external_id'),
3414 3414 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3415 3415 'mysql_charset': 'utf8'})
3416 3416
3417 3417 external_id = Column('external_id', Unicode(255), default=u'',
3418 3418 primary_key=True)
3419 3419 external_username = Column('external_username', Unicode(1024), default=u'')
3420 3420 local_user_id = Column('local_user_id', Integer(),
3421 3421 ForeignKey('users.user_id'), primary_key=True)
3422 3422 provider_name = Column('provider_name', Unicode(255), default=u'',
3423 3423 primary_key=True)
3424 3424 access_token = Column('access_token', String(1024), default=u'')
3425 3425 alt_token = Column('alt_token', String(1024), default=u'')
3426 3426 token_secret = Column('token_secret', String(1024), default=u'')
3427 3427
3428 3428 @classmethod
3429 3429 def by_external_id_and_provider(cls, external_id, provider_name,
3430 3430 local_user_id=None):
3431 3431 """
3432 3432 Returns ExternalIdentity instance based on search params
3433 3433
3434 3434 :param external_id:
3435 3435 :param provider_name:
3436 3436 :return: ExternalIdentity
3437 3437 """
3438 3438 query = cls.query()
3439 3439 query = query.filter(cls.external_id == external_id)
3440 3440 query = query.filter(cls.provider_name == provider_name)
3441 3441 if local_user_id:
3442 3442 query = query.filter(cls.local_user_id == local_user_id)
3443 3443 return query.first()
3444 3444
3445 3445 @classmethod
3446 3446 def user_by_external_id_and_provider(cls, external_id, provider_name):
3447 3447 """
3448 3448 Returns User instance based on search params
3449 3449
3450 3450 :param external_id:
3451 3451 :param provider_name:
3452 3452 :return: User
3453 3453 """
3454 3454 query = User.query()
3455 3455 query = query.filter(cls.external_id == external_id)
3456 3456 query = query.filter(cls.provider_name == provider_name)
3457 3457 query = query.filter(User.user_id == cls.local_user_id)
3458 3458 return query.first()
3459 3459
3460 3460 @classmethod
3461 3461 def by_local_user_id(cls, local_user_id):
3462 3462 """
3463 3463 Returns all tokens for user
3464 3464
3465 3465 :param local_user_id:
3466 3466 :return: ExternalIdentity
3467 3467 """
3468 3468 query = cls.query()
3469 3469 query = query.filter(cls.local_user_id == local_user_id)
3470 3470 return query
3471 3471
3472 3472
3473 3473 class Integration(Base, BaseModel):
3474 3474 __tablename__ = 'integrations'
3475 3475 __table_args__ = (
3476 3476 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3477 3477 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3478 3478 )
3479 3479
3480 3480 integration_id = Column('integration_id', Integer(), primary_key=True)
3481 3481 integration_type = Column('integration_type', String(255))
3482 3482 enabled = Column('enabled', Boolean(), nullable=False)
3483 3483 name = Column('name', String(255), nullable=False)
3484
3485 3484 settings = Column(
3486 3485 'settings_json', MutationObj.as_mutable(
3487 3486 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3488 3487 repo_id = Column(
3489 3488 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3490 3489 nullable=True, unique=None, default=None)
3491 3490 repo = relationship('Repository', lazy='joined')
3492 3491
3493 3492 repo_group_id = Column(
3494 3493 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3495 3494 nullable=True, unique=None, default=None)
3496 3495 repo_group = relationship('RepoGroup', lazy='joined')
3497 3496
3498 3497 def __repr__(self):
3499 3498 if self.repo:
3500 3499 scope = 'repo=%r' % self.repo
3501 3500 elif self.repo_group:
3502 3501 scope = 'repo_group=%r' % self.repo_group
3503 3502 else:
3504 3503 scope = 'global'
3505 3504
3506 3505 return '<Integration(%r, %r)>' % (self.integration_type, scope)
@@ -1,3506 +1,3534 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 os
26 26 import sys
27 27 import time
28 28 import hashlib
29 29 import logging
30 30 import datetime
31 31 import warnings
32 32 import ipaddress
33 33 import functools
34 34 import traceback
35 35 import collections
36 36
37 37
38 38 from sqlalchemy import *
39 39 from sqlalchemy.exc import IntegrityError
40 40 from sqlalchemy.ext.declarative import declared_attr
41 41 from sqlalchemy.ext.hybrid import hybrid_property
42 42 from sqlalchemy.orm import (
43 43 relationship, joinedload, class_mapper, validates, aliased)
44 44 from sqlalchemy.sql.expression import true
45 45 from beaker.cache import cache_region, region_invalidate
46 46 from webob.exc import HTTPNotFound
47 47 from zope.cachedescriptors.property import Lazy as LazyProperty
48 48
49 49 from pylons import url
50 50 from pylons.i18n.translation import lazy_ugettext as _
51 51
52 52 from rhodecode.lib.vcs import get_backend, get_vcs_instance
53 53 from rhodecode.lib.vcs.utils.helpers import get_scm
54 54 from rhodecode.lib.vcs.exceptions import VCSError
55 55 from rhodecode.lib.vcs.backends.base import (
56 56 EmptyCommit, Reference, MergeFailureReason)
57 57 from rhodecode.lib.utils2 import (
58 58 str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
59 59 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict)
60 60 from rhodecode.lib.jsonalchemy import MutationObj, JsonType, JSONDict
61 61 from rhodecode.lib.ext_json import json
62 62 from rhodecode.lib.caching_query import FromCache
63 63 from rhodecode.lib.encrypt import AESCipher
64 64
65 65 from rhodecode.model.meta import Base, Session
66 66
67 67 URL_SEP = '/'
68 68 log = logging.getLogger(__name__)
69 69
70 70 # =============================================================================
71 71 # BASE CLASSES
72 72 # =============================================================================
73 73
74 74 # this is propagated from .ini file rhodecode.encrypted_values.secret or
75 75 # beaker.session.secret if first is not set.
76 76 # and initialized at environment.py
77 77 ENCRYPTION_KEY = None
78 78
79 79 # used to sort permissions by types, '#' used here is not allowed to be in
80 80 # usernames, and it's very early in sorted string.printable table.
81 81 PERMISSION_TYPE_SORT = {
82 82 'admin': '####',
83 83 'write': '###',
84 84 'read': '##',
85 85 'none': '#',
86 86 }
87 87
88 88
89 89 def display_sort(obj):
90 90 """
91 91 Sort function used to sort permissions in .permissions() function of
92 92 Repository, RepoGroup, UserGroup. Also it put the default user in front
93 93 of all other resources
94 94 """
95 95
96 96 if obj.username == User.DEFAULT_USER:
97 97 return '#####'
98 98 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
99 99 return prefix + obj.username
100 100
101 101
102 102 def _hash_key(k):
103 103 return md5_safe(k)
104 104
105 105
106 106 class EncryptedTextValue(TypeDecorator):
107 107 """
108 108 Special column for encrypted long text data, use like::
109 109
110 110 value = Column("encrypted_value", EncryptedValue(), nullable=False)
111 111
112 112 This column is intelligent so if value is in unencrypted form it return
113 113 unencrypted form, but on save it always encrypts
114 114 """
115 115 impl = Text
116 116
117 117 def process_bind_param(self, value, dialect):
118 118 if not value:
119 119 return value
120 120 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
121 121 # protect against double encrypting if someone manually starts
122 122 # doing
123 123 raise ValueError('value needs to be in unencrypted format, ie. '
124 124 'not starting with enc$aes')
125 125 return 'enc$aes_hmac$%s' % AESCipher(
126 126 ENCRYPTION_KEY, hmac=True).encrypt(value)
127 127
128 128 def process_result_value(self, value, dialect):
129 129 import rhodecode
130 130
131 131 if not value:
132 132 return value
133 133
134 134 parts = value.split('$', 3)
135 135 if not len(parts) == 3:
136 136 # probably not encrypted values
137 137 return value
138 138 else:
139 139 if parts[0] != 'enc':
140 140 # parts ok but without our header ?
141 141 return value
142 142 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
143 143 'rhodecode.encrypted_values.strict') or True)
144 144 # at that stage we know it's our encryption
145 145 if parts[1] == 'aes':
146 146 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
147 147 elif parts[1] == 'aes_hmac':
148 148 decrypted_data = AESCipher(
149 149 ENCRYPTION_KEY, hmac=True,
150 150 strict_verification=enc_strict_mode).decrypt(parts[2])
151 151 else:
152 152 raise ValueError(
153 153 'Encryption type part is wrong, must be `aes` '
154 154 'or `aes_hmac`, got `%s` instead' % (parts[1]))
155 155 return decrypted_data
156 156
157 157
158 158 class BaseModel(object):
159 159 """
160 160 Base Model for all classes
161 161 """
162 162
163 163 @classmethod
164 164 def _get_keys(cls):
165 165 """return column names for this model """
166 166 return class_mapper(cls).c.keys()
167 167
168 168 def get_dict(self):
169 169 """
170 170 return dict with keys and values corresponding
171 171 to this model data """
172 172
173 173 d = {}
174 174 for k in self._get_keys():
175 175 d[k] = getattr(self, k)
176 176
177 177 # also use __json__() if present to get additional fields
178 178 _json_attr = getattr(self, '__json__', None)
179 179 if _json_attr:
180 180 # update with attributes from __json__
181 181 if callable(_json_attr):
182 182 _json_attr = _json_attr()
183 183 for k, val in _json_attr.iteritems():
184 184 d[k] = val
185 185 return d
186 186
187 187 def get_appstruct(self):
188 188 """return list with keys and values tuples corresponding
189 189 to this model data """
190 190
191 191 l = []
192 192 for k in self._get_keys():
193 193 l.append((k, getattr(self, k),))
194 194 return l
195 195
196 196 def populate_obj(self, populate_dict):
197 197 """populate model with data from given populate_dict"""
198 198
199 199 for k in self._get_keys():
200 200 if k in populate_dict:
201 201 setattr(self, k, populate_dict[k])
202 202
203 203 @classmethod
204 204 def query(cls):
205 205 return Session().query(cls)
206 206
207 207 @classmethod
208 208 def get(cls, id_):
209 209 if id_:
210 210 return cls.query().get(id_)
211 211
212 212 @classmethod
213 213 def get_or_404(cls, id_):
214 214 try:
215 215 id_ = int(id_)
216 216 except (TypeError, ValueError):
217 217 raise HTTPNotFound
218 218
219 219 res = cls.query().get(id_)
220 220 if not res:
221 221 raise HTTPNotFound
222 222 return res
223 223
224 224 @classmethod
225 225 def getAll(cls):
226 226 # deprecated and left for backward compatibility
227 227 return cls.get_all()
228 228
229 229 @classmethod
230 230 def get_all(cls):
231 231 return cls.query().all()
232 232
233 233 @classmethod
234 234 def delete(cls, id_):
235 235 obj = cls.query().get(id_)
236 236 Session().delete(obj)
237 237
238 238 @classmethod
239 239 def identity_cache(cls, session, attr_name, value):
240 240 exist_in_session = []
241 241 for (item_cls, pkey), instance in session.identity_map.items():
242 242 if cls == item_cls and getattr(instance, attr_name) == value:
243 243 exist_in_session.append(instance)
244 244 if exist_in_session:
245 245 if len(exist_in_session) == 1:
246 246 return exist_in_session[0]
247 247 log.exception(
248 248 'multiple objects with attr %s and '
249 249 'value %s found with same name: %r',
250 250 attr_name, value, exist_in_session)
251 251
252 252 def __repr__(self):
253 253 if hasattr(self, '__unicode__'):
254 254 # python repr needs to return str
255 255 try:
256 256 return safe_str(self.__unicode__())
257 257 except UnicodeDecodeError:
258 258 pass
259 259 return '<DB:%s>' % (self.__class__.__name__)
260 260
261 261
262 262 class RhodeCodeSetting(Base, BaseModel):
263 263 __tablename__ = 'rhodecode_settings'
264 264 __table_args__ = (
265 265 UniqueConstraint('app_settings_name'),
266 266 {'extend_existing': True, 'mysql_engine': 'InnoDB',
267 267 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
268 268 )
269 269
270 270 SETTINGS_TYPES = {
271 271 'str': safe_str,
272 272 'int': safe_int,
273 273 'unicode': safe_unicode,
274 274 'bool': str2bool,
275 275 'list': functools.partial(aslist, sep=',')
276 276 }
277 277 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
278 278 GLOBAL_CONF_KEY = 'app_settings'
279 279
280 280 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
281 281 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
282 282 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
283 283 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
284 284
285 285 def __init__(self, key='', val='', type='unicode'):
286 286 self.app_settings_name = key
287 287 self.app_settings_type = type
288 288 self.app_settings_value = val
289 289
290 290 @validates('_app_settings_value')
291 291 def validate_settings_value(self, key, val):
292 292 assert type(val) == unicode
293 293 return val
294 294
295 295 @hybrid_property
296 296 def app_settings_value(self):
297 297 v = self._app_settings_value
298 298 _type = self.app_settings_type
299 299 if _type:
300 300 _type = self.app_settings_type.split('.')[0]
301 301 # decode the encrypted value
302 302 if 'encrypted' in self.app_settings_type:
303 303 cipher = EncryptedTextValue()
304 304 v = safe_unicode(cipher.process_result_value(v, None))
305 305
306 306 converter = self.SETTINGS_TYPES.get(_type) or \
307 307 self.SETTINGS_TYPES['unicode']
308 308 return converter(v)
309 309
310 310 @app_settings_value.setter
311 311 def app_settings_value(self, val):
312 312 """
313 313 Setter that will always make sure we use unicode in app_settings_value
314 314
315 315 :param val:
316 316 """
317 317 val = safe_unicode(val)
318 318 # encode the encrypted value
319 319 if 'encrypted' in self.app_settings_type:
320 320 cipher = EncryptedTextValue()
321 321 val = safe_unicode(cipher.process_bind_param(val, None))
322 322 self._app_settings_value = val
323 323
324 324 @hybrid_property
325 325 def app_settings_type(self):
326 326 return self._app_settings_type
327 327
328 328 @app_settings_type.setter
329 329 def app_settings_type(self, val):
330 330 if val.split('.')[0] not in self.SETTINGS_TYPES:
331 331 raise Exception('type must be one of %s got %s'
332 332 % (self.SETTINGS_TYPES.keys(), val))
333 333 self._app_settings_type = val
334 334
335 335 def __unicode__(self):
336 336 return u"<%s('%s:%s[%s]')>" % (
337 337 self.__class__.__name__,
338 338 self.app_settings_name, self.app_settings_value,
339 339 self.app_settings_type
340 340 )
341 341
342 342
343 343 class RhodeCodeUi(Base, BaseModel):
344 344 __tablename__ = 'rhodecode_ui'
345 345 __table_args__ = (
346 346 UniqueConstraint('ui_key'),
347 347 {'extend_existing': True, 'mysql_engine': 'InnoDB',
348 348 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
349 349 )
350 350
351 351 HOOK_REPO_SIZE = 'changegroup.repo_size'
352 352 # HG
353 353 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
354 354 HOOK_PULL = 'outgoing.pull_logger'
355 355 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
356 356 HOOK_PUSH = 'changegroup.push_logger'
357 357
358 358 # TODO: johbo: Unify way how hooks are configured for git and hg,
359 359 # git part is currently hardcoded.
360 360
361 361 # SVN PATTERNS
362 362 SVN_BRANCH_ID = 'vcs_svn_branch'
363 363 SVN_TAG_ID = 'vcs_svn_tag'
364 364
365 365 ui_id = Column(
366 366 "ui_id", Integer(), nullable=False, unique=True, default=None,
367 367 primary_key=True)
368 368 ui_section = Column(
369 369 "ui_section", String(255), nullable=True, unique=None, default=None)
370 370 ui_key = Column(
371 371 "ui_key", String(255), nullable=True, unique=None, default=None)
372 372 ui_value = Column(
373 373 "ui_value", String(255), nullable=True, unique=None, default=None)
374 374 ui_active = Column(
375 375 "ui_active", Boolean(), nullable=True, unique=None, default=True)
376 376
377 377 def __repr__(self):
378 378 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
379 379 self.ui_key, self.ui_value)
380 380
381 381
382 382 class RepoRhodeCodeSetting(Base, BaseModel):
383 383 __tablename__ = 'repo_rhodecode_settings'
384 384 __table_args__ = (
385 385 UniqueConstraint(
386 386 'app_settings_name', 'repository_id',
387 387 name='uq_repo_rhodecode_setting_name_repo_id'),
388 388 {'extend_existing': True, 'mysql_engine': 'InnoDB',
389 389 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
390 390 )
391 391
392 392 repository_id = Column(
393 393 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
394 394 nullable=False)
395 395 app_settings_id = Column(
396 396 "app_settings_id", Integer(), nullable=False, unique=True,
397 397 default=None, primary_key=True)
398 398 app_settings_name = Column(
399 399 "app_settings_name", String(255), nullable=True, unique=None,
400 400 default=None)
401 401 _app_settings_value = Column(
402 402 "app_settings_value", String(4096), nullable=True, unique=None,
403 403 default=None)
404 404 _app_settings_type = Column(
405 405 "app_settings_type", String(255), nullable=True, unique=None,
406 406 default=None)
407 407
408 408 repository = relationship('Repository')
409 409
410 410 def __init__(self, repository_id, key='', val='', type='unicode'):
411 411 self.repository_id = repository_id
412 412 self.app_settings_name = key
413 413 self.app_settings_type = type
414 414 self.app_settings_value = val
415 415
416 416 @validates('_app_settings_value')
417 417 def validate_settings_value(self, key, val):
418 418 assert type(val) == unicode
419 419 return val
420 420
421 421 @hybrid_property
422 422 def app_settings_value(self):
423 423 v = self._app_settings_value
424 424 type_ = self.app_settings_type
425 425 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
426 426 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
427 427 return converter(v)
428 428
429 429 @app_settings_value.setter
430 430 def app_settings_value(self, val):
431 431 """
432 432 Setter that will always make sure we use unicode in app_settings_value
433 433
434 434 :param val:
435 435 """
436 436 self._app_settings_value = safe_unicode(val)
437 437
438 438 @hybrid_property
439 439 def app_settings_type(self):
440 440 return self._app_settings_type
441 441
442 442 @app_settings_type.setter
443 443 def app_settings_type(self, val):
444 444 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
445 445 if val not in SETTINGS_TYPES:
446 446 raise Exception('type must be one of %s got %s'
447 447 % (SETTINGS_TYPES.keys(), val))
448 448 self._app_settings_type = val
449 449
450 450 def __unicode__(self):
451 451 return u"<%s('%s:%s:%s[%s]')>" % (
452 452 self.__class__.__name__, self.repository.repo_name,
453 453 self.app_settings_name, self.app_settings_value,
454 454 self.app_settings_type
455 455 )
456 456
457 457
458 458 class RepoRhodeCodeUi(Base, BaseModel):
459 459 __tablename__ = 'repo_rhodecode_ui'
460 460 __table_args__ = (
461 461 UniqueConstraint(
462 462 'repository_id', 'ui_section', 'ui_key',
463 463 name='uq_repo_rhodecode_ui_repository_id_section_key'),
464 464 {'extend_existing': True, 'mysql_engine': 'InnoDB',
465 465 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
466 466 )
467 467
468 468 repository_id = Column(
469 469 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
470 470 nullable=False)
471 471 ui_id = Column(
472 472 "ui_id", Integer(), nullable=False, unique=True, default=None,
473 473 primary_key=True)
474 474 ui_section = Column(
475 475 "ui_section", String(255), nullable=True, unique=None, default=None)
476 476 ui_key = Column(
477 477 "ui_key", String(255), nullable=True, unique=None, default=None)
478 478 ui_value = Column(
479 479 "ui_value", String(255), nullable=True, unique=None, default=None)
480 480 ui_active = Column(
481 481 "ui_active", Boolean(), nullable=True, unique=None, default=True)
482 482
483 483 repository = relationship('Repository')
484 484
485 485 def __repr__(self):
486 486 return '<%s[%s:%s]%s=>%s]>' % (
487 487 self.__class__.__name__, self.repository.repo_name,
488 488 self.ui_section, self.ui_key, self.ui_value)
489 489
490 490
491 491 class User(Base, BaseModel):
492 492 __tablename__ = 'users'
493 493 __table_args__ = (
494 494 UniqueConstraint('username'), UniqueConstraint('email'),
495 495 Index('u_username_idx', 'username'),
496 496 Index('u_email_idx', 'email'),
497 497 {'extend_existing': True, 'mysql_engine': 'InnoDB',
498 498 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
499 499 )
500 500 DEFAULT_USER = 'default'
501 501 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
502 502 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
503 503
504 504 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
505 505 username = Column("username", String(255), nullable=True, unique=None, default=None)
506 506 password = Column("password", String(255), nullable=True, unique=None, default=None)
507 507 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
508 508 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
509 509 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
510 510 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
511 511 _email = Column("email", String(255), nullable=True, unique=None, default=None)
512 512 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
513 513 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
514 514 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
515 515 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
516 516 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
517 517 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
518 518 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
519 519
520 520 user_log = relationship('UserLog')
521 521 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
522 522
523 523 repositories = relationship('Repository')
524 524 repository_groups = relationship('RepoGroup')
525 525 user_groups = relationship('UserGroup')
526 526
527 527 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
528 528 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
529 529
530 530 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
531 531 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
532 532 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
533 533
534 534 group_member = relationship('UserGroupMember', cascade='all')
535 535
536 536 notifications = relationship('UserNotification', cascade='all')
537 537 # notifications assigned to this user
538 538 user_created_notifications = relationship('Notification', cascade='all')
539 539 # comments created by this user
540 540 user_comments = relationship('ChangesetComment', cascade='all')
541 541 # user profile extra info
542 542 user_emails = relationship('UserEmailMap', cascade='all')
543 543 user_ip_map = relationship('UserIpMap', cascade='all')
544 544 user_auth_tokens = relationship('UserApiKeys', cascade='all')
545 545 # gists
546 546 user_gists = relationship('Gist', cascade='all')
547 547 # user pull requests
548 548 user_pull_requests = relationship('PullRequest', cascade='all')
549 549 # external identities
550 550 extenal_identities = relationship(
551 551 'ExternalIdentity',
552 552 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
553 553 cascade='all')
554 554
555 555 def __unicode__(self):
556 556 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
557 557 self.user_id, self.username)
558 558
559 559 @hybrid_property
560 560 def email(self):
561 561 return self._email
562 562
563 563 @email.setter
564 564 def email(self, val):
565 565 self._email = val.lower() if val else None
566 566
567 567 @property
568 568 def firstname(self):
569 569 # alias for future
570 570 return self.name
571 571
572 572 @property
573 573 def emails(self):
574 574 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
575 575 return [self.email] + [x.email for x in other]
576 576
577 577 @property
578 578 def auth_tokens(self):
579 579 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
580 580
581 581 @property
582 582 def extra_auth_tokens(self):
583 583 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
584 584
585 585 @property
586 586 def feed_token(self):
587 587 feed_tokens = UserApiKeys.query()\
588 588 .filter(UserApiKeys.user == self)\
589 589 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
590 590 .all()
591 591 if feed_tokens:
592 592 return feed_tokens[0].api_key
593 593 else:
594 594 # use the main token so we don't end up with nothing...
595 595 return self.api_key
596 596
597 597 @classmethod
598 598 def extra_valid_auth_tokens(cls, user, role=None):
599 599 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
600 600 .filter(or_(UserApiKeys.expires == -1,
601 601 UserApiKeys.expires >= time.time()))
602 602 if role:
603 603 tokens = tokens.filter(or_(UserApiKeys.role == role,
604 604 UserApiKeys.role == UserApiKeys.ROLE_ALL))
605 605 return tokens.all()
606 606
607 607 @property
608 608 def ip_addresses(self):
609 609 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
610 610 return [x.ip_addr for x in ret]
611 611
612 612 @property
613 613 def username_and_name(self):
614 614 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
615 615
616 616 @property
617 617 def username_or_name_or_email(self):
618 618 full_name = self.full_name if self.full_name is not ' ' else None
619 619 return self.username or full_name or self.email
620 620
621 621 @property
622 622 def full_name(self):
623 623 return '%s %s' % (self.firstname, self.lastname)
624 624
625 625 @property
626 626 def full_name_or_username(self):
627 627 return ('%s %s' % (self.firstname, self.lastname)
628 628 if (self.firstname and self.lastname) else self.username)
629 629
630 630 @property
631 631 def full_contact(self):
632 632 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
633 633
634 634 @property
635 635 def short_contact(self):
636 636 return '%s %s' % (self.firstname, self.lastname)
637 637
638 638 @property
639 639 def is_admin(self):
640 640 return self.admin
641 641
642 642 @property
643 643 def AuthUser(self):
644 644 """
645 645 Returns instance of AuthUser for this user
646 646 """
647 647 from rhodecode.lib.auth import AuthUser
648 648 return AuthUser(user_id=self.user_id, api_key=self.api_key,
649 649 username=self.username)
650 650
651 651 @hybrid_property
652 652 def user_data(self):
653 653 if not self._user_data:
654 654 return {}
655 655
656 656 try:
657 657 return json.loads(self._user_data)
658 658 except TypeError:
659 659 return {}
660 660
661 661 @user_data.setter
662 662 def user_data(self, val):
663 663 if not isinstance(val, dict):
664 664 raise Exception('user_data must be dict, got %s' % type(val))
665 665 try:
666 666 self._user_data = json.dumps(val)
667 667 except Exception:
668 668 log.error(traceback.format_exc())
669 669
670 670 @classmethod
671 671 def get_by_username(cls, username, case_insensitive=False,
672 672 cache=False, identity_cache=False):
673 673 session = Session()
674 674
675 675 if case_insensitive:
676 676 q = cls.query().filter(
677 677 func.lower(cls.username) == func.lower(username))
678 678 else:
679 679 q = cls.query().filter(cls.username == username)
680 680
681 681 if cache:
682 682 if identity_cache:
683 683 val = cls.identity_cache(session, 'username', username)
684 684 if val:
685 685 return val
686 686 else:
687 687 q = q.options(
688 688 FromCache("sql_cache_short",
689 689 "get_user_by_name_%s" % _hash_key(username)))
690 690
691 691 return q.scalar()
692 692
693 693 @classmethod
694 694 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
695 695 q = cls.query().filter(cls.api_key == auth_token)
696 696
697 697 if cache:
698 698 q = q.options(FromCache("sql_cache_short",
699 699 "get_auth_token_%s" % auth_token))
700 700 res = q.scalar()
701 701
702 702 if fallback and not res:
703 703 #fallback to additional keys
704 704 _res = UserApiKeys.query()\
705 705 .filter(UserApiKeys.api_key == auth_token)\
706 706 .filter(or_(UserApiKeys.expires == -1,
707 707 UserApiKeys.expires >= time.time()))\
708 708 .first()
709 709 if _res:
710 710 res = _res.user
711 711 return res
712 712
713 713 @classmethod
714 714 def get_by_email(cls, email, case_insensitive=False, cache=False):
715 715
716 716 if case_insensitive:
717 717 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
718 718
719 719 else:
720 720 q = cls.query().filter(cls.email == email)
721 721
722 722 if cache:
723 723 q = q.options(FromCache("sql_cache_short",
724 724 "get_email_key_%s" % _hash_key(email)))
725 725
726 726 ret = q.scalar()
727 727 if ret is None:
728 728 q = UserEmailMap.query()
729 729 # try fetching in alternate email map
730 730 if case_insensitive:
731 731 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
732 732 else:
733 733 q = q.filter(UserEmailMap.email == email)
734 734 q = q.options(joinedload(UserEmailMap.user))
735 735 if cache:
736 736 q = q.options(FromCache("sql_cache_short",
737 737 "get_email_map_key_%s" % email))
738 738 ret = getattr(q.scalar(), 'user', None)
739 739
740 740 return ret
741 741
742 742 @classmethod
743 743 def get_from_cs_author(cls, author):
744 744 """
745 745 Tries to get User objects out of commit author string
746 746
747 747 :param author:
748 748 """
749 749 from rhodecode.lib.helpers import email, author_name
750 750 # Valid email in the attribute passed, see if they're in the system
751 751 _email = email(author)
752 752 if _email:
753 753 user = cls.get_by_email(_email, case_insensitive=True)
754 754 if user:
755 755 return user
756 756 # Maybe we can match by username?
757 757 _author = author_name(author)
758 758 user = cls.get_by_username(_author, case_insensitive=True)
759 759 if user:
760 760 return user
761 761
762 762 def update_userdata(self, **kwargs):
763 763 usr = self
764 764 old = usr.user_data
765 765 old.update(**kwargs)
766 766 usr.user_data = old
767 767 Session().add(usr)
768 768 log.debug('updated userdata with ', kwargs)
769 769
770 770 def update_lastlogin(self):
771 771 """Update user lastlogin"""
772 772 self.last_login = datetime.datetime.now()
773 773 Session().add(self)
774 774 log.debug('updated user %s lastlogin', self.username)
775 775
776 776 def update_lastactivity(self):
777 777 """Update user lastactivity"""
778 778 usr = self
779 779 old = usr.user_data
780 780 old.update({'last_activity': time.time()})
781 781 usr.user_data = old
782 782 Session().add(usr)
783 783 log.debug('updated user %s lastactivity', usr.username)
784 784
785 785 def update_password(self, new_password, change_api_key=False):
786 786 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
787 787
788 788 self.password = get_crypt_password(new_password)
789 789 if change_api_key:
790 790 self.api_key = generate_auth_token(self.username)
791 791 Session().add(self)
792 792
793 793 @classmethod
794 794 def get_first_super_admin(cls):
795 795 user = User.query().filter(User.admin == true()).first()
796 796 if user is None:
797 797 raise Exception('FATAL: Missing administrative account!')
798 798 return user
799 799
800 800 @classmethod
801 801 def get_all_super_admins(cls):
802 802 """
803 803 Returns all admin accounts sorted by username
804 804 """
805 805 return User.query().filter(User.admin == true())\
806 806 .order_by(User.username.asc()).all()
807 807
808 808 @classmethod
809 809 def get_default_user(cls, cache=False):
810 810 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
811 811 if user is None:
812 812 raise Exception('FATAL: Missing default account!')
813 813 return user
814 814
815 815 def _get_default_perms(self, user, suffix=''):
816 816 from rhodecode.model.permission import PermissionModel
817 817 return PermissionModel().get_default_perms(user.user_perms, suffix)
818 818
819 819 def get_default_perms(self, suffix=''):
820 820 return self._get_default_perms(self, suffix)
821 821
822 822 def get_api_data(self, include_secrets=False, details='full'):
823 823 """
824 824 Common function for generating user related data for API
825 825
826 826 :param include_secrets: By default secrets in the API data will be replaced
827 827 by a placeholder value to prevent exposing this data by accident. In case
828 828 this data shall be exposed, set this flag to ``True``.
829 829
830 830 :param details: details can be 'basic|full' basic gives only a subset of
831 831 the available user information that includes user_id, name and emails.
832 832 """
833 833 user = self
834 834 user_data = self.user_data
835 835 data = {
836 836 'user_id': user.user_id,
837 837 'username': user.username,
838 838 'firstname': user.name,
839 839 'lastname': user.lastname,
840 840 'email': user.email,
841 841 'emails': user.emails,
842 842 }
843 843 if details == 'basic':
844 844 return data
845 845
846 846 api_key_length = 40
847 847 api_key_replacement = '*' * api_key_length
848 848
849 849 extras = {
850 850 'api_key': api_key_replacement,
851 851 'api_keys': [api_key_replacement],
852 852 'active': user.active,
853 853 'admin': user.admin,
854 854 'extern_type': user.extern_type,
855 855 'extern_name': user.extern_name,
856 856 'last_login': user.last_login,
857 857 'ip_addresses': user.ip_addresses,
858 858 'language': user_data.get('language')
859 859 }
860 860 data.update(extras)
861 861
862 862 if include_secrets:
863 863 data['api_key'] = user.api_key
864 864 data['api_keys'] = user.auth_tokens
865 865 return data
866 866
867 867 def __json__(self):
868 868 data = {
869 869 'full_name': self.full_name,
870 870 'full_name_or_username': self.full_name_or_username,
871 871 'short_contact': self.short_contact,
872 872 'full_contact': self.full_contact,
873 873 }
874 874 data.update(self.get_api_data())
875 875 return data
876 876
877 877
878 878 class UserApiKeys(Base, BaseModel):
879 879 __tablename__ = 'user_api_keys'
880 880 __table_args__ = (
881 881 Index('uak_api_key_idx', 'api_key'),
882 882 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
883 883 UniqueConstraint('api_key'),
884 884 {'extend_existing': True, 'mysql_engine': 'InnoDB',
885 885 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
886 886 )
887 887 __mapper_args__ = {}
888 888
889 889 # ApiKey role
890 890 ROLE_ALL = 'token_role_all'
891 891 ROLE_HTTP = 'token_role_http'
892 892 ROLE_VCS = 'token_role_vcs'
893 893 ROLE_API = 'token_role_api'
894 894 ROLE_FEED = 'token_role_feed'
895 895 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
896 896
897 897 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
898 898 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
899 899 api_key = Column("api_key", String(255), nullable=False, unique=True)
900 900 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
901 901 expires = Column('expires', Float(53), nullable=False)
902 902 role = Column('role', String(255), nullable=True)
903 903 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
904 904
905 905 user = relationship('User', lazy='joined')
906 906
907 907 @classmethod
908 908 def _get_role_name(cls, role):
909 909 return {
910 910 cls.ROLE_ALL: _('all'),
911 911 cls.ROLE_HTTP: _('http/web interface'),
912 912 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
913 913 cls.ROLE_API: _('api calls'),
914 914 cls.ROLE_FEED: _('feed access'),
915 915 }.get(role, role)
916 916
917 917 @property
918 918 def expired(self):
919 919 if self.expires == -1:
920 920 return False
921 921 return time.time() > self.expires
922 922
923 923 @property
924 924 def role_humanized(self):
925 925 return self._get_role_name(self.role)
926 926
927 927
928 928 class UserEmailMap(Base, BaseModel):
929 929 __tablename__ = 'user_email_map'
930 930 __table_args__ = (
931 931 Index('uem_email_idx', 'email'),
932 932 UniqueConstraint('email'),
933 933 {'extend_existing': True, 'mysql_engine': 'InnoDB',
934 934 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
935 935 )
936 936 __mapper_args__ = {}
937 937
938 938 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
939 939 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
940 940 _email = Column("email", String(255), nullable=True, unique=False, default=None)
941 941 user = relationship('User', lazy='joined')
942 942
943 943 @validates('_email')
944 944 def validate_email(self, key, email):
945 945 # check if this email is not main one
946 946 main_email = Session().query(User).filter(User.email == email).scalar()
947 947 if main_email is not None:
948 948 raise AttributeError('email %s is present is user table' % email)
949 949 return email
950 950
951 951 @hybrid_property
952 952 def email(self):
953 953 return self._email
954 954
955 955 @email.setter
956 956 def email(self, val):
957 957 self._email = val.lower() if val else None
958 958
959 959
960 960 class UserIpMap(Base, BaseModel):
961 961 __tablename__ = 'user_ip_map'
962 962 __table_args__ = (
963 963 UniqueConstraint('user_id', 'ip_addr'),
964 964 {'extend_existing': True, 'mysql_engine': 'InnoDB',
965 965 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
966 966 )
967 967 __mapper_args__ = {}
968 968
969 969 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
970 970 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
971 971 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
972 972 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
973 973 description = Column("description", String(10000), nullable=True, unique=None, default=None)
974 974 user = relationship('User', lazy='joined')
975 975
976 976 @classmethod
977 977 def _get_ip_range(cls, ip_addr):
978 978 net = ipaddress.ip_network(ip_addr, strict=False)
979 979 return [str(net.network_address), str(net.broadcast_address)]
980 980
981 981 def __json__(self):
982 982 return {
983 983 'ip_addr': self.ip_addr,
984 984 'ip_range': self._get_ip_range(self.ip_addr),
985 985 }
986 986
987 987 def __unicode__(self):
988 988 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
989 989 self.user_id, self.ip_addr)
990 990
991 991 class UserLog(Base, BaseModel):
992 992 __tablename__ = 'user_logs'
993 993 __table_args__ = (
994 994 {'extend_existing': True, 'mysql_engine': 'InnoDB',
995 995 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
996 996 )
997 997 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
998 998 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
999 999 username = Column("username", String(255), nullable=True, unique=None, default=None)
1000 1000 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1001 1001 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1002 1002 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1003 1003 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1004 1004 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1005 1005
1006 1006 def __unicode__(self):
1007 1007 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1008 1008 self.repository_name,
1009 1009 self.action)
1010 1010
1011 1011 @property
1012 1012 def action_as_day(self):
1013 1013 return datetime.date(*self.action_date.timetuple()[:3])
1014 1014
1015 1015 user = relationship('User')
1016 1016 repository = relationship('Repository', cascade='')
1017 1017
1018 1018
1019 1019 class UserGroup(Base, BaseModel):
1020 1020 __tablename__ = 'users_groups'
1021 1021 __table_args__ = (
1022 1022 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1023 1023 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1024 1024 )
1025 1025
1026 1026 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1027 1027 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1028 1028 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1029 1029 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1030 1030 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1031 1031 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1032 1032 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1033 1033 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1034 1034
1035 1035 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1036 1036 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1037 1037 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1038 1038 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1039 1039 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1040 1040 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1041 1041
1042 1042 user = relationship('User')
1043 1043
1044 1044 @hybrid_property
1045 1045 def group_data(self):
1046 1046 if not self._group_data:
1047 1047 return {}
1048 1048
1049 1049 try:
1050 1050 return json.loads(self._group_data)
1051 1051 except TypeError:
1052 1052 return {}
1053 1053
1054 1054 @group_data.setter
1055 1055 def group_data(self, val):
1056 1056 try:
1057 1057 self._group_data = json.dumps(val)
1058 1058 except Exception:
1059 1059 log.error(traceback.format_exc())
1060 1060
1061 1061 def __unicode__(self):
1062 1062 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1063 1063 self.users_group_id,
1064 1064 self.users_group_name)
1065 1065
1066 1066 @classmethod
1067 1067 def get_by_group_name(cls, group_name, cache=False,
1068 1068 case_insensitive=False):
1069 1069 if case_insensitive:
1070 1070 q = cls.query().filter(func.lower(cls.users_group_name) ==
1071 1071 func.lower(group_name))
1072 1072
1073 1073 else:
1074 1074 q = cls.query().filter(cls.users_group_name == group_name)
1075 1075 if cache:
1076 1076 q = q.options(FromCache(
1077 1077 "sql_cache_short",
1078 1078 "get_group_%s" % _hash_key(group_name)))
1079 1079 return q.scalar()
1080 1080
1081 1081 @classmethod
1082 1082 def get(cls, user_group_id, cache=False):
1083 1083 user_group = cls.query()
1084 1084 if cache:
1085 1085 user_group = user_group.options(FromCache("sql_cache_short",
1086 1086 "get_users_group_%s" % user_group_id))
1087 1087 return user_group.get(user_group_id)
1088 1088
1089 1089 def permissions(self, with_admins=True, with_owner=True):
1090 1090 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1091 1091 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1092 1092 joinedload(UserUserGroupToPerm.user),
1093 1093 joinedload(UserUserGroupToPerm.permission),)
1094 1094
1095 1095 # get owners and admins and permissions. We do a trick of re-writing
1096 1096 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1097 1097 # has a global reference and changing one object propagates to all
1098 1098 # others. This means if admin is also an owner admin_row that change
1099 1099 # would propagate to both objects
1100 1100 perm_rows = []
1101 1101 for _usr in q.all():
1102 1102 usr = AttributeDict(_usr.user.get_dict())
1103 1103 usr.permission = _usr.permission.permission_name
1104 1104 perm_rows.append(usr)
1105 1105
1106 1106 # filter the perm rows by 'default' first and then sort them by
1107 1107 # admin,write,read,none permissions sorted again alphabetically in
1108 1108 # each group
1109 1109 perm_rows = sorted(perm_rows, key=display_sort)
1110 1110
1111 1111 _admin_perm = 'usergroup.admin'
1112 1112 owner_row = []
1113 1113 if with_owner:
1114 1114 usr = AttributeDict(self.user.get_dict())
1115 1115 usr.owner_row = True
1116 1116 usr.permission = _admin_perm
1117 1117 owner_row.append(usr)
1118 1118
1119 1119 super_admin_rows = []
1120 1120 if with_admins:
1121 1121 for usr in User.get_all_super_admins():
1122 1122 # if this admin is also owner, don't double the record
1123 1123 if usr.user_id == owner_row[0].user_id:
1124 1124 owner_row[0].admin_row = True
1125 1125 else:
1126 1126 usr = AttributeDict(usr.get_dict())
1127 1127 usr.admin_row = True
1128 1128 usr.permission = _admin_perm
1129 1129 super_admin_rows.append(usr)
1130 1130
1131 1131 return super_admin_rows + owner_row + perm_rows
1132 1132
1133 1133 def permission_user_groups(self):
1134 1134 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1135 1135 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1136 1136 joinedload(UserGroupUserGroupToPerm.target_user_group),
1137 1137 joinedload(UserGroupUserGroupToPerm.permission),)
1138 1138
1139 1139 perm_rows = []
1140 1140 for _user_group in q.all():
1141 1141 usr = AttributeDict(_user_group.user_group.get_dict())
1142 1142 usr.permission = _user_group.permission.permission_name
1143 1143 perm_rows.append(usr)
1144 1144
1145 1145 return perm_rows
1146 1146
1147 1147 def _get_default_perms(self, user_group, suffix=''):
1148 1148 from rhodecode.model.permission import PermissionModel
1149 1149 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1150 1150
1151 1151 def get_default_perms(self, suffix=''):
1152 1152 return self._get_default_perms(self, suffix)
1153 1153
1154 1154 def get_api_data(self, with_group_members=True, include_secrets=False):
1155 1155 """
1156 1156 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1157 1157 basically forwarded.
1158 1158
1159 1159 """
1160 1160 user_group = self
1161 1161
1162 1162 data = {
1163 1163 'users_group_id': user_group.users_group_id,
1164 1164 'group_name': user_group.users_group_name,
1165 1165 'group_description': user_group.user_group_description,
1166 1166 'active': user_group.users_group_active,
1167 1167 'owner': user_group.user.username,
1168 1168 }
1169 1169 if with_group_members:
1170 1170 users = []
1171 1171 for user in user_group.members:
1172 1172 user = user.user
1173 1173 users.append(user.get_api_data(include_secrets=include_secrets))
1174 1174 data['users'] = users
1175 1175
1176 1176 return data
1177 1177
1178 1178
1179 1179 class UserGroupMember(Base, BaseModel):
1180 1180 __tablename__ = 'users_groups_members'
1181 1181 __table_args__ = (
1182 1182 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1183 1183 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1184 1184 )
1185 1185
1186 1186 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1187 1187 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1188 1188 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1189 1189
1190 1190 user = relationship('User', lazy='joined')
1191 1191 users_group = relationship('UserGroup')
1192 1192
1193 1193 def __init__(self, gr_id='', u_id=''):
1194 1194 self.users_group_id = gr_id
1195 1195 self.user_id = u_id
1196 1196
1197 1197
1198 1198 class RepositoryField(Base, BaseModel):
1199 1199 __tablename__ = 'repositories_fields'
1200 1200 __table_args__ = (
1201 1201 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1202 1202 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1203 1203 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1204 1204 )
1205 1205 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1206 1206
1207 1207 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1208 1208 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1209 1209 field_key = Column("field_key", String(250))
1210 1210 field_label = Column("field_label", String(1024), nullable=False)
1211 1211 field_value = Column("field_value", String(10000), nullable=False)
1212 1212 field_desc = Column("field_desc", String(1024), nullable=False)
1213 1213 field_type = Column("field_type", String(255), nullable=False, unique=None)
1214 1214 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1215 1215
1216 1216 repository = relationship('Repository')
1217 1217
1218 1218 @property
1219 1219 def field_key_prefixed(self):
1220 1220 return 'ex_%s' % self.field_key
1221 1221
1222 1222 @classmethod
1223 1223 def un_prefix_key(cls, key):
1224 1224 if key.startswith(cls.PREFIX):
1225 1225 return key[len(cls.PREFIX):]
1226 1226 return key
1227 1227
1228 1228 @classmethod
1229 1229 def get_by_key_name(cls, key, repo):
1230 1230 row = cls.query()\
1231 1231 .filter(cls.repository == repo)\
1232 1232 .filter(cls.field_key == key).scalar()
1233 1233 return row
1234 1234
1235 1235
1236 1236 class Repository(Base, BaseModel):
1237 1237 __tablename__ = 'repositories'
1238 1238 __table_args__ = (
1239 1239 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1240 1240 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1241 1241 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1242 1242 )
1243 1243 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1244 1244 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1245 1245
1246 1246 STATE_CREATED = 'repo_state_created'
1247 1247 STATE_PENDING = 'repo_state_pending'
1248 1248 STATE_ERROR = 'repo_state_error'
1249 1249
1250 1250 LOCK_AUTOMATIC = 'lock_auto'
1251 1251 LOCK_API = 'lock_api'
1252 1252 LOCK_WEB = 'lock_web'
1253 1253 LOCK_PULL = 'lock_pull'
1254 1254
1255 1255 NAME_SEP = URL_SEP
1256 1256
1257 1257 repo_id = Column(
1258 1258 "repo_id", Integer(), nullable=False, unique=True, default=None,
1259 1259 primary_key=True)
1260 1260 _repo_name = Column(
1261 1261 "repo_name", Text(), nullable=False, default=None)
1262 1262 _repo_name_hash = Column(
1263 1263 "repo_name_hash", String(255), nullable=False, unique=True)
1264 1264 repo_state = Column("repo_state", String(255), nullable=True)
1265 1265
1266 1266 clone_uri = Column(
1267 1267 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1268 1268 default=None)
1269 1269 repo_type = Column(
1270 1270 "repo_type", String(255), nullable=False, unique=False, default=None)
1271 1271 user_id = Column(
1272 1272 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1273 1273 unique=False, default=None)
1274 1274 private = Column(
1275 1275 "private", Boolean(), nullable=True, unique=None, default=None)
1276 1276 enable_statistics = Column(
1277 1277 "statistics", Boolean(), nullable=True, unique=None, default=True)
1278 1278 enable_downloads = Column(
1279 1279 "downloads", Boolean(), nullable=True, unique=None, default=True)
1280 1280 description = Column(
1281 1281 "description", String(10000), nullable=True, unique=None, default=None)
1282 1282 created_on = Column(
1283 1283 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1284 1284 default=datetime.datetime.now)
1285 1285 updated_on = Column(
1286 1286 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1287 1287 default=datetime.datetime.now)
1288 1288 _landing_revision = Column(
1289 1289 "landing_revision", String(255), nullable=False, unique=False,
1290 1290 default=None)
1291 1291 enable_locking = Column(
1292 1292 "enable_locking", Boolean(), nullable=False, unique=None,
1293 1293 default=False)
1294 1294 _locked = Column(
1295 1295 "locked", String(255), nullable=True, unique=False, default=None)
1296 1296 _changeset_cache = Column(
1297 1297 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1298 1298
1299 1299 fork_id = Column(
1300 1300 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1301 1301 nullable=True, unique=False, default=None)
1302 1302 group_id = Column(
1303 1303 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1304 1304 unique=False, default=None)
1305 1305
1306 1306 user = relationship('User', lazy='joined')
1307 1307 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1308 1308 group = relationship('RepoGroup', lazy='joined')
1309 1309 repo_to_perm = relationship(
1310 1310 'UserRepoToPerm', cascade='all',
1311 1311 order_by='UserRepoToPerm.repo_to_perm_id')
1312 1312 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1313 1313 stats = relationship('Statistics', cascade='all', uselist=False)
1314 1314
1315 1315 followers = relationship(
1316 1316 'UserFollowing',
1317 1317 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1318 1318 cascade='all')
1319 1319 extra_fields = relationship(
1320 1320 'RepositoryField', cascade="all, delete, delete-orphan")
1321 1321 logs = relationship('UserLog')
1322 1322 comments = relationship(
1323 1323 'ChangesetComment', cascade="all, delete, delete-orphan")
1324 1324 pull_requests_source = relationship(
1325 1325 'PullRequest',
1326 1326 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1327 1327 cascade="all, delete, delete-orphan")
1328 1328 pull_requests_target = relationship(
1329 1329 'PullRequest',
1330 1330 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1331 1331 cascade="all, delete, delete-orphan")
1332 1332 ui = relationship('RepoRhodeCodeUi', cascade="all")
1333 1333 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1334 1334 integrations = relationship('Integration',
1335 1335 cascade="all, delete, delete-orphan")
1336 1336
1337 1337 def __unicode__(self):
1338 1338 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1339 1339 safe_unicode(self.repo_name))
1340 1340
1341 1341 @hybrid_property
1342 1342 def landing_rev(self):
1343 1343 # always should return [rev_type, rev]
1344 1344 if self._landing_revision:
1345 1345 _rev_info = self._landing_revision.split(':')
1346 1346 if len(_rev_info) < 2:
1347 1347 _rev_info.insert(0, 'rev')
1348 1348 return [_rev_info[0], _rev_info[1]]
1349 1349 return [None, None]
1350 1350
1351 1351 @landing_rev.setter
1352 1352 def landing_rev(self, val):
1353 1353 if ':' not in val:
1354 1354 raise ValueError('value must be delimited with `:` and consist '
1355 1355 'of <rev_type>:<rev>, got %s instead' % val)
1356 1356 self._landing_revision = val
1357 1357
1358 1358 @hybrid_property
1359 1359 def locked(self):
1360 1360 if self._locked:
1361 1361 user_id, timelocked, reason = self._locked.split(':')
1362 1362 lock_values = int(user_id), timelocked, reason
1363 1363 else:
1364 1364 lock_values = [None, None, None]
1365 1365 return lock_values
1366 1366
1367 1367 @locked.setter
1368 1368 def locked(self, val):
1369 1369 if val and isinstance(val, (list, tuple)):
1370 1370 self._locked = ':'.join(map(str, val))
1371 1371 else:
1372 1372 self._locked = None
1373 1373
1374 1374 @hybrid_property
1375 1375 def changeset_cache(self):
1376 1376 from rhodecode.lib.vcs.backends.base import EmptyCommit
1377 1377 dummy = EmptyCommit().__json__()
1378 1378 if not self._changeset_cache:
1379 1379 return dummy
1380 1380 try:
1381 1381 return json.loads(self._changeset_cache)
1382 1382 except TypeError:
1383 1383 return dummy
1384 1384 except Exception:
1385 1385 log.error(traceback.format_exc())
1386 1386 return dummy
1387 1387
1388 1388 @changeset_cache.setter
1389 1389 def changeset_cache(self, val):
1390 1390 try:
1391 1391 self._changeset_cache = json.dumps(val)
1392 1392 except Exception:
1393 1393 log.error(traceback.format_exc())
1394 1394
1395 1395 @hybrid_property
1396 1396 def repo_name(self):
1397 1397 return self._repo_name
1398 1398
1399 1399 @repo_name.setter
1400 1400 def repo_name(self, value):
1401 1401 self._repo_name = value
1402 1402 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1403 1403
1404 1404 @classmethod
1405 1405 def normalize_repo_name(cls, repo_name):
1406 1406 """
1407 1407 Normalizes os specific repo_name to the format internally stored inside
1408 1408 database using URL_SEP
1409 1409
1410 1410 :param cls:
1411 1411 :param repo_name:
1412 1412 """
1413 1413 return cls.NAME_SEP.join(repo_name.split(os.sep))
1414 1414
1415 1415 @classmethod
1416 1416 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1417 1417 session = Session()
1418 1418 q = session.query(cls).filter(cls.repo_name == repo_name)
1419 1419
1420 1420 if cache:
1421 1421 if identity_cache:
1422 1422 val = cls.identity_cache(session, 'repo_name', repo_name)
1423 1423 if val:
1424 1424 return val
1425 1425 else:
1426 1426 q = q.options(
1427 1427 FromCache("sql_cache_short",
1428 1428 "get_repo_by_name_%s" % _hash_key(repo_name)))
1429 1429
1430 1430 return q.scalar()
1431 1431
1432 1432 @classmethod
1433 1433 def get_by_full_path(cls, repo_full_path):
1434 1434 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1435 1435 repo_name = cls.normalize_repo_name(repo_name)
1436 1436 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1437 1437
1438 1438 @classmethod
1439 1439 def get_repo_forks(cls, repo_id):
1440 1440 return cls.query().filter(Repository.fork_id == repo_id)
1441 1441
1442 1442 @classmethod
1443 1443 def base_path(cls):
1444 1444 """
1445 1445 Returns base path when all repos are stored
1446 1446
1447 1447 :param cls:
1448 1448 """
1449 1449 q = Session().query(RhodeCodeUi)\
1450 1450 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1451 1451 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1452 1452 return q.one().ui_value
1453 1453
1454 1454 @classmethod
1455 1455 def is_valid(cls, repo_name):
1456 1456 """
1457 1457 returns True if given repo name is a valid filesystem repository
1458 1458
1459 1459 :param cls:
1460 1460 :param repo_name:
1461 1461 """
1462 1462 from rhodecode.lib.utils import is_valid_repo
1463 1463
1464 1464 return is_valid_repo(repo_name, cls.base_path())
1465 1465
1466 1466 @classmethod
1467 1467 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1468 1468 case_insensitive=True):
1469 1469 q = Repository.query()
1470 1470
1471 1471 if not isinstance(user_id, Optional):
1472 1472 q = q.filter(Repository.user_id == user_id)
1473 1473
1474 1474 if not isinstance(group_id, Optional):
1475 1475 q = q.filter(Repository.group_id == group_id)
1476 1476
1477 1477 if case_insensitive:
1478 1478 q = q.order_by(func.lower(Repository.repo_name))
1479 1479 else:
1480 1480 q = q.order_by(Repository.repo_name)
1481 1481 return q.all()
1482 1482
1483 1483 @property
1484 1484 def forks(self):
1485 1485 """
1486 1486 Return forks of this repo
1487 1487 """
1488 1488 return Repository.get_repo_forks(self.repo_id)
1489 1489
1490 1490 @property
1491 1491 def parent(self):
1492 1492 """
1493 1493 Returns fork parent
1494 1494 """
1495 1495 return self.fork
1496 1496
1497 1497 @property
1498 1498 def just_name(self):
1499 1499 return self.repo_name.split(self.NAME_SEP)[-1]
1500 1500
1501 1501 @property
1502 1502 def groups_with_parents(self):
1503 1503 groups = []
1504 1504 if self.group is None:
1505 1505 return groups
1506 1506
1507 1507 cur_gr = self.group
1508 1508 groups.insert(0, cur_gr)
1509 1509 while 1:
1510 1510 gr = getattr(cur_gr, 'parent_group', None)
1511 1511 cur_gr = cur_gr.parent_group
1512 1512 if gr is None:
1513 1513 break
1514 1514 groups.insert(0, gr)
1515 1515
1516 1516 return groups
1517 1517
1518 1518 @property
1519 1519 def groups_and_repo(self):
1520 1520 return self.groups_with_parents, self
1521 1521
1522 1522 @LazyProperty
1523 1523 def repo_path(self):
1524 1524 """
1525 1525 Returns base full path for that repository means where it actually
1526 1526 exists on a filesystem
1527 1527 """
1528 1528 q = Session().query(RhodeCodeUi).filter(
1529 1529 RhodeCodeUi.ui_key == self.NAME_SEP)
1530 1530 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1531 1531 return q.one().ui_value
1532 1532
1533 1533 @property
1534 1534 def repo_full_path(self):
1535 1535 p = [self.repo_path]
1536 1536 # we need to split the name by / since this is how we store the
1537 1537 # names in the database, but that eventually needs to be converted
1538 1538 # into a valid system path
1539 1539 p += self.repo_name.split(self.NAME_SEP)
1540 1540 return os.path.join(*map(safe_unicode, p))
1541 1541
1542 1542 @property
1543 1543 def cache_keys(self):
1544 1544 """
1545 1545 Returns associated cache keys for that repo
1546 1546 """
1547 1547 return CacheKey.query()\
1548 1548 .filter(CacheKey.cache_args == self.repo_name)\
1549 1549 .order_by(CacheKey.cache_key)\
1550 1550 .all()
1551 1551
1552 1552 def get_new_name(self, repo_name):
1553 1553 """
1554 1554 returns new full repository name based on assigned group and new new
1555 1555
1556 1556 :param group_name:
1557 1557 """
1558 1558 path_prefix = self.group.full_path_splitted if self.group else []
1559 1559 return self.NAME_SEP.join(path_prefix + [repo_name])
1560 1560
1561 1561 @property
1562 1562 def _config(self):
1563 1563 """
1564 1564 Returns db based config object.
1565 1565 """
1566 1566 from rhodecode.lib.utils import make_db_config
1567 1567 return make_db_config(clear_session=False, repo=self)
1568 1568
1569 1569 def permissions(self, with_admins=True, with_owner=True):
1570 1570 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1571 1571 q = q.options(joinedload(UserRepoToPerm.repository),
1572 1572 joinedload(UserRepoToPerm.user),
1573 1573 joinedload(UserRepoToPerm.permission),)
1574 1574
1575 1575 # get owners and admins and permissions. We do a trick of re-writing
1576 1576 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1577 1577 # has a global reference and changing one object propagates to all
1578 1578 # others. This means if admin is also an owner admin_row that change
1579 1579 # would propagate to both objects
1580 1580 perm_rows = []
1581 1581 for _usr in q.all():
1582 1582 usr = AttributeDict(_usr.user.get_dict())
1583 1583 usr.permission = _usr.permission.permission_name
1584 1584 perm_rows.append(usr)
1585 1585
1586 1586 # filter the perm rows by 'default' first and then sort them by
1587 1587 # admin,write,read,none permissions sorted again alphabetically in
1588 1588 # each group
1589 1589 perm_rows = sorted(perm_rows, key=display_sort)
1590 1590
1591 1591 _admin_perm = 'repository.admin'
1592 1592 owner_row = []
1593 1593 if with_owner:
1594 1594 usr = AttributeDict(self.user.get_dict())
1595 1595 usr.owner_row = True
1596 1596 usr.permission = _admin_perm
1597 1597 owner_row.append(usr)
1598 1598
1599 1599 super_admin_rows = []
1600 1600 if with_admins:
1601 1601 for usr in User.get_all_super_admins():
1602 1602 # if this admin is also owner, don't double the record
1603 1603 if usr.user_id == owner_row[0].user_id:
1604 1604 owner_row[0].admin_row = True
1605 1605 else:
1606 1606 usr = AttributeDict(usr.get_dict())
1607 1607 usr.admin_row = True
1608 1608 usr.permission = _admin_perm
1609 1609 super_admin_rows.append(usr)
1610 1610
1611 1611 return super_admin_rows + owner_row + perm_rows
1612 1612
1613 1613 def permission_user_groups(self):
1614 1614 q = UserGroupRepoToPerm.query().filter(
1615 1615 UserGroupRepoToPerm.repository == self)
1616 1616 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1617 1617 joinedload(UserGroupRepoToPerm.users_group),
1618 1618 joinedload(UserGroupRepoToPerm.permission),)
1619 1619
1620 1620 perm_rows = []
1621 1621 for _user_group in q.all():
1622 1622 usr = AttributeDict(_user_group.users_group.get_dict())
1623 1623 usr.permission = _user_group.permission.permission_name
1624 1624 perm_rows.append(usr)
1625 1625
1626 1626 return perm_rows
1627 1627
1628 1628 def get_api_data(self, include_secrets=False):
1629 1629 """
1630 1630 Common function for generating repo api data
1631 1631
1632 1632 :param include_secrets: See :meth:`User.get_api_data`.
1633 1633
1634 1634 """
1635 1635 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1636 1636 # move this methods on models level.
1637 1637 from rhodecode.model.settings import SettingsModel
1638 1638
1639 1639 repo = self
1640 1640 _user_id, _time, _reason = self.locked
1641 1641
1642 1642 data = {
1643 1643 'repo_id': repo.repo_id,
1644 1644 'repo_name': repo.repo_name,
1645 1645 'repo_type': repo.repo_type,
1646 1646 'clone_uri': repo.clone_uri or '',
1647 1647 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1648 1648 'private': repo.private,
1649 1649 'created_on': repo.created_on,
1650 1650 'description': repo.description,
1651 1651 'landing_rev': repo.landing_rev,
1652 1652 'owner': repo.user.username,
1653 1653 'fork_of': repo.fork.repo_name if repo.fork else None,
1654 1654 'enable_statistics': repo.enable_statistics,
1655 1655 'enable_locking': repo.enable_locking,
1656 1656 'enable_downloads': repo.enable_downloads,
1657 1657 'last_changeset': repo.changeset_cache,
1658 1658 'locked_by': User.get(_user_id).get_api_data(
1659 1659 include_secrets=include_secrets) if _user_id else None,
1660 1660 'locked_date': time_to_datetime(_time) if _time else None,
1661 1661 'lock_reason': _reason if _reason else None,
1662 1662 }
1663 1663
1664 1664 # TODO: mikhail: should be per-repo settings here
1665 1665 rc_config = SettingsModel().get_all_settings()
1666 1666 repository_fields = str2bool(
1667 1667 rc_config.get('rhodecode_repository_fields'))
1668 1668 if repository_fields:
1669 1669 for f in self.extra_fields:
1670 1670 data[f.field_key_prefixed] = f.field_value
1671 1671
1672 1672 return data
1673 1673
1674 1674 @classmethod
1675 1675 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1676 1676 if not lock_time:
1677 1677 lock_time = time.time()
1678 1678 if not lock_reason:
1679 1679 lock_reason = cls.LOCK_AUTOMATIC
1680 1680 repo.locked = [user_id, lock_time, lock_reason]
1681 1681 Session().add(repo)
1682 1682 Session().commit()
1683 1683
1684 1684 @classmethod
1685 1685 def unlock(cls, repo):
1686 1686 repo.locked = None
1687 1687 Session().add(repo)
1688 1688 Session().commit()
1689 1689
1690 1690 @classmethod
1691 1691 def getlock(cls, repo):
1692 1692 return repo.locked
1693 1693
1694 1694 def is_user_lock(self, user_id):
1695 1695 if self.lock[0]:
1696 1696 lock_user_id = safe_int(self.lock[0])
1697 1697 user_id = safe_int(user_id)
1698 1698 # both are ints, and they are equal
1699 1699 return all([lock_user_id, user_id]) and lock_user_id == user_id
1700 1700
1701 1701 return False
1702 1702
1703 1703 def get_locking_state(self, action, user_id, only_when_enabled=True):
1704 1704 """
1705 1705 Checks locking on this repository, if locking is enabled and lock is
1706 1706 present returns a tuple of make_lock, locked, locked_by.
1707 1707 make_lock can have 3 states None (do nothing) True, make lock
1708 1708 False release lock, This value is later propagated to hooks, which
1709 1709 do the locking. Think about this as signals passed to hooks what to do.
1710 1710
1711 1711 """
1712 1712 # TODO: johbo: This is part of the business logic and should be moved
1713 1713 # into the RepositoryModel.
1714 1714
1715 1715 if action not in ('push', 'pull'):
1716 1716 raise ValueError("Invalid action value: %s" % repr(action))
1717 1717
1718 1718 # defines if locked error should be thrown to user
1719 1719 currently_locked = False
1720 1720 # defines if new lock should be made, tri-state
1721 1721 make_lock = None
1722 1722 repo = self
1723 1723 user = User.get(user_id)
1724 1724
1725 1725 lock_info = repo.locked
1726 1726
1727 1727 if repo and (repo.enable_locking or not only_when_enabled):
1728 1728 if action == 'push':
1729 1729 # check if it's already locked !, if it is compare users
1730 1730 locked_by_user_id = lock_info[0]
1731 1731 if user.user_id == locked_by_user_id:
1732 1732 log.debug(
1733 1733 'Got `push` action from user %s, now unlocking', user)
1734 1734 # unlock if we have push from user who locked
1735 1735 make_lock = False
1736 1736 else:
1737 1737 # we're not the same user who locked, ban with
1738 1738 # code defined in settings (default is 423 HTTP Locked) !
1739 1739 log.debug('Repo %s is currently locked by %s', repo, user)
1740 1740 currently_locked = True
1741 1741 elif action == 'pull':
1742 1742 # [0] user [1] date
1743 1743 if lock_info[0] and lock_info[1]:
1744 1744 log.debug('Repo %s is currently locked by %s', repo, user)
1745 1745 currently_locked = True
1746 1746 else:
1747 1747 log.debug('Setting lock on repo %s by %s', repo, user)
1748 1748 make_lock = True
1749 1749
1750 1750 else:
1751 1751 log.debug('Repository %s do not have locking enabled', repo)
1752 1752
1753 1753 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1754 1754 make_lock, currently_locked, lock_info)
1755 1755
1756 1756 from rhodecode.lib.auth import HasRepoPermissionAny
1757 1757 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1758 1758 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1759 1759 # if we don't have at least write permission we cannot make a lock
1760 1760 log.debug('lock state reset back to FALSE due to lack '
1761 1761 'of at least read permission')
1762 1762 make_lock = False
1763 1763
1764 1764 return make_lock, currently_locked, lock_info
1765 1765
1766 1766 @property
1767 1767 def last_db_change(self):
1768 1768 return self.updated_on
1769 1769
1770 1770 @property
1771 1771 def clone_uri_hidden(self):
1772 1772 clone_uri = self.clone_uri
1773 1773 if clone_uri:
1774 1774 import urlobject
1775 1775 url_obj = urlobject.URLObject(clone_uri)
1776 1776 if url_obj.password:
1777 1777 clone_uri = url_obj.with_password('*****')
1778 1778 return clone_uri
1779 1779
1780 1780 def clone_url(self, **override):
1781 1781 qualified_home_url = url('home', qualified=True)
1782 1782
1783 1783 uri_tmpl = None
1784 1784 if 'with_id' in override:
1785 1785 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1786 1786 del override['with_id']
1787 1787
1788 1788 if 'uri_tmpl' in override:
1789 1789 uri_tmpl = override['uri_tmpl']
1790 1790 del override['uri_tmpl']
1791 1791
1792 1792 # we didn't override our tmpl from **overrides
1793 1793 if not uri_tmpl:
1794 1794 uri_tmpl = self.DEFAULT_CLONE_URI
1795 1795 try:
1796 1796 from pylons import tmpl_context as c
1797 1797 uri_tmpl = c.clone_uri_tmpl
1798 1798 except Exception:
1799 1799 # in any case if we call this outside of request context,
1800 1800 # ie, not having tmpl_context set up
1801 1801 pass
1802 1802
1803 1803 return get_clone_url(uri_tmpl=uri_tmpl,
1804 1804 qualifed_home_url=qualified_home_url,
1805 1805 repo_name=self.repo_name,
1806 1806 repo_id=self.repo_id, **override)
1807 1807
1808 1808 def set_state(self, state):
1809 1809 self.repo_state = state
1810 1810 Session().add(self)
1811 1811 #==========================================================================
1812 1812 # SCM PROPERTIES
1813 1813 #==========================================================================
1814 1814
1815 1815 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1816 1816 return get_commit_safe(
1817 1817 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1818 1818
1819 1819 def get_changeset(self, rev=None, pre_load=None):
1820 1820 warnings.warn("Use get_commit", DeprecationWarning)
1821 1821 commit_id = None
1822 1822 commit_idx = None
1823 1823 if isinstance(rev, basestring):
1824 1824 commit_id = rev
1825 1825 else:
1826 1826 commit_idx = rev
1827 1827 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1828 1828 pre_load=pre_load)
1829 1829
1830 1830 def get_landing_commit(self):
1831 1831 """
1832 1832 Returns landing commit, or if that doesn't exist returns the tip
1833 1833 """
1834 1834 _rev_type, _rev = self.landing_rev
1835 1835 commit = self.get_commit(_rev)
1836 1836 if isinstance(commit, EmptyCommit):
1837 1837 return self.get_commit()
1838 1838 return commit
1839 1839
1840 1840 def update_commit_cache(self, cs_cache=None, config=None):
1841 1841 """
1842 1842 Update cache of last changeset for repository, keys should be::
1843 1843
1844 1844 short_id
1845 1845 raw_id
1846 1846 revision
1847 1847 parents
1848 1848 message
1849 1849 date
1850 1850 author
1851 1851
1852 1852 :param cs_cache:
1853 1853 """
1854 1854 from rhodecode.lib.vcs.backends.base import BaseChangeset
1855 1855 if cs_cache is None:
1856 1856 # use no-cache version here
1857 1857 scm_repo = self.scm_instance(cache=False, config=config)
1858 1858 if scm_repo:
1859 1859 cs_cache = scm_repo.get_commit(
1860 1860 pre_load=["author", "date", "message", "parents"])
1861 1861 else:
1862 1862 cs_cache = EmptyCommit()
1863 1863
1864 1864 if isinstance(cs_cache, BaseChangeset):
1865 1865 cs_cache = cs_cache.__json__()
1866 1866
1867 1867 def is_outdated(new_cs_cache):
1868 1868 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1869 1869 new_cs_cache['revision'] != self.changeset_cache['revision']):
1870 1870 return True
1871 1871 return False
1872 1872
1873 1873 # check if we have maybe already latest cached revision
1874 1874 if is_outdated(cs_cache) or not self.changeset_cache:
1875 1875 _default = datetime.datetime.fromtimestamp(0)
1876 1876 last_change = cs_cache.get('date') or _default
1877 1877 log.debug('updated repo %s with new cs cache %s',
1878 1878 self.repo_name, cs_cache)
1879 1879 self.updated_on = last_change
1880 1880 self.changeset_cache = cs_cache
1881 1881 Session().add(self)
1882 1882 Session().commit()
1883 1883 else:
1884 1884 log.debug('Skipping update_commit_cache for repo:`%s` '
1885 1885 'commit already with latest changes', self.repo_name)
1886 1886
1887 1887 @property
1888 1888 def tip(self):
1889 1889 return self.get_commit('tip')
1890 1890
1891 1891 @property
1892 1892 def author(self):
1893 1893 return self.tip.author
1894 1894
1895 1895 @property
1896 1896 def last_change(self):
1897 1897 return self.scm_instance().last_change
1898 1898
1899 1899 def get_comments(self, revisions=None):
1900 1900 """
1901 1901 Returns comments for this repository grouped by revisions
1902 1902
1903 1903 :param revisions: filter query by revisions only
1904 1904 """
1905 1905 cmts = ChangesetComment.query()\
1906 1906 .filter(ChangesetComment.repo == self)
1907 1907 if revisions:
1908 1908 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1909 1909 grouped = collections.defaultdict(list)
1910 1910 for cmt in cmts.all():
1911 1911 grouped[cmt.revision].append(cmt)
1912 1912 return grouped
1913 1913
1914 1914 def statuses(self, revisions=None):
1915 1915 """
1916 1916 Returns statuses for this repository
1917 1917
1918 1918 :param revisions: list of revisions to get statuses for
1919 1919 """
1920 1920 statuses = ChangesetStatus.query()\
1921 1921 .filter(ChangesetStatus.repo == self)\
1922 1922 .filter(ChangesetStatus.version == 0)
1923 1923
1924 1924 if revisions:
1925 1925 # Try doing the filtering in chunks to avoid hitting limits
1926 1926 size = 500
1927 1927 status_results = []
1928 1928 for chunk in xrange(0, len(revisions), size):
1929 1929 status_results += statuses.filter(
1930 1930 ChangesetStatus.revision.in_(
1931 1931 revisions[chunk: chunk+size])
1932 1932 ).all()
1933 1933 else:
1934 1934 status_results = statuses.all()
1935 1935
1936 1936 grouped = {}
1937 1937
1938 1938 # maybe we have open new pullrequest without a status?
1939 1939 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1940 1940 status_lbl = ChangesetStatus.get_status_lbl(stat)
1941 1941 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1942 1942 for rev in pr.revisions:
1943 1943 pr_id = pr.pull_request_id
1944 1944 pr_repo = pr.target_repo.repo_name
1945 1945 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1946 1946
1947 1947 for stat in status_results:
1948 1948 pr_id = pr_repo = None
1949 1949 if stat.pull_request:
1950 1950 pr_id = stat.pull_request.pull_request_id
1951 1951 pr_repo = stat.pull_request.target_repo.repo_name
1952 1952 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1953 1953 pr_id, pr_repo]
1954 1954 return grouped
1955 1955
1956 1956 # ==========================================================================
1957 1957 # SCM CACHE INSTANCE
1958 1958 # ==========================================================================
1959 1959
1960 1960 def scm_instance(self, **kwargs):
1961 1961 import rhodecode
1962 1962
1963 1963 # Passing a config will not hit the cache currently only used
1964 1964 # for repo2dbmapper
1965 1965 config = kwargs.pop('config', None)
1966 1966 cache = kwargs.pop('cache', None)
1967 1967 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1968 1968 # if cache is NOT defined use default global, else we have a full
1969 1969 # control over cache behaviour
1970 1970 if cache is None and full_cache and not config:
1971 1971 return self._get_instance_cached()
1972 1972 return self._get_instance(cache=bool(cache), config=config)
1973 1973
1974 1974 def _get_instance_cached(self):
1975 1975 @cache_region('long_term')
1976 1976 def _get_repo(cache_key):
1977 1977 return self._get_instance()
1978 1978
1979 1979 invalidator_context = CacheKey.repo_context_cache(
1980 1980 _get_repo, self.repo_name, None, thread_scoped=True)
1981 1981
1982 1982 with invalidator_context as context:
1983 1983 context.invalidate()
1984 1984 repo = context.compute()
1985 1985
1986 1986 return repo
1987 1987
1988 1988 def _get_instance(self, cache=True, config=None):
1989 1989 config = config or self._config
1990 1990 custom_wire = {
1991 1991 'cache': cache # controls the vcs.remote cache
1992 1992 }
1993 1993
1994 1994 repo = get_vcs_instance(
1995 1995 repo_path=safe_str(self.repo_full_path),
1996 1996 config=config,
1997 1997 with_wire=custom_wire,
1998 1998 create=False)
1999 1999
2000 2000 return repo
2001 2001
2002 2002 def __json__(self):
2003 2003 return {'landing_rev': self.landing_rev}
2004 2004
2005 2005 def get_dict(self):
2006 2006
2007 2007 # Since we transformed `repo_name` to a hybrid property, we need to
2008 2008 # keep compatibility with the code which uses `repo_name` field.
2009 2009
2010 2010 result = super(Repository, self).get_dict()
2011 2011 result['repo_name'] = result.pop('_repo_name', None)
2012 2012 return result
2013 2013
2014 2014
2015 2015 class RepoGroup(Base, BaseModel):
2016 2016 __tablename__ = 'groups'
2017 2017 __table_args__ = (
2018 2018 UniqueConstraint('group_name', 'group_parent_id'),
2019 2019 CheckConstraint('group_id != group_parent_id'),
2020 2020 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2021 2021 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2022 2022 )
2023 2023 __mapper_args__ = {'order_by': 'group_name'}
2024 2024
2025 2025 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2026 2026
2027 2027 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2028 2028 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2029 2029 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2030 2030 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2031 2031 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2032 2032 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2033 2033 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2034 2034
2035 2035 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2036 2036 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2037 2037 parent_group = relationship('RepoGroup', remote_side=group_id)
2038 2038 user = relationship('User')
2039 integrations = relationship('Integration',
2040 cascade="all, delete, delete-orphan")
2039 2041
2040 2042 def __init__(self, group_name='', parent_group=None):
2041 2043 self.group_name = group_name
2042 2044 self.parent_group = parent_group
2043 2045
2044 2046 def __unicode__(self):
2045 2047 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2046 2048 self.group_name)
2047 2049
2048 2050 @classmethod
2049 2051 def _generate_choice(cls, repo_group):
2050 2052 from webhelpers.html import literal as _literal
2051 2053 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2052 2054 return repo_group.group_id, _name(repo_group.full_path_splitted)
2053 2055
2054 2056 @classmethod
2055 2057 def groups_choices(cls, groups=None, show_empty_group=True):
2056 2058 if not groups:
2057 2059 groups = cls.query().all()
2058 2060
2059 2061 repo_groups = []
2060 2062 if show_empty_group:
2061 2063 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2062 2064
2063 2065 repo_groups.extend([cls._generate_choice(x) for x in groups])
2064 2066
2065 2067 repo_groups = sorted(
2066 2068 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2067 2069 return repo_groups
2068 2070
2069 2071 @classmethod
2070 2072 def url_sep(cls):
2071 2073 return URL_SEP
2072 2074
2073 2075 @classmethod
2074 2076 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2075 2077 if case_insensitive:
2076 2078 gr = cls.query().filter(func.lower(cls.group_name)
2077 2079 == func.lower(group_name))
2078 2080 else:
2079 2081 gr = cls.query().filter(cls.group_name == group_name)
2080 2082 if cache:
2081 2083 gr = gr.options(FromCache(
2082 2084 "sql_cache_short",
2083 2085 "get_group_%s" % _hash_key(group_name)))
2084 2086 return gr.scalar()
2085 2087
2086 2088 @classmethod
2087 2089 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2088 2090 case_insensitive=True):
2089 2091 q = RepoGroup.query()
2090 2092
2091 2093 if not isinstance(user_id, Optional):
2092 2094 q = q.filter(RepoGroup.user_id == user_id)
2093 2095
2094 2096 if not isinstance(group_id, Optional):
2095 2097 q = q.filter(RepoGroup.group_parent_id == group_id)
2096 2098
2097 2099 if case_insensitive:
2098 2100 q = q.order_by(func.lower(RepoGroup.group_name))
2099 2101 else:
2100 2102 q = q.order_by(RepoGroup.group_name)
2101 2103 return q.all()
2102 2104
2103 2105 @property
2104 2106 def parents(self):
2105 2107 parents_recursion_limit = 10
2106 2108 groups = []
2107 2109 if self.parent_group is None:
2108 2110 return groups
2109 2111 cur_gr = self.parent_group
2110 2112 groups.insert(0, cur_gr)
2111 2113 cnt = 0
2112 2114 while 1:
2113 2115 cnt += 1
2114 2116 gr = getattr(cur_gr, 'parent_group', None)
2115 2117 cur_gr = cur_gr.parent_group
2116 2118 if gr is None:
2117 2119 break
2118 2120 if cnt == parents_recursion_limit:
2119 2121 # this will prevent accidental infinit loops
2120 2122 log.error(('more than %s parents found for group %s, stopping '
2121 2123 'recursive parent fetching' % (parents_recursion_limit, self)))
2122 2124 break
2123 2125
2124 2126 groups.insert(0, gr)
2125 2127 return groups
2126 2128
2127 2129 @property
2128 2130 def children(self):
2129 2131 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2130 2132
2131 2133 @property
2132 2134 def name(self):
2133 2135 return self.group_name.split(RepoGroup.url_sep())[-1]
2134 2136
2135 2137 @property
2136 2138 def full_path(self):
2137 2139 return self.group_name
2138 2140
2139 2141 @property
2140 2142 def full_path_splitted(self):
2141 2143 return self.group_name.split(RepoGroup.url_sep())
2142 2144
2143 2145 @property
2144 2146 def repositories(self):
2145 2147 return Repository.query()\
2146 2148 .filter(Repository.group == self)\
2147 2149 .order_by(Repository.repo_name)
2148 2150
2149 2151 @property
2150 2152 def repositories_recursive_count(self):
2151 2153 cnt = self.repositories.count()
2152 2154
2153 2155 def children_count(group):
2154 2156 cnt = 0
2155 2157 for child in group.children:
2156 2158 cnt += child.repositories.count()
2157 2159 cnt += children_count(child)
2158 2160 return cnt
2159 2161
2160 2162 return cnt + children_count(self)
2161 2163
2162 2164 def _recursive_objects(self, include_repos=True):
2163 2165 all_ = []
2164 2166
2165 2167 def _get_members(root_gr):
2166 2168 if include_repos:
2167 2169 for r in root_gr.repositories:
2168 2170 all_.append(r)
2169 2171 childs = root_gr.children.all()
2170 2172 if childs:
2171 2173 for gr in childs:
2172 2174 all_.append(gr)
2173 2175 _get_members(gr)
2174 2176
2175 2177 _get_members(self)
2176 2178 return [self] + all_
2177 2179
2178 2180 def recursive_groups_and_repos(self):
2179 2181 """
2180 2182 Recursive return all groups, with repositories in those groups
2181 2183 """
2182 2184 return self._recursive_objects()
2183 2185
2184 2186 def recursive_groups(self):
2185 2187 """
2186 2188 Returns all children groups for this group including children of children
2187 2189 """
2188 2190 return self._recursive_objects(include_repos=False)
2189 2191
2190 2192 def get_new_name(self, group_name):
2191 2193 """
2192 2194 returns new full group name based on parent and new name
2193 2195
2194 2196 :param group_name:
2195 2197 """
2196 2198 path_prefix = (self.parent_group.full_path_splitted if
2197 2199 self.parent_group else [])
2198 2200 return RepoGroup.url_sep().join(path_prefix + [group_name])
2199 2201
2200 2202 def permissions(self, with_admins=True, with_owner=True):
2201 2203 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2202 2204 q = q.options(joinedload(UserRepoGroupToPerm.group),
2203 2205 joinedload(UserRepoGroupToPerm.user),
2204 2206 joinedload(UserRepoGroupToPerm.permission),)
2205 2207
2206 2208 # get owners and admins and permissions. We do a trick of re-writing
2207 2209 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2208 2210 # has a global reference and changing one object propagates to all
2209 2211 # others. This means if admin is also an owner admin_row that change
2210 2212 # would propagate to both objects
2211 2213 perm_rows = []
2212 2214 for _usr in q.all():
2213 2215 usr = AttributeDict(_usr.user.get_dict())
2214 2216 usr.permission = _usr.permission.permission_name
2215 2217 perm_rows.append(usr)
2216 2218
2217 2219 # filter the perm rows by 'default' first and then sort them by
2218 2220 # admin,write,read,none permissions sorted again alphabetically in
2219 2221 # each group
2220 2222 perm_rows = sorted(perm_rows, key=display_sort)
2221 2223
2222 2224 _admin_perm = 'group.admin'
2223 2225 owner_row = []
2224 2226 if with_owner:
2225 2227 usr = AttributeDict(self.user.get_dict())
2226 2228 usr.owner_row = True
2227 2229 usr.permission = _admin_perm
2228 2230 owner_row.append(usr)
2229 2231
2230 2232 super_admin_rows = []
2231 2233 if with_admins:
2232 2234 for usr in User.get_all_super_admins():
2233 2235 # if this admin is also owner, don't double the record
2234 2236 if usr.user_id == owner_row[0].user_id:
2235 2237 owner_row[0].admin_row = True
2236 2238 else:
2237 2239 usr = AttributeDict(usr.get_dict())
2238 2240 usr.admin_row = True
2239 2241 usr.permission = _admin_perm
2240 2242 super_admin_rows.append(usr)
2241 2243
2242 2244 return super_admin_rows + owner_row + perm_rows
2243 2245
2244 2246 def permission_user_groups(self):
2245 2247 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2246 2248 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2247 2249 joinedload(UserGroupRepoGroupToPerm.users_group),
2248 2250 joinedload(UserGroupRepoGroupToPerm.permission),)
2249 2251
2250 2252 perm_rows = []
2251 2253 for _user_group in q.all():
2252 2254 usr = AttributeDict(_user_group.users_group.get_dict())
2253 2255 usr.permission = _user_group.permission.permission_name
2254 2256 perm_rows.append(usr)
2255 2257
2256 2258 return perm_rows
2257 2259
2258 2260 def get_api_data(self):
2259 2261 """
2260 2262 Common function for generating api data
2261 2263
2262 2264 """
2263 2265 group = self
2264 2266 data = {
2265 2267 'group_id': group.group_id,
2266 2268 'group_name': group.group_name,
2267 2269 'group_description': group.group_description,
2268 2270 'parent_group': group.parent_group.group_name if group.parent_group else None,
2269 2271 'repositories': [x.repo_name for x in group.repositories],
2270 2272 'owner': group.user.username,
2271 2273 }
2272 2274 return data
2273 2275
2274 2276
2275 2277 class Permission(Base, BaseModel):
2276 2278 __tablename__ = 'permissions'
2277 2279 __table_args__ = (
2278 2280 Index('p_perm_name_idx', 'permission_name'),
2279 2281 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2280 2282 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2281 2283 )
2282 2284 PERMS = [
2283 2285 ('hg.admin', _('RhodeCode Super Administrator')),
2284 2286
2285 2287 ('repository.none', _('Repository no access')),
2286 2288 ('repository.read', _('Repository read access')),
2287 2289 ('repository.write', _('Repository write access')),
2288 2290 ('repository.admin', _('Repository admin access')),
2289 2291
2290 2292 ('group.none', _('Repository group no access')),
2291 2293 ('group.read', _('Repository group read access')),
2292 2294 ('group.write', _('Repository group write access')),
2293 2295 ('group.admin', _('Repository group admin access')),
2294 2296
2295 2297 ('usergroup.none', _('User group no access')),
2296 2298 ('usergroup.read', _('User group read access')),
2297 2299 ('usergroup.write', _('User group write access')),
2298 2300 ('usergroup.admin', _('User group admin access')),
2299 2301
2300 2302 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2301 2303 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2302 2304
2303 2305 ('hg.usergroup.create.false', _('User Group creation disabled')),
2304 2306 ('hg.usergroup.create.true', _('User Group creation enabled')),
2305 2307
2306 2308 ('hg.create.none', _('Repository creation disabled')),
2307 2309 ('hg.create.repository', _('Repository creation enabled')),
2308 2310 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2309 2311 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2310 2312
2311 2313 ('hg.fork.none', _('Repository forking disabled')),
2312 2314 ('hg.fork.repository', _('Repository forking enabled')),
2313 2315
2314 2316 ('hg.register.none', _('Registration disabled')),
2315 2317 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2316 2318 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2317 2319
2318 2320 ('hg.extern_activate.manual', _('Manual activation of external account')),
2319 2321 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2320 2322
2321 2323 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2322 2324 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2323 2325 ]
2324 2326
2325 2327 # definition of system default permissions for DEFAULT user
2326 2328 DEFAULT_USER_PERMISSIONS = [
2327 2329 'repository.read',
2328 2330 'group.read',
2329 2331 'usergroup.read',
2330 2332 'hg.create.repository',
2331 2333 'hg.repogroup.create.false',
2332 2334 'hg.usergroup.create.false',
2333 2335 'hg.create.write_on_repogroup.true',
2334 2336 'hg.fork.repository',
2335 2337 'hg.register.manual_activate',
2336 2338 'hg.extern_activate.auto',
2337 2339 'hg.inherit_default_perms.true',
2338 2340 ]
2339 2341
2340 2342 # defines which permissions are more important higher the more important
2341 2343 # Weight defines which permissions are more important.
2342 2344 # The higher number the more important.
2343 2345 PERM_WEIGHTS = {
2344 2346 'repository.none': 0,
2345 2347 'repository.read': 1,
2346 2348 'repository.write': 3,
2347 2349 'repository.admin': 4,
2348 2350
2349 2351 'group.none': 0,
2350 2352 'group.read': 1,
2351 2353 'group.write': 3,
2352 2354 'group.admin': 4,
2353 2355
2354 2356 'usergroup.none': 0,
2355 2357 'usergroup.read': 1,
2356 2358 'usergroup.write': 3,
2357 2359 'usergroup.admin': 4,
2358 2360
2359 2361 'hg.repogroup.create.false': 0,
2360 2362 'hg.repogroup.create.true': 1,
2361 2363
2362 2364 'hg.usergroup.create.false': 0,
2363 2365 'hg.usergroup.create.true': 1,
2364 2366
2365 2367 'hg.fork.none': 0,
2366 2368 'hg.fork.repository': 1,
2367 2369 'hg.create.none': 0,
2368 2370 'hg.create.repository': 1
2369 2371 }
2370 2372
2371 2373 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2372 2374 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2373 2375 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2374 2376
2375 2377 def __unicode__(self):
2376 2378 return u"<%s('%s:%s')>" % (
2377 2379 self.__class__.__name__, self.permission_id, self.permission_name
2378 2380 )
2379 2381
2380 2382 @classmethod
2381 2383 def get_by_key(cls, key):
2382 2384 return cls.query().filter(cls.permission_name == key).scalar()
2383 2385
2384 2386 @classmethod
2385 2387 def get_default_repo_perms(cls, user_id, repo_id=None):
2386 2388 q = Session().query(UserRepoToPerm, Repository, Permission)\
2387 2389 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2388 2390 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2389 2391 .filter(UserRepoToPerm.user_id == user_id)
2390 2392 if repo_id:
2391 2393 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2392 2394 return q.all()
2393 2395
2394 2396 @classmethod
2395 2397 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2396 2398 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2397 2399 .join(
2398 2400 Permission,
2399 2401 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2400 2402 .join(
2401 2403 Repository,
2402 2404 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2403 2405 .join(
2404 2406 UserGroup,
2405 2407 UserGroupRepoToPerm.users_group_id ==
2406 2408 UserGroup.users_group_id)\
2407 2409 .join(
2408 2410 UserGroupMember,
2409 2411 UserGroupRepoToPerm.users_group_id ==
2410 2412 UserGroupMember.users_group_id)\
2411 2413 .filter(
2412 2414 UserGroupMember.user_id == user_id,
2413 2415 UserGroup.users_group_active == true())
2414 2416 if repo_id:
2415 2417 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2416 2418 return q.all()
2417 2419
2418 2420 @classmethod
2419 2421 def get_default_group_perms(cls, user_id, repo_group_id=None):
2420 2422 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2421 2423 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2422 2424 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2423 2425 .filter(UserRepoGroupToPerm.user_id == user_id)
2424 2426 if repo_group_id:
2425 2427 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2426 2428 return q.all()
2427 2429
2428 2430 @classmethod
2429 2431 def get_default_group_perms_from_user_group(
2430 2432 cls, user_id, repo_group_id=None):
2431 2433 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2432 2434 .join(
2433 2435 Permission,
2434 2436 UserGroupRepoGroupToPerm.permission_id ==
2435 2437 Permission.permission_id)\
2436 2438 .join(
2437 2439 RepoGroup,
2438 2440 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2439 2441 .join(
2440 2442 UserGroup,
2441 2443 UserGroupRepoGroupToPerm.users_group_id ==
2442 2444 UserGroup.users_group_id)\
2443 2445 .join(
2444 2446 UserGroupMember,
2445 2447 UserGroupRepoGroupToPerm.users_group_id ==
2446 2448 UserGroupMember.users_group_id)\
2447 2449 .filter(
2448 2450 UserGroupMember.user_id == user_id,
2449 2451 UserGroup.users_group_active == true())
2450 2452 if repo_group_id:
2451 2453 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2452 2454 return q.all()
2453 2455
2454 2456 @classmethod
2455 2457 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2456 2458 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2457 2459 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2458 2460 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2459 2461 .filter(UserUserGroupToPerm.user_id == user_id)
2460 2462 if user_group_id:
2461 2463 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2462 2464 return q.all()
2463 2465
2464 2466 @classmethod
2465 2467 def get_default_user_group_perms_from_user_group(
2466 2468 cls, user_id, user_group_id=None):
2467 2469 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2468 2470 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2469 2471 .join(
2470 2472 Permission,
2471 2473 UserGroupUserGroupToPerm.permission_id ==
2472 2474 Permission.permission_id)\
2473 2475 .join(
2474 2476 TargetUserGroup,
2475 2477 UserGroupUserGroupToPerm.target_user_group_id ==
2476 2478 TargetUserGroup.users_group_id)\
2477 2479 .join(
2478 2480 UserGroup,
2479 2481 UserGroupUserGroupToPerm.user_group_id ==
2480 2482 UserGroup.users_group_id)\
2481 2483 .join(
2482 2484 UserGroupMember,
2483 2485 UserGroupUserGroupToPerm.user_group_id ==
2484 2486 UserGroupMember.users_group_id)\
2485 2487 .filter(
2486 2488 UserGroupMember.user_id == user_id,
2487 2489 UserGroup.users_group_active == true())
2488 2490 if user_group_id:
2489 2491 q = q.filter(
2490 2492 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2491 2493
2492 2494 return q.all()
2493 2495
2494 2496
2495 2497 class UserRepoToPerm(Base, BaseModel):
2496 2498 __tablename__ = 'repo_to_perm'
2497 2499 __table_args__ = (
2498 2500 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2499 2501 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2500 2502 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2501 2503 )
2502 2504 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2503 2505 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2504 2506 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2505 2507 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2506 2508
2507 2509 user = relationship('User')
2508 2510 repository = relationship('Repository')
2509 2511 permission = relationship('Permission')
2510 2512
2511 2513 @classmethod
2512 2514 def create(cls, user, repository, permission):
2513 2515 n = cls()
2514 2516 n.user = user
2515 2517 n.repository = repository
2516 2518 n.permission = permission
2517 2519 Session().add(n)
2518 2520 return n
2519 2521
2520 2522 def __unicode__(self):
2521 2523 return u'<%s => %s >' % (self.user, self.repository)
2522 2524
2523 2525
2524 2526 class UserUserGroupToPerm(Base, BaseModel):
2525 2527 __tablename__ = 'user_user_group_to_perm'
2526 2528 __table_args__ = (
2527 2529 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2528 2530 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2529 2531 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2530 2532 )
2531 2533 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2532 2534 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2533 2535 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2534 2536 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2535 2537
2536 2538 user = relationship('User')
2537 2539 user_group = relationship('UserGroup')
2538 2540 permission = relationship('Permission')
2539 2541
2540 2542 @classmethod
2541 2543 def create(cls, user, user_group, permission):
2542 2544 n = cls()
2543 2545 n.user = user
2544 2546 n.user_group = user_group
2545 2547 n.permission = permission
2546 2548 Session().add(n)
2547 2549 return n
2548 2550
2549 2551 def __unicode__(self):
2550 2552 return u'<%s => %s >' % (self.user, self.user_group)
2551 2553
2552 2554
2553 2555 class UserToPerm(Base, BaseModel):
2554 2556 __tablename__ = 'user_to_perm'
2555 2557 __table_args__ = (
2556 2558 UniqueConstraint('user_id', 'permission_id'),
2557 2559 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2558 2560 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2559 2561 )
2560 2562 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2561 2563 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2562 2564 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2563 2565
2564 2566 user = relationship('User')
2565 2567 permission = relationship('Permission', lazy='joined')
2566 2568
2567 2569 def __unicode__(self):
2568 2570 return u'<%s => %s >' % (self.user, self.permission)
2569 2571
2570 2572
2571 2573 class UserGroupRepoToPerm(Base, BaseModel):
2572 2574 __tablename__ = 'users_group_repo_to_perm'
2573 2575 __table_args__ = (
2574 2576 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2575 2577 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2576 2578 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2577 2579 )
2578 2580 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2579 2581 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2580 2582 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2581 2583 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2582 2584
2583 2585 users_group = relationship('UserGroup')
2584 2586 permission = relationship('Permission')
2585 2587 repository = relationship('Repository')
2586 2588
2587 2589 @classmethod
2588 2590 def create(cls, users_group, repository, permission):
2589 2591 n = cls()
2590 2592 n.users_group = users_group
2591 2593 n.repository = repository
2592 2594 n.permission = permission
2593 2595 Session().add(n)
2594 2596 return n
2595 2597
2596 2598 def __unicode__(self):
2597 2599 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2598 2600
2599 2601
2600 2602 class UserGroupUserGroupToPerm(Base, BaseModel):
2601 2603 __tablename__ = 'user_group_user_group_to_perm'
2602 2604 __table_args__ = (
2603 2605 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2604 2606 CheckConstraint('target_user_group_id != user_group_id'),
2605 2607 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2606 2608 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2607 2609 )
2608 2610 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)
2609 2611 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2610 2612 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2611 2613 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2612 2614
2613 2615 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2614 2616 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2615 2617 permission = relationship('Permission')
2616 2618
2617 2619 @classmethod
2618 2620 def create(cls, target_user_group, user_group, permission):
2619 2621 n = cls()
2620 2622 n.target_user_group = target_user_group
2621 2623 n.user_group = user_group
2622 2624 n.permission = permission
2623 2625 Session().add(n)
2624 2626 return n
2625 2627
2626 2628 def __unicode__(self):
2627 2629 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2628 2630
2629 2631
2630 2632 class UserGroupToPerm(Base, BaseModel):
2631 2633 __tablename__ = 'users_group_to_perm'
2632 2634 __table_args__ = (
2633 2635 UniqueConstraint('users_group_id', 'permission_id',),
2634 2636 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2635 2637 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2636 2638 )
2637 2639 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2638 2640 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2639 2641 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2640 2642
2641 2643 users_group = relationship('UserGroup')
2642 2644 permission = relationship('Permission')
2643 2645
2644 2646
2645 2647 class UserRepoGroupToPerm(Base, BaseModel):
2646 2648 __tablename__ = 'user_repo_group_to_perm'
2647 2649 __table_args__ = (
2648 2650 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2649 2651 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2650 2652 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2651 2653 )
2652 2654
2653 2655 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2654 2656 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2655 2657 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2656 2658 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2657 2659
2658 2660 user = relationship('User')
2659 2661 group = relationship('RepoGroup')
2660 2662 permission = relationship('Permission')
2661 2663
2662 2664 @classmethod
2663 2665 def create(cls, user, repository_group, permission):
2664 2666 n = cls()
2665 2667 n.user = user
2666 2668 n.group = repository_group
2667 2669 n.permission = permission
2668 2670 Session().add(n)
2669 2671 return n
2670 2672
2671 2673
2672 2674 class UserGroupRepoGroupToPerm(Base, BaseModel):
2673 2675 __tablename__ = 'users_group_repo_group_to_perm'
2674 2676 __table_args__ = (
2675 2677 UniqueConstraint('users_group_id', 'group_id'),
2676 2678 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2677 2679 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2678 2680 )
2679 2681
2680 2682 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)
2681 2683 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2682 2684 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2683 2685 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2684 2686
2685 2687 users_group = relationship('UserGroup')
2686 2688 permission = relationship('Permission')
2687 2689 group = relationship('RepoGroup')
2688 2690
2689 2691 @classmethod
2690 2692 def create(cls, user_group, repository_group, permission):
2691 2693 n = cls()
2692 2694 n.users_group = user_group
2693 2695 n.group = repository_group
2694 2696 n.permission = permission
2695 2697 Session().add(n)
2696 2698 return n
2697 2699
2698 2700 def __unicode__(self):
2699 2701 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2700 2702
2701 2703
2702 2704 class Statistics(Base, BaseModel):
2703 2705 __tablename__ = 'statistics'
2704 2706 __table_args__ = (
2705 2707 UniqueConstraint('repository_id'),
2706 2708 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2707 2709 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2708 2710 )
2709 2711 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2710 2712 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2711 2713 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2712 2714 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2713 2715 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2714 2716 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2715 2717
2716 2718 repository = relationship('Repository', single_parent=True)
2717 2719
2718 2720
2719 2721 class UserFollowing(Base, BaseModel):
2720 2722 __tablename__ = 'user_followings'
2721 2723 __table_args__ = (
2722 2724 UniqueConstraint('user_id', 'follows_repository_id'),
2723 2725 UniqueConstraint('user_id', 'follows_user_id'),
2724 2726 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2725 2727 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2726 2728 )
2727 2729
2728 2730 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2729 2731 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2730 2732 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2731 2733 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2732 2734 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2733 2735
2734 2736 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2735 2737
2736 2738 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2737 2739 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2738 2740
2739 2741 @classmethod
2740 2742 def get_repo_followers(cls, repo_id):
2741 2743 return cls.query().filter(cls.follows_repo_id == repo_id)
2742 2744
2743 2745
2744 2746 class CacheKey(Base, BaseModel):
2745 2747 __tablename__ = 'cache_invalidation'
2746 2748 __table_args__ = (
2747 2749 UniqueConstraint('cache_key'),
2748 2750 Index('key_idx', 'cache_key'),
2749 2751 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2750 2752 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2751 2753 )
2752 2754 CACHE_TYPE_ATOM = 'ATOM'
2753 2755 CACHE_TYPE_RSS = 'RSS'
2754 2756 CACHE_TYPE_README = 'README'
2755 2757
2756 2758 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2757 2759 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2758 2760 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2759 2761 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2760 2762
2761 2763 def __init__(self, cache_key, cache_args=''):
2762 2764 self.cache_key = cache_key
2763 2765 self.cache_args = cache_args
2764 2766 self.cache_active = False
2765 2767
2766 2768 def __unicode__(self):
2767 2769 return u"<%s('%s:%s[%s]')>" % (
2768 2770 self.__class__.__name__,
2769 2771 self.cache_id, self.cache_key, self.cache_active)
2770 2772
2771 2773 def _cache_key_partition(self):
2772 2774 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2773 2775 return prefix, repo_name, suffix
2774 2776
2775 2777 def get_prefix(self):
2776 2778 """
2777 2779 Try to extract prefix from existing cache key. The key could consist
2778 2780 of prefix, repo_name, suffix
2779 2781 """
2780 2782 # this returns prefix, repo_name, suffix
2781 2783 return self._cache_key_partition()[0]
2782 2784
2783 2785 def get_suffix(self):
2784 2786 """
2785 2787 get suffix that might have been used in _get_cache_key to
2786 2788 generate self.cache_key. Only used for informational purposes
2787 2789 in repo_edit.html.
2788 2790 """
2789 2791 # prefix, repo_name, suffix
2790 2792 return self._cache_key_partition()[2]
2791 2793
2792 2794 @classmethod
2793 2795 def delete_all_cache(cls):
2794 2796 """
2795 2797 Delete all cache keys from database.
2796 2798 Should only be run when all instances are down and all entries
2797 2799 thus stale.
2798 2800 """
2799 2801 cls.query().delete()
2800 2802 Session().commit()
2801 2803
2802 2804 @classmethod
2803 2805 def get_cache_key(cls, repo_name, cache_type):
2804 2806 """
2805 2807
2806 2808 Generate a cache key for this process of RhodeCode instance.
2807 2809 Prefix most likely will be process id or maybe explicitly set
2808 2810 instance_id from .ini file.
2809 2811 """
2810 2812 import rhodecode
2811 2813 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2812 2814
2813 2815 repo_as_unicode = safe_unicode(repo_name)
2814 2816 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2815 2817 if cache_type else repo_as_unicode
2816 2818
2817 2819 return u'{}{}'.format(prefix, key)
2818 2820
2819 2821 @classmethod
2820 2822 def set_invalidate(cls, repo_name, delete=False):
2821 2823 """
2822 2824 Mark all caches of a repo as invalid in the database.
2823 2825 """
2824 2826
2825 2827 try:
2826 2828 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2827 2829 if delete:
2828 2830 log.debug('cache objects deleted for repo %s',
2829 2831 safe_str(repo_name))
2830 2832 qry.delete()
2831 2833 else:
2832 2834 log.debug('cache objects marked as invalid for repo %s',
2833 2835 safe_str(repo_name))
2834 2836 qry.update({"cache_active": False})
2835 2837
2836 2838 Session().commit()
2837 2839 except Exception:
2838 2840 log.exception(
2839 2841 'Cache key invalidation failed for repository %s',
2840 2842 safe_str(repo_name))
2841 2843 Session().rollback()
2842 2844
2843 2845 @classmethod
2844 2846 def get_active_cache(cls, cache_key):
2845 2847 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2846 2848 if inv_obj:
2847 2849 return inv_obj
2848 2850 return None
2849 2851
2850 2852 @classmethod
2851 2853 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2852 2854 thread_scoped=False):
2853 2855 """
2854 2856 @cache_region('long_term')
2855 2857 def _heavy_calculation(cache_key):
2856 2858 return 'result'
2857 2859
2858 2860 cache_context = CacheKey.repo_context_cache(
2859 2861 _heavy_calculation, repo_name, cache_type)
2860 2862
2861 2863 with cache_context as context:
2862 2864 context.invalidate()
2863 2865 computed = context.compute()
2864 2866
2865 2867 assert computed == 'result'
2866 2868 """
2867 2869 from rhodecode.lib import caches
2868 2870 return caches.InvalidationContext(
2869 2871 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2870 2872
2871 2873
2872 2874 class ChangesetComment(Base, BaseModel):
2873 2875 __tablename__ = 'changeset_comments'
2874 2876 __table_args__ = (
2875 2877 Index('cc_revision_idx', 'revision'),
2876 2878 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2877 2879 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2878 2880 )
2879 2881
2880 2882 COMMENT_OUTDATED = u'comment_outdated'
2881 2883
2882 2884 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2883 2885 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2884 2886 revision = Column('revision', String(40), nullable=True)
2885 2887 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2886 2888 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2887 2889 line_no = Column('line_no', Unicode(10), nullable=True)
2888 2890 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2889 2891 f_path = Column('f_path', Unicode(1000), nullable=True)
2890 2892 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2891 2893 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2892 2894 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2893 2895 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2894 2896 renderer = Column('renderer', Unicode(64), nullable=True)
2895 2897 display_state = Column('display_state', Unicode(128), nullable=True)
2896 2898
2897 2899 author = relationship('User', lazy='joined')
2898 2900 repo = relationship('Repository')
2899 2901 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2900 2902 pull_request = relationship('PullRequest', lazy='joined')
2901 2903 pull_request_version = relationship('PullRequestVersion')
2902 2904
2903 2905 @classmethod
2904 2906 def get_users(cls, revision=None, pull_request_id=None):
2905 2907 """
2906 2908 Returns user associated with this ChangesetComment. ie those
2907 2909 who actually commented
2908 2910
2909 2911 :param cls:
2910 2912 :param revision:
2911 2913 """
2912 2914 q = Session().query(User)\
2913 2915 .join(ChangesetComment.author)
2914 2916 if revision:
2915 2917 q = q.filter(cls.revision == revision)
2916 2918 elif pull_request_id:
2917 2919 q = q.filter(cls.pull_request_id == pull_request_id)
2918 2920 return q.all()
2919 2921
2920 2922 def render(self, mentions=False):
2921 2923 from rhodecode.lib import helpers as h
2922 2924 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2923 2925
2924 2926 def __repr__(self):
2925 2927 if self.comment_id:
2926 2928 return '<DB:ChangesetComment #%s>' % self.comment_id
2927 2929 else:
2928 2930 return '<DB:ChangesetComment at %#x>' % id(self)
2929 2931
2930 2932
2931 2933 class ChangesetStatus(Base, BaseModel):
2932 2934 __tablename__ = 'changeset_statuses'
2933 2935 __table_args__ = (
2934 2936 Index('cs_revision_idx', 'revision'),
2935 2937 Index('cs_version_idx', 'version'),
2936 2938 UniqueConstraint('repo_id', 'revision', 'version'),
2937 2939 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2938 2940 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2939 2941 )
2940 2942 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2941 2943 STATUS_APPROVED = 'approved'
2942 2944 STATUS_REJECTED = 'rejected'
2943 2945 STATUS_UNDER_REVIEW = 'under_review'
2944 2946
2945 2947 STATUSES = [
2946 2948 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2947 2949 (STATUS_APPROVED, _("Approved")),
2948 2950 (STATUS_REJECTED, _("Rejected")),
2949 2951 (STATUS_UNDER_REVIEW, _("Under Review")),
2950 2952 ]
2951 2953
2952 2954 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2953 2955 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2954 2956 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2955 2957 revision = Column('revision', String(40), nullable=False)
2956 2958 status = Column('status', String(128), nullable=False, default=DEFAULT)
2957 2959 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2958 2960 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2959 2961 version = Column('version', Integer(), nullable=False, default=0)
2960 2962 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2961 2963
2962 2964 author = relationship('User', lazy='joined')
2963 2965 repo = relationship('Repository')
2964 2966 comment = relationship('ChangesetComment', lazy='joined')
2965 2967 pull_request = relationship('PullRequest', lazy='joined')
2966 2968
2967 2969 def __unicode__(self):
2968 2970 return u"<%s('%s[%s]:%s')>" % (
2969 2971 self.__class__.__name__,
2970 2972 self.status, self.version, self.author
2971 2973 )
2972 2974
2973 2975 @classmethod
2974 2976 def get_status_lbl(cls, value):
2975 2977 return dict(cls.STATUSES).get(value)
2976 2978
2977 2979 @property
2978 2980 def status_lbl(self):
2979 2981 return ChangesetStatus.get_status_lbl(self.status)
2980 2982
2981 2983
2982 2984 class _PullRequestBase(BaseModel):
2983 2985 """
2984 2986 Common attributes of pull request and version entries.
2985 2987 """
2986 2988
2987 2989 # .status values
2988 2990 STATUS_NEW = u'new'
2989 2991 STATUS_OPEN = u'open'
2990 2992 STATUS_CLOSED = u'closed'
2991 2993
2992 2994 title = Column('title', Unicode(255), nullable=True)
2993 2995 description = Column(
2994 2996 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2995 2997 nullable=True)
2996 2998 # new/open/closed status of pull request (not approve/reject/etc)
2997 2999 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
2998 3000 created_on = Column(
2999 3001 'created_on', DateTime(timezone=False), nullable=False,
3000 3002 default=datetime.datetime.now)
3001 3003 updated_on = Column(
3002 3004 'updated_on', DateTime(timezone=False), nullable=False,
3003 3005 default=datetime.datetime.now)
3004 3006
3005 3007 @declared_attr
3006 3008 def user_id(cls):
3007 3009 return Column(
3008 3010 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3009 3011 unique=None)
3010 3012
3011 3013 # 500 revisions max
3012 3014 _revisions = Column(
3013 3015 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3014 3016
3015 3017 @declared_attr
3016 3018 def source_repo_id(cls):
3017 3019 # TODO: dan: rename column to source_repo_id
3018 3020 return Column(
3019 3021 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3020 3022 nullable=False)
3021 3023
3022 3024 source_ref = Column('org_ref', Unicode(255), nullable=False)
3023 3025
3024 3026 @declared_attr
3025 3027 def target_repo_id(cls):
3026 3028 # TODO: dan: rename column to target_repo_id
3027 3029 return Column(
3028 3030 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3029 3031 nullable=False)
3030 3032
3031 3033 target_ref = Column('other_ref', Unicode(255), nullable=False)
3032 3034
3033 3035 # TODO: dan: rename column to last_merge_source_rev
3034 3036 _last_merge_source_rev = Column(
3035 3037 'last_merge_org_rev', String(40), nullable=True)
3036 3038 # TODO: dan: rename column to last_merge_target_rev
3037 3039 _last_merge_target_rev = Column(
3038 3040 'last_merge_other_rev', String(40), nullable=True)
3039 3041 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3040 3042 merge_rev = Column('merge_rev', String(40), nullable=True)
3041 3043
3042 3044 @hybrid_property
3043 3045 def revisions(self):
3044 3046 return self._revisions.split(':') if self._revisions else []
3045 3047
3046 3048 @revisions.setter
3047 3049 def revisions(self, val):
3048 3050 self._revisions = ':'.join(val)
3049 3051
3050 3052 @declared_attr
3051 3053 def author(cls):
3052 3054 return relationship('User', lazy='joined')
3053 3055
3054 3056 @declared_attr
3055 3057 def source_repo(cls):
3056 3058 return relationship(
3057 3059 'Repository',
3058 3060 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3059 3061
3060 3062 @property
3061 3063 def source_ref_parts(self):
3062 3064 refs = self.source_ref.split(':')
3063 3065 return Reference(refs[0], refs[1], refs[2])
3064 3066
3065 3067 @declared_attr
3066 3068 def target_repo(cls):
3067 3069 return relationship(
3068 3070 'Repository',
3069 3071 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3070 3072
3071 3073 @property
3072 3074 def target_ref_parts(self):
3073 3075 refs = self.target_ref.split(':')
3074 3076 return Reference(refs[0], refs[1], refs[2])
3075 3077
3076 3078
3077 3079 class PullRequest(Base, _PullRequestBase):
3078 3080 __tablename__ = 'pull_requests'
3079 3081 __table_args__ = (
3080 3082 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3081 3083 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3082 3084 )
3083 3085
3084 3086 pull_request_id = Column(
3085 3087 'pull_request_id', Integer(), nullable=False, primary_key=True)
3086 3088
3087 3089 def __repr__(self):
3088 3090 if self.pull_request_id:
3089 3091 return '<DB:PullRequest #%s>' % self.pull_request_id
3090 3092 else:
3091 3093 return '<DB:PullRequest at %#x>' % id(self)
3092 3094
3093 3095 reviewers = relationship('PullRequestReviewers',
3094 3096 cascade="all, delete, delete-orphan")
3095 3097 statuses = relationship('ChangesetStatus')
3096 3098 comments = relationship('ChangesetComment',
3097 3099 cascade="all, delete, delete-orphan")
3098 3100 versions = relationship('PullRequestVersion',
3099 3101 cascade="all, delete, delete-orphan")
3100 3102
3101 3103 def is_closed(self):
3102 3104 return self.status == self.STATUS_CLOSED
3103 3105
3104 3106 def get_api_data(self):
3105 3107 from rhodecode.model.pull_request import PullRequestModel
3106 3108 pull_request = self
3107 3109 merge_status = PullRequestModel().merge_status(pull_request)
3108 3110 data = {
3109 3111 'pull_request_id': pull_request.pull_request_id,
3110 3112 'url': url('pullrequest_show', repo_name=self.target_repo.repo_name,
3111 3113 pull_request_id=self.pull_request_id,
3112 3114 qualified=True),
3113 3115 'title': pull_request.title,
3114 3116 'description': pull_request.description,
3115 3117 'status': pull_request.status,
3116 3118 'created_on': pull_request.created_on,
3117 3119 'updated_on': pull_request.updated_on,
3118 3120 'commit_ids': pull_request.revisions,
3119 3121 'review_status': pull_request.calculated_review_status(),
3120 3122 'mergeable': {
3121 3123 'status': merge_status[0],
3122 3124 'message': unicode(merge_status[1]),
3123 3125 },
3124 3126 'source': {
3125 3127 'clone_url': pull_request.source_repo.clone_url(),
3126 3128 'repository': pull_request.source_repo.repo_name,
3127 3129 'reference': {
3128 3130 'name': pull_request.source_ref_parts.name,
3129 3131 'type': pull_request.source_ref_parts.type,
3130 3132 'commit_id': pull_request.source_ref_parts.commit_id,
3131 3133 },
3132 3134 },
3133 3135 'target': {
3134 3136 'clone_url': pull_request.target_repo.clone_url(),
3135 3137 'repository': pull_request.target_repo.repo_name,
3136 3138 'reference': {
3137 3139 'name': pull_request.target_ref_parts.name,
3138 3140 'type': pull_request.target_ref_parts.type,
3139 3141 'commit_id': pull_request.target_ref_parts.commit_id,
3140 3142 },
3141 3143 },
3142 3144 'author': pull_request.author.get_api_data(include_secrets=False,
3143 3145 details='basic'),
3144 3146 'reviewers': [
3145 3147 {
3146 3148 'user': reviewer.get_api_data(include_secrets=False,
3147 3149 details='basic'),
3148 3150 'review_status': st[0][1].status if st else 'not_reviewed',
3149 3151 }
3150 3152 for reviewer, st in pull_request.reviewers_statuses()
3151 3153 ]
3152 3154 }
3153 3155
3154 3156 return data
3155 3157
3156 3158 def __json__(self):
3157 3159 return {
3158 3160 'revisions': self.revisions,
3159 3161 }
3160 3162
3161 3163 def calculated_review_status(self):
3162 3164 # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html
3163 3165 # because it's tricky on how to use ChangesetStatusModel from there
3164 3166 warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning)
3165 3167 from rhodecode.model.changeset_status import ChangesetStatusModel
3166 3168 return ChangesetStatusModel().calculated_review_status(self)
3167 3169
3168 3170 def reviewers_statuses(self):
3169 3171 warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
3170 3172 from rhodecode.model.changeset_status import ChangesetStatusModel
3171 3173 return ChangesetStatusModel().reviewers_statuses(self)
3172 3174
3173 3175
3174 3176 class PullRequestVersion(Base, _PullRequestBase):
3175 3177 __tablename__ = 'pull_request_versions'
3176 3178 __table_args__ = (
3177 3179 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3178 3180 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3179 3181 )
3180 3182
3181 3183 pull_request_version_id = Column(
3182 3184 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3183 3185 pull_request_id = Column(
3184 3186 'pull_request_id', Integer(),
3185 3187 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3186 3188 pull_request = relationship('PullRequest')
3187 3189
3188 3190 def __repr__(self):
3189 3191 if self.pull_request_version_id:
3190 3192 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3191 3193 else:
3192 3194 return '<DB:PullRequestVersion at %#x>' % id(self)
3193 3195
3194 3196
3195 3197 class PullRequestReviewers(Base, BaseModel):
3196 3198 __tablename__ = 'pull_request_reviewers'
3197 3199 __table_args__ = (
3198 3200 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3199 3201 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3200 3202 )
3201 3203
3202 3204 def __init__(self, user=None, pull_request=None):
3203 3205 self.user = user
3204 3206 self.pull_request = pull_request
3205 3207
3206 3208 pull_requests_reviewers_id = Column(
3207 3209 'pull_requests_reviewers_id', Integer(), nullable=False,
3208 3210 primary_key=True)
3209 3211 pull_request_id = Column(
3210 3212 "pull_request_id", Integer(),
3211 3213 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3212 3214 user_id = Column(
3213 3215 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3214 3216
3215 3217 user = relationship('User')
3216 3218 pull_request = relationship('PullRequest')
3217 3219
3218 3220
3219 3221 class Notification(Base, BaseModel):
3220 3222 __tablename__ = 'notifications'
3221 3223 __table_args__ = (
3222 3224 Index('notification_type_idx', 'type'),
3223 3225 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3224 3226 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3225 3227 )
3226 3228
3227 3229 TYPE_CHANGESET_COMMENT = u'cs_comment'
3228 3230 TYPE_MESSAGE = u'message'
3229 3231 TYPE_MENTION = u'mention'
3230 3232 TYPE_REGISTRATION = u'registration'
3231 3233 TYPE_PULL_REQUEST = u'pull_request'
3232 3234 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3233 3235
3234 3236 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3235 3237 subject = Column('subject', Unicode(512), nullable=True)
3236 3238 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3237 3239 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3238 3240 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3239 3241 type_ = Column('type', Unicode(255))
3240 3242
3241 3243 created_by_user = relationship('User')
3242 3244 notifications_to_users = relationship('UserNotification', lazy='joined',
3243 3245 cascade="all, delete, delete-orphan")
3244 3246
3245 3247 @property
3246 3248 def recipients(self):
3247 3249 return [x.user for x in UserNotification.query()\
3248 3250 .filter(UserNotification.notification == self)\
3249 3251 .order_by(UserNotification.user_id.asc()).all()]
3250 3252
3251 3253 @classmethod
3252 3254 def create(cls, created_by, subject, body, recipients, type_=None):
3253 3255 if type_ is None:
3254 3256 type_ = Notification.TYPE_MESSAGE
3255 3257
3256 3258 notification = cls()
3257 3259 notification.created_by_user = created_by
3258 3260 notification.subject = subject
3259 3261 notification.body = body
3260 3262 notification.type_ = type_
3261 3263 notification.created_on = datetime.datetime.now()
3262 3264
3263 3265 for u in recipients:
3264 3266 assoc = UserNotification()
3265 3267 assoc.notification = notification
3266 3268
3267 3269 # if created_by is inside recipients mark his notification
3268 3270 # as read
3269 3271 if u.user_id == created_by.user_id:
3270 3272 assoc.read = True
3271 3273
3272 3274 u.notifications.append(assoc)
3273 3275 Session().add(notification)
3274 3276
3275 3277 return notification
3276 3278
3277 3279 @property
3278 3280 def description(self):
3279 3281 from rhodecode.model.notification import NotificationModel
3280 3282 return NotificationModel().make_description(self)
3281 3283
3282 3284
3283 3285 class UserNotification(Base, BaseModel):
3284 3286 __tablename__ = 'user_to_notification'
3285 3287 __table_args__ = (
3286 3288 UniqueConstraint('user_id', 'notification_id'),
3287 3289 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3288 3290 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3289 3291 )
3290 3292 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3291 3293 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3292 3294 read = Column('read', Boolean, default=False)
3293 3295 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3294 3296
3295 3297 user = relationship('User', lazy="joined")
3296 3298 notification = relationship('Notification', lazy="joined",
3297 3299 order_by=lambda: Notification.created_on.desc(),)
3298 3300
3299 3301 def mark_as_read(self):
3300 3302 self.read = True
3301 3303 Session().add(self)
3302 3304
3303 3305
3304 3306 class Gist(Base, BaseModel):
3305 3307 __tablename__ = 'gists'
3306 3308 __table_args__ = (
3307 3309 Index('g_gist_access_id_idx', 'gist_access_id'),
3308 3310 Index('g_created_on_idx', 'created_on'),
3309 3311 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3310 3312 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3311 3313 )
3312 3314 GIST_PUBLIC = u'public'
3313 3315 GIST_PRIVATE = u'private'
3314 3316 DEFAULT_FILENAME = u'gistfile1.txt'
3315 3317
3316 3318 ACL_LEVEL_PUBLIC = u'acl_public'
3317 3319 ACL_LEVEL_PRIVATE = u'acl_private'
3318 3320
3319 3321 gist_id = Column('gist_id', Integer(), primary_key=True)
3320 3322 gist_access_id = Column('gist_access_id', Unicode(250))
3321 3323 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3322 3324 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3323 3325 gist_expires = Column('gist_expires', Float(53), nullable=False)
3324 3326 gist_type = Column('gist_type', Unicode(128), nullable=False)
3325 3327 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3326 3328 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3327 3329 acl_level = Column('acl_level', Unicode(128), nullable=True)
3328 3330
3329 3331 owner = relationship('User')
3330 3332
3331 3333 def __repr__(self):
3332 3334 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3333 3335
3334 3336 @classmethod
3335 3337 def get_or_404(cls, id_):
3336 3338 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3337 3339 if not res:
3338 3340 raise HTTPNotFound
3339 3341 return res
3340 3342
3341 3343 @classmethod
3342 3344 def get_by_access_id(cls, gist_access_id):
3343 3345 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3344 3346
3345 3347 def gist_url(self):
3346 3348 import rhodecode
3347 3349 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3348 3350 if alias_url:
3349 3351 return alias_url.replace('{gistid}', self.gist_access_id)
3350 3352
3351 3353 return url('gist', gist_id=self.gist_access_id, qualified=True)
3352 3354
3353 3355 @classmethod
3354 3356 def base_path(cls):
3355 3357 """
3356 3358 Returns base path when all gists are stored
3357 3359
3358 3360 :param cls:
3359 3361 """
3360 3362 from rhodecode.model.gist import GIST_STORE_LOC
3361 3363 q = Session().query(RhodeCodeUi)\
3362 3364 .filter(RhodeCodeUi.ui_key == URL_SEP)
3363 3365 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3364 3366 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3365 3367
3366 3368 def get_api_data(self):
3367 3369 """
3368 3370 Common function for generating gist related data for API
3369 3371 """
3370 3372 gist = self
3371 3373 data = {
3372 3374 'gist_id': gist.gist_id,
3373 3375 'type': gist.gist_type,
3374 3376 'access_id': gist.gist_access_id,
3375 3377 'description': gist.gist_description,
3376 3378 'url': gist.gist_url(),
3377 3379 'expires': gist.gist_expires,
3378 3380 'created_on': gist.created_on,
3379 3381 'modified_at': gist.modified_at,
3380 3382 'content': None,
3381 3383 'acl_level': gist.acl_level,
3382 3384 }
3383 3385 return data
3384 3386
3385 3387 def __json__(self):
3386 3388 data = dict(
3387 3389 )
3388 3390 data.update(self.get_api_data())
3389 3391 return data
3390 3392 # SCM functions
3391 3393
3392 3394 def scm_instance(self, **kwargs):
3393 3395 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3394 3396 return get_vcs_instance(
3395 3397 repo_path=safe_str(full_repo_path), create=False)
3396 3398
3397 3399
3398 3400 class DbMigrateVersion(Base, BaseModel):
3399 3401 __tablename__ = 'db_migrate_version'
3400 3402 __table_args__ = (
3401 3403 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3402 3404 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3403 3405 )
3404 3406 repository_id = Column('repository_id', String(250), primary_key=True)
3405 3407 repository_path = Column('repository_path', Text)
3406 3408 version = Column('version', Integer)
3407 3409
3408 3410
3409 3411 class ExternalIdentity(Base, BaseModel):
3410 3412 __tablename__ = 'external_identities'
3411 3413 __table_args__ = (
3412 3414 Index('local_user_id_idx', 'local_user_id'),
3413 3415 Index('external_id_idx', 'external_id'),
3414 3416 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3415 3417 'mysql_charset': 'utf8'})
3416 3418
3417 3419 external_id = Column('external_id', Unicode(255), default=u'',
3418 3420 primary_key=True)
3419 3421 external_username = Column('external_username', Unicode(1024), default=u'')
3420 3422 local_user_id = Column('local_user_id', Integer(),
3421 3423 ForeignKey('users.user_id'), primary_key=True)
3422 3424 provider_name = Column('provider_name', Unicode(255), default=u'',
3423 3425 primary_key=True)
3424 3426 access_token = Column('access_token', String(1024), default=u'')
3425 3427 alt_token = Column('alt_token', String(1024), default=u'')
3426 3428 token_secret = Column('token_secret', String(1024), default=u'')
3427 3429
3428 3430 @classmethod
3429 3431 def by_external_id_and_provider(cls, external_id, provider_name,
3430 3432 local_user_id=None):
3431 3433 """
3432 3434 Returns ExternalIdentity instance based on search params
3433 3435
3434 3436 :param external_id:
3435 3437 :param provider_name:
3436 3438 :return: ExternalIdentity
3437 3439 """
3438 3440 query = cls.query()
3439 3441 query = query.filter(cls.external_id == external_id)
3440 3442 query = query.filter(cls.provider_name == provider_name)
3441 3443 if local_user_id:
3442 3444 query = query.filter(cls.local_user_id == local_user_id)
3443 3445 return query.first()
3444 3446
3445 3447 @classmethod
3446 3448 def user_by_external_id_and_provider(cls, external_id, provider_name):
3447 3449 """
3448 3450 Returns User instance based on search params
3449 3451
3450 3452 :param external_id:
3451 3453 :param provider_name:
3452 3454 :return: User
3453 3455 """
3454 3456 query = User.query()
3455 3457 query = query.filter(cls.external_id == external_id)
3456 3458 query = query.filter(cls.provider_name == provider_name)
3457 3459 query = query.filter(User.user_id == cls.local_user_id)
3458 3460 return query.first()
3459 3461
3460 3462 @classmethod
3461 3463 def by_local_user_id(cls, local_user_id):
3462 3464 """
3463 3465 Returns all tokens for user
3464 3466
3465 3467 :param local_user_id:
3466 3468 :return: ExternalIdentity
3467 3469 """
3468 3470 query = cls.query()
3469 3471 query = query.filter(cls.local_user_id == local_user_id)
3470 3472 return query
3471 3473
3472 3474
3473 3475 class Integration(Base, BaseModel):
3474 3476 __tablename__ = 'integrations'
3475 3477 __table_args__ = (
3476 3478 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3477 3479 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3478 3480 )
3479 3481
3480 3482 integration_id = Column('integration_id', Integer(), primary_key=True)
3481 3483 integration_type = Column('integration_type', String(255))
3482 3484 enabled = Column('enabled', Boolean(), nullable=False)
3483 3485 name = Column('name', String(255), nullable=False)
3486 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3487 default=False)
3484 3488
3485 3489 settings = Column(
3486 3490 'settings_json', MutationObj.as_mutable(
3487 3491 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3488 3492 repo_id = Column(
3489 3493 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3490 3494 nullable=True, unique=None, default=None)
3491 3495 repo = relationship('Repository', lazy='joined')
3492 3496
3493 3497 repo_group_id = Column(
3494 3498 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3495 3499 nullable=True, unique=None, default=None)
3496 3500 repo_group = relationship('RepoGroup', lazy='joined')
3497 3501
3498 def __repr__(self):
3502 @hybrid_property
3503 def scope(self):
3499 3504 if self.repo:
3500 scope = 'repo=%r' % self.repo
3501 elif self.repo_group:
3502 scope = 'repo_group=%r' % self.repo_group
3505 return self.repo
3506 if self.repo_group:
3507 return self.repo_group
3508 if self.child_repos_only:
3509 return 'root_repos'
3510 return 'global'
3511
3512 @scope.setter
3513 def scope(self, value):
3514 self.repo = None
3515 self.repo_id = None
3516 self.repo_group_id = None
3517 self.repo_group = None
3518 self.child_repos_only = False
3519 if isinstance(value, Repository):
3520 self.repo_id = value.repo_id
3521 self.repo = value
3522 elif isinstance(value, RepoGroup):
3523 self.repo_group_id = value.group_id
3524 self.repo_group = value
3525 elif value == 'root_repos':
3526 self.child_repos_only = True
3527 elif value == 'global':
3528 pass
3503 3529 else:
3504 scope = 'global'
3505
3506 return '<Integration(%r, %r)>' % (self.integration_type, scope)
3530 raise Exception("invalid scope: %s, must be one of "
3531 "['global', 'root_repos', <RepoGroup>. <Repository>]" % value)
3532
3533 def __repr__(self):
3534 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
@@ -1,140 +1,213 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 Model for integrations
24 24 """
25 25
26 26
27 27 import logging
28 28 import traceback
29 29
30 30 from pylons import tmpl_context as c
31 31 from pylons.i18n.translation import _, ungettext
32 from sqlalchemy import or_
32 from sqlalchemy import or_, and_
33 33 from sqlalchemy.sql.expression import false, true
34 34 from mako import exceptions
35 35
36 36 import rhodecode
37 37 from rhodecode import events
38 38 from rhodecode.lib import helpers as h
39 39 from rhodecode.lib.caching_query import FromCache
40 40 from rhodecode.lib.utils import PartialRenderer
41 41 from rhodecode.model import BaseModel
42 from rhodecode.model.db import Integration, User
42 from rhodecode.model.db import Integration, User, Repository, RepoGroup
43 43 from rhodecode.model.meta import Session
44 44 from rhodecode.integrations import integration_type_registry
45 45 from rhodecode.integrations.types.base import IntegrationTypeBase
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class IntegrationModel(BaseModel):
51 51
52 52 cls = Integration
53 53
54 54 def __get_integration(self, integration):
55 55 if isinstance(integration, Integration):
56 56 return integration
57 57 elif isinstance(integration, (int, long)):
58 58 return self.sa.query(Integration).get(integration)
59 59 else:
60 60 if integration:
61 61 raise Exception('integration must be int, long or Instance'
62 62 ' of Integration got %s' % type(integration))
63 63
64 def create(self, IntegrationType, enabled, name, settings, repo=None):
64 def create(self, IntegrationType, name, enabled, scope, settings):
65 65 """ Create an IntegrationType integration """
66 66 integration = Integration()
67 67 integration.integration_type = IntegrationType.key
68 integration.settings = {}
69 integration.repo = repo
70 integration.enabled = enabled
71 integration.name = name
72
73 68 self.sa.add(integration)
69 self.update_integration(integration, name, enabled, scope, settings)
74 70 self.sa.commit()
75 71 return integration
76 72
73 def update_integration(self, integration, name, enabled, scope, settings):
74 """
75 :param scope: one of ['global', 'root_repos', <RepoGroup>. <Repository>]
76 """
77
78 integration = self.__get_integration(integration)
79
80 integration.scope = scope
81 integration.name = name
82 integration.enabled = enabled
83 integration.settings = settings
84
85 return integration
86
77 87 def delete(self, integration):
78 try:
79 88 integration = self.__get_integration(integration)
80 89 if integration:
81 90 self.sa.delete(integration)
82 91 return True
83 except Exception:
84 log.error(traceback.format_exc())
85 raise
86 92 return False
87 93
88 94 def get_integration_handler(self, integration):
89 95 TypeClass = integration_type_registry.get(integration.integration_type)
90 96 if not TypeClass:
91 97 log.error('No class could be found for integration type: {}'.format(
92 98 integration.integration_type))
93 99 return None
94 100
95 101 return TypeClass(integration.settings)
96 102
97 103 def send_event(self, integration, event):
98 104 """ Send an event to an integration """
99 105 handler = self.get_integration_handler(integration)
100 106 if handler:
101 107 handler.send_event(event)
102 108
103 def get_integrations(self, repo=None, repo_group=None):
104 if repo:
105 return self.sa.query(Integration).filter(
106 Integration.repo_id==repo.repo_id).all()
107 elif repo_group:
108 return self.sa.query(Integration).filter(
109 Integration.repo_group_id==repo_group.group_id).all()
109 def get_integrations(self, scope, IntegrationType=None):
110 """
111 Return integrations for a scope, which must be one of:
112
113 'all' - every integration, global/repogroup/repo
114 'global' - global integrations only
115 <Repository> instance - integrations for this repo only
116 <RepoGroup> instance - integrations for this repogroup only
117 """
110 118
119 if isinstance(scope, Repository):
120 query = self.sa.query(Integration).filter(
121 Integration.repo==scope)
122 elif isinstance(scope, RepoGroup):
123 query = self.sa.query(Integration).filter(
124 Integration.repo_group==scope)
125 elif scope == 'global':
111 126 # global integrations
112 return self.sa.query(Integration).filter(
113 Integration.repo_id==None).all()
127 query = self.sa.query(Integration).filter(
128 and_(Integration.repo_id==None, Integration.repo_group_id==None)
129 )
130 elif scope == 'root_repos':
131 query = self.sa.query(Integration).filter(
132 and_(Integration.repo_id==None,
133 Integration.repo_group_id==None,
134 Integration.child_repos_only==True)
135 )
136 elif scope == 'all':
137 query = self.sa.query(Integration)
138 else:
139 raise Exception(
140 "invalid `scope`, must be one of: "
141 "['global', 'all', <Repository>, <RepoGroup>]")
142
143 if IntegrationType is not None:
144 query = query.filter(
145 Integration.integration_type==IntegrationType.key)
146
147 result = []
148 for integration in query.all():
149 IntType = integration_type_registry.get(integration.integration_type)
150 result.append((IntType, integration))
151 return result
114 152
115 153 def get_for_event(self, event, cache=False):
116 154 """
117 155 Get integrations that match an event
118 156 """
119 query = self.sa.query(Integration).filter(Integration.enabled==True)
157 query = self.sa.query(
158 Integration
159 ).filter(
160 Integration.enabled==True
161 )
162
163 global_integrations_filter = and_(
164 Integration.repo_id==None,
165 Integration.repo_group_id==None,
166 Integration.child_repos_only==False,
167 )
168
169 if isinstance(event, events.RepoEvent):
170 root_repos_integrations_filter = and_(
171 Integration.repo_id==None,
172 Integration.repo_group_id==None,
173 Integration.child_repos_only==True,
174 )
175
176 clauses = [
177 global_integrations_filter,
178 ]
120 179
121 if isinstance(event, events.RepoEvent): # global + repo integrations
122 # + repo_group integrations
123 parent_groups = event.repo.groups_with_parents
124 query = query.filter(
125 or_(Integration.repo_id==None,
126 Integration.repo_id==event.repo.repo_id,
127 Integration.repo_group_id.in_(
128 [group.group_id for group in parent_groups]
129 )))
180 # repo integrations
181 if event.repo.repo_id: # pre create events dont have a repo_id yet
182 clauses.append(
183 Integration.repo_id==event.repo.repo_id
184 )
185
186 if event.repo.group:
187 clauses.append(
188 Integration.repo_group_id == event.repo.group.group_id
189 )
190 # repo group cascade to kids (maybe implement this sometime?)
191 # clauses.append(Integration.repo_group_id.in_(
192 # [group.group_id for group in
193 # event.repo.groups_with_parents]
194 # ))
195
196
197 if not event.repo.group: # root repo
198 clauses.append(root_repos_integrations_filter)
199
200 query = query.filter(or_(*clauses))
201
130 202 if cache:
131 203 query = query.options(FromCache(
132 204 "sql_cache_short",
133 205 "get_enabled_repo_integrations_%i" % event.repo.repo_id))
134 206 else: # only global integrations
135 query = query.filter(Integration.repo_id==None)
207 query = query.filter(global_integrations_filter)
136 208 if cache:
137 209 query = query.options(FromCache(
138 210 "sql_cache_short", "get_enabled_global_integrations"))
139 211
140 return query.all()
212 result = query.all()
213 return result No newline at end of file
@@ -1,656 +1,659 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2011-2016 RhodeCode GmbH
4 4 #
5 5 # This program is free software: you can redistribute it and/or modify
6 6 # it under the terms of the GNU Affero General Public License, version 3
7 7 # (only), as published by the Free Software Foundation.
8 8 #
9 9 # This program is distributed in the hope that it will be useful,
10 10 # but WITHOUT ANY WARRANTY; without even the implied warranty of
11 11 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12 12 # GNU General Public License for more details.
13 13 #
14 14 # You should have received a copy of the GNU Affero General Public License
15 15 # along with this program. If not, see <http://www.gnu.org/licenses/>.
16 16 #
17 17 # This program is dual-licensed. If you wish to learn more about the
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 21
22 22 """
23 23 repo group model for RhodeCode
24 24 """
25 25
26 26
27 27 import datetime
28 28 import itertools
29 29 import logging
30 30 import os
31 31 import shutil
32 32 import traceback
33 33
34 34 from zope.cachedescriptors.property import Lazy as LazyProperty
35 35
36 36 from rhodecode import events
37 37 from rhodecode.model import BaseModel
38 38 from rhodecode.model.db import (
39 39 RepoGroup, UserRepoGroupToPerm, User, Permission, UserGroupRepoGroupToPerm,
40 40 UserGroup, Repository)
41 41 from rhodecode.model.settings import VcsSettingsModel
42 42 from rhodecode.lib.caching_query import FromCache
43 43 from rhodecode.lib.utils2 import action_logger_generic
44 44
45 45 log = logging.getLogger(__name__)
46 46
47 47
48 48 class RepoGroupModel(BaseModel):
49 49
50 50 cls = RepoGroup
51 51 PERSONAL_GROUP_DESC = '[personal] repo group: owner `%(username)s`'
52 52
53 53 def _get_user_group(self, users_group):
54 54 return self._get_instance(UserGroup, users_group,
55 55 callback=UserGroup.get_by_group_name)
56 56
57 57 def _get_repo_group(self, repo_group):
58 58 return self._get_instance(RepoGroup, repo_group,
59 59 callback=RepoGroup.get_by_group_name)
60 60
61 61 @LazyProperty
62 62 def repos_path(self):
63 63 """
64 64 Gets the repositories root path from database
65 65 """
66 66
67 67 settings_model = VcsSettingsModel(sa=self.sa)
68 68 return settings_model.get_repos_location()
69 69
70 70 def get_by_group_name(self, repo_group_name, cache=None):
71 71 repo = self.sa.query(RepoGroup) \
72 72 .filter(RepoGroup.group_name == repo_group_name)
73 73
74 74 if cache:
75 75 repo = repo.options(FromCache(
76 76 "sql_cache_short", "get_repo_group_%s" % repo_group_name))
77 77 return repo.scalar()
78 78
79 79 def _create_default_perms(self, new_group):
80 80 # create default permission
81 81 default_perm = 'group.read'
82 82 def_user = User.get_default_user()
83 83 for p in def_user.user_perms:
84 84 if p.permission.permission_name.startswith('group.'):
85 85 default_perm = p.permission.permission_name
86 86 break
87 87
88 88 repo_group_to_perm = UserRepoGroupToPerm()
89 89 repo_group_to_perm.permission = Permission.get_by_key(default_perm)
90 90
91 91 repo_group_to_perm.group = new_group
92 92 repo_group_to_perm.user_id = def_user.user_id
93 93 return repo_group_to_perm
94 94
95 95 def _get_group_name_and_parent(self, group_name_full, repo_in_path=False):
96 96 """
97 97 Get's the group name and a parent group name from given group name.
98 98 If repo_in_path is set to truth, we asume the full path also includes
99 99 repo name, in such case we clean the last element.
100 100
101 101 :param group_name_full:
102 102 """
103 103 split_paths = 1
104 104 if repo_in_path:
105 105 split_paths = 2
106 106 _parts = group_name_full.rsplit(RepoGroup.url_sep(), split_paths)
107 107
108 108 if repo_in_path and len(_parts) > 1:
109 109 # such case last element is the repo_name
110 110 _parts.pop(-1)
111 111 group_name_cleaned = _parts[-1] # just the group name
112 112 parent_repo_group_name = None
113 113
114 114 if len(_parts) > 1:
115 115 parent_repo_group_name = _parts[0]
116 116
117 117 if parent_repo_group_name:
118 118 parent_group = RepoGroup.get_by_group_name(parent_repo_group_name)
119 119
120 120 return group_name_cleaned, parent_repo_group_name
121 121
122 122 def check_exist_filesystem(self, group_name, exc_on_failure=True):
123 123 create_path = os.path.join(self.repos_path, group_name)
124 124 log.debug('creating new group in %s', create_path)
125 125
126 126 if os.path.isdir(create_path):
127 127 if exc_on_failure:
128 128 raise Exception('That directory already exists !')
129 129 return False
130 130 return True
131 131
132 132 def _create_group(self, group_name):
133 133 """
134 134 makes repository group on filesystem
135 135
136 136 :param repo_name:
137 137 :param parent_id:
138 138 """
139 139
140 140 self.check_exist_filesystem(group_name)
141 141 create_path = os.path.join(self.repos_path, group_name)
142 142 log.debug('creating new group in %s', create_path)
143 143 os.makedirs(create_path, mode=0755)
144 144 log.debug('created group in %s', create_path)
145 145
146 146 def _rename_group(self, old, new):
147 147 """
148 148 Renames a group on filesystem
149 149
150 150 :param group_name:
151 151 """
152 152
153 153 if old == new:
154 154 log.debug('skipping group rename')
155 155 return
156 156
157 157 log.debug('renaming repository group from %s to %s', old, new)
158 158
159 159 old_path = os.path.join(self.repos_path, old)
160 160 new_path = os.path.join(self.repos_path, new)
161 161
162 162 log.debug('renaming repos paths from %s to %s', old_path, new_path)
163 163
164 164 if os.path.isdir(new_path):
165 165 raise Exception('Was trying to rename to already '
166 166 'existing dir %s' % new_path)
167 167 shutil.move(old_path, new_path)
168 168
169 169 def _delete_filesystem_group(self, group, force_delete=False):
170 170 """
171 171 Deletes a group from a filesystem
172 172
173 173 :param group: instance of group from database
174 174 :param force_delete: use shutil rmtree to remove all objects
175 175 """
176 176 paths = group.full_path.split(RepoGroup.url_sep())
177 177 paths = os.sep.join(paths)
178 178
179 179 rm_path = os.path.join(self.repos_path, paths)
180 180 log.info("Removing group %s", rm_path)
181 181 # delete only if that path really exists
182 182 if os.path.isdir(rm_path):
183 183 if force_delete:
184 184 shutil.rmtree(rm_path)
185 185 else:
186 186 # archive that group`
187 187 _now = datetime.datetime.now()
188 188 _ms = str(_now.microsecond).rjust(6, '0')
189 189 _d = 'rm__%s_GROUP_%s' % (
190 190 _now.strftime('%Y%m%d_%H%M%S_' + _ms), group.name)
191 191 shutil.move(rm_path, os.path.join(self.repos_path, _d))
192 192
193 193 def create(self, group_name, group_description, owner, just_db=False,
194 194 copy_permissions=False, commit_early=True):
195 195
196 196 (group_name_cleaned,
197 197 parent_group_name) = RepoGroupModel()._get_group_name_and_parent(group_name)
198 198
199 199 parent_group = None
200 200 if parent_group_name:
201 201 parent_group = self._get_repo_group(parent_group_name)
202 202
203 203 # becase we are doing a cleanup, we need to check if such directory
204 204 # already exists. If we don't do that we can accidentally delete existing
205 205 # directory via cleanup that can cause data issues, since delete does a
206 206 # folder rename to special syntax later cleanup functions can delete this
207 207 cleanup_group = self.check_exist_filesystem(group_name,
208 208 exc_on_failure=False)
209 209 try:
210 210 user = self._get_user(owner)
211 211 new_repo_group = RepoGroup()
212 212 new_repo_group.user = user
213 213 new_repo_group.group_description = group_description or group_name
214 214 new_repo_group.parent_group = parent_group
215 215 new_repo_group.group_name = group_name
216 216
217 217 self.sa.add(new_repo_group)
218 218
219 219 # create an ADMIN permission for owner except if we're super admin,
220 220 # later owner should go into the owner field of groups
221 221 if not user.is_admin:
222 222 self.grant_user_permission(repo_group=new_repo_group,
223 223 user=owner, perm='group.admin')
224 224
225 225 if parent_group and copy_permissions:
226 226 # copy permissions from parent
227 227 user_perms = UserRepoGroupToPerm.query() \
228 228 .filter(UserRepoGroupToPerm.group == parent_group).all()
229 229
230 230 group_perms = UserGroupRepoGroupToPerm.query() \
231 231 .filter(UserGroupRepoGroupToPerm.group == parent_group).all()
232 232
233 233 for perm in user_perms:
234 234 # don't copy over the permission for user who is creating
235 235 # this group, if he is not super admin he get's admin
236 236 # permission set above
237 237 if perm.user != user or user.is_admin:
238 238 UserRepoGroupToPerm.create(
239 239 perm.user, new_repo_group, perm.permission)
240 240
241 241 for perm in group_perms:
242 242 UserGroupRepoGroupToPerm.create(
243 243 perm.users_group, new_repo_group, perm.permission)
244 244 else:
245 245 perm_obj = self._create_default_perms(new_repo_group)
246 246 self.sa.add(perm_obj)
247 247
248 248 # now commit the changes, earlier so we are sure everything is in
249 249 # the database.
250 250 if commit_early:
251 251 self.sa.commit()
252 252 if not just_db:
253 253 self._create_group(new_repo_group.group_name)
254 254
255 255 # trigger the post hook
256 256 from rhodecode.lib.hooks_base import log_create_repository_group
257 257 repo_group = RepoGroup.get_by_group_name(group_name)
258 258 log_create_repository_group(
259 259 created_by=user.username, **repo_group.get_dict())
260 260
261 261 # Trigger create event.
262 262 events.trigger(events.RepoGroupCreateEvent(repo_group))
263 263
264 264 return new_repo_group
265 265 except Exception:
266 266 self.sa.rollback()
267 267 log.exception('Exception occurred when creating repository group, '
268 268 'doing cleanup...')
269 269 # rollback things manually !
270 270 repo_group = RepoGroup.get_by_group_name(group_name)
271 271 if repo_group:
272 272 RepoGroup.delete(repo_group.group_id)
273 273 self.sa.commit()
274 274 if cleanup_group:
275 275 RepoGroupModel()._delete_filesystem_group(repo_group)
276 276 raise
277 277
278 278 def update_permissions(
279 279 self, repo_group, perm_additions=None, perm_updates=None,
280 280 perm_deletions=None, recursive=None, check_perms=True,
281 281 cur_user=None):
282 282 from rhodecode.model.repo import RepoModel
283 283 from rhodecode.lib.auth import HasUserGroupPermissionAny
284 284
285 285 if not perm_additions:
286 286 perm_additions = []
287 287 if not perm_updates:
288 288 perm_updates = []
289 289 if not perm_deletions:
290 290 perm_deletions = []
291 291
292 292 req_perms = ('usergroup.read', 'usergroup.write', 'usergroup.admin')
293 293
294 294 def _set_perm_user(obj, user, perm):
295 295 if isinstance(obj, RepoGroup):
296 296 self.grant_user_permission(
297 297 repo_group=obj, user=user, perm=perm)
298 298 elif isinstance(obj, Repository):
299 299 # private repos will not allow to change the default
300 300 # permissions using recursive mode
301 301 if obj.private and user == User.DEFAULT_USER:
302 302 return
303 303
304 304 # we set group permission but we have to switch to repo
305 305 # permission
306 306 perm = perm.replace('group.', 'repository.')
307 307 RepoModel().grant_user_permission(
308 308 repo=obj, user=user, perm=perm)
309 309
310 310 def _set_perm_group(obj, users_group, perm):
311 311 if isinstance(obj, RepoGroup):
312 312 self.grant_user_group_permission(
313 313 repo_group=obj, group_name=users_group, perm=perm)
314 314 elif isinstance(obj, Repository):
315 315 # we set group permission but we have to switch to repo
316 316 # permission
317 317 perm = perm.replace('group.', 'repository.')
318 318 RepoModel().grant_user_group_permission(
319 319 repo=obj, group_name=users_group, perm=perm)
320 320
321 321 def _revoke_perm_user(obj, user):
322 322 if isinstance(obj, RepoGroup):
323 323 self.revoke_user_permission(repo_group=obj, user=user)
324 324 elif isinstance(obj, Repository):
325 325 RepoModel().revoke_user_permission(repo=obj, user=user)
326 326
327 327 def _revoke_perm_group(obj, user_group):
328 328 if isinstance(obj, RepoGroup):
329 329 self.revoke_user_group_permission(
330 330 repo_group=obj, group_name=user_group)
331 331 elif isinstance(obj, Repository):
332 332 RepoModel().revoke_user_group_permission(
333 333 repo=obj, group_name=user_group)
334 334
335 335 # start updates
336 336 updates = []
337 337 log.debug('Now updating permissions for %s in recursive mode:%s',
338 338 repo_group, recursive)
339 339
340 340 # initialize check function, we'll call that multiple times
341 341 has_group_perm = HasUserGroupPermissionAny(*req_perms)
342 342
343 343 for obj in repo_group.recursive_groups_and_repos():
344 344 # iterated obj is an instance of a repos group or repository in
345 345 # that group, recursive option can be: none, repos, groups, all
346 346 if recursive == 'all':
347 347 obj = obj
348 348 elif recursive == 'repos':
349 349 # skip groups, other than this one
350 350 if isinstance(obj, RepoGroup) and not obj == repo_group:
351 351 continue
352 352 elif recursive == 'groups':
353 353 # skip repos
354 354 if isinstance(obj, Repository):
355 355 continue
356 356 else: # recursive == 'none':
357 357 # DEFAULT option - don't apply to iterated objects
358 358 # also we do a break at the end of this loop. if we are not
359 359 # in recursive mode
360 360 obj = repo_group
361 361
362 362 # update permissions
363 363 for member_id, perm, member_type in perm_updates:
364 364 member_id = int(member_id)
365 365 if member_type == 'user':
366 366 # this updates also current one if found
367 367 _set_perm_user(obj, user=member_id, perm=perm)
368 368 else: # set for user group
369 369 member_name = UserGroup.get(member_id).users_group_name
370 370 if not check_perms or has_group_perm(member_name,
371 371 user=cur_user):
372 372 _set_perm_group(obj, users_group=member_id, perm=perm)
373 373
374 374 # set new permissions
375 375 for member_id, perm, member_type in perm_additions:
376 376 member_id = int(member_id)
377 377 if member_type == 'user':
378 378 _set_perm_user(obj, user=member_id, perm=perm)
379 379 else: # set for user group
380 380 # check if we have permissions to alter this usergroup
381 381 member_name = UserGroup.get(member_id).users_group_name
382 382 if not check_perms or has_group_perm(member_name,
383 383 user=cur_user):
384 384 _set_perm_group(obj, users_group=member_id, perm=perm)
385 385
386 386 # delete permissions
387 387 for member_id, perm, member_type in perm_deletions:
388 388 member_id = int(member_id)
389 389 if member_type == 'user':
390 390 _revoke_perm_user(obj, user=member_id)
391 391 else: # set for user group
392 392 # check if we have permissions to alter this usergroup
393 393 member_name = UserGroup.get(member_id).users_group_name
394 394 if not check_perms or has_group_perm(member_name,
395 395 user=cur_user):
396 396 _revoke_perm_group(obj, user_group=member_id)
397 397
398 398 updates.append(obj)
399 399 # if it's not recursive call for all,repos,groups
400 400 # break the loop and don't proceed with other changes
401 401 if recursive not in ['all', 'repos', 'groups']:
402 402 break
403 403
404 404 return updates
405 405
406 406 def update(self, repo_group, form_data):
407 407 try:
408 408 repo_group = self._get_repo_group(repo_group)
409 409 old_path = repo_group.full_path
410 410
411 411 # change properties
412 412 if 'group_description' in form_data:
413 413 repo_group.group_description = form_data['group_description']
414 414
415 415 if 'enable_locking' in form_data:
416 416 repo_group.enable_locking = form_data['enable_locking']
417 417
418 418 if 'group_parent_id' in form_data:
419 419 parent_group = (
420 420 self._get_repo_group(form_data['group_parent_id']))
421 421 repo_group.group_parent_id = (
422 422 parent_group.group_id if parent_group else None)
423 423 repo_group.parent_group = parent_group
424 424
425 425 # mikhail: to update the full_path, we have to explicitly
426 426 # update group_name
427 427 group_name = form_data.get('group_name', repo_group.name)
428 428 repo_group.group_name = repo_group.get_new_name(group_name)
429 429
430 430 new_path = repo_group.full_path
431 431
432 432 if 'user' in form_data:
433 433 repo_group.user = User.get_by_username(form_data['user'])
434 434
435 435 self.sa.add(repo_group)
436 436
437 437 # iterate over all members of this groups and do fixes
438 438 # set locking if given
439 439 # if obj is a repoGroup also fix the name of the group according
440 440 # to the parent
441 441 # if obj is a Repo fix it's name
442 442 # this can be potentially heavy operation
443 443 for obj in repo_group.recursive_groups_and_repos():
444 444 # set the value from it's parent
445 445 obj.enable_locking = repo_group.enable_locking
446 446 if isinstance(obj, RepoGroup):
447 447 new_name = obj.get_new_name(obj.name)
448 448 log.debug('Fixing group %s to new name %s',
449 449 obj.group_name, new_name)
450 450 obj.group_name = new_name
451 451 elif isinstance(obj, Repository):
452 452 # we need to get all repositories from this new group and
453 453 # rename them accordingly to new group path
454 454 new_name = obj.get_new_name(obj.just_name)
455 455 log.debug('Fixing repo %s to new name %s',
456 456 obj.repo_name, new_name)
457 457 obj.repo_name = new_name
458 458 self.sa.add(obj)
459 459
460 460 self._rename_group(old_path, new_path)
461 461
462 462 # Trigger update event.
463 463 events.trigger(events.RepoGroupUpdateEvent(repo_group))
464 464
465 465 return repo_group
466 466 except Exception:
467 467 log.error(traceback.format_exc())
468 468 raise
469 469
470 470 def delete(self, repo_group, force_delete=False, fs_remove=True):
471 471 repo_group = self._get_repo_group(repo_group)
472 if not repo_group:
473 return False
472 474 try:
473 475 self.sa.delete(repo_group)
474 476 if fs_remove:
475 477 self._delete_filesystem_group(repo_group, force_delete)
476 478 else:
477 479 log.debug('skipping removal from filesystem')
478 480
479 481 # Trigger delete event.
480 482 events.trigger(events.RepoGroupDeleteEvent(repo_group))
483 return True
481 484
482 485 except Exception:
483 486 log.error('Error removing repo_group %s', repo_group)
484 487 raise
485 488
486 489 def grant_user_permission(self, repo_group, user, perm):
487 490 """
488 491 Grant permission for user on given repository group, or update
489 492 existing one if found
490 493
491 494 :param repo_group: Instance of RepoGroup, repositories_group_id,
492 495 or repositories_group name
493 496 :param user: Instance of User, user_id or username
494 497 :param perm: Instance of Permission, or permission_name
495 498 """
496 499
497 500 repo_group = self._get_repo_group(repo_group)
498 501 user = self._get_user(user)
499 502 permission = self._get_perm(perm)
500 503
501 504 # check if we have that permission already
502 505 obj = self.sa.query(UserRepoGroupToPerm)\
503 506 .filter(UserRepoGroupToPerm.user == user)\
504 507 .filter(UserRepoGroupToPerm.group == repo_group)\
505 508 .scalar()
506 509 if obj is None:
507 510 # create new !
508 511 obj = UserRepoGroupToPerm()
509 512 obj.group = repo_group
510 513 obj.user = user
511 514 obj.permission = permission
512 515 self.sa.add(obj)
513 516 log.debug('Granted perm %s to %s on %s', perm, user, repo_group)
514 517 action_logger_generic(
515 518 'granted permission: {} to user: {} on repogroup: {}'.format(
516 519 perm, user, repo_group), namespace='security.repogroup')
517 520 return obj
518 521
519 522 def revoke_user_permission(self, repo_group, user):
520 523 """
521 524 Revoke permission for user on given repository group
522 525
523 526 :param repo_group: Instance of RepoGroup, repositories_group_id,
524 527 or repositories_group name
525 528 :param user: Instance of User, user_id or username
526 529 """
527 530
528 531 repo_group = self._get_repo_group(repo_group)
529 532 user = self._get_user(user)
530 533
531 534 obj = self.sa.query(UserRepoGroupToPerm)\
532 535 .filter(UserRepoGroupToPerm.user == user)\
533 536 .filter(UserRepoGroupToPerm.group == repo_group)\
534 537 .scalar()
535 538 if obj:
536 539 self.sa.delete(obj)
537 540 log.debug('Revoked perm on %s on %s', repo_group, user)
538 541 action_logger_generic(
539 542 'revoked permission from user: {} on repogroup: {}'.format(
540 543 user, repo_group), namespace='security.repogroup')
541 544
542 545 def grant_user_group_permission(self, repo_group, group_name, perm):
543 546 """
544 547 Grant permission for user group on given repository group, or update
545 548 existing one if found
546 549
547 550 :param repo_group: Instance of RepoGroup, repositories_group_id,
548 551 or repositories_group name
549 552 :param group_name: Instance of UserGroup, users_group_id,
550 553 or user group name
551 554 :param perm: Instance of Permission, or permission_name
552 555 """
553 556 repo_group = self._get_repo_group(repo_group)
554 557 group_name = self._get_user_group(group_name)
555 558 permission = self._get_perm(perm)
556 559
557 560 # check if we have that permission already
558 561 obj = self.sa.query(UserGroupRepoGroupToPerm)\
559 562 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
560 563 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
561 564 .scalar()
562 565
563 566 if obj is None:
564 567 # create new
565 568 obj = UserGroupRepoGroupToPerm()
566 569
567 570 obj.group = repo_group
568 571 obj.users_group = group_name
569 572 obj.permission = permission
570 573 self.sa.add(obj)
571 574 log.debug('Granted perm %s to %s on %s', perm, group_name, repo_group)
572 575 action_logger_generic(
573 576 'granted permission: {} to usergroup: {} on repogroup: {}'.format(
574 577 perm, group_name, repo_group), namespace='security.repogroup')
575 578 return obj
576 579
577 580 def revoke_user_group_permission(self, repo_group, group_name):
578 581 """
579 582 Revoke permission for user group on given repository group
580 583
581 584 :param repo_group: Instance of RepoGroup, repositories_group_id,
582 585 or repositories_group name
583 586 :param group_name: Instance of UserGroup, users_group_id,
584 587 or user group name
585 588 """
586 589 repo_group = self._get_repo_group(repo_group)
587 590 group_name = self._get_user_group(group_name)
588 591
589 592 obj = self.sa.query(UserGroupRepoGroupToPerm)\
590 593 .filter(UserGroupRepoGroupToPerm.group == repo_group)\
591 594 .filter(UserGroupRepoGroupToPerm.users_group == group_name)\
592 595 .scalar()
593 596 if obj:
594 597 self.sa.delete(obj)
595 598 log.debug('Revoked perm to %s on %s', repo_group, group_name)
596 599 action_logger_generic(
597 600 'revoked permission from usergroup: {} on repogroup: {}'.format(
598 601 group_name, repo_group), namespace='security.repogroup')
599 602
600 603 def get_repo_groups_as_dict(self, repo_group_list=None, admin=False,
601 604 super_user_actions=False):
602 605
603 606 from rhodecode.lib.utils import PartialRenderer
604 607 _render = PartialRenderer('data_table/_dt_elements.html')
605 608 c = _render.c
606 609 h = _render.h
607 610
608 611 def quick_menu(repo_group_name):
609 612 return _render('quick_repo_group_menu', repo_group_name)
610 613
611 614 def repo_group_lnk(repo_group_name):
612 615 return _render('repo_group_name', repo_group_name)
613 616
614 617 def desc(desc):
615 618 if c.visual.stylify_metatags:
616 619 return h.urlify_text(h.escaped_stylize(h.truncate(desc, 60)))
617 620 else:
618 621 return h.urlify_text(h.html_escape(h.truncate(desc, 60)))
619 622
620 623 def repo_group_actions(repo_group_id, repo_group_name, gr_count):
621 624 return _render(
622 625 'repo_group_actions', repo_group_id, repo_group_name, gr_count)
623 626
624 627 def repo_group_name(repo_group_name, children_groups):
625 628 return _render("repo_group_name", repo_group_name, children_groups)
626 629
627 630 def user_profile(username):
628 631 return _render('user_profile', username)
629 632
630 633 repo_group_data = []
631 634 for group in repo_group_list:
632 635
633 636 row = {
634 637 "menu": quick_menu(group.group_name),
635 638 "name": repo_group_lnk(group.group_name),
636 639 "name_raw": group.group_name,
637 640 "desc": desc(group.group_description),
638 641 "top_level_repos": 0,
639 642 "owner": user_profile(group.user.username)
640 643 }
641 644 if admin:
642 645 repo_count = group.repositories.count()
643 646 children_groups = map(
644 647 h.safe_unicode,
645 648 itertools.chain((g.name for g in group.parents),
646 649 (x.name for x in [group])))
647 650 row.update({
648 651 "action": repo_group_actions(
649 652 group.group_id, group.group_name, repo_count),
650 653 "top_level_repos": repo_count,
651 654 "name": repo_group_name(group.group_name, children_groups),
652 655
653 656 })
654 657 repo_group_data.append(row)
655 658
656 659 return repo_group_data
@@ -1,106 +1,117 b''
1 1 .deform {
2 2
3 3 * {
4 4 box-sizing: border-box;
5 5 }
6 6
7 7 .required:after {
8 8 color: #e32;
9 9 content: '*';
10 10 display:inline;
11 11 }
12 12
13 13 .control-label {
14 14 width: 200px;
15 15 padding: 10px;
16 16 float: left;
17 17 }
18 18 .control-inputs {
19 19 width: 400px;
20 20 float: left;
21 21 }
22 22 .form-group .radio, .form-group .checkbox {
23 23 position: relative;
24 24 display: block;
25 25 /* margin-bottom: 10px; */
26 26 }
27 27
28 28 .form-group {
29 29 clear: left;
30 30 margin-bottom: 20px;
31 31
32 32 &:after { /* clear fix */
33 33 content: " ";
34 34 display: block;
35 35 clear: left;
36 36 }
37 37 }
38 38
39 39 .form-control {
40 40 width: 100%;
41 padding: 0.9em;
42 border: 1px solid #979797;
43 border-radius: 2px;
44 }
45 .form-control.select2-container {
46 padding: 0; /* padding already applied in .drop-menu a */
47 }
48
49 .form-control.readonly {
50 background: #eeeeee;
51 cursor: not-allowed;
41 52 }
42 53
43 54 .error-block {
44 55 color: red;
45 56 margin: 0;
46 57 }
47 58
48 59 .help-block {
49 60 margin: 0;
50 61 }
51 62
52 63 .deform-seq-container .control-inputs {
53 64 width: 100%;
54 65 }
55 66
56 67 .deform-seq-container .deform-seq-item-handle {
57 68 width: 8.3%;
58 69 float: left;
59 70 }
60 71
61 72 .deform-seq-container .deform-seq-item-group {
62 73 width: 91.6%;
63 74 float: left;
64 75 }
65 76
66 77 .form-control {
67 78 input {
68 79 height: 40px;
69 80 }
70 81 input[type=checkbox], input[type=radio] {
71 82 height: auto;
72 83 }
73 84 select {
74 85 height: 40px;
75 86 }
76 87 }
77 88
78 89 .form-control.select2-container {
79 90 height: 40px;
80 91 }
81 92
82 93 .deform-two-field-sequence .deform-seq-container .deform-seq-item label {
83 94 display: none;
84 95 }
85 96 .deform-two-field-sequence .deform-seq-container .deform-seq-item:first-child label {
86 97 display: block;
87 98 }
88 99 .deform-two-field-sequence .deform-seq-container .deform-seq-item .panel-heading {
89 100 display: none;
90 101 }
91 102 .deform-two-field-sequence .deform-seq-container .deform-seq-item.form-group {
92 103 margin: 0;
93 104 }
94 105 .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group .form-group {
95 106 width: 45%; padding: 0 2px; float: left; clear: none;
96 107 }
97 108 .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group > .panel {
98 109 padding: 0;
99 110 margin: 5px 0;
100 111 border: none;
101 112 }
102 113 .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group > .panel > .panel-body {
103 114 padding: 0;
104 115 }
105 116
106 117 }
@@ -1,2109 +1,2147 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 'fonts';
9 9 @import 'variables';
10 10 @import 'bootstrap-variables';
11 11 @import 'form-bootstrap';
12 12 @import 'codemirror';
13 13 @import 'legacy_code_styles';
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
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-family: @text-semibold;
39 39 font-size: 120%;
40 40 color: white;
41 41 background-color: @alert2;
42 42 padding: 5px 0 5px 0;
43 43 }
44 44
45 45 html {
46 46 display: table;
47 47 height: 100%;
48 48 width: 100%;
49 49 }
50 50
51 51 body {
52 52 display: table-cell;
53 53 width: 100%;
54 54 }
55 55
56 56 //--- LAYOUT ------------------//
57 57
58 58 .hidden{
59 59 display: none !important;
60 60 }
61 61
62 62 .box{
63 63 float: left;
64 64 width: 100%;
65 65 }
66 66
67 67 .browser-header {
68 68 clear: both;
69 69 }
70 70 .main {
71 71 clear: both;
72 72 padding:0 0 @pagepadding;
73 73 height: auto;
74 74
75 75 &:after { //clearfix
76 76 content:"";
77 77 clear:both;
78 78 width:100%;
79 79 display:block;
80 80 }
81 81 }
82 82
83 83 .action-link{
84 84 margin-left: @padding;
85 85 padding-left: @padding;
86 86 border-left: @border-thickness solid @border-default-color;
87 87 }
88 88
89 89 input + .action-link, .action-link.first{
90 90 border-left: none;
91 91 }
92 92
93 93 .action-link.last{
94 94 margin-right: @padding;
95 95 padding-right: @padding;
96 96 }
97 97
98 98 .action-link.active,
99 99 .action-link.active a{
100 100 color: @grey4;
101 101 }
102 102
103 103 ul.simple-list{
104 104 list-style: none;
105 105 margin: 0;
106 106 padding: 0;
107 107 }
108 108
109 109 .main-content {
110 110 padding-bottom: @pagepadding;
111 111 }
112 112
113 113 .wrapper {
114 114 position: relative;
115 115 max-width: @wrapper-maxwidth;
116 116 margin: 0 auto;
117 117 }
118 118
119 119 #content {
120 120 clear: both;
121 121 padding: 0 @contentpadding;
122 122 }
123 123
124 124 .advanced-settings-fields{
125 125 input{
126 126 margin-left: @textmargin;
127 127 margin-right: @padding/2;
128 128 }
129 129 }
130 130
131 131 .cs_files_title {
132 132 margin: @pagepadding 0 0;
133 133 }
134 134
135 135 input.inline[type="file"] {
136 136 display: inline;
137 137 }
138 138
139 139 .error_page {
140 140 margin: 10% auto;
141 141
142 142 h1 {
143 143 color: @grey2;
144 144 }
145 145
146 146 .error-branding {
147 147 font-family: @text-semibold;
148 148 color: @grey4;
149 149 }
150 150
151 151 .error_message {
152 152 font-family: @text-regular;
153 153 }
154 154
155 155 .sidebar {
156 156 min-height: 275px;
157 157 margin: 0;
158 158 padding: 0 0 @sidebarpadding @sidebarpadding;
159 159 border: none;
160 160 }
161 161
162 162 .main-content {
163 163 position: relative;
164 164 margin: 0 @sidebarpadding @sidebarpadding;
165 165 padding: 0 0 0 @sidebarpadding;
166 166 border-left: @border-thickness solid @grey5;
167 167
168 168 @media (max-width:767px) {
169 169 clear: both;
170 170 width: 100%;
171 171 margin: 0;
172 172 border: none;
173 173 }
174 174 }
175 175
176 176 .inner-column {
177 177 float: left;
178 178 width: 29.75%;
179 179 min-height: 150px;
180 180 margin: @sidebarpadding 2% 0 0;
181 181 padding: 0 2% 0 0;
182 182 border-right: @border-thickness solid @grey5;
183 183
184 184 @media (max-width:767px) {
185 185 clear: both;
186 186 width: 100%;
187 187 border: none;
188 188 }
189 189
190 190 ul {
191 191 padding-left: 1.25em;
192 192 }
193 193
194 194 &:last-child {
195 195 margin: @sidebarpadding 0 0;
196 196 border: none;
197 197 }
198 198
199 199 h4 {
200 200 margin: 0 0 @padding;
201 201 font-family: @text-semibold;
202 202 }
203 203 }
204 204 }
205 205 .error-page-logo {
206 206 width: 130px;
207 207 height: 160px;
208 208 }
209 209
210 210 // HEADER
211 211 .header {
212 212
213 213 // TODO: johbo: Fix login pages, so that they work without a min-height
214 214 // for the header and then remove the min-height. I chose a smaller value
215 215 // intentionally here to avoid rendering issues in the main navigation.
216 216 min-height: 49px;
217 217
218 218 position: relative;
219 219 vertical-align: bottom;
220 220 padding: 0 @header-padding;
221 221 background-color: @grey2;
222 222 color: @grey5;
223 223
224 224 .title {
225 225 overflow: visible;
226 226 }
227 227
228 228 &:before,
229 229 &:after {
230 230 content: "";
231 231 clear: both;
232 232 width: 100%;
233 233 }
234 234
235 235 // TODO: johbo: Avoids breaking "Repositories" chooser
236 236 .select2-container .select2-choice .select2-arrow {
237 237 display: none;
238 238 }
239 239 }
240 240
241 241 #header-inner {
242 242 &.title {
243 243 margin: 0;
244 244 }
245 245 &:before,
246 246 &:after {
247 247 content: "";
248 248 clear: both;
249 249 }
250 250 }
251 251
252 252 // Gists
253 253 #files_data {
254 254 clear: both; //for firefox
255 255 }
256 256 #gistid {
257 257 margin-right: @padding;
258 258 }
259 259
260 260 // Global Settings Editor
261 261 .textarea.editor {
262 262 float: left;
263 263 position: relative;
264 264 max-width: @texteditor-width;
265 265
266 266 select {
267 267 position: absolute;
268 268 top:10px;
269 269 right:0;
270 270 }
271 271
272 272 .CodeMirror {
273 273 margin: 0;
274 274 }
275 275
276 276 .help-block {
277 277 margin: 0 0 @padding;
278 278 padding:.5em;
279 279 background-color: @grey6;
280 280 }
281 281 }
282 282
283 283 ul.auth_plugins {
284 284 margin: @padding 0 @padding @legend-width;
285 285 padding: 0;
286 286
287 287 li {
288 288 margin-bottom: @padding;
289 289 line-height: 1em;
290 290 list-style-type: none;
291 291
292 292 .auth_buttons .btn {
293 293 margin-right: @padding;
294 294 }
295 295
296 296 &:before { content: none; }
297 297 }
298 298 }
299 299
300 300
301 301 // My Account PR list
302 302
303 303 #show_closed {
304 304 margin: 0 1em 0 0;
305 305 }
306 306
307 307 .pullrequestlist {
308 308 .closed {
309 309 background-color: @grey6;
310 310 }
311 311 .td-status {
312 312 padding-left: .5em;
313 313 }
314 314 .log-container .truncate {
315 315 height: 2.75em;
316 316 white-space: pre-line;
317 317 }
318 318 table.rctable .user {
319 319 padding-left: 0;
320 320 }
321 321 table.rctable {
322 322 td.td-description,
323 323 .rc-user {
324 324 min-width: auto;
325 325 }
326 326 }
327 327 }
328 328
329 329 // Pull Requests
330 330
331 331 .pullrequests_section_head {
332 332 display: block;
333 333 clear: both;
334 334 margin: @padding 0;
335 335 font-family: @text-bold;
336 336 }
337 337
338 338 .pr-origininfo, .pr-targetinfo {
339 339 position: relative;
340 340
341 341 .tag {
342 342 display: inline-block;
343 343 margin: 0 1em .5em 0;
344 344 }
345 345
346 346 .clone-url {
347 347 display: inline-block;
348 348 margin: 0 0 .5em 0;
349 349 padding: 0;
350 350 line-height: 1.2em;
351 351 }
352 352 }
353 353
354 354 .pr-pullinfo {
355 355 clear: both;
356 356 margin: .5em 0;
357 357 }
358 358
359 359 #pr-title-input {
360 360 width: 72%;
361 361 font-size: 1em;
362 362 font-family: @text-bold;
363 363 margin: 0;
364 364 padding: 0 0 0 @padding/4;
365 365 line-height: 1.7em;
366 366 color: @text-color;
367 367 letter-spacing: .02em;
368 368 }
369 369
370 370 #pullrequest_title {
371 371 width: 100%;
372 372 box-sizing: border-box;
373 373 }
374 374
375 375 #pr_open_message {
376 376 border: @border-thickness solid #fff;
377 377 border-radius: @border-radius;
378 378 padding: @padding-large-vertical @padding-large-vertical @padding-large-vertical 0;
379 379 text-align: right;
380 380 overflow: hidden;
381 381 }
382 382
383 383 .pr-submit-button {
384 384 float: right;
385 385 margin: 0 0 0 5px;
386 386 }
387 387
388 388 .pr-spacing-container {
389 389 padding: 20px;
390 390 clear: both
391 391 }
392 392
393 393 #pr-description-input {
394 394 margin-bottom: 0;
395 395 }
396 396
397 397 .pr-description-label {
398 398 vertical-align: top;
399 399 }
400 400
401 401 .perms_section_head {
402 402 min-width: 625px;
403 403
404 404 h2 {
405 405 margin-bottom: 0;
406 406 }
407 407
408 408 .label-checkbox {
409 409 float: left;
410 410 }
411 411
412 412 &.field {
413 413 margin: @space 0 @padding;
414 414 }
415 415
416 416 &:first-child.field {
417 417 margin-top: 0;
418 418
419 419 .label {
420 420 margin-top: 0;
421 421 padding-top: 0;
422 422 }
423 423
424 424 .radios {
425 425 padding-top: 0;
426 426 }
427 427 }
428 428
429 429 .radios {
430 430 float: right;
431 431 position: relative;
432 432 width: 405px;
433 433 }
434 434 }
435 435
436 436 //--- MODULES ------------------//
437 437
438 438
439 439 // Fixed Sidebar Column
440 440 .sidebar-col-wrapper {
441 441 padding-left: @sidebar-all-width;
442 442
443 443 .sidebar {
444 444 width: @sidebar-width;
445 445 margin-left: -@sidebar-all-width;
446 446 }
447 447 }
448 448
449 449 .sidebar-col-wrapper.scw-small {
450 450 padding-left: @sidebar-small-all-width;
451 451
452 452 .sidebar {
453 453 width: @sidebar-small-width;
454 454 margin-left: -@sidebar-small-all-width;
455 455 }
456 456 }
457 457
458 458
459 459 // FOOTER
460 460 #footer {
461 461 padding: 0;
462 462 text-align: center;
463 463 vertical-align: middle;
464 464 color: @grey2;
465 465 background-color: @grey6;
466 466
467 467 p {
468 468 margin: 0;
469 469 padding: 1em;
470 470 line-height: 1em;
471 471 }
472 472
473 473 .server-instance { //server instance
474 474 display: none;
475 475 }
476 476
477 477 .title {
478 478 float: none;
479 479 margin: 0 auto;
480 480 }
481 481 }
482 482
483 483 button.close {
484 484 padding: 0;
485 485 cursor: pointer;
486 486 background: transparent;
487 487 border: 0;
488 488 .box-shadow(none);
489 489 -webkit-appearance: none;
490 490 }
491 491
492 492 .close {
493 493 float: right;
494 494 font-size: 21px;
495 495 font-family: @text-bootstrap;
496 496 line-height: 1em;
497 497 font-weight: bold;
498 498 color: @grey2;
499 499
500 500 &:hover,
501 501 &:focus {
502 502 color: @grey1;
503 503 text-decoration: none;
504 504 cursor: pointer;
505 505 }
506 506 }
507 507
508 508 // GRID
509 509 .sorting,
510 510 .sorting_desc,
511 511 .sorting_asc {
512 512 cursor: pointer;
513 513 }
514 514 .sorting_desc:after {
515 515 content: "\00A0\25B2";
516 516 font-size: .75em;
517 517 }
518 518 .sorting_asc:after {
519 519 content: "\00A0\25BC";
520 520 font-size: .68em;
521 521 }
522 522
523 523
524 524 .user_auth_tokens {
525 525
526 526 &.truncate {
527 527 white-space: nowrap;
528 528 overflow: hidden;
529 529 text-overflow: ellipsis;
530 530 }
531 531
532 532 .fields .field .input {
533 533 margin: 0;
534 534 }
535 535
536 536 input#description {
537 537 width: 100px;
538 538 margin: 0;
539 539 }
540 540
541 541 .drop-menu {
542 542 // TODO: johbo: Remove this, should work out of the box when
543 543 // having multiple inputs inline
544 544 margin: 0 0 0 5px;
545 545 }
546 546 }
547 547 #user_list_table {
548 548 .closed {
549 549 background-color: @grey6;
550 550 }
551 551 }
552 552
553 553
554 554 input {
555 555 &.disabled {
556 556 opacity: .5;
557 557 }
558 558 }
559 559
560 560 // remove extra padding in firefox
561 561 input::-moz-focus-inner { border:0; padding:0 }
562 562
563 563 .adjacent input {
564 564 margin-bottom: @padding;
565 565 }
566 566
567 567 .permissions_boxes {
568 568 display: block;
569 569 }
570 570
571 571 //TODO: lisa: this should be in tables
572 572 .show_more_col {
573 573 width: 20px;
574 574 }
575 575
576 576 //FORMS
577 577
578 578 .medium-inline,
579 579 input#description.medium-inline {
580 580 display: inline;
581 581 width: @medium-inline-input-width;
582 582 min-width: 100px;
583 583 }
584 584
585 585 select {
586 586 //reset
587 587 -webkit-appearance: none;
588 588 -moz-appearance: none;
589 589
590 590 display: inline-block;
591 591 height: 28px;
592 592 width: auto;
593 593 margin: 0 @padding @padding 0;
594 594 padding: 0 18px 0 8px;
595 595 line-height:1em;
596 596 font-size: @basefontsize;
597 597 border: @border-thickness solid @rcblue;
598 598 background:white url("../images/dt-arrow-dn.png") no-repeat 100% 50%;
599 599 color: @rcblue;
600 600
601 601 &:after {
602 602 content: "\00A0\25BE";
603 603 }
604 604
605 605 &:focus {
606 606 outline: none;
607 607 }
608 608 }
609 609
610 610 option {
611 611 &:focus {
612 612 outline: none;
613 613 }
614 614 }
615 615
616 616 input,
617 617 textarea {
618 618 padding: @input-padding;
619 619 border: @input-border-thickness solid @border-highlight-color;
620 620 .border-radius (@border-radius);
621 621 font-family: @text-light;
622 622 font-size: @basefontsize;
623 623
624 624 &.input-sm {
625 625 padding: 5px;
626 626 }
627 627
628 628 &#description {
629 629 min-width: @input-description-minwidth;
630 630 min-height: 1em;
631 631 padding: 10px;
632 632 }
633 633 }
634 634
635 635 .field-sm {
636 636 input,
637 637 textarea {
638 638 padding: 5px;
639 639 }
640 640 }
641 641
642 642 textarea {
643 643 display: block;
644 644 clear: both;
645 645 width: 100%;
646 646 min-height: 100px;
647 647 margin-bottom: @padding;
648 648 .box-sizing(border-box);
649 649 overflow: auto;
650 650 }
651 651
652 652 label {
653 653 font-family: @text-light;
654 654 }
655 655
656 656 // GRAVATARS
657 657 // centers gravatar on username to the right
658 658
659 659 .gravatar {
660 660 display: inline;
661 661 min-width: 16px;
662 662 min-height: 16px;
663 663 margin: -5px 0;
664 664 padding: 0;
665 665 line-height: 1em;
666 666 border: 1px solid @grey4;
667 667
668 668 &.gravatar-large {
669 669 margin: -0.5em .25em -0.5em 0;
670 670 }
671 671
672 672 & + .user {
673 673 display: inline;
674 674 margin: 0;
675 675 padding: 0 0 0 .17em;
676 676 line-height: 1em;
677 677 }
678 678 }
679 679
680 680 .user-inline-data {
681 681 display: inline-block;
682 682 float: left;
683 683 padding-left: .5em;
684 684 line-height: 1.3em;
685 685 }
686 686
687 687 .rc-user { // gravatar + user wrapper
688 688 float: left;
689 689 position: relative;
690 690 min-width: 100px;
691 691 max-width: 200px;
692 692 min-height: (@gravatar-size + @border-thickness * 2); // account for border
693 693 display: block;
694 694 padding: 0 0 0 (@gravatar-size + @basefontsize/2 + @border-thickness * 2);
695 695
696 696
697 697 .gravatar {
698 698 display: block;
699 699 position: absolute;
700 700 top: 0;
701 701 left: 0;
702 702 min-width: @gravatar-size;
703 703 min-height: @gravatar-size;
704 704 margin: 0;
705 705 }
706 706
707 707 .user {
708 708 display: block;
709 709 max-width: 175px;
710 710 padding-top: 2px;
711 711 overflow: hidden;
712 712 text-overflow: ellipsis;
713 713 }
714 714 }
715 715
716 716 .gist-gravatar,
717 717 .journal_container {
718 718 .gravatar-large {
719 719 margin: 0 .5em -10px 0;
720 720 }
721 721 }
722 722
723 723
724 724 // ADMIN SETTINGS
725 725
726 726 // Tag Patterns
727 727 .tag_patterns {
728 728 .tag_input {
729 729 margin-bottom: @padding;
730 730 }
731 731 }
732 732
733 733 .locked_input {
734 734 position: relative;
735 735
736 736 input {
737 737 display: inline;
738 738 margin-top: 3px;
739 739 }
740 740
741 741 br {
742 742 display: none;
743 743 }
744 744
745 745 .error-message {
746 746 float: left;
747 747 width: 100%;
748 748 }
749 749
750 750 .lock_input_button {
751 751 display: inline;
752 752 }
753 753
754 754 .help-block {
755 755 clear: both;
756 756 }
757 757 }
758 758
759 759 // Notifications
760 760
761 761 .notifications_buttons {
762 762 margin: 0 0 @space 0;
763 763 padding: 0;
764 764
765 765 .btn {
766 766 display: inline-block;
767 767 }
768 768 }
769 769
770 770 .notification-list {
771 771
772 772 div {
773 773 display: inline-block;
774 774 vertical-align: middle;
775 775 }
776 776
777 777 .container {
778 778 display: block;
779 779 margin: 0 0 @padding 0;
780 780 }
781 781
782 782 .delete-notifications {
783 783 margin-left: @padding;
784 784 text-align: right;
785 785 cursor: pointer;
786 786 }
787 787
788 788 .read-notifications {
789 789 margin-left: @padding/2;
790 790 text-align: right;
791 791 width: 35px;
792 792 cursor: pointer;
793 793 }
794 794
795 795 .icon-minus-sign {
796 796 color: @alert2;
797 797 }
798 798
799 799 .icon-ok-sign {
800 800 color: @alert1;
801 801 }
802 802 }
803 803
804 804 .user_settings {
805 805 float: left;
806 806 clear: both;
807 807 display: block;
808 808 width: 100%;
809 809
810 810 .gravatar_box {
811 811 margin-bottom: @padding;
812 812
813 813 &:after {
814 814 content: " ";
815 815 clear: both;
816 816 width: 100%;
817 817 }
818 818 }
819 819
820 820 .fields .field {
821 821 clear: both;
822 822 }
823 823 }
824 824
825 825 .advanced_settings {
826 826 margin-bottom: @space;
827 827
828 828 .help-block {
829 829 margin-left: 0;
830 830 }
831 831
832 832 button + .help-block {
833 833 margin-top: @padding;
834 834 }
835 835 }
836 836
837 837 // admin settings radio buttons and labels
838 838 .label-2 {
839 839 float: left;
840 840 width: @label2-width;
841 841
842 842 label {
843 843 color: @grey1;
844 844 }
845 845 }
846 846 .checkboxes {
847 847 float: left;
848 848 width: @checkboxes-width;
849 849 margin-bottom: @padding;
850 850
851 851 .checkbox {
852 852 width: 100%;
853 853
854 854 label {
855 855 margin: 0;
856 856 padding: 0;
857 857 }
858 858 }
859 859
860 860 .checkbox + .checkbox {
861 861 display: inline-block;
862 862 }
863 863
864 864 label {
865 865 margin-right: 1em;
866 866 }
867 867 }
868 868
869 869 // CHANGELOG
870 870 .container_header {
871 871 float: left;
872 872 display: block;
873 873 width: 100%;
874 874 margin: @padding 0 @padding;
875 875
876 876 #filter_changelog {
877 877 float: left;
878 878 margin-right: @padding;
879 879 }
880 880
881 881 .breadcrumbs_light {
882 882 display: inline-block;
883 883 }
884 884 }
885 885
886 886 .info_box {
887 887 float: right;
888 888 }
889 889
890 890
891 891 #graph_nodes {
892 892 padding-top: 43px;
893 893 }
894 894
895 895 #graph_content{
896 896
897 897 // adjust for table headers so that graph renders properly
898 898 // #graph_nodes padding - table cell padding
899 899 padding-top: (@space - (@basefontsize * 2.4));
900 900
901 901 &.graph_full_width {
902 902 width: 100%;
903 903 max-width: 100%;
904 904 }
905 905 }
906 906
907 907 #graph {
908 908 .flag_status {
909 909 margin: 0;
910 910 }
911 911
912 912 .pagination-left {
913 913 float: left;
914 914 clear: both;
915 915 }
916 916
917 917 .log-container {
918 918 max-width: 345px;
919 919
920 920 .message{
921 921 max-width: 340px;
922 922 }
923 923 }
924 924
925 925 .graph-col-wrapper {
926 926 padding-left: 110px;
927 927
928 928 #graph_nodes {
929 929 width: 100px;
930 930 margin-left: -110px;
931 931 float: left;
932 932 clear: left;
933 933 }
934 934 }
935 935 }
936 936
937 937 #filter_changelog {
938 938 float: left;
939 939 }
940 940
941 941
942 942 //--- THEME ------------------//
943 943
944 944 #logo {
945 945 float: left;
946 946 margin: 9px 0 0 0;
947 947
948 948 .header {
949 949 background-color: transparent;
950 950 }
951 951
952 952 a {
953 953 display: inline-block;
954 954 }
955 955
956 956 img {
957 957 height:30px;
958 958 }
959 959 }
960 960
961 961 .logo-wrapper {
962 962 float:left;
963 963 }
964 964
965 965 .branding{
966 966 float: left;
967 967 padding: 9px 2px;
968 968 line-height: 1em;
969 969 font-size: @navigation-fontsize;
970 970 }
971 971
972 972 img {
973 973 border: none;
974 974 outline: none;
975 975 }
976 976 user-profile-header
977 977 label {
978 978
979 979 input[type="checkbox"] {
980 980 margin-right: 1em;
981 981 }
982 982 input[type="radio"] {
983 983 margin-right: 1em;
984 984 }
985 985 }
986 986
987 987 .flag_status {
988 988 margin: 2px 8px 6px 2px;
989 989 &.under_review {
990 990 .circle(5px, @alert3);
991 991 }
992 992 &.approved {
993 993 .circle(5px, @alert1);
994 994 }
995 995 &.rejected,
996 996 &.forced_closed{
997 997 .circle(5px, @alert2);
998 998 }
999 999 &.not_reviewed {
1000 1000 .circle(5px, @grey5);
1001 1001 }
1002 1002 }
1003 1003
1004 1004 .flag_status_comment_box {
1005 1005 margin: 5px 6px 0px 2px;
1006 1006 }
1007 1007 .test_pattern_preview {
1008 1008 margin: @space 0;
1009 1009
1010 1010 p {
1011 1011 margin-bottom: 0;
1012 1012 border-bottom: @border-thickness solid @border-default-color;
1013 1013 color: @grey3;
1014 1014 }
1015 1015
1016 1016 .btn {
1017 1017 margin-bottom: @padding;
1018 1018 }
1019 1019 }
1020 1020 #test_pattern_result {
1021 1021 display: none;
1022 1022 &:extend(pre);
1023 1023 padding: .9em;
1024 1024 color: @grey3;
1025 1025 background-color: @grey7;
1026 1026 border-right: @border-thickness solid @border-default-color;
1027 1027 border-bottom: @border-thickness solid @border-default-color;
1028 1028 border-left: @border-thickness solid @border-default-color;
1029 1029 }
1030 1030
1031 1031 #repo_vcs_settings {
1032 1032 #inherit_overlay_vcs_default {
1033 1033 display: none;
1034 1034 }
1035 1035 #inherit_overlay_vcs_custom {
1036 1036 display: custom;
1037 1037 }
1038 1038 &.inherited {
1039 1039 #inherit_overlay_vcs_default {
1040 1040 display: block;
1041 1041 }
1042 1042 #inherit_overlay_vcs_custom {
1043 1043 display: none;
1044 1044 }
1045 1045 }
1046 1046 }
1047 1047
1048 1048 .issue-tracker-link {
1049 1049 color: @rcblue;
1050 1050 }
1051 1051
1052 1052 // Issue Tracker Table Show/Hide
1053 1053 #repo_issue_tracker {
1054 1054 #inherit_overlay {
1055 1055 display: none;
1056 1056 }
1057 1057 #custom_overlay {
1058 1058 display: custom;
1059 1059 }
1060 1060 &.inherited {
1061 1061 #inherit_overlay {
1062 1062 display: block;
1063 1063 }
1064 1064 #custom_overlay {
1065 1065 display: none;
1066 1066 }
1067 1067 }
1068 1068 }
1069 1069 table.issuetracker {
1070 1070 &.readonly {
1071 1071 tr, td {
1072 1072 color: @grey3;
1073 1073 }
1074 1074 }
1075 1075 .edit {
1076 1076 display: none;
1077 1077 }
1078 1078 .editopen {
1079 1079 .edit {
1080 1080 display: inline;
1081 1081 }
1082 1082 .entry {
1083 1083 display: none;
1084 1084 }
1085 1085 }
1086 1086 tr td.td-action {
1087 1087 min-width: 117px;
1088 1088 }
1089 1089 td input {
1090 1090 max-width: none;
1091 1091 min-width: 30px;
1092 1092 width: 80%;
1093 1093 }
1094 1094 .issuetracker_pref input {
1095 1095 width: 40%;
1096 1096 }
1097 1097 input.edit_issuetracker_update {
1098 1098 margin-right: 0;
1099 1099 width: auto;
1100 1100 }
1101 1101 }
1102 1102
1103 table.integrations {
1104 .td-icon {
1105 width: 20px;
1106 .integration-icon {
1107 height: 20px;
1108 width: 20px;
1109 }
1110 }
1111 }
1112
1113 .integrations {
1114 a.integration-box {
1115 color: @text-color;
1116 &:hover {
1117 .panel {
1118 background: #fbfbfb;
1119 }
1120 }
1121 .integration-icon {
1122 width: 30px;
1123 height: 30px;
1124 margin-right: 20px;
1125 float: left;
1126 }
1127
1128 .panel-body {
1129 padding: 10px;
1130 }
1131 .panel {
1132 margin-bottom: 10px;
1133 }
1134 h2 {
1135 display: inline-block;
1136 margin: 0;
1137 min-width: 140px;
1138 }
1139 }
1140 }
1103 1141
1104 1142 //Permissions Settings
1105 1143 #add_perm {
1106 1144 margin: 0 0 @padding;
1107 1145 cursor: pointer;
1108 1146 }
1109 1147
1110 1148 .perm_ac {
1111 1149 input {
1112 1150 width: 95%;
1113 1151 }
1114 1152 }
1115 1153
1116 1154 .autocomplete-suggestions {
1117 1155 width: auto !important; // overrides autocomplete.js
1118 1156 margin: 0;
1119 1157 border: @border-thickness solid @rcblue;
1120 1158 border-radius: @border-radius;
1121 1159 color: @rcblue;
1122 1160 background-color: white;
1123 1161 }
1124 1162 .autocomplete-selected {
1125 1163 background: #F0F0F0;
1126 1164 }
1127 1165 .ac-container-wrap {
1128 1166 margin: 0;
1129 1167 padding: 8px;
1130 1168 border-bottom: @border-thickness solid @rclightblue;
1131 1169 list-style-type: none;
1132 1170 cursor: pointer;
1133 1171
1134 1172 &:hover {
1135 1173 background-color: @rclightblue;
1136 1174 }
1137 1175
1138 1176 img {
1139 1177 margin-right: 1em;
1140 1178 }
1141 1179
1142 1180 strong {
1143 1181 font-weight: normal;
1144 1182 }
1145 1183 }
1146 1184
1147 1185 // Settings Dropdown
1148 1186 .user-menu .container {
1149 1187 padding: 0 4px;
1150 1188 margin: 0;
1151 1189 }
1152 1190
1153 1191 .user-menu .gravatar {
1154 1192 cursor: pointer;
1155 1193 }
1156 1194
1157 1195 .codeblock {
1158 1196 margin-bottom: @padding;
1159 1197 clear: both;
1160 1198
1161 1199 .stats{
1162 1200 overflow: hidden;
1163 1201 }
1164 1202
1165 1203 .message{
1166 1204 textarea{
1167 1205 margin: 0;
1168 1206 }
1169 1207 }
1170 1208
1171 1209 .code-header {
1172 1210 .stats {
1173 1211 line-height: 2em;
1174 1212
1175 1213 .revision_id {
1176 1214 margin-left: 0;
1177 1215 }
1178 1216 .buttons {
1179 1217 padding-right: 0;
1180 1218 }
1181 1219 }
1182 1220
1183 1221 .item{
1184 1222 margin-right: 0.5em;
1185 1223 }
1186 1224 }
1187 1225
1188 1226 #editor_container{
1189 1227 position: relative;
1190 1228 margin: @padding;
1191 1229 }
1192 1230 }
1193 1231
1194 1232 #file_history_container {
1195 1233 display: none;
1196 1234 }
1197 1235
1198 1236 .file-history-inner {
1199 1237 margin-bottom: 10px;
1200 1238 }
1201 1239
1202 1240 // Pull Requests
1203 1241 .summary-details {
1204 1242 width: 72%;
1205 1243 }
1206 1244 .pr-summary {
1207 1245 border-bottom: @border-thickness solid @grey5;
1208 1246 margin-bottom: @space;
1209 1247 }
1210 1248 .reviewers-title {
1211 1249 width: 25%;
1212 1250 min-width: 200px;
1213 1251 }
1214 1252 .reviewers {
1215 1253 width: 25%;
1216 1254 min-width: 200px;
1217 1255 }
1218 1256 .reviewers ul li {
1219 1257 position: relative;
1220 1258 width: 100%;
1221 1259 margin-bottom: 8px;
1222 1260 }
1223 1261 .reviewers_member {
1224 1262 width: 100%;
1225 1263 overflow: auto;
1226 1264 }
1227 1265 .reviewer_status {
1228 1266 display: inline-block;
1229 1267 vertical-align: top;
1230 1268 width: 7%;
1231 1269 min-width: 20px;
1232 1270 height: 1.2em;
1233 1271 margin-top: 3px;
1234 1272 line-height: 1em;
1235 1273 }
1236 1274
1237 1275 .reviewer_name {
1238 1276 display: inline-block;
1239 1277 max-width: 83%;
1240 1278 padding-right: 20px;
1241 1279 vertical-align: middle;
1242 1280 line-height: 1;
1243 1281
1244 1282 .rc-user {
1245 1283 min-width: 0;
1246 1284 margin: -2px 1em 0 0;
1247 1285 }
1248 1286
1249 1287 .reviewer {
1250 1288 float: left;
1251 1289 }
1252 1290
1253 1291 &.to-delete {
1254 1292 .user,
1255 1293 .reviewer {
1256 1294 text-decoration: line-through;
1257 1295 }
1258 1296 }
1259 1297 }
1260 1298
1261 1299 .reviewer_member_remove {
1262 1300 position: absolute;
1263 1301 right: 0;
1264 1302 top: 0;
1265 1303 width: 16px;
1266 1304 margin-bottom: 10px;
1267 1305 padding: 0;
1268 1306 color: black;
1269 1307 }
1270 1308 .reviewer_member_status {
1271 1309 margin-top: 5px;
1272 1310 }
1273 1311 .pr-summary #summary{
1274 1312 width: 100%;
1275 1313 }
1276 1314 .pr-summary .action_button:hover {
1277 1315 border: 0;
1278 1316 cursor: pointer;
1279 1317 }
1280 1318 .pr-details-title {
1281 1319 padding-bottom: 8px;
1282 1320 border-bottom: @border-thickness solid @grey5;
1283 1321 .action_button {
1284 1322 color: @rcblue;
1285 1323 }
1286 1324 }
1287 1325 .pr-details-content {
1288 1326 margin-top: @textmargin;
1289 1327 margin-bottom: @textmargin;
1290 1328 }
1291 1329 .pr-description {
1292 1330 white-space:pre-wrap;
1293 1331 }
1294 1332 .group_members {
1295 1333 margin-top: 0;
1296 1334 padding: 0;
1297 1335 list-style: outside none none;
1298 1336 }
1299 1337 .reviewer_ac .ac-input {
1300 1338 width: 92%;
1301 1339 margin-bottom: 1em;
1302 1340 }
1303 1341 #update_commits {
1304 1342 float: right;
1305 1343 }
1306 1344 .compare_view_commits tr{
1307 1345 height: 20px;
1308 1346 }
1309 1347 .compare_view_commits td {
1310 1348 vertical-align: top;
1311 1349 padding-top: 10px;
1312 1350 }
1313 1351 .compare_view_commits .author {
1314 1352 margin-left: 5px;
1315 1353 }
1316 1354
1317 1355 .compare_view_files {
1318 1356 width: 100%;
1319 1357
1320 1358 td {
1321 1359 vertical-align: middle;
1322 1360 }
1323 1361 }
1324 1362
1325 1363 .compare_view_filepath {
1326 1364 color: @grey1;
1327 1365 }
1328 1366
1329 1367 .show_more {
1330 1368 display: inline-block;
1331 1369 position: relative;
1332 1370 vertical-align: middle;
1333 1371 width: 4px;
1334 1372 height: @basefontsize;
1335 1373
1336 1374 &:after {
1337 1375 content: "\00A0\25BE";
1338 1376 display: inline-block;
1339 1377 width:10px;
1340 1378 line-height: 5px;
1341 1379 font-size: 12px;
1342 1380 cursor: pointer;
1343 1381 }
1344 1382 }
1345 1383
1346 1384 .journal_more .show_more {
1347 1385 display: inline;
1348 1386
1349 1387 &:after {
1350 1388 content: none;
1351 1389 }
1352 1390 }
1353 1391
1354 1392 .open .show_more:after,
1355 1393 .select2-dropdown-open .show_more:after {
1356 1394 .rotate(180deg);
1357 1395 margin-left: 4px;
1358 1396 }
1359 1397
1360 1398
1361 1399 .compare_view_commits .collapse_commit:after {
1362 1400 cursor: pointer;
1363 1401 content: "\00A0\25B4";
1364 1402 margin-left: -3px;
1365 1403 font-size: 17px;
1366 1404 color: @grey4;
1367 1405 }
1368 1406
1369 1407 .diff_links {
1370 1408 margin-left: 8px;
1371 1409 }
1372 1410
1373 1411 p.ancestor {
1374 1412 margin: @padding 0;
1375 1413 }
1376 1414
1377 1415 .cs_icon_td input[type="checkbox"] {
1378 1416 display: none;
1379 1417 }
1380 1418
1381 1419 .cs_icon_td .expand_file_icon:after {
1382 1420 cursor: pointer;
1383 1421 content: "\00A0\25B6";
1384 1422 font-size: 12px;
1385 1423 color: @grey4;
1386 1424 }
1387 1425
1388 1426 .cs_icon_td .collapse_file_icon:after {
1389 1427 cursor: pointer;
1390 1428 content: "\00A0\25BC";
1391 1429 font-size: 12px;
1392 1430 color: @grey4;
1393 1431 }
1394 1432
1395 1433 /*new binary
1396 1434 NEW_FILENODE = 1
1397 1435 DEL_FILENODE = 2
1398 1436 MOD_FILENODE = 3
1399 1437 RENAMED_FILENODE = 4
1400 1438 COPIED_FILENODE = 5
1401 1439 CHMOD_FILENODE = 6
1402 1440 BIN_FILENODE = 7
1403 1441 */
1404 1442 .cs_files_expand {
1405 1443 font-size: @basefontsize + 5px;
1406 1444 line-height: 1.8em;
1407 1445 float: right;
1408 1446 }
1409 1447
1410 1448 .cs_files_expand span{
1411 1449 color: @rcblue;
1412 1450 cursor: pointer;
1413 1451 }
1414 1452 .cs_files {
1415 1453 clear: both;
1416 1454 padding-bottom: @padding;
1417 1455
1418 1456 .cur_cs {
1419 1457 margin: 10px 2px;
1420 1458 font-weight: bold;
1421 1459 }
1422 1460
1423 1461 .node {
1424 1462 float: left;
1425 1463 }
1426 1464
1427 1465 .changes {
1428 1466 float: right;
1429 1467 color: white;
1430 1468 font-size: @basefontsize - 4px;
1431 1469 margin-top: 4px;
1432 1470 opacity: 0.6;
1433 1471 filter: Alpha(opacity=60); /* IE8 and earlier */
1434 1472
1435 1473 .added {
1436 1474 background-color: @alert1;
1437 1475 float: left;
1438 1476 text-align: center;
1439 1477 }
1440 1478
1441 1479 .deleted {
1442 1480 background-color: @alert2;
1443 1481 float: left;
1444 1482 text-align: center;
1445 1483 }
1446 1484
1447 1485 .bin {
1448 1486 background-color: @alert1;
1449 1487 text-align: center;
1450 1488 }
1451 1489
1452 1490 /*new binary*/
1453 1491 .bin.bin1 {
1454 1492 background-color: @alert1;
1455 1493 text-align: center;
1456 1494 }
1457 1495
1458 1496 /*deleted binary*/
1459 1497 .bin.bin2 {
1460 1498 background-color: @alert2;
1461 1499 text-align: center;
1462 1500 }
1463 1501
1464 1502 /*mod binary*/
1465 1503 .bin.bin3 {
1466 1504 background-color: @grey2;
1467 1505 text-align: center;
1468 1506 }
1469 1507
1470 1508 /*rename file*/
1471 1509 .bin.bin4 {
1472 1510 background-color: @alert4;
1473 1511 text-align: center;
1474 1512 }
1475 1513
1476 1514 /*copied file*/
1477 1515 .bin.bin5 {
1478 1516 background-color: @alert4;
1479 1517 text-align: center;
1480 1518 }
1481 1519
1482 1520 /*chmod file*/
1483 1521 .bin.bin6 {
1484 1522 background-color: @grey2;
1485 1523 text-align: center;
1486 1524 }
1487 1525 }
1488 1526 }
1489 1527
1490 1528 .cs_files .cs_added, .cs_files .cs_A,
1491 1529 .cs_files .cs_added, .cs_files .cs_M,
1492 1530 .cs_files .cs_added, .cs_files .cs_D {
1493 1531 height: 16px;
1494 1532 padding-right: 10px;
1495 1533 margin-top: 7px;
1496 1534 text-align: left;
1497 1535 }
1498 1536
1499 1537 .cs_icon_td {
1500 1538 min-width: 16px;
1501 1539 width: 16px;
1502 1540 }
1503 1541
1504 1542 .pull-request-merge {
1505 1543 padding: 10px 0;
1506 1544 margin-top: 10px;
1507 1545 margin-bottom: 20px;
1508 1546 }
1509 1547
1510 1548 .pull-request-merge .pull-request-wrap {
1511 1549 height: 25px;
1512 1550 padding: 5px 0;
1513 1551 }
1514 1552
1515 1553 .pull-request-merge span {
1516 1554 margin-right: 10px;
1517 1555 }
1518 1556 #close_pull_request {
1519 1557 margin-right: 0px;
1520 1558 }
1521 1559
1522 1560 .empty_data {
1523 1561 color: @grey4;
1524 1562 }
1525 1563
1526 1564 #changeset_compare_view_content {
1527 1565 margin-bottom: @space;
1528 1566 clear: both;
1529 1567 width: 100%;
1530 1568 box-sizing: border-box;
1531 1569 .border-radius(@border-radius);
1532 1570
1533 1571 .help-block {
1534 1572 margin: @padding 0;
1535 1573 color: @text-color;
1536 1574 }
1537 1575
1538 1576 .empty_data {
1539 1577 margin: @padding 0;
1540 1578 }
1541 1579
1542 1580 .alert {
1543 1581 margin-bottom: @space;
1544 1582 }
1545 1583 }
1546 1584
1547 1585 .table_disp {
1548 1586 .status {
1549 1587 width: auto;
1550 1588
1551 1589 .flag_status {
1552 1590 float: left;
1553 1591 }
1554 1592 }
1555 1593 }
1556 1594
1557 1595 .status_box_menu {
1558 1596 margin: 0;
1559 1597 }
1560 1598
1561 1599 .notification-table{
1562 1600 margin-bottom: @space;
1563 1601 display: table;
1564 1602 width: 100%;
1565 1603
1566 1604 .container{
1567 1605 display: table-row;
1568 1606
1569 1607 .notification-header{
1570 1608 border-bottom: @border-thickness solid @border-default-color;
1571 1609 }
1572 1610
1573 1611 .notification-subject{
1574 1612 display: table-cell;
1575 1613 }
1576 1614 }
1577 1615 }
1578 1616
1579 1617 // Notifications
1580 1618 .notification-header{
1581 1619 display: table;
1582 1620 width: 100%;
1583 1621 padding: floor(@basefontsize/2) 0;
1584 1622 line-height: 1em;
1585 1623
1586 1624 .desc, .delete-notifications, .read-notifications{
1587 1625 display: table-cell;
1588 1626 text-align: left;
1589 1627 }
1590 1628
1591 1629 .desc{
1592 1630 width: 1163px;
1593 1631 }
1594 1632
1595 1633 .delete-notifications, .read-notifications{
1596 1634 width: 35px;
1597 1635 min-width: 35px; //fixes when only one button is displayed
1598 1636 }
1599 1637 }
1600 1638
1601 1639 .notification-body {
1602 1640 .markdown-block,
1603 1641 .rst-block {
1604 1642 padding: @padding 0;
1605 1643 }
1606 1644
1607 1645 .notification-subject {
1608 1646 padding: @textmargin 0;
1609 1647 border-bottom: @border-thickness solid @border-default-color;
1610 1648 }
1611 1649 }
1612 1650
1613 1651
1614 1652 .notifications_buttons{
1615 1653 float: right;
1616 1654 }
1617 1655
1618 1656 #notification-status{
1619 1657 display: inline;
1620 1658 }
1621 1659
1622 1660 // Repositories
1623 1661
1624 1662 #summary.fields{
1625 1663 display: table;
1626 1664
1627 1665 .field{
1628 1666 display: table-row;
1629 1667
1630 1668 .label-summary{
1631 1669 display: table-cell;
1632 1670 min-width: @label-summary-minwidth;
1633 1671 padding-top: @padding/2;
1634 1672 padding-bottom: @padding/2;
1635 1673 padding-right: @padding/2;
1636 1674 }
1637 1675
1638 1676 .input{
1639 1677 display: table-cell;
1640 1678 padding: @padding/2;
1641 1679
1642 1680 input{
1643 1681 min-width: 29em;
1644 1682 padding: @padding/4;
1645 1683 }
1646 1684 }
1647 1685 .statistics, .downloads{
1648 1686 .disabled{
1649 1687 color: @grey4;
1650 1688 }
1651 1689 }
1652 1690 }
1653 1691 }
1654 1692
1655 1693 #summary{
1656 1694 width: 70%;
1657 1695 }
1658 1696
1659 1697
1660 1698 // Journal
1661 1699 .journal.title {
1662 1700 h5 {
1663 1701 float: left;
1664 1702 margin: 0;
1665 1703 width: 70%;
1666 1704 }
1667 1705
1668 1706 ul {
1669 1707 float: right;
1670 1708 display: inline-block;
1671 1709 margin: 0;
1672 1710 width: 30%;
1673 1711 text-align: right;
1674 1712
1675 1713 li {
1676 1714 display: inline;
1677 1715 font-size: @journal-fontsize;
1678 1716 line-height: 1em;
1679 1717
1680 1718 &:before { content: none; }
1681 1719 }
1682 1720 }
1683 1721 }
1684 1722
1685 1723 .filterexample {
1686 1724 position: absolute;
1687 1725 top: 95px;
1688 1726 left: @contentpadding;
1689 1727 color: @rcblue;
1690 1728 font-size: 11px;
1691 1729 font-family: @text-regular;
1692 1730 cursor: help;
1693 1731
1694 1732 &:hover {
1695 1733 color: @rcdarkblue;
1696 1734 }
1697 1735
1698 1736 @media (max-width:768px) {
1699 1737 position: relative;
1700 1738 top: auto;
1701 1739 left: auto;
1702 1740 display: block;
1703 1741 }
1704 1742 }
1705 1743
1706 1744
1707 1745 #journal{
1708 1746 margin-bottom: @space;
1709 1747
1710 1748 .journal_day{
1711 1749 margin-bottom: @textmargin/2;
1712 1750 padding-bottom: @textmargin/2;
1713 1751 font-size: @journal-fontsize;
1714 1752 border-bottom: @border-thickness solid @border-default-color;
1715 1753 }
1716 1754
1717 1755 .journal_container{
1718 1756 margin-bottom: @space;
1719 1757
1720 1758 .journal_user{
1721 1759 display: inline-block;
1722 1760 }
1723 1761 .journal_action_container{
1724 1762 display: block;
1725 1763 margin-top: @textmargin;
1726 1764
1727 1765 div{
1728 1766 display: inline;
1729 1767 }
1730 1768
1731 1769 div.journal_action_params{
1732 1770 display: block;
1733 1771 }
1734 1772
1735 1773 div.journal_repo:after{
1736 1774 content: "\A";
1737 1775 white-space: pre;
1738 1776 }
1739 1777
1740 1778 div.date{
1741 1779 display: block;
1742 1780 margin-bottom: @textmargin;
1743 1781 }
1744 1782 }
1745 1783 }
1746 1784 }
1747 1785
1748 1786 // Files
1749 1787 .edit-file-title {
1750 1788 border-bottom: @border-thickness solid @border-default-color;
1751 1789
1752 1790 .breadcrumbs {
1753 1791 margin-bottom: 0;
1754 1792 }
1755 1793 }
1756 1794
1757 1795 .edit-file-fieldset {
1758 1796 margin-top: @sidebarpadding;
1759 1797
1760 1798 .fieldset {
1761 1799 .left-label {
1762 1800 width: 13%;
1763 1801 }
1764 1802 .right-content {
1765 1803 width: 87%;
1766 1804 max-width: 100%;
1767 1805 }
1768 1806 .filename-label {
1769 1807 margin-top: 13px;
1770 1808 }
1771 1809 .commit-message-label {
1772 1810 margin-top: 4px;
1773 1811 }
1774 1812 .file-upload-input {
1775 1813 input {
1776 1814 display: none;
1777 1815 }
1778 1816 }
1779 1817 p {
1780 1818 margin-top: 5px;
1781 1819 }
1782 1820
1783 1821 }
1784 1822 .custom-path-link {
1785 1823 margin-left: 5px;
1786 1824 }
1787 1825 #commit {
1788 1826 resize: vertical;
1789 1827 }
1790 1828 }
1791 1829
1792 1830 .delete-file-preview {
1793 1831 max-height: 250px;
1794 1832 }
1795 1833
1796 1834 .new-file,
1797 1835 #filter_activate,
1798 1836 #filter_deactivate {
1799 1837 float: left;
1800 1838 margin: 0 0 0 15px;
1801 1839 }
1802 1840
1803 1841 h3.files_location{
1804 1842 line-height: 2.4em;
1805 1843 }
1806 1844
1807 1845 .browser-nav {
1808 1846 display: table;
1809 1847 margin-bottom: @space;
1810 1848
1811 1849
1812 1850 .info_box {
1813 1851 display: inline-table;
1814 1852 height: 2.5em;
1815 1853
1816 1854 .browser-cur-rev, .info_box_elem {
1817 1855 display: table-cell;
1818 1856 vertical-align: middle;
1819 1857 }
1820 1858
1821 1859 .info_box_elem {
1822 1860 border-top: @border-thickness solid @rcblue;
1823 1861 border-bottom: @border-thickness solid @rcblue;
1824 1862
1825 1863 #at_rev, a {
1826 1864 padding: 0.6em 0.9em;
1827 1865 margin: 0;
1828 1866 .box-shadow(none);
1829 1867 border: 0;
1830 1868 height: 12px;
1831 1869 }
1832 1870
1833 1871 input#at_rev {
1834 1872 max-width: 50px;
1835 1873 text-align: right;
1836 1874 }
1837 1875
1838 1876 &.previous {
1839 1877 border: @border-thickness solid @rcblue;
1840 1878 .disabled {
1841 1879 color: @grey4;
1842 1880 cursor: not-allowed;
1843 1881 }
1844 1882 }
1845 1883
1846 1884 &.next {
1847 1885 border: @border-thickness solid @rcblue;
1848 1886 .disabled {
1849 1887 color: @grey4;
1850 1888 cursor: not-allowed;
1851 1889 }
1852 1890 }
1853 1891 }
1854 1892
1855 1893 .browser-cur-rev {
1856 1894
1857 1895 span{
1858 1896 margin: 0;
1859 1897 color: @rcblue;
1860 1898 height: 12px;
1861 1899 display: inline-block;
1862 1900 padding: 0.7em 1em ;
1863 1901 border: @border-thickness solid @rcblue;
1864 1902 margin-right: @padding;
1865 1903 }
1866 1904 }
1867 1905 }
1868 1906
1869 1907 .search_activate {
1870 1908 display: table-cell;
1871 1909 vertical-align: middle;
1872 1910
1873 1911 input, label{
1874 1912 margin: 0;
1875 1913 padding: 0;
1876 1914 }
1877 1915
1878 1916 input{
1879 1917 margin-left: @textmargin;
1880 1918 }
1881 1919
1882 1920 }
1883 1921 }
1884 1922
1885 1923 .browser-cur-rev{
1886 1924 margin-bottom: @textmargin;
1887 1925 }
1888 1926
1889 1927 #node_filter_box_loading{
1890 1928 .info_text;
1891 1929 }
1892 1930
1893 1931 .browser-search {
1894 1932 margin: -25px 0px 5px 0px;
1895 1933 }
1896 1934
1897 1935 .node-filter {
1898 1936 font-size: @repo-title-fontsize;
1899 1937 padding: 4px 0px 0px 0px;
1900 1938
1901 1939 .node-filter-path {
1902 1940 float: left;
1903 1941 color: @grey4;
1904 1942 }
1905 1943 .node-filter-input {
1906 1944 float: left;
1907 1945 margin: -2px 0px 0px 2px;
1908 1946 input {
1909 1947 padding: 2px;
1910 1948 border: none;
1911 1949 font-size: @repo-title-fontsize;
1912 1950 }
1913 1951 }
1914 1952 }
1915 1953
1916 1954
1917 1955 .browser-result{
1918 1956 td a{
1919 1957 margin-left: 0.5em;
1920 1958 display: inline-block;
1921 1959
1922 1960 em{
1923 1961 font-family: @text-bold;
1924 1962 }
1925 1963 }
1926 1964 }
1927 1965
1928 1966 .browser-highlight{
1929 1967 background-color: @grey5-alpha;
1930 1968 }
1931 1969
1932 1970
1933 1971 // Search
1934 1972
1935 1973 .search-form{
1936 1974 #q {
1937 1975 width: @search-form-width;
1938 1976 }
1939 1977 .fields{
1940 1978 margin: 0 0 @space;
1941 1979 }
1942 1980
1943 1981 label{
1944 1982 display: inline-block;
1945 1983 margin-right: @textmargin;
1946 1984 padding-top: 0.25em;
1947 1985 }
1948 1986
1949 1987
1950 1988 .results{
1951 1989 clear: both;
1952 1990 margin: 0 0 @padding;
1953 1991 }
1954 1992 }
1955 1993
1956 1994 div.search-feedback-items {
1957 1995 display: inline-block;
1958 1996 padding:0px 0px 0px 96px;
1959 1997 }
1960 1998
1961 1999 div.search-code-body {
1962 2000 background-color: #ffffff; padding: 5px 0 5px 10px;
1963 2001 pre {
1964 2002 .match { background-color: #faffa6;}
1965 2003 .break { display: block; width: 100%; background-color: #DDE7EF; color: #747474; }
1966 2004 }
1967 2005 }
1968 2006
1969 2007 .expand_commit.search {
1970 2008 .show_more.open {
1971 2009 height: auto;
1972 2010 max-height: none;
1973 2011 }
1974 2012 }
1975 2013
1976 2014 .search-results {
1977 2015
1978 2016 h2 {
1979 2017 margin-bottom: 0;
1980 2018 }
1981 2019 .codeblock {
1982 2020 border: none;
1983 2021 background: transparent;
1984 2022 }
1985 2023
1986 2024 .codeblock-header {
1987 2025 border: none;
1988 2026 background: transparent;
1989 2027 }
1990 2028
1991 2029 .code-body {
1992 2030 border: @border-thickness solid @border-default-color;
1993 2031 .border-radius(@border-radius);
1994 2032 }
1995 2033
1996 2034 .td-commit {
1997 2035 &:extend(pre);
1998 2036 border-bottom: @border-thickness solid @border-default-color;
1999 2037 }
2000 2038
2001 2039 .message {
2002 2040 height: auto;
2003 2041 max-width: 350px;
2004 2042 white-space: normal;
2005 2043 text-overflow: initial;
2006 2044 overflow: visible;
2007 2045
2008 2046 .match { background-color: #faffa6;}
2009 2047 .break { background-color: #DDE7EF; width: 100%; color: #747474; display: block; }
2010 2048 }
2011 2049
2012 2050 }
2013 2051
2014 2052 table.rctable td.td-search-results div {
2015 2053 max-width: 100%;
2016 2054 }
2017 2055
2018 2056 #tip-box, .tip-box{
2019 2057 padding: @menupadding/2;
2020 2058 display: block;
2021 2059 border: @border-thickness solid @border-highlight-color;
2022 2060 .border-radius(@border-radius);
2023 2061 background-color: white;
2024 2062 z-index: 99;
2025 2063 white-space: pre-wrap;
2026 2064 }
2027 2065
2028 2066 #linktt {
2029 2067 width: 79px;
2030 2068 }
2031 2069
2032 2070 #help_kb .modal-content{
2033 2071 max-width: 750px;
2034 2072 margin: 10% auto;
2035 2073
2036 2074 table{
2037 2075 td,th{
2038 2076 border-bottom: none;
2039 2077 line-height: 2.5em;
2040 2078 }
2041 2079 th{
2042 2080 padding-bottom: @textmargin/2;
2043 2081 }
2044 2082 td.keys{
2045 2083 text-align: center;
2046 2084 }
2047 2085 }
2048 2086
2049 2087 .block-left{
2050 2088 width: 45%;
2051 2089 margin-right: 5%;
2052 2090 }
2053 2091 .modal-footer{
2054 2092 clear: both;
2055 2093 }
2056 2094 .key.tag{
2057 2095 padding: 0.5em;
2058 2096 background-color: @rcblue;
2059 2097 color: white;
2060 2098 border-color: @rcblue;
2061 2099 .box-shadow(none);
2062 2100 }
2063 2101 }
2064 2102
2065 2103
2066 2104
2067 2105 //--- IMPORTS FOR REFACTORED STYLES ------------------//
2068 2106
2069 2107 @import 'statistics-graph';
2070 2108 @import 'tables';
2071 2109 @import 'forms';
2072 2110 @import 'diff';
2073 2111 @import 'summary';
2074 2112 @import 'navigation';
2075 2113
2076 2114 //--- SHOW/HIDE SECTIONS --//
2077 2115
2078 2116 .btn-collapse {
2079 2117 float: right;
2080 2118 text-align: right;
2081 2119 font-family: @text-light;
2082 2120 font-size: @basefontsize;
2083 2121 cursor: pointer;
2084 2122 border: none;
2085 2123 color: @rcblue;
2086 2124 }
2087 2125
2088 2126 table.rctable,
2089 2127 table.dataTable {
2090 2128 .btn-collapse {
2091 2129 float: right;
2092 2130 text-align: right;
2093 2131 }
2094 2132 }
2095 2133
2096 2134
2097 2135 // TODO: johbo: Fix for IE10, this avoids that we see a border
2098 2136 // and padding around checkboxes and radio boxes. Move to the right place,
2099 2137 // or better: Remove this once we did the form refactoring.
2100 2138 input[type=checkbox],
2101 2139 input[type=radio] {
2102 2140 padding: 0;
2103 2141 border: none;
2104 2142 }
2105 2143
2106 2144 .toggle-ajax-spinner{
2107 2145 height: 16px;
2108 2146 width: 16px;
2109 2147 }
@@ -1,542 +1,542 b''
1 1 //
2 2 // Typography
3 3 // modified from Bootstrap
4 4 // --------------------------------------------------
5 5
6 6 // Base
7 7 body {
8 8 font-size: @basefontsize;
9 9 font-family: @text-light;
10 10 letter-spacing: .02em;
11 11 color: @grey2;
12 12 }
13 13
14 14 #content, label{
15 15 font-size: @basefontsize;
16 16 }
17 17
18 18 label {
19 19 color: @grey2;
20 20 }
21 21
22 22 ::selection { background: @rchighlightblue; }
23 23
24 24 // Headings
25 25 // -------------------------
26 26
27 27 h1, h2, h3, h4, h5, h6,
28 28 .h1, .h2, .h3, .h4, .h5, .h6 {
29 29 margin: 0 0 @textmargin 0;
30 30 padding: 0;
31 31 line-height: 1.8em;
32 32 color: @text-color;
33 33 a {
34 34 color: @rcblue;
35 35 }
36 36 }
37 37
38 38 h1, .h1 { font-size: 1.54em; font-family: @text-bold; }
39 39 h2, .h2 { font-size: 1.23em; font-family: @text-semibold; }
40 40 h3, .h3 { font-size: 1.23em; font-family: @text-regular; }
41 41 h4, .h4 { font-size: 1em; font-family: @text-bold; }
42 42 h5, .h5 { font-size: 1em; font-family: @text-bold-italic; }
43 43 h6, .h6 { font-size: 1em; font-family: @text-bold-italic; }
44 44
45 45 // Breadcrumbs
46 46 .breadcrumbs {
47 47 &:extend(h1);
48 48 margin: 0;
49 49 }
50 50
51 51 .breadcrumbs_light {
52 52 float:left;
53 53 margin: @padding 0;
54 54 }
55 55
56 56 // Body text
57 57 // -------------------------
58 58
59 59 p {
60 60 margin: 0 0 @textmargin 0;
61 61 padding: 0;
62 62 line-height: 2em;
63 63 }
64 64
65 65 .lead {
66 66 margin-bottom: @textmargin;
67 67 font-weight: 300;
68 68 line-height: 1.4;
69 69
70 70 @media (min-width: @screen-sm-min) {
71 71 font-size: (@basefontsize * 1.5);
72 72 }
73 73 }
74 74
75 75 a,
76 76 .link {
77 77 color: @rcblue;
78 78 text-decoration: none;
79 79 outline: none;
80 80 cursor: pointer;
81 81
82 82 &:focus {
83 83 outline: none;
84 84 }
85 85
86 86 &:hover {
87 87 color: @rcdarkblue;
88 88 }
89 89 }
90 90
91 91 img {
92 92 border: none;
93 93 outline: none;
94 94 }
95 95
96 96 strong {
97 97 font-family: @text-bold;
98 98 }
99 99
100 100 em {
101 101 font-family: @text-italic;
102 102 }
103 103
104 104 strong em,
105 105 em strong {
106 106 font-family: @text-bold-italic;
107 107 }
108 108
109 109 //TODO: lisa: b and i are depreciated, but we are still using them in places.
110 110 // Should probably make some decision whether to keep or lose these.
111 111 b {
112 112
113 113 }
114 114
115 115 i {
116 116 font-style: normal;
117 117 }
118 118
119 119 label {
120 120 color: @text-color;
121 121
122 122 input[type="checkbox"] {
123 123 margin-right: 1em;
124 124 }
125 125 input[type="radio"] {
126 126 margin-right: 1em;
127 127 }
128 128 }
129 129
130 130 code,
131 131 .code {
132 132 font-size: .95em;
133 133 font-family: "Lucida Console", Monaco, monospace;
134 134 color: @grey3;
135 135
136 136 a {
137 137 color: lighten(@rcblue,10%)
138 138 }
139 139 }
140 140
141 141 pre {
142 142 margin: 0;
143 143 padding: 0;
144 144 border: 0;
145 145 outline: 0;
146 146 font-size: @basefontsize*.95;
147 147 line-height: 1.4em;
148 148 font-family: "Lucida Console", Monaco, monospace;
149 149 color: @grey3;
150 150 }
151 151
152 152 // Emphasis & misc
153 153 // -------------------------
154 154
155 155 small,
156 156 .small {
157 157 font-size: 75%;
158 158 font-weight: normal;
159 159 line-height: 1em;
160 160 }
161 161
162 162 mark,
163 163 .mark {
164 164 background-color: @rclightblue;
165 165 padding: .2em;
166 166 }
167 167
168 168 // Alignment
169 169 .text-left { text-align: left; }
170 170 .text-right { text-align: right; }
171 171 .text-center { text-align: center; }
172 172 .text-justify { text-align: justify; }
173 173 .text-nowrap { white-space: nowrap; }
174 174
175 175 // Transformation
176 176 .text-lowercase { text-transform: lowercase; }
177 177 .text-uppercase { text-transform: uppercase; }
178 178 .text-capitalize { text-transform: capitalize; }
179 179
180 180 // Contextual colors
181 181 .text-muted {
182 182 color: @grey4;
183 183 }
184 184 .text-primary {
185 185 color: @rcblue;
186 186 }
187 187 .text-success {
188 188 color: @alert1;
189 189 }
190 190 .text-info {
191 191 color: @alert4;
192 192 }
193 193 .text-warning {
194 194 color: @alert3;
195 195 }
196 196 .text-danger {
197 197 color: @alert2;
198 198 }
199 199
200 200 // Contextual backgrounds
201 201 .bg-primary {
202 202 background-color: white;
203 203 }
204 204 .bg-success {
205 205 background-color: @alert1;
206 206 }
207 207 .bg-info {
208 208 background-color: @alert4;
209 209 }
210 210 .bg-warning {
211 211 background-color: @alert3;
212 212 }
213 213 .bg-danger {
214 214 background-color: @alert2;
215 215 }
216 216
217 217
218 218 // Page header
219 219 // -------------------------
220 220
221 221 .page-header {
222 222 margin: @pagepadding 0 @textmargin;
223 223 border-bottom: @border-thickness solid @grey5;
224 224 }
225 225
226 226 .title {
227 227 clear: both;
228 228 float: left;
229 229 width: 100%;
230 230 margin: @pagepadding 0 @pagepadding;
231 231
232 232 .breadcrumbs{
233 233 float: left;
234 234 clear: both;
235 235 width: 700px;
236 236 margin: 0;
237 237
238 238 .q_filter_box {
239 239 margin-right: @padding;
240 240 }
241 241 }
242 242
243 243 h1 a {
244 244 color: @rcblue;
245 245 }
246 246
247 247 input{
248 248 margin-right: @padding;
249 249 }
250 250
251 251 h5, .h5 {
252 252 color: @grey1;
253 253 margin-bottom: @space;
254 254
255 255 span {
256 256 display: inline-block;
257 257 }
258 258 }
259 259
260 260 p {
261 261 margin-bottom: 0;
262 262 }
263 263
264 264 .links{
265 265 float: right;
266 266 display: inline;
267 267 margin: 0;
268 268 padding-left: 0;
269 269 list-style: none;
270 270 text-align: right;
271 271
272 272 li:before { content: none; }
273
273 li { float: right; }
274 274 a {
275 275 display: inline-block;
276 276 margin-left: @textmargin/2;
277 277 }
278 278 }
279 279
280 280 .title-content {
281 281 float: left;
282 282 margin: 0;
283 283 padding: 0;
284 284
285 285 & + .breadcrumbs {
286 286 margin-top: @padding;
287 287 }
288 288
289 289 & + .links {
290 290 margin-top: -@button-padding;
291 291
292 292 & + .breadcrumbs {
293 293 margin-top: @padding;
294 294 }
295 295 }
296 296 }
297 297
298 298 .title-main {
299 299 font-size: @repo-title-fontsize;
300 300 }
301 301
302 302 .title-description {
303 303 margin-top: .5em;
304 304 }
305 305
306 306 .q_filter_box {
307 307 width: 200px;
308 308 }
309 309
310 310 }
311 311
312 312 #readme .title {
313 313 text-transform: none;
314 314 }
315 315
316 316 // Lists
317 317 // -------------------------
318 318
319 319 // Unordered and Ordered lists
320 320 ul,
321 321 ol {
322 322 margin-top: 0;
323 323 margin-bottom: @textmargin;
324 324 ul,
325 325 ol {
326 326 margin-bottom: 0;
327 327 }
328 328 }
329 329
330 330 li {
331 331 line-height: 2em;
332 332 }
333 333
334 334 ul li {
335 335 position: relative;
336 336 display: block;
337 337 list-style-type: none;
338 338
339 339 &:before {
340 340 content: "\2014\00A0";
341 341 position: absolute;
342 342 top: 0;
343 343 left: -1.25em;
344 344 }
345 345
346 346 p:first-child {
347 347 display:inline;
348 348 }
349 349 }
350 350
351 351 // List options
352 352
353 353 // Unstyled keeps list items block level, just removes default browser padding and list-style
354 354 .list-unstyled {
355 355 padding-left: 0;
356 356 list-style: none;
357 357 li:before { content: none; }
358 358 }
359 359
360 360 // Inline turns list items into inline-block
361 361 .list-inline {
362 362 .list-unstyled();
363 363 margin-left: -5px;
364 364
365 365 > li {
366 366 display: inline-block;
367 367 padding-left: 5px;
368 368 padding-right: 5px;
369 369 }
370 370 }
371 371
372 372 // Description Lists
373 373
374 374 dl {
375 375 margin-top: 0; // Remove browser default
376 376 margin-bottom: @textmargin;
377 377 }
378 378
379 379 dt,
380 380 dd {
381 381 line-height: 1.4em;
382 382 }
383 383
384 384 dt {
385 385 margin: @textmargin 0 0 0;
386 386 font-family: @text-bold;
387 387 }
388 388
389 389 dd {
390 390 margin-left: 0; // Undo browser default
391 391 }
392 392
393 393 // Horizontal description lists
394 394 // Defaults to being stacked without any of the below styles applied, until the
395 395 // grid breakpoint is reached (default of ~768px).
396 396 // These are used in forms as well; see style guide.
397 397 // TODO: lisa: These should really not be used in forms.
398 398
399 399 .dl-horizontal {
400 400
401 401 overflow: hidden;
402 402 margin-top: -5px;
403 403 margin-bottom: @space;
404 404
405 405 dt, dd {
406 406 float: left;
407 407 margin: 5px 0 5px 0;
408 408 }
409 409
410 410 dt {
411 411 clear: left;
412 412 width: @label-width - @form-vertical-margin;
413 413 }
414 414
415 415 dd {
416 416 &:extend(.clearfix all); // Clear the floated `dt` if an empty `dd` is present
417 417 margin-left: @form-vertical-margin;
418 418 max-width: @form-max-width - (@label-width - @form-vertical-margin) - @form-vertical-margin;
419 419 }
420 420
421 421 pre {
422 422 margin: 0;
423 423 }
424 424
425 425 &.settings {
426 426 dt {
427 427 text-align: left;
428 428 }
429 429 }
430 430
431 431 @media (min-width: 768px) {
432 432 dt {
433 433 float: left;
434 434 width: 180px;
435 435 clear: left;
436 436 text-align: right;
437 437 }
438 438 dd {
439 439 margin-left: 20px;
440 440 }
441 441 }
442 442 }
443 443
444 444
445 445 // Misc
446 446 // -------------------------
447 447
448 448 // Abbreviations and acronyms
449 449 abbr[title],
450 450 abbr[data-original-title] {
451 451 cursor: help;
452 452 border-bottom: @border-thickness dotted @grey4;
453 453 }
454 454 .initialism {
455 455 font-size: 90%;
456 456 text-transform: uppercase;
457 457 }
458 458
459 459 // Blockquotes
460 460 blockquote {
461 461 padding: 1em 2em;
462 462 margin: 0 0 2em;
463 463 font-size: @basefontsize;
464 464 border-left: 2px solid @grey6;
465 465
466 466 p,
467 467 ul,
468 468 ol {
469 469 &:last-child {
470 470 margin-bottom: 0;
471 471 }
472 472 }
473 473
474 474 footer,
475 475 small,
476 476 .small {
477 477 display: block;
478 478 font-size: 80%;
479 479
480 480 &:before {
481 481 content: '\2014 \00A0'; // em dash, nbsp
482 482 }
483 483 }
484 484 }
485 485
486 486 // Opposite alignment of blockquote
487 487 //
488 488 .blockquote-reverse,
489 489 blockquote.pull-right {
490 490 padding-right: 15px;
491 491 padding-left: 0;
492 492 border-right: 5px solid @grey6;
493 493 border-left: 0;
494 494 text-align: right;
495 495
496 496 // Account for citation
497 497 footer,
498 498 small,
499 499 .small {
500 500 &:before { content: ''; }
501 501 &:after {
502 502 content: '\00A0 \2014'; // nbsp, em dash
503 503 }
504 504 }
505 505 }
506 506
507 507 // Addresses
508 508 address {
509 509 margin-bottom: 2em;
510 510 font-style: normal;
511 511 line-height: 1.8em;
512 512 }
513 513
514 514 .error-message {
515 515 display: block;
516 516 margin: @padding/3 0;
517 517 color: @alert2;
518 518 }
519 519
520 520 .issue-tracker-link {
521 521 color: @rcblue;
522 522 }
523 523
524 524 .info_text{
525 525 font-size: @basefontsize;
526 526 color: @grey4;
527 527 font-family: @text-regular;
528 528 }
529 529
530 530 // help block text
531 531 .help-block {
532 532 display: block;
533 533 margin: 0 0 @padding;
534 534 color: @grey4;
535 535 font-family: @text-light;
536 536 }
537 537
538 538 .error-message {
539 539 display: block;
540 540 margin: @padding/3 0;
541 541 color: @alert2;
542 542 }
@@ -1,43 +1,69 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.html"/>
3 3
4 4 <%def name="breadcrumbs_links()">
5 5 %if c.repo:
6 6 ${h.link_to('Settings',h.url('edit_repo', repo_name=c.repo.repo_name))}
7 7 &raquo;
8 8 ${h.link_to(_('Integrations'),request.route_url(route_name='repo_integrations_home', repo_name=c.repo.repo_name))}
9 9 &raquo;
10 10 ${h.link_to(current_IntegrationType.display_name,
11 11 request.route_url(route_name='repo_integrations_list',
12 12 repo_name=c.repo.repo_name,
13 13 integration=current_IntegrationType.key))}
14 %elif c.repo_group:
15 ${h.link_to(_('Admin'),h.url('admin_home'))}
16 &raquo;
17 ${h.link_to(_('Repository Groups'),h.url('repo_groups'))}
18 &raquo;
19 ${h.link_to(c.repo_group.group_name,h.url('edit_repo_group', group_name=c.repo_group.group_name))}
20 &raquo;
21 ${h.link_to(_('Integrations'),request.route_url(route_name='repo_group_integrations_home', repo_group_name=c.repo_group.group_name))}
22 &raquo;
23 ${h.link_to(current_IntegrationType.display_name,
24 request.route_url(route_name='repo_group_integrations_list',
25 repo_group_name=c.repo_group.group_name,
26 integration=current_IntegrationType.key))}
14 27 %else:
15 28 ${h.link_to(_('Admin'),h.url('admin_home'))}
16 29 &raquo;
17 30 ${h.link_to(_('Settings'),h.url('admin_settings'))}
18 31 &raquo;
19 32 ${h.link_to(_('Integrations'),request.route_url(route_name='global_integrations_home'))}
20 33 &raquo;
21 34 ${h.link_to(current_IntegrationType.display_name,
22 35 request.route_url(route_name='global_integrations_list',
23 36 integration=current_IntegrationType.key))}
24 37 %endif
38
25 39 %if integration:
26 40 &raquo;
27 41 ${integration.name}
42 %elif current_IntegrationType:
43 &raquo;
44 ${current_IntegrationType.display_name}
28 45 %endif
29 46 </%def>
47
48 <style>
49 .control-inputs.item-options, .control-inputs.item-settings {
50 float: left;
51 width: 100%;
52 }
53 </style>
30 54 <div class="panel panel-default">
31 55 <div class="panel-heading">
32 56 <h2 class="panel-title">
33 57 %if integration:
34 58 ${current_IntegrationType.display_name} - ${integration.name}
35 59 %else:
36 ${_('Create New %(integration_type)s Integration') % {'integration_type': current_IntegrationType.display_name}}
60 ${_('Create New %(integration_type)s Integration') % {
61 'integration_type': current_IntegrationType.display_name
62 }}
37 63 %endif
38 64 </h2>
39 65 </div>
40 66 <div class="panel-body">
41 67 ${form.render() | n}
42 68 </div>
43 69 </div>
@@ -1,156 +1,249 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%inherit file="base.html"/>
3 3
4 4 <%def name="breadcrumbs_links()">
5 5 %if c.repo:
6 6 ${h.link_to('Settings',h.url('edit_repo', repo_name=c.repo.repo_name))}
7 %elif c.repo_group:
8 ${h.link_to(_('Admin'),h.url('admin_home'))}
9 &raquo;
10 ${h.link_to(_('Repository Groups'),h.url('repo_groups'))}
11 &raquo;
12 ${h.link_to(c.repo_group.group_name,h.url('edit_repo_group', group_name=c.repo_group.group_name))}
7 13 %else:
8 14 ${h.link_to(_('Admin'),h.url('admin_home'))}
9 15 &raquo;
10 16 ${h.link_to(_('Settings'),h.url('admin_settings'))}
11 17 %endif
12 18 %if current_IntegrationType:
13 19 &raquo;
14 20 %if c.repo:
15 21 ${h.link_to(_('Integrations'),
16 22 request.route_url(route_name='repo_integrations_home',
17 23 repo_name=c.repo.repo_name))}
24 %elif c.repo_group:
25 ${h.link_to(_('Integrations'),
26 request.route_url(route_name='repo_group_integrations_home',
27 repo_group_name=c.repo_group.group_name))}
18 28 %else:
19 29 ${h.link_to(_('Integrations'),
20 30 request.route_url(route_name='global_integrations_home'))}
21 31 %endif
22 32 &raquo;
23 33 ${current_IntegrationType.display_name}
24 34 %else:
25 35 &raquo;
26 36 ${_('Integrations')}
27 37 %endif
28 38 </%def>
39
29 40 <div class="panel panel-default">
30 41 <div class="panel-heading">
31 <h3 class="panel-title">${_('Create New Integration')}</h3>
42 <h3 class="panel-title">
43 %if c.repo:
44 ${_('Current Integrations for Repository: {repo_name}').format(repo_name=c.repo.repo_name)}
45 %elif c.repo_group:
46 ${_('Current Integrations for repository group: {repo_group_name}').format(repo_group_name=c.repo_group.group_name)}
47 %else:
48 ${_('Current Integrations')}
49 %endif
50 </h3>
32 51 </div>
33 52 <div class="panel-body">
34 %if not available_integrations:
35 ${_('No integrations available.')}
36 %else:
37 %for integration in available_integrations:
53 <%
54 if c.repo:
55 home_url = request.route_path('repo_integrations_home',
56 repo_name=c.repo.repo_name)
57 elif c.repo_group:
58 home_url = request.route_path('repo_group_integrations_home',
59 repo_group_name=c.repo_group.group_name)
60 else:
61 home_url = request.route_path('global_integrations_home')
62 %>
63
64 <a href="${home_url}" class="btn ${not current_IntegrationType and 'btn-primary' or ''}">${_('All')}</a>
65
66 %for integration_key, IntegrationType in available_integrations.items():
38 67 <%
39 68 if c.repo:
40 create_url = request.route_path('repo_integrations_create',
69 list_url = request.route_path('repo_integrations_list',
41 70 repo_name=c.repo.repo_name,
42 integration=integration)
71 integration=integration_key)
43 72 elif c.repo_group:
44 create_url = request.route_path('repo_group_integrations_create',
73 list_url = request.route_path('repo_group_integrations_list',
45 74 repo_group_name=c.repo_group.group_name,
46 integration=integration)
75 integration=integration_key)
47 76 else:
48 create_url = request.route_path('global_integrations_create',
49 integration=integration)
77 list_url = request.route_path('global_integrations_list',
78 integration=integration_key)
50 79 %>
51 <a href="${create_url}" class="btn">
52 ${integration}
80 <a href="${list_url}"
81 class="btn ${current_IntegrationType and integration_key == current_IntegrationType.key and 'btn-primary' or ''}">
82 ${IntegrationType.display_name}
53 83 </a>
54 84 %endfor
55 %endif
56 </div>
57 </div>
58 <div class="panel panel-default">
59 <div class="panel-heading">
60 <h3 class="panel-title">${_('Current Integrations')}</h3>
61 </div>
62 <div class="panel-body">
63 <table class="rctable issuetracker">
85
86 <%
87 if c.repo:
88 create_url = h.route_path('repo_integrations_new', repo_name=c.repo.repo_name)
89 elif c.repo_group:
90 create_url = h.route_path('repo_group_integrations_new', repo_group_name=c.repo_group.group_name)
91 else:
92 create_url = h.route_path('global_integrations_new')
93 %>
94 <p class="pull-right">
95 <a href="${create_url}" class="btn btn-small btn-success">${_(u'Create new integration')}</a>
96 </p>
97
98 <table class="rctable integrations">
64 99 <thead>
65 100 <tr>
66 <th>${_('Enabled')}</th>
67 <th>${_('Description')}</th>
68 <th>${_('Type')}</th>
101 <th><a href="?sort=enabled:${rev_sort_dir}">${_('Enabled')}</a></th>
102 <th><a href="?sort=name:${rev_sort_dir}">${_('Name')}</a></th>
103 <th colspan="2"><a href="?sort=integration_type:${rev_sort_dir}">${_('Type')}</a></th>
104 <th><a href="?sort=scope:${rev_sort_dir}">${_('Scope')}</a></th>
69 105 <th>${_('Actions')}</th>
70 106 <th></th>
71 107 </tr>
72 108 </thead>
73 109 <tbody>
110 %if not integrations_list:
111 <tr>
112 <td colspan="7">
113 <% integration_type = current_IntegrationType and current_IntegrationType.display_name or '' %>
114 %if c.repo:
115 ${_('No {type} integrations for repo {repo} exist yet.').format(type=integration_type, repo=c.repo.repo_name)}
116 %elif c.repo_group:
117 ${_('No {type} integrations for repogroup {repogroup} exist yet.').format(type=integration_type, repogroup=c.repo_group.group_name)}
118 %else:
119 ${_('No {type} integrations exist yet.').format(type=integration_type)}
120 %endif
74 121
75 %for integration_type, integrations in sorted(current_integrations.items()):
76 %for integration in sorted(integrations, key=lambda x: x.name):
122 %if current_IntegrationType:
123 <%
124 if c.repo:
125 create_url = h.route_path('repo_integrations_create', repo_name=c.repo.repo_name, integration=current_IntegrationType.key)
126 elif c.repo_group:
127 create_url = h.route_path('repo_group_integrations_create', repo_group_name=c.repo_group.group_name, integration=current_IntegrationType.key)
128 else:
129 create_url = h.route_path('global_integrations_create', integration=current_IntegrationType.key)
130 %>
131 %endif
132
133 <a href="${create_url}">${_(u'Create one')}</a>
134 </td>
135 </tr>
136 %endif
137 %for IntegrationType, integration in integrations_list:
77 138 <tr id="integration_${integration.integration_id}">
78 139 <td class="td-enabled">
79 140 %if integration.enabled:
80 141 <div class="flag_status approved pull-left"></div>
81 142 %else:
82 143 <div class="flag_status rejected pull-left"></div>
83 144 %endif
84 145 </td>
85 146 <td class="td-description">
86 147 ${integration.name}
87 148 </td>
88 <td class="td-regex">
149 <td class="td-icon">
150 %if integration.integration_type in available_integrations:
151 <div class="integration-icon">
152 ${available_integrations[integration.integration_type].icon|n}
153 </div>
154 %else:
155 ?
156 %endif
157 </td>
158 <td class="td-type">
89 159 ${integration.integration_type}
90 160 </td>
161 <td class="td-scope">
162 %if integration.repo:
163 <a href="${h.url('summary_home', repo_name=integration.repo.repo_name)}">
164 ${_('repo')}:${integration.repo.repo_name}
165 </a>
166 %elif integration.repo_group:
167 <a href="${h.url('repo_group_home', group_name=integration.repo_group.group_name)}">
168 ${_('repogroup')}:${integration.repo_group.group_name}
169 </a>
170 %else:
171 %if integration.scope == 'root_repos':
172 ${_('top level repos only')}
173 %elif integration.scope == 'global':
174 ${_('global')}
175 %else:
176 ${_('unknown scope')}: ${integration.scope}
177 %endif
178 </td>
179 %endif
91 180 <td class="td-action">
92 %if integration_type not in available_integrations:
181 %if not IntegrationType:
93 182 ${_('unknown integration')}
94 183 %else:
95 184 <%
96 185 if c.repo:
97 186 edit_url = request.route_path('repo_integrations_edit',
98 187 repo_name=c.repo.repo_name,
99 188 integration=integration.integration_type,
100 189 integration_id=integration.integration_id)
101 190 elif c.repo_group:
102 191 edit_url = request.route_path('repo_group_integrations_edit',
103 192 repo_group_name=c.repo_group.group_name,
104 193 integration=integration.integration_type,
105 194 integration_id=integration.integration_id)
106 195 else:
107 196 edit_url = request.route_path('global_integrations_edit',
108 197 integration=integration.integration_type,
109 198 integration_id=integration.integration_id)
110 199 %>
111 200 <div class="grid_edit">
112 201 <a href="${edit_url}">${_('Edit')}</a>
113 202 </div>
114 203 <div class="grid_delete">
115 204 <a href="${edit_url}"
116 205 class="btn btn-link btn-danger delete_integration_entry"
117 206 data-desc="${integration.name}"
118 207 data-uid="${integration.integration_id}">
119 208 ${_('Delete')}
120 209 </a>
121 210 </div>
122 211 %endif
123 212 </td>
124 213 </tr>
125 214 %endfor
126 %endfor
127 215 <tr id="last-row"></tr>
128 216 </tbody>
129 217 </table>
218 <div class="integrations-paginator">
219 <div class="pagination-wh pagination-left">
220 ${integrations_list.pager('$link_previous ~2~ $link_next')}
221 </div>
222 </div>
130 223 </div>
131 224 </div>
132 225 <script type="text/javascript">
133 226 var delete_integration = function(entry) {
134 227 if (confirm("Confirm to remove this integration: "+$(entry).data('desc'))) {
135 228 var request = $.ajax({
136 229 type: "POST",
137 230 url: $(entry).attr('href'),
138 231 data: {
139 232 'delete': 'delete',
140 233 'csrf_token': CSRF_TOKEN
141 234 },
142 235 success: function(){
143 236 location.reload();
144 237 },
145 238 error: function(data, textStatus, errorThrown){
146 239 alert("Error while deleting entry.\nError code {0} ({1}). URL: {2}".format(data.status,data.statusText,$(entry)[0].url));
147 240 }
148 241 });
149 242 };
150 243 }
151 244
152 245 $('.delete_integration_entry').on('click', function(e){
153 246 e.preventDefault();
154 247 delete_integration(this);
155 248 });
156 249 </script> No newline at end of file
@@ -1,47 +1,46 b''
1 1 <div tal:define="error_class error_class|field.widget.error_class;
2 2 description description|field.description;
3 3 title title|field.title;
4 4 oid oid|field.oid;
5 5 hidden hidden|field.widget.hidden;
6 6 category category|field.widget.category;
7 7 structural hidden or category == 'structural';
8 8 required required|field.required;"
9 9 class="form-group ${field.error and 'has-error' or ''} ${field.widget.item_css_class or ''}"
10 10 id="item-${oid}"
11 11 tal:omit-tag="structural"
12 12 i18n:domain="deform">
13
14 13 <label for="${oid}"
15 14 class="control-label ${required and 'required' or ''}"
16 15 tal:condition="not structural"
17 16 id="req-${oid}"
18 17 >
19 18 ${title}
20 19 </label>
21 <div class="control-inputs">
20 <div class="control-inputs ${field.widget.item_css_class or ''}">
22 21 <div tal:define="input_prepend field.widget.input_prepend | None;
23 22 input_append field.widget.input_append | None"
24 23 tal:omit-tag="not (input_prepend or input_append)"
25 24 class="input-group">
26 25 <span class="input-group-addon"
27 26 tal:condition="input_prepend">${input_prepend}</span
28 27 ><span tal:replace="structure field.serialize(cstruct).strip()"
29 28 /><span class="input-group-addon"
30 29 tal:condition="input_append">${input_append}</span>
31 30 </div>
32 31 <p class="help-block error-block"
33 32 tal:define="errstr 'error-%s' % field.oid"
34 33 tal:repeat="msg field.error.messages()"
35 34 i18n:translate=""
36 35 tal:attributes="id repeat.msg.index==0 and errstr or
37 36 ('%s-%s' % (errstr, repeat.msg.index))"
38 37 tal:condition="field.error and not field.widget.hidden and not field.typ.__class__.__name__=='Mapping'">
39 38 ${msg}
40 39 </p>
41 40
42 41 <p tal:condition="field.description and not field.widget.hidden"
43 42 class="help-block" >
44 43 ${field.description}
45 44 </p>
46 45 </div>
47 46 </div> No newline at end of file
@@ -1,10 +1,18 b''
1 <%def name="panel(title, class_='default')">
2 <div class="panel panel-${class_}">
1 <%def name="panel(title='', category='default', class_='')">
2 <div class="panel panel-${category} ${class_}">
3 %if title or hasattr(caller, 'title'):
3 4 <div class="panel-heading">
4 <h3 class="panel-title">${title}</h3>
5 <h3 class="panel-title">
6 %if title:
7 ${title}
8 %else:
9 ${caller.title()}
10 %endif
11 </h3>
5 12 </div>
13 %endif
6 14 <div class="panel-body">
7 15 ${caller.body()}
8 16 </div>
9 17 </div>
10 18 </%def>
@@ -1,78 +1,192 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 import time
21 22 import pytest
22 import requests
23 from mock import Mock, patch
24 23
25 24 from rhodecode import events
25 from rhodecode.tests.fixture import Fixture
26 26 from rhodecode.model.db import Session, Integration
27 27 from rhodecode.model.integration import IntegrationModel
28 28 from rhodecode.integrations.types.base import IntegrationTypeBase
29 29
30 30
31 class TestIntegrationType(IntegrationTypeBase):
32 """ Test integration type class """
33
34 key = 'test-integration'
35 display_name = 'Test integration type'
31 class TestDeleteScopesDeletesIntegrations(object):
32 def test_delete_repo_with_integration_deletes_integration(self,
33 repo_integration_stub):
34 Session().delete(repo_integration_stub.repo)
35 Session().commit()
36 Session().expire_all()
37 integration = Integration.get(repo_integration_stub.integration_id)
38 assert integration is None
36 39
37 def __init__(self, settings):
38 super(IntegrationTypeBase, self).__init__(settings)
39 self.sent_events = [] # for testing
40 40
41 def send_event(self, event):
42 self.sent_events.append(event)
41 def test_delete_repo_group_with_integration_deletes_integration(self,
42 repogroup_integration_stub):
43 Session().delete(repogroup_integration_stub.repo_group)
44 Session().commit()
45 Session().expire_all()
46 integration = Integration.get(repogroup_integration_stub.integration_id)
47 assert integration is None
43 48
44 49
45 50 @pytest.fixture
46 def repo_integration_stub(request, repo_stub):
47 settings = {'test_key': 'test_value'}
48 integration = IntegrationModel().create(
49 TestIntegrationType, settings=settings, repo=repo_stub, enabled=True,
50 name='test repo integration')
51 def integration_repos(request, StubIntegrationType, stub_integration_settings):
52 """
53 Create repositories and integrations for testing, and destroy them after
54 """
55 fixture = Fixture()
56
57 repo_group_1_id = 'int_test_repo_group_1_%s' % time.time()
58 repo_group_1 = fixture.create_repo_group(repo_group_1_id)
59 repo_group_2_id = 'int_test_repo_group_2_%s' % time.time()
60 repo_group_2 = fixture.create_repo_group(repo_group_2_id)
61
62 repo_1_id = 'int_test_repo_1_%s' % time.time()
63 repo_1 = fixture.create_repo(repo_1_id, repo_group=repo_group_1)
64 repo_2_id = 'int_test_repo_2_%s' % time.time()
65 repo_2 = fixture.create_repo(repo_2_id, repo_group=repo_group_2)
66
67 root_repo_id = 'int_test_repo_root_%s' % time.time()
68 root_repo = fixture.create_repo(root_repo_id)
51 69
52 @request.addfinalizer
53 def cleanup():
54 IntegrationModel().delete(integration)
70 integration_global = IntegrationModel().create(
71 StubIntegrationType, settings=stub_integration_settings,
72 enabled=True, name='test global integration', scope='global')
73 integration_root_repos = IntegrationModel().create(
74 StubIntegrationType, settings=stub_integration_settings,
75 enabled=True, name='test root repos integration', scope='root_repos')
76 integration_repo_1 = IntegrationModel().create(
77 StubIntegrationType, settings=stub_integration_settings,
78 enabled=True, name='test repo 1 integration', scope=repo_1)
79 integration_repo_group_1 = IntegrationModel().create(
80 StubIntegrationType, settings=stub_integration_settings,
81 enabled=True, name='test repo group 1 integration', scope=repo_group_1)
82 integration_repo_2 = IntegrationModel().create(
83 StubIntegrationType, settings=stub_integration_settings,
84 enabled=True, name='test repo 2 integration', scope=repo_2)
85 integration_repo_group_2 = IntegrationModel().create(
86 StubIntegrationType, settings=stub_integration_settings,
87 enabled=True, name='test repo group 2 integration', scope=repo_group_2)
88
89 Session().commit()
55 90
56 return integration
91 def _cleanup():
92 Session().delete(integration_global)
93 Session().delete(integration_root_repos)
94 Session().delete(integration_repo_1)
95 Session().delete(integration_repo_group_1)
96 Session().delete(integration_repo_2)
97 Session().delete(integration_repo_group_2)
98 fixture.destroy_repo(root_repo)
99 fixture.destroy_repo(repo_1)
100 fixture.destroy_repo(repo_2)
101 fixture.destroy_repo_group(repo_group_1)
102 fixture.destroy_repo_group(repo_group_2)
103
104 request.addfinalizer(_cleanup)
105
106 return {
107 'repos': {
108 'repo_1': repo_1,
109 'repo_2': repo_2,
110 'root_repo': root_repo,
111 },
112 'repo_groups': {
113 'repo_group_1': repo_group_1,
114 'repo_group_2': repo_group_2,
115 },
116 'integrations': {
117 'global': integration_global,
118 'root_repos': integration_root_repos,
119 'repo_1': integration_repo_1,
120 'repo_2': integration_repo_2,
121 'repo_group_1': integration_repo_group_1,
122 'repo_group_2': integration_repo_group_2,
123 }
124 }
57 125
58 126
59 @pytest.fixture
60 def global_integration_stub(request):
61 settings = {'test_key': 'test_value'}
62 integration = IntegrationModel().create(
63 TestIntegrationType, settings=settings, enabled=True,
64 name='test global integration')
127 def test_enabled_integration_repo_scopes(integration_repos):
128 integrations = integration_repos['integrations']
129 repos = integration_repos['repos']
130
131 triggered_integrations = IntegrationModel().get_for_event(
132 events.RepoEvent(repos['root_repo']))
133
134 assert triggered_integrations == [
135 integrations['global'],
136 integrations['root_repos']
137 ]
138
139
140 triggered_integrations = IntegrationModel().get_for_event(
141 events.RepoEvent(repos['repo_1']))
65 142
66 @request.addfinalizer
67 def cleanup():
68 IntegrationModel().delete(integration)
143 assert triggered_integrations == [
144 integrations['global'],
145 integrations['repo_1'],
146 integrations['repo_group_1']
147 ]
148
69 149
70 return integration
150 triggered_integrations = IntegrationModel().get_for_event(
151 events.RepoEvent(repos['repo_2']))
152
153 assert triggered_integrations == [
154 integrations['global'],
155 integrations['repo_2'],
156 integrations['repo_group_2'],
157 ]
71 158
72 159
73 def test_delete_repo_with_integration_deletes_integration(repo_integration_stub):
74 Session().delete(repo_integration_stub.repo)
160 def test_disabled_integration_repo_scopes(integration_repos):
161 integrations = integration_repos['integrations']
162 repos = integration_repos['repos']
163
164 for integration in integrations.values():
165 integration.enabled = False
75 166 Session().commit()
76 Session().expire_all()
77 assert Integration.get(repo_integration_stub.integration_id) is None
167
168 triggered_integrations = IntegrationModel().get_for_event(
169 events.RepoEvent(repos['root_repo']))
170
171 assert triggered_integrations == []
172
173
174 triggered_integrations = IntegrationModel().get_for_event(
175 events.RepoEvent(repos['repo_1']))
176
177 assert triggered_integrations == []
178
78 179
180 triggered_integrations = IntegrationModel().get_for_event(
181 events.RepoEvent(repos['repo_2']))
182
183 assert triggered_integrations == []
184
185
186 def test_enabled_non_repo_integrations(integration_repos):
187 integrations = integration_repos['integrations']
188
189 triggered_integrations = IntegrationModel().get_for_event(
190 events.UserPreCreate({}))
191
192 assert triggered_integrations == [integrations['global']]
@@ -1,1638 +1,1740 b''
1 1 # -*- coding: utf-8 -*-
2 2
3 3 # Copyright (C) 2010-2016 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 collections
22 22 import datetime
23 23 import hashlib
24 24 import os
25 25 import re
26 26 import pprint
27 27 import shutil
28 28 import socket
29 29 import subprocess
30 30 import time
31 31 import uuid
32 32
33 33 import mock
34 34 import pyramid.testing
35 35 import pytest
36 import colander
36 37 import requests
37 38 from webtest.app import TestApp
38 39
39 40 import rhodecode
40 41 from rhodecode.model.changeset_status import ChangesetStatusModel
41 42 from rhodecode.model.comment import ChangesetCommentsModel
42 43 from rhodecode.model.db import (
43 44 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
44 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
45 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi, Integration)
45 46 from rhodecode.model.meta import Session
46 47 from rhodecode.model.pull_request import PullRequestModel
47 48 from rhodecode.model.repo import RepoModel
48 49 from rhodecode.model.repo_group import RepoGroupModel
49 50 from rhodecode.model.user import UserModel
50 51 from rhodecode.model.settings import VcsSettingsModel
51 52 from rhodecode.model.user_group import UserGroupModel
53 from rhodecode.model.integration import IntegrationModel
54 from rhodecode.integrations import integration_type_registry
55 from rhodecode.integrations.types.base import IntegrationTypeBase
52 56 from rhodecode.lib.utils import repo2db_mapper
53 57 from rhodecode.lib.vcs import create_vcsserver_proxy
54 58 from rhodecode.lib.vcs.backends import get_backend
55 59 from rhodecode.lib.vcs.nodes import FileNode
56 60 from rhodecode.tests import (
57 61 login_user_session, get_new_dir, utils, TESTS_TMP_PATH,
58 62 TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR2_LOGIN,
59 63 TEST_USER_REGULAR_PASS)
60 64 from rhodecode.tests.fixture import Fixture
61 65
62 66
63 67 def _split_comma(value):
64 68 return value.split(',')
65 69
66 70
67 71 def pytest_addoption(parser):
68 72 parser.addoption(
69 73 '--keep-tmp-path', action='store_true',
70 74 help="Keep the test temporary directories")
71 75 parser.addoption(
72 76 '--backends', action='store', type=_split_comma,
73 77 default=['git', 'hg', 'svn'],
74 78 help="Select which backends to test for backend specific tests.")
75 79 parser.addoption(
76 80 '--dbs', action='store', type=_split_comma,
77 81 default=['sqlite'],
78 82 help="Select which database to test for database specific tests. "
79 83 "Possible options are sqlite,postgres,mysql")
80 84 parser.addoption(
81 85 '--appenlight', '--ae', action='store_true',
82 86 help="Track statistics in appenlight.")
83 87 parser.addoption(
84 88 '--appenlight-api-key', '--ae-key',
85 89 help="API key for Appenlight.")
86 90 parser.addoption(
87 91 '--appenlight-url', '--ae-url',
88 92 default="https://ae.rhodecode.com",
89 93 help="Appenlight service URL, defaults to https://ae.rhodecode.com")
90 94 parser.addoption(
91 95 '--sqlite-connection-string', action='store',
92 96 default='', help="Connection string for the dbs tests with SQLite")
93 97 parser.addoption(
94 98 '--postgres-connection-string', action='store',
95 99 default='', help="Connection string for the dbs tests with Postgres")
96 100 parser.addoption(
97 101 '--mysql-connection-string', action='store',
98 102 default='', help="Connection string for the dbs tests with MySQL")
99 103 parser.addoption(
100 104 '--repeat', type=int, default=100,
101 105 help="Number of repetitions in performance tests.")
102 106
103 107
104 108 def pytest_configure(config):
105 109 # Appy the kombu patch early on, needed for test discovery on Python 2.7.11
106 110 from rhodecode.config import patches
107 111 patches.kombu_1_5_1_python_2_7_11()
108 112
109 113
110 114 def pytest_collection_modifyitems(session, config, items):
111 115 # nottest marked, compare nose, used for transition from nose to pytest
112 116 remaining = [
113 117 i for i in items if getattr(i.obj, '__test__', True)]
114 118 items[:] = remaining
115 119
116 120
117 121 def pytest_generate_tests(metafunc):
118 122 # Support test generation based on --backend parameter
119 123 if 'backend_alias' in metafunc.fixturenames:
120 124 backends = get_backends_from_metafunc(metafunc)
121 125 scope = None
122 126 if not backends:
123 127 pytest.skip("Not enabled for any of selected backends")
124 128 metafunc.parametrize('backend_alias', backends, scope=scope)
125 129 elif hasattr(metafunc.function, 'backends'):
126 130 backends = get_backends_from_metafunc(metafunc)
127 131 if not backends:
128 132 pytest.skip("Not enabled for any of selected backends")
129 133
130 134
131 135 def get_backends_from_metafunc(metafunc):
132 136 requested_backends = set(metafunc.config.getoption('--backends'))
133 137 if hasattr(metafunc.function, 'backends'):
134 138 # Supported backends by this test function, created from
135 139 # pytest.mark.backends
136 140 backends = metafunc.function.backends.args
137 141 elif hasattr(metafunc.cls, 'backend_alias'):
138 142 # Support class attribute "backend_alias", this is mainly
139 143 # for legacy reasons for tests not yet using pytest.mark.backends
140 144 backends = [metafunc.cls.backend_alias]
141 145 else:
142 146 backends = metafunc.config.getoption('--backends')
143 147 return requested_backends.intersection(backends)
144 148
145 149
146 150 @pytest.fixture(scope='session', autouse=True)
147 151 def activate_example_rcextensions(request):
148 152 """
149 153 Patch in an example rcextensions module which verifies passed in kwargs.
150 154 """
151 155 from rhodecode.tests.other import example_rcextensions
152 156
153 157 old_extensions = rhodecode.EXTENSIONS
154 158 rhodecode.EXTENSIONS = example_rcextensions
155 159
156 160 @request.addfinalizer
157 161 def cleanup():
158 162 rhodecode.EXTENSIONS = old_extensions
159 163
160 164
161 165 @pytest.fixture
162 166 def capture_rcextensions():
163 167 """
164 168 Returns the recorded calls to entry points in rcextensions.
165 169 """
166 170 calls = rhodecode.EXTENSIONS.calls
167 171 calls.clear()
168 172 # Note: At this moment, it is still the empty dict, but that will
169 173 # be filled during the test run and since it is a reference this
170 174 # is enough to make it work.
171 175 return calls
172 176
173 177
174 178 @pytest.fixture(scope='session')
175 179 def http_environ_session():
176 180 """
177 181 Allow to use "http_environ" in session scope.
178 182 """
179 183 return http_environ(
180 184 http_host_stub=http_host_stub())
181 185
182 186
183 187 @pytest.fixture
184 188 def http_host_stub():
185 189 """
186 190 Value of HTTP_HOST in the test run.
187 191 """
188 192 return 'test.example.com:80'
189 193
190 194
191 195 @pytest.fixture
192 196 def http_environ(http_host_stub):
193 197 """
194 198 HTTP extra environ keys.
195 199
196 200 User by the test application and as well for setting up the pylons
197 201 environment. In the case of the fixture "app" it should be possible
198 202 to override this for a specific test case.
199 203 """
200 204 return {
201 205 'SERVER_NAME': http_host_stub.split(':')[0],
202 206 'SERVER_PORT': http_host_stub.split(':')[1],
203 207 'HTTP_HOST': http_host_stub,
204 208 }
205 209
206 210
207 211 @pytest.fixture(scope='function')
208 212 def app(request, pylonsapp, http_environ):
209 213 app = TestApp(
210 214 pylonsapp,
211 215 extra_environ=http_environ)
212 216 if request.cls:
213 217 request.cls.app = app
214 218 return app
215 219
216 220
217 221 @pytest.fixture()
218 222 def app_settings(pylonsapp, pylons_config):
219 223 """
220 224 Settings dictionary used to create the app.
221 225
222 226 Parses the ini file and passes the result through the sanitize and apply
223 227 defaults mechanism in `rhodecode.config.middleware`.
224 228 """
225 229 from paste.deploy.loadwsgi import loadcontext, APP
226 230 from rhodecode.config.middleware import (
227 231 sanitize_settings_and_apply_defaults)
228 232 context = loadcontext(APP, 'config:' + pylons_config)
229 233 settings = sanitize_settings_and_apply_defaults(context.config())
230 234 return settings
231 235
232 236
233 237 LoginData = collections.namedtuple('LoginData', ('csrf_token', 'user'))
234 238
235 239
236 240 def _autologin_user(app, *args):
237 241 session = login_user_session(app, *args)
238 242 csrf_token = rhodecode.lib.auth.get_csrf_token(session)
239 243 return LoginData(csrf_token, session['rhodecode_user'])
240 244
241 245
242 246 @pytest.fixture
243 247 def autologin_user(app):
244 248 """
245 249 Utility fixture which makes sure that the admin user is logged in
246 250 """
247 251 return _autologin_user(app)
248 252
249 253
250 254 @pytest.fixture
251 255 def autologin_regular_user(app):
252 256 """
253 257 Utility fixture which makes sure that the regular user is logged in
254 258 """
255 259 return _autologin_user(
256 260 app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
257 261
258 262
259 263 @pytest.fixture(scope='function')
260 264 def csrf_token(request, autologin_user):
261 265 return autologin_user.csrf_token
262 266
263 267
264 268 @pytest.fixture(scope='function')
265 269 def xhr_header(request):
266 270 return {'HTTP_X_REQUESTED_WITH': 'XMLHttpRequest'}
267 271
268 272
269 273 @pytest.fixture
270 274 def real_crypto_backend(monkeypatch):
271 275 """
272 276 Switch the production crypto backend on for this test.
273 277
274 278 During the test run the crypto backend is replaced with a faster
275 279 implementation based on the MD5 algorithm.
276 280 """
277 281 monkeypatch.setattr(rhodecode, 'is_test', False)
278 282
279 283
280 284 @pytest.fixture(scope='class')
281 285 def index_location(request, pylonsapp):
282 286 index_location = pylonsapp.config['app_conf']['search.location']
283 287 if request.cls:
284 288 request.cls.index_location = index_location
285 289 return index_location
286 290
287 291
288 292 @pytest.fixture(scope='session', autouse=True)
289 293 def tests_tmp_path(request):
290 294 """
291 295 Create temporary directory to be used during the test session.
292 296 """
293 297 if not os.path.exists(TESTS_TMP_PATH):
294 298 os.makedirs(TESTS_TMP_PATH)
295 299
296 300 if not request.config.getoption('--keep-tmp-path'):
297 301 @request.addfinalizer
298 302 def remove_tmp_path():
299 303 shutil.rmtree(TESTS_TMP_PATH)
300 304
301 305 return TESTS_TMP_PATH
302 306
303 307
304 308 @pytest.fixture(scope='session', autouse=True)
305 309 def patch_pyro_request_scope_proxy_factory(request):
306 310 """
307 311 Patch the pyro proxy factory to always use the same dummy request object
308 312 when under test. This will return the same pyro proxy on every call.
309 313 """
310 314 dummy_request = pyramid.testing.DummyRequest()
311 315
312 316 def mocked_call(self, request=None):
313 317 return self.getProxy(request=dummy_request)
314 318
315 319 patcher = mock.patch(
316 320 'rhodecode.lib.vcs.client.RequestScopeProxyFactory.__call__',
317 321 new=mocked_call)
318 322 patcher.start()
319 323
320 324 @request.addfinalizer
321 325 def undo_patching():
322 326 patcher.stop()
323 327
324 328
325 329 @pytest.fixture
326 330 def test_repo_group(request):
327 331 """
328 332 Create a temporary repository group, and destroy it after
329 333 usage automatically
330 334 """
331 335 fixture = Fixture()
332 336 repogroupid = 'test_repo_group_%s' % int(time.time())
333 337 repo_group = fixture.create_repo_group(repogroupid)
334 338
335 339 def _cleanup():
336 340 fixture.destroy_repo_group(repogroupid)
337 341
338 342 request.addfinalizer(_cleanup)
339 343 return repo_group
340 344
341 345
342 346 @pytest.fixture
343 347 def test_user_group(request):
344 348 """
345 349 Create a temporary user group, and destroy it after
346 350 usage automatically
347 351 """
348 352 fixture = Fixture()
349 353 usergroupid = 'test_user_group_%s' % int(time.time())
350 354 user_group = fixture.create_user_group(usergroupid)
351 355
352 356 def _cleanup():
353 357 fixture.destroy_user_group(user_group)
354 358
355 359 request.addfinalizer(_cleanup)
356 360 return user_group
357 361
358 362
359 363 @pytest.fixture(scope='session')
360 364 def test_repo(request):
361 365 container = TestRepoContainer()
362 366 request.addfinalizer(container._cleanup)
363 367 return container
364 368
365 369
366 370 class TestRepoContainer(object):
367 371 """
368 372 Container for test repositories which are used read only.
369 373
370 374 Repositories will be created on demand and re-used during the lifetime
371 375 of this object.
372 376
373 377 Usage to get the svn test repository "minimal"::
374 378
375 379 test_repo = TestContainer()
376 380 repo = test_repo('minimal', 'svn')
377 381
378 382 """
379 383
380 384 dump_extractors = {
381 385 'git': utils.extract_git_repo_from_dump,
382 386 'hg': utils.extract_hg_repo_from_dump,
383 387 'svn': utils.extract_svn_repo_from_dump,
384 388 }
385 389
386 390 def __init__(self):
387 391 self._cleanup_repos = []
388 392 self._fixture = Fixture()
389 393 self._repos = {}
390 394
391 395 def __call__(self, dump_name, backend_alias):
392 396 key = (dump_name, backend_alias)
393 397 if key not in self._repos:
394 398 repo = self._create_repo(dump_name, backend_alias)
395 399 self._repos[key] = repo.repo_id
396 400 return Repository.get(self._repos[key])
397 401
398 402 def _create_repo(self, dump_name, backend_alias):
399 403 repo_name = '%s-%s' % (backend_alias, dump_name)
400 404 backend_class = get_backend(backend_alias)
401 405 dump_extractor = self.dump_extractors[backend_alias]
402 406 repo_path = dump_extractor(dump_name, repo_name)
403 407 vcs_repo = backend_class(repo_path)
404 408 repo2db_mapper({repo_name: vcs_repo})
405 409 repo = RepoModel().get_by_repo_name(repo_name)
406 410 self._cleanup_repos.append(repo_name)
407 411 return repo
408 412
409 413 def _cleanup(self):
410 414 for repo_name in reversed(self._cleanup_repos):
411 415 self._fixture.destroy_repo(repo_name)
412 416
413 417
414 418 @pytest.fixture
415 419 def backend(request, backend_alias, pylonsapp, test_repo):
416 420 """
417 421 Parametrized fixture which represents a single backend implementation.
418 422
419 423 It respects the option `--backends` to focus the test run on specific
420 424 backend implementations.
421 425
422 426 It also supports `pytest.mark.xfail_backends` to mark tests as failing
423 427 for specific backends. This is intended as a utility for incremental
424 428 development of a new backend implementation.
425 429 """
426 430 if backend_alias not in request.config.getoption('--backends'):
427 431 pytest.skip("Backend %s not selected." % (backend_alias, ))
428 432
429 433 utils.check_xfail_backends(request.node, backend_alias)
430 434 utils.check_skip_backends(request.node, backend_alias)
431 435
432 436 repo_name = 'vcs_test_%s' % (backend_alias, )
433 437 backend = Backend(
434 438 alias=backend_alias,
435 439 repo_name=repo_name,
436 440 test_name=request.node.name,
437 441 test_repo_container=test_repo)
438 442 request.addfinalizer(backend.cleanup)
439 443 return backend
440 444
441 445
442 446 @pytest.fixture
443 447 def backend_git(request, pylonsapp, test_repo):
444 448 return backend(request, 'git', pylonsapp, test_repo)
445 449
446 450
447 451 @pytest.fixture
448 452 def backend_hg(request, pylonsapp, test_repo):
449 453 return backend(request, 'hg', pylonsapp, test_repo)
450 454
451 455
452 456 @pytest.fixture
453 457 def backend_svn(request, pylonsapp, test_repo):
454 458 return backend(request, 'svn', pylonsapp, test_repo)
455 459
456 460
457 461 @pytest.fixture
458 462 def backend_random(backend_git):
459 463 """
460 464 Use this to express that your tests need "a backend.
461 465
462 466 A few of our tests need a backend, so that we can run the code. This
463 467 fixture is intended to be used for such cases. It will pick one of the
464 468 backends and run the tests.
465 469
466 470 The fixture `backend` would run the test multiple times for each
467 471 available backend which is a pure waste of time if the test is
468 472 independent of the backend type.
469 473 """
470 474 # TODO: johbo: Change this to pick a random backend
471 475 return backend_git
472 476
473 477
474 478 @pytest.fixture
475 479 def backend_stub(backend_git):
476 480 """
477 481 Use this to express that your tests need a backend stub
478 482
479 483 TODO: mikhail: Implement a real stub logic instead of returning
480 484 a git backend
481 485 """
482 486 return backend_git
483 487
484 488
485 489 @pytest.fixture
486 490 def repo_stub(backend_stub):
487 491 """
488 492 Use this to express that your tests need a repository stub
489 493 """
490 494 return backend_stub.create_repo()
491 495
492 496
493 497 class Backend(object):
494 498 """
495 499 Represents the test configuration for one supported backend
496 500
497 501 Provides easy access to different test repositories based on
498 502 `__getitem__`. Such repositories will only be created once per test
499 503 session.
500 504 """
501 505
502 506 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
503 507 _master_repo = None
504 508 _commit_ids = {}
505 509
506 510 def __init__(self, alias, repo_name, test_name, test_repo_container):
507 511 self.alias = alias
508 512 self.repo_name = repo_name
509 513 self._cleanup_repos = []
510 514 self._test_name = test_name
511 515 self._test_repo_container = test_repo_container
512 516 # TODO: johbo: Used as a delegate interim. Not yet sure if Backend or
513 517 # Fixture will survive in the end.
514 518 self._fixture = Fixture()
515 519
516 520 def __getitem__(self, key):
517 521 return self._test_repo_container(key, self.alias)
518 522
519 523 @property
520 524 def repo(self):
521 525 """
522 526 Returns the "current" repository. This is the vcs_test repo or the
523 527 last repo which has been created with `create_repo`.
524 528 """
525 529 from rhodecode.model.db import Repository
526 530 return Repository.get_by_repo_name(self.repo_name)
527 531
528 532 @property
529 533 def default_branch_name(self):
530 534 VcsRepository = get_backend(self.alias)
531 535 return VcsRepository.DEFAULT_BRANCH_NAME
532 536
533 537 @property
534 538 def default_head_id(self):
535 539 """
536 540 Returns the default head id of the underlying backend.
537 541
538 542 This will be the default branch name in case the backend does have a
539 543 default branch. In the other cases it will point to a valid head
540 544 which can serve as the base to create a new commit on top of it.
541 545 """
542 546 vcsrepo = self.repo.scm_instance()
543 547 head_id = (
544 548 vcsrepo.DEFAULT_BRANCH_NAME or
545 549 vcsrepo.commit_ids[-1])
546 550 return head_id
547 551
548 552 @property
549 553 def commit_ids(self):
550 554 """
551 555 Returns the list of commits for the last created repository
552 556 """
553 557 return self._commit_ids
554 558
555 559 def create_master_repo(self, commits):
556 560 """
557 561 Create a repository and remember it as a template.
558 562
559 563 This allows to easily create derived repositories to construct
560 564 more complex scenarios for diff, compare and pull requests.
561 565
562 566 Returns a commit map which maps from commit message to raw_id.
563 567 """
564 568 self._master_repo = self.create_repo(commits=commits)
565 569 return self._commit_ids
566 570
567 571 def create_repo(
568 572 self, commits=None, number_of_commits=0, heads=None,
569 573 name_suffix=u'', **kwargs):
570 574 """
571 575 Create a repository and record it for later cleanup.
572 576
573 577 :param commits: Optional. A sequence of dict instances.
574 578 Will add a commit per entry to the new repository.
575 579 :param number_of_commits: Optional. If set to a number, this number of
576 580 commits will be added to the new repository.
577 581 :param heads: Optional. Can be set to a sequence of of commit
578 582 names which shall be pulled in from the master repository.
579 583
580 584 """
581 585 self.repo_name = self._next_repo_name() + name_suffix
582 586 repo = self._fixture.create_repo(
583 587 self.repo_name, repo_type=self.alias, **kwargs)
584 588 self._cleanup_repos.append(repo.repo_name)
585 589
586 590 commits = commits or [
587 591 {'message': 'Commit %s of %s' % (x, self.repo_name)}
588 592 for x in xrange(number_of_commits)]
589 593 self._add_commits_to_repo(repo.scm_instance(), commits)
590 594 if heads:
591 595 self.pull_heads(repo, heads)
592 596
593 597 return repo
594 598
595 599 def pull_heads(self, repo, heads):
596 600 """
597 601 Make sure that repo contains all commits mentioned in `heads`
598 602 """
599 603 vcsmaster = self._master_repo.scm_instance()
600 604 vcsrepo = repo.scm_instance()
601 605 vcsrepo.config.clear_section('hooks')
602 606 commit_ids = [self._commit_ids[h] for h in heads]
603 607 vcsrepo.pull(vcsmaster.path, commit_ids=commit_ids)
604 608
605 609 def create_fork(self):
606 610 repo_to_fork = self.repo_name
607 611 self.repo_name = self._next_repo_name()
608 612 repo = self._fixture.create_fork(repo_to_fork, self.repo_name)
609 613 self._cleanup_repos.append(self.repo_name)
610 614 return repo
611 615
612 616 def new_repo_name(self, suffix=u''):
613 617 self.repo_name = self._next_repo_name() + suffix
614 618 self._cleanup_repos.append(self.repo_name)
615 619 return self.repo_name
616 620
617 621 def _next_repo_name(self):
618 622 return u"%s_%s" % (
619 623 self.invalid_repo_name.sub(u'_', self._test_name),
620 624 len(self._cleanup_repos))
621 625
622 626 def ensure_file(self, filename, content='Test content\n'):
623 627 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
624 628 commits = [
625 629 {'added': [
626 630 FileNode(filename, content=content),
627 631 ]},
628 632 ]
629 633 self._add_commits_to_repo(self.repo.scm_instance(), commits)
630 634
631 635 def enable_downloads(self):
632 636 repo = self.repo
633 637 repo.enable_downloads = True
634 638 Session().add(repo)
635 639 Session().commit()
636 640
637 641 def cleanup(self):
638 642 for repo_name in reversed(self._cleanup_repos):
639 643 self._fixture.destroy_repo(repo_name)
640 644
641 645 def _add_commits_to_repo(self, repo, commits):
642 646 if not commits:
643 647 return
644 648
645 649 imc = repo.in_memory_commit
646 650 commit = None
647 651 self._commit_ids = {}
648 652
649 653 for idx, commit in enumerate(commits):
650 654 message = unicode(commit.get('message', 'Commit %s' % idx))
651 655
652 656 for node in commit.get('added', []):
653 657 imc.add(FileNode(node.path, content=node.content))
654 658 for node in commit.get('changed', []):
655 659 imc.change(FileNode(node.path, content=node.content))
656 660 for node in commit.get('removed', []):
657 661 imc.remove(FileNode(node.path))
658 662
659 663 parents = [
660 664 repo.get_commit(commit_id=self._commit_ids[p])
661 665 for p in commit.get('parents', [])]
662 666
663 667 operations = ('added', 'changed', 'removed')
664 668 if not any((commit.get(o) for o in operations)):
665 669 imc.add(FileNode('file_%s' % idx, content=message))
666 670
667 671 commit = imc.commit(
668 672 message=message,
669 673 author=unicode(commit.get('author', 'Automatic')),
670 674 date=commit.get('date'),
671 675 branch=commit.get('branch'),
672 676 parents=parents)
673 677
674 678 self._commit_ids[commit.message] = commit.raw_id
675 679
676 680 # Creating refs for Git to allow fetching them from remote repository
677 681 if self.alias == 'git':
678 682 refs = {}
679 683 for message in self._commit_ids:
680 684 # TODO: mikhail: do more special chars replacements
681 685 ref_name = 'refs/test-refs/{}'.format(
682 686 message.replace(' ', ''))
683 687 refs[ref_name] = self._commit_ids[message]
684 688 self._create_refs(repo, refs)
685 689
686 690 return commit
687 691
688 692 def _create_refs(self, repo, refs):
689 693 for ref_name in refs:
690 694 repo.set_refs(ref_name, refs[ref_name])
691 695
692 696
693 697 @pytest.fixture
694 698 def vcsbackend(request, backend_alias, tests_tmp_path, pylonsapp, test_repo):
695 699 """
696 700 Parametrized fixture which represents a single vcs backend implementation.
697 701
698 702 See the fixture `backend` for more details. This one implements the same
699 703 concept, but on vcs level. So it does not provide model instances etc.
700 704
701 705 Parameters are generated dynamically, see :func:`pytest_generate_tests`
702 706 for how this works.
703 707 """
704 708 if backend_alias not in request.config.getoption('--backends'):
705 709 pytest.skip("Backend %s not selected." % (backend_alias, ))
706 710
707 711 utils.check_xfail_backends(request.node, backend_alias)
708 712 utils.check_skip_backends(request.node, backend_alias)
709 713
710 714 repo_name = 'vcs_test_%s' % (backend_alias, )
711 715 repo_path = os.path.join(tests_tmp_path, repo_name)
712 716 backend = VcsBackend(
713 717 alias=backend_alias,
714 718 repo_path=repo_path,
715 719 test_name=request.node.name,
716 720 test_repo_container=test_repo)
717 721 request.addfinalizer(backend.cleanup)
718 722 return backend
719 723
720 724
721 725 @pytest.fixture
722 726 def vcsbackend_git(request, tests_tmp_path, pylonsapp, test_repo):
723 727 return vcsbackend(request, 'git', tests_tmp_path, pylonsapp, test_repo)
724 728
725 729
726 730 @pytest.fixture
727 731 def vcsbackend_hg(request, tests_tmp_path, pylonsapp, test_repo):
728 732 return vcsbackend(request, 'hg', tests_tmp_path, pylonsapp, test_repo)
729 733
730 734
731 735 @pytest.fixture
732 736 def vcsbackend_svn(request, tests_tmp_path, pylonsapp, test_repo):
733 737 return vcsbackend(request, 'svn', tests_tmp_path, pylonsapp, test_repo)
734 738
735 739
736 740 @pytest.fixture
737 741 def vcsbackend_random(vcsbackend_git):
738 742 """
739 743 Use this to express that your tests need "a vcsbackend".
740 744
741 745 The fixture `vcsbackend` would run the test multiple times for each
742 746 available vcs backend which is a pure waste of time if the test is
743 747 independent of the vcs backend type.
744 748 """
745 749 # TODO: johbo: Change this to pick a random backend
746 750 return vcsbackend_git
747 751
748 752
749 753 class VcsBackend(object):
750 754 """
751 755 Represents the test configuration for one supported vcs backend.
752 756 """
753 757
754 758 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
755 759
756 760 def __init__(self, alias, repo_path, test_name, test_repo_container):
757 761 self.alias = alias
758 762 self._repo_path = repo_path
759 763 self._cleanup_repos = []
760 764 self._test_name = test_name
761 765 self._test_repo_container = test_repo_container
762 766
763 767 def __getitem__(self, key):
764 768 return self._test_repo_container(key, self.alias).scm_instance()
765 769
766 770 @property
767 771 def repo(self):
768 772 """
769 773 Returns the "current" repository. This is the vcs_test repo of the last
770 774 repo which has been created.
771 775 """
772 776 Repository = get_backend(self.alias)
773 777 return Repository(self._repo_path)
774 778
775 779 @property
776 780 def backend(self):
777 781 """
778 782 Returns the backend implementation class.
779 783 """
780 784 return get_backend(self.alias)
781 785
782 786 def create_repo(self, number_of_commits=0, _clone_repo=None):
783 787 repo_name = self._next_repo_name()
784 788 self._repo_path = get_new_dir(repo_name)
785 789 Repository = get_backend(self.alias)
786 790 src_url = None
787 791 if _clone_repo:
788 792 src_url = _clone_repo.path
789 793 repo = Repository(self._repo_path, create=True, src_url=src_url)
790 794 self._cleanup_repos.append(repo)
791 795 for idx in xrange(number_of_commits):
792 796 self.ensure_file(filename='file_%s' % idx, content=repo.name)
793 797 return repo
794 798
795 799 def clone_repo(self, repo):
796 800 return self.create_repo(_clone_repo=repo)
797 801
798 802 def cleanup(self):
799 803 for repo in self._cleanup_repos:
800 804 shutil.rmtree(repo.path)
801 805
802 806 def new_repo_path(self):
803 807 repo_name = self._next_repo_name()
804 808 self._repo_path = get_new_dir(repo_name)
805 809 return self._repo_path
806 810
807 811 def _next_repo_name(self):
808 812 return "%s_%s" % (
809 813 self.invalid_repo_name.sub('_', self._test_name),
810 814 len(self._cleanup_repos))
811 815
812 816 def add_file(self, repo, filename, content='Test content\n'):
813 817 imc = repo.in_memory_commit
814 818 imc.add(FileNode(filename, content=content))
815 819 imc.commit(
816 820 message=u'Automatic commit from vcsbackend fixture',
817 821 author=u'Automatic')
818 822
819 823 def ensure_file(self, filename, content='Test content\n'):
820 824 assert self._cleanup_repos, "Avoid writing into vcs_test repos"
821 825 self.add_file(self.repo, filename, content)
822 826
823 827
824 828 @pytest.fixture
825 829 def reposerver(request):
826 830 """
827 831 Allows to serve a backend repository
828 832 """
829 833
830 834 repo_server = RepoServer()
831 835 request.addfinalizer(repo_server.cleanup)
832 836 return repo_server
833 837
834 838
835 839 class RepoServer(object):
836 840 """
837 841 Utility to serve a local repository for the duration of a test case.
838 842
839 843 Supports only Subversion so far.
840 844 """
841 845
842 846 url = None
843 847
844 848 def __init__(self):
845 849 self._cleanup_servers = []
846 850
847 851 def serve(self, vcsrepo):
848 852 if vcsrepo.alias != 'svn':
849 853 raise TypeError("Backend %s not supported" % vcsrepo.alias)
850 854
851 855 proc = subprocess.Popen(
852 856 ['svnserve', '-d', '--foreground', '--listen-host', 'localhost',
853 857 '--root', vcsrepo.path])
854 858 self._cleanup_servers.append(proc)
855 859 self.url = 'svn://localhost'
856 860
857 861 def cleanup(self):
858 862 for proc in self._cleanup_servers:
859 863 proc.terminate()
860 864
861 865
862 866 @pytest.fixture
863 867 def pr_util(backend, request):
864 868 """
865 869 Utility for tests of models and for functional tests around pull requests.
866 870
867 871 It gives an instance of :class:`PRTestUtility` which provides various
868 872 utility methods around one pull request.
869 873
870 874 This fixture uses `backend` and inherits its parameterization.
871 875 """
872 876
873 877 util = PRTestUtility(backend)
874 878
875 879 @request.addfinalizer
876 880 def cleanup():
877 881 util.cleanup()
878 882
879 883 return util
880 884
881 885
882 886 class PRTestUtility(object):
883 887
884 888 pull_request = None
885 889 pull_request_id = None
886 890 mergeable_patcher = None
887 891 mergeable_mock = None
888 892 notification_patcher = None
889 893
890 894 def __init__(self, backend):
891 895 self.backend = backend
892 896
893 897 def create_pull_request(
894 898 self, commits=None, target_head=None, source_head=None,
895 899 revisions=None, approved=False, author=None, mergeable=False,
896 900 enable_notifications=True, name_suffix=u'', reviewers=None,
897 901 title=u"Test", description=u"Description"):
898 902 self.set_mergeable(mergeable)
899 903 if not enable_notifications:
900 904 # mock notification side effect
901 905 self.notification_patcher = mock.patch(
902 906 'rhodecode.model.notification.NotificationModel.create')
903 907 self.notification_patcher.start()
904 908
905 909 if not self.pull_request:
906 910 if not commits:
907 911 commits = [
908 912 {'message': 'c1'},
909 913 {'message': 'c2'},
910 914 {'message': 'c3'},
911 915 ]
912 916 target_head = 'c1'
913 917 source_head = 'c2'
914 918 revisions = ['c2']
915 919
916 920 self.commit_ids = self.backend.create_master_repo(commits)
917 921 self.target_repository = self.backend.create_repo(
918 922 heads=[target_head], name_suffix=name_suffix)
919 923 self.source_repository = self.backend.create_repo(
920 924 heads=[source_head], name_suffix=name_suffix)
921 925 self.author = author or UserModel().get_by_username(
922 926 TEST_USER_ADMIN_LOGIN)
923 927
924 928 model = PullRequestModel()
925 929 self.create_parameters = {
926 930 'created_by': self.author,
927 931 'source_repo': self.source_repository.repo_name,
928 932 'source_ref': self._default_branch_reference(source_head),
929 933 'target_repo': self.target_repository.repo_name,
930 934 'target_ref': self._default_branch_reference(target_head),
931 935 'revisions': [self.commit_ids[r] for r in revisions],
932 936 'reviewers': reviewers or self._get_reviewers(),
933 937 'title': title,
934 938 'description': description,
935 939 }
936 940 self.pull_request = model.create(**self.create_parameters)
937 941 assert model.get_versions(self.pull_request) == []
938 942
939 943 self.pull_request_id = self.pull_request.pull_request_id
940 944
941 945 if approved:
942 946 self.approve()
943 947
944 948 Session().add(self.pull_request)
945 949 Session().commit()
946 950
947 951 return self.pull_request
948 952
949 953 def approve(self):
950 954 self.create_status_votes(
951 955 ChangesetStatus.STATUS_APPROVED,
952 956 *self.pull_request.reviewers)
953 957
954 958 def close(self):
955 959 PullRequestModel().close_pull_request(self.pull_request, self.author)
956 960
957 961 def _default_branch_reference(self, commit_message):
958 962 reference = '%s:%s:%s' % (
959 963 'branch',
960 964 self.backend.default_branch_name,
961 965 self.commit_ids[commit_message])
962 966 return reference
963 967
964 968 def _get_reviewers(self):
965 969 model = UserModel()
966 970 return [
967 971 model.get_by_username(TEST_USER_REGULAR_LOGIN),
968 972 model.get_by_username(TEST_USER_REGULAR2_LOGIN),
969 973 ]
970 974
971 975 def update_source_repository(self, head=None):
972 976 heads = [head or 'c3']
973 977 self.backend.pull_heads(self.source_repository, heads=heads)
974 978
975 979 def add_one_commit(self, head=None):
976 980 self.update_source_repository(head=head)
977 981 old_commit_ids = set(self.pull_request.revisions)
978 982 PullRequestModel().update_commits(self.pull_request)
979 983 commit_ids = set(self.pull_request.revisions)
980 984 new_commit_ids = commit_ids - old_commit_ids
981 985 assert len(new_commit_ids) == 1
982 986 return new_commit_ids.pop()
983 987
984 988 def remove_one_commit(self):
985 989 assert len(self.pull_request.revisions) == 2
986 990 source_vcs = self.source_repository.scm_instance()
987 991 removed_commit_id = source_vcs.commit_ids[-1]
988 992
989 993 # TODO: johbo: Git and Mercurial have an inconsistent vcs api here,
990 994 # remove the if once that's sorted out.
991 995 if self.backend.alias == "git":
992 996 kwargs = {'branch_name': self.backend.default_branch_name}
993 997 else:
994 998 kwargs = {}
995 999 source_vcs.strip(removed_commit_id, **kwargs)
996 1000
997 1001 PullRequestModel().update_commits(self.pull_request)
998 1002 assert len(self.pull_request.revisions) == 1
999 1003 return removed_commit_id
1000 1004
1001 1005 def create_comment(self, linked_to=None):
1002 1006 comment = ChangesetCommentsModel().create(
1003 1007 text=u"Test comment",
1004 1008 repo=self.target_repository.repo_name,
1005 1009 user=self.author,
1006 1010 pull_request=self.pull_request)
1007 1011 assert comment.pull_request_version_id is None
1008 1012
1009 1013 if linked_to:
1010 1014 PullRequestModel()._link_comments_to_version(linked_to)
1011 1015
1012 1016 return comment
1013 1017
1014 1018 def create_inline_comment(
1015 1019 self, linked_to=None, line_no=u'n1', file_path='file_1'):
1016 1020 comment = ChangesetCommentsModel().create(
1017 1021 text=u"Test comment",
1018 1022 repo=self.target_repository.repo_name,
1019 1023 user=self.author,
1020 1024 line_no=line_no,
1021 1025 f_path=file_path,
1022 1026 pull_request=self.pull_request)
1023 1027 assert comment.pull_request_version_id is None
1024 1028
1025 1029 if linked_to:
1026 1030 PullRequestModel()._link_comments_to_version(linked_to)
1027 1031
1028 1032 return comment
1029 1033
1030 1034 def create_version_of_pull_request(self):
1031 1035 pull_request = self.create_pull_request()
1032 1036 version = PullRequestModel()._create_version_from_snapshot(
1033 1037 pull_request)
1034 1038 return version
1035 1039
1036 1040 def create_status_votes(self, status, *reviewers):
1037 1041 for reviewer in reviewers:
1038 1042 ChangesetStatusModel().set_status(
1039 1043 repo=self.pull_request.target_repo,
1040 1044 status=status,
1041 1045 user=reviewer.user_id,
1042 1046 pull_request=self.pull_request)
1043 1047
1044 1048 def set_mergeable(self, value):
1045 1049 if not self.mergeable_patcher:
1046 1050 self.mergeable_patcher = mock.patch.object(
1047 1051 VcsSettingsModel, 'get_general_settings')
1048 1052 self.mergeable_mock = self.mergeable_patcher.start()
1049 1053 self.mergeable_mock.return_value = {
1050 1054 'rhodecode_pr_merge_enabled': value}
1051 1055
1052 1056 def cleanup(self):
1053 1057 # In case the source repository is already cleaned up, the pull
1054 1058 # request will already be deleted.
1055 1059 pull_request = PullRequest().get(self.pull_request_id)
1056 1060 if pull_request:
1057 1061 PullRequestModel().delete(pull_request)
1058 1062 Session().commit()
1059 1063
1060 1064 if self.notification_patcher:
1061 1065 self.notification_patcher.stop()
1062 1066
1063 1067 if self.mergeable_patcher:
1064 1068 self.mergeable_patcher.stop()
1065 1069
1066 1070
1067 1071 @pytest.fixture
1068 1072 def user_admin(pylonsapp):
1069 1073 """
1070 1074 Provides the default admin test user as an instance of `db.User`.
1071 1075 """
1072 1076 user = UserModel().get_by_username(TEST_USER_ADMIN_LOGIN)
1073 1077 return user
1074 1078
1075 1079
1076 1080 @pytest.fixture
1077 1081 def user_regular(pylonsapp):
1078 1082 """
1079 1083 Provides the default regular test user as an instance of `db.User`.
1080 1084 """
1081 1085 user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
1082 1086 return user
1083 1087
1084 1088
1085 1089 @pytest.fixture
1086 1090 def user_util(request, pylonsapp):
1087 1091 """
1088 1092 Provides a wired instance of `UserUtility` with integrated cleanup.
1089 1093 """
1090 1094 utility = UserUtility(test_name=request.node.name)
1091 1095 request.addfinalizer(utility.cleanup)
1092 1096 return utility
1093 1097
1094 1098
1095 1099 # TODO: johbo: Split this up into utilities per domain or something similar
1096 1100 class UserUtility(object):
1097 1101
1098 1102 def __init__(self, test_name="test"):
1099 1103 self._test_name = test_name
1100 1104 self.fixture = Fixture()
1101 1105 self.repo_group_ids = []
1102 1106 self.user_ids = []
1103 1107 self.user_group_ids = []
1104 1108 self.user_repo_permission_ids = []
1105 1109 self.user_group_repo_permission_ids = []
1106 1110 self.user_repo_group_permission_ids = []
1107 1111 self.user_group_repo_group_permission_ids = []
1108 1112 self.user_user_group_permission_ids = []
1109 1113 self.user_group_user_group_permission_ids = []
1110 1114 self.user_permissions = []
1111 1115
1112 1116 def create_repo_group(
1113 1117 self, owner=TEST_USER_ADMIN_LOGIN, auto_cleanup=True):
1114 1118 group_name = "{prefix}_repogroup_{count}".format(
1115 1119 prefix=self._test_name,
1116 1120 count=len(self.repo_group_ids))
1117 1121 repo_group = self.fixture.create_repo_group(
1118 1122 group_name, cur_user=owner)
1119 1123 if auto_cleanup:
1120 1124 self.repo_group_ids.append(repo_group.group_id)
1121 1125 return repo_group
1122 1126
1123 1127 def create_user(self, auto_cleanup=True, **kwargs):
1124 1128 user_name = "{prefix}_user_{count}".format(
1125 1129 prefix=self._test_name,
1126 1130 count=len(self.user_ids))
1127 1131 user = self.fixture.create_user(user_name, **kwargs)
1128 1132 if auto_cleanup:
1129 1133 self.user_ids.append(user.user_id)
1130 1134 return user
1131 1135
1132 1136 def create_user_with_group(self):
1133 1137 user = self.create_user()
1134 1138 user_group = self.create_user_group(members=[user])
1135 1139 return user, user_group
1136 1140
1137 1141 def create_user_group(self, members=None, auto_cleanup=True, **kwargs):
1138 1142 group_name = "{prefix}_usergroup_{count}".format(
1139 1143 prefix=self._test_name,
1140 1144 count=len(self.user_group_ids))
1141 1145 user_group = self.fixture.create_user_group(group_name, **kwargs)
1142 1146 if auto_cleanup:
1143 1147 self.user_group_ids.append(user_group.users_group_id)
1144 1148 if members:
1145 1149 for user in members:
1146 1150 UserGroupModel().add_user_to_group(user_group, user)
1147 1151 return user_group
1148 1152
1149 1153 def grant_user_permission(self, user_name, permission_name):
1150 1154 self._inherit_default_user_permissions(user_name, False)
1151 1155 self.user_permissions.append((user_name, permission_name))
1152 1156
1153 1157 def grant_user_permission_to_repo_group(
1154 1158 self, repo_group, user, permission_name):
1155 1159 permission = RepoGroupModel().grant_user_permission(
1156 1160 repo_group, user, permission_name)
1157 1161 self.user_repo_group_permission_ids.append(
1158 1162 (repo_group.group_id, user.user_id))
1159 1163 return permission
1160 1164
1161 1165 def grant_user_group_permission_to_repo_group(
1162 1166 self, repo_group, user_group, permission_name):
1163 1167 permission = RepoGroupModel().grant_user_group_permission(
1164 1168 repo_group, user_group, permission_name)
1165 1169 self.user_group_repo_group_permission_ids.append(
1166 1170 (repo_group.group_id, user_group.users_group_id))
1167 1171 return permission
1168 1172
1169 1173 def grant_user_permission_to_repo(
1170 1174 self, repo, user, permission_name):
1171 1175 permission = RepoModel().grant_user_permission(
1172 1176 repo, user, permission_name)
1173 1177 self.user_repo_permission_ids.append(
1174 1178 (repo.repo_id, user.user_id))
1175 1179 return permission
1176 1180
1177 1181 def grant_user_group_permission_to_repo(
1178 1182 self, repo, user_group, permission_name):
1179 1183 permission = RepoModel().grant_user_group_permission(
1180 1184 repo, user_group, permission_name)
1181 1185 self.user_group_repo_permission_ids.append(
1182 1186 (repo.repo_id, user_group.users_group_id))
1183 1187 return permission
1184 1188
1185 1189 def grant_user_permission_to_user_group(
1186 1190 self, target_user_group, user, permission_name):
1187 1191 permission = UserGroupModel().grant_user_permission(
1188 1192 target_user_group, user, permission_name)
1189 1193 self.user_user_group_permission_ids.append(
1190 1194 (target_user_group.users_group_id, user.user_id))
1191 1195 return permission
1192 1196
1193 1197 def grant_user_group_permission_to_user_group(
1194 1198 self, target_user_group, user_group, permission_name):
1195 1199 permission = UserGroupModel().grant_user_group_permission(
1196 1200 target_user_group, user_group, permission_name)
1197 1201 self.user_group_user_group_permission_ids.append(
1198 1202 (target_user_group.users_group_id, user_group.users_group_id))
1199 1203 return permission
1200 1204
1201 1205 def revoke_user_permission(self, user_name, permission_name):
1202 1206 self._inherit_default_user_permissions(user_name, True)
1203 1207 UserModel().revoke_perm(user_name, permission_name)
1204 1208
1205 1209 def _inherit_default_user_permissions(self, user_name, value):
1206 1210 user = UserModel().get_by_username(user_name)
1207 1211 user.inherit_default_permissions = value
1208 1212 Session().add(user)
1209 1213 Session().commit()
1210 1214
1211 1215 def cleanup(self):
1212 1216 self._cleanup_permissions()
1213 1217 self._cleanup_repo_groups()
1214 1218 self._cleanup_user_groups()
1215 1219 self._cleanup_users()
1216 1220
1217 1221 def _cleanup_permissions(self):
1218 1222 if self.user_permissions:
1219 1223 for user_name, permission_name in self.user_permissions:
1220 1224 self.revoke_user_permission(user_name, permission_name)
1221 1225
1222 1226 for permission in self.user_repo_permission_ids:
1223 1227 RepoModel().revoke_user_permission(*permission)
1224 1228
1225 1229 for permission in self.user_group_repo_permission_ids:
1226 1230 RepoModel().revoke_user_group_permission(*permission)
1227 1231
1228 1232 for permission in self.user_repo_group_permission_ids:
1229 1233 RepoGroupModel().revoke_user_permission(*permission)
1230 1234
1231 1235 for permission in self.user_group_repo_group_permission_ids:
1232 1236 RepoGroupModel().revoke_user_group_permission(*permission)
1233 1237
1234 1238 for permission in self.user_user_group_permission_ids:
1235 1239 UserGroupModel().revoke_user_permission(*permission)
1236 1240
1237 1241 for permission in self.user_group_user_group_permission_ids:
1238 1242 UserGroupModel().revoke_user_group_permission(*permission)
1239 1243
1240 1244 def _cleanup_repo_groups(self):
1241 1245 def _repo_group_compare(first_group_id, second_group_id):
1242 1246 """
1243 1247 Gives higher priority to the groups with the most complex paths
1244 1248 """
1245 1249 first_group = RepoGroup.get(first_group_id)
1246 1250 second_group = RepoGroup.get(second_group_id)
1247 1251 first_group_parts = (
1248 1252 len(first_group.group_name.split('/')) if first_group else 0)
1249 1253 second_group_parts = (
1250 1254 len(second_group.group_name.split('/')) if second_group else 0)
1251 1255 return cmp(second_group_parts, first_group_parts)
1252 1256
1253 1257 sorted_repo_group_ids = sorted(
1254 1258 self.repo_group_ids, cmp=_repo_group_compare)
1255 1259 for repo_group_id in sorted_repo_group_ids:
1256 1260 self.fixture.destroy_repo_group(repo_group_id)
1257 1261
1258 1262 def _cleanup_user_groups(self):
1259 1263 def _user_group_compare(first_group_id, second_group_id):
1260 1264 """
1261 1265 Gives higher priority to the groups with the most complex paths
1262 1266 """
1263 1267 first_group = UserGroup.get(first_group_id)
1264 1268 second_group = UserGroup.get(second_group_id)
1265 1269 first_group_parts = (
1266 1270 len(first_group.users_group_name.split('/'))
1267 1271 if first_group else 0)
1268 1272 second_group_parts = (
1269 1273 len(second_group.users_group_name.split('/'))
1270 1274 if second_group else 0)
1271 1275 return cmp(second_group_parts, first_group_parts)
1272 1276
1273 1277 sorted_user_group_ids = sorted(
1274 1278 self.user_group_ids, cmp=_user_group_compare)
1275 1279 for user_group_id in sorted_user_group_ids:
1276 1280 self.fixture.destroy_user_group(user_group_id)
1277 1281
1278 1282 def _cleanup_users(self):
1279 1283 for user_id in self.user_ids:
1280 1284 self.fixture.destroy_user(user_id)
1281 1285
1282 1286
1283 1287 # TODO: Think about moving this into a pytest-pyro package and make it a
1284 1288 # pytest plugin
1285 1289 @pytest.hookimpl(tryfirst=True, hookwrapper=True)
1286 1290 def pytest_runtest_makereport(item, call):
1287 1291 """
1288 1292 Adding the remote traceback if the exception has this information.
1289 1293
1290 1294 Pyro4 attaches this information as the attribute `_pyroTraceback`
1291 1295 to the exception instance.
1292 1296 """
1293 1297 outcome = yield
1294 1298 report = outcome.get_result()
1295 1299 if call.excinfo:
1296 1300 _add_pyro_remote_traceback(report, call.excinfo.value)
1297 1301
1298 1302
1299 1303 def _add_pyro_remote_traceback(report, exc):
1300 1304 pyro_traceback = getattr(exc, '_pyroTraceback', None)
1301 1305
1302 1306 if pyro_traceback:
1303 1307 traceback = ''.join(pyro_traceback)
1304 1308 section = 'Pyro4 remote traceback ' + report.when
1305 1309 report.sections.append((section, traceback))
1306 1310
1307 1311
1308 1312 @pytest.fixture(scope='session')
1309 1313 def testrun():
1310 1314 return {
1311 1315 'uuid': uuid.uuid4(),
1312 1316 'start': datetime.datetime.utcnow().isoformat(),
1313 1317 'timestamp': int(time.time()),
1314 1318 }
1315 1319
1316 1320
1317 1321 @pytest.fixture(autouse=True)
1318 1322 def collect_appenlight_stats(request, testrun):
1319 1323 """
1320 1324 This fixture reports memory consumtion of single tests.
1321 1325
1322 1326 It gathers data based on `psutil` and sends them to Appenlight. The option
1323 1327 ``--ae`` has te be used to enable this fixture and the API key for your
1324 1328 application has to be provided in ``--ae-key``.
1325 1329 """
1326 1330 try:
1327 1331 # cygwin cannot have yet psutil support.
1328 1332 import psutil
1329 1333 except ImportError:
1330 1334 return
1331 1335
1332 1336 if not request.config.getoption('--appenlight'):
1333 1337 return
1334 1338 else:
1335 1339 # Only request the pylonsapp fixture if appenlight tracking is
1336 1340 # enabled. This will speed up a test run of unit tests by 2 to 3
1337 1341 # seconds if appenlight is not enabled.
1338 1342 pylonsapp = request.getfuncargvalue("pylonsapp")
1339 1343 url = '{}/api/logs'.format(request.config.getoption('--appenlight-url'))
1340 1344 client = AppenlightClient(
1341 1345 url=url,
1342 1346 api_key=request.config.getoption('--appenlight-api-key'),
1343 1347 namespace=request.node.nodeid,
1344 1348 request=str(testrun['uuid']),
1345 1349 testrun=testrun)
1346 1350
1347 1351 client.collect({
1348 1352 'message': "Starting",
1349 1353 })
1350 1354
1351 1355 server_and_port = pylonsapp.config['vcs.server']
1352 1356 server = create_vcsserver_proxy(server_and_port)
1353 1357 with server:
1354 1358 vcs_pid = server.get_pid()
1355 1359 server.run_gc()
1356 1360 vcs_process = psutil.Process(vcs_pid)
1357 1361 mem = vcs_process.memory_info()
1358 1362 client.tag_before('vcsserver.rss', mem.rss)
1359 1363 client.tag_before('vcsserver.vms', mem.vms)
1360 1364
1361 1365 test_process = psutil.Process()
1362 1366 mem = test_process.memory_info()
1363 1367 client.tag_before('test.rss', mem.rss)
1364 1368 client.tag_before('test.vms', mem.vms)
1365 1369
1366 1370 client.tag_before('time', time.time())
1367 1371
1368 1372 @request.addfinalizer
1369 1373 def send_stats():
1370 1374 client.tag_after('time', time.time())
1371 1375 with server:
1372 1376 gc_stats = server.run_gc()
1373 1377 for tag, value in gc_stats.items():
1374 1378 client.tag_after(tag, value)
1375 1379 mem = vcs_process.memory_info()
1376 1380 client.tag_after('vcsserver.rss', mem.rss)
1377 1381 client.tag_after('vcsserver.vms', mem.vms)
1378 1382
1379 1383 mem = test_process.memory_info()
1380 1384 client.tag_after('test.rss', mem.rss)
1381 1385 client.tag_after('test.vms', mem.vms)
1382 1386
1383 1387 client.collect({
1384 1388 'message': "Finished",
1385 1389 })
1386 1390 client.send_stats()
1387 1391
1388 1392 return client
1389 1393
1390 1394
1391 1395 class AppenlightClient():
1392 1396
1393 1397 url_template = '{url}?protocol_version=0.5'
1394 1398
1395 1399 def __init__(
1396 1400 self, url, api_key, add_server=True, add_timestamp=True,
1397 1401 namespace=None, request=None, testrun=None):
1398 1402 self.url = self.url_template.format(url=url)
1399 1403 self.api_key = api_key
1400 1404 self.add_server = add_server
1401 1405 self.add_timestamp = add_timestamp
1402 1406 self.namespace = namespace
1403 1407 self.request = request
1404 1408 self.server = socket.getfqdn(socket.gethostname())
1405 1409 self.tags_before = {}
1406 1410 self.tags_after = {}
1407 1411 self.stats = []
1408 1412 self.testrun = testrun or {}
1409 1413
1410 1414 def tag_before(self, tag, value):
1411 1415 self.tags_before[tag] = value
1412 1416
1413 1417 def tag_after(self, tag, value):
1414 1418 self.tags_after[tag] = value
1415 1419
1416 1420 def collect(self, data):
1417 1421 if self.add_server:
1418 1422 data.setdefault('server', self.server)
1419 1423 if self.add_timestamp:
1420 1424 data.setdefault('date', datetime.datetime.utcnow().isoformat())
1421 1425 if self.namespace:
1422 1426 data.setdefault('namespace', self.namespace)
1423 1427 if self.request:
1424 1428 data.setdefault('request', self.request)
1425 1429 self.stats.append(data)
1426 1430
1427 1431 def send_stats(self):
1428 1432 tags = [
1429 1433 ('testrun', self.request),
1430 1434 ('testrun.start', self.testrun['start']),
1431 1435 ('testrun.timestamp', self.testrun['timestamp']),
1432 1436 ('test', self.namespace),
1433 1437 ]
1434 1438 for key, value in self.tags_before.items():
1435 1439 tags.append((key + '.before', value))
1436 1440 try:
1437 1441 delta = self.tags_after[key] - value
1438 1442 tags.append((key + '.delta', delta))
1439 1443 except Exception:
1440 1444 pass
1441 1445 for key, value in self.tags_after.items():
1442 1446 tags.append((key + '.after', value))
1443 1447 self.collect({
1444 1448 'message': "Collected tags",
1445 1449 'tags': tags,
1446 1450 })
1447 1451
1448 1452 response = requests.post(
1449 1453 self.url,
1450 1454 headers={
1451 1455 'X-appenlight-api-key': self.api_key},
1452 1456 json=self.stats,
1453 1457 )
1454 1458
1455 1459 if not response.status_code == 200:
1456 1460 pprint.pprint(self.stats)
1457 1461 print response.headers
1458 1462 print response.text
1459 1463 raise Exception('Sending to appenlight failed')
1460 1464
1461 1465
1462 1466 @pytest.fixture
1463 1467 def gist_util(request, pylonsapp):
1464 1468 """
1465 1469 Provides a wired instance of `GistUtility` with integrated cleanup.
1466 1470 """
1467 1471 utility = GistUtility()
1468 1472 request.addfinalizer(utility.cleanup)
1469 1473 return utility
1470 1474
1471 1475
1472 1476 class GistUtility(object):
1473 1477 def __init__(self):
1474 1478 self.fixture = Fixture()
1475 1479 self.gist_ids = []
1476 1480
1477 1481 def create_gist(self, **kwargs):
1478 1482 gist = self.fixture.create_gist(**kwargs)
1479 1483 self.gist_ids.append(gist.gist_id)
1480 1484 return gist
1481 1485
1482 1486 def cleanup(self):
1483 1487 for id_ in self.gist_ids:
1484 1488 self.fixture.destroy_gists(str(id_))
1485 1489
1486 1490
1487 1491 @pytest.fixture
1488 1492 def enabled_backends(request):
1489 1493 backends = request.config.option.backends
1490 1494 return backends[:]
1491 1495
1492 1496
1493 1497 @pytest.fixture
1494 1498 def settings_util(request):
1495 1499 """
1496 1500 Provides a wired instance of `SettingsUtility` with integrated cleanup.
1497 1501 """
1498 1502 utility = SettingsUtility()
1499 1503 request.addfinalizer(utility.cleanup)
1500 1504 return utility
1501 1505
1502 1506
1503 1507 class SettingsUtility(object):
1504 1508 def __init__(self):
1505 1509 self.rhodecode_ui_ids = []
1506 1510 self.rhodecode_setting_ids = []
1507 1511 self.repo_rhodecode_ui_ids = []
1508 1512 self.repo_rhodecode_setting_ids = []
1509 1513
1510 1514 def create_repo_rhodecode_ui(
1511 1515 self, repo, section, value, key=None, active=True, cleanup=True):
1512 1516 key = key or hashlib.sha1(
1513 1517 '{}{}{}'.format(section, value, repo.repo_id)).hexdigest()
1514 1518
1515 1519 setting = RepoRhodeCodeUi()
1516 1520 setting.repository_id = repo.repo_id
1517 1521 setting.ui_section = section
1518 1522 setting.ui_value = value
1519 1523 setting.ui_key = key
1520 1524 setting.ui_active = active
1521 1525 Session().add(setting)
1522 1526 Session().commit()
1523 1527
1524 1528 if cleanup:
1525 1529 self.repo_rhodecode_ui_ids.append(setting.ui_id)
1526 1530 return setting
1527 1531
1528 1532 def create_rhodecode_ui(
1529 1533 self, section, value, key=None, active=True, cleanup=True):
1530 1534 key = key or hashlib.sha1('{}{}'.format(section, value)).hexdigest()
1531 1535
1532 1536 setting = RhodeCodeUi()
1533 1537 setting.ui_section = section
1534 1538 setting.ui_value = value
1535 1539 setting.ui_key = key
1536 1540 setting.ui_active = active
1537 1541 Session().add(setting)
1538 1542 Session().commit()
1539 1543
1540 1544 if cleanup:
1541 1545 self.rhodecode_ui_ids.append(setting.ui_id)
1542 1546 return setting
1543 1547
1544 1548 def create_repo_rhodecode_setting(
1545 1549 self, repo, name, value, type_, cleanup=True):
1546 1550 setting = RepoRhodeCodeSetting(
1547 1551 repo.repo_id, key=name, val=value, type=type_)
1548 1552 Session().add(setting)
1549 1553 Session().commit()
1550 1554
1551 1555 if cleanup:
1552 1556 self.repo_rhodecode_setting_ids.append(setting.app_settings_id)
1553 1557 return setting
1554 1558
1555 1559 def create_rhodecode_setting(self, name, value, type_, cleanup=True):
1556 1560 setting = RhodeCodeSetting(key=name, val=value, type=type_)
1557 1561 Session().add(setting)
1558 1562 Session().commit()
1559 1563
1560 1564 if cleanup:
1561 1565 self.rhodecode_setting_ids.append(setting.app_settings_id)
1562 1566
1563 1567 return setting
1564 1568
1565 1569 def cleanup(self):
1566 1570 for id_ in self.rhodecode_ui_ids:
1567 1571 setting = RhodeCodeUi.get(id_)
1568 1572 Session().delete(setting)
1569 1573
1570 1574 for id_ in self.rhodecode_setting_ids:
1571 1575 setting = RhodeCodeSetting.get(id_)
1572 1576 Session().delete(setting)
1573 1577
1574 1578 for id_ in self.repo_rhodecode_ui_ids:
1575 1579 setting = RepoRhodeCodeUi.get(id_)
1576 1580 Session().delete(setting)
1577 1581
1578 1582 for id_ in self.repo_rhodecode_setting_ids:
1579 1583 setting = RepoRhodeCodeSetting.get(id_)
1580 1584 Session().delete(setting)
1581 1585
1582 1586 Session().commit()
1583 1587
1584 1588
1585 1589 @pytest.fixture
1586 1590 def no_notifications(request):
1587 1591 notification_patcher = mock.patch(
1588 1592 'rhodecode.model.notification.NotificationModel.create')
1589 1593 notification_patcher.start()
1590 1594 request.addfinalizer(notification_patcher.stop)
1591 1595
1592 1596
1593 1597 @pytest.fixture
1594 1598 def silence_action_logger(request):
1595 1599 notification_patcher = mock.patch(
1596 1600 'rhodecode.lib.utils.action_logger')
1597 1601 notification_patcher.start()
1598 1602 request.addfinalizer(notification_patcher.stop)
1599 1603
1600 1604
1601 1605 @pytest.fixture(scope='session')
1602 1606 def repeat(request):
1603 1607 """
1604 1608 The number of repetitions is based on this fixture.
1605 1609
1606 1610 Slower calls may divide it by 10 or 100. It is chosen in a way so that the
1607 1611 tests are not too slow in our default test suite.
1608 1612 """
1609 1613 return request.config.getoption('--repeat')
1610 1614
1611 1615
1612 1616 @pytest.fixture
1613 1617 def rhodecode_fixtures():
1614 1618 return Fixture()
1615 1619
1616 1620
1617 1621 @pytest.fixture
1618 1622 def request_stub():
1619 1623 """
1620 1624 Stub request object.
1621 1625 """
1622 1626 request = pyramid.testing.DummyRequest()
1623 1627 request.scheme = 'https'
1624 1628 return request
1625 1629
1626 1630
1627 1631 @pytest.fixture
1628 1632 def config_stub(request, request_stub):
1629 1633 """
1630 1634 Set up pyramid.testing and return the Configurator.
1631 1635 """
1632 1636 config = pyramid.testing.setUp(request=request_stub)
1633 1637
1634 1638 @request.addfinalizer
1635 1639 def cleanup():
1636 1640 pyramid.testing.tearDown()
1637 1641
1638 1642 return config
1643
1644
1645 @pytest.fixture
1646 def StubIntegrationType():
1647 class _StubIntegrationType(IntegrationTypeBase):
1648 """ Test integration type class """
1649
1650 key = 'test'
1651 display_name = 'Test integration type'
1652 description = 'A test integration type for testing'
1653 icon = 'test_icon_html_image'
1654
1655 def __init__(self, settings):
1656 super(_StubIntegrationType, self).__init__(settings)
1657 self.sent_events = [] # for testing
1658
1659 def send_event(self, event):
1660 self.sent_events.append(event)
1661
1662 def settings_schema(self):
1663 class SettingsSchema(colander.Schema):
1664 test_string_field = colander.SchemaNode(
1665 colander.String(),
1666 missing=colander.required,
1667 title='test string field',
1668 )
1669 test_int_field = colander.SchemaNode(
1670 colander.Int(),
1671 title='some integer setting',
1672 )
1673 return SettingsSchema()
1674
1675
1676 integration_type_registry.register_integration_type(_StubIntegrationType)
1677 return _StubIntegrationType
1678
1679 @pytest.fixture
1680 def stub_integration_settings():
1681 return {
1682 'test_string_field': 'some data',
1683 'test_int_field': 100,
1684 }
1685
1686
1687 @pytest.fixture
1688 def repo_integration_stub(request, repo_stub, StubIntegrationType,
1689 stub_integration_settings):
1690 integration = IntegrationModel().create(
1691 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1692 name='test repo integration', scope=repo_stub)
1693
1694 @request.addfinalizer
1695 def cleanup():
1696 IntegrationModel().delete(integration)
1697
1698 return integration
1699
1700
1701 @pytest.fixture
1702 def repogroup_integration_stub(request, test_repo_group, StubIntegrationType,
1703 stub_integration_settings):
1704 integration = IntegrationModel().create(
1705 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1706 name='test repogroup integration', scope=test_repo_group)
1707
1708 @request.addfinalizer
1709 def cleanup():
1710 IntegrationModel().delete(integration)
1711
1712 return integration
1713
1714
1715 @pytest.fixture
1716 def global_integration_stub(request, StubIntegrationType,
1717 stub_integration_settings):
1718 integration = IntegrationModel().create(
1719 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1720 name='test global integration', scope='global')
1721
1722 @request.addfinalizer
1723 def cleanup():
1724 IntegrationModel().delete(integration)
1725
1726 return integration
1727
1728
1729 @pytest.fixture
1730 def root_repos_integration_stub(request, StubIntegrationType,
1731 stub_integration_settings):
1732 integration = IntegrationModel().create(
1733 StubIntegrationType, settings=stub_integration_settings, enabled=True,
1734 name='test global integration', scope='root_repos')
1735
1736 @request.addfinalizer
1737 def cleanup():
1738 IntegrationModel().delete(integration)
1739
1740 return integration
General Comments 0
You need to be logged in to leave comments. Login now