merge: merged default changes into new-ui

This commit is contained in:
Marcin Kuzminski 2019-04-19 13:21:58 +02:00
commit 442096cbde
31 changed files with 698 additions and 208 deletions

View file

@ -0,0 +1,41 @@
|RCE| 4.16.2 |RNS|
------------------
Release Date
^^^^^^^^^^^^
- 2019-04-02
New Features
^^^^^^^^^^^^
General
^^^^^^^
Security
^^^^^^^^
Performance
^^^^^^^^^^^
Fixes
^^^^^
- Integrations: fixed missing template variable for fork reference checks.
- Permissions: fixed server error when showing permissions for user groups.
- Pull requests: fixed a bug in removal of multiple reviewers at once.
Upgrade notes
^^^^^^^^^^^^^
- Scheduled release addressing problems in 4.16.X releases.

View file

@ -9,6 +9,7 @@ Release Notes
.. toctree::
:maxdepth: 1
release-notes-4.16.2.rst
release-notes-4.16.1.rst
release-notes-4.16.0.rst
release-notes-4.15.2.rst

View file

@ -5,7 +5,7 @@
self: super: {
"alembic" = super.buildPythonPackage {
name = "alembic-1.0.8";
name = "alembic-1.0.9";
doCheck = false;
propagatedBuildInputs = [
self."sqlalchemy"
@ -14,8 +14,8 @@ self: super: {
self."python-dateutil"
];
src = fetchurl {
url = "https://files.pythonhosted.org/packages/d6/bb/ec1e21f2e303689ad2170eb47fc67df9ad4199ade6759a99474c4d3535c8/alembic-1.0.8.tar.gz";
sha256 = "1s34i1j0dsxbflxligwhnkf37a5hvcshsv8ibkcfdjf03ph42pah";
url = "https://files.pythonhosted.org/packages/fc/42/8729e2491fa9b8eae160d1cbb429f61712bfc2d779816488c25cfdabf7b8/alembic-1.0.9.tar.gz";
sha256 = "0a88rwp7fp0y8ykczj82ivr4ww1kiflcvb882lgfl9azm8csdfa0";
};
meta = {
license = [ pkgs.lib.licenses.mit ];
@ -310,11 +310,11 @@ self: super: {
};
};
"coverage" = super.buildPythonPackage {
name = "coverage-4.5.1";
name = "coverage-4.5.3";
doCheck = false;
src = fetchurl {
url = "https://files.pythonhosted.org/packages/35/fe/e7df7289d717426093c68d156e0fd9117c8f4872b6588e8a8928a0f68424/coverage-4.5.1.tar.gz";
sha256 = "1wbrzpxka3xd4nmmkc6q0ir343d91kymwsm8pbmwa0d2a7q4ir2n";
url = "https://files.pythonhosted.org/packages/82/70/2280b5b29a0352519bb95ab0ef1ea942d40466ca71c53a2085bdeff7b0eb/coverage-4.5.3.tar.gz";
sha256 = "02f6m073qdispn96rc616hg0rnmw1pgqzw3bgxwiwza4zf9hirlx";
};
meta = {
license = [ pkgs.lib.licenses.asl20 ];
@ -1118,14 +1118,14 @@ self: super: {
};
};
"pexpect" = super.buildPythonPackage {
name = "pexpect-4.6.0";
name = "pexpect-4.7.0";
doCheck = false;
propagatedBuildInputs = [
self."ptyprocess"
];
src = fetchurl {
url = "https://files.pythonhosted.org/packages/89/43/07d07654ee3e25235d8cea4164cdee0ec39d1fda8e9203156ebe403ffda4/pexpect-4.6.0.tar.gz";
sha256 = "1fla85g47iaxxpjhp9vkxdnv4pgc7rplfy6ja491smrrk0jqi3ia";
url = "https://files.pythonhosted.org/packages/1c/b1/362a0d4235496cb42c33d1d8732b5e2c607b0129ad5fdd76f5a583b9fcb3/pexpect-4.7.0.tar.gz";
sha256 = "1sv2rri15zwhds85a4kamwh9pj49qcxv7m4miyr4jfpfwv81yb4y";
};
meta = {
license = [ pkgs.lib.licenses.isc { fullName = "ISC License (ISCL)"; } ];
@ -1160,15 +1160,15 @@ self: super: {
};
};
"plaster-pastedeploy" = super.buildPythonPackage {
name = "plaster-pastedeploy-0.6";
name = "plaster-pastedeploy-0.7";
doCheck = false;
propagatedBuildInputs = [
self."pastedeploy"
self."plaster"
];
src = fetchurl {
url = "https://files.pythonhosted.org/packages/3f/e7/6a6833158d2038ec40085433308a1e164fd1dac595513f6dd556d5669bb8/plaster_pastedeploy-0.6.tar.gz";
sha256 = "1bkggk18f4z2bmsmxyxabvf62znvjwbivzh880419r3ap0616cf2";
url = "https://files.pythonhosted.org/packages/99/69/2d3bc33091249266a1bd3cf24499e40ab31d54dffb4a7d76fe647950b98c/plaster_pastedeploy-0.7.tar.gz";
sha256 = "1zg7gcsvc1kzay1ry5p699rg2qavfsxqwl17mqxzr0gzw6j9679r";
};
meta = {
license = [ pkgs.lib.licenses.mit ];
@ -1186,15 +1186,15 @@ self: super: {
};
};
"prompt-toolkit" = super.buildPythonPackage {
name = "prompt-toolkit-1.0.15";
name = "prompt-toolkit-1.0.16";
doCheck = false;
propagatedBuildInputs = [
self."six"
self."wcwidth"
];
src = fetchurl {
url = "https://files.pythonhosted.org/packages/8a/ad/cf6b128866e78ad6d7f1dc5b7f99885fb813393d9860778b2984582e81b5/prompt_toolkit-1.0.15.tar.gz";
sha256 = "05v9h5nydljwpj5nm8n804ms0glajwfy1zagrzqrg91wk3qqi1c5";
url = "https://files.pythonhosted.org/packages/f1/03/bb36771dc9fa7553ac4bdc639a9ecdf6fda0ff4176faf940d97e3c16e41d/prompt_toolkit-1.0.16.tar.gz";
sha256 = "1d65hm6nf0cbq0q0121m60zzy4s1fpg9fn761s1yxf08dridvkn1";
};
meta = {
license = [ pkgs.lib.licenses.bsdOriginal ];
@ -1212,11 +1212,11 @@ self: super: {
};
};
"psycopg2" = super.buildPythonPackage {
name = "psycopg2-2.7.7";
name = "psycopg2-2.8.2";
doCheck = false;
src = fetchurl {
url = "https://files.pythonhosted.org/packages/63/54/c039eb0f46f9a9406b59a638415c2012ad7be9b4b97bfddb1f48c280df3a/psycopg2-2.7.7.tar.gz";
sha256 = "0zjbabb4qjx9dm07imhf8y5a9rpa06d5zah80myiimgdi83nslpl";
url = "https://files.pythonhosted.org/packages/23/7e/93c325482c328619870b6cd09370f6dbe1148283daca65115cd63642e60f/psycopg2-2.8.2.tar.gz";
sha256 = "122mn2z3r0zgs8jyq682jjjr6vq5690qmxqf22gj6g41dwdz5b2w";
};
meta = {
license = [ pkgs.lib.licenses.zpl21 { fullName = "GNU Library or Lesser General Public License (LGPL)"; } { fullName = "LGPL with exceptions or ZPL"; } ];
@ -1373,7 +1373,7 @@ self: super: {
};
};
"pyramid" = super.buildPythonPackage {
name = "pyramid-1.10.2";
name = "pyramid-1.10.4";
doCheck = false;
propagatedBuildInputs = [
self."hupper"
@ -1388,8 +1388,8 @@ self: super: {
self."repoze.lru"
];
src = fetchurl {
url = "https://files.pythonhosted.org/packages/bc/0e/73de9b189ff00a963beeedaff90e27b134eedf2806279a1a3fe122fd65b6/pyramid-1.10.2.tar.gz";
sha256 = "0gn6sw6ml67ir150ffivc0ad5hd448p43p9z2bkyp12jh2n9n2p7";
url = "https://files.pythonhosted.org/packages/c2/43/1ae701c9c6bb3a434358e678a5e72c96e8aa55cf4cb1d2fa2041b5dd38b7/pyramid-1.10.4.tar.gz";
sha256 = "0rkxs1ajycg2zh1c94xlmls56mx5m161sn8112skj0amza6cn36q";
};
meta = {
license = [ { fullName = "Repoze Public License"; } { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];
@ -1931,11 +1931,11 @@ self: super: {
};
};
"setuptools" = super.buildPythonPackage {
name = "setuptools-40.8.0";
name = "setuptools-41.0.0";
doCheck = false;
src = fetchurl {
url = "https://files.pythonhosted.org/packages/c2/f7/c7b501b783e5a74cf1768bc174ee4fb0a8a6ee5af6afa92274ff964703e0/setuptools-40.8.0.zip";
sha256 = "0k9hifpgahnw2a26w3cr346iy733k6d3nwh3f7g9m13y6f8fqkkf";
url = "https://files.pythonhosted.org/packages/ed/69/c805067de1feedbb98c53174b0f2df44cc05e0e9ee73bb85eebc59e508c6/setuptools-41.0.0.zip";
sha256 = "1cfwy2g23qj3262ivj0b1182lgwz7bqqbka35rkqwypynra05lvr";
};
meta = {
license = [ pkgs.lib.licenses.mit ];
@ -2012,14 +2012,14 @@ self: super: {
};
};
"supervisor" = super.buildPythonPackage {
name = "supervisor-3.3.5";
name = "supervisor-4.0.1";
doCheck = false;
propagatedBuildInputs = [
self."meld3"
];
src = fetchurl {
url = "https://files.pythonhosted.org/packages/ba/65/92575a8757ed576beaee59251f64a3287bde82bdc03964b89df9e1d29e1b/supervisor-3.3.5.tar.gz";
sha256 = "1w3ahridzbc6rxfpbyx8lij6pjlcgf2ymzyg53llkjqxalp6sk8v";
url = "https://files.pythonhosted.org/packages/96/ec/f8190beeb0c6d29a30aea10389c11d0164b6ff221931ee84093315ecde6a/supervisor-4.0.1.tar.gz";
sha256 = "10l3z7v6v1fyv7m5zbazzxciwvli2n9a41pxi27p4kixgsfp0s1j";
};
meta = {
license = [ { fullName = "BSD-derived (http://www.repoze.org/LICENSE.txt)"; } ];

View file

@ -47,7 +47,7 @@ pyparsing==2.3.0
pyramid-beaker==0.8
pyramid-debugtoolbar==4.5.0
pyramid-mako==1.0.2
pyramid==1.10.2
pyramid==1.10.4
pyramid_mailer==0.15.1
python-dateutil
python-ldap==3.1.0
@ -67,7 +67,7 @@ six==1.11.0
sqlalchemy==1.1.18
sshpubkeys==3.1.0
subprocess32==3.5.3
supervisor==3.3.5
supervisor==4.0.1
translationstring==1.3
urllib3==1.24.1
urlobject==2.4.3
@ -87,7 +87,7 @@ zope.interface==4.6.0
mysql-python==1.2.5
pymysql==0.8.1
pysqlite==2.8.3
psycopg2==2.7.7
psycopg2==2.8.2
# IPYTHON RENDERING
# entrypoints backport, pypi version doesn't support egg installs
@ -97,7 +97,7 @@ nbformat==4.4.0
jupyter_client==5.0.0
## cli tools
alembic==1.0.8
alembic==1.0.9
invoke==0.13.0
bumpversion==0.5.3

View file

@ -10,7 +10,7 @@ gprof2dot==2017.9.19
mock==1.0.1
cov-core==1.15.0
coverage==4.5.1
coverage==4.5.3
webtest==2.0.33
beautifulsoup4==4.6.3

View file

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

View file

@ -423,6 +423,10 @@ def admin_routes(config):
name='repo_groups',
pattern='/repo_groups')
config.add_route(
name='repo_groups_data',
pattern='/repo_groups_data')
config.add_route(
name='repo_group_new',
pattern='/repo_group/new')

View file

@ -23,11 +23,11 @@ import pytest
from rhodecode.apps._base import ADMIN_PREFIX
from rhodecode.lib import helpers as h
from rhodecode.model.db import Repository, UserRepoToPerm, User
from rhodecode.model.db import Repository, UserRepoToPerm, User, RepoGroup
from rhodecode.model.meta import Session
from rhodecode.model.repo_group import RepoGroupModel
from rhodecode.tests import (
assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH, TestController)
assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH)
from rhodecode.tests.fixture import Fixture
fixture = Fixture()
@ -38,6 +38,7 @@ def route_path(name, params=None, **kwargs):
base_url = {
'repo_groups': ADMIN_PREFIX + '/repo_groups',
'repo_groups_data': ADMIN_PREFIX + '/repo_groups_data',
'repo_group_new': ADMIN_PREFIX + '/repo_group/new',
'repo_group_create': ADMIN_PREFIX + '/repo_group/create',
@ -59,13 +60,30 @@ def _get_permission_for_user(user, repo):
@pytest.mark.usefixtures("app")
class TestAdminRepositoryGroups(object):
def test_show_repo_groups(self, autologin_user):
response = self.app.get(route_path('repo_groups'))
response.mustcontain('data: []')
def test_show_repo_groups_after_creating_group(self, autologin_user):
def test_show_repo_groups(self, autologin_user):
self.app.get(route_path('repo_groups'))
def test_show_repo_groups_data(self, autologin_user, xhr_header):
response = self.app.get(route_path(
'repo_groups_data'), extra_environ=xhr_header)
all_repo_groups = RepoGroup.query().count()
assert response.json['recordsTotal'] == all_repo_groups
def test_show_repo_groups_data_filtered(self, autologin_user, xhr_header):
response = self.app.get(route_path(
'repo_groups_data', params={'search[value]': 'empty_search'}),
extra_environ=xhr_header)
all_repo_groups = RepoGroup.query().count()
assert response.json['recordsTotal'] == all_repo_groups
assert response.json['recordsFiltered'] == 0
def test_show_repo_groups_after_creating_group(self, autologin_user, xhr_header):
fixture.create_repo_group('test_repo_group')
response = self.app.get(route_path('repo_groups'))
response = self.app.get(route_path(
'repo_groups_data'), extra_environ=xhr_header)
response.mustcontain('"name_raw": "test_repo_group"')
fixture.destroy_repo_group('test_repo_group')

View file

@ -17,7 +17,7 @@
# This program is dual-licensed. If you wish to learn more about the
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import datetime
import logging
import formencode
import formencode.htmlfill
@ -30,16 +30,16 @@ from pyramid.response import Response
from rhodecode import events
from rhodecode.apps._base import BaseAppView, DataGridAppView
from rhodecode.lib.ext_json import json
from rhodecode.lib.auth import (
LoginRequired, CSRFRequired, NotAnonymous,
HasPermissionAny, HasRepoGroupPermissionAny)
from rhodecode.lib import helpers as h, audit_logger
from rhodecode.lib.utils2 import safe_int, safe_unicode
from rhodecode.lib.utils2 import safe_int, safe_unicode, datetime_to_time
from rhodecode.model.forms import RepoGroupForm
from rhodecode.model.repo_group import RepoGroupModel
from rhodecode.model.scm import RepoGroupList
from rhodecode.model.db import Session, RepoGroup
from rhodecode.model.db import (
or_, count, func, in_filter_generator, Session, RepoGroup, User, Repository)
log = logging.getLogger(__name__)
@ -88,23 +88,169 @@ class AdminRepoGroupsView(BaseAppView, DataGridAppView):
return False
return False
# permission check in data loading of
# `repo_group_list_data` via RepoGroupList
@LoginRequired()
@NotAnonymous()
# perms check inside
@view_config(
route_name='repo_groups', request_method='GET',
renderer='rhodecode:templates/admin/repo_groups/repo_groups.mako')
def repo_group_list(self):
c = self.load_default_context()
repo_group_list = RepoGroup.get_all_repo_groups()
repo_group_list_acl = RepoGroupList(
repo_group_list, perm_set=['group.admin'])
repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
repo_group_list=repo_group_list_acl, admin=True)
c.data = json.dumps(repo_group_data)
return self._get_template_context(c)
# permission check inside
@LoginRequired()
@NotAnonymous()
@view_config(
route_name='repo_groups_data', request_method='GET',
renderer='json_ext', xhr=True)
def repo_group_list_data(self):
self.load_default_context()
column_map = {
'name_raw': 'group_name_hash',
'desc': 'group_description',
'last_change_raw': 'updated_on',
'top_level_repos': 'repos_total',
'owner': 'user_username',
}
draw, start, limit = self._extract_chunk(self.request)
search_q, order_by, order_dir = self._extract_ordering(
self.request, column_map=column_map)
_render = self.request.get_partial_renderer(
'rhodecode:templates/data_table/_dt_elements.mako')
c = _render.get_call_context()
def quick_menu(repo_group_name):
return _render('quick_repo_group_menu', repo_group_name)
def repo_group_lnk(repo_group_name):
return _render('repo_group_name', repo_group_name)
def last_change(last_change):
if isinstance(last_change, datetime.datetime) and not last_change.tzinfo:
delta = datetime.timedelta(
seconds=(datetime.datetime.now() - datetime.datetime.utcnow()).seconds)
last_change = last_change + delta
return _render("last_change", last_change)
def desc(desc, personal):
return _render(
'repo_group_desc', desc, personal, c.visual.stylify_metatags)
def repo_group_actions(repo_group_id, repo_group_name, gr_count):
return _render(
'repo_group_actions', repo_group_id, repo_group_name, gr_count)
def user_profile(username):
return _render('user_profile', username)
auth_repo_group_list = RepoGroupList(
RepoGroup.query().all(), perm_set=['group.admin'])
allowed_ids = [-1]
for repo_group in auth_repo_group_list:
allowed_ids.append(repo_group.group_id)
repo_groups_data_total_count = RepoGroup.query()\
.filter(or_(
# generate multiple IN to fix limitation problems
*in_filter_generator(RepoGroup.group_id, allowed_ids)
)) \
.count()
repo_groups_data_total_inactive_count = RepoGroup.query()\
.filter(RepoGroup.group_id.in_(allowed_ids))\
.count()
repo_count = count(Repository.repo_id)
base_q = Session.query(
RepoGroup.group_name,
RepoGroup.group_name_hash,
RepoGroup.group_description,
RepoGroup.group_id,
RepoGroup.personal,
RepoGroup.updated_on,
User,
repo_count.label('repos_count')
) \
.filter(or_(
# generate multiple IN to fix limitation problems
*in_filter_generator(RepoGroup.group_id, allowed_ids)
)) \
.outerjoin(Repository) \
.join(User, User.user_id == RepoGroup.user_id) \
.group_by(RepoGroup, User)
if search_q:
like_expression = u'%{}%'.format(safe_unicode(search_q))
base_q = base_q.filter(or_(
RepoGroup.group_name.ilike(like_expression),
))
repo_groups_data_total_filtered_count = base_q.count()
# the inactive isn't really used, but we still make it same as other data grids
# which use inactive (users,user groups)
repo_groups_data_total_filtered_inactive_count = repo_groups_data_total_filtered_count
sort_defined = False
if order_by == 'group_name':
sort_col = func.lower(RepoGroup.group_name)
sort_defined = True
elif order_by == 'repos_total':
sort_col = repo_count
sort_defined = True
elif order_by == 'user_username':
sort_col = User.username
else:
sort_col = getattr(RepoGroup, order_by, None)
if sort_defined or sort_col:
if order_dir == 'asc':
sort_col = sort_col.asc()
else:
sort_col = sort_col.desc()
base_q = base_q.order_by(sort_col)
base_q = base_q.offset(start).limit(limit)
# authenticated access to user groups
auth_repo_group_list = base_q.all()
repo_groups_data = []
for repo_gr in auth_repo_group_list:
row = {
"menu": quick_menu(repo_gr.group_name),
"name": repo_group_lnk(repo_gr.group_name),
"name_raw": repo_gr.group_name,
"last_change": last_change(repo_gr.updated_on),
"last_change_raw": datetime_to_time(repo_gr.updated_on),
"last_changeset": "",
"last_changeset_raw": "",
"desc": desc(repo_gr.group_description, repo_gr.personal),
"owner": user_profile(repo_gr.User.username),
"top_level_repos": repo_gr.repos_count,
"action": repo_group_actions(
repo_gr.group_id, repo_gr.group_name, repo_gr.repos_count),
}
repo_groups_data.append(row)
data = ({
'draw': draw,
'data': repo_groups_data,
'recordsTotal': repo_groups_data_total_count,
'recordsTotalInactive': repo_groups_data_total_inactive_count,
'recordsFiltered': repo_groups_data_total_filtered_count,
'recordsFilteredInactive': repo_groups_data_total_filtered_inactive_count,
})
return data
@LoginRequired()
@NotAnonymous()
# perm checks inside

View file

@ -39,7 +39,7 @@ from rhodecode.model.forms import UserGroupForm
from rhodecode.model.permission import PermissionModel
from rhodecode.model.scm import UserGroupList
from rhodecode.model.db import (
or_, count, User, UserGroup, UserGroupMember)
or_, count, User, UserGroup, UserGroupMember, in_filter_generator)
from rhodecode.model.meta import Session
from rhodecode.model.user_group import UserGroupModel
from rhodecode.model.db import true
@ -107,11 +107,17 @@ class AdminUserGroupsView(BaseAppView, DataGridAppView):
allowed_ids.append(user_group.users_group_id)
user_groups_data_total_count = UserGroup.query()\
.filter(UserGroup.users_group_id.in_(allowed_ids))\
.filter(or_(
# generate multiple IN to fix limitation problems
*in_filter_generator(UserGroup.users_group_id, allowed_ids)
))\
.count()
user_groups_data_total_inactive_count = UserGroup.query()\
.filter(UserGroup.users_group_id.in_(allowed_ids))\
.filter(or_(
# generate multiple IN to fix limitation problems
*in_filter_generator(UserGroup.users_group_id, allowed_ids)
))\
.filter(UserGroup.users_group_active != true()).count()
member_count = count(UserGroupMember.user_id)
@ -123,11 +129,14 @@ class AdminUserGroupsView(BaseAppView, DataGridAppView):
UserGroup.group_data,
User,
member_count.label('member_count')
) \
.filter(UserGroup.users_group_id.in_(allowed_ids)) \
.outerjoin(UserGroupMember) \
.join(User, User.user_id == UserGroup.user_id) \
.group_by(UserGroup, User)
) \
.filter(or_(
# generate multiple IN to fix limitation problems
*in_filter_generator(UserGroup.users_group_id, allowed_ids)
)) \
.outerjoin(UserGroupMember) \
.join(User, User.user_id == UserGroup.user_id) \
.group_by(UserGroup, User)
base_q_inactive = base_q.filter(UserGroup.users_group_active != true())
@ -141,14 +150,16 @@ class AdminUserGroupsView(BaseAppView, DataGridAppView):
user_groups_data_total_filtered_count = base_q.count()
user_groups_data_total_filtered_inactive_count = base_q_inactive.count()
sort_defined = False
if order_by == 'members_total':
sort_col = member_count
sort_defined = True
elif order_by == 'user_username':
sort_col = User.username
else:
sort_col = getattr(UserGroup, order_by, None)
if isinstance(sort_col, count) or sort_col:
if sort_defined or sort_col:
if order_dir == 'asc':
sort_col = sort_col.asc()
else:
@ -162,7 +173,7 @@ class AdminUserGroupsView(BaseAppView, DataGridAppView):
user_groups_data = []
for user_gr in auth_user_group_list:
user_groups_data.append({
row = {
"users_group_name": user_group_name(user_gr.users_group_name),
"name_raw": h.escape(user_gr.users_group_name),
"description": h.escape(user_gr.user_group_description),
@ -175,7 +186,8 @@ class AdminUserGroupsView(BaseAppView, DataGridAppView):
"owner": user_profile(user_gr.User.username),
"action": user_group_actions(
user_gr.users_group_id, user_gr.users_group_name)
})
}
user_groups_data.append(row)
data = ({
'draw': draw,

View file

@ -67,12 +67,6 @@ class RepoSettingsView(RepoAppView):
.filter(UserFollowing.user_id == c.default_user_id) \
.filter(UserFollowing.follows_repository == self.db_repo).scalar()
c.has_origin_repo_read_perm = False
if self.db_repo.fork:
c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
'repository.write', 'repository.read', 'repository.admin')(
self.db_repo.fork.repo_name, 'repo set as fork page')
c.ver_info_dict = self.rhodecode_vcs_repo.get_hooks_info()
return self._get_template_context(c)

View file

@ -20,12 +20,13 @@
import os
import sys
import shutil
import logging
import tempfile
import textwrap
import collections
from .base import VcsServer
from rhodecode.model.db import RhodeCodeUi
from rhodecode.model.settings import VcsSettingsModel
log = logging.getLogger(__name__)
@ -37,62 +38,46 @@ class MercurialTunnelWrapper(object):
self.server = server
self.stdin = sys.stdin
self.stdout = sys.stdout
self.svn_conf_fd, self.svn_conf_path = tempfile.mkstemp()
self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp()
self.hooks_env_fd, self.hooks_env_path = tempfile.mkstemp(prefix='hgrc_rhodecode_')
def create_hooks_env(self):
repo_name = self.server.repo_name
hg_flags = self.config_to_hgrc(repo_name)
content = textwrap.dedent(
'''
# SSH hooks version=1.0.0
[hooks]
pretxnchangegroup.ssh_auth=python:vcsserver.hooks.pre_push_ssh_auth
pretxnchangegroup.ssh=python:vcsserver.hooks.pre_push_ssh
changegroup.ssh=python:vcsserver.hooks.post_push_ssh
preoutgoing.ssh=python:vcsserver.hooks.pre_pull_ssh
outgoing.ssh=python:vcsserver.hooks.post_pull_ssh
# RhodeCode SSH hooks version=2.0.0
{custom}
'''
)
).format(custom='\n'.join(hg_flags))
root = self.server.get_root_store()
hgrc_custom = os.path.join(root, repo_name, '.hg', 'hgrc_rhodecode')
hgrc_main = os.path.join(root, repo_name, '.hg', 'hgrc')
# cleanup custom hgrc file
if os.path.isfile(hgrc_custom):
with open(hgrc_custom, 'wb') as f:
f.write('')
log.debug('Cleanup custom hgrc file under %s', hgrc_custom)
# write temp
with os.fdopen(self.hooks_env_fd, 'w') as hooks_env_file:
hooks_env_file.write(content)
root = self.server.get_root_store()
hgrc_custom = os.path.join(
root, self.server.repo_name, '.hg', 'hgrc_rhodecode')
log.debug('Wrote custom hgrc file under %s', hgrc_custom)
shutil.move(
self.hooks_env_path, hgrc_custom)
return self.hooks_env_path
hgrc_main = os.path.join(
root, self.server.repo_name, '.hg', 'hgrc')
include_marker = '%include hgrc_rhodecode'
def remove_configs(self):
os.remove(self.hooks_env_path)
if not os.path.isfile(hgrc_main):
os.mknod(hgrc_main)
with open(hgrc_main, 'rb') as f:
data = f.read()
has_marker = include_marker in data
if not has_marker:
log.debug('Adding include marker for hooks')
with open(hgrc_main, 'wa') as f:
f.write(textwrap.dedent('''
# added by RhodeCode
{}
'''.format(include_marker)))
def command(self):
def command(self, hgrc_path):
root = self.server.get_root_store()
command = (
"cd {root}; {hg_path} -R {root}{repo_name} "
"cd {root}; HGRCPATH={hgrc} {hg_path} -R {root}{repo_name} "
"serve --stdio".format(
root=root, hg_path=self.server.hg_path,
repo_name=self.server.repo_name))
repo_name=self.server.repo_name, hgrc=hgrc_path))
log.debug("Final CMD: %s", command)
return command
@ -102,22 +87,61 @@ class MercurialTunnelWrapper(object):
action = '?'
# permissions are check via `pre_push_ssh_auth` hook
self.server.update_environment(action=action, extras=extras)
self.create_hooks_env()
return os.system(self.command())
custom_hgrc_file = self.create_hooks_env()
try:
return os.system(self.command(custom_hgrc_file))
finally:
self.remove_configs()
class MercurialServer(VcsServer):
backend = 'hg'
cli_flags = ['phases', 'largefiles', 'extensions', 'experimental', 'hooks']
def __init__(self, store, ini_path, repo_name,
user, user_permissions, config, env):
super(MercurialServer, self).\
__init__(user, user_permissions, config, env)
def __init__(self, store, ini_path, repo_name, user, user_permissions, config, env):
super(MercurialServer, self).__init__(user, user_permissions, config, env)
self.store = store
self.ini_path = ini_path
self.repo_name = repo_name
self._path = self.hg_path = config.get(
'app:main', 'ssh.executable.hg')
self._path = self.hg_path = config.get('app:main', 'ssh.executable.hg')
self.tunnel = MercurialTunnelWrapper(server=self)
def config_to_hgrc(self, repo_name):
ui_sections = collections.defaultdict(list)
ui = VcsSettingsModel(repo=repo_name).get_ui_settings(section=None, key=None)
# write default hooks
default_hooks = [
('pretxnchangegroup.ssh_auth', 'python:vcsserver.hooks.pre_push_ssh_auth'),
('pretxnchangegroup.ssh', 'python:vcsserver.hooks.pre_push_ssh'),
('changegroup.ssh', 'python:vcsserver.hooks.post_push_ssh'),
('preoutgoing.ssh', 'python:vcsserver.hooks.pre_pull_ssh'),
('outgoing.ssh', 'python:vcsserver.hooks.post_pull_ssh'),
]
for k, v in default_hooks:
ui_sections['hooks'].append((k, v))
for entry in ui:
if not entry.active:
continue
sec = entry.section
key = entry.key
if sec in self.cli_flags:
# we want only custom hooks, so we skip builtins
if sec == 'hooks' and key in RhodeCodeUi.HOOKS_BUILTIN:
continue
ui_sections[sec].append([key, entry.value])
flags = []
for _sec, key_val in ui_sections.items():
flags.append(' ')
flags.append('[{}]'.format(_sec))
for key, val in key_val:
flags.append('{}= {}'.format(key, val))
return flags

View file

@ -18,6 +18,7 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
import os
import mock
import pytest
@ -68,14 +69,16 @@ def hg_server(app):
class TestMercurialServer(object):
def test_command(self, hg_server):
def test_command(self, hg_server, tmpdir):
server = hg_server.create()
custom_hgrc = os.path.join(str(tmpdir), 'hgrc')
expected_command = (
'cd {root}; {hg_path} -R {root}{repo_name} serve --stdio'.format(
root=hg_server.root, hg_path=hg_server.hg_path,
'cd {root}; HGRCPATH={custom_hgrc} {hg_path} -R {root}{repo_name} serve --stdio'.format(
root=hg_server.root, custom_hgrc=custom_hgrc, hg_path=hg_server.hg_path,
repo_name=hg_server.repo_name)
)
assert expected_command == server.tunnel.command()
server_command = server.tunnel.command(custom_hgrc)
assert expected_command == server_command
@pytest.mark.parametrize('permissions, action, code', [
({}, 'pull', -2),

View file

@ -114,7 +114,6 @@ class IntegrationSettingsViewBase(BaseAppView):
_ = self.request.translate
c = super(IntegrationSettingsViewBase, self)._get_local_tmpl_context(
include_app_defaults=include_app_defaults)
c.active = 'integrations'
return c
@ -404,6 +403,11 @@ class RepoIntegrationsView(IntegrationSettingsViewBase):
c.repo_name = self.db_repo.repo_name
c.repository_pull_requests = ScmModel().get_pull_requests(self.repo)
c.has_origin_repo_read_perm = False
if self.db_repo.fork:
c.has_origin_repo_read_perm = h.HasRepoPermissionAny(
'repository.write', 'repository.read', 'repository.admin')(
self.db_repo.fork.repo_name, 'summary fork link')
return c
@LoginRequired()

View file

@ -375,6 +375,27 @@ class DbManage(object):
hgevolve.ui_active = False
self.sa.add(hgevolve)
hgevolve = RhodeCodeUi()
hgevolve.ui_section = 'experimental'
hgevolve.ui_key = 'evolution'
hgevolve.ui_value = ''
hgevolve.ui_active = False
self.sa.add(hgevolve)
hgevolve = RhodeCodeUi()
hgevolve.ui_section = 'experimental'
hgevolve.ui_key = 'evolution.exchange'
hgevolve.ui_value = ''
hgevolve.ui_active = False
self.sa.add(hgevolve)
hgevolve = RhodeCodeUi()
hgevolve.ui_section = 'extensions'
hgevolve.ui_key = 'topic'
hgevolve.ui_value = ''
hgevolve.ui_active = False
self.sa.add(hgevolve)
# enable hggit disabled by default
hggit = RhodeCodeUi()
hggit.ui_section = 'extensions'

View file

@ -0,0 +1,54 @@
# -*- coding: utf-8 -*-
import logging
from alembic.migration import MigrationContext
from alembic.operations import Operations
from sqlalchemy import String, Column
from sqlalchemy.sql import text
from rhodecode.lib.dbmigrate.versions import _reset_base
from rhodecode.model import meta, init_model_encryption
from rhodecode.model.db import RepoGroup
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_16_0_2
init_model_encryption(db_4_16_0_2)
context = MigrationContext.configure(migrate_engine.connect())
op = Operations(context)
repo_group = db_4_16_0_2.RepoGroup.__table__
with op.batch_alter_table(repo_group.name) as batch_op:
batch_op.add_column(
Column("repo_group_name_hash", String(1024), nullable=True, unique=False))
_generate_repo_group_name_hashes(db_4_16_0_2, op, meta.Session)
def downgrade(migrate_engine):
pass
def _generate_repo_group_name_hashes(models, op, session):
repo_groups = models.RepoGroup.get_all()
for repo_group in repo_groups:
print(repo_group.group_name)
hash_ = RepoGroup.hash_repo_group_name(repo_group.group_name)
params = {'hash': hash_, 'id': repo_group.group_id}
query = text(
'UPDATE groups SET repo_group_name_hash = :hash'
' WHERE group_id = :id').bindparams(**params)
op.execute(query)
session().commit()

View file

@ -0,0 +1,39 @@
# -*- coding: utf-8 -*-
import logging
from alembic.migration import MigrationContext
from alembic.operations import Operations
from rhodecode.lib.dbmigrate.versions import _reset_base
from rhodecode.model import init_model_encryption
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_16_0_2
init_model_encryption(db_4_16_0_2)
context = MigrationContext.configure(migrate_engine.connect())
op = Operations(context)
repo_group = db_4_16_0_2.RepoGroup.__table__
with op.batch_alter_table(repo_group.name) as batch_op:
batch_op.alter_column("repo_group_name_hash", nullable=False)
def downgrade(migrate_engine):
pass
def _generate_repo_group_name_hashes(models, op, session):
pass

View file

@ -80,7 +80,7 @@ def trigger_log_create_pull_request_hook(username, repo_name, repo_alias,
extras = _get_rc_scm_extras(username, repo_name, repo_alias,
'create_pull_request')
events.trigger(events.PullRequestCreateEvent(pull_request))
extras.update(pull_request.get_api_data())
extras.update(pull_request.get_api_data(with_merge_state=False))
hooks_base.log_create_pull_request(**extras)

View file

@ -371,7 +371,8 @@ def config_data_from_db(clear_session=True, repo=None):
config.append((
safe_str(setting.section), safe_str(setting.key), False))
log.debug(
'settings ui from db: %s',
'settings ui from db@repo[%s]: %s',
repo,
','.join(map(lambda s: '[{}] {}={}'.format(*s), ui_data)))
if clear_session:
meta.Session.remove()

View file

@ -161,7 +161,7 @@ class MergeResponse(object):
u'This pull request cannot be merged because the source contains '
u'more branches than the target.'),
MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS: lazy_ugettext(
u'This pull request cannot be merged because the target '
u'This pull request cannot be merged because the target `{target_ref.name}` '
u'has multiple heads: `{heads}`.'),
MergeFailureReason.TARGET_IS_LOCKED: lazy_ugettext(
u'This pull request cannot be merged because the target repository is '
@ -309,6 +309,9 @@ class BaseRepository(object):
def _remote(self):
raise NotImplementedError
def _heads(self, branch=None):
return []
@LazyProperty
def EMPTY_COMMIT(self):
return EmptyCommit(self.EMPTY_COMMIT_ID)

View file

@ -715,11 +715,16 @@ class MercurialRepository(BaseRepository):
try:
if target_ref.type == 'branch' and len(self._heads(target_ref.name)) != 1:
heads = ','.join(self._heads(target_ref.name))
heads = '\n,'.join(self._heads(target_ref.name))
metadata = {
'target_ref': target_ref,
'source_ref': source_ref,
'heads': heads
}
return MergeResponse(
False, False, None,
MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS,
metadata={'heads': heads})
metadata=metadata)
except CommitDoesNotExistError:
log.exception('Failure when looking up branch heads on hg target')
return MergeResponse(

View file

@ -25,6 +25,7 @@ Database Models for RhodeCode Enterprise
import re
import os
import time
import string
import hashlib
import logging
import datetime
@ -50,6 +51,7 @@ from sqlalchemy.dialects.mysql import LONGTEXT
from zope.cachedescriptors.property import Lazy as LazyProperty
from pyramid import compat
from pyramid.threadlocal import get_current_request
from webhelpers.text import collapse, remove_formatting
from rhodecode.translation import _
from rhodecode.lib.vcs import get_vcs_instance
@ -409,6 +411,15 @@ class RhodeCodeUi(Base, BaseModel):
HOOK_PUSH = 'changegroup.push_logger'
HOOK_PUSH_KEY = 'pushkey.key_push'
HOOKS_BUILTIN = [
HOOK_PRE_PULL,
HOOK_PULL,
HOOK_PRE_PUSH,
HOOK_PRETX_PUSH,
HOOK_PUSH,
HOOK_PUSH_KEY,
]
# TODO: johbo: Unify way how hooks are configured for git and hg,
# git part is currently hardcoded.
@ -2469,7 +2480,8 @@ class RepoGroup(Base, BaseModel):
CHOICES_SEPARATOR = '/' # used to generate select2 choices for nested groups
group_id = Column("group_id", Integer(), nullable=False, unique=True, default=None, primary_key=True)
group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
_group_name = Column("group_name", String(255), nullable=False, unique=True, default=None)
group_name_hash = Column("repo_group_name_hash", String(1024), nullable=False, unique=False)
group_parent_id = Column("group_parent_id", Integer(), ForeignKey('groups.group_id'), nullable=True, unique=None, default=None)
group_description = Column("group_description", String(10000), nullable=True, unique=None, default=None)
enable_locking = Column("enable_locking", Boolean(), nullable=False, unique=None, default=False)
@ -2492,6 +2504,15 @@ class RepoGroup(Base, BaseModel):
return u"<%s('id:%s:%s')>" % (
self.__class__.__name__, self.group_id, self.group_name)
@hybrid_property
def group_name(self):
return self._group_name
@group_name.setter
def group_name(self, value):
self._group_name = value
self.group_name_hash = self.hash_repo_group_name(value)
@validates('group_parent_id')
def validate_group_parent_id(self, key, val):
"""
@ -2507,6 +2528,18 @@ class RepoGroup(Base, BaseModel):
from rhodecode.lib import helpers as h
return h.escape(self.group_description)
@classmethod
def hash_repo_group_name(cls, repo_group_name):
val = remove_formatting(repo_group_name)
val = safe_str(val).lower()
chars = []
for c in val:
if c not in string.ascii_letters:
c = str(ord(c))
chars.append(c)
return ''.join(chars)
@classmethod
def _generate_choice(cls, repo_group):
from webhelpers.html import literal as _literal
@ -2770,6 +2803,13 @@ class RepoGroup(Base, BaseModel):
}
return data
def get_dict(self):
# Since we transformed `group_name` to a hybrid property, we need to
# keep compatibility with the code which uses `group_name` field.
result = super(RepoGroup, self).get_dict()
result['group_name'] = result.pop('_group_name', None)
return result
class Permission(Base, BaseModel):
__tablename__ = 'permissions'

View file

@ -1020,6 +1020,9 @@ class PullRequestModel(BaseModel):
log.debug("Adding %s reviewers", ids_to_add)
log.debug("Removing %s reviewers", ids_to_remove)
changed = False
added_audit_reviewers = []
removed_audit_reviewers = []
for uid in ids_to_add:
changed = True
_usr = self._get_user(uid)
@ -1030,29 +1033,37 @@ class PullRequestModel(BaseModel):
# NOTE(marcink): mandatory shouldn't be changed now
# reviewer.mandatory = reviewers[uid]['reasons']
Session().add(reviewer)
self._log_audit_action(
'repo.pull_request.reviewer.add', {'data': reviewer.get_dict()},
user, pull_request)
added_audit_reviewers.append(reviewer.get_dict())
for uid in ids_to_remove:
changed = True
# NOTE(marcink): we fetch "ALL" reviewers using .all(). This is an edge case
# that prevents and fixes cases that we added the same reviewer twice.
# this CAN happen due to the lack of DB checks
reviewers = PullRequestReviewers.query()\
.filter(PullRequestReviewers.user_id == uid,
PullRequestReviewers.pull_request == pull_request)\
.all()
# use .all() in case we accidentally added the same person twice
# this CAN happen due to the lack of DB checks
for obj in reviewers:
old_data = obj.get_dict()
added_audit_reviewers.append(obj.get_dict())
Session().delete(obj)
self._log_audit_action(
'repo.pull_request.reviewer.delete',
{'old_data': old_data}, user, pull_request)
if changed:
Session().expire_all()
pull_request.updated_on = datetime.datetime.now()
Session().add(pull_request)
# finally store audit logs
for user_data in added_audit_reviewers:
self._log_audit_action(
'repo.pull_request.reviewer.add', {'data': user_data},
user, pull_request)
for user_data in removed_audit_reviewers:
self._log_audit_action(
'repo.pull_request.reviewer.delete', {'old_data': user_data},
user, pull_request)
self.notify_reviewers(pull_request, ids_to_add)
return ids_to_add, ids_to_remove
@ -1306,8 +1317,16 @@ class PullRequestModel(BaseModel):
possible = pull_request.last_merge_status == MergeFailureReason.NONE
metadata = {
'target_ref': pull_request.target_ref_parts,
'source_ref': pull_request.source_ref_parts
'source_ref': pull_request.source_ref_parts,
}
if not possible and target_ref.type == 'branch':
# NOTE(marcink): case for mercurial multiple heads on branch
heads = target_vcs._heads(target_ref.name)
if len(heads) != 1:
heads = '\n,'.join(target_vcs._heads(target_ref.name))
metadata.update({
'heads': heads
})
merge_state = MergeResponse(
possible, False, None, pull_request.last_merge_status, metadata=metadata)

View file

@ -119,6 +119,7 @@ class SettingsModel(BaseModel):
new_ui.ui_value = val
new_ui.ui_active = active
repository_id = ''
if self.repo:
repo = self._get_repo(self.repo)
repository_id = repo.repo_id
@ -440,26 +441,39 @@ class VcsSettingsModel(object):
HOOKS_SETTINGS = (
('hooks', 'changegroup.repo_size'),
('hooks', 'changegroup.push_logger'),
('hooks', 'outgoing.pull_logger'),)
('hooks', 'outgoing.pull_logger'),
)
HG_SETTINGS = (
('extensions', 'largefiles'),
('phases', 'publish'),
('extensions', 'evolve'),)
('extensions', 'evolve'),
('extensions', 'topic'),
('experimental', 'evolution'),
('experimental', 'evolution.exchange'),
)
GIT_SETTINGS = (
('vcs_git_lfs', 'enabled'),)
('vcs_git_lfs', 'enabled'),
)
GLOBAL_HG_SETTINGS = (
('extensions', 'largefiles'),
('largefiles', 'usercache'),
('phases', 'publish'),
('extensions', 'hgsubversion'),
('extensions', 'evolve'),)
('extensions', 'evolve'),
('extensions', 'topic'),
('experimental', 'evolution'),
('experimental', 'evolution.exchange'),
)
GLOBAL_GIT_SETTINGS = (
('vcs_git_lfs', 'enabled'),
('vcs_git_lfs', 'store_location'))
('vcs_git_lfs', 'store_location')
)
GLOBAL_SVN_SETTINGS = (
('vcs_svn_proxy', 'http_requests_enabled'),
('vcs_svn_proxy', 'http_server_url'))
('vcs_svn_proxy', 'http_server_url')
)
SVN_BRANCH_SECTION = 'vcs_svn_branch'
SVN_TAG_SECTION = 'vcs_svn_tag'
@ -574,12 +588,38 @@ class VcsSettingsModel(object):
def create_repo_svn_settings(self, data):
return self._create_svn_settings(self.repo_settings, data)
def _set_evolution(self, settings, is_enabled):
if is_enabled:
# if evolve is active set evolution=all
self._create_or_update_ui(
settings, *('experimental', 'evolution'), value='all',
active=True)
self._create_or_update_ui(
settings, *('experimental', 'evolution.exchange'), value='yes',
active=True)
# if evolve is active set topics server support
self._create_or_update_ui(
settings, *('extensions', 'topic'), value='',
active=True)
else:
self._create_or_update_ui(
settings, *('experimental', 'evolution'), value='',
active=False)
self._create_or_update_ui(
settings, *('experimental', 'evolution.exchange'), value='no',
active=False)
self._create_or_update_ui(
settings, *('extensions', 'topic'), value='',
active=False)
@assert_repo_settings
def create_or_update_repo_hg_settings(self, data):
largefiles, phases, evolve = \
self.HG_SETTINGS
self.HG_SETTINGS[:3]
largefiles_key, phases_key, evolve_key = \
self._get_settings_keys(self.HG_SETTINGS, data)
self._get_settings_keys(self.HG_SETTINGS[:3], data)
self._create_or_update_ui(
self.repo_settings, *largefiles, value='',
@ -587,21 +627,22 @@ class VcsSettingsModel(object):
self._create_or_update_ui(
self.repo_settings, *evolve, value='',
active=data[evolve_key])
self._set_evolution(self.repo_settings, is_enabled=data[evolve_key])
self._create_or_update_ui(
self.repo_settings, *phases, value=safe_str(data[phases_key]))
def create_or_update_global_hg_settings(self, data):
largefiles, largefiles_store, phases, hgsubversion, evolve \
= self.GLOBAL_HG_SETTINGS
= self.GLOBAL_HG_SETTINGS[:5]
largefiles_key, largefiles_store_key, phases_key, subversion_key, evolve_key \
= self._get_settings_keys(self.GLOBAL_HG_SETTINGS, data)
= self._get_settings_keys(self.GLOBAL_HG_SETTINGS[:5], data)
self._create_or_update_ui(
self.global_settings, *largefiles, value='',
active=data[largefiles_key])
self._create_or_update_ui(
self.global_settings, *largefiles_store,
value=data[largefiles_store_key])
self.global_settings, *largefiles_store, value=data[largefiles_store_key])
self._create_or_update_ui(
self.global_settings, *phases, value=safe_str(data[phases_key]))
self._create_or_update_ui(
@ -609,9 +650,10 @@ class VcsSettingsModel(object):
self._create_or_update_ui(
self.global_settings, *evolve, value='',
active=data[evolve_key])
self._set_evolution(self.global_settings, is_enabled=data[evolve_key])
def create_or_update_repo_git_settings(self, data):
# NOTE(marcink): # comma make unpack work properly
# NOTE(marcink): # comma makes unpack work properly
lfs_enabled, \
= self.GIT_SETTINGS
@ -675,6 +717,7 @@ class VcsSettingsModel(object):
def get_repo_ui_settings(self, section=None, key=None):
global_uis = self.global_settings.get_ui(section, key)
repo_uis = self.repo_settings.get_ui(section, key)
filtered_repo_uis = self._filter_ui_settings(repo_uis)
filtered_repo_uis_keys = [
(s.section, s.key) for s in filtered_repo_uis]

View file

@ -40,15 +40,32 @@
<script>
$(document).ready(function() {
var get_datatable_count = function(){
var api = $('#group_list_table').dataTable().api();
$('#repo_group_count').text(api.page.info().recordsDisplay);
};
var $repoGroupsListTable = $('#group_list_table');
// repo group list
$('#group_list_table').DataTable({
data: ${c.data|n},
$repoGroupsListTable.DataTable({
processing: true,
serverSide: true,
ajax: {
"url": "${h.route_path('repo_groups_data')}",
"dataSrc": function (json) {
var filteredCount = json.recordsFiltered;
var filteredInactiveCount = json.recordsFilteredInactive;
var totalInactive = json.recordsTotalInactive;
var total = json.recordsTotal;
var _text = _gettext(
"{0} of {1} repository groups").format(
filteredCount, total);
if (total === filteredCount) {
_text = _gettext("{0} repository groups").format(total);
}
$('#repo_group_count').text(_text);
return json.data;
},
},
dom: 'rtp',
pageLength: ${c.visual.admin_grid_items},
order: [[ 0, "asc" ]],
@ -66,36 +83,34 @@ $(document).ready(function() {
{ data: {"_": "owner",
"sort": "owner"}, title: "${_('Owner')}", className: "td-user" },
{ data: {"_": "action",
"sort": "action"}, title: "${_('Action')}", className: "td-action" }
"sort": "action"}, title: "${_('Action')}", className: "td-action", orderable: false }
],
language: {
paginate: DEFAULT_GRID_PAGINATION,
sProcessing: _gettext('loading...'),
emptyTable: _gettext("No repository groups available yet.")
},
"initComplete": function( settings, json ) {
get_datatable_count();
quick_repo_menu();
}
});
// update the counter when doing search
$('#group_list_table').on( 'search.dt', function (e,settings) {
get_datatable_count();
$repoGroupsListTable.on('xhr.dt', function(e, settings, json, xhr){
$repoGroupsListTable.css('opacity', 1);
});
// filter, filter both grids
$('#q_filter').on( 'keyup', function () {
var repo_group_api = $('#group_list_table').dataTable().api();
repo_group_api
.columns(0)
.search(this.value)
.draw();
$repoGroupsListTable.on('preXhr.dt', function(e, settings, data){
$repoGroupsListTable.css('opacity', 0.3);
});
// refilter table if page load via back button
$("#q_filter").trigger('keyup');
// filter
$('#q_filter').on('keyup',
$.debounce(250, function() {
$repoGroupsListTable.DataTable().search(
$('#q_filter').val()
).draw();
})
);
});
</script>
</%def>

View file

@ -70,7 +70,7 @@ $(document).ready(function() {
{ data: {"_": "state",
"sort": "state"}, title: "${_('State')}", className: "td-tags td-state" },
{ data: {"_": "action",
"sort": "action"}, title: "${_('Action')}", className: "td-action" }
"sort": "action"}, title: "${_('Action')}", className: "td-action", orderable: false }
],
language: {
paginate: DEFAULT_GRID_PAGINATION,

View file

@ -105,7 +105,7 @@ $(document).ready(function() {
// filter
$('#q_filter').on('keyup',
$.debounce(250, function() {
$('#user_group_list_table').DataTable().search(
$userGroupsListTable.DataTable().search(
$('#q_filter').val()
).draw();
})

View file

@ -133,13 +133,13 @@
<div class="checkbox">
${h.checkbox('extensions_evolve' + suffix, 'True', **kwargs)}
<label for="extensions_evolve${suffix}">${_('Enable evolve extension')}</label>
<label for="extensions_evolve${suffix}">${_('Enable Evolve and Topic extension')}</label>
</div>
<div class="label">
% if display_globals:
<span class="help-block">${_('Enable evolve extension for all repositories.')}</span>
<span class="help-block">${_('Enable Evolve and Topic extensions for all repositories.')}</span>
% else:
<span class="help-block">${_('Enable evolve extension for this repository.')}</span>
<span class="help-block">${_('Enable Evolve and Topic extensions for this repository.')}</span>
% endif
</div>

View file

@ -35,6 +35,8 @@ def route_path(name, params=None, **kwargs):
ADMIN_PREFIX + '/repos',
'repo_groups':
ADMIN_PREFIX + '/repo_groups',
'repo_groups_data':
ADMIN_PREFIX + '/repo_groups_data',
'user_groups':
ADMIN_PREFIX + '/user_groups',
'user_groups_data':
@ -63,8 +65,9 @@ class TestAdminDelegatedUser(TestController):
response = self.app.get(route_path('repos'), status=200)
response.mustcontain('data: []')
response = self.app.get(route_path('repo_groups'), status=200)
response.mustcontain('data: []')
response = self.app.get(route_path('repo_groups_data'),
status=200, extra_environ=xhr_header)
assert response.json['data'] == []
response = self.app.get(route_path('user_groups_data'),
status=200, extra_environ=xhr_header)
@ -97,7 +100,8 @@ class TestAdminDelegatedUser(TestController):
response = self.app.get(route_path('repos'), status=200)
response.mustcontain('"name_raw": "{}"'.format(repo_name))
response = self.app.get(route_path('repo_groups'), status=200)
response = self.app.get(route_path('repo_groups_data'),
extra_environ=xhr_header, status=200)
response.mustcontain('"name_raw": "{}"'.format(repo_group_name))
response = self.app.get(route_path('user_groups_data'),
@ -139,7 +143,8 @@ class TestAdminDelegatedUser(TestController):
response = self.app.get(route_path('repos'), status=200)
response.mustcontain('"name_raw": "{}"'.format(repo_name))
response = self.app.get(route_path('repo_groups'), status=200)
response = self.app.get(route_path('repo_groups_data'),
extra_environ=xhr_header, status=200)
response.mustcontain('"name_raw": "{}"'.format(repo_group_name))
response = self.app.get(route_path('user_groups_data'),

View file

@ -501,8 +501,8 @@ class TestCreateOrUpdateUi(object):
def test_update(self, repo_stub, settings_util):
model = VcsSettingsModel(repo=repo_stub.repo_name)
largefiles, phases, evolve = model.HG_SETTINGS
# care about only 3 first settings
largefiles, phases, evolve = model.HG_SETTINGS[:3]
section = 'test-section'
key = 'test-key'
@ -531,10 +531,11 @@ class TestCreateOrUpdateRepoHgSettings(object):
with mock.patch.object(model, '_create_or_update_ui') as create_mock:
model.create_or_update_repo_hg_settings(self.FORM_DATA)
expected_calls = [
mock.call(model.repo_settings, 'extensions', 'largefiles',
active=False, value=''),
mock.call(model.repo_settings, 'extensions', 'evolve',
active=False, value=''),
mock.call(model.repo_settings, 'extensions', 'largefiles', active=False, value=''),
mock.call(model.repo_settings, 'extensions', 'evolve', active=False, value=''),
mock.call(model.repo_settings, 'experimental', 'evolution', active=False, value=''),
mock.call(model.repo_settings, 'experimental', 'evolution.exchange', active=False, value='no'),
mock.call(model.repo_settings, 'extensions', 'topic', active=False, value=''),
mock.call(model.repo_settings, 'phases', 'publish', value='False'),
]
assert expected_calls == create_mock.call_args_list
@ -589,17 +590,16 @@ class TestCreateOrUpdateGlobalHgSettings(object):
with mock.patch.object(model, '_create_or_update_ui') as create_mock:
model.create_or_update_global_hg_settings(self.FORM_DATA)
expected_calls = [
mock.call(model.global_settings, 'extensions', 'largefiles',
active=False, value=''),
mock.call(model.global_settings, 'largefiles', 'usercache',
value='/example/largefiles-store'),
mock.call(model.global_settings, 'phases', 'publish',
value='False'),
mock.call(model.global_settings, 'extensions', 'hgsubversion',
active=False),
mock.call(model.global_settings, 'extensions', 'evolve',
active=False, value='')
mock.call(model.global_settings, 'extensions', 'largefiles', active=False, value=''),
mock.call(model.global_settings, 'largefiles', 'usercache', value='/example/largefiles-store'),
mock.call(model.global_settings, 'phases', 'publish', value='False'),
mock.call(model.global_settings, 'extensions', 'hgsubversion', active=False),
mock.call(model.global_settings, 'extensions', 'evolve', active=False, value=''),
mock.call(model.global_settings, 'experimental', 'evolution', active=False, value=''),
mock.call(model.global_settings, 'experimental', 'evolution.exchange', active=False, value='no'),
mock.call(model.global_settings, 'extensions', 'topic', active=False, value=''),
]
assert expected_calls == create_mock.call_args_list
@pytest.mark.parametrize('field_to_remove', FORM_DATA.keys())
@ -625,10 +625,8 @@ class TestCreateOrUpdateGlobalGitSettings(object):
with mock.patch.object(model, '_create_or_update_ui') as create_mock:
model.create_or_update_global_git_settings(self.FORM_DATA)
expected_calls = [
mock.call(model.global_settings, 'vcs_git_lfs', 'enabled',
active=False, value=False),
mock.call(model.global_settings, 'vcs_git_lfs', 'store_location',
value='/example/lfs-store'),
mock.call(model.global_settings, 'vcs_git_lfs', 'enabled', active=False, value=False),
mock.call(model.global_settings, 'vcs_git_lfs', 'store_location', value='/example/lfs-store'),
]
assert expected_calls == create_mock.call_args_list

View file

@ -512,7 +512,7 @@ def test_outdated_comments(
(MergeFailureReason.HG_SOURCE_HAS_MORE_BRANCHES,
'This pull request cannot be merged because the source contains more branches than the target.'),
(MergeFailureReason.HG_TARGET_HAS_MULTIPLE_HEADS,
'This pull request cannot be merged because the target has multiple heads: `a,b,c`.'),
'This pull request cannot be merged because the target `ref_name` has multiple heads: `a,b,c`.'),
(MergeFailureReason.TARGET_IS_LOCKED,
'This pull request cannot be merged because the target repository is locked by user:123.'),
(MergeFailureReason.MISSING_TARGET_REF,