##// END OF EJS Templates
release: Merge default into stable for release preparation
marcink -
r4456:279b3293 merge stable
parent child Browse files
Show More
@@ -0,0 +1,74 b''
1 |RCE| 4.20.0 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2020-07-20
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13 - Comments: users can now edit comments body.
14 Editing is versioned and all older versions are kept for auditing.
15 - Pull requests: changed the order of close-branch after merge,
16 so branch heads are no longer left open after the merge.
17 - Diffs: added diff navigation to improve UX when browsing the full context diffs.
18 - Emails: set the `References` header for threading in emails with different subjects.
19 Only some Email clients supports this.
20 - Emails: added logic to allow overwriting the default email titles via rcextensions.
21 - Markdown: support summary/details tags to allow setting a link with expansion menu.
22 - Integrations: added `store_file` integration. This allows storing
23 selected files from repository on disk on push.
24
25
26 General
27 ^^^^^^^
28
29 - License: individual users can hide license flash messages warning about upcoming
30 license expiration.
31 - Downloads: the default download commit is now the landing revision set in repo settings.
32 - Auth-tokens: expose all roles with explanation to help users understand it better.
33 - Pull requests: make auto generated title for pull requests show also source Ref type
34 eg. branch feature1, instead of just name of the branch.
35 - UI: added secondary action instead of two buttons on files page, and download page.
36 - Emails: reduce excessive warning logs on pre-mailer.
37
38
39 Security
40 ^^^^^^^^
41
42 - Branch permissions: protect from XSS on branch rules forbidden flash message.
43
44
45 Performance
46 ^^^^^^^^^^^
47
48
49
50 Fixes
51 ^^^^^
52
53 - Pull requests: detect missing commits on diffs from new PR ancestor logic. This fixes
54 problem with older PRs opened before 4.19.X that had special ancestor set, which could
55 lead in some cases to crash when viewing older pull requests.
56 - Permissions: fixed a case when a duplicate permission made repository settings active on archived repository.
57 - Permissions: fixed missing user info on global and repository permissions pages.
58 - Permissions: allow users to update settings for repository groups they still own,
59 or have admin perms, when they don't change their name.
60 - Permissions: flush all when running remap and rescan.
61 - Repositories: fixed a bug for repo groups that didn't pre-fill the repo group from GET param.
62 - Repositories: allow updating repository settings for users without
63 store-in-root permissions in case repository name didn't change.
64 - Comments: fixed line display icons.
65 - Summary: fixed summary page total commits count.
66
67
68 Upgrade notes
69 ^^^^^^^^^^^^^
70
71 - Schedule feature update.
72 - On Mercurial repositories we changed the order of commits when the close branch on merge features is used.
73 Before the commits was made after a merge leaving an open head.
74 This backward incompatible change now reverses that order, which is the correct way of doing it.
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,91 b''
1 // jQuery Scrollstop Plugin v1.2.0
2 // https://github.com/ssorallen/jquery-scrollstop
3
4 (function (factory) {
5 // UMD[2] wrapper for jQuery plugins to work in AMD or in CommonJS.
6 //
7 // [2] https://github.com/umdjs/umd
8
9 if (typeof define === 'function' && define.amd) {
10 // AMD. Register as an anonymous module.
11 define(['jquery'], factory);
12 } else if (typeof exports === 'object') {
13 // Node/CommonJS
14 module.exports = factory(require('jquery'));
15 } else {
16 // Browser globals
17 factory(jQuery);
18 }
19 }(function ($) {
20 // $.event.dispatch was undocumented and was deprecated in jQuery 1.7[1]. It
21 // was replaced by $.event.handle in jQuery 1.9.
22 //
23 // Use the first of the available functions to support jQuery <1.8.
24 //
25 // [1] https://github.com/jquery/jquery-migrate/blob/master/src/event.js#L25
26 var dispatch = $.event.dispatch || $.event.handle;
27
28 var special = $.event.special,
29 uid1 = 'D' + (+new Date()),
30 uid2 = 'D' + (+new Date() + 1);
31
32 special.scrollstart = {
33 setup: function(data) {
34 var _data = $.extend({
35 latency: special.scrollstop.latency
36 }, data);
37
38 var timer,
39 handler = function(evt) {
40 var _self = this,
41 _args = arguments;
42
43 if (timer) {
44 clearTimeout(timer);
45 } else {
46 evt.type = 'scrollstart';
47 dispatch.apply(_self, _args);
48 }
49
50 timer = setTimeout(function() {
51 timer = null;
52 }, _data.latency);
53 };
54
55 $(this).bind('scroll', handler).data(uid1, handler);
56 },
57 teardown: function() {
58 $(this).unbind('scroll', $(this).data(uid1));
59 }
60 };
61
62 special.scrollstop = {
63 latency: 250,
64 setup: function(data) {
65 var _data = $.extend({
66 latency: special.scrollstop.latency
67 }, data);
68
69 var timer,
70 handler = function(evt) {
71 var _self = this,
72 _args = arguments;
73
74 if (timer) {
75 clearTimeout(timer);
76 }
77
78 timer = setTimeout(function() {
79 timer = null;
80 evt.type = 'scrollstop';
81 dispatch.apply(_self, _args);
82 }, _data.latency);
83 };
84
85 $(this).bind('scroll', handler).data(uid2, handler);
86 },
87 teardown: function() {
88 $(this).unbind('scroll', $(this).data(uid2));
89 }
90 };
91 }));
@@ -0,0 +1,171 b''
1 /**
2 * Within Viewport jQuery Plugin
3 *
4 * @description Companion plugin for withinviewport.js - determines whether an element is completely within the browser viewport
5 * @author Craig Patik, http://patik.com/
6 * @version 2.1.2
7 * @date 2019-08-16
8 */
9 (function ($) {
10 /**
11 * $.withinviewport()
12 * @description jQuery method
13 * @param {Object} [settings] optional settings
14 * @return {Collection} Contains all elements that were within the viewport
15 */
16 $.fn.withinviewport = function (settings) {
17 var opts;
18 var elems;
19
20 if (typeof settings === 'string') {
21 settings = {
22 sides: settings
23 };
24 }
25
26 opts = $.extend({}, settings, {
27 sides: 'all'
28 });
29 elems = [];
30
31 this.each(function () {
32 if (withinviewport(this, opts)) {
33 elems.push(this);
34 }
35 });
36
37 return $(elems);
38 };
39
40 // Main custom selector
41 $.extend($.expr[':'], {
42 'within-viewport': function (element) {
43 return withinviewport(element, 'all');
44 }
45 });
46
47 /**
48 * Optional enhancements and shortcuts
49 *
50 * @description Uncomment or comment these pieces as they apply to your project and coding preferences
51 */
52
53 // Shorthand jQuery methods
54
55 $.fn.withinviewporttop = function (settings) {
56 var opts;
57 var elems;
58
59 if (typeof settings === 'string') {
60 settings = {
61 sides: settings
62 };
63 }
64
65 opts = $.extend({}, settings, {
66 sides: 'top'
67 });
68 elems = [];
69
70 this.each(function () {
71 if (withinviewport(this, opts)) {
72 elems.push(this);
73 }
74 });
75
76 return $(elems);
77 };
78
79 $.fn.withinviewportright = function (settings) {
80 var opts;
81 var elems;
82
83 if (typeof settings === 'string') {
84 settings = {
85 sides: settings
86 };
87 }
88
89 opts = $.extend({}, settings, {
90 sides: 'right'
91 });
92 elems = [];
93
94 this.each(function () {
95 if (withinviewport(this, opts)) {
96 elems.push(this);
97 }
98 });
99
100 return $(elems);
101 };
102
103 $.fn.withinviewportbottom = function (settings) {
104 var opts;
105 var elems;
106
107 if (typeof settings === 'string') {
108 settings = {
109 sides: settings
110 };
111 }
112
113 opts = $.extend({}, settings, {
114 sides: 'bottom'
115 });
116 elems = [];
117
118 this.each(function () {
119 if (withinviewport(this, opts)) {
120 elems.push(this);
121 }
122 });
123
124 return $(elems);
125 };
126
127 $.fn.withinviewportleft = function (settings) {
128 var opts;
129 var elems;
130
131 if (typeof settings === 'string') {
132 settings = {
133 sides: settings
134 };
135 }
136
137 opts = $.extend({}, settings, {
138 sides: 'left'
139 });
140 elems = [];
141
142 this.each(function () {
143 if (withinviewport(this, opts)) {
144 elems.push(this);
145 }
146 });
147
148 return $(elems);
149 };
150
151 // Custom jQuery selectors
152 $.extend($.expr[':'], {
153 'within-viewport-top': function (element) {
154 return withinviewport(element, 'top');
155 },
156 'within-viewport-right': function (element) {
157 return withinviewport(element, 'right');
158 },
159 'within-viewport-bottom': function (element) {
160 return withinviewport(element, 'bottom');
161 },
162 'within-viewport-left': function (element) {
163 return withinviewport(element, 'left');
164 }
165 // Example custom selector:
166 //,
167 // 'within-viewport-top-left-45': function (element) {
168 // return withinviewport(element, {sides:'top left', top: 45, left: 45});
169 // }
170 });
171 }(jQuery)); No newline at end of file
@@ -0,0 +1,235 b''
1 /**
2 * Within Viewport
3 *
4 * @description Determines whether an element is completely within the browser viewport
5 * @author Craig Patik, http://patik.com/
6 * @version 2.1.2
7 * @date 2019-08-16
8 */
9 (function (root, name, factory) {
10 // AMD
11 if (typeof define === 'function' && define.amd) {
12 define([], factory);
13 }
14 // Node and CommonJS-like environments
15 else if (typeof module !== 'undefined' && typeof exports === 'object') {
16 module.exports = factory();
17 }
18 // Browser global
19 else {
20 root[name] = factory();
21 }
22 }(this, 'withinviewport', function () {
23 var canUseWindowDimensions = typeof window !== 'undefined' && window.innerHeight !== undefined; // IE 8 and lower fail this
24
25 /**
26 * Determines whether an element is within the viewport
27 * @param {Object} elem DOM Element (required)
28 * @param {Object} options Optional settings
29 * @return {Boolean} Whether the element was completely within the viewport
30 */
31 var withinviewport = function withinviewport(elem, options) {
32 var result = false;
33 var metadata = {};
34 var config = {};
35 var settings;
36 var isWithin;
37 var isContainerTheWindow;
38 var elemBoundingRect;
39 var containerBoundingRect;
40 var containerScrollTop;
41 var containerScrollLeft;
42 var scrollBarWidths = [0, 0];
43 var sideNamesPattern;
44 var sides;
45 var side;
46 var i;
47
48 // If invoked by the jQuery plugin, get the actual DOM element
49 if (typeof jQuery !== 'undefined' && elem instanceof jQuery) {
50 elem = elem.get(0);
51 }
52
53 if (typeof elem !== 'object' || elem.nodeType !== 1) {
54 throw new Error('First argument must be an element');
55 }
56
57 // Look for inline settings on the element
58 if (elem.getAttribute('data-withinviewport-settings') && window.JSON) {
59 metadata = JSON.parse(elem.getAttribute('data-withinviewport-settings'));
60 }
61
62 // Settings argument may be a simple string (`top`, `right`, etc)
63 if (typeof options === 'string') {
64 settings = {
65 sides: options
66 };
67 } else {
68 settings = options || {};
69 }
70
71 // Build configuration from defaults and user-provided settings and metadata
72 config.container = settings.container || metadata.container || withinviewport.defaults.container || window;
73 config.sides = settings.sides || metadata.sides || withinviewport.defaults.sides || 'all';
74 config.top = settings.top || metadata.top || withinviewport.defaults.top || 0;
75 config.right = settings.right || metadata.right || withinviewport.defaults.right || 0;
76 config.bottom = settings.bottom || metadata.bottom || withinviewport.defaults.bottom || 0;
77 config.left = settings.left || metadata.left || withinviewport.defaults.left || 0;
78
79 // Extract the DOM node from a jQuery collection
80 if (typeof jQuery !== 'undefined' && config.container instanceof jQuery) {
81 config.container = config.container.get(0);
82 }
83
84 // Use the window as the container if the user specified the body or a non-element
85 if (config.container === document.body || config.container.nodeType !== 1) {
86 config.container = window;
87 }
88
89 isContainerTheWindow = (config.container === window);
90
91 // Element testing methods
92 isWithin = {
93 // Element is below the top edge of the viewport
94 top: function _isWithin_top() {
95 if (isContainerTheWindow) {
96 return (elemBoundingRect.top >= config.top);
97 } else {
98 return (elemBoundingRect.top >= containerScrollTop - (containerScrollTop - containerBoundingRect.top) + config.top);
99 }
100 },
101
102 // Element is to the left of the right edge of the viewport
103 right: function _isWithin_right() {
104 // Note that `elemBoundingRect.right` is the distance from the *left* of the viewport to the element's far right edge
105
106 if (isContainerTheWindow) {
107 return (elemBoundingRect.right <= (containerBoundingRect.right + containerScrollLeft) - config.right);
108 } else {
109 return (elemBoundingRect.right <= containerBoundingRect.right - scrollBarWidths[0] - config.right);
110 }
111 },
112
113 // Element is above the bottom edge of the viewport
114 bottom: function _isWithin_bottom() {
115 var containerHeight = 0;
116
117 if (isContainerTheWindow) {
118 if (canUseWindowDimensions) {
119 containerHeight = config.container.innerHeight;
120 } else if (document && document.documentElement) {
121 containerHeight = document.documentElement.clientHeight;
122 }
123 } else {
124 containerHeight = containerBoundingRect.bottom;
125 }
126
127 // Note that `elemBoundingRect.bottom` is the distance from the *top* of the viewport to the element's bottom edge
128 return (elemBoundingRect.bottom <= containerHeight - scrollBarWidths[1] - config.bottom);
129 },
130
131 // Element is to the right of the left edge of the viewport
132 left: function _isWithin_left() {
133 if (isContainerTheWindow) {
134 return (elemBoundingRect.left >= config.left);
135 } else {
136 return (elemBoundingRect.left >= containerScrollLeft - (containerScrollLeft - containerBoundingRect.left) + config.left);
137 }
138 },
139
140 // Element is within all four boundaries
141 all: function _isWithin_all() {
142 // Test each boundary in order of efficiency and likeliness to be false. This way we can avoid running all four functions on most elements.
143 // 1. Top: Quickest to calculate + most likely to be false
144 // 2. Bottom: Note quite as quick to calculate, but also very likely to be false
145 // 3-4. Left and right are both equally unlikely to be false since most sites only scroll vertically, but left is faster to calculate
146 return (isWithin.top() && isWithin.bottom() && isWithin.left() && isWithin.right());
147 }
148 };
149
150 // Get the element's bounding rectangle with respect to the viewport
151 elemBoundingRect = elem.getBoundingClientRect();
152
153 // Get viewport dimensions and offsets
154 if (isContainerTheWindow) {
155 containerBoundingRect = document.documentElement.getBoundingClientRect();
156 containerScrollTop = document.body.scrollTop;
157 containerScrollLeft = window.scrollX || document.body.scrollLeft;
158 } else {
159 containerBoundingRect = config.container.getBoundingClientRect();
160 containerScrollTop = config.container.scrollTop;
161 containerScrollLeft = config.container.scrollLeft;
162 }
163
164 // Don't count the space consumed by scrollbars
165 if (containerScrollLeft) {
166 scrollBarWidths[0] = 18;
167 }
168
169 if (containerScrollTop) {
170 scrollBarWidths[1] = 16;
171 }
172
173 // Test the element against each side of the viewport that was requested
174 sideNamesPattern = /^top$|^right$|^bottom$|^left$|^all$/;
175
176 // Loop through all of the sides
177 sides = config.sides.split(' ');
178 i = sides.length;
179
180 while (i--) {
181 side = sides[i].toLowerCase();
182
183 if (sideNamesPattern.test(side)) {
184 if (isWithin[side]()) {
185 result = true;
186 } else {
187 result = false;
188
189 // Quit as soon as the first failure is found
190 break;
191 }
192 }
193 }
194
195 return result;
196 };
197
198 // Default settings
199 withinviewport.prototype.defaults = {
200 container: typeof document !== 'undefined' ? document.body : {},
201 sides: 'all',
202 top: 0,
203 right: 0,
204 bottom: 0,
205 left: 0
206 };
207
208 withinviewport.defaults = withinviewport.prototype.defaults;
209
210 /**
211 * Optional enhancements and shortcuts
212 *
213 * @description Uncomment or comment these pieces as they apply to your project and coding preferences
214 */
215
216 // Shortcut methods for each side of the viewport
217 // Example: `withinviewport.top(elem)` is the same as `withinviewport(elem, 'top')`
218 withinviewport.prototype.top = function _withinviewport_top(element) {
219 return withinviewport(element, 'top');
220 };
221
222 withinviewport.prototype.right = function _withinviewport_right(element) {
223 return withinviewport(element, 'right');
224 };
225
226 withinviewport.prototype.bottom = function _withinviewport_bottom(element) {
227 return withinviewport(element, 'bottom');
228 };
229
230 withinviewport.prototype.left = function _withinviewport_left(element) {
231 return withinviewport(element, 'left');
232 };
233
234 return withinviewport;
235 })); No newline at end of file
@@ -0,0 +1,31 b''
1 <%namespace name="base" file="/base/base.mako"/>
2
3 <%
4 active_pattern_entries = h.get_active_pattern_entries(getattr(c, 'repo_name', None))
5 %>
6
7 ## NOTE, inline styles are here to override the default rendering of
8 ## the swal JS dialog which this template is displayed
9
10 <div style="text-align: left;">
11
12 <div style="border-bottom: 1px solid #dbd9da; padding-bottom: 5px; height: 20px">
13
14 <div class="pull-left">
15 ${base.gravatar_with_user(c.comment_history.author.email, 16, tooltip=True)}
16 </div>
17
18 <div class="pull-right">
19 <code>edited: ${h.age_component(c.comment_history.created_on)}</code>
20 </div>
21
22 </div>
23
24 <div style="margin: 5px 0px">
25 <code>comment body at v${c.comment_history.version}:</code>
26 </div>
27 <div class="text" style="padding-top: 20px; border: 1px solid #dbd9da">
28 ${h.render(c.comment_history.text, renderer=c.comment_history.comment.renderer, mentions=True, repo_name=getattr(c, 'repo_name', None), active_pattern_entries=active_pattern_entries)}
29 </div>
30
31 </div> No newline at end of file
@@ -1,6 +1,5 b''
1 1 [bumpversion]
2 current_version = 4.19.3
2 current_version = 4.20.0
3 3 message = release: Bump version {current_version} to {new_version}
4 4
5 5 [bumpversion:file:rhodecode/VERSION]
6
@@ -5,25 +5,20 b' done = false'
5 5 done = true
6 6
7 7 [task:rc_tools_pinned]
8 done = true
9 8
10 9 [task:fixes_on_stable]
11 done = true
12 10
13 11 [task:pip2nix_generated]
14 done = true
15 12
16 13 [task:changelog_updated]
17 done = true
18 14
19 15 [task:generate_api_docs]
20 done = true
16
17 [task:updated_translation]
21 18
22 19 [release]
23 state = prepared
24 version = 4.19.3
25
26 [task:updated_translation]
20 state = in_progress
21 version = 4.20.0
27 22
28 23 [task:generate_js_routes]
29 24
@@ -238,7 +238,7 b' following URL: ``{instance-URL}/_admin/p'
238 238 pong[rce-7880] => 203.0.113.23
239 239
240 240 .. _Markdown: http://daringfireball.net/projects/markdown/
241 .. _reStructured Text: http://docutils.sourceforge.net/docs/index.html
241 .. _reStructured Text: http://docutils.sourceforge.io/docs/index.html
242 242
243 243
244 244 Unarchiving a repository
@@ -75,7 +75,7 b' Below config if for an Apache Reverse Pr'
75 75 # Url to running RhodeCode instance. This is shown as `- URL:` when
76 76 # running rccontrol status.
77 77
78 ProxyPass / http://127.0.0.1:10002/ timeout=7200 Keepalive=On
78 ProxyPass / http://127.0.0.1:10002/ connectiontimeout=7200 timeout=7200 Keepalive=On
79 79 ProxyPassReverse / http://127.0.0.1:10002/
80 80
81 81 # strict http prevents from https -> http downgrade
@@ -252,6 +252,7 b' get_pull_request_comments'
252 252 },
253 253 "comment_text": "Example text",
254 254 "comment_type": null,
255 "comment_last_version: 0,
255 256 "pull_request_version": null,
256 257 "comment_commit_id": None,
257 258 "comment_pull_request_id": <pull_request_id>
@@ -173,6 +173,37 b' delete_repo'
173 173 error: null
174 174
175 175
176 edit_comment
177 ------------
178
179 .. py:function:: edit_comment(apiuser, message, comment_id, version, userid=<Optional:<OptionalAttr:apiuser>>)
180
181 Edit comment on the pull request or commit,
182 specified by the `comment_id` and version. Initially version should be 0
183
184 :param apiuser: This is filled automatically from the |authtoken|.
185 :type apiuser: AuthUser
186 :param comment_id: Specify the comment_id for editing
187 :type comment_id: int
188 :param version: version of the comment that will be created, starts from 0
189 :type version: int
190 :param message: The text content of the comment.
191 :type message: str
192 :param userid: Comment on the pull request as this user
193 :type userid: Optional(str or int)
194
195 Example output:
196
197 .. code-block:: bash
198
199 id : <id_given_in_input>
200 result : {
201 "comment": "<comment data>",
202 "version": "<Integer>",
203 },
204 error : null
205
206
176 207 fork_repo
177 208 ---------
178 209
@@ -236,6 +267,40 b' fork_repo'
236 267 error: null
237 268
238 269
270 get_comment
271 -----------
272
273 .. py:function:: get_comment(apiuser, comment_id)
274
275 Get single comment from repository or pull_request
276
277 :param apiuser: This is filled automatically from the |authtoken|.
278 :type apiuser: AuthUser
279 :param comment_id: comment id found in the URL of comment
280 :type comment_id: str or int
281
282 Example error output:
283
284 .. code-block:: bash
285
286 {
287 "id" : <id_given_in_input>,
288 "result" : {
289 "comment_author": <USER_DETAILS>,
290 "comment_created_on": "2017-02-01T14:38:16.309",
291 "comment_f_path": "file.txt",
292 "comment_id": 282,
293 "comment_lineno": "n1",
294 "comment_resolved_by": null,
295 "comment_status": [],
296 "comment_text": "This file needs a header",
297 "comment_type": "todo",
298 "comment_last_version: 0
299 },
300 "error" : null
301 }
302
303
239 304 get_repo
240 305 --------
241 306
@@ -436,7 +501,8 b' get_repo_comments'
436 501 "comment_resolved_by": null,
437 502 "comment_status": [],
438 503 "comment_text": "This file needs a header",
439 "comment_type": "todo"
504 "comment_type": "todo",
505 "comment_last_version: 0
440 506 }
441 507 ],
442 508 "error" : null
@@ -11,16 +11,22 b' use the below example to insert it.'
11 11 Once configured you can check the settings for your |RCE| instance on the
12 12 :menuselection:`Admin --> Settings --> Email` page.
13 13
14 Please be aware that both section should be changed the `[DEFAULT]` for main applications
15 email config, and `[server:main]` for exception tracking email
16
14 17 .. code-block:: ini
15 18
16 ################################################################################
17 ## Uncomment and replace with the email address which should receive ##
18 ## any error reports after an application crash ##
19 ## Additionally these settings will be used by the RhodeCode mailing system ##
20 ################################################################################
21 #email_to = admin@localhost
19 [DEFAULT]
20 ; ########################################################################
21 ; EMAIL CONFIGURATION
22 ; These settings will be used by the RhodeCode mailing system
23 ; ########################################################################
24
25 ; prefix all emails subjects with given prefix, helps filtering out emails
26 #email_prefix = [RhodeCode]
27
28 ; email FROM address all mails will be sent
22 29 #app_email_from = rhodecode-noreply@localhost
23 #email_prefix = [RhodeCode]
24 30
25 31 #smtp_server = mail.server.com
26 32 #smtp_username =
@@ -28,3 +34,12 b' Once configured you can check the settin'
28 34 #smtp_port =
29 35 #smtp_use_tls = false
30 36 #smtp_use_ssl = true
37
38 [server:main]
39 ; Send email with exception details when it happens
40 #exception_tracker.send_email = true
41
42 ; Comma separated list of recipients for exception emails,
43 ; e.g admin@rhodecode.com,devops@rhodecode.com
44 ; Can be left empty, then emails will be sent to ALL super-admins
45 #exception_tracker.send_email_recipients =
@@ -9,6 +9,7 b' Release Notes'
9 9 .. toctree::
10 10 :maxdepth: 1
11 11
12 release-notes-4.20.0.rst
12 13 release-notes-4.19.3.rst
13 14 release-notes-4.19.2.rst
14 15 release-notes-4.19.1.rst
@@ -51,9 +51,12 b''
51 51 "<%= dirs.js.src %>/plugins/jquery.pjax.js",
52 52 "<%= dirs.js.src %>/plugins/jquery.dataTables.js",
53 53 "<%= dirs.js.src %>/plugins/flavoured_checkbox.js",
54 "<%= dirs.js.src %>/plugins/within_viewport.js",
54 55 "<%= dirs.js.src %>/plugins/jquery.auto-grow-input.js",
55 56 "<%= dirs.js.src %>/plugins/jquery.autocomplete.js",
56 57 "<%= dirs.js.src %>/plugins/jquery.debounce.js",
58 "<%= dirs.js.src %>/plugins/jquery.scrollstop.js",
59 "<%= dirs.js.src %>/plugins/jquery.within-viewport.js",
57 60 "<%= dirs.js.node_modules %>/mark.js/dist/jquery.mark.min.js",
58 61 "<%= dirs.js.src %>/plugins/jquery.timeago.js",
59 62 "<%= dirs.js.src %>/plugins/jquery.timeago-extension.js",
@@ -1819,7 +1819,7 b' self: super: {'
1819 1819 };
1820 1820 };
1821 1821 "rhodecode-enterprise-ce" = super.buildPythonPackage {
1822 name = "rhodecode-enterprise-ce-4.19.3";
1822 name = "rhodecode-enterprise-ce-4.20.0";
1823 1823 buildInputs = [
1824 1824 self."pytest"
1825 1825 self."py"
@@ -10,6 +10,8 b' vcsserver_config_http = rhodecode/tests/'
10 10 addopts =
11 11 --pdbcls=IPython.terminal.debugger:TerminalPdb
12 12 --strict-markers
13 --capture=no
14 --show-capture=no
13 15
14 16 markers =
15 17 vcs_operations: Mark tests depending on a running RhodeCode instance.
@@ -1,1 +1,1 b''
1 4.19.3 No newline at end of file
1 4.20.0 No newline at end of file
@@ -48,7 +48,7 b' PYRAMID_SETTINGS = {}'
48 48 EXTENSIONS = {}
49 49
50 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 52 __platform__ = platform.system()
53 53 __license__ = 'AGPLv3, and Commercial License'
54 54 __author__ = 'RhodeCode GmbH'
@@ -88,7 +88,8 b' class TestApi(object):'
88 88 response = api_call(self.app, params)
89 89 expected = 'No such method: comment. ' \
90 90 'Similar methods: changeset_comment, comment_pull_request, ' \
91 'get_pull_request_comments, comment_commit, get_repo_comments'
91 'get_pull_request_comments, comment_commit, edit_comment, ' \
92 'get_comment, get_repo_comments'
92 93 assert_error(id_, expected, given=response.body)
93 94
94 95 def test_api_disabled_user(self, request):
@@ -21,7 +21,7 b''
21 21 import pytest
22 22
23 23 from rhodecode.model.comment import CommentsModel
24 from rhodecode.model.db import UserLog, User
24 from rhodecode.model.db import UserLog, User, ChangesetComment
25 25 from rhodecode.model.pull_request import PullRequestModel
26 26 from rhodecode.tests import TEST_USER_ADMIN_LOGIN
27 27 from rhodecode.api.tests.utils import (
@@ -218,8 +218,20 b' class TestCommentPullRequest(object):'
218 218 assert_error(id_, expected, given=response.body)
219 219
220 220 @pytest.mark.backends("git", "hg")
221 def test_api_comment_pull_request_non_admin_with_userid_error(
222 self, pr_util):
221 def test_api_comment_pull_request_non_admin_with_userid_error(self, pr_util):
222 pull_request = pr_util.create_pull_request()
223 id_, params = build_data(
224 self.apikey_regular, 'comment_pull_request',
225 repoid=pull_request.target_repo.repo_name,
226 pullrequestid=pull_request.pull_request_id,
227 userid=TEST_USER_ADMIN_LOGIN)
228 response = api_call(self.app, params)
229
230 expected = 'userid is not the same as your user'
231 assert_error(id_, expected, given=response.body)
232
233 @pytest.mark.backends("git", "hg")
234 def test_api_comment_pull_request_non_admin_with_userid_error(self, pr_util):
223 235 pull_request = pr_util.create_pull_request()
224 236 id_, params = build_data(
225 237 self.apikey_regular, 'comment_pull_request',
@@ -244,3 +256,135 b' class TestCommentPullRequest(object):'
244 256
245 257 expected = 'Invalid commit_id `XXX` for this pull request.'
246 258 assert_error(id_, expected, given=response.body)
259
260 @pytest.mark.backends("git", "hg")
261 def test_api_edit_comment(self, pr_util):
262 pull_request = pr_util.create_pull_request()
263
264 id_, params = build_data(
265 self.apikey,
266 'comment_pull_request',
267 repoid=pull_request.target_repo.repo_name,
268 pullrequestid=pull_request.pull_request_id,
269 message='test message',
270 )
271 response = api_call(self.app, params)
272 json_response = response.json
273 comment_id = json_response['result']['comment_id']
274
275 message_after_edit = 'just message'
276 id_, params = build_data(
277 self.apikey,
278 'edit_comment',
279 comment_id=comment_id,
280 message=message_after_edit,
281 version=0,
282 )
283 response = api_call(self.app, params)
284 json_response = response.json
285 assert json_response['result']['version'] == 1
286
287 text_form_db = ChangesetComment.get(comment_id).text
288 assert message_after_edit == text_form_db
289
290 @pytest.mark.backends("git", "hg")
291 def test_api_edit_comment_wrong_version(self, pr_util):
292 pull_request = pr_util.create_pull_request()
293
294 id_, params = build_data(
295 self.apikey, 'comment_pull_request',
296 repoid=pull_request.target_repo.repo_name,
297 pullrequestid=pull_request.pull_request_id,
298 message='test message')
299 response = api_call(self.app, params)
300 json_response = response.json
301 comment_id = json_response['result']['comment_id']
302
303 message_after_edit = 'just message'
304 id_, params = build_data(
305 self.apikey_regular,
306 'edit_comment',
307 comment_id=comment_id,
308 message=message_after_edit,
309 version=1,
310 )
311 response = api_call(self.app, params)
312 expected = 'comment ({}) version ({}) mismatch'.format(comment_id, 1)
313 assert_error(id_, expected, given=response.body)
314
315 @pytest.mark.backends("git", "hg")
316 def test_api_edit_comment_wrong_version(self, pr_util):
317 pull_request = pr_util.create_pull_request()
318
319 id_, params = build_data(
320 self.apikey, 'comment_pull_request',
321 repoid=pull_request.target_repo.repo_name,
322 pullrequestid=pull_request.pull_request_id,
323 message='test message')
324 response = api_call(self.app, params)
325 json_response = response.json
326 comment_id = json_response['result']['comment_id']
327
328 id_, params = build_data(
329 self.apikey,
330 'edit_comment',
331 comment_id=comment_id,
332 message='',
333 version=0,
334 )
335 response = api_call(self.app, params)
336 expected = "comment ({}) can't be changed with empty string".format(comment_id, 1)
337 assert_error(id_, expected, given=response.body)
338
339 @pytest.mark.backends("git", "hg")
340 def test_api_edit_comment_wrong_user_set_by_non_admin(self, pr_util):
341 pull_request = pr_util.create_pull_request()
342 pull_request_id = pull_request.pull_request_id
343 id_, params = build_data(
344 self.apikey,
345 'comment_pull_request',
346 repoid=pull_request.target_repo.repo_name,
347 pullrequestid=pull_request_id,
348 message='test message'
349 )
350 response = api_call(self.app, params)
351 json_response = response.json
352 comment_id = json_response['result']['comment_id']
353
354 id_, params = build_data(
355 self.apikey_regular,
356 'edit_comment',
357 comment_id=comment_id,
358 message='just message',
359 version=0,
360 userid=TEST_USER_ADMIN_LOGIN
361 )
362 response = api_call(self.app, params)
363 expected = 'userid is not the same as your user'
364 assert_error(id_, expected, given=response.body)
365
366 @pytest.mark.backends("git", "hg")
367 def test_api_edit_comment_wrong_user_with_permissions_to_edit_comment(self, pr_util):
368 pull_request = pr_util.create_pull_request()
369 pull_request_id = pull_request.pull_request_id
370 id_, params = build_data(
371 self.apikey,
372 'comment_pull_request',
373 repoid=pull_request.target_repo.repo_name,
374 pullrequestid=pull_request_id,
375 message='test message'
376 )
377 response = api_call(self.app, params)
378 json_response = response.json
379 comment_id = json_response['result']['comment_id']
380
381 id_, params = build_data(
382 self.apikey_regular,
383 'edit_comment',
384 comment_id=comment_id,
385 message='just message',
386 version=0,
387 )
388 response = api_call(self.app, params)
389 expected = "you don't have access to edit this comment"
390 assert_error(id_, expected, given=response.body)
@@ -233,8 +233,8 b' class TestCreateRepoGroup(object):'
233 233
234 234 expected = {
235 235 'repo_group':
236 'Parent repository group `{}` does not exist'.format(
237 repo_group_name)}
236 u"You do not have the permissions to store "
237 u"repository groups inside repository group `{}`".format(repo_group_name)}
238 238 try:
239 239 assert_error(id_, expected, given=response.body)
240 240 finally:
@@ -37,8 +37,10 b' class TestGetMethod(object):'
37 37 id_, params = build_data(self.apikey, 'get_method', pattern='*comment*')
38 38 response = api_call(self.app, params)
39 39
40 expected = ['changeset_comment', 'comment_pull_request',
41 'get_pull_request_comments', 'comment_commit', 'get_repo_comments']
40 expected = [
41 'changeset_comment', 'comment_pull_request', 'get_pull_request_comments',
42 'comment_commit', 'edit_comment', 'get_comment', 'get_repo_comments'
43 ]
42 44 assert_ok(id_, expected, given=response.body)
43 45
44 46 def test_get_methods_on_single_match(self):
@@ -61,6 +61,7 b' class TestGetPullRequestComments(object)'
61 61 'comment_type': 'note',
62 62 'comment_resolved_by': None,
63 63 'pull_request_version': None,
64 'comment_last_version': 0,
64 65 'comment_commit_id': None,
65 66 'comment_pull_request_id': pull_request.pull_request_id
66 67 }
@@ -42,26 +42,27 b' def make_repo_comments_factory(request):'
42 42 comments = []
43 43
44 44 # general
45 CommentsModel().create(
45 comment = CommentsModel().create(
46 46 text='General Comment', repo=repo, user=user, commit_id=commit_id,
47 47 comment_type=ChangesetComment.COMMENT_TYPE_NOTE, send_email=False)
48 comments.append(comment)
48 49
49 50 # inline
50 CommentsModel().create(
51 comment = CommentsModel().create(
51 52 text='Inline Comment', repo=repo, user=user, commit_id=commit_id,
52 53 f_path=file_0, line_no='n1',
53 54 comment_type=ChangesetComment.COMMENT_TYPE_NOTE, send_email=False)
55 comments.append(comment)
54 56
55 57 # todo
56 CommentsModel().create(
58 comment = CommentsModel().create(
57 59 text='INLINE TODO Comment', repo=repo, user=user, commit_id=commit_id,
58 60 f_path=file_0, line_no='n1',
59 61 comment_type=ChangesetComment.COMMENT_TYPE_TODO, send_email=False)
62 comments.append(comment)
60 63
61 @request.addfinalizer
62 def cleanup():
63 for comment in comments:
64 Session().delete(comment)
64 return comments
65
65 66 return Make()
66 67
67 68
@@ -108,3 +109,34 b' class TestGetRepo(object):'
108 109 id_, params = build_data(self.apikey, 'get_repo_comments', **api_call_params)
109 110 response = api_call(self.app, params)
110 111 assert_error(id_, expected, given=response.body)
112
113 def test_api_get_comment(self, make_repo_comments_factory, backend_hg):
114 commits = [{'message': 'A'}, {'message': 'B'}]
115 repo = backend_hg.create_repo(commits=commits)
116
117 comments = make_repo_comments_factory.make_comments(repo)
118 comment_ids = [x.comment_id for x in comments]
119 Session().commit()
120
121 for comment_id in comment_ids:
122 id_, params = build_data(self.apikey, 'get_comment',
123 **{'comment_id': comment_id})
124 response = api_call(self.app, params)
125 result = assert_call_ok(id_, given=response.body)
126 assert result['comment_id'] == comment_id
127
128 def test_api_get_comment_no_access(self, make_repo_comments_factory, backend_hg, user_util):
129 commits = [{'message': 'A'}, {'message': 'B'}]
130 repo = backend_hg.create_repo(commits=commits)
131 comments = make_repo_comments_factory.make_comments(repo)
132 comment_id = comments[0].comment_id
133
134 test_user = user_util.create_user()
135 user_util.grant_user_permission_to_repo(repo, test_user, 'repository.none')
136
137 id_, params = build_data(test_user.api_key, 'get_comment',
138 **{'comment_id': comment_id})
139 response = api_call(self.app, params)
140 assert_error(id_,
141 expected='comment `{}` does not exist'.format(comment_id),
142 given=response.body)
@@ -21,7 +21,6 b''
21 21
22 22 import logging
23 23
24 from rhodecode import events
25 24 from rhodecode.api import jsonrpc_method, JSONRPCError, JSONRPCValidationError
26 25 from rhodecode.api.utils import (
27 26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
@@ -36,8 +35,7 b' from rhodecode.model.db import Session, '
36 35 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
37 36 from rhodecode.model.settings import SettingsModel
38 37 from rhodecode.model.validation_schema import Invalid
39 from rhodecode.model.validation_schema.schemas.reviewer_schema import(
40 ReviewerListSchema)
38 from rhodecode.model.validation_schema.schemas.reviewer_schema import ReviewerListSchema
41 39
42 40 log = logging.getLogger(__name__)
43 41
@@ -292,10 +290,11 b' def merge_pull_request('
292 290 else:
293 291 repo = pull_request.target_repo
294 292 auth_user = apiuser
293
295 294 if not isinstance(userid, Optional):
296 if (has_superadmin_permission(apiuser) or
297 HasRepoPermissionAnyApi('repository.admin')(
298 user=apiuser, repo_name=repo.repo_name)):
295 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
296 user=apiuser, repo_name=repo.repo_name)
297 if has_superadmin_permission(apiuser) or is_repo_admin:
299 298 apiuser = get_user_or_error(userid)
300 299 auth_user = apiuser.AuthUser()
301 300 else:
@@ -379,6 +378,7 b' def get_pull_request_comments('
379 378 },
380 379 "comment_text": "Example text",
381 380 "comment_type": null,
381 "comment_last_version: 0,
382 382 "pull_request_version": null,
383 383 "comment_commit_id": None,
384 384 "comment_pull_request_id": <pull_request_id>
@@ -510,9 +510,9 b' def comment_pull_request('
510 510
511 511 auth_user = apiuser
512 512 if not isinstance(userid, Optional):
513 if (has_superadmin_permission(apiuser) or
514 HasRepoPermissionAnyApi('repository.admin')(
515 user=apiuser, repo_name=repo.repo_name)):
513 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
514 user=apiuser, repo_name=repo.repo_name)
515 if has_superadmin_permission(apiuser) or is_repo_admin:
516 516 apiuser = get_user_or_error(userid)
517 517 auth_user = apiuser.AuthUser()
518 518 else:
@@ -979,10 +979,10 b' def close_pull_request('
979 979 else:
980 980 repo = pull_request.target_repo
981 981
982 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
983 user=apiuser, repo_name=repo.repo_name)
982 984 if not isinstance(userid, Optional):
983 if (has_superadmin_permission(apiuser) or
984 HasRepoPermissionAnyApi('repository.admin')(
985 user=apiuser, repo_name=repo.repo_name)):
985 if has_superadmin_permission(apiuser) or is_repo_admin:
986 986 apiuser = get_user_or_error(userid)
987 987 else:
988 988 raise JSONRPCError('userid is not the same as your user')
@@ -31,11 +31,15 b' from rhodecode.api.utils import ('
31 31 validate_set_owner_permissions)
32 32 from rhodecode.lib import audit_logger, rc_cache
33 33 from rhodecode.lib import repo_maintenance
34 from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi
34 from rhodecode.lib.auth import (
35 HasPermissionAnyApi, HasUserGroupPermissionAnyApi,
36 HasRepoPermissionAnyApi)
35 37 from rhodecode.lib.celerylib.utils import get_task_id
36 from rhodecode.lib.utils2 import str2bool, time_to_datetime, safe_str, safe_int, safe_unicode
38 from rhodecode.lib.utils2 import (
39 str2bool, time_to_datetime, safe_str, safe_int, safe_unicode)
37 40 from rhodecode.lib.ext_json import json
38 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
41 from rhodecode.lib.exceptions import (
42 StatusChangeOnClosedPullRequestError, CommentVersionMismatch)
39 43 from rhodecode.lib.vcs import RepositoryError
40 44 from rhodecode.lib.vcs.exceptions import NodeDoesNotExistError
41 45 from rhodecode.model.changeset_status import ChangesetStatusModel
@@ -44,6 +48,7 b' from rhodecode.model.db import ('
44 48 Session, ChangesetStatus, RepositoryField, Repository, RepoGroup,
45 49 ChangesetComment)
46 50 from rhodecode.model.permission import PermissionModel
51 from rhodecode.model.pull_request import PullRequestModel
47 52 from rhodecode.model.repo import RepoModel
48 53 from rhodecode.model.scm import ScmModel, RepoList
49 54 from rhodecode.model.settings import SettingsModel, VcsSettingsModel
@@ -1719,7 +1724,8 b' def get_repo_comments(request, apiuser, '
1719 1724 "comment_resolved_by": null,
1720 1725 "comment_status": [],
1721 1726 "comment_text": "This file needs a header",
1722 "comment_type": "todo"
1727 "comment_type": "todo",
1728 "comment_last_version: 0
1723 1729 }
1724 1730 ],
1725 1731 "error" : null
@@ -1752,6 +1758,157 b' def get_repo_comments(request, apiuser, '
1752 1758
1753 1759
1754 1760 @jsonrpc_method()
1761 def get_comment(request, apiuser, comment_id):
1762 """
1763 Get single comment from repository or pull_request
1764
1765 :param apiuser: This is filled automatically from the |authtoken|.
1766 :type apiuser: AuthUser
1767 :param comment_id: comment id found in the URL of comment
1768 :type comment_id: str or int
1769
1770 Example error output:
1771
1772 .. code-block:: bash
1773
1774 {
1775 "id" : <id_given_in_input>,
1776 "result" : {
1777 "comment_author": <USER_DETAILS>,
1778 "comment_created_on": "2017-02-01T14:38:16.309",
1779 "comment_f_path": "file.txt",
1780 "comment_id": 282,
1781 "comment_lineno": "n1",
1782 "comment_resolved_by": null,
1783 "comment_status": [],
1784 "comment_text": "This file needs a header",
1785 "comment_type": "todo",
1786 "comment_last_version: 0
1787 },
1788 "error" : null
1789 }
1790
1791 """
1792
1793 comment = ChangesetComment.get(comment_id)
1794 if not comment:
1795 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1796
1797 perms = ('repository.read', 'repository.write', 'repository.admin')
1798 has_comment_perm = HasRepoPermissionAnyApi(*perms)\
1799 (user=apiuser, repo_name=comment.repo.repo_name)
1800
1801 if not has_comment_perm:
1802 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1803
1804 return comment
1805
1806
1807 @jsonrpc_method()
1808 def edit_comment(request, apiuser, message, comment_id, version,
1809 userid=Optional(OAttr('apiuser'))):
1810 """
1811 Edit comment on the pull request or commit,
1812 specified by the `comment_id` and version. Initially version should be 0
1813
1814 :param apiuser: This is filled automatically from the |authtoken|.
1815 :type apiuser: AuthUser
1816 :param comment_id: Specify the comment_id for editing
1817 :type comment_id: int
1818 :param version: version of the comment that will be created, starts from 0
1819 :type version: int
1820 :param message: The text content of the comment.
1821 :type message: str
1822 :param userid: Comment on the pull request as this user
1823 :type userid: Optional(str or int)
1824
1825 Example output:
1826
1827 .. code-block:: bash
1828
1829 id : <id_given_in_input>
1830 result : {
1831 "comment": "<comment data>",
1832 "version": "<Integer>",
1833 },
1834 error : null
1835 """
1836
1837 auth_user = apiuser
1838 comment = ChangesetComment.get(comment_id)
1839 if not comment:
1840 raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1841
1842 is_super_admin = has_superadmin_permission(apiuser)
1843 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1844 (user=apiuser, repo_name=comment.repo.repo_name)
1845
1846 if not isinstance(userid, Optional):
1847 if is_super_admin or is_repo_admin:
1848 apiuser = get_user_or_error(userid)
1849 auth_user = apiuser.AuthUser()
1850 else:
1851 raise JSONRPCError('userid is not the same as your user')
1852
1853 comment_author = comment.author.user_id == auth_user.user_id
1854 if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1855 raise JSONRPCError("you don't have access to edit this comment")
1856
1857 try:
1858 comment_history = CommentsModel().edit(
1859 comment_id=comment_id,
1860 text=message,
1861 auth_user=auth_user,
1862 version=version,
1863 )
1864 Session().commit()
1865 except CommentVersionMismatch:
1866 raise JSONRPCError(
1867 'comment ({}) version ({}) mismatch'.format(comment_id, version)
1868 )
1869 if not comment_history and not message:
1870 raise JSONRPCError(
1871 "comment ({}) can't be changed with empty string".format(comment_id)
1872 )
1873
1874 if comment.pull_request:
1875 pull_request = comment.pull_request
1876 PullRequestModel().trigger_pull_request_hook(
1877 pull_request, apiuser, 'comment_edit',
1878 data={'comment': comment})
1879 else:
1880 db_repo = comment.repo
1881 commit_id = comment.revision
1882 commit = db_repo.get_commit(commit_id)
1883 CommentsModel().trigger_commit_comment_hook(
1884 db_repo, apiuser, 'edit',
1885 data={'comment': comment, 'commit': commit})
1886
1887 data = {
1888 'comment': comment,
1889 'version': comment_history.version if comment_history else None,
1890 }
1891 return data
1892
1893
1894 # TODO(marcink): write this with all required logic for deleting a comments in PR or commits
1895 # @jsonrpc_method()
1896 # def delete_comment(request, apiuser, comment_id):
1897 # auth_user = apiuser
1898 #
1899 # comment = ChangesetComment.get(comment_id)
1900 # if not comment:
1901 # raise JSONRPCError('comment `%s` does not exist' % (comment_id,))
1902 #
1903 # is_super_admin = has_superadmin_permission(apiuser)
1904 # is_repo_admin = HasRepoPermissionAnyApi('repository.admin')\
1905 # (user=apiuser, repo_name=comment.repo.repo_name)
1906 #
1907 # comment_author = comment.author.user_id == auth_user.user_id
1908 # if not (comment.immutable is False and (is_super_admin or is_repo_admin) or comment_author):
1909 # raise JSONRPCError("you don't have access to edit this comment")
1910
1911 @jsonrpc_method()
1755 1912 def grant_user_permission(request, apiuser, repoid, userid, perm):
1756 1913 """
1757 1914 Grant permissions for the specified user on the given repository,
@@ -69,6 +69,7 b' class AdminRepoGroupsView(BaseAppView, D'
69 69 c.repo_groups = RepoGroup.groups_choices(
70 70 groups=groups_with_admin_rights,
71 71 show_empty_group=allow_empty_group)
72 c.personal_repo_group = self._rhodecode_user.personal_repo_group
72 73
73 74 def _can_create_repo_group(self, parent_group_id=None):
74 75 is_admin = HasPermissionAny('hg.admin')('group create controller')
@@ -261,15 +262,28 b' class AdminRepoGroupsView(BaseAppView, D'
261 262
262 263 # perm check for admin, create_group perm or admin of parent_group
263 264 parent_group_id = safe_int(self.request.GET.get('parent_group'))
265 _gr = RepoGroup.get(parent_group_id)
264 266 if not self._can_create_repo_group(parent_group_id):
265 267 raise HTTPForbidden()
266 268
267 269 self._load_form_data(c)
268 270
269 271 defaults = {} # Future proof for default of repo group
272
273 parent_group_choice = '-1'
274 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
275 parent_group_choice = self._rhodecode_user.personal_repo_group
276
277 if parent_group_id and _gr:
278 if parent_group_id in [x[0] for x in c.repo_groups]:
279 parent_group_choice = safe_unicode(parent_group_id)
280
281 defaults.update({'group_parent_id': parent_group_choice})
282
270 283 data = render(
271 284 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
272 285 self._get_template_context(c), self.request)
286
273 287 html = formencode.htmlfill.render(
274 288 data,
275 289 defaults=defaults,
@@ -169,8 +169,8 b' class AdminReposView(BaseAppView, DataGr'
169 169 c = self.load_default_context()
170 170
171 171 new_repo = self.request.GET.get('repo', '')
172 parent_group = safe_int(self.request.GET.get('parent_group'))
173 _gr = RepoGroup.get(parent_group)
172 parent_group_id = safe_int(self.request.GET.get('parent_group'))
173 _gr = RepoGroup.get(parent_group_id)
174 174
175 175 if not HasPermissionAny('hg.admin', 'hg.create.repository')():
176 176 # you're not super admin nor have global create permissions,
@@ -196,9 +196,9 b' class AdminReposView(BaseAppView, DataGr'
196 196 if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
197 197 parent_group_choice = self._rhodecode_user.personal_repo_group
198 198
199 if parent_group and _gr:
200 if parent_group in [x[0] for x in c.repo_groups]:
201 parent_group_choice = safe_unicode(parent_group)
199 if parent_group_id and _gr:
200 if parent_group_id in [x[0] for x in c.repo_groups]:
201 parent_group_choice = safe_unicode(parent_group_id)
202 202
203 203 defaults.update({'repo_group': parent_group_choice})
204 204
@@ -47,6 +47,7 b' from rhodecode.model.db import RhodeCode'
47 47 from rhodecode.model.forms import (ApplicationSettingsForm,
48 48 ApplicationUiSettingsForm, ApplicationVisualisationForm,
49 49 LabsSettingsForm, IssueTrackerPatternsForm)
50 from rhodecode.model.permission import PermissionModel
50 51 from rhodecode.model.repo_group import RepoGroupModel
51 52
52 53 from rhodecode.model.scm import ScmModel
@@ -253,8 +254,7 b' class AdminSettingsView(BaseAppView):'
253 254 c.active = 'mapping'
254 255 rm_obsolete = self.request.POST.get('destroy', False)
255 256 invalidate_cache = self.request.POST.get('invalidate', False)
256 log.debug(
257 'rescanning repo location with destroy obsolete=%s', rm_obsolete)
257 log.debug('rescanning repo location with destroy obsolete=%s', rm_obsolete)
258 258
259 259 if invalidate_cache:
260 260 log.debug('invalidating all repositories cache')
@@ -263,6 +263,8 b' class AdminSettingsView(BaseAppView):'
263 263
264 264 filesystem_repos = ScmModel().repo_scan()
265 265 added, removed = repo2db_mapper(filesystem_repos, rm_obsolete)
266 PermissionModel().trigger_permission_flush()
267
266 268 _repr = lambda l: ', '.join(map(safe_unicode, l)) or '-'
267 269 h.flash(_('Repositories successfully '
268 270 'rescanned added: %s ; removed: %s') %
@@ -576,8 +578,7 b' class AdminSettingsView(BaseAppView):'
576 578 'user': self._rhodecode_db_user
577 579 }
578 580
579 (subject, headers, email_body,
580 email_body_plaintext) = EmailNotificationModel().render_email(
581 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
581 582 EmailNotificationModel.TYPE_EMAIL_TEST, **email_kwargs)
582 583
583 584 recipients = [test_email] if test_email else None
@@ -376,8 +376,7 b' users: description edit fixes'
376 376 }
377 377
378 378 template_type = email_id.split('+')[0]
379 (c.subject, c.headers, c.email_body,
380 c.email_body_plaintext) = EmailNotificationModel().render_email(
379 (c.subject, c.email_body, c.email_body_plaintext) = EmailNotificationModel().render_email(
381 380 template_type, **email_kwargs.get(email_id, {}))
382 381
383 382 test_email = self.request.GET.get('email')
@@ -302,7 +302,7 b' class TestGistsController(TestController'
302 302 assert_response = response.assert_response()
303 303 assert_response.element_equals_to(
304 304 'div.rc-user span.user',
305 '<a href="/_profiles/test_admin">test_admin</a></span>')
305 '<a href="/_profiles/test_admin">test_admin</a>')
306 306
307 307 response.mustcontain('gist-desc')
308 308
@@ -328,7 +328,7 b' class TestGistsController(TestController'
328 328 assert_response = response.assert_response()
329 329 assert_response.element_equals_to(
330 330 'div.rc-user span.user',
331 '<a href="/_profiles/test_admin">test_admin</a></span>')
331 '<a href="/_profiles/test_admin">test_admin</a>')
332 332 response.mustcontain('gist-desc')
333 333
334 334 def test_show_as_raw(self, create_gist):
@@ -79,6 +79,10 b' def includeme(config):'
79 79 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
80 80
81 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 86 name='repo_commit_comment_attachment_upload',
83 87 pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/attachment_upload', repo_route=True)
84 88
@@ -86,6 +90,10 b' def includeme(config):'
86 90 name='repo_commit_comment_delete',
87 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 97 # still working url for backward compat.
90 98 config.add_route(
91 99 name='repo_commit_raw_deprecated',
@@ -328,6 +336,11 b' def includeme(config):'
328 336 repo_route=True)
329 337
330 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 344 name='pullrequest_comment_delete',
332 345 pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
333 346 repo_route=True, repo_accepted_types=['hg', 'git'])
@@ -35,6 +35,7 b' def route_path(name, params=None, **kwar'
35 35 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
36 36 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
37 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 39 }[name].format(**kwargs)
39 40
40 41 if params:
@@ -268,6 +269,164 b' class TestRepoCommitCommentsView(TestCon'
268 269 repo_name=backend.repo_name, commit_id=commit_id))
269 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=409,
382 )
383 assert response.status_int == 409
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 430 def test_delete_forbidden_for_immutable_comments(self, backend):
272 431 self.log_user()
273 432 commit_id = backend.repo.get_commit('300').raw_id
@@ -30,6 +30,7 b' from rhodecode.model.db import ('
30 30 from rhodecode.model.meta import Session
31 31 from rhodecode.model.pull_request import PullRequestModel
32 32 from rhodecode.model.user import UserModel
33 from rhodecode.model.comment import CommentsModel
33 34 from rhodecode.tests import (
34 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 55 'pullrequest_delete': '/{repo_name}/pull-request/{pull_request_id}/delete',
55 56 'pullrequest_comment_create': '/{repo_name}/pull-request/{pull_request_id}/comment',
56 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 59 }[name].format(**kwargs)
58 60
59 61 if params:
@@ -114,6 +116,223 b' class TestPullrequestsView(object):'
114 116 if range_diff == "1":
115 117 response.mustcontain('Turn off: Show the diff as commit range')
116 118
119 def test_show_versions_of_pr(self, backend, csrf_token):
120 commits = [
121 {'message': 'initial-commit',
122 'added': [FileNode('test-file.txt', 'LINE1\n')]},
123
124 {'message': 'commit-1',
125 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\n')]},
126 # Above is the initial version of PR that changes a single line
127
128 # from now on we'll add 3x commit adding a nother line on each step
129 {'message': 'commit-2',
130 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\n')]},
131
132 {'message': 'commit-3',
133 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\n')]},
134
135 {'message': 'commit-4',
136 'changed': [FileNode('test-file.txt', 'LINE1\nLINE2\nLINE3\nLINE4\nLINE5\n')]},
137 ]
138
139 commit_ids = backend.create_master_repo(commits)
140 target = backend.create_repo(heads=['initial-commit'])
141 source = backend.create_repo(heads=['commit-1'])
142 source_repo_name = source.repo_name
143 target_repo_name = target.repo_name
144
145 target_ref = 'branch:{branch}:{commit_id}'.format(
146 branch=backend.default_branch_name, commit_id=commit_ids['initial-commit'])
147 source_ref = 'branch:{branch}:{commit_id}'.format(
148 branch=backend.default_branch_name, commit_id=commit_ids['commit-1'])
149
150 response = self.app.post(
151 route_path('pullrequest_create', repo_name=source.repo_name),
152 [
153 ('source_repo', source.repo_name),
154 ('source_ref', source_ref),
155 ('target_repo', target.repo_name),
156 ('target_ref', target_ref),
157 ('common_ancestor', commit_ids['initial-commit']),
158 ('pullrequest_title', 'Title'),
159 ('pullrequest_desc', 'Description'),
160 ('description_renderer', 'markdown'),
161 ('__start__', 'review_members:sequence'),
162 ('__start__', 'reviewer:mapping'),
163 ('user_id', '1'),
164 ('__start__', 'reasons:sequence'),
165 ('reason', 'Some reason'),
166 ('__end__', 'reasons:sequence'),
167 ('__start__', 'rules:sequence'),
168 ('__end__', 'rules:sequence'),
169 ('mandatory', 'False'),
170 ('__end__', 'reviewer:mapping'),
171 ('__end__', 'review_members:sequence'),
172 ('__start__', 'revisions:sequence'),
173 ('revisions', commit_ids['commit-1']),
174 ('__end__', 'revisions:sequence'),
175 ('user', ''),
176 ('csrf_token', csrf_token),
177 ],
178 status=302)
179
180 location = response.headers['Location']
181
182 pull_request_id = location.rsplit('/', 1)[1]
183 assert pull_request_id != 'new'
184 pull_request = PullRequest.get(int(pull_request_id))
185
186 pull_request_id = pull_request.pull_request_id
187
188 # Show initial version of PR
189 response = self.app.get(
190 route_path('pullrequest_show',
191 repo_name=target_repo_name,
192 pull_request_id=pull_request_id))
193
194 response.mustcontain('commit-1')
195 response.mustcontain(no=['commit-2'])
196 response.mustcontain(no=['commit-3'])
197 response.mustcontain(no=['commit-4'])
198
199 response.mustcontain('cb-addition"></span><span>LINE2</span>')
200 response.mustcontain(no=['LINE3'])
201 response.mustcontain(no=['LINE4'])
202 response.mustcontain(no=['LINE5'])
203
204 # update PR #1
205 source_repo = Repository.get_by_repo_name(source_repo_name)
206 backend.pull_heads(source_repo, heads=['commit-2'])
207 response = self.app.post(
208 route_path('pullrequest_update',
209 repo_name=target_repo_name, pull_request_id=pull_request_id),
210 params={'update_commits': 'true', 'csrf_token': csrf_token})
211
212 # update PR #2
213 source_repo = Repository.get_by_repo_name(source_repo_name)
214 backend.pull_heads(source_repo, heads=['commit-3'])
215 response = self.app.post(
216 route_path('pullrequest_update',
217 repo_name=target_repo_name, pull_request_id=pull_request_id),
218 params={'update_commits': 'true', 'csrf_token': csrf_token})
219
220 # update PR #3
221 source_repo = Repository.get_by_repo_name(source_repo_name)
222 backend.pull_heads(source_repo, heads=['commit-4'])
223 response = self.app.post(
224 route_path('pullrequest_update',
225 repo_name=target_repo_name, pull_request_id=pull_request_id),
226 params={'update_commits': 'true', 'csrf_token': csrf_token})
227
228 # Show final version !
229 response = self.app.get(
230 route_path('pullrequest_show',
231 repo_name=target_repo_name,
232 pull_request_id=pull_request_id))
233
234 # 3 updates, and the latest == 4
235 response.mustcontain('4 versions available for this pull request')
236 response.mustcontain(no=['rhodecode diff rendering error'])
237
238 # initial show must have 3 commits, and 3 adds
239 response.mustcontain('commit-1')
240 response.mustcontain('commit-2')
241 response.mustcontain('commit-3')
242 response.mustcontain('commit-4')
243
244 response.mustcontain('cb-addition"></span><span>LINE2</span>')
245 response.mustcontain('cb-addition"></span><span>LINE3</span>')
246 response.mustcontain('cb-addition"></span><span>LINE4</span>')
247 response.mustcontain('cb-addition"></span><span>LINE5</span>')
248
249 # fetch versions
250 pr = PullRequest.get(pull_request_id)
251 versions = [x.pull_request_version_id for x in pr.versions.all()]
252 assert len(versions) == 3
253
254 # show v1,v2,v3,v4
255 def cb_line(text):
256 return 'cb-addition"></span><span>{}</span>'.format(text)
257
258 def cb_context(text):
259 return '<span class="cb-code"><span class="cb-action cb-context">' \
260 '</span><span>{}</span></span>'.format(text)
261
262 commit_tests = {
263 # in response, not in response
264 1: (['commit-1'], ['commit-2', 'commit-3', 'commit-4']),
265 2: (['commit-1', 'commit-2'], ['commit-3', 'commit-4']),
266 3: (['commit-1', 'commit-2', 'commit-3'], ['commit-4']),
267 4: (['commit-1', 'commit-2', 'commit-3', 'commit-4'], []),
268 }
269 diff_tests = {
270 1: (['LINE2'], ['LINE3', 'LINE4', 'LINE5']),
271 2: (['LINE2', 'LINE3'], ['LINE4', 'LINE5']),
272 3: (['LINE2', 'LINE3', 'LINE4'], ['LINE5']),
273 4: (['LINE2', 'LINE3', 'LINE4', 'LINE5'], []),
274 }
275 for idx, ver in enumerate(versions, 1):
276
277 response = self.app.get(
278 route_path('pullrequest_show',
279 repo_name=target_repo_name,
280 pull_request_id=pull_request_id,
281 params={'version': ver}))
282
283 response.mustcontain(no=['rhodecode diff rendering error'])
284 response.mustcontain('Showing changes at v{}'.format(idx))
285
286 yes, no = commit_tests[idx]
287 for y in yes:
288 response.mustcontain(y)
289 for n in no:
290 response.mustcontain(no=n)
291
292 yes, no = diff_tests[idx]
293 for y in yes:
294 response.mustcontain(cb_line(y))
295 for n in no:
296 response.mustcontain(no=n)
297
298 # show diff between versions
299 diff_compare_tests = {
300 1: (['LINE3'], ['LINE1', 'LINE2']),
301 2: (['LINE3', 'LINE4'], ['LINE1', 'LINE2']),
302 3: (['LINE3', 'LINE4', 'LINE5'], ['LINE1', 'LINE2']),
303 }
304 for idx, ver in enumerate(versions, 1):
305 adds, context = diff_compare_tests[idx]
306
307 to_ver = ver+1
308 if idx == 3:
309 to_ver = 'latest'
310
311 response = self.app.get(
312 route_path('pullrequest_show',
313 repo_name=target_repo_name,
314 pull_request_id=pull_request_id,
315 params={'from_version': versions[0], 'version': to_ver}))
316
317 response.mustcontain(no=['rhodecode diff rendering error'])
318
319 for a in adds:
320 response.mustcontain(cb_line(a))
321 for c in context:
322 response.mustcontain(cb_context(c))
323
324 # test version v2 -> v3
325 response = self.app.get(
326 route_path('pullrequest_show',
327 repo_name=target_repo_name,
328 pull_request_id=pull_request_id,
329 params={'from_version': versions[1], 'version': versions[2]}))
330
331 response.mustcontain(cb_context('LINE1'))
332 response.mustcontain(cb_context('LINE2'))
333 response.mustcontain(cb_context('LINE3'))
334 response.mustcontain(cb_line('LINE4'))
335
117 336 def test_close_status_visibility(self, pr_util, user_util, csrf_token):
118 337 # Logout
119 338 response = self.app.post(
@@ -338,8 +557,8 b' class TestPullrequestsView(object):'
338 557
339 558 response = self.app.post(
340 559 route_path('pullrequest_comment_create',
341 repo_name=pull_request.target_repo.scm_instance().name,
342 pull_request_id=pull_request.pull_request_id),
560 repo_name=pull_request.target_repo.scm_instance().name,
561 pull_request_id=pull_request.pull_request_id),
343 562 params={
344 563 'close_pull_request': 'true',
345 564 'csrf_token': csrf_token},
@@ -355,6 +574,222 b' class TestPullrequestsView(object):'
355 574 pull_request.source_repo, pull_request=pull_request)
356 575 assert status == ChangesetStatus.STATUS_REJECTED
357 576
577 def test_comment_and_close_pull_request_try_edit_comment(
578 self, pr_util, csrf_token, xhr_header
579 ):
580 pull_request = pr_util.create_pull_request()
581 pull_request_id = pull_request.pull_request_id
582 target_scm = pull_request.target_repo.scm_instance()
583 target_scm_name = target_scm.name
584
585 response = self.app.post(
586 route_path(
587 'pullrequest_comment_create',
588 repo_name=target_scm_name,
589 pull_request_id=pull_request_id,
590 ),
591 params={
592 'close_pull_request': 'true',
593 'csrf_token': csrf_token,
594 },
595 extra_environ=xhr_header)
596
597 assert response.json
598
599 pull_request = PullRequest.get(pull_request_id)
600 target_scm = pull_request.target_repo.scm_instance()
601 target_scm_name = target_scm.name
602 assert pull_request.is_closed()
603
604 # check only the latest status, not the review status
605 status = ChangesetStatusModel().get_status(
606 pull_request.source_repo, pull_request=pull_request)
607 assert status == ChangesetStatus.STATUS_REJECTED
608
609 comment_id = response.json.get('comment_id', None)
610 test_text = 'test'
611 response = self.app.post(
612 route_path(
613 'pullrequest_comment_edit',
614 repo_name=target_scm_name,
615 pull_request_id=pull_request_id,
616 comment_id=comment_id,
617 ),
618 extra_environ=xhr_header,
619 params={
620 'csrf_token': csrf_token,
621 'text': test_text,
622 },
623 status=403,
624 )
625 assert response.status_int == 403
626
627 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
628 pull_request = pr_util.create_pull_request()
629 target_scm = pull_request.target_repo.scm_instance()
630 target_scm_name = target_scm.name
631
632 response = self.app.post(
633 route_path(
634 'pullrequest_comment_create',
635 repo_name=target_scm_name,
636 pull_request_id=pull_request.pull_request_id),
637 params={
638 'csrf_token': csrf_token,
639 'text': 'init',
640 },
641 extra_environ=xhr_header,
642 )
643 assert response.json
644
645 comment_id = response.json.get('comment_id', None)
646 assert comment_id
647 test_text = 'test'
648 self.app.post(
649 route_path(
650 'pullrequest_comment_edit',
651 repo_name=target_scm_name,
652 pull_request_id=pull_request.pull_request_id,
653 comment_id=comment_id,
654 ),
655 extra_environ=xhr_header,
656 params={
657 'csrf_token': csrf_token,
658 'text': test_text,
659 'version': '0',
660 },
661
662 )
663 text_form_db = ChangesetComment.query().filter(
664 ChangesetComment.comment_id == comment_id).first().text
665 assert test_text == text_form_db
666
667 def test_comment_and_comment_edit(self, pr_util, csrf_token, xhr_header):
668 pull_request = pr_util.create_pull_request()
669 target_scm = pull_request.target_repo.scm_instance()
670 target_scm_name = target_scm.name
671
672 response = self.app.post(
673 route_path(
674 'pullrequest_comment_create',
675 repo_name=target_scm_name,
676 pull_request_id=pull_request.pull_request_id),
677 params={
678 'csrf_token': csrf_token,
679 'text': 'init',
680 },
681 extra_environ=xhr_header,
682 )
683 assert response.json
684
685 comment_id = response.json.get('comment_id', None)
686 assert comment_id
687 test_text = 'init'
688 response = self.app.post(
689 route_path(
690 'pullrequest_comment_edit',
691 repo_name=target_scm_name,
692 pull_request_id=pull_request.pull_request_id,
693 comment_id=comment_id,
694 ),
695 extra_environ=xhr_header,
696 params={
697 'csrf_token': csrf_token,
698 'text': test_text,
699 'version': '0',
700 },
701 status=404,
702
703 )
704 assert response.status_int == 404
705
706 def test_comment_and_try_edit_already_edited(self, pr_util, csrf_token, xhr_header):
707 pull_request = pr_util.create_pull_request()
708 target_scm = pull_request.target_repo.scm_instance()
709 target_scm_name = target_scm.name
710
711 response = self.app.post(
712 route_path(
713 'pullrequest_comment_create',
714 repo_name=target_scm_name,
715 pull_request_id=pull_request.pull_request_id),
716 params={
717 'csrf_token': csrf_token,
718 'text': 'init',
719 },
720 extra_environ=xhr_header,
721 )
722 assert response.json
723 comment_id = response.json.get('comment_id', None)
724 assert comment_id
725
726 test_text = 'test'
727 self.app.post(
728 route_path(
729 'pullrequest_comment_edit',
730 repo_name=target_scm_name,
731 pull_request_id=pull_request.pull_request_id,
732 comment_id=comment_id,
733 ),
734 extra_environ=xhr_header,
735 params={
736 'csrf_token': csrf_token,
737 'text': test_text,
738 'version': '0',
739 },
740
741 )
742 test_text_v2 = 'test_v2'
743 response = self.app.post(
744 route_path(
745 'pullrequest_comment_edit',
746 repo_name=target_scm_name,
747 pull_request_id=pull_request.pull_request_id,
748 comment_id=comment_id,
749 ),
750 extra_environ=xhr_header,
751 params={
752 'csrf_token': csrf_token,
753 'text': test_text_v2,
754 'version': '0',
755 },
756 status=409,
757 )
758 assert response.status_int == 409
759
760 text_form_db = ChangesetComment.query().filter(
761 ChangesetComment.comment_id == comment_id).first().text
762
763 assert test_text == text_form_db
764 assert test_text_v2 != text_form_db
765
766 def test_comment_and_comment_edit_permissions_forbidden(
767 self, autologin_regular_user, user_regular, user_admin, pr_util,
768 csrf_token, xhr_header):
769 pull_request = pr_util.create_pull_request(
770 author=user_admin.username, enable_notifications=False)
771 comment = CommentsModel().create(
772 text='test',
773 repo=pull_request.target_repo.scm_instance().name,
774 user=user_admin,
775 pull_request=pull_request,
776 )
777 response = self.app.post(
778 route_path(
779 'pullrequest_comment_edit',
780 repo_name=pull_request.target_repo.scm_instance().name,
781 pull_request_id=pull_request.pull_request_id,
782 comment_id=comment.comment_id,
783 ),
784 extra_environ=xhr_header,
785 params={
786 'csrf_token': csrf_token,
787 'text': 'test_text',
788 },
789 status=403,
790 )
791 assert response.status_int == 403
792
358 793 def test_create_pull_request(self, backend, csrf_token):
359 794 commits = [
360 795 {'message': 'ancestor'},
@@ -20,9 +20,9 b''
20 20
21 21
22 22 import logging
23 import collections
24 23
25 from pyramid.httpexceptions import HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden
24 from pyramid.httpexceptions import (
25 HTTPNotFound, HTTPBadRequest, HTTPFound, HTTPForbidden, HTTPConflict)
26 26 from pyramid.view import view_config
27 27 from pyramid.renderers import render
28 28 from pyramid.response import Response
@@ -39,13 +39,14 b' from rhodecode.lib.compat import Ordered'
39 39 from rhodecode.lib.diffs import (
40 40 cache_diff, load_cached_diff, diff_cache_exist, get_diff_context,
41 41 get_diff_whitespace_flag)
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
42 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError, CommentVersionMismatch
43 43 import rhodecode.lib.helpers as h
44 44 from rhodecode.lib.utils2 import safe_unicode, str2bool
45 45 from rhodecode.lib.vcs.backends.base import EmptyCommit
46 46 from rhodecode.lib.vcs.exceptions import (
47 47 RepositoryError, CommitDoesNotExistError)
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore
48 from rhodecode.model.db import ChangesetComment, ChangesetStatus, FileStore, \
49 ChangesetCommentHistory
49 50 from rhodecode.model.changeset_status import ChangesetStatusModel
50 51 from rhodecode.model.comment import CommentsModel
51 52 from rhodecode.model.meta import Session
@@ -431,6 +432,34 b' class RepoCommitsView(RepoAppView):'
431 432 'repository.read', 'repository.write', 'repository.admin')
432 433 @CSRFRequired()
433 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 c = self.load_default_context()
439
440 comment_history_id = self.request.matchdict['comment_history_id']
441 comment_history = ChangesetCommentHistory.get_or_404(comment_history_id)
442 is_repo_comment = comment_history.comment.repo.repo_id == self.db_repo.repo_id
443
444 if is_repo_comment:
445 c.comment_history = comment_history
446
447 rendered_comment = render(
448 'rhodecode:templates/changeset/comment_history.mako',
449 self._get_template_context(c)
450 , self.request)
451 return rendered_comment
452 else:
453 log.warning('No permissions for user %s to show comment_history_id: %s',
454 self._rhodecode_db_user, comment_history_id)
455 raise HTTPNotFound()
456
457 @LoginRequired()
458 @NotAnonymous()
459 @HasRepoPermissionAnyDecorator(
460 'repository.read', 'repository.write', 'repository.admin')
461 @CSRFRequired()
462 @view_config(
434 463 route_name='repo_commit_comment_attachment_upload', request_method='POST',
435 464 renderer='json_ext', xhr=True)
436 465 def repo_commit_comment_attachment_upload(self):
@@ -545,7 +574,7 b' class RepoCommitsView(RepoAppView):'
545 574 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
546 575 super_admin = h.HasPermissionAny('hg.admin')()
547 576 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
548 is_repo_comment = comment.repo.repo_name == self.db_repo_name
577 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
549 578 comment_repo_admin = is_repo_admin and is_repo_comment
550 579
551 580 if super_admin or comment_owner or comment_repo_admin:
@@ -558,6 +587,90 b' class RepoCommitsView(RepoAppView):'
558 587 raise HTTPNotFound()
559 588
560 589 @LoginRequired()
590 @NotAnonymous()
591 @HasRepoPermissionAnyDecorator(
592 'repository.read', 'repository.write', 'repository.admin')
593 @CSRFRequired()
594 @view_config(
595 route_name='repo_commit_comment_edit', request_method='POST',
596 renderer='json_ext')
597 def repo_commit_comment_edit(self):
598 self.load_default_context()
599
600 comment_id = self.request.matchdict['comment_id']
601 comment = ChangesetComment.get_or_404(comment_id)
602
603 if comment.immutable:
604 # don't allow deleting comments that are immutable
605 raise HTTPForbidden()
606
607 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
608 super_admin = h.HasPermissionAny('hg.admin')()
609 comment_owner = (comment.author.user_id == self._rhodecode_db_user.user_id)
610 is_repo_comment = comment.repo.repo_id == self.db_repo.repo_id
611 comment_repo_admin = is_repo_admin and is_repo_comment
612
613 if super_admin or comment_owner or comment_repo_admin:
614 text = self.request.POST.get('text')
615 version = self.request.POST.get('version')
616 if text == comment.text:
617 log.warning(
618 'Comment(repo): '
619 'Trying to create new version '
620 'with the same comment body {}'.format(
621 comment_id,
622 )
623 )
624 raise HTTPNotFound()
625
626 if version.isdigit():
627 version = int(version)
628 else:
629 log.warning(
630 'Comment(repo): Wrong version type {} {} '
631 'for comment {}'.format(
632 version,
633 type(version),
634 comment_id,
635 )
636 )
637 raise HTTPNotFound()
638
639 try:
640 comment_history = CommentsModel().edit(
641 comment_id=comment_id,
642 text=text,
643 auth_user=self._rhodecode_user,
644 version=version,
645 )
646 except CommentVersionMismatch:
647 raise HTTPConflict()
648
649 if not comment_history:
650 raise HTTPNotFound()
651
652 commit_id = self.request.matchdict['commit_id']
653 commit = self.db_repo.get_commit(commit_id)
654 CommentsModel().trigger_commit_comment_hook(
655 self.db_repo, self._rhodecode_user, 'edit',
656 data={'comment': comment, 'commit': commit})
657
658 Session().commit()
659 return {
660 'comment_history_id': comment_history.comment_history_id,
661 'comment_id': comment.comment_id,
662 'comment_version': comment_history.version,
663 'comment_author_username': comment_history.author.username,
664 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
665 'comment_created_on': h.age_component(comment_history.created_on,
666 time_is_local=True),
667 }
668 else:
669 log.warning('No permissions for user %s to edit comment_id: %s',
670 self._rhodecode_db_user, comment_id)
671 raise HTTPNotFound()
672
673 @LoginRequired()
561 674 @HasRepoPermissionAnyDecorator(
562 675 'repository.read', 'repository.write', 'repository.admin')
563 676 @view_config(
@@ -125,7 +125,7 b' class RepoFilesView(RepoAppView):'
125 125 self.db_repo_name, branch_name)
126 126 if branch_perm and branch_perm not in ['branch.push', 'branch.push_force']:
127 127 message = _('Branch `{}` changes forbidden by rule {}.').format(
128 h.escape(branch_name), rule)
128 h.escape(branch_name), h.escape(rule))
129 129 h.flash(message, 'warning')
130 130
131 131 if json_mode:
@@ -25,7 +25,7 b' import formencode'
25 25 import formencode.htmlfill
26 26 import peppercorn
27 27 from pyramid.httpexceptions import (
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest)
28 HTTPFound, HTTPNotFound, HTTPForbidden, HTTPBadRequest, HTTPConflict)
29 29 from pyramid.view import view_config
30 30 from pyramid.renderers import render
31 31
@@ -34,6 +34,7 b' from rhodecode.apps._base import RepoApp'
34 34 from rhodecode.lib import helpers as h, diffs, codeblocks, channelstream
35 35 from rhodecode.lib.base import vcs_operation_context
36 36 from rhodecode.lib.diffs import load_cached_diff, cache_diff, diff_cache_exist
37 from rhodecode.lib.exceptions import CommentVersionMismatch
37 38 from rhodecode.lib.ext_json import json
38 39 from rhodecode.lib.auth import (
39 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
@@ -213,9 +214,12 b' class RepoPullRequestsView(RepoAppView, '
213 214 ancestor_commit,
214 215 source_ref_id, target_ref_id,
215 216 target_commit, source_commit, diff_limit, file_limit,
216 fulldiff, hide_whitespace_changes, diff_context):
217 fulldiff, hide_whitespace_changes, diff_context, use_ancestor=True):
217 218
218 target_ref_id = ancestor_commit.raw_id
219 if use_ancestor:
220 # we might want to not use it for versions
221 target_ref_id = ancestor_commit.raw_id
222
219 223 vcs_diff = PullRequestModel().get_diff(
220 224 source_repo, source_ref_id, target_ref_id,
221 225 hide_whitespace_changes, diff_context)
@@ -568,7 +572,6 b' class RepoPullRequestsView(RepoAppView, '
568 572 c.commit_ranges.append(comm)
569 573
570 574 c.missing_requirements = missing_requirements
571
572 575 c.ancestor_commit = ancestor_commit
573 576 c.statuses = source_repo.statuses(
574 577 [x.raw_id for x in c.commit_ranges])
@@ -593,6 +596,10 b' class RepoPullRequestsView(RepoAppView, '
593 596 else:
594 597 c.inline_comments = display_inline_comments
595 598
599 use_ancestor = True
600 if from_version_normalized != version_normalized:
601 use_ancestor = False
602
596 603 has_proper_diff_cache = cached_diff and cached_diff.get('commits')
597 604 if not force_recache and has_proper_diff_cache:
598 605 c.diffset = cached_diff['diff']
@@ -604,7 +611,10 b' class RepoPullRequestsView(RepoAppView, '
604 611 source_ref_id, target_ref_id,
605 612 target_commit, source_commit,
606 613 diff_limit, file_limit, c.fulldiff,
607 hide_whitespace_changes, diff_context)
614 hide_whitespace_changes, diff_context,
615 use_ancestor=use_ancestor
616 )
617
608 618 # save cached diff
609 619 if caching_enabled:
610 620 cache_diff(cache_file_path, c.diffset, diff_commit_cache)
@@ -1524,3 +1534,104 b' class RepoPullRequestsView(RepoAppView, '
1524 1534 log.warning('No permissions for user %s to delete comment_id: %s',
1525 1535 self._rhodecode_db_user, comment_id)
1526 1536 raise HTTPNotFound()
1537
1538 @LoginRequired()
1539 @NotAnonymous()
1540 @HasRepoPermissionAnyDecorator(
1541 'repository.read', 'repository.write', 'repository.admin')
1542 @CSRFRequired()
1543 @view_config(
1544 route_name='pullrequest_comment_edit', request_method='POST',
1545 renderer='json_ext')
1546 def pull_request_comment_edit(self):
1547 self.load_default_context()
1548
1549 pull_request = PullRequest.get_or_404(
1550 self.request.matchdict['pull_request_id']
1551 )
1552 comment = ChangesetComment.get_or_404(
1553 self.request.matchdict['comment_id']
1554 )
1555 comment_id = comment.comment_id
1556
1557 if comment.immutable:
1558 # don't allow deleting comments that are immutable
1559 raise HTTPForbidden()
1560
1561 if pull_request.is_closed():
1562 log.debug('comment: forbidden because pull request is closed')
1563 raise HTTPForbidden()
1564
1565 if not comment:
1566 log.debug('Comment with id:%s not found, skipping', comment_id)
1567 # comment already deleted in another call probably
1568 return True
1569
1570 if comment.pull_request.is_closed():
1571 # don't allow deleting comments on closed pull request
1572 raise HTTPForbidden()
1573
1574 is_repo_admin = h.HasRepoPermissionAny('repository.admin')(self.db_repo_name)
1575 super_admin = h.HasPermissionAny('hg.admin')()
1576 comment_owner = comment.author.user_id == self._rhodecode_user.user_id
1577 is_repo_comment = comment.repo.repo_name == self.db_repo_name
1578 comment_repo_admin = is_repo_admin and is_repo_comment
1579
1580 if super_admin or comment_owner or comment_repo_admin:
1581 text = self.request.POST.get('text')
1582 version = self.request.POST.get('version')
1583 if text == comment.text:
1584 log.warning(
1585 'Comment(PR): '
1586 'Trying to create new version '
1587 'with the same comment body {}'.format(
1588 comment_id,
1589 )
1590 )
1591 raise HTTPNotFound()
1592
1593 if version.isdigit():
1594 version = int(version)
1595 else:
1596 log.warning(
1597 'Comment(PR): Wrong version type {} {} '
1598 'for comment {}'.format(
1599 version,
1600 type(version),
1601 comment_id,
1602 )
1603 )
1604 raise HTTPNotFound()
1605
1606 try:
1607 comment_history = CommentsModel().edit(
1608 comment_id=comment_id,
1609 text=text,
1610 auth_user=self._rhodecode_user,
1611 version=version,
1612 )
1613 except CommentVersionMismatch:
1614 raise HTTPConflict()
1615
1616 if not comment_history:
1617 raise HTTPNotFound()
1618
1619 Session().commit()
1620
1621 PullRequestModel().trigger_pull_request_hook(
1622 pull_request, self._rhodecode_user, 'comment_edit',
1623 data={'comment': comment})
1624
1625 return {
1626 'comment_history_id': comment_history.comment_history_id,
1627 'comment_id': comment.comment_id,
1628 'comment_version': comment_history.version,
1629 'comment_author_username': comment_history.author.username,
1630 'comment_author_gravatar': h.gravatar_url(comment_history.author.email, 16),
1631 'comment_created_on': h.age_component(comment_history.created_on,
1632 time_is_local=True),
1633 }
1634 else:
1635 log.warning('No permissions for user %s to edit comment_id: %s',
1636 self._rhodecode_db_user, comment_id)
1637 raise HTTPNotFound()
@@ -743,7 +743,7 b' def authenticate(username, password, env'
743 743 log.debug('AUTH_CACHE_TTL for plugin `%s` active: %s (TTL: %s)',
744 744 plugin.get_id(), plugin_cache_active, cache_ttl)
745 745
746 user_id = user.user_id if user else None
746 user_id = user.user_id if user else 'no-user'
747 747 # don't cache for empty users
748 748 plugin_cache_active = plugin_cache_active and user_id
749 749 cache_namespace_uid = 'cache_user_auth.{}'.format(user_id)
@@ -26,6 +26,7 b' from .hooks import ('
26 26 _pre_create_user_hook,
27 27 _create_user_hook,
28 28 _comment_commit_repo_hook,
29 _comment_edit_commit_repo_hook,
29 30 _delete_repo_hook,
30 31 _delete_user_hook,
31 32 _pre_push_hook,
@@ -35,6 +36,7 b' from .hooks import ('
35 36 _create_pull_request_hook,
36 37 _review_pull_request_hook,
37 38 _comment_pull_request_hook,
39 _comment_edit_pull_request_hook,
38 40 _update_pull_request_hook,
39 41 _merge_pull_request_hook,
40 42 _close_pull_request_hook,
@@ -43,6 +45,7 b' from .hooks import ('
43 45 # set as module attributes, we use those to call hooks. *do not change this*
44 46 CREATE_REPO_HOOK = _create_repo_hook
45 47 COMMENT_COMMIT_REPO_HOOK = _comment_commit_repo_hook
48 COMMENT_EDIT_COMMIT_REPO_HOOK = _comment_edit_commit_repo_hook
46 49 CREATE_REPO_GROUP_HOOK = _create_repo_group_hook
47 50 PRE_CREATE_USER_HOOK = _pre_create_user_hook
48 51 CREATE_USER_HOOK = _create_user_hook
@@ -55,6 +58,7 b' PULL_HOOK = _pull_hook'
55 58 CREATE_PULL_REQUEST = _create_pull_request_hook
56 59 REVIEW_PULL_REQUEST = _review_pull_request_hook
57 60 COMMENT_PULL_REQUEST = _comment_pull_request_hook
61 COMMENT_EDIT_PULL_REQUEST = _comment_edit_pull_request_hook
58 62 UPDATE_PULL_REQUEST = _update_pull_request_hook
59 63 MERGE_PULL_REQUEST = _merge_pull_request_hook
60 64 CLOSE_PULL_REQUEST = _close_pull_request_hook
@@ -1,5 +1,6 b''
1 # This code allows override the integrations templates.
2 # Put this into the __init__.py file of rcextensions to override the templates
1 # Below code examples allows override the integrations templates, or email titles.
2 # Append selected parts at the end of the __init__.py file of rcextensions directory
3 # to override the templates
3 4
4 5
5 6 # EMAIL Integration
@@ -185,3 +186,18 b' message:'
185 186 ```
186 187
187 188 ''')
189
190
191 # Example to modify emails default title
192 from rhodecode.model import notification
193
194 notification.EMAIL_PR_UPDATE_SUBJECT_TEMPLATE = '{updating_user} updated pull request. !{pr_id}: "{pr_title}"'
195 notification.EMAIL_PR_REVIEW_SUBJECT_TEMPLATE = '{user} requested a pull request review. !{pr_id}: "{pr_title}"'
196
197 notification.EMAIL_PR_COMMENT_SUBJECT_TEMPLATE = '{mention_prefix}{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"'
198 notification.EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = '{mention_prefix}[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"'
199 notification.EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE = '{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"'
200
201 notification.EMAIL_COMMENT_SUBJECT_TEMPLATE = '{mention_prefix}{user} left a {comment_type} on commit `{commit_id}`'
202 notification.EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = '{mention_prefix}[status: {status}] {user} left a {comment_type} on commit `{commit_id}`'
203 notification.EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE = '{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}`'
@@ -83,6 +83,33 b' def _comment_commit_repo_hook(*args, **k'
83 83
84 84
85 85 @has_kwargs({
86 'repo_name': '',
87 'repo_type': '',
88 'description': '',
89 'private': '',
90 'created_on': '',
91 'enable_downloads': '',
92 'repo_id': '',
93 'user_id': '',
94 'enable_statistics': '',
95 'clone_uri': '',
96 'fork_id': '',
97 'group_id': '',
98 'created_by': '',
99 'repository': '',
100 'comment': '',
101 'commit': ''
102 })
103 def _comment_edit_commit_repo_hook(*args, **kwargs):
104 """
105 POST CREATE REPOSITORY COMMENT ON COMMIT HOOK. This function will be executed after
106 a comment is made on this repository commit.
107
108 """
109 return HookResponse(0, '')
110
111
112 @has_kwargs({
86 113 'group_name': '',
87 114 'group_parent_id': '',
88 115 'group_description': '',
@@ -408,6 +435,38 b' def _comment_pull_request_hook(*args, **'
408 435 'scm': 'type of version control "git", "hg", "svn"',
409 436 'username': 'username of actor who triggered this event',
410 437 'ip': 'ip address of actor who triggered this hook',
438
439 'action': '',
440 'repository': 'repository name',
441 'pull_request_id': '',
442 'url': '',
443 'title': '',
444 'description': '',
445 'status': '',
446 'comment': '',
447 'created_on': '',
448 'updated_on': '',
449 'commit_ids': '',
450 'review_status': '',
451 'mergeable': '',
452 'source': '',
453 'target': '',
454 'author': '',
455 'reviewers': '',
456 })
457 def _comment_edit_pull_request_hook(*args, **kwargs):
458 """
459 This hook will be executed after comment is made on a pull request
460 """
461 return HookResponse(0, '')
462
463
464 @has_kwargs({
465 'server_url': 'url of instance that triggered this hook',
466 'config': 'path to .ini config used',
467 'scm': 'type of version control "git", "hg", "svn"',
468 'username': 'username of actor who triggered this event',
469 'ip': 'ip address of actor who triggered this hook',
411 470 'action': '',
412 471 'repository': 'repository name',
413 472 'pull_request_id': '',
@@ -74,7 +74,7 b' link_config = ['
74 74 },
75 75 {
76 76 "name": "rst_help",
77 "target": "http://docutils.sourceforge.net/docs/user/rst/quickref.html",
77 "target": "http://docutils.sourceforge.io/docs/user/rst/quickref.html",
78 78 "external_target": "https://docutils.sourceforge.io/docs/user/rst/quickref.html",
79 79 },
80 80 {
@@ -53,7 +53,8 b' from rhodecode.events.user import ( # p'
53 53 )
54 54
55 55 from rhodecode.events.repo import ( # pragma: no cover
56 RepoEvent, RepoCommitCommentEvent,
56 RepoEvent,
57 RepoCommitCommentEvent, RepoCommitCommentEditEvent,
57 58 RepoPreCreateEvent, RepoCreateEvent,
58 59 RepoPreDeleteEvent, RepoDeleteEvent,
59 60 RepoPrePushEvent, RepoPushEvent,
@@ -72,8 +73,8 b' from rhodecode.events.pullrequest import'
72 73 PullRequestCreateEvent,
73 74 PullRequestUpdateEvent,
74 75 PullRequestCommentEvent,
76 PullRequestCommentEditEvent,
75 77 PullRequestReviewEvent,
76 78 PullRequestMergeEvent,
77 79 PullRequestCloseEvent,
78 PullRequestCommentEvent,
79 80 )
@@ -19,8 +19,7 b''
19 19 import logging
20 20
21 21 from rhodecode.translation import lazy_ugettext
22 from rhodecode.events.repo import (
23 RepoEvent, _commits_as_dict, _issues_as_dict)
22 from rhodecode.events.repo import (RepoEvent, _commits_as_dict, _issues_as_dict)
24 23
25 24 log = logging.getLogger(__name__)
26 25
@@ -155,6 +154,7 b' class PullRequestCommentEvent(PullReques'
155 154 'type': self.comment.comment_type,
156 155 'file': self.comment.f_path,
157 156 'line': self.comment.line_no,
157 'version': self.comment.last_version,
158 158 'url': CommentsModel().get_url(
159 159 self.comment, request=self.request),
160 160 'permalink_url': CommentsModel().get_url(
@@ -162,3 +162,42 b' class PullRequestCommentEvent(PullReques'
162 162 }
163 163 })
164 164 return data
165
166
167 class PullRequestCommentEditEvent(PullRequestEvent):
168 """
169 An instance of this class is emitted as an :term:`event` after a pull
170 request comment is edited.
171 """
172 name = 'pullrequest-comment-edit'
173 display_name = lazy_ugettext('pullrequest comment edited')
174 description = lazy_ugettext('Event triggered after a comment was edited on a code '
175 'in the pull request')
176
177 def __init__(self, pullrequest, comment):
178 super(PullRequestCommentEditEvent, self).__init__(pullrequest)
179 self.comment = comment
180
181 def as_dict(self):
182 from rhodecode.model.comment import CommentsModel
183 data = super(PullRequestCommentEditEvent, self).as_dict()
184
185 status = None
186 if self.comment.status_change:
187 status = self.comment.status_change[0].status
188
189 data.update({
190 'comment': {
191 'status': status,
192 'text': self.comment.text,
193 'type': self.comment.comment_type,
194 'file': self.comment.f_path,
195 'line': self.comment.line_no,
196 'version': self.comment.last_version,
197 'url': CommentsModel().get_url(
198 self.comment, request=self.request),
199 'permalink_url': CommentsModel().get_url(
200 self.comment, request=self.request, permalink=True),
201 }
202 })
203 return data
@@ -211,6 +211,42 b' class RepoCommitCommentEvent(RepoEvent):'
211 211 'comment_type': self.comment.comment_type,
212 212 'comment_f_path': self.comment.f_path,
213 213 'comment_line_no': self.comment.line_no,
214 'comment_version': self.comment.last_version,
215 }
216 return data
217
218
219 class RepoCommitCommentEditEvent(RepoEvent):
220 """
221 An instance of this class is emitted as an :term:`event` after a comment is edited
222 on repository commit.
223 """
224
225 name = 'repo-commit-edit-comment'
226 display_name = lazy_ugettext('repository commit edit comment')
227 description = lazy_ugettext('Event triggered after a comment was edited '
228 'on commit inside a repository')
229
230 def __init__(self, repo, commit, comment):
231 super(RepoCommitCommentEditEvent, self).__init__(repo)
232 self.commit = commit
233 self.comment = comment
234
235 def as_dict(self):
236 data = super(RepoCommitCommentEditEvent, self).as_dict()
237 data['commit'] = {
238 'commit_id': self.commit.raw_id,
239 'commit_message': self.commit.message,
240 'commit_branch': self.commit.branch,
241 }
242
243 data['comment'] = {
244 'comment_id': self.comment.comment_id,
245 'comment_text': self.comment.text,
246 'comment_type': self.comment.comment_type,
247 'comment_f_path': self.comment.f_path,
248 'comment_line_no': self.comment.line_no,
249 'comment_version': self.comment.last_version,
214 250 }
215 251 return data
216 252
@@ -331,6 +331,26 b' class WebhookDataHandler(CommitParsingDa'
331 331
332 332 return [(url, self.headers, data)]
333 333
334 def repo_commit_comment_edit_handler(self, event, data):
335 url = self.get_base_parsed_template(data)
336 log.debug('register %s call(%s) to url %s', self.name, event, url)
337 comment_vars = [
338 ('commit_comment_id', data['comment']['comment_id']),
339 ('commit_comment_text', data['comment']['comment_text']),
340 ('commit_comment_type', data['comment']['comment_type']),
341
342 ('commit_comment_f_path', data['comment']['comment_f_path']),
343 ('commit_comment_line_no', data['comment']['comment_line_no']),
344
345 ('commit_comment_commit_id', data['commit']['commit_id']),
346 ('commit_comment_commit_branch', data['commit']['commit_branch']),
347 ('commit_comment_commit_message', data['commit']['commit_message']),
348 ]
349 for k, v in comment_vars:
350 url = UrlTmpl(url).safe_substitute(**{k: v})
351
352 return [(url, self.headers, data)]
353
334 354 def repo_create_event_handler(self, event, data):
335 355 url = self.get_base_parsed_template(data)
336 356 log.debug('register %s call(%s) to url %s', self.name, event, url)
@@ -360,6 +380,8 b' class WebhookDataHandler(CommitParsingDa'
360 380 return self.repo_create_event_handler(event, data)
361 381 elif isinstance(event, events.RepoCommitCommentEvent):
362 382 return self.repo_commit_comment_handler(event, data)
383 elif isinstance(event, events.RepoCommitCommentEditEvent):
384 return self.repo_commit_comment_edit_handler(event, data)
363 385 elif isinstance(event, events.PullRequestEvent):
364 386 return self.pull_request_event_handler(event, data)
365 387 else:
@@ -133,6 +133,8 b' class HipchatIntegrationType(Integration'
133 133
134 134 if isinstance(event, events.PullRequestCommentEvent):
135 135 text = self.format_pull_request_comment_event(event, data)
136 elif isinstance(event, events.PullRequestCommentEditEvent):
137 text = self.format_pull_request_comment_event(event, data)
136 138 elif isinstance(event, events.PullRequestReviewEvent):
137 139 text = self.format_pull_request_review_event(event, data)
138 140 elif isinstance(event, events.PullRequestEvent):
@@ -157,6 +157,9 b' class SlackIntegrationType(IntegrationTy'
157 157 if isinstance(event, events.PullRequestCommentEvent):
158 158 (title, text, fields, overrides) \
159 159 = self.format_pull_request_comment_event(event, data)
160 elif isinstance(event, events.PullRequestCommentEditEvent):
161 (title, text, fields, overrides) \
162 = self.format_pull_request_comment_event(event, data)
160 163 elif isinstance(event, events.PullRequestReviewEvent):
161 164 title, text = self.format_pull_request_review_event(event, data)
162 165 elif isinstance(event, events.PullRequestEvent):
@@ -144,11 +144,13 b' class WebhookIntegrationType(Integration'
144 144 events.PullRequestMergeEvent,
145 145 events.PullRequestUpdateEvent,
146 146 events.PullRequestCommentEvent,
147 events.PullRequestCommentEditEvent,
147 148 events.PullRequestReviewEvent,
148 149 events.PullRequestCreateEvent,
149 150 events.RepoPushEvent,
150 151 events.RepoCreateEvent,
151 152 events.RepoCommitCommentEvent,
153 events.RepoCommitCommentEditEvent,
152 154 ]
153 155
154 156 def settings_schema(self):
@@ -82,6 +82,7 b' ACTIONS_V1 = {'
82 82 'repo.pull_request.merge': '',
83 83 'repo.pull_request.vote': '',
84 84 'repo.pull_request.comment.create': '',
85 'repo.pull_request.comment.edit': '',
85 86 'repo.pull_request.comment.delete': '',
86 87
87 88 'repo.pull_request.reviewer.add': '',
@@ -90,6 +91,7 b' ACTIONS_V1 = {'
90 91 'repo.commit.strip': {'commit_id': ''},
91 92 'repo.commit.comment.create': {'data': {}},
92 93 'repo.commit.comment.delete': {'data': {}},
94 'repo.commit.comment.edit': {'data': {}},
93 95 'repo.commit.vote': '',
94 96
95 97 'repo.artifact.add': '',
@@ -367,8 +367,7 b' class PermOriginDict(dict):'
367 367 self.perm_origin_stack = collections.OrderedDict()
368 368
369 369 def __setitem__(self, key, (perm, origin, obj_id)):
370 self.perm_origin_stack.setdefault(key, []).append(
371 (perm, origin, obj_id))
370 self.perm_origin_stack.setdefault(key, []).append((perm, origin, obj_id))
372 371 dict.__setitem__(self, key, perm)
373 372
374 373
@@ -441,7 +440,7 b' class PermissionCalculator(object):'
441 440
442 441 def calculate(self):
443 442 if self.user_is_admin and not self.calculate_super_admin_as_user:
444 return self._calculate_admin_permissions()
443 return self._calculate_super_admin_permissions()
445 444
446 445 self._calculate_global_default_permissions()
447 446 self._calculate_global_permissions()
@@ -452,9 +451,9 b' class PermissionCalculator(object):'
452 451 self._calculate_user_group_permissions()
453 452 return self._permission_structure()
454 453
455 def _calculate_admin_permissions(self):
454 def _calculate_super_admin_permissions(self):
456 455 """
457 admin user have all default rights for repositories
456 super-admin user have all default rights for repositories
458 457 and groups set to admin
459 458 """
460 459 self.permissions_global.add('hg.admin')
@@ -774,6 +773,7 b' class PermissionCalculator(object):'
774 773 for perm in user_repo_perms:
775 774 r_k = perm.UserRepoToPerm.repository.repo_name
776 775 obj_id = perm.UserRepoToPerm.repository.repo_id
776 archived = perm.UserRepoToPerm.repository.archived
777 777 p = perm.Permission.permission_name
778 778 o = PermOrigin.REPO_USER % perm.UserRepoToPerm.user.username
779 779
@@ -795,6 +795,15 b' class PermissionCalculator(object):'
795 795 o = PermOrigin.SUPER_ADMIN
796 796 self.permissions_repositories[r_k] = p, o, obj_id
797 797
798 # finally in case of archived repositories, we downgrade higher
799 # permissions to read
800 if archived:
801 current_perm = self.permissions_repositories[r_k]
802 if current_perm in ['repository.write', 'repository.admin']:
803 p = 'repository.read'
804 o = PermOrigin.ARCHIVED
805 self.permissions_repositories[r_k] = p, o, obj_id
806
798 807 def _calculate_repository_branch_permissions(self):
799 808 # user group for repositories permissions
800 809 user_repo_branch_perms_from_user_group = Permission\
@@ -384,7 +384,8 b' def attach_context_attributes(context, r'
384 384 session_attrs = {
385 385 # defaults
386 386 "clone_url_format": "http",
387 "diffmode": "sideside"
387 "diffmode": "sideside",
388 "license_fingerprint": request.session.get('license_fingerprint')
388 389 }
389 390
390 391 if not is_api:
@@ -61,6 +61,8 b' markdown_tags = ['
61 61 "img",
62 62 "a",
63 63 "input",
64 "details",
65 "summary"
64 66 ]
65 67
66 68 markdown_attrs = {
@@ -29,18 +29,20 b' import time'
29 29 from pyramid import compat
30 30 from pyramid_mailer.mailer import Mailer
31 31 from pyramid_mailer.message import Message
32 from email.utils import formatdate
32 33
33 34 import rhodecode
34 35 from rhodecode.lib import audit_logger
35 36 from rhodecode.lib.celerylib import get_logger, async_task, RequestContextTask
36 from rhodecode.lib.hooks_base import log_create_repository
37 from rhodecode.lib import hooks_base
37 38 from rhodecode.lib.utils2 import safe_int, str2bool
38 39 from rhodecode.model.db import (
39 40 Session, IntegrityError, true, Repository, RepoGroup, User)
40 41
41 42
42 43 @async_task(ignore_result=True, base=RequestContextTask)
43 def send_email(recipients, subject, body='', html_body='', email_config=None):
44 def send_email(recipients, subject, body='', html_body='', email_config=None,
45 extra_headers=None):
44 46 """
45 47 Sends an email with defined parameters from the .ini files.
46 48
@@ -50,6 +52,7 b' def send_email(recipients, subject, body'
50 52 :param body: body of the mail
51 53 :param html_body: html version of body
52 54 :param email_config: specify custom configuration for mailer
55 :param extra_headers: specify custom headers
53 56 """
54 57 log = get_logger(send_email)
55 58
@@ -108,13 +111,23 b' def send_email(recipients, subject, body'
108 111 # sendmail_template='',
109 112 )
110 113
114 if extra_headers is None:
115 extra_headers = {}
116
117 extra_headers.setdefault('Date', formatdate(time.time()))
118
119 if 'thread_ids' in extra_headers:
120 thread_ids = extra_headers.pop('thread_ids')
121 extra_headers['References'] = ' '.join('<{}>'.format(t) for t in thread_ids)
122
111 123 try:
112 124 mailer = Mailer(**email_conf)
113 125
114 126 message = Message(subject=subject,
115 127 sender=email_conf['default_sender'],
116 128 recipients=recipients,
117 body=body, html=html_body)
129 body=body, html=html_body,
130 extra_headers=extra_headers)
118 131 mailer.send_immediately(message)
119 132
120 133 except Exception:
@@ -187,7 +200,7 b' def create_repo(form_data, cur_user):'
187 200 clone_uri=clone_uri,
188 201 )
189 202 repo = Repository.get_by_repo_name(repo_name_full)
190 log_create_repository(created_by=owner.username, **repo.get_dict())
203 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
191 204
192 205 # update repo commit caches initially
193 206 repo.update_commit_cache()
@@ -273,7 +286,7 b' def create_repo_fork(form_data, cur_user'
273 286 clone_uri=source_repo_path,
274 287 )
275 288 repo = Repository.get_by_repo_name(repo_name_full)
276 log_create_repository(created_by=owner.username, **repo.get_dict())
289 hooks_base.create_repository(created_by=owner.username, **repo.get_dict())
277 290
278 291 # update repo commit caches initially
279 292 config = repo._config
@@ -540,10 +540,11 b' class DiffSet(object):'
540 540 })
541 541
542 542 file_chunks = patch['chunks'][1:]
543 for hunk in file_chunks:
543 for i, hunk in enumerate(file_chunks, 1):
544 544 hunkbit = self.parse_hunk(hunk, source_file, target_file)
545 545 hunkbit.source_file_path = source_file_path
546 546 hunkbit.target_file_path = target_file_path
547 hunkbit.index = i
547 548 filediff.hunks.append(hunkbit)
548 549
549 550 # Simulate hunk on OPS type line which doesn't really contain any diff
@@ -143,8 +143,7 b' def send_exc_email(request, exc_id, exc_'
143 143 'exc_traceback': read_exception(exc_id, prefix=None),
144 144 }
145 145
146 (subject, headers, email_body,
147 email_body_plaintext) = EmailNotificationModel().render_email(
146 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
148 147 EmailNotificationModel.TYPE_EMAIL_EXCEPTION, **email_kwargs)
149 148
150 149 run_task(tasks.send_email, recipients, subject,
@@ -177,3 +177,7 b' class ArtifactMetadataDuplicate(ValueErr'
177 177
178 178 class ArtifactMetadataBadValueType(ValueError):
179 179 pass
180
181
182 class CommentVersionMismatch(ValueError):
183 pass
@@ -24,6 +24,7 b' Helper functions'
24 24 Consists of functions to typically be used within templates, but also
25 25 available to Controllers. This module is available to both as 'h'.
26 26 """
27 import base64
27 28
28 29 import os
29 30 import random
@@ -52,7 +53,7 b' from pygments.lexers import ('
52 53 get_lexer_by_name, get_lexer_for_filename, get_lexer_for_mimetype)
53 54
54 55 from pyramid.threadlocal import get_current_request
55
56 from tempita import looper
56 57 from webhelpers2.html import literal, HTML, escape
57 58 from webhelpers2.html._autolink import _auto_link_urls
58 59 from webhelpers2.html.tools import (
@@ -85,10 +86,11 b' from rhodecode.lib.utils2 import ('
85 86 from rhodecode.lib.markup_renderer import MarkupRenderer, relative_links
86 87 from rhodecode.lib.vcs.exceptions import CommitDoesNotExistError
87 88 from rhodecode.lib.vcs.backends.base import BaseChangeset, EmptyCommit
89 from rhodecode.lib.vcs.conf.settings import ARCHIVE_SPECS
88 90 from rhodecode.lib.index.search_utils import get_matching_line_offsets
89 91 from rhodecode.config.conf import DATE_FORMAT, DATETIME_FORMAT
90 92 from rhodecode.model.changeset_status import ChangesetStatusModel
91 from rhodecode.model.db import Permission, User, Repository
93 from rhodecode.model.db import Permission, User, Repository, UserApiKeys
92 94 from rhodecode.model.repo_group import RepoGroupModel
93 95 from rhodecode.model.settings import IssueTrackerSettingsModel
94 96
@@ -783,13 +785,24 b' flash = Flash()'
783 785 # SCM FILTERS available via h.
784 786 #==============================================================================
785 787 from rhodecode.lib.vcs.utils import author_name, author_email
786 from rhodecode.lib.utils2 import credentials_filter, age, age_from_seconds
788 from rhodecode.lib.utils2 import age, age_from_seconds
787 789 from rhodecode.model.db import User, ChangesetStatus
788 790
789 capitalize = lambda x: x.capitalize()
791
790 792 email = author_email
791 short_id = lambda x: x[:12]
792 hide_credentials = lambda x: ''.join(credentials_filter(x))
793
794
795 def capitalize(raw_text):
796 return raw_text.capitalize()
797
798
799 def short_id(long_id):
800 return long_id[:12]
801
802
803 def hide_credentials(url):
804 from rhodecode.lib.utils2 import credentials_filter
805 return credentials_filter(url)
793 806
794 807
795 808 import pytz
@@ -948,7 +961,7 b' def link_to_user(author, length=0, **kwa'
948 961 if length:
949 962 display_person = shorter(display_person, length)
950 963
951 if user:
964 if user and user.username != user.DEFAULT_USER:
952 965 return link_to(
953 966 escape(display_person),
954 967 route_path('user_profile', username=user.username),
@@ -1341,7 +1354,7 b' class InitialsGravatar(object):'
1341 1354
1342 1355 def generate_svg(self, svg_type=None):
1343 1356 img_data = self.get_img_data(svg_type)
1344 return "data:image/svg+xml;base64,%s" % img_data.encode('base64')
1357 return "data:image/svg+xml;base64,%s" % base64.b64encode(img_data)
1345 1358
1346 1359
1347 1360 def initials_gravatar(email_address, first_name, last_name, size=30):
@@ -400,7 +400,7 b' pre_create_user = ExtensionCallback('
400 400 'admin', 'created_by'))
401 401
402 402
403 log_create_pull_request = ExtensionCallback(
403 create_pull_request = ExtensionCallback(
404 404 hook_name='CREATE_PULL_REQUEST',
405 405 kwargs_keys=(
406 406 'server_url', 'config', 'scm', 'username', 'ip', 'action',
@@ -409,7 +409,7 b' log_create_pull_request = ExtensionCallb'
409 409 'mergeable', 'source', 'target', 'author', 'reviewers'))
410 410
411 411
412 log_merge_pull_request = ExtensionCallback(
412 merge_pull_request = ExtensionCallback(
413 413 hook_name='MERGE_PULL_REQUEST',
414 414 kwargs_keys=(
415 415 'server_url', 'config', 'scm', 'username', 'ip', 'action',
@@ -418,7 +418,7 b' log_merge_pull_request = ExtensionCallba'
418 418 'mergeable', 'source', 'target', 'author', 'reviewers'))
419 419
420 420
421 log_close_pull_request = ExtensionCallback(
421 close_pull_request = ExtensionCallback(
422 422 hook_name='CLOSE_PULL_REQUEST',
423 423 kwargs_keys=(
424 424 'server_url', 'config', 'scm', 'username', 'ip', 'action',
@@ -427,7 +427,7 b' log_close_pull_request = ExtensionCallba'
427 427 'mergeable', 'source', 'target', 'author', 'reviewers'))
428 428
429 429
430 log_review_pull_request = ExtensionCallback(
430 review_pull_request = ExtensionCallback(
431 431 hook_name='REVIEW_PULL_REQUEST',
432 432 kwargs_keys=(
433 433 'server_url', 'config', 'scm', 'username', 'ip', 'action',
@@ -436,7 +436,7 b' log_review_pull_request = ExtensionCallb'
436 436 'mergeable', 'source', 'target', 'author', 'reviewers'))
437 437
438 438
439 log_comment_pull_request = ExtensionCallback(
439 comment_pull_request = ExtensionCallback(
440 440 hook_name='COMMENT_PULL_REQUEST',
441 441 kwargs_keys=(
442 442 'server_url', 'config', 'scm', 'username', 'ip', 'action',
@@ -445,7 +445,16 b' log_comment_pull_request = ExtensionCall'
445 445 'mergeable', 'source', 'target', 'author', 'reviewers'))
446 446
447 447
448 log_update_pull_request = ExtensionCallback(
448 comment_edit_pull_request = ExtensionCallback(
449 hook_name='COMMENT_EDIT_PULL_REQUEST',
450 kwargs_keys=(
451 'server_url', 'config', 'scm', 'username', 'ip', 'action',
452 'repository', 'pull_request_id', 'url', 'title', 'description',
453 'status', 'comment', 'created_on', 'updated_on', 'commit_ids', 'review_status',
454 'mergeable', 'source', 'target', 'author', 'reviewers'))
455
456
457 update_pull_request = ExtensionCallback(
449 458 hook_name='UPDATE_PULL_REQUEST',
450 459 kwargs_keys=(
451 460 'server_url', 'config', 'scm', 'username', 'ip', 'action',
@@ -454,7 +463,7 b' log_update_pull_request = ExtensionCallb'
454 463 'mergeable', 'source', 'target', 'author', 'reviewers'))
455 464
456 465
457 log_create_user = ExtensionCallback(
466 create_user = ExtensionCallback(
458 467 hook_name='CREATE_USER_HOOK',
459 468 kwargs_keys=(
460 469 'username', 'full_name_or_username', 'full_contact', 'user_id',
@@ -465,7 +474,7 b' log_create_user = ExtensionCallback('
465 474 'inherit_default_permissions', 'created_by', 'created_on'))
466 475
467 476
468 log_delete_user = ExtensionCallback(
477 delete_user = ExtensionCallback(
469 478 hook_name='DELETE_USER_HOOK',
470 479 kwargs_keys=(
471 480 'username', 'full_name_or_username', 'full_contact', 'user_id',
@@ -476,7 +485,7 b' log_delete_user = ExtensionCallback('
476 485 'inherit_default_permissions', 'deleted_by'))
477 486
478 487
479 log_create_repository = ExtensionCallback(
488 create_repository = ExtensionCallback(
480 489 hook_name='CREATE_REPO_HOOK',
481 490 kwargs_keys=(
482 491 'repo_name', 'repo_type', 'description', 'private', 'created_on',
@@ -484,7 +493,7 b' log_create_repository = ExtensionCallbac'
484 493 'clone_uri', 'fork_id', 'group_id', 'created_by'))
485 494
486 495
487 log_delete_repository = ExtensionCallback(
496 delete_repository = ExtensionCallback(
488 497 hook_name='DELETE_REPO_HOOK',
489 498 kwargs_keys=(
490 499 'repo_name', 'repo_type', 'description', 'private', 'created_on',
@@ -492,7 +501,7 b' log_delete_repository = ExtensionCallbac'
492 501 'clone_uri', 'fork_id', 'group_id', 'deleted_by', 'deleted_on'))
493 502
494 503
495 log_comment_commit_repository = ExtensionCallback(
504 comment_commit_repository = ExtensionCallback(
496 505 hook_name='COMMENT_COMMIT_REPO_HOOK',
497 506 kwargs_keys=(
498 507 'repo_name', 'repo_type', 'description', 'private', 'created_on',
@@ -500,8 +509,16 b' log_comment_commit_repository = Extensio'
500 509 'clone_uri', 'fork_id', 'group_id',
501 510 'repository', 'created_by', 'comment', 'commit'))
502 511
512 comment_edit_commit_repository = ExtensionCallback(
513 hook_name='COMMENT_EDIT_COMMIT_REPO_HOOK',
514 kwargs_keys=(
515 'repo_name', 'repo_type', 'description', 'private', 'created_on',
516 'enable_downloads', 'repo_id', 'user_id', 'enable_statistics',
517 'clone_uri', 'fork_id', 'group_id',
518 'repository', 'created_by', 'comment', 'commit'))
503 519
504 log_create_repository_group = ExtensionCallback(
520
521 create_repository_group = ExtensionCallback(
505 522 hook_name='CREATE_REPO_GROUP_HOOK',
506 523 kwargs_keys=(
507 524 'group_name', 'group_parent_id', 'group_description',
@@ -94,7 +94,34 b' def trigger_comment_commit_hooks(usernam'
94 94 extras.commit = commit.serialize()
95 95 extras.comment = comment.get_api_data()
96 96 extras.created_by = username
97 hooks_base.log_comment_commit_repository(**extras)
97 hooks_base.comment_commit_repository(**extras)
98
99
100 def trigger_comment_commit_edit_hooks(username, repo_name, repo_type, repo, data=None):
101 """
102 Triggers when a comment is edited on a commit
103
104 :param username: username who edits the comment
105 :param repo_name: name of target repo
106 :param repo_type: the type of SCM target repo
107 :param repo: the repo object we trigger the event for
108 :param data: extra data for specific events e.g {'comment': comment_obj, 'commit': commit_obj}
109 """
110 if not _supports_repo_type(repo_type):
111 return
112
113 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'comment_commit')
114
115 comment = data['comment']
116 commit = data['commit']
117
118 events.trigger(events.RepoCommitCommentEditEvent(repo, commit, comment))
119 extras.update(repo.get_dict())
120
121 extras.commit = commit.serialize()
122 extras.comment = comment.get_api_data()
123 extras.created_by = username
124 hooks_base.comment_edit_commit_repository(**extras)
98 125
99 126
100 127 def trigger_create_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
@@ -113,7 +140,7 b' def trigger_create_pull_request_hook(use'
113 140 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'create_pull_request')
114 141 events.trigger(events.PullRequestCreateEvent(pull_request))
115 142 extras.update(pull_request.get_api_data(with_merge_state=False))
116 hooks_base.log_create_pull_request(**extras)
143 hooks_base.create_pull_request(**extras)
117 144
118 145
119 146 def trigger_merge_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
@@ -132,7 +159,7 b' def trigger_merge_pull_request_hook(user'
132 159 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'merge_pull_request')
133 160 events.trigger(events.PullRequestMergeEvent(pull_request))
134 161 extras.update(pull_request.get_api_data())
135 hooks_base.log_merge_pull_request(**extras)
162 hooks_base.merge_pull_request(**extras)
136 163
137 164
138 165 def trigger_close_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
@@ -151,7 +178,7 b' def trigger_close_pull_request_hook(user'
151 178 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'close_pull_request')
152 179 events.trigger(events.PullRequestCloseEvent(pull_request))
153 180 extras.update(pull_request.get_api_data())
154 hooks_base.log_close_pull_request(**extras)
181 hooks_base.close_pull_request(**extras)
155 182
156 183
157 184 def trigger_review_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
@@ -171,7 +198,7 b' def trigger_review_pull_request_hook(use'
171 198 status = data.get('status')
172 199 events.trigger(events.PullRequestReviewEvent(pull_request, status))
173 200 extras.update(pull_request.get_api_data())
174 hooks_base.log_review_pull_request(**extras)
201 hooks_base.review_pull_request(**extras)
175 202
176 203
177 204 def trigger_comment_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
@@ -193,7 +220,29 b' def trigger_comment_pull_request_hook(us'
193 220 events.trigger(events.PullRequestCommentEvent(pull_request, comment))
194 221 extras.update(pull_request.get_api_data())
195 222 extras.comment = comment.get_api_data()
196 hooks_base.log_comment_pull_request(**extras)
223 hooks_base.comment_pull_request(**extras)
224
225
226 def trigger_comment_pull_request_edit_hook(username, repo_name, repo_type, pull_request, data=None):
227 """
228 Triggers when a comment was edited on a pull request
229
230 :param username: username who made the edit
231 :param repo_name: name of target repo
232 :param repo_type: the type of SCM target repo
233 :param pull_request: the pull request that comment was made on
234 :param data: extra data for specific events e.g {'comment': comment_obj}
235 """
236 if not _supports_repo_type(repo_type):
237 return
238
239 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'comment_pull_request')
240
241 comment = data['comment']
242 events.trigger(events.PullRequestCommentEditEvent(pull_request, comment))
243 extras.update(pull_request.get_api_data())
244 extras.comment = comment.get_api_data()
245 hooks_base.comment_edit_pull_request(**extras)
197 246
198 247
199 248 def trigger_update_pull_request_hook(username, repo_name, repo_type, pull_request, data=None):
@@ -212,4 +261,4 b' def trigger_update_pull_request_hook(use'
212 261 extras = _get_vcs_operation_context(username, repo_name, repo_type, 'update_pull_request')
213 262 events.trigger(events.PullRequestUpdateEvent(pull_request))
214 263 extras.update(pull_request.get_api_data())
215 hooks_base.log_update_pull_request(**extras)
264 hooks_base.update_pull_request(**extras)
@@ -48,6 +48,7 b' from .utils import ('
48 48
49 49
50 50 FILE_TREE_CACHE_VER = 'v4'
51 LICENSE_CACHE_VER = 'v2'
51 52
52 53
53 54 def configure_dogpile_cache(settings):
@@ -159,7 +159,14 b' class FileNamespaceBackend(PickleSeriali'
159 159
160 160 def __init__(self, arguments):
161 161 arguments['lock_factory'] = CustomLockFactory
162 super(FileNamespaceBackend, self).__init__(arguments)
162 db_file = arguments.get('filename')
163
164 log.debug('initialing %s DB in %s', self.__class__.__name__, db_file)
165 try:
166 super(FileNamespaceBackend, self).__init__(arguments)
167 except Exception:
168 log.error('Failed to initialize db at: %s', db_file)
169 raise
163 170
164 171 def __repr__(self):
165 172 return '{} `{}`'.format(self.__class__, self.filename)
@@ -30,6 +30,7 b' import os'
30 30 import re
31 31 import sys
32 32 import shutil
33 import socket
33 34 import tempfile
34 35 import traceback
35 36 import tarfile
@@ -782,3 +783,18 b' def generate_platform_uuid():'
782 783 except Exception as e:
783 784 log.error('Failed to generate host uuid: %s', e)
784 785 return 'UNDEFINED'
786
787
788 def send_test_email(recipients, email_body='TEST EMAIL'):
789 """
790 Simple code for generating test emails.
791 Usage::
792
793 from rhodecode.lib import utils
794 utils.send_test_email()
795 """
796 from rhodecode.lib.celerylib import tasks, run_task
797
798 email_body = email_body_plaintext = email_body
799 subject = 'SUBJECT FROM: {}'.format(socket.gethostname())
800 tasks.send_email(recipients, subject, email_body_plaintext, email_body)
@@ -628,34 +628,42 b' class MercurialRepository(BaseRepository'
628 628 push_branches=push_branches)
629 629
630 630 def _local_merge(self, target_ref, merge_message, user_name, user_email,
631 source_ref, use_rebase=False, dry_run=False):
631 source_ref, use_rebase=False, close_commit_id=None, dry_run=False):
632 632 """
633 633 Merge the given source_revision into the checked out revision.
634 634
635 635 Returns the commit id of the merge and a boolean indicating if the
636 636 commit needs to be pushed.
637 637 """
638 self._update(target_ref.commit_id, clean=True)
638 source_ref_commit_id = source_ref.commit_id
639 target_ref_commit_id = target_ref.commit_id
639 640
640 ancestor = self._ancestor(target_ref.commit_id, source_ref.commit_id)
641 # update our workdir to target ref, for proper merge
642 self._update(target_ref_commit_id, clean=True)
643
644 ancestor = self._ancestor(target_ref_commit_id, source_ref_commit_id)
641 645 is_the_same_branch = self._is_the_same_branch(target_ref, source_ref)
642 646
643 if ancestor == source_ref.commit_id:
644 # Nothing to do, the changes were already integrated
645 return target_ref.commit_id, False
647 if close_commit_id:
648 # NOTE(marcink): if we get the close commit, this is our new source
649 # which will include the close commit itself.
650 source_ref_commit_id = close_commit_id
646 651
647 elif ancestor == target_ref.commit_id and is_the_same_branch:
652 if ancestor == source_ref_commit_id:
653 # Nothing to do, the changes were already integrated
654 return target_ref_commit_id, False
655
656 elif ancestor == target_ref_commit_id and is_the_same_branch:
648 657 # In this case we should force a commit message
649 return source_ref.commit_id, True
658 return source_ref_commit_id, True
650 659
651 660 unresolved = None
652 661 if use_rebase:
653 662 try:
654 bookmark_name = 'rcbook%s%s' % (source_ref.commit_id,
655 target_ref.commit_id)
663 bookmark_name = 'rcbook%s%s' % (source_ref_commit_id, target_ref_commit_id)
656 664 self.bookmark(bookmark_name, revision=source_ref.commit_id)
657 665 self._remote.rebase(
658 source=source_ref.commit_id, dest=target_ref.commit_id)
666 source=source_ref_commit_id, dest=target_ref_commit_id)
659 667 self._remote.invalidate_vcs_cache()
660 668 self._update(bookmark_name, clean=True)
661 669 return self._identify(), True
@@ -678,7 +686,7 b' class MercurialRepository(BaseRepository'
678 686 raise
679 687 else:
680 688 try:
681 self._remote.merge(source_ref.commit_id)
689 self._remote.merge(source_ref_commit_id)
682 690 self._remote.invalidate_vcs_cache()
683 691 self._remote.commit(
684 692 message=safe_str(merge_message),
@@ -820,10 +828,12 b' class MercurialRepository(BaseRepository'
820 828
821 829 needs_push = False
822 830 if merge_possible:
831
823 832 try:
824 833 merge_commit_id, needs_push = shadow_repo._local_merge(
825 834 target_ref, merge_message, merger_name, merger_email,
826 source_ref, use_rebase=use_rebase, dry_run=dry_run)
835 source_ref, use_rebase=use_rebase,
836 close_commit_id=close_commit_id, dry_run=dry_run)
827 837 merge_possible = True
828 838
829 839 # read the state of the close action, if it
@@ -41,7 +41,7 b' BACKENDS = {'
41 41
42 42
43 43 ARCHIVE_SPECS = [
44 ('tbz2', 'application/x-bzip2', 'tbz2'),
44 ('tbz2', 'application/x-bzip2', '.tbz2'),
45 45 ('tbz2', 'application/x-bzip2', '.tar.bz2'),
46 46
47 47 ('tgz', 'application/x-gzip', '.tgz'),
@@ -21,6 +21,7 b''
21 21 """
22 22 comments model for RhodeCode
23 23 """
24 import datetime
24 25
25 26 import logging
26 27 import traceback
@@ -32,10 +33,17 b' from sqlalchemy.sql.functions import coa'
32 33
33 34 from rhodecode.lib import helpers as h, diffs, channelstream, hooks_utils
34 35 from rhodecode.lib import audit_logger
35 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str
36 from rhodecode.lib.exceptions import CommentVersionMismatch
37 from rhodecode.lib.utils2 import extract_mentioned_users, safe_str, safe_int
36 38 from rhodecode.model import BaseModel
37 39 from rhodecode.model.db import (
38 ChangesetComment, User, Notification, PullRequest, AttributeDict)
40 ChangesetComment,
41 User,
42 Notification,
43 PullRequest,
44 AttributeDict,
45 ChangesetCommentHistory,
46 )
39 47 from rhodecode.model.notification import NotificationModel
40 48 from rhodecode.model.meta import Session
41 49 from rhodecode.model.settings import VcsSettingsModel
@@ -362,13 +370,18 b' class CommentsModel(BaseModel):'
362 370 repo.repo_name,
363 371 h.route_url('repo_summary', repo_name=repo.repo_name))
364 372
373 commit_url = h.route_url('repo_commit', repo_name=repo.repo_name,
374 commit_id=commit_id)
375
365 376 # commit specifics
366 377 kwargs.update({
367 378 'commit': commit_obj,
368 379 'commit_message': commit_obj.message,
369 380 'commit_target_repo_url': target_repo_url,
370 381 'commit_comment_url': commit_comment_url,
371 'commit_comment_reply_url': commit_comment_reply_url
382 'commit_comment_reply_url': commit_comment_reply_url,
383 'commit_url': commit_url,
384 'thread_ids': [commit_url, commit_comment_url],
372 385 })
373 386
374 387 elif pull_request_obj:
@@ -413,15 +426,14 b' class CommentsModel(BaseModel):'
413 426 'pr_comment_url': pr_comment_url,
414 427 'pr_comment_reply_url': pr_comment_reply_url,
415 428 'pr_closing': closing_pr,
429 'thread_ids': [pr_url, pr_comment_url],
416 430 })
417 431
418 432 recipients += [self._get_user(u) for u in (extra_recipients or [])]
419 433
420 434 if send_email:
421 435 # pre-generate the subject for notification itself
422 (subject,
423 _h, _e, # we don't care about those
424 body_plaintext) = EmailNotificationModel().render_email(
436 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
425 437 notification_type, **kwargs)
426 438
427 439 mention_recipients = set(
@@ -479,6 +491,60 b' class CommentsModel(BaseModel):'
479 491
480 492 return comment
481 493
494 def edit(self, comment_id, text, auth_user, version):
495 """
496 Change existing comment for commit or pull request.
497
498 :param comment_id:
499 :param text:
500 :param auth_user: current authenticated user calling this method
501 :param version: last comment version
502 """
503 if not text:
504 log.warning('Missing text for comment, skipping...')
505 return
506
507 comment = ChangesetComment.get(comment_id)
508 old_comment_text = comment.text
509 comment.text = text
510 comment.modified_at = datetime.datetime.now()
511 version = safe_int(version)
512
513 # NOTE(marcink): this returns initial comment + edits, so v2 from ui
514 # would return 3 here
515 comment_version = ChangesetCommentHistory.get_version(comment_id)
516
517 if isinstance(version, (int, long)) and (comment_version - version) != 1:
518 log.warning(
519 'Version mismatch comment_version {} submitted {}, skipping'.format(
520 comment_version-1, # -1 since note above
521 version
522 )
523 )
524 raise CommentVersionMismatch()
525
526 comment_history = ChangesetCommentHistory()
527 comment_history.comment_id = comment_id
528 comment_history.version = comment_version
529 comment_history.created_by_user_id = auth_user.user_id
530 comment_history.text = old_comment_text
531 # TODO add email notification
532 Session().add(comment_history)
533 Session().add(comment)
534 Session().flush()
535
536 if comment.pull_request:
537 action = 'repo.pull_request.comment.edit'
538 else:
539 action = 'repo.commit.comment.edit'
540
541 comment_data = comment.get_api_data()
542 comment_data['old_comment_text'] = old_comment_text
543 self._log_audit_action(
544 action, {'data': comment_data}, auth_user, comment)
545
546 return comment_history
547
482 548 def delete(self, comment, auth_user):
483 549 """
484 550 Deletes given comment
@@ -712,6 +778,7 b' class CommentsModel(BaseModel):'
712 778 .filter(ChangesetComment.line_no == None)\
713 779 .filter(ChangesetComment.f_path == None)\
714 780 .filter(ChangesetComment.pull_request == pull_request)
781
715 782 return comments
716 783
717 784 @staticmethod
@@ -726,8 +793,7 b' class CommentsModel(BaseModel):'
726 793 if action == 'create':
727 794 trigger_hook = hooks_utils.trigger_comment_commit_hooks
728 795 elif action == 'edit':
729 # TODO(dan): when this is supported we trigger edit hook too
730 return
796 trigger_hook = hooks_utils.trigger_comment_commit_edit_hooks
731 797 else:
732 798 return
733 799
@@ -103,7 +103,12 b' def display_user_sort(obj):'
103 103 if obj.username == User.DEFAULT_USER:
104 104 return '#####'
105 105 prefix = PERMISSION_TYPE_SORT.get(obj.permission.split('.')[-1], '')
106 return prefix + obj.username
106 extra_sort_num = '1' # default
107
108 # NOTE(dan): inactive duplicates goes last
109 if getattr(obj, 'duplicate_perm', None):
110 extra_sort_num = '9'
111 return prefix + extra_sort_num + obj.username
107 112
108 113
109 114 def display_user_group_sort(obj):
@@ -1128,14 +1133,16 b' class UserApiKeys(Base, BaseModel):'
1128 1133
1129 1134 # ApiKey role
1130 1135 ROLE_ALL = 'token_role_all'
1131 ROLE_HTTP = 'token_role_http'
1132 1136 ROLE_VCS = 'token_role_vcs'
1133 1137 ROLE_API = 'token_role_api'
1138 ROLE_HTTP = 'token_role_http'
1134 1139 ROLE_FEED = 'token_role_feed'
1135 1140 ROLE_ARTIFACT_DOWNLOAD = 'role_artifact_download'
1141 # The last one is ignored in the list as we only
1142 # use it for one action, and cannot be created by users
1136 1143 ROLE_PASSWORD_RESET = 'token_password_reset'
1137 1144
1138 ROLES = [ROLE_ALL, ROLE_HTTP, ROLE_VCS, ROLE_API, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1145 ROLES = [ROLE_ALL, ROLE_VCS, ROLE_API, ROLE_HTTP, ROLE_FEED, ROLE_ARTIFACT_DOWNLOAD]
1139 1146
1140 1147 user_api_key_id = Column("user_api_key_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
1141 1148 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=True, unique=None, default=None)
@@ -1200,6 +1207,22 b' class UserApiKeys(Base, BaseModel):'
1200 1207 cls.ROLE_ARTIFACT_DOWNLOAD: _('artifacts downloads'),
1201 1208 }.get(role, role)
1202 1209
1210 @classmethod
1211 def _get_role_description(cls, role):
1212 return {
1213 cls.ROLE_ALL: _('Token for all actions.'),
1214 cls.ROLE_HTTP: _('Token to access RhodeCode pages via web interface without '
1215 'login using `api_access_controllers_whitelist` functionality.'),
1216 cls.ROLE_VCS: _('Token to interact over git/hg/svn protocols. '
1217 'Requires auth_token authentication plugin to be active. <br/>'
1218 'Such Token should be used then instead of a password to '
1219 'interact with a repository, and additionally can be '
1220 'limited to single repository using repo scope.'),
1221 cls.ROLE_API: _('Token limited to api calls.'),
1222 cls.ROLE_FEED: _('Token to read RSS/ATOM feed.'),
1223 cls.ROLE_ARTIFACT_DOWNLOAD: _('Token for artifacts downloads.'),
1224 }.get(role, role)
1225
1203 1226 @property
1204 1227 def role_humanized(self):
1205 1228 return self._get_role_name(self.role)
@@ -3755,6 +3778,7 b' class ChangesetComment(Base, BaseModel):'
3755 3778 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
3756 3779 pull_request = relationship('PullRequest', lazy='joined')
3757 3780 pull_request_version = relationship('PullRequestVersion')
3781 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='joined', order_by='ChangesetCommentHistory.version')
3758 3782
3759 3783 @classmethod
3760 3784 def get_users(cls, revision=None, pull_request_id=None):
@@ -3777,7 +3801,7 b' class ChangesetComment(Base, BaseModel):'
3777 3801 def get_index_from_version(cls, pr_version, versions):
3778 3802 num_versions = [x.pull_request_version_id for x in versions]
3779 3803 try:
3780 return num_versions.index(pr_version) +1
3804 return num_versions.index(pr_version) + 1
3781 3805 except (IndexError, ValueError):
3782 3806 return
3783 3807
@@ -3805,6 +3829,11 b' class ChangesetComment(Base, BaseModel):'
3805 3829 return self.pull_request_version_id < version
3806 3830
3807 3831 @property
3832 def commit_id(self):
3833 """New style naming to stop using .revision"""
3834 return self.revision
3835
3836 @property
3808 3837 def resolved(self):
3809 3838 return self.resolved_by[0] if self.resolved_by else None
3810 3839
@@ -3816,6 +3845,13 b' class ChangesetComment(Base, BaseModel):'
3816 3845 def is_inline(self):
3817 3846 return self.line_no and self.f_path
3818 3847
3848 @property
3849 def last_version(self):
3850 version = 0
3851 if self.history:
3852 version = self.history[-1].version
3853 return version
3854
3819 3855 def get_index_version(self, versions):
3820 3856 return self.get_index_from_version(
3821 3857 self.pull_request_version_id, versions)
@@ -3828,6 +3864,7 b' class ChangesetComment(Base, BaseModel):'
3828 3864
3829 3865 def get_api_data(self):
3830 3866 comment = self
3867
3831 3868 data = {
3832 3869 'comment_id': comment.comment_id,
3833 3870 'comment_type': comment.comment_type,
@@ -3840,6 +3877,7 b' class ChangesetComment(Base, BaseModel):'
3840 3877 'comment_resolved_by': self.resolved,
3841 3878 'comment_commit_id': comment.revision,
3842 3879 'comment_pull_request_id': comment.pull_request_id,
3880 'comment_last_version': self.last_version
3843 3881 }
3844 3882 return data
3845 3883
@@ -3849,6 +3887,36 b' class ChangesetComment(Base, BaseModel):'
3849 3887 return data
3850 3888
3851 3889
3890 class ChangesetCommentHistory(Base, BaseModel):
3891 __tablename__ = 'changeset_comments_history'
3892 __table_args__ = (
3893 Index('cch_comment_id_idx', 'comment_id'),
3894 base_table_args,
3895 )
3896
3897 comment_history_id = Column('comment_history_id', Integer(), nullable=False, primary_key=True)
3898 comment_id = Column('comment_id', Integer(), ForeignKey('changeset_comments.comment_id'), nullable=False)
3899 version = Column("version", Integer(), nullable=False, default=0)
3900 created_by_user_id = Column('created_by_user_id', Integer(), ForeignKey('users.user_id'), nullable=False)
3901 text = Column('text', UnicodeText().with_variant(UnicodeText(25000), 'mysql'), nullable=False)
3902 created_on = Column('created_on', DateTime(timezone=False), nullable=False, default=datetime.datetime.now)
3903 deleted = Column('deleted', Boolean(), default=False)
3904
3905 author = relationship('User', lazy='joined')
3906 comment = relationship('ChangesetComment', cascade="all, delete")
3907
3908 @classmethod
3909 def get_version(cls, comment_id):
3910 q = Session().query(ChangesetCommentHistory).filter(
3911 ChangesetCommentHistory.comment_id == comment_id).order_by(ChangesetCommentHistory.version.desc())
3912 if q.count() == 0:
3913 return 1
3914 elif q.count() >= q[0].version:
3915 return q.count() + 1
3916 else:
3917 return q[0].version + 1
3918
3919
3852 3920 class ChangesetStatus(Base, BaseModel):
3853 3921 __tablename__ = 'changeset_statuses'
3854 3922 __table_args__ = (
@@ -131,15 +131,17 b' class NotificationModel(BaseModel):'
131 131 # inject current recipient
132 132 email_kwargs['recipient'] = recipient
133 133 email_kwargs['mention'] = recipient in mention_recipients
134 (subject, headers, email_body,
135 email_body_plaintext) = EmailNotificationModel().render_email(
134 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
136 135 notification_type, **email_kwargs)
137 136
138 log.debug(
139 'Creating notification email task for user:`%s`', recipient)
137 extra_headers = None
138 if 'thread_ids' in email_kwargs:
139 extra_headers = {'thread_ids': email_kwargs.pop('thread_ids')}
140
141 log.debug('Creating notification email task for user:`%s`', recipient)
140 142 task = run_task(
141 143 tasks.send_email, recipient.email, subject,
142 email_body_plaintext, email_body)
144 email_body_plaintext, email_body, extra_headers=extra_headers)
143 145 log.debug('Created email task: %s', task)
144 146
145 147 return notification
@@ -293,6 +295,27 b' class NotificationModel(BaseModel):'
293 295 }
294 296
295 297
298 # Templates for Titles, that could be overwritten by rcextensions
299 # Title of email for pull-request update
300 EMAIL_PR_UPDATE_SUBJECT_TEMPLATE = ''
301 # Title of email for request for pull request review
302 EMAIL_PR_REVIEW_SUBJECT_TEMPLATE = ''
303
304 # Title of email for general comment on pull request
305 EMAIL_PR_COMMENT_SUBJECT_TEMPLATE = ''
306 # Title of email for general comment which includes status change on pull request
307 EMAIL_PR_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
308 # Title of email for inline comment on a file in pull request
309 EMAIL_PR_COMMENT_FILE_SUBJECT_TEMPLATE = ''
310
311 # Title of email for general comment on commit
312 EMAIL_COMMENT_SUBJECT_TEMPLATE = ''
313 # Title of email for general comment which includes status change on commit
314 EMAIL_COMMENT_STATUS_CHANGE_SUBJECT_TEMPLATE = ''
315 # Title of email for inline comment on a file in commit
316 EMAIL_COMMENT_FILE_SUBJECT_TEMPLATE = ''
317
318
296 319 class EmailNotificationModel(BaseModel):
297 320 TYPE_COMMIT_COMMENT = Notification.TYPE_CHANGESET_COMMENT
298 321 TYPE_REGISTRATION = Notification.TYPE_REGISTRATION
@@ -333,7 +356,7 b' class EmailNotificationModel(BaseModel):'
333 356 }
334 357
335 358 premailer_instance = premailer.Premailer(
336 cssutils_logging_level=logging.WARNING,
359 cssutils_logging_level=logging.ERROR,
337 360 cssutils_logging_handler=logging.getLogger().handlers[0]
338 361 if logging.getLogger().handlers else None,
339 362 )
@@ -342,8 +365,7 b' class EmailNotificationModel(BaseModel):'
342 365 """
343 366 Example usage::
344 367
345 (subject, headers, email_body,
346 email_body_plaintext) = EmailNotificationModel().render_email(
368 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
347 369 EmailNotificationModel.TYPE_TEST, **email_kwargs)
348 370
349 371 """
@@ -387,12 +409,6 b' class EmailNotificationModel(BaseModel):'
387 409 subject = email_template.render('subject', **_kwargs)
388 410
389 411 try:
390 headers = email_template.render('headers', **_kwargs)
391 except AttributeError:
392 # it's not defined in template, ok we can skip it
393 headers = ''
394
395 try:
396 412 body_plaintext = email_template.render('body_plaintext', **_kwargs)
397 413 except AttributeError:
398 414 # it's not defined in template, ok we can skip it
@@ -408,4 +424,4 b' class EmailNotificationModel(BaseModel):'
408 424 log.exception('Failed to parse body with premailer')
409 425 pass
410 426
411 return subject, headers, body, body_plaintext
427 return subject, body, body_plaintext
@@ -577,7 +577,8 b' class PermissionModel(BaseModel):'
577 577 user_group_write_permissions[p.users_group_id] = p
578 578 return user_group_write_permissions
579 579
580 def trigger_permission_flush(self, affected_user_ids):
580 def trigger_permission_flush(self, affected_user_ids=None):
581 affected_user_ids or User.get_all_user_ids()
581 582 events.trigger(events.UserPermissionsChange(affected_user_ids))
582 583
583 584 def flush_user_permission_caches(self, changes, affected_user_ids=None):
@@ -703,6 +703,8 b' class PullRequestModel(BaseModel):'
703 703 trigger_hook = hooks_utils.trigger_update_pull_request_hook
704 704 elif action == 'comment':
705 705 trigger_hook = hooks_utils.trigger_comment_pull_request_hook
706 elif action == 'comment_edit':
707 trigger_hook = hooks_utils.trigger_comment_pull_request_edit_hook
706 708 else:
707 709 return
708 710
@@ -1342,12 +1344,11 b' class PullRequestModel(BaseModel):'
1342 1344 'pull_request_source_repo_url': pr_source_repo_url,
1343 1345
1344 1346 'pull_request_url': pr_url,
1347 'thread_ids': [pr_url],
1345 1348 }
1346 1349
1347 1350 # pre-generate the subject for notification itself
1348 (subject,
1349 _h, _e, # we don't care about those
1350 body_plaintext) = EmailNotificationModel().render_email(
1351 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1351 1352 notification_type, **kwargs)
1352 1353
1353 1354 # create notification objects, and emails
@@ -1412,11 +1413,10 b' class PullRequestModel(BaseModel):'
1412 1413 'added_files': file_changes.added,
1413 1414 'modified_files': file_changes.modified,
1414 1415 'removed_files': file_changes.removed,
1416 'thread_ids': [pr_url],
1415 1417 }
1416 1418
1417 (subject,
1418 _h, _e, # we don't care about those
1419 body_plaintext) = EmailNotificationModel().render_email(
1419 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
1420 1420 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **email_kwargs)
1421 1421
1422 1422 # create notification objects, and emails
@@ -2053,9 +2053,9 b' class MergeCheck(object):'
2053 2053 repo_type = pull_request.target_repo.repo_type
2054 2054 close_msg = ''
2055 2055 if repo_type == 'hg':
2056 close_msg = _('Source branch will be closed after merge.')
2056 close_msg = _('Source branch will be closed before the merge.')
2057 2057 elif repo_type == 'git':
2058 close_msg = _('Source branch will be deleted after merge.')
2058 close_msg = _('Source branch will be deleted after the merge.')
2059 2059
2060 2060 merge_details['close_branch'] = dict(
2061 2061 details={},
@@ -33,7 +33,7 b' from rhodecode import events'
33 33 from rhodecode.lib.auth import HasUserGroupPermissionAny
34 34 from rhodecode.lib.caching_query import FromCache
35 35 from rhodecode.lib.exceptions import AttachedForksError, AttachedPullRequestsError
36 from rhodecode.lib.hooks_base import log_delete_repository
36 from rhodecode.lib import hooks_base
37 37 from rhodecode.lib.user_log_filter import user_log_filter
38 38 from rhodecode.lib.utils import make_db_config
39 39 from rhodecode.lib.utils2 import (
@@ -767,7 +767,7 b' class RepoModel(BaseModel):'
767 767 'deleted_by': cur_user,
768 768 'deleted_on': time.time(),
769 769 })
770 log_delete_repository(**old_repo_dict)
770 hooks_base.delete_repository(**old_repo_dict)
771 771 events.trigger(events.RepoDeleteEvent(repo))
772 772 except Exception:
773 773 log.error(traceback.format_exc())
@@ -308,13 +308,13 b' class RepoGroupModel(BaseModel):'
308 308 self._create_group(new_repo_group.group_name)
309 309
310 310 # trigger the post hook
311 from rhodecode.lib.hooks_base import log_create_repository_group
311 from rhodecode.lib import hooks_base
312 312 repo_group = RepoGroup.get_by_group_name(group_name)
313 313
314 314 # update repo group commit caches initially
315 315 repo_group.update_commit_cache()
316 316
317 log_create_repository_group(
317 hooks_base.create_repository_group(
318 318 created_by=user.username, **repo_group.get_dict())
319 319
320 320 # Trigger create event.
@@ -262,8 +262,7 b' class UserModel(BaseModel):'
262 262
263 263 from rhodecode.lib.auth import (
264 264 get_crypt_password, check_password)
265 from rhodecode.lib.hooks_base import (
266 log_create_user, check_allowed_create_user)
265 from rhodecode.lib import hooks_base
267 266
268 267 def _password_change(new_user, password):
269 268 old_password = new_user.password or ''
@@ -327,7 +326,7 b' class UserModel(BaseModel):'
327 326 if new_active_user and strict_creation_check:
328 327 # raises UserCreationError if it's not allowed for any reason to
329 328 # create new active user, this also executes pre-create hooks
330 check_allowed_create_user(user_data, cur_user, strict_check=True)
329 hooks_base.check_allowed_create_user(user_data, cur_user, strict_check=True)
331 330 events.trigger(events.UserPreCreate(user_data))
332 331 new_user = User()
333 332 edit = False
@@ -390,7 +389,7 b' class UserModel(BaseModel):'
390 389 kwargs = new_user.get_dict()
391 390 # backward compat, require api_keys present
392 391 kwargs['api_keys'] = kwargs['auth_tokens']
393 log_create_user(created_by=cur_user, **kwargs)
392 hooks_base.create_user(created_by=cur_user, **kwargs)
394 393 events.trigger(events.UserPostCreate(user_data))
395 394 return new_user
396 395 except (DatabaseError,):
@@ -423,9 +422,7 b' class UserModel(BaseModel):'
423 422 }
424 423 notification_type = EmailNotificationModel.TYPE_REGISTRATION
425 424 # pre-generate the subject for notification itself
426 (subject,
427 _h, _e, # we don't care about those
428 body_plaintext) = EmailNotificationModel().render_email(
425 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
429 426 notification_type, **kwargs)
430 427
431 428 # create notification objects, and emails
@@ -569,7 +566,7 b' class UserModel(BaseModel):'
569 566 def delete(self, user, cur_user=None, handle_repos=None,
570 567 handle_repo_groups=None, handle_user_groups=None,
571 568 handle_pull_requests=None, handle_artifacts=None, handle_new_owner=None):
572 from rhodecode.lib.hooks_base import log_delete_user
569 from rhodecode.lib import hooks_base
573 570
574 571 if not cur_user:
575 572 cur_user = getattr(get_current_rhodecode_user(), 'username', None)
@@ -638,7 +635,7 b' class UserModel(BaseModel):'
638 635 self.sa.expire(user)
639 636 self.sa.delete(user)
640 637
641 log_delete_user(deleted_by=cur_user, **user_data)
638 hooks_base.delete_user(deleted_by=cur_user, **user_data)
642 639 except Exception:
643 640 log.error(traceback.format_exc())
644 641 raise
@@ -660,8 +657,7 b' class UserModel(BaseModel):'
660 657 'first_admin_email': User.get_first_super_admin().email
661 658 }
662 659
663 (subject, headers, email_body,
664 email_body_plaintext) = EmailNotificationModel().render_email(
660 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
665 661 EmailNotificationModel.TYPE_PASSWORD_RESET, **email_kwargs)
666 662
667 663 recipients = [user_email]
@@ -719,8 +715,7 b' class UserModel(BaseModel):'
719 715 'first_admin_email': User.get_first_super_admin().email
720 716 }
721 717
722 (subject, headers, email_body,
723 email_body_plaintext) = EmailNotificationModel().render_email(
718 (subject, email_body, email_body_plaintext) = EmailNotificationModel().render_email(
724 719 EmailNotificationModel.TYPE_PASSWORD_RESET_CONFIRMATION,
725 720 **email_kwargs)
726 721
@@ -53,7 +53,8 b' def deferred_can_write_to_group_validato'
53 53 # permissions denied we expose as not existing, to prevent
54 54 # resource discovery
55 55 'permission_denied_parent_group':
56 _(u"Parent repository group `{}` does not exist"),
56 _(u"You do not have the permissions to store "
57 u"repository groups inside repository group `{}`"),
57 58 'permission_denied_root':
58 59 _(u"You do not have the permission to store "
59 60 u"repository groups in the root location.")
@@ -100,9 +101,15 b' def deferred_can_write_to_group_validato'
100 101 # we want to allow this...
101 102 forbidden = not (group_admin or (group_write and create_on_write and 0))
102 103
104 old_name = old_values.get('group_name')
105 if old_name and old_name == old_values.get('submitted_repo_group_name'):
106 # we're editing a repository group, we didn't change the name
107 # we skip the check for write into parent group now
108 # this allows changing settings for this repo group
109 return
110
103 111 if parent_group and forbidden:
104 msg = messages['permission_denied_parent_group'].format(
105 parent_group_name)
112 msg = messages['permission_denied_parent_group'].format(parent_group_name)
106 113 raise colander.Invalid(node, msg)
107 114
108 115 return can_write_group_validator
@@ -248,6 +255,9 b' class RepoGroupSchema(colander.Schema):'
248 255 validated_name = appstruct['repo_group_name']
249 256
250 257 # second pass to validate permissions to repo_group
258 if 'old_values' in self.bindings:
259 # save current repo name for name change checks
260 self.bindings['old_values']['submitted_repo_group_name'] = validated_name
251 261 second = RepoGroupAccessSchema().bind(**self.bindings)
252 262 appstruct_second = second.deserialize({'repo_group': validated_name})
253 263 # save result
@@ -286,6 +296,9 b' class RepoGroupSettingsSchema(RepoGroupS'
286 296 validated_name = separator.join([group.group_name, validated_name])
287 297
288 298 # second pass to validate permissions to repo_group
299 if 'old_values' in self.bindings:
300 # save current repo name for name change checks
301 self.bindings['old_values']['submitted_repo_group_name'] = validated_name
289 302 second = RepoGroupAccessSchema().bind(**self.bindings)
290 303 appstruct_second = second.deserialize({'repo_group': validated_name})
291 304 # save result
@@ -141,17 +141,23 b' def deferred_can_write_to_group_validato'
141 141
142 142 is_root_location = value is types.RootLocation
143 143 # NOT initialized validators, we must call them
144 can_create_repos_at_root = HasPermissionAny(
145 'hg.admin', 'hg.create.repository')
144 can_create_repos_at_root = HasPermissionAny('hg.admin', 'hg.create.repository')
146 145
147 146 # if values is root location, we simply need to check if we can write
148 147 # to root location !
149 148 if is_root_location:
149
150 150 if can_create_repos_at_root(user=request_user):
151 151 # we can create repo group inside tool-level. No more checks
152 152 # are required
153 153 return
154 154 else:
155 old_name = old_values.get('repo_name')
156 if old_name and old_name == old_values.get('submitted_repo_name'):
157 # since we didn't change the name, we can skip validation and
158 # allow current users without store-in-root permissions to update
159 return
160
155 161 # "fake" node name as repo_name, otherwise we oddly report
156 162 # the error as if it was coming form repo_group
157 163 # however repo_group is empty when using root location.
@@ -372,6 +378,9 b' class RepoSchema(colander.MappingSchema)'
372 378 validated_name = appstruct['repo_name']
373 379
374 380 # second pass to validate permissions to repo_group
381 if 'old_values' in self.bindings:
382 # save current repo name for name change checks
383 self.bindings['old_values']['submitted_repo_name'] = validated_name
375 384 second = RepoGroupAccessSchema().bind(**self.bindings)
376 385 appstruct_second = second.deserialize({'repo_group': validated_name})
377 386 # save result
@@ -429,6 +438,9 b' class RepoSettingsSchema(RepoSchema):'
429 438 validated_name = separator.join([group.group_name, validated_name])
430 439
431 440 # second pass to validate permissions to repo_group
441 if 'old_values' in self.bindings:
442 # save current repo name for name change checks
443 self.bindings['old_values']['submitted_repo_name'] = validated_name
432 444 second = RepoGroupAccessSchema().bind(**self.bindings)
433 445 appstruct_second = second.deserialize({'repo_group': validated_name})
434 446 # save result
@@ -259,21 +259,34 b' input[type="button"] {'
259 259 &:not(.open) .btn-action-switcher-container {
260 260 display: none;
261 261 }
262
263 .btn-more-option {
264 margin-left: -1px;
265 padding-left: 2px;
266 padding-right: 2px;
267 border-left: 1px solid @grey3;
268 }
262 269 }
263 270
264 271
265 .btn-action-switcher-container{
272 .btn-action-switcher-container {
266 273 position: absolute;
267 top: 30px;
268 left: -82px;
274 top: 100%;
275
276 &.left-align {
277 left: 0;
278 }
279 &.right-align {
280 right: 0;
281 }
282
269 283 }
270 284
271 285 .btn-action-switcher {
272 286 display: block;
273 287 position: relative;
274 288 z-index: 300;
275 min-width: 240px;
276 max-width: 500px;
289 max-width: 600px;
277 290 margin-top: 4px;
278 291 margin-bottom: 24px;
279 292 font-size: 14px;
@@ -283,6 +296,7 b' input[type="button"] {'
283 296 border: 1px solid @grey4;
284 297 border-radius: 3px;
285 298 box-shadow: @dropdown-shadow;
299 overflow: auto;
286 300
287 301 li {
288 302 display: block;
@@ -998,6 +998,21 b' input.filediff-collapse-state {'
998 998
999 999 /**** END COMMENTS ****/
1000 1000
1001
1002 .nav-chunk {
1003 position: absolute;
1004 right: 20px;
1005 margin-top: -17px;
1006 }
1007
1008 .nav-chunk.selected {
1009 visibility: visible !important;
1010 }
1011
1012 #diff_nav {
1013 color: @grey3;
1014 }
1015
1001 1016 }
1002 1017
1003 1018
@@ -1063,6 +1078,10 b' input.filediff-collapse-state {'
1063 1078 background: @color5;
1064 1079 color: white;
1065 1080 }
1081 &[op="comments"] { /* comments on file */
1082 background: @grey4;
1083 color: white;
1084 }
1066 1085 }
1067 1086 }
1068 1087
@@ -65,7 +65,7 b' tr.inline-comments div {'
65 65 float: left;
66 66
67 67 padding: 0.4em 0.4em;
68 margin: 3px 5px 0px -10px;
68 margin: 2px 4px 0px 0px;
69 69 display: inline-block;
70 70 min-height: 0;
71 71
@@ -76,12 +76,13 b' tr.inline-comments div {'
76 76 font-family: @text-italic;
77 77 font-style: italic;
78 78 background: #fff none;
79 color: @grey4;
79 color: @grey3;
80 80 border: 1px solid @grey4;
81 81 white-space: nowrap;
82 82
83 83 text-transform: uppercase;
84 min-width: 40px;
84 min-width: 50px;
85 border-radius: 4px;
85 86
86 87 &.todo {
87 88 color: @color5;
@@ -253,12 +254,10 b' tr.inline-comments div {'
253 254 }
254 255
255 256 .pr-version {
256 float: left;
257 margin: 0px 4px;
257 display: inline-block;
258 258 }
259 259 .pr-version-inline {
260 float: left;
261 margin: 0px 4px;
260 display: inline-block;
262 261 }
263 262 .pr-version-num {
264 263 font-size: 10px;
@@ -447,6 +446,13 b' form.comment-form {'
447 446 }
448 447 }
449 448
449 .comment-version-select {
450 margin: 0px;
451 border-radius: inherit;
452 border-color: @grey6;
453 height: 20px;
454 }
455
450 456 .comment-type {
451 457 margin: 0px;
452 458 border-radius: inherit;
@@ -97,6 +97,11 b' input + .action-link, .action-link.first'
97 97 border-left: none;
98 98 }
99 99
100 .link-disabled {
101 color: @grey4;
102 cursor: default;
103 }
104
100 105 .action-link.last{
101 106 margin-right: @padding;
102 107 padding-right: @padding;
@@ -148,6 +148,38 b' select.select2{height:28px;visibility:hi'
148 148 margin: 0;
149 149 }
150 150
151
152 .drop-menu-comment-history {
153 .drop-menu-core;
154 border: none;
155 padding: 0 6px 0 0;
156 width: auto;
157 min-width: 0;
158 margin: 0;
159 position: relative;
160 display: inline-block;
161 line-height: 1em;
162 z-index: 2;
163 cursor: pointer;
164
165 a {
166 display:block;
167 padding: 0;
168 position: relative;
169
170 &:after {
171 position: absolute;
172 content: "\00A0\25BE";
173 right: -0.80em;
174 line-height: 1em;
175 top: -0.20em;
176 width: 1em;
177 font-size: 16px;
178 }
179 }
180
181 }
182
151 183 .field-sm .drop-menu {
152 184 padding: 1px 0 0 0;
153 185 a {
@@ -33,6 +33,12 b' table.dataTable {'
33 33 .rc-user {
34 34 white-space: nowrap;
35 35 }
36 .user-perm-duplicate {
37 color: @grey4;
38 a {
39 color: @grey4;
40 }
41 }
36 42 }
37 43
38 44 .td-email {
@@ -37,6 +37,10 b''
37 37 &:hover {
38 38 border-color: @grey4;
39 39 }
40
41 &.authortag {
42 padding: 2px;
43 }
40 44 }
41 45
42 46 .tag0 { .border ( @border-thickness-tags, @grey4 ); color:@grey4; }
@@ -185,8 +185,10 b' function registerRCRoutes() {'
185 185 pyroutes.register('repo_commit_data', '/%(repo_name)s/changeset-data/%(commit_id)s', ['repo_name', 'commit_id']);
186 186 pyroutes.register('repo_commit_comment_create', '/%(repo_name)s/changeset/%(commit_id)s/comment/create', ['repo_name', 'commit_id']);
187 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 189 pyroutes.register('repo_commit_comment_attachment_upload', '/%(repo_name)s/changeset/%(commit_id)s/comment/attachment_upload', ['repo_name', 'commit_id']);
189 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 192 pyroutes.register('repo_commit_raw_deprecated', '/%(repo_name)s/raw-changeset/%(commit_id)s', ['repo_name', 'commit_id']);
191 193 pyroutes.register('repo_archivefile', '/%(repo_name)s/archive/%(fname)s', ['repo_name', 'fname']);
192 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 244 pyroutes.register('pullrequest_merge', '/%(repo_name)s/pull-request/%(pull_request_id)s/merge', ['repo_name', 'pull_request_id']);
243 245 pyroutes.register('pullrequest_delete', '/%(repo_name)s/pull-request/%(pull_request_id)s/delete', ['repo_name', 'pull_request_id']);
244 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 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 249 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
247 250 pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']);
@@ -9,6 +9,7 b''
9 9 margin: 0;
10 10 float: right;
11 11 cursor: pointer;
12 padding: 8px 0 8px 8px;
12 13 }
13 14
14 15 .toast-message-holder{
@@ -80,9 +80,10 b' var _submitAjaxPOST = function(url, post'
80 80 })(function() {
81 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 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 89 // bind the element instance to our Form
@@ -126,10 +127,20 b' var _submitAjaxPOST = function(url, post'
126 127 this.submitButton = $(this.submitForm).find('input[type="submit"]');
127 128 this.submitButtonText = this.submitButton.val();
128 129
130
129 131 this.previewUrl = pyroutes.url('repo_commit_comment_preview',
130 132 {'repo_name': templateContext.repo_name,
131 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 '';
141 $(editInfo).insertBefore($(this.editButton).parent());
142 }
143
133 144 if (resolvesCommentId){
134 145 this.resolvesId = '#resolve_comment_{0}'.format(resolvesCommentId);
135 146 this.resolvesActionId = '#resolve_comment_action_{0}'.format(resolvesCommentId);
@@ -153,17 +164,27 b' var _submitAjaxPOST = function(url, post'
153 164 // based on commitId, or pullRequestId decide where do we submit
154 165 // out data
155 166 if (this.commitId){
156 this.submitUrl = pyroutes.url('repo_commit_comment_create',
167 var pyurl = 'repo_commit_comment_create';
168 if(edit){
169 pyurl = 'repo_commit_comment_edit';
170 }
171 this.submitUrl = pyroutes.url(pyurl,
157 172 {'repo_name': templateContext.repo_name,
158 'commit_id': this.commitId});
173 'commit_id': this.commitId,
174 'comment_id': comment_id});
159 175 this.selfUrl = pyroutes.url('repo_commit',
160 176 {'repo_name': templateContext.repo_name,
161 177 'commit_id': this.commitId});
162 178
163 179 } else if (this.pullRequestId) {
164 this.submitUrl = pyroutes.url('pullrequest_comment_create',
180 var pyurl = 'pullrequest_comment_create';
181 if(edit){
182 pyurl = 'pullrequest_comment_edit';
183 }
184 this.submitUrl = pyroutes.url(pyurl,
165 185 {'repo_name': templateContext.repo_name,
166 'pull_request_id': this.pullRequestId});
186 'pull_request_id': this.pullRequestId,
187 'comment_id': comment_id});
167 188 this.selfUrl = pyroutes.url('pullrequest_show',
168 189 {'repo_name': templateContext.repo_name,
169 190 'pull_request_id': this.pullRequestId});
@@ -277,7 +298,7 b' var _submitAjaxPOST = function(url, post'
277 298 this.globalSubmitSuccessCallback = function(){
278 299 // default behaviour is to call GLOBAL hook, if it's registered.
279 300 if (window.commentFormGlobalSubmitSuccessCallback !== undefined){
280 commentFormGlobalSubmitSuccessCallback()
301 commentFormGlobalSubmitSuccessCallback();
281 302 }
282 303 };
283 304
@@ -475,18 +496,97 b' var _submitAjaxPOST = function(url, post'
475 496 return CommentForm;
476 497 });
477 498
499 /* selector for comment versions */
500 var initVersionSelector = function(selector, initialData) {
501
502 var formatResult = function(result, container, query, escapeMarkup) {
503
504 return renderTemplate('commentVersion', {
505 show_disabled: true,
506 version: result.comment_version,
507 user_name: result.comment_author_username,
508 gravatar_url: result.comment_author_gravatar,
509 size: 16,
510 timeago_component: result.comment_created_on,
511 })
512 };
513
514 $(selector).select2({
515 placeholder: "Edited",
516 containerCssClass: "drop-menu-comment-history",
517 dropdownCssClass: "drop-menu-dropdown",
518 dropdownAutoWidth: true,
519 minimumResultsForSearch: -1,
520 data: initialData,
521 formatResult: formatResult,
522 });
523
524 $(selector).on('select2-selecting', function (e) {
525 // hide the mast as we later do preventDefault()
526 $("#select2-drop-mask").click();
527 e.preventDefault();
528 e.choice.action();
529 });
530
531 $(selector).on("select2-open", function() {
532 timeagoActivate();
533 });
534 };
535
478 536 /* comments controller */
479 537 var CommentsController = function() {
480 538 var mainComment = '#text';
481 539 var self = this;
482 540
483 this.cancelComment = function(node) {
541 this.cancelComment = function (node) {
484 542 var $node = $(node);
485 var $td = $node.closest('td');
543 var edit = $(this).attr('edit');
544 if (edit) {
545 var $general_comments = null;
546 var $inline_comments = $node.closest('div.inline-comments');
547 if (!$inline_comments.length) {
548 $general_comments = $('#comments');
549 var $comment = $general_comments.parent().find('div.comment:hidden');
550 // show hidden general comment form
551 $('#cb-comment-general-form-placeholder').show();
552 } else {
553 var $comment = $inline_comments.find('div.comment:hidden');
554 }
555 $comment.show();
556 }
486 557 $node.closest('.comment-inline-form').remove();
487 558 return false;
488 559 };
489 560
561 this.showVersion = function (comment_id, comment_history_id) {
562
563 var historyViewUrl = pyroutes.url(
564 'repo_commit_comment_history_view',
565 {
566 'repo_name': templateContext.repo_name,
567 'commit_id': comment_id,
568 'comment_history_id': comment_history_id,
569 }
570 );
571 successRenderCommit = function (data) {
572 SwalNoAnimation.fire({
573 html: data,
574 title: '',
575 });
576 };
577 failRenderCommit = function () {
578 SwalNoAnimation.fire({
579 html: 'Error while loading comment history',
580 title: '',
581 });
582 };
583 _submitAjaxPOST(
584 historyViewUrl, {'csrf_token': CSRF_TOKEN},
585 successRenderCommit,
586 failRenderCommit
587 );
588 };
589
490 590 this.getLineNumber = function(node) {
491 591 var $node = $(node);
492 592 var lineNo = $node.closest('td').attr('data-line-no');
@@ -638,12 +738,12 b' var CommentsController = function() {'
638 738 $node.closest('tr').toggleClass('hide-line-comments');
639 739 };
640 740
641 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId){
741 this.createCommentForm = function(formElement, lineno, placeholderText, initAutocompleteActions, resolvesCommentId, edit, comment_id){
642 742 var pullRequestId = templateContext.pull_request_data.pull_request_id;
643 743 var commitId = templateContext.commit_data.commit_id;
644 744
645 745 var commentForm = new CommentForm(
646 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId);
746 formElement, commitId, pullRequestId, lineno, initAutocompleteActions, resolvesCommentId, edit, comment_id);
647 747 var cm = commentForm.getCmInstance();
648 748
649 749 if (resolvesCommentId){
@@ -780,18 +880,234 b' var CommentsController = function() {'
780 880
781 881 var _form = $($form[0]);
782 882 var autocompleteActions = ['approve', 'reject', 'as_note', 'as_todo'];
883 var edit = false;
884 var comment_id = null;
783 885 var commentForm = this.createCommentForm(
784 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId);
886 _form, lineNo, placeholderText, autocompleteActions, resolvesCommentId, edit, comment_id);
785 887 commentForm.initStatusChangeSelector();
786 888
787 889 return commentForm;
788 890 };
789 891
892 this.editComment = function(node) {
893 var $node = $(node);
894 var $comment = $(node).closest('.comment');
895 var comment_id = $comment.attr('data-comment-id');
896 var $form = null
897
898 var $comments = $node.closest('div.inline-comments');
899 var $general_comments = null;
900 var lineno = null;
901
902 if($comments.length){
903 // inline comments setup
904 $form = $comments.find('.comment-inline-form');
905 lineno = self.getLineNumber(node)
906 }
907 else{
908 // general comments setup
909 $comments = $('#comments');
910 $form = $comments.find('.comment-inline-form');
911 lineno = $comment[0].id
912 $('#cb-comment-general-form-placeholder').hide();
913 }
914
915 this.edit = true;
916
917 if (!$form.length) {
918
919 var $filediff = $node.closest('.filediff');
920 $filediff.removeClass('hide-comments');
921 var f_path = $filediff.attr('data-f-path');
922
923 // create a new HTML from template
924
925 var tmpl = $('#cb-comment-inline-form-template').html();
926 tmpl = tmpl.format(escapeHtml(f_path), lineno);
927 $form = $(tmpl);
928 $comment.after($form)
929
930 var _form = $($form[0]).find('form');
931 var autocompleteActions = ['as_note',];
932 var commentForm = this.createCommentForm(
933 _form, lineno, '', autocompleteActions, resolvesCommentId,
934 this.edit, comment_id);
935 var old_comment_text_binary = $comment.attr('data-comment-text');
936 var old_comment_text = b64DecodeUnicode(old_comment_text_binary);
937 commentForm.cm.setValue(old_comment_text);
938 $comment.hide();
939
940 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
941 form: _form,
942 parent: $comments,
943 lineno: lineno,
944 f_path: f_path}
945 );
946
947 // set a CUSTOM submit handler for inline comments.
948 commentForm.setHandleFormSubmit(function(o) {
949 var text = commentForm.cm.getValue();
950 var commentType = commentForm.getCommentType();
951
952 if (text === "") {
953 return;
954 }
955
956 if (old_comment_text == text) {
957 SwalNoAnimation.fire({
958 title: 'Unable to edit comment',
959 html: _gettext('Comment body was not changed.'),
960 });
961 return;
962 }
963 var excludeCancelBtn = false;
964 var submitEvent = true;
965 commentForm.setActionButtonsDisabled(true, excludeCancelBtn, submitEvent);
966 commentForm.cm.setOption("readOnly", true);
967
968 // Read last version known
969 var versionSelector = $('#comment_versions_{0}'.format(comment_id));
970 var version = versionSelector.data('lastVersion');
971
972 if (!version) {
973 version = 0;
974 }
975
976 var postData = {
977 'text': text,
978 'f_path': f_path,
979 'line': lineno,
980 'comment_type': commentType,
981 'version': version,
982 'csrf_token': CSRF_TOKEN
983 };
984
985 var submitSuccessCallback = function(json_data) {
986 $form.remove();
987 $comment.show();
988 var postData = {
989 'text': text,
990 'renderer': $comment.attr('data-comment-renderer'),
991 'csrf_token': CSRF_TOKEN
992 };
993
994 /* Inject new edited version selector */
995 var updateCommentVersionDropDown = function () {
996 var versionSelectId = '#comment_versions_'+comment_id;
997 var preLoadVersionData = [
998 {
999 id: json_data['comment_version'],
1000 text: "v{0}".format(json_data['comment_version']),
1001 action: function () {
1002 Rhodecode.comments.showVersion(
1003 json_data['comment_id'],
1004 json_data['comment_history_id']
1005 )
1006 },
1007 comment_version: json_data['comment_version'],
1008 comment_author_username: json_data['comment_author_username'],
1009 comment_author_gravatar: json_data['comment_author_gravatar'],
1010 comment_created_on: json_data['comment_created_on'],
1011 },
1012 ]
1013
1014
1015 if ($(versionSelectId).data('select2')) {
1016 var oldData = $(versionSelectId).data('select2').opts.data.results;
1017 $(versionSelectId).select2("destroy");
1018 preLoadVersionData = oldData.concat(preLoadVersionData)
1019 }
1020
1021 initVersionSelector(versionSelectId, {results: preLoadVersionData});
1022
1023 $comment.attr('data-comment-text', utf8ToB64(text));
1024
1025 var versionSelector = $('#comment_versions_'+comment_id);
1026
1027 // set lastVersion so we know our last edit version
1028 versionSelector.data('lastVersion', json_data['comment_version'])
1029 versionSelector.parent().show();
1030 }
1031 updateCommentVersionDropDown();
1032
1033 // by default we reset state of comment preserving the text
1034 var failRenderCommit = function(jqXHR, textStatus, errorThrown) {
1035 var prefix = "Error while editing this comment.\n"
1036 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1037 ajaxErrorSwal(message);
1038 };
1039
1040 var successRenderCommit = function(o){
1041 $comment.show();
1042 $comment[0].lastElementChild.innerHTML = o;
1043 };
1044
1045 var previewUrl = pyroutes.url(
1046 'repo_commit_comment_preview',
1047 {'repo_name': templateContext.repo_name,
1048 'commit_id': templateContext.commit_data.commit_id});
1049
1050 _submitAjaxPOST(
1051 previewUrl, postData, successRenderCommit,
1052 failRenderCommit
1053 );
1054
1055 try {
1056 var html = json_data.rendered_text;
1057 var lineno = json_data.line_no;
1058 var target_id = json_data.target_id;
1059
1060 $comments.find('.cb-comment-add-button').before(html);
1061
1062 // run global callback on submit
1063 commentForm.globalSubmitSuccessCallback();
1064
1065 } catch (e) {
1066 console.error(e);
1067 }
1068
1069 // re trigger the linkification of next/prev navigation
1070 linkifyComments($('.inline-comment-injected'));
1071 timeagoActivate();
1072 tooltipActivate();
1073
1074 if (window.updateSticky !== undefined) {
1075 // potentially our comments change the active window size, so we
1076 // notify sticky elements
1077 updateSticky()
1078 }
1079
1080 commentForm.setActionButtonsDisabled(false);
1081
1082 };
1083 var submitFailCallback = function(jqXHR, textStatus, errorThrown) {
1084 var prefix = "Error while editing comment.\n"
1085 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
1086 if (jqXHR.status == 409){
1087 message = 'This comment was probably changed somewhere else. Please reload the content of this comment.'
1088 ajaxErrorSwal(message, 'Comment version mismatch.');
1089 } else {
1090 ajaxErrorSwal(message);
1091 }
1092
1093 commentForm.resetCommentFormState(text)
1094 };
1095 commentForm.submitAjaxPOST(
1096 commentForm.submitUrl, postData,
1097 submitSuccessCallback,
1098 submitFailCallback);
1099 });
1100 }
1101
1102 $form.addClass('comment-inline-form-open');
1103 };
1104
790 1105 this.createComment = function(node, resolutionComment) {
791 1106 var resolvesCommentId = resolutionComment || null;
792 1107 var $node = $(node);
793 1108 var $td = $node.closest('td');
794 1109 var $form = $td.find('.comment-inline-form');
1110 this.edit = false;
795 1111
796 1112 if (!$form.length) {
797 1113
@@ -816,8 +1132,9 b' var CommentsController = function() {'
816 1132 var placeholderText = _gettext('Leave a comment on line {0}.').format(lineno);
817 1133 var _form = $($form[0]).find('form');
818 1134 var autocompleteActions = ['as_note', 'as_todo'];
1135 var comment_id=null;
819 1136 var commentForm = this.createCommentForm(
820 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId);
1137 _form, lineno, placeholderText, autocompleteActions, resolvesCommentId, this.edit, comment_id);
821 1138
822 1139 $.Topic('/ui/plugins/code/comment_form_built').prepareOrPublish({
823 1140 form: _form,
@@ -70,7 +70,7 b" replacing '-' and '_' into spaces"
70 70 * @param limit
71 71 * @returns {*[]}
72 72 */
73 var getTitleAndDescription = function(sourceRef, elements, limit) {
73 var getTitleAndDescription = function(sourceRefType, sourceRef, elements, limit) {
74 74 var title = '';
75 75 var desc = '';
76 76
@@ -85,7 +85,9 b' var getTitleAndDescription = function(so'
85 85 }
86 86 else {
87 87 // use reference name
88 title = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter();
88 var normalizedRef = sourceRef.replace(/-/g, ' ').replace(/_/g, ' ').capitalizeFirstLetter()
89 var refType = sourceRefType;
90 title = 'Changes from {0}: {1}'.format(refType, normalizedRef);
89 91 }
90 92
91 93 return [title, desc]
@@ -130,10 +130,13 b' function formatErrorMessage(jqXHR, textS'
130 130 }
131 131 }
132 132
133 function ajaxErrorSwal(message) {
133 function ajaxErrorSwal(message, title) {
134
135 var title = (typeof title !== 'undefined') ? title : _gettext('Ajax Request Error');
136
134 137 SwalNoAnimation.fire({
135 138 icon: 'error',
136 title: _gettext('Ajax Request Error'),
139 title: title,
137 140 html: '<span style="white-space: pre-line">{0}</span>'.format(message),
138 141 showClass: {
139 142 popup: 'swal2-noanimation',
@@ -182,3 +182,13 b' var htmlEnDeCode = (function() {'
182 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 }
191
192 function utf8ToB64( str ) {
193 return window.btoa(unescape(encodeURIComponent( str )));
194 } No newline at end of file
@@ -11,9 +11,15 b''
11 11 <div class="panel-body">
12 12 <div class="apikeys_wrap">
13 13 <p>
14 ${_('Authentication tokens can be used to interact with the API, or VCS-over-http. '
15 'Each token can have a role. Token with a role can be used only in given context, '
16 'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
14 ${_('Available roles')}:
15 <ul>
16 % for role in h.UserApiKeys.ROLES:
17 <li>
18 <span class="tag disabled">${h.UserApiKeys._get_role_name(role)}</span>
19 <span>${h.UserApiKeys._get_role_description(role) |n}</span>
20 </li>
21 % endfor
22 </ul>
17 23 </p>
18 24 <table class="rctable auth_tokens">
19 25 <tr>
@@ -36,7 +42,7 b''
36 42 </td>
37 43 <td class="td-wrap">${auth_token.description}</td>
38 44 <td class="td-tags">
39 <span class="tag disabled">${auth_token.role_humanized}</span>
45 <span class="tooltip tag disabled" title="${h.UserApiKeys._get_role_description(auth_token.role)}">${auth_token.role_humanized}</span>
40 46 </td>
41 47 <td class="td">${auth_token.scope_humanized}</td>
42 48 <td class="td-exp">
@@ -44,7 +44,12 b''
44 44 <label for="group_parent_id">${_('Repository group')}:</label>
45 45 </div>
46 46 <div class="select">
47 ${h.select('group_parent_id',request.GET.get('parent_group'),c.repo_groups,class_="medium")}
47 ${h.select('group_parent_id', request.GET.get('parent_group'),c.repo_groups,class_="medium")}
48 % if c.personal_repo_group:
49 <a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}">
50 ${_('Select my personal group ({})').format(c.personal_repo_group.group_name)}
51 </a>
52 % endif
48 53 </div>
49 54 </div>
50 55
@@ -106,6 +111,12 b''
106 111 setCopyPermsOption(e.val)
107 112 });
108 113 $('#group_name').focus();
114
115 $('#select_my_group').on('click', function(e){
116 e.preventDefault();
117 $("#group_parent_id").val($(this).data('personalGroupId')).trigger("change");
118 })
119
109 120 })
110 121 </script>
111 122 </%def>
@@ -68,10 +68,15 b''
68 68 <span class="user-perm-help-text"> - ${_('permission for other logged in users')}</span>
69 69 % endif
70 70 % else:
71 ${h.link_to_user(_user.username)}
72 %if getattr(_user, 'duplicate_perm', None):
73 (${_('inactive duplicate')})
74 %endif
71 % if getattr(_user, 'duplicate_perm', None):
72 <span class="user-perm-duplicate">
73 ${h.link_to_user(_user.username)}
74 <span class="tooltip" title="${_('This entry is a duplicate, most probably left-over from previously set permission. This user has a higher permission set, so this entry is inactive. Please revoke this permission manually.')}">(${_('inactive duplicate')})
75 </span>
76 </span>
77 % else:
78 ${h.link_to_user(_user.username)}
79 % endif
75 80 % endif
76 81 </span>
77 82 </td>
@@ -116,10 +121,15 b''
116 121 <span class="user-perm-help-text"> - ${_('permission for other logged in users')}</span>
117 122 % endif
118 123 % else:
119 ${h.link_to_user(_user.username)}
120 %if getattr(_user, 'duplicate_perm', None):
121 (${_('inactive duplicate')})
122 %endif
124 % if getattr(_user, 'duplicate_perm', None):
125 <span class="user-perm-duplicate">
126 ${h.link_to_user(_user.username)}
127 <span class="tooltip" title="${_('This entry is a duplicate, most probably left-over from previously set permission. This user has a higher permission set, so this entry is inactive. Please revoke this permission manually.')}">(${_('inactive duplicate')})
128 </span>
129 </span>
130 % else:
131 ${h.link_to_user(_user.username)}
132 % endif
123 133 % endif
124 134 <span class="user-perm-help-text">(${_('delegated admin')})</span>
125 135 </span>
@@ -46,7 +46,7 b''
46 46 ${h.select('repo_group',request.GET.get('parent_group'),c.repo_groups,class_="medium")}
47 47 % if c.personal_repo_group:
48 48 <a class="btn" href="#" id="select_my_group" data-personal-group-id="${c.personal_repo_group.group_id}">
49 ${_('Select my personal group (%(repo_group_name)s)') % {'repo_group_name': c.personal_repo_group.group_name}}
49 ${_('Select my personal group ({})').format(c.personal_repo_group.group_name)}
50 50 </a>
51 51 % endif
52 52 <span class="help-block">${_('Optionally select a group to put this repository into.')}</span>
@@ -167,11 +167,16 b''
167 167 <div style="margin: 0 0 20px 0" class="fake-space"></div>
168 168
169 169 <div class="field">
170 % if c.rhodecode_db_repo.archived:
171 This repository is already archived. Only super-admin users can un-archive this repository.
172 % else:
170 173 <button class="btn btn-small btn-warning" type="submit"
171 174 onclick="submitConfirm(event, this, _gettext('Confirm to archive this repository'), _gettext('Archive'), '${c.rhodecode_db_repo.repo_name}')"
172 175 >
173 176 ${_('Archive this repository')}
174 177 </button>
178 % endif
179
175 180 </div>
176 181 <div class="field">
177 182 <span class="help-block">
@@ -94,10 +94,16 b''
94 94 <span class="user-perm-help-text"> - ${_('permission for other logged in users')}</span>
95 95 % endif
96 96 % else:
97 ${h.link_to_user(_user.username)}
98 %if getattr(_user, 'duplicate_perm', None):
99 (${_('inactive duplicate')})
100 %endif
97 % if getattr(_user, 'duplicate_perm', None):
98 <span class="user-perm-duplicate">
99 ${h.link_to_user(_user.username)}
100 <span class="tooltip" title="${_('This entry is a duplicate, most probably left-over from previously set permission. This user has a higher permission set, so this entry is inactive. Please revoke this permission manually.')}">(${_('inactive duplicate')})
101 </span>
102 </span>
103 % else:
104 ${h.link_to_user(_user.username)}
105 % endif
106
101 107 %if getattr(_user, 'branch_rules', None):
102 108 % if used_by_n_rules == 1:
103 109 (${_('used by {} branch rule, requires write+ permissions').format(used_by_n_rules)})
@@ -74,10 +74,15 b''
74 74 <span class="user-perm-help-text"> - ${_('permission for other logged in users')}</span>
75 75 % endif
76 76 % else:
77 ${h.link_to_user(_user.username)}
78 %if getattr(_user, 'duplicate_perm', None):
79 (${_('inactive duplicate')})
80 %endif
77 % if getattr(_user, 'duplicate_perm', None):
78 <span class="user-perm-duplicate">
79 ${h.link_to_user(_user.username)}
80 <span class="tooltip" title="${_('This entry is a duplicate, most probably left-over from previously set permission. This user has a higher permission set, so this entry is inactive. Please revoke this permission manually.')}">(${_('inactive duplicate')})
81 </span>
82 </span>
83 % else:
84 ${h.link_to_user(_user.username)}
85 % endif
81 86 % endif
82 87 </span>
83 88 </td>
@@ -122,10 +127,15 b''
122 127 <span class="user-perm-help-text"> - ${_('permission for other logged in users')}</span>
123 128 % endif
124 129 % else:
125 ${h.link_to_user(_user.username)}
126 %if getattr(_user, 'duplicate_perm', None):
127 (${_('inactive duplicate')})
128 %endif
130 % if getattr(_user, 'duplicate_perm', None):
131 <span class="user-perm-duplicate">
132 ${h.link_to_user(_user.username)}
133 <span class="tooltip" title="${_('This entry is a duplicate, most probably left-over from previously set permission. This user has a higher permission set, so this entry is inactive. Please revoke this permission manually.')}">(${_('inactive duplicate')})
134 </span>
135 </span>
136 % else:
137 ${h.link_to_user(_user.username)}
138 % endif
129 139 % endif
130 140 <span class="user-perm-help-text">(${_('delegated admin')})</span>
131 141 </span>
@@ -27,8 +27,8 b''
27 27 <%def name="main()">
28 28 <div class="box user_settings">
29 29 % if not c.user.active:
30 <div class="alert alert-warning text-center">
31 <strong>${_('This user is set as disabled')}</strong>
30 <div class="alert alert-warning text-center" style="margin: 0 0 15px 0">
31 <strong>${_('This user is set as non-active and disabled.')}</strong>
32 32 </div>
33 33 % endif
34 34
@@ -16,9 +16,15 b''
16 16 <div class="panel-body">
17 17 <div class="apikeys_wrap">
18 18 <p>
19 ${_('Authentication tokens can be used to interact with the API, or VCS-over-http. '
20 'Each token can have a role. Token with a role can be used only in given context, '
21 'e.g. VCS tokens can be used together with the authtoken auth plugin for git/hg/svn operations only.')}
19 ${_('Available roles')}:
20 <ul>
21 % for role in h.UserApiKeys.ROLES:
22 <li>
23 <span class="tag disabled">${h.UserApiKeys._get_role_name(role)}</span>
24 <span>${h.UserApiKeys._get_role_description(role) |n}</span>
25 </li>
26 % endfor
27 </ul>
22 28 </p>
23 29 <table class="rctable auth_tokens">
24 30 <tr>
@@ -41,7 +47,7 b''
41 47 </td>
42 48 <td class="td-wrap">${auth_token.description}</td>
43 49 <td class="td-tags">
44 <span class="tag disabled">${auth_token.role_humanized}</span>
50 <span class="tooltip tag disabled" title="${h.UserApiKeys._get_role_description(auth_token.role)}">${auth_token.role_humanized}</span>
45 51 </td>
46 52 <td class="td">${auth_token.scope_humanized}</td>
47 53 <td class="td-exp">
@@ -1,11 +1,7 b''
1 1 ## -*- coding: utf-8 -*-
2 2
3 3 <%!
4 ## base64 filter e.g ${ example | base64 }
5 def base64(text):
6 import base64
7 from rhodecode.lib.helpers import safe_str
8 return base64.encodestring(safe_str(text))
4 from rhodecode.lib import html_filters
9 5 %>
10 6
11 7 <%inherit file="root.mako"/>
@@ -247,7 +243,9 b''
247 243
248 244 <div class="${_class}">
249 245 ${self.gravatar(email, size, tooltip=tooltip, tooltip_alt=contact, user=rc_user)}
250 <span class="${('user user-disabled' if show_disabled else 'user')}"> ${h.link_to_user(rc_user or contact)}</span>
246 <span class="${('user user-disabled' if show_disabled else 'user')}">
247 ${h.link_to_user(rc_user or contact)}
248 </span>
251 249 </div>
252 250 </%def>
253 251
@@ -396,7 +394,7 b''
396 394 </a>
397 395 </li>
398 396
399 %if h.HasRepoPermissionAll('repository.admin')(c.repo_name):
397 %if not c.rhodecode_db_repo.archived and h.HasRepoPermissionAll('repository.admin')(c.repo_name):
400 398 <li class="${h.is_active('settings', active)}"><a class="menulink" href="${h.route_path('edit_repo',repo_name=c.repo_name)}"><div class="menulabel">${_('Repository Settings')}</div></a></li>
401 399 %endif
402 400
@@ -510,7 +508,7 b''
510 508 ## create action
511 509 <li>
512 510 <a href="#create-actions" onclick="return false;" class="menulink childs">
513 <i class="tooltip icon-plus-circled" title="${_('Create')}"></i>
511 <i class="icon-plus-circled"></i>
514 512 </a>
515 513
516 514 <div class="action-menu submenu">
@@ -1132,6 +1130,19 b''
1132 1130 };
1133 1131 ajaxPOST(url, postData, success, failure);
1134 1132 }
1133
1134 var hideLicenseWarning = function () {
1135 var fingerprint = templateContext.session_attrs.license_fingerprint;
1136 storeUserSessionAttr('rc_user_session_attr.hide_license_warning', fingerprint);
1137 $('#notifications').hide();
1138 }
1139
1140 var hideLicenseError = function () {
1141 var fingerprint = templateContext.session_attrs.license_fingerprint;
1142 storeUserSessionAttr('rc_user_session_attr.hide_license_error', fingerprint);
1143 $('#notifications').hide();
1144 }
1145
1135 1146 </script>
1136 1147 <script src="${h.asset('js/rhodecode/base/keyboard-bindings.js', ver=c.rhodecode_version_hash)}"></script>
1137 1148 </%def>
@@ -3,6 +3,7 b''
3 3 ## <%namespace name="dpb" file="/base/default_perms_box.mako"/>
4 4 ## ${dpb.default_perms_box(<url_to_form>)}
5 5 ## ${dpb.default_perms_radios()}
6 <%namespace name="base" file="/base/base.mako"/>
6 7
7 8 <%def name="default_perms_radios(global_permissions_template = False, suffix='', **kwargs)">
8 9 <div class="main-content-full-width">
@@ -11,10 +12,22 b''
11 12 ## displayed according to checkbox selection
12 13 <div class="panel-heading">
13 14 %if not global_permissions_template:
14 <h3 class="inherit_overlay_default panel-title">${_('Inherited Permissions')}</h3>
15 <h3 class="inherit_overlay panel-title">${_('Custom Permissions')}</h3>
15 <h3 class="inherit_overlay_default panel-title">
16 % if hasattr(c, 'user'):
17 ${base.gravatar_with_user(c.user.username, 16, tooltip=False, _class='pull-left')} &nbsp;-
18 % endif
19 ${_('Inherited Permissions')}
20 </h3>
21 <h3 class="inherit_overlay panel-title">
22 % if hasattr(c, 'user'):
23 ${base.gravatar_with_user(c.user.username, 16, tooltip=False, _class='pull-left')} &nbsp;-
24 % endif
25 ${_('Custom Permissions')}
26 </h3>
16 27 %else:
17 <h3 class="panel-title">${_('Default Global Permissions')}</h3>
28 <h3 class="panel-title">
29 ${_('Default Global Permissions')}
30 </h3>
18 31 %endif
19 32 </div>
20 33
@@ -2,14 +2,15 b''
2 2 ## usage:
3 3 ## <%namespace name="p" file="/base/perms_summary.mako"/>
4 4 ## ${p.perms_summary(c.perm_user.permissions)}
5 <%namespace name="base" file="/base/base.mako"/>
5 6
6 7 <%def name="perms_summary(permissions, show_all=False, actions=True, side_link=None)">
7 8 <% section_to_label = {
8 'global': 'Global Permissions',
9 'repository_branches': 'Repository Branch Rules',
10 'repositories': 'Repository Access Permissions',
11 'user_groups': 'User Group Permissions',
12 'repositories_groups': 'Repository Group Permissions',
9 'global': 'Global Permissions Summary',
10 'repository_branches': 'Repository Branch Rules Summary',
11 'repositories': 'Repository Access Permissions Summary',
12 'user_groups': 'User Group Permissions Summary',
13 'repositories_groups': 'Repository Group Permissions Summary',
13 14 } %>
14 15
15 16 <div id="perms" class="table fields">
@@ -18,7 +19,11 b''
18 19
19 20 <div class="panel panel-default">
20 21 <div class="panel-heading" id="${section.replace("_","-")}-permissions">
21 <h3 class="panel-title">${section_to_label.get(section, section)} - <span id="total_count_${section}"></span>
22 <h3 class="panel-title">
23 % if hasattr(c, 'user'):
24 ${base.gravatar_with_user(c.user.username, 16, tooltip=False, _class='pull-left')} &nbsp;-
25 % endif
26 ${section_to_label.get(section, section)} - <span id="total_count_${section}"></span>
22 27 <a class="permalink" href="#${section.replace("_","-")}-permissions"> ΒΆ</a>
23 28 </h3>
24 29 % if side_link:
@@ -158,7 +158,7 b''
158 158
159 159 <div class="cs_files">
160 160 <%namespace name="cbdiffs" file="/codeblocks/diffs.mako"/>
161 ${cbdiffs.render_diffset_menu(c.changes[c.commit.raw_id])}
161 ${cbdiffs.render_diffset_menu(c.changes[c.commit.raw_id], commit=c.commit)}
162 162 ${cbdiffs.render_diffset(
163 163 c.changes[c.commit.raw_id], commit=c.commit, use_comments=True,inline_comments=c.inline_comments )}
164 164 </div>
@@ -3,8 +3,12 b''
3 3 ## <%namespace name="comment" file="/changeset/changeset_file_comment.mako"/>
4 4 ## ${comment.comment_block(comment)}
5 5 ##
6
7 <%!
8 from rhodecode.lib import html_filters
9 %>
10
6 11 <%namespace name="base" file="/base/base.mako"/>
7
8 12 <%def name="comment_block(comment, inline=False, active_pattern_entries=None)">
9 13 <% pr_index_ver = comment.get_index_version(getattr(c, 'versions', [])) %>
10 14 <% latest_ver = len(getattr(c, 'versions', [])) %>
@@ -21,113 +25,170 b''
21 25 line="${comment.line_no}"
22 26 data-comment-id="${comment.comment_id}"
23 27 data-comment-type="${comment.comment_type}"
28 data-comment-renderer="${comment.renderer}"
29 data-comment-text="${comment.text | html_filters.base64,n}"
24 30 data-comment-line-no="${comment.line_no}"
25 31 data-comment-inline=${h.json.dumps(inline)}
26 32 style="${'display: none;' if outdated_at_ver else ''}">
27 33
28 34 <div class="meta">
29 35 <div class="comment-type-label">
30 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}" title="line: ${comment.line_no}">
36 <div class="comment-label ${comment.comment_type or 'note'}" id="comment-label-${comment.comment_id}">
37
38 ## TODO COMMENT
31 39 % if comment.comment_type == 'todo':
32 40 % if comment.resolved:
33 41 <div class="resolved tooltip" title="${_('Resolved by comment #{}').format(comment.resolved.comment_id)}">
42 <i class="icon-flag-filled"></i>
34 43 <a href="#comment-${comment.resolved.comment_id}">${comment.comment_type}</a>
35 44 </div>
36 45 % else:
37 46 <div class="resolved tooltip" style="display: none">
38 47 <span>${comment.comment_type}</span>
39 48 </div>
40 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to resolve this comment')}">
49 <div class="resolve tooltip" onclick="return Rhodecode.comments.createResolutionComment(${comment.comment_id});" title="${_('Click to create resolution comment.')}">
50 <i class="icon-flag-filled"></i>
41 51 ${comment.comment_type}
42 52 </div>
43 53 % endif
54 ## NOTE COMMENT
44 55 % else:
56 ## RESOLVED NOTE
45 57 % if comment.resolved_comment:
46 fix
47 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
48 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
49 </a>
58 <div class="tooltip" title="${_('This comment resolves TODO #{}').format(comment.resolved_comment.comment_id)}">
59 fix
60 <a href="#comment-${comment.resolved_comment.comment_id}" onclick="Rhodecode.comments.scrollToComment($('#comment-${comment.resolved_comment.comment_id}'), 0, ${h.json.dumps(comment.resolved_comment.outdated)})">
61 <span style="text-decoration: line-through">#${comment.resolved_comment.comment_id}</span>
62 </a>
63 </div>
64 ## STATUS CHANGE NOTE
65 % elif not comment.is_inline and comment.status_change:
66 <%
67 if comment.pull_request:
68 status_change_title = 'Status of review for pull request !{}'.format(comment.pull_request.pull_request_id)
69 else:
70 status_change_title = 'Status of review for commit {}'.format(h.short_id(comment.commit_id))
71 %>
72
73 <i class="icon-circle review-status-${comment.status_change[0].status}"></i>
74 <div class="changeset-status-lbl tooltip" title="${status_change_title}">
75 ${comment.status_change[0].status_lbl}
76 </div>
50 77 % else:
51 ${comment.comment_type or 'note'}
78 <div>
79 <i class="icon-comment"></i>
80 ${(comment.comment_type or 'note')}
81 </div>
52 82 % endif
53 83 % endif
84
54 85 </div>
55 86 </div>
56 87
88 % if 0 and comment.status_change:
89 <div class="pull-left">
90 <span class="tag authortag tooltip" title="${_('Status from pull request.')}">
91 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
92 ${'!{}'.format(comment.pull_request.pull_request_id)}
93 </a>
94 </span>
95 </div>
96 % endif
97
57 98 <div class="author ${'author-inline' if inline else 'author-general'}">
58 99 ${base.gravatar_with_user(comment.author.email, 16, tooltip=True)}
59 100 </div>
101
60 102 <div class="date">
61 103 ${h.age_component(comment.modified_at, time_is_local=True)}
62 104 </div>
63 % if inline:
64 <span></span>
65 % else:
66 <div class="status-change">
67 % if comment.pull_request:
68 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id)}">
69 % if comment.status_change:
70 ${_('pull request !{}').format(comment.pull_request.pull_request_id)}:
71 % else:
72 ${_('pull request !{}').format(comment.pull_request.pull_request_id)}
73 % endif
74 </a>
75 % else:
76 % if comment.status_change:
77 ${_('Status change on commit')}:
78 % endif
79 % endif
80 </div>
105
106 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
107 <span class="tag authortag tooltip" title="${_('Pull request author')}">
108 ${_('author')}
109 </span>
81 110 % endif
82 111
83 % if comment.status_change:
84 <i class="icon-circle review-status-${comment.status_change[0].status}"></i>
85 <div title="${_('Commit status')}" class="changeset-status-lbl">
86 ${comment.status_change[0].status_lbl}
87 </div>
88 % endif
112 <%
113 comment_version_selector = 'comment_versions_{}'.format(comment.comment_id)
114 %>
115
116 % if comment.history:
117 <div class="date">
118
119 <input id="${comment_version_selector}" name="${comment_version_selector}"
120 type="hidden"
121 data-last-version="${comment.history[-1].version}">
122
123 <script type="text/javascript">
124
125 var preLoadVersionData = [
126 % for comment_history in comment.history:
127 {
128 id: ${comment_history.comment_history_id},
129 text: 'v${comment_history.version}',
130 action: function () {
131 Rhodecode.comments.showVersion(
132 "${comment.comment_id}",
133 "${comment_history.comment_history_id}"
134 )
135 },
136 comment_version: "${comment_history.version}",
137 comment_author_username: "${comment_history.author.username}",
138 comment_author_gravatar: "${h.gravatar_url(comment_history.author.email, 16)}",
139 comment_created_on: '${h.age_component(comment_history.created_on, time_is_local=True)}',
140 },
141 % endfor
142 ]
143 initVersionSelector("#${comment_version_selector}", {results: preLoadVersionData});
144
145 </script>
146
147 </div>
148 % else:
149 <div class="date" style="display: none">
150 <input id="${comment_version_selector}" name="${comment_version_selector}"
151 type="hidden"
152 data-last-version="0">
153 </div>
154 %endif
89 155
90 156 <a class="permalink" href="#comment-${comment.comment_id}"> &para;</a>
91 157
92 158 <div class="comment-links-block">
93 % if comment.pull_request and comment.pull_request.author.user_id == comment.author.user_id:
94 <span class="tag authortag tooltip" title="${_('Pull request author')}">
95 ${_('author')}
96 </span>
97 |
98 % endif
159
99 160 % if inline:
100 <div class="pr-version-inline">
101 <a href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
161 <a class="pr-version-inline" href="${request.current_route_path(_query=dict(version=comment.pull_request_version_id), _anchor='comment-{}'.format(comment.comment_id))}">
102 162 % if outdated_at_ver:
103 <code class="pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
163 <code class="tooltip pr-version-num" title="${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
104 164 outdated ${'v{}'.format(pr_index_ver)} |
105 165 </code>
106 166 % elif pr_index_ver:
107 <code class="pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
167 <code class="tooltip pr-version-num" title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
108 168 ${'v{}'.format(pr_index_ver)} |
109 169 </code>
110 170 % endif
111 171 </a>
112 </div>
113 172 % else:
114 % if comment.pull_request_version_id and pr_index_ver:
115 |
116 <div class="pr-version">
173 % if pr_index_ver:
174
117 175 % if comment.outdated:
118 <a href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}">
176 <a class="pr-version"
177 href="?version=${comment.pull_request_version_id}#comment-${comment.comment_id}"
178 >
119 179 ${_('Outdated comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}
120 </a>
180 </a> |
121 181 % else:
122 <div title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}">
123 <a href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}">
124 <code class="pr-version-num">
125 ${'v{}'.format(pr_index_ver)}
126 </code>
127 </a>
128 </div>
182 <a class="tooltip pr-version"
183 title="${_('Comment from pull request version v{0}, latest v{1}').format(pr_index_ver, latest_ver)}"
184 href="${h.route_path('pullrequest_show',repo_name=comment.pull_request.target_repo.repo_name,pull_request_id=comment.pull_request.pull_request_id, version=comment.pull_request_version_id)}"
185 >
186 <code class="pr-version-num">
187 ${'v{}'.format(pr_index_ver)}
188 </code>
189 </a> |
129 190 % endif
130 </div>
191
131 192 % endif
132 193 % endif
133 194
@@ -136,21 +197,25 b''
136 197 %if not outdated_at_ver and (not comment.pull_request or (comment.pull_request and not comment.pull_request.is_closed())):
137 198 ## permissions to delete
138 199 %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
140 <a onclick="return Rhodecode.comments.deleteComment(this);" class="delete-comment"> ${_('Delete')}</a>
200 <a onclick="return Rhodecode.comments.editComment(this);"
201 class="edit-comment">${_('Edit')}</a>
202 | <a onclick="return Rhodecode.comments.deleteComment(this);"
203 class="delete-comment">${_('Delete')}</a>
141 204 %else:
142 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
205 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
206 | <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
143 207 %endif
144 208 %else:
145 <button class="btn-link" disabled="disabled"> ${_('Delete')}</button>
209 <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Edit')}</a>
210 | <a class="tooltip edit-comment link-disabled" disabled="disabled" title="${_('Action unavailable')}">${_('Delete')}</a>
146 211 %endif
147 212
148 213 % if outdated_at_ver:
149 | <a onclick="return Rhodecode.comments.prevOutdatedComment(this);" class="prev-comment"> ${_('Prev')}</a>
150 | <a onclick="return Rhodecode.comments.nextOutdatedComment(this);" class="next-comment"> ${_('Next')}</a>
214 | <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>
215 | <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 216 % else:
152 | <a onclick="return Rhodecode.comments.prevComment(this);" class="prev-comment"> ${_('Prev')}</a>
153 | <a onclick="return Rhodecode.comments.nextComment(this);" class="next-comment"> ${_('Next')}</a>
217 | <a onclick="return Rhodecode.comments.prevComment(this);" class="tooltip prev-comment" title="${_('Jump to the previous comment')}"> <i class="icon-angle-left"></i></a>
218 | <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 219 % endif
155 220
156 221 </div>
@@ -259,7 +259,10 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
259 259 %>
260 260 <div class="filediff-collapse-indicator icon-"></div>
261 261 <span class="pill-group pull-right" >
262 <span class="pill"><i class="icon-comment"></i> ${len(total_file_comments)}</span>
262 <span class="pill" op="comments">
263
264 <i class="icon-comment"></i> ${len(total_file_comments)}
265 </span>
263 266 </span>
264 267 ${diff_ops(filediff)}
265 268
@@ -311,6 +314,7 b" return '%s_%s_%i' % (h.md5_safe(commit+f"
311 314 ${hunk.section_header}
312 315 </td>
313 316 </tr>
317
314 318 ${render_hunk_lines(filediff, c.user_session_attrs["diffmode"], hunk, use_comments=use_comments, inline_comments=inline_comments, active_pattern_entries=active_pattern_entries)}
315 319 % endfor
316 320
@@ -654,21 +658,28 b' def get_comments_for(diff_type, comments'
654 658 %>
655 659
656 660 <%def name="render_hunk_lines_sideside(filediff, hunk, use_comments=False, inline_comments=None, active_pattern_entries=None)">
657 %for i, line in enumerate(hunk.sideside):
661
662 <% chunk_count = 1 %>
663 %for loop_obj, item in h.looper(hunk.sideside):
658 664 <%
665 line = item
666 i = loop_obj.index
667 prev_line = loop_obj.previous
659 668 old_line_anchor, new_line_anchor = None, None
660 669
661 670 if line.original.lineno:
662 671 old_line_anchor = diff_line_anchor(filediff.raw_id, hunk.source_file_path, line.original.lineno, 'o')
663 672 if line.modified.lineno:
664 673 new_line_anchor = diff_line_anchor(filediff.raw_id, hunk.target_file_path, line.modified.lineno, 'n')
674
675 line_action = line.modified.action or line.original.action
676 prev_line_action = prev_line and (prev_line.modified.action or prev_line.original.action)
665 677 %>
666 678
667 679 <tr class="cb-line">
668 680 <td class="cb-data ${action_class(line.original.action)}"
669 681 data-line-no="${line.original.lineno}"
670 682 >
671 <div>
672 683
673 684 <% line_old_comments = None %>
674 685 %if line.original.get_comment_args:
@@ -677,12 +688,11 b' def get_comments_for(diff_type, comments'
677 688 %if line_old_comments:
678 689 <% has_outdated = any([x.outdated for x in line_old_comments]) %>
679 690 % if has_outdated:
680 <i class="tooltip" title="${_('comments including outdated, click to show them')}:${len(line_old_comments)}" class="icon-comment-toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
691 <i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(line_old_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
681 692 % else:
682 <i class="tooltip" title="${_('comments')}: ${len(line_old_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
693 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_old_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
683 694 % endif
684 695 %endif
685 </div>
686 696 </td>
687 697 <td class="cb-lineno ${action_class(line.original.action)}"
688 698 data-line-no="${line.original.lineno}"
@@ -718,11 +728,12 b' def get_comments_for(diff_type, comments'
718 728 <% line_new_comments = None%>
719 729 %endif
720 730 %if line_new_comments:
731
721 732 <% has_outdated = any([x.outdated for x in line_new_comments]) %>
722 733 % if has_outdated:
723 <i class="tooltip" title="${_('comments including outdated, click to show them')}:${len(line_new_comments)}" class="icon-comment-toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
734 <i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(line_new_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
724 735 % else:
725 <i class="tooltip" title="${_('comments')}: ${len(line_new_comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
736 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(line_new_comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
726 737 % endif
727 738 %endif
728 739 </div>
@@ -747,6 +758,12 b' def get_comments_for(diff_type, comments'
747 758 %if use_comments and line.modified.lineno and line_new_comments:
748 759 ${inline_comments_container(line_new_comments, active_pattern_entries=active_pattern_entries)}
749 760 %endif
761 % if line_action in ['+', '-'] and prev_line_action not in ['+', '-']:
762 <div class="nav-chunk" style="visibility: hidden">
763 <i class="icon-eye" title="viewing diff hunk-${hunk.index}-${chunk_count}"></i>
764 </div>
765 <% chunk_count +=1 %>
766 % endif
750 767 </td>
751 768 </tr>
752 769 %endfor
@@ -776,9 +793,9 b' def get_comments_for(diff_type, comments'
776 793 % if comments:
777 794 <% has_outdated = any([x.outdated for x in comments]) %>
778 795 % if has_outdated:
779 <i class="tooltip" title="${_('comments including outdated, click to show them')}:${len(comments)}" class="icon-comment-toggle" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
796 <i class="tooltip icon-comment-toggle" title="${_('comments including outdated: {}. Click here to display them.').format(len(comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
780 797 % else:
781 <i class="tooltip" title="${_('comments')}: ${len(comments)}" class="icon-comment" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
798 <i class="tooltip icon-comment" title="${_('comments: {}. Click to toggle them.').format(len(comments))}" onclick="return Rhodecode.comments.toggleLineComments(this)"></i>
782 799 % endif
783 800 % endif
784 801 </div>
@@ -838,7 +855,7 b' def get_comments_for(diff_type, comments'
838 855 </button>
839 856 </%def>
840 857
841 <%def name="render_diffset_menu(diffset, range_diff_on=None)">
858 <%def name="render_diffset_menu(diffset, range_diff_on=None, commit=None, pull_request_menu=None)">
842 859 <% diffset_container_id = h.md5(diffset.target_ref) %>
843 860
844 861 <div id="diff-file-sticky" class="diffset-menu clearinner">
@@ -899,12 +916,33 b' def get_comments_for(diff_type, comments'
899 916 </div>
900 917 </div>
901 918 </div>
902 <div class="fpath-placeholder">
919 <div class="fpath-placeholder pull-left">
903 920 <i class="icon-file-text"></i>
904 921 <strong class="fpath-placeholder-text">
905 922 Context file:
906 923 </strong>
907 924 </div>
925 <div class="pull-right noselect">
926
927 %if commit:
928 <span>
929 <code>${h.show_id(commit)}</code>
930 </span>
931 %elif pull_request_menu and pull_request_menu.get('pull_request'):
932 <span>
933 <code>!${pull_request_menu['pull_request'].pull_request_id}</code>
934 </span>
935 %endif
936 % if commit or pull_request_menu:
937 <span id="diff_nav">Loading diff...:</span>
938 <span class="cursor-pointer" onclick="scrollToPrevChunk(); return false">
939 <i class="icon-angle-up"></i>
940 </span>
941 <span class="cursor-pointer" onclick="scrollToNextChunk(); return false">
942 <i class="icon-angle-down"></i>
943 </span>
944 % endif
945 </div>
908 946 <div class="sidebar_inner_shadow"></div>
909 947 </div>
910 948 </div>
@@ -1027,10 +1065,86 b' def get_comments_for(diff_type, comments'
1027 1065 e.preventDefault();
1028 1066 });
1029 1067
1068 diffNavText = 'diff navigation:'
1069
1070 getCurrentChunk = function () {
1071
1072 var chunksAll = $('.nav-chunk').filter(function () {
1073 return $(this).parents('.filediff').prev().get(0).checked !== true
1074 })
1075 var chunkSelected = $('.nav-chunk.selected');
1076 var initial = false;
1077
1078 if (chunkSelected.length === 0) {
1079 // no initial chunk selected, we pick first
1080 chunkSelected = $(chunksAll.get(0));
1081 var initial = true;
1082 }
1083
1084 return {
1085 'all': chunksAll,
1086 'selected': chunkSelected,
1087 'initial': initial,
1088 }
1089 }
1090
1091 animateDiffNavText = function () {
1092 var $diffNav = $('#diff_nav')
1093
1094 var callback = function () {
1095 $diffNav.animate({'opacity': 1.00}, 200)
1096 };
1097 $diffNav.animate({'opacity': 0.15}, 200, callback);
1098 }
1099
1100 scrollToChunk = function (moveBy) {
1101 var chunk = getCurrentChunk();
1102 var all = chunk.all
1103 var selected = chunk.selected
1104
1105 var curPos = all.index(selected);
1106 var newPos = curPos;
1107 if (!chunk.initial) {
1108 var newPos = curPos + moveBy;
1109 }
1110
1111 var curElem = all.get(newPos);
1112
1113 if (curElem === undefined) {
1114 // end or back
1115 $('#diff_nav').html('no next diff element:')
1116 animateDiffNavText()
1117 return
1118 } else if (newPos < 0) {
1119 $('#diff_nav').html('no previous diff element:')
1120 animateDiffNavText()
1121 return
1122 } else {
1123 $('#diff_nav').html(diffNavText)
1124 }
1125
1126 curElem = $(curElem)
1127 var offset = 100;
1128 $(window).scrollTop(curElem.position().top - offset);
1129
1130 //clear selection
1131 all.removeClass('selected')
1132 curElem.addClass('selected')
1133 }
1134
1135 scrollToPrevChunk = function () {
1136 scrollToChunk(-1)
1137 }
1138 scrollToNextChunk = function () {
1139 scrollToChunk(1)
1140 }
1141
1030 1142 </script>
1031 1143 % endif
1032 1144
1033 1145 <script type="text/javascript">
1146 $('#diff_nav').html('loading diff...') // wait until whole page is loaded
1147
1034 1148 $(document).ready(function () {
1035 1149
1036 1150 var contextPrefix = _gettext('Context file: ');
@@ -1209,6 +1323,46 b' def get_comments_for(diff_type, comments'
1209 1323 $('.toggle-wide-diff').addClass('btn-active');
1210 1324 updateSticky();
1211 1325 }
1326
1327 // DIFF NAV //
1328
1329 // element to detect scroll direction of
1330 var $window = $(window);
1331
1332 // initialize last scroll position
1333 var lastScrollY = $window.scrollTop();
1334
1335 $window.on('resize scrollstop', {latency: 350}, function () {
1336 var visibleChunks = $('.nav-chunk').withinviewport({top: 75});
1337
1338 // get current scroll position
1339 var currentScrollY = $window.scrollTop();
1340
1341 // determine current scroll direction
1342 if (currentScrollY > lastScrollY) {
1343 var y = 'down'
1344 } else if (currentScrollY !== lastScrollY) {
1345 var y = 'up';
1346 }
1347
1348 var pos = -1; // by default we use last element in viewport
1349 if (y === 'down') {
1350 pos = -1;
1351 } else if (y === 'up') {
1352 pos = 0;
1353 }
1354
1355 if (visibleChunks.length > 0) {
1356 $('.nav-chunk').removeClass('selected');
1357 $(visibleChunks.get(pos)).addClass('selected');
1358 }
1359
1360 // update last scroll position to current position
1361 lastScrollY = currentScrollY;
1362
1363 });
1364 $('#diff_nav').html(diffNavText);
1365
1212 1366 });
1213 1367 </script>
1214 1368
@@ -940,7 +940,7 b' with multiple lines</p>'
940 940 Commenting on line o80.
941 941 </div>
942 942 <div class="comment-help pull-right">
943 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
943 Comments parsed using <a href="http://docutils.sourceforge.io/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
944 944 </div>
945 945 <div style="clear: both"></div>
946 946 <textarea id="text_o80" name="text" class="comment-block-ta ac-input" autocomplete="off"></textarea>
@@ -735,7 +735,7 b''
735 735 Commenting on line {1}.
736 736 </div>
737 737 <div class="comment-help pull-right">
738 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
738 Comments parsed using <a href="http://docutils.sourceforge.io/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
739 739 </div>
740 740 <div style="clear: both"></div>
741 741 <textarea id="text_{1}" name="text" class="comment-block-ta ac-input"></textarea>
@@ -786,7 +786,7 b''
786 786 Create a comment on this Pull Request.
787 787 </div>
788 788 <div class="comment-help pull-right">
789 Comments parsed using <a href="http://docutils.sourceforge.net/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
789 Comments parsed using <a href="http://docutils.sourceforge.io/docs/user/rst/quickref.html">RST</a> syntax with <span class="tooltip" title="Use @username inside this text to send notification to this RhodeCode user">@mention</span> support.
790 790 </div>
791 791 <div style="clear: both"></div>
792 792 <textarea class="comment-block-ta" id="text" name="text"></textarea>
@@ -8,11 +8,6 b''
8 8 SUBJECT:
9 9 <pre>${c.subject}</pre>
10 10
11 HEADERS:
12 <pre>
13 ${c.headers}
14 </pre>
15
16 11 PLAINTEXT:
17 12 <pre>
18 13 ${c.email_body_plaintext|n}
@@ -130,6 +130,34 b' var CG = new ColorGenerator();'
130 130 </script>
131 131
132 132
133 <script id="ejs_commentVersion" type="text/template" class="ejsTemplate">
134
135 <%
136 if (size > 16) {
137 var gravatar_class = 'gravatar gravatar-large';
138 } else {
139 var gravatar_class = 'gravatar';
140 }
141
142 %>
143
144 <%
145 if (show_disabled) {
146 var user_cls = 'user user-disabled';
147 } else {
148 var user_cls = 'user';
149 }
150
151 %>
152
153 <div style='line-height: 20px'>
154 <img style="margin: -3px 0" class="<%= gravatar_class %>" height="<%= size %>" width="<%= size %>" src="<%- gravatar_url -%>">
155 <strong><%- user_name -%></strong>, <code>v<%- version -%></code> edited <%- timeago_component -%>
156 </div>
157
158 </script>
159
160
133 161 </div>
134 162
135 163 <script>
@@ -68,9 +68,6 b' text_monospace = "\'Menlo\', \'Liberation M'
68 68
69 69 %>
70 70
71 ## headers we additionally can set for email
72 <%def name="headers()" filter="n,trim"></%def>
73
74 71 <%def name="plaintext_footer()" filter="trim">
75 72 ${_('This is a notification from RhodeCode.')} ${instance_url}
76 73 </%def>
@@ -15,20 +15,24 b' data = {'
15 15 'comment_id': comment_id,
16 16
17 17 'commit_id': h.show_id(commit),
18 'mention_prefix': '[mention] ' if mention else '',
18 19 }
20
21
22 if comment_file:
23 subject_template = email_comment_file_subject_template or \
24 _('{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}` in the `{repo_name}` repository').format(**data)
25 else:
26 if status_change:
27 subject_template = email_comment_status_change_subject_template or \
28 _('{mention_prefix}[status: {status}] {user} left a {comment_type} on commit `{commit_id}` in the `{repo_name}` repository').format(**data)
29 else:
30 subject_template = email_comment_subject_template or \
31 _('{mention_prefix}{user} left a {comment_type} on commit `{commit_id}` in the `{repo_name}` repository').format(**data)
19 32 %>
20 33
21 34
22 % if comment_file:
23 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on file `{comment_file}` in commit `{commit_id}`').format(**data)} ${_('in the `{repo_name}` repository').format(**data) |n}
24 % else:
25 % if status_change:
26 ${(_('[mention]') if mention else '')} ${_('[status: {status}] {user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the `{repo_name}` repository').format(**data) |n}
27 % else:
28 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on commit `{commit_id}`').format(**data) |n} ${_('in the `{repo_name}` repository').format(**data) |n}
29 % endif
30 % endif
31
35 ${subject_template.format(**data) |n}
32 36 </%def>
33 37
34 38 ## PLAINTEXT VERSION OF BODY
@@ -16,20 +16,23 b' data = {'
16 16
17 17 'pr_title': pull_request.title,
18 18 'pr_id': pull_request.pull_request_id,
19 'mention_prefix': '[mention] ' if mention else '',
19 20 }
21
22 if comment_file:
23 subject_template = email_pr_comment_file_subject_template or \
24 _('{mention_prefix}{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data)
25 else:
26 if status_change:
27 subject_template = email_pr_comment_status_change_subject_template or \
28 _('{mention_prefix}[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
29 else:
30 subject_template = email_pr_comment_subject_template or \
31 _('{mention_prefix}{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data)
20 32 %>
21 33
22 34
23 % if comment_file:
24 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on file `{comment_file}` in pull request !{pr_id}: "{pr_title}"').format(**data) |n}
25 % else:
26 % if status_change:
27 ${(_('[mention]') if mention else '')} ${_('[status: {status}] {user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data) |n}
28 % else:
29 ${(_('[mention]') if mention else '')} ${_('{user} left a {comment_type} on pull request !{pr_id}: "{pr_title}"').format(**data) |n}
30 % endif
31 % endif
32
35 ${subject_template.format(**data) |n}
33 36 </%def>
34 37
35 38 ## PLAINTEXT VERSION OF BODY
@@ -10,9 +10,11 b' data = {'
10 10 'pr_id': pull_request.pull_request_id,
11 11 'pr_title': pull_request.title,
12 12 }
13
14 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
13 15 %>
14 16
15 ${_('{user} requested a pull request review. !{pr_id}: "{pr_title}"').format(**data) |n}
17 ${subject_template.format(**data) |n}
16 18 </%def>
17 19
18 20 ## PLAINTEXT VERSION OF BODY
@@ -10,9 +10,11 b' data = {'
10 10 'pr_id': pull_request.pull_request_id,
11 11 'pr_title': pull_request.title,
12 12 }
13
14 subject_template = email_pr_update_subject_template or _('{updating_user} updated pull request. !{pr_id}: "{pr_title}"')
13 15 %>
14 16
15 ${_('{updating_user} updated pull request. !{pr_id}: "{pr_title}"').format(**data) |n}
17 ${subject_template.format(**data) |n}
16 18 </%def>
17 19
18 20 ## PLAINTEXT VERSION OF BODY
@@ -5,10 +5,6 b''
5 5 Test "Subject" ${_('hello "world"')|n}
6 6 </%def>
7 7
8 <%def name="headers()" filter="n,trim">
9 X=Y
10 </%def>
11
12 8 ## plain text version of the email. Empty by default
13 9 <%def name="body_plaintext()" filter="n,trim">
14 10 Email Plaintext Body
@@ -17,14 +17,30 b''
17 17 </div>
18 18
19 19 % if h.HasRepoPermissionAny('repository.write','repository.admin')(c.repo_name):
20 <div>
21 <a class="btn btn-primary new-file" href="${h.route_path('repo_files_upload_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
22 ${_('Upload File')}
23 </a>
24 <a class="btn btn-primary new-file" href="${h.route_path('repo_files_add_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
25 ${_('Add File')}
26 </a>
27 </div>
20
21 <div class="new-file">
22 <div class="btn-group btn-group-actions">
23 <a class="btn btn-primary no-margin" href="${h.route_path('repo_files_add_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
24 ${_('Add File')}
25 </a>
26
27 <a class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more options')}">
28 <i class="icon-down"></i>
29 </a>
30
31 <div class="btn-action-switcher-container right-align">
32 <ul class="btn-action-switcher" role="menu" style="min-width: 200px">
33 <li>
34 <a class="action_button" href="${h.route_path('repo_files_upload_file',repo_name=c.repo_name,commit_id=c.commit.raw_id,f_path=c.f_path)}">
35 <i class="icon-upload"></i>
36 ${_('Upload File')}
37 </a>
38 </li>
39 </ul>
40 </div>
41 </div>
42 </div>
43
28 44 % endif
29 45
30 46 % if c.enable_downloads:
@@ -61,11 +61,11 b''
61 61 ##${h.secure_form(form_url, id='eform', enctype="multipart/form-data", request=request)}
62 62 <div class="edit-file-fieldset">
63 63 <div class="path-items">
64 <ul>
64 <ul class="tooltip" title="Repository path to store uploaded files. To change it, navigate to different path and click upload from there.">
65 65 <li class="breadcrumb-path">
66 66 <div>
67 67 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path='')}"><i class="icon-home"></i></a> /
68 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path)}">${c.f_path}</a> ${('/' if c.f_path else '')}
68 <a href="${h.route_path('repo_files', repo_name=c.repo_name, commit_id=c.commit.raw_id, f_path=c.f_path)}">${c.f_path}</a>${('/' if c.f_path else '')}
69 69 </div>
70 70 </li>
71 71 <li class="location-path">
@@ -79,7 +79,7 b''
79 79 <div class="upload-form table">
80 80 <div>
81 81
82 <div class="dropzone-wrapper" id="file-uploader">
82 <div class="dropzone-wrapper" id="file-uploader" style="border: none; padding: 40px 0">
83 83 <div class="dropzone-pure">
84 84 <div class="dz-message">
85 85 <i class="icon-upload" style="font-size:36px"></i></br>
@@ -250,9 +250,9 b''
250 250 var added = data['stats'][0]
251 251 var deleted = data['stats'][1]
252 252 var commonAncestorId = data['ancestor'];
253
254 var prTitleAndDesc = getTitleAndDescription(
255 sourceRef()[1], commitElements, 5);
253 var _sourceRefType = sourceRef()[0];
254 var _sourceRefName = sourceRef()[1];
255 var prTitleAndDesc = getTitleAndDescription(_sourceRefType, _sourceRefName, commitElements, 5);
256 256
257 257 var title = prTitleAndDesc[0];
258 258 var proposedDescription = prTitleAndDesc[1];
@@ -471,13 +471,11 b''
471 471 </div>
472 472 </div>
473 473 </div>
474 % elif c.pr_merge_source_commit.changed:
474 % elif c.pr_merge_source_commit.changed and not c.pull_request.is_closed():
475 475 <div class="box">
476 476 <div class="alert alert-info">
477 477 <div>
478 % if c.pr_merge_source_commit.changed:
479 478 <strong>${_('There are new changes for `{}:{}` in source repository, please consider updating this pull request.').format(c.pr_merge_source_commit.ref_spec.type, c.pr_merge_source_commit.ref_spec.name)}</strong>
480 % endif
481 479 </div>
482 480 </div>
483 481 </div>
@@ -514,12 +512,12 b''
514 512 ${_('Update commits')}
515 513 </a>
516 514
517 <a id="update_commits_switcher" class="tooltip btn btn-primary" style="margin-left: -1px" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more update options')}">
515 <a id="update_commits_switcher" class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more update options')}">
518 516 <i class="icon-down"></i>
519 517 </a>
520 518
521 <div class="btn-action-switcher-container" id="update-commits-switcher">
522 <ul class="btn-action-switcher" role="menu">
519 <div class="btn-action-switcher-container right-align" id="update-commits-switcher">
520 <ul class="btn-action-switcher" role="menu" style="min-width: 300px;">
523 521 <li>
524 522 <a href="#forceUpdate" onclick="updateController.forceUpdateCommits(this); return false">
525 523 ${_('Force update commits')}
@@ -626,11 +624,12 b''
626 624
627 625 <%
628 626 pr_menu_data = {
629 'outdated_comm_count_ver': outdated_comm_count_ver
627 'outdated_comm_count_ver': outdated_comm_count_ver,
628 'pull_request': c.pull_request
630 629 }
631 630 %>
632 631
633 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on)}
632 ${cbdiffs.render_diffset_menu(c.diffset, range_diff_on=c.range_diff_on, pull_request_menu=pr_menu_data)}
634 633
635 634 % if c.range_diff_on:
636 635 % for commit in c.commit_ranges:
@@ -97,7 +97,7 b''
97 97 <div class="right-content">
98 98 <div class="commit-info">
99 99 <div class="tags">
100 <% commit_rev = c.rhodecode_db_repo.changeset_cache.get('revision') %>
100 <% commit_rev = h.safe_int(c.rhodecode_db_repo.changeset_cache.get('revision'), 0) + 1 %>
101 101 % if c.rhodecode_repo:
102 102 ${refs_counters(
103 103 c.rhodecode_repo.branches,
@@ -184,12 +184,37 b''
184 184 ${h.link_to(_('Enable downloads'),h.route_path('edit_repo',repo_name=c.repo_name, _anchor='repo_enable_downloads'))}
185 185 % endif
186 186 % else:
187 <span class="enabled">
188 <a id="archive_link" class="btn btn-small" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name,fname='tip.zip')}">
189 tip.zip
190 ## replaced by some JS on select
191 </a>
192 </span>
187 <div class="enabled pull-left" style="margin-right: 10px">
188
189 <div class="btn-group btn-group-actions">
190 <a class="archive_link btn btn-small" data-ext=".zip" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname=c.rhodecode_db_repo.landing_ref_name+'.zip')}">
191 <i class="icon-download"></i>
192 ${c.rhodecode_db_repo.landing_ref_name}.zip
193 ## replaced by some JS on select
194 </a>
195
196 <a class="tooltip btn btn-primary btn-more-option" data-toggle="dropdown" aria-pressed="false" role="button" title="${_('more download options')}">
197 <i class="icon-down"></i>
198 </a>
199
200 <div class="btn-action-switcher-container left-align">
201 <ul class="btn-action-switcher" role="menu" style="min-width: 200px">
202 % for a_type, content_type, extension in h.ARCHIVE_SPECS:
203 % if extension not in ['.zip']:
204 <li>
205
206 <a class="archive_link" data-ext="${extension}" href="${h.route_path('repo_archivefile',repo_name=c.rhodecode_db_repo.repo_name, fname=c.rhodecode_db_repo.landing_ref_name+extension)}">
207 <i class="icon-download"></i>
208 ${c.rhodecode_db_repo.landing_ref_name+extension}
209 </a>
210 </li>
211 % endif
212 % endfor
213 </ul>
214 </div>
215 </div>
216
217 </div>
193 218 ${h.hidden('download_options')}
194 219 % endif
195 220 </div>
@@ -76,8 +76,8 b''
76 76
77 77 var initialCommitData = {
78 78 id: null,
79 text: 'tip',
80 type: 'tag',
79 text: '${c.rhodecode_db_repo.landing_ref_name}',
80 type: '${c.rhodecode_db_repo.landing_ref_type}',
81 81 raw_id: null,
82 82 files_url: null
83 83 };
@@ -87,15 +87,22 b''
87 87 // on change of download options
88 88 $('#download_options').on('change', function(e) {
89 89 // format of Object {text: "v0.0.3", type: "tag", id: "rev"}
90 var ext = '.zip';
91 var selected_cs = e.added;
92 var fname = e.added.raw_id + ext;
93 var href = pyroutes.url('repo_archivefile', {'repo_name': templateContext.repo_name, 'fname':fname});
94 // set new label
95 $('#archive_link').html('{0}{1}'.format(escapeHtml(e.added.text), ext));
90 var selectedReference = e.added;
91 var ico = '<i class="icon-download"></i>';
96 92
97 // set new url to button,
98 $('#archive_link').attr('href', href)
93 $.each($('.archive_link'), function (key, val) {
94 var ext = $(this).data('ext');
95 var fname = selectedReference.raw_id + ext;
96 var href = pyroutes.url('repo_archivefile', {
97 'repo_name': templateContext.repo_name,
98 'fname': fname
99 });
100 // set new label
101 $(this).html(ico + ' {0}{1}'.format(escapeHtml(e.added.text), ext));
102 // set new url to button,
103 $(this).attr('href', href)
104 });
105
99 106 });
100 107
101 108
@@ -28,6 +28,7 b' from rhodecode.events import ('
28 28 PullRequestCreateEvent,
29 29 PullRequestUpdateEvent,
30 30 PullRequestCommentEvent,
31 PullRequestCommentEditEvent,
31 32 PullRequestReviewEvent,
32 33 PullRequestMergeEvent,
33 34 PullRequestCloseEvent,
@@ -80,6 +81,21 b' def test_pullrequest_comment_events_seri'
80 81
81 82
82 83 @pytest.mark.backends("git", "hg")
84 def test_pullrequest_comment_edit_events_serialized(pr_util, config_stub):
85 pr = pr_util.create_pull_request()
86 comment = CommentsModel().get_comments(
87 pr.target_repo.repo_id, pull_request=pr)[0]
88 event = PullRequestCommentEditEvent(pr, comment)
89 data = event.as_dict()
90 assert data['name'] == PullRequestCommentEditEvent.name
91 assert data['repo']['repo_name'] == pr.target_repo.repo_name
92 assert data['pullrequest']['pull_request_id'] == pr.pull_request_id
93 assert data['pullrequest']['url']
94 assert data['pullrequest']['permalink_url']
95 assert data['comment']['text'] == comment.text
96
97
98 @pytest.mark.backends("git", "hg")
83 99 def test_close_pull_request_events(pr_util, user_admin, config_stub):
84 100 pr = pr_util.create_pull_request()
85 101
@@ -29,7 +29,8 b' from rhodecode.events.repo import ('
29 29 RepoPrePullEvent, RepoPullEvent,
30 30 RepoPrePushEvent, RepoPushEvent,
31 31 RepoPreCreateEvent, RepoCreateEvent,
32 RepoPreDeleteEvent, RepoDeleteEvent, RepoCommitCommentEvent,
32 RepoPreDeleteEvent, RepoDeleteEvent,
33 RepoCommitCommentEvent, RepoCommitCommentEditEvent
33 34 )
34 35
35 36
@@ -138,8 +139,32 b' def test_repo_commit_event(config_stub, '
138 139 'comment_type': 'comment_type',
139 140 'f_path': 'f_path',
140 141 'line_no': 'line_no',
142 'last_version': 0,
141 143 })
142 144 event = EventClass(repo=repo_stub, commit=commit, comment=comment)
143 145 data = event.as_dict()
144 146 assert data['commit']['commit_id']
145 147 assert data['comment']['comment_id']
148
149
150 @pytest.mark.parametrize('EventClass', [RepoCommitCommentEditEvent])
151 def test_repo_commit_edit_event(config_stub, repo_stub, EventClass):
152
153 commit = StrictAttributeDict({
154 'raw_id': 'raw_id',
155 'message': 'message',
156 'branch': 'branch',
157 })
158
159 comment = StrictAttributeDict({
160 'comment_id': 'comment_id',
161 'text': 'text',
162 'comment_type': 'comment_type',
163 'f_path': 'f_path',
164 'line_no': 'line_no',
165 'last_version': 0,
166 })
167 event = EventClass(repo=repo_stub, commit=commit, comment=comment)
168 data = event.as_dict()
169 assert data['commit']['commit_id']
170 assert data['comment']['comment_id']
@@ -433,7 +433,7 b' def test_permission_calculator_admin_per'
433 433
434 434 calculator = auth.PermissionCalculator(
435 435 user.user_id, {}, False, False, True, 'higherwin')
436 permissions = calculator._calculate_admin_permissions()
436 permissions = calculator._calculate_super_admin_permissions()
437 437
438 438 assert permissions['repositories_groups'][repo_group.group_name] == \
439 439 'group.admin'
@@ -35,15 +35,12 b' def test_get_template_obj(app, request_s'
35 35
36 36 def test_render_email(app, http_host_only_stub):
37 37 kwargs = {}
38 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
38 subject, body, body_plaintext = EmailNotificationModel().render_email(
39 39 EmailNotificationModel.TYPE_TEST, **kwargs)
40 40
41 41 # subject
42 42 assert subject == 'Test "Subject" hello "world"'
43 43
44 # headers
45 assert headers == 'X=Y'
46
47 44 # body plaintext
48 45 assert body_plaintext == 'Email Plaintext Body'
49 46
@@ -80,7 +77,7 b' def test_render_pr_email(app, user_admin'
80 77 'pull_request_url': 'http://localhost/pr1',
81 78 }
82 79
83 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
80 subject, body, body_plaintext = EmailNotificationModel().render_email(
84 81 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
85 82
86 83 # subject
@@ -133,7 +130,7 b' def test_render_pr_update_email(app, use'
133 130 'removed_files': file_changes.removed,
134 131 }
135 132
136 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
133 subject, body, body_plaintext = EmailNotificationModel().render_email(
137 134 EmailNotificationModel.TYPE_PULL_REQUEST_UPDATE, **kwargs)
138 135
139 136 # subject
@@ -188,7 +185,6 b' def test_render_comment_subject_no_newli'
188 185
189 186 'pull_request_url': 'http://code.rc.com/_pr/123'
190 187 }
191 subject, headers, body, body_plaintext = EmailNotificationModel().render_email(
192 email_type, **kwargs)
188 subject, body, body_plaintext = EmailNotificationModel().render_email(email_type, **kwargs)
193 189
194 190 assert '\n' not in subject
@@ -113,6 +113,7 b' class TestRepoGroupSchema(object):'
113 113 repo_group_owner=user_regular.username
114 114 ))
115 115
116 expected = 'Parent repository group `{}` does not exist'.format(
117 test_repo_group.group_name)
116 expected = 'You do not have the permissions to store ' \
117 'repository groups inside repository group `{}`'\
118 .format(test_repo_group.group_name)
118 119 assert excinfo.value.asdict()['repo_group'] == expected
@@ -560,6 +560,7 b' class Backend(object):'
560 560
561 561 invalid_repo_name = re.compile(r'[^0-9a-zA-Z]+')
562 562 _master_repo = None
563 _master_repo_path = ''
563 564 _commit_ids = {}
564 565
565 566 def __init__(self, alias, repo_name, test_name, test_repo_container):
@@ -624,6 +625,8 b' class Backend(object):'
624 625 Returns a commit map which maps from commit message to raw_id.
625 626 """
626 627 self._master_repo = self.create_repo(commits=commits)
628 self._master_repo_path = self._master_repo.repo_full_path
629
627 630 return self._commit_ids
628 631
629 632 def create_repo(
@@ -661,11 +664,10 b' class Backend(object):'
661 664 """
662 665 Make sure that repo contains all commits mentioned in `heads`
663 666 """
664 vcsmaster = self._master_repo.scm_instance()
665 667 vcsrepo = repo.scm_instance()
666 668 vcsrepo.config.clear_section('hooks')
667 669 commit_ids = [self._commit_ids[h] for h in heads]
668 vcsrepo.pull(vcsmaster.path, commit_ids=commit_ids)
670 vcsrepo.pull(self._master_repo_path, commit_ids=commit_ids)
669 671
670 672 def create_fork(self):
671 673 repo_to_fork = self.repo_name
General Comments 0
You need to be logged in to leave comments. Login now