##// END OF EJS Templates
reviewers: added observers as another way to define reviewers....
marcink -
r4500:bfede169 stable
parent child
Show More
@@ -0,0 +1,68
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
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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
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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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