##// END OF EJS Templates
reviewers: added observers as another way to define reviewers....
marcink -
r4500:bfede169 stable
parent child Browse files
Show More
@@ -0,0 +1,68 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
9 from rhodecode.lib.dbmigrate.versions import _reset_base
10 from rhodecode.model import meta, init_model_encryption
11
12
13 log = logging.getLogger(__name__)
14
15
16 def upgrade(migrate_engine):
17 """
18 Upgrade operations go here.
19 Don't create your own engine; bind migrate_engine to your metadata
20 """
21 _reset_base(migrate_engine)
22 from rhodecode.lib.dbmigrate.schema import db_4_20_0_0 as db
23
24 init_model_encryption(db)
25
26 context = MigrationContext.configure(migrate_engine.connect())
27 op = Operations(context)
28
29 table = db.RepoReviewRuleUser.__table__
30 with op.batch_alter_table(table.name) as batch_op:
31 new_column = Column('role', Unicode(255), nullable=True)
32 batch_op.add_column(new_column)
33
34 _fill_rule_user_role(op, meta.Session)
35
36 table = db.RepoReviewRuleUserGroup.__table__
37 with op.batch_alter_table(table.name) as batch_op:
38 new_column = Column('role', Unicode(255), nullable=True)
39 batch_op.add_column(new_column)
40
41 _fill_rule_user_group_role(op, meta.Session)
42
43
44 def downgrade(migrate_engine):
45 meta = MetaData()
46 meta.bind = migrate_engine
47
48
49 def fixups(models, _SESSION):
50 pass
51
52
53 def _fill_rule_user_role(op, session):
54 params = {'role': 'reviewer'}
55 query = text(
56 'UPDATE repo_review_rules_users SET role = :role'
57 ).bindparams(**params)
58 op.execute(query)
59 session().commit()
60
61
62 def _fill_rule_user_group_role(op, session):
63 params = {'role': 'reviewer'}
64 query = text(
65 'UPDATE repo_review_rules_users_groups SET role = :role'
66 ).bindparams(**params)
67 op.execute(query)
68 session().commit()
@@ -0,0 +1,35 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 def html(info):
23 """
24 Custom string as html content_type renderer for pyramid
25 """
26 def _render(value, system):
27 request = system.get('request')
28 if request is not None:
29 response = request.response
30 ct = response.content_type
31 if ct == response.default_content_type:
32 response.content_type = 'text/html'
33 return value
34
35 return _render
@@ -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__ = 109 # defines current db version for migrations
51 __dbversion__ = 110 # defines current db version for migrations
52 52 __platform__ = platform.system()
53 53 __license__ = 'AGPLv3, and Commercial License'
54 54 __author__ = 'RhodeCode GmbH'
@@ -704,7 +704,7 b' def create_pull_request('
704 704 user = get_user_or_error(reviewer_object['username'])
705 705 reviewer_object['user_id'] = user.user_id
706 706
707 get_default_reviewers_data, validate_default_reviewers = \
707 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
708 708 PullRequestModel().get_reviewer_functions()
709 709
710 710 # recalculate reviewers logic, to make sure we can validate this
@@ -865,14 +865,13 b' def update_pull_request('
865 865 user = get_user_or_error(reviewer_object['username'])
866 866 reviewer_object['user_id'] = user.user_id
867 867
868 get_default_reviewers_data, get_validated_reviewers = \
868 get_default_reviewers_data, get_validated_reviewers, validate_observers = \
869 869 PullRequestModel().get_reviewer_functions()
870 870
871 871 # re-use stored rules
872 872 reviewer_rules = pull_request.reviewer_data
873 873 try:
874 reviewers = get_validated_reviewers(
875 reviewer_objects, reviewer_rules)
874 reviewers = get_validated_reviewers(reviewer_objects, reviewer_rules)
876 875 except ValueError as e:
877 876 raise JSONRPCError('Reviewers Validation: {}'.format(e))
878 877 else:
@@ -34,6 +34,7 b' log = logging.getLogger(__name__)'
34 34
35 35
36 36 class DebugStyleView(BaseAppView):
37
37 38 def load_default_context(self):
38 39 c = self._get_local_tmpl_context()
39 40
@@ -75,6 +76,7 b' Check if we should use full-topic or min'
75 76 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
76 77 target_ref_parts=AttributeDict(type='branch', name='master'),
77 78 )
79
78 80 target_repo = AttributeDict(repo_name='repo_group/target_repo')
79 81 source_repo = AttributeDict(repo_name='repo_group/source_repo')
80 82 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
@@ -83,6 +85,7 b' Check if we should use full-topic or min'
83 85 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
84 86 'removed': ['eeeeeeeeeee'],
85 87 })
88
86 89 file_changes = AttributeDict({
87 90 'added': ['a/file1.md', 'file2.py'],
88 91 'modified': ['b/modified_file.rst'],
@@ -97,15 +100,19 b' Check if we should use full-topic or min'
97 100 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n',
98 101 'exc_type': 'AttributeError'
99 102 }
103
100 104 email_kwargs = {
101 105 'test': {},
106
102 107 'message': {
103 108 'body': 'message body !'
104 109 },
110
105 111 'email_test': {
106 112 'user': user,
107 113 'date': datetime.datetime.now(),
108 114 },
115
109 116 'exception': {
110 117 'email_prefix': '[RHODECODE ERROR]',
111 118 'exc_id': exc_traceback['exc_id'],
@@ -113,6 +120,7 b' Check if we should use full-topic or min'
113 120 'exc_type_name': 'NameError',
114 121 'exc_traceback': exc_traceback,
115 122 },
123
116 124 'password_reset': {
117 125 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
118 126
@@ -121,6 +129,7 b' Check if we should use full-topic or min'
121 129 'email': 'test@rhodecode.com',
122 130 'first_admin_email': User.get_first_super_admin().email
123 131 },
132
124 133 'password_reset_confirmation': {
125 134 'new_password': 'new-password-example',
126 135 'user': user,
@@ -128,6 +137,7 b' Check if we should use full-topic or min'
128 137 'email': 'test@rhodecode.com',
129 138 'first_admin_email': User.get_first_super_admin().email
130 139 },
140
131 141 'registration': {
132 142 'user': user,
133 143 'date': datetime.datetime.now(),
@@ -161,6 +171,7 b' Check if we should use full-topic or min'
161 171 'mention': True,
162 172
163 173 },
174
164 175 'pull_request_comment+status': {
165 176 'user': user,
166 177
@@ -201,6 +212,7 b' def db():'
201 212 'mention': True,
202 213
203 214 },
215
204 216 'pull_request_comment+file': {
205 217 'user': user,
206 218
@@ -303,6 +315,7 b' This should work better !'
303 315 'renderer_type': 'markdown',
304 316 'mention': True,
305 317 },
318
306 319 'cs_comment+status': {
307 320 'user': user,
308 321 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
@@ -328,6 +341,7 b' This is a multiline comment :)'
328 341 'renderer_type': 'markdown',
329 342 'mention': True,
330 343 },
344
331 345 'cs_comment+file': {
332 346 'user': user,
333 347 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
@@ -348,12 +362,37 b' This is a multiline comment :)'
348 362 'renderer_type': 'markdown',
349 363 'mention': True,
350 364 },
351
365
352 366 'pull_request': {
353 367 'user': user,
354 368 'pull_request': pr,
355 369 'pull_request_commits': [
356 370 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
371 my-account: moved email closer to profile as it's similar data just moved outside.
372 '''),
373 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
374 users: description edit fixes
375
376 - tests
377 - added metatags info
378 '''),
379 ],
380
381 'pull_request_target_repo': target_repo,
382 'pull_request_target_repo_url': 'http://target-repo/url',
383
384 'pull_request_source_repo': source_repo,
385 'pull_request_source_repo_url': 'http://source-repo/url',
386
387 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
388 'user_role': 'reviewer',
389 },
390
391 'pull_request+reviewer_role': {
392 'user': user,
393 'pull_request': pr,
394 'pull_request_commits': [
395 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
357 396 my-account: moved email closer to profile as it's similar data just moved outside.
358 397 '''),
359 398 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
@@ -371,8 +410,33 b' users: description edit fixes'
371 410 'pull_request_source_repo_url': 'http://source-repo/url',
372 411
373 412 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
413 'user_role': 'reviewer',
414 },
415
416 'pull_request+observer_role': {
417 'user': user,
418 'pull_request': pr,
419 'pull_request_commits': [
420 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
421 my-account: moved email closer to profile as it's similar data just moved outside.
422 '''),
423 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
424 users: description edit fixes
425
426 - tests
427 - added metatags info
428 '''),
429 ],
430
431 'pull_request_target_repo': target_repo,
432 'pull_request_target_repo_url': 'http://target-repo/url',
433
434 'pull_request_source_repo': source_repo,
435 'pull_request_source_repo_url': 'http://source-repo/url',
436
437 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
438 'user_role': 'observer'
374 439 }
375
376 440 }
377 441
378 442 template_type = email_id.split('+')[0]
@@ -401,6 +465,7 b' users: description edit fixes'
401 465 c = self.load_default_context()
402 466 c.active = os.path.splitext(t_path)[0]
403 467 c.came_from = ''
468 # NOTE(marcink): extend the email types with variations based on data sets
404 469 c.email_types = {
405 470 'cs_comment+file': {},
406 471 'cs_comment+status': {},
@@ -409,6 +474,9 b' users: description edit fixes'
409 474 'pull_request_comment+status': {},
410 475
411 476 'pull_request_update': {},
477
478 'pull_request+reviewer_role': {},
479 'pull_request+observer_role': {},
412 480 }
413 481 c.email_types.update(EmailNotificationModel.email_types)
414 482
@@ -21,11 +21,13 b''
21 21 from rhodecode.lib import helpers as h
22 22 from rhodecode.lib.utils2 import safe_int
23 23 from rhodecode.model.pull_request import get_diff_info
24
25 REVIEWER_API_VERSION = 'V3'
24 from rhodecode.model.db import PullRequestReviewers
25 # V3 - Reviewers, with default rules data
26 # v4 - Added observers metadata
27 REVIEWER_API_VERSION = 'V4'
26 28
27 29
28 def reviewer_as_json(user, reasons=None, mandatory=False, rules=None, user_group=None):
30 def reviewer_as_json(user, reasons=None, role=None, mandatory=False, rules=None, user_group=None):
29 31 """
30 32 Returns json struct of a reviewer for frontend
31 33
@@ -33,11 +35,15 b' def reviewer_as_json(user, reasons=None,'
33 35 :param reasons: list of strings of why they are reviewers
34 36 :param mandatory: bool, to set user as mandatory
35 37 """
38 role = role or PullRequestReviewers.ROLE_REVIEWER
39 if role not in PullRequestReviewers.ROLES:
40 raise ValueError('role is not one of %s', PullRequestReviewers.ROLES)
36 41
37 42 return {
38 43 'user_id': user.user_id,
39 44 'reasons': reasons or [],
40 45 'rules': rules or [],
46 'role': role,
41 47 'mandatory': mandatory,
42 48 'user_group': user_group,
43 49 'username': user.username,
@@ -48,8 +54,7 b' def reviewer_as_json(user, reasons=None,'
48 54 }
49 55
50 56
51 def get_default_reviewers_data(
52 current_user, source_repo, source_commit, target_repo, target_commit):
57 def get_default_reviewers_data(current_user, source_repo, source_commit, target_repo, target_commit):
53 58 """
54 59 Return json for default reviewers of a repository
55 60 """
@@ -59,7 +64,7 b' def get_default_reviewers_data('
59 64
60 65 reasons = ['Default reviewer', 'Repository owner']
61 66 json_reviewers = [reviewer_as_json(
62 user=target_repo.user, reasons=reasons, mandatory=False, rules=None)]
67 user=target_repo.user, reasons=reasons, mandatory=False, rules=None, role=None)]
63 68
64 69 return {
65 70 'api_ver': REVIEWER_API_VERSION, # define version for later possible schema upgrade
@@ -73,15 +78,18 b' def get_default_reviewers_data('
73 78 def validate_default_reviewers(review_members, reviewer_rules):
74 79 """
75 80 Function to validate submitted reviewers against the saved rules
76
77 81 """
78 82 reviewers = []
79 83 reviewer_by_id = {}
80 84 for r in review_members:
81 85 reviewer_user_id = safe_int(r['user_id'])
82 entry = (reviewer_user_id, r['reasons'], r['mandatory'], r['rules'])
86 entry = (reviewer_user_id, r['reasons'], r['mandatory'], r['role'], r['rules'])
83 87
84 88 reviewer_by_id[reviewer_user_id] = entry
85 89 reviewers.append(entry)
86 90
87 91 return reviewers
92
93
94 def validate_observers(observer_members):
95 return {}
@@ -193,7 +193,7 b' class RepoCommitsView(RepoAppView):'
193 193
194 194 for review_obj, member, reasons, mandatory, status in review_statuses:
195 195 member_reviewer = h.reviewer_as_json(
196 member, reasons=reasons, mandatory=mandatory,
196 member, reasons=reasons, mandatory=mandatory, role=None,
197 197 user_group=None
198 198 )
199 199
@@ -39,14 +39,15 b' from rhodecode.lib.ext_json import json'
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 44 from rhodecode.lib.vcs.exceptions import (
45 45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 47 from rhodecode.model.comment import CommentsModel
48 48 from rhodecode.model.db import (
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 PullRequestReviewers)
50 51 from rhodecode.model.forms import PullRequestForm
51 52 from rhodecode.model.meta import Session
52 53 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
@@ -455,14 +456,18 b' class RepoPullRequestsView(RepoAppView, '
455 456 return self._get_template_context(c)
456 457
457 458 c.allowed_reviewers = [obj.user_id for obj in pull_request.reviewers if obj.user]
459 c.reviewers_count = pull_request.reviewers_count
460 c.observers_count = pull_request.observers_count
458 461
459 462 # reviewers and statuses
460 463 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
461 464 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
465 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
462 466
463 467 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
464 468 member_reviewer = h.reviewer_as_json(
465 469 member, reasons=reasons, mandatory=mandatory,
470 role=review_obj.role,
466 471 user_group=review_obj.rule_user_group_data()
467 472 )
468 473
@@ -474,6 +479,17 b' class RepoPullRequestsView(RepoAppView, '
474 479
475 480 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
476 481
482 for observer_obj, member in pull_request_at_ver.observers():
483 member_observer = h.reviewer_as_json(
484 member, reasons=[], mandatory=False,
485 role=observer_obj.role,
486 user_group=observer_obj.rule_user_group_data()
487 )
488 member_observer['allowed_to_update'] = c.allowed_to_update
489 c.pull_request_set_observers_data_json['observers'].append(member_observer)
490
491 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
492
477 493 general_comments, inline_comments = \
478 494 self.register_comments_vars(c, pull_request_latest, versions)
479 495
@@ -967,7 +983,7 b' class RepoPullRequestsView(RepoAppView, '
967 983 'repository.read', 'repository.write', 'repository.admin')
968 984 @view_config(
969 985 route_name='pullrequest_comments', request_method='POST',
970 renderer='string', xhr=True)
986 renderer='string_html', xhr=True)
971 987 def pullrequest_comments(self):
972 988 self.load_default_context()
973 989
@@ -998,7 +1014,8 b' class RepoPullRequestsView(RepoAppView, '
998 1014 all_comments = c.inline_comments_flat + c.comments
999 1015
1000 1016 existing_ids = filter(
1001 lambda e: e, map(safe_int, self.request.POST.getall('comments[]')))
1017 lambda e: e, map(safe_int, aslist(self.request.POST.get('comments'))))
1018
1002 1019 return _render('comments_table', all_comments, len(all_comments),
1003 1020 existing_ids=existing_ids)
1004 1021
@@ -1008,7 +1025,7 b' class RepoPullRequestsView(RepoAppView, '
1008 1025 'repository.read', 'repository.write', 'repository.admin')
1009 1026 @view_config(
1010 1027 route_name='pullrequest_todos', request_method='POST',
1011 renderer='string', xhr=True)
1028 renderer='string_html', xhr=True)
1012 1029 def pullrequest_todos(self):
1013 1030 self.load_default_context()
1014 1031
@@ -1138,7 +1155,7 b' class RepoPullRequestsView(RepoAppView, '
1138 1155 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1139 1156 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1140 1157
1141 get_default_reviewers_data, validate_default_reviewers = \
1158 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1142 1159 PullRequestModel().get_reviewer_functions()
1143 1160
1144 1161 # recalculate reviewers logic, to make sure we can validate this
@@ -1146,9 +1163,8 b' class RepoPullRequestsView(RepoAppView, '
1146 1163 self._rhodecode_db_user, source_db_repo,
1147 1164 source_commit, target_db_repo, target_commit)
1148 1165
1149 given_reviewers = _form['review_members']
1150 reviewers = validate_default_reviewers(
1151 given_reviewers, reviewer_rules)
1166 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1167 observers = validate_observers(_form['observer_members'], reviewer_rules)
1152 1168
1153 1169 pullrequest_title = _form['pullrequest_title']
1154 1170 title_source_ref = source_ref.split(':', 2)[1]
@@ -1172,6 +1188,7 b' class RepoPullRequestsView(RepoAppView, '
1172 1188 revisions=commit_ids,
1173 1189 common_ancestor_id=common_ancestor_id,
1174 1190 reviewers=reviewers,
1191 observers=observers,
1175 1192 title=pullrequest_title,
1176 1193 description=description,
1177 1194 description_renderer=description_renderer,
@@ -1227,14 +1244,23 b' class RepoPullRequestsView(RepoAppView, '
1227 1244 # only owner or admin can update it
1228 1245 allowed_to_update = PullRequestModel().check_user_update(
1229 1246 pull_request, self._rhodecode_user)
1247
1230 1248 if allowed_to_update:
1231 1249 controls = peppercorn.parse(self.request.POST.items())
1232 1250 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1233 1251
1234 1252 if 'review_members' in controls:
1235 1253 self._update_reviewers(
1254 c,
1236 1255 pull_request, controls['review_members'],
1237 pull_request.reviewer_data)
1256 pull_request.reviewer_data,
1257 PullRequestReviewers.ROLE_REVIEWER)
1258 elif 'observer_members' in controls:
1259 self._update_reviewers(
1260 c,
1261 pull_request, controls['observer_members'],
1262 pull_request.reviewer_data,
1263 PullRequestReviewers.ROLE_OBSERVER)
1238 1264 elif str2bool(self.request.POST.get('update_commits', 'false')):
1239 1265 if is_state_changing:
1240 1266 log.debug('commits update: forbidden because pull request is in state %s',
@@ -1255,6 +1281,7 b' class RepoPullRequestsView(RepoAppView, '
1255 1281 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1256 1282 self._edit_pull_request(pull_request)
1257 1283 else:
1284 log.error('Unhandled update data.')
1258 1285 raise HTTPBadRequest()
1259 1286
1260 1287 return {'response': True,
@@ -1262,6 +1289,9 b' class RepoPullRequestsView(RepoAppView, '
1262 1289 raise HTTPForbidden()
1263 1290
1264 1291 def _edit_pull_request(self, pull_request):
1292 """
1293 Edit title and description
1294 """
1265 1295 _ = self.request.translate
1266 1296
1267 1297 try:
@@ -1302,27 +1332,14 b' class RepoPullRequestsView(RepoAppView, '
1302 1332
1303 1333 msg = _(u'Pull request updated to "{source_commit_id}" with '
1304 1334 u'{count_added} added, {count_removed} removed commits. '
1305 u'Source of changes: {change_source}')
1335 u'Source of changes: {change_source}.')
1306 1336 msg = msg.format(
1307 1337 source_commit_id=pull_request.source_ref_parts.commit_id,
1308 1338 count_added=len(resp.changes.added),
1309 1339 count_removed=len(resp.changes.removed),
1310 1340 change_source=changed)
1311 1341 h.flash(msg, category='success')
1312
1313 message = msg + (
1314 ' - <a onclick="window.location.reload()">'
1315 '<strong>{}</strong></a>'.format(_('Reload page')))
1316
1317 message_obj = {
1318 'message': message,
1319 'level': 'success',
1320 'topic': '/notifications'
1321 }
1322
1323 channelstream.post_message(
1324 c.pr_broadcast_channel, message_obj, self._rhodecode_user.username,
1325 registry=self.request.registry)
1342 self._pr_update_channelstream_push(c.pr_broadcast_channel, msg)
1326 1343 else:
1327 1344 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1328 1345 warning_reasons = [
@@ -1332,6 +1349,53 b' class RepoPullRequestsView(RepoAppView, '
1332 1349 category = 'warning' if resp.reason in warning_reasons else 'error'
1333 1350 h.flash(msg, category=category)
1334 1351
1352 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1353 _ = self.request.translate
1354
1355 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1356 PullRequestModel().get_reviewer_functions()
1357
1358 if role == PullRequestReviewers.ROLE_REVIEWER:
1359 try:
1360 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1361 except ValueError as e:
1362 log.error('Reviewers Validation: {}'.format(e))
1363 h.flash(e, category='error')
1364 return
1365
1366 old_calculated_status = pull_request.calculated_review_status()
1367 PullRequestModel().update_reviewers(
1368 pull_request, reviewers, self._rhodecode_user)
1369
1370 Session().commit()
1371
1372 msg = _('Pull request reviewers updated.')
1373 h.flash(msg, category='success')
1374 self._pr_update_channelstream_push(c.pr_broadcast_channel, msg)
1375
1376 # trigger status changed if change in reviewers changes the status
1377 calculated_status = pull_request.calculated_review_status()
1378 if old_calculated_status != calculated_status:
1379 PullRequestModel().trigger_pull_request_hook(
1380 pull_request, self._rhodecode_user, 'review_status_change',
1381 data={'status': calculated_status})
1382
1383 elif role == PullRequestReviewers.ROLE_OBSERVER:
1384 try:
1385 observers = validate_observers(review_members, reviewer_rules)
1386 except ValueError as e:
1387 log.error('Observers Validation: {}'.format(e))
1388 h.flash(e, category='error')
1389 return
1390
1391 PullRequestModel().update_observers(
1392 pull_request, observers, self._rhodecode_user)
1393
1394 Session().commit()
1395 msg = _('Pull request observers updated.')
1396 h.flash(msg, category='success')
1397 self._pr_update_channelstream_push(c.pr_broadcast_channel, msg)
1398
1335 1399 @LoginRequired()
1336 1400 @NotAnonymous()
1337 1401 @HasRepoPermissionAnyDecorator(
@@ -1408,32 +1472,6 b' class RepoPullRequestsView(RepoAppView, '
1408 1472 msg = merge_resp.merge_status_message
1409 1473 h.flash(msg, category='error')
1410 1474
1411 def _update_reviewers(self, pull_request, review_members, reviewer_rules):
1412 _ = self.request.translate
1413
1414 get_default_reviewers_data, validate_default_reviewers = \
1415 PullRequestModel().get_reviewer_functions()
1416
1417 try:
1418 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1419 except ValueError as e:
1420 log.error('Reviewers Validation: {}'.format(e))
1421 h.flash(e, category='error')
1422 return
1423
1424 old_calculated_status = pull_request.calculated_review_status()
1425 PullRequestModel().update_reviewers(
1426 pull_request, reviewers, self._rhodecode_user)
1427 h.flash(_('Pull request reviewers updated.'), category='success')
1428 Session().commit()
1429
1430 # trigger status changed if change in reviewers changes the status
1431 calculated_status = pull_request.calculated_review_status()
1432 if old_calculated_status != calculated_status:
1433 PullRequestModel().trigger_pull_request_hook(
1434 pull_request, self._rhodecode_user, 'review_status_change',
1435 data={'status': calculated_status})
1436
1437 1475 @LoginRequired()
1438 1476 @NotAnonymous()
1439 1477 @HasRepoPermissionAnyDecorator(
@@ -1488,8 +1526,7 b' class RepoPullRequestsView(RepoAppView, '
1488 1526 allowed_to_comment = PullRequestModel().check_user_comment(
1489 1527 pull_request, self._rhodecode_user)
1490 1528 if not allowed_to_comment:
1491 log.debug(
1492 'comment: forbidden because pull request is from forbidden repo')
1529 log.debug('comment: forbidden because pull request is from forbidden repo')
1493 1530 raise HTTPForbidden()
1494 1531
1495 1532 c = self.load_default_context()
@@ -341,6 +341,10 b' def includeme(config):'
341 341 name='json_ext',
342 342 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
343 343
344 config.add_renderer(
345 name='string_html',
346 factory='rhodecode.lib.string_renderer.html')
347
344 348 # include RhodeCode plugins
345 349 includes = aslist(settings.get('rhodecode.includes', []))
346 350 for inc in includes:
@@ -88,6 +88,9 b' ACTIONS_V1 = {'
88 88 'repo.pull_request.reviewer.add': '',
89 89 'repo.pull_request.reviewer.delete': '',
90 90
91 'repo.pull_request.observer.add': '',
92 'repo.pull_request.observer.delete': '',
93
91 94 'repo.commit.strip': {'commit_id': ''},
92 95 'repo.commit.comment.create': {'data': {}},
93 96 'repo.commit.comment.delete': {'data': {}},
@@ -1104,6 +1104,10 b' def bool2icon(value, show_at_false=True)'
1104 1104 return HTML.tag('i', class_="icon-false", title='False')
1105 1105 return HTML.tag('i')
1106 1106
1107
1108 def b64(inp):
1109 return base64.b64encode(inp)
1110
1107 1111 #==============================================================================
1108 1112 # PERMS
1109 1113 #==============================================================================
@@ -25,7 +25,7 b' import collections'
25 25
26 26 from rhodecode.model import BaseModel
27 27 from rhodecode.model.db import (
28 ChangesetStatus, ChangesetComment, PullRequest, Session)
28 ChangesetStatus, ChangesetComment, PullRequest, PullRequestReviewers, Session)
29 29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
30 30 from rhodecode.lib.markup_renderer import (
31 31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
@@ -383,15 +383,14 b' class ChangesetStatusModel(BaseModel):'
383 383 pull_request.source_repo,
384 384 pull_request=pull_request,
385 385 with_revisions=True)
386 reviewers = pull_request.get_pull_request_reviewers(
387 role=PullRequestReviewers.ROLE_REVIEWER)
388 return self.aggregate_votes_by_user(_commit_statuses, reviewers)
386 389
387 return self.aggregate_votes_by_user(_commit_statuses, pull_request.reviewers)
388
389 def calculated_review_status(self, pull_request, reviewers_statuses=None):
390 def calculated_review_status(self, pull_request):
390 391 """
391 392 calculate pull request status based on reviewers, it should be a list
392 393 of two element lists.
393
394 :param reviewers_statuses:
395 394 """
396 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
395 reviewers = self.reviewers_statuses(pull_request)
397 396 return self.calculate_status(reviewers)
@@ -436,9 +436,8 b' class CommentsModel(BaseModel):'
436 436 'thread_ids': [pr_url, pr_comment_url],
437 437 })
438 438
439 recipients += [self._get_user(u) for u in (extra_recipients or [])]
440
441 439 if send_email:
440 recipients += [self._get_user(u) for u in (extra_recipients or [])]
442 441 # pre-generate the subject for notification itself
443 442 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
444 443 notification_type, **kwargs)
@@ -4465,6 +4465,37 b' class PullRequest(Base, _PullRequestBase'
4465 4465 from rhodecode.model.changeset_status import ChangesetStatusModel
4466 4466 return ChangesetStatusModel().reviewers_statuses(self)
4467 4467
4468 def get_pull_request_reviewers(self, role=None):
4469 qry = PullRequestReviewers.query()\
4470 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4471 if role:
4472 qry = qry.filter(PullRequestReviewers.role == role)
4473
4474 return qry.all()
4475
4476 @property
4477 def reviewers_count(self):
4478 qry = PullRequestReviewers.query()\
4479 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4480 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4481 return qry.count()
4482
4483 @property
4484 def observers_count(self):
4485 qry = PullRequestReviewers.query()\
4486 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4487 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4488 return qry.count()
4489
4490 def observers(self):
4491 qry = PullRequestReviewers.query()\
4492 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4493 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4494 .all()
4495
4496 for entry in qry:
4497 yield entry, entry.user
4498
4468 4499 @property
4469 4500 def workspace_id(self):
4470 4501 from rhodecode.model.pull_request import PullRequestModel
@@ -4530,6 +4561,9 b' class PullRequestVersion(Base, _PullRequ'
4530 4561 def reviewers_statuses(self):
4531 4562 return self.pull_request.reviewers_statuses()
4532 4563
4564 def observer(self):
4565 return self.pull_request.observers()
4566
4533 4567
4534 4568 class PullRequestReviewers(Base, BaseModel):
4535 4569 __tablename__ = 'pull_request_reviewers'
@@ -4538,6 +4572,7 b' class PullRequestReviewers(Base, BaseMod'
4538 4572 )
4539 4573 ROLE_REVIEWER = u'reviewer'
4540 4574 ROLE_OBSERVER = u'observer'
4575 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4541 4576
4542 4577 @hybrid_property
4543 4578 def reasons(self):
@@ -4589,6 +4624,15 b' class PullRequestReviewers(Base, BaseMod'
4589 4624
4590 4625 return user_group_data
4591 4626
4627 @classmethod
4628 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4629 qry = PullRequestReviewers.query()\
4630 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4631 if role:
4632 qry = qry.filter(PullRequestReviewers.role == role)
4633
4634 return qry.all()
4635
4592 4636 def __unicode__(self):
4593 4637 return u"<%s('id:%s')>" % (self.__class__.__name__,
4594 4638 self.pull_requests_reviewers_id)
@@ -4954,16 +4998,21 b' class RepoReviewRuleUser(Base, BaseModel'
4954 4998 __table_args__ = (
4955 4999 base_table_args
4956 5000 )
5001 ROLE_REVIEWER = u'reviewer'
5002 ROLE_OBSERVER = u'observer'
5003 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4957 5004
4958 5005 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4959 5006 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4960 5007 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4961 5008 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5009 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4962 5010 user = relationship('User')
4963 5011
4964 5012 def rule_data(self):
4965 5013 return {
4966 'mandatory': self.mandatory
5014 'mandatory': self.mandatory,
5015 'role': self.role,
4967 5016 }
4968 5017
4969 5018
@@ -4974,17 +5023,22 b' class RepoReviewRuleUserGroup(Base, Base'
4974 5023 )
4975 5024
4976 5025 VOTE_RULE_ALL = -1
5026 ROLE_REVIEWER = u'reviewer'
5027 ROLE_OBSERVER = u'observer'
5028 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4977 5029
4978 5030 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4979 5031 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4980 users_group_id = Column("users_group_id", Integer(),ForeignKey('users_groups.users_group_id'), nullable=False)
5032 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
4981 5033 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5034 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4982 5035 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4983 5036 users_group = relationship('UserGroup')
4984 5037
4985 5038 def rule_data(self):
4986 5039 return {
4987 5040 'mandatory': self.mandatory,
5041 'role': self.role,
4988 5042 'vote_rule': self.vote_rule
4989 5043 }
4990 5044
@@ -601,6 +601,14 b' def PullRequestForm(localizer, repo_id):'
601 601 reasons = All()
602 602 rules = All(v.UniqueList(localizer, convert=int)())
603 603 mandatory = v.StringBoolean()
604 role = v.String(if_missing='reviewer')
605
606 class ObserverForm(formencode.Schema):
607 user_id = v.Int(not_empty=True)
608 reasons = All()
609 rules = All(v.UniqueList(localizer, convert=int)())
610 mandatory = v.StringBoolean()
611 role = v.String(if_missing='observer')
604 612
605 613 class _PullRequestForm(formencode.Schema):
606 614 allow_extra_fields = True
@@ -614,6 +622,7 b' def PullRequestForm(localizer, repo_id):'
614 622 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
615 623 v.UniqueList(localizer)(not_empty=True))
616 624 review_members = formencode.ForEach(ReviewerForm())
625 observer_members = formencode.ForEach(ObserverForm())
617 626 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
618 627 pullrequest_desc = v.UnicodeString(strip=True, required=False)
619 628 description_renderer = v.UnicodeString(strip=True, required=False)
@@ -575,7 +575,7 b' class PullRequestModel(BaseModel):'
575 575 pull_request_display_obj, at_version
576 576
577 577 def create(self, created_by, source_repo, source_ref, target_repo,
578 target_ref, revisions, reviewers, title, description=None,
578 target_ref, revisions, reviewers, observers, title, description=None,
579 579 common_ancestor_id=None,
580 580 description_renderer=None,
581 581 reviewer_data=None, translator=None, auth_user=None):
@@ -606,7 +606,7 b' class PullRequestModel(BaseModel):'
606 606 reviewer_ids = set()
607 607 # members / reviewers
608 608 for reviewer_object in reviewers:
609 user_id, reasons, mandatory, rules = reviewer_object
609 user_id, reasons, mandatory, role, rules = reviewer_object
610 610 user = self._get_user(user_id)
611 611
612 612 # skip duplicates
@@ -620,6 +620,7 b' class PullRequestModel(BaseModel):'
620 620 reviewer.pull_request = pull_request
621 621 reviewer.reasons = reasons
622 622 reviewer.mandatory = mandatory
623 reviewer.role = role
623 624
624 625 # NOTE(marcink): pick only first rule for now
625 626 rule_id = list(rules)[0] if rules else None
@@ -653,6 +654,33 b' class PullRequestModel(BaseModel):'
653 654 Session().add(reviewer)
654 655 Session().flush()
655 656
657 for observer_object in observers:
658 user_id, reasons, mandatory, role, rules = observer_object
659 user = self._get_user(user_id)
660
661 # skip duplicates from reviewers
662 if user.user_id in reviewer_ids:
663 continue
664
665 #reviewer_ids.add(user.user_id)
666
667 observer = PullRequestReviewers()
668 observer.user = user
669 observer.pull_request = pull_request
670 observer.reasons = reasons
671 observer.mandatory = mandatory
672 observer.role = role
673
674 # NOTE(marcink): pick only first rule for now
675 rule_id = list(rules)[0] if rules else None
676 rule = RepoReviewRule.get(rule_id) if rule_id else None
677 if rule:
678 # TODO(marcink): do we need this for observers ??
679 pass
680
681 Session().add(observer)
682 Session().flush()
683
656 684 # Set approval status to "Under Review" for all commits which are
657 685 # part of this pull request.
658 686 ChangesetStatusModel().set_status(
@@ -1204,23 +1232,25 b' class PullRequestModel(BaseModel):'
1204 1232
1205 1233 :param pull_request: the pr to update
1206 1234 :param reviewer_data: list of tuples
1207 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1235 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1236 :param user: current use who triggers this action
1208 1237 """
1238
1209 1239 pull_request = self.__get_pull_request(pull_request)
1210 1240 if pull_request.is_closed():
1211 1241 raise ValueError('This pull request is closed')
1212 1242
1213 1243 reviewers = {}
1214 for user_id, reasons, mandatory, rules in reviewer_data:
1244 for user_id, reasons, mandatory, role, rules in reviewer_data:
1215 1245 if isinstance(user_id, (int, compat.string_types)):
1216 1246 user_id = self._get_user(user_id).user_id
1217 1247 reviewers[user_id] = {
1218 'reasons': reasons, 'mandatory': mandatory}
1248 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1219 1249
1220 1250 reviewers_ids = set(reviewers.keys())
1221 current_reviewers = PullRequestReviewers.query()\
1222 .filter(PullRequestReviewers.pull_request ==
1223 pull_request).all()
1251 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1252 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1253
1224 1254 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1225 1255
1226 1256 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
@@ -1241,16 +1271,19 b' class PullRequestModel(BaseModel):'
1241 1271 reviewer.reasons = reviewers[uid]['reasons']
1242 1272 # NOTE(marcink): mandatory shouldn't be changed now
1243 1273 # reviewer.mandatory = reviewers[uid]['reasons']
1274 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1275 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1244 1276 Session().add(reviewer)
1245 1277 added_audit_reviewers.append(reviewer.get_dict())
1246 1278
1247 1279 for uid in ids_to_remove:
1248 1280 changed = True
1249 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1250 # that prevents and fixes cases that we added the same reviewer twice.
1281 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1282 # This is an edge case that handles previous state of having the same reviewer twice.
1251 1283 # this CAN happen due to the lack of DB checks
1252 1284 reviewers = PullRequestReviewers.query()\
1253 1285 .filter(PullRequestReviewers.user_id == uid,
1286 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1254 1287 PullRequestReviewers.pull_request == pull_request)\
1255 1288 .all()
1256 1289
@@ -1273,7 +1306,90 b' class PullRequestModel(BaseModel):'
1273 1306 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1274 1307 user, pull_request)
1275 1308
1276 self.notify_reviewers(pull_request, ids_to_add)
1309 self.notify_reviewers(pull_request, ids_to_add, user.get_instance())
1310 return ids_to_add, ids_to_remove
1311
1312 def update_observers(self, pull_request, observer_data, user):
1313 """
1314 Update the observers in the pull request
1315
1316 :param pull_request: the pr to update
1317 :param observer_data: list of tuples
1318 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1319 :param user: current use who triggers this action
1320 """
1321 pull_request = self.__get_pull_request(pull_request)
1322 if pull_request.is_closed():
1323 raise ValueError('This pull request is closed')
1324
1325 observers = {}
1326 for user_id, reasons, mandatory, role, rules in observer_data:
1327 if isinstance(user_id, (int, compat.string_types)):
1328 user_id = self._get_user(user_id).user_id
1329 observers[user_id] = {
1330 'reasons': reasons, 'observers': mandatory, 'role': role}
1331
1332 observers_ids = set(observers.keys())
1333 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1334 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1335
1336 current_observers_ids = set([x.user.user_id for x in current_observers])
1337
1338 ids_to_add = observers_ids.difference(current_observers_ids)
1339 ids_to_remove = current_observers_ids.difference(observers_ids)
1340
1341 log.debug("Adding %s observer", ids_to_add)
1342 log.debug("Removing %s observer", ids_to_remove)
1343 changed = False
1344 added_audit_observers = []
1345 removed_audit_observers = []
1346
1347 for uid in ids_to_add:
1348 changed = True
1349 _usr = self._get_user(uid)
1350 observer = PullRequestReviewers()
1351 observer.user = _usr
1352 observer.pull_request = pull_request
1353 observer.reasons = observers[uid]['reasons']
1354 # NOTE(marcink): mandatory shouldn't be changed now
1355 # observer.mandatory = observer[uid]['reasons']
1356
1357 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1358 observer.role = PullRequestReviewers.ROLE_OBSERVER
1359 Session().add(observer)
1360 added_audit_observers.append(observer.get_dict())
1361
1362 for uid in ids_to_remove:
1363 changed = True
1364 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1365 # This is an edge case that handles previous state of having the same reviewer twice.
1366 # this CAN happen due to the lack of DB checks
1367 observers = PullRequestReviewers.query()\
1368 .filter(PullRequestReviewers.user_id == uid,
1369 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1370 PullRequestReviewers.pull_request == pull_request)\
1371 .all()
1372
1373 for obj in observers:
1374 added_audit_observers.append(obj.get_dict())
1375 Session().delete(obj)
1376
1377 if changed:
1378 Session().expire_all()
1379 pull_request.updated_on = datetime.datetime.now()
1380 Session().add(pull_request)
1381
1382 # finally store audit logs
1383 for user_data in added_audit_observers:
1384 self._log_audit_action(
1385 'repo.pull_request.observer.add', {'data': user_data},
1386 user, pull_request)
1387 for user_data in removed_audit_observers:
1388 self._log_audit_action(
1389 'repo.pull_request.observer.delete', {'old_data': user_data},
1390 user, pull_request)
1391
1392 self.notify_observers(pull_request, ids_to_add, user.get_instance())
1277 1393 return ids_to_add, ids_to_remove
1278 1394
1279 1395 def get_url(self, pull_request, request=None, permalink=False):
@@ -1301,16 +1417,16 b' class PullRequestModel(BaseModel):'
1301 1417 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1302 1418 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1303 1419
1304 def notify_reviewers(self, pull_request, reviewers_ids):
1305 # notification to reviewers
1306 if not reviewers_ids:
1420 def _notify_reviewers(self, pull_request, user_ids, role, user):
1421 # notification to reviewers/observers
1422 if not user_ids:
1307 1423 return
1308 1424
1309 log.debug('Notify following reviewers about pull-request %s', reviewers_ids)
1425 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1310 1426
1311 1427 pull_request_obj = pull_request
1312 1428 # get the current participants of this pull request
1313 recipients = reviewers_ids
1429 recipients = user_ids
1314 1430 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1315 1431
1316 1432 pr_source_repo = pull_request_obj.source_repo
@@ -1332,8 +1448,10 b' class PullRequestModel(BaseModel):'
1332 1448 (x.raw_id, x.message)
1333 1449 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1334 1450
1451 current_rhodecode_user = user
1335 1452 kwargs = {
1336 'user': pull_request.author,
1453 'user': current_rhodecode_user,
1454 'pull_request_author': pull_request.author,
1337 1455 'pull_request': pull_request_obj,
1338 1456 'pull_request_commits': pull_request_commits,
1339 1457
@@ -1345,6 +1463,7 b' class PullRequestModel(BaseModel):'
1345 1463
1346 1464 'pull_request_url': pr_url,
1347 1465 'thread_ids': [pr_url],
1466 'user_role': role
1348 1467 }
1349 1468
1350 1469 # pre-generate the subject for notification itself
@@ -1353,7 +1472,7 b' class PullRequestModel(BaseModel):'
1353 1472
1354 1473 # create notification objects, and emails
1355 1474 NotificationModel().create(
1356 created_by=pull_request.author,
1475 created_by=current_rhodecode_user,
1357 1476 notification_subject=subject,
1358 1477 notification_body=body_plaintext,
1359 1478 notification_type=notification_type,
@@ -1361,6 +1480,14 b' class PullRequestModel(BaseModel):'
1361 1480 email_kwargs=kwargs,
1362 1481 )
1363 1482
1483 def notify_reviewers(self, pull_request, reviewers_ids, user):
1484 return self._notify_reviewers(pull_request, reviewers_ids,
1485 PullRequestReviewers.ROLE_REVIEWER, user)
1486
1487 def notify_observers(self, pull_request, observers_ids, user):
1488 return self._notify_reviewers(pull_request, observers_ids,
1489 PullRequestReviewers.ROLE_OBSERVER, user)
1490
1364 1491 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1365 1492 commit_changes, file_changes):
1366 1493
@@ -1874,11 +2001,13 b' class PullRequestModel(BaseModel):'
1874 2001 try:
1875 2002 from rc_reviewers.utils import get_default_reviewers_data
1876 2003 from rc_reviewers.utils import validate_default_reviewers
2004 from rc_reviewers.utils import validate_observers
1877 2005 except ImportError:
1878 2006 from rhodecode.apps.repository.utils import get_default_reviewers_data
1879 2007 from rhodecode.apps.repository.utils import validate_default_reviewers
2008 from rhodecode.apps.repository.utils import validate_observers
1880 2009
1881 return get_default_reviewers_data, validate_default_reviewers
2010 return get_default_reviewers_data, validate_default_reviewers, validate_observers
1882 2011
1883 2012
1884 2013 class MergeCheck(object):
@@ -1700,8 +1700,33 b' table.group_members {'
1700 1700 }
1701 1701
1702 1702 .reviewer_ac .ac-input {
1703 width: 98%;
1704 margin-bottom: 1em;
1705 }
1706
1707 .observer_ac .ac-input {
1708 width: 98%;
1709 margin-bottom: 1em;
1710 }
1711
1712 .rule-table {
1703 1713 width: 100%;
1704 margin-bottom: 1em;
1714 }
1715
1716 .rule-table td {
1717
1718 }
1719
1720 .rule-table .td-role {
1721 width: 100px
1722 }
1723
1724 .rule-table .td-mandatory {
1725 width: 100px
1726 }
1727
1728 .rule-table .td-group-votes {
1729 width: 150px
1705 1730 }
1706 1731
1707 1732 .compare_view_commits tr{
@@ -94,21 +94,26 b' var getTitleAndDescription = function(so'
94 94 };
95 95
96 96
97 ReviewersController = function () {
97 window.ReviewersController = function () {
98 98 var self = this;
99 this.$loadingIndicator = $('.calculate-reviewers');
99 100 this.$reviewRulesContainer = $('#review_rules');
100 101 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
101 102 this.$userRule = $('.pr-user-rule-container');
102 this.forbidReviewUsers = undefined;
103 103 this.$reviewMembers = $('#review_members');
104 this.$observerMembers = $('#observer_members');
105
104 106 this.currentRequest = null;
105 107 this.diffData = null;
106 108 this.enabledRules = [];
109 // sync with db.py entries
110 this.ROLE_REVIEWER = 'reviewer';
111 this.ROLE_OBSERVER = 'observer'
107 112
108 113 //dummy handler, we might register our own later
109 this.diffDataHandler = function(data){};
114 this.diffDataHandler = function (data) {};
110 115
111 this.defaultForbidReviewUsers = function () {
116 this.defaultForbidUsers = function () {
112 117 return [
113 118 {
114 119 'username': 'default',
@@ -117,6 +122,9 b' ReviewersController = function () {'
117 122 ];
118 123 };
119 124
125 // init default forbidden users
126 this.forbidUsers = this.defaultForbidUsers();
127
120 128 this.hideReviewRules = function () {
121 129 self.$reviewRulesContainer.hide();
122 130 $(self.$userRule.selector).hide();
@@ -133,11 +141,40 b' ReviewersController = function () {'
133 141 return '<div>- {0}</div>'.format(ruleText)
134 142 };
135 143
144 this.increaseCounter = function(role) {
145 if (role === self.ROLE_REVIEWER) {
146 var $elem = $('#reviewers-cnt')
147 var cnt = parseInt($elem.data('count') || 0)
148 cnt +=1
149 $elem.html(cnt);
150 $elem.data('count', cnt);
151 }
152 else if (role === self.ROLE_OBSERVER) {
153 var $elem = $('#observers-cnt');
154 var cnt = parseInt($elem.data('count') || 0)
155 cnt +=1
156 $elem.html(cnt);
157 $elem.data('count', cnt);
158 }
159 }
160
161 this.resetCounter = function () {
162 var $elem = $('#reviewers-cnt');
163
164 $elem.data('count', 0);
165 $elem.html(0);
166
167 var $elem = $('#observers-cnt');
168
169 $elem.data('count', 0);
170 $elem.html(0);
171 }
172
136 173 this.loadReviewRules = function (data) {
137 174 self.diffData = data;
138 175
139 176 // reset forbidden Users
140 this.forbidReviewUsers = self.defaultForbidReviewUsers();
177 this.forbidUsers = self.defaultForbidUsers();
141 178
142 179 // reset state of review rules
143 180 self.$rulesList.html('');
@@ -148,7 +185,7 b' ReviewersController = function () {'
148 185 self.addRule(
149 186 _gettext('All reviewers must vote.'))
150 187 );
151 return self.forbidReviewUsers
188 return self.forbidUsers
152 189 }
153 190
154 191 if (data.rules.voting !== undefined) {
@@ -195,7 +232,7 b' ReviewersController = function () {'
195 232 }
196 233
197 234 if (data.rules.forbid_author_to_review) {
198 self.forbidReviewUsers.push(data.rules_data.pr_author);
235 self.forbidUsers.push(data.rules_data.pr_author);
199 236 self.$rulesList.append(
200 237 self.addRule(
201 238 _gettext('Author is not allowed to be a reviewer.'))
@@ -206,9 +243,8 b' ReviewersController = function () {'
206 243
207 244 if (data.rules_data.forbidden_users) {
208 245 $.each(data.rules_data.forbidden_users, function (index, member_data) {
209 self.forbidReviewUsers.push(member_data)
246 self.forbidUsers.push(member_data)
210 247 });
211
212 248 }
213 249
214 250 self.$rulesList.append(
@@ -223,9 +259,31 b' ReviewersController = function () {'
223 259 _gettext('No review rules set.'))
224 260 }
225 261
226 return self.forbidReviewUsers
262 return self.forbidUsers
227 263 };
228 264
265 this.emptyTables = function () {
266 self.emptyReviewersTable();
267 self.emptyObserversTable();
268
269 // Also reset counters.
270 self.resetCounter();
271 }
272
273 this.emptyReviewersTable = function (withText) {
274 self.$reviewMembers.empty();
275 if (withText !== undefined) {
276 self.$reviewMembers.html(withText)
277 }
278 };
279
280 this.emptyObserversTable = function (withText) {
281 self.$observerMembers.empty();
282 if (withText !== undefined) {
283 self.$observerMembers.html(withText)
284 }
285 }
286
229 287 this.loadDefaultReviewers = function (sourceRepo, sourceRef, targetRepo, targetRef) {
230 288
231 289 if (self.currentRequest) {
@@ -233,19 +291,21 b' ReviewersController = function () {'
233 291 self.currentRequest.abort();
234 292 }
235 293
236 $('.calculate-reviewers').show();
237 // reset reviewer members
238 self.$reviewMembers.empty();
294 self.$loadingIndicator.show();
295
296 // reset reviewer/observe members
297 self.emptyTables();
239 298
240 299 prButtonLock(true, null, 'reviewers');
241 300 $('#user').hide(); // hide user autocomplete before load
301 $('#observer').hide(); //hide observer autocomplete before load
242 302
243 303 // lock PR button, so we cannot send PR before it's calculated
244 304 prButtonLock(true, _gettext('Loading diff ...'), 'compare');
245 305
246 306 if (sourceRef.length !== 3 || targetRef.length !== 3) {
247 307 // don't load defaults in case we're missing some refs...
248 $('.calculate-reviewers').hide();
308 self.$loadingIndicator.hide();
249 309 return
250 310 }
251 311
@@ -272,11 +332,16 b' ReviewersController = function () {'
272 332
273 333 for (var i = 0; i < data.reviewers.length; i++) {
274 334 var reviewer = data.reviewers[i];
275 self.addReviewMember(reviewer, reviewer.reasons, reviewer.mandatory);
335 // load reviewer rules from the repo data
336 self.addMember(reviewer, reviewer.reasons, reviewer.mandatory, reviewer.role);
276 337 }
277 $('.calculate-reviewers').hide();
338
339
340 self.$loadingIndicator.hide();
278 341 prButtonLock(false, null, 'reviewers');
279 $('#user').show(); // show user autocomplete after load
342
343 $('#user').show(); // show user autocomplete before load
344 $('#observer').show(); // show observer autocomplete before load
280 345
281 346 var commitElements = data["diff_info"]['commits'];
282 347
@@ -292,7 +357,7 b' ReviewersController = function () {'
292 357
293 358 },
294 359 error: function (jqXHR, textStatus, errorThrown) {
295 var prefix = "Loading diff and reviewers failed\n"
360 var prefix = "Loading diff and reviewers/observers failed\n"
296 361 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
297 362 ajaxErrorSwal(message);
298 363 }
@@ -301,7 +366,7 b' ReviewersController = function () {'
301 366 };
302 367
303 368 // check those, refactor
304 this.removeReviewMember = function (reviewer_id, mark_delete) {
369 this.removeMember = function (reviewer_id, mark_delete) {
305 370 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
306 371
307 372 if (typeof (mark_delete) === undefined) {
@@ -312,6 +377,7 b' ReviewersController = function () {'
312 377 if (reviewer) {
313 378 // now delete the input
314 379 $('#reviewer_{0} input'.format(reviewer_id)).remove();
380 $('#reviewer_{0}_rules input'.format(reviewer_id)).remove();
315 381 // mark as to-delete
316 382 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
317 383 obj.addClass('to-delete');
@@ -322,27 +388,26 b' ReviewersController = function () {'
322 388 }
323 389 };
324 390
325 this.reviewMemberEntry = function () {
391 this.addMember = function (reviewer_obj, reasons, mandatory, role) {
326 392
327 };
328
329 this.addReviewMember = function (reviewer_obj, reasons, mandatory) {
330 393 var id = reviewer_obj.user_id;
331 394 var username = reviewer_obj.username;
332 395
333 var reasons = reasons || [];
334 var mandatory = mandatory || false;
396 reasons = reasons || [];
397 mandatory = mandatory || false;
398 role = role || self.ROLE_REVIEWER
335 399
336 // register IDS to check if we don't have this ID already in
400 // register current set IDS to check if we don't have this ID already in
401 // and prevent duplicates
337 402 var currentIds = [];
338 403
339 $.each(self.$reviewMembers.find('.reviewer_entry'), function (index, value) {
404 $.each($('.reviewer_entry'), function (index, value) {
340 405 currentIds.push($(value).data('reviewerUserId'))
341 406 })
342 407
343 408 var userAllowedReview = function (userId) {
344 409 var allowed = true;
345 $.each(self.forbidReviewUsers, function (index, member_data) {
410 $.each(self.forbidUsers, function (index, member_data) {
346 411 if (parseInt(userId) === member_data['user_id']) {
347 412 allowed = false;
348 413 return false // breaks the loop
@@ -352,6 +417,7 b' ReviewersController = function () {'
352 417 };
353 418
354 419 var userAllowed = userAllowedReview(id);
420
355 421 if (!userAllowed) {
356 422 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
357 423 } else {
@@ -359,11 +425,13 b' ReviewersController = function () {'
359 425 var alreadyReviewer = currentIds.indexOf(id) != -1;
360 426
361 427 if (alreadyReviewer) {
362 alert(_gettext('User `{0}` already in reviewers').format(username));
428 alert(_gettext('User `{0}` already in reviewers/observers').format(username));
363 429 } else {
430
364 431 var reviewerEntry = renderTemplate('reviewMemberEntry', {
365 432 'member': reviewer_obj,
366 433 'mandatory': mandatory,
434 'role': role,
367 435 'reasons': reasons,
368 436 'allowed_to_update': true,
369 437 'review_status': 'not_reviewed',
@@ -372,16 +440,32 b' ReviewersController = function () {'
372 440 'create': true,
373 441 'rule_show': true,
374 442 })
375 $(self.$reviewMembers.selector).append(reviewerEntry);
443
444 if (role === self.ROLE_REVIEWER) {
445 $(self.$reviewMembers.selector).append(reviewerEntry);
446 self.increaseCounter(self.ROLE_REVIEWER);
447 $('#reviewer-empty-msg').remove()
448 }
449 else if (role === self.ROLE_OBSERVER) {
450 $(self.$observerMembers.selector).append(reviewerEntry);
451 self.increaseCounter(self.ROLE_OBSERVER);
452 $('#observer-empty-msg').remove();
453 }
454
376 455 tooltipActivate();
377 456 }
378 457 }
379 458
380 459 };
381 460
382 this.updateReviewers = function (repo_name, pull_request_id) {
383 var postData = $('#reviewers input').serialize();
384 _updatePullRequest(repo_name, pull_request_id, postData);
461 this.updateReviewers = function (repo_name, pull_request_id, role) {
462 if (role === 'reviewer') {
463 var postData = $('#reviewers input').serialize();
464 _updatePullRequest(repo_name, pull_request_id, postData);
465 } else if (role === 'observer') {
466 var postData = $('#observers input').serialize();
467 _updatePullRequest(repo_name, pull_request_id, postData);
468 }
385 469 };
386 470
387 471 this.handleDiffData = function (data) {
@@ -449,35 +533,26 b' var editPullRequest = function(repo_name'
449 533
450 534
451 535 /**
452 * Reviewer autocomplete
536 * autocomplete handler for reviewers/observers
453 537 */
454 var ReviewerAutoComplete = function(inputId) {
455 $(inputId).autocomplete({
456 serviceUrl: pyroutes.url('user_autocomplete_data'),
457 minChars:2,
458 maxHeight:400,
459 deferRequestBy: 300, //miliseconds
460 showNoSuggestionNotice: true,
461 tabDisabled: true,
462 autoSelectFirst: true,
463 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
464 formatResult: autocompleteFormatResult,
465 lookupFilter: autocompleteFilterResult,
466 onSelect: function(element, data) {
538 var autoCompleteHandler = function (inputId, controller, role) {
539
540 return function (element, data) {
467 541 var mandatory = false;
468 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
542 var reasons = [_gettext('added manually by "{0}"').format(
543 templateContext.rhodecode_user.username)];
469 544
470 545 // add whole user groups
471 546 if (data.value_type == 'user_group') {
472 547 reasons.push(_gettext('member of "{0}"').format(data.value_display));
473 548
474 $.each(data.members, function(index, member_data) {
549 $.each(data.members, function (index, member_data) {
475 550 var reviewer = member_data;
476 551 reviewer['user_id'] = member_data['id'];
477 552 reviewer['gravatar_link'] = member_data['icon_link'];
478 553 reviewer['user_link'] = member_data['profile_link'];
479 554 reviewer['rules'] = [];
480 reviewersController.addReviewMember(reviewer, reasons, mandatory);
555 controller.addMember(reviewer, reasons, mandatory, role);
481 556 })
482 557 }
483 558 // add single user
@@ -487,14 +562,71 b' var ReviewerAutoComplete = function(inpu'
487 562 reviewer['gravatar_link'] = data['icon_link'];
488 563 reviewer['user_link'] = data['profile_link'];
489 564 reviewer['rules'] = [];
490 reviewersController.addReviewMember(reviewer, reasons, mandatory);
565 controller.addMember(reviewer, reasons, mandatory, role);
491 566 }
492 567
493 $(inputId).val('');
568 $(inputId).val('');
494 569 }
495 });
570 }
571
572 /**
573 * Reviewer autocomplete
574 */
575 var ReviewerAutoComplete = function (inputId, controller) {
576 var self = this;
577 self.controller = controller;
578 self.inputId = inputId;
579 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_REVIEWER);
580
581 $(inputId).autocomplete({
582 serviceUrl: pyroutes.url('user_autocomplete_data'),
583 minChars: 2,
584 maxHeight: 400,
585 deferRequestBy: 300, //miliseconds
586 showNoSuggestionNotice: true,
587 tabDisabled: true,
588 autoSelectFirst: true,
589 params: {
590 user_id: templateContext.rhodecode_user.user_id,
591 user_groups: true,
592 user_groups_expand: true,
593 skip_default_user: true
594 },
595 formatResult: autocompleteFormatResult,
596 lookupFilter: autocompleteFilterResult,
597 onSelect: handler
598 });
496 599 };
497 600
601 /**
602 * Observers autocomplete
603 */
604 var ObserverAutoComplete = function(inputId, controller) {
605 var self = this;
606 self.controller = controller;
607 self.inputId = inputId;
608 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_OBSERVER);
609
610 $(inputId).autocomplete({
611 serviceUrl: pyroutes.url('user_autocomplete_data'),
612 minChars: 2,
613 maxHeight: 400,
614 deferRequestBy: 300, //miliseconds
615 showNoSuggestionNotice: true,
616 tabDisabled: true,
617 autoSelectFirst: true,
618 params: {
619 user_id: templateContext.rhodecode_user.user_id,
620 user_groups: true,
621 user_groups_expand: true,
622 skip_default_user: true
623 },
624 formatResult: autocompleteFormatResult,
625 lookupFilter: autocompleteFilterResult,
626 onSelect: handler
627 });
628 }
629
498 630
499 631 window.VersionController = function () {
500 632 var self = this;
@@ -504,7 +636,7 b' window.VersionController = function () {'
504 636
505 637 this.adjustRadioSelectors = function (curNode) {
506 638 var getVal = function (item) {
507 if (item == 'latest') {
639 if (item === 'latest') {
508 640 return Number.MAX_SAFE_INTEGER
509 641 }
510 642 else {
@@ -663,6 +795,7 b' window.UpdatePrController = function () '
663 795 };
664 796 };
665 797
798
666 799 /**
667 800 * Reviewer display panel
668 801 */
@@ -702,26 +835,37 b' window.ReviewersPanel = {'
702 835 },
703 836
704 837 renderReviewers: function () {
838 if (this.setReviewers.reviewers === undefined) {
839 return
840 }
841 if (this.setReviewers.reviewers.length === 0) {
842 reviewersController.emptyReviewersTable('<tr id="reviewer-empty-msg"><td colspan="6">No reviewers</td></tr>');
843 return
844 }
705 845
706 $('#review_members').html('')
846 reviewersController.emptyReviewersTable();
847
707 848 $.each(this.setReviewers.reviewers, function (key, val) {
708 var member = val;
709 849
710 var entry = renderTemplate('reviewMemberEntry', {
711 'member': member,
712 'mandatory': member.mandatory,
713 'reasons': member.reasons,
714 'allowed_to_update': member.allowed_to_update,
715 'review_status': member.review_status,
716 'review_status_label': member.review_status_label,
717 'user_group': member.user_group,
718 'create': false
719 });
850 var member = val;
851 if (member.role === reviewersController.ROLE_REVIEWER) {
852 var entry = renderTemplate('reviewMemberEntry', {
853 'member': member,
854 'mandatory': member.mandatory,
855 'role': member.role,
856 'reasons': member.reasons,
857 'allowed_to_update': member.allowed_to_update,
858 'review_status': member.review_status,
859 'review_status_label': member.review_status_label,
860 'user_group': member.user_group,
861 'create': false
862 });
720 863
721 $('#review_members').append(entry)
864 $(reviewersController.$reviewMembers.selector).append(entry)
865 }
722 866 });
867
723 868 tooltipActivate();
724
725 869 },
726 870
727 871 edit: function (event) {
@@ -739,10 +883,142 b' window.ReviewersPanel = {'
739 883 this.addButton.hide();
740 884 $(this.removeButtons.selector).css('visibility', 'hidden');
741 885 // hide review rules
742 reviewersController.hideReviewRules()
886 reviewersController.hideReviewRules();
743 887 }
744 888 };
745 889
890 /**
891 * Reviewer display panel
892 */
893 window.ObserversPanel = {
894 editButton: null,
895 closeButton: null,
896 addButton: null,
897 removeButtons: null,
898 reviewRules: null,
899 setReviewers: null,
900
901 setSelectors: function () {
902 var self = this;
903 self.editButton = $('#open_edit_observers');
904 self.closeButton =$('#close_edit_observers');
905 self.addButton = $('#add_observer');
906 self.removeButtons = $('.observer_member_remove,.observer_member_mandatory_remove');
907 },
908
909 init: function (reviewRules, setReviewers) {
910 var self = this;
911 self.setSelectors();
912
913 this.reviewRules = reviewRules;
914 this.setReviewers = setReviewers;
915
916 this.editButton.on('click', function (e) {
917 self.edit();
918 });
919 this.closeButton.on('click', function (e) {
920 self.close();
921 self.renderObservers();
922 });
923
924 self.renderObservers();
925
926 },
927
928 renderObservers: function () {
929 if (this.setReviewers.observers === undefined) {
930 return
931 }
932 if (this.setReviewers.observers.length === 0) {
933 reviewersController.emptyObserversTable('<tr id="observer-empty-msg"><td colspan="6">No observers</td></tr>');
934 return
935 }
936
937 reviewersController.emptyObserversTable();
938
939 $.each(this.setReviewers.observers, function (key, val) {
940 var member = val;
941 if (member.role === reviewersController.ROLE_OBSERVER) {
942 var entry = renderTemplate('reviewMemberEntry', {
943 'member': member,
944 'mandatory': member.mandatory,
945 'role': member.role,
946 'reasons': member.reasons,
947 'allowed_to_update': member.allowed_to_update,
948 'review_status': member.review_status,
949 'review_status_label': member.review_status_label,
950 'user_group': member.user_group,
951 'create': false
952 });
953
954 $(reviewersController.$observerMembers.selector).append(entry)
955 }
956 });
957
958 tooltipActivate();
959 },
960
961 edit: function (event) {
962 this.editButton.hide();
963 this.closeButton.show();
964 this.addButton.show();
965 $(this.removeButtons.selector).css('visibility', 'visible');
966 },
967
968 close: function (event) {
969 this.editButton.show();
970 this.closeButton.hide();
971 this.addButton.hide();
972 $(this.removeButtons.selector).css('visibility', 'hidden');
973 }
974
975 };
976
977 window.PRDetails = {
978 editButton: null,
979 closeButton: null,
980 deleteButton: null,
981 viewFields: null,
982 editFields: null,
983
984 setSelectors: function () {
985 var self = this;
986 self.editButton = $('#open_edit_pullrequest')
987 self.closeButton = $('#close_edit_pullrequest')
988 self.deleteButton = $('#delete_pullrequest')
989 self.viewFields = $('#pr-desc, #pr-title')
990 self.editFields = $('#pr-desc-edit, #pr-title-edit, .pr-save')
991 },
992
993 init: function () {
994 var self = this;
995 self.setSelectors();
996 self.editButton.on('click', function (e) {
997 self.edit();
998 });
999 self.closeButton.on('click', function (e) {
1000 self.view();
1001 });
1002 },
1003
1004 edit: function (event) {
1005 var cmInstance = $('#pr-description-input').get(0).MarkupForm.cm;
1006 this.viewFields.hide();
1007 this.editButton.hide();
1008 this.deleteButton.hide();
1009 this.closeButton.show();
1010 this.editFields.show();
1011 cmInstance.refresh();
1012 },
1013
1014 view: function (event) {
1015 this.editButton.show();
1016 this.deleteButton.show();
1017 this.editFields.hide();
1018 this.closeButton.hide();
1019 this.viewFields.show();
1020 }
1021 };
746 1022
747 1023 /**
748 1024 * OnLine presence using channelstream
@@ -813,29 +1089,29 b' window.refreshComments = function (versi'
813 1089 $.each($('.comment'), function (idx, element) {
814 1090 currentIDs.push($(element).data('commentId'));
815 1091 });
816 var data = {"comments[]": currentIDs};
1092 var data = {"comments": currentIDs};
817 1093
818 1094 var $targetElem = $('.comments-content-table');
819 1095 $targetElem.css('opacity', 0.3);
820 $targetElem.load(
821 loadUrl, data, function (responseText, textStatus, jqXHR) {
822 if (jqXHR.status !== 200) {
823 return false;
824 }
825 var $counterElem = $('#comments-count');
826 var newCount = $(responseText).data('counter');
827 if (newCount !== undefined) {
828 var callback = function () {
829 $counterElem.animate({'opacity': 1.00}, 200)
830 $counterElem.html(newCount);
831 };
832 $counterElem.animate({'opacity': 0.15}, 200, callback);
833 }
834 1096
835 $targetElem.css('opacity', 1);
836 tooltipActivate();
1097 var success = function (data) {
1098 var $counterElem = $('#comments-count');
1099 var newCount = $(data).data('counter');
1100 if (newCount !== undefined) {
1101 var callback = function () {
1102 $counterElem.animate({'opacity': 1.00}, 200)
1103 $counterElem.html(newCount);
1104 };
1105 $counterElem.animate({'opacity': 0.15}, 200, callback);
837 1106 }
838 );
1107
1108 $targetElem.css('opacity', 1);
1109 $targetElem.html(data);
1110 tooltipActivate();
1111 }
1112
1113 ajaxPOST(loadUrl, data, success, null, {})
1114
839 1115 }
840 1116
841 1117 window.refreshTODOs = function (version) {
@@ -858,28 +1134,28 b' window.refreshTODOs = function (version)'
858 1134 currentIDs.push($(element).data('commentId'));
859 1135 });
860 1136
861 var data = {"comments[]": currentIDs};
1137 var data = {"comments": currentIDs};
862 1138 var $targetElem = $('.todos-content-table');
863 1139 $targetElem.css('opacity', 0.3);
864 $targetElem.load(
865 loadUrl, data, function (responseText, textStatus, jqXHR) {
866 if (jqXHR.status !== 200) {
867 return false;
868 }
869 var $counterElem = $('#todos-count')
870 var newCount = $(responseText).data('counter');
871 if (newCount !== undefined) {
872 var callback = function () {
873 $counterElem.animate({'opacity': 1.00}, 200)
874 $counterElem.html(newCount);
875 };
876 $counterElem.animate({'opacity': 0.15}, 200, callback);
877 }
878 1140
879 $targetElem.css('opacity', 1);
880 tooltipActivate();
1141 var success = function (data) {
1142 var $counterElem = $('#todos-count')
1143 var newCount = $(data).data('counter');
1144 if (newCount !== undefined) {
1145 var callback = function () {
1146 $counterElem.animate({'opacity': 1.00}, 200)
1147 $counterElem.html(newCount);
1148 };
1149 $counterElem.animate({'opacity': 0.15}, 200, callback);
881 1150 }
882 );
1151
1152 $targetElem.css('opacity', 1);
1153 $targetElem.html(data);
1154 tooltipActivate();
1155 }
1156
1157 ajaxPOST(loadUrl, data, success, null, {})
1158
883 1159 }
884 1160
885 1161 window.refreshAllComments = function (version) {
@@ -888,3 +1164,12 b' window.refreshAllComments = function (ve'
888 1164 refreshComments(version);
889 1165 refreshTODOs(version);
890 1166 };
1167
1168 window.sidebarComment = function (commentId) {
1169 var jsonData = $('#commentHovercard{0}'.format(commentId)).data('commentJsonB64');
1170 if (!jsonData) {
1171 return 'Failed to load comment {0}'.format(commentId)
1172 }
1173 var funcData = JSON.parse(atob(jsonData));
1174 return renderTemplate('sideBarCommentHovercard', funcData)
1175 };
@@ -57,15 +57,18 b' var ajaxGET = function (url, success, fa'
57 57 return request;
58 58 };
59 59
60 var ajaxPOST = function (url, postData, success, failure) {
61 var sUrl = url;
62 var postData = toQueryString(postData);
63 var request = $.ajax({
60 var ajaxPOST = function (url, postData, success, failure, options) {
61
62 var ajaxSettings = $.extend({
64 63 type: 'POST',
65 url: sUrl,
66 data: postData,
64 url: url,
65 data: toQueryString(postData),
67 66 headers: {'X-PARTIAL-XHR': true}
68 })
67 }, options);
68
69 var request = $.ajax(
70 ajaxSettings
71 )
69 72 .done(function (data) {
70 73 success(data);
71 74 })
@@ -126,7 +129,8 b' function formatErrorMessage(jqXHR, textS'
126 129 } else if (errorThrown === 'abort') {
127 130 return (prefix + 'Ajax request aborted.');
128 131 } else {
129 return (prefix + 'Uncaught Error.\n' + jqXHR.responseText);
132 var errInfo = 'Uncaught Error. code: {0}\n'.format(jqXHR.status)
133 return (prefix + errInfo + jqXHR.responseText);
130 134 }
131 135 }
132 136
@@ -89,36 +89,41 b''
89 89 if is_pr:
90 90 version_info = (' made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else ' made in this version')
91 91 %>
92
93 <script type="text/javascript">
94 // closure function helper
95 var sidebarComment${comment_obj.comment_id} = function() {
96 return renderTemplate('sideBarCommentHovercard', {
97 version_info: "${version_info}",
98 file_name: "${comment_obj.f_path}",
99 line_no: "${comment_obj.line_no}",
100 outdated: ${h.json.dumps(comment_obj.outdated)},
101 inline: ${h.json.dumps(comment_obj.is_inline)},
102 is_todo: ${h.json.dumps(comment_obj.is_todo)},
103 created_on: "${h.format_date(comment_obj.created_on)}",
104 datetime: "${comment_obj.created_on}${h.get_timezone(comment_obj.created_on, time_is_local=True)}",
105 review_status: "${(comment_obj.review_status or '')}"
106 })
107 }
108 </script>
109
110 % if comment_obj.outdated:
111 <i class="icon-comment-toggle tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
112 % elif comment_obj.is_inline:
113 <i class="icon-code tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
114 % else:
115 <i class="icon-comment tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
92 ## NEW, since refresh
93 % if existing_ids and comment_obj.comment_id not in existing_ids:
94 <div class="tooltip" style="position: absolute; left: 8px" title="New comment">
95 !
96 </div>
116 97 % endif
117 98
118 ## NEW, since refresh
119 % if existing_ids and comment_obj.comment_id not in existing_ids:
120 <span class="tag">NEW</span>
121 % endif
99 <%
100 data = h.json.dumps({
101 'comment_id': comment_obj.comment_id,
102 'version_info': version_info,
103 'file_name': comment_obj.f_path,
104 'line_no': comment_obj.line_no,
105 'outdated': comment_obj.outdated,
106 'inline': comment_obj.is_inline,
107 'is_todo': comment_obj.is_todo,
108 'created_on': h.format_date(comment_obj.created_on),
109 'datetime': '{}{}'.format(comment_obj.created_on, h.get_timezone(comment_obj.created_on, time_is_local=True)),
110 'review_status': (comment_obj.review_status or '')
111 })
112
113 if comment_obj.outdated:
114 icon = 'icon-comment-toggle'
115 elif comment_obj.is_inline:
116 icon = 'icon-code'
117 else:
118 icon = 'icon-comment'
119 %>
120
121 <i id="commentHovercard${comment_obj.comment_id}"
122 class="${icon} tooltip-hovercard"
123 data-hovercard-url="javascript:sidebarComment(${comment_obj.comment_id})"
124 data-comment-json-b64='${h.b64(data)}'>
125 </i>
126
122 127 </td>
123 128
124 129 <td class="td-todo-gravatar">
@@ -187,12 +187,12 b''
187 187 <div class="sidebar-element clear-both">
188 188 <% vote_title = _ungettext(
189 189 'Status calculated based on votes from {} reviewer',
190 'Status calculated based on votes from {} reviewers', len(c.allowed_reviewers)).format(len(c.allowed_reviewers))
190 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
191 191 %>
192 192
193 193 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
194 194 <i class="icon-circle review-status-${c.commit_review_status}"></i>
195 ${len(c.allowed_reviewers)}
195 ${c.reviewers_count}
196 196 </div>
197 197 </div>
198 198
@@ -149,7 +149,7 b''
149 149 <span class="user"> <a href="/_profiles/jenkins-tests">jenkins-tests</a> (reviewer)</span>
150 150 </div>
151 151 <input id="reviewer_70_input" type="hidden" value="70" name="review_members">
152 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(70, true)" style="visibility: hidden;">
152 <div class="reviewer_member_remove action_button" onclick="removeMember(70, true)" style="visibility: hidden;">
153 153 <i class="icon-remove"></i>
154 154 </div>
155 155 </li>
@@ -66,9 +66,18 b" var data_hovercard_url = pyroutes.url('h"
66 66 <tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
67 67
68 68 <td style="width: 20px">
69 <div class="tooltip presence-state" style="display: none; position: absolute; left: 2px" title="This users is currently at this page">
70 <i class="icon-eye" style="color: #0ac878"></i>
71 </div>
72 <% if (role === 'reviewer') { %>
69 73 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
70 74 <i class="icon-circle review-status-<%= review_status %>"></i>
71 75 </div>
76 <% } else if (role === 'observer') { %>
77 <div class="tooltip" title="Observer without voting right.">
78 <i class="icon-circle-thin"></i>
79 </div>
80 <% } %>
72 81 </td>
73 82
74 83 <td>
@@ -84,9 +93,6 b" var data_hovercard_url = pyroutes.url('h"
84 93 'gravatar_url': member.gravatar_link
85 94 })
86 95 %>
87 <span class="tooltip presence-state" style="display: none" title="This users is currently at this page">
88 <i class="icon-eye" style="color: #0ac878"></i>
89 </span>
90 96 </div>
91 97 </td>
92 98
@@ -108,7 +114,7 b" var data_hovercard_url = pyroutes.url('h"
108 114 <% } else { %>
109 115 <td style="text-align: right;width: 10px;">
110 116 <% if (allowed_to_update) { %>
111 <div class="reviewer_member_remove" onclick="reviewersController.removeReviewMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
117 <div class="<%=role %>_member_remove" onclick="reviewersController.removeMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
112 118 <i class="icon-remove"></i>
113 119 </div>
114 120 <% } %>
@@ -117,7 +123,7 b" var data_hovercard_url = pyroutes.url('h"
117 123
118 124 </tr>
119 125
120 <tr>
126 <tr id="reviewer_<%= member.user_id %>_rules">
121 127 <td colspan="4" style="display: <%= rule_visibility %>" class="pr-user-rule-container">
122 128 <input type="hidden" name="__start__" value="reviewer:mapping">
123 129
@@ -149,6 +155,7 b" var data_hovercard_url = pyroutes.url('h"
149 155
150 156 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
151 157 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
158 <input type="hidden" name="role" value="<%= role %>"/>
152 159
153 160 <input type="hidden" name="__end__" value="reviewer:mapping">
154 161 </td>
@@ -11,7 +11,10 b' data = {'
11 11 'pr_title': pull_request.title,
12 12 }
13 13
14 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
14 if user_role == 'observer':
15 subject_template = email_pr_review_subject_template or _('{user} added you as observer to pull request. !{pr_id}: "{pr_title}"')
16 else:
17 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
15 18 %>
16 19
17 20 ${subject_template.format(**data) |n}
@@ -34,6 +37,7 b' data = {'
34 37 'source_repo_url': pull_request_source_repo_url,
35 38 'target_repo_url': pull_request_target_repo_url,
36 39 }
40
37 41 %>
38 42
39 43 * ${_('Pull Request link')}: ${pull_request_url}
@@ -51,7 +55,7 b' data = {'
51 55
52 56 % for commit_id, message in pull_request_commits:
53 57 - ${h.short_id(commit_id)}
54 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
58 ${h.chop_at_smart(message.lstrip(), '\n', suffix_if_chopped='...')}
55 59
56 60 % endfor
57 61
@@ -78,19 +82,23 b' data = {'
78 82 <table style="text-align:left;vertical-align:middle;width: 100%">
79 83 <tr>
80 84 <td style="width:100%;border-bottom:1px solid #dbd9da;">
81
82 85 <div style="margin: 0; font-weight: bold">
86 % if user_role == 'observer':
87 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
88 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
89 ${_('added you as observer to')}
90 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a>.
91 </div>
92 % else:
83 93 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
84 94 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
85 95 ${_('requested a')}
86 <a href="${pull_request_url}" style="${base.link_css()}">
87 ${_('pull request review.').format(**data) }
88 </a>
96 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a> review.
89 97 </div>
98 % endif
90 99 <div style="margin-top: 10px"></div>
91 100 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
92 101 </div>
93
94 102 </td>
95 103 </tr>
96 104
@@ -112,7 +112,7 b''
112 112 ## REVIEWERS
113 113 <div class="field">
114 114 <div class="label label-textarea">
115 <label for="pullrequest_reviewers">${_('Reviewers')}:</label>
115 <label for="pullrequest_reviewers">${_('Reviewers / Observers')}:</label>
116 116 </div>
117 117 <div class="content">
118 118 ## REVIEW RULES
@@ -125,29 +125,79 b''
125 125 </div>
126 126 </div>
127 127
128 ## REVIEWERS
128 ## REVIEWERS / OBSERVERS
129 129 <div class="reviewers-title">
130 <div class="pr-details-title">
131 ${_('Pull request reviewers')}
132 <span class="calculate-reviewers"> - ${_('loading...')}</span>
133 </div>
134 </div>
135 <div id="reviewers" class="pr-details-content reviewers">
136 ## members goes here, filled via JS based on initial selection !
137 <input type="hidden" name="__start__" value="review_members:sequence">
138 <table id="review_members" class="group_members">
139 ## This content is loaded via JS and ReviewersPanel
140 </table>
141 <input type="hidden" name="__end__" value="review_members:sequence">
130
131 <ul class="nav-links clearfix">
132
133 ## TAB1 MANDATORY REVIEWERS
134 <li class="active">
135 <a id="reviewers-btn" href="#showReviewers" tabindex="-1">
136 Reviewers
137 <span id="reviewers-cnt" data-count="0" class="menulink-counter">0</span>
138 </a>
139 </li>
140
141 ## TAB2 OBSERVERS
142 <li class="">
143 <a id="observers-btn" href="#showObservers" tabindex="-1">
144 Observers
145 <span id="observers-cnt" data-count="0" class="menulink-counter">0</span>
146 </a>
147 </li>
148
149 </ul>
142 150
143 <div id="add_reviewer_input" class='ac'>
144 <div class="reviewer_ac">
145 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
146 <div id="reviewers_container"></div>
151 ## TAB1 MANDATORY REVIEWERS
152 <div id="reviewers-container">
153 <span class="calculate-reviewers">
154 <h4>${_('loading...')}</h4>
155 </span>
156
157 <div id="reviewers" class="pr-details-content reviewers">
158 ## members goes here, filled via JS based on initial selection !
159 <input type="hidden" name="__start__" value="review_members:sequence">
160 <table id="review_members" class="group_members">
161 ## This content is loaded via JS and ReviewersPanel, an sets reviewer_entry class on each element
162 </table>
163 <input type="hidden" name="__end__" value="review_members:sequence">
164
165 <div id="add_reviewer_input" class='ac'>
166 <div class="reviewer_ac">
167 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
168 <div id="reviewers_container"></div>
169 </div>
170 </div>
171
147 172 </div>
148 173 </div>
149 174
175 ## TAB2 OBSERVERS
176 <div id="observers-container" style="display: none">
177 <span class="calculate-reviewers">
178 <h4>${_('loading...')}</h4>
179 </span>
180
181 <div id="observers" class="pr-details-content observers">
182 ## members goes here, filled via JS based on initial selection !
183 <input type="hidden" name="__start__" value="observer_members:sequence">
184 <table id="observer_members" class="group_members">
185 ## This content is loaded via JS and ReviewersPanel, an sets reviewer_entry class on each element
186 </table>
187 <input type="hidden" name="__end__" value="observer_members:sequence">
188
189 <div id="add_observer_input" class='ac'>
190 <div class="observer_ac">
191 ${h.text('observer', class_='ac-input', placeholder=_('Add observer or observer group'))}
192 <div id="observers_container"></div>
193 </div>
194 </div>
195 </div>
196
197 </div>
198
150 199 </div>
200
151 201 </div>
152 202 </div>
153 203
@@ -339,7 +389,6 b''
339 389
340 390 //make both panels equal
341 391 $('.target-panel').height($('.source-panel').height())
342
343 392 };
344 393
345 394 reviewersController = new ReviewersController();
@@ -465,8 +514,7 b''
465 514 queryTargetRefs(initialData, query)
466 515 },
467 516 initSelection: initRefSelection()
468 }
469 );
517 });
470 518
471 519 var sourceRepoSelect2 = Select2Box($sourceRepo, {
472 520 query: function(query) {}
@@ -521,12 +569,12 b''
521 569
522 570 });
523 571
524 $pullRequestForm.on('submit', function(e){
525 // Flush changes into textarea
526 codeMirrorInstance.save();
527 prButtonLock(true, null, 'all');
528 $pullRequestSubmit.val(_gettext('Please wait creating pull request...'));
529 });
572 $pullRequestForm.on('submit', function(e){
573 // Flush changes into textarea
574 codeMirrorInstance.save();
575 prButtonLock(true, null, 'all');
576 $pullRequestSubmit.val(_gettext('Please wait creating pull request...'));
577 });
530 578
531 579 prButtonLock(true, "${_('Please select source and target')}", 'all');
532 580
@@ -543,12 +591,44 b''
543 591 $sourceRef.select2('val', '${c.default_source_ref}');
544 592
545 593
546 // default reviewers
594 // default reviewers / observers
547 595 reviewersController.loadDefaultReviewers(
548 596 sourceRepo(), sourceRef(), targetRepo(), targetRef());
549 597 % endif
550 598
551 ReviewerAutoComplete('#user');
599 ReviewerAutoComplete('#user', reviewersController);
600 ObserverAutoComplete('#observer', reviewersController);
601
602 // TODO, move this to another handler
603
604 var $reviewersBtn = $('#reviewers-btn');
605 var $reviewersContainer = $('#reviewers-container');
606
607 var $observersBtn = $('#observers-btn')
608 var $observersContainer = $('#observers-container');
609
610 $reviewersBtn.on('click', function (e) {
611
612 $observersContainer.hide();
613 $reviewersContainer.show();
614
615 $observersBtn.parent().removeClass('active');
616 $reviewersBtn.parent().addClass('active');
617 e.preventDefault();
618
619 })
620
621 $observersBtn.on('click', function (e) {
622
623 $reviewersContainer.hide();
624 $observersContainer.show();
625
626 $reviewersBtn.parent().removeClass('active');
627 $observersBtn.parent().addClass('active');
628 e.preventDefault();
629
630 })
631
552 632 });
553 633 </script>
554 634
@@ -556,12 +556,12 b''
556 556 <div class="sidebar-element clear-both">
557 557 <% vote_title = _ungettext(
558 558 'Status calculated based on votes from {} reviewer',
559 'Status calculated based on votes from {} reviewers', len(c.allowed_reviewers)).format(len(c.allowed_reviewers))
559 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
560 560 %>
561 561
562 562 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
563 563 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
564 ${len(c.allowed_reviewers)}
564 ${c.reviewers_count}
565 565 </div>
566 566
567 567 ## REVIEW RULES
@@ -609,13 +609,13 b''
609 609 <div id="add_reviewer" class="ac" style="display: none;">
610 610 %if c.allowed_to_update:
611 611 % if not c.forbid_adding_reviewers:
612 <div id="add_reviewer_input" class="reviewer_ac">
613 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
612 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px">
613 <input class="ac-input" id="user" name="user" placeholder="${_('Add reviewer or reviewer group')}" type="text" autocomplete="off">
614 614 <div id="reviewers_container"></div>
615 615 </div>
616 616 % endif
617 <div class="pull-right">
618 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
617 <div class="pull-right" style="margin-bottom: 15px">
618 <button data-role="reviewer" id="update_reviewers" class="btn btn-small no-margin">${_('Save Changes')}</button>
619 619 </div>
620 620 %endif
621 621 </div>
@@ -623,23 +623,52 b''
623 623 </div>
624 624 </div>
625 625
626 ## ## OBSERVERS
627 ## <div class="sidebar-element clear-both">
628 ## <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Observers')}">
629 ## <i class="icon-eye"></i>
630 ## 0
631 ## </div>
632 ##
633 ## <div class="right-sidebar-expanded-state pr-details-title">
634 ## <span class="sidebar-heading">
635 ## <i class="icon-eye"></i>
636 ## ${_('Observers')}
637 ## </span>
638 ## </div>
639 ## <div class="right-sidebar-expanded-state pr-details-content">
640 ## No observers
641 ## </div>
642 ## </div>
626 ## OBSERVERS
627 <div class="sidebar-element clear-both">
628 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Observers')}">
629 <i class="icon-circle-thin"></i>
630 ${c.observers_count}
631 </div>
632
633 <div class="right-sidebar-expanded-state pr-details-title">
634 <span class="sidebar-heading">
635 <i class="icon-circle-thin"></i>
636 ${_('Observers')}
637 </span>
638 %if c.allowed_to_update:
639 <span id="open_edit_observers" class="block-right action_button last-item">${_('Edit')}</span>
640 <span id="close_edit_observers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
641 %endif
642 </div>
643
644 <div id="observers" class="right-sidebar-expanded-state pr-details-content reviewers">
645 ## members redering block
646 <input type="hidden" name="__start__" value="observer_members:sequence">
647
648 <table id="observer_members" class="group_members">
649 ## This content is loaded via JS and ReviewersPanel
650 </table>
651
652 <input type="hidden" name="__end__" value="observer_members:sequence">
653 ## end members redering block
654
655 %if not c.pull_request.is_closed():
656 <div id="add_observer" class="ac" style="display: none;">
657 %if c.allowed_to_update:
658 % if not c.forbid_adding_reviewers or 1:
659 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px" >
660 <input class="ac-input" id="observer" name="observer" placeholder="${_('Add observer or observer group')}" type="text" autocomplete="off">
661 <div id="observers_container"></div>
662 </div>
663 % endif
664 <div class="pull-right" style="margin-bottom: 15px">
665 <button data-role="observer" id="update_observers" class="btn btn-small no-margin">${_('Save Changes')}</button>
666 </div>
667 %endif
668 </div>
669 %endif
670 </div>
671 </div>
643 672
644 673 ## TODOs
645 674 <div class="sidebar-element clear-both">
@@ -815,6 +844,7 b' updateController = new UpdatePrControlle'
815 844
816 845 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
817 846 window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
847 window.setObserversData = ${c.pull_request_set_observers_data_json | n};
818 848
819 849 (function () {
820 850 "use strict";
@@ -822,44 +852,9 b' window.setReviewersData = ${c.pull_reque'
822 852 // custom code mirror
823 853 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
824 854
825 var PRDetails = {
826 editButton: $('#open_edit_pullrequest'),
827 closeButton: $('#close_edit_pullrequest'),
828 deleteButton: $('#delete_pullrequest'),
829 viewFields: $('#pr-desc, #pr-title'),
830 editFields: $('#pr-desc-edit, #pr-title-edit, .pr-save'),
831
832 init: function () {
833 var that = this;
834 this.editButton.on('click', function (e) {
835 that.edit();
836 });
837 this.closeButton.on('click', function (e) {
838 that.view();
839 });
840 },
841
842 edit: function (event) {
843 var cmInstance = $('#pr-description-input').get(0).MarkupForm.cm;
844 this.viewFields.hide();
845 this.editButton.hide();
846 this.deleteButton.hide();
847 this.closeButton.show();
848 this.editFields.show();
849 cmInstance.refresh();
850 },
851
852 view: function (event) {
853 this.editButton.show();
854 this.deleteButton.show();
855 this.editFields.hide();
856 this.closeButton.hide();
857 this.viewFields.show();
858 }
859 };
860
861 855 PRDetails.init();
862 856 ReviewersPanel.init(reviewerRulesData, setReviewersData);
857 ObserversPanel.init(reviewerRulesData, setObserversData);
863 858
864 859 window.showOutdated = function (self) {
865 860 $('.comment-inline.comment-outdated').show();
@@ -929,12 +924,17 b' window.setReviewersData = ${c.pull_reque'
929 924 title, description, renderer);
930 925 });
931 926
932 $('#update_pull_request').on('click', function (e) {
933 $(this).attr('disabled', 'disabled');
934 $(this).addClass('disabled');
935 $(this).html(_gettext('Saving...'));
927 var $updateButtons = $('#update_reviewers,#update_observers');
928 $updateButtons.on('click', function (e) {
929 var role = $(this).data('role');
930 $updateButtons.attr('disabled', 'disabled');
931 $updateButtons.addClass('disabled');
932 $updateButtons.html(_gettext('Saving...'));
936 933 reviewersController.updateReviewers(
937 "${c.repo_name}", "${c.pull_request.pull_request_id}");
934 templateContext.repo_name,
935 templateContext.pull_request_data.pull_request_id,
936 role
937 );
938 938 });
939 939
940 940 // fixing issue with caches on firefox
@@ -978,7 +978,8 b' window.setReviewersData = ${c.pull_reque'
978 978 refreshMergeChecks();
979 979 };
980 980
981 ReviewerAutoComplete('#user');
981 ReviewerAutoComplete('#user', reviewersController);
982 ObserverAutoComplete('#observer', reviewersController);
982 983
983 984 })();
984 985
@@ -23,7 +23,7 b' import collections'
23 23
24 24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
25 25 from rhodecode.lib.utils2 import AttributeDict
26 from rhodecode.model.db import User
26 from rhodecode.model.db import User, PullRequestReviewers
27 27 from rhodecode.model.notification import EmailNotificationModel
28 28
29 29
@@ -52,7 +52,8 b' def test_render_email(app, http_host_onl'
52 52 assert 'Email Body' in body
53 53
54 54
55 def test_render_pr_email(app, user_admin):
55 @pytest.mark.parametrize('role', PullRequestReviewers.ROLES)
56 def test_render_pr_email(app, user_admin, role):
56 57 ref = collections.namedtuple(
57 58 'Ref', 'name, type')('fxies123', 'book')
58 59
@@ -75,13 +76,17 b' def test_render_pr_email(app, user_admin'
75 76 'pull_request_source_repo_url': 'x',
76 77
77 78 'pull_request_url': 'http://localhost/pr1',
79 'user_role': role,
78 80 }
79 81
80 82 subject, body, body_plaintext = EmailNotificationModel().render_email(
81 83 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
82 84
83 85 # subject
84 assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
86 if role == PullRequestReviewers.ROLE_REVIEWER:
87 assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
88 elif role == PullRequestReviewers.ROLE_OBSERVER:
89 assert subject == '@test_admin (RhodeCode Admin) added you as observer to pull request. !200: "Example Pull Request"'
85 90
86 91
87 92 def test_render_pr_update_email(app, user_admin):
General Comments 0
You need to be logged in to leave comments. Login now