##// END OF EJS Templates
reviewers: add repo review rule models and expose default...
dan -
r821:618c046d default
parent child Browse files
Show More

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

1 NO CONTENT: new file 100644
The requested commit or file is too big and content was truncated. Show full diff
@@ -0,0 +1,35 b''
1 import logging
2 import datetime
3
4 from sqlalchemy import *
5 from sqlalchemy.exc import DatabaseError
6 from sqlalchemy.orm import relation, backref, class_mapper, joinedload
7 from sqlalchemy.orm.session import Session
8 from sqlalchemy.ext.declarative import declarative_base
9
10 from rhodecode.lib.dbmigrate.migrate import *
11 from rhodecode.lib.dbmigrate.migrate.changeset import *
12 from rhodecode.lib.utils2 import str2bool
13
14 from rhodecode.model.meta import Base
15 from rhodecode.model import meta
16 from rhodecode.lib.dbmigrate.versions import _reset_base, notify
17
18 log = logging.getLogger(__name__)
19
20
21 def upgrade(migrate_engine):
22 """
23 Upgrade operations go here.
24 Don't create your own engine; bind migrate_engine to your metadata
25 """
26 _reset_base(migrate_engine)
27 from rhodecode.lib.dbmigrate.schema import db_4_4_0_2
28
29 db_4_4_0_2.RepoReviewRule.__table__.create()
30 db_4_4_0_2.RepoReviewRuleUser.__table__.create()
31 db_4_4_0_2.RepoReviewRuleUserGroup.__table__.create()
32
33 def downgrade(migrate_engine):
34 meta = MetaData()
35 meta.bind = migrate_engine
@@ -51,7 +51,7 b' PYRAMID_SETTINGS = {}'
51 51 EXTENSIONS = {}
52 52
53 53 __version__ = ('.'.join((str(each) for each in VERSION[:3])))
54 __dbversion__ = 58 # defines current db version for migrations
54 __dbversion__ = 59 # defines current db version for migrations
55 55 __platform__ = platform.system()
56 56 __license__ = 'AGPLv3, and Commercial License'
57 57 __author__ = 'RhodeCode GmbH'
@@ -196,7 +196,7 b' def make_map(config):'
196 196 rmap.connect('user_autocomplete_data', '/_users', controller='home',
197 197 action='user_autocomplete_data', jsroute=True)
198 198 rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
199 action='user_group_autocomplete_data')
199 action='user_group_autocomplete_data', jsroute=True)
200 200
201 201 rmap.connect(
202 202 'user_profile', '/_profiles/{username}', controller='users',
@@ -699,6 +699,9 b' def make_map(config):'
699 699 rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
700 700 controller='summary', action='repo_refs_changelog_data',
701 701 requirements=URL_NAME_REQUIREMENTS, jsroute=True)
702 rmap.connect('repo_default_reviewers_data', '/{repo_name}/default-reviewers',
703 controller='summary', action='repo_default_reviewers_data',
704 jsroute=True, requirements=URL_NAME_REQUIREMENTS)
702 705
703 706 rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
704 707 controller='changeset', revision='tip', jsroute=True,
@@ -824,6 +827,10 b' def make_map(config):'
824 827 controller='admin/repos', action='repo_delete_svn_pattern',
825 828 conditions={'method': ['DELETE'], 'function': check_repo},
826 829 requirements=URL_NAME_REQUIREMENTS)
830 rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
831 controller='admin/repos', action='repo_settings_pullrequest',
832 conditions={'method': ['GET', 'POST'], 'function': check_repo},
833 requirements=URL_NAME_REQUIREMENTS)
827 834
828 835 # still working url for backward compat.
829 836 rmap.connect('raw_changeset_home_depraced',
@@ -286,4 +286,3 b' class HomeController(BaseController):'
286 286 _user_groups = _user_groups
287 287
288 288 return {'suggestions': _user_groups}
289
@@ -251,6 +251,16 b' class SummaryController(BaseRepoControll'
251 251 }
252 252 return data
253 253
254 @LoginRequired()
255 @HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
256 'repository.admin')
257 @jsonify
258 def repo_default_reviewers_data(self, repo_name):
259 return {
260 'reviewers': [utils.reviewer_as_json(
261 user=c.rhodecode_db_repo.user, reasons=None)]
262 }
263
254 264 @jsonify
255 265 def repo_refs_changelog_data(self, repo_name):
256 266 repo = c.rhodecode_repo
@@ -86,3 +86,21 b' def get_commit_from_ref_name(repo, ref_n'
86 86 '%s "%s" does not exist' % (ref_type, ref_name))
87 87
88 88 return repo_scm.get_commit(commit_id)
89
90
91 def reviewer_as_json(user, reasons):
92 """
93 Returns json struct of a reviewer for frontend
94
95 :param user: the reviewer
96 :param reasons: list of strings of why they are reviewers
97 """
98
99 return {
100 'user_id': user.user_id,
101 'reasons': reasons,
102 'username': user.username,
103 'firstname': user.firstname,
104 'lastname': user.lastname,
105 'gravatar_link': h.gravatar_url(user.email, 14),
106 }
@@ -893,3 +893,44 b' def get_routes_generator_for_server_url('
893 893 environ['wsgi.url_scheme'] = 'https'
894 894
895 895 return routes.util.URLGenerator(rhodecode.CONFIG['routes.map'], environ)
896
897
898 def glob2re(pat):
899 """
900 Translate a shell PATTERN to a regular expression.
901
902 There is no way to quote meta-characters.
903 """
904
905 i, n = 0, len(pat)
906 res = ''
907 while i < n:
908 c = pat[i]
909 i = i+1
910 if c == '*':
911 #res = res + '.*'
912 res = res + '[^/]*'
913 elif c == '?':
914 #res = res + '.'
915 res = res + '[^/]'
916 elif c == '[':
917 j = i
918 if j < n and pat[j] == '!':
919 j = j+1
920 if j < n and pat[j] == ']':
921 j = j+1
922 while j < n and pat[j] != ']':
923 j = j+1
924 if j >= n:
925 res = res + '\\['
926 else:
927 stuff = pat[i:j].replace('\\','\\\\')
928 i = j+1
929 if stuff[0] == '!':
930 stuff = '^' + stuff[1:]
931 elif stuff[0] == '^':
932 stuff = '\\' + stuff
933 res = '%s[%s]' % (res, stuff)
934 else:
935 res = res + re.escape(c)
936 return res + '\Z(?ms)'
@@ -22,6 +22,7 b''
22 22 Database Models for RhodeCode Enterprise
23 23 """
24 24
25 import re
25 26 import os
26 27 import sys
27 28 import time
@@ -56,7 +57,8 b' from rhodecode.lib.vcs.backends.base imp'
56 57 EmptyCommit, Reference, MergeFailureReason)
57 58 from rhodecode.lib.utils2 import (
58 59 str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
59 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict)
60 time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
61 glob2re)
60 62 from rhodecode.lib.jsonalchemy import MutationObj, JsonType, JSONDict
61 63 from rhodecode.lib.ext_json import json
62 64 from rhodecode.lib.caching_query import FromCache
@@ -3514,3 +3516,125 b' class Integration(Base, BaseModel):'
3514 3516
3515 3517 def __repr__(self):
3516 3518 return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
3519
3520
3521 class RepoReviewRuleUser(Base, BaseModel):
3522 __tablename__ = 'repo_review_rules_users'
3523 __table_args__ = (
3524 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3525 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3526 )
3527 repo_review_rule_user_id = Column(
3528 'repo_review_rule_user_id', Integer(), primary_key=True)
3529 repo_review_rule_id = Column("repo_review_rule_id",
3530 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3531 user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
3532 nullable=False)
3533 user = relationship('User')
3534
3535
3536 class RepoReviewRuleUserGroup(Base, BaseModel):
3537 __tablename__ = 'repo_review_rules_users_groups'
3538 __table_args__ = (
3539 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3540 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3541 )
3542 repo_review_rule_users_group_id = Column(
3543 'repo_review_rule_users_group_id', Integer(), primary_key=True)
3544 repo_review_rule_id = Column("repo_review_rule_id",
3545 Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
3546 users_group_id = Column("users_group_id", Integer(),
3547 ForeignKey('users_groups.users_group_id'), nullable=False)
3548 users_group = relationship('UserGroup')
3549
3550
3551 class RepoReviewRule(Base, BaseModel):
3552 __tablename__ = 'repo_review_rules'
3553 __table_args__ = (
3554 {'extend_existing': True, 'mysql_engine': 'InnoDB',
3555 'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
3556 )
3557
3558 repo_review_rule_id = Column(
3559 'repo_review_rule_id', Integer(), primary_key=True)
3560 repo_id = Column(
3561 "repo_id", Integer(), ForeignKey('repositories.repo_id'))
3562 repo = relationship('Repository', backref='review_rules')
3563
3564 _branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3565 default=u'*') # glob
3566 _file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
3567 default=u'*') # glob
3568
3569 use_authors_for_review = Column("use_authors_for_review", Boolean(),
3570 nullable=False, default=False)
3571 rule_users = relationship('RepoReviewRuleUser')
3572 rule_user_groups = relationship('RepoReviewRuleUserGroup')
3573
3574 @hybrid_property
3575 def branch_pattern(self):
3576 return self._branch_pattern or '*'
3577
3578 def _validate_glob(self, value):
3579 re.compile('^' + glob2re(value) + '$')
3580
3581 @branch_pattern.setter
3582 def branch_pattern(self, value):
3583 self._validate_glob(value)
3584 self._branch_pattern = value or '*'
3585
3586 @hybrid_property
3587 def file_pattern(self):
3588 return self._file_pattern or '*'
3589
3590 @file_pattern.setter
3591 def file_pattern(self, value):
3592 self._validate_glob(value)
3593 self._file_pattern = value or '*'
3594
3595 def matches(self, branch, files_changed):
3596 """
3597 Check if this review rule matches a branch/files in a pull request
3598
3599 :param branch: branch name for the commit
3600 :param files_changed: list of file paths changed in the pull request
3601 """
3602
3603 branch = branch or ''
3604 files_changed = files_changed or []
3605
3606 branch_matches = True
3607 if branch:
3608 branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
3609 branch_matches = bool(branch_regex.search(branch))
3610
3611 files_matches = True
3612 if self.file_pattern != '*':
3613 files_matches = False
3614 file_regex = re.compile(glob2re(self.file_pattern))
3615 for filename in files_changed:
3616 if file_regex.search(filename):
3617 files_matches = True
3618 break
3619
3620 return branch_matches and files_matches
3621
3622 @property
3623 def review_users(self):
3624 """ Returns the users which this rule applies to """
3625
3626 users = set()
3627 users |= set([
3628 rule_user.user for rule_user in self.rule_users
3629 if rule_user.user.active])
3630 users |= set(
3631 member.user
3632 for rule_user_group in self.rule_user_groups
3633 for member in rule_user_group.users_group.members
3634 if member.user.active
3635 )
3636 return users
3637
3638 def __repr__(self):
3639 return '<RepoReviewerRule(id=%r, repo=%r)>' % (
3640 self.repo_review_rule_id, self.repo)
@@ -1,9 +1,11 b''
1 1 import os
2 import re
2 3
3 4 import ipaddress
4 5 import colander
5 6
6 7 from rhodecode.translation import _
8 from rhodecode.lib.utils2 import glob2re
7 9
8 10
9 11 def ip_addr_validator(node, value):
@@ -13,3 +15,12 b' def ip_addr_validator(node, value):'
13 15 except ValueError:
14 16 msg = _(u'Please enter a valid IPv4 or IpV6 address')
15 17 raise colander.Invalid(node, msg)
18
19
20 def glob_validator(node, value):
21 try:
22 re.compile('^' + glob2re(value) + '$')
23 except Exception:
24 raise
25 msg = _(u'Invalid glob pattern')
26 raise colander.Invalid(node, msg)
@@ -90,28 +90,50 b''
90 90 height: 40px;
91 91 }
92 92
93 .deform-two-field-sequence .deform-seq-container .deform-seq-item label {
94 display: none;
95 }
96 .deform-two-field-sequence .deform-seq-container .deform-seq-item:first-child label {
97 display: block;
98 }
99 .deform-two-field-sequence .deform-seq-container .deform-seq-item .panel-heading {
100 display: none;
101 }
102 .deform-two-field-sequence .deform-seq-container .deform-seq-item.form-group {
103 margin: 0;
104 }
105 .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group .form-group {
106 width: 45%; padding: 0 2px; float: left; clear: none;
107 }
108 .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group > .panel {
109 padding: 0;
110 margin: 5px 0;
111 border: none;
112 }
113 .deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group > .panel > .panel-body {
114 padding: 0;
93 .deform-full-field-sequence.control-inputs {
94 width: 100%;
115 95 }
116 96
97 .deform-table-sequence {
98 .deform-seq-container {
99 .deform-seq-item {
100 margin: 0;
101 label {
102 display: none;
103 }
104 .panel-heading {
105 display: none;
106 }
107 .deform-seq-item-group > .panel {
108 padding: 0;
109 margin: 5px 0;
110 border: none;
111 &> .panel-body {
112 padding: 0;
113 }
114 }
115 &:first-child label {
116 display: block;
117 }
118 }
119 }
120 }
121 .deform-table-2-sequence {
122 .deform-seq-container {
123 .deform-seq-item {
124 .form-group {
125 width: 45% !important; padding: 0 2px; float: left; clear: none;
126 }
127 }
128 }
129 }
130 .deform-table-3-sequence {
131 .deform-seq-container {
132 .deform-seq-item {
133 .form-group {
134 width: 30% !important; padding: 0 2px; float: left; clear: none;
135 }
136 }
137 }
138 }
117 139 }
@@ -14,6 +14,7 b' function registerRCRoutes() {'
14 14 // routes registration
15 15 pyroutes.register('home', '/', []);
16 16 pyroutes.register('user_autocomplete_data', '/_users', []);
17 pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
17 18 pyroutes.register('new_repo', '/_admin/create_repository', []);
18 19 pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
19 20 pyroutes.register('gists', '/_admin/gists', []);
@@ -22,6 +23,7 b' function registerRCRoutes() {'
22 23 pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
23 24 pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
24 25 pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
26 pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/default-reviewers', ['repo_name']);
25 27 pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
26 28 pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
27 29 pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);
@@ -40,14 +40,23 b' var removeReviewMember = function(review'
40 40 }
41 41 };
42 42
43 var addReviewMember = function(id,fname,lname,nname,gravatar_link){
43 var addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons) {
44 44 var members = $('#review_members').get(0);
45 var reasons_html = '';
46 if (reasons) {
47 for (var i = 0; i < reasons.length; i++) {
48 reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(
49 reasons[i]
50 );
51 }
52 }
45 53 var tmpl = '<li id="reviewer_{2}">'+
46 54 '<div class="reviewer_status">'+
47 55 '<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
48 56 '</div>'+
49 57 '<img alt="gravatar" class="gravatar" src="{0}"/>'+
50 58 '<span class="reviewer_name user">{1}</span>'+
59 reasons_html +
51 60 '<input type="hidden" value="{2}" name="review_members" />'+
52 61 '<div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">' +
53 62 '<i class="icon-remove-sign"></i>'+
@@ -71,6 +71,22 b''
71 71 <li class="${'active' if c.active=='integrations' else ''}">
72 72 <a href="${h.route_path('repo_integrations_home', repo_name=c.repo_name)}">${_('Integrations')}</a>
73 73 </li>
74 ## TODO: dan: replace repo navigation with navlist registry like with
75 ## admin menu. First must find way to allow runtime configuration
76 ## it to account for the c.repo_info.repo_type != 'svn' call above
77 <%
78 reviewer_settings = False
79 try:
80 import rc_reviewers
81 reviewer_settings = True
82 except ImportError:
83 pass
84 %>
85 %if reviewer_settings:
86 <li class="${'active' if c.active=='reviewers' else ''}">
87 <a href="${h.route_path('repo_reviewers_home', repo_name=c.repo_name)}">${_('Reviewers')}</a>
88 </li>
89 %endif
74 90 </ul>
75 91 </div>
76 92
@@ -439,13 +439,6 b''
439 439 };
440 440
441 441 var targetRepoChanged = function(repoData) {
442 // reset && add the reviewer based on selected repo
443 $('#review_members').html('');
444 addReviewMember(
445 repoData.user.user_id, repoData.user.firstname,
446 repoData.user.lastname, repoData.user.username,
447 repoData.user.gravatar_link);
448
449 442 // generate new DESC of target repo displayed next to select
450 443 $('#target_repo_desc').html(
451 444 "<strong>${_('Destination repository')}</strong>: {0}".format(repoData['description'])
@@ -488,10 +481,12 b''
488 481
489 482 $sourceRef.on('change', function(e){
490 483 loadRepoRefDiffPreview();
484 loadDefaultReviewers();
491 485 });
492 486
493 487 $targetRef.on('change', function(e){
494 488 loadRepoRefDiffPreview();
489 loadDefaultReviewers();
495 490 });
496 491
497 492 $targetRepo.on('change', function(e){
@@ -518,6 +513,36 b''
518 513
519 514 });
520 515
516 var loadDefaultReviewers = function() {
517 if (loadDefaultReviewers._currentRequest) {
518 loadDefaultReviewers._currentRequest.abort();
519 }
520 var url = pyroutes.url('repo_default_reviewers_data', {'repo_name': targetRepoName});
521
522 var sourceRepo = $sourceRepo.eq(0).val();
523 var sourceRef = $sourceRef.eq(0).val().split(':');
524 var targetRepo = $targetRepo.eq(0).val();
525 var targetRef = $targetRef.eq(0).val().split(':');
526 url += '?source_repo=' + sourceRepo;
527 url += '&source_ref=' + sourceRef[2];
528 url += '&target_repo=' + targetRepo;
529 url += '&target_ref=' + targetRef[2];
530
531 loadDefaultReviewers._currentRequest = $.get(url)
532 .done(function(data) {
533 loadDefaultReviewers._currentRequest = null;
534
535 // reset && add the reviewer based on selected repo
536 $('#review_members').html('');
537 for (var i = 0; i < data.reviewers.length; i++) {
538 var reviewer = data.reviewers[i];
539 addReviewMember(
540 reviewer.user_id, reviewer.firstname,
541 reviewer.lastname, reviewer.username,
542 reviewer.gravatar_link, reviewer.reasons);
543 }
544 });
545 };
521 546 prButtonLock(true, "${_('Please select origin and destination')}");
522 547
523 548 // auto-load on init, the target refs select2
@@ -532,6 +557,7 b''
532 557 // in case we have a pre-selected value, use it now
533 558 $sourceRef.select2('val', '${c.default_source_ref}');
534 559 loadRepoRefDiffPreview();
560 loadDefaultReviewers();
535 561 %endif
536 562
537 563 ReviewerAutoComplete('user');
General Comments 0
You need to be logged in to leave comments. Login now