reviewers: add repo review rule models and expose default

reviewers route for pull requests via ajax, refs #1131
This commit is contained in:
Daniel Dourvaris 2016-09-06 16:02:35 +03:00
parent f28e511072
commit 9b38ba8c8f
15 changed files with 3994 additions and 34 deletions

View file

@ -51,7 +51,7 @@ PYRAMID_SETTINGS = {}
EXTENSIONS = {}
__version__ = ('.'.join((str(each) for each in VERSION[:3])))
__dbversion__ = 58 # defines current db version for migrations
__dbversion__ = 59 # defines current db version for migrations
__platform__ = platform.system()
__license__ = 'AGPLv3, and Commercial License'
__author__ = 'RhodeCode GmbH'

View file

@ -196,7 +196,7 @@ def make_map(config):
rmap.connect('user_autocomplete_data', '/_users', controller='home',
action='user_autocomplete_data', jsroute=True)
rmap.connect('user_group_autocomplete_data', '/_user_groups', controller='home',
action='user_group_autocomplete_data')
action='user_group_autocomplete_data', jsroute=True)
rmap.connect(
'user_profile', '/_profiles/{username}', controller='users',
@ -699,6 +699,9 @@ def make_map(config):
rmap.connect('repo_refs_changelog_data', '/{repo_name}/refs-data-changelog',
controller='summary', action='repo_refs_changelog_data',
requirements=URL_NAME_REQUIREMENTS, jsroute=True)
rmap.connect('repo_default_reviewers_data', '/{repo_name}/default-reviewers',
controller='summary', action='repo_default_reviewers_data',
jsroute=True, requirements=URL_NAME_REQUIREMENTS)
rmap.connect('changeset_home', '/{repo_name}/changeset/{revision}',
controller='changeset', revision='tip', jsroute=True,
@ -824,6 +827,10 @@ def make_map(config):
controller='admin/repos', action='repo_delete_svn_pattern',
conditions={'method': ['DELETE'], 'function': check_repo},
requirements=URL_NAME_REQUIREMENTS)
rmap.connect('repo_pullrequest_settings', '/{repo_name}/settings/pullrequest',
controller='admin/repos', action='repo_settings_pullrequest',
conditions={'method': ['GET', 'POST'], 'function': check_repo},
requirements=URL_NAME_REQUIREMENTS)
# still working url for backward compat.
rmap.connect('raw_changeset_home_depraced',

View file

@ -286,4 +286,3 @@ class HomeController(BaseController):
_user_groups = _user_groups
return {'suggestions': _user_groups}

View file

@ -251,6 +251,16 @@ class SummaryController(BaseRepoController):
}
return data
@LoginRequired()
@HasRepoPermissionAnyDecorator('repository.read', 'repository.write',
'repository.admin')
@jsonify
def repo_default_reviewers_data(self, repo_name):
return {
'reviewers': [utils.reviewer_as_json(
user=c.rhodecode_db_repo.user, reasons=None)]
}
@jsonify
def repo_refs_changelog_data(self, repo_name):
repo = c.rhodecode_repo

View file

@ -86,3 +86,21 @@ def get_commit_from_ref_name(repo, ref_name, ref_type=None):
'%s "%s" does not exist' % (ref_type, ref_name))
return repo_scm.get_commit(commit_id)
def reviewer_as_json(user, reasons):
"""
Returns json struct of a reviewer for frontend
:param user: the reviewer
:param reasons: list of strings of why they are reviewers
"""
return {
'user_id': user.user_id,
'reasons': reasons,
'username': user.username,
'firstname': user.firstname,
'lastname': user.lastname,
'gravatar_link': h.gravatar_url(user.email, 14),
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,35 @@
import logging
import datetime
from sqlalchemy import *
from sqlalchemy.exc import DatabaseError
from sqlalchemy.orm import relation, backref, class_mapper, joinedload
from sqlalchemy.orm.session import Session
from sqlalchemy.ext.declarative import declarative_base
from rhodecode.lib.dbmigrate.migrate import *
from rhodecode.lib.dbmigrate.migrate.changeset import *
from rhodecode.lib.utils2 import str2bool
from rhodecode.model.meta import Base
from rhodecode.model import meta
from rhodecode.lib.dbmigrate.versions import _reset_base, notify
log = logging.getLogger(__name__)
def upgrade(migrate_engine):
"""
Upgrade operations go here.
Don't create your own engine; bind migrate_engine to your metadata
"""
_reset_base(migrate_engine)
from rhodecode.lib.dbmigrate.schema import db_4_4_0_2
db_4_4_0_2.RepoReviewRule.__table__.create()
db_4_4_0_2.RepoReviewRuleUser.__table__.create()
db_4_4_0_2.RepoReviewRuleUserGroup.__table__.create()
def downgrade(migrate_engine):
meta = MetaData()
meta.bind = migrate_engine

View file

@ -893,3 +893,44 @@ def get_routes_generator_for_server_url(server_url):
environ['wsgi.url_scheme'] = 'https'
return routes.util.URLGenerator(rhodecode.CONFIG['routes.map'], environ)
def glob2re(pat):
"""
Translate a shell PATTERN to a regular expression.
There is no way to quote meta-characters.
"""
i, n = 0, len(pat)
res = ''
while i < n:
c = pat[i]
i = i+1
if c == '*':
#res = res + '.*'
res = res + '[^/]*'
elif c == '?':
#res = res + '.'
res = res + '[^/]'
elif c == '[':
j = i
if j < n and pat[j] == '!':
j = j+1
if j < n and pat[j] == ']':
j = j+1
while j < n and pat[j] != ']':
j = j+1
if j >= n:
res = res + '\\['
else:
stuff = pat[i:j].replace('\\','\\\\')
i = j+1
if stuff[0] == '!':
stuff = '^' + stuff[1:]
elif stuff[0] == '^':
stuff = '\\' + stuff
res = '%s[%s]' % (res, stuff)
else:
res = res + re.escape(c)
return res + '\Z(?ms)'

View file

@ -22,6 +22,7 @@
Database Models for RhodeCode Enterprise
"""
import re
import os
import sys
import time
@ -56,7 +57,8 @@ from rhodecode.lib.vcs.backends.base import (
EmptyCommit, Reference, MergeFailureReason)
from rhodecode.lib.utils2 import (
str2bool, safe_str, get_commit_safe, safe_unicode, remove_prefix, md5_safe,
time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict)
time_to_datetime, aslist, Optional, safe_int, get_clone_url, AttributeDict,
glob2re)
from rhodecode.lib.jsonalchemy import MutationObj, JsonType, JSONDict
from rhodecode.lib.ext_json import json
from rhodecode.lib.caching_query import FromCache
@ -3514,3 +3516,125 @@ class Integration(Base, BaseModel):
def __repr__(self):
return '<Integration(%r, %r)>' % (self.integration_type, self.scope)
class RepoReviewRuleUser(Base, BaseModel):
__tablename__ = 'repo_review_rules_users'
__table_args__ = (
{'extend_existing': True, 'mysql_engine': 'InnoDB',
'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
)
repo_review_rule_user_id = Column(
'repo_review_rule_user_id', Integer(), primary_key=True)
repo_review_rule_id = Column("repo_review_rule_id",
Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
user_id = Column("user_id", Integer(), ForeignKey('users.user_id'),
nullable=False)
user = relationship('User')
class RepoReviewRuleUserGroup(Base, BaseModel):
__tablename__ = 'repo_review_rules_users_groups'
__table_args__ = (
{'extend_existing': True, 'mysql_engine': 'InnoDB',
'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
)
repo_review_rule_users_group_id = Column(
'repo_review_rule_users_group_id', Integer(), primary_key=True)
repo_review_rule_id = Column("repo_review_rule_id",
Integer(), ForeignKey('repo_review_rules.repo_review_rule_id'))
users_group_id = Column("users_group_id", Integer(),
ForeignKey('users_groups.users_group_id'), nullable=False)
users_group = relationship('UserGroup')
class RepoReviewRule(Base, BaseModel):
__tablename__ = 'repo_review_rules'
__table_args__ = (
{'extend_existing': True, 'mysql_engine': 'InnoDB',
'mysql_charset': 'utf8', 'sqlite_autoincrement': True,}
)
repo_review_rule_id = Column(
'repo_review_rule_id', Integer(), primary_key=True)
repo_id = Column(
"repo_id", Integer(), ForeignKey('repositories.repo_id'))
repo = relationship('Repository', backref='review_rules')
_branch_pattern = Column("branch_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
default=u'*') # glob
_file_pattern = Column("file_pattern", UnicodeText().with_variant(UnicodeText(255), 'mysql'),
default=u'*') # glob
use_authors_for_review = Column("use_authors_for_review", Boolean(),
nullable=False, default=False)
rule_users = relationship('RepoReviewRuleUser')
rule_user_groups = relationship('RepoReviewRuleUserGroup')
@hybrid_property
def branch_pattern(self):
return self._branch_pattern or '*'
def _validate_glob(self, value):
re.compile('^' + glob2re(value) + '$')
@branch_pattern.setter
def branch_pattern(self, value):
self._validate_glob(value)
self._branch_pattern = value or '*'
@hybrid_property
def file_pattern(self):
return self._file_pattern or '*'
@file_pattern.setter
def file_pattern(self, value):
self._validate_glob(value)
self._file_pattern = value or '*'
def matches(self, branch, files_changed):
"""
Check if this review rule matches a branch/files in a pull request
:param branch: branch name for the commit
:param files_changed: list of file paths changed in the pull request
"""
branch = branch or ''
files_changed = files_changed or []
branch_matches = True
if branch:
branch_regex = re.compile('^' + glob2re(self.branch_pattern) + '$')
branch_matches = bool(branch_regex.search(branch))
files_matches = True
if self.file_pattern != '*':
files_matches = False
file_regex = re.compile(glob2re(self.file_pattern))
for filename in files_changed:
if file_regex.search(filename):
files_matches = True
break
return branch_matches and files_matches
@property
def review_users(self):
""" Returns the users which this rule applies to """
users = set()
users |= set([
rule_user.user for rule_user in self.rule_users
if rule_user.user.active])
users |= set(
member.user
for rule_user_group in self.rule_user_groups
for member in rule_user_group.users_group.members
if member.user.active
)
return users
def __repr__(self):
return '<RepoReviewerRule(id=%r, repo=%r)>' % (
self.repo_review_rule_id, self.repo)

View file

@ -1,9 +1,11 @@
import os
import re
import ipaddress
import colander
from rhodecode.translation import _
from rhodecode.lib.utils2 import glob2re
def ip_addr_validator(node, value):
@ -13,3 +15,12 @@ def ip_addr_validator(node, value):
except ValueError:
msg = _(u'Please enter a valid IPv4 or IpV6 address')
raise colander.Invalid(node, msg)
def glob_validator(node, value):
try:
re.compile('^' + glob2re(value) + '$')
except Exception:
raise
msg = _(u'Invalid glob pattern')
raise colander.Invalid(node, msg)

View file

@ -90,28 +90,50 @@
height: 40px;
}
.deform-two-field-sequence .deform-seq-container .deform-seq-item label {
display: none;
}
.deform-two-field-sequence .deform-seq-container .deform-seq-item:first-child label {
display: block;
}
.deform-two-field-sequence .deform-seq-container .deform-seq-item .panel-heading {
display: none;
}
.deform-two-field-sequence .deform-seq-container .deform-seq-item.form-group {
margin: 0;
}
.deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group .form-group {
width: 45%; padding: 0 2px; float: left; clear: none;
}
.deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group > .panel {
padding: 0;
margin: 5px 0;
border: none;
}
.deform-two-field-sequence .deform-seq-container .deform-seq-item .deform-seq-item-group > .panel > .panel-body {
padding: 0;
.deform-full-field-sequence.control-inputs {
width: 100%;
}
.deform-table-sequence {
.deform-seq-container {
.deform-seq-item {
margin: 0;
label {
display: none;
}
.panel-heading {
display: none;
}
.deform-seq-item-group > .panel {
padding: 0;
margin: 5px 0;
border: none;
&> .panel-body {
padding: 0;
}
}
&:first-child label {
display: block;
}
}
}
}
.deform-table-2-sequence {
.deform-seq-container {
.deform-seq-item {
.form-group {
width: 45% !important; padding: 0 2px; float: left; clear: none;
}
}
}
}
.deform-table-3-sequence {
.deform-seq-container {
.deform-seq-item {
.form-group {
width: 30% !important; padding: 0 2px; float: left; clear: none;
}
}
}
}
}

View file

@ -14,6 +14,7 @@ function registerRCRoutes() {
// routes registration
pyroutes.register('home', '/', []);
pyroutes.register('user_autocomplete_data', '/_users', []);
pyroutes.register('user_group_autocomplete_data', '/_user_groups', []);
pyroutes.register('new_repo', '/_admin/create_repository', []);
pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']);
pyroutes.register('gists', '/_admin/gists', []);
@ -22,6 +23,7 @@ function registerRCRoutes() {
pyroutes.register('repo_stats', '/%(repo_name)s/repo_stats/%(commit_id)s', ['repo_name', 'commit_id']);
pyroutes.register('repo_refs_data', '/%(repo_name)s/refs-data', ['repo_name']);
pyroutes.register('repo_refs_changelog_data', '/%(repo_name)s/refs-data-changelog', ['repo_name']);
pyroutes.register('repo_default_reviewers_data', '/%(repo_name)s/default-reviewers', ['repo_name']);
pyroutes.register('changeset_home', '/%(repo_name)s/changeset/%(revision)s', ['repo_name', 'revision']);
pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']);
pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']);

View file

@ -40,14 +40,23 @@ var removeReviewMember = function(reviewer_id, mark_delete){
}
};
var addReviewMember = function(id,fname,lname,nname,gravatar_link){
var addReviewMember = function(id, fname, lname, nname, gravatar_link, reasons) {
var members = $('#review_members').get(0);
var reasons_html = '';
if (reasons) {
for (var i = 0; i < reasons.length; i++) {
reasons_html += '<div class="reviewer_reason">- {0}</div>'.format(
reasons[i]
);
}
}
var tmpl = '<li id="reviewer_{2}">'+
'<div class="reviewer_status">'+
'<div class="flag_status not_reviewed pull-left reviewer_member_status"></div>'+
'</div>'+
'<img alt="gravatar" class="gravatar" src="{0}"/>'+
'<span class="reviewer_name user">{1}</span>'+
reasons_html +
'<input type="hidden" value="{2}" name="review_members" />'+
'<div class="reviewer_member_remove action_button" onclick="removeReviewMember({2})">' +
'<i class="icon-remove-sign"></i>'+

View file

@ -71,6 +71,22 @@
<li class="${'active' if c.active=='integrations' else ''}">
<a href="${h.route_path('repo_integrations_home', repo_name=c.repo_name)}">${_('Integrations')}</a>
</li>
## TODO: dan: replace repo navigation with navlist registry like with
## admin menu. First must find way to allow runtime configuration
## it to account for the c.repo_info.repo_type != 'svn' call above
<%
reviewer_settings = False
try:
import rc_reviewers
reviewer_settings = True
except ImportError:
pass
%>
%if reviewer_settings:
<li class="${'active' if c.active=='reviewers' else ''}">
<a href="${h.route_path('repo_reviewers_home', repo_name=c.repo_name)}">${_('Reviewers')}</a>
</li>
%endif
</ul>
</div>

View file

@ -439,13 +439,6 @@ $(function(){
};
var targetRepoChanged = function(repoData) {
// reset && add the reviewer based on selected repo
$('#review_members').html('');
addReviewMember(
repoData.user.user_id, repoData.user.firstname,
repoData.user.lastname, repoData.user.username,
repoData.user.gravatar_link);
// generate new DESC of target repo displayed next to select
$('#target_repo_desc').html(
"<strong>${_('Destination repository')}</strong>: {0}".format(repoData['description'])
@ -488,10 +481,12 @@ $(function(){
$sourceRef.on('change', function(e){
loadRepoRefDiffPreview();
loadDefaultReviewers();
});
$targetRef.on('change', function(e){
loadRepoRefDiffPreview();
loadDefaultReviewers();
});
$targetRepo.on('change', function(e){
@ -518,6 +513,36 @@ $(function(){
});
var loadDefaultReviewers = function() {
if (loadDefaultReviewers._currentRequest) {
loadDefaultReviewers._currentRequest.abort();
}
var url = pyroutes.url('repo_default_reviewers_data', {'repo_name': targetRepoName});
var sourceRepo = $sourceRepo.eq(0).val();
var sourceRef = $sourceRef.eq(0).val().split(':');
var targetRepo = $targetRepo.eq(0).val();
var targetRef = $targetRef.eq(0).val().split(':');
url += '?source_repo=' + sourceRepo;
url += '&source_ref=' + sourceRef[2];
url += '&target_repo=' + targetRepo;
url += '&target_ref=' + targetRef[2];
loadDefaultReviewers._currentRequest = $.get(url)
.done(function(data) {
loadDefaultReviewers._currentRequest = null;
// reset && add the reviewer based on selected repo
$('#review_members').html('');
for (var i = 0; i < data.reviewers.length; i++) {
var reviewer = data.reviewers[i];
addReviewMember(
reviewer.user_id, reviewer.firstname,
reviewer.lastname, reviewer.username,
reviewer.gravatar_link, reviewer.reasons);
}
});
};
prButtonLock(true, "${_('Please select origin and destination')}");
// auto-load on init, the target refs select2
@ -532,6 +557,7 @@ $(function(){
// in case we have a pre-selected value, use it now
$sourceRef.select2('val', '${c.default_source_ref}');
loadRepoRefDiffPreview();
loadDefaultReviewers();
%endif
ReviewerAutoComplete('user');