##// END OF EJS Templates
release: merge back stable branch into default
marcink -
r4528:5055a30b merge default
parent child Browse files
Show More

The requested changes are too big and content was truncated. Show full diff

@@ -0,0 +1,54 b''
1 |RCE| 4.21.0 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2020-09-28
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13 - Pull requests: overhaul of the UX/UI by adding new sidebar
14 - Pull requests: new live reviewer present indicator (requires channelstream enabled)
15 - Pull requests: new live new comments indicator (requires channelstream enabled)
16 - Pull requests: new sidebar with comments/todos/referenced tickets navigation
17 - Commits page: Introduced sidebar for single commits pages
18
19
20 General
21 ^^^^^^^
22
23 - API: allow repo admins to get/set settings.
24 Previously it was only super-admins that could do that.
25 - Sessions: patch baker to take expire time for redis for auto session cleanup feature.
26 - Git: bumped git version to 2.27.0
27 - Packages: bumped to channelstream==0.6.14
28
29
30 Security
31 ^^^^^^^^
32
33 - Issue trackers: fix XSS with description field.
34
35
36 Performance
37 ^^^^^^^^^^^
38
39 - Artifacts: speed-up of artifacts download request processing.
40
41
42 Fixes
43 ^^^^^
44
45 - Pull requests: properly save merge failure metadata.
46 In rare cases merge check reported conflicts which there were none.
47 - Sessions: fixed cleanup with corrupted session data issue.
48
49
50 Upgrade notes
51 ^^^^^^^^^^^^^
52
53 - Scheduled feature release.
54 - Git version was bumped to 2.27.0
@@ -0,0 +1,55 b''
1 |RCE| 4.22.0 |RNS|
2 ------------------
3
4 Release Date
5 ^^^^^^^^^^^^
6
7 - 2020-10-12
8
9
10 New Features
11 ^^^^^^^^^^^^
12
13 - Reviewers: added observers as another role for reviewers.
14 Observers is a role that doesn't require voting, but still gets notified about
15 PR and should participate in review process.
16 - Issue trackers: implemented more sophisticated ticket data extraction based on
17 advanced regex module. This allows using ticket references without false positives
18 like catching ticket data in an URL.
19 - Channelstream: Notification about updates and comments now works via API, and both
20 Pull-requests and individual commits.
21
22
23 General
24 ^^^^^^^
25
26 - Data tables: unified tables look for main pages of rhodecode repo pages.
27 - Users: autocomplete now sorts by matched username to show best matches first.
28 - Pull requests: only allow actual reviewers to leave status/votes in order to not
29 confuse others users about voting from people who aren't actual reviewers.
30
31 Security
32 ^^^^^^^^
33
34
35
36 Performance
37 ^^^^^^^^^^^
38
39 - Default reviewers: optimize diff data, and creation of PR with advanced default reviewers
40 - default-reviewers: diff data should load more things lazy for better performance.
41 - Pull requests: limit the amount of data saved in default reviewers data for better memory usage
42 - DB: don't use lazy loaders on PR related objects, to optimize memory usage on large
43 Pull requests with lots of comments, and commits.
44
45 Fixes
46 ^^^^^
47
48 - Quick search bar: fixes #5634, crash when search on non-ascii characters.
49 - Sidebar: few fixes for panel rendering of reviewers/observers for both commits and PRS.
50
51 Upgrade notes
52 ^^^^^^^^^^^^^
53
54 - Scheduled feature release.
55
@@ -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
@@ -68,3 +68,5 b' 7ac623a4a2405917e2af660d645ded662011e40d'
68 68 ef7ffda65eeb90c3ba88590a6cb816ef9b0bc232 v4.19.3
69 69 3e635489bb7961df93b01e42454ad1a8730ae968 v4.20.0
70 70 7e2eb896a02ca7cd2cd9f0f853ef3dac3f0039e3 v4.20.1
71 8bb5fece08ab65986225b184e46f53d2a71729cb v4.21.0
72 90734aac31ee4563bbe665a43ff73190cc762275 v4.22.0
@@ -90,7 +90,7 b' comment_pull_request'
90 90 create_pull_request
91 91 -------------------
92 92
93 .. py:function:: create_pull_request(apiuser, source_repo, target_repo, source_ref, target_ref, owner=<Optional:<OptionalAttr:apiuser>>, title=<Optional:''>, description=<Optional:''>, description_renderer=<Optional:''>, reviewers=<Optional:None>)
93 .. py:function:: create_pull_request(apiuser, source_repo, target_repo, source_ref, target_ref, owner=<Optional:<OptionalAttr:apiuser>>, title=<Optional:''>, description=<Optional:''>, description_renderer=<Optional:''>, reviewers=<Optional:None>, observers=<Optional:None>)
94 94
95 95 Creates a new pull request.
96 96
@@ -128,6 +128,13 b' create_pull_request'
128 128 Accepts username strings or objects of the format:
129 129
130 130 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
131 :param observers: Set the new pull request observers list.
132 Reviewer defined by review rules will be added automatically to the
133 defined list. This feature is only available in RhodeCode EE
134 :type observers: Optional(list)
135 Accepts username strings or objects of the format:
136
137 [{'username': 'nick', 'reasons': ['original author']}]
131 138
132 139
133 140 get_pull_request
@@ -392,7 +399,7 b' merge_pull_request'
392 399 update_pull_request
393 400 -------------------
394 401
395 .. py:function:: update_pull_request(apiuser, pullrequestid, repoid=<Optional:None>, title=<Optional:''>, description=<Optional:''>, description_renderer=<Optional:''>, reviewers=<Optional:None>, update_commits=<Optional:None>)
402 .. py:function:: update_pull_request(apiuser, pullrequestid, repoid=<Optional:None>, title=<Optional:''>, description=<Optional:''>, description_renderer=<Optional:''>, reviewers=<Optional:None>, observers=<Optional:None>, update_commits=<Optional:None>)
396 403
397 404 Updates a pull request.
398 405
@@ -414,7 +421,11 b' update_pull_request'
414 421 Accepts username strings or objects of the format:
415 422
416 423 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
424 :param observers: Update pull request observers list with new value.
425 :type observers: Optional(list)
426 Accepts username strings or objects of the format:
417 427
428 [{'username': 'nick', 'reasons': ['should be aware about this PR']}]
418 429 :param update_commits: Trigger update of commits for this pull request
419 430 :type: update_commits: Optional(bool)
420 431
@@ -432,6 +443,12 b' update_pull_request'
432 443 ],
433 444 "removed": []
434 445 },
446 "updated_observers": {
447 "added": [
448 "username"
449 ],
450 "removed": []
451 },
435 452 "updated_commits": {
436 453 "added": [
437 454 "<sha1_hash>"
@@ -9,6 +9,8 b' Release Notes'
9 9 .. toctree::
10 10 :maxdepth: 1
11 11
12 release-notes-4.22.0.rst
13 release-notes-4.21.0.rst
12 14 release-notes-4.20.1.rst
13 15 release-notes-4.20.0.rst
14 16 release-notes-4.19.3.rst
@@ -1816,6 +1816,17 b' self: super: {'
1816 1816 license = [ pkgs.lib.licenses.mit ];
1817 1817 };
1818 1818 };
1819 "regex" = super.buildPythonPackage {
1820 name = "regex-2020.9.27";
1821 doCheck = false;
1822 src = fetchurl {
1823 url = "https://files.pythonhosted.org/packages/93/8c/17f45cdfb39b13d4b5f909e4b4c2917abcbdef9c0036919a0399769148cf/regex-2020.9.27.tar.gz";
1824 sha256 = "179ngfzwbsjvn5vhyzdahvmg0f7acahkwwy9bpjy1pv08bm2mwx6";
1825 };
1826 meta = {
1827 license = [ pkgs.lib.licenses.psfl ];
1828 };
1829 };
1819 1830 "redis" = super.buildPythonPackage {
1820 1831 name = "redis-3.4.1";
1821 1832 doCheck = false;
@@ -1872,7 +1883,7 b' self: super: {'
1872 1883 };
1873 1884 };
1874 1885 "rhodecode-enterprise-ce" = super.buildPythonPackage {
1875 name = "rhodecode-enterprise-ce-4.20.0";
1886 name = "rhodecode-enterprise-ce-4.22.0";
1876 1887 buildInputs = [
1877 1888 self."pytest"
1878 1889 self."py"
@@ -1946,6 +1957,7 b' self: super: {'
1946 1957 self."tzlocal"
1947 1958 self."pyzmq"
1948 1959 self."py-gfm"
1960 self."regex"
1949 1961 self."redis"
1950 1962 self."repoze.lru"
1951 1963 self."requests"
@@ -56,6 +56,7 b' pytz==2019.3'
56 56 tzlocal==1.5.1
57 57 pyzmq==14.6.0
58 58 py-gfm==0.1.4
59 regex==2020.9.27
59 60 redis==3.4.1
60 61 repoze.lru==0.7
61 62 requests==2.22.0
@@ -48,7 +48,7 b' PYRAMID_SETTINGS = {}'
48 48 EXTENSIONS = {}
49 49
50 50 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
51 __dbversion__ = 109 # defines current db version for migrations
51 __dbversion__ = 110 # defines current db version for migrations
52 52 __platform__ = platform.system()
53 53 __license__ = 'AGPLv3, and Commercial License'
54 54 __author__ = 'RhodeCode GmbH'
@@ -320,7 +320,7 b' class TestCreatePullRequestApi(object):'
320 320 id_, params = build_data(
321 321 self.apikey_regular, 'create_pull_request', **data)
322 322 response = api_call(self.app, params)
323 expected_message = 'no commits found'
323 expected_message = 'no commits found for merge between specified references'
324 324 assert_error(id_, expected_message, given=response.body)
325 325
326 326 @pytest.mark.backends("git", "hg")
@@ -29,6 +29,7 b' from rhodecode.api.tests.utils import ('
29 29
30 30 @pytest.mark.usefixtures("testuser_api", "app")
31 31 class TestGetPullRequest(object):
32
32 33 @pytest.mark.backends("git", "hg")
33 34 def test_api_get_pull_requests(self, pr_util):
34 35 pull_request = pr_util.create_pull_request()
@@ -40,6 +41,7 b' class TestGetPullRequest(object):'
40 41 target_ref=pull_request.target_ref,
41 42 revisions=pull_request.revisions,
42 43 reviewers=(),
44 observers=(),
43 45 title=pull_request.title,
44 46 description=pull_request.description,
45 47 )
@@ -51,6 +51,7 b' class TestUpdatePullRequest(object):'
51 51 "pull_request": response.json['result']['pull_request'],
52 52 "updated_commits": {"added": [], "common": [], "removed": []},
53 53 "updated_reviewers": {"added": [], "removed": []},
54 "updated_observers": {"added": [], "removed": []},
54 55 }
55 56
56 57 response_json = response.json['result']
@@ -111,6 +112,7 b' class TestUpdatePullRequest(object):'
111 112 "total": total_commits,
112 113 "removed": []},
113 114 "updated_reviewers": {"added": [], "removed": []},
115 "updated_observers": {"added": [], "removed": []},
114 116 }
115 117
116 118 assert_ok(id_, expected, response.body)
@@ -122,7 +124,7 b' class TestUpdatePullRequest(object):'
122 124 b = user_util.create_user()
123 125 c = user_util.create_user()
124 126 new_reviewers = [
125 {'username': b.username,'reasons': ['updated via API'],
127 {'username': b.username, 'reasons': ['updated via API'],
126 128 'mandatory':False},
127 129 {'username': c.username, 'reasons': ['updated via API'],
128 130 'mandatory':False},
@@ -132,7 +134,7 b' class TestUpdatePullRequest(object):'
132 134 removed = [a.username]
133 135
134 136 pull_request = pr_util.create_pull_request(
135 reviewers=[(a.username, ['added via API'], False, [])])
137 reviewers=[(a.username, ['added via API'], False, 'reviewer', [])])
136 138
137 139 id_, params = build_data(
138 140 self.apikey, 'update_pull_request',
@@ -146,6 +148,7 b' class TestUpdatePullRequest(object):'
146 148 "pull_request": response.json['result']['pull_request'],
147 149 "updated_commits": {"added": [], "common": [], "removed": []},
148 150 "updated_reviewers": {"added": added, "removed": removed},
151 "updated_observers": {"added": [], "removed": []},
149 152 }
150 153
151 154 assert_ok(id_, expected, response.body)
@@ -26,12 +26,15 b' from rhodecode.api.utils import ('
26 26 has_superadmin_permission, Optional, OAttr, get_repo_or_error,
27 27 get_pull_request_or_error, get_commit_or_error, get_user_or_error,
28 28 validate_repo_permissions, resolve_ref_or_error, validate_set_owner_permissions)
29 from rhodecode.lib import channelstream
29 30 from rhodecode.lib.auth import (HasRepoPermissionAnyApi)
30 31 from rhodecode.lib.base import vcs_operation_context
31 32 from rhodecode.lib.utils2 import str2bool
33 from rhodecode.lib.vcs.backends.base import unicode_to_reference
32 34 from rhodecode.model.changeset_status import ChangesetStatusModel
33 35 from rhodecode.model.comment import CommentsModel
34 from rhodecode.model.db import Session, ChangesetStatus, ChangesetComment, PullRequest
36 from rhodecode.model.db import (
37 Session, ChangesetStatus, ChangesetComment, PullRequest, PullRequestReviewers)
35 38 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
36 39 from rhodecode.model.settings import SettingsModel
37 40 from rhodecode.model.validation_schema import Invalid
@@ -502,16 +505,19 b' def comment_pull_request('
502 505 },
503 506 error : null
504 507 """
508 _ = request.translate
509
505 510 pull_request = get_pull_request_or_error(pullrequestid)
506 511 if Optional.extract(repoid):
507 512 repo = get_repo_or_error(repoid)
508 513 else:
509 514 repo = pull_request.target_repo
510 515
516 db_repo_name = repo.repo_name
511 517 auth_user = apiuser
512 518 if not isinstance(userid, Optional):
513 519 is_repo_admin = HasRepoPermissionAnyApi('repository.admin')(
514 user=apiuser, repo_name=repo.repo_name)
520 user=apiuser, repo_name=db_repo_name)
515 521 if has_superadmin_permission(apiuser) or is_repo_admin:
516 522 apiuser = get_user_or_error(userid)
517 523 auth_user = apiuser.AuthUser()
@@ -596,6 +602,7 b' def comment_pull_request('
596 602 extra_recipients=extra_recipients,
597 603 send_email=send_email
598 604 )
605 is_inline = comment.is_inline
599 606
600 607 if allowed_to_change_status and status:
601 608 old_calculated_status = pull_request.calculated_review_status()
@@ -628,14 +635,39 b' def comment_pull_request('
628 635 'comment_id': comment.comment_id if comment else None,
629 636 'status': {'given': status, 'was_changed': status_change},
630 637 }
638
639 comment_broadcast_channel = channelstream.comment_channel(
640 db_repo_name, pull_request_obj=pull_request)
641
642 comment_data = data
643 comment_type = 'inline' if is_inline else 'general'
644 channelstream.comment_channelstream_push(
645 request, comment_broadcast_channel, apiuser,
646 _('posted a new {} comment').format(comment_type),
647 comment_data=comment_data)
648
631 649 return data
632 650
651 def _reviewers_validation(obj_list):
652 schema = ReviewerListSchema()
653 try:
654 reviewer_objects = schema.deserialize(obj_list)
655 except Invalid as err:
656 raise JSONRPCValidationError(colander_exc=err)
657
658 # validate users
659 for reviewer_object in reviewer_objects:
660 user = get_user_or_error(reviewer_object['username'])
661 reviewer_object['user_id'] = user.user_id
662 return reviewer_objects
663
633 664
634 665 @jsonrpc_method()
635 666 def create_pull_request(
636 667 request, apiuser, source_repo, target_repo, source_ref, target_ref,
637 668 owner=Optional(OAttr('apiuser')), title=Optional(''), description=Optional(''),
638 description_renderer=Optional(''), reviewers=Optional(None)):
669 description_renderer=Optional(''),
670 reviewers=Optional(None), observers=Optional(None)):
639 671 """
640 672 Creates a new pull request.
641 673
@@ -673,6 +705,13 b' def create_pull_request('
673 705 Accepts username strings or objects of the format:
674 706
675 707 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
708 :param observers: Set the new pull request observers list.
709 Reviewer defined by review rules will be added automatically to the
710 defined list. This feature is only available in RhodeCode EE
711 :type observers: Optional(list)
712 Accepts username strings or objects of the format:
713
714 [{'username': 'nick', 'reasons': ['original author']}]
676 715 """
677 716
678 717 source_db_repo = get_repo_or_error(source_repo)
@@ -686,34 +725,39 b' def create_pull_request('
686 725 full_source_ref = resolve_ref_or_error(source_ref, source_db_repo)
687 726 full_target_ref = resolve_ref_or_error(target_ref, target_db_repo)
688 727
689 source_commit = get_commit_or_error(full_source_ref, source_db_repo)
690 target_commit = get_commit_or_error(full_target_ref, target_db_repo)
728 get_commit_or_error(full_source_ref, source_db_repo)
729 get_commit_or_error(full_target_ref, target_db_repo)
691 730
692 731 reviewer_objects = Optional.extract(reviewers) or []
732 observer_objects = Optional.extract(observers) or []
693 733
694 734 # serialize and validate passed in given reviewers
695 735 if reviewer_objects:
696 schema = ReviewerListSchema()
697 try:
698 reviewer_objects = schema.deserialize(reviewer_objects)
699 except Invalid as err:
700 raise JSONRPCValidationError(colander_exc=err)
736 reviewer_objects = _reviewers_validation(reviewer_objects)
737
738 if observer_objects:
739 observer_objects = _reviewers_validation(reviewer_objects)
701 740
702 # validate users
703 for reviewer_object in reviewer_objects:
704 user = get_user_or_error(reviewer_object['username'])
705 reviewer_object['user_id'] = user.user_id
741 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
742 PullRequestModel().get_reviewer_functions()
706 743
707 get_default_reviewers_data, validate_default_reviewers = \
708 PullRequestModel().get_reviewer_functions()
744 source_ref_obj = unicode_to_reference(full_source_ref)
745 target_ref_obj = unicode_to_reference(full_target_ref)
709 746
710 747 # recalculate reviewers logic, to make sure we can validate this
711 748 default_reviewers_data = get_default_reviewers_data(
712 owner, source_db_repo,
713 source_commit, target_db_repo, target_commit)
749 owner,
750 source_db_repo,
751 source_ref_obj,
752 target_db_repo,
753 target_ref_obj,
754 )
714 755
715 # now MERGE our given with the calculated
716 reviewer_objects = default_reviewers_data['reviewers'] + reviewer_objects
756 # now MERGE our given with the calculated from the default rules
757 just_reviewers = [
758 x for x in default_reviewers_data['reviewers']
759 if x['role'] == PullRequestReviewers.ROLE_REVIEWER]
760 reviewer_objects = just_reviewers + reviewer_objects
717 761
718 762 try:
719 763 reviewers = validate_default_reviewers(
@@ -721,9 +765,21 b' def create_pull_request('
721 765 except ValueError as e:
722 766 raise JSONRPCError('Reviewers Validation: {}'.format(e))
723 767
768 # now MERGE our given with the calculated from the default rules
769 just_observers = [
770 x for x in default_reviewers_data['reviewers']
771 if x['role'] == PullRequestReviewers.ROLE_OBSERVER]
772 observer_objects = just_observers + observer_objects
773
774 try:
775 observers = validate_observers(
776 observer_objects, default_reviewers_data)
777 except ValueError as e:
778 raise JSONRPCError('Observer Validation: {}'.format(e))
779
724 780 title = Optional.extract(title)
725 781 if not title:
726 title_source_ref = source_ref.split(':', 2)[1]
782 title_source_ref = source_ref_obj.name
727 783 title = PullRequestModel().generate_pullrequest_title(
728 784 source=source_repo,
729 785 source_ref=title_source_ref,
@@ -732,20 +788,17 b' def create_pull_request('
732 788
733 789 diff_info = default_reviewers_data['diff_info']
734 790 common_ancestor_id = diff_info['ancestor']
735 commits = diff_info['commits']
791 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
792 commits = [commit['commit_id'] for commit in reversed(diff_info['commits'])]
736 793
737 794 if not common_ancestor_id:
738 raise JSONRPCError('no common ancestor found')
795 raise JSONRPCError('no common ancestor found between specified references')
739 796
740 797 if not commits:
741 raise JSONRPCError('no commits found')
742
743 # NOTE(marcink): reversed is consistent with how we open it in the WEB interface
744 revisions = [commit.raw_id for commit in reversed(commits)]
798 raise JSONRPCError('no commits found for merge between specified references')
745 799
746 800 # recalculate target ref based on ancestor
747 target_ref_type, target_ref_name, __ = full_target_ref.split(':')
748 full_target_ref = ':'.join((target_ref_type, target_ref_name, common_ancestor_id))
801 full_target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, common_ancestor_id))
749 802
750 803 # fetch renderer, if set fallback to plain in case of PR
751 804 rc_config = SettingsModel().get_all_settings()
@@ -760,8 +813,9 b' def create_pull_request('
760 813 target_repo=target_repo,
761 814 target_ref=full_target_ref,
762 815 common_ancestor_id=common_ancestor_id,
763 revisions=revisions,
816 revisions=commits,
764 817 reviewers=reviewers,
818 observers=observers,
765 819 title=title,
766 820 description=description,
767 821 description_renderer=description_renderer,
@@ -781,7 +835,7 b' def create_pull_request('
781 835 def update_pull_request(
782 836 request, apiuser, pullrequestid, repoid=Optional(None),
783 837 title=Optional(''), description=Optional(''), description_renderer=Optional(''),
784 reviewers=Optional(None), update_commits=Optional(None)):
838 reviewers=Optional(None), observers=Optional(None), update_commits=Optional(None)):
785 839 """
786 840 Updates a pull request.
787 841
@@ -803,7 +857,11 b' def update_pull_request('
803 857 Accepts username strings or objects of the format:
804 858
805 859 [{'username': 'nick', 'reasons': ['original author'], 'mandatory': <bool>}]
860 :param observers: Update pull request observers list with new value.
861 :type observers: Optional(list)
862 Accepts username strings or objects of the format:
806 863
864 [{'username': 'nick', 'reasons': ['should be aware about this PR']}]
807 865 :param update_commits: Trigger update of commits for this pull request
808 866 :type: update_commits: Optional(bool)
809 867
@@ -821,6 +879,12 b' def update_pull_request('
821 879 ],
822 880 "removed": []
823 881 },
882 "updated_observers": {
883 "added": [
884 "username"
885 ],
886 "removed": []
887 },
824 888 "updated_commits": {
825 889 "added": [
826 890 "<sha1_hash>"
@@ -852,36 +916,14 b' def update_pull_request('
852 916 pullrequestid,))
853 917
854 918 reviewer_objects = Optional.extract(reviewers) or []
855
856 if reviewer_objects:
857 schema = ReviewerListSchema()
858 try:
859 reviewer_objects = schema.deserialize(reviewer_objects)
860 except Invalid as err:
861 raise JSONRPCValidationError(colander_exc=err)
862
863 # validate users
864 for reviewer_object in reviewer_objects:
865 user = get_user_or_error(reviewer_object['username'])
866 reviewer_object['user_id'] = user.user_id
867
868 get_default_reviewers_data, get_validated_reviewers = \
869 PullRequestModel().get_reviewer_functions()
870
871 # re-use stored rules
872 reviewer_rules = pull_request.reviewer_data
873 try:
874 reviewers = get_validated_reviewers(
875 reviewer_objects, reviewer_rules)
876 except ValueError as e:
877 raise JSONRPCError('Reviewers Validation: {}'.format(e))
878 else:
879 reviewers = []
919 observer_objects = Optional.extract(observers) or []
880 920
881 921 title = Optional.extract(title)
882 922 description = Optional.extract(description)
883 923 description_renderer = Optional.extract(description_renderer)
884 924
925 # Update title/description
926 title_changed = False
885 927 if title or description:
886 928 PullRequestModel().edit(
887 929 pull_request,
@@ -890,8 +932,12 b' def update_pull_request('
890 932 description_renderer or pull_request.description_renderer,
891 933 apiuser)
892 934 Session().commit()
935 title_changed = True
893 936
894 937 commit_changes = {"added": [], "common": [], "removed": []}
938
939 # Update commits
940 commits_changed = False
895 941 if str2bool(Optional.extract(update_commits)):
896 942
897 943 if pull_request.pull_request_state != PullRequest.STATE_CREATED:
@@ -907,12 +953,44 b' def update_pull_request('
907 953 pull_request, db_user)
908 954 commit_changes = update_response.changes or commit_changes
909 955 Session().commit()
956 commits_changed = True
910 957
958 # Update reviewers
959 # serialize and validate passed in given reviewers
960 if reviewer_objects:
961 reviewer_objects = _reviewers_validation(reviewer_objects)
962
963 if observer_objects:
964 observer_objects = _reviewers_validation(reviewer_objects)
965
966 # re-use stored rules
967 default_reviewers_data = pull_request.reviewer_data
968
969 __, validate_default_reviewers, validate_observers = \
970 PullRequestModel().get_reviewer_functions()
971
972 if reviewer_objects:
973 try:
974 reviewers = validate_default_reviewers(reviewer_objects, default_reviewers_data)
975 except ValueError as e:
976 raise JSONRPCError('Reviewers Validation: {}'.format(e))
977 else:
978 reviewers = []
979
980 if observer_objects:
981 try:
982 observers = validate_default_reviewers(reviewer_objects, default_reviewers_data)
983 except ValueError as e:
984 raise JSONRPCError('Observer Validation: {}'.format(e))
985 else:
986 observers = []
987
988 reviewers_changed = False
911 989 reviewers_changes = {"added": [], "removed": []}
912 990 if reviewers:
913 991 old_calculated_status = pull_request.calculated_review_status()
914 992 added_reviewers, removed_reviewers = \
915 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser)
993 PullRequestModel().update_reviewers(pull_request, reviewers, apiuser.get_instance())
916 994
917 995 reviewers_changes['added'] = sorted(
918 996 [get_user_or_error(n).username for n in added_reviewers])
@@ -926,13 +1004,35 b' def update_pull_request('
926 1004 PullRequestModel().trigger_pull_request_hook(
927 1005 pull_request, apiuser, 'review_status_change',
928 1006 data={'status': calculated_status})
1007 reviewers_changed = True
1008
1009 observers_changed = False
1010 observers_changes = {"added": [], "removed": []}
1011 if observers:
1012 added_observers, removed_observers = \
1013 PullRequestModel().update_observers(pull_request, observers, apiuser.get_instance())
1014
1015 observers_changes['added'] = sorted(
1016 [get_user_or_error(n).username for n in added_observers])
1017 observers_changes['removed'] = sorted(
1018 [get_user_or_error(n).username for n in removed_observers])
1019 Session().commit()
1020
1021 reviewers_changed = True
1022
1023 # push changed to channelstream
1024 if commits_changed or reviewers_changed or observers_changed:
1025 pr_broadcast_channel = channelstream.pr_channel(pull_request)
1026 msg = 'Pull request was updated.'
1027 channelstream.pr_update_channelstream_push(
1028 request, pr_broadcast_channel, apiuser, msg)
929 1029
930 1030 data = {
931 'msg': 'Updated pull request `{}`'.format(
932 pull_request.pull_request_id),
1031 'msg': 'Updated pull request `{}`'.format(pull_request.pull_request_id),
933 1032 'pull_request': pull_request.get_api_data(),
934 1033 'updated_commits': commit_changes,
935 'updated_reviewers': reviewers_changes
1034 'updated_reviewers': reviewers_changes,
1035 'updated_observers': observers_changes,
936 1036 }
937 1037
938 1038 return data
@@ -29,7 +29,7 b' from rhodecode.api.utils import ('
29 29 get_user_group_or_error, get_user_or_error, validate_repo_permissions,
30 30 get_perm_or_error, parse_args, get_origin, build_commit_data,
31 31 validate_set_owner_permissions)
32 from rhodecode.lib import audit_logger, rc_cache
32 from rhodecode.lib import audit_logger, rc_cache, channelstream
33 33 from rhodecode.lib import repo_maintenance
34 34 from rhodecode.lib.auth import (
35 35 HasPermissionAnyApi, HasUserGroupPermissionAnyApi,
@@ -1597,10 +1597,13 b' def comment_commit('
1597 1597 }
1598 1598
1599 1599 """
1600 _ = request.translate
1601
1600 1602 repo = get_repo_or_error(repoid)
1601 1603 if not has_superadmin_permission(apiuser):
1602 1604 _perms = ('repository.read', 'repository.write', 'repository.admin')
1603 1605 validate_repo_permissions(apiuser, repoid, repo, _perms)
1606 db_repo_name = repo.repo_name
1604 1607
1605 1608 try:
1606 1609 commit = repo.scm_instance().get_commit(commit_id=commit_id)
@@ -1650,6 +1653,8 b' def comment_commit('
1650 1653 extra_recipients=extra_recipients,
1651 1654 send_email=send_email
1652 1655 )
1656 is_inline = comment.is_inline
1657
1653 1658 if status:
1654 1659 # also do a status change
1655 1660 try:
@@ -1669,6 +1674,17 b' def comment_commit('
1669 1674 data={'comment': comment, 'commit': commit})
1670 1675
1671 1676 Session().commit()
1677
1678 comment_broadcast_channel = channelstream.comment_channel(
1679 db_repo_name, commit_obj=commit)
1680
1681 comment_data = {'comment': comment, 'comment_id': comment.comment_id}
1682 comment_type = 'inline' if is_inline else 'general'
1683 channelstream.comment_channelstream_push(
1684 request, comment_broadcast_channel, apiuser,
1685 _('posted a new {} comment').format(comment_type),
1686 comment_data=comment_data)
1687
1672 1688 return {
1673 1689 'msg': (
1674 1690 'Commented on commit `%s` for repository `%s`' % (
@@ -474,9 +474,18 b' class AdminSettingsView(BaseAppView):'
474 474 route_name='admin_settings_issuetracker_test', request_method='POST',
475 475 renderer='string', xhr=True)
476 476 def settings_issuetracker_test(self):
477 return h.urlify_commit_message(
477 error_container = []
478
479 urlified_commit = h.urlify_commit_message(
478 480 self.request.POST.get('test_text', ''),
479 'repo_group/test_repo1')
481 'repo_group/test_repo1', error_container=error_container)
482 if error_container:
483 def converter(inp):
484 return h.html_escape(unicode(inp))
485
486 return 'ERRORS: ' + '\n'.join(map(converter, error_container))
487
488 return urlified_commit
480 489
481 490 @LoginRequired()
482 491 @HasPermissionAllDecorator('hg.admin')
@@ -34,6 +34,7 b' log = logging.getLogger(__name__)'
34 34
35 35
36 36 class DebugStyleView(BaseAppView):
37
37 38 def load_default_context(self):
38 39 c = self._get_local_tmpl_context()
39 40
@@ -75,6 +76,7 b' Check if we should use full-topic or min'
75 76 source_ref_parts=AttributeDict(type='branch', name='fix-ticket-2000'),
76 77 target_ref_parts=AttributeDict(type='branch', name='master'),
77 78 )
79
78 80 target_repo = AttributeDict(repo_name='repo_group/target_repo')
79 81 source_repo = AttributeDict(repo_name='repo_group/source_repo')
80 82 user = User.get_by_username(self.request.GET.get('user')) or self._rhodecode_db_user
@@ -83,6 +85,7 b' Check if we should use full-topic or min'
83 85 'added': ['aaaaaaabbbbb', 'cccccccddddddd'],
84 86 'removed': ['eeeeeeeeeee'],
85 87 })
88
86 89 file_changes = AttributeDict({
87 90 'added': ['a/file1.md', 'file2.py'],
88 91 'modified': ['b/modified_file.rst'],
@@ -97,15 +100,19 b' Check if we should use full-topic or min'
97 100 'exc_message': 'Traceback (most recent call last):\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/tweens.py", line 41, in excview_tween\n response = handler(request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/router.py", line 148, in handle_request\n registry, request, context, context_iface, view_name\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/view.py", line 667, in _call_view\n response = view_callable(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 188, in attr_view\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/config/views.py", line 214, in predicate_wrapper\n return view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 401, in viewresult_to_response\n result = view(context, request)\n File "/nix/store/s43k2r9rysfbzmsjdqnxgzvvb7zjhkxb-python2.7-pyramid-1.10.4/lib/python2.7/site-packages/pyramid/viewderivers.py", line 132, in _class_view\n response = getattr(inst, attr)()\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/apps/debug_style/views.py", line 355, in render_email\n template_type, **email_kwargs.get(email_id, {}))\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/model/notification.py", line 402, in render_email\n body = email_template.render(None, **_kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 95, in render\n return self._render_with_exc(tmpl, args, kwargs)\n File "/mnt/hgfs/marcink/workspace/rhodecode-enterprise-ce/rhodecode/lib/partial_renderer.py", line 79, in _render_with_exc\n return render_func.render(*args, **kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/template.py", line 476, in render\n return runtime._render(self, self.callable_, args, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 883, in _render\n **_kwargs_for_callable(callable_, data)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 920, in _render_context\n _exec_template(inherit, lclcontext, args=args, kwargs=kwargs)\n File "/nix/store/dakh34sxz4yfr435c0cwjz0sd6hnd5g3-python2.7-mako-1.1.0/lib/python2.7/site-packages/mako/runtime.py", line 947, in _exec_template\n callable_(context, *args, **kwargs)\n File "rhodecode_templates_email_templates_base_mako", line 63, in render_body\n File "rhodecode_templates_email_templates_exception_tracker_mako", line 43, in render_body\nAttributeError: \'str\' object has no attribute \'get\'\n',
98 101 'exc_type': 'AttributeError'
99 102 }
103
100 104 email_kwargs = {
101 105 'test': {},
106
102 107 'message': {
103 108 'body': 'message body !'
104 109 },
110
105 111 'email_test': {
106 112 'user': user,
107 113 'date': datetime.datetime.now(),
108 114 },
115
109 116 'exception': {
110 117 'email_prefix': '[RHODECODE ERROR]',
111 118 'exc_id': exc_traceback['exc_id'],
@@ -113,6 +120,7 b' Check if we should use full-topic or min'
113 120 'exc_type_name': 'NameError',
114 121 'exc_traceback': exc_traceback,
115 122 },
123
116 124 'password_reset': {
117 125 'password_reset_url': 'http://example.com/reset-rhodecode-password/token',
118 126
@@ -121,6 +129,7 b' Check if we should use full-topic or min'
121 129 'email': 'test@rhodecode.com',
122 130 'first_admin_email': User.get_first_super_admin().email
123 131 },
132
124 133 'password_reset_confirmation': {
125 134 'new_password': 'new-password-example',
126 135 'user': user,
@@ -128,6 +137,7 b' Check if we should use full-topic or min'
128 137 'email': 'test@rhodecode.com',
129 138 'first_admin_email': User.get_first_super_admin().email
130 139 },
140
131 141 'registration': {
132 142 'user': user,
133 143 'date': datetime.datetime.now(),
@@ -161,6 +171,7 b' Check if we should use full-topic or min'
161 171 'mention': True,
162 172
163 173 },
174
164 175 'pull_request_comment+status': {
165 176 'user': user,
166 177
@@ -201,6 +212,7 b' def db():'
201 212 'mention': True,
202 213
203 214 },
215
204 216 'pull_request_comment+file': {
205 217 'user': user,
206 218
@@ -303,6 +315,7 b' This should work better !'
303 315 'renderer_type': 'markdown',
304 316 'mention': True,
305 317 },
318
306 319 'cs_comment+status': {
307 320 'user': user,
308 321 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
@@ -328,6 +341,7 b' This is a multiline comment :)'
328 341 'renderer_type': 'markdown',
329 342 'mention': True,
330 343 },
344
331 345 'cs_comment+file': {
332 346 'user': user,
333 347 'commit': AttributeDict(idx=123, raw_id='a' * 40, message='Commit message'),
@@ -348,12 +362,37 b' This is a multiline comment :)'
348 362 'renderer_type': 'markdown',
349 363 'mention': True,
350 364 },
351
365
352 366 'pull_request': {
353 367 'user': user,
354 368 'pull_request': pr,
355 369 'pull_request_commits': [
356 370 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
371 my-account: moved email closer to profile as it's similar data just moved outside.
372 '''),
373 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
374 users: description edit fixes
375
376 - tests
377 - added metatags info
378 '''),
379 ],
380
381 'pull_request_target_repo': target_repo,
382 'pull_request_target_repo_url': 'http://target-repo/url',
383
384 'pull_request_source_repo': source_repo,
385 'pull_request_source_repo_url': 'http://source-repo/url',
386
387 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
388 'user_role': 'reviewer',
389 },
390
391 'pull_request+reviewer_role': {
392 'user': user,
393 'pull_request': pr,
394 'pull_request_commits': [
395 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
357 396 my-account: moved email closer to profile as it's similar data just moved outside.
358 397 '''),
359 398 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
@@ -371,8 +410,33 b' users: description edit fixes'
371 410 'pull_request_source_repo_url': 'http://source-repo/url',
372 411
373 412 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
413 'user_role': 'reviewer',
414 },
415
416 'pull_request+observer_role': {
417 'user': user,
418 'pull_request': pr,
419 'pull_request_commits': [
420 ('472d1df03bf7206e278fcedc6ac92b46b01c4e21', '''\
421 my-account: moved email closer to profile as it's similar data just moved outside.
422 '''),
423 ('cbfa3061b6de2696c7161ed15ba5c6a0045f90a7', '''\
424 users: description edit fixes
425
426 - tests
427 - added metatags info
428 '''),
429 ],
430
431 'pull_request_target_repo': target_repo,
432 'pull_request_target_repo_url': 'http://target-repo/url',
433
434 'pull_request_source_repo': source_repo,
435 'pull_request_source_repo_url': 'http://source-repo/url',
436
437 'pull_request_url': 'http://code.rhodecode.com/_pull-request/123',
438 'user_role': 'observer'
374 439 }
375
376 440 }
377 441
378 442 template_type = email_id.split('+')[0]
@@ -401,6 +465,7 b' users: description edit fixes'
401 465 c = self.load_default_context()
402 466 c.active = os.path.splitext(t_path)[0]
403 467 c.came_from = ''
468 # NOTE(marcink): extend the email types with variations based on data sets
404 469 c.email_types = {
405 470 'cs_comment+file': {},
406 471 'cs_comment+status': {},
@@ -409,6 +474,9 b' users: description edit fixes'
409 474 'pull_request_comment+status': {},
410 475
411 476 'pull_request_update': {},
477
478 'pull_request+reviewer_role': {},
479 'pull_request+observer_role': {},
412 480 }
413 481 c.email_types.update(EmailNotificationModel.email_types)
414 482
@@ -32,7 +32,7 b' from rhodecode.lib.auth import ('
32 32 HasRepoGroupPermissionAny, AuthUser)
33 33 from rhodecode.lib.codeblocks import filenode_as_lines_tokens
34 34 from rhodecode.lib.index import searcher_from_config
35 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int
35 from rhodecode.lib.utils2 import safe_unicode, str2bool, safe_int, safe_str
36 36 from rhodecode.lib.vcs.nodes import FileNode
37 37 from rhodecode.model.db import (
38 38 func, true, or_, case, cast, in_filter_generator, String, Session,
@@ -331,7 +331,8 b' class HomeView(BaseAppView, DataGridAppV'
331 331 {
332 332 'id': obj.pull_request_id,
333 333 'value': org_query,
334 'value_display': 'pull request: `!{} - {}`'.format(obj.pull_request_id, obj.title[:50]),
334 'value_display': 'pull request: `!{} - {}`'.format(
335 obj.pull_request_id, safe_str(obj.title[:50])),
335 336 'type': 'pull_request',
336 337 'url': h.route_path('pull_requests_global', pull_request_id=obj.pull_request_id)
337 338 }
@@ -734,8 +734,8 b' class MyAccountView(BaseAppView, DataGri'
734 734 comments_model = CommentsModel()
735 735 for pr in pull_requests:
736 736 repo_id = pr.target_repo_id
737 comments = comments_model.get_all_comments(
738 repo_id, pull_request=pr)
737 comments_count = comments_model.get_all_comments(
738 repo_id, pull_request=pr, count_only=True)
739 739 owned = pr.user_id == self._rhodecode_user.user_id
740 740
741 741 data.append({
@@ -760,8 +760,8 b' class MyAccountView(BaseAppView, DataGri'
760 760 'author': _render('pullrequest_author',
761 761 pr.author.full_contact, ),
762 762 'author_raw': pr.author.full_name,
763 'comments': _render('pullrequest_comments', len(comments)),
764 'comments_raw': len(comments),
763 'comments': _render('pullrequest_comments', comments_count),
764 'comments_raw': comments_count,
765 765 'closed': pr.is_closed(),
766 766 'owned': owned
767 767 })
@@ -523,7 +523,9 b' class TestPullrequestsView(object):'
523 523 pull_request = pr_util.create_pull_request()
524 524 pull_request_id = pull_request.pull_request_id
525 525 PullRequestModel().update_reviewers(
526 pull_request_id, [(1, ['reason'], False, []), (2, ['reason2'], False, [])],
526 pull_request_id, [
527 (1, ['reason'], False, 'reviewer', []),
528 (2, ['reason2'], False, 'reviewer', [])],
527 529 pull_request.author)
528 530 author = pull_request.user_id
529 531 repo = pull_request.target_repo.repo_id
@@ -906,12 +908,13 b' class TestPullrequestsView(object):'
906 908
907 909 # Change reviewers and check that a notification was made
908 910 PullRequestModel().update_reviewers(
909 pull_request.pull_request_id, [(1, [], False, [])],
911 pull_request.pull_request_id, [
912 (1, [], False, 'reviewer', [])
913 ],
910 914 pull_request.author)
911 915 assert len(notifications.all()) == 2
912 916
913 def test_create_pull_request_stores_ancestor_commit_id(self, backend,
914 csrf_token):
917 def test_create_pull_request_stores_ancestor_commit_id(self, backend, csrf_token):
915 918 commits = [
916 919 {'message': 'ancestor',
917 920 'added': [FileNode('file_A', content='content_of_ancestor')]},
@@ -18,14 +18,16 b''
18 18 # RhodeCode Enterprise Edition, including its added features, Support services,
19 19 # and proprietary license terms, please see https://rhodecode.com/licenses/
20 20
21 from rhodecode.lib import helpers as h
21 from rhodecode.lib import helpers as h, rc_cache
22 22 from rhodecode.lib.utils2 import safe_int
23 23 from rhodecode.model.pull_request import get_diff_info
24
25 REVIEWER_API_VERSION = 'V3'
24 from rhodecode.model.db import PullRequestReviewers
25 # V3 - Reviewers, with default rules data
26 # v4 - Added observers metadata
27 REVIEWER_API_VERSION = 'V4'
26 28
27 29
28 def reviewer_as_json(user, reasons=None, mandatory=False, rules=None, user_group=None):
30 def reviewer_as_json(user, reasons=None, role=None, mandatory=False, rules=None, user_group=None):
29 31 """
30 32 Returns json struct of a reviewer for frontend
31 33
@@ -33,11 +35,15 b' def reviewer_as_json(user, reasons=None,'
33 35 :param reasons: list of strings of why they are reviewers
34 36 :param mandatory: bool, to set user as mandatory
35 37 """
38 role = role or PullRequestReviewers.ROLE_REVIEWER
39 if role not in PullRequestReviewers.ROLES:
40 raise ValueError('role is not one of %s', PullRequestReviewers.ROLES)
36 41
37 42 return {
38 43 'user_id': user.user_id,
39 44 'reasons': reasons or [],
40 45 'rules': rules or [],
46 'role': role,
41 47 'mandatory': mandatory,
42 48 'user_group': user_group,
43 49 'username': user.username,
@@ -48,21 +54,36 b' def reviewer_as_json(user, reasons=None,'
48 54 }
49 55
50 56
51 def get_default_reviewers_data(
52 current_user, source_repo, source_commit, target_repo, target_commit):
57 def to_reviewers(e):
58 if isinstance(e, (tuple, list)):
59 return map(reviewer_as_json, e)
60 else:
61 return reviewer_as_json(e)
62
63
64 def get_default_reviewers_data(current_user, source_repo, source_ref, target_repo, target_ref,
65 include_diff_info=True):
53 66 """
54 67 Return json for default reviewers of a repository
55 68 """
56 69
57 diff_info = get_diff_info(
58 source_repo, source_commit.raw_id, target_repo, target_commit.raw_id)
70 diff_info = {}
71 if include_diff_info:
72 diff_info = get_diff_info(
73 source_repo, source_ref.commit_id, target_repo, target_ref.commit_id)
59 74
60 75 reasons = ['Default reviewer', 'Repository owner']
61 76 json_reviewers = [reviewer_as_json(
62 user=target_repo.user, reasons=reasons, mandatory=False, rules=None)]
77 user=target_repo.user, reasons=reasons, mandatory=False, rules=None, role=None)]
78
79 compute_key = rc_cache.utils.compute_key_from_params(
80 current_user.user_id, source_repo.repo_id, source_ref.type, source_ref.name,
81 source_ref.commit_id, target_repo.repo_id, target_ref.type, target_ref.name,
82 target_ref.commit_id)
63 83
64 84 return {
65 85 'api_ver': REVIEWER_API_VERSION, # define version for later possible schema upgrade
86 'compute_key': compute_key,
66 87 'diff_info': diff_info,
67 88 'reviewers': json_reviewers,
68 89 'rules': {},
@@ -73,15 +94,18 b' def get_default_reviewers_data('
73 94 def validate_default_reviewers(review_members, reviewer_rules):
74 95 """
75 96 Function to validate submitted reviewers against the saved rules
76
77 97 """
78 98 reviewers = []
79 99 reviewer_by_id = {}
80 100 for r in review_members:
81 101 reviewer_user_id = safe_int(r['user_id'])
82 entry = (reviewer_user_id, r['reasons'], r['mandatory'], r['rules'])
102 entry = (reviewer_user_id, r['reasons'], r['mandatory'], r['role'], r['rules'])
83 103
84 104 reviewer_by_id[reviewer_user_id] = entry
85 105 reviewers.append(entry)
86 106
87 107 return reviewers
108
109
110 def validate_observers(observer_members, reviewer_rules):
111 return {}
@@ -31,7 +31,7 b' from rhodecode.apps._base import RepoApp'
31 31 from rhodecode.apps.file_store import utils as store_utils
32 32 from rhodecode.apps.file_store.exceptions import FileNotAllowedException, FileOverSizeException
33 33
34 from rhodecode.lib import diffs, codeblocks
34 from rhodecode.lib import diffs, codeblocks, channelstream
35 35 from rhodecode.lib.auth import (
36 36 LoginRequired, HasRepoPermissionAnyDecorator, NotAnonymous, CSRFRequired)
37 37 from rhodecode.lib.ext_json import json
@@ -170,7 +170,9 b' class RepoCommitsView(RepoAppView):'
170 170 )
171 171 reviewers_duplicates.add(_user_id)
172 172
173 c.allowed_reviewers = reviewers
173 c.reviewers_count = len(reviewers)
174 c.observers_count = 0
175
174 176 # from associated statuses, check the pull requests, and
175 177 # show comments from them
176 178 for pr in prs:
@@ -193,7 +195,7 b' class RepoCommitsView(RepoAppView):'
193 195
194 196 for review_obj, member, reasons, mandatory, status in review_statuses:
195 197 member_reviewer = h.reviewer_as_json(
196 member, reasons=reasons, mandatory=mandatory,
198 member, reasons=reasons, mandatory=mandatory, role=None,
197 199 user_group=None
198 200 )
199 201
@@ -207,10 +209,7 b' class RepoCommitsView(RepoAppView):'
207 209
208 210 # NOTE(marcink): this uses the same voting logic as in pull-requests
209 211 c.commit_review_status = ChangesetStatusModel().calculate_status(review_statuses)
210 c.commit_broadcast_channel = u'/repo${}$/commit/{}'.format(
211 c.repo_name,
212 commit.raw_id
213 )
212 c.commit_broadcast_channel = channelstream.comment_channel(c.repo_name, commit_obj=commit)
214 213
215 214 diff = None
216 215 # Iterate over ranges (default commit view is always one commit)
@@ -414,6 +413,7 b' class RepoCommitsView(RepoAppView):'
414 413 resolves_comment_id=resolves_comment_id,
415 414 auth_user=self._rhodecode_user
416 415 )
416 is_inline = comment.is_inline
417 417
418 418 # get status if set !
419 419 if status:
@@ -461,6 +461,16 b' class RepoCommitsView(RepoAppView):'
461 461 data.update(comment.get_dict())
462 462 data.update({'rendered_text': rendered_comment})
463 463
464 comment_broadcast_channel = channelstream.comment_channel(
465 self.db_repo_name, commit_obj=commit)
466
467 comment_data = data
468 comment_type = 'inline' if is_inline else 'general'
469 channelstream.comment_channelstream_push(
470 self.request, comment_broadcast_channel, self._rhodecode_user,
471 _('posted a new {} comment').format(comment_type),
472 comment_data=comment_data)
473
464 474 return data
465 475
466 476 @LoginRequired()
@@ -39,14 +39,16 b' from rhodecode.lib.ext_json import json'
39 39 from rhodecode.lib.auth import (
40 40 LoginRequired, HasRepoPermissionAny, HasRepoPermissionAnyDecorator,
41 41 NotAnonymous, CSRFRequired)
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int
43 from rhodecode.lib.vcs.backends.base import EmptyCommit, UpdateFailureReason
42 from rhodecode.lib.utils2 import str2bool, safe_str, safe_unicode, safe_int, aslist
43 from rhodecode.lib.vcs.backends.base import (
44 EmptyCommit, UpdateFailureReason, unicode_to_reference)
44 45 from rhodecode.lib.vcs.exceptions import (
45 46 CommitDoesNotExistError, RepositoryRequirementError, EmptyRepositoryError)
46 47 from rhodecode.model.changeset_status import ChangesetStatusModel
47 48 from rhodecode.model.comment import CommentsModel
48 49 from rhodecode.model.db import (
49 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository)
50 func, or_, PullRequest, ChangesetComment, ChangesetStatus, Repository,
51 PullRequestReviewers)
50 52 from rhodecode.model.forms import PullRequestForm
51 53 from rhodecode.model.meta import Session
52 54 from rhodecode.model.pull_request import PullRequestModel, MergeCheck
@@ -104,13 +106,14 b' class RepoPullRequestsView(RepoAppView, '
104 106 data = []
105 107 comments_model = CommentsModel()
106 108 for pr in pull_requests:
107 comments = comments_model.get_all_comments(
108 self.db_repo.repo_id, pull_request=pr)
109 comments_count = comments_model.get_all_comments(
110 self.db_repo.repo_id, pull_request=pr, count_only=True)
109 111
110 112 data.append({
111 113 'name': _render('pullrequest_name',
112 114 pr.pull_request_id, pr.pull_request_state,
113 pr.work_in_progress, pr.target_repo.repo_name),
115 pr.work_in_progress, pr.target_repo.repo_name,
116 short=True),
114 117 'name_raw': pr.pull_request_id,
115 118 'status': _render('pullrequest_status',
116 119 pr.calculated_review_status()),
@@ -126,8 +129,8 b' class RepoPullRequestsView(RepoAppView, '
126 129 'author': _render('pullrequest_author',
127 130 pr.author.full_contact, ),
128 131 'author_raw': pr.author.full_name,
129 'comments': _render('pullrequest_comments', len(comments)),
130 'comments_raw': len(comments),
132 'comments': _render('pullrequest_comments', comments_count),
133 'comments_raw': comments_count,
131 134 'closed': pr.is_closed(),
132 135 })
133 136
@@ -310,8 +313,7 b' class RepoPullRequestsView(RepoAppView, '
310 313 pull_request_id = pull_request.pull_request_id
311 314
312 315 c.state_progressing = pull_request.is_state_changing()
313 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
314 pull_request.target_repo.repo_name, pull_request.pull_request_id)
316 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
315 317
316 318 _new_state = {
317 319 'created': PullRequest.STATE_CREATED,
@@ -454,15 +456,18 b' class RepoPullRequestsView(RepoAppView, '
454 456 'rhodecode:templates/pullrequests/pullrequest_merge_checks.mako'
455 457 return self._get_template_context(c)
456 458
457 c.allowed_reviewers = [obj.user_id for obj in pull_request.reviewers if obj.user]
459 c.reviewers_count = pull_request.reviewers_count
460 c.observers_count = pull_request.observers_count
458 461
459 462 # reviewers and statuses
460 463 c.pull_request_default_reviewers_data_json = json.dumps(pull_request.reviewer_data)
461 464 c.pull_request_set_reviewers_data_json = collections.OrderedDict({'reviewers': []})
465 c.pull_request_set_observers_data_json = collections.OrderedDict({'observers': []})
462 466
463 467 for review_obj, member, reasons, mandatory, status in pull_request_at_ver.reviewers_statuses():
464 468 member_reviewer = h.reviewer_as_json(
465 469 member, reasons=reasons, mandatory=mandatory,
470 role=review_obj.role,
466 471 user_group=review_obj.rule_user_group_data()
467 472 )
468 473
@@ -474,6 +479,17 b' class RepoPullRequestsView(RepoAppView, '
474 479
475 480 c.pull_request_set_reviewers_data_json = json.dumps(c.pull_request_set_reviewers_data_json)
476 481
482 for observer_obj, member in pull_request_at_ver.observers():
483 member_observer = h.reviewer_as_json(
484 member, reasons=[], mandatory=False,
485 role=observer_obj.role,
486 user_group=observer_obj.rule_user_group_data()
487 )
488 member_observer['allowed_to_update'] = c.allowed_to_update
489 c.pull_request_set_observers_data_json['observers'].append(member_observer)
490
491 c.pull_request_set_observers_data_json = json.dumps(c.pull_request_set_observers_data_json)
492
477 493 general_comments, inline_comments = \
478 494 self.register_comments_vars(c, pull_request_latest, versions)
479 495
@@ -745,7 +761,9 b' class RepoPullRequestsView(RepoAppView, '
745 761
746 762 # current user review statuses for each version
747 763 c.review_versions = {}
748 if self._rhodecode_user.user_id in c.allowed_reviewers:
764 is_reviewer = PullRequestModel().is_user_reviewer(
765 pull_request, self._rhodecode_user)
766 if is_reviewer:
749 767 for co in general_comments:
750 768 if co.author.user_id == self._rhodecode_user.user_id:
751 769 status = co.status_change
@@ -961,13 +979,16 b' class RepoPullRequestsView(RepoAppView, '
961 979 }
962 980 return data
963 981
982 def _get_existing_ids(self, post_data):
983 return filter(lambda e: e, map(safe_int, aslist(post_data.get('comments'), ',')))
984
964 985 @LoginRequired()
965 986 @NotAnonymous()
966 987 @HasRepoPermissionAnyDecorator(
967 988 'repository.read', 'repository.write', 'repository.admin')
968 989 @view_config(
969 990 route_name='pullrequest_comments', request_method='POST',
970 renderer='string', xhr=True)
991 renderer='string_html', xhr=True)
971 992 def pullrequest_comments(self):
972 993 self.load_default_context()
973 994
@@ -997,8 +1018,7 b' class RepoPullRequestsView(RepoAppView, '
997 1018 self.register_comments_vars(c, pull_request_latest, versions)
998 1019 all_comments = c.inline_comments_flat + c.comments
999 1020
1000 existing_ids = filter(
1001 lambda e: e, map(safe_int, self.request.POST.getall('comments[]')))
1021 existing_ids = self._get_existing_ids(self.request.POST)
1002 1022 return _render('comments_table', all_comments, len(all_comments),
1003 1023 existing_ids=existing_ids)
1004 1024
@@ -1008,7 +1028,7 b' class RepoPullRequestsView(RepoAppView, '
1008 1028 'repository.read', 'repository.write', 'repository.admin')
1009 1029 @view_config(
1010 1030 route_name='pullrequest_todos', request_method='POST',
1011 renderer='string', xhr=True)
1031 renderer='string_html', xhr=True)
1012 1032 def pullrequest_todos(self):
1013 1033 self.load_default_context()
1014 1034
@@ -1040,8 +1060,7 b' class RepoPullRequestsView(RepoAppView, '
1040 1060 .get_pull_request_resolved_todos(pull_request)
1041 1061
1042 1062 all_comments = c.unresolved_comments + c.resolved_comments
1043 existing_ids = filter(
1044 lambda e: e, map(safe_int, self.request.POST.getall('comments[]')))
1063 existing_ids = self._get_existing_ids(self.request.POST)
1045 1064 return _render('comments_table', all_comments, len(c.unresolved_comments),
1046 1065 todo_comments=True, existing_ids=existing_ids)
1047 1066
@@ -1128,30 +1147,35 b' class RepoPullRequestsView(RepoAppView, '
1128 1147 source_scm = source_db_repo.scm_instance()
1129 1148 target_scm = target_db_repo.scm_instance()
1130 1149
1131 source_commit = source_scm.get_commit(source_ref.split(':')[-1])
1132 target_commit = target_scm.get_commit(target_ref.split(':')[-1])
1150 source_ref_obj = unicode_to_reference(source_ref)
1151 target_ref_obj = unicode_to_reference(target_ref)
1152
1153 source_commit = source_scm.get_commit(source_ref_obj.commit_id)
1154 target_commit = target_scm.get_commit(target_ref_obj.commit_id)
1133 1155
1134 1156 ancestor = source_scm.get_common_ancestor(
1135 1157 source_commit.raw_id, target_commit.raw_id, target_scm)
1136 1158
1137 1159 # recalculate target ref based on ancestor
1138 target_ref_type, target_ref_name, __ = _form['target_ref'].split(':')
1139 target_ref = ':'.join((target_ref_type, target_ref_name, ancestor))
1160 target_ref = ':'.join((target_ref_obj.type, target_ref_obj.name, ancestor))
1140 1161
1141 get_default_reviewers_data, validate_default_reviewers = \
1162 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1142 1163 PullRequestModel().get_reviewer_functions()
1143 1164
1144 1165 # recalculate reviewers logic, to make sure we can validate this
1145 1166 reviewer_rules = get_default_reviewers_data(
1146 self._rhodecode_db_user, source_db_repo,
1147 source_commit, target_db_repo, target_commit)
1167 self._rhodecode_db_user,
1168 source_db_repo,
1169 source_ref_obj,
1170 target_db_repo,
1171 target_ref_obj,
1172 include_diff_info=False)
1148 1173
1149 given_reviewers = _form['review_members']
1150 reviewers = validate_default_reviewers(
1151 given_reviewers, reviewer_rules)
1174 reviewers = validate_default_reviewers(_form['review_members'], reviewer_rules)
1175 observers = validate_observers(_form['observer_members'], reviewer_rules)
1152 1176
1153 1177 pullrequest_title = _form['pullrequest_title']
1154 title_source_ref = source_ref.split(':', 2)[1]
1178 title_source_ref = source_ref_obj.name
1155 1179 if not pullrequest_title:
1156 1180 pullrequest_title = PullRequestModel().generate_pullrequest_title(
1157 1181 source=source_repo,
@@ -1172,6 +1196,7 b' class RepoPullRequestsView(RepoAppView, '
1172 1196 revisions=commit_ids,
1173 1197 common_ancestor_id=common_ancestor_id,
1174 1198 reviewers=reviewers,
1199 observers=observers,
1175 1200 title=pullrequest_title,
1176 1201 description=description,
1177 1202 description_renderer=description_renderer,
@@ -1221,20 +1246,28 b' class RepoPullRequestsView(RepoAppView, '
1221 1246 'redirect_url': redirect_url}
1222 1247
1223 1248 is_state_changing = pull_request.is_state_changing()
1224 c.pr_broadcast_channel = '/repo${}$/pr/{}'.format(
1225 pull_request.target_repo.repo_name, pull_request.pull_request_id)
1249 c.pr_broadcast_channel = channelstream.pr_channel(pull_request)
1226 1250
1227 1251 # only owner or admin can update it
1228 1252 allowed_to_update = PullRequestModel().check_user_update(
1229 1253 pull_request, self._rhodecode_user)
1254
1230 1255 if allowed_to_update:
1231 1256 controls = peppercorn.parse(self.request.POST.items())
1232 1257 force_refresh = str2bool(self.request.POST.get('force_refresh'))
1233 1258
1234 1259 if 'review_members' in controls:
1235 1260 self._update_reviewers(
1261 c,
1236 1262 pull_request, controls['review_members'],
1237 pull_request.reviewer_data)
1263 pull_request.reviewer_data,
1264 PullRequestReviewers.ROLE_REVIEWER)
1265 elif 'observer_members' in controls:
1266 self._update_reviewers(
1267 c,
1268 pull_request, controls['observer_members'],
1269 pull_request.reviewer_data,
1270 PullRequestReviewers.ROLE_OBSERVER)
1238 1271 elif str2bool(self.request.POST.get('update_commits', 'false')):
1239 1272 if is_state_changing:
1240 1273 log.debug('commits update: forbidden because pull request is in state %s',
@@ -1255,6 +1288,7 b' class RepoPullRequestsView(RepoAppView, '
1255 1288 elif str2bool(self.request.POST.get('edit_pull_request', 'false')):
1256 1289 self._edit_pull_request(pull_request)
1257 1290 else:
1291 log.error('Unhandled update data.')
1258 1292 raise HTTPBadRequest()
1259 1293
1260 1294 return {'response': True,
@@ -1262,6 +1296,9 b' class RepoPullRequestsView(RepoAppView, '
1262 1296 raise HTTPForbidden()
1263 1297
1264 1298 def _edit_pull_request(self, pull_request):
1299 """
1300 Edit title and description
1301 """
1265 1302 _ = self.request.translate
1266 1303
1267 1304 try:
@@ -1302,27 +1339,15 b' class RepoPullRequestsView(RepoAppView, '
1302 1339
1303 1340 msg = _(u'Pull request updated to "{source_commit_id}" with '
1304 1341 u'{count_added} added, {count_removed} removed commits. '
1305 u'Source of changes: {change_source}')
1342 u'Source of changes: {change_source}.')
1306 1343 msg = msg.format(
1307 1344 source_commit_id=pull_request.source_ref_parts.commit_id,
1308 1345 count_added=len(resp.changes.added),
1309 1346 count_removed=len(resp.changes.removed),
1310 1347 change_source=changed)
1311 1348 h.flash(msg, category='success')
1312
1313 message = msg + (
1314 ' - <a onclick="window.location.reload()">'
1315 '<strong>{}</strong></a>'.format(_('Reload page')))
1316
1317 message_obj = {
1318 'message': message,
1319 'level': 'success',
1320 'topic': '/notifications'
1321 }
1322
1323 channelstream.post_message(
1324 c.pr_broadcast_channel, message_obj, self._rhodecode_user.username,
1325 registry=self.request.registry)
1349 channelstream.pr_update_channelstream_push(
1350 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1326 1351 else:
1327 1352 msg = PullRequestModel.UPDATE_STATUS_MESSAGES[resp.reason]
1328 1353 warning_reasons = [
@@ -1332,6 +1357,55 b' class RepoPullRequestsView(RepoAppView, '
1332 1357 category = 'warning' if resp.reason in warning_reasons else 'error'
1333 1358 h.flash(msg, category=category)
1334 1359
1360 def _update_reviewers(self, c, pull_request, review_members, reviewer_rules, role):
1361 _ = self.request.translate
1362
1363 get_default_reviewers_data, validate_default_reviewers, validate_observers = \
1364 PullRequestModel().get_reviewer_functions()
1365
1366 if role == PullRequestReviewers.ROLE_REVIEWER:
1367 try:
1368 reviewers = validate_default_reviewers(review_members, reviewer_rules)
1369 except ValueError as e:
1370 log.error('Reviewers Validation: {}'.format(e))
1371 h.flash(e, category='error')
1372 return
1373
1374 old_calculated_status = pull_request.calculated_review_status()
1375 PullRequestModel().update_reviewers(
1376 pull_request, reviewers, self._rhodecode_db_user)
1377
1378 Session().commit()
1379
1380 msg = _('Pull request reviewers updated.')
1381 h.flash(msg, category='success')
1382 channelstream.pr_update_channelstream_push(
1383 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1384
1385 # trigger status changed if change in reviewers changes the status
1386 calculated_status = pull_request.calculated_review_status()
1387 if old_calculated_status != calculated_status:
1388 PullRequestModel().trigger_pull_request_hook(
1389 pull_request, self._rhodecode_user, 'review_status_change',
1390 data={'status': calculated_status})
1391
1392 elif role == PullRequestReviewers.ROLE_OBSERVER:
1393 try:
1394 observers = validate_observers(review_members, reviewer_rules)
1395 except ValueError as e:
1396 log.error('Observers Validation: {}'.format(e))
1397 h.flash(e, category='error')
1398 return
1399
1400 PullRequestModel().update_observers(
1401 pull_request, observers, self._rhodecode_db_user)
1402
1403 Session().commit()
1404 msg = _('Pull request observers updated.')
1405 h.flash(msg, category='success')
1406 channelstream.pr_update_channelstream_push(
1407 self.request, c.pr_broadcast_channel, self._rhodecode_user, msg)
1408
1335 1409 @LoginRequired()
1336 1410 @NotAnonymous()
1337 1411 @HasRepoPermissionAnyDecorator(
@@ -1408,32 +1482,6 b' class RepoPullRequestsView(RepoAppView, '
1408 1482 msg = merge_resp.merge_status_message
1409 1483 h.flash(msg, category='error')
1410 1484
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 1485 @LoginRequired()
1438 1486 @NotAnonymous()
1439 1487 @HasRepoPermissionAnyDecorator(
@@ -1488,8 +1536,7 b' class RepoPullRequestsView(RepoAppView, '
1488 1536 allowed_to_comment = PullRequestModel().check_user_comment(
1489 1537 pull_request, self._rhodecode_user)
1490 1538 if not allowed_to_comment:
1491 log.debug(
1492 'comment: forbidden because pull request is from forbidden repo')
1539 log.debug('comment: forbidden because pull request is from forbidden repo')
1493 1540 raise HTTPForbidden()
1494 1541
1495 1542 c = self.load_default_context()
@@ -1518,6 +1565,7 b' class RepoPullRequestsView(RepoAppView, '
1518 1565 pull_request, self._rhodecode_user, self.db_repo, message=text,
1519 1566 auth_user=self._rhodecode_user)
1520 1567 Session().flush()
1568 is_inline = comment.is_inline
1521 1569
1522 1570 PullRequestModel().trigger_pull_request_hook(
1523 1571 pull_request, self._rhodecode_user, 'comment',
@@ -1551,6 +1599,7 b' class RepoPullRequestsView(RepoAppView, '
1551 1599 resolves_comment_id=resolves_comment_id,
1552 1600 auth_user=self._rhodecode_user
1553 1601 )
1602 is_inline = comment.is_inline
1554 1603
1555 1604 if allowed_to_change_status:
1556 1605 # calculate old status before we change it
@@ -1599,6 +1648,16 b' class RepoPullRequestsView(RepoAppView, '
1599 1648 data.update(comment.get_dict())
1600 1649 data.update({'rendered_text': rendered_comment})
1601 1650
1651 comment_broadcast_channel = channelstream.comment_channel(
1652 self.db_repo_name, pull_request_obj=pull_request)
1653
1654 comment_data = data
1655 comment_type = 'inline' if is_inline else 'general'
1656 channelstream.comment_channelstream_push(
1657 self.request, comment_broadcast_channel, self._rhodecode_user,
1658 _('posted a new {} comment').format(comment_type),
1659 comment_data=comment_data)
1660
1602 1661 return data
1603 1662
1604 1663 @LoginRequired()
@@ -25,6 +25,7 b' from pyramid.view import view_config'
25 25 from rhodecode.apps._base import RepoAppView
26 26 from rhodecode.apps.repository.utils import get_default_reviewers_data
27 27 from rhodecode.lib.auth import LoginRequired, HasRepoPermissionAnyDecorator
28 from rhodecode.lib.vcs.backends.base import Reference
28 29 from rhodecode.model.db import Repository
29 30
30 31 log = logging.getLogger(__name__)
@@ -61,13 +62,28 b' class RepoReviewRulesView(RepoAppView):'
61 62 target_repo_name = request.GET.get('target_repo', source_repo_name)
62 63 target_repo = Repository.get_by_repo_name(target_repo_name)
63 64
64 source_ref = request.GET['source_ref']
65 target_ref = request.GET['target_ref']
66 source_commit = source_repo.get_commit(source_ref)
67 target_commit = target_repo.get_commit(target_ref)
65 current_user = request.user.get_instance()
66
67 source_commit_id = request.GET['source_ref']
68 source_type = request.GET['source_ref_type']
69 source_name = request.GET['source_ref_name']
70
71 target_commit_id = request.GET['target_ref']
72 target_type = request.GET['target_ref_type']
73 target_name = request.GET['target_ref_name']
68 74
69 current_user = request.user.get_instance()
70 review_data = get_default_reviewers_data(
71 current_user, source_repo, source_commit, target_repo, target_commit)
75 try:
76 review_data = get_default_reviewers_data(
77 current_user,
78 source_repo,
79 Reference(source_type, source_name, source_commit_id),
80 target_repo,
81 Reference(target_type, target_name, target_commit_id)
82 )
83 except ValueError:
84 # No common ancestor
85 msg = "No Common ancestor found between target and source reference"
86 log.exception(msg)
87 return {'diff_info': {'error': msg}}
72 88
73 89 return review_data
@@ -341,6 +341,10 b' def includeme(config):'
341 341 name='json_ext',
342 342 factory='rhodecode.lib.ext_json_renderer.pyramid_ext_json')
343 343
344 config.add_renderer(
345 name='string_html',
346 factory='rhodecode.lib.string_renderer.html')
347
344 348 # include RhodeCode plugins
345 349 includes = aslist(settings.get('rhodecode.includes', []))
346 350 for inc in includes:
@@ -408,6 +412,7 b' def sanitize_settings_and_apply_defaults'
408 412 """
409 413
410 414 settings.setdefault('rhodecode.edition', 'Community Edition')
415 settings.setdefault('rhodecode.edition_id', 'CE')
411 416
412 417 if 'mako.default_filters' not in settings:
413 418 # set custom default filters if we don't have it defined
@@ -113,7 +113,7 b' def _commits_as_dict(event, commit_ids, '
113 113 cs_data['permalink_url'] = RepoModel().get_commit_url(
114 114 repo, cs_data['raw_id'], request=event.request,
115 115 permalink=True)
116 urlified_message, issues_data = process_patterns(
116 urlified_message, issues_data, errors = process_patterns(
117 117 cs_data['message'], repo.repo_name)
118 118 cs_data['issues'] = issues_data
119 119 cs_data['message_html'] = urlify_commit_message(
1 NO CONTENT: modified file
The requested commit or file is too big and content was truncated. Show full diff
@@ -88,6 +88,9 b' ACTIONS_V1 = {'
88 88 'repo.pull_request.reviewer.add': '',
89 89 'repo.pull_request.reviewer.delete': '',
90 90
91 'repo.pull_request.observer.add': '',
92 'repo.pull_request.observer.delete': '',
93
91 94 'repo.commit.strip': {'commit_id': ''},
92 95 'repo.commit.comment.create': {'data': {}},
93 96 'repo.commit.comment.delete': {'data': {}},
@@ -293,6 +293,7 b' def attach_context_attributes(context, r'
293 293 context.rc_config = rc_config
294 294 context.rhodecode_version = rhodecode.__version__
295 295 context.rhodecode_edition = config.get('rhodecode.edition')
296 context.rhodecode_edition_id = config.get('rhodecode.edition_id')
296 297 # unique secret + version does not leak the version but keep consistency
297 298 context.rhodecode_version_hash = calculate_version_hash(config)
298 299
@@ -225,14 +225,26 b' def write_history(config, message):'
225 225
226 226 def get_connection_validators(registry):
227 227 validators = []
228 for k, config in registry.rhodecode_plugins.iteritems():
228 for k, config in registry.rhodecode_plugins.items():
229 229 validator = config.get('channelstream', {}).get('connect_validator')
230 230 if validator:
231 231 validators.append(validator)
232 232 return validators
233 233
234 234
235 def get_channelstream_config(registry=None):
236 if not registry:
237 registry = get_current_registry()
238
239 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
240 channelstream_config = rhodecode_plugins.get('channelstream', {})
241 return channelstream_config
242
243
235 244 def post_message(channel, message, username, registry=None):
245 channelstream_config = get_channelstream_config(registry)
246 if not channelstream_config.get('enabled'):
247 return
236 248
237 249 message_obj = message
238 250 if isinstance(message, basestring):
@@ -242,26 +254,118 b' def post_message(channel, message, usern'
242 254 'topic': '/notifications'
243 255 }
244 256
245 if not registry:
246 registry = get_current_registry()
257 log.debug('Channelstream: sending notification to channel %s', channel)
258 payload = {
259 'type': 'message',
260 'timestamp': datetime.datetime.utcnow(),
261 'user': 'system',
262 'exclude_users': [username],
263 'channel': channel,
264 'message': message_obj
265 }
266
267 try:
268 return channelstream_request(
269 channelstream_config, [payload], '/message',
270 raise_exc=False)
271 except ChannelstreamException:
272 log.exception('Failed to send channelstream data')
273 raise
274
275
276 def _reload_link(label):
277 return (
278 '<a onclick="window.location.reload()">'
279 '<strong>{}</strong>'
280 '</a>'.format(label)
281 )
282
283
284 def pr_channel(pull_request):
285 repo_name = pull_request.target_repo.repo_name
286 pull_request_id = pull_request.pull_request_id
287 channel = '/repo${}$/pr/{}'.format(repo_name, pull_request_id)
288 log.debug('Getting pull-request channelstream broadcast channel: %s', channel)
289 return channel
290
291
292 def comment_channel(repo_name, commit_obj=None, pull_request_obj=None):
293 channel = None
294 if commit_obj:
295 channel = u'/repo${}$/commit/{}'.format(
296 repo_name, commit_obj.raw_id
297 )
298 elif pull_request_obj:
299 channel = u'/repo${}$/pr/{}'.format(
300 repo_name, pull_request_obj.pull_request_id
301 )
302 log.debug('Getting comment channelstream broadcast channel: %s', channel)
303
304 return channel
305
306
307 def pr_update_channelstream_push(request, pr_broadcast_channel, user, msg, **kwargs):
308 """
309 Channel push on pull request update
310 """
311 if not pr_broadcast_channel:
312 return
247 313
248 log.debug('Channelstream: sending notification to channel %s', channel)
249 rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {})
250 channelstream_config = rhodecode_plugins.get('channelstream', {})
251 if channelstream_config.get('enabled'):
252 payload = {
253 'type': 'message',
254 'timestamp': datetime.datetime.utcnow(),
255 'user': 'system',
256 'exclude_users': [username],
257 'channel': channel,
258 'message': message_obj
259 }
314 _ = request.translate
315
316 message = '{} {}'.format(
317 msg,
318 _reload_link(_(' Reload page to load changes')))
319
320 message_obj = {
321 'message': message,
322 'level': 'success',
323 'topic': '/notifications'
324 }
325
326 post_message(
327 pr_broadcast_channel, message_obj, user.username,
328 registry=request.registry)
329
330
331 def comment_channelstream_push(request, comment_broadcast_channel, user, msg, **kwargs):
332 """
333 Channelstream push on comment action, on commit, or pull-request
334 """
335 if not comment_broadcast_channel:
336 return
337
338 _ = request.translate
260 339
261 try:
262 return channelstream_request(
263 channelstream_config, [payload], '/message',
264 raise_exc=False)
265 except ChannelstreamException:
266 log.exception('Failed to send channelstream data')
267 raise
340 comment_data = kwargs.pop('comment_data', {})
341 user_data = kwargs.pop('user_data', {})
342 comment_id = comment_data.get('comment_id')
343
344 message = '<strong>{}</strong> {} #{}, {}'.format(
345 user.username,
346 msg,
347 comment_id,
348 _reload_link(_('Reload page to see new comments')),
349 )
350
351 message_obj = {
352 'message': message,
353 'level': 'success',
354 'topic': '/notifications'
355 }
356
357 post_message(
358 comment_broadcast_channel, message_obj, user.username,
359 registry=request.registry)
360
361 message_obj = {
362 'message': None,
363 'user': user.username,
364 'comment_id': comment_id,
365 'comment_data': comment_data,
366 'user_data': user_data,
367 'topic': '/comment'
368 }
369 post_message(
370 comment_broadcast_channel, message_obj, user.username,
371 registry=request.registry)
@@ -131,7 +131,7 b' def send_exc_email(request, exc_id, exc_'
131 131
132 132 # NOTE(marcink): needed for email template rendering
133 133 user_id = None
134 if request:
134 if hasattr(request, 'user'):
135 135 user_id = request.user.user_id
136 136 attach_context_attributes(TemplateArgs(), request, user_id=user_id, is_api=True)
137 137
@@ -38,6 +38,7 b' import re'
38 38 import time
39 39 import string
40 40 import hashlib
41 import regex
41 42 from collections import OrderedDict
42 43
43 44 import pygments
@@ -1103,6 +1104,10 b' def bool2icon(value, show_at_false=True)'
1103 1104 return HTML.tag('i', class_="icon-false", title='False')
1104 1105 return HTML.tag('i')
1105 1106
1107
1108 def b64(inp):
1109 return base64.b64encode(inp)
1110
1106 1111 #==============================================================================
1107 1112 # PERMS
1108 1113 #==============================================================================
@@ -1653,7 +1658,7 b' def get_active_pattern_entries(repo_name'
1653 1658 return active_entries
1654 1659
1655 1660
1656 pr_pattern_re = re.compile(r'(?:(?:^!)|(?: !))(\d+)')
1661 pr_pattern_re = regex.compile(r'(?:(?:^!)|(?: !))(\d+)')
1657 1662
1658 1663 allowed_link_formats = [
1659 1664 'html', 'rst', 'markdown', 'html+hovercard', 'rst+hovercard', 'markdown+hovercard']
@@ -1670,6 +1675,7 b' def process_patterns(text_string, repo_n'
1670 1675 active_entries = get_active_pattern_entries(repo_name)
1671 1676
1672 1677 issues_data = []
1678 errors = []
1673 1679 new_text = text_string
1674 1680
1675 1681 log.debug('Got %s entries to process', len(active_entries))
@@ -1687,9 +1693,11 b' def process_patterns(text_string, repo_n'
1687 1693 pattern = entry['pat_compiled']
1688 1694 else:
1689 1695 try:
1690 pattern = re.compile(r'%s' % entry['pat'])
1691 except re.error:
1692 log.exception('issue tracker pattern: `%s` failed to compile', entry['pat'])
1696 pattern = regex.compile(r'%s' % entry['pat'])
1697 except regex.error as e:
1698 regex_err = ValueError('{}:{}'.format(entry['pat'], e))
1699 log.exception('issue tracker pattern: `%s` failed to compile', regex_err)
1700 errors.append(regex_err)
1693 1701 continue
1694 1702
1695 1703 data_func = partial(
@@ -1721,11 +1729,11 b' def process_patterns(text_string, repo_n'
1721 1729 new_text = pr_pattern_re.sub(pr_url_func, new_text)
1722 1730 log.debug('processed !pr pattern')
1723 1731
1724 return new_text, issues_data
1732 return new_text, issues_data, errors
1725 1733
1726 1734
1727 1735 def urlify_commit_message(commit_text, repository=None, active_pattern_entries=None,
1728 issues_container=None):
1736 issues_container=None, error_container=None):
1729 1737 """
1730 1738 Parses given text message and makes proper links.
1731 1739 issues are linked to given issue-server, and rest is a commit link
@@ -1745,12 +1753,15 b' def urlify_commit_message(commit_text, r'
1745 1753 new_text = urlify_commits(new_text, repository)
1746 1754
1747 1755 # process issue tracker patterns
1748 new_text, issues = process_patterns(new_text, repository or '',
1749 active_entries=active_pattern_entries)
1756 new_text, issues, errors = process_patterns(
1757 new_text, repository or '', active_entries=active_pattern_entries)
1750 1758
1751 1759 if issues_container is not None:
1752 1760 issues_container.extend(issues)
1753 1761
1762 if error_container is not None:
1763 error_container.extend(errors)
1764
1754 1765 return literal(new_text)
1755 1766
1756 1767
@@ -1805,7 +1816,7 b" def render(source, renderer='rst', menti"
1805 1816 elif renderer == 'rst':
1806 1817 if repo_name:
1807 1818 # process patterns on comments if we pass in repo name
1808 source, issues = process_patterns(
1819 source, issues, errors = process_patterns(
1809 1820 source, repo_name, link_format='rst',
1810 1821 active_entries=active_pattern_entries)
1811 1822 if issues_container is not None:
@@ -1819,7 +1830,7 b" def render(source, renderer='rst', menti"
1819 1830 elif renderer == 'markdown':
1820 1831 if repo_name:
1821 1832 # process patterns on comments if we pass in repo name
1822 source, issues = process_patterns(
1833 source, issues, errors = process_patterns(
1823 1834 source, repo_name, link_format='markdown',
1824 1835 active_entries=active_pattern_entries)
1825 1836 if issues_container is not None:
@@ -57,7 +57,43 b' FILEMODE_DEFAULT = 0o100644'
57 57 FILEMODE_EXECUTABLE = 0o100755
58 58 EMPTY_COMMIT_ID = '0' * 40
59 59
60 Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
60 _Reference = collections.namedtuple('Reference', ('type', 'name', 'commit_id'))
61
62
63 class Reference(_Reference):
64
65 @property
66 def branch(self):
67 if self.type == 'branch':
68 return self.name
69
70 @property
71 def bookmark(self):
72 if self.type == 'book':
73 return self.name
74
75
76 def unicode_to_reference(raw):
77 """
78 Convert a unicode (or string) to a reference object.
79 If unicode evaluates to False it returns None.
80 """
81 if raw:
82 refs = raw.split(':')
83 return Reference(*refs)
84 else:
85 return None
86
87
88 def reference_to_unicode(ref):
89 """
90 Convert a reference object to unicode.
91 If reference is None it returns None.
92 """
93 if ref:
94 return u':'.join(ref)
95 else:
96 return None
61 97
62 98
63 99 class MergeFailureReason(object):
@@ -25,7 +25,7 b' import collections'
25 25
26 26 from rhodecode.model import BaseModel
27 27 from rhodecode.model.db import (
28 ChangesetStatus, ChangesetComment, PullRequest, Session)
28 ChangesetStatus, ChangesetComment, PullRequest, PullRequestReviewers, Session)
29 29 from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError
30 30 from rhodecode.lib.markup_renderer import (
31 31 DEFAULT_COMMENTS_RENDERER, RstTemplateRenderer)
@@ -383,15 +383,14 b' class ChangesetStatusModel(BaseModel):'
383 383 pull_request.source_repo,
384 384 pull_request=pull_request,
385 385 with_revisions=True)
386 reviewers = pull_request.get_pull_request_reviewers(
387 role=PullRequestReviewers.ROLE_REVIEWER)
388 return self.aggregate_votes_by_user(_commit_statuses, reviewers)
386 389
387 return self.aggregate_votes_by_user(_commit_statuses, pull_request.reviewers)
388
389 def calculated_review_status(self, pull_request, reviewers_statuses=None):
390 def calculated_review_status(self, pull_request):
390 391 """
391 392 calculate pull request status based on reviewers, it should be a list
392 393 of two element lists.
393
394 :param reviewers_statuses:
395 394 """
396 reviewers = reviewers_statuses or self.reviewers_statuses(pull_request)
395 reviewers = self.reviewers_statuses(pull_request)
397 396 return self.calculate_status(reviewers)
@@ -399,7 +399,7 b' class CommentsModel(BaseModel):'
399 399 recipients += [pull_request_obj.author]
400 400
401 401 # add the reviewers to notification
402 recipients += [x.user for x in pull_request_obj.reviewers]
402 recipients += [x.user for x in pull_request_obj.get_pull_request_reviewers()]
403 403
404 404 pr_target_repo = pull_request_obj.target_repo
405 405 pr_source_repo = pull_request_obj.source_repo
@@ -436,9 +436,8 b' class CommentsModel(BaseModel):'
436 436 'thread_ids': [pr_url, pr_comment_url],
437 437 })
438 438
439 recipients += [self._get_user(u) for u in (extra_recipients or [])]
440
441 439 if send_email:
440 recipients += [self._get_user(u) for u in (extra_recipients or [])]
442 441 # pre-generate the subject for notification itself
443 442 (subject, _e, body_plaintext) = EmailNotificationModel().render_email(
444 443 notification_type, **kwargs)
@@ -463,55 +462,11 b' class CommentsModel(BaseModel):'
463 462 else:
464 463 action = 'repo.commit.comment.create'
465 464
466 comment_id = comment.comment_id
467 465 comment_data = comment.get_api_data()
468 466
469 467 self._log_audit_action(
470 468 action, {'data': comment_data}, auth_user, comment)
471 469
472 channel = None
473 if commit_obj:
474 repo_name = repo.repo_name
475 channel = u'/repo${}$/commit/{}'.format(
476 repo_name,
477 commit_obj.raw_id
478 )
479 elif pull_request_obj:
480 repo_name = pr_target_repo.repo_name
481 channel = u'/repo${}$/pr/{}'.format(
482 repo_name,
483 pull_request_obj.pull_request_id
484 )
485
486 if channel:
487 username = user.username
488 message = '<strong>{}</strong> {} #{}, {}'
489 message = message.format(
490 username,
491 _('posted a new comment'),
492 comment_id,
493 _('Refresh the page to see new comments.'))
494
495 message_obj = {
496 'message': message,
497 'level': 'success',
498 'topic': '/notifications'
499 }
500
501 channelstream.post_message(
502 channel, message_obj, user.username,
503 registry=get_current_registry())
504
505 message_obj = {
506 'message': None,
507 'user': username,
508 'comment_id': comment_id,
509 'topic': '/comment'
510 }
511 channelstream.post_message(
512 channel, message_obj, user.username,
513 registry=get_current_registry())
514
515 470 return comment
516 471
517 472 def edit(self, comment_id, text, auth_user, version):
@@ -586,17 +541,20 b' class CommentsModel(BaseModel):'
586 541
587 542 return comment
588 543
589 def get_all_comments(self, repo_id, revision=None, pull_request=None):
544 def get_all_comments(self, repo_id, revision=None, pull_request=None, count_only=False):
590 545 q = ChangesetComment.query()\
591 546 .filter(ChangesetComment.repo_id == repo_id)
592 547 if revision:
593 548 q = q.filter(ChangesetComment.revision == revision)
594 549 elif pull_request:
595 550 pull_request = self.__get_pull_request(pull_request)
596 q = q.filter(ChangesetComment.pull_request == pull_request)
551 q = q.filter(ChangesetComment.pull_request_id == pull_request.pull_request_id)
597 552 else:
598 553 raise Exception('Please specify commit or pull_request')
599 554 q = q.order_by(ChangesetComment.created_on)
555 if count_only:
556 return q.count()
557
600 558 return q.all()
601 559
602 560 def get_url(self, comment, request=None, permalink=False, anchor=None):
@@ -56,7 +56,8 b' from webhelpers2.text import remove_form'
56 56
57 57 from rhodecode.translation import _
58 58 from rhodecode.lib.vcs import get_vcs_instance, VCSError
59 from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference
59 from rhodecode.lib.vcs.backends.base import (
60 EmptyCommit, Reference, unicode_to_reference, reference_to_unicode)
60 61 from rhodecode.lib.utils2 import (
61 62 str2bool, safe_str, get_commit_safe, safe_unicode, sha1_safe,
62 63 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
@@ -3773,12 +3774,12 b' class ChangesetComment(Base, BaseModel):'
3773 3774 resolved_comment = relationship('ChangesetComment', remote_side=comment_id, back_populates='resolved_by')
3774 3775 resolved_by = relationship('ChangesetComment', back_populates='resolved_comment')
3775 3776
3776 author = relationship('User', lazy='joined')
3777 author = relationship('User', lazy='select')
3777 3778 repo = relationship('Repository')
3778 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='joined')
3779 pull_request = relationship('PullRequest', lazy='joined')
3780 pull_request_version = relationship('PullRequestVersion')
3781 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='joined', order_by='ChangesetCommentHistory.version')
3779 status_change = relationship('ChangesetStatus', cascade="all, delete-orphan", lazy='select')
3780 pull_request = relationship('PullRequest', lazy='select')
3781 pull_request_version = relationship('PullRequestVersion', lazy='select')
3782 history = relationship('ChangesetCommentHistory', cascade='all, delete-orphan', lazy='select', order_by='ChangesetCommentHistory.version')
3782 3783
3783 3784 @classmethod
3784 3785 def get_users(cls, revision=None, pull_request_id=None):
@@ -3983,10 +3984,10 b' class ChangesetStatus(Base, BaseModel):'
3983 3984 version = Column('version', Integer(), nullable=False, default=0)
3984 3985 pull_request_id = Column("pull_request_id", Integer(), ForeignKey('pull_requests.pull_request_id'), nullable=True)
3985 3986
3986 author = relationship('User', lazy='joined')
3987 repo = relationship('Repository')
3988 comment = relationship('ChangesetComment', lazy='joined')
3989 pull_request = relationship('PullRequest', lazy='joined')
3987 author = relationship('User', lazy='select')
3988 repo = relationship('Repository', lazy='select')
3989 comment = relationship('ChangesetComment', lazy='select')
3990 pull_request = relationship('PullRequest', lazy='select')
3990 3991
3991 3992 def __unicode__(self):
3992 3993 return u"<%s('%s[v%s]:%s')>" % (
@@ -4248,26 +4249,11 b' class _PullRequestBase(BaseModel):'
4248 4249
4249 4250 @staticmethod
4250 4251 def unicode_to_reference(raw):
4251 """
4252 Convert a unicode (or string) to a reference object.
4253 If unicode evaluates to False it returns None.
4254 """
4255 if raw:
4256 refs = raw.split(':')
4257 return Reference(*refs)
4258 else:
4259 return None
4252 return unicode_to_reference(raw)
4260 4253
4261 4254 @staticmethod
4262 4255 def reference_to_unicode(ref):
4263 """
4264 Convert a reference object to unicode.
4265 If reference is None it returns None.
4266 """
4267 if ref:
4268 return u':'.join(ref)
4269 else:
4270 return None
4256 return reference_to_unicode(ref)
4271 4257
4272 4258 def get_api_data(self, with_merge_state=True):
4273 4259 from rhodecode.model.pull_request import PullRequestModel
@@ -4465,6 +4451,37 b' class PullRequest(Base, _PullRequestBase'
4465 4451 from rhodecode.model.changeset_status import ChangesetStatusModel
4466 4452 return ChangesetStatusModel().reviewers_statuses(self)
4467 4453
4454 def get_pull_request_reviewers(self, role=None):
4455 qry = PullRequestReviewers.query()\
4456 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)
4457 if role:
4458 qry = qry.filter(PullRequestReviewers.role == role)
4459
4460 return qry.all()
4461
4462 @property
4463 def reviewers_count(self):
4464 qry = PullRequestReviewers.query()\
4465 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4466 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER)
4467 return qry.count()
4468
4469 @property
4470 def observers_count(self):
4471 qry = PullRequestReviewers.query()\
4472 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4473 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)
4474 return qry.count()
4475
4476 def observers(self):
4477 qry = PullRequestReviewers.query()\
4478 .filter(PullRequestReviewers.pull_request_id == self.pull_request_id)\
4479 .filter(PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER)\
4480 .all()
4481
4482 for entry in qry:
4483 yield entry, entry.user
4484
4468 4485 @property
4469 4486 def workspace_id(self):
4470 4487 from rhodecode.model.pull_request import PullRequestModel
@@ -4512,6 +4529,9 b' class PullRequestVersion(Base, _PullRequ'
4512 4529 @property
4513 4530 def reviewers(self):
4514 4531 return self.pull_request.reviewers
4532 @property
4533 def reviewers(self):
4534 return self.pull_request.reviewers
4515 4535
4516 4536 @property
4517 4537 def versions(self):
@@ -4530,6 +4550,9 b' class PullRequestVersion(Base, _PullRequ'
4530 4550 def reviewers_statuses(self):
4531 4551 return self.pull_request.reviewers_statuses()
4532 4552
4553 def observers(self):
4554 return self.pull_request.observers()
4555
4533 4556
4534 4557 class PullRequestReviewers(Base, BaseModel):
4535 4558 __tablename__ = 'pull_request_reviewers'
@@ -4538,6 +4561,7 b' class PullRequestReviewers(Base, BaseMod'
4538 4561 )
4539 4562 ROLE_REVIEWER = u'reviewer'
4540 4563 ROLE_OBSERVER = u'observer'
4564 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4541 4565
4542 4566 @hybrid_property
4543 4567 def reasons(self):
@@ -4589,6 +4613,15 b' class PullRequestReviewers(Base, BaseMod'
4589 4613
4590 4614 return user_group_data
4591 4615
4616 @classmethod
4617 def get_pull_request_reviewers(cls, pull_request_id, role=None):
4618 qry = PullRequestReviewers.query()\
4619 .filter(PullRequestReviewers.pull_request_id == pull_request_id)
4620 if role:
4621 qry = qry.filter(PullRequestReviewers.role == role)
4622
4623 return qry.all()
4624
4592 4625 def __unicode__(self):
4593 4626 return u"<%s('id:%s')>" % (self.__class__.__name__,
4594 4627 self.pull_requests_reviewers_id)
@@ -4954,16 +4987,21 b' class RepoReviewRuleUser(Base, BaseModel'
4954 4987 __table_args__ = (
4955 4988 base_table_args
4956 4989 )
4990 ROLE_REVIEWER = u'reviewer'
4991 ROLE_OBSERVER = u'observer'
4992 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4957 4993
4958 4994 repo_review_rule_user_id = Column('repo_review_rule_user_id', Integer(), primary_key=True)
4959 4995 repo_review_rule_id = Column("repo_review_rule_id", Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
4960 4996 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'), nullable=False)
4961 4997 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
4998 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4962 4999 user = relationship('User')
4963 5000
4964 5001 def rule_data(self):
4965 5002 return {
4966 'mandatory': self.mandatory
5003 'mandatory': self.mandatory,
5004 'role': self.role,
4967 5005 }
4968 5006
4969 5007
@@ -4974,17 +5012,22 b' class RepoReviewRuleUserGroup(Base, Base'
4974 5012 )
4975 5013
4976 5014 VOTE_RULE_ALL = -1
5015 ROLE_REVIEWER = u'reviewer'
5016 ROLE_OBSERVER = u'observer'
5017 ROLES = [ROLE_REVIEWER, ROLE_OBSERVER]
4977 5018
4978 5019 repo_review_rule_users_group_id = Column('repo_review_rule_users_group_id', Integer(), primary_key=True)
4979 5020 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)
5021 users_group_id = Column("users_group_id", Integer(), ForeignKey('users_groups.users_group_id'), nullable=False)
4981 5022 mandatory = Column("mandatory", Boolean(), nullable=False, default=False)
5023 role = Column('role', Unicode(255), nullable=True, default=ROLE_REVIEWER)
4982 5024 vote_rule = Column("vote_rule", Integer(), nullable=True, default=VOTE_RULE_ALL)
4983 5025 users_group = relationship('UserGroup')
4984 5026
4985 5027 def rule_data(self):
4986 5028 return {
4987 5029 'mandatory': self.mandatory,
5030 'role': self.role,
4988 5031 'vote_rule': self.vote_rule
4989 5032 }
4990 5033
@@ -601,6 +601,14 b' def PullRequestForm(localizer, repo_id):'
601 601 reasons = All()
602 602 rules = All(v.UniqueList(localizer, convert=int)())
603 603 mandatory = v.StringBoolean()
604 role = v.String(if_missing='reviewer')
605
606 class ObserverForm(formencode.Schema):
607 user_id = v.Int(not_empty=True)
608 reasons = All()
609 rules = All(v.UniqueList(localizer, convert=int)())
610 mandatory = v.StringBoolean()
611 role = v.String(if_missing='observer')
604 612
605 613 class _PullRequestForm(formencode.Schema):
606 614 allow_extra_fields = True
@@ -614,6 +622,7 b' def PullRequestForm(localizer, repo_id):'
614 622 revisions = All(#v.NotReviewedRevisions(localizer, repo_id)(),
615 623 v.UniqueList(localizer)(not_empty=True))
616 624 review_members = formencode.ForEach(ReviewerForm())
625 observer_members = formencode.ForEach(ObserverForm())
617 626 pullrequest_title = v.UnicodeString(strip=True, required=True, min=1, max=255)
618 627 pullrequest_desc = v.UnicodeString(strip=True, required=False)
619 628 description_renderer = v.UnicodeString(strip=True, required=False)
@@ -154,28 +154,56 b' def get_diff_info('
154 154
155 155 commits = []
156 156 if get_commit_authors:
157 commits = target_scm.compare(
157 log.debug('Obtaining commit authors from set of commits')
158 _compare_data = target_scm.compare(
158 159 target_ref, source_ref, source_scm, merge=True,
159 pre_load=["author"])
160 pre_load=["author", "date", "message"]
161 )
160 162
161 for commit in commits:
162 user = User.get_from_cs_author(commit.author)
163 for commit in _compare_data:
164 # NOTE(marcink): we serialize here, so we don't produce more vcsserver calls on data returned
165 # at this function which is later called via JSON serialization
166 serialized_commit = dict(
167 author=commit.author,
168 date=commit.date,
169 message=commit.message,
170 commit_id=commit.raw_id,
171 raw_id=commit.raw_id
172 )
173 commits.append(serialized_commit)
174 user = User.get_from_cs_author(serialized_commit['author'])
163 175 if user and user not in commit_authors:
164 176 commit_authors.append(user)
165 177
166 178 # lines
167 179 if get_authors:
180 log.debug('Calculating authors of changed files')
168 181 target_commit = source_repo.get_commit(ancestor_id)
169 182
170 183 for fname, lines in changed_lines.items():
184
171 185 try:
172 node = target_commit.get_node(fname)
186 node = target_commit.get_node(fname, pre_load=["is_binary"])
173 187 except Exception:
188 log.exception("Failed to load node with path %s", fname)
174 189 continue
175 190
176 191 if not isinstance(node, FileNode):
177 192 continue
178 193
194 # NOTE(marcink): for binary node we don't do annotation, just use last author
195 if node.is_binary:
196 author = node.last_commit.author
197 email = node.last_commit.author_email
198
199 user = User.get_from_cs_author(author)
200 if user:
201 user_counts[user.user_id] = user_counts.get(user.user_id, 0) + 1
202 author_counts[author] = author_counts.get(author, 0) + 1
203 email_counts[email] = email_counts.get(email, 0) + 1
204
205 continue
206
179 207 for annotation in node.annotate:
180 208 line_no, commit_id, get_commit_func, line_text = annotation
181 209 if line_no in lines:
@@ -190,6 +218,8 b' def get_diff_info('
190 218 author_counts[author] = author_counts.get(author, 0) + 1
191 219 email_counts[email] = email_counts.get(email, 0) + 1
192 220
221 log.debug('Default reviewers processing finished')
222
193 223 return {
194 224 'commits': commits,
195 225 'files': all_files_changes,
@@ -260,10 +290,16 b' class PullRequestModel(BaseModel):'
260 290 _perms = ('repository.admin',)
261 291 return self._check_perms(_perms, pull_request, user) or owner
262 292
293 def is_user_reviewer(self, pull_request, user):
294 return user.user_id in [
295 x.user_id for x in
296 pull_request.get_pull_request_reviewers(PullRequestReviewers.ROLE_REVIEWER)
297 if x.user
298 ]
299
263 300 def check_user_change_status(self, pull_request, user, api=False):
264 reviewer = user.user_id in [x.user_id for x in
265 pull_request.reviewers]
266 return self.check_user_update(pull_request, user, api) or reviewer
301 return self.check_user_update(pull_request, user, api) \
302 or self.is_user_reviewer(pull_request, user)
267 303
268 304 def check_user_comment(self, pull_request, user):
269 305 owner = user.user_id == pull_request.user_id
@@ -575,7 +611,7 b' class PullRequestModel(BaseModel):'
575 611 pull_request_display_obj, at_version
576 612
577 613 def create(self, created_by, source_repo, source_ref, target_repo,
578 target_ref, revisions, reviewers, title, description=None,
614 target_ref, revisions, reviewers, observers, title, description=None,
579 615 common_ancestor_id=None,
580 616 description_renderer=None,
581 617 reviewer_data=None, translator=None, auth_user=None):
@@ -606,7 +642,7 b' class PullRequestModel(BaseModel):'
606 642 reviewer_ids = set()
607 643 # members / reviewers
608 644 for reviewer_object in reviewers:
609 user_id, reasons, mandatory, rules = reviewer_object
645 user_id, reasons, mandatory, role, rules = reviewer_object
610 646 user = self._get_user(user_id)
611 647
612 648 # skip duplicates
@@ -620,6 +656,7 b' class PullRequestModel(BaseModel):'
620 656 reviewer.pull_request = pull_request
621 657 reviewer.reasons = reasons
622 658 reviewer.mandatory = mandatory
659 reviewer.role = role
623 660
624 661 # NOTE(marcink): pick only first rule for now
625 662 rule_id = list(rules)[0] if rules else None
@@ -653,6 +690,33 b' class PullRequestModel(BaseModel):'
653 690 Session().add(reviewer)
654 691 Session().flush()
655 692
693 for observer_object in observers:
694 user_id, reasons, mandatory, role, rules = observer_object
695 user = self._get_user(user_id)
696
697 # skip duplicates from reviewers
698 if user.user_id in reviewer_ids:
699 continue
700
701 #reviewer_ids.add(user.user_id)
702
703 observer = PullRequestReviewers()
704 observer.user = user
705 observer.pull_request = pull_request
706 observer.reasons = reasons
707 observer.mandatory = mandatory
708 observer.role = role
709
710 # NOTE(marcink): pick only first rule for now
711 rule_id = list(rules)[0] if rules else None
712 rule = RepoReviewRule.get(rule_id) if rule_id else None
713 if rule:
714 # TODO(marcink): do we need this for observers ??
715 pass
716
717 Session().add(observer)
718 Session().flush()
719
656 720 # Set approval status to "Under Review" for all commits which are
657 721 # part of this pull request.
658 722 ChangesetStatusModel().set_status(
@@ -678,7 +742,7 b' class PullRequestModel(BaseModel):'
678 742 MergeCheck.validate(
679 743 pull_request, auth_user=auth_user, translator=translator)
680 744
681 self.notify_reviewers(pull_request, reviewer_ids)
745 self.notify_reviewers(pull_request, reviewer_ids, created_by_user)
682 746 self.trigger_pull_request_hook(pull_request, created_by_user, 'create')
683 747
684 748 creation_data = pull_request.get_api_data(with_merge_state=False)
@@ -1204,23 +1268,25 b' class PullRequestModel(BaseModel):'
1204 1268
1205 1269 :param pull_request: the pr to update
1206 1270 :param reviewer_data: list of tuples
1207 [(user, ['reason1', 'reason2'], mandatory_flag, [rules])]
1271 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1272 :param user: current use who triggers this action
1208 1273 """
1274
1209 1275 pull_request = self.__get_pull_request(pull_request)
1210 1276 if pull_request.is_closed():
1211 1277 raise ValueError('This pull request is closed')
1212 1278
1213 1279 reviewers = {}
1214 for user_id, reasons, mandatory, rules in reviewer_data:
1280 for user_id, reasons, mandatory, role, rules in reviewer_data:
1215 1281 if isinstance(user_id, (int, compat.string_types)):
1216 1282 user_id = self._get_user(user_id).user_id
1217 1283 reviewers[user_id] = {
1218 'reasons': reasons, 'mandatory': mandatory}
1284 'reasons': reasons, 'mandatory': mandatory, 'role': role}
1219 1285
1220 1286 reviewers_ids = set(reviewers.keys())
1221 current_reviewers = PullRequestReviewers.query()\
1222 .filter(PullRequestReviewers.pull_request ==
1223 pull_request).all()
1287 current_reviewers = PullRequestReviewers.get_pull_request_reviewers(
1288 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_REVIEWER)
1289
1224 1290 current_reviewers_ids = set([x.user.user_id for x in current_reviewers])
1225 1291
1226 1292 ids_to_add = reviewers_ids.difference(current_reviewers_ids)
@@ -1241,16 +1307,19 b' class PullRequestModel(BaseModel):'
1241 1307 reviewer.reasons = reviewers[uid]['reasons']
1242 1308 # NOTE(marcink): mandatory shouldn't be changed now
1243 1309 # reviewer.mandatory = reviewers[uid]['reasons']
1310 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1311 reviewer.role = PullRequestReviewers.ROLE_REVIEWER
1244 1312 Session().add(reviewer)
1245 1313 added_audit_reviewers.append(reviewer.get_dict())
1246 1314
1247 1315 for uid in ids_to_remove:
1248 1316 changed = True
1249 # NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
1250 # that prevents and fixes cases that we added the same reviewer twice.
1317 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1318 # This is an edge case that handles previous state of having the same reviewer twice.
1251 1319 # this CAN happen due to the lack of DB checks
1252 1320 reviewers = PullRequestReviewers.query()\
1253 1321 .filter(PullRequestReviewers.user_id == uid,
1322 PullRequestReviewers.role == PullRequestReviewers.ROLE_REVIEWER,
1254 1323 PullRequestReviewers.pull_request == pull_request)\
1255 1324 .all()
1256 1325
@@ -1273,7 +1342,90 b' class PullRequestModel(BaseModel):'
1273 1342 'repo.pull_request.reviewer.delete', {'old_data': user_data},
1274 1343 user, pull_request)
1275 1344
1276 self.notify_reviewers(pull_request, ids_to_add)
1345 self.notify_reviewers(pull_request, ids_to_add, user)
1346 return ids_to_add, ids_to_remove
1347
1348 def update_observers(self, pull_request, observer_data, user):
1349 """
1350 Update the observers in the pull request
1351
1352 :param pull_request: the pr to update
1353 :param observer_data: list of tuples
1354 [(user, ['reason1', 'reason2'], mandatory_flag, role, [rules])]
1355 :param user: current use who triggers this action
1356 """
1357 pull_request = self.__get_pull_request(pull_request)
1358 if pull_request.is_closed():
1359 raise ValueError('This pull request is closed')
1360
1361 observers = {}
1362 for user_id, reasons, mandatory, role, rules in observer_data:
1363 if isinstance(user_id, (int, compat.string_types)):
1364 user_id = self._get_user(user_id).user_id
1365 observers[user_id] = {
1366 'reasons': reasons, 'observers': mandatory, 'role': role}
1367
1368 observers_ids = set(observers.keys())
1369 current_observers = PullRequestReviewers.get_pull_request_reviewers(
1370 pull_request.pull_request_id, role=PullRequestReviewers.ROLE_OBSERVER)
1371
1372 current_observers_ids = set([x.user.user_id for x in current_observers])
1373
1374 ids_to_add = observers_ids.difference(current_observers_ids)
1375 ids_to_remove = current_observers_ids.difference(observers_ids)
1376
1377 log.debug("Adding %s observer", ids_to_add)
1378 log.debug("Removing %s observer", ids_to_remove)
1379 changed = False
1380 added_audit_observers = []
1381 removed_audit_observers = []
1382
1383 for uid in ids_to_add:
1384 changed = True
1385 _usr = self._get_user(uid)
1386 observer = PullRequestReviewers()
1387 observer.user = _usr
1388 observer.pull_request = pull_request
1389 observer.reasons = observers[uid]['reasons']
1390 # NOTE(marcink): mandatory shouldn't be changed now
1391 # observer.mandatory = observer[uid]['reasons']
1392
1393 # NOTE(marcink): role should be hardcoded, so we won't edit it.
1394 observer.role = PullRequestReviewers.ROLE_OBSERVER
1395 Session().add(observer)
1396 added_audit_observers.append(observer.get_dict())
1397
1398 for uid in ids_to_remove:
1399 changed = True
1400 # NOTE(marcink): we fetch "ALL" reviewers objects using .all().
1401 # This is an edge case that handles previous state of having the same reviewer twice.
1402 # this CAN happen due to the lack of DB checks
1403 observers = PullRequestReviewers.query()\
1404 .filter(PullRequestReviewers.user_id == uid,
1405 PullRequestReviewers.role == PullRequestReviewers.ROLE_OBSERVER,
1406 PullRequestReviewers.pull_request == pull_request)\
1407 .all()
1408
1409 for obj in observers:
1410 added_audit_observers.append(obj.get_dict())
1411 Session().delete(obj)
1412
1413 if changed:
1414 Session().expire_all()
1415 pull_request.updated_on = datetime.datetime.now()
1416 Session().add(pull_request)
1417
1418 # finally store audit logs
1419 for user_data in added_audit_observers:
1420 self._log_audit_action(
1421 'repo.pull_request.observer.add', {'data': user_data},
1422 user, pull_request)
1423 for user_data in removed_audit_observers:
1424 self._log_audit_action(
1425 'repo.pull_request.observer.delete', {'old_data': user_data},
1426 user, pull_request)
1427
1428 self.notify_observers(pull_request, ids_to_add, user)
1277 1429 return ids_to_add, ids_to_remove
1278 1430
1279 1431 def get_url(self, pull_request, request=None, permalink=False):
@@ -1301,16 +1453,16 b' class PullRequestModel(BaseModel):'
1301 1453 pr_url = urllib.unquote(self.get_url(pull_request, request=request))
1302 1454 return safe_unicode('{pr_url}/repository'.format(pr_url=pr_url))
1303 1455
1304 def notify_reviewers(self, pull_request, reviewers_ids):
1305 # notification to reviewers
1306 if not reviewers_ids:
1456 def _notify_reviewers(self, pull_request, user_ids, role, user):
1457 # notification to reviewers/observers
1458 if not user_ids:
1307 1459 return
1308 1460
1309 log.debug('Notify following reviewers about pull-request %s', reviewers_ids)
1461 log.debug('Notify following %s users about pull-request %s', role, user_ids)
1310 1462
1311 1463 pull_request_obj = pull_request
1312 1464 # get the current participants of this pull request
1313 recipients = reviewers_ids
1465 recipients = user_ids
1314 1466 notification_type = EmailNotificationModel.TYPE_PULL_REQUEST
1315 1467
1316 1468 pr_source_repo = pull_request_obj.source_repo
@@ -1332,8 +1484,10 b' class PullRequestModel(BaseModel):'
1332 1484 (x.raw_id, x.message)
1333 1485 for x in map(pr_source_repo.get_commit, pull_request.revisions)]
1334 1486
1487 current_rhodecode_user = user
1335 1488 kwargs = {
1336 'user': pull_request.author,
1489 'user': current_rhodecode_user,
1490 'pull_request_author': pull_request.author,
1337 1491 'pull_request': pull_request_obj,
1338 1492 'pull_request_commits': pull_request_commits,
1339 1493
@@ -1345,6 +1499,7 b' class PullRequestModel(BaseModel):'
1345 1499
1346 1500 'pull_request_url': pr_url,
1347 1501 'thread_ids': [pr_url],
1502 'user_role': role
1348 1503 }
1349 1504
1350 1505 # pre-generate the subject for notification itself
@@ -1353,7 +1508,7 b' class PullRequestModel(BaseModel):'
1353 1508
1354 1509 # create notification objects, and emails
1355 1510 NotificationModel().create(
1356 created_by=pull_request.author,
1511 created_by=current_rhodecode_user,
1357 1512 notification_subject=subject,
1358 1513 notification_body=body_plaintext,
1359 1514 notification_type=notification_type,
@@ -1361,11 +1516,19 b' class PullRequestModel(BaseModel):'
1361 1516 email_kwargs=kwargs,
1362 1517 )
1363 1518
1519 def notify_reviewers(self, pull_request, reviewers_ids, user):
1520 return self._notify_reviewers(pull_request, reviewers_ids,
1521 PullRequestReviewers.ROLE_REVIEWER, user)
1522
1523 def notify_observers(self, pull_request, observers_ids, user):
1524 return self._notify_reviewers(pull_request, observers_ids,
1525 PullRequestReviewers.ROLE_OBSERVER, user)
1526
1364 1527 def notify_users(self, pull_request, updating_user, ancestor_commit_id,
1365 1528 commit_changes, file_changes):
1366 1529
1367 1530 updating_user_id = updating_user.user_id
1368 reviewers = set([x.user.user_id for x in pull_request.reviewers])
1531 reviewers = set([x.user.user_id for x in pull_request.get_pull_request_reviewers()])
1369 1532 # NOTE(marcink): send notification to all other users except to
1370 1533 # person who updated the PR
1371 1534 recipients = reviewers.difference(set([updating_user_id]))
@@ -1874,11 +2037,13 b' class PullRequestModel(BaseModel):'
1874 2037 try:
1875 2038 from rc_reviewers.utils import get_default_reviewers_data
1876 2039 from rc_reviewers.utils import validate_default_reviewers
2040 from rc_reviewers.utils import validate_observers
1877 2041 except ImportError:
1878 2042 from rhodecode.apps.repository.utils import get_default_reviewers_data
1879 2043 from rhodecode.apps.repository.utils import validate_default_reviewers
2044 from rhodecode.apps.repository.utils import validate_observers
1880 2045
1881 return get_default_reviewers_data, validate_default_reviewers
2046 return get_default_reviewers_data, validate_default_reviewers, validate_observers
1882 2047
1883 2048
1884 2049 class MergeCheck(object):
@@ -42,7 +42,7 b' from rhodecode.lib.exceptions import ('
42 42 from rhodecode.lib.caching_query import FromCache
43 43 from rhodecode.model import BaseModel
44 44 from rhodecode.model.db import (
45 _hash_key, true, false, or_, joinedload, User, UserToPerm,
45 _hash_key, func, true, false, or_, joinedload, User, UserToPerm,
46 46 UserEmailMap, UserIpMap, UserLog)
47 47 from rhodecode.model.meta import Session
48 48 from rhodecode.model.auth_token import AuthTokenModel
@@ -96,7 +96,11 b' class UserModel(BaseModel):'
96 96 User.username.ilike(ilike_expression)
97 97 )
98 98 )
99 # sort by len to have top most matches first
100 query = query.order_by(func.length(User.username))\
101 .order_by(User.username)
99 102 query = query.limit(limit)
103
100 104 users = query.all()
101 105
102 106 _users = [
@@ -21,12 +21,17 b''
21 21 import colander
22 22 from rhodecode.model.validation_schema import validators, preparers, types
23 23
24 DEFAULT_ROLE = 'reviewer'
25 VALID_ROLES = ['reviewer', 'observer']
26
24 27
25 28 class ReviewerSchema(colander.MappingSchema):
26 29 username = colander.SchemaNode(types.StrOrIntType())
27 30 reasons = colander.SchemaNode(colander.List(), missing=['no reason specified'])
28 31 mandatory = colander.SchemaNode(colander.Boolean(), missing=False)
29 32 rules = colander.SchemaNode(colander.List(), missing=[])
33 role = colander.SchemaNode(colander.String(), missing=DEFAULT_ROLE,
34 validator=colander.OneOf(VALID_ROLES))
30 35
31 36
32 37 class ReviewerListSchema(colander.SequenceSchema):
@@ -97,6 +97,7 b''
97 97 <li>The server is being restarted.</li>
98 98 <li>The server is overloaded.</li>
99 99 <li>The link may be incorrect.</li>
100 <li><a onclick="window.location.reload()">Reload page</a></li>
100 101 </ul>
101 102 </div>
102 103 <div class="inner-column">
@@ -374,9 +374,6 b' ul.auth_plugins {'
374 374 background-color: @grey6;
375 375 }
376 376
377 .td-status {
378 padding-left: .5em;
379 }
380 377 .log-container .truncate {
381 378 height: 2.75em;
382 379 white-space: pre-line;
@@ -384,6 +381,10 b' ul.auth_plugins {'
384 381 table.rctable .user {
385 382 padding-left: 0;
386 383 }
384 .td-status {
385 padding: 0 0px 0px 10px;
386 width: 15px;
387 }
387 388 table.rctable {
388 389 td.td-description,
389 390 .rc-user {
@@ -494,7 +495,8 b' ul.auth_plugins {'
494 495 padding-top: 10px;
495 496 }
496 497
497 #add_reviewer_input {
498 #add_reviewer_input,
499 #add_observer_input {
498 500 padding-top: 10px
499 501 }
500 502
@@ -1700,8 +1702,33 b' table.group_members {'
1700 1702 }
1701 1703
1702 1704 .reviewer_ac .ac-input {
1705 width: 98%;
1706 margin-bottom: 1em;
1707 }
1708
1709 .observer_ac .ac-input {
1710 width: 98%;
1711 margin-bottom: 1em;
1712 }
1713
1714 .rule-table {
1703 1715 width: 100%;
1704 margin-bottom: 1em;
1716 }
1717
1718 .rule-table td {
1719
1720 }
1721
1722 .rule-table .td-role {
1723 width: 100px
1724 }
1725
1726 .rule-table .td-mandatory {
1727 width: 100px
1728 }
1729
1730 .rule-table .td-group-votes {
1731 width: 150px
1705 1732 }
1706 1733
1707 1734 .compare_view_commits tr{
@@ -2635,6 +2662,7 b' h3.files_location{'
2635 2662 li {
2636 2663 list-style-type: none
2637 2664 }
2665
2638 2666 }
2639 2667
2640 2668 .grid-filter-box-icon {
@@ -477,23 +477,3 b''
477 477 }
478 478
479 479 }
480
481 .rctable.repo_summary {
482 border: 1px solid #eaeaea;
483 border-radius: 2px;
484 border-collapse: inherit;
485 border-bottom: 0;
486
487 th {
488 background: @grey7;
489 border-bottom: 0;
490 }
491
492 td {
493 border-color: #eaeaea;
494 }
495
496 td.td-status {
497 padding: 0 0 0 10px;
498 }
499 }
@@ -4,6 +4,25 b''
4 4 // see style guide documentation for guidelines.
5 5
6 6 // TABLES
7 table.rctable.table-bordered {
8 border: 1px solid #eaeaea;
9 border-radius: 2px;
10 border-collapse: inherit;
11 border-bottom: 0;
12
13 th {
14 background: @grey7;
15 border-bottom: 0;
16 }
17
18 td {
19 border-color: #eaeaea;
20 }
21
22 td.td-status {
23 padding: 0 0 0 10px;
24 }
25 }
7 26
8 27 .rctable,
9 28 table.rctable,
@@ -306,12 +325,14 b' table.dataTable {'
306 325 }
307 326 }
308 327 }
328
309 329 .rctable.audit-log {
310 330 td {
311 331 vertical-align: top;
312 332 }
313 333 }
314 334
335
315 336 // TRUNCATING
316 337 // TODO: lisaq: should this possibly be moved out of tables.less?
317 338 // for truncated text
@@ -426,15 +447,6 b' table.keyboard-mappings {'
426 447 }
427 448 }
428 449
429 // Pull Request List Table
430 #pull_request_list_table.dataTable {
431
432 //TODO: lisa: This needs to be removed once the description is adjusted
433 // for using an expand_commit button (see issue 765)
434 td {
435 vertical-align: middle;
436 }
437 }
438 450
439 451 // Settings (no border)
440 452 table.rctable.dl-settings {
@@ -484,9 +496,6 b' table.trending_language_tbl {'
484 496
485 497 // Changesets
486 498 #changesets.rctable {
487 th {
488 padding: 0 1em 0.65em 0;
489 }
490 499
491 500 // td must be fixed height for graph
492 501 td {
@@ -344,6 +344,10 b' mark,'
344 344 width: 200px;
345 345 }
346 346
347 #obj_count {
348 line-height: 34px;
349 }
350
347 351 }
348 352
349 353 #readme .title {
@@ -6,6 +6,8 b''
6 6 //JS translations map
7 7 var _TM = {
8 8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 13 'Add another comment': 'Add another comment',
@@ -27,6 +29,8 b' var _TM = {'
27 29 'Comment body was not changed.': 'Comment body was not changed.',
28 30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 34 'Context file: ': 'Context file: ',
31 35 'Delete': 'Delete',
32 36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 68 'No repository groups available yet.': 'No repository groups available yet.',
65 69 'No repository groups present.': 'No repository groups present.',
66 70 'No results': 'No results',
71 'No review rules set.': 'No review rules set.',
67 72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 73 'No tags available yet.': 'No tags available yet.',
69 74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 106 'Stop following this repository': 'Stop following this repository',
102 107 'Stopped watching this repository': 'Stopped watching this repository',
103 108 'Submitting...': 'Submitting...',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 110 'Switch to chat': 'Switch to chat',
105 111 'Switch to comment': 'Switch to comment',
106 112 'TODO comment': 'TODO comment',
107 113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -6,6 +6,8 b''
6 6 //JS translations map
7 7 var _TM = {
8 8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 13 'Add another comment': 'Add another comment',
@@ -27,6 +29,8 b' var _TM = {'
27 29 'Comment body was not changed.': 'Comment body was not changed.',
28 30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 34 'Context file: ': 'Context file: ',
31 35 'Delete': 'Löschen',
32 36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 68 'No repository groups available yet.': 'No repository groups available yet.',
65 69 'No repository groups present.': 'No repository groups present.',
66 70 'No results': 'No results',
71 'No review rules set.': 'No review rules set.',
67 72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 73 'No tags available yet.': 'No tags available yet.',
69 74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 106 'Stop following this repository': 'Stop following this repository',
102 107 'Stopped watching this repository': 'Stopped watching this repository',
103 108 'Submitting...': 'Submitting...',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 110 'Switch to chat': 'Switch to chat',
105 111 'Switch to comment': 'Switch to comment',
106 112 'TODO comment': 'TODO comment',
107 113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -6,6 +6,8 b''
6 6 //JS translations map
7 7 var _TM = {
8 8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 13 'Add another comment': 'Add another comment',
@@ -27,6 +29,8 b' var _TM = {'
27 29 'Comment body was not changed.': 'Comment body was not changed.',
28 30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 34 'Context file: ': 'Context file: ',
31 35 'Delete': 'Delete',
32 36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 68 'No repository groups available yet.': 'No repository groups available yet.',
65 69 'No repository groups present.': 'No repository groups present.',
66 70 'No results': 'No results',
71 'No review rules set.': 'No review rules set.',
67 72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 73 'No tags available yet.': 'No tags available yet.',
69 74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 106 'Stop following this repository': 'Stop following this repository',
102 107 'Stopped watching this repository': 'Stopped watching this repository',
103 108 'Submitting...': 'Submitting...',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 110 'Switch to chat': 'Switch to chat',
105 111 'Switch to comment': 'Switch to comment',
106 112 'TODO comment': 'TODO comment',
107 113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -6,6 +6,8 b''
6 6 //JS translations map
7 7 var _TM = {
8 8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 13 'Add another comment': 'Add another comment',
@@ -27,6 +29,8 b' var _TM = {'
27 29 'Comment body was not changed.': 'Comment body was not changed.',
28 30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 34 'Context file: ': 'Context file: ',
31 35 'Delete': 'Delete',
32 36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 68 'No repository groups available yet.': 'No repository groups available yet.',
65 69 'No repository groups present.': 'No repository groups present.',
66 70 'No results': 'No results',
71 'No review rules set.': 'No review rules set.',
67 72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 73 'No tags available yet.': 'No tags available yet.',
69 74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 106 'Stop following this repository': 'Stop following this repository',
102 107 'Stopped watching this repository': 'Stopped watching this repository',
103 108 'Submitting...': 'Submitting...',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 110 'Switch to chat': 'Switch to chat',
105 111 'Switch to comment': 'Switch to comment',
106 112 'TODO comment': 'TODO comment',
107 113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -6,6 +6,8 b''
6 6 //JS translations map
7 7 var _TM = {
8 8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 13 'Add another comment': 'Add another comment',
@@ -27,6 +29,8 b' var _TM = {'
27 29 'Comment body was not changed.': 'Comment body was not changed.',
28 30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 34 'Context file: ': 'Context file: ',
31 35 'Delete': 'Supprimer',
32 36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 68 'No repository groups available yet.': 'No repository groups available yet.',
65 69 'No repository groups present.': 'No repository groups present.',
66 70 'No results': 'No results',
71 'No review rules set.': 'No review rules set.',
67 72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 73 'No tags available yet.': 'No tags available yet.',
69 74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 106 'Stop following this repository': 'Arrêter de suivre ce dépôt',
102 107 'Stopped watching this repository': 'Stopped watching this repository',
103 108 'Submitting...': 'Envoi…',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 110 'Switch to chat': 'Switch to chat',
105 111 'Switch to comment': 'Switch to comment',
106 112 'TODO comment': 'TODO comment',
107 113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -6,6 +6,8 b''
6 6 //JS translations map
7 7 var _TM = {
8 8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 13 'Add another comment': 'Aggiungi un altro commento',
@@ -27,6 +29,8 b' var _TM = {'
27 29 'Comment body was not changed.': 'Comment body was not changed.',
28 30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 34 'Context file: ': 'Context file: ',
31 35 'Delete': 'Elimina',
32 36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 68 'No repository groups available yet.': 'No repository groups available yet.',
65 69 'No repository groups present.': 'No repository groups present.',
66 70 'No results': 'Nessun risultato',
71 'No review rules set.': 'No review rules set.',
67 72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 73 'No tags available yet.': 'No tags available yet.',
69 74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 106 'Stop following this repository': 'Smetti di seguire il repository',
102 107 'Stopped watching this repository': 'Stopped watching this repository',
103 108 'Submitting...': 'Inoltro...',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 110 'Switch to chat': 'Switch to chat',
105 111 'Switch to comment': 'Switch to comment',
106 112 'TODO comment': 'TODO comment',
107 113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 114 'There are currently no open pull requests requiring your participation.': 'Al momento non ci sono richieste di PULL che richiedono il tuo intervento',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -6,6 +6,8 b''
6 6 //JS translations map
7 7 var _TM = {
8 8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 13 'Add another comment': '別のコメントを追加',
@@ -27,6 +29,8 b' var _TM = {'
27 29 'Comment body was not changed.': 'Comment body was not changed.',
28 30 'Comment text will be set automatically based on currently selected status ({0}) ...': '選択したステータス ({0}) を元にコメントが自動的に設定されます...',
29 31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 34 'Context file: ': 'Context file: ',
31 35 'Delete': '削除',
32 36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 68 'No repository groups available yet.': 'まだリポジトリグループがありません。',
65 69 'No repository groups present.': 'No repository groups present.',
66 70 'No results': '結果がありません',
71 'No review rules set.': 'No review rules set.',
67 72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 73 'No tags available yet.': 'まだタグがありません。',
69 74 'No user groups available yet.': 'まだユーザーグループがありません。',
@@ -101,11 +106,13 b' var _TM = {'
101 106 'Stop following this repository': 'このリポジトリのフォローをやめる',
102 107 'Stopped watching this repository': 'Stopped watching this repository',
103 108 'Submitting...': '送信中...',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 110 'Switch to chat': 'Switch to chat',
105 111 'Switch to comment': 'Switch to comment',
106 112 'TODO comment': 'TODO comment',
107 113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -1,5 +1,7 b''
1 1 // AUTO GENERATED FILE FOR Babel JS-GETTEXT EXTRACTORS, DO NOT CHANGE
2 2 _gettext('(from usergroup {0})');
3 _gettext('<strong>, and {0} file</strong> changed.');
4 _gettext('<strong>, and {0} files</strong> changed.');
3 5 _gettext('<strong>{0} file</strong> changed, ');
4 6 _gettext('<strong>{0} files</strong> changed, ');
5 7 _gettext('Add another comment');
@@ -21,6 +23,8 b''
21 23 _gettext('Comment body was not changed.');
22 24 _gettext('Comment text will be set automatically based on currently selected status ({0}) ...');
23 25 _gettext('Commit Authors are not allowed to be a reviewer.');
26 _gettext('Compare summary: <strong>{0} commit</strong>');
27 _gettext('Compare summary: <strong>{0} commits</strong>');
24 28 _gettext('Context file: ');
25 29 _gettext('Delete');
26 30 _gettext('Delete this comment?');
@@ -58,6 +62,7 b''
58 62 _gettext('No repository groups available yet.');
59 63 _gettext('No repository groups present.');
60 64 _gettext('No results');
65 _gettext('No review rules set.');
61 66 _gettext('No ssh keys available yet.');
62 67 _gettext('No tags available yet.');
63 68 _gettext('No user groups available yet.');
@@ -95,11 +100,13 b''
95 100 _gettext('Stop following this repository');
96 101 _gettext('Stopped watching this repository');
97 102 _gettext('Submitting...');
103 _gettext('Switch target repository with the source.');
98 104 _gettext('Switch to chat');
99 105 _gettext('Switch to comment');
100 106 _gettext('TODO comment');
101 107 _gettext('TODO from comment {0} was fixed.');
102 108 _gettext('There are currently no open pull requests requiring your participation.');
109 _gettext('There are no commits to merge.');
103 110 _gettext('There is a later version of file tree available. Click {0} to create a file at the latest tree.');
104 111 _gettext('There is an existing path `{0}` at this commit.');
105 112 _gettext('This pull requests will consist of <strong>{0} commit</strong>.');
@@ -6,6 +6,8 b''
6 6 //JS translations map
7 7 var _TM = {
8 8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 13 'Add another comment': 'Dodaj kolejny komentarz',
@@ -27,6 +29,8 b' var _TM = {'
27 29 'Comment body was not changed.': 'Comment body was not changed.',
28 30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 34 'Context file: ': 'Context file: ',
31 35 'Delete': 'Usuń',
32 36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 68 'No repository groups available yet.': 'No repository groups available yet.',
65 69 'No repository groups present.': 'No repository groups present.',
66 70 'No results': 'No results',
71 'No review rules set.': 'No review rules set.',
67 72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 73 'No tags available yet.': 'No tags available yet.',
69 74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 106 'Stop following this repository': 'Zakończyć obserwację tego repozytorium',
102 107 'Stopped watching this repository': 'Stopped watching this repository',
103 108 'Submitting...': 'Przesyłanie...',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 110 'Switch to chat': 'Switch to chat',
105 111 'Switch to comment': 'Switch to comment',
106 112 'TODO comment': 'TODO comment',
107 113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -6,6 +6,8 b''
6 6 //JS translations map
7 7 var _TM = {
8 8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 13 'Add another comment': 'Adicionar outro comentário',
@@ -27,6 +29,8 b' var _TM = {'
27 29 'Comment body was not changed.': 'Comment body was not changed.',
28 30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 34 'Context file: ': 'Context file: ',
31 35 'Delete': 'Excluir',
32 36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 68 'No repository groups available yet.': 'No repository groups available yet.',
65 69 'No repository groups present.': 'No repository groups present.',
66 70 'No results': 'No results',
71 'No review rules set.': 'No review rules set.',
67 72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 73 'No tags available yet.': 'No tags available yet.',
69 74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 106 'Stop following this repository': 'Parar de seguir este repositório',
102 107 'Stopped watching this repository': 'Stopped watching this repository',
103 108 'Submitting...': 'Enviando...',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 110 'Switch to chat': 'Switch to chat',
105 111 'Switch to comment': 'Switch to comment',
106 112 'TODO comment': 'TODO comment',
107 113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -6,6 +6,8 b''
6 6 //JS translations map
7 7 var _TM = {
8 8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 13 'Add another comment': 'Добавить другой комментарий',
@@ -27,6 +29,8 b' var _TM = {'
27 29 'Comment body was not changed.': 'Comment body was not changed.',
28 30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 34 'Context file: ': 'Context file: ',
31 35 'Delete': 'Удалить',
32 36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 68 'No repository groups available yet.': 'No repository groups available yet.',
65 69 'No repository groups present.': 'No repository groups present.',
66 70 'No results': 'No results',
71 'No review rules set.': 'No review rules set.',
67 72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 73 'No tags available yet.': 'No tags available yet.',
69 74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 106 'Stop following this repository': 'Отменить наблюдение за репозиторием',
102 107 'Stopped watching this repository': 'Stopped watching this repository',
103 108 'Submitting...': 'Применение...',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 110 'Switch to chat': 'Switch to chat',
105 111 'Switch to comment': 'Switch to comment',
106 112 'TODO comment': 'TODO comment',
107 113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -6,6 +6,8 b''
6 6 //JS translations map
7 7 var _TM = {
8 8 '(from usergroup {0})': '(from usergroup {0})',
9 '<strong>, and {0} file</strong> changed.': '<strong>, and {0} file</strong> changed.',
10 '<strong>, and {0} files</strong> changed.': '<strong>, and {0} files</strong> changed.',
9 11 '<strong>{0} file</strong> changed, ': '<strong>{0} file</strong> changed, ',
10 12 '<strong>{0} files</strong> changed, ': '<strong>{0} files</strong> changed, ',
11 13 'Add another comment': 'Add another comment',
@@ -27,6 +29,8 b' var _TM = {'
27 29 'Comment body was not changed.': 'Comment body was not changed.',
28 30 'Comment text will be set automatically based on currently selected status ({0}) ...': 'Comment text will be set automatically based on currently selected status ({0}) ...',
29 31 'Commit Authors are not allowed to be a reviewer.': 'Commit Authors are not allowed to be a reviewer.',
32 'Compare summary: <strong>{0} commit</strong>': 'Compare summary: <strong>{0} commit</strong>',
33 'Compare summary: <strong>{0} commits</strong>': 'Compare summary: <strong>{0} commits</strong>',
30 34 'Context file: ': 'Context file: ',
31 35 'Delete': '删除',
32 36 'Delete this comment?': 'Delete this comment?',
@@ -64,6 +68,7 b' var _TM = {'
64 68 'No repository groups available yet.': 'No repository groups available yet.',
65 69 'No repository groups present.': 'No repository groups present.',
66 70 'No results': 'No results',
71 'No review rules set.': 'No review rules set.',
67 72 'No ssh keys available yet.': 'No ssh keys available yet.',
68 73 'No tags available yet.': 'No tags available yet.',
69 74 'No user groups available yet.': 'No user groups available yet.',
@@ -101,11 +106,13 b' var _TM = {'
101 106 'Stop following this repository': '停止关注该版本库',
102 107 'Stopped watching this repository': 'Stopped watching this repository',
103 108 'Submitting...': '提交中……',
109 'Switch target repository with the source.': 'Switch target repository with the source.',
104 110 'Switch to chat': 'Switch to chat',
105 111 'Switch to comment': 'Switch to comment',
106 112 'TODO comment': 'TODO comment',
107 113 'TODO from comment {0} was fixed.': 'TODO from comment {0} was fixed.',
108 114 'There are currently no open pull requests requiring your participation.': 'There are currently no open pull requests requiring your participation.',
115 'There are no commits to merge.': 'There are no commits to merge.',
109 116 'There is a later version of file tree available. Click {0} to create a file at the latest tree.': 'There is a later version of file tree available. Click {0} to create a file at the latest tree.',
110 117 'There is an existing path `{0}` at this commit.': 'There is an existing path `{0}` at this commit.',
111 118 'This pull requests will consist of <strong>{0} commit</strong>.': 'This pull requests will consist of <strong>{0} commit</strong>.',
@@ -75,8 +75,8 b' var CommitsController = function () {'
75 75 height: height,
76 76 x_step: x_step,
77 77 y_step: 42,
78 dotRadius: 3.5,
79 lineWidth: 2.5
78 dotRadius: 3.8,
79 lineWidth: 2.8
80 80 };
81 81
82 82 var prevCommitsData = this.$graphCanvas.data('commits') || [];
@@ -98,11 +98,12 b' var CommitsController = function () {'
98 98
99 99 this.setLabelText(edgeData);
100 100
101 var padding = 90;
101 // main padding from top, aligns the first dot graph
102 var padding = 100;
102 103 if (prev_link) {
103 104 padding += 34;
105 }
104 106
105 }
106 107 $('#graph_nodes').css({'padding-top': padding});
107 108
108 109 $.each($('.message.truncate'), function(idx, value) {
This diff has been collapsed as it changes many lines, (535 lines changed) Show them Hide them
@@ -94,21 +94,26 b' var getTitleAndDescription = function(so'
94 94 };
95 95
96 96
97 ReviewersController = function () {
97 window.ReviewersController = function () {
98 98 var self = this;
99 this.$loadingIndicator = $('.calculate-reviewers');
99 100 this.$reviewRulesContainer = $('#review_rules');
100 101 this.$rulesList = this.$reviewRulesContainer.find('.pr-reviewer-rules');
101 102 this.$userRule = $('.pr-user-rule-container');
102 this.forbidReviewUsers = undefined;
103 103 this.$reviewMembers = $('#review_members');
104 this.$observerMembers = $('#observer_members');
105
104 106 this.currentRequest = null;
105 107 this.diffData = null;
106 108 this.enabledRules = [];
109 // sync with db.py entries
110 this.ROLE_REVIEWER = 'reviewer';
111 this.ROLE_OBSERVER = 'observer'
107 112
108 113 //dummy handler, we might register our own later
109 this.diffDataHandler = function(data){};
114 this.diffDataHandler = function (data) {};
110 115
111 this.defaultForbidReviewUsers = function () {
116 this.defaultForbidUsers = function () {
112 117 return [
113 118 {
114 119 'username': 'default',
@@ -117,6 +122,9 b' ReviewersController = function () {'
117 122 ];
118 123 };
119 124
125 // init default forbidden users
126 this.forbidUsers = this.defaultForbidUsers();
127
120 128 this.hideReviewRules = function () {
121 129 self.$reviewRulesContainer.hide();
122 130 $(self.$userRule.selector).hide();
@@ -133,11 +141,40 b' ReviewersController = function () {'
133 141 return '<div>- {0}</div>'.format(ruleText)
134 142 };
135 143
144 this.increaseCounter = function(role) {
145 if (role === self.ROLE_REVIEWER) {
146 var $elem = $('#reviewers-cnt')
147 var cnt = parseInt($elem.data('count') || 0)
148 cnt +=1
149 $elem.html(cnt);
150 $elem.data('count', cnt);
151 }
152 else if (role === self.ROLE_OBSERVER) {
153 var $elem = $('#observers-cnt');
154 var cnt = parseInt($elem.data('count') || 0)
155 cnt +=1
156 $elem.html(cnt);
157 $elem.data('count', cnt);
158 }
159 }
160
161 this.resetCounter = function () {
162 var $elem = $('#reviewers-cnt');
163
164 $elem.data('count', 0);
165 $elem.html(0);
166
167 var $elem = $('#observers-cnt');
168
169 $elem.data('count', 0);
170 $elem.html(0);
171 }
172
136 173 this.loadReviewRules = function (data) {
137 174 self.diffData = data;
138 175
139 176 // reset forbidden Users
140 this.forbidReviewUsers = self.defaultForbidReviewUsers();
177 this.forbidUsers = self.defaultForbidUsers();
141 178
142 179 // reset state of review rules
143 180 self.$rulesList.html('');
@@ -148,7 +185,7 b' ReviewersController = function () {'
148 185 self.addRule(
149 186 _gettext('All reviewers must vote.'))
150 187 );
151 return self.forbidReviewUsers
188 return self.forbidUsers
152 189 }
153 190
154 191 if (data.rules.voting !== undefined) {
@@ -195,7 +232,7 b' ReviewersController = function () {'
195 232 }
196 233
197 234 if (data.rules.forbid_author_to_review) {
198 self.forbidReviewUsers.push(data.rules_data.pr_author);
235 self.forbidUsers.push(data.rules_data.pr_author);
199 236 self.$rulesList.append(
200 237 self.addRule(
201 238 _gettext('Author is not allowed to be a reviewer.'))
@@ -206,9 +243,8 b' ReviewersController = function () {'
206 243
207 244 if (data.rules_data.forbidden_users) {
208 245 $.each(data.rules_data.forbidden_users, function (index, member_data) {
209 self.forbidReviewUsers.push(member_data)
246 self.forbidUsers.push(member_data)
210 247 });
211
212 248 }
213 249
214 250 self.$rulesList.append(
@@ -223,9 +259,31 b' ReviewersController = function () {'
223 259 _gettext('No review rules set.'))
224 260 }
225 261
226 return self.forbidReviewUsers
262 return self.forbidUsers
227 263 };
228 264
265 this.emptyTables = function () {
266 self.emptyReviewersTable();
267 self.emptyObserversTable();
268
269 // Also reset counters.
270 self.resetCounter();
271 }
272
273 this.emptyReviewersTable = function (withText) {
274 self.$reviewMembers.empty();
275 if (withText !== undefined) {
276 self.$reviewMembers.html(withText)
277 }
278 };
279
280 this.emptyObserversTable = function (withText) {
281 self.$observerMembers.empty();
282 if (withText !== undefined) {
283 self.$observerMembers.html(withText)
284 }
285 }
286
229 287 this.loadDefaultReviewers = function (sourceRepo, sourceRef, targetRepo, targetRef) {
230 288
231 289 if (self.currentRequest) {
@@ -233,19 +291,21 b' ReviewersController = function () {'
233 291 self.currentRequest.abort();
234 292 }
235 293
236 $('.calculate-reviewers').show();
237 // reset reviewer members
238 self.$reviewMembers.empty();
294 self.$loadingIndicator.show();
295
296 // reset reviewer/observe members
297 self.emptyTables();
239 298
240 299 prButtonLock(true, null, 'reviewers');
241 300 $('#user').hide(); // hide user autocomplete before load
301 $('#observer').hide(); //hide observer autocomplete before load
242 302
243 303 // lock PR button, so we cannot send PR before it's calculated
244 304 prButtonLock(true, _gettext('Loading diff ...'), 'compare');
245 305
246 306 if (sourceRef.length !== 3 || targetRef.length !== 3) {
247 307 // don't load defaults in case we're missing some refs...
248 $('.calculate-reviewers').hide();
308 self.$loadingIndicator.hide();
249 309 return
250 310 }
251 311
@@ -253,9 +313,13 b' ReviewersController = function () {'
253 313 {
254 314 'repo_name': templateContext.repo_name,
255 315 'source_repo': sourceRepo,
316 'source_ref_type': sourceRef[0],
317 'source_ref_name': sourceRef[1],
256 318 'source_ref': sourceRef[2],
257 319 'target_repo': targetRepo,
258 'target_ref': targetRef[2]
320 'target_ref': targetRef[2],
321 'target_ref_type': sourceRef[0],
322 'target_ref_name': sourceRef[1]
259 323 });
260 324
261 325 self.currentRequest = $.ajax({
@@ -268,15 +332,23 b' ReviewersController = function () {'
268 332
269 333 // review rules
270 334 self.loadReviewRules(data);
271 self.handleDiffData(data["diff_info"]);
335 var diffHandled = self.handleDiffData(data["diff_info"]);
336 if (diffHandled === false) {
337 return
338 }
272 339
273 340 for (var i = 0; i < data.reviewers.length; i++) {
274 341 var reviewer = data.reviewers[i];
275 self.addReviewMember(reviewer, reviewer.reasons, reviewer.mandatory);
342 // load reviewer rules from the repo data
343 self.addMember(reviewer, reviewer.reasons, reviewer.mandatory, reviewer.role);
276 344 }
277 $('.calculate-reviewers').hide();
345
346
347 self.$loadingIndicator.hide();
278 348 prButtonLock(false, null, 'reviewers');
279 $('#user').show(); // show user autocomplete after load
349
350 $('#user').show(); // show user autocomplete before load
351 $('#observer').show(); // show observer autocomplete before load
280 352
281 353 var commitElements = data["diff_info"]['commits'];
282 354
@@ -292,7 +364,7 b' ReviewersController = function () {'
292 364
293 365 },
294 366 error: function (jqXHR, textStatus, errorThrown) {
295 var prefix = "Loading diff and reviewers failed\n"
367 var prefix = "Loading diff and reviewers/observers failed\n"
296 368 var message = formatErrorMessage(jqXHR, textStatus, errorThrown, prefix);
297 369 ajaxErrorSwal(message);
298 370 }
@@ -301,7 +373,7 b' ReviewersController = function () {'
301 373 };
302 374
303 375 // check those, refactor
304 this.removeReviewMember = function (reviewer_id, mark_delete) {
376 this.removeMember = function (reviewer_id, mark_delete) {
305 377 var reviewer = $('#reviewer_{0}'.format(reviewer_id));
306 378
307 379 if (typeof (mark_delete) === undefined) {
@@ -312,6 +384,7 b' ReviewersController = function () {'
312 384 if (reviewer) {
313 385 // now delete the input
314 386 $('#reviewer_{0} input'.format(reviewer_id)).remove();
387 $('#reviewer_{0}_rules input'.format(reviewer_id)).remove();
315 388 // mark as to-delete
316 389 var obj = $('#reviewer_{0}_name'.format(reviewer_id));
317 390 obj.addClass('to-delete');
@@ -322,27 +395,26 b' ReviewersController = function () {'
322 395 }
323 396 };
324 397
325 this.reviewMemberEntry = function () {
398 this.addMember = function (reviewer_obj, reasons, mandatory, role) {
326 399
327 };
328
329 this.addReviewMember = function (reviewer_obj, reasons, mandatory) {
330 400 var id = reviewer_obj.user_id;
331 401 var username = reviewer_obj.username;
332 402
333 var reasons = reasons || [];
334 var mandatory = mandatory || false;
403 reasons = reasons || [];
404 mandatory = mandatory || false;
405 role = role || self.ROLE_REVIEWER
335 406
336 // register IDS to check if we don't have this ID already in
407 // register current set IDS to check if we don't have this ID already in
408 // and prevent duplicates
337 409 var currentIds = [];
338 410
339 $.each(self.$reviewMembers.find('.reviewer_entry'), function (index, value) {
411 $.each($('.reviewer_entry'), function (index, value) {
340 412 currentIds.push($(value).data('reviewerUserId'))
341 413 })
342 414
343 415 var userAllowedReview = function (userId) {
344 416 var allowed = true;
345 $.each(self.forbidReviewUsers, function (index, member_data) {
417 $.each(self.forbidUsers, function (index, member_data) {
346 418 if (parseInt(userId) === member_data['user_id']) {
347 419 allowed = false;
348 420 return false // breaks the loop
@@ -352,6 +424,7 b' ReviewersController = function () {'
352 424 };
353 425
354 426 var userAllowed = userAllowedReview(id);
427
355 428 if (!userAllowed) {
356 429 alert(_gettext('User `{0}` not allowed to be a reviewer').format(username));
357 430 } else {
@@ -359,11 +432,13 b' ReviewersController = function () {'
359 432 var alreadyReviewer = currentIds.indexOf(id) != -1;
360 433
361 434 if (alreadyReviewer) {
362 alert(_gettext('User `{0}` already in reviewers').format(username));
435 alert(_gettext('User `{0}` already in reviewers/observers').format(username));
363 436 } else {
437
364 438 var reviewerEntry = renderTemplate('reviewMemberEntry', {
365 439 'member': reviewer_obj,
366 440 'mandatory': mandatory,
441 'role': role,
367 442 'reasons': reasons,
368 443 'allowed_to_update': true,
369 444 'review_status': 'not_reviewed',
@@ -372,20 +447,36 b' ReviewersController = function () {'
372 447 'create': true,
373 448 'rule_show': true,
374 449 })
375 $(self.$reviewMembers.selector).append(reviewerEntry);
450
451 if (role === self.ROLE_REVIEWER) {
452 $(self.$reviewMembers.selector).append(reviewerEntry);
453 self.increaseCounter(self.ROLE_REVIEWER);
454 $('#reviewer-empty-msg').remove()
455 }
456 else if (role === self.ROLE_OBSERVER) {
457 $(self.$observerMembers.selector).append(reviewerEntry);
458 self.increaseCounter(self.ROLE_OBSERVER);
459 $('#observer-empty-msg').remove();
460 }
461
376 462 tooltipActivate();
377 463 }
378 464 }
379 465
380 466 };
381 467
382 this.updateReviewers = function (repo_name, pull_request_id) {
383 var postData = $('#reviewers input').serialize();
384 _updatePullRequest(repo_name, pull_request_id, postData);
468 this.updateReviewers = function (repo_name, pull_request_id, role) {
469 if (role === 'reviewer') {
470 var postData = $('#reviewers input').serialize();
471 _updatePullRequest(repo_name, pull_request_id, postData);
472 } else if (role === 'observer') {
473 var postData = $('#observers input').serialize();
474 _updatePullRequest(repo_name, pull_request_id, postData);
475 }
385 476 };
386 477
387 478 this.handleDiffData = function (data) {
388 self.diffDataHandler(data)
479 return self.diffDataHandler(data)
389 480 }
390 481 };
391 482
@@ -449,35 +540,26 b' var editPullRequest = function(repo_name'
449 540
450 541
451 542 /**
452 * Reviewer autocomplete
543 * autocomplete handler for reviewers/observers
453 544 */
454 var ReviewerAutoComplete = function(inputId) {
455 $(inputId).autocomplete({
456 serviceUrl: pyroutes.url('user_autocomplete_data'),
457 minChars:2,
458 maxHeight:400,
459 deferRequestBy: 300, //miliseconds
460 showNoSuggestionNotice: true,
461 tabDisabled: true,
462 autoSelectFirst: true,
463 params: { user_id: templateContext.rhodecode_user.user_id, user_groups:true, user_groups_expand:true, skip_default_user:true },
464 formatResult: autocompleteFormatResult,
465 lookupFilter: autocompleteFilterResult,
466 onSelect: function(element, data) {
545 var autoCompleteHandler = function (inputId, controller, role) {
546
547 return function (element, data) {
467 548 var mandatory = false;
468 var reasons = [_gettext('added manually by "{0}"').format(templateContext.rhodecode_user.username)];
549 var reasons = [_gettext('added manually by "{0}"').format(
550 templateContext.rhodecode_user.username)];
469 551
470 552 // add whole user groups
471 553 if (data.value_type == 'user_group') {
472 554 reasons.push(_gettext('member of "{0}"').format(data.value_display));
473 555
474 $.each(data.members, function(index, member_data) {
556 $.each(data.members, function (index, member_data) {
475 557 var reviewer = member_data;
476 558 reviewer['user_id'] = member_data['id'];
477 559 reviewer['gravatar_link'] = member_data['icon_link'];
478 560 reviewer['user_link'] = member_data['profile_link'];
479 561 reviewer['rules'] = [];
480 reviewersController.addReviewMember(reviewer, reasons, mandatory);
562 controller.addMember(reviewer, reasons, mandatory, role);
481 563 })
482 564 }
483 565 // add single user
@@ -487,14 +569,71 b' var ReviewerAutoComplete = function(inpu'
487 569 reviewer['gravatar_link'] = data['icon_link'];
488 570 reviewer['user_link'] = data['profile_link'];
489 571 reviewer['rules'] = [];
490 reviewersController.addReviewMember(reviewer, reasons, mandatory);
572 controller.addMember(reviewer, reasons, mandatory, role);
491 573 }
492 574
493 $(inputId).val('');
575 $(inputId).val('');
494 576 }
495 });
577 }
578
579 /**
580 * Reviewer autocomplete
581 */
582 var ReviewerAutoComplete = function (inputId, controller) {
583 var self = this;
584 self.controller = controller;
585 self.inputId = inputId;
586 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_REVIEWER);
587
588 $(inputId).autocomplete({
589 serviceUrl: pyroutes.url('user_autocomplete_data'),
590 minChars: 2,
591 maxHeight: 400,
592 deferRequestBy: 300, //miliseconds
593 showNoSuggestionNotice: true,
594 tabDisabled: true,
595 autoSelectFirst: true,
596 params: {
597 user_id: templateContext.rhodecode_user.user_id,
598 user_groups: true,
599 user_groups_expand: true,
600 skip_default_user: true
601 },
602 formatResult: autocompleteFormatResult,
603 lookupFilter: autocompleteFilterResult,
604 onSelect: handler
605 });
496 606 };
497 607
608 /**
609 * Observers autocomplete
610 */
611 var ObserverAutoComplete = function(inputId, controller) {
612 var self = this;
613 self.controller = controller;
614 self.inputId = inputId;
615 var handler = autoCompleteHandler(inputId, controller, controller.ROLE_OBSERVER);
616
617 $(inputId).autocomplete({
618 serviceUrl: pyroutes.url('user_autocomplete_data'),
619 minChars: 2,
620 maxHeight: 400,
621 deferRequestBy: 300, //miliseconds
622 showNoSuggestionNotice: true,
623 tabDisabled: true,
624 autoSelectFirst: true,
625 params: {
626 user_id: templateContext.rhodecode_user.user_id,
627 user_groups: true,
628 user_groups_expand: true,
629 skip_default_user: true
630 },
631 formatResult: autocompleteFormatResult,
632 lookupFilter: autocompleteFilterResult,
633 onSelect: handler
634 });
635 }
636
498 637
499 638 window.VersionController = function () {
500 639 var self = this;
@@ -504,7 +643,7 b' window.VersionController = function () {'
504 643
505 644 this.adjustRadioSelectors = function (curNode) {
506 645 var getVal = function (item) {
507 if (item == 'latest') {
646 if (item === 'latest') {
508 647 return Number.MAX_SAFE_INTEGER
509 648 }
510 649 else {
@@ -663,6 +802,7 b' window.UpdatePrController = function () '
663 802 };
664 803 };
665 804
805
666 806 /**
667 807 * Reviewer display panel
668 808 */
@@ -673,6 +813,7 b' window.ReviewersPanel = {'
673 813 removeButtons: null,
674 814 reviewRules: null,
675 815 setReviewers: null,
816 controller: null,
676 817
677 818 setSelectors: function () {
678 819 var self = this;
@@ -682,17 +823,18 b' window.ReviewersPanel = {'
682 823 self.removeButtons = $('.reviewer_member_remove,.reviewer_member_mandatory_remove');
683 824 },
684 825
685 init: function (reviewRules, setReviewers) {
826 init: function (controller, reviewRules, setReviewers) {
686 827 var self = this;
687 828 self.setSelectors();
688 829
689 this.reviewRules = reviewRules;
690 this.setReviewers = setReviewers;
830 self.controller = controller;
831 self.reviewRules = reviewRules;
832 self.setReviewers = setReviewers;
691 833
692 this.editButton.on('click', function (e) {
834 self.editButton.on('click', function (e) {
693 835 self.edit();
694 836 });
695 this.closeButton.on('click', function (e) {
837 self.closeButton.on('click', function (e) {
696 838 self.close();
697 839 self.renderReviewers();
698 840 });
@@ -702,26 +844,134 b' window.ReviewersPanel = {'
702 844 },
703 845
704 846 renderReviewers: function () {
847 var self = this;
705 848
706 $('#review_members').html('')
707 $.each(this.setReviewers.reviewers, function (key, val) {
849 if (self.setReviewers.reviewers === undefined) {
850 return
851 }
852 if (self.setReviewers.reviewers.length === 0) {
853 self.controller.emptyReviewersTable('<tr id="reviewer-empty-msg"><td colspan="6">No reviewers</td></tr>');
854 return
855 }
856
857 self.controller.emptyReviewersTable();
858
859 $.each(self.setReviewers.reviewers, function (key, val) {
860
708 861 var member = val;
862 if (member.role === self.controller.ROLE_REVIEWER) {
863 var entry = renderTemplate('reviewMemberEntry', {
864 'member': member,
865 'mandatory': member.mandatory,
866 'role': member.role,
867 'reasons': member.reasons,
868 'allowed_to_update': member.allowed_to_update,
869 'review_status': member.review_status,
870 'review_status_label': member.review_status_label,
871 'user_group': member.user_group,
872 'create': false
873 });
874
875 $(self.controller.$reviewMembers.selector).append(entry)
876 }
877 });
878
879 tooltipActivate();
880 },
881
882 edit: function (event) {
883 var self = this;
884 self.editButton.hide();
885 self.closeButton.show();
886 self.addButton.show();
887 $(self.removeButtons.selector).css('visibility', 'visible');
888 // review rules
889 self.controller.loadReviewRules(this.reviewRules);
890 },
891
892 close: function (event) {
893 var self = this;
894 this.editButton.show();
895 this.closeButton.hide();
896 this.addButton.hide();
897 $(this.removeButtons.selector).css('visibility', 'hidden');
898 // hide review rules
899 self.controller.hideReviewRules();
900 }
901 };
709 902
710 var entry = renderTemplate('reviewMemberEntry', {
711 'member': member,
712 'mandatory': member.mandatory,
713 'reasons': member.reasons,
714 'allowed_to_update': member.allowed_to_update,
715 'review_status': member.review_status,
716 'review_status_label': member.review_status_label,
717 'user_group': member.user_group,
718 'create': false
719 });
903 /**
904 * Reviewer display panel
905 */
906 window.ObserversPanel = {
907 editButton: null,
908 closeButton: null,
909 addButton: null,
910 removeButtons: null,
911 reviewRules: null,
912 setReviewers: null,
913 controller: null,
914
915 setSelectors: function () {
916 var self = this;
917 self.editButton = $('#open_edit_observers');
918 self.closeButton =$('#close_edit_observers');
919 self.addButton = $('#add_observer');
920 self.removeButtons = $('.observer_member_remove,.observer_member_mandatory_remove');
921 },
922
923 init: function (controller, reviewRules, setReviewers) {
924 var self = this;
925 self.setSelectors();
926
927 self.controller = controller;
928 self.reviewRules = reviewRules;
929 self.setReviewers = setReviewers;
930
931 self.editButton.on('click', function (e) {
932 self.edit();
933 });
934 self.closeButton.on('click', function (e) {
935 self.close();
936 self.renderObservers();
937 });
720 938
721 $('#review_members').append(entry)
939 self.renderObservers();
940
941 },
942
943 renderObservers: function () {
944 var self = this;
945 if (self.setReviewers.observers === undefined) {
946 return
947 }
948 if (self.setReviewers.observers.length === 0) {
949 self.controller.emptyObserversTable('<tr id="observer-empty-msg"><td colspan="6">No observers</td></tr>');
950 return
951 }
952
953 self.controller.emptyObserversTable();
954
955 $.each(self.setReviewers.observers, function (key, val) {
956 var member = val;
957 if (member.role === self.controller.ROLE_OBSERVER) {
958 var entry = renderTemplate('reviewMemberEntry', {
959 'member': member,
960 'mandatory': member.mandatory,
961 'role': member.role,
962 'reasons': member.reasons,
963 'allowed_to_update': member.allowed_to_update,
964 'review_status': member.review_status,
965 'review_status_label': member.review_status_label,
966 'user_group': member.user_group,
967 'create': false
968 });
969
970 $(self.controller.$observerMembers.selector).append(entry)
971 }
722 972 });
973
723 974 tooltipActivate();
724
725 975 },
726 976
727 977 edit: function (event) {
@@ -729,8 +979,6 b' window.ReviewersPanel = {'
729 979 this.closeButton.show();
730 980 this.addButton.show();
731 981 $(this.removeButtons.selector).css('visibility', 'visible');
732 // review rules
733 reviewersController.loadReviewRules(this.reviewRules);
734 982 },
735 983
736 984 close: function (event) {
@@ -738,12 +986,56 b' window.ReviewersPanel = {'
738 986 this.closeButton.hide();
739 987 this.addButton.hide();
740 988 $(this.removeButtons.selector).css('visibility', 'hidden');
741 // hide review rules
742 reviewersController.hideReviewRules()
989 }
990
991 };
992
993 window.PRDetails = {
994 editButton: null,
995 closeButton: null,
996 deleteButton: null,
997 viewFields: null,
998 editFields: null,
999
1000 setSelectors: function () {
1001 var self = this;
1002 self.editButton = $('#open_edit_pullrequest')
1003 self.closeButton = $('#close_edit_pullrequest')
1004 self.deleteButton = $('#delete_pullrequest')
1005 self.viewFields = $('#pr-desc, #pr-title')
1006 self.editFields = $('#pr-desc-edit, #pr-title-edit, .pr-save')
1007 },
1008
1009 init: function () {
1010 var self = this;
1011 self.setSelectors();
1012 self.editButton.on('click', function (e) {
1013 self.edit();
1014 });
1015 self.closeButton.on('click', function (e) {
1016 self.view();
1017 });
1018 },
1019
1020 edit: function (event) {
1021 var cmInstance = $('#pr-description-input').get(0).MarkupForm.cm;
1022 this.viewFields.hide();
1023 this.editButton.hide();
1024 this.deleteButton.hide();
1025 this.closeButton.show();
1026 this.editFields.show();
1027 cmInstance.refresh();
1028 },
1029
1030 view: function (event) {
1031 this.editButton.show();
1032 this.deleteButton.show();
1033 this.editFields.hide();
1034 this.closeButton.hide();
1035 this.viewFields.show();
743 1036 }
744 1037 };
745 1038
746
747 1039 /**
748 1040 * OnLine presence using channelstream
749 1041 */
@@ -813,29 +1105,29 b' window.refreshComments = function (versi'
813 1105 $.each($('.comment'), function (idx, element) {
814 1106 currentIDs.push($(element).data('commentId'));
815 1107 });
816 var data = {"comments[]": currentIDs};
1108 var data = {"comments": currentIDs};
817 1109
818 1110 var $targetElem = $('.comments-content-table');
819 1111 $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 1112
835 $targetElem.css('opacity', 1);
836 tooltipActivate();
1113 var success = function (data) {
1114 var $counterElem = $('#comments-count');
1115 var newCount = $(data).data('counter');
1116 if (newCount !== undefined) {
1117 var callback = function () {
1118 $counterElem.animate({'opacity': 1.00}, 200)
1119 $counterElem.html(newCount);
1120 };
1121 $counterElem.animate({'opacity': 0.15}, 200, callback);
837 1122 }
838 );
1123
1124 $targetElem.css('opacity', 1);
1125 $targetElem.html(data);
1126 tooltipActivate();
1127 }
1128
1129 ajaxPOST(loadUrl, data, success, null, {})
1130
839 1131 }
840 1132
841 1133 window.refreshTODOs = function (version) {
@@ -858,28 +1150,28 b' window.refreshTODOs = function (version)'
858 1150 currentIDs.push($(element).data('commentId'));
859 1151 });
860 1152
861 var data = {"comments[]": currentIDs};
1153 var data = {"comments": currentIDs};
862 1154 var $targetElem = $('.todos-content-table');
863 1155 $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 1156
879 $targetElem.css('opacity', 1);
880 tooltipActivate();
1157 var success = function (data) {
1158 var $counterElem = $('#todos-count')
1159 var newCount = $(data).data('counter');
1160 if (newCount !== undefined) {
1161 var callback = function () {
1162 $counterElem.animate({'opacity': 1.00}, 200)
1163 $counterElem.html(newCount);
1164 };
1165 $counterElem.animate({'opacity': 0.15}, 200, callback);
881 1166 }
882 );
1167
1168 $targetElem.css('opacity', 1);
1169 $targetElem.html(data);
1170 tooltipActivate();
1171 }
1172
1173 ajaxPOST(loadUrl, data, success, null, {})
1174
883 1175 }
884 1176
885 1177 window.refreshAllComments = function (version) {
@@ -888,3 +1180,12 b' window.refreshAllComments = function (ve'
888 1180 refreshComments(version);
889 1181 refreshTODOs(version);
890 1182 };
1183
1184 window.sidebarComment = function (commentId) {
1185 var jsonData = $('#commentHovercard{0}'.format(commentId)).data('commentJsonB64');
1186 if (!jsonData) {
1187 return 'Failed to load comment {0}'.format(commentId)
1188 }
1189 var funcData = JSON.parse(atob(jsonData));
1190 return renderTemplate('sideBarCommentHovercard', funcData)
1191 };
@@ -57,15 +57,18 b' var ajaxGET = function (url, success, fa'
57 57 return request;
58 58 };
59 59
60 var ajaxPOST = function (url, postData, success, failure) {
61 var sUrl = url;
62 var postData = toQueryString(postData);
63 var request = $.ajax({
60 var ajaxPOST = function (url, postData, success, failure, options) {
61
62 var ajaxSettings = $.extend({
64 63 type: 'POST',
65 url: sUrl,
66 data: postData,
64 url: url,
65 data: toQueryString(postData),
67 66 headers: {'X-PARTIAL-XHR': true}
68 })
67 }, options);
68
69 var request = $.ajax(
70 ajaxSettings
71 )
69 72 .done(function (data) {
70 73 success(data);
71 74 })
@@ -126,7 +129,8 b' function formatErrorMessage(jqXHR, textS'
126 129 } else if (errorThrown === 'abort') {
127 130 return (prefix + 'Ajax request aborted.');
128 131 } else {
129 return (prefix + 'Uncaught Error.\n' + jqXHR.responseText);
132 var errInfo = 'Uncaught Error. code: {0}\n'.format(jqXHR.status)
133 return (prefix + errInfo + jqXHR.responseText);
130 134 }
131 135 }
132 136
@@ -33,7 +33,7 b''
33 33 <h3 class="panel-title">${_('Pull Requests You Participate In')}</h3>
34 34 </div>
35 35 <div class="panel-body panel-body-min-height">
36 <table id="pull_request_list_table" class="display"></table>
36 <table id="pull_request_list_table" class="rctable table-bordered"></table>
37 37 </div>
38 38 </div>
39 39
@@ -58,16 +58,10 b''
58 58
59 59 dom: 'rtp',
60 60 pageLength: ${c.visual.dashboard_items},
61 order: [[2, "desc"]],
61 order: [[1, "desc"]],
62 62 columns: [
63 63 {
64 64 data: {
65 "_": "target_repo",
66 "sort": "target_repo"
67 }, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false
68 },
69 {
70 data: {
71 65 "_": "status",
72 66 "sort": "status"
73 67 }, title: "", className: "td-status", orderable: false
@@ -101,7 +95,13 b''
101 95 "_": "updated_on",
102 96 "sort": "updated_on_raw"
103 97 }, title: "${_('Last Update')}", className: "td-time"
104 }
98 },
99 {
100 data: {
101 "_": "target_repo",
102 "sort": "target_repo"
103 }, title: "${_('Target Repo')}", className: "td-targetrepo", orderable: false
104 },
105 105 ],
106 106 language: {
107 107 paginate: DEFAULT_GRID_PAGINATION,
@@ -785,15 +785,15 b''
785 785
786 786 - Prefix query to allow special search:
787 787
788 user:admin, to search for usernames, always global
788 <strong>user:</strong>admin, to search for usernames, always global
789 789
790 user_group:devops, to search for user groups, always global
790 <strong>user_group:</strong>devops, to search for user groups, always global
791 791
792 pr:303, to search for pull request number, title, or description, always global
792 <strong>pr:</strong>303, to search for pull request number, title, or description, always global
793 793
794 commit:efced4, to search for commits, scoped to repositories or groups
794 <strong>commit:</strong>efced4, to search for commits, scoped to repositories or groups
795 795
796 file:models.py, to search for file paths, scoped to repositories or groups
796 <strong>file:</strong>models.py, to search for file paths, scoped to repositories or groups
797 797
798 798 % if c.template_context['search_context']['repo_id']:
799 799 For advanced full text search visit: <a href="${h.route_path('search_repo',repo_name=c.template_context['search_context']['repo_name'])}">repository search</a>
@@ -16,8 +16,8 b' examples = ['
16 16 ),
17 17
18 18 (
19 'Redmine',
20 '(^#|\s#)(?P<issue_id>\d+)',
19 'Tickets with #123 (Redmine etc)',
20 '(?<![a-zA-Z0-9_/]{1,10}-?)(#)(?P<issue_id>\d+)',
21 21 'https://myissueserver.com/${repo}/issue/${issue_id}',
22 22 ''
23 23 ),
@@ -38,14 +38,15 b' examples = ['
38 38
39 39 (
40 40 'JIRA - All tickets',
41 '(^|\s\w+-\d+)',
42 'https://myjira.com/browse/${id}',
41 # official JIRA ticket pattern
42 '(?<![a-zA-Z0-9_/#]-?)(?P<issue_id>[A-Z]{1,6}-(?:[1-9][0-9]{0,7}))',
43 'https://myjira.com/browse/${issue_id}',
43 44 ''
44 45 ),
45 46
46 47 (
47 'JIRA - Project (JRA)',
48 '(?:(^|\s)(?P<issue_id>(?:JRA-|JRA-)(?:\d+)))',
48 'JIRA - Single project (JRA-XXXXXXXX)',
49 '(?<![a-zA-Z0-9_/#]-?)(?P<issue_id>JRA-(?:[1-9][0-9]{0,7}))',
49 50 'https://myjira.com/${issue_id}',
50 51 ''
51 52 ),
@@ -275,13 +276,19 b' examples = ['
275 276 <div class='textarea-full'>
276 277 <textarea id="test_pattern_data" rows="12">
277 278 This is an example text for testing issue tracker patterns.
278 This commit fixes ticket #451 and ticket #910.
279 Following tickets will get mentioned:
279 This commit fixes ticket #451 and ticket #910, reference for JRA-401.
280 The following tickets will get mentioned:
280 281 #123
281 #456
282 JRA-123
283 JRA-456
284 Open a pull request !101 to contribute !
282 #456 and PROJ-101
283 JRA-123 and #123
284 PROJ-456
285
286 [my artifact](http://something.com/JRA-1234-build.zip)
287
288 - #1001
289 - JRA-998
290
291 Open a pull request !101 to contribute!
285 292 Added tag v1.3.0 for commit 0f3b629be725
286 293
287 294 Add a test pattern here and hit preview to see the link.
@@ -89,36 +89,41 b''
89 89 if is_pr:
90 90 version_info = (' made in older version (v{})'.format(comment_ver_index) if is_from_old_ver == 'true' else ' made in this version')
91 91 %>
92
93 <script type="text/javascript">
94 // closure function helper
95 var sidebarComment${comment_obj.comment_id} = function() {
96 return renderTemplate('sideBarCommentHovercard', {
97 version_info: "${version_info}",
98 file_name: "${comment_obj.f_path}",
99 line_no: "${comment_obj.line_no}",
100 outdated: ${h.json.dumps(comment_obj.outdated)},
101 inline: ${h.json.dumps(comment_obj.is_inline)},
102 is_todo: ${h.json.dumps(comment_obj.is_todo)},
103 created_on: "${h.format_date(comment_obj.created_on)}",
104 datetime: "${comment_obj.created_on}${h.get_timezone(comment_obj.created_on, time_is_local=True)}",
105 review_status: "${(comment_obj.review_status or '')}"
106 })
107 }
108 </script>
109
110 % if comment_obj.outdated:
111 <i class="icon-comment-toggle tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
112 % elif comment_obj.is_inline:
113 <i class="icon-code tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
114 % else:
115 <i class="icon-comment tooltip-hovercard" data-hovercard-url="javascript:sidebarComment${comment_obj.comment_id}()"></i>
92 ## new comments, since refresh
93 % if existing_ids and comment_obj.comment_id not in existing_ids:
94 <div class="tooltip" style="position: absolute; left: 8px; color: #682668" title="New comment">
95 !
96 </div>
116 97 % endif
117 98
118 ## NEW, since refresh
119 % if existing_ids and comment_obj.comment_id not in existing_ids:
120 <span class="tag">NEW</span>
121 % endif
99 <%
100 data = h.json.dumps({
101 'comment_id': comment_obj.comment_id,
102 'version_info': version_info,
103 'file_name': comment_obj.f_path,
104 'line_no': comment_obj.line_no,
105 'outdated': comment_obj.outdated,
106 'inline': comment_obj.is_inline,
107 'is_todo': comment_obj.is_todo,
108 'created_on': h.format_date(comment_obj.created_on),
109 'datetime': '{}{}'.format(comment_obj.created_on, h.get_timezone(comment_obj.created_on, time_is_local=True)),
110 'review_status': (comment_obj.review_status or '')
111 })
112
113 if comment_obj.outdated:
114 icon = 'icon-comment-toggle'
115 elif comment_obj.is_inline:
116 icon = 'icon-code'
117 else:
118 icon = 'icon-comment'
119 %>
120
121 <i id="commentHovercard${comment_obj.comment_id}"
122 class="${icon} tooltip-hovercard"
123 data-hovercard-url="javascript:sidebarComment(${comment_obj.comment_id})"
124 data-comment-json-b64='${h.b64(data)}'>
125 </i>
126
122 127 </td>
123 128
124 129 <td class="td-todo-gravatar">
@@ -30,11 +30,21 b''
30 30 </ul>
31 31 %endif
32 32 %if c.has_references:
33 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
34 <span id="obj_count">0</span> ${_('bookmarks')}
33 <div class="grid-quick-filter">
34 <ul class="grid-filter-box">
35 <li class="grid-filter-box-icon">
36 <i class="icon-search"></i>
37 </li>
38 <li class="grid-filter-box-input">
39 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
40 </li>
41 </ul>
42 </div>
43 <div id="obj_count">0</div>
35 44 %endif
36 45 </div>
37 <table id="obj_list_table" class="display"></table>
46
47 <table id="obj_list_table" class="rctable table-bordered"></table>
38 48 </div>
39 49
40 50
@@ -43,7 +53,9 b''
43 53
44 54 var get_datatable_count = function(){
45 55 var api = $('#obj_list_table').dataTable().api();
46 $('#obj_count').text(api.page.info().recordsDisplay);
56 var total = api.page.info().recordsDisplay
57 var _text = _ngettext('{0} bookmark', '{0} bookmarks', total).format(total);
58 $('#obj_count').text(_text);
47 59 };
48 60
49 61 // object list
@@ -30,11 +30,20 b''
30 30 </ul>
31 31 %endif
32 32 %if c.has_references:
33 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
34 <span id="obj_count">0</span> ${_('branches')}
33 <div class="grid-quick-filter">
34 <ul class="grid-filter-box">
35 <li class="grid-filter-box-icon">
36 <i class="icon-search"></i>
37 </li>
38 <li class="grid-filter-box-input">
39 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
40 </li>
41 </ul>
42 </div>
43 <div id="obj_count">0</div>
35 44 %endif
36 45 </div>
37 <table id="obj_list_table" class="display"></table>
46 <table id="obj_list_table" class="rctable table-bordered"></table>
38 47 </div>
39 48
40 49 <script type="text/javascript">
@@ -42,7 +51,10 b''
42 51
43 52 var get_datatable_count = function(){
44 53 var api = $('#obj_list_table').dataTable().api();
45 $('#obj_count').text(api.page.info().recordsDisplay);
54 var total = api.page.info().recordsDisplay
55 var _text = _ngettext('{0} branch', '{0} branches', total).format(total);
56
57 $('#obj_count').text(_text);
46 58 };
47 59
48 60 // object list
@@ -187,12 +187,12 b''
187 187 <div class="sidebar-element clear-both">
188 188 <% vote_title = _ungettext(
189 189 'Status calculated based on votes from {} reviewer',
190 'Status calculated based on votes from {} reviewers', len(c.allowed_reviewers)).format(len(c.allowed_reviewers))
190 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
191 191 %>
192 192
193 193 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
194 194 <i class="icon-circle review-status-${c.commit_review_status}"></i>
195 ${len(c.allowed_reviewers)}
195 ${c.reviewers_count}
196 196 </div>
197 197 </div>
198 198
@@ -420,7 +420,8 b''
420 420 e.preventDefault();
421 421 });
422 422
423 ReviewersPanel.init(null, setReviewersData);
423 reviewersController = new ReviewersController();
424 ReviewersPanel.init(reviewersController, null, setReviewersData);
424 425
425 426 var channel = '${c.commit_broadcast_channel}';
426 427 new ReviewerPresenceController(channel)
@@ -99,7 +99,7 b''
99 99 <div id="graph_content" class="graph_full_width">
100 100
101 101 <div class="table">
102 <table id="changesets" class="rctable">
102 <table id="changesets" class="rctable table-bordered">
103 103 <tr>
104 104 ## checkbox
105 105 <th colspan="4">
@@ -379,22 +379,22 b''
379 379 </%def>
380 380
381 381 <%def name="pullrequest_name(pull_request_id, state, is_wip, target_repo_name, short=False)">
382 <code>
382 383 <a href="${h.route_path('pullrequest_show',repo_name=target_repo_name,pull_request_id=pull_request_id)}">
383
384 384 % if short:
385 385 !${pull_request_id}
386 386 % else:
387 387 ${_('Pull request !{}').format(pull_request_id)}
388 388 % endif
389
390 % if state not in ['created']:
391 <span class="tag tag-merge-state-${state} tooltip" title="Pull request state is changing">${state}</span>
392 % endif
389 </a>
390 </code>
391 % if state not in ['created']:
392 <span class="tag tag-merge-state-${state} tooltip" title="Pull request state is changing">${state}</span>
393 % endif
393 394
394 % if is_wip:
395 <span class="tag tooltip" title="${_('Work in progress')}">wip</span>
396 % endif
397 </a>
395 % if is_wip:
396 <span class="tag tooltip" title="${_('Work in progress')}">wip</span>
397 % endif
398 398 </%def>
399 399
400 400 <%def name="pullrequest_updated_on(updated_on)">
@@ -149,7 +149,7 b''
149 149 <span class="user"> <a href="/_profiles/jenkins-tests">jenkins-tests</a> (reviewer)</span>
150 150 </div>
151 151 <input id="reviewer_70_input" type="hidden" value="70" name="review_members">
152 <div class="reviewer_member_remove action_button" onclick="removeReviewMember(70, true)" style="visibility: hidden;">
152 <div class="reviewer_member_remove action_button" onclick="removeMember(70, true)" style="visibility: hidden;">
153 153 <i class="icon-remove"></i>
154 154 </div>
155 155 </li>
@@ -66,9 +66,18 b" var data_hovercard_url = pyroutes.url('h"
66 66 <tr id="reviewer_<%= member.user_id %>" class="reviewer_entry" tooltip="Review Group" data-reviewer-user-id="<%= member.user_id %>">
67 67
68 68 <td style="width: 20px">
69 <div class="tooltip presence-state" style="display: none; position: absolute; left: 2px" title="This users is currently at this page">
70 <i class="icon-eye" style="color: #0ac878"></i>
71 </div>
72 <% if (role === 'reviewer') { %>
69 73 <div class="reviewer_status tooltip" title="<%= review_status_label %>">
70 74 <i class="icon-circle review-status-<%= review_status %>"></i>
71 75 </div>
76 <% } else if (role === 'observer') { %>
77 <div class="tooltip" title="Observer without voting right.">
78 <i class="icon-circle-thin"></i>
79 </div>
80 <% } %>
72 81 </td>
73 82
74 83 <td>
@@ -84,9 +93,6 b" var data_hovercard_url = pyroutes.url('h"
84 93 'gravatar_url': member.gravatar_link
85 94 })
86 95 %>
87 <span class="tooltip presence-state" style="display: none" title="This users is currently at this page">
88 <i class="icon-eye" style="color: #0ac878"></i>
89 </span>
90 96 </div>
91 97 </td>
92 98
@@ -108,7 +114,7 b" var data_hovercard_url = pyroutes.url('h"
108 114 <% } else { %>
109 115 <td style="text-align: right;width: 10px;">
110 116 <% if (allowed_to_update) { %>
111 <div class="reviewer_member_remove" onclick="reviewersController.removeReviewMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
117 <div class="<%=role %>_member_remove" onclick="reviewersController.removeMember(<%= member.user_id %>, true)" style="visibility: <%= edit_visibility %>;">
112 118 <i class="icon-remove"></i>
113 119 </div>
114 120 <% } %>
@@ -117,7 +123,7 b" var data_hovercard_url = pyroutes.url('h"
117 123
118 124 </tr>
119 125
120 <tr>
126 <tr id="reviewer_<%= member.user_id %>_rules">
121 127 <td colspan="4" style="display: <%= rule_visibility %>" class="pr-user-rule-container">
122 128 <input type="hidden" name="__start__" value="reviewer:mapping">
123 129
@@ -149,6 +155,7 b" var data_hovercard_url = pyroutes.url('h"
149 155
150 156 <input id="reviewer_<%= member.user_id %>_input" type="hidden" value="<%= member.user_id %>" name="user_id" />
151 157 <input type="hidden" name="mandatory" value="<%= mandatory %>"/>
158 <input type="hidden" name="role" value="<%= role %>"/>
152 159
153 160 <input type="hidden" name="__end__" value="reviewer:mapping">
154 161 </td>
@@ -11,7 +11,10 b' data = {'
11 11 'pr_title': pull_request.title,
12 12 }
13 13
14 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
14 if user_role == 'observer':
15 subject_template = email_pr_review_subject_template or _('{user} added you as observer to pull request. !{pr_id}: "{pr_title}"')
16 else:
17 subject_template = email_pr_review_subject_template or _('{user} requested a pull request review. !{pr_id}: "{pr_title}"')
15 18 %>
16 19
17 20 ${subject_template.format(**data) |n}
@@ -34,6 +37,7 b' data = {'
34 37 'source_repo_url': pull_request_source_repo_url,
35 38 'target_repo_url': pull_request_target_repo_url,
36 39 }
40
37 41 %>
38 42
39 43 * ${_('Pull Request link')}: ${pull_request_url}
@@ -51,7 +55,7 b' data = {'
51 55
52 56 % for commit_id, message in pull_request_commits:
53 57 - ${h.short_id(commit_id)}
54 ${h.chop_at_smart(message, '\n', suffix_if_chopped='...')}
58 ${h.chop_at_smart(message.lstrip(), '\n', suffix_if_chopped='...')}
55 59
56 60 % endfor
57 61
@@ -78,19 +82,23 b' data = {'
78 82 <table style="text-align:left;vertical-align:middle;width: 100%">
79 83 <tr>
80 84 <td style="width:100%;border-bottom:1px solid #dbd9da;">
81
82 85 <div style="margin: 0; font-weight: bold">
86 % if user_role == 'observer':
87 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
88 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
89 ${_('added you as observer to')}
90 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a>.
91 </div>
92 % else:
83 93 <div class="clear-both" class="clear-both" style="margin-bottom: 4px">
84 94 <span style="color:#7E7F7F">@${h.person(user.username)}</span>
85 95 ${_('requested a')}
86 <a href="${pull_request_url}" style="${base.link_css()}">
87 ${_('pull request review.').format(**data) }
88 </a>
96 <a href="${pull_request_url}" style="${base.link_css()}">pull request</a> review.
89 97 </div>
98 % endif
90 99 <div style="margin-top: 10px"></div>
91 100 ${_('Pull request')} <code>!${data['pr_id']}: ${data['pr_title']}</code>
92 101 </div>
93
94 102 </td>
95 103 </tr>
96 104
@@ -10,7 +10,7 b''
10 10 default_landing_ref = c.commit.raw_id
11 11 %>
12 12 <div id="file-tree-wrapper" class="browser-body ${('full-load' if c.full_load else '')}">
13 <table class="code-browser rctable repo_summary">
13 <table class="code-browser rctable table-bordered">
14 14 <thead>
15 15 <tr>
16 16 <th>${_('Name')}</th>
@@ -112,7 +112,7 b''
112 112 ## REVIEWERS
113 113 <div class="field">
114 114 <div class="label label-textarea">
115 <label for="pullrequest_reviewers">${_('Reviewers')}:</label>
115 <label for="pullrequest_reviewers">${_('Reviewers / Observers')}:</label>
116 116 </div>
117 117 <div class="content">
118 118 ## REVIEW RULES
@@ -125,29 +125,84 b''
125 125 </div>
126 126 </div>
127 127
128 ## REVIEWERS
128 ## REVIEWERS / OBSERVERS
129 129 <div class="reviewers-title">
130 <div class="pr-details-title">
131 ${_('Pull request reviewers')}
132 <span class="calculate-reviewers"> - ${_('loading...')}</span>
133 </div>
134 </div>
135 <div id="reviewers" class="pr-details-content reviewers">
136 ## members goes here, filled via JS based on initial selection !
137 <input type="hidden" name="__start__" value="review_members:sequence">
138 <table id="review_members" class="group_members">
139 ## This content is loaded via JS and ReviewersPanel
140 </table>
141 <input type="hidden" name="__end__" value="review_members:sequence">
130
131 <ul class="nav-links clearfix">
132
133 ## TAB1 MANDATORY REVIEWERS
134 <li class="active">
135 <a id="reviewers-btn" href="#showReviewers" tabindex="-1">
136 Reviewers
137 <span id="reviewers-cnt" data-count="0" class="menulink-counter">0</span>
138 </a>
139 </li>
140
141 ## TAB2 OBSERVERS
142 <li class="">
143 <a id="observers-btn" href="#showObservers" tabindex="-1">
144 Observers
145 <span id="observers-cnt" data-count="0" class="menulink-counter">0</span>
146 </a>
147 </li>
148
149 </ul>
142 150
143 <div id="add_reviewer_input" class='ac'>
144 <div class="reviewer_ac">
145 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
146 <div id="reviewers_container"></div>
151 ## TAB1 MANDATORY REVIEWERS
152 <div id="reviewers-container">
153 <span class="calculate-reviewers">
154 <h4>${_('loading...')}</h4>
155 </span>
156
157 <div id="reviewers" class="pr-details-content reviewers">
158 ## members goes here, filled via JS based on initial selection !
159 <input type="hidden" name="__start__" value="review_members:sequence">
160 <table id="review_members" class="group_members">
161 ## This content is loaded via JS and ReviewersPanel, an sets reviewer_entry class on each element
162 </table>
163 <input type="hidden" name="__end__" value="review_members:sequence">
164
165 <div id="add_reviewer_input" class='ac'>
166 <div class="reviewer_ac">
167 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
168 <div id="reviewers_container"></div>
169 </div>
170 </div>
171
147 172 </div>
148 173 </div>
149 174
175 ## TAB2 OBSERVERS
176 <div id="observers-container" style="display: none">
177 <span class="calculate-reviewers">
178 <h4>${_('loading...')}</h4>
179 </span>
180 % if c.rhodecode_edition_id == 'EE':
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 % else:
197 <h4>${_('This feature is available in RhodeCode EE edition only. Contact {sales_email} to obtain a trial license.').format(sales_email='<a href="mailto:sales@rhodecode.com">sales@rhodecode.com</a>')|n}</h4>
198 <p>
199 Pull request observers allows adding users who don't need to leave mandatory votes, but need to be aware about certain changes.
200 </p>
201 % endif
202 </div>
203
150 204 </div>
205
151 206 </div>
152 207 </div>
153 208
@@ -248,12 +303,19 b''
248 303
249 304 var originalOption = data.element;
250 305 return prefix + escapeMarkup(data.text);
251 };formatSelection:
306 };
252 307
253 308 // custom code mirror
254 309 var codeMirrorInstance = $('#pullrequest_desc').get(0).MarkupForm.cm;
255 310
256 311 var diffDataHandler = function(data) {
312 if (data['error'] !== undefined) {
313 var noCommitsMsg = '<span class="alert-text-error">{0}</span>'.format(data['error']);
314 prButtonLock(true, noCommitsMsg, 'compare');
315 //make both panels equal
316 $('.target-panel').height($('.source-panel').height())
317 return false
318 }
257 319
258 320 var commitElements = data['commits'];
259 321 var files = data['files'];
@@ -307,8 +369,10 b''
307 369 var msg = '<input id="common_ancestor" type="hidden" name="common_ancestor" value="{0}">'.format(commonAncestorId);
308 370 msg += '<input type="hidden" name="__start__" value="revisions:sequence">'
309 371
372
310 373 $.each(commitElements, function(idx, value) {
311 msg += '<input type="hidden" name="revisions" value="{0}">'.format(value["raw_id"]);
374 var commit_id = value["commit_id"]
375 msg += '<input type="hidden" name="revisions" value="{0}">'.format(commit_id);
312 376 });
313 377
314 378 msg += '<input type="hidden" name="__end__" value="revisions:sequence">'
@@ -338,8 +402,8 b''
338 402 }
339 403
340 404 //make both panels equal
341 $('.target-panel').height($('.source-panel').height())
342
405 $('.target-panel').height($('.source-panel').height());
406 return true
343 407 };
344 408
345 409 reviewersController = new ReviewersController();
@@ -465,8 +529,7 b''
465 529 queryTargetRefs(initialData, query)
466 530 },
467 531 initSelection: initRefSelection()
468 }
469 );
532 });
470 533
471 534 var sourceRepoSelect2 = Select2Box($sourceRepo, {
472 535 query: function(query) {}
@@ -521,12 +584,12 b''
521 584
522 585 });
523 586
524 $pullRequestForm.on('submit', function(e){
525 // Flush changes into textarea
526 codeMirrorInstance.save();
527 prButtonLock(true, null, 'all');
528 $pullRequestSubmit.val(_gettext('Please wait creating pull request...'));
529 });
587 $pullRequestForm.on('submit', function(e){
588 // Flush changes into textarea
589 codeMirrorInstance.save();
590 prButtonLock(true, null, 'all');
591 $pullRequestSubmit.val(_gettext('Please wait creating pull request...'));
592 });
530 593
531 594 prButtonLock(true, "${_('Please select source and target')}", 'all');
532 595
@@ -543,12 +606,44 b''
543 606 $sourceRef.select2('val', '${c.default_source_ref}');
544 607
545 608
546 // default reviewers
609 // default reviewers / observers
547 610 reviewersController.loadDefaultReviewers(
548 611 sourceRepo(), sourceRef(), targetRepo(), targetRef());
549 612 % endif
550 613
551 ReviewerAutoComplete('#user');
614 ReviewerAutoComplete('#user', reviewersController);
615 ObserverAutoComplete('#observer', reviewersController);
616
617 // TODO, move this to another handler
618
619 var $reviewersBtn = $('#reviewers-btn');
620 var $reviewersContainer = $('#reviewers-container');
621
622 var $observersBtn = $('#observers-btn')
623 var $observersContainer = $('#observers-container');
624
625 $reviewersBtn.on('click', function (e) {
626
627 $observersContainer.hide();
628 $reviewersContainer.show();
629
630 $observersBtn.parent().removeClass('active');
631 $reviewersBtn.parent().addClass('active');
632 e.preventDefault();
633
634 })
635
636 $observersBtn.on('click', function (e) {
637
638 $reviewersContainer.hide();
639 $observersContainer.show();
640
641 $reviewersBtn.parent().removeClass('active');
642 $observersBtn.parent().addClass('active');
643 e.preventDefault();
644
645 })
646
552 647 });
553 648 </script>
554 649
@@ -556,12 +556,12 b''
556 556 <div class="sidebar-element clear-both">
557 557 <% vote_title = _ungettext(
558 558 'Status calculated based on votes from {} reviewer',
559 'Status calculated based on votes from {} reviewers', len(c.allowed_reviewers)).format(len(c.allowed_reviewers))
559 'Status calculated based on votes from {} reviewers', c.reviewers_count).format(c.reviewers_count)
560 560 %>
561 561
562 562 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
563 563 <i class="icon-circle review-status-${c.pull_request_review_status}"></i>
564 ${len(c.allowed_reviewers)}
564 ${c.reviewers_count}
565 565 </div>
566 566
567 567 ## REVIEW RULES
@@ -609,13 +609,13 b''
609 609 <div id="add_reviewer" class="ac" style="display: none;">
610 610 %if c.allowed_to_update:
611 611 % if not c.forbid_adding_reviewers:
612 <div id="add_reviewer_input" class="reviewer_ac">
613 ${h.text('user', class_='ac-input', placeholder=_('Add reviewer or reviewer group'))}
612 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px">
613 <input class="ac-input" id="user" name="user" placeholder="${_('Add reviewer or reviewer group')}" type="text" autocomplete="off">
614 614 <div id="reviewers_container"></div>
615 615 </div>
616 616 % endif
617 <div class="pull-right">
618 <button id="update_pull_request" class="btn btn-small no-margin">${_('Save Changes')}</button>
617 <div class="pull-right" style="margin-bottom: 15px">
618 <button data-role="reviewer" id="update_reviewers" class="btn btn-small no-margin">${_('Save Changes')}</button>
619 619 </div>
620 620 %endif
621 621 </div>
@@ -623,23 +623,59 b''
623 623 </div>
624 624 </div>
625 625
626 ## ## OBSERVERS
627 ## <div class="sidebar-element clear-both">
628 ## <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${_('Observers')}">
629 ## <i class="icon-eye"></i>
630 ## 0
631 ## </div>
632 ##
633 ## <div class="right-sidebar-expanded-state pr-details-title">
634 ## <span class="sidebar-heading">
635 ## <i class="icon-eye"></i>
636 ## ${_('Observers')}
637 ## </span>
638 ## </div>
639 ## <div class="right-sidebar-expanded-state pr-details-content">
640 ## No observers
641 ## </div>
642 ## </div>
626 ## OBSERVERS
627 % if c.rhodecode_edition_id == 'EE':
628 <div class="sidebar-element clear-both">
629 <% vote_title = _ungettext(
630 '{} observer without voting right.',
631 '{} observers without voting right.', c.observers_count).format(c.observers_count)
632 %>
633
634 <div class="tooltip right-sidebar-collapsed-state" style="display: none" onclick="toggleSidebar(); return false" title="${vote_title}">
635 <i class="icon-circle-thin"></i>
636 ${c.observers_count}
637 </div>
638
639 <div class="right-sidebar-expanded-state pr-details-title">
640 <span class="tooltip sidebar-heading" title="${vote_title}">
641 <i class="icon-circle-thin"></i>
642 ${_('Observers')}
643 </span>
644 %if c.allowed_to_update:
645 <span id="open_edit_observers" class="block-right action_button last-item">${_('Edit')}</span>
646 <span id="close_edit_observers" class="block-right action_button last-item" style="display: none;">${_('Close')}</span>
647 %endif
648 </div>
649
650 <div id="observers" class="right-sidebar-expanded-state pr-details-content reviewers">
651 ## members redering block
652 <input type="hidden" name="__start__" value="observer_members:sequence">
653
654 <table id="observer_members" class="group_members">
655 ## This content is loaded via JS and ReviewersPanel
656 </table>
657
658 <input type="hidden" name="__end__" value="observer_members:sequence">
659 ## end members redering block
660
661 %if not c.pull_request.is_closed():
662 <div id="add_observer" class="ac" style="display: none;">
663 %if c.allowed_to_update:
664 % if not c.forbid_adding_reviewers or 1:
665 <div id="add_reviewer_input" class="reviewer_ac" style="width: 240px" >
666 <input class="ac-input" id="observer" name="observer" placeholder="${_('Add observer or observer group')}" type="text" autocomplete="off">
667 <div id="observers_container"></div>
668 </div>
669 % endif
670 <div class="pull-right" style="margin-bottom: 15px">
671 <button data-role="observer" id="update_observers" class="btn btn-small no-margin">${_('Save Changes')}</button>
672 </div>
673 %endif
674 </div>
675 %endif
676 </div>
677 </div>
678 % endif
643 679
644 680 ## TODOs
645 681 <div class="sidebar-element clear-both">
@@ -757,7 +793,7 b''
757 793
758 794 <tr><td><code>${_('In pull request description')}:</code></td></tr>
759 795 % if c.referenced_desc_issues:
760 % for ticket_dict in c.referenced_desc_issues:
796 % for ticket_dict in sorted(c.referenced_desc_issues):
761 797 <tr>
762 798 <td>
763 799 <a href="${ticket_dict.get('url')}">
@@ -776,7 +812,7 b''
776 812
777 813 <tr><td style="padding-top: 10px"><code>${_('In commit messages')}:</code></td></tr>
778 814 % if c.referenced_commit_issues:
779 % for ticket_dict in c.referenced_commit_issues:
815 % for ticket_dict in sorted(c.referenced_commit_issues):
780 816 <tr>
781 817 <td>
782 818 <a href="${ticket_dict.get('url')}">
@@ -815,6 +851,7 b' updateController = new UpdatePrControlle'
815 851
816 852 window.reviewerRulesData = ${c.pull_request_default_reviewers_data_json | n};
817 853 window.setReviewersData = ${c.pull_request_set_reviewers_data_json | n};
854 window.setObserversData = ${c.pull_request_set_observers_data_json | n};
818 855
819 856 (function () {
820 857 "use strict";
@@ -822,44 +859,9 b' window.setReviewersData = ${c.pull_reque'
822 859 // custom code mirror
823 860 var codeMirrorInstance = $('#pr-description-input').get(0).MarkupForm.cm;
824 861
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 862 PRDetails.init();
862 ReviewersPanel.init(reviewerRulesData, setReviewersData);
863 ReviewersPanel.init(reviewersController, reviewerRulesData, setReviewersData);
864 ObserversPanel.init(reviewersController, reviewerRulesData, setObserversData);
863 865
864 866 window.showOutdated = function (self) {
865 867 $('.comment-inline.comment-outdated').show();
@@ -929,12 +931,17 b' window.setReviewersData = ${c.pull_reque'
929 931 title, description, renderer);
930 932 });
931 933
932 $('#update_pull_request').on('click', function (e) {
933 $(this).attr('disabled', 'disabled');
934 $(this).addClass('disabled');
935 $(this).html(_gettext('Saving...'));
934 var $updateButtons = $('#update_reviewers,#update_observers');
935 $updateButtons.on('click', function (e) {
936 var role = $(this).data('role');
937 $updateButtons.attr('disabled', 'disabled');
938 $updateButtons.addClass('disabled');
939 $updateButtons.html(_gettext('Saving...'));
936 940 reviewersController.updateReviewers(
937 "${c.repo_name}", "${c.pull_request.pull_request_id}");
941 templateContext.repo_name,
942 templateContext.pull_request_data.pull_request_id,
943 role
944 );
938 945 });
939 946
940 947 // fixing issue with caches on firefox
@@ -978,7 +985,8 b' window.setReviewersData = ${c.pull_reque'
978 985 refreshMergeChecks();
979 986 };
980 987
981 ReviewerAutoComplete('#user');
988 ReviewerAutoComplete('#user', reviewersController);
989 ObserverAutoComplete('#observer', reviewersController);
982 990
983 991 })();
984 992
@@ -61,7 +61,7 b''
61 61 </div>
62 62
63 63 <div class="main-content-full-width">
64 <table id="pull_request_list_table" class="display"></table>
64 <table id="pull_request_list_table" class="rctable table-bordered"></table>
65 65 </div>
66 66
67 67 </div>
@@ -1,11 +1,11 b''
1 1 ## -*- coding: utf-8 -*-
2 2 <%namespace name="base" file="/base/base.mako"/>
3 3 %if c.repo_commits:
4 <table class="rctable repo_summary table_disp">
4 <table class="rctable table-bordered">
5 5 <tr>
6 6
7 7 <th class="status"></th>
8 <th>${_('Commit')}</th>
8 <th></th>
9 9 <th>${_('Commit message')}</th>
10 10 <th>${_('Age')}</th>
11 11 <th>${_('Author')}</th>
@@ -30,11 +30,20 b''
30 30 </ul>
31 31 %endif
32 32 %if c.has_references:
33 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
34 <span id="obj_count">0</span> ${_('tags')}
33 <div class="grid-quick-filter">
34 <ul class="grid-filter-box">
35 <li class="grid-filter-box-icon">
36 <i class="icon-search"></i>
37 </li>
38 <li class="grid-filter-box-input">
39 <input class="q_filter_box" id="q_filter" size="15" type="text" name="filter" placeholder="${_('quick filter...')}" value=""/>
40 </li>
41 </ul>
42 </div>
43 <div id="obj_count">0</div>
35 44 %endif
36 45 </div>
37 <table id="obj_list_table" class="display"></table>
46 <table id="obj_list_table" class="rctable table-bordered"></table>
38 47 </div>
39 48
40 49
@@ -43,7 +52,10 b''
43 52
44 53 var get_datatable_count = function(){
45 54 var api = $('#obj_list_table').dataTable().api();
46 $('#obj_count').text(api.page.info().recordsDisplay);
55 var total = api.page.info().recordsDisplay
56 var _text = _ngettext('{0} tag', '{0} tags', total).format(total);
57
58 $('#obj_count').text(_text);
47 59 };
48 60
49 61 // object list
@@ -121,7 +121,7 b' def test_extract_issues(backend, text_st'
121 121
122 122 with mock.patch.object(IssueTrackerSettingsModel,
123 123 'get_settings', get_settings_mock):
124 text, issues = helpers.process_patterns(text_string, repo.repo_name)
124 text, issues, errors = helpers.process_patterns(text_string, repo.repo_name)
125 125
126 126 expected = copy.deepcopy(expected)
127 127 for item in expected:
@@ -159,7 +159,7 b' def test_process_patterns_repo(backend, '
159 159
160 160 with mock.patch.object(IssueTrackerSettingsModel,
161 161 'get_settings', get_settings_mock):
162 processed_text, issues = helpers.process_patterns(
162 processed_text, issues, error = helpers.process_patterns(
163 163 text_string, repo.repo_name, link_format)
164 164
165 165 assert processed_text == expected_text.format(repo=repo.repo_name)
@@ -186,7 +186,7 b' def test_process_patterns_no_repo(text_s'
186 186
187 187 with mock.patch.object(IssueTrackerSettingsModel,
188 188 'get_global_settings', get_settings_mock):
189 processed_text, issues = helpers.process_patterns(
189 processed_text, issues, errors = helpers.process_patterns(
190 190 text_string, '')
191 191
192 192 assert processed_text == expected_text
@@ -211,7 +211,7 b' def test_process_patterns_non_existent_r'
211 211
212 212 with mock.patch.object(IssueTrackerSettingsModel,
213 213 'get_global_settings', get_settings_mock):
214 processed_text, issues = helpers.process_patterns(
214 processed_text, issues, errors = helpers.process_patterns(
215 215 text_string, 'do-not-exist')
216 216
217 217 assert processed_text == expected_text
@@ -23,7 +23,7 b' import collections'
23 23
24 24 from rhodecode.lib.partial_renderer import PyramidPartialRenderer
25 25 from rhodecode.lib.utils2 import AttributeDict
26 from rhodecode.model.db import User
26 from rhodecode.model.db import User, PullRequestReviewers
27 27 from rhodecode.model.notification import EmailNotificationModel
28 28
29 29
@@ -52,7 +52,8 b' def test_render_email(app, http_host_onl'
52 52 assert 'Email Body' in body
53 53
54 54
55 def test_render_pr_email(app, user_admin):
55 @pytest.mark.parametrize('role', PullRequestReviewers.ROLES)
56 def test_render_pr_email(app, user_admin, role):
56 57 ref = collections.namedtuple(
57 58 'Ref', 'name, type')('fxies123', 'book')
58 59
@@ -75,13 +76,17 b' def test_render_pr_email(app, user_admin'
75 76 'pull_request_source_repo_url': 'x',
76 77
77 78 'pull_request_url': 'http://localhost/pr1',
79 'user_role': role,
78 80 }
79 81
80 82 subject, body, body_plaintext = EmailNotificationModel().render_email(
81 83 EmailNotificationModel.TYPE_PULL_REQUEST, **kwargs)
82 84
83 85 # subject
84 assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
86 if role == PullRequestReviewers.ROLE_REVIEWER:
87 assert subject == '@test_admin (RhodeCode Admin) requested a pull request review. !200: "Example Pull Request"'
88 elif role == PullRequestReviewers.ROLE_OBSERVER:
89 assert subject == '@test_admin (RhodeCode Admin) added you as observer to pull request. !200: "Example Pull Request"'
85 90
86 91
87 92 def test_render_pr_update_email(app, user_admin):
@@ -122,7 +122,7 b' class TestPullRequestModel(object):'
122 122
123 123 def test_get_awaiting_my_review(self, pull_request):
124 124 PullRequestModel().update_reviewers(
125 pull_request, [(pull_request.author, ['author'], False, [])],
125 pull_request, [(pull_request.author, ['author'], False, 'reviewer', [])],
126 126 pull_request.author)
127 127 Session().commit()
128 128
@@ -133,7 +133,7 b' class TestPullRequestModel(object):'
133 133
134 134 def test_count_awaiting_my_review(self, pull_request):
135 135 PullRequestModel().update_reviewers(
136 pull_request, [(pull_request.author, ['author'], False, [])],
136 pull_request, [(pull_request.author, ['author'], False, 'reviewer', [])],
137 137 pull_request.author)
138 138 Session().commit()
139 139
@@ -43,8 +43,8 b' from rhodecode.lib.utils2 import Attribu'
43 43 from rhodecode.model.changeset_status import ChangesetStatusModel
44 44 from rhodecode.model.comment import CommentsModel
45 45 from rhodecode.model.db import (
46 PullRequest, Repository, RhodeCodeSetting, ChangesetStatus, RepoGroup,
47 UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
46 PullRequest, PullRequestReviewers, Repository, RhodeCodeSetting, ChangesetStatus,
47 RepoGroup, UserGroup, RepoRhodeCodeUi, RepoRhodeCodeSetting, RhodeCodeUi)
48 48 from rhodecode.model.meta import Session
49 49 from rhodecode.model.pull_request import PullRequestModel
50 50 from rhodecode.model.repo import RepoModel
@@ -968,7 +968,7 b' class PRTestUtility(object):'
968 968 def create_pull_request(
969 969 self, commits=None, target_head=None, source_head=None,
970 970 revisions=None, approved=False, author=None, mergeable=False,
971 enable_notifications=True, name_suffix=u'', reviewers=None,
971 enable_notifications=True, name_suffix=u'', reviewers=None, observers=None,
972 972 title=u"Test", description=u"Description"):
973 973 self.set_mergeable(mergeable)
974 974 if not enable_notifications:
@@ -1005,6 +1005,7 b' class PRTestUtility(object):'
1005 1005 'target_ref': self._default_branch_reference(target_head),
1006 1006 'revisions': [self.commit_ids[r] for r in revisions],
1007 1007 'reviewers': reviewers or self._get_reviewers(),
1008 'observers': observers or self._get_observers(),
1008 1009 'title': title,
1009 1010 'description': description,
1010 1011 }
@@ -1037,9 +1038,15 b' class PRTestUtility(object):'
1037 1038 return reference
1038 1039
1039 1040 def _get_reviewers(self):
1041 role = PullRequestReviewers.ROLE_REVIEWER
1040 1042 return [
1041 (TEST_USER_REGULAR_LOGIN, ['default1'], False, []),
1042 (TEST_USER_REGULAR2_LOGIN, ['default2'], False, []),
1043 (TEST_USER_REGULAR_LOGIN, ['default1'], False, role, []),
1044 (TEST_USER_REGULAR2_LOGIN, ['default2'], False, role, []),
1045 ]
1046
1047 def _get_observers(self):
1048 return [
1049
1043 1050 ]
1044 1051
1045 1052 def update_source_repository(self, head=None):
General Comments 0
You need to be logged in to leave comments. Login now