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