##// END OF EJS Templates
reviewers: add repo review rule models and expose default...
dan -
r821:618c046d default
parent child Browse files
Show More
This diff has been collapsed as it changes many lines, (3640 lines changed) Show them Hide them
@@ -0,0 +1,3640 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 re
26 import os
27 import sys
28 import time
29 import hashlib
30 import logging
31 import datetime
32 import warnings
33 import ipaddress
34 import functools
35 import traceback
36 import collections
37
38
39 from sqlalchemy import *
40 from sqlalchemy.exc import IntegrityError
41 from sqlalchemy.ext.declarative import declared_attr
42 from sqlalchemy.ext.hybrid import hybrid_property
43 from sqlalchemy.orm import (
44 relationship, joinedload, class_mapper, validates, aliased)
45 from sqlalchemy.sql.expression import true
46 from beaker.cache import cache_region, region_invalidate
47 from webob.exc import HTTPNotFound
48 from zope.cachedescriptors.property import Lazy as LazyProperty
49
50 from pylons import url
51 from pylons.i18n.translation import lazy_ugettext as _
52
53 from rhodecode.lib.vcs import get_backend, get_vcs_instance
54 from rhodecode.lib.vcs.utils.helpers import get_scm
55 from rhodecode.lib.vcs.exceptions import VCSError
56 from rhodecode.lib.vcs.backends.base import (
57 EmptyCommit, Reference, MergeFailureReason)
58 from rhodecode.lib.utils2 import (
59 str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
61 glob2re)
62 from rhodecode.lib.jsonalchemy import MutationObj, JsonType, JSONDict
63 from rhodecode.lib.ext_json import json
64 from rhodecode.lib.caching_query import FromCache
65 from rhodecode.lib.encrypt import AESCipher
66
67 from rhodecode.model.meta import Base, Session
68
69 URL_SEP = '/'
70 log = logging.getLogger(__name__)
71
72 # =============================================================================
73 # BASE CLASSES
74 # =============================================================================
75
76 # this is propagated from .ini file rhodecode.encrypted_values.secret or
77 # beaker.session.secret if first is not set.
78 # and initialized at environment.py
79 ENCRYPTION_KEY = None
80
81 # used to sort permissions by types, '#' used here is not allowed to be in
82 # usernames, and it's very early in sorted string.printable table.
83 PERMISSION_TYPE_SORT = {
84 'admin': '####',
85 'write': '###',
86 'read': '##',
87 'none': '#',
88 }
89
90
91 def display_sort(obj):
92 """
93 Sort function used to sort permissions in .permissions() function of
94 Repository, RepoGroup, UserGroup. Also it put the default user in front
95 of all other resources
96 """
97
98 if obj.username == User.DEFAULT_USER:
99 return '#####'
100 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
101 return prefix + obj.username
102
103
104 def _hash_key(k):
105 return md5_safe(k)
106
107
108 class EncryptedTextValue(TypeDecorator):
109 """
110 Special column for encrypted long text data, use like::
111
112 value = Column("encrypted_value", EncryptedValue(), nullable=False)
113
114 This column is intelligent so if value is in unencrypted form it return
115 unencrypted form, but on save it always encrypts
116 """
117 impl = Text
118
119 def process_bind_param(self, value, dialect):
120 if not value:
121 return value
122 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
123 # protect against double encrypting if someone manually starts
124 # doing
125 raise ValueError('value needs to be in unencrypted format, ie. '
126 'not starting with enc$aes')
127 return 'enc$aes_hmac$%s' % AESCipher(
128 ENCRYPTION_KEY, hmac=True).encrypt(value)
129
130 def process_result_value(self, value, dialect):
131 import rhodecode
132
133 if not value:
134 return value
135
136 parts = value.split('$', 3)
137 if not len(parts) == 3:
138 # probably not encrypted values
139 return value
140 else:
141 if parts[0] != 'enc':
142 # parts ok but without our header ?
143 return value
144 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
145 'rhodecode.encrypted_values.strict') or True)
146 # at that stage we know it's our encryption
147 if parts[1] == 'aes':
148 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
149 elif parts[1] == 'aes_hmac':
150 decrypted_data = AESCipher(
151 ENCRYPTION_KEY, hmac=True,
152 strict_verification=enc_strict_mode).decrypt(parts[2])
153 else:
154 raise ValueError(
155 'Encryption type part is wrong, must be `aes` '
156 'or `aes_hmac`, got `%s` instead' % (parts[1]))
157 return decrypted_data
158
159
160 class BaseModel(object):
161 """
162 Base Model for all classes
163 """
164
165 @classmethod
166 def _get_keys(cls):
167 """return column names for this model """
168 return class_mapper(cls).c.keys()
169
170 def get_dict(self):
171 """
172 return dict with keys and values corresponding
173 to this model data """
174
175 d = {}
176 for k in self._get_keys():
177 d[k] = getattr(self, k)
178
179 # also use __json__() if present to get additional fields
180 _json_attr = getattr(self, '__json__', None)
181 if _json_attr:
182 # update with attributes from __json__
183 if callable(_json_attr):
184 _json_attr = _json_attr()
185 for k, val in _json_attr.iteritems():
186 d[k] = val
187 return d
188
189 def get_appstruct(self):
190 """return list with keys and values tuples corresponding
191 to this model data """
192
193 l = []
194 for k in self._get_keys():
195 l.append((k, getattr(self, k),))
196 return l
197
198 def populate_obj(self, populate_dict):
199 """populate model with data from given populate_dict"""
200
201 for k in self._get_keys():
202 if k in populate_dict:
203 setattr(self, k, populate_dict[k])
204
205 @classmethod
206 def query(cls):
207 return Session().query(cls)
208
209 @classmethod
210 def get(cls, id_):
211 if id_:
212 return cls.query().get(id_)
213
214 @classmethod
215 def get_or_404(cls, id_):
216 try:
217 id_ = int(id_)
218 except (TypeError, ValueError):
219 raise HTTPNotFound
220
221 res = cls.query().get(id_)
222 if not res:
223 raise HTTPNotFound
224 return res
225
226 @classmethod
227 def getAll(cls):
228 # deprecated and left for backward compatibility
229 return cls.get_all()
230
231 @classmethod
232 def get_all(cls):
233 return cls.query().all()
234
235 @classmethod
236 def delete(cls, id_):
237 obj = cls.query().get(id_)
238 Session().delete(obj)
239
240 @classmethod
241 def identity_cache(cls, session, attr_name, value):
242 exist_in_session = []
243 for (item_cls, pkey), instance in session.identity_map.items():
244 if cls == item_cls and getattr(instance, attr_name) == value:
245 exist_in_session.append(instance)
246 if exist_in_session:
247 if len(exist_in_session) == 1:
248 return exist_in_session[0]
249 log.exception(
250 'multiple objects with attr %s and '
251 'value %s found with same name: %r',
252 attr_name, value, exist_in_session)
253
254 def __repr__(self):
255 if hasattr(self, '__unicode__'):
256 # python repr needs to return str
257 try:
258 return safe_str(self.__unicode__())
259 except UnicodeDecodeError:
260 pass
261 return '<DB:%s>' % (self.__class__.__name__)
262
263
264 class RhodeCodeSetting(Base, BaseModel):
265 __tablename__ = 'rhodecode_settings'
266 __table_args__ = (
267 UniqueConstraint('app_settings_name'),
268 {'extend_existing': True, 'mysql_engine': 'InnoDB',
269 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
270 )
271
272 SETTINGS_TYPES = {
273 'str': safe_str,
274 'int': safe_int,
275 'unicode': safe_unicode,
276 'bool': str2bool,
277 'list': functools.partial(aslist, sep=',')
278 }
279 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
280 GLOBAL_CONF_KEY = 'app_settings'
281
282 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
283 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
284 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
285 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
286
287 def __init__(self, key='', val='', type='unicode'):
288 self.app_settings_name = key
289 self.app_settings_type = type
290 self.app_settings_value = val
291
292 @validates('_app_settings_value')
293 def validate_settings_value(self, key, val):
294 assert type(val) == unicode
295 return val
296
297 @hybrid_property
298 def app_settings_value(self):
299 v = self._app_settings_value
300 _type = self.app_settings_type
301 if _type:
302 _type = self.app_settings_type.split('.')[0]
303 # decode the encrypted value
304 if 'encrypted' in self.app_settings_type:
305 cipher = EncryptedTextValue()
306 v = safe_unicode(cipher.process_result_value(v, None))
307
308 converter = self.SETTINGS_TYPES.get(_type) or \
309 self.SETTINGS_TYPES['unicode']
310 return converter(v)
311
312 @app_settings_value.setter
313 def app_settings_value(self, val):
314 """
315 Setter that will always make sure we use unicode in app_settings_value
316
317 :param val:
318 """
319 val = safe_unicode(val)
320 # encode the encrypted value
321 if 'encrypted' in self.app_settings_type:
322 cipher = EncryptedTextValue()
323 val = safe_unicode(cipher.process_bind_param(val, None))
324 self._app_settings_value = val
325
326 @hybrid_property
327 def app_settings_type(self):
328 return self._app_settings_type
329
330 @app_settings_type.setter
331 def app_settings_type(self, val):
332 if val.split('.')[0] not in self.SETTINGS_TYPES:
333 raise Exception('type must be one of %s got %s'
334 % (self.SETTINGS_TYPES.keys(), val))
335 self._app_settings_type = val
336
337 def __unicode__(self):
338 return u"<%s('%s:%s[%s]')>" % (
339 self.__class__.__name__,
340 self.app_settings_name, self.app_settings_value,
341 self.app_settings_type
342 )
343
344
345 class RhodeCodeUi(Base, BaseModel):
346 __tablename__ = 'rhodecode_ui'
347 __table_args__ = (
348 UniqueConstraint('ui_key'),
349 {'extend_existing': True, 'mysql_engine': 'InnoDB',
350 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
351 )
352
353 HOOK_REPO_SIZE = 'changegroup.repo_size'
354 # HG
355 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
356 HOOK_PULL = 'outgoing.pull_logger'
357 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
358 HOOK_PUSH = 'changegroup.push_logger'
359
360 # TODO: johbo: Unify way how hooks are configured for git and hg,
361 # git part is currently hardcoded.
362
363 # SVN PATTERNS
364 SVN_BRANCH_ID = 'vcs_svn_branch'
365 SVN_TAG_ID = 'vcs_svn_tag'
366
367 ui_id = Column(
368 "ui_id", Integer(), nullable=False, unique=True, default=None,
369 primary_key=True)
370 ui_section = Column(
371 "ui_section", String(255), nullable=True, unique=None, default=None)
372 ui_key = Column(
373 "ui_key", String(255), nullable=True, unique=None, default=None)
374 ui_value = Column(
375 "ui_value", String(255), nullable=True, unique=None, default=None)
376 ui_active = Column(
377 "ui_active", Boolean(), nullable=True, unique=None, default=True)
378
379 def __repr__(self):
380 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
381 self.ui_key, self.ui_value)
382
383
384 class RepoRhodeCodeSetting(Base, BaseModel):
385 __tablename__ = 'repo_rhodecode_settings'
386 __table_args__ = (
387 UniqueConstraint(
388 'app_settings_name', 'repository_id',
389 name='uq_repo_rhodecode_setting_name_repo_id'),
390 {'extend_existing': True, 'mysql_engine': 'InnoDB',
391 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
392 )
393
394 repository_id = Column(
395 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
396 nullable=False)
397 app_settings_id = Column(
398 "app_settings_id", Integer(), nullable=False, unique=True,
399 default=None, primary_key=True)
400 app_settings_name = Column(
401 "app_settings_name", String(255), nullable=True, unique=None,
402 default=None)
403 _app_settings_value = Column(
404 "app_settings_value", String(4096), nullable=True, unique=None,
405 default=None)
406 _app_settings_type = Column(
407 "app_settings_type", String(255), nullable=True, unique=None,
408 default=None)
409
410 repository = relationship('Repository')
411
412 def __init__(self, repository_id, key='', val='', type='unicode'):
413 self.repository_id = repository_id
414 self.app_settings_name = key
415 self.app_settings_type = type
416 self.app_settings_value = val
417
418 @validates('_app_settings_value')
419 def validate_settings_value(self, key, val):
420 assert type(val) == unicode
421 return val
422
423 @hybrid_property
424 def app_settings_value(self):
425 v = self._app_settings_value
426 type_ = self.app_settings_type
427 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
428 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
429 return converter(v)
430
431 @app_settings_value.setter
432 def app_settings_value(self, val):
433 """
434 Setter that will always make sure we use unicode in app_settings_value
435
436 :param val:
437 """
438 self._app_settings_value = safe_unicode(val)
439
440 @hybrid_property
441 def app_settings_type(self):
442 return self._app_settings_type
443
444 @app_settings_type.setter
445 def app_settings_type(self, val):
446 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
447 if val not in SETTINGS_TYPES:
448 raise Exception('type must be one of %s got %s'
449 % (SETTINGS_TYPES.keys(), val))
450 self._app_settings_type = val
451
452 def __unicode__(self):
453 return u"<%s('%s:%s:%s[%s]')>" % (
454 self.__class__.__name__, self.repository.repo_name,
455 self.app_settings_name, self.app_settings_value,
456 self.app_settings_type
457 )
458
459
460 class RepoRhodeCodeUi(Base, BaseModel):
461 __tablename__ = 'repo_rhodecode_ui'
462 __table_args__ = (
463 UniqueConstraint(
464 'repository_id', 'ui_section', 'ui_key',
465 name='uq_repo_rhodecode_ui_repository_id_section_key'),
466 {'extend_existing': True, 'mysql_engine': 'InnoDB',
467 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
468 )
469
470 repository_id = Column(
471 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
472 nullable=False)
473 ui_id = Column(
474 "ui_id", Integer(), nullable=False, unique=True, default=None,
475 primary_key=True)
476 ui_section = Column(
477 "ui_section", String(255), nullable=True, unique=None, default=None)
478 ui_key = Column(
479 "ui_key", String(255), nullable=True, unique=None, default=None)
480 ui_value = Column(
481 "ui_value", String(255), nullable=True, unique=None, default=None)
482 ui_active = Column(
483 "ui_active", Boolean(), nullable=True, unique=None, default=True)
484
485 repository = relationship('Repository')
486
487 def __repr__(self):
488 return '<%s[%s:%s]%s=>%s]>' % (
489 self.__class__.__name__, self.repository.repo_name,
490 self.ui_section, self.ui_key, self.ui_value)
491
492
493 class User(Base, BaseModel):
494 __tablename__ = 'users'
495 __table_args__ = (
496 UniqueConstraint('username'), UniqueConstraint('email'),
497 Index('u_username_idx', 'username'),
498 Index('u_email_idx', 'email'),
499 {'extend_existing': True, 'mysql_engine': 'InnoDB',
500 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
501 )
502 DEFAULT_USER = 'default'
503 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
504 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
505
506 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
507 username = Column("username", String(255), nullable=True, unique=None, default=None)
508 password = Column("password", String(255), nullable=True, unique=None, default=None)
509 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
510 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
511 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
512 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
513 _email = Column("email", String(255), nullable=True, unique=None, default=None)
514 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
515 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
516 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
517 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
518 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
519 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
520 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
521
522 user_log = relationship('UserLog')
523 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
524
525 repositories = relationship('Repository')
526 repository_groups = relationship('RepoGroup')
527 user_groups = relationship('UserGroup')
528
529 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
530 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
531
532 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
533 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
534 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
535
536 group_member = relationship('UserGroupMember', cascade='all')
537
538 notifications = relationship('UserNotification', cascade='all')
539 # notifications assigned to this user
540 user_created_notifications = relationship('Notification', cascade='all')
541 # comments created by this user
542 user_comments = relationship('ChangesetComment', cascade='all')
543 # user profile extra info
544 user_emails = relationship('UserEmailMap', cascade='all')
545 user_ip_map = relationship('UserIpMap', cascade='all')
546 user_auth_tokens = relationship('UserApiKeys', cascade='all')
547 # gists
548 user_gists = relationship('Gist', cascade='all')
549 # user pull requests
550 user_pull_requests = relationship('PullRequest', cascade='all')
551 # external identities
552 extenal_identities = relationship(
553 'ExternalIdentity',
554 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
555 cascade='all')
556
557 def __unicode__(self):
558 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
559 self.user_id, self.username)
560
561 @hybrid_property
562 def email(self):
563 return self._email
564
565 @email.setter
566 def email(self, val):
567 self._email = val.lower() if val else None
568
569 @property
570 def firstname(self):
571 # alias for future
572 return self.name
573
574 @property
575 def emails(self):
576 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
577 return [self.email] + [x.email for x in other]
578
579 @property
580 def auth_tokens(self):
581 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
582
583 @property
584 def extra_auth_tokens(self):
585 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
586
587 @property
588 def feed_token(self):
589 feed_tokens = UserApiKeys.query()\
590 .filter(UserApiKeys.user == self)\
591 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
592 .all()
593 if feed_tokens:
594 return feed_tokens[0].api_key
595 else:
596 # use the main token so we don't end up with nothing...
597 return self.api_key
598
599 @classmethod
600 def extra_valid_auth_tokens(cls, user, role=None):
601 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
602 .filter(or_(UserApiKeys.expires == -1,
603 UserApiKeys.expires >= time.time()))
604 if role:
605 tokens = tokens.filter(or_(UserApiKeys.role == role,
606 UserApiKeys.role == UserApiKeys.ROLE_ALL))
607 return tokens.all()
608
609 @property
610 def ip_addresses(self):
611 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
612 return [x.ip_addr for x in ret]
613
614 @property
615 def username_and_name(self):
616 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
617
618 @property
619 def username_or_name_or_email(self):
620 full_name = self.full_name if self.full_name is not ' ' else None
621 return self.username or full_name or self.email
622
623 @property
624 def full_name(self):
625 return '%s %s' % (self.firstname, self.lastname)
626
627 @property
628 def full_name_or_username(self):
629 return ('%s %s' % (self.firstname, self.lastname)
630 if (self.firstname and self.lastname) else self.username)
631
632 @property
633 def full_contact(self):
634 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
635
636 @property
637 def short_contact(self):
638 return '%s %s' % (self.firstname, self.lastname)
639
640 @property
641 def is_admin(self):
642 return self.admin
643
644 @property
645 def AuthUser(self):
646 """
647 Returns instance of AuthUser for this user
648 """
649 from rhodecode.lib.auth import AuthUser
650 return AuthUser(user_id=self.user_id, api_key=self.api_key,
651 username=self.username)
652
653 @hybrid_property
654 def user_data(self):
655 if not self._user_data:
656 return {}
657
658 try:
659 return json.loads(self._user_data)
660 except TypeError:
661 return {}
662
663 @user_data.setter
664 def user_data(self, val):
665 if not isinstance(val, dict):
666 raise Exception('user_data must be dict, got %s' % type(val))
667 try:
668 self._user_data = json.dumps(val)
669 except Exception:
670 log.error(traceback.format_exc())
671
672 @classmethod
673 def get_by_username(cls, username, case_insensitive=False,
674 cache=False, identity_cache=False):
675 session = Session()
676
677 if case_insensitive:
678 q = cls.query().filter(
679 func.lower(cls.username) == func.lower(username))
680 else:
681 q = cls.query().filter(cls.username == username)
682
683 if cache:
684 if identity_cache:
685 val = cls.identity_cache(session, 'username', username)
686 if val:
687 return val
688 else:
689 q = q.options(
690 FromCache("sql_cache_short",
691 "get_user_by_name_%s" % _hash_key(username)))
692
693 return q.scalar()
694
695 @classmethod
696 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
697 q = cls.query().filter(cls.api_key == auth_token)
698
699 if cache:
700 q = q.options(FromCache("sql_cache_short",
701 "get_auth_token_%s" % auth_token))
702 res = q.scalar()
703
704 if fallback and not res:
705 #fallback to additional keys
706 _res = UserApiKeys.query()\
707 .filter(UserApiKeys.api_key == auth_token)\
708 .filter(or_(UserApiKeys.expires == -1,
709 UserApiKeys.expires >= time.time()))\
710 .first()
711 if _res:
712 res = _res.user
713 return res
714
715 @classmethod
716 def get_by_email(cls, email, case_insensitive=False, cache=False):
717
718 if case_insensitive:
719 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
720
721 else:
722 q = cls.query().filter(cls.email == email)
723
724 if cache:
725 q = q.options(FromCache("sql_cache_short",
726 "get_email_key_%s" % _hash_key(email)))
727
728 ret = q.scalar()
729 if ret is None:
730 q = UserEmailMap.query()
731 # try fetching in alternate email map
732 if case_insensitive:
733 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
734 else:
735 q = q.filter(UserEmailMap.email == email)
736 q = q.options(joinedload(UserEmailMap.user))
737 if cache:
738 q = q.options(FromCache("sql_cache_short",
739 "get_email_map_key_%s" % email))
740 ret = getattr(q.scalar(), 'user', None)
741
742 return ret
743
744 @classmethod
745 def get_from_cs_author(cls, author):
746 """
747 Tries to get User objects out of commit author string
748
749 :param author:
750 """
751 from rhodecode.lib.helpers import email, author_name
752 # Valid email in the attribute passed, see if they're in the system
753 _email = email(author)
754 if _email:
755 user = cls.get_by_email(_email, case_insensitive=True)
756 if user:
757 return user
758 # Maybe we can match by username?
759 _author = author_name(author)
760 user = cls.get_by_username(_author, case_insensitive=True)
761 if user:
762 return user
763
764 def update_userdata(self, **kwargs):
765 usr = self
766 old = usr.user_data
767 old.update(**kwargs)
768 usr.user_data = old
769 Session().add(usr)
770 log.debug('updated userdata with ', kwargs)
771
772 def update_lastlogin(self):
773 """Update user lastlogin"""
774 self.last_login = datetime.datetime.now()
775 Session().add(self)
776 log.debug('updated user %s lastlogin', self.username)
777
778 def update_lastactivity(self):
779 """Update user lastactivity"""
780 usr = self
781 old = usr.user_data
782 old.update({'last_activity': time.time()})
783 usr.user_data = old
784 Session().add(usr)
785 log.debug('updated user %s lastactivity', usr.username)
786
787 def update_password(self, new_password, change_api_key=False):
788 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
789
790 self.password = get_crypt_password(new_password)
791 if change_api_key:
792 self.api_key = generate_auth_token(self.username)
793 Session().add(self)
794
795 @classmethod
796 def get_first_super_admin(cls):
797 user = User.query().filter(User.admin == true()).first()
798 if user is None:
799 raise Exception('FATAL: Missing administrative account!')
800 return user
801
802 @classmethod
803 def get_all_super_admins(cls):
804 """
805 Returns all admin accounts sorted by username
806 """
807 return User.query().filter(User.admin == true())\
808 .order_by(User.username.asc()).all()
809
810 @classmethod
811 def get_default_user(cls, cache=False):
812 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
813 if user is None:
814 raise Exception('FATAL: Missing default account!')
815 return user
816
817 def _get_default_perms(self, user, suffix=''):
818 from rhodecode.model.permission import PermissionModel
819 return PermissionModel().get_default_perms(user.user_perms, suffix)
820
821 def get_default_perms(self, suffix=''):
822 return self._get_default_perms(self, suffix)
823
824 def get_api_data(self, include_secrets=False, details='full'):
825 """
826 Common function for generating user related data for API
827
828 :param include_secrets: By default secrets in the API data will be replaced
829 by a placeholder value to prevent exposing this data by accident. In case
830 this data shall be exposed, set this flag to ``True``.
831
832 :param details: details can be 'basic|full' basic gives only a subset of
833 the available user information that includes user_id, name and emails.
834 """
835 user = self
836 user_data = self.user_data
837 data = {
838 'user_id': user.user_id,
839 'username': user.username,
840 'firstname': user.name,
841 'lastname': user.lastname,
842 'email': user.email,
843 'emails': user.emails,
844 }
845 if details == 'basic':
846 return data
847
848 api_key_length = 40
849 api_key_replacement = '*' * api_key_length
850
851 extras = {
852 'api_key': api_key_replacement,
853 'api_keys': [api_key_replacement],
854 'active': user.active,
855 'admin': user.admin,
856 'extern_type': user.extern_type,
857 'extern_name': user.extern_name,
858 'last_login': user.last_login,
859 'ip_addresses': user.ip_addresses,
860 'language': user_data.get('language')
861 }
862 data.update(extras)
863
864 if include_secrets:
865 data['api_key'] = user.api_key
866 data['api_keys'] = user.auth_tokens
867 return data
868
869 def __json__(self):
870 data = {
871 'full_name': self.full_name,
872 'full_name_or_username': self.full_name_or_username,
873 'short_contact': self.short_contact,
874 'full_contact': self.full_contact,
875 }
876 data.update(self.get_api_data())
877 return data
878
879
880 class UserApiKeys(Base, BaseModel):
881 __tablename__ = 'user_api_keys'
882 __table_args__ = (
883 Index('uak_api_key_idx', 'api_key'),
884 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
885 UniqueConstraint('api_key'),
886 {'extend_existing': True, 'mysql_engine': 'InnoDB',
887 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
888 )
889 __mapper_args__ = {}
890
891 # ApiKey role
892 ROLE_ALL = 'token_role_all'
893 ROLE_HTTP = 'token_role_http'
894 ROLE_VCS = 'token_role_vcs'
895 ROLE_API = 'token_role_api'
896 ROLE_FEED = 'token_role_feed'
897 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
898
899 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
900 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
901 api_key = Column("api_key", String(255), nullable=False, unique=True)
902 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
903 expires = Column('expires', Float(53), nullable=False)
904 role = Column('role', String(255), nullable=True)
905 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
906
907 user = relationship('User', lazy='joined')
908
909 @classmethod
910 def _get_role_name(cls, role):
911 return {
912 cls.ROLE_ALL: _('all'),
913 cls.ROLE_HTTP: _('http/web interface'),
914 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
915 cls.ROLE_API: _('api calls'),
916 cls.ROLE_FEED: _('feed access'),
917 }.get(role, role)
918
919 @property
920 def expired(self):
921 if self.expires == -1:
922 return False
923 return time.time() > self.expires
924
925 @property
926 def role_humanized(self):
927 return self._get_role_name(self.role)
928
929
930 class UserEmailMap(Base, BaseModel):
931 __tablename__ = 'user_email_map'
932 __table_args__ = (
933 Index('uem_email_idx', 'email'),
934 UniqueConstraint('email'),
935 {'extend_existing': True, 'mysql_engine': 'InnoDB',
936 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
937 )
938 __mapper_args__ = {}
939
940 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
941 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
942 _email = Column("email", String(255), nullable=True, unique=False, default=None)
943 user = relationship('User', lazy='joined')
944
945 @validates('_email')
946 def validate_email(self, key, email):
947 # check if this email is not main one
948 main_email = Session().query(User).filter(User.email == email).scalar()
949 if main_email is not None:
950 raise AttributeError('email %s is present is user table' % email)
951 return email
952
953 @hybrid_property
954 def email(self):
955 return self._email
956
957 @email.setter
958 def email(self, val):
959 self._email = val.lower() if val else None
960
961
962 class UserIpMap(Base, BaseModel):
963 __tablename__ = 'user_ip_map'
964 __table_args__ = (
965 UniqueConstraint('user_id', 'ip_addr'),
966 {'extend_existing': True, 'mysql_engine': 'InnoDB',
967 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
968 )
969 __mapper_args__ = {}
970
971 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
972 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
973 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
974 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
975 description = Column("description", String(10000), nullable=True, unique=None, default=None)
976 user = relationship('User', lazy='joined')
977
978 @classmethod
979 def _get_ip_range(cls, ip_addr):
980 net = ipaddress.ip_network(ip_addr, strict=False)
981 return [str(net.network_address), str(net.broadcast_address)]
982
983 def __json__(self):
984 return {
985 'ip_addr': self.ip_addr,
986 'ip_range': self._get_ip_range(self.ip_addr),
987 }
988
989 def __unicode__(self):
990 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
991 self.user_id, self.ip_addr)
992
993 class UserLog(Base, BaseModel):
994 __tablename__ = 'user_logs'
995 __table_args__ = (
996 {'extend_existing': True, 'mysql_engine': 'InnoDB',
997 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
998 )
999 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1000 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
1001 username = Column("username", String(255), nullable=True, unique=None, default=None)
1002 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1003 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1004 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1005 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1006 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1007
1008 def __unicode__(self):
1009 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1010 self.repository_name,
1011 self.action)
1012
1013 @property
1014 def action_as_day(self):
1015 return datetime.date(*self.action_date.timetuple()[:3])
1016
1017 user = relationship('User')
1018 repository = relationship('Repository', cascade='')
1019
1020
1021 class UserGroup(Base, BaseModel):
1022 __tablename__ = 'users_groups'
1023 __table_args__ = (
1024 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1025 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1026 )
1027
1028 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1029 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1030 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1031 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1032 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1033 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1034 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1035 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1036
1037 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1038 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1039 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1040 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1041 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1042 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1043
1044 user = relationship('User')
1045
1046 @hybrid_property
1047 def group_data(self):
1048 if not self._group_data:
1049 return {}
1050
1051 try:
1052 return json.loads(self._group_data)
1053 except TypeError:
1054 return {}
1055
1056 @group_data.setter
1057 def group_data(self, val):
1058 try:
1059 self._group_data = json.dumps(val)
1060 except Exception:
1061 log.error(traceback.format_exc())
1062
1063 def __unicode__(self):
1064 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1065 self.users_group_id,
1066 self.users_group_name)
1067
1068 @classmethod
1069 def get_by_group_name(cls, group_name, cache=False,
1070 case_insensitive=False):
1071 if case_insensitive:
1072 q = cls.query().filter(func.lower(cls.users_group_name) ==
1073 func.lower(group_name))
1074
1075 else:
1076 q = cls.query().filter(cls.users_group_name == group_name)
1077 if cache:
1078 q = q.options(FromCache(
1079 "sql_cache_short",
1080 "get_group_%s" % _hash_key(group_name)))
1081 return q.scalar()
1082
1083 @classmethod
1084 def get(cls, user_group_id, cache=False):
1085 user_group = cls.query()
1086 if cache:
1087 user_group = user_group.options(FromCache("sql_cache_short",
1088 "get_users_group_%s" % user_group_id))
1089 return user_group.get(user_group_id)
1090
1091 def permissions(self, with_admins=True, with_owner=True):
1092 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1093 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1094 joinedload(UserUserGroupToPerm.user),
1095 joinedload(UserUserGroupToPerm.permission),)
1096
1097 # get owners and admins and permissions. We do a trick of re-writing
1098 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1099 # has a global reference and changing one object propagates to all
1100 # others. This means if admin is also an owner admin_row that change
1101 # would propagate to both objects
1102 perm_rows = []
1103 for _usr in q.all():
1104 usr = AttributeDict(_usr.user.get_dict())
1105 usr.permission = _usr.permission.permission_name
1106 perm_rows.append(usr)
1107
1108 # filter the perm rows by 'default' first and then sort them by
1109 # admin,write,read,none permissions sorted again alphabetically in
1110 # each group
1111 perm_rows = sorted(perm_rows, key=display_sort)
1112
1113 _admin_perm = 'usergroup.admin'
1114 owner_row = []
1115 if with_owner:
1116 usr = AttributeDict(self.user.get_dict())
1117 usr.owner_row = True
1118 usr.permission = _admin_perm
1119 owner_row.append(usr)
1120
1121 super_admin_rows = []
1122 if with_admins:
1123 for usr in User.get_all_super_admins():
1124 # if this admin is also owner, don't double the record
1125 if usr.user_id == owner_row[0].user_id:
1126 owner_row[0].admin_row = True
1127 else:
1128 usr = AttributeDict(usr.get_dict())
1129 usr.admin_row = True
1130 usr.permission = _admin_perm
1131 super_admin_rows.append(usr)
1132
1133 return super_admin_rows + owner_row + perm_rows
1134
1135 def permission_user_groups(self):
1136 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1137 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1138 joinedload(UserGroupUserGroupToPerm.target_user_group),
1139 joinedload(UserGroupUserGroupToPerm.permission),)
1140
1141 perm_rows = []
1142 for _user_group in q.all():
1143 usr = AttributeDict(_user_group.user_group.get_dict())
1144 usr.permission = _user_group.permission.permission_name
1145 perm_rows.append(usr)
1146
1147 return perm_rows
1148
1149 def _get_default_perms(self, user_group, suffix=''):
1150 from rhodecode.model.permission import PermissionModel
1151 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1152
1153 def get_default_perms(self, suffix=''):
1154 return self._get_default_perms(self, suffix)
1155
1156 def get_api_data(self, with_group_members=True, include_secrets=False):
1157 """
1158 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1159 basically forwarded.
1160
1161 """
1162 user_group = self
1163
1164 data = {
1165 'users_group_id': user_group.users_group_id,
1166 'group_name': user_group.users_group_name,
1167 'group_description': user_group.user_group_description,
1168 'active': user_group.users_group_active,
1169 'owner': user_group.user.username,
1170 }
1171 if with_group_members:
1172 users = []
1173 for user in user_group.members:
1174 user = user.user
1175 users.append(user.get_api_data(include_secrets=include_secrets))
1176 data['users'] = users
1177
1178 return data
1179
1180
1181 class UserGroupMember(Base, BaseModel):
1182 __tablename__ = 'users_groups_members'
1183 __table_args__ = (
1184 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1185 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1186 )
1187
1188 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1189 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1190 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1191
1192 user = relationship('User', lazy='joined')
1193 users_group = relationship('UserGroup')
1194
1195 def __init__(self, gr_id='', u_id=''):
1196 self.users_group_id = gr_id
1197 self.user_id = u_id
1198
1199
1200 class RepositoryField(Base, BaseModel):
1201 __tablename__ = 'repositories_fields'
1202 __table_args__ = (
1203 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1204 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1205 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1206 )
1207 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1208
1209 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1210 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1211 field_key = Column("field_key", String(250))
1212 field_label = Column("field_label", String(1024), nullable=False)
1213 field_value = Column("field_value", String(10000), nullable=False)
1214 field_desc = Column("field_desc", String(1024), nullable=False)
1215 field_type = Column("field_type", String(255), nullable=False, unique=None)
1216 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1217
1218 repository = relationship('Repository')
1219
1220 @property
1221 def field_key_prefixed(self):
1222 return 'ex_%s' % self.field_key
1223
1224 @classmethod
1225 def un_prefix_key(cls, key):
1226 if key.startswith(cls.PREFIX):
1227 return key[len(cls.PREFIX):]
1228 return key
1229
1230 @classmethod
1231 def get_by_key_name(cls, key, repo):
1232 row = cls.query()\
1233 .filter(cls.repository == repo)\
1234 .filter(cls.field_key == key).scalar()
1235 return row
1236
1237
1238 class Repository(Base, BaseModel):
1239 __tablename__ = 'repositories'
1240 __table_args__ = (
1241 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1242 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1243 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1244 )
1245 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1246 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1247
1248 STATE_CREATED = 'repo_state_created'
1249 STATE_PENDING = 'repo_state_pending'
1250 STATE_ERROR = 'repo_state_error'
1251
1252 LOCK_AUTOMATIC = 'lock_auto'
1253 LOCK_API = 'lock_api'
1254 LOCK_WEB = 'lock_web'
1255 LOCK_PULL = 'lock_pull'
1256
1257 NAME_SEP = URL_SEP
1258
1259 repo_id = Column(
1260 "repo_id", Integer(), nullable=False, unique=True, default=None,
1261 primary_key=True)
1262 _repo_name = Column(
1263 "repo_name", Text(), nullable=False, default=None)
1264 _repo_name_hash = Column(
1265 "repo_name_hash", String(255), nullable=False, unique=True)
1266 repo_state = Column("repo_state", String(255), nullable=True)
1267
1268 clone_uri = Column(
1269 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1270 default=None)
1271 repo_type = Column(
1272 "repo_type", String(255), nullable=False, unique=False, default=None)
1273 user_id = Column(
1274 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1275 unique=False, default=None)
1276 private = Column(
1277 "private", Boolean(), nullable=True, unique=None, default=None)
1278 enable_statistics = Column(
1279 "statistics", Boolean(), nullable=True, unique=None, default=True)
1280 enable_downloads = Column(
1281 "downloads", Boolean(), nullable=True, unique=None, default=True)
1282 description = Column(
1283 "description", String(10000), nullable=True, unique=None, default=None)
1284 created_on = Column(
1285 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1286 default=datetime.datetime.now)
1287 updated_on = Column(
1288 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1289 default=datetime.datetime.now)
1290 _landing_revision = Column(
1291 "landing_revision", String(255), nullable=False, unique=False,
1292 default=None)
1293 enable_locking = Column(
1294 "enable_locking", Boolean(), nullable=False, unique=None,
1295 default=False)
1296 _locked = Column(
1297 "locked", String(255), nullable=True, unique=False, default=None)
1298 _changeset_cache = Column(
1299 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1300
1301 fork_id = Column(
1302 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1303 nullable=True, unique=False, default=None)
1304 group_id = Column(
1305 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1306 unique=False, default=None)
1307
1308 user = relationship('User', lazy='joined')
1309 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1310 group = relationship('RepoGroup', lazy='joined')
1311 repo_to_perm = relationship(
1312 'UserRepoToPerm', cascade='all',
1313 order_by='UserRepoToPerm.repo_to_perm_id')
1314 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1315 stats = relationship('Statistics', cascade='all', uselist=False)
1316
1317 followers = relationship(
1318 'UserFollowing',
1319 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1320 cascade='all')
1321 extra_fields = relationship(
1322 'RepositoryField', cascade="all, delete, delete-orphan")
1323 logs = relationship('UserLog')
1324 comments = relationship(
1325 'ChangesetComment', cascade="all, delete, delete-orphan")
1326 pull_requests_source = relationship(
1327 'PullRequest',
1328 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1329 cascade="all, delete, delete-orphan")
1330 pull_requests_target = relationship(
1331 'PullRequest',
1332 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1333 cascade="all, delete, delete-orphan")
1334 ui = relationship('RepoRhodeCodeUi', cascade="all")
1335 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1336 integrations = relationship('Integration',
1337 cascade="all, delete, delete-orphan")
1338
1339 def __unicode__(self):
1340 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1341 safe_unicode(self.repo_name))
1342
1343 @hybrid_property
1344 def landing_rev(self):
1345 # always should return [rev_type, rev]
1346 if self._landing_revision:
1347 _rev_info = self._landing_revision.split(':')
1348 if len(_rev_info) < 2:
1349 _rev_info.insert(0, 'rev')
1350 return [_rev_info[0], _rev_info[1]]
1351 return [None, None]
1352
1353 @landing_rev.setter
1354 def landing_rev(self, val):
1355 if ':' not in val:
1356 raise ValueError('value must be delimited with `:` and consist '
1357 'of <rev_type>:<rev>, got %s instead' % val)
1358 self._landing_revision = val
1359
1360 @hybrid_property
1361 def locked(self):
1362 if self._locked:
1363 user_id, timelocked, reason = self._locked.split(':')
1364 lock_values = int(user_id), timelocked, reason
1365 else:
1366 lock_values = [None, None, None]
1367 return lock_values
1368
1369 @locked.setter
1370 def locked(self, val):
1371 if val and isinstance(val, (list, tuple)):
1372 self._locked = ':'.join(map(str, val))
1373 else:
1374 self._locked = None
1375
1376 @hybrid_property
1377 def changeset_cache(self):
1378 from rhodecode.lib.vcs.backends.base import EmptyCommit
1379 dummy = EmptyCommit().__json__()
1380 if not self._changeset_cache:
1381 return dummy
1382 try:
1383 return json.loads(self._changeset_cache)
1384 except TypeError:
1385 return dummy
1386 except Exception:
1387 log.error(traceback.format_exc())
1388 return dummy
1389
1390 @changeset_cache.setter
1391 def changeset_cache(self, val):
1392 try:
1393 self._changeset_cache = json.dumps(val)
1394 except Exception:
1395 log.error(traceback.format_exc())
1396
1397 @hybrid_property
1398 def repo_name(self):
1399 return self._repo_name
1400
1401 @repo_name.setter
1402 def repo_name(self, value):
1403 self._repo_name = value
1404 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1405
1406 @classmethod
1407 def normalize_repo_name(cls, repo_name):
1408 """
1409 Normalizes os specific repo_name to the format internally stored inside
1410 database using URL_SEP
1411
1412 :param cls:
1413 :param repo_name:
1414 """
1415 return cls.NAME_SEP.join(repo_name.split(os.sep))
1416
1417 @classmethod
1418 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1419 session = Session()
1420 q = session.query(cls).filter(cls.repo_name == repo_name)
1421
1422 if cache:
1423 if identity_cache:
1424 val = cls.identity_cache(session, 'repo_name', repo_name)
1425 if val:
1426 return val
1427 else:
1428 q = q.options(
1429 FromCache("sql_cache_short",
1430 "get_repo_by_name_%s" % _hash_key(repo_name)))
1431
1432 return q.scalar()
1433
1434 @classmethod
1435 def get_by_full_path(cls, repo_full_path):
1436 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1437 repo_name = cls.normalize_repo_name(repo_name)
1438 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1439
1440 @classmethod
1441 def get_repo_forks(cls, repo_id):
1442 return cls.query().filter(Repository.fork_id == repo_id)
1443
1444 @classmethod
1445 def base_path(cls):
1446 """
1447 Returns base path when all repos are stored
1448
1449 :param cls:
1450 """
1451 q = Session().query(RhodeCodeUi)\
1452 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1453 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1454 return q.one().ui_value
1455
1456 @classmethod
1457 def is_valid(cls, repo_name):
1458 """
1459 returns True if given repo name is a valid filesystem repository
1460
1461 :param cls:
1462 :param repo_name:
1463 """
1464 from rhodecode.lib.utils import is_valid_repo
1465
1466 return is_valid_repo(repo_name, cls.base_path())
1467
1468 @classmethod
1469 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1470 case_insensitive=True):
1471 q = Repository.query()
1472
1473 if not isinstance(user_id, Optional):
1474 q = q.filter(Repository.user_id == user_id)
1475
1476 if not isinstance(group_id, Optional):
1477 q = q.filter(Repository.group_id == group_id)
1478
1479 if case_insensitive:
1480 q = q.order_by(func.lower(Repository.repo_name))
1481 else:
1482 q = q.order_by(Repository.repo_name)
1483 return q.all()
1484
1485 @property
1486 def forks(self):
1487 """
1488 Return forks of this repo
1489 """
1490 return Repository.get_repo_forks(self.repo_id)
1491
1492 @property
1493 def parent(self):
1494 """
1495 Returns fork parent
1496 """
1497 return self.fork
1498
1499 @property
1500 def just_name(self):
1501 return self.repo_name.split(self.NAME_SEP)[-1]
1502
1503 @property
1504 def groups_with_parents(self):
1505 groups = []
1506 if self.group is None:
1507 return groups
1508
1509 cur_gr = self.group
1510 groups.insert(0, cur_gr)
1511 while 1:
1512 gr = getattr(cur_gr, 'parent_group', None)
1513 cur_gr = cur_gr.parent_group
1514 if gr is None:
1515 break
1516 groups.insert(0, gr)
1517
1518 return groups
1519
1520 @property
1521 def groups_and_repo(self):
1522 return self.groups_with_parents, self
1523
1524 @LazyProperty
1525 def repo_path(self):
1526 """
1527 Returns base full path for that repository means where it actually
1528 exists on a filesystem
1529 """
1530 q = Session().query(RhodeCodeUi).filter(
1531 RhodeCodeUi.ui_key == self.NAME_SEP)
1532 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1533 return q.one().ui_value
1534
1535 @property
1536 def repo_full_path(self):
1537 p = [self.repo_path]
1538 # we need to split the name by / since this is how we store the
1539 # names in the database, but that eventually needs to be converted
1540 # into a valid system path
1541 p += self.repo_name.split(self.NAME_SEP)
1542 return os.path.join(*map(safe_unicode, p))
1543
1544 @property
1545 def cache_keys(self):
1546 """
1547 Returns associated cache keys for that repo
1548 """
1549 return CacheKey.query()\
1550 .filter(CacheKey.cache_args == self.repo_name)\
1551 .order_by(CacheKey.cache_key)\
1552 .all()
1553
1554 def get_new_name(self, repo_name):
1555 """
1556 returns new full repository name based on assigned group and new new
1557
1558 :param group_name:
1559 """
1560 path_prefix = self.group.full_path_splitted if self.group else []
1561 return self.NAME_SEP.join(path_prefix + [repo_name])
1562
1563 @property
1564 def _config(self):
1565 """
1566 Returns db based config object.
1567 """
1568 from rhodecode.lib.utils import make_db_config
1569 return make_db_config(clear_session=False, repo=self)
1570
1571 def permissions(self, with_admins=True, with_owner=True):
1572 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1573 q = q.options(joinedload(UserRepoToPerm.repository),
1574 joinedload(UserRepoToPerm.user),
1575 joinedload(UserRepoToPerm.permission),)
1576
1577 # get owners and admins and permissions. We do a trick of re-writing
1578 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1579 # has a global reference and changing one object propagates to all
1580 # others. This means if admin is also an owner admin_row that change
1581 # would propagate to both objects
1582 perm_rows = []
1583 for _usr in q.all():
1584 usr = AttributeDict(_usr.user.get_dict())
1585 usr.permission = _usr.permission.permission_name
1586 perm_rows.append(usr)
1587
1588 # filter the perm rows by 'default' first and then sort them by
1589 # admin,write,read,none permissions sorted again alphabetically in
1590 # each group
1591 perm_rows = sorted(perm_rows, key=display_sort)
1592
1593 _admin_perm = 'repository.admin'
1594 owner_row = []
1595 if with_owner:
1596 usr = AttributeDict(self.user.get_dict())
1597 usr.owner_row = True
1598 usr.permission = _admin_perm
1599 owner_row.append(usr)
1600
1601 super_admin_rows = []
1602 if with_admins:
1603 for usr in User.get_all_super_admins():
1604 # if this admin is also owner, don't double the record
1605 if usr.user_id == owner_row[0].user_id:
1606 owner_row[0].admin_row = True
1607 else:
1608 usr = AttributeDict(usr.get_dict())
1609 usr.admin_row = True
1610 usr.permission = _admin_perm
1611 super_admin_rows.append(usr)
1612
1613 return super_admin_rows + owner_row + perm_rows
1614
1615 def permission_user_groups(self):
1616 q = UserGroupRepoToPerm.query().filter(
1617 UserGroupRepoToPerm.repository == self)
1618 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1619 joinedload(UserGroupRepoToPerm.users_group),
1620 joinedload(UserGroupRepoToPerm.permission),)
1621
1622 perm_rows = []
1623 for _user_group in q.all():
1624 usr = AttributeDict(_user_group.users_group.get_dict())
1625 usr.permission = _user_group.permission.permission_name
1626 perm_rows.append(usr)
1627
1628 return perm_rows
1629
1630 def get_api_data(self, include_secrets=False):
1631 """
1632 Common function for generating repo api data
1633
1634 :param include_secrets: See :meth:`User.get_api_data`.
1635
1636 """
1637 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1638 # move this methods on models level.
1639 from rhodecode.model.settings import SettingsModel
1640
1641 repo = self
1642 _user_id, _time, _reason = self.locked
1643
1644 data = {
1645 'repo_id': repo.repo_id,
1646 'repo_name': repo.repo_name,
1647 'repo_type': repo.repo_type,
1648 'clone_uri': repo.clone_uri or '',
1649 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1650 'private': repo.private,
1651 'created_on': repo.created_on,
1652 'description': repo.description,
1653 'landing_rev': repo.landing_rev,
1654 'owner': repo.user.username,
1655 'fork_of': repo.fork.repo_name if repo.fork else None,
1656 'enable_statistics': repo.enable_statistics,
1657 'enable_locking': repo.enable_locking,
1658 'enable_downloads': repo.enable_downloads,
1659 'last_changeset': repo.changeset_cache,
1660 'locked_by': User.get(_user_id).get_api_data(
1661 include_secrets=include_secrets) if _user_id else None,
1662 'locked_date': time_to_datetime(_time) if _time else None,
1663 'lock_reason': _reason if _reason else None,
1664 }
1665
1666 # TODO: mikhail: should be per-repo settings here
1667 rc_config = SettingsModel().get_all_settings()
1668 repository_fields = str2bool(
1669 rc_config.get('rhodecode_repository_fields'))
1670 if repository_fields:
1671 for f in self.extra_fields:
1672 data[f.field_key_prefixed] = f.field_value
1673
1674 return data
1675
1676 @classmethod
1677 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1678 if not lock_time:
1679 lock_time = time.time()
1680 if not lock_reason:
1681 lock_reason = cls.LOCK_AUTOMATIC
1682 repo.locked = [user_id, lock_time, lock_reason]
1683 Session().add(repo)
1684 Session().commit()
1685
1686 @classmethod
1687 def unlock(cls, repo):
1688 repo.locked = None
1689 Session().add(repo)
1690 Session().commit()
1691
1692 @classmethod
1693 def getlock(cls, repo):
1694 return repo.locked
1695
1696 def is_user_lock(self, user_id):
1697 if self.lock[0]:
1698 lock_user_id = safe_int(self.lock[0])
1699 user_id = safe_int(user_id)
1700 # both are ints, and they are equal
1701 return all([lock_user_id, user_id]) and lock_user_id == user_id
1702
1703 return False
1704
1705 def get_locking_state(self, action, user_id, only_when_enabled=True):
1706 """
1707 Checks locking on this repository, if locking is enabled and lock is
1708 present returns a tuple of make_lock, locked, locked_by.
1709 make_lock can have 3 states None (do nothing) True, make lock
1710 False release lock, This value is later propagated to hooks, which
1711 do the locking. Think about this as signals passed to hooks what to do.
1712
1713 """
1714 # TODO: johbo: This is part of the business logic and should be moved
1715 # into the RepositoryModel.
1716
1717 if action not in ('push', 'pull'):
1718 raise ValueError("Invalid action value: %s" % repr(action))
1719
1720 # defines if locked error should be thrown to user
1721 currently_locked = False
1722 # defines if new lock should be made, tri-state
1723 make_lock = None
1724 repo = self
1725 user = User.get(user_id)
1726
1727 lock_info = repo.locked
1728
1729 if repo and (repo.enable_locking or not only_when_enabled):
1730 if action == 'push':
1731 # check if it's already locked !, if it is compare users
1732 locked_by_user_id = lock_info[0]
1733 if user.user_id == locked_by_user_id:
1734 log.debug(
1735 'Got `push` action from user %s, now unlocking', user)
1736 # unlock if we have push from user who locked
1737 make_lock = False
1738 else:
1739 # we're not the same user who locked, ban with
1740 # code defined in settings (default is 423 HTTP Locked) !
1741 log.debug('Repo %s is currently locked by %s', repo, user)
1742 currently_locked = True
1743 elif action == 'pull':
1744 # [0] user [1] date
1745 if lock_info[0] and lock_info[1]:
1746 log.debug('Repo %s is currently locked by %s', repo, user)
1747 currently_locked = True
1748 else:
1749 log.debug('Setting lock on repo %s by %s', repo, user)
1750 make_lock = True
1751
1752 else:
1753 log.debug('Repository %s do not have locking enabled', repo)
1754
1755 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1756 make_lock, currently_locked, lock_info)
1757
1758 from rhodecode.lib.auth import HasRepoPermissionAny
1759 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1760 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1761 # if we don't have at least write permission we cannot make a lock
1762 log.debug('lock state reset back to FALSE due to lack '
1763 'of at least read permission')
1764 make_lock = False
1765
1766 return make_lock, currently_locked, lock_info
1767
1768 @property
1769 def last_db_change(self):
1770 return self.updated_on
1771
1772 @property
1773 def clone_uri_hidden(self):
1774 clone_uri = self.clone_uri
1775 if clone_uri:
1776 import urlobject
1777 url_obj = urlobject.URLObject(clone_uri)
1778 if url_obj.password:
1779 clone_uri = url_obj.with_password('*****')
1780 return clone_uri
1781
1782 def clone_url(self, **override):
1783 qualified_home_url = url('home', qualified=True)
1784
1785 uri_tmpl = None
1786 if 'with_id' in override:
1787 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1788 del override['with_id']
1789
1790 if 'uri_tmpl' in override:
1791 uri_tmpl = override['uri_tmpl']
1792 del override['uri_tmpl']
1793
1794 # we didn't override our tmpl from **overrides
1795 if not uri_tmpl:
1796 uri_tmpl = self.DEFAULT_CLONE_URI
1797 try:
1798 from pylons import tmpl_context as c
1799 uri_tmpl = c.clone_uri_tmpl
1800 except Exception:
1801 # in any case if we call this outside of request context,
1802 # ie, not having tmpl_context set up
1803 pass
1804
1805 return get_clone_url(uri_tmpl=uri_tmpl,
1806 qualifed_home_url=qualified_home_url,
1807 repo_name=self.repo_name,
1808 repo_id=self.repo_id, **override)
1809
1810 def set_state(self, state):
1811 self.repo_state = state
1812 Session().add(self)
1813 #==========================================================================
1814 # SCM PROPERTIES
1815 #==========================================================================
1816
1817 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1818 return get_commit_safe(
1819 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1820
1821 def get_changeset(self, rev=None, pre_load=None):
1822 warnings.warn("Use get_commit", DeprecationWarning)
1823 commit_id = None
1824 commit_idx = None
1825 if isinstance(rev, basestring):
1826 commit_id = rev
1827 else:
1828 commit_idx = rev
1829 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1830 pre_load=pre_load)
1831
1832 def get_landing_commit(self):
1833 """
1834 Returns landing commit, or if that doesn't exist returns the tip
1835 """
1836 _rev_type, _rev = self.landing_rev
1837 commit = self.get_commit(_rev)
1838 if isinstance(commit, EmptyCommit):
1839 return self.get_commit()
1840 return commit
1841
1842 def update_commit_cache(self, cs_cache=None, config=None):
1843 """
1844 Update cache of last changeset for repository, keys should be::
1845
1846 short_id
1847 raw_id
1848 revision
1849 parents
1850 message
1851 date
1852 author
1853
1854 :param cs_cache:
1855 """
1856 from rhodecode.lib.vcs.backends.base import BaseChangeset
1857 if cs_cache is None:
1858 # use no-cache version here
1859 scm_repo = self.scm_instance(cache=False, config=config)
1860 if scm_repo:
1861 cs_cache = scm_repo.get_commit(
1862 pre_load=["author", "date", "message", "parents"])
1863 else:
1864 cs_cache = EmptyCommit()
1865
1866 if isinstance(cs_cache, BaseChangeset):
1867 cs_cache = cs_cache.__json__()
1868
1869 def is_outdated(new_cs_cache):
1870 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1871 new_cs_cache['revision'] != self.changeset_cache['revision']):
1872 return True
1873 return False
1874
1875 # check if we have maybe already latest cached revision
1876 if is_outdated(cs_cache) or not self.changeset_cache:
1877 _default = datetime.datetime.fromtimestamp(0)
1878 last_change = cs_cache.get('date') or _default
1879 log.debug('updated repo %s with new cs cache %s',
1880 self.repo_name, cs_cache)
1881 self.updated_on = last_change
1882 self.changeset_cache = cs_cache
1883 Session().add(self)
1884 Session().commit()
1885 else:
1886 log.debug('Skipping update_commit_cache for repo:`%s` '
1887 'commit already with latest changes', self.repo_name)
1888
1889 @property
1890 def tip(self):
1891 return self.get_commit('tip')
1892
1893 @property
1894 def author(self):
1895 return self.tip.author
1896
1897 @property
1898 def last_change(self):
1899 return self.scm_instance().last_change
1900
1901 def get_comments(self, revisions=None):
1902 """
1903 Returns comments for this repository grouped by revisions
1904
1905 :param revisions: filter query by revisions only
1906 """
1907 cmts = ChangesetComment.query()\
1908 .filter(ChangesetComment.repo == self)
1909 if revisions:
1910 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1911 grouped = collections.defaultdict(list)
1912 for cmt in cmts.all():
1913 grouped[cmt.revision].append(cmt)
1914 return grouped
1915
1916 def statuses(self, revisions=None):
1917 """
1918 Returns statuses for this repository
1919
1920 :param revisions: list of revisions to get statuses for
1921 """
1922 statuses = ChangesetStatus.query()\
1923 .filter(ChangesetStatus.repo == self)\
1924 .filter(ChangesetStatus.version == 0)
1925
1926 if revisions:
1927 # Try doing the filtering in chunks to avoid hitting limits
1928 size = 500
1929 status_results = []
1930 for chunk in xrange(0, len(revisions), size):
1931 status_results += statuses.filter(
1932 ChangesetStatus.revision.in_(
1933 revisions[chunk: chunk+size])
1934 ).all()
1935 else:
1936 status_results = statuses.all()
1937
1938 grouped = {}
1939
1940 # maybe we have open new pullrequest without a status?
1941 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1942 status_lbl = ChangesetStatus.get_status_lbl(stat)
1943 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1944 for rev in pr.revisions:
1945 pr_id = pr.pull_request_id
1946 pr_repo = pr.target_repo.repo_name
1947 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1948
1949 for stat in status_results:
1950 pr_id = pr_repo = None
1951 if stat.pull_request:
1952 pr_id = stat.pull_request.pull_request_id
1953 pr_repo = stat.pull_request.target_repo.repo_name
1954 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1955 pr_id, pr_repo]
1956 return grouped
1957
1958 # ==========================================================================
1959 # SCM CACHE INSTANCE
1960 # ==========================================================================
1961
1962 def scm_instance(self, **kwargs):
1963 import rhodecode
1964
1965 # Passing a config will not hit the cache currently only used
1966 # for repo2dbmapper
1967 config = kwargs.pop('config', None)
1968 cache = kwargs.pop('cache', None)
1969 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1970 # if cache is NOT defined use default global, else we have a full
1971 # control over cache behaviour
1972 if cache is None and full_cache and not config:
1973 return self._get_instance_cached()
1974 return self._get_instance(cache=bool(cache), config=config)
1975
1976 def _get_instance_cached(self):
1977 @cache_region('long_term')
1978 def _get_repo(cache_key):
1979 return self._get_instance()
1980
1981 invalidator_context = CacheKey.repo_context_cache(
1982 _get_repo, self.repo_name, None, thread_scoped=True)
1983
1984 with invalidator_context as context:
1985 context.invalidate()
1986 repo = context.compute()
1987
1988 return repo
1989
1990 def _get_instance(self, cache=True, config=None):
1991 config = config or self._config
1992 custom_wire = {
1993 'cache': cache # controls the vcs.remote cache
1994 }
1995
1996 repo = get_vcs_instance(
1997 repo_path=safe_str(self.repo_full_path),
1998 config=config,
1999 with_wire=custom_wire,
2000 create=False)
2001
2002 return repo
2003
2004 def __json__(self):
2005 return {'landing_rev': self.landing_rev}
2006
2007 def get_dict(self):
2008
2009 # Since we transformed `repo_name` to a hybrid property, we need to
2010 # keep compatibility with the code which uses `repo_name` field.
2011
2012 result = super(Repository, self).get_dict()
2013 result['repo_name'] = result.pop('_repo_name', None)
2014 return result
2015
2016
2017 class RepoGroup(Base, BaseModel):
2018 __tablename__ = 'groups'
2019 __table_args__ = (
2020 UniqueConstraint('group_name', 'group_parent_id'),
2021 CheckConstraint('group_id != group_parent_id'),
2022 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2023 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2024 )
2025 __mapper_args__ = {'order_by': 'group_name'}
2026
2027 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2028
2029 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2030 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2031 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2032 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2033 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2036
2037 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2038 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2039 parent_group = relationship('RepoGroup', remote_side=group_id)
2040 user = relationship('User')
2041 integrations = relationship('Integration',
2042 cascade="all, delete, delete-orphan")
2043
2044 def __init__(self, group_name='', parent_group=None):
2045 self.group_name = group_name
2046 self.parent_group = parent_group
2047
2048 def __unicode__(self):
2049 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2050 self.group_name)
2051
2052 @classmethod
2053 def _generate_choice(cls, repo_group):
2054 from webhelpers.html import literal as _literal
2055 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2056 return repo_group.group_id, _name(repo_group.full_path_splitted)
2057
2058 @classmethod
2059 def groups_choices(cls, groups=None, show_empty_group=True):
2060 if not groups:
2061 groups = cls.query().all()
2062
2063 repo_groups = []
2064 if show_empty_group:
2065 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2066
2067 repo_groups.extend([cls._generate_choice(x) for x in groups])
2068
2069 repo_groups = sorted(
2070 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2071 return repo_groups
2072
2073 @classmethod
2074 def url_sep(cls):
2075 return URL_SEP
2076
2077 @classmethod
2078 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2079 if case_insensitive:
2080 gr = cls.query().filter(func.lower(cls.group_name)
2081 == func.lower(group_name))
2082 else:
2083 gr = cls.query().filter(cls.group_name == group_name)
2084 if cache:
2085 gr = gr.options(FromCache(
2086 "sql_cache_short",
2087 "get_group_%s" % _hash_key(group_name)))
2088 return gr.scalar()
2089
2090 @classmethod
2091 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2092 case_insensitive=True):
2093 q = RepoGroup.query()
2094
2095 if not isinstance(user_id, Optional):
2096 q = q.filter(RepoGroup.user_id == user_id)
2097
2098 if not isinstance(group_id, Optional):
2099 q = q.filter(RepoGroup.group_parent_id == group_id)
2100
2101 if case_insensitive:
2102 q = q.order_by(func.lower(RepoGroup.group_name))
2103 else:
2104 q = q.order_by(RepoGroup.group_name)
2105 return q.all()
2106
2107 @property
2108 def parents(self):
2109 parents_recursion_limit = 10
2110 groups = []
2111 if self.parent_group is None:
2112 return groups
2113 cur_gr = self.parent_group
2114 groups.insert(0, cur_gr)
2115 cnt = 0
2116 while 1:
2117 cnt += 1
2118 gr = getattr(cur_gr, 'parent_group', None)
2119 cur_gr = cur_gr.parent_group
2120 if gr is None:
2121 break
2122 if cnt == parents_recursion_limit:
2123 # this will prevent accidental infinit loops
2124 log.error(('more than %s parents found for group %s, stopping '
2125 'recursive parent fetching' % (parents_recursion_limit, self)))
2126 break
2127
2128 groups.insert(0, gr)
2129 return groups
2130
2131 @property
2132 def children(self):
2133 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2134
2135 @property
2136 def name(self):
2137 return self.group_name.split(RepoGroup.url_sep())[-1]
2138
2139 @property
2140 def full_path(self):
2141 return self.group_name
2142
2143 @property
2144 def full_path_splitted(self):
2145 return self.group_name.split(RepoGroup.url_sep())
2146
2147 @property
2148 def repositories(self):
2149 return Repository.query()\
2150 .filter(Repository.group == self)\
2151 .order_by(Repository.repo_name)
2152
2153 @property
2154 def repositories_recursive_count(self):
2155 cnt = self.repositories.count()
2156
2157 def children_count(group):
2158 cnt = 0
2159 for child in group.children:
2160 cnt += child.repositories.count()
2161 cnt += children_count(child)
2162 return cnt
2163
2164 return cnt + children_count(self)
2165
2166 def _recursive_objects(self, include_repos=True):
2167 all_ = []
2168
2169 def _get_members(root_gr):
2170 if include_repos:
2171 for r in root_gr.repositories:
2172 all_.append(r)
2173 childs = root_gr.children.all()
2174 if childs:
2175 for gr in childs:
2176 all_.append(gr)
2177 _get_members(gr)
2178
2179 _get_members(self)
2180 return [self] + all_
2181
2182 def recursive_groups_and_repos(self):
2183 """
2184 Recursive return all groups, with repositories in those groups
2185 """
2186 return self._recursive_objects()
2187
2188 def recursive_groups(self):
2189 """
2190 Returns all children groups for this group including children of children
2191 """
2192 return self._recursive_objects(include_repos=False)
2193
2194 def get_new_name(self, group_name):
2195 """
2196 returns new full group name based on parent and new name
2197
2198 :param group_name:
2199 """
2200 path_prefix = (self.parent_group.full_path_splitted if
2201 self.parent_group else [])
2202 return RepoGroup.url_sep().join(path_prefix + [group_name])
2203
2204 def permissions(self, with_admins=True, with_owner=True):
2205 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2206 q = q.options(joinedload(UserRepoGroupToPerm.group),
2207 joinedload(UserRepoGroupToPerm.user),
2208 joinedload(UserRepoGroupToPerm.permission),)
2209
2210 # get owners and admins and permissions. We do a trick of re-writing
2211 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2212 # has a global reference and changing one object propagates to all
2213 # others. This means if admin is also an owner admin_row that change
2214 # would propagate to both objects
2215 perm_rows = []
2216 for _usr in q.all():
2217 usr = AttributeDict(_usr.user.get_dict())
2218 usr.permission = _usr.permission.permission_name
2219 perm_rows.append(usr)
2220
2221 # filter the perm rows by 'default' first and then sort them by
2222 # admin,write,read,none permissions sorted again alphabetically in
2223 # each group
2224 perm_rows = sorted(perm_rows, key=display_sort)
2225
2226 _admin_perm = 'group.admin'
2227 owner_row = []
2228 if with_owner:
2229 usr = AttributeDict(self.user.get_dict())
2230 usr.owner_row = True
2231 usr.permission = _admin_perm
2232 owner_row.append(usr)
2233
2234 super_admin_rows = []
2235 if with_admins:
2236 for usr in User.get_all_super_admins():
2237 # if this admin is also owner, don't double the record
2238 if usr.user_id == owner_row[0].user_id:
2239 owner_row[0].admin_row = True
2240 else:
2241 usr = AttributeDict(usr.get_dict())
2242 usr.admin_row = True
2243 usr.permission = _admin_perm
2244 super_admin_rows.append(usr)
2245
2246 return super_admin_rows + owner_row + perm_rows
2247
2248 def permission_user_groups(self):
2249 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2250 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2251 joinedload(UserGroupRepoGroupToPerm.users_group),
2252 joinedload(UserGroupRepoGroupToPerm.permission),)
2253
2254 perm_rows = []
2255 for _user_group in q.all():
2256 usr = AttributeDict(_user_group.users_group.get_dict())
2257 usr.permission = _user_group.permission.permission_name
2258 perm_rows.append(usr)
2259
2260 return perm_rows
2261
2262 def get_api_data(self):
2263 """
2264 Common function for generating api data
2265
2266 """
2267 group = self
2268 data = {
2269 'group_id': group.group_id,
2270 'group_name': group.group_name,
2271 'group_description': group.group_description,
2272 'parent_group': group.parent_group.group_name if group.parent_group else None,
2273 'repositories': [x.repo_name for x in group.repositories],
2274 'owner': group.user.username,
2275 }
2276 return data
2277
2278
2279 class Permission(Base, BaseModel):
2280 __tablename__ = 'permissions'
2281 __table_args__ = (
2282 Index('p_perm_name_idx', 'permission_name'),
2283 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2284 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2285 )
2286 PERMS = [
2287 ('hg.admin', _('RhodeCode Super Administrator')),
2288
2289 ('repository.none', _('Repository no access')),
2290 ('repository.read', _('Repository read access')),
2291 ('repository.write', _('Repository write access')),
2292 ('repository.admin', _('Repository admin access')),
2293
2294 ('group.none', _('Repository group no access')),
2295 ('group.read', _('Repository group read access')),
2296 ('group.write', _('Repository group write access')),
2297 ('group.admin', _('Repository group admin access')),
2298
2299 ('usergroup.none', _('User group no access')),
2300 ('usergroup.read', _('User group read access')),
2301 ('usergroup.write', _('User group write access')),
2302 ('usergroup.admin', _('User group admin access')),
2303
2304 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2305 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2306
2307 ('hg.usergroup.create.false', _('User Group creation disabled')),
2308 ('hg.usergroup.create.true', _('User Group creation enabled')),
2309
2310 ('hg.create.none', _('Repository creation disabled')),
2311 ('hg.create.repository', _('Repository creation enabled')),
2312 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2313 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2314
2315 ('hg.fork.none', _('Repository forking disabled')),
2316 ('hg.fork.repository', _('Repository forking enabled')),
2317
2318 ('hg.register.none', _('Registration disabled')),
2319 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2320 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2321
2322 ('hg.extern_activate.manual', _('Manual activation of external account')),
2323 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2324
2325 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2326 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2327 ]
2328
2329 # definition of system default permissions for DEFAULT user
2330 DEFAULT_USER_PERMISSIONS = [
2331 'repository.read',
2332 'group.read',
2333 'usergroup.read',
2334 'hg.create.repository',
2335 'hg.repogroup.create.false',
2336 'hg.usergroup.create.false',
2337 'hg.create.write_on_repogroup.true',
2338 'hg.fork.repository',
2339 'hg.register.manual_activate',
2340 'hg.extern_activate.auto',
2341 'hg.inherit_default_perms.true',
2342 ]
2343
2344 # defines which permissions are more important higher the more important
2345 # Weight defines which permissions are more important.
2346 # The higher number the more important.
2347 PERM_WEIGHTS = {
2348 'repository.none': 0,
2349 'repository.read': 1,
2350 'repository.write': 3,
2351 'repository.admin': 4,
2352
2353 'group.none': 0,
2354 'group.read': 1,
2355 'group.write': 3,
2356 'group.admin': 4,
2357
2358 'usergroup.none': 0,
2359 'usergroup.read': 1,
2360 'usergroup.write': 3,
2361 'usergroup.admin': 4,
2362
2363 'hg.repogroup.create.false': 0,
2364 'hg.repogroup.create.true': 1,
2365
2366 'hg.usergroup.create.false': 0,
2367 'hg.usergroup.create.true': 1,
2368
2369 'hg.fork.none': 0,
2370 'hg.fork.repository': 1,
2371 'hg.create.none': 0,
2372 'hg.create.repository': 1
2373 }
2374
2375 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2376 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2377 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2378
2379 def __unicode__(self):
2380 return u"<%s('%s:%s')>" % (
2381 self.__class__.__name__, self.permission_id, self.permission_name
2382 )
2383
2384 @classmethod
2385 def get_by_key(cls, key):
2386 return cls.query().filter(cls.permission_name == key).scalar()
2387
2388 @classmethod
2389 def get_default_repo_perms(cls, user_id, repo_id=None):
2390 q = Session().query(UserRepoToPerm, Repository, Permission)\
2391 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2392 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2393 .filter(UserRepoToPerm.user_id == user_id)
2394 if repo_id:
2395 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2396 return q.all()
2397
2398 @classmethod
2399 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2400 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2401 .join(
2402 Permission,
2403 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2404 .join(
2405 Repository,
2406 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2407 .join(
2408 UserGroup,
2409 UserGroupRepoToPerm.users_group_id ==
2410 UserGroup.users_group_id)\
2411 .join(
2412 UserGroupMember,
2413 UserGroupRepoToPerm.users_group_id ==
2414 UserGroupMember.users_group_id)\
2415 .filter(
2416 UserGroupMember.user_id == user_id,
2417 UserGroup.users_group_active == true())
2418 if repo_id:
2419 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2420 return q.all()
2421
2422 @classmethod
2423 def get_default_group_perms(cls, user_id, repo_group_id=None):
2424 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2425 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2426 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2427 .filter(UserRepoGroupToPerm.user_id == user_id)
2428 if repo_group_id:
2429 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2430 return q.all()
2431
2432 @classmethod
2433 def get_default_group_perms_from_user_group(
2434 cls, user_id, repo_group_id=None):
2435 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2436 .join(
2437 Permission,
2438 UserGroupRepoGroupToPerm.permission_id ==
2439 Permission.permission_id)\
2440 .join(
2441 RepoGroup,
2442 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2443 .join(
2444 UserGroup,
2445 UserGroupRepoGroupToPerm.users_group_id ==
2446 UserGroup.users_group_id)\
2447 .join(
2448 UserGroupMember,
2449 UserGroupRepoGroupToPerm.users_group_id ==
2450 UserGroupMember.users_group_id)\
2451 .filter(
2452 UserGroupMember.user_id == user_id,
2453 UserGroup.users_group_active == true())
2454 if repo_group_id:
2455 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2456 return q.all()
2457
2458 @classmethod
2459 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2460 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2461 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2462 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2463 .filter(UserUserGroupToPerm.user_id == user_id)
2464 if user_group_id:
2465 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2466 return q.all()
2467
2468 @classmethod
2469 def get_default_user_group_perms_from_user_group(
2470 cls, user_id, user_group_id=None):
2471 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2472 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2473 .join(
2474 Permission,
2475 UserGroupUserGroupToPerm.permission_id ==
2476 Permission.permission_id)\
2477 .join(
2478 TargetUserGroup,
2479 UserGroupUserGroupToPerm.target_user_group_id ==
2480 TargetUserGroup.users_group_id)\
2481 .join(
2482 UserGroup,
2483 UserGroupUserGroupToPerm.user_group_id ==
2484 UserGroup.users_group_id)\
2485 .join(
2486 UserGroupMember,
2487 UserGroupUserGroupToPerm.user_group_id ==
2488 UserGroupMember.users_group_id)\
2489 .filter(
2490 UserGroupMember.user_id == user_id,
2491 UserGroup.users_group_active == true())
2492 if user_group_id:
2493 q = q.filter(
2494 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2495
2496 return q.all()
2497
2498
2499 class UserRepoToPerm(Base, BaseModel):
2500 __tablename__ = 'repo_to_perm'
2501 __table_args__ = (
2502 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2503 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2504 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2505 )
2506 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2507 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2508 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2509 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2510
2511 user = relationship('User')
2512 repository = relationship('Repository')
2513 permission = relationship('Permission')
2514
2515 @classmethod
2516 def create(cls, user, repository, permission):
2517 n = cls()
2518 n.user = user
2519 n.repository = repository
2520 n.permission = permission
2521 Session().add(n)
2522 return n
2523
2524 def __unicode__(self):
2525 return u'<%s => %s >' % (self.user, self.repository)
2526
2527
2528 class UserUserGroupToPerm(Base, BaseModel):
2529 __tablename__ = 'user_user_group_to_perm'
2530 __table_args__ = (
2531 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2532 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2533 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2534 )
2535 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2536 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2537 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2538 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2539
2540 user = relationship('User')
2541 user_group = relationship('UserGroup')
2542 permission = relationship('Permission')
2543
2544 @classmethod
2545 def create(cls, user, user_group, permission):
2546 n = cls()
2547 n.user = user
2548 n.user_group = user_group
2549 n.permission = permission
2550 Session().add(n)
2551 return n
2552
2553 def __unicode__(self):
2554 return u'<%s => %s >' % (self.user, self.user_group)
2555
2556
2557 class UserToPerm(Base, BaseModel):
2558 __tablename__ = 'user_to_perm'
2559 __table_args__ = (
2560 UniqueConstraint('user_id', 'permission_id'),
2561 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2562 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2563 )
2564 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2566 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2567
2568 user = relationship('User')
2569 permission = relationship('Permission', lazy='joined')
2570
2571 def __unicode__(self):
2572 return u'<%s => %s >' % (self.user, self.permission)
2573
2574
2575 class UserGroupRepoToPerm(Base, BaseModel):
2576 __tablename__ = 'users_group_repo_to_perm'
2577 __table_args__ = (
2578 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2579 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2580 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2581 )
2582 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2583 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2584 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2585 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2586
2587 users_group = relationship('UserGroup')
2588 permission = relationship('Permission')
2589 repository = relationship('Repository')
2590
2591 @classmethod
2592 def create(cls, users_group, repository, permission):
2593 n = cls()
2594 n.users_group = users_group
2595 n.repository = repository
2596 n.permission = permission
2597 Session().add(n)
2598 return n
2599
2600 def __unicode__(self):
2601 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2602
2603
2604 class UserGroupUserGroupToPerm(Base, BaseModel):
2605 __tablename__ = 'user_group_user_group_to_perm'
2606 __table_args__ = (
2607 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2608 CheckConstraint('target_user_group_id != user_group_id'),
2609 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2610 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2611 )
2612 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)
2613 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2614 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2615 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2616
2617 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2618 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2619 permission = relationship('Permission')
2620
2621 @classmethod
2622 def create(cls, target_user_group, user_group, permission):
2623 n = cls()
2624 n.target_user_group = target_user_group
2625 n.user_group = user_group
2626 n.permission = permission
2627 Session().add(n)
2628 return n
2629
2630 def __unicode__(self):
2631 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2632
2633
2634 class UserGroupToPerm(Base, BaseModel):
2635 __tablename__ = 'users_group_to_perm'
2636 __table_args__ = (
2637 UniqueConstraint('users_group_id', 'permission_id',),
2638 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2639 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2640 )
2641 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2642 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2643 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2644
2645 users_group = relationship('UserGroup')
2646 permission = relationship('Permission')
2647
2648
2649 class UserRepoGroupToPerm(Base, BaseModel):
2650 __tablename__ = 'user_repo_group_to_perm'
2651 __table_args__ = (
2652 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2653 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2654 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2655 )
2656
2657 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2658 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2659 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2660 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2661
2662 user = relationship('User')
2663 group = relationship('RepoGroup')
2664 permission = relationship('Permission')
2665
2666 @classmethod
2667 def create(cls, user, repository_group, permission):
2668 n = cls()
2669 n.user = user
2670 n.group = repository_group
2671 n.permission = permission
2672 Session().add(n)
2673 return n
2674
2675
2676 class UserGroupRepoGroupToPerm(Base, BaseModel):
2677 __tablename__ = 'users_group_repo_group_to_perm'
2678 __table_args__ = (
2679 UniqueConstraint('users_group_id', 'group_id'),
2680 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2681 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2682 )
2683
2684 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)
2685 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2686 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2687 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2688
2689 users_group = relationship('UserGroup')
2690 permission = relationship('Permission')
2691 group = relationship('RepoGroup')
2692
2693 @classmethod
2694 def create(cls, user_group, repository_group, permission):
2695 n = cls()
2696 n.users_group = user_group
2697 n.group = repository_group
2698 n.permission = permission
2699 Session().add(n)
2700 return n
2701
2702 def __unicode__(self):
2703 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2704
2705
2706 class Statistics(Base, BaseModel):
2707 __tablename__ = 'statistics'
2708 __table_args__ = (
2709 UniqueConstraint('repository_id'),
2710 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2711 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2712 )
2713 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2714 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2715 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2716 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2717 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2718 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2719
2720 repository = relationship('Repository', single_parent=True)
2721
2722
2723 class UserFollowing(Base, BaseModel):
2724 __tablename__ = 'user_followings'
2725 __table_args__ = (
2726 UniqueConstraint('user_id', 'follows_repository_id'),
2727 UniqueConstraint('user_id', 'follows_user_id'),
2728 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2729 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2730 )
2731
2732 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2733 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2734 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2735 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2736 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2737
2738 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2739
2740 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2741 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2742
2743 @classmethod
2744 def get_repo_followers(cls, repo_id):
2745 return cls.query().filter(cls.follows_repo_id == repo_id)
2746
2747
2748 class CacheKey(Base, BaseModel):
2749 __tablename__ = 'cache_invalidation'
2750 __table_args__ = (
2751 UniqueConstraint('cache_key'),
2752 Index('key_idx', 'cache_key'),
2753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2754 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2755 )
2756 CACHE_TYPE_ATOM = 'ATOM'
2757 CACHE_TYPE_RSS = 'RSS'
2758 CACHE_TYPE_README = 'README'
2759
2760 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2761 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2762 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2763 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2764
2765 def __init__(self, cache_key, cache_args=''):
2766 self.cache_key = cache_key
2767 self.cache_args = cache_args
2768 self.cache_active = False
2769
2770 def __unicode__(self):
2771 return u"<%s('%s:%s[%s]')>" % (
2772 self.__class__.__name__,
2773 self.cache_id, self.cache_key, self.cache_active)
2774
2775 def _cache_key_partition(self):
2776 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2777 return prefix, repo_name, suffix
2778
2779 def get_prefix(self):
2780 """
2781 Try to extract prefix from existing cache key. The key could consist
2782 of prefix, repo_name, suffix
2783 """
2784 # this returns prefix, repo_name, suffix
2785 return self._cache_key_partition()[0]
2786
2787 def get_suffix(self):
2788 """
2789 get suffix that might have been used in _get_cache_key to
2790 generate self.cache_key. Only used for informational purposes
2791 in repo_edit.html.
2792 """
2793 # prefix, repo_name, suffix
2794 return self._cache_key_partition()[2]
2795
2796 @classmethod
2797 def delete_all_cache(cls):
2798 """
2799 Delete all cache keys from database.
2800 Should only be run when all instances are down and all entries
2801 thus stale.
2802 """
2803 cls.query().delete()
2804 Session().commit()
2805
2806 @classmethod
2807 def get_cache_key(cls, repo_name, cache_type):
2808 """
2809
2810 Generate a cache key for this process of RhodeCode instance.
2811 Prefix most likely will be process id or maybe explicitly set
2812 instance_id from .ini file.
2813 """
2814 import rhodecode
2815 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2816
2817 repo_as_unicode = safe_unicode(repo_name)
2818 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2819 if cache_type else repo_as_unicode
2820
2821 return u'{}{}'.format(prefix, key)
2822
2823 @classmethod
2824 def set_invalidate(cls, repo_name, delete=False):
2825 """
2826 Mark all caches of a repo as invalid in the database.
2827 """
2828
2829 try:
2830 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2831 if delete:
2832 log.debug('cache objects deleted for repo %s',
2833 safe_str(repo_name))
2834 qry.delete()
2835 else:
2836 log.debug('cache objects marked as invalid for repo %s',
2837 safe_str(repo_name))
2838 qry.update({"cache_active": False})
2839
2840 Session().commit()
2841 except Exception:
2842 log.exception(
2843 'Cache key invalidation failed for repository %s',
2844 safe_str(repo_name))
2845 Session().rollback()
2846
2847 @classmethod
2848 def get_active_cache(cls, cache_key):
2849 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2850 if inv_obj:
2851 return inv_obj
2852 return None
2853
2854 @classmethod
2855 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2856 thread_scoped=False):
2857 """
2858 @cache_region('long_term')
2859 def _heavy_calculation(cache_key):
2860 return 'result'
2861
2862 cache_context = CacheKey.repo_context_cache(
2863 _heavy_calculation, repo_name, cache_type)
2864
2865 with cache_context as context:
2866 context.invalidate()
2867 computed = context.compute()
2868
2869 assert computed == 'result'
2870 """
2871 from rhodecode.lib import caches
2872 return caches.InvalidationContext(
2873 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2874
2875
2876 class ChangesetComment(Base, BaseModel):
2877 __tablename__ = 'changeset_comments'
2878 __table_args__ = (
2879 Index('cc_revision_idx', 'revision'),
2880 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2881 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2882 )
2883
2884 COMMENT_OUTDATED = u'comment_outdated'
2885
2886 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2887 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2888 revision = Column('revision', String(40), nullable=True)
2889 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2890 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2891 line_no = Column('line_no', Unicode(10), nullable=True)
2892 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2893 f_path = Column('f_path', Unicode(1000), nullable=True)
2894 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2895 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2896 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2897 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2898 renderer = Column('renderer', Unicode(64), nullable=True)
2899 display_state = Column('display_state', Unicode(128), nullable=True)
2900
2901 author = relationship('User', lazy='joined')
2902 repo = relationship('Repository')
2903 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2904 pull_request = relationship('PullRequest', lazy='joined')
2905 pull_request_version = relationship('PullRequestVersion')
2906
2907 @classmethod
2908 def get_users(cls, revision=None, pull_request_id=None):
2909 """
2910 Returns user associated with this ChangesetComment. ie those
2911 who actually commented
2912
2913 :param cls:
2914 :param revision:
2915 """
2916 q = Session().query(User)\
2917 .join(ChangesetComment.author)
2918 if revision:
2919 q = q.filter(cls.revision == revision)
2920 elif pull_request_id:
2921 q = q.filter(cls.pull_request_id == pull_request_id)
2922 return q.all()
2923
2924 def render(self, mentions=False):
2925 from rhodecode.lib import helpers as h
2926 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2927
2928 def __repr__(self):
2929 if self.comment_id:
2930 return '<DB:ChangesetComment #%s>' % self.comment_id
2931 else:
2932 return '<DB:ChangesetComment at %#x>' % id(self)
2933
2934
2935 class ChangesetStatus(Base, BaseModel):
2936 __tablename__ = 'changeset_statuses'
2937 __table_args__ = (
2938 Index('cs_revision_idx', 'revision'),
2939 Index('cs_version_idx', 'version'),
2940 UniqueConstraint('repo_id', 'revision', 'version'),
2941 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2942 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2943 )
2944 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2945 STATUS_APPROVED = 'approved'
2946 STATUS_REJECTED = 'rejected'
2947 STATUS_UNDER_REVIEW = 'under_review'
2948
2949 STATUSES = [
2950 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2951 (STATUS_APPROVED, _("Approved")),
2952 (STATUS_REJECTED, _("Rejected")),
2953 (STATUS_UNDER_REVIEW, _("Under Review")),
2954 ]
2955
2956 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2957 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2958 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2959 revision = Column('revision', String(40), nullable=False)
2960 status = Column('status', String(128), nullable=False, default=DEFAULT)
2961 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2962 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2963 version = Column('version', Integer(), nullable=False, default=0)
2964 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2965
2966 author = relationship('User', lazy='joined')
2967 repo = relationship('Repository')
2968 comment = relationship('ChangesetComment', lazy='joined')
2969 pull_request = relationship('PullRequest', lazy='joined')
2970
2971 def __unicode__(self):
2972 return u"<%s('%s[%s]:%s')>" % (
2973 self.__class__.__name__,
2974 self.status, self.version, self.author
2975 )
2976
2977 @classmethod
2978 def get_status_lbl(cls, value):
2979 return dict(cls.STATUSES).get(value)
2980
2981 @property
2982 def status_lbl(self):
2983 return ChangesetStatus.get_status_lbl(self.status)
2984
2985
2986 class _PullRequestBase(BaseModel):
2987 """
2988 Common attributes of pull request and version entries.
2989 """
2990
2991 # .status values
2992 STATUS_NEW = u'new'
2993 STATUS_OPEN = u'open'
2994 STATUS_CLOSED = u'closed'
2995
2996 title = Column('title', Unicode(255), nullable=True)
2997 description = Column(
2998 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2999 nullable=True)
3000 # new/open/closed status of pull request (not approve/reject/etc)
3001 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3002 created_on = Column(
3003 'created_on', DateTime(timezone=False), nullable=False,
3004 default=datetime.datetime.now)
3005 updated_on = Column(
3006 'updated_on', DateTime(timezone=False), nullable=False,
3007 default=datetime.datetime.now)
3008
3009 @declared_attr
3010 def user_id(cls):
3011 return Column(
3012 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3013 unique=None)
3014
3015 # 500 revisions max
3016 _revisions = Column(
3017 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3018
3019 @declared_attr
3020 def source_repo_id(cls):
3021 # TODO: dan: rename column to source_repo_id
3022 return Column(
3023 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3024 nullable=False)
3025
3026 source_ref = Column('org_ref', Unicode(255), nullable=False)
3027
3028 @declared_attr
3029 def target_repo_id(cls):
3030 # TODO: dan: rename column to target_repo_id
3031 return Column(
3032 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3033 nullable=False)
3034
3035 target_ref = Column('other_ref', Unicode(255), nullable=False)
3036
3037 # TODO: dan: rename column to last_merge_source_rev
3038 _last_merge_source_rev = Column(
3039 'last_merge_org_rev', String(40), nullable=True)
3040 # TODO: dan: rename column to last_merge_target_rev
3041 _last_merge_target_rev = Column(
3042 'last_merge_other_rev', String(40), nullable=True)
3043 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3044 merge_rev = Column('merge_rev', String(40), nullable=True)
3045
3046 @hybrid_property
3047 def revisions(self):
3048 return self._revisions.split(':') if self._revisions else []
3049
3050 @revisions.setter
3051 def revisions(self, val):
3052 self._revisions = ':'.join(val)
3053
3054 @declared_attr
3055 def author(cls):
3056 return relationship('User', lazy='joined')
3057
3058 @declared_attr
3059 def source_repo(cls):
3060 return relationship(
3061 'Repository',
3062 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3063
3064 @property
3065 def source_ref_parts(self):
3066 refs = self.source_ref.split(':')
3067 return Reference(refs[0], refs[1], refs[2])
3068
3069 @declared_attr
3070 def target_repo(cls):
3071 return relationship(
3072 'Repository',
3073 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3074
3075 @property
3076 def target_ref_parts(self):
3077 refs = self.target_ref.split(':')
3078 return Reference(refs[0], refs[1], refs[2])
3079
3080
3081 class PullRequest(Base, _PullRequestBase):
3082 __tablename__ = 'pull_requests'
3083 __table_args__ = (
3084 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3085 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3086 )
3087
3088 pull_request_id = Column(
3089 'pull_request_id', Integer(), nullable=False, primary_key=True)
3090
3091 def __repr__(self):
3092 if self.pull_request_id:
3093 return '<DB:PullRequest #%s>' % self.pull_request_id
3094 else:
3095 return '<DB:PullRequest at %#x>' % id(self)
3096
3097 reviewers = relationship('PullRequestReviewers',
3098 cascade="all, delete, delete-orphan")
3099 statuses = relationship('ChangesetStatus')
3100 comments = relationship('ChangesetComment',
3101 cascade="all, delete, delete-orphan")
3102 versions = relationship('PullRequestVersion',
3103 cascade="all, delete, delete-orphan")
3104
3105 def is_closed(self):
3106 return self.status == self.STATUS_CLOSED
3107
3108 def get_api_data(self):
3109 from rhodecode.model.pull_request import PullRequestModel
3110 pull_request = self
3111 merge_status = PullRequestModel().merge_status(pull_request)
3112 data = {
3113 'pull_request_id': pull_request.pull_request_id,
3114 'url': url('pullrequest_show', repo_name=self.target_repo.repo_name,
3115 pull_request_id=self.pull_request_id,
3116 qualified=True),
3117 'title': pull_request.title,
3118 'description': pull_request.description,
3119 'status': pull_request.status,
3120 'created_on': pull_request.created_on,
3121 'updated_on': pull_request.updated_on,
3122 'commit_ids': pull_request.revisions,
3123 'review_status': pull_request.calculated_review_status(),
3124 'mergeable': {
3125 'status': merge_status[0],
3126 'message': unicode(merge_status[1]),
3127 },
3128 'source': {
3129 'clone_url': pull_request.source_repo.clone_url(),
3130 'repository': pull_request.source_repo.repo_name,
3131 'reference': {
3132 'name': pull_request.source_ref_parts.name,
3133 'type': pull_request.source_ref_parts.type,
3134 'commit_id': pull_request.source_ref_parts.commit_id,
3135 },
3136 },
3137 'target': {
3138 'clone_url': pull_request.target_repo.clone_url(),
3139 'repository': pull_request.target_repo.repo_name,
3140 'reference': {
3141 'name': pull_request.target_ref_parts.name,
3142 'type': pull_request.target_ref_parts.type,
3143 'commit_id': pull_request.target_ref_parts.commit_id,
3144 },
3145 },
3146 'author': pull_request.author.get_api_data(include_secrets=False,
3147 details='basic'),
3148 'reviewers': [
3149 {
3150 'user': reviewer.get_api_data(include_secrets=False,
3151 details='basic'),
3152 'review_status': st[0][1].status if st else 'not_reviewed',
3153 }
3154 for reviewer, st in pull_request.reviewers_statuses()
3155 ]
3156 }
3157
3158 return data
3159
3160 def __json__(self):
3161 return {
3162 'revisions': self.revisions,
3163 }
3164
3165 def calculated_review_status(self):
3166 # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html
3167 # because it's tricky on how to use ChangesetStatusModel from there
3168 warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning)
3169 from rhodecode.model.changeset_status import ChangesetStatusModel
3170 return ChangesetStatusModel().calculated_review_status(self)
3171
3172 def reviewers_statuses(self):
3173 warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
3174 from rhodecode.model.changeset_status import ChangesetStatusModel
3175 return ChangesetStatusModel().reviewers_statuses(self)
3176
3177
3178 class PullRequestVersion(Base, _PullRequestBase):
3179 __tablename__ = 'pull_request_versions'
3180 __table_args__ = (
3181 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3182 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3183 )
3184
3185 pull_request_version_id = Column(
3186 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3187 pull_request_id = Column(
3188 'pull_request_id', Integer(),
3189 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3190 pull_request = relationship('PullRequest')
3191
3192 def __repr__(self):
3193 if self.pull_request_version_id:
3194 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3195 else:
3196 return '<DB:PullRequestVersion at %#x>' % id(self)
3197
3198
3199 class PullRequestReviewers(Base, BaseModel):
3200 __tablename__ = 'pull_request_reviewers'
3201 __table_args__ = (
3202 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3203 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3204 )
3205
3206 def __init__(self, user=None, pull_request=None):
3207 self.user = user
3208 self.pull_request = pull_request
3209
3210 pull_requests_reviewers_id = Column(
3211 'pull_requests_reviewers_id', Integer(), nullable=False,
3212 primary_key=True)
3213 pull_request_id = Column(
3214 "pull_request_id", Integer(),
3215 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3216 user_id = Column(
3217 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3218
3219 user = relationship('User')
3220 pull_request = relationship('PullRequest')
3221
3222
3223 class Notification(Base, BaseModel):
3224 __tablename__ = 'notifications'
3225 __table_args__ = (
3226 Index('notification_type_idx', 'type'),
3227 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3228 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3229 )
3230
3231 TYPE_CHANGESET_COMMENT = u'cs_comment'
3232 TYPE_MESSAGE = u'message'
3233 TYPE_MENTION = u'mention'
3234 TYPE_REGISTRATION = u'registration'
3235 TYPE_PULL_REQUEST = u'pull_request'
3236 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3237
3238 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3239 subject = Column('subject', Unicode(512), nullable=True)
3240 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3241 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3242 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3243 type_ = Column('type', Unicode(255))
3244
3245 created_by_user = relationship('User')
3246 notifications_to_users = relationship('UserNotification', lazy='joined',
3247 cascade="all, delete, delete-orphan")
3248
3249 @property
3250 def recipients(self):
3251 return [x.user for x in UserNotification.query()\
3252 .filter(UserNotification.notification == self)\
3253 .order_by(UserNotification.user_id.asc()).all()]
3254
3255 @classmethod
3256 def create(cls, created_by, subject, body, recipients, type_=None):
3257 if type_ is None:
3258 type_ = Notification.TYPE_MESSAGE
3259
3260 notification = cls()
3261 notification.created_by_user = created_by
3262 notification.subject = subject
3263 notification.body = body
3264 notification.type_ = type_
3265 notification.created_on = datetime.datetime.now()
3266
3267 for u in recipients:
3268 assoc = UserNotification()
3269 assoc.notification = notification
3270
3271 # if created_by is inside recipients mark his notification
3272 # as read
3273 if u.user_id == created_by.user_id:
3274 assoc.read = True
3275
3276 u.notifications.append(assoc)
3277 Session().add(notification)
3278
3279 return notification
3280
3281 @property
3282 def description(self):
3283 from rhodecode.model.notification import NotificationModel
3284 return NotificationModel().make_description(self)
3285
3286
3287 class UserNotification(Base, BaseModel):
3288 __tablename__ = 'user_to_notification'
3289 __table_args__ = (
3290 UniqueConstraint('user_id', 'notification_id'),
3291 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3292 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3293 )
3294 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3295 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3296 read = Column('read', Boolean, default=False)
3297 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3298
3299 user = relationship('User', lazy="joined")
3300 notification = relationship('Notification', lazy="joined",
3301 order_by=lambda: Notification.created_on.desc(),)
3302
3303 def mark_as_read(self):
3304 self.read = True
3305 Session().add(self)
3306
3307
3308 class Gist(Base, BaseModel):
3309 __tablename__ = 'gists'
3310 __table_args__ = (
3311 Index('g_gist_access_id_idx', 'gist_access_id'),
3312 Index('g_created_on_idx', 'created_on'),
3313 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3314 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3315 )
3316 GIST_PUBLIC = u'public'
3317 GIST_PRIVATE = u'private'
3318 DEFAULT_FILENAME = u'gistfile1.txt'
3319
3320 ACL_LEVEL_PUBLIC = u'acl_public'
3321 ACL_LEVEL_PRIVATE = u'acl_private'
3322
3323 gist_id = Column('gist_id', Integer(), primary_key=True)
3324 gist_access_id = Column('gist_access_id', Unicode(250))
3325 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3326 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3327 gist_expires = Column('gist_expires', Float(53), nullable=False)
3328 gist_type = Column('gist_type', Unicode(128), nullable=False)
3329 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3330 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3331 acl_level = Column('acl_level', Unicode(128), nullable=True)
3332
3333 owner = relationship('User')
3334
3335 def __repr__(self):
3336 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3337
3338 @classmethod
3339 def get_or_404(cls, id_):
3340 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3341 if not res:
3342 raise HTTPNotFound
3343 return res
3344
3345 @classmethod
3346 def get_by_access_id(cls, gist_access_id):
3347 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3348
3349 def gist_url(self):
3350 import rhodecode
3351 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3352 if alias_url:
3353 return alias_url.replace('{gistid}', self.gist_access_id)
3354
3355 return url('gist', gist_id=self.gist_access_id, qualified=True)
3356
3357 @classmethod
3358 def base_path(cls):
3359 """
3360 Returns base path when all gists are stored
3361
3362 :param cls:
3363 """
3364 from rhodecode.model.gist import GIST_STORE_LOC
3365 q = Session().query(RhodeCodeUi)\
3366 .filter(RhodeCodeUi.ui_key == URL_SEP)
3367 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3368 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3369
3370 def get_api_data(self):
3371 """
3372 Common function for generating gist related data for API
3373 """
3374 gist = self
3375 data = {
3376 'gist_id': gist.gist_id,
3377 'type': gist.gist_type,
3378 'access_id': gist.gist_access_id,
3379 'description': gist.gist_description,
3380 'url': gist.gist_url(),
3381 'expires': gist.gist_expires,
3382 'created_on': gist.created_on,
3383 'modified_at': gist.modified_at,
3384 'content': None,
3385 'acl_level': gist.acl_level,
3386 }
3387 return data
3388
3389 def __json__(self):
3390 data = dict(
3391 )
3392 data.update(self.get_api_data())
3393 return data
3394 # SCM functions
3395
3396 def scm_instance(self, **kwargs):
3397 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3398 return get_vcs_instance(
3399 repo_path=safe_str(full_repo_path), create=False)
3400
3401
3402 class DbMigrateVersion(Base, BaseModel):
3403 __tablename__ = 'db_migrate_version'
3404 __table_args__ = (
3405 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3406 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3407 )
3408 repository_id = Column('repository_id', String(250), primary_key=True)
3409 repository_path = Column('repository_path', Text)
3410 version = Column('version', Integer)
3411
3412
3413 class ExternalIdentity(Base, BaseModel):
3414 __tablename__ = 'external_identities'
3415 __table_args__ = (
3416 Index('local_user_id_idx', 'local_user_id'),
3417 Index('external_id_idx', 'external_id'),
3418 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3419 'mysql_charset': 'utf8'})
3420
3421 external_id = Column('external_id', Unicode(255), default=u'',
3422 primary_key=True)
3423 external_username = Column('external_username', Unicode(1024), default=u'')
3424 local_user_id = Column('local_user_id', Integer(),
3425 ForeignKey('users.user_id'), primary_key=True)
3426 provider_name = Column('provider_name', Unicode(255), default=u'',
3427 primary_key=True)
3428 access_token = Column('access_token', String(1024), default=u'')
3429 alt_token = Column('alt_token', String(1024), default=u'')
3430 token_secret = Column('token_secret', String(1024), default=u'')
3431
3432 @classmethod
3433 def by_external_id_and_provider(cls, external_id, provider_name,
3434 local_user_id=None):
3435 """
3436 Returns ExternalIdentity instance based on search params
3437
3438 :param external_id:
3439 :param provider_name:
3440 :return: ExternalIdentity
3441 """
3442 query = cls.query()
3443 query = query.filter(cls.external_id == external_id)
3444 query = query.filter(cls.provider_name == provider_name)
3445 if local_user_id:
3446 query = query.filter(cls.local_user_id == local_user_id)
3447 return query.first()
3448
3449 @classmethod
3450 def user_by_external_id_and_provider(cls, external_id, provider_name):
3451 """
3452 Returns User instance based on search params
3453
3454 :param external_id:
3455 :param provider_name:
3456 :return: User
3457 """
3458 query = User.query()
3459 query = query.filter(cls.external_id == external_id)
3460 query = query.filter(cls.provider_name == provider_name)
3461 query = query.filter(User.user_id == cls.local_user_id)
3462 return query.first()
3463
3464 @classmethod
3465 def by_local_user_id(cls, local_user_id):
3466 """
3467 Returns all tokens for user
3468
3469 :param local_user_id:
3470 :return: ExternalIdentity
3471 """
3472 query = cls.query()
3473 query = query.filter(cls.local_user_id == local_user_id)
3474 return query
3475
3476
3477 class Integration(Base, BaseModel):
3478 __tablename__ = 'integrations'
3479 __table_args__ = (
3480 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3481 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3482 )
3483
3484 integration_id = Column('integration_id', Integer(), primary_key=True)
3485 integration_type = Column('integration_type', String(255))
3486 enabled = Column('enabled', Boolean(), nullable=False)
3487 name = Column('name', String(255), nullable=False)
3488 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3489 default=False)
3490
3491 settings = Column(
3492 'settings_json', MutationObj.as_mutable(
3493 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3494 repo_id = Column(
3495 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3496 nullable=True, unique=None, default=None)
3497 repo = relationship('Repository', lazy='joined')
3498
3499 repo_group_id = Column(
3500 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3501 nullable=True, unique=None, default=None)
3502 repo_group = relationship('RepoGroup', lazy='joined')
3503
3504 @property
3505 def scope(self):
3506 if self.repo:
3507 return repr(self.repo)
3508 if self.repo_group:
3509 if self.child_repos_only:
3510 return repr(self.repo_group) + ' (child repos only)'
3511 else:
3512 return repr(self.repo_group) + ' (recursive)'
3513 if self.child_repos_only:
3514 return 'root_repos'
3515 return 'global'
3516
3517 def __repr__(self):
3518 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3519
3520
3521 class RepoReviewRuleUser(Base, BaseModel):
3522 __tablename__ = 'repo_review_rules_users'
3523 __table_args__ = (
3524 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3525 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3526 )
3527 repo_review_rule_user_id = Column(
3528 'repo_review_rule_user_id', Integer(), primary_key=True)
3529 repo_review_rule_id = Column("repo_review_rule_id",
3530 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3531 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3532 nullable=False)
3533 user = relationship('User')
3534
3535
3536 class RepoReviewRuleUserGroup(Base, BaseModel):
3537 __tablename__ = 'repo_review_rules_users_groups'
3538 __table_args__ = (
3539 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3540 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3541 )
3542 repo_review_rule_users_group_id = Column(
3543 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3544 repo_review_rule_id = Column("repo_review_rule_id",
3545 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3546 users_group_id = Column("users_group_id", Integer(),
3547 ForeignKey('users_groups.users_group_id'), nullable=False)
3548 users_group = relationship('UserGroup')
3549
3550
3551 class RepoReviewRule(Base, BaseModel):
3552 __tablename__ = 'repo_review_rules'
3553 __table_args__ = (
3554 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3555 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3556 )
3557
3558 repo_review_rule_id = Column(
3559 'repo_review_rule_id', Integer(), primary_key=True)
3560 repo_id = Column(
3561 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3562 repo = relationship('Repository', backref='review_rules')
3563
3564 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3565 default=u'*') # glob
3566 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3567 default=u'*') # glob
3568
3569 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3570 nullable=False, default=False)
3571 rule_users = relationship('RepoReviewRuleUser')
3572 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3573
3574 @hybrid_property
3575 def branch_pattern(self):
3576 return self._branch_pattern or '*'
3577
3578 def _validate_pattern(self, value):
3579 re.compile('^' + glob2re(value) + '$')
3580
3581 @branch_pattern.setter
3582 def branch_pattern(self, value):
3583 self._validate_glob(value)
3584 self._branch_pattern = value or '*'
3585
3586 @hybrid_property
3587 def file_pattern(self):
3588 return self._file_pattern or '*'
3589
3590 @file_pattern.setter
3591 def file_pattern(self, value):
3592 self._validate_glob(value)
3593 self._file_pattern = value or '*'
3594
3595 def matches(self, branch, files_changed):
3596 """
3597 Check if this review rule matches a branch/files in a pull request
3598
3599 :param branch: branch name for the commit
3600 :param files_changed: list of file paths changed in the pull request
3601 """
3602
3603 branch = branch or ''
3604 files_changed = files_changed or []
3605
3606 branch_matches = True
3607 if branch:
3608 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3609 branch_matches = bool(branch_regex.search(branch))
3610
3611 files_matches = True
3612 if self.file_pattern != '*':
3613 files_matches = False
3614 file_regex = re.compile(glob2re(self.file_pattern))
3615 for filename in files_changed:
3616 if file_regex.search(filename):
3617 files_matches = True
3618 break
3619
3620 return branch_matches and files_matches
3621
3622 @property
3623 def review_users(self):
3624 """ Returns the users which this rule applies to """
3625
3626 users = set()
3627 users |= set([
3628 rule_user.user for rule_user in self.rule_users
3629 if rule_user.user.active])
3630 users |= set(
3631 member.user
3632 for rule_user_group in self.rule_user_groups
3633 for member in rule_user_group.users_group.members
3634 if member.user.active
3635 )
3636 return users
3637
3638 def __repr__(self):
3639 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3640 self.repo_review_rule_id, self.repo)
@@ -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_2
28
29 db_4_4_0_2.RepoReviewRule.__table__.create()
30 db_4_4_0_2.RepoReviewRuleUser.__table__.create()
31 db_4_4_0_2.RepoReviewRuleUserGroup.__table__.create()
32
33 def downgrade(migrate_engine):
34 meta = MetaData()
35 meta.bind = migrate_engine
@@ -1,63 +1,63 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__ = 58 # defines current db version for migrations
54 __dbversion__ = 59 # 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
63 63 disable_error_handler = False
@@ -1,1160 +1,1167 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 45 'repo_group_name': r'.*?[^/]',
46 46 # repo names can have a slash in them, but they must not end with a slash
47 47 'repo_name': r'.*?[^/]',
48 48 # file path eats up everything at the end
49 49 'f_path': r'.*',
50 50 # reference types
51 51 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
52 52 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
53 53 }
54 54
55 55
56 56 def add_route_requirements(route_path, requirements):
57 57 """
58 58 Adds regex requirements to pyramid routes using a mapping dict
59 59
60 60 >>> add_route_requirements('/{action}/{id}', {'id': r'\d+'})
61 61 '/{action}/{id:\d+}'
62 62
63 63 """
64 64 for key, regex in requirements.items():
65 65 route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
66 66 return route_path
67 67
68 68
69 69 class JSRoutesMapper(Mapper):
70 70 """
71 71 Wrapper for routes.Mapper to make pyroutes compatible url definitions
72 72 """
73 73 _named_route_regex = re.compile(r'^[a-z-_0-9A-Z]+$')
74 74 _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
75 75 def __init__(self, *args, **kw):
76 76 super(JSRoutesMapper, self).__init__(*args, **kw)
77 77 self._jsroutes = []
78 78
79 79 def connect(self, *args, **kw):
80 80 """
81 81 Wrapper for connect to take an extra argument jsroute=True
82 82
83 83 :param jsroute: boolean, if True will add the route to the pyroutes list
84 84 """
85 85 if kw.pop('jsroute', False):
86 86 if not self._named_route_regex.match(args[0]):
87 87 raise Exception('only named routes can be added to pyroutes')
88 88 self._jsroutes.append(args[0])
89 89
90 90 super(JSRoutesMapper, self).connect(*args, **kw)
91 91
92 92 def _extract_route_information(self, route):
93 93 """
94 94 Convert a route into tuple(name, path, args), eg:
95 95 ('user_profile', '/profile/%(username)s', ['username'])
96 96 """
97 97 routepath = route.routepath
98 98 def replace(matchobj):
99 99 if matchobj.group(1):
100 100 return "%%(%s)s" % matchobj.group(1).split(':')[0]
101 101 else:
102 102 return "%%(%s)s" % matchobj.group(2)
103 103
104 104 routepath = self._argument_prog.sub(replace, routepath)
105 105 return (
106 106 route.name,
107 107 routepath,
108 108 [(arg[0].split(':')[0] if arg[0] != '' else arg[1])
109 109 for arg in self._argument_prog.findall(route.routepath)]
110 110 )
111 111
112 112 def jsroutes(self):
113 113 """
114 114 Return a list of pyroutes.js compatible routes
115 115 """
116 116 for route_name in self._jsroutes:
117 117 yield self._extract_route_information(self._routenames[route_name])
118 118
119 119
120 120 def make_map(config):
121 121 """Create, configure and return the routes Mapper"""
122 122 rmap = JSRoutesMapper(directory=config['pylons.paths']['controllers'],
123 123 always_scan=config['debug'])
124 124 rmap.minimization = False
125 125 rmap.explicit = False
126 126
127 127 from rhodecode.lib.utils2 import str2bool
128 128 from rhodecode.model import repo, repo_group
129 129
130 130 def check_repo(environ, match_dict):
131 131 """
132 132 check for valid repository for proper 404 handling
133 133
134 134 :param environ:
135 135 :param match_dict:
136 136 """
137 137 repo_name = match_dict.get('repo_name')
138 138
139 139 if match_dict.get('f_path'):
140 140 # fix for multiple initial slashes that causes errors
141 141 match_dict['f_path'] = match_dict['f_path'].lstrip('/')
142 142 repo_model = repo.RepoModel()
143 143 by_name_match = repo_model.get_by_repo_name(repo_name)
144 144 # if we match quickly from database, short circuit the operation,
145 145 # and validate repo based on the type.
146 146 if by_name_match:
147 147 return True
148 148
149 149 by_id_match = repo_model.get_repo_by_id(repo_name)
150 150 if by_id_match:
151 151 repo_name = by_id_match.repo_name
152 152 match_dict['repo_name'] = repo_name
153 153 return True
154 154
155 155 return False
156 156
157 157 def check_group(environ, match_dict):
158 158 """
159 159 check for valid repository group path for proper 404 handling
160 160
161 161 :param environ:
162 162 :param match_dict:
163 163 """
164 164 repo_group_name = match_dict.get('group_name')
165 165 repo_group_model = repo_group.RepoGroupModel()
166 166 by_name_match = repo_group_model.get_by_group_name(repo_group_name)
167 167 if by_name_match:
168 168 return True
169 169
170 170 return False
171 171
172 172 def check_user_group(environ, match_dict):
173 173 """
174 174 check for valid user group for proper 404 handling
175 175
176 176 :param environ:
177 177 :param match_dict:
178 178 """
179 179 return True
180 180
181 181 def check_int(environ, match_dict):
182 182 return match_dict.get('id').isdigit()
183 183
184 184
185 185 #==========================================================================
186 186 # CUSTOM ROUTES HERE
187 187 #==========================================================================
188 188
189 189 # MAIN PAGE
190 190 rmap.connect('home', '/', controller='home', action='index', jsroute=True)
191 191 rmap.connect('goto_switcher_data', '/_goto_data', controller='home',
192 192 action='goto_switcher_data')
193 193 rmap.connect('repo_list_data', '/_repos', controller='home',
194 194 action='repo_list_data')
195 195
196 196 rmap.connect('user_autocomplete_data', '/_users', controller='home',
197 197 action='user_autocomplete_data', jsroute=True)
198 198 rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
199 action='user_group_autocomplete_data')
199 action='user_group_autocomplete_data', jsroute=True)
200 200
201 201 rmap.connect(
202 202 'user_profile', '/_profiles/{username}', controller='users',
203 203 action='user_profile')
204 204
205 205 # TODO: johbo: Static links, to be replaced by our redirection mechanism
206 206 rmap.connect('rst_help',
207 207 'http://docutils.sourceforge.net/docs/user/rst/quickref.html',
208 208 _static=True)
209 209 rmap.connect('markdown_help',
210 210 'http://daringfireball.net/projects/markdown/syntax',
211 211 _static=True)
212 212 rmap.connect('rhodecode_official', 'https://rhodecode.com', _static=True)
213 213 rmap.connect('rhodecode_support', 'https://rhodecode.com/help/', _static=True)
214 214 rmap.connect('rhodecode_translations', 'https://rhodecode.com/translate/enterprise', _static=True)
215 215 # TODO: anderson - making this a static link since redirect won't play
216 216 # nice with POST requests
217 217 rmap.connect('enterprise_license_convert_from_old',
218 218 'https://rhodecode.com/u/license-upgrade',
219 219 _static=True)
220 220
221 221 routing_links.connect_redirection_links(rmap)
222 222
223 223 rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping')
224 224 rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test')
225 225
226 226 # ADMIN REPOSITORY ROUTES
227 227 with rmap.submapper(path_prefix=ADMIN_PREFIX,
228 228 controller='admin/repos') as m:
229 229 m.connect('repos', '/repos',
230 230 action='create', conditions={'method': ['POST']})
231 231 m.connect('repos', '/repos',
232 232 action='index', conditions={'method': ['GET']})
233 233 m.connect('new_repo', '/create_repository', jsroute=True,
234 234 action='create_repository', conditions={'method': ['GET']})
235 235 m.connect('/repos/{repo_name}',
236 236 action='update', conditions={'method': ['PUT'],
237 237 'function': check_repo},
238 238 requirements=URL_NAME_REQUIREMENTS)
239 239 m.connect('delete_repo', '/repos/{repo_name}',
240 240 action='delete', conditions={'method': ['DELETE']},
241 241 requirements=URL_NAME_REQUIREMENTS)
242 242 m.connect('repo', '/repos/{repo_name}',
243 243 action='show', conditions={'method': ['GET'],
244 244 'function': check_repo},
245 245 requirements=URL_NAME_REQUIREMENTS)
246 246
247 247 # ADMIN REPOSITORY GROUPS ROUTES
248 248 with rmap.submapper(path_prefix=ADMIN_PREFIX,
249 249 controller='admin/repo_groups') as m:
250 250 m.connect('repo_groups', '/repo_groups',
251 251 action='create', conditions={'method': ['POST']})
252 252 m.connect('repo_groups', '/repo_groups',
253 253 action='index', conditions={'method': ['GET']})
254 254 m.connect('new_repo_group', '/repo_groups/new',
255 255 action='new', conditions={'method': ['GET']})
256 256 m.connect('update_repo_group', '/repo_groups/{group_name}',
257 257 action='update', conditions={'method': ['PUT'],
258 258 'function': check_group},
259 259 requirements=URL_NAME_REQUIREMENTS)
260 260
261 261 # EXTRAS REPO GROUP ROUTES
262 262 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
263 263 action='edit',
264 264 conditions={'method': ['GET'], 'function': check_group},
265 265 requirements=URL_NAME_REQUIREMENTS)
266 266 m.connect('edit_repo_group', '/repo_groups/{group_name}/edit',
267 267 action='edit',
268 268 conditions={'method': ['PUT'], 'function': check_group},
269 269 requirements=URL_NAME_REQUIREMENTS)
270 270
271 271 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
272 272 action='edit_repo_group_advanced',
273 273 conditions={'method': ['GET'], 'function': check_group},
274 274 requirements=URL_NAME_REQUIREMENTS)
275 275 m.connect('edit_repo_group_advanced', '/repo_groups/{group_name}/edit/advanced',
276 276 action='edit_repo_group_advanced',
277 277 conditions={'method': ['PUT'], 'function': check_group},
278 278 requirements=URL_NAME_REQUIREMENTS)
279 279
280 280 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
281 281 action='edit_repo_group_perms',
282 282 conditions={'method': ['GET'], 'function': check_group},
283 283 requirements=URL_NAME_REQUIREMENTS)
284 284 m.connect('edit_repo_group_perms', '/repo_groups/{group_name}/edit/permissions',
285 285 action='update_perms',
286 286 conditions={'method': ['PUT'], 'function': check_group},
287 287 requirements=URL_NAME_REQUIREMENTS)
288 288
289 289 m.connect('delete_repo_group', '/repo_groups/{group_name}',
290 290 action='delete', conditions={'method': ['DELETE'],
291 291 'function': check_group},
292 292 requirements=URL_NAME_REQUIREMENTS)
293 293
294 294 # ADMIN USER ROUTES
295 295 with rmap.submapper(path_prefix=ADMIN_PREFIX,
296 296 controller='admin/users') as m:
297 297 m.connect('users', '/users',
298 298 action='create', conditions={'method': ['POST']})
299 299 m.connect('users', '/users',
300 300 action='index', conditions={'method': ['GET']})
301 301 m.connect('new_user', '/users/new',
302 302 action='new', conditions={'method': ['GET']})
303 303 m.connect('update_user', '/users/{user_id}',
304 304 action='update', conditions={'method': ['PUT']})
305 305 m.connect('delete_user', '/users/{user_id}',
306 306 action='delete', conditions={'method': ['DELETE']})
307 307 m.connect('edit_user', '/users/{user_id}/edit',
308 308 action='edit', conditions={'method': ['GET']})
309 309 m.connect('user', '/users/{user_id}',
310 310 action='show', conditions={'method': ['GET']})
311 311 m.connect('force_password_reset_user', '/users/{user_id}/password_reset',
312 312 action='reset_password', conditions={'method': ['POST']})
313 313 m.connect('create_personal_repo_group', '/users/{user_id}/create_repo_group',
314 314 action='create_personal_repo_group', conditions={'method': ['POST']})
315 315
316 316 # EXTRAS USER ROUTES
317 317 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
318 318 action='edit_advanced', conditions={'method': ['GET']})
319 319 m.connect('edit_user_advanced', '/users/{user_id}/edit/advanced',
320 320 action='update_advanced', conditions={'method': ['PUT']})
321 321
322 322 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
323 323 action='edit_auth_tokens', conditions={'method': ['GET']})
324 324 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
325 325 action='add_auth_token', conditions={'method': ['PUT']})
326 326 m.connect('edit_user_auth_tokens', '/users/{user_id}/edit/auth_tokens',
327 327 action='delete_auth_token', conditions={'method': ['DELETE']})
328 328
329 329 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
330 330 action='edit_global_perms', conditions={'method': ['GET']})
331 331 m.connect('edit_user_global_perms', '/users/{user_id}/edit/global_permissions',
332 332 action='update_global_perms', conditions={'method': ['PUT']})
333 333
334 334 m.connect('edit_user_perms_summary', '/users/{user_id}/edit/permissions_summary',
335 335 action='edit_perms_summary', conditions={'method': ['GET']})
336 336
337 337 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
338 338 action='edit_emails', conditions={'method': ['GET']})
339 339 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
340 340 action='add_email', conditions={'method': ['PUT']})
341 341 m.connect('edit_user_emails', '/users/{user_id}/edit/emails',
342 342 action='delete_email', conditions={'method': ['DELETE']})
343 343
344 344 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
345 345 action='edit_ips', conditions={'method': ['GET']})
346 346 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
347 347 action='add_ip', conditions={'method': ['PUT']})
348 348 m.connect('edit_user_ips', '/users/{user_id}/edit/ips',
349 349 action='delete_ip', conditions={'method': ['DELETE']})
350 350
351 351 # ADMIN USER GROUPS REST ROUTES
352 352 with rmap.submapper(path_prefix=ADMIN_PREFIX,
353 353 controller='admin/user_groups') as m:
354 354 m.connect('users_groups', '/user_groups',
355 355 action='create', conditions={'method': ['POST']})
356 356 m.connect('users_groups', '/user_groups',
357 357 action='index', conditions={'method': ['GET']})
358 358 m.connect('new_users_group', '/user_groups/new',
359 359 action='new', conditions={'method': ['GET']})
360 360 m.connect('update_users_group', '/user_groups/{user_group_id}',
361 361 action='update', conditions={'method': ['PUT']})
362 362 m.connect('delete_users_group', '/user_groups/{user_group_id}',
363 363 action='delete', conditions={'method': ['DELETE']})
364 364 m.connect('edit_users_group', '/user_groups/{user_group_id}/edit',
365 365 action='edit', conditions={'method': ['GET']},
366 366 function=check_user_group)
367 367
368 368 # EXTRAS USER GROUP ROUTES
369 369 m.connect('edit_user_group_global_perms',
370 370 '/user_groups/{user_group_id}/edit/global_permissions',
371 371 action='edit_global_perms', conditions={'method': ['GET']})
372 372 m.connect('edit_user_group_global_perms',
373 373 '/user_groups/{user_group_id}/edit/global_permissions',
374 374 action='update_global_perms', conditions={'method': ['PUT']})
375 375 m.connect('edit_user_group_perms_summary',
376 376 '/user_groups/{user_group_id}/edit/permissions_summary',
377 377 action='edit_perms_summary', conditions={'method': ['GET']})
378 378
379 379 m.connect('edit_user_group_perms',
380 380 '/user_groups/{user_group_id}/edit/permissions',
381 381 action='edit_perms', conditions={'method': ['GET']})
382 382 m.connect('edit_user_group_perms',
383 383 '/user_groups/{user_group_id}/edit/permissions',
384 384 action='update_perms', conditions={'method': ['PUT']})
385 385
386 386 m.connect('edit_user_group_advanced',
387 387 '/user_groups/{user_group_id}/edit/advanced',
388 388 action='edit_advanced', conditions={'method': ['GET']})
389 389
390 390 m.connect('edit_user_group_members',
391 391 '/user_groups/{user_group_id}/edit/members', jsroute=True,
392 392 action='edit_members', conditions={'method': ['GET']})
393 393
394 394 # ADMIN PERMISSIONS ROUTES
395 395 with rmap.submapper(path_prefix=ADMIN_PREFIX,
396 396 controller='admin/permissions') as m:
397 397 m.connect('admin_permissions_application', '/permissions/application',
398 398 action='permission_application_update', conditions={'method': ['POST']})
399 399 m.connect('admin_permissions_application', '/permissions/application',
400 400 action='permission_application', conditions={'method': ['GET']})
401 401
402 402 m.connect('admin_permissions_global', '/permissions/global',
403 403 action='permission_global_update', conditions={'method': ['POST']})
404 404 m.connect('admin_permissions_global', '/permissions/global',
405 405 action='permission_global', conditions={'method': ['GET']})
406 406
407 407 m.connect('admin_permissions_object', '/permissions/object',
408 408 action='permission_objects_update', conditions={'method': ['POST']})
409 409 m.connect('admin_permissions_object', '/permissions/object',
410 410 action='permission_objects', conditions={'method': ['GET']})
411 411
412 412 m.connect('admin_permissions_ips', '/permissions/ips',
413 413 action='permission_ips', conditions={'method': ['POST']})
414 414 m.connect('admin_permissions_ips', '/permissions/ips',
415 415 action='permission_ips', conditions={'method': ['GET']})
416 416
417 417 m.connect('admin_permissions_overview', '/permissions/overview',
418 418 action='permission_perms', conditions={'method': ['GET']})
419 419
420 420 # ADMIN DEFAULTS REST ROUTES
421 421 with rmap.submapper(path_prefix=ADMIN_PREFIX,
422 422 controller='admin/defaults') as m:
423 423 m.connect('admin_defaults_repositories', '/defaults/repositories',
424 424 action='update_repository_defaults', conditions={'method': ['POST']})
425 425 m.connect('admin_defaults_repositories', '/defaults/repositories',
426 426 action='index', conditions={'method': ['GET']})
427 427
428 428 # ADMIN DEBUG STYLE ROUTES
429 429 if str2bool(config.get('debug_style')):
430 430 with rmap.submapper(path_prefix=ADMIN_PREFIX + '/debug_style',
431 431 controller='debug_style') as m:
432 432 m.connect('debug_style_home', '',
433 433 action='index', conditions={'method': ['GET']})
434 434 m.connect('debug_style_template', '/t/{t_path}',
435 435 action='template', conditions={'method': ['GET']})
436 436
437 437 # ADMIN SETTINGS ROUTES
438 438 with rmap.submapper(path_prefix=ADMIN_PREFIX,
439 439 controller='admin/settings') as m:
440 440
441 441 # default
442 442 m.connect('admin_settings', '/settings',
443 443 action='settings_global_update',
444 444 conditions={'method': ['POST']})
445 445 m.connect('admin_settings', '/settings',
446 446 action='settings_global', conditions={'method': ['GET']})
447 447
448 448 m.connect('admin_settings_vcs', '/settings/vcs',
449 449 action='settings_vcs_update',
450 450 conditions={'method': ['POST']})
451 451 m.connect('admin_settings_vcs', '/settings/vcs',
452 452 action='settings_vcs',
453 453 conditions={'method': ['GET']})
454 454 m.connect('admin_settings_vcs', '/settings/vcs',
455 455 action='delete_svn_pattern',
456 456 conditions={'method': ['DELETE']})
457 457
458 458 m.connect('admin_settings_mapping', '/settings/mapping',
459 459 action='settings_mapping_update',
460 460 conditions={'method': ['POST']})
461 461 m.connect('admin_settings_mapping', '/settings/mapping',
462 462 action='settings_mapping', conditions={'method': ['GET']})
463 463
464 464 m.connect('admin_settings_global', '/settings/global',
465 465 action='settings_global_update',
466 466 conditions={'method': ['POST']})
467 467 m.connect('admin_settings_global', '/settings/global',
468 468 action='settings_global', conditions={'method': ['GET']})
469 469
470 470 m.connect('admin_settings_visual', '/settings/visual',
471 471 action='settings_visual_update',
472 472 conditions={'method': ['POST']})
473 473 m.connect('admin_settings_visual', '/settings/visual',
474 474 action='settings_visual', conditions={'method': ['GET']})
475 475
476 476 m.connect('admin_settings_issuetracker',
477 477 '/settings/issue-tracker', action='settings_issuetracker',
478 478 conditions={'method': ['GET']})
479 479 m.connect('admin_settings_issuetracker_save',
480 480 '/settings/issue-tracker/save',
481 481 action='settings_issuetracker_save',
482 482 conditions={'method': ['POST']})
483 483 m.connect('admin_issuetracker_test', '/settings/issue-tracker/test',
484 484 action='settings_issuetracker_test',
485 485 conditions={'method': ['POST']})
486 486 m.connect('admin_issuetracker_delete',
487 487 '/settings/issue-tracker/delete',
488 488 action='settings_issuetracker_delete',
489 489 conditions={'method': ['DELETE']})
490 490
491 491 m.connect('admin_settings_email', '/settings/email',
492 492 action='settings_email_update',
493 493 conditions={'method': ['POST']})
494 494 m.connect('admin_settings_email', '/settings/email',
495 495 action='settings_email', conditions={'method': ['GET']})
496 496
497 497 m.connect('admin_settings_hooks', '/settings/hooks',
498 498 action='settings_hooks_update',
499 499 conditions={'method': ['POST', 'DELETE']})
500 500 m.connect('admin_settings_hooks', '/settings/hooks',
501 501 action='settings_hooks', conditions={'method': ['GET']})
502 502
503 503 m.connect('admin_settings_search', '/settings/search',
504 504 action='settings_search', conditions={'method': ['GET']})
505 505
506 506 m.connect('admin_settings_system', '/settings/system',
507 507 action='settings_system', conditions={'method': ['GET']})
508 508
509 509 m.connect('admin_settings_system_update', '/settings/system/updates',
510 510 action='settings_system_update', conditions={'method': ['GET']})
511 511
512 512 m.connect('admin_settings_supervisor', '/settings/supervisor',
513 513 action='settings_supervisor', conditions={'method': ['GET']})
514 514 m.connect('admin_settings_supervisor_log', '/settings/supervisor/{procid}/log',
515 515 action='settings_supervisor_log', conditions={'method': ['GET']})
516 516
517 517 m.connect('admin_settings_labs', '/settings/labs',
518 518 action='settings_labs_update',
519 519 conditions={'method': ['POST']})
520 520 m.connect('admin_settings_labs', '/settings/labs',
521 521 action='settings_labs', conditions={'method': ['GET']})
522 522
523 523 # ADMIN MY ACCOUNT
524 524 with rmap.submapper(path_prefix=ADMIN_PREFIX,
525 525 controller='admin/my_account') as m:
526 526
527 527 m.connect('my_account', '/my_account',
528 528 action='my_account', conditions={'method': ['GET']})
529 529 m.connect('my_account_edit', '/my_account/edit',
530 530 action='my_account_edit', conditions={'method': ['GET']})
531 531 m.connect('my_account', '/my_account',
532 532 action='my_account_update', conditions={'method': ['POST']})
533 533
534 534 m.connect('my_account_password', '/my_account/password',
535 535 action='my_account_password', conditions={'method': ['GET', 'POST']})
536 536
537 537 m.connect('my_account_repos', '/my_account/repos',
538 538 action='my_account_repos', conditions={'method': ['GET']})
539 539
540 540 m.connect('my_account_watched', '/my_account/watched',
541 541 action='my_account_watched', conditions={'method': ['GET']})
542 542
543 543 m.connect('my_account_pullrequests', '/my_account/pull_requests',
544 544 action='my_account_pullrequests', conditions={'method': ['GET']})
545 545
546 546 m.connect('my_account_perms', '/my_account/perms',
547 547 action='my_account_perms', conditions={'method': ['GET']})
548 548
549 549 m.connect('my_account_emails', '/my_account/emails',
550 550 action='my_account_emails', conditions={'method': ['GET']})
551 551 m.connect('my_account_emails', '/my_account/emails',
552 552 action='my_account_emails_add', conditions={'method': ['POST']})
553 553 m.connect('my_account_emails', '/my_account/emails',
554 554 action='my_account_emails_delete', conditions={'method': ['DELETE']})
555 555
556 556 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
557 557 action='my_account_auth_tokens', conditions={'method': ['GET']})
558 558 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
559 559 action='my_account_auth_tokens_add', conditions={'method': ['POST']})
560 560 m.connect('my_account_auth_tokens', '/my_account/auth_tokens',
561 561 action='my_account_auth_tokens_delete', conditions={'method': ['DELETE']})
562 562 m.connect('my_account_notifications', '/my_account/notifications',
563 563 action='my_notifications',
564 564 conditions={'method': ['GET']})
565 565 m.connect('my_account_notifications_toggle_visibility',
566 566 '/my_account/toggle_visibility',
567 567 action='my_notifications_toggle_visibility',
568 568 conditions={'method': ['POST']})
569 569
570 570 # NOTIFICATION REST ROUTES
571 571 with rmap.submapper(path_prefix=ADMIN_PREFIX,
572 572 controller='admin/notifications') as m:
573 573 m.connect('notifications', '/notifications',
574 574 action='index', conditions={'method': ['GET']})
575 575 m.connect('notifications_mark_all_read', '/notifications/mark_all_read',
576 576 action='mark_all_read', conditions={'method': ['POST']})
577 577 m.connect('/notifications/{notification_id}',
578 578 action='update', conditions={'method': ['PUT']})
579 579 m.connect('/notifications/{notification_id}',
580 580 action='delete', conditions={'method': ['DELETE']})
581 581 m.connect('notification', '/notifications/{notification_id}',
582 582 action='show', conditions={'method': ['GET']})
583 583
584 584 # ADMIN GIST
585 585 with rmap.submapper(path_prefix=ADMIN_PREFIX,
586 586 controller='admin/gists') as m:
587 587 m.connect('gists', '/gists',
588 588 action='create', conditions={'method': ['POST']})
589 589 m.connect('gists', '/gists', jsroute=True,
590 590 action='index', conditions={'method': ['GET']})
591 591 m.connect('new_gist', '/gists/new', jsroute=True,
592 592 action='new', conditions={'method': ['GET']})
593 593
594 594 m.connect('/gists/{gist_id}',
595 595 action='delete', conditions={'method': ['DELETE']})
596 596 m.connect('edit_gist', '/gists/{gist_id}/edit',
597 597 action='edit_form', conditions={'method': ['GET']})
598 598 m.connect('edit_gist', '/gists/{gist_id}/edit',
599 599 action='edit', conditions={'method': ['POST']})
600 600 m.connect(
601 601 'edit_gist_check_revision', '/gists/{gist_id}/edit/check_revision',
602 602 action='check_revision', conditions={'method': ['GET']})
603 603
604 604 m.connect('gist', '/gists/{gist_id}',
605 605 action='show', conditions={'method': ['GET']})
606 606 m.connect('gist_rev', '/gists/{gist_id}/{revision}',
607 607 revision='tip',
608 608 action='show', conditions={'method': ['GET']})
609 609 m.connect('formatted_gist', '/gists/{gist_id}/{revision}/{format}',
610 610 revision='tip',
611 611 action='show', conditions={'method': ['GET']})
612 612 m.connect('formatted_gist_file', '/gists/{gist_id}/{revision}/{format}/{f_path}',
613 613 revision='tip',
614 614 action='show', conditions={'method': ['GET']},
615 615 requirements=URL_NAME_REQUIREMENTS)
616 616
617 617 # ADMIN MAIN PAGES
618 618 with rmap.submapper(path_prefix=ADMIN_PREFIX,
619 619 controller='admin/admin') as m:
620 620 m.connect('admin_home', '', action='index')
621 621 m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}',
622 622 action='add_repo')
623 623 m.connect(
624 624 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}',
625 625 action='pull_requests')
626 626 m.connect(
627 627 'pull_requests_global', '/pull-requests/{pull_request_id:[0-9]+}',
628 628 action='pull_requests')
629 629
630 630
631 631 # USER JOURNAL
632 632 rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,),
633 633 controller='journal', action='index')
634 634 rmap.connect('journal_rss', '%s/journal/rss' % (ADMIN_PREFIX,),
635 635 controller='journal', action='journal_rss')
636 636 rmap.connect('journal_atom', '%s/journal/atom' % (ADMIN_PREFIX,),
637 637 controller='journal', action='journal_atom')
638 638
639 639 rmap.connect('public_journal', '%s/public_journal' % (ADMIN_PREFIX,),
640 640 controller='journal', action='public_journal')
641 641
642 642 rmap.connect('public_journal_rss', '%s/public_journal/rss' % (ADMIN_PREFIX,),
643 643 controller='journal', action='public_journal_rss')
644 644
645 645 rmap.connect('public_journal_rss_old', '%s/public_journal_rss' % (ADMIN_PREFIX,),
646 646 controller='journal', action='public_journal_rss')
647 647
648 648 rmap.connect('public_journal_atom',
649 649 '%s/public_journal/atom' % (ADMIN_PREFIX,), controller='journal',
650 650 action='public_journal_atom')
651 651
652 652 rmap.connect('public_journal_atom_old',
653 653 '%s/public_journal_atom' % (ADMIN_PREFIX,), controller='journal',
654 654 action='public_journal_atom')
655 655
656 656 rmap.connect('toggle_following', '%s/toggle_following' % (ADMIN_PREFIX,),
657 657 controller='journal', action='toggle_following', jsroute=True,
658 658 conditions={'method': ['POST']})
659 659
660 660 # FULL TEXT SEARCH
661 661 rmap.connect('search', '%s/search' % (ADMIN_PREFIX,),
662 662 controller='search')
663 663 rmap.connect('search_repo_home', '/{repo_name}/search',
664 664 controller='search',
665 665 action='index',
666 666 conditions={'function': check_repo},
667 667 requirements=URL_NAME_REQUIREMENTS)
668 668
669 669 # FEEDS
670 670 rmap.connect('rss_feed_home', '/{repo_name}/feed/rss',
671 671 controller='feed', action='rss',
672 672 conditions={'function': check_repo},
673 673 requirements=URL_NAME_REQUIREMENTS)
674 674
675 675 rmap.connect('atom_feed_home', '/{repo_name}/feed/atom',
676 676 controller='feed', action='atom',
677 677 conditions={'function': check_repo},
678 678 requirements=URL_NAME_REQUIREMENTS)
679 679
680 680 #==========================================================================
681 681 # REPOSITORY ROUTES
682 682 #==========================================================================
683 683
684 684 rmap.connect('repo_creating_home', '/{repo_name}/repo_creating',
685 685 controller='admin/repos', action='repo_creating',
686 686 requirements=URL_NAME_REQUIREMENTS)
687 687 rmap.connect('repo_check_home', '/{repo_name}/crepo_check',
688 688 controller='admin/repos', action='repo_check',
689 689 requirements=URL_NAME_REQUIREMENTS)
690 690
691 691 rmap.connect('repo_stats', '/{repo_name}/repo_stats/{commit_id}',
692 692 controller='summary', action='repo_stats',
693 693 conditions={'function': check_repo},
694 694 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
695 695
696 696 rmap.connect('repo_refs_data', '/{repo_name}/refs-data',
697 697 controller='summary', action='repo_refs_data', jsroute=True,
698 698 requirements=URL_NAME_REQUIREMENTS)
699 699 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
700 700 controller='summary', action='repo_refs_changelog_data',
701 701 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
702 rmap.connect('repo_default_reviewers_data', '/{repo_name}/default-reviewers',
703 controller='summary', action='repo_default_reviewers_data',
704 jsroute=True, requirements=URL_NAME_REQUIREMENTS)
702 705
703 706 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
704 707 controller='changeset', revision='tip', jsroute=True,
705 708 conditions={'function': check_repo},
706 709 requirements=URL_NAME_REQUIREMENTS)
707 710 rmap.connect('changeset_children', '/{repo_name}/changeset_children/{revision}',
708 711 controller='changeset', revision='tip', action='changeset_children',
709 712 conditions={'function': check_repo},
710 713 requirements=URL_NAME_REQUIREMENTS)
711 714 rmap.connect('changeset_parents', '/{repo_name}/changeset_parents/{revision}',
712 715 controller='changeset', revision='tip', action='changeset_parents',
713 716 conditions={'function': check_repo},
714 717 requirements=URL_NAME_REQUIREMENTS)
715 718
716 719 # repo edit options
717 720 rmap.connect('edit_repo', '/{repo_name}/settings', jsroute=True,
718 721 controller='admin/repos', action='edit',
719 722 conditions={'method': ['GET'], 'function': check_repo},
720 723 requirements=URL_NAME_REQUIREMENTS)
721 724
722 725 rmap.connect('edit_repo_perms', '/{repo_name}/settings/permissions',
723 726 jsroute=True,
724 727 controller='admin/repos', action='edit_permissions',
725 728 conditions={'method': ['GET'], 'function': check_repo},
726 729 requirements=URL_NAME_REQUIREMENTS)
727 730 rmap.connect('edit_repo_perms_update', '/{repo_name}/settings/permissions',
728 731 controller='admin/repos', action='edit_permissions_update',
729 732 conditions={'method': ['PUT'], 'function': check_repo},
730 733 requirements=URL_NAME_REQUIREMENTS)
731 734
732 735 rmap.connect('edit_repo_fields', '/{repo_name}/settings/fields',
733 736 controller='admin/repos', action='edit_fields',
734 737 conditions={'method': ['GET'], 'function': check_repo},
735 738 requirements=URL_NAME_REQUIREMENTS)
736 739 rmap.connect('create_repo_fields', '/{repo_name}/settings/fields/new',
737 740 controller='admin/repos', action='create_repo_field',
738 741 conditions={'method': ['PUT'], 'function': check_repo},
739 742 requirements=URL_NAME_REQUIREMENTS)
740 743 rmap.connect('delete_repo_fields', '/{repo_name}/settings/fields/{field_id}',
741 744 controller='admin/repos', action='delete_repo_field',
742 745 conditions={'method': ['DELETE'], 'function': check_repo},
743 746 requirements=URL_NAME_REQUIREMENTS)
744 747
745 748 rmap.connect('edit_repo_advanced', '/{repo_name}/settings/advanced',
746 749 controller='admin/repos', action='edit_advanced',
747 750 conditions={'method': ['GET'], 'function': check_repo},
748 751 requirements=URL_NAME_REQUIREMENTS)
749 752
750 753 rmap.connect('edit_repo_advanced_locking', '/{repo_name}/settings/advanced/locking',
751 754 controller='admin/repos', action='edit_advanced_locking',
752 755 conditions={'method': ['PUT'], 'function': check_repo},
753 756 requirements=URL_NAME_REQUIREMENTS)
754 757 rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle',
755 758 controller='admin/repos', action='toggle_locking',
756 759 conditions={'method': ['GET'], 'function': check_repo},
757 760 requirements=URL_NAME_REQUIREMENTS)
758 761
759 762 rmap.connect('edit_repo_advanced_journal', '/{repo_name}/settings/advanced/journal',
760 763 controller='admin/repos', action='edit_advanced_journal',
761 764 conditions={'method': ['PUT'], 'function': check_repo},
762 765 requirements=URL_NAME_REQUIREMENTS)
763 766
764 767 rmap.connect('edit_repo_advanced_fork', '/{repo_name}/settings/advanced/fork',
765 768 controller='admin/repos', action='edit_advanced_fork',
766 769 conditions={'method': ['PUT'], 'function': check_repo},
767 770 requirements=URL_NAME_REQUIREMENTS)
768 771
769 772 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
770 773 controller='admin/repos', action='edit_caches_form',
771 774 conditions={'method': ['GET'], 'function': check_repo},
772 775 requirements=URL_NAME_REQUIREMENTS)
773 776 rmap.connect('edit_repo_caches', '/{repo_name}/settings/caches',
774 777 controller='admin/repos', action='edit_caches',
775 778 conditions={'method': ['PUT'], 'function': check_repo},
776 779 requirements=URL_NAME_REQUIREMENTS)
777 780
778 781 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
779 782 controller='admin/repos', action='edit_remote_form',
780 783 conditions={'method': ['GET'], 'function': check_repo},
781 784 requirements=URL_NAME_REQUIREMENTS)
782 785 rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote',
783 786 controller='admin/repos', action='edit_remote',
784 787 conditions={'method': ['PUT'], 'function': check_repo},
785 788 requirements=URL_NAME_REQUIREMENTS)
786 789
787 790 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
788 791 controller='admin/repos', action='edit_statistics_form',
789 792 conditions={'method': ['GET'], 'function': check_repo},
790 793 requirements=URL_NAME_REQUIREMENTS)
791 794 rmap.connect('edit_repo_statistics', '/{repo_name}/settings/statistics',
792 795 controller='admin/repos', action='edit_statistics',
793 796 conditions={'method': ['PUT'], 'function': check_repo},
794 797 requirements=URL_NAME_REQUIREMENTS)
795 798 rmap.connect('repo_settings_issuetracker',
796 799 '/{repo_name}/settings/issue-tracker',
797 800 controller='admin/repos', action='repo_issuetracker',
798 801 conditions={'method': ['GET'], 'function': check_repo},
799 802 requirements=URL_NAME_REQUIREMENTS)
800 803 rmap.connect('repo_issuetracker_test',
801 804 '/{repo_name}/settings/issue-tracker/test',
802 805 controller='admin/repos', action='repo_issuetracker_test',
803 806 conditions={'method': ['POST'], 'function': check_repo},
804 807 requirements=URL_NAME_REQUIREMENTS)
805 808 rmap.connect('repo_issuetracker_delete',
806 809 '/{repo_name}/settings/issue-tracker/delete',
807 810 controller='admin/repos', action='repo_issuetracker_delete',
808 811 conditions={'method': ['DELETE'], 'function': check_repo},
809 812 requirements=URL_NAME_REQUIREMENTS)
810 813 rmap.connect('repo_issuetracker_save',
811 814 '/{repo_name}/settings/issue-tracker/save',
812 815 controller='admin/repos', action='repo_issuetracker_save',
813 816 conditions={'method': ['POST'], 'function': check_repo},
814 817 requirements=URL_NAME_REQUIREMENTS)
815 818 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
816 819 controller='admin/repos', action='repo_settings_vcs_update',
817 820 conditions={'method': ['POST'], 'function': check_repo},
818 821 requirements=URL_NAME_REQUIREMENTS)
819 822 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
820 823 controller='admin/repos', action='repo_settings_vcs',
821 824 conditions={'method': ['GET'], 'function': check_repo},
822 825 requirements=URL_NAME_REQUIREMENTS)
823 826 rmap.connect('repo_vcs_settings', '/{repo_name}/settings/vcs',
824 827 controller='admin/repos', action='repo_delete_svn_pattern',
825 828 conditions={'method': ['DELETE'], 'function': check_repo},
826 829 requirements=URL_NAME_REQUIREMENTS)
830 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
831 controller='admin/repos', action='repo_settings_pullrequest',
832 conditions={'method': ['GET', 'POST'], 'function': check_repo},
833 requirements=URL_NAME_REQUIREMENTS)
827 834
828 835 # still working url for backward compat.
829 836 rmap.connect('raw_changeset_home_depraced',
830 837 '/{repo_name}/raw-changeset/{revision}',
831 838 controller='changeset', action='changeset_raw',
832 839 revision='tip', conditions={'function': check_repo},
833 840 requirements=URL_NAME_REQUIREMENTS)
834 841
835 842 # new URLs
836 843 rmap.connect('changeset_raw_home',
837 844 '/{repo_name}/changeset-diff/{revision}',
838 845 controller='changeset', action='changeset_raw',
839 846 revision='tip', conditions={'function': check_repo},
840 847 requirements=URL_NAME_REQUIREMENTS)
841 848
842 849 rmap.connect('changeset_patch_home',
843 850 '/{repo_name}/changeset-patch/{revision}',
844 851 controller='changeset', action='changeset_patch',
845 852 revision='tip', conditions={'function': check_repo},
846 853 requirements=URL_NAME_REQUIREMENTS)
847 854
848 855 rmap.connect('changeset_download_home',
849 856 '/{repo_name}/changeset-download/{revision}',
850 857 controller='changeset', action='changeset_download',
851 858 revision='tip', conditions={'function': check_repo},
852 859 requirements=URL_NAME_REQUIREMENTS)
853 860
854 861 rmap.connect('changeset_comment',
855 862 '/{repo_name}/changeset/{revision}/comment', jsroute=True,
856 863 controller='changeset', revision='tip', action='comment',
857 864 conditions={'function': check_repo},
858 865 requirements=URL_NAME_REQUIREMENTS)
859 866
860 867 rmap.connect('changeset_comment_preview',
861 868 '/{repo_name}/changeset/comment/preview', jsroute=True,
862 869 controller='changeset', action='preview_comment',
863 870 conditions={'function': check_repo, 'method': ['POST']},
864 871 requirements=URL_NAME_REQUIREMENTS)
865 872
866 873 rmap.connect('changeset_comment_delete',
867 874 '/{repo_name}/changeset/comment/{comment_id}/delete',
868 875 controller='changeset', action='delete_comment',
869 876 conditions={'function': check_repo, 'method': ['DELETE']},
870 877 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
871 878
872 879 rmap.connect('changeset_info', '/{repo_name}/changeset_info/{revision}',
873 880 controller='changeset', action='changeset_info',
874 881 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
875 882
876 883 rmap.connect('compare_home',
877 884 '/{repo_name}/compare',
878 885 controller='compare', action='index',
879 886 conditions={'function': check_repo},
880 887 requirements=URL_NAME_REQUIREMENTS)
881 888
882 889 rmap.connect('compare_url',
883 890 '/{repo_name}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}',
884 891 controller='compare', action='compare',
885 892 conditions={'function': check_repo},
886 893 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
887 894
888 895 rmap.connect('pullrequest_home',
889 896 '/{repo_name}/pull-request/new', controller='pullrequests',
890 897 action='index', conditions={'function': check_repo,
891 898 'method': ['GET']},
892 899 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
893 900
894 901 rmap.connect('pullrequest',
895 902 '/{repo_name}/pull-request/new', controller='pullrequests',
896 903 action='create', conditions={'function': check_repo,
897 904 'method': ['POST']},
898 905 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
899 906
900 907 rmap.connect('pullrequest_repo_refs',
901 908 '/{repo_name}/pull-request/refs/{target_repo_name:.*?[^/]}',
902 909 controller='pullrequests',
903 910 action='get_repo_refs',
904 911 conditions={'function': check_repo, 'method': ['GET']},
905 912 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
906 913
907 914 rmap.connect('pullrequest_repo_destinations',
908 915 '/{repo_name}/pull-request/repo-destinations',
909 916 controller='pullrequests',
910 917 action='get_repo_destinations',
911 918 conditions={'function': check_repo, 'method': ['GET']},
912 919 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
913 920
914 921 rmap.connect('pullrequest_show',
915 922 '/{repo_name}/pull-request/{pull_request_id}',
916 923 controller='pullrequests',
917 924 action='show', conditions={'function': check_repo,
918 925 'method': ['GET']},
919 926 requirements=URL_NAME_REQUIREMENTS)
920 927
921 928 rmap.connect('pullrequest_update',
922 929 '/{repo_name}/pull-request/{pull_request_id}',
923 930 controller='pullrequests',
924 931 action='update', conditions={'function': check_repo,
925 932 'method': ['PUT']},
926 933 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
927 934
928 935 rmap.connect('pullrequest_merge',
929 936 '/{repo_name}/pull-request/{pull_request_id}',
930 937 controller='pullrequests',
931 938 action='merge', conditions={'function': check_repo,
932 939 'method': ['POST']},
933 940 requirements=URL_NAME_REQUIREMENTS)
934 941
935 942 rmap.connect('pullrequest_delete',
936 943 '/{repo_name}/pull-request/{pull_request_id}',
937 944 controller='pullrequests',
938 945 action='delete', conditions={'function': check_repo,
939 946 'method': ['DELETE']},
940 947 requirements=URL_NAME_REQUIREMENTS)
941 948
942 949 rmap.connect('pullrequest_show_all',
943 950 '/{repo_name}/pull-request',
944 951 controller='pullrequests',
945 952 action='show_all', conditions={'function': check_repo,
946 953 'method': ['GET']},
947 954 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
948 955
949 956 rmap.connect('pullrequest_comment',
950 957 '/{repo_name}/pull-request-comment/{pull_request_id}',
951 958 controller='pullrequests',
952 959 action='comment', conditions={'function': check_repo,
953 960 'method': ['POST']},
954 961 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
955 962
956 963 rmap.connect('pullrequest_comment_delete',
957 964 '/{repo_name}/pull-request-comment/{comment_id}/delete',
958 965 controller='pullrequests', action='delete_comment',
959 966 conditions={'function': check_repo, 'method': ['DELETE']},
960 967 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
961 968
962 969 rmap.connect('summary_home_explicit', '/{repo_name}/summary',
963 970 controller='summary', conditions={'function': check_repo},
964 971 requirements=URL_NAME_REQUIREMENTS)
965 972
966 973 rmap.connect('branches_home', '/{repo_name}/branches',
967 974 controller='branches', conditions={'function': check_repo},
968 975 requirements=URL_NAME_REQUIREMENTS)
969 976
970 977 rmap.connect('tags_home', '/{repo_name}/tags',
971 978 controller='tags', conditions={'function': check_repo},
972 979 requirements=URL_NAME_REQUIREMENTS)
973 980
974 981 rmap.connect('bookmarks_home', '/{repo_name}/bookmarks',
975 982 controller='bookmarks', conditions={'function': check_repo},
976 983 requirements=URL_NAME_REQUIREMENTS)
977 984
978 985 rmap.connect('changelog_home', '/{repo_name}/changelog', jsroute=True,
979 986 controller='changelog', conditions={'function': check_repo},
980 987 requirements=URL_NAME_REQUIREMENTS)
981 988
982 989 rmap.connect('changelog_summary_home', '/{repo_name}/changelog_summary',
983 990 controller='changelog', action='changelog_summary',
984 991 conditions={'function': check_repo},
985 992 requirements=URL_NAME_REQUIREMENTS)
986 993
987 994 rmap.connect('changelog_file_home',
988 995 '/{repo_name}/changelog/{revision}/{f_path}',
989 996 controller='changelog', f_path=None,
990 997 conditions={'function': check_repo},
991 998 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
992 999
993 1000 rmap.connect('changelog_details', '/{repo_name}/changelog_details/{cs}',
994 1001 controller='changelog', action='changelog_details',
995 1002 conditions={'function': check_repo},
996 1003 requirements=URL_NAME_REQUIREMENTS)
997 1004
998 1005 rmap.connect('files_home', '/{repo_name}/files/{revision}/{f_path}',
999 1006 controller='files', revision='tip', f_path='',
1000 1007 conditions={'function': check_repo},
1001 1008 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1002 1009
1003 1010 rmap.connect('files_home_simple_catchrev',
1004 1011 '/{repo_name}/files/{revision}',
1005 1012 controller='files', revision='tip', f_path='',
1006 1013 conditions={'function': check_repo},
1007 1014 requirements=URL_NAME_REQUIREMENTS)
1008 1015
1009 1016 rmap.connect('files_home_simple_catchall',
1010 1017 '/{repo_name}/files',
1011 1018 controller='files', revision='tip', f_path='',
1012 1019 conditions={'function': check_repo},
1013 1020 requirements=URL_NAME_REQUIREMENTS)
1014 1021
1015 1022 rmap.connect('files_history_home',
1016 1023 '/{repo_name}/history/{revision}/{f_path}',
1017 1024 controller='files', action='history', revision='tip', f_path='',
1018 1025 conditions={'function': check_repo},
1019 1026 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1020 1027
1021 1028 rmap.connect('files_authors_home',
1022 1029 '/{repo_name}/authors/{revision}/{f_path}',
1023 1030 controller='files', action='authors', revision='tip', f_path='',
1024 1031 conditions={'function': check_repo},
1025 1032 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1026 1033
1027 1034 rmap.connect('files_diff_home', '/{repo_name}/diff/{f_path}',
1028 1035 controller='files', action='diff', f_path='',
1029 1036 conditions={'function': check_repo},
1030 1037 requirements=URL_NAME_REQUIREMENTS)
1031 1038
1032 1039 rmap.connect('files_diff_2way_home',
1033 1040 '/{repo_name}/diff-2way/{f_path}',
1034 1041 controller='files', action='diff_2way', f_path='',
1035 1042 conditions={'function': check_repo},
1036 1043 requirements=URL_NAME_REQUIREMENTS)
1037 1044
1038 1045 rmap.connect('files_rawfile_home',
1039 1046 '/{repo_name}/rawfile/{revision}/{f_path}',
1040 1047 controller='files', action='rawfile', revision='tip',
1041 1048 f_path='', conditions={'function': check_repo},
1042 1049 requirements=URL_NAME_REQUIREMENTS)
1043 1050
1044 1051 rmap.connect('files_raw_home',
1045 1052 '/{repo_name}/raw/{revision}/{f_path}',
1046 1053 controller='files', action='raw', revision='tip', f_path='',
1047 1054 conditions={'function': check_repo},
1048 1055 requirements=URL_NAME_REQUIREMENTS)
1049 1056
1050 1057 rmap.connect('files_render_home',
1051 1058 '/{repo_name}/render/{revision}/{f_path}',
1052 1059 controller='files', action='index', revision='tip', f_path='',
1053 1060 rendered=True, conditions={'function': check_repo},
1054 1061 requirements=URL_NAME_REQUIREMENTS)
1055 1062
1056 1063 rmap.connect('files_annotate_home',
1057 1064 '/{repo_name}/annotate/{revision}/{f_path}',
1058 1065 controller='files', action='index', revision='tip',
1059 1066 f_path='', annotate=True, conditions={'function': check_repo},
1060 1067 requirements=URL_NAME_REQUIREMENTS)
1061 1068
1062 1069 rmap.connect('files_edit',
1063 1070 '/{repo_name}/edit/{revision}/{f_path}',
1064 1071 controller='files', action='edit', revision='tip',
1065 1072 f_path='',
1066 1073 conditions={'function': check_repo, 'method': ['POST']},
1067 1074 requirements=URL_NAME_REQUIREMENTS)
1068 1075
1069 1076 rmap.connect('files_edit_home',
1070 1077 '/{repo_name}/edit/{revision}/{f_path}',
1071 1078 controller='files', action='edit_home', revision='tip',
1072 1079 f_path='', conditions={'function': check_repo},
1073 1080 requirements=URL_NAME_REQUIREMENTS)
1074 1081
1075 1082 rmap.connect('files_add',
1076 1083 '/{repo_name}/add/{revision}/{f_path}',
1077 1084 controller='files', action='add', revision='tip',
1078 1085 f_path='',
1079 1086 conditions={'function': check_repo, 'method': ['POST']},
1080 1087 requirements=URL_NAME_REQUIREMENTS)
1081 1088
1082 1089 rmap.connect('files_add_home',
1083 1090 '/{repo_name}/add/{revision}/{f_path}',
1084 1091 controller='files', action='add_home', revision='tip',
1085 1092 f_path='', conditions={'function': check_repo},
1086 1093 requirements=URL_NAME_REQUIREMENTS)
1087 1094
1088 1095 rmap.connect('files_delete',
1089 1096 '/{repo_name}/delete/{revision}/{f_path}',
1090 1097 controller='files', action='delete', revision='tip',
1091 1098 f_path='',
1092 1099 conditions={'function': check_repo, 'method': ['POST']},
1093 1100 requirements=URL_NAME_REQUIREMENTS)
1094 1101
1095 1102 rmap.connect('files_delete_home',
1096 1103 '/{repo_name}/delete/{revision}/{f_path}',
1097 1104 controller='files', action='delete_home', revision='tip',
1098 1105 f_path='', conditions={'function': check_repo},
1099 1106 requirements=URL_NAME_REQUIREMENTS)
1100 1107
1101 1108 rmap.connect('files_archive_home', '/{repo_name}/archive/{fname}',
1102 1109 controller='files', action='archivefile',
1103 1110 conditions={'function': check_repo},
1104 1111 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1105 1112
1106 1113 rmap.connect('files_nodelist_home',
1107 1114 '/{repo_name}/nodelist/{revision}/{f_path}',
1108 1115 controller='files', action='nodelist',
1109 1116 conditions={'function': check_repo},
1110 1117 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1111 1118
1112 1119 rmap.connect('files_nodetree_full',
1113 1120 '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
1114 1121 controller='files', action='nodetree_full',
1115 1122 conditions={'function': check_repo},
1116 1123 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
1117 1124
1118 1125 rmap.connect('repo_fork_create_home', '/{repo_name}/fork',
1119 1126 controller='forks', action='fork_create',
1120 1127 conditions={'function': check_repo, 'method': ['POST']},
1121 1128 requirements=URL_NAME_REQUIREMENTS)
1122 1129
1123 1130 rmap.connect('repo_fork_home', '/{repo_name}/fork',
1124 1131 controller='forks', action='fork',
1125 1132 conditions={'function': check_repo},
1126 1133 requirements=URL_NAME_REQUIREMENTS)
1127 1134
1128 1135 rmap.connect('repo_forks_home', '/{repo_name}/forks',
1129 1136 controller='forks', action='forks',
1130 1137 conditions={'function': check_repo},
1131 1138 requirements=URL_NAME_REQUIREMENTS)
1132 1139
1133 1140 rmap.connect('repo_followers_home', '/{repo_name}/followers',
1134 1141 controller='followers', action='followers',
1135 1142 conditions={'function': check_repo},
1136 1143 requirements=URL_NAME_REQUIREMENTS)
1137 1144
1138 1145 # must be here for proper group/repo catching pattern
1139 1146 _connect_with_slash(
1140 1147 rmap, 'repo_group_home', '/{group_name}',
1141 1148 controller='home', action='index_repo_group',
1142 1149 conditions={'function': check_group},
1143 1150 requirements=URL_NAME_REQUIREMENTS)
1144 1151
1145 1152 # catch all, at the end
1146 1153 _connect_with_slash(
1147 1154 rmap, 'summary_home', '/{repo_name}', jsroute=True,
1148 1155 controller='summary', action='index',
1149 1156 conditions={'function': check_repo},
1150 1157 requirements=URL_NAME_REQUIREMENTS)
1151 1158
1152 1159 return rmap
1153 1160
1154 1161
1155 1162 def _connect_with_slash(mapper, name, path, *args, **kwargs):
1156 1163 """
1157 1164 Connect a route with an optional trailing slash in `path`.
1158 1165 """
1159 1166 mapper.connect(name + '_slash', path + '/', *args, **kwargs)
1160 1167 mapper.connect(name, path, *args, **kwargs)
@@ -1,289 +1,288 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 Home controller for RhodeCode Enterprise
23 23 """
24 24
25 25 import logging
26 26 import time
27 27 import re
28 28
29 29 from pylons import tmpl_context as c, request, url, config
30 30 from pylons.i18n.translation import _
31 31 from sqlalchemy.sql import func
32 32
33 33 from rhodecode.lib.auth import (
34 34 LoginRequired, HasPermissionAllDecorator, AuthUser,
35 35 HasRepoGroupPermissionAnyDecorator, XHRRequired)
36 36 from rhodecode.lib.base import BaseController, render
37 37 from rhodecode.lib.index import searcher_from_config
38 38 from rhodecode.lib.ext_json import json
39 39 from rhodecode.lib.utils import jsonify
40 40 from rhodecode.lib.utils2 import safe_unicode, str2bool
41 41 from rhodecode.model.db import Repository, RepoGroup
42 42 from rhodecode.model.repo import RepoModel
43 43 from rhodecode.model.repo_group import RepoGroupModel
44 44 from rhodecode.model.scm import RepoList, RepoGroupList
45 45
46 46
47 47 log = logging.getLogger(__name__)
48 48
49 49
50 50 class HomeController(BaseController):
51 51 def __before__(self):
52 52 super(HomeController, self).__before__()
53 53
54 54 def ping(self):
55 55 """
56 56 Ping, doesn't require login, good for checking out the platform
57 57 """
58 58 instance_id = getattr(c, 'rhodecode_instanceid', '')
59 59 return 'pong[%s] => %s' % (instance_id, self.ip_addr,)
60 60
61 61 @LoginRequired()
62 62 @HasPermissionAllDecorator('hg.admin')
63 63 def error_test(self):
64 64 """
65 65 Test exception handling and emails on errors
66 66 """
67 67 class TestException(Exception):
68 68 pass
69 69
70 70 msg = ('RhodeCode Enterprise %s test exception. Generation time: %s'
71 71 % (c.rhodecode_name, time.time()))
72 72 raise TestException(msg)
73 73
74 74 def _get_groups_and_repos(self, repo_group_id=None):
75 75 # repo groups groups
76 76 repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id)
77 77 _perms = ['group.read', 'group.write', 'group.admin']
78 78 repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms)
79 79 repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
80 80 repo_group_list=repo_group_list_acl, admin=False)
81 81
82 82 # repositories
83 83 repo_list = Repository.get_all_repos(group_id=repo_group_id)
84 84 _perms = ['repository.read', 'repository.write', 'repository.admin']
85 85 repo_list_acl = RepoList(repo_list, perm_set=_perms)
86 86 repo_data = RepoModel().get_repos_as_dict(
87 87 repo_list=repo_list_acl, admin=False)
88 88
89 89 return repo_data, repo_group_data
90 90
91 91 @LoginRequired()
92 92 def index(self):
93 93 c.repo_group = None
94 94
95 95 repo_data, repo_group_data = self._get_groups_and_repos()
96 96 # json used to render the grids
97 97 c.repos_data = json.dumps(repo_data)
98 98 c.repo_groups_data = json.dumps(repo_group_data)
99 99
100 100 return render('/index.html')
101 101
102 102 @LoginRequired()
103 103 @HasRepoGroupPermissionAnyDecorator('group.read', 'group.write',
104 104 'group.admin')
105 105 def index_repo_group(self, group_name):
106 106 """GET /repo_group_name: Show a specific item"""
107 107 c.repo_group = RepoGroupModel()._get_repo_group(group_name)
108 108 repo_data, repo_group_data = self._get_groups_and_repos(
109 109 c.repo_group.group_id)
110 110
111 111 # json used to render the grids
112 112 c.repos_data = json.dumps(repo_data)
113 113 c.repo_groups_data = json.dumps(repo_group_data)
114 114
115 115 return render('index_repo_group.html')
116 116
117 117 def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
118 118 query = Repository.query()\
119 119 .order_by(func.length(Repository.repo_name))\
120 120 .order_by(Repository.repo_name)
121 121
122 122 if repo_type:
123 123 query = query.filter(Repository.repo_type == repo_type)
124 124
125 125 if name_contains:
126 126 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
127 127 query = query.filter(
128 128 Repository.repo_name.ilike(ilike_expression))
129 129 query = query.limit(limit)
130 130
131 131 all_repos = query.all()
132 132 repo_iter = self.scm_model.get_repos(all_repos)
133 133 return [
134 134 {
135 135 'id': obj['name'],
136 136 'text': obj['name'],
137 137 'type': 'repo',
138 138 'obj': obj['dbrepo'],
139 139 'url': url('summary_home', repo_name=obj['name'])
140 140 }
141 141 for obj in repo_iter]
142 142
143 143 def _get_repo_group_list(self, name_contains=None, limit=20):
144 144 query = RepoGroup.query()\
145 145 .order_by(func.length(RepoGroup.group_name))\
146 146 .order_by(RepoGroup.group_name)
147 147
148 148 if name_contains:
149 149 ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
150 150 query = query.filter(
151 151 RepoGroup.group_name.ilike(ilike_expression))
152 152 query = query.limit(limit)
153 153
154 154 all_groups = query.all()
155 155 repo_groups_iter = self.scm_model.get_repo_groups(all_groups)
156 156 return [
157 157 {
158 158 'id': obj.group_name,
159 159 'text': obj.group_name,
160 160 'type': 'group',
161 161 'obj': {},
162 162 'url': url('repo_group_home', group_name=obj.group_name)
163 163 }
164 164 for obj in repo_groups_iter]
165 165
166 166 def _get_hash_commit_list(self, hash_starts_with=None, limit=20):
167 167 if not hash_starts_with or len(hash_starts_with) < 3:
168 168 return []
169 169
170 170 commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with)
171 171
172 172 if len(commit_hashes) != 1:
173 173 return []
174 174
175 175 commit_hash_prefix = commit_hashes[0]
176 176
177 177 auth_user = AuthUser(
178 178 user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr)
179 179 searcher = searcher_from_config(config)
180 180 result = searcher.search(
181 181 'commit_id:%s*' % commit_hash_prefix, 'commit', auth_user)
182 182
183 183 return [
184 184 {
185 185 'id': entry['commit_id'],
186 186 'text': entry['commit_id'],
187 187 'type': 'commit',
188 188 'obj': {'repo': entry['repository']},
189 189 'url': url('changeset_home',
190 190 repo_name=entry['repository'], revision=entry['commit_id'])
191 191 }
192 192 for entry in result['results']]
193 193
194 194 @LoginRequired()
195 195 @XHRRequired()
196 196 @jsonify
197 197 def goto_switcher_data(self):
198 198 query = request.GET.get('query')
199 199 log.debug('generating goto switcher list, query %s', query)
200 200
201 201 res = []
202 202 repo_groups = self._get_repo_group_list(query)
203 203 if repo_groups:
204 204 res.append({
205 205 'text': _('Groups'),
206 206 'children': repo_groups
207 207 })
208 208
209 209 repos = self._get_repo_list(query)
210 210 if repos:
211 211 res.append({
212 212 'text': _('Repositories'),
213 213 'children': repos
214 214 })
215 215
216 216 commits = self._get_hash_commit_list(query)
217 217 if commits:
218 218 unique_repos = {}
219 219 for commit in commits:
220 220 unique_repos.setdefault(commit['obj']['repo'], []
221 221 ).append(commit)
222 222
223 223 for repo in unique_repos:
224 224 res.append({
225 225 'text': _('Commits in %(repo)s') % {'repo': repo},
226 226 'children': unique_repos[repo]
227 227 })
228 228
229 229 data = {
230 230 'more': False,
231 231 'results': res
232 232 }
233 233 return data
234 234
235 235 @LoginRequired()
236 236 @XHRRequired()
237 237 @jsonify
238 238 def repo_list_data(self):
239 239 query = request.GET.get('query')
240 240 repo_type = request.GET.get('repo_type')
241 241 log.debug('generating repo list, query:%s', query)
242 242
243 243 res = []
244 244 repos = self._get_repo_list(query, repo_type=repo_type)
245 245 if repos:
246 246 res.append({
247 247 'text': _('Repositories'),
248 248 'children': repos
249 249 })
250 250
251 251 data = {
252 252 'more': False,
253 253 'results': res
254 254 }
255 255 return data
256 256
257 257 @LoginRequired()
258 258 @XHRRequired()
259 259 @jsonify
260 260 def user_autocomplete_data(self):
261 261 query = request.GET.get('query')
262 262 active = str2bool(request.GET.get('active') or True)
263 263
264 264 repo_model = RepoModel()
265 265 _users = repo_model.get_users(
266 266 name_contains=query, only_active=active)
267 267
268 268 if request.GET.get('user_groups'):
269 269 # extend with user groups
270 270 _user_groups = repo_model.get_user_groups(
271 271 name_contains=query, only_active=active)
272 272 _users = _users + _user_groups
273 273
274 274 return {'suggestions': _users}
275 275
276 276 @LoginRequired()
277 277 @XHRRequired()
278 278 @jsonify
279 279 def user_group_autocomplete_data(self):
280 280 query = request.GET.get('query')
281 281 active = str2bool(request.GET.get('active') or True)
282 282
283 283 repo_model = RepoModel()
284 284 _user_groups = repo_model.get_user_groups(
285 285 name_contains=query, only_active=active)
286 286 _user_groups = _user_groups
287 287
288 288 return {'suggestions': _user_groups}
289
@@ -1,308 +1,318 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 Summary controller for RhodeCode Enterprise
23 23 """
24 24
25 25 import logging
26 26 from string import lower
27 27
28 28 from pylons import tmpl_context as c, request
29 29 from pylons.i18n.translation import _
30 30 from beaker.cache import cache_region, region_invalidate
31 31
32 32 from rhodecode.config.conf import (LANGUAGES_EXTENSIONS_MAP)
33 33 from rhodecode.controllers import utils
34 34 from rhodecode.controllers.changelog import _load_changelog_summary
35 35 from rhodecode.lib import caches, helpers as h
36 36 from rhodecode.lib.utils import jsonify
37 37 from rhodecode.lib.utils2 import safe_str
38 38 from rhodecode.lib.auth import (
39 39 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, XHRRequired)
40 40 from rhodecode.lib.base import BaseRepoController, render
41 41 from rhodecode.lib.markup_renderer import MarkupRenderer
42 42 from rhodecode.lib.ext_json import json
43 43 from rhodecode.lib.vcs.backends.base import EmptyCommit
44 44 from rhodecode.lib.vcs.exceptions import (
45 45 CommitError, EmptyRepositoryError, NodeDoesNotExistError)
46 46 from rhodecode.model.db import Statistics, CacheKey, User
47 47 from rhodecode.model.repo import ReadmeFinder
48 48
49 49
50 50 log = logging.getLogger(__name__)
51 51
52 52
53 53 class SummaryController(BaseRepoController):
54 54
55 55 def __before__(self):
56 56 super(SummaryController, self).__before__()
57 57
58 58 def __get_readme_data(self, db_repo):
59 59 repo_name = db_repo.repo_name
60 60 log.debug('Looking for README file')
61 61 default_renderer = c.visual.default_renderer
62 62
63 63 @cache_region('long_term')
64 64 def _generate_readme(cache_key):
65 65 readme_data = None
66 66 readme_node = None
67 67 readme_filename = None
68 68 commit = self._get_landing_commit_or_none(db_repo)
69 69 if commit:
70 70 log.debug("Searching for a README file.")
71 71 readme_node = ReadmeFinder(default_renderer).search(commit)
72 72 if readme_node:
73 73 readme_data = self._render_readme_or_none(commit, readme_node)
74 74 readme_filename = readme_node.path
75 75 return readme_data, readme_filename
76 76
77 77 invalidator_context = CacheKey.repo_context_cache(
78 78 _generate_readme, repo_name, CacheKey.CACHE_TYPE_README)
79 79
80 80 with invalidator_context as context:
81 81 context.invalidate()
82 82 computed = context.compute()
83 83
84 84 return computed
85 85
86 86 def _get_landing_commit_or_none(self, db_repo):
87 87 log.debug("Getting the landing commit.")
88 88 try:
89 89 commit = db_repo.get_landing_commit()
90 90 if not isinstance(commit, EmptyCommit):
91 91 return commit
92 92 else:
93 93 log.debug("Repository is empty, no README to render.")
94 94 except CommitError:
95 95 log.exception(
96 96 "Problem getting commit when trying to render the README.")
97 97
98 98 def _render_readme_or_none(self, commit, readme_node):
99 99 log.debug(
100 100 'Found README file `%s` rendering...', readme_node.path)
101 101 renderer = MarkupRenderer()
102 102 try:
103 103 return renderer.render(
104 104 readme_node.content, filename=readme_node.path)
105 105 except Exception:
106 106 log.exception(
107 107 "Exception while trying to render the README")
108 108
109 109 @LoginRequired()
110 110 @HasRepoPermissionAnyDecorator(
111 111 'repository.read', 'repository.write', 'repository.admin')
112 112 def index(self, repo_name):
113 113
114 114 # Prepare the clone URL
115 115
116 116 username = ''
117 117 if c.rhodecode_user.username != User.DEFAULT_USER:
118 118 username = safe_str(c.rhodecode_user.username)
119 119
120 120 _def_clone_uri = _def_clone_uri_by_id = c.clone_uri_tmpl
121 121 if '{repo}' in _def_clone_uri:
122 122 _def_clone_uri_by_id = _def_clone_uri.replace(
123 123 '{repo}', '_{repoid}')
124 124 elif '{repoid}' in _def_clone_uri:
125 125 _def_clone_uri_by_id = _def_clone_uri.replace(
126 126 '_{repoid}', '{repo}')
127 127
128 128 c.clone_repo_url = c.rhodecode_db_repo.clone_url(
129 129 user=username, uri_tmpl=_def_clone_uri)
130 130 c.clone_repo_url_id = c.rhodecode_db_repo.clone_url(
131 131 user=username, uri_tmpl=_def_clone_uri_by_id)
132 132
133 133 # If enabled, get statistics data
134 134
135 135 c.show_stats = bool(c.rhodecode_db_repo.enable_statistics)
136 136
137 137 stats = self.sa.query(Statistics)\
138 138 .filter(Statistics.repository == c.rhodecode_db_repo)\
139 139 .scalar()
140 140
141 141 c.stats_percentage = 0
142 142
143 143 if stats and stats.languages:
144 144 c.no_data = False is c.rhodecode_db_repo.enable_statistics
145 145 lang_stats_d = json.loads(stats.languages)
146 146
147 147 # Sort first by decreasing count and second by the file extension,
148 148 # so we have a consistent output.
149 149 lang_stats_items = sorted(lang_stats_d.iteritems(),
150 150 key=lambda k: (-k[1], k[0]))[:10]
151 151 lang_stats = [(x, {"count": y,
152 152 "desc": LANGUAGES_EXTENSIONS_MAP.get(x)})
153 153 for x, y in lang_stats_items]
154 154
155 155 c.trending_languages = json.dumps(lang_stats)
156 156 else:
157 157 c.no_data = True
158 158 c.trending_languages = json.dumps({})
159 159
160 160 c.enable_downloads = c.rhodecode_db_repo.enable_downloads
161 161 c.repository_followers = self.scm_model.get_followers(
162 162 c.rhodecode_db_repo)
163 163 c.repository_forks = self.scm_model.get_forks(c.rhodecode_db_repo)
164 164 c.repository_is_user_following = self.scm_model.is_following_repo(
165 165 c.repo_name, c.rhodecode_user.user_id)
166 166
167 167 if c.repository_requirements_missing:
168 168 return render('summary/missing_requirements.html')
169 169
170 170 c.readme_data, c.readme_file = \
171 171 self.__get_readme_data(c.rhodecode_db_repo)
172 172
173 173 _load_changelog_summary()
174 174
175 175 if request.is_xhr:
176 176 return render('changelog/changelog_summary_data.html')
177 177
178 178 return render('summary/summary.html')
179 179
180 180 @LoginRequired()
181 181 @XHRRequired()
182 182 @HasRepoPermissionAnyDecorator(
183 183 'repository.read', 'repository.write', 'repository.admin')
184 184 @jsonify
185 185 def repo_stats(self, repo_name, commit_id):
186 186 _namespace = caches.get_repo_namespace_key(
187 187 caches.SUMMARY_STATS, repo_name)
188 188 show_stats = bool(c.rhodecode_db_repo.enable_statistics)
189 189 cache_manager = caches.get_cache_manager('repo_cache_long', _namespace)
190 190 _cache_key = caches.compute_key_from_params(
191 191 repo_name, commit_id, show_stats)
192 192
193 193 def compute_stats():
194 194 code_stats = {}
195 195 size = 0
196 196 try:
197 197 scm_instance = c.rhodecode_db_repo.scm_instance()
198 198 commit = scm_instance.get_commit(commit_id)
199 199
200 200 for node in commit.get_filenodes_generator():
201 201 size += node.size
202 202 if not show_stats:
203 203 continue
204 204 ext = lower(node.extension)
205 205 ext_info = LANGUAGES_EXTENSIONS_MAP.get(ext)
206 206 if ext_info:
207 207 if ext in code_stats:
208 208 code_stats[ext]['count'] += 1
209 209 else:
210 210 code_stats[ext] = {"count": 1, "desc": ext_info}
211 211 except EmptyRepositoryError:
212 212 pass
213 213 return {'size': h.format_byte_size_binary(size),
214 214 'code_stats': code_stats}
215 215
216 216 stats = cache_manager.get(_cache_key, createfunc=compute_stats)
217 217 return stats
218 218
219 219 def _switcher_reference_data(self, repo_name, references, is_svn):
220 220 """Prepare reference data for given `references`"""
221 221 items = []
222 222 for name, commit_id in references.items():
223 223 use_commit_id = '/' in name or is_svn
224 224 items.append({
225 225 'name': name,
226 226 'commit_id': commit_id,
227 227 'files_url': h.url(
228 228 'files_home',
229 229 repo_name=repo_name,
230 230 f_path=name if is_svn else '',
231 231 revision=commit_id if use_commit_id else name,
232 232 at=name)
233 233 })
234 234 return items
235 235
236 236 @LoginRequired()
237 237 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
238 238 'repository.admin')
239 239 @jsonify
240 240 def repo_refs_data(self, repo_name):
241 241 repo = c.rhodecode_repo
242 242 refs_to_create = [
243 243 (_("Branch"), repo.branches, 'branch'),
244 244 (_("Tag"), repo.tags, 'tag'),
245 245 (_("Bookmark"), repo.bookmarks, 'book'),
246 246 ]
247 247 res = self._create_reference_data(repo, repo_name, refs_to_create)
248 248 data = {
249 249 'more': False,
250 250 'results': res
251 251 }
252 252 return data
253 253
254 @LoginRequired()
255 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
256 'repository.admin')
257 @jsonify
258 def repo_default_reviewers_data(self, repo_name):
259 return {
260 'reviewers': [utils.reviewer_as_json(
261 user=c.rhodecode_db_repo.user, reasons=None)]
262 }
263
254 264 @jsonify
255 265 def repo_refs_changelog_data(self, repo_name):
256 266 repo = c.rhodecode_repo
257 267
258 268 refs_to_create = [
259 269 (_("Branches"), repo.branches, 'branch'),
260 270 (_("Closed branches"), repo.branches_closed, 'branch_closed'),
261 271 # TODO: enable when vcs can handle bookmarks filters
262 272 # (_("Bookmarks"), repo.bookmarks, "book"),
263 273 ]
264 274 res = self._create_reference_data(repo, repo_name, refs_to_create)
265 275 data = {
266 276 'more': False,
267 277 'results': res
268 278 }
269 279 return data
270 280
271 281 def _create_reference_data(self, repo, full_repo_name, refs_to_create):
272 282 format_ref_id = utils.get_format_ref_id(repo)
273 283
274 284 result = []
275 285 for title, refs, ref_type in refs_to_create:
276 286 if refs:
277 287 result.append({
278 288 'text': title,
279 289 'children': self._create_reference_items(
280 290 repo, full_repo_name, refs, ref_type, format_ref_id),
281 291 })
282 292 return result
283 293
284 294 def _create_reference_items(self, repo, full_repo_name, refs, ref_type,
285 295 format_ref_id):
286 296 result = []
287 297 is_svn = h.is_svn(repo)
288 298 for ref_name, raw_id in refs.iteritems():
289 299 files_url = self._create_files_url(
290 300 repo, full_repo_name, ref_name, raw_id, is_svn)
291 301 result.append({
292 302 'text': ref_name,
293 303 'id': format_ref_id(ref_name, raw_id),
294 304 'raw_id': raw_id,
295 305 'type': ref_type,
296 306 'files_url': files_url,
297 307 })
298 308 return result
299 309
300 310 def _create_files_url(self, repo, full_repo_name, ref_name, raw_id,
301 311 is_svn):
302 312 use_commit_id = '/' in ref_name or is_svn
303 313 return h.url(
304 314 'files_home',
305 315 repo_name=full_repo_name,
306 316 f_path=ref_name if is_svn else '',
307 317 revision=raw_id if use_commit_id else ref_name,
308 318 at=ref_name)
@@ -1,88 +1,106 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 Utilities to be shared by multiple controllers.
23 23
24 24 Should only contain utilities to be shared in the controller layer.
25 25 """
26 26
27 27 from rhodecode.lib import helpers as h
28 28 from rhodecode.lib.vcs.exceptions import RepositoryError
29 29
30 30 def parse_path_ref(ref, default_path=None):
31 31 """
32 32 Parse out a path and reference combination and return both parts of it.
33 33
34 34 This is used to allow support of path based comparisons for Subversion
35 35 as an iterim solution in parameter handling.
36 36 """
37 37 if '@' in ref:
38 38 return ref.rsplit('@', 1)
39 39 else:
40 40 return default_path, ref
41 41
42 42
43 43 def get_format_ref_id(repo):
44 44 """Returns a `repo` specific reference formatter function"""
45 45 if h.is_svn(repo):
46 46 return _format_ref_id_svn
47 47 else:
48 48 return _format_ref_id
49 49
50 50
51 51 def _format_ref_id(name, raw_id):
52 52 """Default formatting of a given reference `name`"""
53 53 return name
54 54
55 55
56 56 def _format_ref_id_svn(name, raw_id):
57 57 """Special way of formatting a reference for Subversion including path"""
58 58 return '%s@%s' % (name, raw_id)
59 59
60 60
61 61 def get_commit_from_ref_name(repo, ref_name, ref_type=None):
62 62 """
63 63 Gets the commit for a `ref_name` taking into account `ref_type`.
64 64 Needed in case a bookmark / tag share the same name.
65 65
66 66 :param repo: the repo instance
67 67 :param ref_name: the name of the ref to get
68 68 :param ref_type: optional, used to disambiguate colliding refs
69 69 """
70 70 repo_scm = repo.scm_instance()
71 71 ref_type_mapping = {
72 72 'book': repo_scm.bookmarks,
73 73 'bookmark': repo_scm.bookmarks,
74 74 'tag': repo_scm.tags,
75 75 'branch': repo_scm.branches,
76 76 }
77 77
78 78 commit_id = ref_name
79 79 if repo_scm.alias != 'svn': # pass svn refs straight to backend until
80 80 # the branch issue with svn is fixed
81 81 if ref_type and ref_type in ref_type_mapping:
82 82 try:
83 83 commit_id = ref_type_mapping[ref_type][ref_name]
84 84 except KeyError:
85 85 raise RepositoryError(
86 86 '%s "%s" does not exist' % (ref_type, ref_name))
87 87
88 88 return repo_scm.get_commit(commit_id)
89
90
91 def reviewer_as_json(user, reasons):
92 """
93 Returns json struct of a reviewer for frontend
94
95 :param user: the reviewer
96 :param reasons: list of strings of why they are reviewers
97 """
98
99 return {
100 'user_id': user.user_id,
101 'reasons': reasons,
102 'username': user.username,
103 'firstname': user.firstname,
104 'lastname': user.lastname,
105 'gravatar_link': h.gravatar_url(user.email, 14),
106 }
@@ -1,895 +1,936 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 Some simple helper functions
24 24 """
25 25
26 26
27 27 import collections
28 28 import datetime
29 29 import dateutil.relativedelta
30 30 import hashlib
31 31 import logging
32 32 import re
33 33 import sys
34 34 import time
35 35 import threading
36 36 import urllib
37 37 import urlobject
38 38 import uuid
39 39
40 40 import pygments.lexers
41 41 import sqlalchemy
42 42 import sqlalchemy.engine.url
43 43 import webob
44 44 import routes.util
45 45
46 46 import rhodecode
47 47
48 48
49 49 def md5(s):
50 50 return hashlib.md5(s).hexdigest()
51 51
52 52
53 53 def md5_safe(s):
54 54 return md5(safe_str(s))
55 55
56 56
57 57 def __get_lem(extra_mapping=None):
58 58 """
59 59 Get language extension map based on what's inside pygments lexers
60 60 """
61 61 d = collections.defaultdict(lambda: [])
62 62
63 63 def __clean(s):
64 64 s = s.lstrip('*')
65 65 s = s.lstrip('.')
66 66
67 67 if s.find('[') != -1:
68 68 exts = []
69 69 start, stop = s.find('['), s.find(']')
70 70
71 71 for suffix in s[start + 1:stop]:
72 72 exts.append(s[:s.find('[')] + suffix)
73 73 return [e.lower() for e in exts]
74 74 else:
75 75 return [s.lower()]
76 76
77 77 for lx, t in sorted(pygments.lexers.LEXERS.items()):
78 78 m = map(__clean, t[-2])
79 79 if m:
80 80 m = reduce(lambda x, y: x + y, m)
81 81 for ext in m:
82 82 desc = lx.replace('Lexer', '')
83 83 d[ext].append(desc)
84 84
85 85 data = dict(d)
86 86
87 87 extra_mapping = extra_mapping or {}
88 88 if extra_mapping:
89 89 for k, v in extra_mapping.items():
90 90 if k not in data:
91 91 # register new mapping2lexer
92 92 data[k] = [v]
93 93
94 94 return data
95 95
96 96
97 97 def str2bool(_str):
98 98 """
99 99 returs True/False value from given string, it tries to translate the
100 100 string into boolean
101 101
102 102 :param _str: string value to translate into boolean
103 103 :rtype: boolean
104 104 :returns: boolean from given string
105 105 """
106 106 if _str is None:
107 107 return False
108 108 if _str in (True, False):
109 109 return _str
110 110 _str = str(_str).strip().lower()
111 111 return _str in ('t', 'true', 'y', 'yes', 'on', '1')
112 112
113 113
114 114 def aslist(obj, sep=None, strip=True):
115 115 """
116 116 Returns given string separated by sep as list
117 117
118 118 :param obj:
119 119 :param sep:
120 120 :param strip:
121 121 """
122 122 if isinstance(obj, (basestring,)):
123 123 lst = obj.split(sep)
124 124 if strip:
125 125 lst = [v.strip() for v in lst]
126 126 return lst
127 127 elif isinstance(obj, (list, tuple)):
128 128 return obj
129 129 elif obj is None:
130 130 return []
131 131 else:
132 132 return [obj]
133 133
134 134
135 135 def convert_line_endings(line, mode):
136 136 """
137 137 Converts a given line "line end" accordingly to given mode
138 138
139 139 Available modes are::
140 140 0 - Unix
141 141 1 - Mac
142 142 2 - DOS
143 143
144 144 :param line: given line to convert
145 145 :param mode: mode to convert to
146 146 :rtype: str
147 147 :return: converted line according to mode
148 148 """
149 149 if mode == 0:
150 150 line = line.replace('\r\n', '\n')
151 151 line = line.replace('\r', '\n')
152 152 elif mode == 1:
153 153 line = line.replace('\r\n', '\r')
154 154 line = line.replace('\n', '\r')
155 155 elif mode == 2:
156 156 line = re.sub('\r(?!\n)|(?<!\r)\n', '\r\n', line)
157 157 return line
158 158
159 159
160 160 def detect_mode(line, default):
161 161 """
162 162 Detects line break for given line, if line break couldn't be found
163 163 given default value is returned
164 164
165 165 :param line: str line
166 166 :param default: default
167 167 :rtype: int
168 168 :return: value of line end on of 0 - Unix, 1 - Mac, 2 - DOS
169 169 """
170 170 if line.endswith('\r\n'):
171 171 return 2
172 172 elif line.endswith('\n'):
173 173 return 0
174 174 elif line.endswith('\r'):
175 175 return 1
176 176 else:
177 177 return default
178 178
179 179
180 180 def safe_int(val, default=None):
181 181 """
182 182 Returns int() of val if val is not convertable to int use default
183 183 instead
184 184
185 185 :param val:
186 186 :param default:
187 187 """
188 188
189 189 try:
190 190 val = int(val)
191 191 except (ValueError, TypeError):
192 192 val = default
193 193
194 194 return val
195 195
196 196
197 197 def safe_unicode(str_, from_encoding=None):
198 198 """
199 199 safe unicode function. Does few trick to turn str_ into unicode
200 200
201 201 In case of UnicodeDecode error, we try to return it with encoding detected
202 202 by chardet library if it fails fallback to unicode with errors replaced
203 203
204 204 :param str_: string to decode
205 205 :rtype: unicode
206 206 :returns: unicode object
207 207 """
208 208 if isinstance(str_, unicode):
209 209 return str_
210 210
211 211 if not from_encoding:
212 212 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
213 213 'utf8'), sep=',')
214 214 from_encoding = DEFAULT_ENCODINGS
215 215
216 216 if not isinstance(from_encoding, (list, tuple)):
217 217 from_encoding = [from_encoding]
218 218
219 219 try:
220 220 return unicode(str_)
221 221 except UnicodeDecodeError:
222 222 pass
223 223
224 224 for enc in from_encoding:
225 225 try:
226 226 return unicode(str_, enc)
227 227 except UnicodeDecodeError:
228 228 pass
229 229
230 230 try:
231 231 import chardet
232 232 encoding = chardet.detect(str_)['encoding']
233 233 if encoding is None:
234 234 raise Exception()
235 235 return str_.decode(encoding)
236 236 except (ImportError, UnicodeDecodeError, Exception):
237 237 return unicode(str_, from_encoding[0], 'replace')
238 238
239 239
240 240 def safe_str(unicode_, to_encoding=None):
241 241 """
242 242 safe str function. Does few trick to turn unicode_ into string
243 243
244 244 In case of UnicodeEncodeError, we try to return it with encoding detected
245 245 by chardet library if it fails fallback to string with errors replaced
246 246
247 247 :param unicode_: unicode to encode
248 248 :rtype: str
249 249 :returns: str object
250 250 """
251 251
252 252 # if it's not basestr cast to str
253 253 if not isinstance(unicode_, basestring):
254 254 return str(unicode_)
255 255
256 256 if isinstance(unicode_, str):
257 257 return unicode_
258 258
259 259 if not to_encoding:
260 260 DEFAULT_ENCODINGS = aslist(rhodecode.CONFIG.get('default_encoding',
261 261 'utf8'), sep=',')
262 262 to_encoding = DEFAULT_ENCODINGS
263 263
264 264 if not isinstance(to_encoding, (list, tuple)):
265 265 to_encoding = [to_encoding]
266 266
267 267 for enc in to_encoding:
268 268 try:
269 269 return unicode_.encode(enc)
270 270 except UnicodeEncodeError:
271 271 pass
272 272
273 273 try:
274 274 import chardet
275 275 encoding = chardet.detect(unicode_)['encoding']
276 276 if encoding is None:
277 277 raise UnicodeEncodeError()
278 278
279 279 return unicode_.encode(encoding)
280 280 except (ImportError, UnicodeEncodeError):
281 281 return unicode_.encode(to_encoding[0], 'replace')
282 282
283 283
284 284 def remove_suffix(s, suffix):
285 285 if s.endswith(suffix):
286 286 s = s[:-1 * len(suffix)]
287 287 return s
288 288
289 289
290 290 def remove_prefix(s, prefix):
291 291 if s.startswith(prefix):
292 292 s = s[len(prefix):]
293 293 return s
294 294
295 295
296 296 def find_calling_context(ignore_modules=None):
297 297 """
298 298 Look through the calling stack and return the frame which called
299 299 this function and is part of core module ( ie. rhodecode.* )
300 300
301 301 :param ignore_modules: list of modules to ignore eg. ['rhodecode.lib']
302 302 """
303 303
304 304 ignore_modules = ignore_modules or []
305 305
306 306 f = sys._getframe(2)
307 307 while f.f_back is not None:
308 308 name = f.f_globals.get('__name__')
309 309 if name and name.startswith(__name__.split('.')[0]):
310 310 if name not in ignore_modules:
311 311 return f
312 312 f = f.f_back
313 313 return None
314 314
315 315
316 316 def engine_from_config(configuration, prefix='sqlalchemy.', **kwargs):
317 317 """Custom engine_from_config functions."""
318 318 log = logging.getLogger('sqlalchemy.engine')
319 319 engine = sqlalchemy.engine_from_config(configuration, prefix, **kwargs)
320 320
321 321 def color_sql(sql):
322 322 color_seq = '\033[1;33m' # This is yellow: code 33
323 323 normal = '\x1b[0m'
324 324 return ''.join([color_seq, sql, normal])
325 325
326 326 if configuration['debug']:
327 327 # attach events only for debug configuration
328 328
329 329 def before_cursor_execute(conn, cursor, statement,
330 330 parameters, context, executemany):
331 331 setattr(conn, 'query_start_time', time.time())
332 332 log.info(color_sql(">>>>> STARTING QUERY >>>>>"))
333 333 calling_context = find_calling_context(ignore_modules=[
334 334 'rhodecode.lib.caching_query',
335 335 'rhodecode.model.settings',
336 336 ])
337 337 if calling_context:
338 338 log.info(color_sql('call context %s:%s' % (
339 339 calling_context.f_code.co_filename,
340 340 calling_context.f_lineno,
341 341 )))
342 342
343 343 def after_cursor_execute(conn, cursor, statement,
344 344 parameters, context, executemany):
345 345 delattr(conn, 'query_start_time')
346 346
347 347 sqlalchemy.event.listen(engine, "before_cursor_execute",
348 348 before_cursor_execute)
349 349 sqlalchemy.event.listen(engine, "after_cursor_execute",
350 350 after_cursor_execute)
351 351
352 352 return engine
353 353
354 354
355 355 def get_encryption_key(config):
356 356 secret = config.get('rhodecode.encrypted_values.secret')
357 357 default = config['beaker.session.secret']
358 358 return secret or default
359 359
360 360
361 361 def age(prevdate, now=None, show_short_version=False, show_suffix=True,
362 362 short_format=False):
363 363 """
364 364 Turns a datetime into an age string.
365 365 If show_short_version is True, this generates a shorter string with
366 366 an approximate age; ex. '1 day ago', rather than '1 day and 23 hours ago'.
367 367
368 368 * IMPORTANT*
369 369 Code of this function is written in special way so it's easier to
370 370 backport it to javascript. If you mean to update it, please also update
371 371 `jquery.timeago-extension.js` file
372 372
373 373 :param prevdate: datetime object
374 374 :param now: get current time, if not define we use
375 375 `datetime.datetime.now()`
376 376 :param show_short_version: if it should approximate the date and
377 377 return a shorter string
378 378 :param show_suffix:
379 379 :param short_format: show short format, eg 2D instead of 2 days
380 380 :rtype: unicode
381 381 :returns: unicode words describing age
382 382 """
383 383 from pylons.i18n.translation import _, ungettext
384 384
385 385 def _get_relative_delta(now, prevdate):
386 386 base = dateutil.relativedelta.relativedelta(now, prevdate)
387 387 return {
388 388 'year': base.years,
389 389 'month': base.months,
390 390 'day': base.days,
391 391 'hour': base.hours,
392 392 'minute': base.minutes,
393 393 'second': base.seconds,
394 394 }
395 395
396 396 def _is_leap_year(year):
397 397 return year % 4 == 0 and (year % 100 != 0 or year % 400 == 0)
398 398
399 399 def get_month(prevdate):
400 400 return prevdate.month
401 401
402 402 def get_year(prevdate):
403 403 return prevdate.year
404 404
405 405 now = now or datetime.datetime.now()
406 406 order = ['year', 'month', 'day', 'hour', 'minute', 'second']
407 407 deltas = {}
408 408 future = False
409 409
410 410 if prevdate > now:
411 411 now_old = now
412 412 now = prevdate
413 413 prevdate = now_old
414 414 future = True
415 415 if future:
416 416 prevdate = prevdate.replace(microsecond=0)
417 417 # Get date parts deltas
418 418 for part in order:
419 419 rel_delta = _get_relative_delta(now, prevdate)
420 420 deltas[part] = rel_delta[part]
421 421
422 422 # Fix negative offsets (there is 1 second between 10:59:59 and 11:00:00,
423 423 # not 1 hour, -59 minutes and -59 seconds)
424 424 offsets = [[5, 60], [4, 60], [3, 24]]
425 425 for element in offsets: # seconds, minutes, hours
426 426 num = element[0]
427 427 length = element[1]
428 428
429 429 part = order[num]
430 430 carry_part = order[num - 1]
431 431
432 432 if deltas[part] < 0:
433 433 deltas[part] += length
434 434 deltas[carry_part] -= 1
435 435
436 436 # Same thing for days except that the increment depends on the (variable)
437 437 # number of days in the month
438 438 month_lengths = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31]
439 439 if deltas['day'] < 0:
440 440 if get_month(prevdate) == 2 and _is_leap_year(get_year(prevdate)):
441 441 deltas['day'] += 29
442 442 else:
443 443 deltas['day'] += month_lengths[get_month(prevdate) - 1]
444 444
445 445 deltas['month'] -= 1
446 446
447 447 if deltas['month'] < 0:
448 448 deltas['month'] += 12
449 449 deltas['year'] -= 1
450 450
451 451 # Format the result
452 452 if short_format:
453 453 fmt_funcs = {
454 454 'year': lambda d: u'%dy' % d,
455 455 'month': lambda d: u'%dm' % d,
456 456 'day': lambda d: u'%dd' % d,
457 457 'hour': lambda d: u'%dh' % d,
458 458 'minute': lambda d: u'%dmin' % d,
459 459 'second': lambda d: u'%dsec' % d,
460 460 }
461 461 else:
462 462 fmt_funcs = {
463 463 'year': lambda d: ungettext(u'%d year', '%d years', d) % d,
464 464 'month': lambda d: ungettext(u'%d month', '%d months', d) % d,
465 465 'day': lambda d: ungettext(u'%d day', '%d days', d) % d,
466 466 'hour': lambda d: ungettext(u'%d hour', '%d hours', d) % d,
467 467 'minute': lambda d: ungettext(u'%d minute', '%d minutes', d) % d,
468 468 'second': lambda d: ungettext(u'%d second', '%d seconds', d) % d,
469 469 }
470 470
471 471 i = 0
472 472 for part in order:
473 473 value = deltas[part]
474 474 if value != 0:
475 475
476 476 if i < 5:
477 477 sub_part = order[i + 1]
478 478 sub_value = deltas[sub_part]
479 479 else:
480 480 sub_value = 0
481 481
482 482 if sub_value == 0 or show_short_version:
483 483 _val = fmt_funcs[part](value)
484 484 if future:
485 485 if show_suffix:
486 486 return _(u'in %s') % _val
487 487 else:
488 488 return _val
489 489
490 490 else:
491 491 if show_suffix:
492 492 return _(u'%s ago') % _val
493 493 else:
494 494 return _val
495 495
496 496 val = fmt_funcs[part](value)
497 497 val_detail = fmt_funcs[sub_part](sub_value)
498 498
499 499 if short_format:
500 500 datetime_tmpl = u'%s, %s'
501 501 if show_suffix:
502 502 datetime_tmpl = _(u'%s, %s ago')
503 503 if future:
504 504 datetime_tmpl = _(u'in %s, %s')
505 505 else:
506 506 datetime_tmpl = _(u'%s and %s')
507 507 if show_suffix:
508 508 datetime_tmpl = _(u'%s and %s ago')
509 509 if future:
510 510 datetime_tmpl = _(u'in %s and %s')
511 511
512 512 return datetime_tmpl % (val, val_detail)
513 513 i += 1
514 514 return _(u'just now')
515 515
516 516
517 517 def uri_filter(uri):
518 518 """
519 519 Removes user:password from given url string
520 520
521 521 :param uri:
522 522 :rtype: unicode
523 523 :returns: filtered list of strings
524 524 """
525 525 if not uri:
526 526 return ''
527 527
528 528 proto = ''
529 529
530 530 for pat in ('https://', 'http://'):
531 531 if uri.startswith(pat):
532 532 uri = uri[len(pat):]
533 533 proto = pat
534 534 break
535 535
536 536 # remove passwords and username
537 537 uri = uri[uri.find('@') + 1:]
538 538
539 539 # get the port
540 540 cred_pos = uri.find(':')
541 541 if cred_pos == -1:
542 542 host, port = uri, None
543 543 else:
544 544 host, port = uri[:cred_pos], uri[cred_pos + 1:]
545 545
546 546 return filter(None, [proto, host, port])
547 547
548 548
549 549 def credentials_filter(uri):
550 550 """
551 551 Returns a url with removed credentials
552 552
553 553 :param uri:
554 554 """
555 555
556 556 uri = uri_filter(uri)
557 557 # check if we have port
558 558 if len(uri) > 2 and uri[2]:
559 559 uri[2] = ':' + uri[2]
560 560
561 561 return ''.join(uri)
562 562
563 563
564 564 def get_clone_url(uri_tmpl, qualifed_home_url, repo_name, repo_id, **override):
565 565 parsed_url = urlobject.URLObject(qualifed_home_url)
566 566 decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/')))
567 567 args = {
568 568 'scheme': parsed_url.scheme,
569 569 'user': '',
570 570 # path if we use proxy-prefix
571 571 'netloc': parsed_url.netloc+decoded_path,
572 572 'prefix': decoded_path,
573 573 'repo': repo_name,
574 574 'repoid': str(repo_id)
575 575 }
576 576 args.update(override)
577 577 args['user'] = urllib.quote(safe_str(args['user']))
578 578
579 579 for k, v in args.items():
580 580 uri_tmpl = uri_tmpl.replace('{%s}' % k, v)
581 581
582 582 # remove leading @ sign if it's present. Case of empty user
583 583 url_obj = urlobject.URLObject(uri_tmpl)
584 584 url = url_obj.with_netloc(url_obj.netloc.lstrip('@'))
585 585
586 586 return safe_unicode(url)
587 587
588 588
589 589 def get_commit_safe(repo, commit_id=None, commit_idx=None, pre_load=None):
590 590 """
591 591 Safe version of get_commit if this commit doesn't exists for a
592 592 repository it returns a Dummy one instead
593 593
594 594 :param repo: repository instance
595 595 :param commit_id: commit id as str
596 596 :param pre_load: optional list of commit attributes to load
597 597 """
598 598 # TODO(skreft): remove these circular imports
599 599 from rhodecode.lib.vcs.backends.base import BaseRepository, EmptyCommit
600 600 from rhodecode.lib.vcs.exceptions import RepositoryError
601 601 if not isinstance(repo, BaseRepository):
602 602 raise Exception('You must pass an Repository '
603 603 'object as first argument got %s', type(repo))
604 604
605 605 try:
606 606 commit = repo.get_commit(
607 607 commit_id=commit_id, commit_idx=commit_idx, pre_load=pre_load)
608 608 except (RepositoryError, LookupError):
609 609 commit = EmptyCommit()
610 610 return commit
611 611
612 612
613 613 def datetime_to_time(dt):
614 614 if dt:
615 615 return time.mktime(dt.timetuple())
616 616
617 617
618 618 def time_to_datetime(tm):
619 619 if tm:
620 620 if isinstance(tm, basestring):
621 621 try:
622 622 tm = float(tm)
623 623 except ValueError:
624 624 return
625 625 return datetime.datetime.fromtimestamp(tm)
626 626
627 627
628 628 def time_to_utcdatetime(tm):
629 629 if tm:
630 630 if isinstance(tm, basestring):
631 631 try:
632 632 tm = float(tm)
633 633 except ValueError:
634 634 return
635 635 return datetime.datetime.utcfromtimestamp(tm)
636 636
637 637
638 638 MENTIONS_REGEX = re.compile(
639 639 # ^@ or @ without any special chars in front
640 640 r'(?:^@|[^a-zA-Z0-9\-\_\.]@)'
641 641 # main body starts with letter, then can be . - _
642 642 r'([a-zA-Z0-9]{1}[a-zA-Z0-9\-\_\.]+)',
643 643 re.VERBOSE | re.MULTILINE)
644 644
645 645
646 646 def extract_mentioned_users(s):
647 647 """
648 648 Returns unique usernames from given string s that have @mention
649 649
650 650 :param s: string to get mentions
651 651 """
652 652 usrs = set()
653 653 for username in MENTIONS_REGEX.findall(s):
654 654 usrs.add(username)
655 655
656 656 return sorted(list(usrs), key=lambda k: k.lower())
657 657
658 658
659 659 class AttributeDict(dict):
660 660 def __getattr__(self, attr):
661 661 return self.get(attr, None)
662 662 __setattr__ = dict.__setitem__
663 663 __delattr__ = dict.__delitem__
664 664
665 665
666 666 def fix_PATH(os_=None):
667 667 """
668 668 Get current active python path, and append it to PATH variable to fix
669 669 issues of subprocess calls and different python versions
670 670 """
671 671 if os_ is None:
672 672 import os
673 673 else:
674 674 os = os_
675 675
676 676 cur_path = os.path.split(sys.executable)[0]
677 677 if not os.environ['PATH'].startswith(cur_path):
678 678 os.environ['PATH'] = '%s:%s' % (cur_path, os.environ['PATH'])
679 679
680 680
681 681 def obfuscate_url_pw(engine):
682 682 _url = engine or ''
683 683 try:
684 684 _url = sqlalchemy.engine.url.make_url(engine)
685 685 if _url.password:
686 686 _url.password = 'XXXXX'
687 687 except Exception:
688 688 pass
689 689 return unicode(_url)
690 690
691 691
692 692 def get_server_url(environ):
693 693 req = webob.Request(environ)
694 694 return req.host_url + req.script_name
695 695
696 696
697 697 def unique_id(hexlen=32):
698 698 alphabet = "23456789ABCDEFGHJKLMNPQRSTUVWXYZabcdefghjklmnpqrstuvwxyz"
699 699 return suuid(truncate_to=hexlen, alphabet=alphabet)
700 700
701 701
702 702 def suuid(url=None, truncate_to=22, alphabet=None):
703 703 """
704 704 Generate and return a short URL safe UUID.
705 705
706 706 If the url parameter is provided, set the namespace to the provided
707 707 URL and generate a UUID.
708 708
709 709 :param url to get the uuid for
710 710 :truncate_to: truncate the basic 22 UUID to shorter version
711 711
712 712 The IDs won't be universally unique any longer, but the probability of
713 713 a collision will still be very low.
714 714 """
715 715 # Define our alphabet.
716 716 _ALPHABET = alphabet or "23456789ABCDEFGHJKLMNPQRSTUVWXYZ"
717 717
718 718 # If no URL is given, generate a random UUID.
719 719 if url is None:
720 720 unique_id = uuid.uuid4().int
721 721 else:
722 722 unique_id = uuid.uuid3(uuid.NAMESPACE_URL, url).int
723 723
724 724 alphabet_length = len(_ALPHABET)
725 725 output = []
726 726 while unique_id > 0:
727 727 digit = unique_id % alphabet_length
728 728 output.append(_ALPHABET[digit])
729 729 unique_id = int(unique_id / alphabet_length)
730 730 return "".join(output)[:truncate_to]
731 731
732 732
733 733 def get_current_rhodecode_user():
734 734 """
735 735 Gets rhodecode user from threadlocal tmpl_context variable if it's
736 736 defined, else returns None.
737 737 """
738 738 from pylons import tmpl_context as c
739 739 if hasattr(c, 'rhodecode_user'):
740 740 return c.rhodecode_user
741 741
742 742 return None
743 743
744 744
745 745 def action_logger_generic(action, namespace=''):
746 746 """
747 747 A generic logger for actions useful to the system overview, tries to find
748 748 an acting user for the context of the call otherwise reports unknown user
749 749
750 750 :param action: logging message eg 'comment 5 deleted'
751 751 :param type: string
752 752
753 753 :param namespace: namespace of the logging message eg. 'repo.comments'
754 754 :param type: string
755 755
756 756 """
757 757
758 758 logger_name = 'rhodecode.actions'
759 759
760 760 if namespace:
761 761 logger_name += '.' + namespace
762 762
763 763 log = logging.getLogger(logger_name)
764 764
765 765 # get a user if we can
766 766 user = get_current_rhodecode_user()
767 767
768 768 logfunc = log.info
769 769
770 770 if not user:
771 771 user = '<unknown user>'
772 772 logfunc = log.warning
773 773
774 774 logfunc('Logging action by {}: {}'.format(user, action))
775 775
776 776
777 777 def escape_split(text, sep=',', maxsplit=-1):
778 778 r"""
779 779 Allows for escaping of the separator: e.g. arg='foo\, bar'
780 780
781 781 It should be noted that the way bash et. al. do command line parsing, those
782 782 single quotes are required.
783 783 """
784 784 escaped_sep = r'\%s' % sep
785 785
786 786 if escaped_sep not in text:
787 787 return text.split(sep, maxsplit)
788 788
789 789 before, _mid, after = text.partition(escaped_sep)
790 790 startlist = before.split(sep, maxsplit) # a regular split is fine here
791 791 unfinished = startlist[-1]
792 792 startlist = startlist[:-1]
793 793
794 794 # recurse because there may be more escaped separators
795 795 endlist = escape_split(after, sep, maxsplit)
796 796
797 797 # finish building the escaped value. we use endlist[0] becaue the first
798 798 # part of the string sent in recursion is the rest of the escaped value.
799 799 unfinished += sep + endlist[0]
800 800
801 801 return startlist + [unfinished] + endlist[1:] # put together all the parts
802 802
803 803
804 804 class OptionalAttr(object):
805 805 """
806 806 Special Optional Option that defines other attribute. Example::
807 807
808 808 def test(apiuser, userid=Optional(OAttr('apiuser')):
809 809 user = Optional.extract(userid)
810 810 # calls
811 811
812 812 """
813 813
814 814 def __init__(self, attr_name):
815 815 self.attr_name = attr_name
816 816
817 817 def __repr__(self):
818 818 return '<OptionalAttr:%s>' % self.attr_name
819 819
820 820 def __call__(self):
821 821 return self
822 822
823 823
824 824 # alias
825 825 OAttr = OptionalAttr
826 826
827 827
828 828 class Optional(object):
829 829 """
830 830 Defines an optional parameter::
831 831
832 832 param = param.getval() if isinstance(param, Optional) else param
833 833 param = param() if isinstance(param, Optional) else param
834 834
835 835 is equivalent of::
836 836
837 837 param = Optional.extract(param)
838 838
839 839 """
840 840
841 841 def __init__(self, type_):
842 842 self.type_ = type_
843 843
844 844 def __repr__(self):
845 845 return '<Optional:%s>' % self.type_.__repr__()
846 846
847 847 def __call__(self):
848 848 return self.getval()
849 849
850 850 def getval(self):
851 851 """
852 852 returns value from this Optional instance
853 853 """
854 854 if isinstance(self.type_, OAttr):
855 855 # use params name
856 856 return self.type_.attr_name
857 857 return self.type_
858 858
859 859 @classmethod
860 860 def extract(cls, val):
861 861 """
862 862 Extracts value from Optional() instance
863 863
864 864 :param val:
865 865 :return: original value if it's not Optional instance else
866 866 value of instance
867 867 """
868 868 if isinstance(val, cls):
869 869 return val.getval()
870 870 return val
871 871
872 872
873 873 def get_routes_generator_for_server_url(server_url):
874 874 parsed_url = urlobject.URLObject(server_url)
875 875 netloc = safe_str(parsed_url.netloc)
876 876 script_name = safe_str(parsed_url.path)
877 877
878 878 if ':' in netloc:
879 879 server_name, server_port = netloc.split(':')
880 880 else:
881 881 server_name = netloc
882 882 server_port = (parsed_url.scheme == 'https' and '443' or '80')
883 883
884 884 environ = {
885 885 'REQUEST_METHOD': 'GET',
886 886 'PATH_INFO': '/',
887 887 'SERVER_NAME': server_name,
888 888 'SERVER_PORT': server_port,
889 889 'SCRIPT_NAME': script_name,
890 890 }
891 891 if parsed_url.scheme == 'https':
892 892 environ['HTTPS'] = 'on'
893 893 environ['wsgi.url_scheme'] = 'https'
894 894
895 895 return routes.util.URLGenerator(rhodecode.CONFIG['routes.map'], environ)
896
897
898 def glob2re(pat):
899 """
900 Translate a shell PATTERN to a regular expression.
901
902 There is no way to quote meta-characters.
903 """
904
905 i, n = 0, len(pat)
906 res = ''
907 while i < n:
908 c = pat[i]
909 i = i+1
910 if c == '*':
911 #res = res + '.*'
912 res = res + '[^/]*'
913 elif c == '?':
914 #res = res + '.'
915 res = res + '[^/]'
916 elif c == '[':
917 j = i
918 if j < n and pat[j] == '!':
919 j = j+1
920 if j < n and pat[j] == ']':
921 j = j+1
922 while j < n and pat[j] != ']':
923 j = j+1
924 if j >= n:
925 res = res + '\\['
926 else:
927 stuff = pat[i:j].replace('\\','\\\\')
928 i = j+1
929 if stuff[0] == '!':
930 stuff = '^' + stuff[1:]
931 elif stuff[0] == '^':
932 stuff = '\\' + stuff
933 res = '%s[%s]' % (res, stuff)
934 else:
935 res = res + re.escape(c)
936 return res + '\Z(?ms)'
@@ -1,3516 +1,3640 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 import re
25 26 import os
26 27 import sys
27 28 import time
28 29 import hashlib
29 30 import logging
30 31 import datetime
31 32 import warnings
32 33 import ipaddress
33 34 import functools
34 35 import traceback
35 36 import collections
36 37
37 38
38 39 from sqlalchemy import *
39 40 from sqlalchemy.exc import IntegrityError
40 41 from sqlalchemy.ext.declarative import declared_attr
41 42 from sqlalchemy.ext.hybrid import hybrid_property
42 43 from sqlalchemy.orm import (
43 44 relationship, joinedload, class_mapper, validates, aliased)
44 45 from sqlalchemy.sql.expression import true
45 46 from beaker.cache import cache_region, region_invalidate
46 47 from webob.exc import HTTPNotFound
47 48 from zope.cachedescriptors.property import Lazy as LazyProperty
48 49
49 50 from pylons import url
50 51 from pylons.i18n.translation import lazy_ugettext as _
51 52
52 53 from rhodecode.lib.vcs import get_backend, get_vcs_instance
53 54 from rhodecode.lib.vcs.utils.helpers import get_scm
54 55 from rhodecode.lib.vcs.exceptions import VCSError
55 56 from rhodecode.lib.vcs.backends.base import (
56 57 EmptyCommit, Reference, MergeFailureReason)
57 58 from rhodecode.lib.utils2 import (
58 59 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 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
61 glob2re)
60 62 from rhodecode.lib.jsonalchemy import MutationObj, JsonType, JSONDict
61 63 from rhodecode.lib.ext_json import json
62 64 from rhodecode.lib.caching_query import FromCache
63 65 from rhodecode.lib.encrypt import AESCipher
64 66
65 67 from rhodecode.model.meta import Base, Session
66 68
67 69 URL_SEP = '/'
68 70 log = logging.getLogger(__name__)
69 71
70 72 # =============================================================================
71 73 # BASE CLASSES
72 74 # =============================================================================
73 75
74 76 # this is propagated from .ini file rhodecode.encrypted_values.secret or
75 77 # beaker.session.secret if first is not set.
76 78 # and initialized at environment.py
77 79 ENCRYPTION_KEY = None
78 80
79 81 # used to sort permissions by types, '#' used here is not allowed to be in
80 82 # usernames, and it's very early in sorted string.printable table.
81 83 PERMISSION_TYPE_SORT = {
82 84 'admin': '####',
83 85 'write': '###',
84 86 'read': '##',
85 87 'none': '#',
86 88 }
87 89
88 90
89 91 def display_sort(obj):
90 92 """
91 93 Sort function used to sort permissions in .permissions() function of
92 94 Repository, RepoGroup, UserGroup. Also it put the default user in front
93 95 of all other resources
94 96 """
95 97
96 98 if obj.username == User.DEFAULT_USER:
97 99 return '#####'
98 100 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
99 101 return prefix + obj.username
100 102
101 103
102 104 def _hash_key(k):
103 105 return md5_safe(k)
104 106
105 107
106 108 class EncryptedTextValue(TypeDecorator):
107 109 """
108 110 Special column for encrypted long text data, use like::
109 111
110 112 value = Column("encrypted_value", EncryptedValue(), nullable=False)
111 113
112 114 This column is intelligent so if value is in unencrypted form it return
113 115 unencrypted form, but on save it always encrypts
114 116 """
115 117 impl = Text
116 118
117 119 def process_bind_param(self, value, dialect):
118 120 if not value:
119 121 return value
120 122 if value.startswith('enc$aes$') or value.startswith('enc$aes_hmac$'):
121 123 # protect against double encrypting if someone manually starts
122 124 # doing
123 125 raise ValueError('value needs to be in unencrypted format, ie. '
124 126 'not starting with enc$aes')
125 127 return 'enc$aes_hmac$%s' % AESCipher(
126 128 ENCRYPTION_KEY, hmac=True).encrypt(value)
127 129
128 130 def process_result_value(self, value, dialect):
129 131 import rhodecode
130 132
131 133 if not value:
132 134 return value
133 135
134 136 parts = value.split('$', 3)
135 137 if not len(parts) == 3:
136 138 # probably not encrypted values
137 139 return value
138 140 else:
139 141 if parts[0] != 'enc':
140 142 # parts ok but without our header ?
141 143 return value
142 144 enc_strict_mode = str2bool(rhodecode.CONFIG.get(
143 145 'rhodecode.encrypted_values.strict') or True)
144 146 # at that stage we know it's our encryption
145 147 if parts[1] == 'aes':
146 148 decrypted_data = AESCipher(ENCRYPTION_KEY).decrypt(parts[2])
147 149 elif parts[1] == 'aes_hmac':
148 150 decrypted_data = AESCipher(
149 151 ENCRYPTION_KEY, hmac=True,
150 152 strict_verification=enc_strict_mode).decrypt(parts[2])
151 153 else:
152 154 raise ValueError(
153 155 'Encryption type part is wrong, must be `aes` '
154 156 'or `aes_hmac`, got `%s` instead' % (parts[1]))
155 157 return decrypted_data
156 158
157 159
158 160 class BaseModel(object):
159 161 """
160 162 Base Model for all classes
161 163 """
162 164
163 165 @classmethod
164 166 def _get_keys(cls):
165 167 """return column names for this model """
166 168 return class_mapper(cls).c.keys()
167 169
168 170 def get_dict(self):
169 171 """
170 172 return dict with keys and values corresponding
171 173 to this model data """
172 174
173 175 d = {}
174 176 for k in self._get_keys():
175 177 d[k] = getattr(self, k)
176 178
177 179 # also use __json__() if present to get additional fields
178 180 _json_attr = getattr(self, '__json__', None)
179 181 if _json_attr:
180 182 # update with attributes from __json__
181 183 if callable(_json_attr):
182 184 _json_attr = _json_attr()
183 185 for k, val in _json_attr.iteritems():
184 186 d[k] = val
185 187 return d
186 188
187 189 def get_appstruct(self):
188 190 """return list with keys and values tuples corresponding
189 191 to this model data """
190 192
191 193 l = []
192 194 for k in self._get_keys():
193 195 l.append((k, getattr(self, k),))
194 196 return l
195 197
196 198 def populate_obj(self, populate_dict):
197 199 """populate model with data from given populate_dict"""
198 200
199 201 for k in self._get_keys():
200 202 if k in populate_dict:
201 203 setattr(self, k, populate_dict[k])
202 204
203 205 @classmethod
204 206 def query(cls):
205 207 return Session().query(cls)
206 208
207 209 @classmethod
208 210 def get(cls, id_):
209 211 if id_:
210 212 return cls.query().get(id_)
211 213
212 214 @classmethod
213 215 def get_or_404(cls, id_):
214 216 try:
215 217 id_ = int(id_)
216 218 except (TypeError, ValueError):
217 219 raise HTTPNotFound
218 220
219 221 res = cls.query().get(id_)
220 222 if not res:
221 223 raise HTTPNotFound
222 224 return res
223 225
224 226 @classmethod
225 227 def getAll(cls):
226 228 # deprecated and left for backward compatibility
227 229 return cls.get_all()
228 230
229 231 @classmethod
230 232 def get_all(cls):
231 233 return cls.query().all()
232 234
233 235 @classmethod
234 236 def delete(cls, id_):
235 237 obj = cls.query().get(id_)
236 238 Session().delete(obj)
237 239
238 240 @classmethod
239 241 def identity_cache(cls, session, attr_name, value):
240 242 exist_in_session = []
241 243 for (item_cls, pkey), instance in session.identity_map.items():
242 244 if cls == item_cls and getattr(instance, attr_name) == value:
243 245 exist_in_session.append(instance)
244 246 if exist_in_session:
245 247 if len(exist_in_session) == 1:
246 248 return exist_in_session[0]
247 249 log.exception(
248 250 'multiple objects with attr %s and '
249 251 'value %s found with same name: %r',
250 252 attr_name, value, exist_in_session)
251 253
252 254 def __repr__(self):
253 255 if hasattr(self, '__unicode__'):
254 256 # python repr needs to return str
255 257 try:
256 258 return safe_str(self.__unicode__())
257 259 except UnicodeDecodeError:
258 260 pass
259 261 return '<DB:%s>' % (self.__class__.__name__)
260 262
261 263
262 264 class RhodeCodeSetting(Base, BaseModel):
263 265 __tablename__ = 'rhodecode_settings'
264 266 __table_args__ = (
265 267 UniqueConstraint('app_settings_name'),
266 268 {'extend_existing': True, 'mysql_engine': 'InnoDB',
267 269 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
268 270 )
269 271
270 272 SETTINGS_TYPES = {
271 273 'str': safe_str,
272 274 'int': safe_int,
273 275 'unicode': safe_unicode,
274 276 'bool': str2bool,
275 277 'list': functools.partial(aslist, sep=',')
276 278 }
277 279 DEFAULT_UPDATE_URL = 'https://rhodecode.com/api/v1/info/versions'
278 280 GLOBAL_CONF_KEY = 'app_settings'
279 281
280 282 app_settings_id = Column("app_settings_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
281 283 app_settings_name = Column("app_settings_name", String(255), nullable=True, unique=None, default=None)
282 284 _app_settings_value = Column("app_settings_value", String(4096), nullable=True, unique=None, default=None)
283 285 _app_settings_type = Column("app_settings_type", String(255), nullable=True, unique=None, default=None)
284 286
285 287 def __init__(self, key='', val='', type='unicode'):
286 288 self.app_settings_name = key
287 289 self.app_settings_type = type
288 290 self.app_settings_value = val
289 291
290 292 @validates('_app_settings_value')
291 293 def validate_settings_value(self, key, val):
292 294 assert type(val) == unicode
293 295 return val
294 296
295 297 @hybrid_property
296 298 def app_settings_value(self):
297 299 v = self._app_settings_value
298 300 _type = self.app_settings_type
299 301 if _type:
300 302 _type = self.app_settings_type.split('.')[0]
301 303 # decode the encrypted value
302 304 if 'encrypted' in self.app_settings_type:
303 305 cipher = EncryptedTextValue()
304 306 v = safe_unicode(cipher.process_result_value(v, None))
305 307
306 308 converter = self.SETTINGS_TYPES.get(_type) or \
307 309 self.SETTINGS_TYPES['unicode']
308 310 return converter(v)
309 311
310 312 @app_settings_value.setter
311 313 def app_settings_value(self, val):
312 314 """
313 315 Setter that will always make sure we use unicode in app_settings_value
314 316
315 317 :param val:
316 318 """
317 319 val = safe_unicode(val)
318 320 # encode the encrypted value
319 321 if 'encrypted' in self.app_settings_type:
320 322 cipher = EncryptedTextValue()
321 323 val = safe_unicode(cipher.process_bind_param(val, None))
322 324 self._app_settings_value = val
323 325
324 326 @hybrid_property
325 327 def app_settings_type(self):
326 328 return self._app_settings_type
327 329
328 330 @app_settings_type.setter
329 331 def app_settings_type(self, val):
330 332 if val.split('.')[0] not in self.SETTINGS_TYPES:
331 333 raise Exception('type must be one of %s got %s'
332 334 % (self.SETTINGS_TYPES.keys(), val))
333 335 self._app_settings_type = val
334 336
335 337 def __unicode__(self):
336 338 return u"<%s('%s:%s[%s]')>" % (
337 339 self.__class__.__name__,
338 340 self.app_settings_name, self.app_settings_value,
339 341 self.app_settings_type
340 342 )
341 343
342 344
343 345 class RhodeCodeUi(Base, BaseModel):
344 346 __tablename__ = 'rhodecode_ui'
345 347 __table_args__ = (
346 348 UniqueConstraint('ui_key'),
347 349 {'extend_existing': True, 'mysql_engine': 'InnoDB',
348 350 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
349 351 )
350 352
351 353 HOOK_REPO_SIZE = 'changegroup.repo_size'
352 354 # HG
353 355 HOOK_PRE_PULL = 'preoutgoing.pre_pull'
354 356 HOOK_PULL = 'outgoing.pull_logger'
355 357 HOOK_PRE_PUSH = 'prechangegroup.pre_push'
356 358 HOOK_PUSH = 'changegroup.push_logger'
357 359
358 360 # TODO: johbo: Unify way how hooks are configured for git and hg,
359 361 # git part is currently hardcoded.
360 362
361 363 # SVN PATTERNS
362 364 SVN_BRANCH_ID = 'vcs_svn_branch'
363 365 SVN_TAG_ID = 'vcs_svn_tag'
364 366
365 367 ui_id = Column(
366 368 "ui_id", Integer(), nullable=False, unique=True, default=None,
367 369 primary_key=True)
368 370 ui_section = Column(
369 371 "ui_section", String(255), nullable=True, unique=None, default=None)
370 372 ui_key = Column(
371 373 "ui_key", String(255), nullable=True, unique=None, default=None)
372 374 ui_value = Column(
373 375 "ui_value", String(255), nullable=True, unique=None, default=None)
374 376 ui_active = Column(
375 377 "ui_active", Boolean(), nullable=True, unique=None, default=True)
376 378
377 379 def __repr__(self):
378 380 return '<%s[%s]%s=>%s]>' % (self.__class__.__name__, self.ui_section,
379 381 self.ui_key, self.ui_value)
380 382
381 383
382 384 class RepoRhodeCodeSetting(Base, BaseModel):
383 385 __tablename__ = 'repo_rhodecode_settings'
384 386 __table_args__ = (
385 387 UniqueConstraint(
386 388 'app_settings_name', 'repository_id',
387 389 name='uq_repo_rhodecode_setting_name_repo_id'),
388 390 {'extend_existing': True, 'mysql_engine': 'InnoDB',
389 391 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
390 392 )
391 393
392 394 repository_id = Column(
393 395 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
394 396 nullable=False)
395 397 app_settings_id = Column(
396 398 "app_settings_id", Integer(), nullable=False, unique=True,
397 399 default=None, primary_key=True)
398 400 app_settings_name = Column(
399 401 "app_settings_name", String(255), nullable=True, unique=None,
400 402 default=None)
401 403 _app_settings_value = Column(
402 404 "app_settings_value", String(4096), nullable=True, unique=None,
403 405 default=None)
404 406 _app_settings_type = Column(
405 407 "app_settings_type", String(255), nullable=True, unique=None,
406 408 default=None)
407 409
408 410 repository = relationship('Repository')
409 411
410 412 def __init__(self, repository_id, key='', val='', type='unicode'):
411 413 self.repository_id = repository_id
412 414 self.app_settings_name = key
413 415 self.app_settings_type = type
414 416 self.app_settings_value = val
415 417
416 418 @validates('_app_settings_value')
417 419 def validate_settings_value(self, key, val):
418 420 assert type(val) == unicode
419 421 return val
420 422
421 423 @hybrid_property
422 424 def app_settings_value(self):
423 425 v = self._app_settings_value
424 426 type_ = self.app_settings_type
425 427 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
426 428 converter = SETTINGS_TYPES.get(type_) or SETTINGS_TYPES['unicode']
427 429 return converter(v)
428 430
429 431 @app_settings_value.setter
430 432 def app_settings_value(self, val):
431 433 """
432 434 Setter that will always make sure we use unicode in app_settings_value
433 435
434 436 :param val:
435 437 """
436 438 self._app_settings_value = safe_unicode(val)
437 439
438 440 @hybrid_property
439 441 def app_settings_type(self):
440 442 return self._app_settings_type
441 443
442 444 @app_settings_type.setter
443 445 def app_settings_type(self, val):
444 446 SETTINGS_TYPES = RhodeCodeSetting.SETTINGS_TYPES
445 447 if val not in SETTINGS_TYPES:
446 448 raise Exception('type must be one of %s got %s'
447 449 % (SETTINGS_TYPES.keys(), val))
448 450 self._app_settings_type = val
449 451
450 452 def __unicode__(self):
451 453 return u"<%s('%s:%s:%s[%s]')>" % (
452 454 self.__class__.__name__, self.repository.repo_name,
453 455 self.app_settings_name, self.app_settings_value,
454 456 self.app_settings_type
455 457 )
456 458
457 459
458 460 class RepoRhodeCodeUi(Base, BaseModel):
459 461 __tablename__ = 'repo_rhodecode_ui'
460 462 __table_args__ = (
461 463 UniqueConstraint(
462 464 'repository_id', 'ui_section', 'ui_key',
463 465 name='uq_repo_rhodecode_ui_repository_id_section_key'),
464 466 {'extend_existing': True, 'mysql_engine': 'InnoDB',
465 467 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
466 468 )
467 469
468 470 repository_id = Column(
469 471 "repository_id", Integer(), ForeignKey('repositories.repo_id'),
470 472 nullable=False)
471 473 ui_id = Column(
472 474 "ui_id", Integer(), nullable=False, unique=True, default=None,
473 475 primary_key=True)
474 476 ui_section = Column(
475 477 "ui_section", String(255), nullable=True, unique=None, default=None)
476 478 ui_key = Column(
477 479 "ui_key", String(255), nullable=True, unique=None, default=None)
478 480 ui_value = Column(
479 481 "ui_value", String(255), nullable=True, unique=None, default=None)
480 482 ui_active = Column(
481 483 "ui_active", Boolean(), nullable=True, unique=None, default=True)
482 484
483 485 repository = relationship('Repository')
484 486
485 487 def __repr__(self):
486 488 return '<%s[%s:%s]%s=>%s]>' % (
487 489 self.__class__.__name__, self.repository.repo_name,
488 490 self.ui_section, self.ui_key, self.ui_value)
489 491
490 492
491 493 class User(Base, BaseModel):
492 494 __tablename__ = 'users'
493 495 __table_args__ = (
494 496 UniqueConstraint('username'), UniqueConstraint('email'),
495 497 Index('u_username_idx', 'username'),
496 498 Index('u_email_idx', 'email'),
497 499 {'extend_existing': True, 'mysql_engine': 'InnoDB',
498 500 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
499 501 )
500 502 DEFAULT_USER = 'default'
501 503 DEFAULT_USER_EMAIL = 'anonymous@rhodecode.org'
502 504 DEFAULT_GRAVATAR_URL = 'https://secure.gravatar.com/avatar/{md5email}?d=identicon&s={size}'
503 505
504 506 user_id = Column("user_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
505 507 username = Column("username", String(255), nullable=True, unique=None, default=None)
506 508 password = Column("password", String(255), nullable=True, unique=None, default=None)
507 509 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
508 510 admin = Column("admin", Boolean(), nullable=True, unique=None, default=False)
509 511 name = Column("firstname", String(255), nullable=True, unique=None, default=None)
510 512 lastname = Column("lastname", String(255), nullable=True, unique=None, default=None)
511 513 _email = Column("email", String(255), nullable=True, unique=None, default=None)
512 514 last_login = Column("last_login", DateTime(timezone=False), nullable=True, unique=None, default=None)
513 515 extern_type = Column("extern_type", String(255), nullable=True, unique=None, default=None)
514 516 extern_name = Column("extern_name", String(255), nullable=True, unique=None, default=None)
515 517 api_key = Column("api_key", String(255), nullable=True, unique=None, default=None)
516 518 inherit_default_permissions = Column("inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
517 519 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
518 520 _user_data = Column("user_data", LargeBinary(), nullable=True) # JSON data
519 521
520 522 user_log = relationship('UserLog')
521 523 user_perms = relationship('UserToPerm', primaryjoin="User.user_id==UserToPerm.user_id", cascade='all')
522 524
523 525 repositories = relationship('Repository')
524 526 repository_groups = relationship('RepoGroup')
525 527 user_groups = relationship('UserGroup')
526 528
527 529 user_followers = relationship('UserFollowing', primaryjoin='UserFollowing.follows_user_id==User.user_id', cascade='all')
528 530 followings = relationship('UserFollowing', primaryjoin='UserFollowing.user_id==User.user_id', cascade='all')
529 531
530 532 repo_to_perm = relationship('UserRepoToPerm', primaryjoin='UserRepoToPerm.user_id==User.user_id', cascade='all')
531 533 repo_group_to_perm = relationship('UserRepoGroupToPerm', primaryjoin='UserRepoGroupToPerm.user_id==User.user_id', cascade='all')
532 534 user_group_to_perm = relationship('UserUserGroupToPerm', primaryjoin='UserUserGroupToPerm.user_id==User.user_id', cascade='all')
533 535
534 536 group_member = relationship('UserGroupMember', cascade='all')
535 537
536 538 notifications = relationship('UserNotification', cascade='all')
537 539 # notifications assigned to this user
538 540 user_created_notifications = relationship('Notification', cascade='all')
539 541 # comments created by this user
540 542 user_comments = relationship('ChangesetComment', cascade='all')
541 543 # user profile extra info
542 544 user_emails = relationship('UserEmailMap', cascade='all')
543 545 user_ip_map = relationship('UserIpMap', cascade='all')
544 546 user_auth_tokens = relationship('UserApiKeys', cascade='all')
545 547 # gists
546 548 user_gists = relationship('Gist', cascade='all')
547 549 # user pull requests
548 550 user_pull_requests = relationship('PullRequest', cascade='all')
549 551 # external identities
550 552 extenal_identities = relationship(
551 553 'ExternalIdentity',
552 554 primaryjoin="User.user_id==ExternalIdentity.local_user_id",
553 555 cascade='all')
554 556
555 557 def __unicode__(self):
556 558 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
557 559 self.user_id, self.username)
558 560
559 561 @hybrid_property
560 562 def email(self):
561 563 return self._email
562 564
563 565 @email.setter
564 566 def email(self, val):
565 567 self._email = val.lower() if val else None
566 568
567 569 @property
568 570 def firstname(self):
569 571 # alias for future
570 572 return self.name
571 573
572 574 @property
573 575 def emails(self):
574 576 other = UserEmailMap.query().filter(UserEmailMap.user==self).all()
575 577 return [self.email] + [x.email for x in other]
576 578
577 579 @property
578 580 def auth_tokens(self):
579 581 return [self.api_key] + [x.api_key for x in self.extra_auth_tokens]
580 582
581 583 @property
582 584 def extra_auth_tokens(self):
583 585 return UserApiKeys.query().filter(UserApiKeys.user == self).all()
584 586
585 587 @property
586 588 def feed_token(self):
587 589 feed_tokens = UserApiKeys.query()\
588 590 .filter(UserApiKeys.user == self)\
589 591 .filter(UserApiKeys.role == UserApiKeys.ROLE_FEED)\
590 592 .all()
591 593 if feed_tokens:
592 594 return feed_tokens[0].api_key
593 595 else:
594 596 # use the main token so we don't end up with nothing...
595 597 return self.api_key
596 598
597 599 @classmethod
598 600 def extra_valid_auth_tokens(cls, user, role=None):
599 601 tokens = UserApiKeys.query().filter(UserApiKeys.user == user)\
600 602 .filter(or_(UserApiKeys.expires == -1,
601 603 UserApiKeys.expires >= time.time()))
602 604 if role:
603 605 tokens = tokens.filter(or_(UserApiKeys.role == role,
604 606 UserApiKeys.role == UserApiKeys.ROLE_ALL))
605 607 return tokens.all()
606 608
607 609 @property
608 610 def ip_addresses(self):
609 611 ret = UserIpMap.query().filter(UserIpMap.user == self).all()
610 612 return [x.ip_addr for x in ret]
611 613
612 614 @property
613 615 def username_and_name(self):
614 616 return '%s (%s %s)' % (self.username, self.firstname, self.lastname)
615 617
616 618 @property
617 619 def username_or_name_or_email(self):
618 620 full_name = self.full_name if self.full_name is not ' ' else None
619 621 return self.username or full_name or self.email
620 622
621 623 @property
622 624 def full_name(self):
623 625 return '%s %s' % (self.firstname, self.lastname)
624 626
625 627 @property
626 628 def full_name_or_username(self):
627 629 return ('%s %s' % (self.firstname, self.lastname)
628 630 if (self.firstname and self.lastname) else self.username)
629 631
630 632 @property
631 633 def full_contact(self):
632 634 return '%s %s <%s>' % (self.firstname, self.lastname, self.email)
633 635
634 636 @property
635 637 def short_contact(self):
636 638 return '%s %s' % (self.firstname, self.lastname)
637 639
638 640 @property
639 641 def is_admin(self):
640 642 return self.admin
641 643
642 644 @property
643 645 def AuthUser(self):
644 646 """
645 647 Returns instance of AuthUser for this user
646 648 """
647 649 from rhodecode.lib.auth import AuthUser
648 650 return AuthUser(user_id=self.user_id, api_key=self.api_key,
649 651 username=self.username)
650 652
651 653 @hybrid_property
652 654 def user_data(self):
653 655 if not self._user_data:
654 656 return {}
655 657
656 658 try:
657 659 return json.loads(self._user_data)
658 660 except TypeError:
659 661 return {}
660 662
661 663 @user_data.setter
662 664 def user_data(self, val):
663 665 if not isinstance(val, dict):
664 666 raise Exception('user_data must be dict, got %s' % type(val))
665 667 try:
666 668 self._user_data = json.dumps(val)
667 669 except Exception:
668 670 log.error(traceback.format_exc())
669 671
670 672 @classmethod
671 673 def get_by_username(cls, username, case_insensitive=False,
672 674 cache=False, identity_cache=False):
673 675 session = Session()
674 676
675 677 if case_insensitive:
676 678 q = cls.query().filter(
677 679 func.lower(cls.username) == func.lower(username))
678 680 else:
679 681 q = cls.query().filter(cls.username == username)
680 682
681 683 if cache:
682 684 if identity_cache:
683 685 val = cls.identity_cache(session, 'username', username)
684 686 if val:
685 687 return val
686 688 else:
687 689 q = q.options(
688 690 FromCache("sql_cache_short",
689 691 "get_user_by_name_%s" % _hash_key(username)))
690 692
691 693 return q.scalar()
692 694
693 695 @classmethod
694 696 def get_by_auth_token(cls, auth_token, cache=False, fallback=True):
695 697 q = cls.query().filter(cls.api_key == auth_token)
696 698
697 699 if cache:
698 700 q = q.options(FromCache("sql_cache_short",
699 701 "get_auth_token_%s" % auth_token))
700 702 res = q.scalar()
701 703
702 704 if fallback and not res:
703 705 #fallback to additional keys
704 706 _res = UserApiKeys.query()\
705 707 .filter(UserApiKeys.api_key == auth_token)\
706 708 .filter(or_(UserApiKeys.expires == -1,
707 709 UserApiKeys.expires >= time.time()))\
708 710 .first()
709 711 if _res:
710 712 res = _res.user
711 713 return res
712 714
713 715 @classmethod
714 716 def get_by_email(cls, email, case_insensitive=False, cache=False):
715 717
716 718 if case_insensitive:
717 719 q = cls.query().filter(func.lower(cls.email) == func.lower(email))
718 720
719 721 else:
720 722 q = cls.query().filter(cls.email == email)
721 723
722 724 if cache:
723 725 q = q.options(FromCache("sql_cache_short",
724 726 "get_email_key_%s" % _hash_key(email)))
725 727
726 728 ret = q.scalar()
727 729 if ret is None:
728 730 q = UserEmailMap.query()
729 731 # try fetching in alternate email map
730 732 if case_insensitive:
731 733 q = q.filter(func.lower(UserEmailMap.email) == func.lower(email))
732 734 else:
733 735 q = q.filter(UserEmailMap.email == email)
734 736 q = q.options(joinedload(UserEmailMap.user))
735 737 if cache:
736 738 q = q.options(FromCache("sql_cache_short",
737 739 "get_email_map_key_%s" % email))
738 740 ret = getattr(q.scalar(), 'user', None)
739 741
740 742 return ret
741 743
742 744 @classmethod
743 745 def get_from_cs_author(cls, author):
744 746 """
745 747 Tries to get User objects out of commit author string
746 748
747 749 :param author:
748 750 """
749 751 from rhodecode.lib.helpers import email, author_name
750 752 # Valid email in the attribute passed, see if they're in the system
751 753 _email = email(author)
752 754 if _email:
753 755 user = cls.get_by_email(_email, case_insensitive=True)
754 756 if user:
755 757 return user
756 758 # Maybe we can match by username?
757 759 _author = author_name(author)
758 760 user = cls.get_by_username(_author, case_insensitive=True)
759 761 if user:
760 762 return user
761 763
762 764 def update_userdata(self, **kwargs):
763 765 usr = self
764 766 old = usr.user_data
765 767 old.update(**kwargs)
766 768 usr.user_data = old
767 769 Session().add(usr)
768 770 log.debug('updated userdata with ', kwargs)
769 771
770 772 def update_lastlogin(self):
771 773 """Update user lastlogin"""
772 774 self.last_login = datetime.datetime.now()
773 775 Session().add(self)
774 776 log.debug('updated user %s lastlogin', self.username)
775 777
776 778 def update_lastactivity(self):
777 779 """Update user lastactivity"""
778 780 usr = self
779 781 old = usr.user_data
780 782 old.update({'last_activity': time.time()})
781 783 usr.user_data = old
782 784 Session().add(usr)
783 785 log.debug('updated user %s lastactivity', usr.username)
784 786
785 787 def update_password(self, new_password, change_api_key=False):
786 788 from rhodecode.lib.auth import get_crypt_password,generate_auth_token
787 789
788 790 self.password = get_crypt_password(new_password)
789 791 if change_api_key:
790 792 self.api_key = generate_auth_token(self.username)
791 793 Session().add(self)
792 794
793 795 @classmethod
794 796 def get_first_super_admin(cls):
795 797 user = User.query().filter(User.admin == true()).first()
796 798 if user is None:
797 799 raise Exception('FATAL: Missing administrative account!')
798 800 return user
799 801
800 802 @classmethod
801 803 def get_all_super_admins(cls):
802 804 """
803 805 Returns all admin accounts sorted by username
804 806 """
805 807 return User.query().filter(User.admin == true())\
806 808 .order_by(User.username.asc()).all()
807 809
808 810 @classmethod
809 811 def get_default_user(cls, cache=False):
810 812 user = User.get_by_username(User.DEFAULT_USER, cache=cache)
811 813 if user is None:
812 814 raise Exception('FATAL: Missing default account!')
813 815 return user
814 816
815 817 def _get_default_perms(self, user, suffix=''):
816 818 from rhodecode.model.permission import PermissionModel
817 819 return PermissionModel().get_default_perms(user.user_perms, suffix)
818 820
819 821 def get_default_perms(self, suffix=''):
820 822 return self._get_default_perms(self, suffix)
821 823
822 824 def get_api_data(self, include_secrets=False, details='full'):
823 825 """
824 826 Common function for generating user related data for API
825 827
826 828 :param include_secrets: By default secrets in the API data will be replaced
827 829 by a placeholder value to prevent exposing this data by accident. In case
828 830 this data shall be exposed, set this flag to ``True``.
829 831
830 832 :param details: details can be 'basic|full' basic gives only a subset of
831 833 the available user information that includes user_id, name and emails.
832 834 """
833 835 user = self
834 836 user_data = self.user_data
835 837 data = {
836 838 'user_id': user.user_id,
837 839 'username': user.username,
838 840 'firstname': user.name,
839 841 'lastname': user.lastname,
840 842 'email': user.email,
841 843 'emails': user.emails,
842 844 }
843 845 if details == 'basic':
844 846 return data
845 847
846 848 api_key_length = 40
847 849 api_key_replacement = '*' * api_key_length
848 850
849 851 extras = {
850 852 'api_key': api_key_replacement,
851 853 'api_keys': [api_key_replacement],
852 854 'active': user.active,
853 855 'admin': user.admin,
854 856 'extern_type': user.extern_type,
855 857 'extern_name': user.extern_name,
856 858 'last_login': user.last_login,
857 859 'ip_addresses': user.ip_addresses,
858 860 'language': user_data.get('language')
859 861 }
860 862 data.update(extras)
861 863
862 864 if include_secrets:
863 865 data['api_key'] = user.api_key
864 866 data['api_keys'] = user.auth_tokens
865 867 return data
866 868
867 869 def __json__(self):
868 870 data = {
869 871 'full_name': self.full_name,
870 872 'full_name_or_username': self.full_name_or_username,
871 873 'short_contact': self.short_contact,
872 874 'full_contact': self.full_contact,
873 875 }
874 876 data.update(self.get_api_data())
875 877 return data
876 878
877 879
878 880 class UserApiKeys(Base, BaseModel):
879 881 __tablename__ = 'user_api_keys'
880 882 __table_args__ = (
881 883 Index('uak_api_key_idx', 'api_key'),
882 884 Index('uak_api_key_expires_idx', 'api_key', 'expires'),
883 885 UniqueConstraint('api_key'),
884 886 {'extend_existing': True, 'mysql_engine': 'InnoDB',
885 887 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
886 888 )
887 889 __mapper_args__ = {}
888 890
889 891 # ApiKey role
890 892 ROLE_ALL = 'token_role_all'
891 893 ROLE_HTTP = 'token_role_http'
892 894 ROLE_VCS = 'token_role_vcs'
893 895 ROLE_API = 'token_role_api'
894 896 ROLE_FEED = 'token_role_feed'
895 897 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED]
896 898
897 899 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
898 900 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
899 901 api_key = Column("api_key", String(255), nullable=False, unique=True)
900 902 description = Column('description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
901 903 expires = Column('expires', Float(53), nullable=False)
902 904 role = Column('role', String(255), nullable=True)
903 905 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
904 906
905 907 user = relationship('User', lazy='joined')
906 908
907 909 @classmethod
908 910 def _get_role_name(cls, role):
909 911 return {
910 912 cls.ROLE_ALL: _('all'),
911 913 cls.ROLE_HTTP: _('http/web interface'),
912 914 cls.ROLE_VCS: _('vcs (git/hg/svn protocol)'),
913 915 cls.ROLE_API: _('api calls'),
914 916 cls.ROLE_FEED: _('feed access'),
915 917 }.get(role, role)
916 918
917 919 @property
918 920 def expired(self):
919 921 if self.expires == -1:
920 922 return False
921 923 return time.time() > self.expires
922 924
923 925 @property
924 926 def role_humanized(self):
925 927 return self._get_role_name(self.role)
926 928
927 929
928 930 class UserEmailMap(Base, BaseModel):
929 931 __tablename__ = 'user_email_map'
930 932 __table_args__ = (
931 933 Index('uem_email_idx', 'email'),
932 934 UniqueConstraint('email'),
933 935 {'extend_existing': True, 'mysql_engine': 'InnoDB',
934 936 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
935 937 )
936 938 __mapper_args__ = {}
937 939
938 940 email_id = Column("email_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
939 941 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
940 942 _email = Column("email", String(255), nullable=True, unique=False, default=None)
941 943 user = relationship('User', lazy='joined')
942 944
943 945 @validates('_email')
944 946 def validate_email(self, key, email):
945 947 # check if this email is not main one
946 948 main_email = Session().query(User).filter(User.email == email).scalar()
947 949 if main_email is not None:
948 950 raise AttributeError('email %s is present is user table' % email)
949 951 return email
950 952
951 953 @hybrid_property
952 954 def email(self):
953 955 return self._email
954 956
955 957 @email.setter
956 958 def email(self, val):
957 959 self._email = val.lower() if val else None
958 960
959 961
960 962 class UserIpMap(Base, BaseModel):
961 963 __tablename__ = 'user_ip_map'
962 964 __table_args__ = (
963 965 UniqueConstraint('user_id', 'ip_addr'),
964 966 {'extend_existing': True, 'mysql_engine': 'InnoDB',
965 967 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
966 968 )
967 969 __mapper_args__ = {}
968 970
969 971 ip_id = Column("ip_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
970 972 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
971 973 ip_addr = Column("ip_addr", String(255), nullable=True, unique=False, default=None)
972 974 active = Column("active", Boolean(), nullable=True, unique=None, default=True)
973 975 description = Column("description", String(10000), nullable=True, unique=None, default=None)
974 976 user = relationship('User', lazy='joined')
975 977
976 978 @classmethod
977 979 def _get_ip_range(cls, ip_addr):
978 980 net = ipaddress.ip_network(ip_addr, strict=False)
979 981 return [str(net.network_address), str(net.broadcast_address)]
980 982
981 983 def __json__(self):
982 984 return {
983 985 'ip_addr': self.ip_addr,
984 986 'ip_range': self._get_ip_range(self.ip_addr),
985 987 }
986 988
987 989 def __unicode__(self):
988 990 return u"<%s('user_id:%s=>%s')>" % (self.__class__.__name__,
989 991 self.user_id, self.ip_addr)
990 992
991 993 class UserLog(Base, BaseModel):
992 994 __tablename__ = 'user_logs'
993 995 __table_args__ = (
994 996 {'extend_existing': True, 'mysql_engine': 'InnoDB',
995 997 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
996 998 )
997 999 user_log_id = Column("user_log_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
998 1000 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
999 1001 username = Column("username", String(255), nullable=True, unique=None, default=None)
1000 1002 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True)
1001 1003 repository_name = Column("repository_name", String(255), nullable=True, unique=None, default=None)
1002 1004 user_ip = Column("user_ip", String(255), nullable=True, unique=None, default=None)
1003 1005 action = Column("action", Text().with_variant(Text(1200000), 'mysql'), nullable=True, unique=None, default=None)
1004 1006 action_date = Column("action_date", DateTime(timezone=False), nullable=True, unique=None, default=None)
1005 1007
1006 1008 def __unicode__(self):
1007 1009 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1008 1010 self.repository_name,
1009 1011 self.action)
1010 1012
1011 1013 @property
1012 1014 def action_as_day(self):
1013 1015 return datetime.date(*self.action_date.timetuple()[:3])
1014 1016
1015 1017 user = relationship('User')
1016 1018 repository = relationship('Repository', cascade='')
1017 1019
1018 1020
1019 1021 class UserGroup(Base, BaseModel):
1020 1022 __tablename__ = 'users_groups'
1021 1023 __table_args__ = (
1022 1024 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1023 1025 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1024 1026 )
1025 1027
1026 1028 users_group_id = Column("users_group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1027 1029 users_group_name = Column("users_group_name", String(255), nullable=False, unique=True, default=None)
1028 1030 user_group_description = Column("user_group_description", String(10000), nullable=True, unique=None, default=None)
1029 1031 users_group_active = Column("users_group_active", Boolean(), nullable=True, unique=None, default=None)
1030 1032 inherit_default_permissions = Column("users_group_inherit_default_permissions", Boolean(), nullable=False, unique=None, default=True)
1031 1033 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
1032 1034 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1033 1035 _group_data = Column("group_data", LargeBinary(), nullable=True) # JSON data
1034 1036
1035 1037 members = relationship('UserGroupMember', cascade="all, delete, delete-orphan", lazy="joined")
1036 1038 users_group_to_perm = relationship('UserGroupToPerm', cascade='all')
1037 1039 users_group_repo_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1038 1040 users_group_repo_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
1039 1041 user_user_group_to_perm = relationship('UserUserGroupToPerm', cascade='all')
1040 1042 user_group_user_group_to_perm = relationship('UserGroupUserGroupToPerm ', primaryjoin="UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id", cascade='all')
1041 1043
1042 1044 user = relationship('User')
1043 1045
1044 1046 @hybrid_property
1045 1047 def group_data(self):
1046 1048 if not self._group_data:
1047 1049 return {}
1048 1050
1049 1051 try:
1050 1052 return json.loads(self._group_data)
1051 1053 except TypeError:
1052 1054 return {}
1053 1055
1054 1056 @group_data.setter
1055 1057 def group_data(self, val):
1056 1058 try:
1057 1059 self._group_data = json.dumps(val)
1058 1060 except Exception:
1059 1061 log.error(traceback.format_exc())
1060 1062
1061 1063 def __unicode__(self):
1062 1064 return u"<%s('id:%s:%s')>" % (self.__class__.__name__,
1063 1065 self.users_group_id,
1064 1066 self.users_group_name)
1065 1067
1066 1068 @classmethod
1067 1069 def get_by_group_name(cls, group_name, cache=False,
1068 1070 case_insensitive=False):
1069 1071 if case_insensitive:
1070 1072 q = cls.query().filter(func.lower(cls.users_group_name) ==
1071 1073 func.lower(group_name))
1072 1074
1073 1075 else:
1074 1076 q = cls.query().filter(cls.users_group_name == group_name)
1075 1077 if cache:
1076 1078 q = q.options(FromCache(
1077 1079 "sql_cache_short",
1078 1080 "get_group_%s" % _hash_key(group_name)))
1079 1081 return q.scalar()
1080 1082
1081 1083 @classmethod
1082 1084 def get(cls, user_group_id, cache=False):
1083 1085 user_group = cls.query()
1084 1086 if cache:
1085 1087 user_group = user_group.options(FromCache("sql_cache_short",
1086 1088 "get_users_group_%s" % user_group_id))
1087 1089 return user_group.get(user_group_id)
1088 1090
1089 1091 def permissions(self, with_admins=True, with_owner=True):
1090 1092 q = UserUserGroupToPerm.query().filter(UserUserGroupToPerm.user_group == self)
1091 1093 q = q.options(joinedload(UserUserGroupToPerm.user_group),
1092 1094 joinedload(UserUserGroupToPerm.user),
1093 1095 joinedload(UserUserGroupToPerm.permission),)
1094 1096
1095 1097 # get owners and admins and permissions. We do a trick of re-writing
1096 1098 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1097 1099 # has a global reference and changing one object propagates to all
1098 1100 # others. This means if admin is also an owner admin_row that change
1099 1101 # would propagate to both objects
1100 1102 perm_rows = []
1101 1103 for _usr in q.all():
1102 1104 usr = AttributeDict(_usr.user.get_dict())
1103 1105 usr.permission = _usr.permission.permission_name
1104 1106 perm_rows.append(usr)
1105 1107
1106 1108 # filter the perm rows by 'default' first and then sort them by
1107 1109 # admin,write,read,none permissions sorted again alphabetically in
1108 1110 # each group
1109 1111 perm_rows = sorted(perm_rows, key=display_sort)
1110 1112
1111 1113 _admin_perm = 'usergroup.admin'
1112 1114 owner_row = []
1113 1115 if with_owner:
1114 1116 usr = AttributeDict(self.user.get_dict())
1115 1117 usr.owner_row = True
1116 1118 usr.permission = _admin_perm
1117 1119 owner_row.append(usr)
1118 1120
1119 1121 super_admin_rows = []
1120 1122 if with_admins:
1121 1123 for usr in User.get_all_super_admins():
1122 1124 # if this admin is also owner, don't double the record
1123 1125 if usr.user_id == owner_row[0].user_id:
1124 1126 owner_row[0].admin_row = True
1125 1127 else:
1126 1128 usr = AttributeDict(usr.get_dict())
1127 1129 usr.admin_row = True
1128 1130 usr.permission = _admin_perm
1129 1131 super_admin_rows.append(usr)
1130 1132
1131 1133 return super_admin_rows + owner_row + perm_rows
1132 1134
1133 1135 def permission_user_groups(self):
1134 1136 q = UserGroupUserGroupToPerm.query().filter(UserGroupUserGroupToPerm.target_user_group == self)
1135 1137 q = q.options(joinedload(UserGroupUserGroupToPerm.user_group),
1136 1138 joinedload(UserGroupUserGroupToPerm.target_user_group),
1137 1139 joinedload(UserGroupUserGroupToPerm.permission),)
1138 1140
1139 1141 perm_rows = []
1140 1142 for _user_group in q.all():
1141 1143 usr = AttributeDict(_user_group.user_group.get_dict())
1142 1144 usr.permission = _user_group.permission.permission_name
1143 1145 perm_rows.append(usr)
1144 1146
1145 1147 return perm_rows
1146 1148
1147 1149 def _get_default_perms(self, user_group, suffix=''):
1148 1150 from rhodecode.model.permission import PermissionModel
1149 1151 return PermissionModel().get_default_perms(user_group.users_group_to_perm, suffix)
1150 1152
1151 1153 def get_default_perms(self, suffix=''):
1152 1154 return self._get_default_perms(self, suffix)
1153 1155
1154 1156 def get_api_data(self, with_group_members=True, include_secrets=False):
1155 1157 """
1156 1158 :param include_secrets: See :meth:`User.get_api_data`, this parameter is
1157 1159 basically forwarded.
1158 1160
1159 1161 """
1160 1162 user_group = self
1161 1163
1162 1164 data = {
1163 1165 'users_group_id': user_group.users_group_id,
1164 1166 'group_name': user_group.users_group_name,
1165 1167 'group_description': user_group.user_group_description,
1166 1168 'active': user_group.users_group_active,
1167 1169 'owner': user_group.user.username,
1168 1170 }
1169 1171 if with_group_members:
1170 1172 users = []
1171 1173 for user in user_group.members:
1172 1174 user = user.user
1173 1175 users.append(user.get_api_data(include_secrets=include_secrets))
1174 1176 data['users'] = users
1175 1177
1176 1178 return data
1177 1179
1178 1180
1179 1181 class UserGroupMember(Base, BaseModel):
1180 1182 __tablename__ = 'users_groups_members'
1181 1183 __table_args__ = (
1182 1184 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1183 1185 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1184 1186 )
1185 1187
1186 1188 users_group_member_id = Column("users_group_member_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1187 1189 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
1188 1190 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
1189 1191
1190 1192 user = relationship('User', lazy='joined')
1191 1193 users_group = relationship('UserGroup')
1192 1194
1193 1195 def __init__(self, gr_id='', u_id=''):
1194 1196 self.users_group_id = gr_id
1195 1197 self.user_id = u_id
1196 1198
1197 1199
1198 1200 class RepositoryField(Base, BaseModel):
1199 1201 __tablename__ = 'repositories_fields'
1200 1202 __table_args__ = (
1201 1203 UniqueConstraint('repository_id', 'field_key'), # no-multi field
1202 1204 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1203 1205 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1204 1206 )
1205 1207 PREFIX = 'ex_' # prefix used in form to not conflict with already existing fields
1206 1208
1207 1209 repo_field_id = Column("repo_field_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1208 1210 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
1209 1211 field_key = Column("field_key", String(250))
1210 1212 field_label = Column("field_label", String(1024), nullable=False)
1211 1213 field_value = Column("field_value", String(10000), nullable=False)
1212 1214 field_desc = Column("field_desc", String(1024), nullable=False)
1213 1215 field_type = Column("field_type", String(255), nullable=False, unique=None)
1214 1216 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
1215 1217
1216 1218 repository = relationship('Repository')
1217 1219
1218 1220 @property
1219 1221 def field_key_prefixed(self):
1220 1222 return 'ex_%s' % self.field_key
1221 1223
1222 1224 @classmethod
1223 1225 def un_prefix_key(cls, key):
1224 1226 if key.startswith(cls.PREFIX):
1225 1227 return key[len(cls.PREFIX):]
1226 1228 return key
1227 1229
1228 1230 @classmethod
1229 1231 def get_by_key_name(cls, key, repo):
1230 1232 row = cls.query()\
1231 1233 .filter(cls.repository == repo)\
1232 1234 .filter(cls.field_key == key).scalar()
1233 1235 return row
1234 1236
1235 1237
1236 1238 class Repository(Base, BaseModel):
1237 1239 __tablename__ = 'repositories'
1238 1240 __table_args__ = (
1239 1241 Index('r_repo_name_idx', 'repo_name', mysql_length=255),
1240 1242 {'extend_existing': True, 'mysql_engine': 'InnoDB',
1241 1243 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
1242 1244 )
1243 1245 DEFAULT_CLONE_URI = '{scheme}://{user}@{netloc}/{repo}'
1244 1246 DEFAULT_CLONE_URI_ID = '{scheme}://{user}@{netloc}/_{repoid}'
1245 1247
1246 1248 STATE_CREATED = 'repo_state_created'
1247 1249 STATE_PENDING = 'repo_state_pending'
1248 1250 STATE_ERROR = 'repo_state_error'
1249 1251
1250 1252 LOCK_AUTOMATIC = 'lock_auto'
1251 1253 LOCK_API = 'lock_api'
1252 1254 LOCK_WEB = 'lock_web'
1253 1255 LOCK_PULL = 'lock_pull'
1254 1256
1255 1257 NAME_SEP = URL_SEP
1256 1258
1257 1259 repo_id = Column(
1258 1260 "repo_id", Integer(), nullable=False, unique=True, default=None,
1259 1261 primary_key=True)
1260 1262 _repo_name = Column(
1261 1263 "repo_name", Text(), nullable=False, default=None)
1262 1264 _repo_name_hash = Column(
1263 1265 "repo_name_hash", String(255), nullable=False, unique=True)
1264 1266 repo_state = Column("repo_state", String(255), nullable=True)
1265 1267
1266 1268 clone_uri = Column(
1267 1269 "clone_uri", EncryptedTextValue(), nullable=True, unique=False,
1268 1270 default=None)
1269 1271 repo_type = Column(
1270 1272 "repo_type", String(255), nullable=False, unique=False, default=None)
1271 1273 user_id = Column(
1272 1274 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
1273 1275 unique=False, default=None)
1274 1276 private = Column(
1275 1277 "private", Boolean(), nullable=True, unique=None, default=None)
1276 1278 enable_statistics = Column(
1277 1279 "statistics", Boolean(), nullable=True, unique=None, default=True)
1278 1280 enable_downloads = Column(
1279 1281 "downloads", Boolean(), nullable=True, unique=None, default=True)
1280 1282 description = Column(
1281 1283 "description", String(10000), nullable=True, unique=None, default=None)
1282 1284 created_on = Column(
1283 1285 'created_on', DateTime(timezone=False), nullable=True, unique=None,
1284 1286 default=datetime.datetime.now)
1285 1287 updated_on = Column(
1286 1288 'updated_on', DateTime(timezone=False), nullable=True, unique=None,
1287 1289 default=datetime.datetime.now)
1288 1290 _landing_revision = Column(
1289 1291 "landing_revision", String(255), nullable=False, unique=False,
1290 1292 default=None)
1291 1293 enable_locking = Column(
1292 1294 "enable_locking", Boolean(), nullable=False, unique=None,
1293 1295 default=False)
1294 1296 _locked = Column(
1295 1297 "locked", String(255), nullable=True, unique=False, default=None)
1296 1298 _changeset_cache = Column(
1297 1299 "changeset_cache", LargeBinary(), nullable=True) # JSON data
1298 1300
1299 1301 fork_id = Column(
1300 1302 "fork_id", Integer(), ForeignKey('repositories.repo_id'),
1301 1303 nullable=True, unique=False, default=None)
1302 1304 group_id = Column(
1303 1305 "group_id", Integer(), ForeignKey('groups.group_id'), nullable=True,
1304 1306 unique=False, default=None)
1305 1307
1306 1308 user = relationship('User', lazy='joined')
1307 1309 fork = relationship('Repository', remote_side=repo_id, lazy='joined')
1308 1310 group = relationship('RepoGroup', lazy='joined')
1309 1311 repo_to_perm = relationship(
1310 1312 'UserRepoToPerm', cascade='all',
1311 1313 order_by='UserRepoToPerm.repo_to_perm_id')
1312 1314 users_group_to_perm = relationship('UserGroupRepoToPerm', cascade='all')
1313 1315 stats = relationship('Statistics', cascade='all', uselist=False)
1314 1316
1315 1317 followers = relationship(
1316 1318 'UserFollowing',
1317 1319 primaryjoin='UserFollowing.follows_repo_id==Repository.repo_id',
1318 1320 cascade='all')
1319 1321 extra_fields = relationship(
1320 1322 'RepositoryField', cascade="all, delete, delete-orphan")
1321 1323 logs = relationship('UserLog')
1322 1324 comments = relationship(
1323 1325 'ChangesetComment', cascade="all, delete, delete-orphan")
1324 1326 pull_requests_source = relationship(
1325 1327 'PullRequest',
1326 1328 primaryjoin='PullRequest.source_repo_id==Repository.repo_id',
1327 1329 cascade="all, delete, delete-orphan")
1328 1330 pull_requests_target = relationship(
1329 1331 'PullRequest',
1330 1332 primaryjoin='PullRequest.target_repo_id==Repository.repo_id',
1331 1333 cascade="all, delete, delete-orphan")
1332 1334 ui = relationship('RepoRhodeCodeUi', cascade="all")
1333 1335 settings = relationship('RepoRhodeCodeSetting', cascade="all")
1334 1336 integrations = relationship('Integration',
1335 1337 cascade="all, delete, delete-orphan")
1336 1338
1337 1339 def __unicode__(self):
1338 1340 return u"<%s('%s:%s')>" % (self.__class__.__name__, self.repo_id,
1339 1341 safe_unicode(self.repo_name))
1340 1342
1341 1343 @hybrid_property
1342 1344 def landing_rev(self):
1343 1345 # always should return [rev_type, rev]
1344 1346 if self._landing_revision:
1345 1347 _rev_info = self._landing_revision.split(':')
1346 1348 if len(_rev_info) < 2:
1347 1349 _rev_info.insert(0, 'rev')
1348 1350 return [_rev_info[0], _rev_info[1]]
1349 1351 return [None, None]
1350 1352
1351 1353 @landing_rev.setter
1352 1354 def landing_rev(self, val):
1353 1355 if ':' not in val:
1354 1356 raise ValueError('value must be delimited with `:` and consist '
1355 1357 'of <rev_type>:<rev>, got %s instead' % val)
1356 1358 self._landing_revision = val
1357 1359
1358 1360 @hybrid_property
1359 1361 def locked(self):
1360 1362 if self._locked:
1361 1363 user_id, timelocked, reason = self._locked.split(':')
1362 1364 lock_values = int(user_id), timelocked, reason
1363 1365 else:
1364 1366 lock_values = [None, None, None]
1365 1367 return lock_values
1366 1368
1367 1369 @locked.setter
1368 1370 def locked(self, val):
1369 1371 if val and isinstance(val, (list, tuple)):
1370 1372 self._locked = ':'.join(map(str, val))
1371 1373 else:
1372 1374 self._locked = None
1373 1375
1374 1376 @hybrid_property
1375 1377 def changeset_cache(self):
1376 1378 from rhodecode.lib.vcs.backends.base import EmptyCommit
1377 1379 dummy = EmptyCommit().__json__()
1378 1380 if not self._changeset_cache:
1379 1381 return dummy
1380 1382 try:
1381 1383 return json.loads(self._changeset_cache)
1382 1384 except TypeError:
1383 1385 return dummy
1384 1386 except Exception:
1385 1387 log.error(traceback.format_exc())
1386 1388 return dummy
1387 1389
1388 1390 @changeset_cache.setter
1389 1391 def changeset_cache(self, val):
1390 1392 try:
1391 1393 self._changeset_cache = json.dumps(val)
1392 1394 except Exception:
1393 1395 log.error(traceback.format_exc())
1394 1396
1395 1397 @hybrid_property
1396 1398 def repo_name(self):
1397 1399 return self._repo_name
1398 1400
1399 1401 @repo_name.setter
1400 1402 def repo_name(self, value):
1401 1403 self._repo_name = value
1402 1404 self._repo_name_hash = hashlib.sha1(safe_str(value)).hexdigest()
1403 1405
1404 1406 @classmethod
1405 1407 def normalize_repo_name(cls, repo_name):
1406 1408 """
1407 1409 Normalizes os specific repo_name to the format internally stored inside
1408 1410 database using URL_SEP
1409 1411
1410 1412 :param cls:
1411 1413 :param repo_name:
1412 1414 """
1413 1415 return cls.NAME_SEP.join(repo_name.split(os.sep))
1414 1416
1415 1417 @classmethod
1416 1418 def get_by_repo_name(cls, repo_name, cache=False, identity_cache=False):
1417 1419 session = Session()
1418 1420 q = session.query(cls).filter(cls.repo_name == repo_name)
1419 1421
1420 1422 if cache:
1421 1423 if identity_cache:
1422 1424 val = cls.identity_cache(session, 'repo_name', repo_name)
1423 1425 if val:
1424 1426 return val
1425 1427 else:
1426 1428 q = q.options(
1427 1429 FromCache("sql_cache_short",
1428 1430 "get_repo_by_name_%s" % _hash_key(repo_name)))
1429 1431
1430 1432 return q.scalar()
1431 1433
1432 1434 @classmethod
1433 1435 def get_by_full_path(cls, repo_full_path):
1434 1436 repo_name = repo_full_path.split(cls.base_path(), 1)[-1]
1435 1437 repo_name = cls.normalize_repo_name(repo_name)
1436 1438 return cls.get_by_repo_name(repo_name.strip(URL_SEP))
1437 1439
1438 1440 @classmethod
1439 1441 def get_repo_forks(cls, repo_id):
1440 1442 return cls.query().filter(Repository.fork_id == repo_id)
1441 1443
1442 1444 @classmethod
1443 1445 def base_path(cls):
1444 1446 """
1445 1447 Returns base path when all repos are stored
1446 1448
1447 1449 :param cls:
1448 1450 """
1449 1451 q = Session().query(RhodeCodeUi)\
1450 1452 .filter(RhodeCodeUi.ui_key == cls.NAME_SEP)
1451 1453 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1452 1454 return q.one().ui_value
1453 1455
1454 1456 @classmethod
1455 1457 def is_valid(cls, repo_name):
1456 1458 """
1457 1459 returns True if given repo name is a valid filesystem repository
1458 1460
1459 1461 :param cls:
1460 1462 :param repo_name:
1461 1463 """
1462 1464 from rhodecode.lib.utils import is_valid_repo
1463 1465
1464 1466 return is_valid_repo(repo_name, cls.base_path())
1465 1467
1466 1468 @classmethod
1467 1469 def get_all_repos(cls, user_id=Optional(None), group_id=Optional(None),
1468 1470 case_insensitive=True):
1469 1471 q = Repository.query()
1470 1472
1471 1473 if not isinstance(user_id, Optional):
1472 1474 q = q.filter(Repository.user_id == user_id)
1473 1475
1474 1476 if not isinstance(group_id, Optional):
1475 1477 q = q.filter(Repository.group_id == group_id)
1476 1478
1477 1479 if case_insensitive:
1478 1480 q = q.order_by(func.lower(Repository.repo_name))
1479 1481 else:
1480 1482 q = q.order_by(Repository.repo_name)
1481 1483 return q.all()
1482 1484
1483 1485 @property
1484 1486 def forks(self):
1485 1487 """
1486 1488 Return forks of this repo
1487 1489 """
1488 1490 return Repository.get_repo_forks(self.repo_id)
1489 1491
1490 1492 @property
1491 1493 def parent(self):
1492 1494 """
1493 1495 Returns fork parent
1494 1496 """
1495 1497 return self.fork
1496 1498
1497 1499 @property
1498 1500 def just_name(self):
1499 1501 return self.repo_name.split(self.NAME_SEP)[-1]
1500 1502
1501 1503 @property
1502 1504 def groups_with_parents(self):
1503 1505 groups = []
1504 1506 if self.group is None:
1505 1507 return groups
1506 1508
1507 1509 cur_gr = self.group
1508 1510 groups.insert(0, cur_gr)
1509 1511 while 1:
1510 1512 gr = getattr(cur_gr, 'parent_group', None)
1511 1513 cur_gr = cur_gr.parent_group
1512 1514 if gr is None:
1513 1515 break
1514 1516 groups.insert(0, gr)
1515 1517
1516 1518 return groups
1517 1519
1518 1520 @property
1519 1521 def groups_and_repo(self):
1520 1522 return self.groups_with_parents, self
1521 1523
1522 1524 @LazyProperty
1523 1525 def repo_path(self):
1524 1526 """
1525 1527 Returns base full path for that repository means where it actually
1526 1528 exists on a filesystem
1527 1529 """
1528 1530 q = Session().query(RhodeCodeUi).filter(
1529 1531 RhodeCodeUi.ui_key == self.NAME_SEP)
1530 1532 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
1531 1533 return q.one().ui_value
1532 1534
1533 1535 @property
1534 1536 def repo_full_path(self):
1535 1537 p = [self.repo_path]
1536 1538 # we need to split the name by / since this is how we store the
1537 1539 # names in the database, but that eventually needs to be converted
1538 1540 # into a valid system path
1539 1541 p += self.repo_name.split(self.NAME_SEP)
1540 1542 return os.path.join(*map(safe_unicode, p))
1541 1543
1542 1544 @property
1543 1545 def cache_keys(self):
1544 1546 """
1545 1547 Returns associated cache keys for that repo
1546 1548 """
1547 1549 return CacheKey.query()\
1548 1550 .filter(CacheKey.cache_args == self.repo_name)\
1549 1551 .order_by(CacheKey.cache_key)\
1550 1552 .all()
1551 1553
1552 1554 def get_new_name(self, repo_name):
1553 1555 """
1554 1556 returns new full repository name based on assigned group and new new
1555 1557
1556 1558 :param group_name:
1557 1559 """
1558 1560 path_prefix = self.group.full_path_splitted if self.group else []
1559 1561 return self.NAME_SEP.join(path_prefix + [repo_name])
1560 1562
1561 1563 @property
1562 1564 def _config(self):
1563 1565 """
1564 1566 Returns db based config object.
1565 1567 """
1566 1568 from rhodecode.lib.utils import make_db_config
1567 1569 return make_db_config(clear_session=False, repo=self)
1568 1570
1569 1571 def permissions(self, with_admins=True, with_owner=True):
1570 1572 q = UserRepoToPerm.query().filter(UserRepoToPerm.repository == self)
1571 1573 q = q.options(joinedload(UserRepoToPerm.repository),
1572 1574 joinedload(UserRepoToPerm.user),
1573 1575 joinedload(UserRepoToPerm.permission),)
1574 1576
1575 1577 # get owners and admins and permissions. We do a trick of re-writing
1576 1578 # objects from sqlalchemy to named-tuples due to sqlalchemy session
1577 1579 # has a global reference and changing one object propagates to all
1578 1580 # others. This means if admin is also an owner admin_row that change
1579 1581 # would propagate to both objects
1580 1582 perm_rows = []
1581 1583 for _usr in q.all():
1582 1584 usr = AttributeDict(_usr.user.get_dict())
1583 1585 usr.permission = _usr.permission.permission_name
1584 1586 perm_rows.append(usr)
1585 1587
1586 1588 # filter the perm rows by 'default' first and then sort them by
1587 1589 # admin,write,read,none permissions sorted again alphabetically in
1588 1590 # each group
1589 1591 perm_rows = sorted(perm_rows, key=display_sort)
1590 1592
1591 1593 _admin_perm = 'repository.admin'
1592 1594 owner_row = []
1593 1595 if with_owner:
1594 1596 usr = AttributeDict(self.user.get_dict())
1595 1597 usr.owner_row = True
1596 1598 usr.permission = _admin_perm
1597 1599 owner_row.append(usr)
1598 1600
1599 1601 super_admin_rows = []
1600 1602 if with_admins:
1601 1603 for usr in User.get_all_super_admins():
1602 1604 # if this admin is also owner, don't double the record
1603 1605 if usr.user_id == owner_row[0].user_id:
1604 1606 owner_row[0].admin_row = True
1605 1607 else:
1606 1608 usr = AttributeDict(usr.get_dict())
1607 1609 usr.admin_row = True
1608 1610 usr.permission = _admin_perm
1609 1611 super_admin_rows.append(usr)
1610 1612
1611 1613 return super_admin_rows + owner_row + perm_rows
1612 1614
1613 1615 def permission_user_groups(self):
1614 1616 q = UserGroupRepoToPerm.query().filter(
1615 1617 UserGroupRepoToPerm.repository == self)
1616 1618 q = q.options(joinedload(UserGroupRepoToPerm.repository),
1617 1619 joinedload(UserGroupRepoToPerm.users_group),
1618 1620 joinedload(UserGroupRepoToPerm.permission),)
1619 1621
1620 1622 perm_rows = []
1621 1623 for _user_group in q.all():
1622 1624 usr = AttributeDict(_user_group.users_group.get_dict())
1623 1625 usr.permission = _user_group.permission.permission_name
1624 1626 perm_rows.append(usr)
1625 1627
1626 1628 return perm_rows
1627 1629
1628 1630 def get_api_data(self, include_secrets=False):
1629 1631 """
1630 1632 Common function for generating repo api data
1631 1633
1632 1634 :param include_secrets: See :meth:`User.get_api_data`.
1633 1635
1634 1636 """
1635 1637 # TODO: mikhail: Here there is an anti-pattern, we probably need to
1636 1638 # move this methods on models level.
1637 1639 from rhodecode.model.settings import SettingsModel
1638 1640
1639 1641 repo = self
1640 1642 _user_id, _time, _reason = self.locked
1641 1643
1642 1644 data = {
1643 1645 'repo_id': repo.repo_id,
1644 1646 'repo_name': repo.repo_name,
1645 1647 'repo_type': repo.repo_type,
1646 1648 'clone_uri': repo.clone_uri or '',
1647 1649 'url': url('summary_home', repo_name=self.repo_name, qualified=True),
1648 1650 'private': repo.private,
1649 1651 'created_on': repo.created_on,
1650 1652 'description': repo.description,
1651 1653 'landing_rev': repo.landing_rev,
1652 1654 'owner': repo.user.username,
1653 1655 'fork_of': repo.fork.repo_name if repo.fork else None,
1654 1656 'enable_statistics': repo.enable_statistics,
1655 1657 'enable_locking': repo.enable_locking,
1656 1658 'enable_downloads': repo.enable_downloads,
1657 1659 'last_changeset': repo.changeset_cache,
1658 1660 'locked_by': User.get(_user_id).get_api_data(
1659 1661 include_secrets=include_secrets) if _user_id else None,
1660 1662 'locked_date': time_to_datetime(_time) if _time else None,
1661 1663 'lock_reason': _reason if _reason else None,
1662 1664 }
1663 1665
1664 1666 # TODO: mikhail: should be per-repo settings here
1665 1667 rc_config = SettingsModel().get_all_settings()
1666 1668 repository_fields = str2bool(
1667 1669 rc_config.get('rhodecode_repository_fields'))
1668 1670 if repository_fields:
1669 1671 for f in self.extra_fields:
1670 1672 data[f.field_key_prefixed] = f.field_value
1671 1673
1672 1674 return data
1673 1675
1674 1676 @classmethod
1675 1677 def lock(cls, repo, user_id, lock_time=None, lock_reason=None):
1676 1678 if not lock_time:
1677 1679 lock_time = time.time()
1678 1680 if not lock_reason:
1679 1681 lock_reason = cls.LOCK_AUTOMATIC
1680 1682 repo.locked = [user_id, lock_time, lock_reason]
1681 1683 Session().add(repo)
1682 1684 Session().commit()
1683 1685
1684 1686 @classmethod
1685 1687 def unlock(cls, repo):
1686 1688 repo.locked = None
1687 1689 Session().add(repo)
1688 1690 Session().commit()
1689 1691
1690 1692 @classmethod
1691 1693 def getlock(cls, repo):
1692 1694 return repo.locked
1693 1695
1694 1696 def is_user_lock(self, user_id):
1695 1697 if self.lock[0]:
1696 1698 lock_user_id = safe_int(self.lock[0])
1697 1699 user_id = safe_int(user_id)
1698 1700 # both are ints, and they are equal
1699 1701 return all([lock_user_id, user_id]) and lock_user_id == user_id
1700 1702
1701 1703 return False
1702 1704
1703 1705 def get_locking_state(self, action, user_id, only_when_enabled=True):
1704 1706 """
1705 1707 Checks locking on this repository, if locking is enabled and lock is
1706 1708 present returns a tuple of make_lock, locked, locked_by.
1707 1709 make_lock can have 3 states None (do nothing) True, make lock
1708 1710 False release lock, This value is later propagated to hooks, which
1709 1711 do the locking. Think about this as signals passed to hooks what to do.
1710 1712
1711 1713 """
1712 1714 # TODO: johbo: This is part of the business logic and should be moved
1713 1715 # into the RepositoryModel.
1714 1716
1715 1717 if action not in ('push', 'pull'):
1716 1718 raise ValueError("Invalid action value: %s" % repr(action))
1717 1719
1718 1720 # defines if locked error should be thrown to user
1719 1721 currently_locked = False
1720 1722 # defines if new lock should be made, tri-state
1721 1723 make_lock = None
1722 1724 repo = self
1723 1725 user = User.get(user_id)
1724 1726
1725 1727 lock_info = repo.locked
1726 1728
1727 1729 if repo and (repo.enable_locking or not only_when_enabled):
1728 1730 if action == 'push':
1729 1731 # check if it's already locked !, if it is compare users
1730 1732 locked_by_user_id = lock_info[0]
1731 1733 if user.user_id == locked_by_user_id:
1732 1734 log.debug(
1733 1735 'Got `push` action from user %s, now unlocking', user)
1734 1736 # unlock if we have push from user who locked
1735 1737 make_lock = False
1736 1738 else:
1737 1739 # we're not the same user who locked, ban with
1738 1740 # code defined in settings (default is 423 HTTP Locked) !
1739 1741 log.debug('Repo %s is currently locked by %s', repo, user)
1740 1742 currently_locked = True
1741 1743 elif action == 'pull':
1742 1744 # [0] user [1] date
1743 1745 if lock_info[0] and lock_info[1]:
1744 1746 log.debug('Repo %s is currently locked by %s', repo, user)
1745 1747 currently_locked = True
1746 1748 else:
1747 1749 log.debug('Setting lock on repo %s by %s', repo, user)
1748 1750 make_lock = True
1749 1751
1750 1752 else:
1751 1753 log.debug('Repository %s do not have locking enabled', repo)
1752 1754
1753 1755 log.debug('FINAL locking values make_lock:%s,locked:%s,locked_by:%s',
1754 1756 make_lock, currently_locked, lock_info)
1755 1757
1756 1758 from rhodecode.lib.auth import HasRepoPermissionAny
1757 1759 perm_check = HasRepoPermissionAny('repository.write', 'repository.admin')
1758 1760 if make_lock and not perm_check(repo_name=repo.repo_name, user=user):
1759 1761 # if we don't have at least write permission we cannot make a lock
1760 1762 log.debug('lock state reset back to FALSE due to lack '
1761 1763 'of at least read permission')
1762 1764 make_lock = False
1763 1765
1764 1766 return make_lock, currently_locked, lock_info
1765 1767
1766 1768 @property
1767 1769 def last_db_change(self):
1768 1770 return self.updated_on
1769 1771
1770 1772 @property
1771 1773 def clone_uri_hidden(self):
1772 1774 clone_uri = self.clone_uri
1773 1775 if clone_uri:
1774 1776 import urlobject
1775 1777 url_obj = urlobject.URLObject(clone_uri)
1776 1778 if url_obj.password:
1777 1779 clone_uri = url_obj.with_password('*****')
1778 1780 return clone_uri
1779 1781
1780 1782 def clone_url(self, **override):
1781 1783 qualified_home_url = url('home', qualified=True)
1782 1784
1783 1785 uri_tmpl = None
1784 1786 if 'with_id' in override:
1785 1787 uri_tmpl = self.DEFAULT_CLONE_URI_ID
1786 1788 del override['with_id']
1787 1789
1788 1790 if 'uri_tmpl' in override:
1789 1791 uri_tmpl = override['uri_tmpl']
1790 1792 del override['uri_tmpl']
1791 1793
1792 1794 # we didn't override our tmpl from **overrides
1793 1795 if not uri_tmpl:
1794 1796 uri_tmpl = self.DEFAULT_CLONE_URI
1795 1797 try:
1796 1798 from pylons import tmpl_context as c
1797 1799 uri_tmpl = c.clone_uri_tmpl
1798 1800 except Exception:
1799 1801 # in any case if we call this outside of request context,
1800 1802 # ie, not having tmpl_context set up
1801 1803 pass
1802 1804
1803 1805 return get_clone_url(uri_tmpl=uri_tmpl,
1804 1806 qualifed_home_url=qualified_home_url,
1805 1807 repo_name=self.repo_name,
1806 1808 repo_id=self.repo_id, **override)
1807 1809
1808 1810 def set_state(self, state):
1809 1811 self.repo_state = state
1810 1812 Session().add(self)
1811 1813 #==========================================================================
1812 1814 # SCM PROPERTIES
1813 1815 #==========================================================================
1814 1816
1815 1817 def get_commit(self, commit_id=None, commit_idx=None, pre_load=None):
1816 1818 return get_commit_safe(
1817 1819 self.scm_instance(), commit_id, commit_idx, pre_load=pre_load)
1818 1820
1819 1821 def get_changeset(self, rev=None, pre_load=None):
1820 1822 warnings.warn("Use get_commit", DeprecationWarning)
1821 1823 commit_id = None
1822 1824 commit_idx = None
1823 1825 if isinstance(rev, basestring):
1824 1826 commit_id = rev
1825 1827 else:
1826 1828 commit_idx = rev
1827 1829 return self.get_commit(commit_id=commit_id, commit_idx=commit_idx,
1828 1830 pre_load=pre_load)
1829 1831
1830 1832 def get_landing_commit(self):
1831 1833 """
1832 1834 Returns landing commit, or if that doesn't exist returns the tip
1833 1835 """
1834 1836 _rev_type, _rev = self.landing_rev
1835 1837 commit = self.get_commit(_rev)
1836 1838 if isinstance(commit, EmptyCommit):
1837 1839 return self.get_commit()
1838 1840 return commit
1839 1841
1840 1842 def update_commit_cache(self, cs_cache=None, config=None):
1841 1843 """
1842 1844 Update cache of last changeset for repository, keys should be::
1843 1845
1844 1846 short_id
1845 1847 raw_id
1846 1848 revision
1847 1849 parents
1848 1850 message
1849 1851 date
1850 1852 author
1851 1853
1852 1854 :param cs_cache:
1853 1855 """
1854 1856 from rhodecode.lib.vcs.backends.base import BaseChangeset
1855 1857 if cs_cache is None:
1856 1858 # use no-cache version here
1857 1859 scm_repo = self.scm_instance(cache=False, config=config)
1858 1860 if scm_repo:
1859 1861 cs_cache = scm_repo.get_commit(
1860 1862 pre_load=["author", "date", "message", "parents"])
1861 1863 else:
1862 1864 cs_cache = EmptyCommit()
1863 1865
1864 1866 if isinstance(cs_cache, BaseChangeset):
1865 1867 cs_cache = cs_cache.__json__()
1866 1868
1867 1869 def is_outdated(new_cs_cache):
1868 1870 if (new_cs_cache['raw_id'] != self.changeset_cache['raw_id'] or
1869 1871 new_cs_cache['revision'] != self.changeset_cache['revision']):
1870 1872 return True
1871 1873 return False
1872 1874
1873 1875 # check if we have maybe already latest cached revision
1874 1876 if is_outdated(cs_cache) or not self.changeset_cache:
1875 1877 _default = datetime.datetime.fromtimestamp(0)
1876 1878 last_change = cs_cache.get('date') or _default
1877 1879 log.debug('updated repo %s with new cs cache %s',
1878 1880 self.repo_name, cs_cache)
1879 1881 self.updated_on = last_change
1880 1882 self.changeset_cache = cs_cache
1881 1883 Session().add(self)
1882 1884 Session().commit()
1883 1885 else:
1884 1886 log.debug('Skipping update_commit_cache for repo:`%s` '
1885 1887 'commit already with latest changes', self.repo_name)
1886 1888
1887 1889 @property
1888 1890 def tip(self):
1889 1891 return self.get_commit('tip')
1890 1892
1891 1893 @property
1892 1894 def author(self):
1893 1895 return self.tip.author
1894 1896
1895 1897 @property
1896 1898 def last_change(self):
1897 1899 return self.scm_instance().last_change
1898 1900
1899 1901 def get_comments(self, revisions=None):
1900 1902 """
1901 1903 Returns comments for this repository grouped by revisions
1902 1904
1903 1905 :param revisions: filter query by revisions only
1904 1906 """
1905 1907 cmts = ChangesetComment.query()\
1906 1908 .filter(ChangesetComment.repo == self)
1907 1909 if revisions:
1908 1910 cmts = cmts.filter(ChangesetComment.revision.in_(revisions))
1909 1911 grouped = collections.defaultdict(list)
1910 1912 for cmt in cmts.all():
1911 1913 grouped[cmt.revision].append(cmt)
1912 1914 return grouped
1913 1915
1914 1916 def statuses(self, revisions=None):
1915 1917 """
1916 1918 Returns statuses for this repository
1917 1919
1918 1920 :param revisions: list of revisions to get statuses for
1919 1921 """
1920 1922 statuses = ChangesetStatus.query()\
1921 1923 .filter(ChangesetStatus.repo == self)\
1922 1924 .filter(ChangesetStatus.version == 0)
1923 1925
1924 1926 if revisions:
1925 1927 # Try doing the filtering in chunks to avoid hitting limits
1926 1928 size = 500
1927 1929 status_results = []
1928 1930 for chunk in xrange(0, len(revisions), size):
1929 1931 status_results += statuses.filter(
1930 1932 ChangesetStatus.revision.in_(
1931 1933 revisions[chunk: chunk+size])
1932 1934 ).all()
1933 1935 else:
1934 1936 status_results = statuses.all()
1935 1937
1936 1938 grouped = {}
1937 1939
1938 1940 # maybe we have open new pullrequest without a status?
1939 1941 stat = ChangesetStatus.STATUS_UNDER_REVIEW
1940 1942 status_lbl = ChangesetStatus.get_status_lbl(stat)
1941 1943 for pr in PullRequest.query().filter(PullRequest.source_repo == self).all():
1942 1944 for rev in pr.revisions:
1943 1945 pr_id = pr.pull_request_id
1944 1946 pr_repo = pr.target_repo.repo_name
1945 1947 grouped[rev] = [stat, status_lbl, pr_id, pr_repo]
1946 1948
1947 1949 for stat in status_results:
1948 1950 pr_id = pr_repo = None
1949 1951 if stat.pull_request:
1950 1952 pr_id = stat.pull_request.pull_request_id
1951 1953 pr_repo = stat.pull_request.target_repo.repo_name
1952 1954 grouped[stat.revision] = [str(stat.status), stat.status_lbl,
1953 1955 pr_id, pr_repo]
1954 1956 return grouped
1955 1957
1956 1958 # ==========================================================================
1957 1959 # SCM CACHE INSTANCE
1958 1960 # ==========================================================================
1959 1961
1960 1962 def scm_instance(self, **kwargs):
1961 1963 import rhodecode
1962 1964
1963 1965 # Passing a config will not hit the cache currently only used
1964 1966 # for repo2dbmapper
1965 1967 config = kwargs.pop('config', None)
1966 1968 cache = kwargs.pop('cache', None)
1967 1969 full_cache = str2bool(rhodecode.CONFIG.get('vcs_full_cache'))
1968 1970 # if cache is NOT defined use default global, else we have a full
1969 1971 # control over cache behaviour
1970 1972 if cache is None and full_cache and not config:
1971 1973 return self._get_instance_cached()
1972 1974 return self._get_instance(cache=bool(cache), config=config)
1973 1975
1974 1976 def _get_instance_cached(self):
1975 1977 @cache_region('long_term')
1976 1978 def _get_repo(cache_key):
1977 1979 return self._get_instance()
1978 1980
1979 1981 invalidator_context = CacheKey.repo_context_cache(
1980 1982 _get_repo, self.repo_name, None, thread_scoped=True)
1981 1983
1982 1984 with invalidator_context as context:
1983 1985 context.invalidate()
1984 1986 repo = context.compute()
1985 1987
1986 1988 return repo
1987 1989
1988 1990 def _get_instance(self, cache=True, config=None):
1989 1991 config = config or self._config
1990 1992 custom_wire = {
1991 1993 'cache': cache # controls the vcs.remote cache
1992 1994 }
1993 1995
1994 1996 repo = get_vcs_instance(
1995 1997 repo_path=safe_str(self.repo_full_path),
1996 1998 config=config,
1997 1999 with_wire=custom_wire,
1998 2000 create=False)
1999 2001
2000 2002 return repo
2001 2003
2002 2004 def __json__(self):
2003 2005 return {'landing_rev': self.landing_rev}
2004 2006
2005 2007 def get_dict(self):
2006 2008
2007 2009 # Since we transformed `repo_name` to a hybrid property, we need to
2008 2010 # keep compatibility with the code which uses `repo_name` field.
2009 2011
2010 2012 result = super(Repository, self).get_dict()
2011 2013 result['repo_name'] = result.pop('_repo_name', None)
2012 2014 return result
2013 2015
2014 2016
2015 2017 class RepoGroup(Base, BaseModel):
2016 2018 __tablename__ = 'groups'
2017 2019 __table_args__ = (
2018 2020 UniqueConstraint('group_name', 'group_parent_id'),
2019 2021 CheckConstraint('group_id != group_parent_id'),
2020 2022 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2021 2023 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2022 2024 )
2023 2025 __mapper_args__ = {'order_by': 'group_name'}
2024 2026
2025 2027 CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
2026 2028
2027 2029 group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2028 2030 group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
2029 2031 group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
2030 2032 group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
2031 2033 enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
2032 2034 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=False, default=None)
2033 2035 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2034 2036
2035 2037 repo_group_to_perm = relationship('UserRepoGroupToPerm', cascade='all', order_by='UserRepoGroupToPerm.group_to_perm_id')
2036 2038 users_group_to_perm = relationship('UserGroupRepoGroupToPerm', cascade='all')
2037 2039 parent_group = relationship('RepoGroup', remote_side=group_id)
2038 2040 user = relationship('User')
2039 2041 integrations = relationship('Integration',
2040 2042 cascade="all, delete, delete-orphan")
2041 2043
2042 2044 def __init__(self, group_name='', parent_group=None):
2043 2045 self.group_name = group_name
2044 2046 self.parent_group = parent_group
2045 2047
2046 2048 def __unicode__(self):
2047 2049 return u"<%s('id:%s:%s')>" % (self.__class__.__name__, self.group_id,
2048 2050 self.group_name)
2049 2051
2050 2052 @classmethod
2051 2053 def _generate_choice(cls, repo_group):
2052 2054 from webhelpers.html import literal as _literal
2053 2055 _name = lambda k: _literal(cls.CHOICES_SEPARATOR.join(k))
2054 2056 return repo_group.group_id, _name(repo_group.full_path_splitted)
2055 2057
2056 2058 @classmethod
2057 2059 def groups_choices(cls, groups=None, show_empty_group=True):
2058 2060 if not groups:
2059 2061 groups = cls.query().all()
2060 2062
2061 2063 repo_groups = []
2062 2064 if show_empty_group:
2063 2065 repo_groups = [('-1', u'-- %s --' % _('No parent'))]
2064 2066
2065 2067 repo_groups.extend([cls._generate_choice(x) for x in groups])
2066 2068
2067 2069 repo_groups = sorted(
2068 2070 repo_groups, key=lambda t: t[1].split(cls.CHOICES_SEPARATOR)[0])
2069 2071 return repo_groups
2070 2072
2071 2073 @classmethod
2072 2074 def url_sep(cls):
2073 2075 return URL_SEP
2074 2076
2075 2077 @classmethod
2076 2078 def get_by_group_name(cls, group_name, cache=False, case_insensitive=False):
2077 2079 if case_insensitive:
2078 2080 gr = cls.query().filter(func.lower(cls.group_name)
2079 2081 == func.lower(group_name))
2080 2082 else:
2081 2083 gr = cls.query().filter(cls.group_name == group_name)
2082 2084 if cache:
2083 2085 gr = gr.options(FromCache(
2084 2086 "sql_cache_short",
2085 2087 "get_group_%s" % _hash_key(group_name)))
2086 2088 return gr.scalar()
2087 2089
2088 2090 @classmethod
2089 2091 def get_all_repo_groups(cls, user_id=Optional(None), group_id=Optional(None),
2090 2092 case_insensitive=True):
2091 2093 q = RepoGroup.query()
2092 2094
2093 2095 if not isinstance(user_id, Optional):
2094 2096 q = q.filter(RepoGroup.user_id == user_id)
2095 2097
2096 2098 if not isinstance(group_id, Optional):
2097 2099 q = q.filter(RepoGroup.group_parent_id == group_id)
2098 2100
2099 2101 if case_insensitive:
2100 2102 q = q.order_by(func.lower(RepoGroup.group_name))
2101 2103 else:
2102 2104 q = q.order_by(RepoGroup.group_name)
2103 2105 return q.all()
2104 2106
2105 2107 @property
2106 2108 def parents(self):
2107 2109 parents_recursion_limit = 10
2108 2110 groups = []
2109 2111 if self.parent_group is None:
2110 2112 return groups
2111 2113 cur_gr = self.parent_group
2112 2114 groups.insert(0, cur_gr)
2113 2115 cnt = 0
2114 2116 while 1:
2115 2117 cnt += 1
2116 2118 gr = getattr(cur_gr, 'parent_group', None)
2117 2119 cur_gr = cur_gr.parent_group
2118 2120 if gr is None:
2119 2121 break
2120 2122 if cnt == parents_recursion_limit:
2121 2123 # this will prevent accidental infinit loops
2122 2124 log.error(('more than %s parents found for group %s, stopping '
2123 2125 'recursive parent fetching' % (parents_recursion_limit, self)))
2124 2126 break
2125 2127
2126 2128 groups.insert(0, gr)
2127 2129 return groups
2128 2130
2129 2131 @property
2130 2132 def children(self):
2131 2133 return RepoGroup.query().filter(RepoGroup.parent_group == self)
2132 2134
2133 2135 @property
2134 2136 def name(self):
2135 2137 return self.group_name.split(RepoGroup.url_sep())[-1]
2136 2138
2137 2139 @property
2138 2140 def full_path(self):
2139 2141 return self.group_name
2140 2142
2141 2143 @property
2142 2144 def full_path_splitted(self):
2143 2145 return self.group_name.split(RepoGroup.url_sep())
2144 2146
2145 2147 @property
2146 2148 def repositories(self):
2147 2149 return Repository.query()\
2148 2150 .filter(Repository.group == self)\
2149 2151 .order_by(Repository.repo_name)
2150 2152
2151 2153 @property
2152 2154 def repositories_recursive_count(self):
2153 2155 cnt = self.repositories.count()
2154 2156
2155 2157 def children_count(group):
2156 2158 cnt = 0
2157 2159 for child in group.children:
2158 2160 cnt += child.repositories.count()
2159 2161 cnt += children_count(child)
2160 2162 return cnt
2161 2163
2162 2164 return cnt + children_count(self)
2163 2165
2164 2166 def _recursive_objects(self, include_repos=True):
2165 2167 all_ = []
2166 2168
2167 2169 def _get_members(root_gr):
2168 2170 if include_repos:
2169 2171 for r in root_gr.repositories:
2170 2172 all_.append(r)
2171 2173 childs = root_gr.children.all()
2172 2174 if childs:
2173 2175 for gr in childs:
2174 2176 all_.append(gr)
2175 2177 _get_members(gr)
2176 2178
2177 2179 _get_members(self)
2178 2180 return [self] + all_
2179 2181
2180 2182 def recursive_groups_and_repos(self):
2181 2183 """
2182 2184 Recursive return all groups, with repositories in those groups
2183 2185 """
2184 2186 return self._recursive_objects()
2185 2187
2186 2188 def recursive_groups(self):
2187 2189 """
2188 2190 Returns all children groups for this group including children of children
2189 2191 """
2190 2192 return self._recursive_objects(include_repos=False)
2191 2193
2192 2194 def get_new_name(self, group_name):
2193 2195 """
2194 2196 returns new full group name based on parent and new name
2195 2197
2196 2198 :param group_name:
2197 2199 """
2198 2200 path_prefix = (self.parent_group.full_path_splitted if
2199 2201 self.parent_group else [])
2200 2202 return RepoGroup.url_sep().join(path_prefix + [group_name])
2201 2203
2202 2204 def permissions(self, with_admins=True, with_owner=True):
2203 2205 q = UserRepoGroupToPerm.query().filter(UserRepoGroupToPerm.group == self)
2204 2206 q = q.options(joinedload(UserRepoGroupToPerm.group),
2205 2207 joinedload(UserRepoGroupToPerm.user),
2206 2208 joinedload(UserRepoGroupToPerm.permission),)
2207 2209
2208 2210 # get owners and admins and permissions. We do a trick of re-writing
2209 2211 # objects from sqlalchemy to named-tuples due to sqlalchemy session
2210 2212 # has a global reference and changing one object propagates to all
2211 2213 # others. This means if admin is also an owner admin_row that change
2212 2214 # would propagate to both objects
2213 2215 perm_rows = []
2214 2216 for _usr in q.all():
2215 2217 usr = AttributeDict(_usr.user.get_dict())
2216 2218 usr.permission = _usr.permission.permission_name
2217 2219 perm_rows.append(usr)
2218 2220
2219 2221 # filter the perm rows by 'default' first and then sort them by
2220 2222 # admin,write,read,none permissions sorted again alphabetically in
2221 2223 # each group
2222 2224 perm_rows = sorted(perm_rows, key=display_sort)
2223 2225
2224 2226 _admin_perm = 'group.admin'
2225 2227 owner_row = []
2226 2228 if with_owner:
2227 2229 usr = AttributeDict(self.user.get_dict())
2228 2230 usr.owner_row = True
2229 2231 usr.permission = _admin_perm
2230 2232 owner_row.append(usr)
2231 2233
2232 2234 super_admin_rows = []
2233 2235 if with_admins:
2234 2236 for usr in User.get_all_super_admins():
2235 2237 # if this admin is also owner, don't double the record
2236 2238 if usr.user_id == owner_row[0].user_id:
2237 2239 owner_row[0].admin_row = True
2238 2240 else:
2239 2241 usr = AttributeDict(usr.get_dict())
2240 2242 usr.admin_row = True
2241 2243 usr.permission = _admin_perm
2242 2244 super_admin_rows.append(usr)
2243 2245
2244 2246 return super_admin_rows + owner_row + perm_rows
2245 2247
2246 2248 def permission_user_groups(self):
2247 2249 q = UserGroupRepoGroupToPerm.query().filter(UserGroupRepoGroupToPerm.group == self)
2248 2250 q = q.options(joinedload(UserGroupRepoGroupToPerm.group),
2249 2251 joinedload(UserGroupRepoGroupToPerm.users_group),
2250 2252 joinedload(UserGroupRepoGroupToPerm.permission),)
2251 2253
2252 2254 perm_rows = []
2253 2255 for _user_group in q.all():
2254 2256 usr = AttributeDict(_user_group.users_group.get_dict())
2255 2257 usr.permission = _user_group.permission.permission_name
2256 2258 perm_rows.append(usr)
2257 2259
2258 2260 return perm_rows
2259 2261
2260 2262 def get_api_data(self):
2261 2263 """
2262 2264 Common function for generating api data
2263 2265
2264 2266 """
2265 2267 group = self
2266 2268 data = {
2267 2269 'group_id': group.group_id,
2268 2270 'group_name': group.group_name,
2269 2271 'group_description': group.group_description,
2270 2272 'parent_group': group.parent_group.group_name if group.parent_group else None,
2271 2273 'repositories': [x.repo_name for x in group.repositories],
2272 2274 'owner': group.user.username,
2273 2275 }
2274 2276 return data
2275 2277
2276 2278
2277 2279 class Permission(Base, BaseModel):
2278 2280 __tablename__ = 'permissions'
2279 2281 __table_args__ = (
2280 2282 Index('p_perm_name_idx', 'permission_name'),
2281 2283 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2282 2284 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2283 2285 )
2284 2286 PERMS = [
2285 2287 ('hg.admin', _('RhodeCode Super Administrator')),
2286 2288
2287 2289 ('repository.none', _('Repository no access')),
2288 2290 ('repository.read', _('Repository read access')),
2289 2291 ('repository.write', _('Repository write access')),
2290 2292 ('repository.admin', _('Repository admin access')),
2291 2293
2292 2294 ('group.none', _('Repository group no access')),
2293 2295 ('group.read', _('Repository group read access')),
2294 2296 ('group.write', _('Repository group write access')),
2295 2297 ('group.admin', _('Repository group admin access')),
2296 2298
2297 2299 ('usergroup.none', _('User group no access')),
2298 2300 ('usergroup.read', _('User group read access')),
2299 2301 ('usergroup.write', _('User group write access')),
2300 2302 ('usergroup.admin', _('User group admin access')),
2301 2303
2302 2304 ('hg.repogroup.create.false', _('Repository Group creation disabled')),
2303 2305 ('hg.repogroup.create.true', _('Repository Group creation enabled')),
2304 2306
2305 2307 ('hg.usergroup.create.false', _('User Group creation disabled')),
2306 2308 ('hg.usergroup.create.true', _('User Group creation enabled')),
2307 2309
2308 2310 ('hg.create.none', _('Repository creation disabled')),
2309 2311 ('hg.create.repository', _('Repository creation enabled')),
2310 2312 ('hg.create.write_on_repogroup.true', _('Repository creation enabled with write permission to a repository group')),
2311 2313 ('hg.create.write_on_repogroup.false', _('Repository creation disabled with write permission to a repository group')),
2312 2314
2313 2315 ('hg.fork.none', _('Repository forking disabled')),
2314 2316 ('hg.fork.repository', _('Repository forking enabled')),
2315 2317
2316 2318 ('hg.register.none', _('Registration disabled')),
2317 2319 ('hg.register.manual_activate', _('User Registration with manual account activation')),
2318 2320 ('hg.register.auto_activate', _('User Registration with automatic account activation')),
2319 2321
2320 2322 ('hg.extern_activate.manual', _('Manual activation of external account')),
2321 2323 ('hg.extern_activate.auto', _('Automatic activation of external account')),
2322 2324
2323 2325 ('hg.inherit_default_perms.false', _('Inherit object permissions from default user disabled')),
2324 2326 ('hg.inherit_default_perms.true', _('Inherit object permissions from default user enabled')),
2325 2327 ]
2326 2328
2327 2329 # definition of system default permissions for DEFAULT user
2328 2330 DEFAULT_USER_PERMISSIONS = [
2329 2331 'repository.read',
2330 2332 'group.read',
2331 2333 'usergroup.read',
2332 2334 'hg.create.repository',
2333 2335 'hg.repogroup.create.false',
2334 2336 'hg.usergroup.create.false',
2335 2337 'hg.create.write_on_repogroup.true',
2336 2338 'hg.fork.repository',
2337 2339 'hg.register.manual_activate',
2338 2340 'hg.extern_activate.auto',
2339 2341 'hg.inherit_default_perms.true',
2340 2342 ]
2341 2343
2342 2344 # defines which permissions are more important higher the more important
2343 2345 # Weight defines which permissions are more important.
2344 2346 # The higher number the more important.
2345 2347 PERM_WEIGHTS = {
2346 2348 'repository.none': 0,
2347 2349 'repository.read': 1,
2348 2350 'repository.write': 3,
2349 2351 'repository.admin': 4,
2350 2352
2351 2353 'group.none': 0,
2352 2354 'group.read': 1,
2353 2355 'group.write': 3,
2354 2356 'group.admin': 4,
2355 2357
2356 2358 'usergroup.none': 0,
2357 2359 'usergroup.read': 1,
2358 2360 'usergroup.write': 3,
2359 2361 'usergroup.admin': 4,
2360 2362
2361 2363 'hg.repogroup.create.false': 0,
2362 2364 'hg.repogroup.create.true': 1,
2363 2365
2364 2366 'hg.usergroup.create.false': 0,
2365 2367 'hg.usergroup.create.true': 1,
2366 2368
2367 2369 'hg.fork.none': 0,
2368 2370 'hg.fork.repository': 1,
2369 2371 'hg.create.none': 0,
2370 2372 'hg.create.repository': 1
2371 2373 }
2372 2374
2373 2375 permission_id = Column("permission_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2374 2376 permission_name = Column("permission_name", String(255), nullable=True, unique=None, default=None)
2375 2377 permission_longname = Column("permission_longname", String(255), nullable=True, unique=None, default=None)
2376 2378
2377 2379 def __unicode__(self):
2378 2380 return u"<%s('%s:%s')>" % (
2379 2381 self.__class__.__name__, self.permission_id, self.permission_name
2380 2382 )
2381 2383
2382 2384 @classmethod
2383 2385 def get_by_key(cls, key):
2384 2386 return cls.query().filter(cls.permission_name == key).scalar()
2385 2387
2386 2388 @classmethod
2387 2389 def get_default_repo_perms(cls, user_id, repo_id=None):
2388 2390 q = Session().query(UserRepoToPerm, Repository, Permission)\
2389 2391 .join((Permission, UserRepoToPerm.permission_id == Permission.permission_id))\
2390 2392 .join((Repository, UserRepoToPerm.repository_id == Repository.repo_id))\
2391 2393 .filter(UserRepoToPerm.user_id == user_id)
2392 2394 if repo_id:
2393 2395 q = q.filter(UserRepoToPerm.repository_id == repo_id)
2394 2396 return q.all()
2395 2397
2396 2398 @classmethod
2397 2399 def get_default_repo_perms_from_user_group(cls, user_id, repo_id=None):
2398 2400 q = Session().query(UserGroupRepoToPerm, Repository, Permission)\
2399 2401 .join(
2400 2402 Permission,
2401 2403 UserGroupRepoToPerm.permission_id == Permission.permission_id)\
2402 2404 .join(
2403 2405 Repository,
2404 2406 UserGroupRepoToPerm.repository_id == Repository.repo_id)\
2405 2407 .join(
2406 2408 UserGroup,
2407 2409 UserGroupRepoToPerm.users_group_id ==
2408 2410 UserGroup.users_group_id)\
2409 2411 .join(
2410 2412 UserGroupMember,
2411 2413 UserGroupRepoToPerm.users_group_id ==
2412 2414 UserGroupMember.users_group_id)\
2413 2415 .filter(
2414 2416 UserGroupMember.user_id == user_id,
2415 2417 UserGroup.users_group_active == true())
2416 2418 if repo_id:
2417 2419 q = q.filter(UserGroupRepoToPerm.repository_id == repo_id)
2418 2420 return q.all()
2419 2421
2420 2422 @classmethod
2421 2423 def get_default_group_perms(cls, user_id, repo_group_id=None):
2422 2424 q = Session().query(UserRepoGroupToPerm, RepoGroup, Permission)\
2423 2425 .join((Permission, UserRepoGroupToPerm.permission_id == Permission.permission_id))\
2424 2426 .join((RepoGroup, UserRepoGroupToPerm.group_id == RepoGroup.group_id))\
2425 2427 .filter(UserRepoGroupToPerm.user_id == user_id)
2426 2428 if repo_group_id:
2427 2429 q = q.filter(UserRepoGroupToPerm.group_id == repo_group_id)
2428 2430 return q.all()
2429 2431
2430 2432 @classmethod
2431 2433 def get_default_group_perms_from_user_group(
2432 2434 cls, user_id, repo_group_id=None):
2433 2435 q = Session().query(UserGroupRepoGroupToPerm, RepoGroup, Permission)\
2434 2436 .join(
2435 2437 Permission,
2436 2438 UserGroupRepoGroupToPerm.permission_id ==
2437 2439 Permission.permission_id)\
2438 2440 .join(
2439 2441 RepoGroup,
2440 2442 UserGroupRepoGroupToPerm.group_id == RepoGroup.group_id)\
2441 2443 .join(
2442 2444 UserGroup,
2443 2445 UserGroupRepoGroupToPerm.users_group_id ==
2444 2446 UserGroup.users_group_id)\
2445 2447 .join(
2446 2448 UserGroupMember,
2447 2449 UserGroupRepoGroupToPerm.users_group_id ==
2448 2450 UserGroupMember.users_group_id)\
2449 2451 .filter(
2450 2452 UserGroupMember.user_id == user_id,
2451 2453 UserGroup.users_group_active == true())
2452 2454 if repo_group_id:
2453 2455 q = q.filter(UserGroupRepoGroupToPerm.group_id == repo_group_id)
2454 2456 return q.all()
2455 2457
2456 2458 @classmethod
2457 2459 def get_default_user_group_perms(cls, user_id, user_group_id=None):
2458 2460 q = Session().query(UserUserGroupToPerm, UserGroup, Permission)\
2459 2461 .join((Permission, UserUserGroupToPerm.permission_id == Permission.permission_id))\
2460 2462 .join((UserGroup, UserUserGroupToPerm.user_group_id == UserGroup.users_group_id))\
2461 2463 .filter(UserUserGroupToPerm.user_id == user_id)
2462 2464 if user_group_id:
2463 2465 q = q.filter(UserUserGroupToPerm.user_group_id == user_group_id)
2464 2466 return q.all()
2465 2467
2466 2468 @classmethod
2467 2469 def get_default_user_group_perms_from_user_group(
2468 2470 cls, user_id, user_group_id=None):
2469 2471 TargetUserGroup = aliased(UserGroup, name='target_user_group')
2470 2472 q = Session().query(UserGroupUserGroupToPerm, UserGroup, Permission)\
2471 2473 .join(
2472 2474 Permission,
2473 2475 UserGroupUserGroupToPerm.permission_id ==
2474 2476 Permission.permission_id)\
2475 2477 .join(
2476 2478 TargetUserGroup,
2477 2479 UserGroupUserGroupToPerm.target_user_group_id ==
2478 2480 TargetUserGroup.users_group_id)\
2479 2481 .join(
2480 2482 UserGroup,
2481 2483 UserGroupUserGroupToPerm.user_group_id ==
2482 2484 UserGroup.users_group_id)\
2483 2485 .join(
2484 2486 UserGroupMember,
2485 2487 UserGroupUserGroupToPerm.user_group_id ==
2486 2488 UserGroupMember.users_group_id)\
2487 2489 .filter(
2488 2490 UserGroupMember.user_id == user_id,
2489 2491 UserGroup.users_group_active == true())
2490 2492 if user_group_id:
2491 2493 q = q.filter(
2492 2494 UserGroupUserGroupToPerm.user_group_id == user_group_id)
2493 2495
2494 2496 return q.all()
2495 2497
2496 2498
2497 2499 class UserRepoToPerm(Base, BaseModel):
2498 2500 __tablename__ = 'repo_to_perm'
2499 2501 __table_args__ = (
2500 2502 UniqueConstraint('user_id', 'repository_id', 'permission_id'),
2501 2503 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2502 2504 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2503 2505 )
2504 2506 repo_to_perm_id = Column("repo_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2505 2507 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2506 2508 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2507 2509 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2508 2510
2509 2511 user = relationship('User')
2510 2512 repository = relationship('Repository')
2511 2513 permission = relationship('Permission')
2512 2514
2513 2515 @classmethod
2514 2516 def create(cls, user, repository, permission):
2515 2517 n = cls()
2516 2518 n.user = user
2517 2519 n.repository = repository
2518 2520 n.permission = permission
2519 2521 Session().add(n)
2520 2522 return n
2521 2523
2522 2524 def __unicode__(self):
2523 2525 return u'<%s => %s >' % (self.user, self.repository)
2524 2526
2525 2527
2526 2528 class UserUserGroupToPerm(Base, BaseModel):
2527 2529 __tablename__ = 'user_user_group_to_perm'
2528 2530 __table_args__ = (
2529 2531 UniqueConstraint('user_id', 'user_group_id', 'permission_id'),
2530 2532 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2531 2533 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2532 2534 )
2533 2535 user_user_group_to_perm_id = Column("user_user_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2534 2536 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2535 2537 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2536 2538 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2537 2539
2538 2540 user = relationship('User')
2539 2541 user_group = relationship('UserGroup')
2540 2542 permission = relationship('Permission')
2541 2543
2542 2544 @classmethod
2543 2545 def create(cls, user, user_group, permission):
2544 2546 n = cls()
2545 2547 n.user = user
2546 2548 n.user_group = user_group
2547 2549 n.permission = permission
2548 2550 Session().add(n)
2549 2551 return n
2550 2552
2551 2553 def __unicode__(self):
2552 2554 return u'<%s => %s >' % (self.user, self.user_group)
2553 2555
2554 2556
2555 2557 class UserToPerm(Base, BaseModel):
2556 2558 __tablename__ = 'user_to_perm'
2557 2559 __table_args__ = (
2558 2560 UniqueConstraint('user_id', 'permission_id'),
2559 2561 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2560 2562 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2561 2563 )
2562 2564 user_to_perm_id = Column("user_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2563 2565 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2564 2566 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2565 2567
2566 2568 user = relationship('User')
2567 2569 permission = relationship('Permission', lazy='joined')
2568 2570
2569 2571 def __unicode__(self):
2570 2572 return u'<%s => %s >' % (self.user, self.permission)
2571 2573
2572 2574
2573 2575 class UserGroupRepoToPerm(Base, BaseModel):
2574 2576 __tablename__ = 'users_group_repo_to_perm'
2575 2577 __table_args__ = (
2576 2578 UniqueConstraint('repository_id', 'users_group_id', 'permission_id'),
2577 2579 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2578 2580 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2579 2581 )
2580 2582 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2581 2583 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2582 2584 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2583 2585 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=None, default=None)
2584 2586
2585 2587 users_group = relationship('UserGroup')
2586 2588 permission = relationship('Permission')
2587 2589 repository = relationship('Repository')
2588 2590
2589 2591 @classmethod
2590 2592 def create(cls, users_group, repository, permission):
2591 2593 n = cls()
2592 2594 n.users_group = users_group
2593 2595 n.repository = repository
2594 2596 n.permission = permission
2595 2597 Session().add(n)
2596 2598 return n
2597 2599
2598 2600 def __unicode__(self):
2599 2601 return u'<UserGroupRepoToPerm:%s => %s >' % (self.users_group, self.repository)
2600 2602
2601 2603
2602 2604 class UserGroupUserGroupToPerm(Base, BaseModel):
2603 2605 __tablename__ = 'user_group_user_group_to_perm'
2604 2606 __table_args__ = (
2605 2607 UniqueConstraint('target_user_group_id', 'user_group_id', 'permission_id'),
2606 2608 CheckConstraint('target_user_group_id != user_group_id'),
2607 2609 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2608 2610 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2609 2611 )
2610 2612 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)
2611 2613 target_user_group_id = Column("target_user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2612 2614 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2613 2615 user_group_id = Column("user_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2614 2616
2615 2617 target_user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.target_user_group_id==UserGroup.users_group_id')
2616 2618 user_group = relationship('UserGroup', primaryjoin='UserGroupUserGroupToPerm.user_group_id==UserGroup.users_group_id')
2617 2619 permission = relationship('Permission')
2618 2620
2619 2621 @classmethod
2620 2622 def create(cls, target_user_group, user_group, permission):
2621 2623 n = cls()
2622 2624 n.target_user_group = target_user_group
2623 2625 n.user_group = user_group
2624 2626 n.permission = permission
2625 2627 Session().add(n)
2626 2628 return n
2627 2629
2628 2630 def __unicode__(self):
2629 2631 return u'<UserGroupUserGroup:%s => %s >' % (self.target_user_group, self.user_group)
2630 2632
2631 2633
2632 2634 class UserGroupToPerm(Base, BaseModel):
2633 2635 __tablename__ = 'users_group_to_perm'
2634 2636 __table_args__ = (
2635 2637 UniqueConstraint('users_group_id', 'permission_id',),
2636 2638 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2637 2639 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2638 2640 )
2639 2641 users_group_to_perm_id = Column("users_group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2640 2642 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2641 2643 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2642 2644
2643 2645 users_group = relationship('UserGroup')
2644 2646 permission = relationship('Permission')
2645 2647
2646 2648
2647 2649 class UserRepoGroupToPerm(Base, BaseModel):
2648 2650 __tablename__ = 'user_repo_group_to_perm'
2649 2651 __table_args__ = (
2650 2652 UniqueConstraint('user_id', 'group_id', 'permission_id'),
2651 2653 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2652 2654 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2653 2655 )
2654 2656
2655 2657 group_to_perm_id = Column("group_to_perm_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2656 2658 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2657 2659 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2658 2660 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2659 2661
2660 2662 user = relationship('User')
2661 2663 group = relationship('RepoGroup')
2662 2664 permission = relationship('Permission')
2663 2665
2664 2666 @classmethod
2665 2667 def create(cls, user, repository_group, permission):
2666 2668 n = cls()
2667 2669 n.user = user
2668 2670 n.group = repository_group
2669 2671 n.permission = permission
2670 2672 Session().add(n)
2671 2673 return n
2672 2674
2673 2675
2674 2676 class UserGroupRepoGroupToPerm(Base, BaseModel):
2675 2677 __tablename__ = 'users_group_repo_group_to_perm'
2676 2678 __table_args__ = (
2677 2679 UniqueConstraint('users_group_id', 'group_id'),
2678 2680 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2679 2681 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2680 2682 )
2681 2683
2682 2684 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)
2683 2685 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False, unique=None, default=None)
2684 2686 group_id = Column("group_id", Integer(), ForeignKey('groups.group_id'), nullable=False, unique=None, default=None)
2685 2687 permission_id = Column("permission_id", Integer(), ForeignKey('permissions.permission_id'), nullable=False, unique=None, default=None)
2686 2688
2687 2689 users_group = relationship('UserGroup')
2688 2690 permission = relationship('Permission')
2689 2691 group = relationship('RepoGroup')
2690 2692
2691 2693 @classmethod
2692 2694 def create(cls, user_group, repository_group, permission):
2693 2695 n = cls()
2694 2696 n.users_group = user_group
2695 2697 n.group = repository_group
2696 2698 n.permission = permission
2697 2699 Session().add(n)
2698 2700 return n
2699 2701
2700 2702 def __unicode__(self):
2701 2703 return u'<UserGroupRepoGroupToPerm:%s => %s >' % (self.users_group, self.group)
2702 2704
2703 2705
2704 2706 class Statistics(Base, BaseModel):
2705 2707 __tablename__ = 'statistics'
2706 2708 __table_args__ = (
2707 2709 UniqueConstraint('repository_id'),
2708 2710 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2709 2711 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2710 2712 )
2711 2713 stat_id = Column("stat_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2712 2714 repository_id = Column("repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=False, unique=True, default=None)
2713 2715 stat_on_revision = Column("stat_on_revision", Integer(), nullable=False)
2714 2716 commit_activity = Column("commit_activity", LargeBinary(1000000), nullable=False)#JSON data
2715 2717 commit_activity_combined = Column("commit_activity_combined", LargeBinary(), nullable=False)#JSON data
2716 2718 languages = Column("languages", LargeBinary(1000000), nullable=False)#JSON data
2717 2719
2718 2720 repository = relationship('Repository', single_parent=True)
2719 2721
2720 2722
2721 2723 class UserFollowing(Base, BaseModel):
2722 2724 __tablename__ = 'user_followings'
2723 2725 __table_args__ = (
2724 2726 UniqueConstraint('user_id', 'follows_repository_id'),
2725 2727 UniqueConstraint('user_id', 'follows_user_id'),
2726 2728 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2727 2729 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2728 2730 )
2729 2731
2730 2732 user_following_id = Column("user_following_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2731 2733 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None, default=None)
2732 2734 follows_repo_id = Column("follows_repository_id", Integer(), ForeignKey('repositories.repo_id'), nullable=True, unique=None, default=None)
2733 2735 follows_user_id = Column("follows_user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
2734 2736 follows_from = Column('follows_from', DateTime(timezone=False), nullable=True, unique=None, default=datetime.datetime.now)
2735 2737
2736 2738 user = relationship('User', primaryjoin='User.user_id==UserFollowing.user_id')
2737 2739
2738 2740 follows_user = relationship('User', primaryjoin='User.user_id==UserFollowing.follows_user_id')
2739 2741 follows_repository = relationship('Repository', order_by='Repository.repo_name')
2740 2742
2741 2743 @classmethod
2742 2744 def get_repo_followers(cls, repo_id):
2743 2745 return cls.query().filter(cls.follows_repo_id == repo_id)
2744 2746
2745 2747
2746 2748 class CacheKey(Base, BaseModel):
2747 2749 __tablename__ = 'cache_invalidation'
2748 2750 __table_args__ = (
2749 2751 UniqueConstraint('cache_key'),
2750 2752 Index('key_idx', 'cache_key'),
2751 2753 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2752 2754 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2753 2755 )
2754 2756 CACHE_TYPE_ATOM = 'ATOM'
2755 2757 CACHE_TYPE_RSS = 'RSS'
2756 2758 CACHE_TYPE_README = 'README'
2757 2759
2758 2760 cache_id = Column("cache_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
2759 2761 cache_key = Column("cache_key", String(255), nullable=True, unique=None, default=None)
2760 2762 cache_args = Column("cache_args", String(255), nullable=True, unique=None, default=None)
2761 2763 cache_active = Column("cache_active", Boolean(), nullable=True, unique=None, default=False)
2762 2764
2763 2765 def __init__(self, cache_key, cache_args=''):
2764 2766 self.cache_key = cache_key
2765 2767 self.cache_args = cache_args
2766 2768 self.cache_active = False
2767 2769
2768 2770 def __unicode__(self):
2769 2771 return u"<%s('%s:%s[%s]')>" % (
2770 2772 self.__class__.__name__,
2771 2773 self.cache_id, self.cache_key, self.cache_active)
2772 2774
2773 2775 def _cache_key_partition(self):
2774 2776 prefix, repo_name, suffix = self.cache_key.partition(self.cache_args)
2775 2777 return prefix, repo_name, suffix
2776 2778
2777 2779 def get_prefix(self):
2778 2780 """
2779 2781 Try to extract prefix from existing cache key. The key could consist
2780 2782 of prefix, repo_name, suffix
2781 2783 """
2782 2784 # this returns prefix, repo_name, suffix
2783 2785 return self._cache_key_partition()[0]
2784 2786
2785 2787 def get_suffix(self):
2786 2788 """
2787 2789 get suffix that might have been used in _get_cache_key to
2788 2790 generate self.cache_key. Only used for informational purposes
2789 2791 in repo_edit.html.
2790 2792 """
2791 2793 # prefix, repo_name, suffix
2792 2794 return self._cache_key_partition()[2]
2793 2795
2794 2796 @classmethod
2795 2797 def delete_all_cache(cls):
2796 2798 """
2797 2799 Delete all cache keys from database.
2798 2800 Should only be run when all instances are down and all entries
2799 2801 thus stale.
2800 2802 """
2801 2803 cls.query().delete()
2802 2804 Session().commit()
2803 2805
2804 2806 @classmethod
2805 2807 def get_cache_key(cls, repo_name, cache_type):
2806 2808 """
2807 2809
2808 2810 Generate a cache key for this process of RhodeCode instance.
2809 2811 Prefix most likely will be process id or maybe explicitly set
2810 2812 instance_id from .ini file.
2811 2813 """
2812 2814 import rhodecode
2813 2815 prefix = safe_unicode(rhodecode.CONFIG.get('instance_id') or '')
2814 2816
2815 2817 repo_as_unicode = safe_unicode(repo_name)
2816 2818 key = u'{}_{}'.format(repo_as_unicode, cache_type) \
2817 2819 if cache_type else repo_as_unicode
2818 2820
2819 2821 return u'{}{}'.format(prefix, key)
2820 2822
2821 2823 @classmethod
2822 2824 def set_invalidate(cls, repo_name, delete=False):
2823 2825 """
2824 2826 Mark all caches of a repo as invalid in the database.
2825 2827 """
2826 2828
2827 2829 try:
2828 2830 qry = Session().query(cls).filter(cls.cache_args == repo_name)
2829 2831 if delete:
2830 2832 log.debug('cache objects deleted for repo %s',
2831 2833 safe_str(repo_name))
2832 2834 qry.delete()
2833 2835 else:
2834 2836 log.debug('cache objects marked as invalid for repo %s',
2835 2837 safe_str(repo_name))
2836 2838 qry.update({"cache_active": False})
2837 2839
2838 2840 Session().commit()
2839 2841 except Exception:
2840 2842 log.exception(
2841 2843 'Cache key invalidation failed for repository %s',
2842 2844 safe_str(repo_name))
2843 2845 Session().rollback()
2844 2846
2845 2847 @classmethod
2846 2848 def get_active_cache(cls, cache_key):
2847 2849 inv_obj = cls.query().filter(cls.cache_key == cache_key).scalar()
2848 2850 if inv_obj:
2849 2851 return inv_obj
2850 2852 return None
2851 2853
2852 2854 @classmethod
2853 2855 def repo_context_cache(cls, compute_func, repo_name, cache_type,
2854 2856 thread_scoped=False):
2855 2857 """
2856 2858 @cache_region('long_term')
2857 2859 def _heavy_calculation(cache_key):
2858 2860 return 'result'
2859 2861
2860 2862 cache_context = CacheKey.repo_context_cache(
2861 2863 _heavy_calculation, repo_name, cache_type)
2862 2864
2863 2865 with cache_context as context:
2864 2866 context.invalidate()
2865 2867 computed = context.compute()
2866 2868
2867 2869 assert computed == 'result'
2868 2870 """
2869 2871 from rhodecode.lib import caches
2870 2872 return caches.InvalidationContext(
2871 2873 compute_func, repo_name, cache_type, thread_scoped=thread_scoped)
2872 2874
2873 2875
2874 2876 class ChangesetComment(Base, BaseModel):
2875 2877 __tablename__ = 'changeset_comments'
2876 2878 __table_args__ = (
2877 2879 Index('cc_revision_idx', 'revision'),
2878 2880 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2879 2881 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
2880 2882 )
2881 2883
2882 2884 COMMENT_OUTDATED = u'comment_outdated'
2883 2885
2884 2886 comment_id = Column('comment_id', Integer(), nullable=False, primary_key=True)
2885 2887 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2886 2888 revision = Column('revision', String(40), nullable=True)
2887 2889 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2888 2890 pull_request_version_id = Column("pull_request_version_id", Integer(), ForeignKey('pull_request_versions.pull_request_version_id'), nullable=True)
2889 2891 line_no = Column('line_no', Unicode(10), nullable=True)
2890 2892 hl_lines = Column('hl_lines', Unicode(512), nullable=True)
2891 2893 f_path = Column('f_path', Unicode(1000), nullable=True)
2892 2894 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
2893 2895 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
2894 2896 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2895 2897 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
2896 2898 renderer = Column('renderer', Unicode(64), nullable=True)
2897 2899 display_state = Column('display_state', Unicode(128), nullable=True)
2898 2900
2899 2901 author = relationship('User', lazy='joined')
2900 2902 repo = relationship('Repository')
2901 2903 status_change = relationship('ChangesetStatus', cascade="all, delete, delete-orphan")
2902 2904 pull_request = relationship('PullRequest', lazy='joined')
2903 2905 pull_request_version = relationship('PullRequestVersion')
2904 2906
2905 2907 @classmethod
2906 2908 def get_users(cls, revision=None, pull_request_id=None):
2907 2909 """
2908 2910 Returns user associated with this ChangesetComment. ie those
2909 2911 who actually commented
2910 2912
2911 2913 :param cls:
2912 2914 :param revision:
2913 2915 """
2914 2916 q = Session().query(User)\
2915 2917 .join(ChangesetComment.author)
2916 2918 if revision:
2917 2919 q = q.filter(cls.revision == revision)
2918 2920 elif pull_request_id:
2919 2921 q = q.filter(cls.pull_request_id == pull_request_id)
2920 2922 return q.all()
2921 2923
2922 2924 def render(self, mentions=False):
2923 2925 from rhodecode.lib import helpers as h
2924 2926 return h.render(self.text, renderer=self.renderer, mentions=mentions)
2925 2927
2926 2928 def __repr__(self):
2927 2929 if self.comment_id:
2928 2930 return '<DB:ChangesetComment #%s>' % self.comment_id
2929 2931 else:
2930 2932 return '<DB:ChangesetComment at %#x>' % id(self)
2931 2933
2932 2934
2933 2935 class ChangesetStatus(Base, BaseModel):
2934 2936 __tablename__ = 'changeset_statuses'
2935 2937 __table_args__ = (
2936 2938 Index('cs_revision_idx', 'revision'),
2937 2939 Index('cs_version_idx', 'version'),
2938 2940 UniqueConstraint('repo_id', 'revision', 'version'),
2939 2941 {'extend_existing': True, 'mysql_engine': 'InnoDB',
2940 2942 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
2941 2943 )
2942 2944 STATUS_NOT_REVIEWED = DEFAULT = 'not_reviewed'
2943 2945 STATUS_APPROVED = 'approved'
2944 2946 STATUS_REJECTED = 'rejected'
2945 2947 STATUS_UNDER_REVIEW = 'under_review'
2946 2948
2947 2949 STATUSES = [
2948 2950 (STATUS_NOT_REVIEWED, _("Not Reviewed")), # (no icon) and default
2949 2951 (STATUS_APPROVED, _("Approved")),
2950 2952 (STATUS_REJECTED, _("Rejected")),
2951 2953 (STATUS_UNDER_REVIEW, _("Under Review")),
2952 2954 ]
2953 2955
2954 2956 changeset_status_id = Column('changeset_status_id', Integer(), nullable=False, primary_key=True)
2955 2957 repo_id = Column('repo_id', Integer(), ForeignKey('repositories.repo_id'), nullable=False)
2956 2958 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False, unique=None)
2957 2959 revision = Column('revision', String(40), nullable=False)
2958 2960 status = Column('status', String(128), nullable=False, default=DEFAULT)
2959 2961 changeset_comment_id = Column('changeset_comment_id', Integer(), ForeignKey('changeset_comments.comment_id'))
2960 2962 modified_at = Column('modified_at', DateTime(), nullable=False, default=datetime.datetime.now)
2961 2963 version = Column('version', Integer(), nullable=False, default=0)
2962 2964 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
2963 2965
2964 2966 author = relationship('User', lazy='joined')
2965 2967 repo = relationship('Repository')
2966 2968 comment = relationship('ChangesetComment', lazy='joined')
2967 2969 pull_request = relationship('PullRequest', lazy='joined')
2968 2970
2969 2971 def __unicode__(self):
2970 2972 return u"<%s('%s[%s]:%s')>" % (
2971 2973 self.__class__.__name__,
2972 2974 self.status, self.version, self.author
2973 2975 )
2974 2976
2975 2977 @classmethod
2976 2978 def get_status_lbl(cls, value):
2977 2979 return dict(cls.STATUSES).get(value)
2978 2980
2979 2981 @property
2980 2982 def status_lbl(self):
2981 2983 return ChangesetStatus.get_status_lbl(self.status)
2982 2984
2983 2985
2984 2986 class _PullRequestBase(BaseModel):
2985 2987 """
2986 2988 Common attributes of pull request and version entries.
2987 2989 """
2988 2990
2989 2991 # .status values
2990 2992 STATUS_NEW = u'new'
2991 2993 STATUS_OPEN = u'open'
2992 2994 STATUS_CLOSED = u'closed'
2993 2995
2994 2996 title = Column('title', Unicode(255), nullable=True)
2995 2997 description = Column(
2996 2998 'description', UnicodeText().with_variant(UnicodeText(10240), 'mysql'),
2997 2999 nullable=True)
2998 3000 # new/open/closed status of pull request (not approve/reject/etc)
2999 3001 status = Column('status', Unicode(255), nullable=False, default=STATUS_NEW)
3000 3002 created_on = Column(
3001 3003 'created_on', DateTime(timezone=False), nullable=False,
3002 3004 default=datetime.datetime.now)
3003 3005 updated_on = Column(
3004 3006 'updated_on', DateTime(timezone=False), nullable=False,
3005 3007 default=datetime.datetime.now)
3006 3008
3007 3009 @declared_attr
3008 3010 def user_id(cls):
3009 3011 return Column(
3010 3012 "user_id", Integer(), ForeignKey('users.user_id'), nullable=False,
3011 3013 unique=None)
3012 3014
3013 3015 # 500 revisions max
3014 3016 _revisions = Column(
3015 3017 'revisions', UnicodeText().with_variant(UnicodeText(20500), 'mysql'))
3016 3018
3017 3019 @declared_attr
3018 3020 def source_repo_id(cls):
3019 3021 # TODO: dan: rename column to source_repo_id
3020 3022 return Column(
3021 3023 'org_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3022 3024 nullable=False)
3023 3025
3024 3026 source_ref = Column('org_ref', Unicode(255), nullable=False)
3025 3027
3026 3028 @declared_attr
3027 3029 def target_repo_id(cls):
3028 3030 # TODO: dan: rename column to target_repo_id
3029 3031 return Column(
3030 3032 'other_repo_id', Integer(), ForeignKey('repositories.repo_id'),
3031 3033 nullable=False)
3032 3034
3033 3035 target_ref = Column('other_ref', Unicode(255), nullable=False)
3034 3036
3035 3037 # TODO: dan: rename column to last_merge_source_rev
3036 3038 _last_merge_source_rev = Column(
3037 3039 'last_merge_org_rev', String(40), nullable=True)
3038 3040 # TODO: dan: rename column to last_merge_target_rev
3039 3041 _last_merge_target_rev = Column(
3040 3042 'last_merge_other_rev', String(40), nullable=True)
3041 3043 _last_merge_status = Column('merge_status', Integer(), nullable=True)
3042 3044 merge_rev = Column('merge_rev', String(40), nullable=True)
3043 3045
3044 3046 @hybrid_property
3045 3047 def revisions(self):
3046 3048 return self._revisions.split(':') if self._revisions else []
3047 3049
3048 3050 @revisions.setter
3049 3051 def revisions(self, val):
3050 3052 self._revisions = ':'.join(val)
3051 3053
3052 3054 @declared_attr
3053 3055 def author(cls):
3054 3056 return relationship('User', lazy='joined')
3055 3057
3056 3058 @declared_attr
3057 3059 def source_repo(cls):
3058 3060 return relationship(
3059 3061 'Repository',
3060 3062 primaryjoin='%s.source_repo_id==Repository.repo_id' % cls.__name__)
3061 3063
3062 3064 @property
3063 3065 def source_ref_parts(self):
3064 3066 refs = self.source_ref.split(':')
3065 3067 return Reference(refs[0], refs[1], refs[2])
3066 3068
3067 3069 @declared_attr
3068 3070 def target_repo(cls):
3069 3071 return relationship(
3070 3072 'Repository',
3071 3073 primaryjoin='%s.target_repo_id==Repository.repo_id' % cls.__name__)
3072 3074
3073 3075 @property
3074 3076 def target_ref_parts(self):
3075 3077 refs = self.target_ref.split(':')
3076 3078 return Reference(refs[0], refs[1], refs[2])
3077 3079
3078 3080
3079 3081 class PullRequest(Base, _PullRequestBase):
3080 3082 __tablename__ = 'pull_requests'
3081 3083 __table_args__ = (
3082 3084 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3083 3085 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3084 3086 )
3085 3087
3086 3088 pull_request_id = Column(
3087 3089 'pull_request_id', Integer(), nullable=False, primary_key=True)
3088 3090
3089 3091 def __repr__(self):
3090 3092 if self.pull_request_id:
3091 3093 return '<DB:PullRequest #%s>' % self.pull_request_id
3092 3094 else:
3093 3095 return '<DB:PullRequest at %#x>' % id(self)
3094 3096
3095 3097 reviewers = relationship('PullRequestReviewers',
3096 3098 cascade="all, delete, delete-orphan")
3097 3099 statuses = relationship('ChangesetStatus')
3098 3100 comments = relationship('ChangesetComment',
3099 3101 cascade="all, delete, delete-orphan")
3100 3102 versions = relationship('PullRequestVersion',
3101 3103 cascade="all, delete, delete-orphan")
3102 3104
3103 3105 def is_closed(self):
3104 3106 return self.status == self.STATUS_CLOSED
3105 3107
3106 3108 def get_api_data(self):
3107 3109 from rhodecode.model.pull_request import PullRequestModel
3108 3110 pull_request = self
3109 3111 merge_status = PullRequestModel().merge_status(pull_request)
3110 3112 data = {
3111 3113 'pull_request_id': pull_request.pull_request_id,
3112 3114 'url': url('pullrequest_show', repo_name=self.target_repo.repo_name,
3113 3115 pull_request_id=self.pull_request_id,
3114 3116 qualified=True),
3115 3117 'title': pull_request.title,
3116 3118 'description': pull_request.description,
3117 3119 'status': pull_request.status,
3118 3120 'created_on': pull_request.created_on,
3119 3121 'updated_on': pull_request.updated_on,
3120 3122 'commit_ids': pull_request.revisions,
3121 3123 'review_status': pull_request.calculated_review_status(),
3122 3124 'mergeable': {
3123 3125 'status': merge_status[0],
3124 3126 'message': unicode(merge_status[1]),
3125 3127 },
3126 3128 'source': {
3127 3129 'clone_url': pull_request.source_repo.clone_url(),
3128 3130 'repository': pull_request.source_repo.repo_name,
3129 3131 'reference': {
3130 3132 'name': pull_request.source_ref_parts.name,
3131 3133 'type': pull_request.source_ref_parts.type,
3132 3134 'commit_id': pull_request.source_ref_parts.commit_id,
3133 3135 },
3134 3136 },
3135 3137 'target': {
3136 3138 'clone_url': pull_request.target_repo.clone_url(),
3137 3139 'repository': pull_request.target_repo.repo_name,
3138 3140 'reference': {
3139 3141 'name': pull_request.target_ref_parts.name,
3140 3142 'type': pull_request.target_ref_parts.type,
3141 3143 'commit_id': pull_request.target_ref_parts.commit_id,
3142 3144 },
3143 3145 },
3144 3146 'author': pull_request.author.get_api_data(include_secrets=False,
3145 3147 details='basic'),
3146 3148 'reviewers': [
3147 3149 {
3148 3150 'user': reviewer.get_api_data(include_secrets=False,
3149 3151 details='basic'),
3150 3152 'review_status': st[0][1].status if st else 'not_reviewed',
3151 3153 }
3152 3154 for reviewer, st in pull_request.reviewers_statuses()
3153 3155 ]
3154 3156 }
3155 3157
3156 3158 return data
3157 3159
3158 3160 def __json__(self):
3159 3161 return {
3160 3162 'revisions': self.revisions,
3161 3163 }
3162 3164
3163 3165 def calculated_review_status(self):
3164 3166 # TODO: anderson: 13.05.15 Used only on templates/my_account_pullrequests.html
3165 3167 # because it's tricky on how to use ChangesetStatusModel from there
3166 3168 warnings.warn("Use calculated_review_status from ChangesetStatusModel", DeprecationWarning)
3167 3169 from rhodecode.model.changeset_status import ChangesetStatusModel
3168 3170 return ChangesetStatusModel().calculated_review_status(self)
3169 3171
3170 3172 def reviewers_statuses(self):
3171 3173 warnings.warn("Use reviewers_statuses from ChangesetStatusModel", DeprecationWarning)
3172 3174 from rhodecode.model.changeset_status import ChangesetStatusModel
3173 3175 return ChangesetStatusModel().reviewers_statuses(self)
3174 3176
3175 3177
3176 3178 class PullRequestVersion(Base, _PullRequestBase):
3177 3179 __tablename__ = 'pull_request_versions'
3178 3180 __table_args__ = (
3179 3181 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3180 3182 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3181 3183 )
3182 3184
3183 3185 pull_request_version_id = Column(
3184 3186 'pull_request_version_id', Integer(), nullable=False, primary_key=True)
3185 3187 pull_request_id = Column(
3186 3188 'pull_request_id', Integer(),
3187 3189 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3188 3190 pull_request = relationship('PullRequest')
3189 3191
3190 3192 def __repr__(self):
3191 3193 if self.pull_request_version_id:
3192 3194 return '<DB:PullRequestVersion #%s>' % self.pull_request_version_id
3193 3195 else:
3194 3196 return '<DB:PullRequestVersion at %#x>' % id(self)
3195 3197
3196 3198
3197 3199 class PullRequestReviewers(Base, BaseModel):
3198 3200 __tablename__ = 'pull_request_reviewers'
3199 3201 __table_args__ = (
3200 3202 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3201 3203 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3202 3204 )
3203 3205
3204 3206 def __init__(self, user=None, pull_request=None):
3205 3207 self.user = user
3206 3208 self.pull_request = pull_request
3207 3209
3208 3210 pull_requests_reviewers_id = Column(
3209 3211 'pull_requests_reviewers_id', Integer(), nullable=False,
3210 3212 primary_key=True)
3211 3213 pull_request_id = Column(
3212 3214 "pull_request_id", Integer(),
3213 3215 ForeignKey('pull_requests.pull_request_id'), nullable=False)
3214 3216 user_id = Column(
3215 3217 "user_id", Integer(), ForeignKey('users.user_id'), nullable=True)
3216 3218
3217 3219 user = relationship('User')
3218 3220 pull_request = relationship('PullRequest')
3219 3221
3220 3222
3221 3223 class Notification(Base, BaseModel):
3222 3224 __tablename__ = 'notifications'
3223 3225 __table_args__ = (
3224 3226 Index('notification_type_idx', 'type'),
3225 3227 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3226 3228 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3227 3229 )
3228 3230
3229 3231 TYPE_CHANGESET_COMMENT = u'cs_comment'
3230 3232 TYPE_MESSAGE = u'message'
3231 3233 TYPE_MENTION = u'mention'
3232 3234 TYPE_REGISTRATION = u'registration'
3233 3235 TYPE_PULL_REQUEST = u'pull_request'
3234 3236 TYPE_PULL_REQUEST_COMMENT = u'pull_request_comment'
3235 3237
3236 3238 notification_id = Column('notification_id', Integer(), nullable=False, primary_key=True)
3237 3239 subject = Column('subject', Unicode(512), nullable=True)
3238 3240 body = Column('body', UnicodeText().with_variant(UnicodeText(50000), 'mysql'), nullable=True)
3239 3241 created_by = Column("created_by", Integer(), ForeignKey('users.user_id'), nullable=True)
3240 3242 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3241 3243 type_ = Column('type', Unicode(255))
3242 3244
3243 3245 created_by_user = relationship('User')
3244 3246 notifications_to_users = relationship('UserNotification', lazy='joined',
3245 3247 cascade="all, delete, delete-orphan")
3246 3248
3247 3249 @property
3248 3250 def recipients(self):
3249 3251 return [x.user for x in UserNotification.query()\
3250 3252 .filter(UserNotification.notification == self)\
3251 3253 .order_by(UserNotification.user_id.asc()).all()]
3252 3254
3253 3255 @classmethod
3254 3256 def create(cls, created_by, subject, body, recipients, type_=None):
3255 3257 if type_ is None:
3256 3258 type_ = Notification.TYPE_MESSAGE
3257 3259
3258 3260 notification = cls()
3259 3261 notification.created_by_user = created_by
3260 3262 notification.subject = subject
3261 3263 notification.body = body
3262 3264 notification.type_ = type_
3263 3265 notification.created_on = datetime.datetime.now()
3264 3266
3265 3267 for u in recipients:
3266 3268 assoc = UserNotification()
3267 3269 assoc.notification = notification
3268 3270
3269 3271 # if created_by is inside recipients mark his notification
3270 3272 # as read
3271 3273 if u.user_id == created_by.user_id:
3272 3274 assoc.read = True
3273 3275
3274 3276 u.notifications.append(assoc)
3275 3277 Session().add(notification)
3276 3278
3277 3279 return notification
3278 3280
3279 3281 @property
3280 3282 def description(self):
3281 3283 from rhodecode.model.notification import NotificationModel
3282 3284 return NotificationModel().make_description(self)
3283 3285
3284 3286
3285 3287 class UserNotification(Base, BaseModel):
3286 3288 __tablename__ = 'user_to_notification'
3287 3289 __table_args__ = (
3288 3290 UniqueConstraint('user_id', 'notification_id'),
3289 3291 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3290 3292 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3291 3293 )
3292 3294 user_id = Column('user_id', Integer(), ForeignKey('users.user_id'), primary_key=True)
3293 3295 notification_id = Column("notification_id", Integer(), ForeignKey('notifications.notification_id'), primary_key=True)
3294 3296 read = Column('read', Boolean, default=False)
3295 3297 sent_on = Column('sent_on', DateTime(timezone=False), nullable=True, unique=None)
3296 3298
3297 3299 user = relationship('User', lazy="joined")
3298 3300 notification = relationship('Notification', lazy="joined",
3299 3301 order_by=lambda: Notification.created_on.desc(),)
3300 3302
3301 3303 def mark_as_read(self):
3302 3304 self.read = True
3303 3305 Session().add(self)
3304 3306
3305 3307
3306 3308 class Gist(Base, BaseModel):
3307 3309 __tablename__ = 'gists'
3308 3310 __table_args__ = (
3309 3311 Index('g_gist_access_id_idx', 'gist_access_id'),
3310 3312 Index('g_created_on_idx', 'created_on'),
3311 3313 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3312 3314 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3313 3315 )
3314 3316 GIST_PUBLIC = u'public'
3315 3317 GIST_PRIVATE = u'private'
3316 3318 DEFAULT_FILENAME = u'gistfile1.txt'
3317 3319
3318 3320 ACL_LEVEL_PUBLIC = u'acl_public'
3319 3321 ACL_LEVEL_PRIVATE = u'acl_private'
3320 3322
3321 3323 gist_id = Column('gist_id', Integer(), primary_key=True)
3322 3324 gist_access_id = Column('gist_access_id', Unicode(250))
3323 3325 gist_description = Column('gist_description', UnicodeText().with_variant(UnicodeText(1024), 'mysql'))
3324 3326 gist_owner = Column('user_id', Integer(), ForeignKey('users.user_id'), nullable=True)
3325 3327 gist_expires = Column('gist_expires', Float(53), nullable=False)
3326 3328 gist_type = Column('gist_type', Unicode(128), nullable=False)
3327 3329 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3328 3330 modified_at = Column('modified_at', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3329 3331 acl_level = Column('acl_level', Unicode(128), nullable=True)
3330 3332
3331 3333 owner = relationship('User')
3332 3334
3333 3335 def __repr__(self):
3334 3336 return '<Gist:[%s]%s>' % (self.gist_type, self.gist_access_id)
3335 3337
3336 3338 @classmethod
3337 3339 def get_or_404(cls, id_):
3338 3340 res = cls.query().filter(cls.gist_access_id == id_).scalar()
3339 3341 if not res:
3340 3342 raise HTTPNotFound
3341 3343 return res
3342 3344
3343 3345 @classmethod
3344 3346 def get_by_access_id(cls, gist_access_id):
3345 3347 return cls.query().filter(cls.gist_access_id == gist_access_id).scalar()
3346 3348
3347 3349 def gist_url(self):
3348 3350 import rhodecode
3349 3351 alias_url = rhodecode.CONFIG.get('gist_alias_url')
3350 3352 if alias_url:
3351 3353 return alias_url.replace('{gistid}', self.gist_access_id)
3352 3354
3353 3355 return url('gist', gist_id=self.gist_access_id, qualified=True)
3354 3356
3355 3357 @classmethod
3356 3358 def base_path(cls):
3357 3359 """
3358 3360 Returns base path when all gists are stored
3359 3361
3360 3362 :param cls:
3361 3363 """
3362 3364 from rhodecode.model.gist import GIST_STORE_LOC
3363 3365 q = Session().query(RhodeCodeUi)\
3364 3366 .filter(RhodeCodeUi.ui_key == URL_SEP)
3365 3367 q = q.options(FromCache("sql_cache_short", "repository_repo_path"))
3366 3368 return os.path.join(q.one().ui_value, GIST_STORE_LOC)
3367 3369
3368 3370 def get_api_data(self):
3369 3371 """
3370 3372 Common function for generating gist related data for API
3371 3373 """
3372 3374 gist = self
3373 3375 data = {
3374 3376 'gist_id': gist.gist_id,
3375 3377 'type': gist.gist_type,
3376 3378 'access_id': gist.gist_access_id,
3377 3379 'description': gist.gist_description,
3378 3380 'url': gist.gist_url(),
3379 3381 'expires': gist.gist_expires,
3380 3382 'created_on': gist.created_on,
3381 3383 'modified_at': gist.modified_at,
3382 3384 'content': None,
3383 3385 'acl_level': gist.acl_level,
3384 3386 }
3385 3387 return data
3386 3388
3387 3389 def __json__(self):
3388 3390 data = dict(
3389 3391 )
3390 3392 data.update(self.get_api_data())
3391 3393 return data
3392 3394 # SCM functions
3393 3395
3394 3396 def scm_instance(self, **kwargs):
3395 3397 full_repo_path = os.path.join(self.base_path(), self.gist_access_id)
3396 3398 return get_vcs_instance(
3397 3399 repo_path=safe_str(full_repo_path), create=False)
3398 3400
3399 3401
3400 3402 class DbMigrateVersion(Base, BaseModel):
3401 3403 __tablename__ = 'db_migrate_version'
3402 3404 __table_args__ = (
3403 3405 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3404 3406 'mysql_charset': 'utf8', 'sqlite_autoincrement': True},
3405 3407 )
3406 3408 repository_id = Column('repository_id', String(250), primary_key=True)
3407 3409 repository_path = Column('repository_path', Text)
3408 3410 version = Column('version', Integer)
3409 3411
3410 3412
3411 3413 class ExternalIdentity(Base, BaseModel):
3412 3414 __tablename__ = 'external_identities'
3413 3415 __table_args__ = (
3414 3416 Index('local_user_id_idx', 'local_user_id'),
3415 3417 Index('external_id_idx', 'external_id'),
3416 3418 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3417 3419 'mysql_charset': 'utf8'})
3418 3420
3419 3421 external_id = Column('external_id', Unicode(255), default=u'',
3420 3422 primary_key=True)
3421 3423 external_username = Column('external_username', Unicode(1024), default=u'')
3422 3424 local_user_id = Column('local_user_id', Integer(),
3423 3425 ForeignKey('users.user_id'), primary_key=True)
3424 3426 provider_name = Column('provider_name', Unicode(255), default=u'',
3425 3427 primary_key=True)
3426 3428 access_token = Column('access_token', String(1024), default=u'')
3427 3429 alt_token = Column('alt_token', String(1024), default=u'')
3428 3430 token_secret = Column('token_secret', String(1024), default=u'')
3429 3431
3430 3432 @classmethod
3431 3433 def by_external_id_and_provider(cls, external_id, provider_name,
3432 3434 local_user_id=None):
3433 3435 """
3434 3436 Returns ExternalIdentity instance based on search params
3435 3437
3436 3438 :param external_id:
3437 3439 :param provider_name:
3438 3440 :return: ExternalIdentity
3439 3441 """
3440 3442 query = cls.query()
3441 3443 query = query.filter(cls.external_id == external_id)
3442 3444 query = query.filter(cls.provider_name == provider_name)
3443 3445 if local_user_id:
3444 3446 query = query.filter(cls.local_user_id == local_user_id)
3445 3447 return query.first()
3446 3448
3447 3449 @classmethod
3448 3450 def user_by_external_id_and_provider(cls, external_id, provider_name):
3449 3451 """
3450 3452 Returns User instance based on search params
3451 3453
3452 3454 :param external_id:
3453 3455 :param provider_name:
3454 3456 :return: User
3455 3457 """
3456 3458 query = User.query()
3457 3459 query = query.filter(cls.external_id == external_id)
3458 3460 query = query.filter(cls.provider_name == provider_name)
3459 3461 query = query.filter(User.user_id == cls.local_user_id)
3460 3462 return query.first()
3461 3463
3462 3464 @classmethod
3463 3465 def by_local_user_id(cls, local_user_id):
3464 3466 """
3465 3467 Returns all tokens for user
3466 3468
3467 3469 :param local_user_id:
3468 3470 :return: ExternalIdentity
3469 3471 """
3470 3472 query = cls.query()
3471 3473 query = query.filter(cls.local_user_id == local_user_id)
3472 3474 return query
3473 3475
3474 3476
3475 3477 class Integration(Base, BaseModel):
3476 3478 __tablename__ = 'integrations'
3477 3479 __table_args__ = (
3478 3480 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3479 3481 'mysql_charset': 'utf8', 'sqlite_autoincrement': True}
3480 3482 )
3481 3483
3482 3484 integration_id = Column('integration_id', Integer(), primary_key=True)
3483 3485 integration_type = Column('integration_type', String(255))
3484 3486 enabled = Column('enabled', Boolean(), nullable=False)
3485 3487 name = Column('name', String(255), nullable=False)
3486 3488 child_repos_only = Column('child_repos_only', Boolean(), nullable=False,
3487 3489 default=False)
3488 3490
3489 3491 settings = Column(
3490 3492 'settings_json', MutationObj.as_mutable(
3491 3493 JsonType(dialect_map=dict(mysql=UnicodeText(16384)))))
3492 3494 repo_id = Column(
3493 3495 'repo_id', Integer(), ForeignKey('repositories.repo_id'),
3494 3496 nullable=True, unique=None, default=None)
3495 3497 repo = relationship('Repository', lazy='joined')
3496 3498
3497 3499 repo_group_id = Column(
3498 3500 'repo_group_id', Integer(), ForeignKey('groups.group_id'),
3499 3501 nullable=True, unique=None, default=None)
3500 3502 repo_group = relationship('RepoGroup', lazy='joined')
3501 3503
3502 3504 @property
3503 3505 def scope(self):
3504 3506 if self.repo:
3505 3507 return repr(self.repo)
3506 3508 if self.repo_group:
3507 3509 if self.child_repos_only:
3508 3510 return repr(self.repo_group) + ' (child repos only)'
3509 3511 else:
3510 3512 return repr(self.repo_group) + ' (recursive)'
3511 3513 if self.child_repos_only:
3512 3514 return 'root_repos'
3513 3515 return 'global'
3514 3516
3515 3517 def __repr__(self):
3516 3518 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3519
3520
3521 class RepoReviewRuleUser(Base, BaseModel):
3522 __tablename__ = 'repo_review_rules_users'
3523 __table_args__ = (
3524 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3525 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3526 )
3527 repo_review_rule_user_id = Column(
3528 'repo_review_rule_user_id', Integer(), primary_key=True)
3529 repo_review_rule_id = Column("repo_review_rule_id",
3530 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3531 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3532 nullable=False)
3533 user = relationship('User')
3534
3535
3536 class RepoReviewRuleUserGroup(Base, BaseModel):
3537 __tablename__ = 'repo_review_rules_users_groups'
3538 __table_args__ = (
3539 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3540 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3541 )
3542 repo_review_rule_users_group_id = Column(
3543 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3544 repo_review_rule_id = Column("repo_review_rule_id",
3545 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3546 users_group_id = Column("users_group_id", Integer(),
3547 ForeignKey('users_groups.users_group_id'), nullable=False)
3548 users_group = relationship('UserGroup')
3549
3550
3551 class RepoReviewRule(Base, BaseModel):
3552 __tablename__ = 'repo_review_rules'
3553 __table_args__ = (
3554 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3555 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3556 )
3557
3558 repo_review_rule_id = Column(
3559 'repo_review_rule_id', Integer(), primary_key=True)
3560 repo_id = Column(
3561 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3562 repo = relationship('Repository', backref='review_rules')
3563
3564 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3565 default=u'*') # glob
3566 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3567 default=u'*') # glob
3568
3569 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3570 nullable=False, default=False)
3571 rule_users = relationship('RepoReviewRuleUser')
3572 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3573
3574 @hybrid_property
3575 def branch_pattern(self):
3576 return self._branch_pattern or '*'
3577
3578 def _validate_glob(self, value):
3579 re.compile('^' + glob2re(value) + '$')
3580
3581 @branch_pattern.setter
3582 def branch_pattern(self, value):
3583 self._validate_glob(value)
3584 self._branch_pattern = value or '*'
3585
3586 @hybrid_property
3587 def file_pattern(self):
3588 return self._file_pattern or '*'
3589
3590 @file_pattern.setter
3591 def file_pattern(self, value):
3592 self._validate_glob(value)
3593 self._file_pattern = value or '*'
3594
3595 def matches(self, branch, files_changed):
3596 """
3597 Check if this review rule matches a branch/files in a pull request
3598
3599 :param branch: branch name for the commit
3600 :param files_changed: list of file paths changed in the pull request
3601 """
3602
3603 branch = branch or ''
3604 files_changed = files_changed or []
3605
3606 branch_matches = True
3607 if branch:
3608 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3609 branch_matches = bool(branch_regex.search(branch))
3610
3611 files_matches = True
3612 if self.file_pattern != '*':
3613 files_matches = False
3614 file_regex = re.compile(glob2re(self.file_pattern))
3615 for filename in files_changed:
3616 if file_regex.search(filename):
3617 files_matches = True
3618 break
3619
3620 return branch_matches and files_matches
3621
3622 @property
3623 def review_users(self):
3624 """ Returns the users which this rule applies to """
3625
3626 users = set()
3627 users |= set([
3628 rule_user.user for rule_user in self.rule_users
3629 if rule_user.user.active])
3630 users |= set(
3631 member.user
3632 for rule_user_group in self.rule_user_groups
3633 for member in rule_user_group.users_group.members
3634 if member.user.active
3635 )
3636 return users
3637
3638 def __repr__(self):
3639 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3640 self.repo_review_rule_id, self.repo)
@@ -1,15 +1,26 b''
1 1 import os
2 import re
2 3
3 4 import ipaddress
4 5 import colander
5 6
6 7 from rhodecode.translation import _
8 from rhodecode.lib.utils2 import glob2re
7 9
8 10
9 11 def ip_addr_validator(node, value):
10 12 try:
11 13 # this raises an ValueError if address is not IpV4 or IpV6
12 14 ipaddress.ip_network(value, strict=False)
13 15 except ValueError:
14 16 msg = _(u'Please enter a valid IPv4 or IpV6 address')
15 17 raise colander.Invalid(node, msg)
18
19
20 def glob_validator(node, value):
21 try:
22 re.compile('^' + glob2re(value) + '$')
23 except Exception:
24 raise
25 msg = _(u'Invalid glob pattern')
26 raise colander.Invalid(node, msg)
@@ -1,117 +1,139 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 41 padding: 0.9em;
42 42 border: 1px solid #979797;
43 43 border-radius: 2px;
44 44 }
45 45 .form-control.select2-container {
46 46 padding: 0; /* padding already applied in .drop-menu a */
47 47 }
48 48
49 49 .form-control.readonly {
50 50 background: #eeeeee;
51 51 cursor: not-allowed;
52 52 }
53 53
54 54 .error-block {
55 55 color: red;
56 56 margin: 0;
57 57 }
58 58
59 59 .help-block {
60 60 margin: 0;
61 61 }
62 62
63 63 .deform-seq-container .control-inputs {
64 64 width: 100%;
65 65 }
66 66
67 67 .deform-seq-container .deform-seq-item-handle {
68 68 width: 8.3%;
69 69 float: left;
70 70 }
71 71
72 72 .deform-seq-container .deform-seq-item-group {
73 73 width: 91.6%;
74 74 float: left;
75 75 }
76 76
77 77 .form-control {
78 78 input {
79 79 height: 40px;
80 80 }
81 81 input[type=checkbox], input[type=radio] {
82 82 height: auto;
83 83 }
84 84 select {
85 85 height: 40px;
86 86 }
87 87 }
88 88
89 89 .form-control.select2-container {
90 90 height: 40px;
91 91 }
92 92
93 .deform-two-field-sequence .deform-seq-container .deform-seq-item label {
93 .deform-full-field-sequence.control-inputs {
94 width: 100%;
95 }
96
97 .deform-table-sequence {
98 .deform-seq-container {
99 .deform-seq-item {
100 margin: 0;
101 label {
94 102 display: none;
95 103 }
96 .deform-two-field-sequence .deform-seq-container .deform-seq-item:first-child label {
97 display: block;
98 }
99 .deform-two-field-sequence .deform-seq-container .deform-seq-item .panel-heading {
104 .panel-heading {
100 105 display: none;
101 106 }
102 .deform-two-field-sequence .deform-seq-container .deform-seq-item.form-group {
103 margin: 0;
104 }
105 .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group .form-group {
106 width: 45%; padding: 0 2px; float: left; clear: none;
107 }
108 .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group > .panel {
107 .deform-seq-item-group > .panel {
109 108 padding: 0;
110 109 margin: 5px 0;
111 110 border: none;
112 }
113 .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group > .panel > .panel-body {
111 &> .panel-body {
114 112 padding: 0;
115 113 }
116
114 }
115 &:first-child label {
116 display: block;
117 }
118 }
119 }
120 }
121 .deform-table-2-sequence {
122 .deform-seq-container {
123 .deform-seq-item {
124 .form-group {
125 width: 45% !important; padding: 0 2px; float: left; clear: none;
117 126 }
127 }
128 }
129 }
130 .deform-table-3-sequence {
131 .deform-seq-container {
132 .deform-seq-item {
133 .form-group {
134 width: 30% !important; padding: 0 2px; float: left; clear: none;
135 }
136 }
137 }
138 }
139 }
@@ -1,51 +1,53 b''
1 1
2 2 /******************************************************************************
3 3 * *
4 4 * DO NOT CHANGE THIS FILE MANUALLY *
5 5 * *
6 6 * *
7 7 * This file is automatically generated when the app starts up with *
8 8 * generate_js_files = true *
9 9 * *
10 10 * To add a route here pass jsroute=True to the route definition in the app *
11 11 * *
12 12 ******************************************************************************/
13 13 function registerRCRoutes() {
14 14 // routes registration
15 15 pyroutes.register('home', '/', []);
16 16 pyroutes.register('user_autocomplete_data', '/_users', []);
17 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
17 18 pyroutes.register('new_repo', '/_admin/create_repository', []);
18 19 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
19 20 pyroutes.register('gists', '/_admin/gists', []);
20 21 pyroutes.register('new_gist', '/_admin/gists/new', []);
21 22 pyroutes.register('toggle_following', '/_admin/toggle_following', []);
22 23 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
23 24 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
24 25 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
26 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/default-reviewers', ['repo_name']);
25 27 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
26 28 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
27 29 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
28 30 pyroutes.register('changeset_comment', '/%(repo_name)s/changeset/%(revision)s/comment', ['repo_name', 'revision']);
29 31 pyroutes.register('changeset_comment_preview', '/%(repo_name)s/changeset/comment/preview', ['repo_name']);
30 32 pyroutes.register('changeset_comment_delete', '/%(repo_name)s/changeset/comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
31 33 pyroutes.register('changeset_info', '/%(repo_name)s/changeset_info/%(revision)s', ['repo_name', 'revision']);
32 34 pyroutes.register('compare_url', '/%(repo_name)s/compare/%(source_ref_type)s@%(source_ref)s...%(target_ref_type)s@%(target_ref)s', ['repo_name', 'source_ref_type', 'source_ref', 'target_ref_type', 'target_ref']);
33 35 pyroutes.register('pullrequest_home', '/%(repo_name)s/pull-request/new', ['repo_name']);
34 36 pyroutes.register('pullrequest', '/%(repo_name)s/pull-request/new', ['repo_name']);
35 37 pyroutes.register('pullrequest_repo_refs', '/%(repo_name)s/pull-request/refs/%(target_repo_name)s', ['repo_name', 'target_repo_name']);
36 38 pyroutes.register('pullrequest_repo_destinations', '/%(repo_name)s/pull-request/repo-destinations', ['repo_name']);
37 39 pyroutes.register('pullrequest_update', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
38 40 pyroutes.register('pullrequest_show_all', '/%(repo_name)s/pull-request', ['repo_name']);
39 41 pyroutes.register('pullrequest_comment', '/%(repo_name)s/pull-request-comment/%(pull_request_id)s', ['repo_name', 'pull_request_id']);
40 42 pyroutes.register('pullrequest_comment_delete', '/%(repo_name)s/pull-request-comment/%(comment_id)s/delete', ['repo_name', 'comment_id']);
41 43 pyroutes.register('changelog_home', '/%(repo_name)s/changelog', ['repo_name']);
42 44 pyroutes.register('changelog_file_home', '/%(repo_name)s/changelog/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
43 45 pyroutes.register('files_home', '/%(repo_name)s/files/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
44 46 pyroutes.register('files_history_home', '/%(repo_name)s/history/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
45 47 pyroutes.register('files_authors_home', '/%(repo_name)s/authors/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
46 48 pyroutes.register('files_archive_home', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
47 49 pyroutes.register('files_nodelist_home', '/%(repo_name)s/nodelist/%(revision)s/%(f_path)s', ['repo_name', 'revision', 'f_path']);
48 50 pyroutes.register('files_nodetree_full', '/%(repo_name)s/nodetree_full/%(commit_id)s/%(f_path)s', ['repo_name', 'commit_id', 'f_path']);
49 51 pyroutes.register('summary_home_slash', '/%(repo_name)s/', ['repo_name']);
50 52 pyroutes.register('summary_home', '/%(repo_name)s', ['repo_name']);
51 53 }
@@ -1,205 +1,214 b''
1 1 // # Copyright (C) 2010-2016 RhodeCode GmbH
2 2 // #
3 3 // # This program is free software: you can redistribute it and/or modify
4 4 // # it under the terms of the GNU Affero General Public License, version 3
5 5 // # (only), as published by the Free Software Foundation.
6 6 // #
7 7 // # This program is distributed in the hope that it will be useful,
8 8 // # but WITHOUT ANY WARRANTY; without even the implied warranty of
9 9 // # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
10 10 // # GNU General Public License for more details.
11 11 // #
12 12 // # You should have received a copy of the GNU Affero General Public License
13 13 // # along with this program. If not, see <http://www.gnu.org/licenses/>.
14 14 // #
15 15 // # This program is dual-licensed. If you wish to learn more about the
16 16 // # RhodeCode Enterprise Edition, including its added features, Support services,
17 17 // # and proprietary license terms, please see https://rhodecode.com/licenses/
18 18
19 19 /**
20 20 * Pull request reviewers
21 21 */
22 22 var removeReviewMember = function(reviewer_id, mark_delete){
23 23 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
24 24
25 25 if(typeof(mark_delete) === undefined){
26 26 mark_delete = false;
27 27 }
28 28
29 29 if(mark_delete === true){
30 30 if (reviewer){
31 31 // mark as to-remove
32 32 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
33 33 obj.addClass('to-delete');
34 34 // now delete the input
35 35 $('#reviewer_{0}_input'.format(reviewer_id)).remove();
36 36 }
37 37 }
38 38 else{
39 39 $('#reviewer_{0}'.format(reviewer_id)).remove();
40 40 }
41 41 };
42 42
43 var addReviewMember = function(id,fname,lname,nname,gravatar_link){
43 var addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons) {
44 44 var members = $('#review_members').get(0);
45 var reasons_html = '';
46 if (reasons) {
47 for (var i = 0; i < reasons.length; i++) {
48 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(
49 reasons[i]
50 );
51 }
52 }
45 53 var tmpl = '<li id="reviewer_{2}">'+
46 54 '<div class="reviewer_status">'+
47 55 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
48 56 '</div>'+
49 57 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
50 58 '<span class="reviewer_name user">{1}</span>'+
59 reasons_html +
51 60 '<input type="hidden" value="{2}" name="review_members" />'+
52 61 '<div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">' +
53 62 '<i class="icon-remove-sign"></i>'+
54 63 '</div>'+
55 64 '</div>'+
56 65 '</li>' ;
57 66 var displayname = "{0} ({1} {2})".format(
58 67 nname, escapeHtml(fname), escapeHtml(lname));
59 68 var element = tmpl.format(gravatar_link,displayname,id);
60 69 // check if we don't have this ID already in
61 70 var ids = [];
62 71 var _els = $('#review_members li').toArray();
63 72 for (el in _els){
64 73 ids.push(_els[el].id)
65 74 }
66 75 if(ids.indexOf('reviewer_'+id) == -1){
67 76 // only add if it's not there
68 77 members.innerHTML += element;
69 78 }
70 79
71 80 };
72 81
73 82 var _updatePullRequest = function(repo_name, pull_request_id, postData) {
74 83 var url = pyroutes.url(
75 84 'pullrequest_update',
76 85 {"repo_name": repo_name, "pull_request_id": pull_request_id});
77 86 postData.csrf_token = CSRF_TOKEN;
78 87 var success = function(o) {
79 88 window.location.reload();
80 89 };
81 90 ajaxPOST(url, postData, success);
82 91 };
83 92
84 93 var updateReviewers = function(reviewers_ids, repo_name, pull_request_id){
85 94 if (reviewers_ids === undefined){
86 95 var reviewers_ids = [];
87 96 var ids = $('#review_members input').toArray();
88 97 for(var i=0; i<ids.length;i++){
89 98 var id = ids[i].value
90 99 reviewers_ids.push(id);
91 100 }
92 101 }
93 102 var postData = {
94 103 '_method':'put',
95 104 'reviewers_ids': reviewers_ids};
96 105 _updatePullRequest(repo_name, pull_request_id, postData);
97 106 };
98 107
99 108 /**
100 109 * PULL REQUEST reject & close
101 110 */
102 111 var closePullRequest = function(repo_name, pull_request_id) {
103 112 var postData = {
104 113 '_method': 'put',
105 114 'close_pull_request': true};
106 115 _updatePullRequest(repo_name, pull_request_id, postData);
107 116 };
108 117
109 118 /**
110 119 * PULL REQUEST update commits
111 120 */
112 121 var updateCommits = function(repo_name, pull_request_id) {
113 122 var postData = {
114 123 '_method': 'put',
115 124 'update_commits': true};
116 125 _updatePullRequest(repo_name, pull_request_id, postData);
117 126 };
118 127
119 128
120 129 /**
121 130 * PULL REQUEST edit info
122 131 */
123 132 var editPullRequest = function(repo_name, pull_request_id, title, description) {
124 133 var url = pyroutes.url(
125 134 'pullrequest_update',
126 135 {"repo_name": repo_name, "pull_request_id": pull_request_id});
127 136
128 137 var postData = {
129 138 '_method': 'put',
130 139 'title': title,
131 140 'description': description,
132 141 'edit_pull_request': true,
133 142 'csrf_token': CSRF_TOKEN
134 143 };
135 144 var success = function(o) {
136 145 window.location.reload();
137 146 };
138 147 ajaxPOST(url, postData, success);
139 148 };
140 149
141 150 var initPullRequestsCodeMirror = function (textAreaId) {
142 151 var ta = $(textAreaId).get(0);
143 152 var initialHeight = '100px';
144 153
145 154 // default options
146 155 var codeMirrorOptions = {
147 156 mode: "text",
148 157 lineNumbers: false,
149 158 indentUnit: 4,
150 159 theme: 'rc-input'
151 160 };
152 161
153 162 var codeMirrorInstance = CodeMirror.fromTextArea(ta, codeMirrorOptions);
154 163 // marker for manually set description
155 164 codeMirrorInstance._userDefinedDesc = false;
156 165 codeMirrorInstance.setSize(null, initialHeight);
157 166 codeMirrorInstance.on("change", function(instance, changeObj) {
158 167 var height = initialHeight;
159 168 var lines = instance.lineCount();
160 169 if (lines > 6 && lines < 20) {
161 170 height = "auto"
162 171 }
163 172 else if (lines >= 20) {
164 173 height = 20 * 15;
165 174 }
166 175 instance.setSize(null, height);
167 176
168 177 // detect if the change was trigger by auto desc, or user input
169 178 changeOrigin = changeObj.origin;
170 179
171 180 if (changeOrigin === "setValue") {
172 181 cmLog.debug('Change triggered by setValue');
173 182 }
174 183 else {
175 184 cmLog.debug('user triggered change !');
176 185 // set special marker to indicate user has created an input.
177 186 instance._userDefinedDesc = true;
178 187 }
179 188
180 189 });
181 190
182 191 return codeMirrorInstance
183 192 };
184 193
185 194 /**
186 195 * Reviewer autocomplete
187 196 */
188 197 var ReviewerAutoComplete = function(input_id) {
189 198 $('#'+input_id).autocomplete({
190 199 serviceUrl: pyroutes.url('user_autocomplete_data'),
191 200 minChars:2,
192 201 maxHeight:400,
193 202 deferRequestBy: 300, //miliseconds
194 203 showNoSuggestionNotice: true,
195 204 tabDisabled: true,
196 205 autoSelectFirst: true,
197 206 formatResult: autocompleteFormatResult,
198 207 lookupFilter: autocompleteFilterResult,
199 208 onSelect: function(suggestion, data){
200 209 addReviewMember(data.id, data.first_name, data.last_name,
201 210 data.username, data.icon_link);
202 211 $('#'+input_id).val('');
203 212 }
204 213 });
205 214 };
@@ -1,84 +1,100 b''
1 1 ## -*- coding: utf-8 -*-
2 2 ##
3 3 ## See also repo_settings.html
4 4 ##
5 5 <%inherit file="/base/base.html"/>
6 6
7 7 <%def name="title()">
8 8 ${_('%s repository settings') % c.repo_info.repo_name}
9 9 %if c.rhodecode_name:
10 10 &middot; ${h.branding(c.rhodecode_name)}
11 11 %endif
12 12 </%def>
13 13
14 14 <%def name="breadcrumbs_links()">
15 15 ${_('Settings')}
16 16 </%def>
17 17
18 18 <%def name="menu_bar_nav()">
19 19 ${self.menu_items(active='repositories')}
20 20 </%def>
21 21
22 22 <%def name="menu_bar_subnav()">
23 23 ${self.repo_menu(active='options')}
24 24 </%def>
25 25
26 26 <%def name="main_content()">
27 27 <%include file="/admin/repos/repo_edit_${c.active}.html"/>
28 28 </%def>
29 29
30 30
31 31 <%def name="main()">
32 32 <div class="box">
33 33 <div class="title">
34 34 ${self.repo_page_title(c.rhodecode_db_repo)}
35 35 ${self.breadcrumbs()}
36 36 </div>
37 37
38 38 <div class="sidebar-col-wrapper scw-small">
39 39 ##main
40 40 <div class="sidebar">
41 41 <ul class="nav nav-pills nav-stacked">
42 42 <li class="${'active' if c.active=='settings' else ''}">
43 43 <a href="${h.url('edit_repo', repo_name=c.repo_name)}">${_('Settings')}</a>
44 44 </li>
45 45 <li class="${'active' if c.active=='permissions' else ''}">
46 46 <a href="${h.url('edit_repo_perms', repo_name=c.repo_name)}">${_('Permissions')}</a>
47 47 </li>
48 48 <li class="${'active' if c.active=='advanced' else ''}">
49 49 <a href="${h.url('edit_repo_advanced', repo_name=c.repo_name)}">${_('Advanced')}</a>
50 50 </li>
51 51 <li class="${'active' if c.active=='vcs' else ''}">
52 52 <a href="${h.url('repo_vcs_settings', repo_name=c.repo_name)}">${_('VCS')}</a>
53 53 </li>
54 54 <li class="${'active' if c.active=='fields' else ''}">
55 55 <a href="${h.url('edit_repo_fields', repo_name=c.repo_name)}">${_('Extra Fields')}</a>
56 56 </li>
57 57 <li class="${'active' if c.active=='issuetracker' else ''}">
58 58 <a href="${h.url('repo_settings_issuetracker', repo_name=c.repo_name)}">${_('Issue Tracker')}</a>
59 59 </li>
60 60 <li class="${'active' if c.active=='caches' else ''}">
61 61 <a href="${h.url('edit_repo_caches', repo_name=c.repo_name)}">${_('Caches')}</a>
62 62 </li>
63 63 %if c.repo_info.repo_type != 'svn':
64 64 <li class="${'active' if c.active=='remote' else ''}">
65 65 <a href="${h.url('edit_repo_remote', repo_name=c.repo_name)}">${_('Remote')}</a>
66 66 </li>
67 67 %endif
68 68 <li class="${'active' if c.active=='statistics' else ''}">
69 69 <a href="${h.url('edit_repo_statistics', repo_name=c.repo_name)}">${_('Statistics')}</a>
70 70 </li>
71 71 <li class="${'active' if c.active=='integrations' else ''}">
72 72 <a href="${h.route_path('repo_integrations_home', repo_name=c.repo_name)}">${_('Integrations')}</a>
73 73 </li>
74 ## TODO: dan: replace repo navigation with navlist registry like with
75 ## admin menu. First must find way to allow runtime configuration
76 ## it to account for the c.repo_info.repo_type != 'svn' call above
77 <%
78 reviewer_settings = False
79 try:
80 import rc_reviewers
81 reviewer_settings = True
82 except ImportError:
83 pass
84 %>
85 %if reviewer_settings:
86 <li class="${'active' if c.active=='reviewers' else ''}">
87 <a href="${h.route_path('repo_reviewers_home', repo_name=c.repo_name)}">${_('Reviewers')}</a>
88 </li>
89 %endif
74 90 </ul>
75 91 </div>
76 92
77 93 <div class="main-content-full-width">
78 94 ${self.main_content()}
79 95 </div>
80 96
81 97 </div>
82 98 </div>
83 99
84 100 </%def> No newline at end of file
@@ -1,541 +1,567 b''
1 1 <%inherit file="/base/base.html"/>
2 2
3 3 <%def name="title()">
4 4 ${c.repo_name} ${_('New pull request')}
5 5 </%def>
6 6
7 7 <%def name="breadcrumbs_links()">
8 8 ${_('New pull request')}
9 9 </%def>
10 10
11 11 <%def name="menu_bar_nav()">
12 12 ${self.menu_items(active='repositories')}
13 13 </%def>
14 14
15 15 <%def name="menu_bar_subnav()">
16 16 ${self.repo_menu(active='showpullrequest')}
17 17 </%def>
18 18
19 19 <%def name="main()">
20 20 <div class="box">
21 21 <div class="title">
22 22 ${self.repo_page_title(c.rhodecode_db_repo)}
23 23 ${self.breadcrumbs()}
24 24 </div>
25 25
26 26 ${h.secure_form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')}
27 27 <div class="box pr-summary">
28 28
29 29 <div class="summary-details block-left">
30 30
31 31 <div class="form">
32 32 <!-- fields -->
33 33
34 34 <div class="fields" >
35 35
36 36 <div class="field">
37 37 <div class="label">
38 38 <label for="pullrequest_title">${_('Title')}:</label>
39 39 </div>
40 40 <div class="input">
41 41 ${h.text('pullrequest_title', c.default_title, class_="medium autogenerated-title")}
42 42 </div>
43 43 </div>
44 44
45 45 <div class="field">
46 46 <div class="label label-textarea">
47 47 <label for="pullrequest_desc">${_('Description')}:</label>
48 48 </div>
49 49 <div class="textarea text-area editor">
50 50 ${h.textarea('pullrequest_desc',size=30, )}
51 51 <span class="help-block">
52 52 ${_('Write a short description on this pull request')}
53 53 </span>
54 54 </div>
55 55 </div>
56 56
57 57 <div class="field">
58 58 <div class="label label-textarea">
59 59 <label for="pullrequest_desc">${_('Commit flow')}:</label>
60 60 </div>
61 61
62 62 ## TODO: johbo: Abusing the "content" class here to get the
63 63 ## desired effect. Should be replaced by a proper solution.
64 64
65 65 ##ORG
66 66 <div class="content">
67 67 <strong>${_('Origin repository')}:</strong>
68 68 ${c.rhodecode_db_repo.description}
69 69 </div>
70 70 <div class="content">
71 71 ${h.hidden('source_repo')}
72 72 ${h.hidden('source_ref')}
73 73 </div>
74 74
75 75 ##OTHER, most Probably the PARENT OF THIS FORK
76 76 <div class="content">
77 77 ## filled with JS
78 78 <div id="target_repo_desc"></div>
79 79 </div>
80 80
81 81 <div class="content">
82 82 ${h.hidden('target_repo')}
83 83 ${h.hidden('target_ref')}
84 84 <span id="target_ref_loading" style="display: none">
85 85 ${_('Loading refs...')}
86 86 </span>
87 87 </div>
88 88 </div>
89 89
90 90 <div class="field">
91 91 <div class="label label-textarea">
92 92 <label for="pullrequest_submit"></label>
93 93 </div>
94 94 <div class="input">
95 95 <div class="pr-submit-button">
96 96 ${h.submit('save',_('Submit Pull Request'),class_="btn")}
97 97 </div>
98 98 <div id="pr_open_message"></div>
99 99 </div>
100 100 </div>
101 101
102 102 <div class="pr-spacing-container"></div>
103 103 </div>
104 104 </div>
105 105 </div>
106 106 <div>
107 107 <div class="reviewers-title block-right">
108 108 <div class="pr-details-title">
109 109 ${_('Pull request reviewers')}
110 110 </div>
111 111 </div>
112 112 <div id="reviewers" class="block-right pr-details-content reviewers">
113 113 ## members goes here, filled via JS based on initial selection !
114 114 <ul id="review_members" class="group_members"></ul>
115 115 <div id="add_reviewer_input" class='ac'>
116 116 <div class="reviewer_ac">
117 117 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer'))}
118 118 <div id="reviewers_container"></div>
119 119 </div>
120 120 </div>
121 121 </div>
122 122 </div>
123 123 </div>
124 124 <div class="box">
125 125 <div>
126 126 ## overview pulled by ajax
127 127 <div id="pull_request_overview"></div>
128 128 </div>
129 129 </div>
130 130 ${h.end_form()}
131 131 </div>
132 132
133 133 <script type="text/javascript">
134 134 $(function(){
135 135 var defaultSourceRepo = '${c.default_repo_data['source_repo_name']}';
136 136 var defaultSourceRepoData = ${c.default_repo_data['source_refs_json']|n};
137 137 var defaultTargetRepo = '${c.default_repo_data['target_repo_name']}';
138 138 var defaultTargetRepoData = ${c.default_repo_data['target_refs_json']|n};
139 139 var targetRepoName = '${c.repo_name}';
140 140
141 141 var $pullRequestForm = $('#pull_request_form');
142 142 var $sourceRepo = $('#source_repo', $pullRequestForm);
143 143 var $targetRepo = $('#target_repo', $pullRequestForm);
144 144 var $sourceRef = $('#source_ref', $pullRequestForm);
145 145 var $targetRef = $('#target_ref', $pullRequestForm);
146 146
147 147 var calculateContainerWidth = function() {
148 148 var maxWidth = 0;
149 149 var repoSelect2Containers = ['#source_repo', '#target_repo'];
150 150 $.each(repoSelect2Containers, function(idx, value) {
151 151 $(value).select2('container').width('auto');
152 152 var curWidth = $(value).select2('container').width();
153 153 if (maxWidth <= curWidth) {
154 154 maxWidth = curWidth;
155 155 }
156 156 $.each(repoSelect2Containers, function(idx, value) {
157 157 $(value).select2('container').width(maxWidth + 10);
158 158 });
159 159 });
160 160 };
161 161
162 162 var initRefSelection = function(selectedRef) {
163 163 return function(element, callback) {
164 164 // translate our select2 id into a text, it's a mapping to show
165 165 // simple label when selecting by internal ID.
166 166 var id, refData;
167 167 if (selectedRef === undefined) {
168 168 id = element.val();
169 169 refData = element.val().split(':');
170 170 } else {
171 171 id = selectedRef;
172 172 refData = selectedRef.split(':');
173 173 }
174 174
175 175 var text = refData[1];
176 176 if (refData[0] === 'rev') {
177 177 text = text.substring(0, 12);
178 178 }
179 179
180 180 var data = {id: id, text: text};
181 181
182 182 callback(data);
183 183 };
184 184 };
185 185
186 186 var formatRefSelection = function(item) {
187 187 var prefix = '';
188 188 var refData = item.id.split(':');
189 189 if (refData[0] === 'branch') {
190 190 prefix = '<i class="icon-branch"></i>';
191 191 }
192 192 else if (refData[0] === 'book') {
193 193 prefix = '<i class="icon-bookmark"></i>';
194 194 }
195 195 else if (refData[0] === 'tag') {
196 196 prefix = '<i class="icon-tag"></i>';
197 197 }
198 198
199 199 var originalOption = item.element;
200 200 return prefix + item.text;
201 201 };
202 202
203 203 // custom code mirror
204 204 var codeMirrorInstance = initPullRequestsCodeMirror('#pullrequest_desc');
205 205
206 206 var queryTargetRepo = function(self, query) {
207 207 // cache ALL results if query is empty
208 208 var cacheKey = query.term || '__';
209 209 var cachedData = self.cachedDataSource[cacheKey];
210 210
211 211 if (cachedData) {
212 212 query.callback({results: cachedData.results});
213 213 } else {
214 214 $.ajax({
215 215 url: pyroutes.url('pullrequest_repo_destinations', {'repo_name': targetRepoName}),
216 216 data: {query: query.term},
217 217 dataType: 'json',
218 218 type: 'GET',
219 219 success: function(data) {
220 220 self.cachedDataSource[cacheKey] = data;
221 221 query.callback({results: data.results});
222 222 },
223 223 error: function(data, textStatus, errorThrown) {
224 224 alert(
225 225 "Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
226 226 }
227 227 });
228 228 }
229 229 };
230 230
231 231 var queryTargetRefs = function(initialData, query) {
232 232 var data = {results: []};
233 233 // filter initialData
234 234 $.each(initialData, function() {
235 235 var section = this.text;
236 236 var children = [];
237 237 $.each(this.children, function() {
238 238 if (query.term.length === 0 ||
239 239 this.text.toUpperCase().indexOf(query.term.toUpperCase()) >= 0 ) {
240 240 children.push({'id': this.id, 'text': this.text})
241 241 }
242 242 });
243 243 data.results.push({'text': section, 'children': children})
244 244 });
245 245 query.callback({results: data.results});
246 246 };
247 247
248 248 var prButtonLock = function(lockEnabled, msg) {
249 249 if (lockEnabled) {
250 250 $('#save').attr('disabled', 'disabled');
251 251 }
252 252 else {
253 253 $('#save').removeAttr('disabled');
254 254 }
255 255
256 256 $('#pr_open_message').html(msg);
257 257
258 258 };
259 259
260 260 var loadRepoRefDiffPreview = function() {
261 261 var sourceRepo = $sourceRepo.eq(0).val();
262 262 var sourceRef = $sourceRef.eq(0).val().split(':');
263 263
264 264 var targetRepo = $targetRepo.eq(0).val();
265 265 var targetRef = $targetRef.eq(0).val().split(':');
266 266
267 267 var url_data = {
268 268 'repo_name': targetRepo,
269 269 'target_repo': sourceRepo,
270 270 'source_ref': targetRef[2],
271 271 'source_ref_type': 'rev',
272 272 'target_ref': sourceRef[2],
273 273 'target_ref_type': 'rev',
274 274 'merge': true,
275 275 '_': Date.now() // bypass browser caching
276 276 }; // gather the source/target ref and repo here
277 277
278 278 if (sourceRef.length !== 3 || targetRef.length !== 3) {
279 279 prButtonLock(true, "${_('Please select origin and destination')}");
280 280 return;
281 281 }
282 282 var url = pyroutes.url('compare_url', url_data);
283 283
284 284 // lock PR button, so we cannot send PR before it's calculated
285 285 prButtonLock(true, "${_('Loading compare ...')}");
286 286
287 287 if (loadRepoRefDiffPreview._currentRequest) {
288 288 loadRepoRefDiffPreview._currentRequest.abort();
289 289 }
290 290
291 291 loadRepoRefDiffPreview._currentRequest = $.get(url)
292 292 .error(function(data, textStatus, errorThrown) {
293 293 alert(
294 294 "Error while processing request.\nError code {0} ({1}).".format(
295 295 data.status, data.statusText));
296 296 })
297 297 .done(function(data) {
298 298 loadRepoRefDiffPreview._currentRequest = null;
299 299 $('#pull_request_overview').html(data);
300 300 var commitElements = $(data).find('tr[commit_id]');
301 301
302 302 var prTitleAndDesc = getTitleAndDescription(sourceRef[1],
303 303 commitElements, 5);
304 304
305 305 var title = prTitleAndDesc[0];
306 306 var proposedDescription = prTitleAndDesc[1];
307 307
308 308 var useGeneratedTitle = (
309 309 $('#pullrequest_title').hasClass('autogenerated-title') ||
310 310 $('#pullrequest_title').val() === "");
311 311
312 312 if (title && useGeneratedTitle) {
313 313 // use generated title if we haven't specified our own
314 314 $('#pullrequest_title').val(title);
315 315 $('#pullrequest_title').addClass('autogenerated-title');
316 316
317 317 }
318 318
319 319 var useGeneratedDescription = (
320 320 !codeMirrorInstance._userDefinedDesc ||
321 321 codeMirrorInstance.getValue() === "");
322 322
323 323 if (proposedDescription && useGeneratedDescription) {
324 324 // set proposed content, if we haven't defined our own,
325 325 // or we don't have description written
326 326 codeMirrorInstance._userDefinedDesc = false; // reset state
327 327 codeMirrorInstance.setValue(proposedDescription);
328 328 }
329 329
330 330 var msg = '';
331 331 if (commitElements.length === 1) {
332 332 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 1)}";
333 333 } else {
334 334 msg = "${ungettext('This pull request will consist of __COMMITS__ commit.', 'This pull request will consist of __COMMITS__ commits.', 2)}";
335 335 }
336 336
337 337 msg += ' <a id="pull_request_overview_url" href="{0}" target="_blank">${_('Show detailed compare.')}</a>'.format(url);
338 338
339 339 if (commitElements.length) {
340 340 var commitsLink = '<a href="#pull_request_overview"><strong>{0}</strong></a>'.format(commitElements.length);
341 341 prButtonLock(false, msg.replace('__COMMITS__', commitsLink));
342 342 }
343 343 else {
344 344 prButtonLock(true, "${_('There are no commits to merge.')}");
345 345 }
346 346
347 347
348 348 });
349 349 };
350 350
351 351 /**
352 352 Generate Title and Description for a PullRequest.
353 353 In case of 1 commits, the title and description is that one commit
354 354 in case of multiple commits, we iterate on them with max N number of commits,
355 355 and build description in a form
356 356 - commitN
357 357 - commitN+1
358 358 ...
359 359
360 360 Title is then constructed from branch names, or other references,
361 361 replacing '-' and '_' into spaces
362 362
363 363 * @param sourceRef
364 364 * @param elements
365 365 * @param limit
366 366 * @returns {*[]}
367 367 */
368 368 var getTitleAndDescription = function(sourceRef, elements, limit) {
369 369 var title = '';
370 370 var desc = '';
371 371
372 372 $.each($(elements).get().reverse().slice(0, limit), function(idx, value) {
373 373 var rawMessage = $(value).find('td.td-description .message').data('messageRaw');
374 374 desc += '- ' + rawMessage.split('\n')[0].replace(/\n+$/, "") + '\n';
375 375 });
376 376 // only 1 commit, use commit message as title
377 377 if (elements.length == 1) {
378 378 title = $(elements[0]).find('td.td-description .message').data('messageRaw').split('\n')[0];
379 379 }
380 380 else {
381 381 // use reference name
382 382 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
383 383 }
384 384
385 385 return [title, desc]
386 386 };
387 387
388 388 var Select2Box = function(element, overrides) {
389 389 var globalDefaults = {
390 390 dropdownAutoWidth: true,
391 391 containerCssClass: "drop-menu",
392 392 dropdownCssClass: "drop-menu-dropdown",
393 393 };
394 394
395 395 var initSelect2 = function(defaultOptions) {
396 396 var options = jQuery.extend(globalDefaults, defaultOptions, overrides);
397 397 element.select2(options);
398 398 };
399 399
400 400 return {
401 401 initRef: function() {
402 402 var defaultOptions = {
403 403 minimumResultsForSearch: 5,
404 404 formatSelection: formatRefSelection
405 405 };
406 406
407 407 initSelect2(defaultOptions);
408 408 },
409 409
410 410 initRepo: function(defaultValue, readOnly) {
411 411 var defaultOptions = {
412 412 initSelection : function (element, callback) {
413 413 var data = {id: defaultValue, text: defaultValue};
414 414 callback(data);
415 415 }
416 416 };
417 417
418 418 initSelect2(defaultOptions);
419 419
420 420 element.select2('val', defaultSourceRepo);
421 421 if (readOnly === true) {
422 422 element.select2('readonly', true);
423 423 };
424 424 }
425 425 };
426 426 };
427 427
428 428 var initTargetRefs = function(refsData, selectedRef){
429 429 Select2Box($targetRef, {
430 430 query: function(query) {
431 431 queryTargetRefs(refsData, query);
432 432 },
433 433 initSelection : initRefSelection(selectedRef)
434 434 }).initRef();
435 435
436 436 if (!(selectedRef === undefined)) {
437 437 $targetRef.select2('val', selectedRef);
438 438 }
439 439 };
440 440
441 441 var targetRepoChanged = function(repoData) {
442 // reset && add the reviewer based on selected repo
443 $('#review_members').html('');
444 addReviewMember(
445 repoData.user.user_id, repoData.user.firstname,
446 repoData.user.lastname, repoData.user.username,
447 repoData.user.gravatar_link);
448
449 442 // generate new DESC of target repo displayed next to select
450 443 $('#target_repo_desc').html(
451 444 "<strong>${_('Destination repository')}</strong>: {0}".format(repoData['description'])
452 445 );
453 446
454 447 // generate dynamic select2 for refs.
455 448 initTargetRefs(repoData['refs']['select2_refs'],
456 449 repoData['refs']['selected_ref']);
457 450
458 451 };
459 452
460 453 var sourceRefSelect2 = Select2Box(
461 454 $sourceRef, {
462 455 placeholder: "${_('Select commit reference')}",
463 456 query: function(query) {
464 457 var initialData = defaultSourceRepoData['refs']['select2_refs'];
465 458 queryTargetRefs(initialData, query)
466 459 },
467 460 initSelection: initRefSelection()
468 461 }
469 462 );
470 463
471 464 var sourceRepoSelect2 = Select2Box($sourceRepo, {
472 465 query: function(query) {}
473 466 });
474 467
475 468 var targetRepoSelect2 = Select2Box($targetRepo, {
476 469 cachedDataSource: {},
477 470 query: $.debounce(250, function(query) {
478 471 queryTargetRepo(this, query);
479 472 }),
480 473 formatResult: formatResult
481 474 });
482 475
483 476 sourceRefSelect2.initRef();
484 477
485 478 sourceRepoSelect2.initRepo(defaultSourceRepo, true);
486 479
487 480 targetRepoSelect2.initRepo(defaultTargetRepo, false);
488 481
489 482 $sourceRef.on('change', function(e){
490 483 loadRepoRefDiffPreview();
484 loadDefaultReviewers();
491 485 });
492 486
493 487 $targetRef.on('change', function(e){
494 488 loadRepoRefDiffPreview();
489 loadDefaultReviewers();
495 490 });
496 491
497 492 $targetRepo.on('change', function(e){
498 493 var repoName = $(this).val();
499 494 calculateContainerWidth();
500 495 $targetRef.select2('destroy');
501 496 $('#target_ref_loading').show();
502 497
503 498 $.ajax({
504 499 url: pyroutes.url('pullrequest_repo_refs',
505 500 {'repo_name': targetRepoName, 'target_repo_name':repoName}),
506 501 data: {},
507 502 dataType: 'json',
508 503 type: 'GET',
509 504 success: function(data) {
510 505 $('#target_ref_loading').hide();
511 506 targetRepoChanged(data);
512 507 loadRepoRefDiffPreview();
513 508 },
514 509 error: function(data, textStatus, errorThrown) {
515 510 alert("Error while fetching entries.\nError code {0} ({1}).".format(data.status, data.statusText));
516 511 }
517 512 })
518 513
519 514 });
520 515
516 var loadDefaultReviewers = function() {
517 if (loadDefaultReviewers._currentRequest) {
518 loadDefaultReviewers._currentRequest.abort();
519 }
520 var url = pyroutes.url('repo_default_reviewers_data', {'repo_name': targetRepoName});
521
522 var sourceRepo = $sourceRepo.eq(0).val();
523 var sourceRef = $sourceRef.eq(0).val().split(':');
524 var targetRepo = $targetRepo.eq(0).val();
525 var targetRef = $targetRef.eq(0).val().split(':');
526 url += '?source_repo=' + sourceRepo;
527 url += '&source_ref=' + sourceRef[2];
528 url += '&target_repo=' + targetRepo;
529 url += '&target_ref=' + targetRef[2];
530
531 loadDefaultReviewers._currentRequest = $.get(url)
532 .done(function(data) {
533 loadDefaultReviewers._currentRequest = null;
534
535 // reset && add the reviewer based on selected repo
536 $('#review_members').html('');
537 for (var i = 0; i < data.reviewers.length; i++) {
538 var reviewer = data.reviewers[i];
539 addReviewMember(
540 reviewer.user_id, reviewer.firstname,
541 reviewer.lastname, reviewer.username,
542 reviewer.gravatar_link, reviewer.reasons);
543 }
544 });
545 };
521 546 prButtonLock(true, "${_('Please select origin and destination')}");
522 547
523 548 // auto-load on init, the target refs select2
524 549 calculateContainerWidth();
525 550 targetRepoChanged(defaultTargetRepoData);
526 551
527 552 $('#pullrequest_title').on('keyup', function(e){
528 553 $(this).removeClass('autogenerated-title');
529 554 });
530 555
531 556 %if c.default_source_ref:
532 557 // in case we have a pre-selected value, use it now
533 558 $sourceRef.select2('val', '${c.default_source_ref}');
534 559 loadRepoRefDiffPreview();
560 loadDefaultReviewers();
535 561 %endif
536 562
537 563 ReviewerAutoComplete('user');
538 564 });
539 565 </script>
540 566
541 567 </%def>
General Comments 0
You need to be logged in to leave comments. Login now