##// 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 EXTENSIONS = {}
48 EXTENSIONS = {}
49
49
50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
51 __dbversion__ = 109 # defines current db version for migrations
51 __dbversion__ = 110 # defines current db version for migrations
52 __platform__ = platform.system()
52 __platform__ = platform.system()
53 __license__ = 'AGPLv3, and Commercial License'
53 __license__ = 'AGPLv3, and Commercial License'
54 __author__ = 'RhodeCode GmbH'
54 __author__ = 'RhodeCode GmbH'
@@ -704,7 +704,7 b' def create_pull_request('
704 user = get_user_or_error(reviewer_object['username'])
704 user = get_user_or_error(reviewer_object['username'])
705 reviewer_object['user_id'] = user.user_id
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 PullRequestModel().get_reviewer_functions()
708 PullRequestModel().get_reviewer_functions()
709
709
710 # recalculate reviewers logic, to make sure we can validate this
710 # recalculate reviewers logic, to make sure we can validate this
@@ -865,14 +865,13 b' def update_pull_request('
865 user = get_user_or_error(reviewer_object['username'])
865 user = get_user_or_error(reviewer_object['username'])
866 reviewer_object['user_id'] = user.user_id
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 PullRequestModel().get_reviewer_functions()
869 PullRequestModel().get_reviewer_functions()
870
870
871 # re-use stored rules
871 # re-use stored rules
872 reviewer_rules = pull_request.reviewer_data
872 reviewer_rules = pull_request.reviewer_data
873 try:
873 try:
874 reviewers = get_validated_reviewers(
874 reviewers = get_validated_reviewers(reviewer_objects, reviewer_rules)
875 reviewer_objects, reviewer_rules)
876 except ValueError as e:
875 except ValueError as e:
877 raise JSONRPCError('Reviewers Validation: {}'.format(e))
876 raise JSONRPCError('Reviewers Validation: {}'.format(e))
878 else:
877 else:
@@ -34,6 +34,7 b' log = logging.getLogger(__name__)'
34
34
35
35
36 class DebugStyleView(BaseAppView):
36 class DebugStyleView(BaseAppView):
37
37 def load_default_context(self):
38 def load_default_context(self):
38 c = self._get_local_tmpl_context()
39 c = self._get_local_tmpl_context()
39
40
@@ -75,6 +76,7 b' Check if we should use full-topic or min'
75 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
76 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
76 target_ref_parts=AttributeDict(type='branch', name='master'),
77 target_ref_parts=AttributeDict(type='branch', name='master'),
77 )
78 )
79
78 target_repo = AttributeDict(repo_name='repo_group/target_repo')
80 target_repo = AttributeDict(repo_name='repo_group/target_repo')
79 source_repo = AttributeDict(repo_name='repo_group/source_repo')
81 source_repo = AttributeDict(repo_name='repo_group/source_repo')
80 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
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 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
85 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
84 'removed': ['eeeeeeeeeee'],
86 'removed': ['eeeeeeeeeee'],
85 })
87 })
88
86 file_changes = AttributeDict({
89 file_changes = AttributeDict({
87 'added': ['a/file1.md', 'file2.py'],
90 'added': ['a/file1.md', 'file2.py'],
88 'modified': ['b/modified_file.rst'],
91 'modified': ['b/modified_file.rst'],
@@ -97,15 +100,19 b' Check if we should use full-topic or min'
97 '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',
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 'exc_type': 'AttributeError'
101 'exc_type': 'AttributeError'
99 }
102 }
103
100 email_kwargs = {
104 email_kwargs = {
101 'test': {},
105 'test': {},
106
102 'message': {
107 'message': {
103 'body': 'message body !'
108 'body': 'message body !'
104 },
109 },
110
105 'email_test': {
111 'email_test': {
106 'user': user,
112 'user': user,
107 'date': datetime.datetime.now(),
113 'date': datetime.datetime.now(),
108 },
114 },
115
109 'exception': {
116 'exception': {
110 'email_prefix': '[RHODECODE ERROR]',
117 'email_prefix': '[RHODECODE ERROR]',
111 'exc_id': exc_traceback['exc_id'],
118 'exc_id': exc_traceback['exc_id'],
@@ -113,6 +120,7 b' Check if we should use full-topic or min'
113 'exc_type_name': 'NameError',
120 'exc_type_name': 'NameError',
114 'exc_traceback': exc_traceback,
121 'exc_traceback': exc_traceback,
115 },
122 },
123
116 'password_reset': {
124 'password_reset': {
117 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
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 'email': 'test@rhodecode.com',
129 'email': 'test@rhodecode.com',
122 'first_admin_email': User.get_first_super_admin().email
130 'first_admin_email': User.get_first_super_admin().email
123 },
131 },
132
124 'password_reset_confirmation': {
133 'password_reset_confirmation': {
125 'new_password': 'new-password-example',
134 'new_password': 'new-password-example',
126 'user': user,
135 'user': user,
@@ -128,6 +137,7 b' Check if we should use full-topic or min'
128 'email': 'test@rhodecode.com',
137 'email': 'test@rhodecode.com',
129 'first_admin_email': User.get_first_super_admin().email
138 'first_admin_email': User.get_first_super_admin().email
130 },
139 },
140
131 'registration': {
141 'registration': {
132 'user': user,
142 'user': user,
133 'date': datetime.datetime.now(),
143 'date': datetime.datetime.now(),
@@ -161,6 +171,7 b' Check if we should use full-topic or min'
161 'mention': True,
171 'mention': True,
162
172
163 },
173 },
174
164 'pull_request_comment+status': {
175 'pull_request_comment+status': {
165 'user': user,
176 'user': user,
166
177
@@ -201,6 +212,7 b' def db():'
201 'mention': True,
212 'mention': True,
202
213
203 },
214 },
215
204 'pull_request_comment+file': {
216 'pull_request_comment+file': {
205 'user': user,
217 'user': user,
206
218
@@ -303,6 +315,7 b' This should work better !'
303 'renderer_type': 'markdown',
315 'renderer_type': 'markdown',
304 'mention': True,
316 'mention': True,
305 },
317 },
318
306 'cs_comment+status': {
319 'cs_comment+status': {
307 'user': user,
320 'user': user,
308 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
321 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
@@ -328,6 +341,7 b' This is a multiline comment :)'
328 'renderer_type': 'markdown',
341 'renderer_type': 'markdown',
329 'mention': True,
342 'mention': True,
330 },
343 },
344
331 'cs_comment+file': {
345 'cs_comment+file': {
332 'user': user,
346 'user': user,
333 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
347 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
@@ -348,12 +362,37 b' This is a multiline comment :)'
348 'renderer_type': 'markdown',
362 'renderer_type': 'markdown',
349 'mention': True,
363 'mention': True,
350 },
364 },
351
365
352 'pull_request': {
366 'pull_request': {
353 'user': user,
367 'user': user,
354 'pull_request': pr,
368 'pull_request': pr,
355 'pull_request_commits': [
369 'pull_request_commits': [
356 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
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 my-account: moved email closer to profile as it's similar data just moved outside.
396 my-account: moved email closer to profile as it's similar data just moved outside.
358 '''),
397 '''),
359 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
398 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
@@ -371,8 +410,33 b' users: description edit fixes'
371 'pull_request_source_repo_url': 'http://source-repo/url',
410 'pull_request_source_repo_url': 'http://source-repo/url',
372
411
373 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
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 template_type = email_id.split('+')[0]
442 template_type = email_id.split('+')[0]
@@ -401,6 +465,7 b' users: description edit fixes'
401 c = self.load_default_context()
465 c = self.load_default_context()
402 c.active = os.path.splitext(t_path)[0]
466 c.active = os.path.splitext(t_path)[0]
403 c.came_from = ''
467 c.came_from = ''
468 # NOTE(marcink): extend the email types with variations based on data sets
404 c.email_types = {
469 c.email_types = {
405 'cs_comment+file': {},
470 'cs_comment+file': {},
406 'cs_comment+status': {},
471 'cs_comment+status': {},
@@ -409,6 +474,9 b' users: description edit fixes'
409 'pull_request_comment+status': {},
474 'pull_request_comment+status': {},
410
475
411 'pull_request_update': {},
476 'pull_request_update': {},
477
478 'pull_request+reviewer_role': {},
479 'pull_request+observer_role': {},
412 }
480 }
413 c.email_types.update(EmailNotificationModel.email_types)
481 c.email_types.update(EmailNotificationModel.email_types)
414
482
@@ -21,11 +21,13 b''
21 from rhodecode.lib import helpers as h
21 from rhodecode.lib import helpers as h
22 from rhodecode.lib.utils2 import safe_int
22 from rhodecode.lib.utils2 import safe_int
23 from rhodecode.model.pull_request import get_diff_info
23 from rhodecode.model.pull_request import get_diff_info
24
24 from rhodecode.model.db import PullRequestReviewers
25 REVIEWER_API_VERSION = 'V3'
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 Returns json struct of a reviewer for frontend
32 Returns json struct of a reviewer for frontend
31
33
@@ -33,11 +35,15 b' def reviewer_as_json(user, reasons=None,'
33 :param reasons: list of strings of why they are reviewers
35 :param reasons: list of strings of why they are reviewers
34 :param mandatory: bool, to set user as mandatory
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 return {
42 return {
38 'user_id': user.user_id,
43 'user_id': user.user_id,
39 'reasons': reasons or [],
44 'reasons': reasons or [],
40 'rules': rules or [],
45 'rules': rules or [],
46 'role': role,
41 'mandatory': mandatory,
47 'mandatory': mandatory,
42 'user_group': user_group,
48 'user_group': user_group,
43 'username': user.username,
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(
57 def get_default_reviewers_data(current_user, source_repo, source_commit, target_repo, target_commit):
52 current_user, source_repo, source_commit, target_repo, target_commit):
53 """
58 """
54 Return json for default reviewers of a repository
59 Return json for default reviewers of a repository
55 """
60 """
@@ -59,7 +64,7 b' def get_default_reviewers_data('
59
64
60 reasons = ['Default reviewer', 'Repository owner']
65 reasons = ['Default reviewer', 'Repository owner']
61 json_reviewers = [reviewer_as_json(
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 return {
69 return {
65 'api_ver': REVIEWER_API_VERSION, # define version for later possible schema upgrade
70 'api_ver': REVIEWER_API_VERSION, # define version for later possible schema upgrade
@@ -73,15 +78,18 b' def get_default_reviewers_data('
73 def validate_default_reviewers(review_members, reviewer_rules):
78 def validate_default_reviewers(review_members, reviewer_rules):
74 """
79 """
75 Function to validate submitted reviewers against the saved rules
80 Function to validate submitted reviewers against the saved rules
76
77 """
81 """
78 reviewers = []
82 reviewers = []
79 reviewer_by_id = {}
83 reviewer_by_id = {}
80 for r in review_members:
84 for r in review_members:
81 reviewer_user_id = safe_int(r['user_id'])
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 reviewer_by_id[reviewer_user_id] = entry
88 reviewer_by_id[reviewer_user_id] = entry
85 reviewers.append(entry)
89 reviewers.append(entry)
86
90
87 return reviewers
91 return reviewers
92
93
94 def validate_observers(observer_members):
95 return {}
@@ -193,7 +193,7 b' class RepoCommitsView(RepoAppView):'
193
193
194 for review_obj, member, reasons, mandatory, status in review_statuses:
194 for review_obj, member, reasons, mandatory, status in review_statuses:
195 member_reviewer = h.reviewer_as_json(
195 member_reviewer = h.reviewer_as_json(
196 member, reasons=reasons, mandatory=mandatory,
196 member, reasons=reasons, mandatory=mandatory, role=None,
197 user_group=None
197 user_group=None
198 )
198 )
199
199
@@ -39,14 +39,15 b' from rhodecode.lib.ext_json import json'
39 from rhodecode.lib.auth import (
39 from rhodecode.lib.auth import (
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 NotAnonymous, CSRFRequired)
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 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
44 from rhodecode.lib.vcs.exceptions import (
44 from rhodecode.lib.vcs.exceptions import (
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
45 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 from rhodecode.model.changeset_status import ChangesetStatusModel
46 from rhodecode.model.changeset_status import ChangesetStatusModel
47 from rhodecode.model.comment import CommentsModel
47 from rhodecode.model.comment import CommentsModel
48 from rhodecode.model.db import (
48 from rhodecode.model.db import (
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
50 PullRequestReviewers)
50 from rhodecode.model.forms import PullRequestForm
51 from rhodecode.model.forms import PullRequestForm
51 from rhodecode.model.meta import Session
52 from rhodecode.model.meta import Session
52 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
53 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
@@ -455,14 +456,18 b' class RepoPullRequestsView(RepoAppView, '
455 return self._get_template_context(c)
456 return self._get_template_context(c)
456
457
457 c.allowed_reviewers = [obj.user_id for obj in pull_request.reviewers if obj.user]
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 # reviewers and statuses
462 # reviewers and statuses
460 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
463 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
461 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
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 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
467 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
464 member_reviewer = h.reviewer_as_json(
468 member_reviewer = h.reviewer_as_json(
465 member, reasons=reasons, mandatory=mandatory,
469 member, reasons=reasons, mandatory=mandatory,
470 role=review_obj.role,
466 user_group=review_obj.rule_user_group_data()
471 user_group=review_obj.rule_user_group_data()
467 )
472 )
468
473
@@ -474,6 +479,17 b' class RepoPullRequestsView(RepoAppView, '
474
479
475 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
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 general_comments, inline_comments = \
493 general_comments, inline_comments = \
478 self.register_comments_vars(c, pull_request_latest, versions)
494 self.register_comments_vars(c, pull_request_latest, versions)
479
495
@@ -967,7 +983,7 b' class RepoPullRequestsView(RepoAppView, '
967 'repository.read', 'repository.write', 'repository.admin')
983 'repository.read', 'repository.write', 'repository.admin')
968 @view_config(
984 @view_config(
969 route_name='pullrequest_comments', request_method='POST',
985 route_name='pullrequest_comments', request_method='POST',
970 renderer='string', xhr=True)
986 renderer='string_html', xhr=True)
971 def pullrequest_comments(self):
987 def pullrequest_comments(self):
972 self.load_default_context()
988 self.load_default_context()
973
989
@@ -998,7 +1014,8 b' class RepoPullRequestsView(RepoAppView, '
998 all_comments = c.inline_comments_flat + c.comments
1014 all_comments = c.inline_comments_flat + c.comments
999
1015
1000 existing_ids = filter(
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 return _render('comments_table', all_comments, len(all_comments),
1019 return _render('comments_table', all_comments, len(all_comments),
1003 existing_ids=existing_ids)
1020 existing_ids=existing_ids)
1004
1021
@@ -1008,7 +1025,7 b' class RepoPullRequestsView(RepoAppView, '
1008 'repository.read', 'repository.write', 'repository.admin')
1025 'repository.read', 'repository.write', 'repository.admin')
1009 @view_config(
1026 @view_config(
1010 route_name='pullrequest_todos', request_method='POST',
1027 route_name='pullrequest_todos', request_method='POST',
1011 renderer='string', xhr=True)
1028 renderer='string_html', xhr=True)
1012 def pullrequest_todos(self):
1029 def pullrequest_todos(self):
1013 self.load_default_context()
1030 self.load_default_context()
1014
1031
@@ -1138,7 +1155,7 b' class RepoPullRequestsView(RepoAppView, '
1138 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1155 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1139 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
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 PullRequestModel().get_reviewer_functions()
1159 PullRequestModel().get_reviewer_functions()
1143
1160
1144 # recalculate reviewers logic, to make sure we can validate this
1161 # recalculate reviewers logic, to make sure we can validate this
@@ -1146,9 +1163,8 b' class RepoPullRequestsView(RepoAppView, '
1146 self._rhodecode_db_user, source_db_repo,
1163 self._rhodecode_db_user, source_db_repo,
1147 source_commit, target_db_repo, target_commit)
1164 source_commit, target_db_repo, target_commit)
1148
1165
1149 given_reviewers = _form['review_members']
1166 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1150 reviewers = validate_default_reviewers(
1167 observers = validate_observers(_form['observer_members'], reviewer_rules)
1151 given_reviewers, reviewer_rules)
1152
1168
1153 pullrequest_title = _form['pullrequest_title']
1169 pullrequest_title = _form['pullrequest_title']
1154 title_source_ref = source_ref.split(':', 2)[1]
1170 title_source_ref = source_ref.split(':', 2)[1]
@@ -1172,6 +1188,7 b' class RepoPullRequestsView(RepoAppView, '
1172 revisions=commit_ids,
1188 revisions=commit_ids,
1173 common_ancestor_id=common_ancestor_id,
1189 common_ancestor_id=common_ancestor_id,
1174 reviewers=reviewers,
1190 reviewers=reviewers,
1191 observers=observers,
1175 title=pullrequest_title,
1192 title=pullrequest_title,
1176 description=description,
1193 description=description,
1177 description_renderer=description_renderer,
1194 description_renderer=description_renderer,
@@ -1227,14 +1244,23 b' class RepoPullRequestsView(RepoAppView, '
1227 # only owner or admin can update it
1244 # only owner or admin can update it
1228 allowed_to_update = PullRequestModel().check_user_update(
1245 allowed_to_update = PullRequestModel().check_user_update(
1229 pull_request, self._rhodecode_user)
1246 pull_request, self._rhodecode_user)
1247
1230 if allowed_to_update:
1248 if allowed_to_update:
1231 controls = peppercorn.parse(self.request.POST.items())
1249 controls = peppercorn.parse(self.request.POST.items())
1232 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1250 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1233
1251
1234 if 'review_members' in controls:
1252 if 'review_members' in controls:
1235 self._update_reviewers(
1253 self._update_reviewers(
1254 c,
1236 pull_request, controls['review_members'],
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 elif str2bool(self.request.POST.get('update_commits', 'false')):
1264 elif str2bool(self.request.POST.get('update_commits', 'false')):
1239 if is_state_changing:
1265 if is_state_changing:
1240 log.debug('commits update: forbidden because pull request is in state %s',
1266 log.debug('commits update: forbidden because pull request is in state %s',
@@ -1255,6 +1281,7 b' class RepoPullRequestsView(RepoAppView, '
1255 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1281 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1256 self._edit_pull_request(pull_request)
1282 self._edit_pull_request(pull_request)
1257 else:
1283 else:
1284 log.error('Unhandled update data.')
1258 raise HTTPBadRequest()
1285 raise HTTPBadRequest()
1259
1286
1260 return {'response': True,
1287 return {'response': True,
@@ -1262,6 +1289,9 b' class RepoPullRequestsView(RepoAppView, '
1262 raise HTTPForbidden()
1289 raise HTTPForbidden()
1263
1290
1264 def _edit_pull_request(self, pull_request):
1291 def _edit_pull_request(self, pull_request):
1292 """
1293 Edit title and description
1294 """
1265 _ = self.request.translate
1295 _ = self.request.translate
1266
1296
1267 try:
1297 try:
@@ -1302,27 +1332,14 b' class RepoPullRequestsView(RepoAppView, '
1302
1332
1303 msg = _(u'Pull request updated to "{source_commit_id}" with '
1333 msg = _(u'Pull request updated to "{source_commit_id}" with '
1304 u'{count_added} added, {count_removed} removed commits. '
1334 u'{count_added} added, {count_removed} removed commits. '
1305 u'Source of changes: {change_source}')
1335 u'Source of changes: {change_source}.')
1306 msg = msg.format(
1336 msg = msg.format(
1307 source_commit_id=pull_request.source_ref_parts.commit_id,
1337 source_commit_id=pull_request.source_ref_parts.commit_id,
1308 count_added=len(resp.changes.added),
1338 count_added=len(resp.changes.added),
1309 count_removed=len(resp.changes.removed),
1339 count_removed=len(resp.changes.removed),
1310 change_source=changed)
1340 change_source=changed)
1311 h.flash(msg, category='success')
1341 h.flash(msg, category='success')
1312
1342 self._pr_update_channelstream_push(c.pr_broadcast_channel, msg)
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)
1326 else:
1343 else:
1327 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1344 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1328 warning_reasons = [
1345 warning_reasons = [
@@ -1332,6 +1349,53 b' class RepoPullRequestsView(RepoAppView, '
1332 category = 'warning' if resp.reason in warning_reasons else 'error'
1349 category = 'warning' if resp.reason in warning_reasons else 'error'
1333 h.flash(msg, category=category)
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 @LoginRequired()
1399 @LoginRequired()
1336 @NotAnonymous()
1400 @NotAnonymous()
1337 @HasRepoPermissionAnyDecorator(
1401 @HasRepoPermissionAnyDecorator(
@@ -1408,32 +1472,6 b' class RepoPullRequestsView(RepoAppView, '
1408 msg = merge_resp.merge_status_message
1472 msg = merge_resp.merge_status_message
1409 h.flash(msg, category='error')
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 @LoginRequired()
1475 @LoginRequired()
1438 @NotAnonymous()
1476 @NotAnonymous()
1439 @HasRepoPermissionAnyDecorator(
1477 @HasRepoPermissionAnyDecorator(
@@ -1488,8 +1526,7 b' class RepoPullRequestsView(RepoAppView, '
1488 allowed_to_comment = PullRequestModel().check_user_comment(
1526 allowed_to_comment = PullRequestModel().check_user_comment(
1489 pull_request, self._rhodecode_user)
1527 pull_request, self._rhodecode_user)
1490 if not allowed_to_comment:
1528 if not allowed_to_comment:
1491 log.debug(
1529 log.debug('comment: forbidden because pull request is from forbidden repo')
1492 'comment: forbidden because pull request is from forbidden repo')
1493 raise HTTPForbidden()
1530 raise HTTPForbidden()
1494
1531
1495 c = self.load_default_context()
1532 c = self.load_default_context()
@@ -341,6 +341,10 b' def includeme(config):'
341 name='json_ext',
341 name='json_ext',
342 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
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 # include RhodeCode plugins
348 # include RhodeCode plugins
345 includes = aslist(settings.get('rhodecode.includes', []))
349 includes = aslist(settings.get('rhodecode.includes', []))
346 for inc in includes:
350 for inc in includes:
@@ -88,6 +88,9 b' ACTIONS_V1 = {'
88 'repo.pull_request.reviewer.add': '',
88 'repo.pull_request.reviewer.add': '',
89 'repo.pull_request.reviewer.delete': '',
89 'repo.pull_request.reviewer.delete': '',
90
90
91 'repo.pull_request.observer.add': '',
92 'repo.pull_request.observer.delete': '',
93
91 'repo.commit.strip': {'commit_id': ''},
94 'repo.commit.strip': {'commit_id': ''},
92 'repo.commit.comment.create': {'data': {}},
95 'repo.commit.comment.create': {'data': {}},
93 'repo.commit.comment.delete': {'data': {}},
96 'repo.commit.comment.delete': {'data': {}},
@@ -1104,6 +1104,10 b' def bool2icon(value, show_at_false=True)'
1104 return HTML.tag('i', class_="icon-false", title='False')
1104 return HTML.tag('i', class_="icon-false", title='False')
1105 return HTML.tag('i')
1105 return HTML.tag('i')
1106
1106
1107
1108 def b64(inp):
1109 return base64.b64encode(inp)
1110
1107 #==============================================================================
1111 #==============================================================================
1108 # PERMS
1112 # PERMS
1109 #==============================================================================
1113 #==============================================================================
@@ -25,7 +25,7 b' import collections'
25
25
26 from rhodecode.model import BaseModel
26 from rhodecode.model import BaseModel
27 from rhodecode.model.db import (
27 from rhodecode.model.db import (
28 ChangesetStatus, ChangesetComment, PullRequest, Session)
28 ChangesetStatus, ChangesetComment, PullRequest, PullRequestReviewers, Session)
29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
30 from rhodecode.lib.markup_renderer import (
30 from rhodecode.lib.markup_renderer import (
31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
@@ -383,15 +383,14 b' class ChangesetStatusModel(BaseModel):'
383 pull_request.source_repo,
383 pull_request.source_repo,
384 pull_request=pull_request,
384 pull_request=pull_request,
385 with_revisions=True)
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)
390 def calculated_review_status(self, pull_request):
388
389 def calculated_review_status(self, pull_request, reviewers_statuses=None):
390 """
391 """
391 calculate pull request status based on reviewers, it should be a list
392 calculate pull request status based on reviewers, it should be a list
392 of two element lists.
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 return self.calculate_status(reviewers)
396 return self.calculate_status(reviewers)
@@ -436,9 +436,8 b' class CommentsModel(BaseModel):'
436 'thread_ids': [pr_url, pr_comment_url],
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 if send_email:
439 if send_email:
440 recipients += [self._get_user(u) for u in (extra_recipients or [])]
442 # pre-generate the subject for notification itself
441 # pre-generate the subject for notification itself
443 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
442 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
444 notification_type, **kwargs)
443 notification_type, **kwargs)
@@ -4465,6 +4465,37 b' class PullRequest(Base, _PullRequestBase'
4465 from rhodecode.model.changeset_status import ChangesetStatusModel
4465 from rhodecode.model.changeset_status import ChangesetStatusModel
4466 return ChangesetStatusModel().reviewers_statuses(self)
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 @property
4499 @property
4469 def workspace_id(self):
4500 def workspace_id(self):
4470 from rhodecode.model.pull_request import PullRequestModel
4501 from rhodecode.model.pull_request import PullRequestModel
@@ -4530,6 +4561,9 b' class PullRequestVersion(Base, _PullRequ'
4530 def reviewers_statuses(self):
4561 def reviewers_statuses(self):
4531 return self.pull_request.reviewers_statuses()
4562 return self.pull_request.reviewers_statuses()
4532
4563
4564 def observer(self):
4565 return self.pull_request.observers()
4566
4533
4567
4534 class PullRequestReviewers(Base, BaseModel):
4568 class PullRequestReviewers(Base, BaseModel):
4535 __tablename__ = 'pull_request_reviewers'
4569 __tablename__ = 'pull_request_reviewers'
@@ -4538,6 +4572,7 b' class PullRequestReviewers(Base, BaseMod'
4538 )
4572 )
4539 ROLE_REVIEWER = u'reviewer'
4573 ROLE_REVIEWER = u'reviewer'
4540 ROLE_OBSERVER = u'observer'
4574 ROLE_OBSERVER = u'observer'
4575 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4541
4576
4542 @hybrid_property
4577 @hybrid_property
4543 def reasons(self):
4578 def reasons(self):
@@ -4589,6 +4624,15 b' class PullRequestReviewers(Base, BaseMod'
4589
4624
4590 return user_group_data
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 def __unicode__(self):
4636 def __unicode__(self):
4593 return u"<%s('id:%s')>" % (self.__class__.__name__,
4637 return u"<%s('id:%s')>" % (self.__class__.__name__,
4594 self.pull_requests_reviewers_id)
4638 self.pull_requests_reviewers_id)
@@ -4954,16 +4998,21 b' class RepoReviewRuleUser(Base, BaseModel'
4954 __table_args__ = (
4998 __table_args__ = (
4955 base_table_args
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 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
5005 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4959 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
5006 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4960 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
5007 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4961 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5008 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5009 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4962 user = relationship('User')
5010 user = relationship('User')
4963
5011
4964 def rule_data(self):
5012 def rule_data(self):
4965 return {
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 VOTE_RULE_ALL = -1
5025 VOTE_RULE_ALL = -1
5026 ROLE_REVIEWER = u'reviewer'
5027 ROLE_OBSERVER = u'observer'
5028 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4977
5029
4978 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
5030 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4979 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
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 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5033 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5034 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4982 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
5035 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4983 users_group = relationship('UserGroup')
5036 users_group = relationship('UserGroup')
4984
5037
4985 def rule_data(self):
5038 def rule_data(self):
4986 return {
5039 return {
4987 'mandatory': self.mandatory,
5040 'mandatory': self.mandatory,
5041 'role': self.role,
4988 'vote_rule': self.vote_rule
5042 'vote_rule': self.vote_rule
4989 }
5043 }
4990
5044
@@ -601,6 +601,14 b' def PullRequestForm(localizer, repo_id):'
601 reasons = All()
601 reasons = All()
602 rules = All(v.UniqueList(localizer, convert=int)())
602 rules = All(v.UniqueList(localizer, convert=int)())
603 mandatory = v.StringBoolean()
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 class _PullRequestForm(formencode.Schema):
613 class _PullRequestForm(formencode.Schema):
606 allow_extra_fields = True
614 allow_extra_fields = True
@@ -614,6 +622,7 b' def PullRequestForm(localizer, repo_id):'
614 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
622 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
615 v.UniqueList(localizer)(not_empty=True))
623 v.UniqueList(localizer)(not_empty=True))
616 review_members = formencode.ForEach(ReviewerForm())
624 review_members = formencode.ForEach(ReviewerForm())
625 observer_members = formencode.ForEach(ObserverForm())
617 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
626 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
618 pullrequest_desc = v.UnicodeString(strip=True, required=False)
627 pullrequest_desc = v.UnicodeString(strip=True, required=False)
619 description_renderer = v.UnicodeString(strip=True, required=False)
628 description_renderer = v.UnicodeString(strip=True, required=False)
@@ -575,7 +575,7 b' class PullRequestModel(BaseModel):'
575 pull_request_display_obj, at_version
575 pull_request_display_obj, at_version
576
576
577 def create(self, created_by, source_repo, source_ref, target_repo,
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 common_ancestor_id=None,
579 common_ancestor_id=None,
580 description_renderer=None,
580 description_renderer=None,
581 reviewer_data=None, translator=None, auth_user=None):
581 reviewer_data=None, translator=None, auth_user=None):
@@ -606,7 +606,7 b' class PullRequestModel(BaseModel):'
606 reviewer_ids = set()
606 reviewer_ids = set()
607 # members / reviewers
607 # members / reviewers
608 for reviewer_object in reviewers:
608 for reviewer_object in reviewers:
609 user_id, reasons, mandatory, rules = reviewer_object
609 user_id, reasons, mandatory, role, rules = reviewer_object
610 user = self._get_user(user_id)
610 user = self._get_user(user_id)
611
611
612 # skip duplicates
612 # skip duplicates
@@ -620,6 +620,7 b' class PullRequestModel(BaseModel):'
620 reviewer.pull_request = pull_request
620 reviewer.pull_request = pull_request
621 reviewer.reasons = reasons
621 reviewer.reasons = reasons
622 reviewer.mandatory = mandatory
622 reviewer.mandatory = mandatory
623 reviewer.role = role
623
624
624 # NOTE(marcink): pick only first rule for now
625 # NOTE(marcink): pick only first rule for now
625 rule_id = list(rules)[0] if rules else None
626 rule_id = list(rules)[0] if rules else None
@@ -653,6 +654,33 b' class PullRequestModel(BaseModel):'
653 Session().add(reviewer)
654 Session().add(reviewer)
654 Session().flush()
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 # Set approval status to "Under Review" for all commits which are
684 # Set approval status to "Under Review" for all commits which are
657 # part of this pull request.
685 # part of this pull request.
658 ChangesetStatusModel().set_status(
686 ChangesetStatusModel().set_status(
@@ -1204,23 +1232,25 b' class PullRequestModel(BaseModel):'
1204
1232
1205 :param pull_request: the pr to update
1233 :param pull_request: the pr to update
1206 :param reviewer_data: list of tuples
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 pull_request = self.__get_pull_request(pull_request)
1239 pull_request = self.__get_pull_request(pull_request)
1210 if pull_request.is_closed():
1240 if pull_request.is_closed():
1211 raise ValueError('This pull request is closed')
1241 raise ValueError('This pull request is closed')
1212
1242
1213 reviewers = {}
1243 reviewers = {}
1214 for user_id, reasons, mandatory, rules in reviewer_data:
1244 for user_id, reasons, mandatory, role, rules in reviewer_data:
1215 if isinstance(user_id, (int, compat.string_types)):
1245 if isinstance(user_id, (int, compat.string_types)):
1216 user_id = self._get_user(user_id).user_id
1246 user_id = self._get_user(user_id).user_id
1217 reviewers[user_id] = {
1247 reviewers[user_id] = {
1218 'reasons': reasons, 'mandatory': mandatory}
1248 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1219
1249
1220 reviewers_ids = set(reviewers.keys())
1250 reviewers_ids = set(reviewers.keys())
1221 current_reviewers = PullRequestReviewers.query()\
1251 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1222 .filter(PullRequestReviewers.pull_request ==
1252 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1223 pull_request).all()
1253
1224 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1254 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1225
1255
1226 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
1256 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
@@ -1241,16 +1271,19 b' class PullRequestModel(BaseModel):'
1241 reviewer.reasons = reviewers[uid]['reasons']
1271 reviewer.reasons = reviewers[uid]['reasons']
1242 # NOTE(marcink): mandatory shouldn't be changed now
1272 # NOTE(marcink): mandatory shouldn't be changed now
1243 # reviewer.mandatory = reviewers[uid]['reasons']
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 Session().add(reviewer)
1276 Session().add(reviewer)
1245 added_audit_reviewers.append(reviewer.get_dict())
1277 added_audit_reviewers.append(reviewer.get_dict())
1246
1278
1247 for uid in ids_to_remove:
1279 for uid in ids_to_remove:
1248 changed = True
1280 changed = True
1249 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1281 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1250 # that prevents and fixes cases that we added the same reviewer twice.
1282 # This is an edge case that handles previous state of having the same reviewer twice.
1251 # this CAN happen due to the lack of DB checks
1283 # this CAN happen due to the lack of DB checks
1252 reviewers = PullRequestReviewers.query()\
1284 reviewers = PullRequestReviewers.query()\
1253 .filter(PullRequestReviewers.user_id == uid,
1285 .filter(PullRequestReviewers.user_id == uid,
1286 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1254 PullRequestReviewers.pull_request == pull_request)\
1287 PullRequestReviewers.pull_request == pull_request)\
1255 .all()
1288 .all()
1256
1289
@@ -1273,7 +1306,90 b' class PullRequestModel(BaseModel):'
1273 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1306 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1274 user, pull_request)
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 return ids_to_add, ids_to_remove
1393 return ids_to_add, ids_to_remove
1278
1394
1279 def get_url(self, pull_request, request=None, permalink=False):
1395 def get_url(self, pull_request, request=None, permalink=False):
@@ -1301,16 +1417,16 b' class PullRequestModel(BaseModel):'
1301 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1417 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1302 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1418 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1303
1419
1304 def notify_reviewers(self, pull_request, reviewers_ids):
1420 def _notify_reviewers(self, pull_request, user_ids, role, user):
1305 # notification to reviewers
1421 # notification to reviewers/observers
1306 if not reviewers_ids:
1422 if not user_ids:
1307 return
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 pull_request_obj = pull_request
1427 pull_request_obj = pull_request
1312 # get the current participants of this pull request
1428 # get the current participants of this pull request
1313 recipients = reviewers_ids
1429 recipients = user_ids
1314 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1430 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1315
1431
1316 pr_source_repo = pull_request_obj.source_repo
1432 pr_source_repo = pull_request_obj.source_repo
@@ -1332,8 +1448,10 b' class PullRequestModel(BaseModel):'
1332 (x.raw_id, x.message)
1448 (x.raw_id, x.message)
1333 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1449 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1334
1450
1451 current_rhodecode_user = user
1335 kwargs = {
1452 kwargs = {
1336 'user': pull_request.author,
1453 'user': current_rhodecode_user,
1454 'pull_request_author': pull_request.author,
1337 'pull_request': pull_request_obj,
1455 'pull_request': pull_request_obj,
1338 'pull_request_commits': pull_request_commits,
1456 'pull_request_commits': pull_request_commits,
1339
1457
@@ -1345,6 +1463,7 b' class PullRequestModel(BaseModel):'
1345
1463
1346 'pull_request_url': pr_url,
1464 'pull_request_url': pr_url,
1347 'thread_ids': [pr_url],
1465 'thread_ids': [pr_url],
1466 'user_role': role
1348 }
1467 }
1349
1468
1350 # pre-generate the subject for notification itself
1469 # pre-generate the subject for notification itself
@@ -1353,7 +1472,7 b' class PullRequestModel(BaseModel):'
1353
1472
1354 # create notification objects, and emails
1473 # create notification objects, and emails
1355 NotificationModel().create(
1474 NotificationModel().create(
1356 created_by=pull_request.author,
1475 created_by=current_rhodecode_user,
1357 notification_subject=subject,
1476 notification_subject=subject,
1358 notification_body=body_plaintext,
1477 notification_body=body_plaintext,
1359 notification_type=notification_type,
1478 notification_type=notification_type,
@@ -1361,6 +1480,14 b' class PullRequestModel(BaseModel):'
1361 email_kwargs=kwargs,
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 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1491 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1365 commit_changes, file_changes):
1492 commit_changes, file_changes):
1366
1493
@@ -1874,11 +2001,13 b' class PullRequestModel(BaseModel):'
1874 try:
2001 try:
1875 from rc_reviewers.utils import get_default_reviewers_data
2002 from rc_reviewers.utils import get_default_reviewers_data
1876 from rc_reviewers.utils import validate_default_reviewers
2003 from rc_reviewers.utils import validate_default_reviewers
2004 from rc_reviewers.utils import validate_observers
1877 except ImportError:
2005 except ImportError:
1878 from rhodecode.apps.repository.utils import get_default_reviewers_data
2006 from rhodecode.apps.repository.utils import get_default_reviewers_data
1879 from rhodecode.apps.repository.utils import validate_default_reviewers
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 class MergeCheck(object):
2013 class MergeCheck(object):
@@ -1700,8 +1700,33 b' table.group_members {'
1700 }
1700 }
1701
1701
1702 .reviewer_ac .ac-input {
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 width: 100%;
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 .compare_view_commits tr{
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 var self = this;
98 var self = this;
99 this.$loadingIndicator = $('.calculate-reviewers');
99 this.$reviewRulesContainer = $('#review_rules');
100 this.$reviewRulesContainer = $('#review_rules');
100 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
101 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
101 this.$userRule = $('.pr-user-rule-container');
102 this.$userRule = $('.pr-user-rule-container');
102 this.forbidReviewUsers = undefined;
103 this.$reviewMembers = $('#review_members');
103 this.$reviewMembers = $('#review_members');
104 this.$observerMembers = $('#observer_members');
105
104 this.currentRequest = null;
106 this.currentRequest = null;
105 this.diffData = null;
107 this.diffData = null;
106 this.enabledRules = [];
108 this.enabledRules = [];
109 // sync with db.py entries
110 this.ROLE_REVIEWER = 'reviewer';
111 this.ROLE_OBSERVER = 'observer'
107
112
108 //dummy handler, we might register our own later
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 return [
117 return [
113 {
118 {
114 'username': 'default',
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 this.hideReviewRules = function () {
128 this.hideReviewRules = function () {
121 self.$reviewRulesContainer.hide();
129 self.$reviewRulesContainer.hide();
122 $(self.$userRule.selector).hide();
130 $(self.$userRule.selector).hide();
@@ -133,11 +141,40 b' ReviewersController = function () {'
133 return '<div>- {0}</div>'.format(ruleText)
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 this.loadReviewRules = function (data) {
173 this.loadReviewRules = function (data) {
137 self.diffData = data;
174 self.diffData = data;
138
175
139 // reset forbidden Users
176 // reset forbidden Users
140 this.forbidReviewUsers = self.defaultForbidReviewUsers();
177 this.forbidUsers = self.defaultForbidUsers();
141
178
142 // reset state of review rules
179 // reset state of review rules
143 self.$rulesList.html('');
180 self.$rulesList.html('');
@@ -148,7 +185,7 b' ReviewersController = function () {'
148 self.addRule(
185 self.addRule(
149 _gettext('All reviewers must vote.'))
186 _gettext('All reviewers must vote.'))
150 );
187 );
151 return self.forbidReviewUsers
188 return self.forbidUsers
152 }
189 }
153
190
154 if (data.rules.voting !== undefined) {
191 if (data.rules.voting !== undefined) {
@@ -195,7 +232,7 b' ReviewersController = function () {'
195 }
232 }
196
233
197 if (data.rules.forbid_author_to_review) {
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 self.$rulesList.append(
236 self.$rulesList.append(
200 self.addRule(
237 self.addRule(
201 _gettext('Author is not allowed to be a reviewer.'))
238 _gettext('Author is not allowed to be a reviewer.'))
@@ -206,9 +243,8 b' ReviewersController = function () {'
206
243
207 if (data.rules_data.forbidden_users) {
244 if (data.rules_data.forbidden_users) {
208 $.each(data.rules_data.forbidden_users, function (index, member_data) {
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 self.$rulesList.append(
250 self.$rulesList.append(
@@ -223,9 +259,31 b' ReviewersController = function () {'
223 _gettext('No review rules set.'))
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 this.loadDefaultReviewers = function (sourceRepo, sourceRef, targetRepo, targetRef) {
287 this.loadDefaultReviewers = function (sourceRepo, sourceRef, targetRepo, targetRef) {
230
288
231 if (self.currentRequest) {
289 if (self.currentRequest) {
@@ -233,19 +291,21 b' ReviewersController = function () {'
233 self.currentRequest.abort();
291 self.currentRequest.abort();
234 }
292 }
235
293
236 $('.calculate-reviewers').show();
294 self.$loadingIndicator.show();
237 // reset reviewer members
295
238 self.$reviewMembers.empty();
296 // reset reviewer/observe members
297 self.emptyTables();
239
298
240 prButtonLock(true, null, 'reviewers');
299 prButtonLock(true, null, 'reviewers');
241 $('#user').hide(); // hide user autocomplete before load
300 $('#user').hide(); // hide user autocomplete before load
301 $('#observer').hide(); //hide observer autocomplete before load
242
302
243 // lock PR button, so we cannot send PR before it's calculated
303 // lock PR button, so we cannot send PR before it's calculated
244 prButtonLock(true, _gettext('Loading diff ...'), 'compare');
304 prButtonLock(true, _gettext('Loading diff ...'), 'compare');
245
305
246 if (sourceRef.length !== 3 || targetRef.length !== 3) {
306 if (sourceRef.length !== 3 || targetRef.length !== 3) {
247 // don't load defaults in case we're missing some refs...
307 // don't load defaults in case we're missing some refs...
248 $('.calculate-reviewers').hide();
308 self.$loadingIndicator.hide();
249 return
309 return
250 }
310 }
251
311
@@ -272,11 +332,16 b' ReviewersController = function () {'
272
332
273 for (var i = 0; i < data.reviewers.length; i++) {
333 for (var i = 0; i < data.reviewers.length; i++) {
274 var reviewer = data.reviewers[i];
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 prButtonLock(false, null, 'reviewers');
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 var commitElements = data["diff_info"]['commits'];
346 var commitElements = data["diff_info"]['commits'];
282
347
@@ -292,7 +357,7 b' ReviewersController = function () {'
292
357
293 },
358 },
294 error: function (jqXHR, textStatus, errorThrown) {
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 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
361 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
297 ajaxErrorSwal(message);
362 ajaxErrorSwal(message);
298 }
363 }
@@ -301,7 +366,7 b' ReviewersController = function () {'
301 };
366 };
302
367
303 // check those, refactor
368 // check those, refactor
304 this.removeReviewMember = function (reviewer_id, mark_delete) {
369 this.removeMember = function (reviewer_id, mark_delete) {
305 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
370 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
306
371
307 if (typeof (mark_delete) === undefined) {
372 if (typeof (mark_delete) === undefined) {
@@ -312,6 +377,7 b' ReviewersController = function () {'
312 if (reviewer) {
377 if (reviewer) {
313 // now delete the input
378 // now delete the input
314 $('#reviewer_{0} input'.format(reviewer_id)).remove();
379 $('#reviewer_{0} input'.format(reviewer_id)).remove();
380 $('#reviewer_{0}_rules input'.format(reviewer_id)).remove();
315 // mark as to-delete
381 // mark as to-delete
316 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
382 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
317 obj.addClass('to-delete');
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 var id = reviewer_obj.user_id;
393 var id = reviewer_obj.user_id;
331 var username = reviewer_obj.username;
394 var username = reviewer_obj.username;
332
395
333 var reasons = reasons || [];
396 reasons = reasons || [];
334 var mandatory = mandatory || false;
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 var currentIds = [];
402 var currentIds = [];
338
403
339 $.each(self.$reviewMembers.find('.reviewer_entry'), function (index, value) {
404 $.each($('.reviewer_entry'), function (index, value) {
340 currentIds.push($(value).data('reviewerUserId'))
405 currentIds.push($(value).data('reviewerUserId'))
341 })
406 })
342
407
343 var userAllowedReview = function (userId) {
408 var userAllowedReview = function (userId) {
344 var allowed = true;
409 var allowed = true;
345 $.each(self.forbidReviewUsers, function (index, member_data) {
410 $.each(self.forbidUsers, function (index, member_data) {
346 if (parseInt(userId) === member_data['user_id']) {
411 if (parseInt(userId) === member_data['user_id']) {
347 allowed = false;
412 allowed = false;
348 return false // breaks the loop
413 return false // breaks the loop
@@ -352,6 +417,7 b' ReviewersController = function () {'
352 };
417 };
353
418
354 var userAllowed = userAllowedReview(id);
419 var userAllowed = userAllowedReview(id);
420
355 if (!userAllowed) {
421 if (!userAllowed) {
356 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
422 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
357 } else {
423 } else {
@@ -359,11 +425,13 b' ReviewersController = function () {'
359 var alreadyReviewer = currentIds.indexOf(id) != -1;
425 var alreadyReviewer = currentIds.indexOf(id) != -1;
360
426
361 if (alreadyReviewer) {
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 } else {
429 } else {
430
364 var reviewerEntry = renderTemplate('reviewMemberEntry', {
431 var reviewerEntry = renderTemplate('reviewMemberEntry', {
365 'member': reviewer_obj,
432 'member': reviewer_obj,
366 'mandatory': mandatory,
433 'mandatory': mandatory,
434 'role': role,
367 'reasons': reasons,
435 'reasons': reasons,
368 'allowed_to_update': true,
436 'allowed_to_update': true,
369 'review_status': 'not_reviewed',
437 'review_status': 'not_reviewed',
@@ -372,16 +440,32 b' ReviewersController = function () {'
372 'create': true,
440 'create': true,
373 'rule_show': true,
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 tooltipActivate();
455 tooltipActivate();
377 }
456 }
378 }
457 }
379
458
380 };
459 };
381
460
382 this.updateReviewers = function (repo_name, pull_request_id) {
461 this.updateReviewers = function (repo_name, pull_request_id, role) {
383 var postData = $('#reviewers input').serialize();
462 if (role === 'reviewer') {
384 _updatePullRequest(repo_name, pull_request_id, postData);
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 this.handleDiffData = function (data) {
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) {
538 var autoCompleteHandler = function (inputId, controller, role) {
455 $(inputId).autocomplete({
539
456 serviceUrl: pyroutes.url('user_autocomplete_data'),
540 return function (element, 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) {
467 var mandatory = false;
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 // add whole user groups
545 // add whole user groups
471 if (data.value_type == 'user_group') {
546 if (data.value_type == 'user_group') {
472 reasons.push(_gettext('member of "{0}"').format(data.value_display));
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 var reviewer = member_data;
550 var reviewer = member_data;
476 reviewer['user_id'] = member_data['id'];
551 reviewer['user_id'] = member_data['id'];
477 reviewer['gravatar_link'] = member_data['icon_link'];
552 reviewer['gravatar_link'] = member_data['icon_link'];
478 reviewer['user_link'] = member_data['profile_link'];
553 reviewer['user_link'] = member_data['profile_link'];
479 reviewer['rules'] = [];
554 reviewer['rules'] = [];
480 reviewersController.addReviewMember(reviewer, reasons, mandatory);
555 controller.addMember(reviewer, reasons, mandatory, role);
481 })
556 })
482 }
557 }
483 // add single user
558 // add single user
@@ -487,14 +562,71 b' var ReviewerAutoComplete = function(inpu'
487 reviewer['gravatar_link'] = data['icon_link'];
562 reviewer['gravatar_link'] = data['icon_link'];
488 reviewer['user_link'] = data['profile_link'];
563 reviewer['user_link'] = data['profile_link'];
489 reviewer['rules'] = [];
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 window.VersionController = function () {
631 window.VersionController = function () {
500 var self = this;
632 var self = this;
@@ -504,7 +636,7 b' window.VersionController = function () {'
504
636
505 this.adjustRadioSelectors = function (curNode) {
637 this.adjustRadioSelectors = function (curNode) {
506 var getVal = function (item) {
638 var getVal = function (item) {
507 if (item == 'latest') {
639 if (item === 'latest') {
508 return Number.MAX_SAFE_INTEGER
640 return Number.MAX_SAFE_INTEGER
509 }
641 }
510 else {
642 else {
@@ -663,6 +795,7 b' window.UpdatePrController = function () '
663 };
795 };
664 };
796 };
665
797
798
666 /**
799 /**
667 * Reviewer display panel
800 * Reviewer display panel
668 */
801 */
@@ -702,26 +835,37 b' window.ReviewersPanel = {'
702 },
835 },
703
836
704 renderReviewers: function () {
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 $.each(this.setReviewers.reviewers, function (key, val) {
848 $.each(this.setReviewers.reviewers, function (key, val) {
708 var member = val;
709
849
710 var entry = renderTemplate('reviewMemberEntry', {
850 var member = val;
711 'member': member,
851 if (member.role === reviewersController.ROLE_REVIEWER) {
712 'mandatory': member.mandatory,
852 var entry = renderTemplate('reviewMemberEntry', {
713 'reasons': member.reasons,
853 'member': member,
714 'allowed_to_update': member.allowed_to_update,
854 'mandatory': member.mandatory,
715 'review_status': member.review_status,
855 'role': member.role,
716 'review_status_label': member.review_status_label,
856 'reasons': member.reasons,
717 'user_group': member.user_group,
857 'allowed_to_update': member.allowed_to_update,
718 'create': false
858 'review_status': member.review_status,
719 });
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 tooltipActivate();
868 tooltipActivate();
724
725 },
869 },
726
870
727 edit: function (event) {
871 edit: function (event) {
@@ -739,10 +883,142 b' window.ReviewersPanel = {'
739 this.addButton.hide();
883 this.addButton.hide();
740 $(this.removeButtons.selector).css('visibility', 'hidden');
884 $(this.removeButtons.selector).css('visibility', 'hidden');
741 // hide review rules
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 * OnLine presence using channelstream
1024 * OnLine presence using channelstream
@@ -813,29 +1089,29 b' window.refreshComments = function (versi'
813 $.each($('.comment'), function (idx, element) {
1089 $.each($('.comment'), function (idx, element) {
814 currentIDs.push($(element).data('commentId'));
1090 currentIDs.push($(element).data('commentId'));
815 });
1091 });
816 var data = {"comments[]": currentIDs};
1092 var data = {"comments": currentIDs};
817
1093
818 var $targetElem = $('.comments-content-table');
1094 var $targetElem = $('.comments-content-table');
819 $targetElem.css('opacity', 0.3);
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);
1097 var success = function (data) {
836 tooltipActivate();
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 window.refreshTODOs = function (version) {
1117 window.refreshTODOs = function (version) {
@@ -858,28 +1134,28 b' window.refreshTODOs = function (version)'
858 currentIDs.push($(element).data('commentId'));
1134 currentIDs.push($(element).data('commentId'));
859 });
1135 });
860
1136
861 var data = {"comments[]": currentIDs};
1137 var data = {"comments": currentIDs};
862 var $targetElem = $('.todos-content-table');
1138 var $targetElem = $('.todos-content-table');
863 $targetElem.css('opacity', 0.3);
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);
1141 var success = function (data) {
880 tooltipActivate();
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 window.refreshAllComments = function (version) {
1161 window.refreshAllComments = function (version) {
@@ -888,3 +1164,12 b' window.refreshAllComments = function (ve'
888 refreshComments(version);
1164 refreshComments(version);
889 refreshTODOs(version);
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 return request;
57 return request;
58 };
58 };
59
59
60 var ajaxPOST = function (url, postData, success, failure) {
60 var ajaxPOST = function (url, postData, success, failure, options) {
61 var sUrl = url;
61
62 var postData = toQueryString(postData);
62 var ajaxSettings = $.extend({
63 var request = $.ajax({
64 type: 'POST',
63 type: 'POST',
65 url: sUrl,
64 url: url,
66 data: postData,
65 data: toQueryString(postData),
67 headers: {'X-PARTIAL-XHR': true}
66 headers: {'X-PARTIAL-XHR': true}
68 })
67 }, options);
68
69 var request = $.ajax(
70 ajaxSettings
71 )
69 .done(function (data) {
72 .done(function (data) {
70 success(data);
73 success(data);
71 })
74 })
@@ -126,7 +129,8 b' function formatErrorMessage(jqXHR, textS'
126 } else if (errorThrown === 'abort') {
129 } else if (errorThrown === 'abort') {
127 return (prefix + 'Ajax request aborted.');
130 return (prefix + 'Ajax request aborted.');
128 } else {
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 if is_pr:
89 if is_pr:
90 version_info = (' made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else ' made in this version')
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
92 ## NEW, since refresh
93 <script type="text/javascript">
93 % if existing_ids and comment_obj.comment_id not in existing_ids:
94 // closure function helper
94 <div class="tooltip" style="position: absolute; left: 8px" title="New comment">
95 var sidebarComment${comment_obj.comment_id} = function() {
95 !
96 return renderTemplate('sideBarCommentHovercard', {
96 </div>
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>
116 % endif
97 % endif
117
98
118 ## NEW, since refresh
99 <%
119 % if existing_ids and comment_obj.comment_id not in existing_ids:
100 data = h.json.dumps({
120 <span class="tag">NEW</span>
101 'comment_id': comment_obj.comment_id,
121 % endif
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 </td>
127 </td>
123
128
124 <td class="td-todo-gravatar">
129 <td class="td-todo-gravatar">
@@ -187,12 +187,12 b''
187 <div class="sidebar-element clear-both">
187 <div class="sidebar-element clear-both">
188 <% vote_title = _ungettext(
188 <% vote_title = _ungettext(
189 'Status calculated based on votes from {} reviewer',
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 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
193 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
194 <i class="icon-circle review-status-${c.commit_review_status}"></i>
194 <i class="icon-circle review-status-${c.commit_review_status}"></i>
195 ${len(c.allowed_reviewers)}
195 ${c.reviewers_count}
196 </div>
196 </div>
197 </div>
197 </div>
198
198
@@ -149,7 +149,7 b''
149 <span class="user"> <a href="/_profiles/jenkins-tests">jenkins-tests</a> (reviewer)</span>
149 <span class="user"> <a href="/_profiles/jenkins-tests">jenkins-tests</a> (reviewer)</span>
150 </div>
150 </div>
151 <input id="reviewer_70_input" type="hidden" value="70" name="review_members">
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 <i class="icon-remove"></i>
153 <i class="icon-remove"></i>
154 </div>
154 </div>
155 </li>
155 </li>
@@ -66,9 +66,18 b" var data_hovercard_url = pyroutes.url('h"
66 <tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
66 <tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
67
67
68 <td style="width: 20px">
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 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
73 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
70 <i class="icon-circle review-status-<%= review_status %>"></i>
74 <i class="icon-circle review-status-<%= review_status %>"></i>
71 </div>
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 </td>
81 </td>
73
82
74 <td>
83 <td>
@@ -84,9 +93,6 b" var data_hovercard_url = pyroutes.url('h"
84 'gravatar_url': member.gravatar_link
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 </div>
96 </div>
91 </td>
97 </td>
92
98
@@ -108,7 +114,7 b" var data_hovercard_url = pyroutes.url('h"
108 <% } else { %>
114 <% } else { %>
109 <td style="text-align: right;width: 10px;">
115 <td style="text-align: right;width: 10px;">
110 <% if (allowed_to_update) { %>
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 <i class="icon-remove"></i>
118 <i class="icon-remove"></i>
113 </div>
119 </div>
114 <% } %>
120 <% } %>
@@ -117,7 +123,7 b" var data_hovercard_url = pyroutes.url('h"
117
123
118 </tr>
124 </tr>
119
125
120 <tr>
126 <tr id="reviewer_<%= member.user_id %>_rules">
121 <td colspan="4" style="display: <%= rule_visibility %>" class="pr-user-rule-container">
127 <td colspan="4" style="display: <%= rule_visibility %>" class="pr-user-rule-container">
122 <input type="hidden" name="__start__" value="reviewer:mapping">
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 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
156 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
151 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
157 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
158 <input type="hidden" name="role" value="<%= role %>"/>
152
159
153 <input type="hidden" name="__end__" value="reviewer:mapping">
160 <input type="hidden" name="__end__" value="reviewer:mapping">
154 </td>
161 </td>
@@ -11,7 +11,10 b' data = {'
11 'pr_title': pull_request.title,
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 ${subject_template.format(**data) |n}
20 ${subject_template.format(**data) |n}
@@ -34,6 +37,7 b' data = {'
34 'source_repo_url': pull_request_source_repo_url,
37 'source_repo_url': pull_request_source_repo_url,
35 'target_repo_url': pull_request_target_repo_url,
38 'target_repo_url': pull_request_target_repo_url,
36 }
39 }
40
37 %>
41 %>
38
42
39 * ${_('Pull Request link')}: ${pull_request_url}
43 * ${_('Pull Request link')}: ${pull_request_url}
@@ -51,7 +55,7 b' data = {'
51
55
52 % for commit_id, message in pull_request_commits:
56 % for commit_id, message in pull_request_commits:
53 - ${h.short_id(commit_id)}
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 % endfor
60 % endfor
57
61
@@ -78,19 +82,23 b' data = {'
78 <table style="text-align:left;vertical-align:middle;width: 100%">
82 <table style="text-align:left;vertical-align:middle;width: 100%">
79 <tr>
83 <tr>
80 <td style="width:100%;border-bottom:1px solid #dbd9da;">
84 <td style="width:100%;border-bottom:1px solid #dbd9da;">
81
82 <div style="margin: 0; font-weight: bold">
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 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
93 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
84 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
94 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
85 ${_('requested a')}
95 ${_('requested a')}
86 <a href="${pull_request_url}" style="${base.link_css()}">
96 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a> review.
87 ${_('pull request review.').format(**data) }
88 </a>
89 </div>
97 </div>
98 % endif
90 <div style="margin-top: 10px"></div>
99 <div style="margin-top: 10px"></div>
91 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
100 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
92 </div>
101 </div>
93
94 </td>
102 </td>
95 </tr>
103 </tr>
96
104
@@ -112,7 +112,7 b''
112 ## REVIEWERS
112 ## REVIEWERS
113 <div class="field">
113 <div class="field">
114 <div class="label label-textarea">
114 <div class="label label-textarea">
115 <label for="pullrequest_reviewers">${_('Reviewers')}:</label>
115 <label for="pullrequest_reviewers">${_('Reviewers / Observers')}:</label>
116 </div>
116 </div>
117 <div class="content">
117 <div class="content">
118 ## REVIEW RULES
118 ## REVIEW RULES
@@ -125,29 +125,79 b''
125 </div>
125 </div>
126 </div>
126 </div>
127
127
128 ## REVIEWERS
128 ## REVIEWERS / OBSERVERS
129 <div class="reviewers-title">
129 <div class="reviewers-title">
130 <div class="pr-details-title">
130
131 ${_('Pull request reviewers')}
131 <ul class="nav-links clearfix">
132 <span class="calculate-reviewers"> - ${_('loading...')}</span>
132
133 </div>
133 ## TAB1 MANDATORY REVIEWERS
134 </div>
134 <li class="active">
135 <div id="reviewers" class="pr-details-content reviewers">
135 <a id="reviewers-btn" href="#showReviewers" tabindex="-1">
136 ## members goes here, filled via JS based on initial selection !
136 Reviewers
137 <input type="hidden" name="__start__" value="review_members:sequence">
137 <span id="reviewers-cnt" data-count="0" class="menulink-counter">0</span>
138 <table id="review_members" class="group_members">
138 </a>
139 ## This content is loaded via JS and ReviewersPanel
139 </li>
140 </table>
140
141 <input type="hidden" name="__end__" value="review_members:sequence">
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'>
151 ## TAB1 MANDATORY REVIEWERS
144 <div class="reviewer_ac">
152 <div id="reviewers-container">
145 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
153 <span class="calculate-reviewers">
146 <div id="reviewers_container"></div>
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 </div>
172 </div>
148 </div>
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 </div>
199 </div>
200
151 </div>
201 </div>
152 </div>
202 </div>
153
203
@@ -339,7 +389,6 b''
339
389
340 //make both panels equal
390 //make both panels equal
341 $('.target-panel').height($('.source-panel').height())
391 $('.target-panel').height($('.source-panel').height())
342
343 };
392 };
344
393
345 reviewersController = new ReviewersController();
394 reviewersController = new ReviewersController();
@@ -465,8 +514,7 b''
465 queryTargetRefs(initialData, query)
514 queryTargetRefs(initialData, query)
466 },
515 },
467 initSelection: initRefSelection()
516 initSelection: initRefSelection()
468 }
517 });
469 );
470
518
471 var sourceRepoSelect2 = Select2Box($sourceRepo, {
519 var sourceRepoSelect2 = Select2Box($sourceRepo, {
472 query: function(query) {}
520 query: function(query) {}
@@ -521,12 +569,12 b''
521
569
522 });
570 });
523
571
524 $pullRequestForm.on('submit', function(e){
572 $pullRequestForm.on('submit', function(e){
525 // Flush changes into textarea
573 // Flush changes into textarea
526 codeMirrorInstance.save();
574 codeMirrorInstance.save();
527 prButtonLock(true, null, 'all');
575 prButtonLock(true, null, 'all');
528 $pullRequestSubmit.val(_gettext('Please wait creating pull request...'));
576 $pullRequestSubmit.val(_gettext('Please wait creating pull request...'));
529 });
577 });
530
578
531 prButtonLock(true, "${_('Please select source and target')}", 'all');
579 prButtonLock(true, "${_('Please select source and target')}", 'all');
532
580
@@ -543,12 +591,44 b''
543 $sourceRef.select2('val', '${c.default_source_ref}');
591 $sourceRef.select2('val', '${c.default_source_ref}');
544
592
545
593
546 // default reviewers
594 // default reviewers / observers
547 reviewersController.loadDefaultReviewers(
595 reviewersController.loadDefaultReviewers(
548 sourceRepo(), sourceRef(), targetRepo(), targetRef());
596 sourceRepo(), sourceRef(), targetRepo(), targetRef());
549 % endif
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 </script>
633 </script>
554
634
@@ -556,12 +556,12 b''
556 <div class="sidebar-element clear-both">
556 <div class="sidebar-element clear-both">
557 <% vote_title = _ungettext(
557 <% vote_title = _ungettext(
558 'Status calculated based on votes from {} reviewer',
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 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
562 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
563 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
563 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
564 ${len(c.allowed_reviewers)}
564 ${c.reviewers_count}
565 </div>
565 </div>
566
566
567 ## REVIEW RULES
567 ## REVIEW RULES
@@ -609,13 +609,13 b''
609 <div id="add_reviewer" class="ac" style="display: none;">
609 <div id="add_reviewer" class="ac" style="display: none;">
610 %if c.allowed_to_update:
610 %if c.allowed_to_update:
611 % if not c.forbid_adding_reviewers:
611 % if not c.forbid_adding_reviewers:
612 <div id="add_reviewer_input" class="reviewer_ac">
612 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px">
613 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
613 <input class="ac-input" id="user" name="user" placeholder="${_('Add reviewer or reviewer group')}" type="text" autocomplete="off">
614 <div id="reviewers_container"></div>
614 <div id="reviewers_container"></div>
615 </div>
615 </div>
616 % endif
616 % endif
617 <div class="pull-right">
617 <div class="pull-right" style="margin-bottom: 15px">
618 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
618 <button data-role="reviewer" id="update_reviewers" class="btn btn-small no-margin">${_('Save Changes')}</button>
619 </div>
619 </div>
620 %endif
620 %endif
621 </div>
621 </div>
@@ -623,23 +623,52 b''
623 </div>
623 </div>
624 </div>
624 </div>
625
625
626 ## ## OBSERVERS
626 ## OBSERVERS
627 ## <div class="sidebar-element clear-both">
627 <div class="sidebar-element clear-both">
628 ## <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Observers')}">
628 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Observers')}">
629 ## <i class="icon-eye"></i>
629 <i class="icon-circle-thin"></i>
630 ## 0
630 ${c.observers_count}
631 ## </div>
631 </div>
632 ##
632
633 ## <div class="right-sidebar-expanded-state pr-details-title">
633 <div class="right-sidebar-expanded-state pr-details-title">
634 ## <span class="sidebar-heading">
634 <span class="sidebar-heading">
635 ## <i class="icon-eye"></i>
635 <i class="icon-circle-thin"></i>
636 ## ${_('Observers')}
636 ${_('Observers')}
637 ## </span>
637 </span>
638 ## </div>
638 %if c.allowed_to_update:
639 ## <div class="right-sidebar-expanded-state pr-details-content">
639 <span id="open_edit_observers" class="block-right action_button last-item">${_('Edit')}</span>
640 ## No observers
640 <span id="close_edit_observers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
641 ## </div>
641 %endif
642 ## </div>
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 ## TODOs
673 ## TODOs
645 <div class="sidebar-element clear-both">
674 <div class="sidebar-element clear-both">
@@ -815,6 +844,7 b' updateController = new UpdatePrControlle'
815
844
816 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
845 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
817 window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
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 (function () {
849 (function () {
820 "use strict";
850 "use strict";
@@ -822,44 +852,9 b' window.setReviewersData = ${c.pull_reque'
822 // custom code mirror
852 // custom code mirror
823 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
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 PRDetails.init();
855 PRDetails.init();
862 ReviewersPanel.init(reviewerRulesData, setReviewersData);
856 ReviewersPanel.init(reviewerRulesData, setReviewersData);
857 ObserversPanel.init(reviewerRulesData, setObserversData);
863
858
864 window.showOutdated = function (self) {
859 window.showOutdated = function (self) {
865 $('.comment-inline.comment-outdated').show();
860 $('.comment-inline.comment-outdated').show();
@@ -929,12 +924,17 b' window.setReviewersData = ${c.pull_reque'
929 title, description, renderer);
924 title, description, renderer);
930 });
925 });
931
926
932 $('#update_pull_request').on('click', function (e) {
927 var $updateButtons = $('#update_reviewers,#update_observers');
933 $(this).attr('disabled', 'disabled');
928 $updateButtons.on('click', function (e) {
934 $(this).addClass('disabled');
929 var role = $(this).data('role');
935 $(this).html(_gettext('Saving...'));
930 $updateButtons.attr('disabled', 'disabled');
931 $updateButtons.addClass('disabled');
932 $updateButtons.html(_gettext('Saving...'));
936 reviewersController.updateReviewers(
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 // fixing issue with caches on firefox
940 // fixing issue with caches on firefox
@@ -978,7 +978,8 b' window.setReviewersData = ${c.pull_reque'
978 refreshMergeChecks();
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 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
25 from rhodecode.lib.utils2 import AttributeDict
25 from rhodecode.lib.utils2 import AttributeDict
26 from rhodecode.model.db import User
26 from rhodecode.model.db import User, PullRequestReviewers
27 from rhodecode.model.notification import EmailNotificationModel
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 assert 'Email Body' in body
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 ref = collections.namedtuple(
57 ref = collections.namedtuple(
57 'Ref', 'name, type')('fxies123', 'book')
58 'Ref', 'name, type')('fxies123', 'book')
58
59
@@ -75,13 +76,17 b' def test_render_pr_email(app, user_admin'
75 'pull_request_source_repo_url': 'x',
76 'pull_request_source_repo_url': 'x',
76
77
77 'pull_request_url': 'http://localhost/pr1',
78 'pull_request_url': 'http://localhost/pr1',
79 'user_role': role,
78 }
80 }
79
81
80 subject, body, body_plaintext = EmailNotificationModel().render_email(
82 subject, body, body_plaintext = EmailNotificationModel().render_email(
81 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
83 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
82
84
83 # subject
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 def test_render_pr_update_email(app, user_admin):
92 def test_render_pr_update_email(app, user_admin):
General Comments 0
You need to be logged in to leave comments. Login now