@@ -37,8 +36,8 @@
${c.error_message} | ${c.error_explanation}

- % if messages: - % for message in messages: + % if c.messages: + % for message in c.messages:
${message}
% endfor % endif From 1f812d6a5ee9961b0e99c92de9184f8ec0777194 Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Mon, 15 May 2017 22:10:36 +0200 Subject: [PATCH 112/211] caches: ensure we don't use non-ascii characters in cache keys. - this could before lead to problems in beaker cache implementation. --- rhodecode/lib/auth.py | 8 ++--- rhodecode/model/db.py | 36 +++++++++---------- rhodecode/model/integration.py | 10 +++--- rhodecode/model/repo.py | 11 +++--- rhodecode/model/repo_group.py | 7 ++-- rhodecode/model/user.py | 11 +++--- .../tests/lib/middleware/test_simplevcs.py | 4 +-- 7 files changed, 45 insertions(+), 42 deletions(-) diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py index d647ad9a..ba6320bb 100644 --- a/rhodecode/lib/auth.py +++ b/rhodecode/lib/auth.py @@ -1045,8 +1045,8 @@ class AuthUser(object): default_ips = UserIpMap.query().filter( UserIpMap.user == User.get_default_user(cache=True)) if cache: - default_ips = default_ips.options(FromCache("sql_cache_short", - "get_user_ips_default")) + default_ips = default_ips.options( + FromCache("sql_cache_short", "get_user_ips_default")) # populate from default user for ip in default_ips: @@ -1059,8 +1059,8 @@ class AuthUser(object): user_ips = UserIpMap.query().filter(UserIpMap.user_id == user_id) if cache: - user_ips = user_ips.options(FromCache("sql_cache_short", - "get_user_ips_%s" % user_id)) + user_ips = user_ips.options( + FromCache("sql_cache_short", "get_user_ips_%s" % user_id)) for ip in user_ips: try: diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py index 720e8b10..284add7e 100644 --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -761,9 +761,9 @@ class User(Base, BaseModel): if val: return val else: + cache_key = "get_user_by_name_%s" % _hash_key(username) q = q.options( - FromCache("sql_cache_short", - "get_user_by_name_%s" % _hash_key(username))) + FromCache("sql_cache_short", cache_key)) return q.scalar() @@ -774,8 +774,8 @@ class User(Base, BaseModel): .filter(or_(UserApiKeys.expires == -1, UserApiKeys.expires >= time.time())) if cache: - q = q.options(FromCache("sql_cache_short", - "get_auth_token_%s" % auth_token)) + q = q.options( + FromCache("sql_cache_short", "get_auth_token_%s" % auth_token)) match = q.first() if match: @@ -790,9 +790,10 @@ class User(Base, BaseModel): else: q = cls.query().filter(cls.email == email) + email_key = _hash_key(email) if cache: - q = q.options(FromCache("sql_cache_short", - "get_email_key_%s" % _hash_key(email))) + q = q.options( + FromCache("sql_cache_short", "get_email_key_%s" % email_key)) ret = q.scalar() if ret is None: @@ -804,8 +805,8 @@ class User(Base, BaseModel): q = q.filter(UserEmailMap.email == email) q = q.options(joinedload(UserEmailMap.user)) if cache: - q = q.options(FromCache("sql_cache_short", - "get_email_map_key_%s" % email)) + q = q.options( + FromCache("sql_cache_short", "get_email_map_key_%s" % email_key)) ret = getattr(q.scalar(), 'user', None) return ret @@ -1195,17 +1196,16 @@ class UserGroup(Base, BaseModel): else: q = cls.query().filter(cls.users_group_name == group_name) if cache: - q = q.options(FromCache( - "sql_cache_short", - "get_group_%s" % _hash_key(group_name))) + q = q.options( + FromCache("sql_cache_short", "get_group_%s" % _hash_key(group_name))) return q.scalar() @classmethod def get(cls, user_group_id, cache=False): user_group = cls.query() if cache: - user_group = user_group.options(FromCache("sql_cache_short", - "get_users_group_%s" % user_group_id)) + user_group = user_group.options( + FromCache("sql_cache_short", "get_users_group_%s" % user_group_id)) return user_group.get(user_group_id) def permissions(self, with_admins=True, with_owner=True): @@ -1546,9 +1546,9 @@ class Repository(Base, BaseModel): if val: return val else: + cache_key = "get_repo_by_name_%s" % _hash_key(repo_name) q = q.options( - FromCache("sql_cache_short", - "get_repo_by_name_%s" % _hash_key(repo_name))) + FromCache("sql_cache_short", cache_key)) return q.scalar() @@ -2204,9 +2204,9 @@ class RepoGroup(Base, BaseModel): else: gr = cls.query().filter(cls.group_name == group_name) if cache: - gr = gr.options(FromCache( - "sql_cache_short", - "get_group_%s" % _hash_key(group_name))) + name_key = _hash_key(group_name) + gr = gr.options( + FromCache("sql_cache_short", "get_group_%s" % name_key)) return gr.scalar() @classmethod diff --git a/rhodecode/model/integration.py b/rhodecode/model/integration.py index 490d7581..72a7c02c 100644 --- a/rhodecode/model/integration.py +++ b/rhodecode/model/integration.py @@ -200,14 +200,14 @@ class IntegrationModel(BaseModel): query = query.filter(or_(*clauses)) if cache: - query = query.options(FromCache( - "sql_cache_short", - "get_enabled_repo_integrations_%i" % event.repo.repo_id)) + cache_key = "get_enabled_repo_integrations_%i" % event.repo.repo_id + query = query.options( + FromCache("sql_cache_short", cache_key)) else: # only global integrations query = query.filter(global_integrations_filter) if cache: - query = query.options(FromCache( - "sql_cache_short", "get_enabled_global_integrations")) + query = query.options( + FromCache("sql_cache_short", "get_enabled_global_integrations")) result = query.all() return result \ No newline at end of file diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py index 3739be7b..507cc2d2 100644 --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -44,7 +44,7 @@ from rhodecode.lib.utils2 import ( get_current_rhodecode_user, safe_int, datetime_to_time, action_logger_generic) from rhodecode.lib.vcs.backends import get_backend from rhodecode.model import BaseModel -from rhodecode.model.db import ( +from rhodecode.model.db import (_hash_key, Repository, UserRepoToPerm, UserGroupRepoToPerm, UserRepoGroupToPerm, UserGroupRepoGroupToPerm, User, Permission, Statistics, UserGroup, RepoGroup, RepositoryField) @@ -99,8 +99,8 @@ class RepoModel(BaseModel): .filter(Repository.repo_id == repo_id) if cache: - repo = repo.options(FromCache("sql_cache_short", - "get_repo_%s" % repo_id)) + repo = repo.options( + FromCache("sql_cache_short", "get_repo_%s" % repo_id)) return repo.scalar() def get_repo(self, repository): @@ -111,8 +111,9 @@ class RepoModel(BaseModel): .filter(Repository.repo_name == repo_name) if cache: - repo = repo.options(FromCache("sql_cache_short", - "get_repo_%s" % repo_name)) + name_key = _hash_key(repo_name) + repo = repo.options( + FromCache("sql_cache_short", "get_repo_%s" % name_key)) return repo.scalar() def _extract_id_from_repo_name(self, repo_name): diff --git a/rhodecode/model/repo_group.py b/rhodecode/model/repo_group.py index 18b4b372..9ffb65e4 100644 --- a/rhodecode/model/repo_group.py +++ b/rhodecode/model/repo_group.py @@ -35,7 +35,7 @@ from zope.cachedescriptors.property import Lazy as LazyProperty from rhodecode import events from rhodecode.model import BaseModel -from rhodecode.model.db import ( +from rhodecode.model.db import (_hash_key, RepoGroup, UserRepoGroupToPerm, User, Permission, UserGroupRepoGroupToPerm, UserGroup, Repository) from rhodecode.model.settings import VcsSettingsModel, SettingsModel @@ -73,8 +73,9 @@ class RepoGroupModel(BaseModel): .filter(RepoGroup.group_name == repo_group_name) if cache: - repo = repo.options(FromCache( - "sql_cache_short", "get_repo_group_%s" % repo_group_name)) + name_key = _hash_key(repo_group_name) + repo = repo.options( + FromCache("sql_cache_short", "get_repo_group_%s" % name_key)) return repo.scalar() def get_default_create_personal_repo_group(self): diff --git a/rhodecode/model/user.py b/rhodecode/model/user.py index 4aa7fb18..62d16a3e 100644 --- a/rhodecode/model/user.py +++ b/rhodecode/model/user.py @@ -40,7 +40,7 @@ from rhodecode.lib.utils2 import ( from rhodecode.lib.caching_query import FromCache from rhodecode.model import BaseModel from rhodecode.model.auth_token import AuthTokenModel -from rhodecode.model.db import ( +from rhodecode.model.db import (_hash_key, or_, joinedload, User, UserToPerm, UserEmailMap, UserIpMap, UserLog) from rhodecode.lib.exceptions import ( DefaultUserException, UserOwnsReposException, UserOwnsRepoGroupsException, @@ -58,8 +58,8 @@ class UserModel(BaseModel): def get(self, user_id, cache=False): user = self.sa.query(User) if cache: - user = user.options(FromCache("sql_cache_short", - "get_user_%s" % user_id)) + user = user.options( + FromCache("sql_cache_short", "get_user_%s" % user_id)) return user.get(user_id) def get_user(self, user): @@ -112,8 +112,9 @@ class UserModel(BaseModel): user = self.sa.query(User)\ .filter(User.username == username) if cache: - user = user.options(FromCache("sql_cache_short", - "get_user_%s" % username)) + name_key = _hash_key(username) + user = user.options( + FromCache("sql_cache_short", "get_user_%s" % name_key)) return user.scalar() def get_by_email(self, email, cache=False, case_insensitive=False): diff --git a/rhodecode/tests/lib/middleware/test_simplevcs.py b/rhodecode/tests/lib/middleware/test_simplevcs.py index b5512265..936ca2dc 100644 --- a/rhodecode/tests/lib/middleware/test_simplevcs.py +++ b/rhodecode/tests/lib/middleware/test_simplevcs.py @@ -87,8 +87,8 @@ def vcscontroller(pylonsapp, config_stub): def _remove_default_user_from_query_cache(): user = User.get_default_user(cache=True) query = Session().query(User).filter(User.username == user.username) - query = query.options(FromCache( - "sql_cache_short", "get_user_%s" % _hash_key(user.username))) + query = query.options( + FromCache("sql_cache_short", "get_user_%s" % _hash_key(user.username))) query.invalidate() Session().expire(user) From 27a8d620429ec370e1159cf163b8515f0f44ae56 Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Tue, 16 May 2017 21:26:18 +0200 Subject: [PATCH 113/211] admin: fix wrong ext_json import from tools. --- rhodecode/apps/admin/views/users.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rhodecode/apps/admin/views/users.py b/rhodecode/apps/admin/views/users.py index 196f1b41..6647051e 100644 --- a/rhodecode/apps/admin/views/users.py +++ b/rhodecode/apps/admin/views/users.py @@ -26,7 +26,7 @@ from pyramid.view import view_config from sqlalchemy.sql.functions import coalesce from rhodecode.lib.helpers import Page -from rhodecode_tools.lib.ext_json import json +from rhodecode.lib.ext_json import json from rhodecode.apps._base import BaseAppView, DataGridAppView from rhodecode.lib.auth import ( From 208136ab4dc105ceaf54b25ab6ce2d2d96018b41 Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Tue, 16 May 2017 12:43:58 +0200 Subject: [PATCH 114/211] repo-settings: moved advanced setion into pyramid views. - improved delete method, and actions. --- rhodecode/apps/repository/__init__.py | 28 +++ .../repository/tests/test_repo_settings.py | 5 +- .../tests/test_repo_settings_advanced.py | 150 ++++++++++++ .../views/repo_settings_advanced.py | 225 ++++++++++++++++++ rhodecode/config/routing.py | 21 +- rhodecode/controllers/admin/repos.py | 144 ----------- rhodecode/lib/audit_logger.py | 5 +- rhodecode/public/js/rhodecode/routes.js | 6 + .../templates/admin/repos/repo_edit.mako | 2 +- .../admin/repos/repo_edit_advanced.mako | 33 ++- .../templates/data_table/_dt_elements.mako | 2 +- .../tests/functional/test_admin_repos.py | 91 ------- rhodecode/tests/functional/test_forks.py | 4 +- 13 files changed, 435 insertions(+), 281 deletions(-) create mode 100644 rhodecode/apps/repository/tests/test_repo_settings_advanced.py create mode 100644 rhodecode/apps/repository/views/repo_settings_advanced.py diff --git a/rhodecode/apps/repository/__init__.py b/rhodecode/apps/repository/__init__.py index 5fbfd135..9f9bb5ab 100644 --- a/rhodecode/apps/repository/__init__.py +++ b/rhodecode/apps/repository/__init__.py @@ -21,6 +21,11 @@ def includeme(config): + # Summary + config.add_route( + name='repo_summary_explicit', + pattern='/{repo_name:.*?[^/]}/summary', repo_route=True) + # Tags config.add_route( name='tags_home', @@ -41,6 +46,23 @@ def includeme(config): name='edit_repo', pattern='/{repo_name:.*?[^/]}/settings', repo_route=True) + # Settings advanced + config.add_route( + name='edit_repo_advanced', + pattern='/{repo_name:.*?[^/]}/settings/advanced', repo_route=True) + config.add_route( + name='edit_repo_advanced_delete', + pattern='/{repo_name:.*?[^/]}/settings/advanced/delete', repo_route=True) + config.add_route( + name='edit_repo_advanced_locking', + pattern='/{repo_name:.*?[^/]}/settings/advanced/locking', repo_route=True) + config.add_route( + name='edit_repo_advanced_journal', + pattern='/{repo_name:.*?[^/]}/settings/advanced/journal', repo_route=True) + config.add_route( + name='edit_repo_advanced_fork', + pattern='/{repo_name:.*?[^/]}/settings/advanced/fork', repo_route=True) + # Caches config.add_route( name='edit_repo_caches', @@ -77,5 +99,11 @@ def includeme(config): config.add_route( name='strip_execute', pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True) + + # NOTE(marcink): needs to be at the end for catch-all + # config.add_route( + # name='repo_summary', + # pattern='/{repo_name:.*?[^/]}', repo_route=True) + # Scan module for configuration decorators. config.scan() diff --git a/rhodecode/apps/repository/tests/test_repo_settings.py b/rhodecode/apps/repository/tests/test_repo_settings.py index 865f990a..9262dda9 100644 --- a/rhodecode/apps/repository/tests/test_repo_settings.py +++ b/rhodecode/apps/repository/tests/test_repo_settings.py @@ -38,6 +38,7 @@ def route_path(name, params=None, **kwargs): base_url = { 'edit_repo': '/{repo_name}/settings', + 'edit_repo_advanced': '/{repo_name}/settings/advanced', 'edit_repo_caches': '/{repo_name}/settings/caches', 'edit_repo_perms': '/{repo_name}/settings/permissions', }[name].format(**kwargs) @@ -62,6 +63,7 @@ class TestAdminRepoSettings(object): 'edit_repo', 'edit_repo_caches', 'edit_repo_perms', + 'edit_repo_advanced', ]) def test_show_page(self, urlname, app, backend): app.get(route_path(urlname, repo_name=backend.repo_name), status=200) @@ -74,10 +76,9 @@ class TestAdminRepoSettings(object): self.app.get(route_path('edit_repo', repo_name=backend_hg.repo_name)) @pytest.mark.parametrize('urlname', [ - 'edit_repo_advanced', 'repo_vcs_settings', - 'edit_repo_fields', 'repo_settings_issuetracker', + 'edit_repo_fields', 'edit_repo_remote', 'edit_repo_statistics', ]) diff --git a/rhodecode/apps/repository/tests/test_repo_settings_advanced.py b/rhodecode/apps/repository/tests/test_repo_settings_advanced.py new file mode 100644 index 00000000..523b5ac7 --- /dev/null +++ b/rhodecode/apps/repository/tests/test_repo_settings_advanced.py @@ -0,0 +1,150 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# 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 pytest + +from rhodecode.lib.utils2 import safe_unicode, safe_str +from rhodecode.model.db import Repository +from rhodecode.model.repo import RepoModel +from rhodecode.tests import ( + HG_REPO, GIT_REPO, assert_session_flash, no_newline_id_generator) +from rhodecode.tests.fixture import Fixture +from rhodecode.tests.utils import repo_on_filesystem + +fixture = Fixture() + + +def route_path(name, params=None, **kwargs): + import urllib + + base_url = { + 'repo_summary_explicit': '/{repo_name}/summary', + 'repo_summary': '/{repo_name}', + 'edit_repo_advanced': '/{repo_name}/settings/advanced', + 'edit_repo_advanced_delete': '/{repo_name}/settings/advanced/delete', + 'edit_repo_advanced_fork': '/{repo_name}/settings/advanced/fork', + 'edit_repo_advanced_locking': '/{repo_name}/settings/advanced/locking', + 'edit_repo_advanced_journal': '/{repo_name}/settings/advanced/journal', + + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +@pytest.mark.usefixtures('autologin_user', 'app') +class TestAdminRepoSettingsAdvanced(object): + + def test_set_repo_fork_has_no_self_id(self, autologin_user, backend): + repo = backend.repo + response = self.app.get( + route_path('edit_repo_advanced', repo_name=backend.repo_name)) + opt = """""" % repo.repo_id + response.mustcontain(no=[opt]) + + def test_set_fork_of_target_repo( + self, autologin_user, backend, csrf_token): + target_repo = 'target_%s' % backend.alias + fixture.create_repo(target_repo, repo_type=backend.alias) + repo2 = Repository.get_by_repo_name(target_repo) + response = self.app.post( + route_path('edit_repo_advanced_fork', repo_name=backend.repo_name), + params={'id_fork_of': repo2.repo_id, + 'csrf_token': csrf_token}) + repo = Repository.get_by_repo_name(backend.repo_name) + repo2 = Repository.get_by_repo_name(target_repo) + assert_session_flash( + response, + 'Marked repo %s as fork of %s' % (repo.repo_name, repo2.repo_name)) + + assert repo.fork == repo2 + response = response.follow() + # check if given repo is selected + + opt = 'This repository is a fork of %s' % ( + route_path('repo_summary', repo_name=repo2.repo_name), + repo2.repo_name) + + response.mustcontain(opt) + + fixture.destroy_repo(target_repo, forks='detach') + + @pytest.mark.backends("hg", "git") + def test_set_fork_of_other_type_repo( + self, autologin_user, backend, csrf_token): + TARGET_REPO_MAP = { + 'git': { + 'type': 'hg', + 'repo_name': HG_REPO}, + 'hg': { + 'type': 'git', + 'repo_name': GIT_REPO}, + } + target_repo = TARGET_REPO_MAP[backend.alias] + + repo2 = Repository.get_by_repo_name(target_repo['repo_name']) + response = self.app.post( + route_path('edit_repo_advanced_fork', repo_name=backend.repo_name), + params={'id_fork_of': repo2.repo_id, + 'csrf_token': csrf_token}) + assert_session_flash( + response, + 'Cannot set repository as fork of repository with other type') + + def test_set_fork_of_none(self, autologin_user, backend, csrf_token): + # mark it as None + response = self.app.post( + route_path('edit_repo_advanced_fork', repo_name=backend.repo_name), + params={'id_fork_of': None, '_method': 'put', + 'csrf_token': csrf_token}) + assert_session_flash( + response, + 'Marked repo %s as fork of %s' + % (backend.repo_name, "Nothing")) + assert backend.repo.fork is None + + def test_set_fork_of_same_repo(self, autologin_user, backend, csrf_token): + repo = Repository.get_by_repo_name(backend.repo_name) + response = self.app.post( + route_path('edit_repo_advanced_fork', repo_name=backend.repo_name), + params={'id_fork_of': repo.repo_id, 'csrf_token': csrf_token}) + assert_session_flash( + response, 'An error occurred during this operation') + + @pytest.mark.parametrize( + "suffix", + ['', u'ąęł' , '123'], + ids=no_newline_id_generator) + def test_advanced_delete(self, autologin_user, backend, suffix, csrf_token): + repo = backend.create_repo(name_suffix=suffix) + repo_name = repo.repo_name + repo_name_str = safe_str(repo.repo_name) + + response = self.app.post( + route_path('edit_repo_advanced_delete', repo_name=repo_name_str), + params={'csrf_token': csrf_token}) + assert_session_flash(response, + u'Deleted repository `{}`'.format(repo_name)) + response.follow() + + # check if repo was deleted from db + assert RepoModel().get_by_repo_name(repo_name) is None + assert not repo_on_filesystem(repo_name_str) diff --git a/rhodecode/apps/repository/views/repo_settings_advanced.py b/rhodecode/apps/repository/views/repo_settings_advanced.py new file mode 100644 index 00000000..c95ef398 --- /dev/null +++ b/rhodecode/apps/repository/views/repo_settings_advanced.py @@ -0,0 +1,225 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2011-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# 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 logging + +from pyramid.view import view_config +from pyramid.httpexceptions import HTTPFound + +from rhodecode.apps._base import RepoAppView +from rhodecode.lib import helpers as h +from rhodecode.lib import audit_logger +from rhodecode.lib.auth import ( + LoginRequired, HasRepoPermissionAnyDecorator, CSRFRequired) +from rhodecode.lib.exceptions import AttachedForksError +from rhodecode.lib.utils2 import safe_int +from rhodecode.lib.vcs import RepositoryError +from rhodecode.model.db import Session, UserFollowing, User, Repository +from rhodecode.model.repo import RepoModel +from rhodecode.model.scm import ScmModel + +log = logging.getLogger(__name__) + + +class RepoSettingsView(RepoAppView): + + def load_default_context(self): + c = self._get_local_tmpl_context() + + # TODO(marcink): remove repo_info and use c.rhodecode_db_repo instead + c.repo_info = self.db_repo + + self._register_global_c(c) + return c + + @LoginRequired() + @HasRepoPermissionAnyDecorator('repository.admin') + @view_config( + route_name='edit_repo_advanced', request_method='GET', + renderer='rhodecode:templates/admin/repos/repo_edit.mako') + def edit_advanced(self): + c = self.load_default_context() + c.active = 'advanced' + + c.default_user_id = User.get_default_user().user_id + c.in_public_journal = UserFollowing.query() \ + .filter(UserFollowing.user_id == c.default_user_id) \ + .filter(UserFollowing.follows_repository == c.repo_info).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') + + return self._get_template_context(c) + + @LoginRequired() + @HasRepoPermissionAnyDecorator('repository.admin') + @view_config( + route_name='edit_repo_advanced_delete', request_method='POST', + renderer='rhodecode:templates/admin/repos/repo_edit.mako') + def edit_advanced_delete(self): + """ + Deletes the repository, or shows warnings if deletion is not possible + because of attached forks or other errors. + """ + _ = self.request.translate + handle_forks = self.request.POST.get('forks', None) + + try: + _forks = self.db_repo.forks.count() + if _forks and handle_forks: + if handle_forks == 'detach_forks': + handle_forks = 'detach' + h.flash(_('Detached %s forks') % _forks, category='success') + elif handle_forks == 'delete_forks': + handle_forks = 'delete' + h.flash(_('Deleted %s forks') % _forks, category='success') + + repo_data = self.db_repo.get_api_data() + RepoModel().delete(self.db_repo, forks=handle_forks) + + repo = audit_logger.RepoWrap(repo_id=self.db_repo.repo_id, + repo_name=self.db_repo.repo_name) + audit_logger.store( + action='repo.delete', action_data={'repo_data': repo_data}, + user=self._rhodecode_user, repo=repo, commit=False) + + ScmModel().mark_for_invalidation(self.db_repo_name) + h.flash( + _('Deleted repository `%s`') % self.db_repo_name, + category='success') + Session().commit() + except AttachedForksError: + repo_advanced_url = h.route_path( + 'edit_repo_advanced', repo_name=self.db_repo_name, + _anchor='advanced-delete') + delete_anchor = h.link_to(_('detach or delete'), repo_advanced_url) + h.flash(_('Cannot delete `{repo}` it still contains attached forks. ' + 'Try using {delete_or_detach} option.') + .format(repo=self.db_repo_name, delete_or_detach=delete_anchor), + category='warning') + + # redirect to advanced for forks handle action ? + raise HTTPFound(repo_advanced_url) + + except Exception: + log.exception("Exception during deletion of repository") + h.flash(_('An error occurred during deletion of `%s`') + % self.db_repo_name, category='error') + # redirect to advanced for more deletion options + raise HTTPFound( + h.route_path('edit_repo_advanced', repo_name=self.db_repo_name), + _anchor='advanced-delete') + + raise HTTPFound(h.route_path('home')) + + @LoginRequired() + @HasRepoPermissionAnyDecorator('repository.admin') + @CSRFRequired() + @view_config( + route_name='edit_repo_advanced_journal', request_method='POST', + renderer='rhodecode:templates/admin/repos/repo_edit.mako') + def edit_advanced_journal(self): + """ + Set's this repository to be visible in public journal, + in other words making default user to follow this repo + """ + _ = self.request.translate + + try: + user_id = User.get_default_user().user_id + ScmModel().toggle_following_repo(self.db_repo.repo_id, user_id) + h.flash(_('Updated repository visibility in public journal'), + category='success') + Session().commit() + except Exception: + h.flash(_('An error occurred during setting this ' + 'repository in public journal'), + category='error') + + raise HTTPFound( + h.route_path('edit_repo_advanced', repo_name=self.db_repo_name)) + + @LoginRequired() + @HasRepoPermissionAnyDecorator('repository.admin') + @CSRFRequired() + @view_config( + route_name='edit_repo_advanced_fork', request_method='POST', + renderer='rhodecode:templates/admin/repos/repo_edit.mako') + def edit_advanced_fork(self): + """ + Mark given repository as a fork of another + """ + _ = self.request.translate + + new_fork_id = self.request.POST.get('id_fork_of') + try: + + if new_fork_id and not new_fork_id.isdigit(): + log.error('Given fork id %s is not an INT', new_fork_id) + + fork_id = safe_int(new_fork_id) + repo = ScmModel().mark_as_fork( + self.db_repo_name, fork_id, self._rhodecode_user.user_id) + fork = repo.fork.repo_name if repo.fork else _('Nothing') + Session().commit() + h.flash(_('Marked repo %s as fork of %s') % (self.db_repo_name, fork), + category='success') + except RepositoryError as e: + log.exception("Repository Error occurred") + h.flash(str(e), category='error') + except Exception as e: + log.exception("Exception while editing fork") + h.flash(_('An error occurred during this operation'), + category='error') + + raise HTTPFound( + h.route_path('edit_repo_advanced', repo_name=self.db_repo_name)) + + @LoginRequired() + @HasRepoPermissionAnyDecorator('repository.admin') + @CSRFRequired() + @view_config( + route_name='edit_repo_advanced_locking', request_method='POST', + renderer='rhodecode:templates/admin/repos/repo_edit.mako') + def edit_advanced_locking(self): + """ + Toggle locking of repository + """ + _ = self.request.translate + set_lock = self.request.POST.get('set_lock') + set_unlock = self.request.POST.get('set_unlock') + + try: + if set_lock: + Repository.lock(self.db_repo, self._rhodecode_user.user_id, + lock_reason=Repository.LOCK_WEB) + h.flash(_('Locked repository'), category='success') + elif set_unlock: + Repository.unlock(self.db_repo) + h.flash(_('Unlocked repository'), category='success') + except Exception as e: + log.exception("Exception during unlocking") + h.flash(_('An error occurred during unlocking'), category='error') + + raise HTTPFound( + h.route_path('edit_repo_advanced', repo_name=self.db_repo_name)) diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py index 54d88dc9..c72c2665 100644 --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -185,7 +185,7 @@ def make_map(config): #========================================================================== # MAIN PAGE - rmap.connect('home', '/', controller='home', action='index', jsroute=True) + rmap.connect('home', '/', controller='home', action='index') # ping and pylons error test rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping') @@ -674,30 +674,11 @@ def make_map(config): conditions={'method': ['DELETE'], 'function': check_repo}, requirements=URL_NAME_REQUIREMENTS) - rmap.connect('edit_repo_advanced', '/{repo_name}/settings/advanced', - controller='admin/repos', action='edit_advanced', - conditions={'method': ['GET'], 'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - - rmap.connect('edit_repo_advanced_locking', '/{repo_name}/settings/advanced/locking', - controller='admin/repos', action='edit_advanced_locking', - conditions={'method': ['PUT'], 'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) rmap.connect('toggle_locking', '/{repo_name}/settings/advanced/locking_toggle', controller='admin/repos', action='toggle_locking', conditions={'method': ['GET'], 'function': check_repo}, requirements=URL_NAME_REQUIREMENTS) - rmap.connect('edit_repo_advanced_journal', '/{repo_name}/settings/advanced/journal', - controller='admin/repos', action='edit_advanced_journal', - conditions={'method': ['PUT'], 'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - - rmap.connect('edit_repo_advanced_fork', '/{repo_name}/settings/advanced/fork', - controller='admin/repos', action='edit_advanced_fork', - conditions={'method': ['PUT'], 'function': check_repo}, - requirements=URL_NAME_REQUIREMENTS) - rmap.connect('edit_repo_remote', '/{repo_name}/settings/remote', controller='admin/repos', action='edit_remote_form', conditions={'method': ['GET'], 'function': check_repo}, diff --git a/rhodecode/controllers/admin/repos.py b/rhodecode/controllers/admin/repos.py index dd8998c3..22f265b3 100644 --- a/rhodecode/controllers/admin/repos.py +++ b/rhodecode/controllers/admin/repos.py @@ -288,51 +288,6 @@ class ReposController(BaseRepoController): return {'result': True} return {'result': False} - @HasRepoPermissionAllDecorator('repository.admin') - @auth.CSRFRequired() - def delete(self, repo_name): - """ - DELETE /repos/repo_name: Delete an existing item""" - # Forms posted to this method should contain a hidden field: - # - # Or using helpers: - # h.form(url('repo', repo_name=ID), - # method='delete') - # url('repo', repo_name=ID) - - repo_model = RepoModel() - repo = repo_model.get_by_repo_name(repo_name) - if not repo: - h.not_mapped_error(repo_name) - return redirect(url('repos')) - try: - _forks = repo.forks.count() - handle_forks = None - if _forks and request.POST.get('forks'): - do = request.POST['forks'] - if do == 'detach_forks': - handle_forks = 'detach' - h.flash(_('Detached %s forks') % _forks, category='success') - elif do == 'delete_forks': - handle_forks = 'delete' - h.flash(_('Deleted %s forks') % _forks, category='success') - repo_model.delete(repo, forks=handle_forks) - action_logger(c.rhodecode_user, 'admin_deleted_repo', - repo_name, self.ip_addr, self.sa) - ScmModel().mark_for_invalidation(repo_name) - h.flash(_('Deleted repository %s') % repo_name, category='success') - Session().commit() - except AttachedForksError: - h.flash(_('Cannot delete %s it still contains attached forks') - % repo_name, category='warning') - - except Exception: - log.exception("Exception during deletion of repository") - h.flash(_('An error occurred during deletion of %s') % repo_name, - category='error') - - return redirect(url('repos')) - @HasPermissionAllDecorator('hg.admin') def show(self, repo_name, format='html'): """GET /repos/repo_name: Show a specific item""" @@ -384,105 +339,6 @@ class ReposController(BaseRepoController): h.flash(msg, category='error') return redirect(url('edit_repo_fields', repo_name=repo_name)) - @HasRepoPermissionAllDecorator('repository.admin') - def edit_advanced(self, repo_name): - """GET /repo_name/settings: Form to edit an existing item""" - c.repo_info = self._load_repo(repo_name) - c.default_user_id = User.get_default_user().user_id - c.in_public_journal = UserFollowing.query()\ - .filter(UserFollowing.user_id == c.default_user_id)\ - .filter(UserFollowing.follows_repository == c.repo_info).scalar() - - c.active = 'advanced' - c.has_origin_repo_read_perm = False - if c.repo_info.fork: - c.has_origin_repo_read_perm = h.HasRepoPermissionAny( - 'repository.write', 'repository.read', 'repository.admin')( - c.repo_info.fork.repo_name, 'repo set as fork page') - - if request.POST: - return redirect(url('repo_edit_advanced')) - return render('admin/repos/repo_edit.mako') - - @HasRepoPermissionAllDecorator('repository.admin') - @auth.CSRFRequired() - def edit_advanced_journal(self, repo_name): - """ - Set's this repository to be visible in public journal, - in other words assing default user to follow this repo - - :param repo_name: - """ - - try: - repo_id = Repository.get_by_repo_name(repo_name).repo_id - user_id = User.get_default_user().user_id - self.scm_model.toggle_following_repo(repo_id, user_id) - h.flash(_('Updated repository visibility in public journal'), - category='success') - Session().commit() - except Exception: - h.flash(_('An error occurred during setting this' - ' repository in public journal'), - category='error') - - return redirect(url('edit_repo_advanced', repo_name=repo_name)) - - @HasRepoPermissionAllDecorator('repository.admin') - @auth.CSRFRequired() - def edit_advanced_fork(self, repo_name): - """ - Mark given repository as a fork of another - - :param repo_name: - """ - - new_fork_id = request.POST.get('id_fork_of') - try: - - if new_fork_id and not new_fork_id.isdigit(): - log.error('Given fork id %s is not an INT', new_fork_id) - - fork_id = safe_int(new_fork_id) - repo = ScmModel().mark_as_fork(repo_name, fork_id, - c.rhodecode_user.username) - fork = repo.fork.repo_name if repo.fork else _('Nothing') - Session().commit() - h.flash(_('Marked repo %s as fork of %s') % (repo_name, fork), - category='success') - except RepositoryError as e: - log.exception("Repository Error occurred") - h.flash(str(e), category='error') - except Exception as e: - log.exception("Exception while editing fork") - h.flash(_('An error occurred during this operation'), - category='error') - - return redirect(url('edit_repo_advanced', repo_name=repo_name)) - - @HasRepoPermissionAllDecorator('repository.admin') - @auth.CSRFRequired() - def edit_advanced_locking(self, repo_name): - """ - Unlock repository when it is locked ! - - :param repo_name: - """ - try: - repo = Repository.get_by_repo_name(repo_name) - if request.POST.get('set_lock'): - Repository.lock(repo, c.rhodecode_user.user_id, - lock_reason=Repository.LOCK_WEB) - h.flash(_('Locked repository'), category='success') - elif request.POST.get('set_unlock'): - Repository.unlock(repo) - h.flash(_('Unlocked repository'), category='success') - except Exception as e: - log.exception("Exception during unlocking") - h.flash(_('An error occurred during unlocking'), - category='error') - return redirect(url('edit_repo_advanced', repo_name=repo_name)) - @HasRepoPermissionAnyDecorator('repository.write', 'repository.admin') @auth.CSRFRequired() def toggle_locking(self, repo_name): diff --git a/rhodecode/lib/audit_logger.py b/rhodecode/lib/audit_logger.py index 7a7b2093..183af07c 100644 --- a/rhodecode/lib/audit_logger.py +++ b/rhodecode/lib/audit_logger.py @@ -36,9 +36,10 @@ ACTIONS = { 'user.push': {}, 'user.pull': {}, - 'repo.add': {}, + 'repo.create': {}, 'repo.edit': {}, 'repo.edit.permissions': {}, + 'repo.delete': {}, 'repo.commit.strip': {}, 'repo.archive.download': {}, } @@ -101,7 +102,7 @@ def store( audit_logger.store( action='repo.edit', user=self._rhodecode_user) audit_logger.store( - action='repo.delete', + action='repo.delete', action_data={'repo_data': repo_data}, user=audit_logger.UserWrap(username='itried-login', ip_addr='8.8.8.8')) # repo action diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js index ce6a741b..425d4214 100644 --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -96,10 +96,16 @@ function registerRCRoutes() { pyroutes.register('user_group_autocomplete_data', '/_user_groups', []); pyroutes.register('repo_list_data', '/_repos', []); pyroutes.register('goto_switcher_data', '/_goto_data', []); + pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']); pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']); pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']); pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']); pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']); + pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']); + pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']); + pyroutes.register('edit_repo_advanced_locking', '/%(repo_name)s/settings/advanced/locking', ['repo_name']); + pyroutes.register('edit_repo_advanced_journal', '/%(repo_name)s/settings/advanced/journal', ['repo_name']); + pyroutes.register('edit_repo_advanced_fork', '/%(repo_name)s/settings/advanced/fork', ['repo_name']); pyroutes.register('edit_repo_caches', '/%(repo_name)s/settings/caches', ['repo_name']); pyroutes.register('edit_repo_perms', '/%(repo_name)s/settings/permissions', ['repo_name']); pyroutes.register('repo_reviewers', '/%(repo_name)s/settings/review/rules', ['repo_name']); diff --git a/rhodecode/templates/admin/repos/repo_edit.mako b/rhodecode/templates/admin/repos/repo_edit.mako index 1a00f1ed..e27a6ada 100644 --- a/rhodecode/templates/admin/repos/repo_edit.mako +++ b/rhodecode/templates/admin/repos/repo_edit.mako @@ -45,7 +45,7 @@ ${_('Permissions')}
  • - ${_('Advanced')} + ${_('Advanced')}
  • ${_('VCS')} diff --git a/rhodecode/templates/admin/repos/repo_edit_advanced.mako b/rhodecode/templates/admin/repos/repo_edit_advanced.mako index f71c8d5c..529843a4 100644 --- a/rhodecode/templates/admin/repos/repo_edit_advanced.mako +++ b/rhodecode/templates/admin/repos/repo_edit_advanced.mako @@ -10,8 +10,8 @@ %>
    -
    -

    ${_('Repository: %s') % c.repo_info.repo_name}

    +
    +

    ${_('Repository: %s') % c.repo_info.repo_name}

    ${base.dt_info_panel(elems)} @@ -20,11 +20,11 @@
    -
    -

    ${_('Fork Reference')}

    +
    +

    ${_('Fork Reference')}

    - ${h.secure_form(url('edit_repo_advanced_fork', repo_name=c.repo_info.repo_name), method='put')} + ${h.secure_form(h.route_path('edit_repo_advanced_fork', repo_name=c.repo_info.repo_name), method='POST')} % if c.repo_info.fork:
    ${h.literal(_('This repository is a fork of %(repo_link)s') % {'repo_link': h.link_to_if(c.has_origin_repo_read_perm,c.repo_info.fork.repo_name, h.url('summary_home', repo_name=c.repo_info.fork.repo_name))})} @@ -44,15 +44,14 @@
    -
    -

    ${_('Public Journal Visibility')}

    +
    +

    ${_('Public Journal Visibility')}

    - ${h.secure_form(url('edit_repo_advanced_journal', repo_name=c.repo_info.repo_name), method='put')} + ${h.secure_form(h.route_path('edit_repo_advanced_journal', repo_name=c.repo_info.repo_name), method='POST')}
    %if c.in_public_journal: %else: @@ -70,11 +69,11 @@
    -
    -

    ${_('Locking state')}

    +
    +

    ${_('Locking state')}

    - ${h.secure_form(url('edit_repo_advanced_locking', repo_name=c.repo_info.repo_name), method='put')} + ${h.secure_form(h.route_path('edit_repo_advanced_locking', repo_name=c.repo_info.repo_name), method='POST')} %if c.repo_info.locked[0]:
    ${'Locked by %s on %s. Lock reason: %s' % (h.person_by_id(c.repo_info.locked[0]), @@ -110,15 +109,15 @@
    -
    -

    ${_('Delete repository')}

    +
    +

    ${_('Delete repository')}

    - ${h.secure_form(url('repo', repo_name=c.repo_name),method='delete')} + ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=c.repo_name), method='POST')}
    - ${ungettext('This repository has %s fork.', 'This repository has %s forks.', c.repo_info. forks.count()) % c.repo_info.forks.count()} + ${_ungettext('This repository has %s fork.', 'This repository has %s forks.', c.repo_info.forks.count()) % c.repo_info.forks.count()} %if c.repo_info.forks.count(): @@ -143,7 +142,7 @@
    - ${_('This repository will be renamed in a special way in order to make it inaccessible to RhodeCode Enterprise and its VCS systems. If you need to fully delete it from the file system, please do it manually, or with rhodecode-cleanup-repos command.')} + ${_('This repository will be renamed in a special way in order to make it inaccessible to RhodeCode Enterprise and its VCS systems. If you need to fully delete it from the file system, please do it manually, or with rhodecode-cleanup-repos command available in rhodecode-tools.')}
    diff --git a/rhodecode/templates/data_table/_dt_elements.mako b/rhodecode/templates/data_table/_dt_elements.mako index 26811250..ac2de551 100644 --- a/rhodecode/templates/data_table/_dt_elements.mako +++ b/rhodecode/templates/data_table/_dt_elements.mako @@ -119,7 +119,7 @@ Edit
    - ${h.secure_form(h.url('repo', repo_name=repo_name),method='delete')} + ${h.secure_form(h.route_path('edit_repo_advanced_delete', repo_name=repo_name), method='POST')} ${h.submit('remove_%s' % repo_name,_('Delete'),class_="btn btn-link btn-danger", onclick="return confirm('"+_('Confirm to delete this repository: %s') % repo_name+"');")} ${h.end_form()} diff --git a/rhodecode/tests/functional/test_admin_repos.py b/rhodecode/tests/functional/test_admin_repos.py index 5f7a63f4..92e4629d 100644 --- a/rhodecode/tests/functional/test_admin_repos.py +++ b/rhodecode/tests/functional/test_admin_repos.py @@ -382,21 +382,6 @@ class TestAdminRepos(object): csrf_token=csrf_token)) response.mustcontain('Repository name cannot end with .git') - @pytest.mark.parametrize("suffix", [u'', u'ąęł'], ids=['', 'non-ascii']) - def test_delete(self, autologin_user, backend, suffix, csrf_token): - repo = backend.create_repo(name_suffix=suffix) - repo_name = repo.repo_name - - response = self.app.post(url('repo', repo_name=repo_name), - params={'_method': 'delete', - 'csrf_token': csrf_token}) - assert_session_flash(response, 'Deleted repository %s' % (repo_name)) - response.follow() - - # check if repo was deleted from db - assert RepoModel().get_by_repo_name(repo_name) is None - assert not repo_on_filesystem(repo_name) - def test_show(self, autologin_user, backend): self.app.get(url('repo', repo_name=backend.repo_name)) @@ -414,82 +399,6 @@ class TestAdminRepos(object): assert permissions[0].permission.permission_name == 'repository.none' assert permissions[0].repository.private is True - def test_set_repo_fork_has_no_self_id(self, autologin_user, backend): - repo = backend.repo - response = self.app.get( - url('edit_repo_advanced', repo_name=backend.repo_name)) - opt = """""" % repo.repo_id - response.mustcontain(no=[opt]) - - def test_set_fork_of_target_repo( - self, autologin_user, backend, csrf_token): - target_repo = 'target_%s' % backend.alias - fixture.create_repo(target_repo, repo_type=backend.alias) - repo2 = Repository.get_by_repo_name(target_repo) - response = self.app.post( - url('edit_repo_advanced_fork', repo_name=backend.repo_name), - params={'id_fork_of': repo2.repo_id, '_method': 'put', - 'csrf_token': csrf_token}) - repo = Repository.get_by_repo_name(backend.repo_name) - repo2 = Repository.get_by_repo_name(target_repo) - assert_session_flash( - response, - 'Marked repo %s as fork of %s' % (repo.repo_name, repo2.repo_name)) - - assert repo.fork == repo2 - response = response.follow() - # check if given repo is selected - - opt = 'This repository is a fork of %s' % ( - url('summary_home', repo_name=repo2.repo_name), repo2.repo_name) - - response.mustcontain(opt) - - fixture.destroy_repo(target_repo, forks='detach') - - @pytest.mark.backends("hg", "git") - def test_set_fork_of_other_type_repo(self, autologin_user, backend, - csrf_token): - TARGET_REPO_MAP = { - 'git': { - 'type': 'hg', - 'repo_name': HG_REPO}, - 'hg': { - 'type': 'git', - 'repo_name': GIT_REPO}, - } - target_repo = TARGET_REPO_MAP[backend.alias] - - repo2 = Repository.get_by_repo_name(target_repo['repo_name']) - response = self.app.post( - url('edit_repo_advanced_fork', repo_name=backend.repo_name), - params={'id_fork_of': repo2.repo_id, '_method': 'put', - 'csrf_token': csrf_token}) - assert_session_flash( - response, - 'Cannot set repository as fork of repository with other type') - - def test_set_fork_of_none(self, autologin_user, backend, csrf_token): - # mark it as None - response = self.app.post( - url('edit_repo_advanced_fork', repo_name=backend.repo_name), - params={'id_fork_of': None, '_method': 'put', - 'csrf_token': csrf_token}) - assert_session_flash( - response, - 'Marked repo %s as fork of %s' - % (backend.repo_name, "Nothing")) - assert backend.repo.fork is None - - def test_set_fork_of_same_repo(self, autologin_user, backend, csrf_token): - repo = Repository.get_by_repo_name(backend.repo_name) - response = self.app.post( - url('edit_repo_advanced_fork', repo_name=backend.repo_name), - params={'id_fork_of': repo.repo_id, '_method': 'put', - 'csrf_token': csrf_token}) - assert_session_flash( - response, 'An error occurred during this operation') - def test_create_on_top_level_without_permissions(self, backend): session = login_user_session( self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS) diff --git a/rhodecode/tests/functional/test_forks.py b/rhodecode/tests/functional/test_forks.py index 1769b680..5bd807b4 100644 --- a/rhodecode/tests/functional/test_forks.py +++ b/rhodecode/tests/functional/test_forks.py @@ -106,9 +106,7 @@ class _BaseTest(TestController): ) # remove this fork - response = self.app.post( - url('repo', repo_name=fork_name), - params={'_method': 'delete', 'csrf_token': self.csrf_token}) + fixture.destroy_repo(fork_name) def test_fork_create_into_group(self): self.log_user() From 5c453c1cb6c7e953ed07645717ae851f9f1970b3 Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Tue, 16 May 2017 14:48:07 +0200 Subject: [PATCH 115/211] audit-logger: unify calls to repo.delete and also store source of call, api/web. --- rhodecode/api/tests/test_delete_repo.py | 14 ++++++++------ rhodecode/api/views/repo_api.py | 19 +++++++++++++++---- .../views/repo_settings_advanced.py | 7 ++++--- rhodecode/public/js/rhodecode/routes.js | 2 +- 4 files changed, 28 insertions(+), 14 deletions(-) diff --git a/rhodecode/api/tests/test_delete_repo.py b/rhodecode/api/tests/test_delete_repo.py index d7ea6596..cab42790 100644 --- a/rhodecode/api/tests/test_delete_repo.py +++ b/rhodecode/api/tests/test_delete_repo.py @@ -30,43 +30,45 @@ from rhodecode.api.tests.utils import ( class TestApiDeleteRepo(object): def test_api_delete_repo(self, backend): repo = backend.create_repo() - + repo_name = repo.repo_name id_, params = build_data( self.apikey, 'delete_repo', repoid=repo.repo_name, ) response = api_call(self.app, params) expected = { - 'msg': 'Deleted repository `%s`' % (repo.repo_name,), + 'msg': 'Deleted repository `%s`' % (repo_name,), 'success': True } assert_ok(id_, expected, given=response.body) def test_api_delete_repo_by_non_admin(self, backend, user_regular): repo = backend.create_repo(cur_user=user_regular.username) + repo_name = repo.repo_name id_, params = build_data( user_regular.api_key, 'delete_repo', repoid=repo.repo_name, ) response = api_call(self.app, params) expected = { - 'msg': 'Deleted repository `%s`' % (repo.repo_name,), + 'msg': 'Deleted repository `%s`' % (repo_name,), 'success': True } assert_ok(id_, expected, given=response.body) def test_api_delete_repo_by_non_admin_no_permission(self, backend): repo = backend.create_repo() + repo_name = repo.repo_name id_, params = build_data( self.apikey_regular, 'delete_repo', repoid=repo.repo_name, ) response = api_call(self.app, params) - expected = 'repository `%s` does not exist' % (repo.repo_name) + expected = 'repository `%s` does not exist' % (repo_name) assert_error(id_, expected, given=response.body) def test_api_delete_repo_exception_occurred(self, backend): repo = backend.create_repo() + repo_name = repo.repo_name id_, params = build_data( self.apikey, 'delete_repo', repoid=repo.repo_name, ) with mock.patch.object(RepoModel, 'delete', crash): response = api_call(self.app, params) - expected = 'failed to delete repository `%s`' % ( - repo.repo_name,) + expected = 'failed to delete repository `%s`' % (repo_name,) assert_error(id_, expected, given=response.body) diff --git a/rhodecode/api/views/repo_api.py b/rhodecode/api/views/repo_api.py index 031a642d..f78f4daf 100644 --- a/rhodecode/api/views/repo_api.py +++ b/rhodecode/api/views/repo_api.py @@ -29,6 +29,7 @@ from rhodecode.api.utils import ( get_user_group_or_error, get_user_or_error, validate_repo_permissions, get_perm_or_error, parse_args, get_origin, build_commit_data, validate_set_owner_permissions) +from rhodecode.lib import audit_logger from rhodecode.lib import repo_maintenance from rhodecode.lib.auth import HasPermissionAnyApi, HasUserGroupPermissionAnyApi from rhodecode.lib.utils2 import str2bool, time_to_datetime @@ -1154,6 +1155,7 @@ def delete_repo(request, apiuser, repoid, forks=Optional('')): """ repo = get_repo_or_error(repoid) + repo_name = repo.repo_name if not has_superadmin_permission(apiuser): _perms = ('repository.admin',) validate_repo_permissions(apiuser, repoid, repo, _perms) @@ -1171,18 +1173,27 @@ def delete_repo(request, apiuser, repoid, forks=Optional('')): 'Cannot delete `%s` it still contains attached forks' % (repo.repo_name,) ) - + repo_data = repo.get_api_data() RepoModel().delete(repo, forks=forks) + + repo = audit_logger.RepoWrap(repo_id=None, + repo_name=repo.repo_name) + + audit_logger.store( + action='repo.delete', + action_data={'repo_data': repo_data, 'source': 'api_call'}, + user=apiuser, repo=repo, commit=False) + + ScmModel().mark_for_invalidation(repo_name, delete=True) Session().commit() return { - 'msg': 'Deleted repository `%s`%s' % ( - repo.repo_name, _forks_msg), + 'msg': 'Deleted repository `%s`%s' % (repo_name, _forks_msg), 'success': True } except Exception: log.exception("Exception occurred while trying to delete repo") raise JSONRPCError( - 'failed to delete repository `%s`' % (repo.repo_name,) + 'failed to delete repository `%s`' % (repo_name,) ) diff --git a/rhodecode/apps/repository/views/repo_settings_advanced.py b/rhodecode/apps/repository/views/repo_settings_advanced.py index c95ef398..8948e4f7 100644 --- a/rhodecode/apps/repository/views/repo_settings_advanced.py +++ b/rhodecode/apps/repository/views/repo_settings_advanced.py @@ -97,13 +97,14 @@ class RepoSettingsView(RepoAppView): repo_data = self.db_repo.get_api_data() RepoModel().delete(self.db_repo, forks=handle_forks) - repo = audit_logger.RepoWrap(repo_id=self.db_repo.repo_id, + repo = audit_logger.RepoWrap(repo_id=None, repo_name=self.db_repo.repo_name) audit_logger.store( - action='repo.delete', action_data={'repo_data': repo_data}, + action='repo.delete', + action_data={'repo_data': repo_data, 'source': 'web_action'}, user=self._rhodecode_user, repo=repo, commit=False) - ScmModel().mark_for_invalidation(self.db_repo_name) + ScmModel().mark_for_invalidation(self.db_repo_name, delete=True) h.flash( _('Deleted repository `%s`') % self.db_repo_name, category='success') diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js index 425d4214..92dad0cd 100644 --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -12,7 +12,6 @@ ******************************************************************************/ function registerRCRoutes() { // routes registration - pyroutes.register('home', '/', []); pyroutes.register('new_repo', '/_admin/create_repository', []); pyroutes.register('edit_user', '/_admin/users/%(user_id)s/edit', ['user_id']); pyroutes.register('edit_user_group_members', '/_admin/user_groups/%(user_group_id)s/edit/members', ['user_group_id']); @@ -92,6 +91,7 @@ function registerRCRoutes() { pyroutes.register('register', '/_admin/register', []); pyroutes.register('reset_password', '/_admin/password_reset', []); pyroutes.register('reset_password_confirmation', '/_admin/password_reset_confirmation', []); + pyroutes.register('home', '/', []); pyroutes.register('user_autocomplete_data', '/_users', []); pyroutes.register('user_group_autocomplete_data', '/_user_groups', []); pyroutes.register('repo_list_data', '/_repos', []); From 59200825e30f5b4c3a3cc87220539ff4eb26c330 Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Wed, 17 May 2017 16:24:34 +0200 Subject: [PATCH 116/211] hooks: added pushkey Mercurial hook. - This will be needed to extract pushed Bookmarks. --- rhodecode/__init__.py | 2 +- .../dbmigrate/versions/073_version_4_8_0.py | 63 +++++++++++++++++++ rhodecode/model/db.py | 1 + 3 files changed, 65 insertions(+), 1 deletion(-) create mode 100644 rhodecode/lib/dbmigrate/versions/073_version_4_8_0.py diff --git a/rhodecode/__init__.py b/rhodecode/__init__.py index e4be6a1a..531167a8 100644 --- a/rhodecode/__init__.py +++ b/rhodecode/__init__.py @@ -51,7 +51,7 @@ PYRAMID_SETTINGS = {} EXTENSIONS = {} __version__ = ('.'.join((str(each) for each in VERSION[:3]))) -__dbversion__ = 72 # defines current db version for migrations +__dbversion__ = 73 # defines current db version for migrations __platform__ = platform.system() __license__ = 'AGPLv3, and Commercial License' __author__ = 'RhodeCode GmbH' diff --git a/rhodecode/lib/dbmigrate/versions/073_version_4_8_0.py b/rhodecode/lib/dbmigrate/versions/073_version_4_8_0.py new file mode 100644 index 00000000..6928cf0e --- /dev/null +++ b/rhodecode/lib/dbmigrate/versions/073_version_4_8_0.py @@ -0,0 +1,63 @@ +import logging + +from sqlalchemy import * +from rhodecode.model import meta +from rhodecode.lib.dbmigrate.versions import _reset_base, notify + +log = logging.getLogger(__name__) + + +def get_by_key(cls, key): + return cls.query().filter(cls.ui_key == key).scalar() + + +def create_or_update_hook(cls, key, val, SESSION): + new_ui = get_by_key(cls, key) or cls() + new_ui.ui_section = 'hooks' + new_ui.ui_active = True + new_ui.ui_key = key + new_ui.ui_value = val + + SESSION().add(new_ui) + + +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_7_0_0 as db + + # issue fixups + fixups(db, meta.Session) + + +def downgrade(migrate_engine): + meta = MetaData() + meta.bind = migrate_engine + + +def fixups(models, _SESSION): + + cleanup_if_present = ( + 'pushkey.key_push', + ) + + for hook in cleanup_if_present: + ui_cfg = models.RhodeCodeUi.query().filter( + models.RhodeCodeUi.ui_key == hook).scalar() + if ui_cfg is not None: + log.info('Removing RhodeCodeUI for hook "%s".', hook) + _SESSION().delete(ui_cfg) + + to_add = [ + ('pushkey.key_push', + 'python:vcsserver.hooks.key_push'), + ] + + for hook, value in to_add: + log.info('Adding RhodeCodeUI for hook "%s".', hook) + create_or_update_hook(models.RhodeCodeUi, hook, value, _SESSION) + + _SESSION().commit() diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py index 284add7e..ac7f7f25 100644 --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -358,6 +358,7 @@ class RhodeCodeUi(Base, BaseModel): HOOK_PRE_PUSH = 'prechangegroup.pre_push' HOOK_PRETX_PUSH = 'pretxnchangegroup.pre_push' HOOK_PUSH = 'changegroup.push_logger' + HOOK_PUSH_KEY = 'pushkey.key_push' # TODO: johbo: Unify way how hooks are configured for git and hg, # git part is currently hardcoded. From 726bc5aba853f69ad1d459e06c1958f572cca98a Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Wed, 17 May 2017 16:42:00 +0200 Subject: [PATCH 117/211] hooks: deprecate old action_logger --- rhodecode/lib/hooks_base.py | 16 ++++------------ rhodecode/tests/functional/test_pullrequests.py | 10 ++++++++-- rhodecode/tests/lib/test_hooks_base.py | 17 +++++++---------- 3 files changed, 19 insertions(+), 24 deletions(-) diff --git a/rhodecode/lib/hooks_base.py b/rhodecode/lib/hooks_base.py index 76b5f6f5..c6ded4e4 100644 --- a/rhodecode/lib/hooks_base.py +++ b/rhodecode/lib/hooks_base.py @@ -31,7 +31,6 @@ import rhodecode from rhodecode import events from rhodecode.lib import helpers as h from rhodecode.lib import audit_logger -from rhodecode.lib.utils import action_logger from rhodecode.lib.utils2 import safe_str from rhodecode.lib.exceptions import HTTPLockedRC, UserCreationError from rhodecode.model.db import Repository, User @@ -153,9 +152,6 @@ def pre_pull(extras): def post_pull(extras): """Hook executed after client pulls the code.""" - user = User.get_by_username(extras.username) - action = 'pull' - action_logger(user, action, extras.repository, extras.ip, commit=True) audit_user = audit_logger.UserWrap( username=extras.username, @@ -175,6 +171,7 @@ def post_pull(extras): output = '' # make lock is a tri state False, True, None. We only make lock on True if extras.make_lock is True and not is_shadow_repo(extras): + user = User.get_by_username(extras.username) Repository.lock(Repository.get_by_repo_name(extras.repository), user.user_id, lock_reason=Repository.LOCK_PULL) @@ -195,16 +192,11 @@ def post_pull(extras): def post_push(extras): """Hook executed after user pushes to the repository.""" - action_tmpl = extras.action + ':%s' - commit_ids = extras.commit_ids[:29000] - - action = action_tmpl % ','.join(commit_ids) - action_logger( - extras.username, action, extras.repository, extras.ip, commit=True) + commit_ids = extras.commit_ids + # log the push call audit_user = audit_logger.UserWrap( - username=extras.username, - ip_addr=extras.ip) + username=extras.username, ip_addr=extras.ip) repo = audit_logger.RepoWrap(repo_name=extras.repository) audit_logger.store( action='user.push', action_data={ diff --git a/rhodecode/tests/functional/test_pullrequests.py b/rhodecode/tests/functional/test_pullrequests.py index e7ee3899..4bed42f0 100644 --- a/rhodecode/tests/functional/test_pullrequests.py +++ b/rhodecode/tests/functional/test_pullrequests.py @@ -539,7 +539,7 @@ class TestPullrequestsController: # Check the relevant log entries were added user_logs = UserLog.query() \ .filter(UserLog.version == UserLog.VERSION_1) \ - .order_by('-user_log_id').limit(4) + .order_by('-user_log_id').limit(3) actions = [log.action for log in user_logs] pr_commit_ids = PullRequestModel()._get_commit_ids(pull_request) expected_actions = [ @@ -547,10 +547,16 @@ class TestPullrequestsController: u'user_merged_pull_request:%d' % pull_request_id, # The action below reflect that the post push actions were executed u'user_commented_pull_request:%d' % pull_request_id, - u'push:%s' % ','.join(pr_commit_ids), ] assert actions == expected_actions + user_logs = UserLog.query() \ + .filter(UserLog.version == UserLog.VERSION_2) \ + .order_by('-user_log_id').limit(1) + actions = [log.action for log in user_logs] + assert actions == ['user.push'] + assert user_logs[0].action_data['commit_ids'] == pr_commit_ids + # Check post_push rcextension was really executed push_calls = rhodecode.EXTENSIONS.calls['post_push'] assert len(push_calls) == 1 diff --git a/rhodecode/tests/lib/test_hooks_base.py b/rhodecode/tests/lib/test_hooks_base.py index 6e54c6e8..b1f956ee 100644 --- a/rhodecode/tests/lib/test_hooks_base.py +++ b/rhodecode/tests/lib/test_hooks_base.py @@ -20,15 +20,10 @@ import mock import pytest - +from rhodecode.model.db import Session, UserLog from rhodecode.lib import hooks_base, utils2 -@mock.patch.multiple( - hooks_base, - action_logger=mock.Mock(), - post_push_extension=mock.Mock(), - Repository=mock.Mock()) def test_post_push_truncates_commits(user_regular, repo_stub): extras = { 'ip': '127.0.0.1', @@ -49,11 +44,13 @@ def test_post_push_truncates_commits(user_regular, repo_stub): hooks_base.post_push(extras) # Calculate appropriate action string here - expected_action = 'push_local:%s' % ','.join(extras.commit_ids[:29000]) + commit_ids = extras.commit_ids[:10000] - hooks_base.action_logger.assert_called_with( - extras.username, expected_action, extras.repository, extras.ip, - commit=True) + entry = UserLog.query().order_by('-user_log_id').first() + assert entry.action == 'user.push' + assert entry.action_data['commit_ids'] == commit_ids + Session().delete(entry) + Session().commit() def assert_called_with_mock(callable_, expected_mock_name): From b942ed82f2be71f4cfa6e636d6b7ff6f77005244 Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Wed, 17 May 2017 22:17:09 +0200 Subject: [PATCH 118/211] hooks: expose refs on push. - allows easier pull request creation --- rhodecode/lib/db_manage.py | 1 + rhodecode/lib/hooks_base.py | 14 +- rhodecode/lib/utils.py | 1 + rhodecode/model/settings.py | 3 +- rhodecode/tests/lib/test_utils.py | 8 +- .../tests/other/vcs_operations/__init__.py | 7 + .../tests/other/vcs_operations/conftest.py | 3 + .../vcs_operations/test_vcs_operations.py | 178 +++++++++++++++++- 8 files changed, 208 insertions(+), 7 deletions(-) diff --git a/rhodecode/lib/db_manage.py b/rhodecode/lib/db_manage.py index 2b4b5a5d..3668517b 100644 --- a/rhodecode/lib/db_manage.py +++ b/rhodecode/lib/db_manage.py @@ -319,6 +319,7 @@ class DbManage(object): (RhodeCodeUi.HOOK_PRE_PUSH, 'python:vcsserver.hooks.pre_push'), (RhodeCodeUi.HOOK_PRETX_PUSH, 'python:vcsserver.hooks.pre_push'), (RhodeCodeUi.HOOK_PUSH, 'python:vcsserver.hooks.log_push_action'), + (RhodeCodeUi.HOOK_PUSH_KEY, 'python:vcsserver.hooks.key_push'), ] diff --git a/rhodecode/lib/hooks_base.py b/rhodecode/lib/hooks_base.py index c6ded4e4..1d919b01 100644 --- a/rhodecode/lib/hooks_base.py +++ b/rhodecode/lib/hooks_base.py @@ -232,8 +232,20 @@ def post_push(extras): # 2xx Codes don't raise exceptions output += _http_ret.title - output += 'RhodeCode: push completed\n' + if extras.new_refs: + tmpl = \ + extras.server_url + '/' + \ + extras.repository + \ + "/pull-request/new?{ref_type}={ref_name}" + for branch_name in extras.new_refs['branches']: + output += 'RhodeCode: open pull request link: {}\n'.format( + tmpl.format(ref_type='branch', ref_name=branch_name)) + for book_name in extras.new_refs['bookmarks']: + output += 'RhodeCode: open pull request link: {}\n'.format( + tmpl.format(ref_type='bookmark', ref_name=book_name)) + + output += 'RhodeCode: push completed\n' return HookResponse(0, output) diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py index 32eed5a9..a3f4fe3d 100644 --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -428,6 +428,7 @@ def config_data_from_db(clear_session=True, repo=None): if 'push' not in enabled_hook_classes: skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRE_PUSH)) skip_entries.append(('hooks', RhodeCodeUi.HOOK_PRETX_PUSH)) + skip_entries.append(('hooks', RhodeCodeUi.HOOK_PUSH_KEY)) config = [entry for entry in config if entry[:2] not in skip_entries] diff --git a/rhodecode/model/settings.py b/rhodecode/model/settings.py index 4770626a..560264cf 100644 --- a/rhodecode/model/settings.py +++ b/rhodecode/model/settings.py @@ -52,7 +52,8 @@ class SettingsModel(BaseModel): BUILTIN_HOOKS = ( RhodeCodeUi.HOOK_REPO_SIZE, RhodeCodeUi.HOOK_PUSH, RhodeCodeUi.HOOK_PRE_PUSH, RhodeCodeUi.HOOK_PRETX_PUSH, - RhodeCodeUi.HOOK_PULL, RhodeCodeUi.HOOK_PRE_PULL) + RhodeCodeUi.HOOK_PULL, RhodeCodeUi.HOOK_PRE_PULL, + RhodeCodeUi.HOOK_PUSH_KEY,) HOOKS_SECTION = 'hooks' def __init__(self, sa=None, repo=None): diff --git a/rhodecode/tests/lib/test_utils.py b/rhodecode/tests/lib/test_utils.py index 3ae1b48e..808cccd7 100644 --- a/rhodecode/tests/lib/test_utils.py +++ b/rhodecode/tests/lib/test_utils.py @@ -86,22 +86,24 @@ HOOK_PUSH = db.RhodeCodeUi.HOOK_PUSH HOOK_PRE_PULL = db.RhodeCodeUi.HOOK_PRE_PULL HOOK_PULL = db.RhodeCodeUi.HOOK_PULL HOOK_REPO_SIZE = db.RhodeCodeUi.HOOK_REPO_SIZE +HOOK_PUSH_KEY = db.RhodeCodeUi.HOOK_PUSH_KEY HG_HOOKS = frozenset( (HOOK_PRE_PULL, HOOK_PULL, HOOK_PRE_PUSH, HOOK_PRETX_PUSH, HOOK_PUSH, - HOOK_REPO_SIZE)) + HOOK_REPO_SIZE, HOOK_PUSH_KEY)) @pytest.mark.parametrize('disabled_hooks,expected_hooks', [ ([], HG_HOOKS), (HG_HOOKS, []), - ([HOOK_PRE_PUSH, HOOK_PRETX_PUSH, HOOK_REPO_SIZE], [HOOK_PRE_PULL, HOOK_PULL, HOOK_PUSH]), + ([HOOK_PRE_PUSH, HOOK_PRETX_PUSH, HOOK_REPO_SIZE, HOOK_PUSH_KEY], [HOOK_PRE_PULL, HOOK_PULL, HOOK_PUSH]), # When a pull/push hook is disabled, its pre-pull/push counterpart should # be disabled too. ([HOOK_PUSH], [HOOK_PRE_PULL, HOOK_PULL, HOOK_REPO_SIZE]), - ([HOOK_PULL], [HOOK_PRE_PUSH, HOOK_PRETX_PUSH, HOOK_PUSH, HOOK_REPO_SIZE]), + ([HOOK_PULL], [HOOK_PRE_PUSH, HOOK_PRETX_PUSH, HOOK_PUSH, HOOK_REPO_SIZE, + HOOK_PUSH_KEY]), ]) def test_make_db_config_hg_hooks(pylonsapp, request, disabled_hooks, expected_hooks): diff --git a/rhodecode/tests/other/vcs_operations/__init__.py b/rhodecode/tests/other/vcs_operations/__init__.py index b26148f3..dda8ba64 100644 --- a/rhodecode/tests/other/vcs_operations/__init__.py +++ b/rhodecode/tests/other/vcs_operations/__init__.py @@ -131,6 +131,13 @@ def _check_proper_git_push( assert "Setting default branch" not in stderr +def _check_proper_hg_push(stdout, stderr, branch='default'): + assert 'pushing to' in stdout + assert 'searching for changes' in stdout + + assert 'abort:' not in stderr + + def _check_proper_clone(stdout, stderr, vcs): if vcs == 'hg': assert 'requesting all changes' in stdout diff --git a/rhodecode/tests/other/vcs_operations/conftest.py b/rhodecode/tests/other/vcs_operations/conftest.py index 11333bbc..36736605 100644 --- a/rhodecode/tests/other/vcs_operations/conftest.py +++ b/rhodecode/tests/other/vcs_operations/conftest.py @@ -84,6 +84,9 @@ class RcWebServer(object): _url = 'http://%(user)s:%(passwd)s@%(host)s/%(cloned_repo)s' % params return _url + def host_url(self): + return 'http://' + get_host_url(self.pylons_config) + @pytest.fixture(scope="module") def rcextensions(request, pylonsapp, tmpdir_factory): diff --git a/rhodecode/tests/other/vcs_operations/test_vcs_operations.py b/rhodecode/tests/other/vcs_operations/test_vcs_operations.py index 6bcb1986..179e3639 100644 --- a/rhodecode/tests/other/vcs_operations/test_vcs_operations.py +++ b/rhodecode/tests/other/vcs_operations/test_vcs_operations.py @@ -34,6 +34,7 @@ import time import pytest from rhodecode.lib.vcs.backends.git.repository import GitRepository +from rhodecode.lib.vcs.backends.hg.repository import MercurialRepository from rhodecode.lib.vcs.nodes import FileNode from rhodecode.model.auth_token import AuthTokenModel from rhodecode.model.db import Repository, UserIpMap, CacheKey @@ -42,7 +43,8 @@ from rhodecode.model.user import UserModel from rhodecode.tests import (GIT_REPO, HG_REPO, TEST_USER_ADMIN_LOGIN) from rhodecode.tests.other.vcs_operations import ( - Command, _check_proper_clone, _check_proper_git_push, _add_files_and_push, + Command, _check_proper_clone, _check_proper_git_push, + _check_proper_hg_push, _add_files_and_push, HG_REPO_WITH_GROUP, GIT_REPO_WITH_GROUP) @@ -396,7 +398,6 @@ class TestVCSOperations(object): 'hg clone', clone_url, tmpdir.strpath) assert 'abort: authorization failed' in stderr - def test_clone_by_auth_token_with_scope( self, rc_web_server, tmpdir, user_util, enable_auth_plugins): enable_auth_plugins(['egg:rhodecode-enterprise-ce#token', @@ -479,3 +480,176 @@ def test_git_fetches_from_remote_repository_with_annotated_tags( source_repo = backend_git['annotated-tag'] target_vcs_repo = backend_git.create_repo().scm_instance() target_vcs_repo.fetch(rc_web_server.repo_clone_url(source_repo.repo_name)) + + +def test_git_push_shows_pull_request_refs(backend_git, rc_web_server, tmpdir): + """ + test if remote info about refs is visible + """ + empty_repo = backend_git.create_repo() + + clone_url = rc_web_server.repo_clone_url(empty_repo.repo_name) + + cmd = Command(tmpdir.strpath) + cmd.execute('git clone', clone_url) + + repo = GitRepository(os.path.join(tmpdir.strpath, empty_repo.repo_name)) + repo.in_memory_commit.add(FileNode('readme.md', content='## Hello')) + repo.in_memory_commit.commit( + message='Commit on branch Master', + author='Automatic test', + branch='master') + + repo_cmd = Command(repo.path) + stdout, stderr = repo_cmd.execute('git push --verbose origin master') + _check_proper_git_push(stdout, stderr, branch='master') + + ref = '{}/{}/pull-request/new?branch=master'.format( + rc_web_server.host_url(), empty_repo.repo_name) + assert 'remote: RhodeCode: open pull request link: {}'.format(ref) in stderr + assert 'remote: RhodeCode: push completed' in stderr + + # push on the same branch + repo = GitRepository(os.path.join(tmpdir.strpath, empty_repo.repo_name)) + repo.in_memory_commit.add(FileNode('setup.py', content='print\n')) + repo.in_memory_commit.commit( + message='Commit2 on branch Master', + author='Automatic test2', + branch='master') + + repo_cmd = Command(repo.path) + stdout, stderr = repo_cmd.execute('git push --verbose origin master') + _check_proper_git_push(stdout, stderr, branch='master') + + assert 'remote: RhodeCode: open pull request link: {}'.format(ref) in stderr + assert 'remote: RhodeCode: push completed' in stderr + + # new Branch + repo = GitRepository(os.path.join(tmpdir.strpath, empty_repo.repo_name)) + repo.in_memory_commit.add(FileNode('feature1.py', content='## Hello world')) + repo.in_memory_commit.commit( + message='Commit on branch feature', + author='Automatic test', + branch='feature') + + repo_cmd = Command(repo.path) + stdout, stderr = repo_cmd.execute('git push --verbose origin feature') + _check_proper_git_push(stdout, stderr, branch='feature') + + ref = '{}/{}/pull-request/new?branch=feature'.format( + rc_web_server.host_url(), empty_repo.repo_name) + assert 'remote: RhodeCode: open pull request link: {}'.format(ref) in stderr + assert 'remote: RhodeCode: push completed' in stderr + + +def test_hg_push_shows_pull_request_refs(backend_hg, rc_web_server, tmpdir): + empty_repo = backend_hg.create_repo() + + clone_url = rc_web_server.repo_clone_url(empty_repo.repo_name) + + cmd = Command(tmpdir.strpath) + cmd.execute('hg clone', clone_url) + + repo = MercurialRepository(os.path.join(tmpdir.strpath, empty_repo.repo_name)) + repo.in_memory_commit.add(FileNode(u'readme.md', content=u'## Hello')) + repo.in_memory_commit.commit( + message=u'Commit on branch default', + author=u'Automatic test', + branch='default') + + repo_cmd = Command(repo.path) + repo_cmd.execute('hg checkout default') + + stdout, stderr = repo_cmd.execute('hg push --verbose', clone_url) + _check_proper_hg_push(stdout, stderr, branch='default') + + ref = '{}/{}/pull-request/new?branch=default'.format( + rc_web_server.host_url(), empty_repo.repo_name) + assert 'remote: RhodeCode: open pull request link: {}'.format(ref) in stdout + assert 'remote: RhodeCode: push completed' in stdout + + # push on the same branch + repo = MercurialRepository(os.path.join(tmpdir.strpath, empty_repo.repo_name)) + repo.in_memory_commit.add(FileNode(u'setup.py', content=u'print\n')) + repo.in_memory_commit.commit( + message=u'Commit2 on branch default', + author=u'Automatic test2', + branch=u'default') + + repo_cmd = Command(repo.path) + repo_cmd.execute('hg checkout default') + + stdout, stderr = repo_cmd.execute('hg push --verbose', clone_url) + _check_proper_hg_push(stdout, stderr, branch='default') + + assert 'remote: RhodeCode: open pull request link: {}'.format(ref) in stdout + assert 'remote: RhodeCode: push completed' in stdout + + # new Branch + repo = MercurialRepository(os.path.join(tmpdir.strpath, empty_repo.repo_name)) + repo.in_memory_commit.add(FileNode(u'feature1.py', content=u'## Hello world')) + repo.in_memory_commit.commit( + message=u'Commit on branch feature', + author=u'Automatic test', + branch=u'feature') + + repo_cmd = Command(repo.path) + repo_cmd.execute('hg checkout feature') + + stdout, stderr = repo_cmd.execute('hg push --new-branch --verbose', clone_url) + _check_proper_hg_push(stdout, stderr, branch='feature') + + ref = '{}/{}/pull-request/new?branch=feature'.format( + rc_web_server.host_url(), empty_repo.repo_name) + assert 'remote: RhodeCode: open pull request link: {}'.format(ref) in stdout + assert 'remote: RhodeCode: push completed' in stdout + + +def test_hg_push_shows_pull_request_refs_book(backend_hg, rc_web_server, tmpdir): + empty_repo = backend_hg.create_repo() + + clone_url = rc_web_server.repo_clone_url(empty_repo.repo_name) + + cmd = Command(tmpdir.strpath) + cmd.execute('hg clone', clone_url) + + repo = MercurialRepository(os.path.join(tmpdir.strpath, empty_repo.repo_name)) + repo.in_memory_commit.add(FileNode(u'readme.md', content=u'## Hello')) + repo.in_memory_commit.commit( + message=u'Commit on branch default', + author=u'Automatic test', + branch='default') + + repo_cmd = Command(repo.path) + repo_cmd.execute('hg checkout default') + + stdout, stderr = repo_cmd.execute('hg push --verbose', clone_url) + _check_proper_hg_push(stdout, stderr, branch='default') + + ref = '{}/{}/pull-request/new?branch=default'.format( + rc_web_server.host_url(), empty_repo.repo_name) + assert 'remote: RhodeCode: open pull request link: {}'.format(ref) in stdout + assert 'remote: RhodeCode: push completed' in stdout + + # add bookmark + repo = MercurialRepository(os.path.join(tmpdir.strpath, empty_repo.repo_name)) + repo.in_memory_commit.add(FileNode(u'setup.py', content=u'print\n')) + repo.in_memory_commit.commit( + message=u'Commit2 on branch default', + author=u'Automatic test2', + branch=u'default') + + repo_cmd = Command(repo.path) + repo_cmd.execute('hg checkout default') + repo_cmd.execute('hg bookmark feature2') + stdout, stderr = repo_cmd.execute('hg push -B feature2 --verbose', clone_url) + _check_proper_hg_push(stdout, stderr, branch='default') + + ref = '{}/{}/pull-request/new?branch=default'.format( + rc_web_server.host_url(), empty_repo.repo_name) + assert 'remote: RhodeCode: open pull request link: {}'.format(ref) in stdout + ref = '{}/{}/pull-request/new?bookmark=feature2'.format( + rc_web_server.host_url(), empty_repo.repo_name) + assert 'remote: RhodeCode: open pull request link: {}'.format(ref) in stdout + assert 'remote: RhodeCode: push completed' in stdout + assert 'exporting bookmark feature2' in stdout From 0465149479024970b9a3c0dded1ba557ae885d5a Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Thu, 18 May 2017 13:37:03 +0200 Subject: [PATCH 119/211] core: moved channelstream test into pyramid. --- rhodecode/apps/my_account/__init__.py | 11 ++++-- rhodecode/apps/my_account/views.py | 39 ++++++++++++++++++- rhodecode/config/routing.py | 4 -- rhodecode/controllers/admin/my_account.py | 34 ---------------- rhodecode/public/js/rhodecode/routes.js | 1 + .../my_account/my_account_notifications.mako | 2 +- 6 files changed, 47 insertions(+), 44 deletions(-) diff --git a/rhodecode/apps/my_account/__init__.py b/rhodecode/apps/my_account/__init__.py index 8b1e5864..5025147b 100644 --- a/rhodecode/apps/my_account/__init__.py +++ b/rhodecode/apps/my_account/__init__.py @@ -41,12 +41,15 @@ def includeme(config): pattern=ADMIN_PREFIX + '/my_account/auth_tokens') config.add_route( name='my_account_auth_tokens_add', - pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new', - ) + pattern=ADMIN_PREFIX + '/my_account/auth_tokens/new') config.add_route( name='my_account_auth_tokens_delete', - pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete', - ) + pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete') + + # channelstream test + config.add_route( + name='my_account_notifications_test_channelstream', + pattern=ADMIN_PREFIX + '/my_account/test_channelstream') # Scan module for configuration decorators. config.scan() diff --git a/rhodecode/apps/my_account/views.py b/rhodecode/apps/my_account/views.py index 3972add9..5287a67b 100644 --- a/rhodecode/apps/my_account/views.py +++ b/rhodecode/apps/my_account/views.py @@ -19,14 +19,17 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ import logging +import datetime from pyramid.httpexceptions import HTTPFound from pyramid.view import view_config from rhodecode.apps._base import BaseAppView from rhodecode import forms -from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired from rhodecode.lib import helpers as h +from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired +from rhodecode.lib.channelstream import channelstream_request, \ + ChannelstreamException from rhodecode.lib.utils2 import safe_int, md5 from rhodecode.model.auth_token import AuthTokenModel from rhodecode.model.meta import Session @@ -192,3 +195,37 @@ class MyAccountView(BaseAppView): h.flash(_("Auth token successfully deleted"), category='success') return HTTPFound(h.route_path('my_account_auth_tokens')) + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + @view_config( + route_name='my_account_notifications_test_channelstream', + request_method='POST', renderer='json_ext') + def my_account_notifications_test_channelstream(self): + message = 'Test message sent via Channelstream by user: {}, on {}'.format( + self._rhodecode_user.username, datetime.datetime.now()) + payload = { + # 'channel': 'broadcast', + 'type': 'message', + 'timestamp': datetime.datetime.utcnow(), + 'user': 'system', + 'pm_users': [self._rhodecode_user.username], + 'message': { + 'message': message, + 'level': 'info', + 'topic': '/notifications' + } + } + + registry = self.request.registry + rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {}) + channelstream_config = rhodecode_plugins.get('channelstream', {}) + + try: + channelstream_request(channelstream_config, [payload], '/message') + except ChannelstreamException as e: + log.exception('Failed to send channelstream data') + return {"response": 'ERROR: {}'.format(e.__class__.__name__)} + return {"response": 'Channelstream data sent. ' + 'You should see a new live message now.'} diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py index c72c2665..6340201d 100644 --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -513,10 +513,6 @@ def make_map(config): '/my_account/toggle_visibility', action='my_notifications_toggle_visibility', conditions={'method': ['POST']}) - m.connect('my_account_notifications_test_channelstream', - '/my_account/test_channelstream', - action='my_account_notifications_test_channelstream', - conditions={'method': ['POST']}) # NOTIFICATION REST ROUTES with rmap.submapper(path_prefix=ADMIN_PREFIX, diff --git a/rhodecode/controllers/admin/my_account.py b/rhodecode/controllers/admin/my_account.py index 51bd737c..e5413d21 100644 --- a/rhodecode/controllers/admin/my_account.py +++ b/rhodecode/controllers/admin/my_account.py @@ -24,11 +24,9 @@ my account controller for RhodeCode admin """ import logging -import datetime import formencode from formencode import htmlfill -from pyramid.threadlocal import get_current_registry from pyramid.httpexceptions import HTTPFound from pylons import request, tmpl_context as c, url @@ -44,8 +42,6 @@ from rhodecode.lib.base import BaseController, render from rhodecode.lib.utils import jsonify from rhodecode.lib.utils2 import safe_int, str2bool from rhodecode.lib.ext_json import json -from rhodecode.lib.channelstream import channelstream_request, \ - ChannelstreamException from rhodecode.model.db import ( Repository, PullRequest, UserEmailMap, User, UserFollowing) @@ -332,33 +328,3 @@ class MyAccountController(BaseController): user.update_userdata(notification_status=new_status) Session().commit() return user.user_data['notification_status'] - - @auth.CSRFRequired() - @jsonify - def my_account_notifications_test_channelstream(self): - message = 'Test message sent via Channelstream by user: {}, on {}'.format( - c.rhodecode_user.username, datetime.datetime.now()) - payload = { - 'type': 'message', - 'timestamp': datetime.datetime.utcnow(), - 'user': 'system', - #'channel': 'broadcast', - 'pm_users': [c.rhodecode_user.username], - 'message': { - 'message': message, - 'level': 'info', - 'topic': '/notifications' - } - } - - registry = get_current_registry() - rhodecode_plugins = getattr(registry, 'rhodecode_plugins', {}) - channelstream_config = rhodecode_plugins.get('channelstream', {}) - - try: - channelstream_request(channelstream_config, [payload], '/message') - except ChannelstreamException as e: - log.exception('Failed to send channelstream data') - return {"response": 'ERROR: {}'.format(e.__class__.__name__)} - return {"response": 'Channelstream data sent. ' - 'You should see a new live message now.'} diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js index 92dad0cd..35b383c5 100644 --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -123,5 +123,6 @@ function registerRCRoutes() { pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []); pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []); pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []); + pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []); pyroutes.register('apiv2', '/_admin/api', []); } diff --git a/rhodecode/templates/admin/my_account/my_account_notifications.mako b/rhodecode/templates/admin/my_account/my_account_notifications.mako index 4b41bd1d..e71e465f 100644 --- a/rhodecode/templates/admin/my_account/my_account_notifications.mako +++ b/rhodecode/templates/admin/my_account/my_account_notifications.mako @@ -10,7 +10,7 @@ From 1e57ca444499544b583a9efb3322f140278c6395 Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Thu, 18 May 2017 13:18:27 +0200 Subject: [PATCH 120/211] audit-logs: deprecated archive download info from audit logs. --- rhodecode/controllers/files.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/rhodecode/controllers/files.py b/rhodecode/controllers/files.py index cb5836e7..af5230f6 100644 --- a/rhodecode/controllers/files.py +++ b/rhodecode/controllers/files.py @@ -814,10 +814,6 @@ class FilesController(BaseRepoController): archive = cached_archive_path # store download action - action_logger(user=c.rhodecode_user, - action='user_downloaded_archive:%s' % archive_name, - repo=repo_name, ipaddr=self.ip_addr, commit=True) - audit_logger.store( action='repo.archive.download', action_data={'user_agent': request.user_agent, From 7216dd64ff15a4a642f2ffa4428590cb543766ed Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Fri, 19 May 2017 10:33:10 +0200 Subject: [PATCH 121/211] audit-logs: introduced new view to replace admin journal. - audit logs will represent the all important actions made inside the system - raw view visible for super-admins --- rhodecode/apps/admin/__init__.py | 16 ++++ .../admin/tests/test_admin_audit_logs.py} | 83 +++++++++++-------- .../apps/admin/tests/test_admin_main_views.py | 82 ++++++++++++++++++ .../admin/views/audit_logs.py} | 60 +++++--------- rhodecode/apps/admin/views/main_views.py | 63 ++++++++++++++ rhodecode/apps/repository/__init__.py | 6 ++ rhodecode/config/routing.py | 16 ---- rhodecode/controllers/journal.py | 2 +- rhodecode/public/js/rhodecode/routes.js | 6 ++ .../{admin.mako => admin_audit_logs.mako} | 8 +- rhodecode/templates/admin/admin_log.mako | 15 ---- .../templates/admin/auth/auth_settings.mako | 2 +- .../templates/admin/auth/plugin_settings.mako | 2 +- .../templates/admin/defaults/defaults.mako | 2 +- .../templates/admin/integrations/base.mako | 2 +- .../templates/admin/integrations/form.mako | 4 +- .../templates/admin/integrations/list.mako | 4 +- .../templates/admin/integrations/new.mako | 4 +- .../admin/permissions/permissions.mako | 2 +- .../admin/repo_groups/repo_group_add.mako | 2 +- .../admin/repo_groups/repo_group_edit.mako | 2 +- .../admin/repo_groups/repo_groups.mako | 2 +- rhodecode/templates/admin/repos/repo_add.mako | 2 +- rhodecode/templates/admin/repos/repos.mako | 2 +- .../templates/admin/settings/settings.mako | 2 +- .../admin/user_groups/user_group_add.mako | 2 +- .../admin/user_groups/user_group_edit.mako | 2 +- .../admin/user_groups/user_groups.mako | 2 +- rhodecode/templates/admin/users/user_add.mako | 2 +- .../templates/admin/users/user_edit.mako | 2 +- rhodecode/templates/admin/users/users.mako | 2 +- rhodecode/templates/base/base.mako | 2 +- .../tests/functional/test_pullrequests.py | 13 --- 33 files changed, 272 insertions(+), 146 deletions(-) rename rhodecode/{tests/functional/test_admin.py => apps/admin/tests/test_admin_audit_logs.py} (64%) create mode 100644 rhodecode/apps/admin/tests/test_admin_main_views.py rename rhodecode/{controllers/admin/admin.py => apps/admin/views/audit_logs.py} (57%) create mode 100644 rhodecode/apps/admin/views/main_views.py rename rhodecode/templates/admin/{admin.mako => admin_audit_logs.mako} (78%) delete mode 100644 rhodecode/templates/admin/admin_log.mako diff --git a/rhodecode/apps/admin/__init__.py b/rhodecode/apps/admin/__init__.py index bd26edff..fbaf3fd6 100644 --- a/rhodecode/apps/admin/__init__.py +++ b/rhodecode/apps/admin/__init__.py @@ -29,6 +29,20 @@ def admin_routes(config): Admin prefixed routes """ + config.add_route( + name='admin_audit_logs', + pattern='/audit_logs') + + config.add_route( + name='pull_requests_global_0', # backward compat + pattern='/pull_requests/{pull_request_id:[0-9]+}') + config.add_route( + name='pull_requests_global_1', # backward compat + pattern='/pull-requests/{pull_request_id:[0-9]+}') + config.add_route( + name='pull_requests_global', + pattern='/pull-request/{pull_request_id:[0-9]+}') + config.add_route( name='admin_settings_open_source', pattern='/settings/open_source') @@ -93,6 +107,8 @@ def includeme(config): navigation_registry = NavigationRegistry(labs_active=labs_active) config.registry.registerUtility(navigation_registry) + # main admin routes + config.add_route(name='admin_home', pattern=ADMIN_PREFIX) config.include(admin_routes, route_prefix=ADMIN_PREFIX) # Scan module for configuration decorators. diff --git a/rhodecode/tests/functional/test_admin.py b/rhodecode/apps/admin/tests/test_admin_audit_logs.py similarity index 64% rename from rhodecode/tests/functional/test_admin.py rename to rhodecode/apps/admin/tests/test_admin_audit_logs.py index 0e68c763..6f14b285 100644 --- a/rhodecode/tests/functional/test_admin.py +++ b/rhodecode/apps/admin/tests/test_admin_audit_logs.py @@ -25,12 +25,25 @@ import datetime import pytest from rhodecode.tests import * +from rhodecode.tests.fixture import FIXTURES from rhodecode.model.db import UserLog from rhodecode.model.meta import Session from rhodecode.lib.utils2 import safe_unicode -dn = os.path.dirname -FIXTURES = os.path.join(dn(dn(os.path.abspath(__file__))), 'fixtures') + +def route_path(name, params=None, **kwargs): + import urllib + from rhodecode.apps._base import ADMIN_PREFIX + + base_url = { + 'admin_home': ADMIN_PREFIX, + 'admin_audit_logs': ADMIN_PREFIX + '/audit_logs', + + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url class TestAdminController(TestController): @@ -73,102 +86,102 @@ class TestAdminController(TestController): def test_index(self): self.log_user() - response = self.app.get(url(controller='admin/admin', action='index')) - response.mustcontain('Admin journal') + response = self.app.get(route_path('admin_audit_logs')) + response.mustcontain('Admin audit logs') def test_filter_all_entries(self): self.log_user() - response = self.app.get(url(controller='admin/admin', action='index',)) + response = self.app.get(route_path('admin_audit_logs')) all_count = UserLog.query().count() response.mustcontain('%s entries' % all_count) def test_filter_journal_filter_exact_match_on_repository(self): self.log_user() - response = self.app.get(url(controller='admin/admin', action='index', - filter='repository:rhodecode')) + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='repository:rhodecode'))) response.mustcontain('3 entries') def test_filter_journal_filter_exact_match_on_repository_CamelCase(self): self.log_user() - response = self.app.get(url(controller='admin/admin', action='index', - filter='repository:RhodeCode')) + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='repository:RhodeCode'))) response.mustcontain('3 entries') def test_filter_journal_filter_wildcard_on_repository(self): self.log_user() - response = self.app.get(url(controller='admin/admin', action='index', - filter='repository:*test*')) + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='repository:*test*'))) response.mustcontain('862 entries') def test_filter_journal_filter_prefix_on_repository(self): self.log_user() - response = self.app.get(url(controller='admin/admin', action='index', - filter='repository:test*')) + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='repository:test*'))) response.mustcontain('257 entries') def test_filter_journal_filter_prefix_on_repository_CamelCase(self): self.log_user() - response = self.app.get(url(controller='admin/admin', action='index', - filter='repository:Test*')) + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='repository:Test*'))) response.mustcontain('257 entries') def test_filter_journal_filter_prefix_on_repository_and_user(self): self.log_user() - response = self.app.get(url(controller='admin/admin', action='index', - filter='repository:test* AND username:demo')) + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='repository:test* AND username:demo'))) response.mustcontain('130 entries') def test_filter_journal_filter_prefix_on_repository_or_target_repo(self): self.log_user() - response = self.app.get(url(controller='admin/admin', action='index', - filter='repository:test* OR repository:rhodecode')) + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='repository:test* OR repository:rhodecode'))) response.mustcontain('260 entries') # 257 + 3 def test_filter_journal_filter_exact_match_on_username(self): self.log_user() - response = self.app.get(url(controller='admin/admin', action='index', - filter='username:demo')) + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='username:demo'))) response.mustcontain('1087 entries') def test_filter_journal_filter_exact_match_on_username_camelCase(self): self.log_user() - response = self.app.get(url(controller='admin/admin', action='index', - filter='username:DemO')) + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='username:DemO'))) response.mustcontain('1087 entries') def test_filter_journal_filter_wildcard_on_username(self): self.log_user() - response = self.app.get(url(controller='admin/admin', action='index', - filter='username:*test*')) + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='username:*test*'))) entries_count = UserLog.query().filter(UserLog.username.ilike('%test%')).count() response.mustcontain('{} entries'.format(entries_count)) def test_filter_journal_filter_prefix_on_username(self): self.log_user() - response = self.app.get(url(controller='admin/admin', action='index', - filter='username:demo*')) + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='username:demo*'))) response.mustcontain('1101 entries') def test_filter_journal_filter_prefix_on_user_or_other_user(self): self.log_user() - response = self.app.get(url(controller='admin/admin', action='index', - filter='username:demo OR username:volcan')) + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='username:demo OR username:volcan'))) response.mustcontain('1095 entries') # 1087 + 8 def test_filter_journal_filter_wildcard_on_action(self): self.log_user() - response = self.app.get(url(controller='admin/admin', action='index', - filter='action:*pull_request*')) + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='action:*pull_request*'))) response.mustcontain('187 entries') def test_filter_journal_filter_on_date(self): self.log_user() - response = self.app.get(url(controller='admin/admin', action='index', - filter='date:20121010')) + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='date:20121010'))) response.mustcontain('47 entries') def test_filter_journal_filter_on_date_2(self): self.log_user() - response = self.app.get(url(controller='admin/admin', action='index', - filter='date:20121020')) + response = self.app.get(route_path('admin_audit_logs', + params=dict(filter='date:20121020'))) response.mustcontain('17 entries') diff --git a/rhodecode/apps/admin/tests/test_admin_main_views.py b/rhodecode/apps/admin/tests/test_admin_main_views.py new file mode 100644 index 00000000..1727b625 --- /dev/null +++ b/rhodecode/apps/admin/tests/test_admin_main_views.py @@ -0,0 +1,82 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# 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 pytest + +from rhodecode.tests import TestController +from rhodecode.tests.fixture import Fixture + +fixture = Fixture() + + +def route_path(name, params=None, **kwargs): + import urllib + from rhodecode.apps._base import ADMIN_PREFIX + + base_url = { + 'admin_home': ADMIN_PREFIX, + 'pullrequest_show': '/{repo_name}/pull-request/{pull_request_id}', + 'pull_requests_global': ADMIN_PREFIX + '/pull-request/{pull_request_id}', + 'pull_requests_global_0': ADMIN_PREFIX + '/pull_requests/{pull_request_id}', + 'pull_requests_global_1': ADMIN_PREFIX + '/pull-requests/{pull_request_id}', + + }[name].format(**kwargs) + + if params: + base_url = '{}?{}'.format(base_url, urllib.urlencode(params)) + return base_url + + +class TestAdminMainView(TestController): + + def test_redirect_admin_home(self): + self.log_user() + response = self.app.get(route_path('admin_home'), status=302) + assert response.location.endswith('/audit_logs') + + def test_redirect_pull_request_view(self, view): + self.log_user() + self.app.get( + route_path(view, pull_request_id='xxxx'), + status=404) + + @pytest.mark.backends("git", "hg") + @pytest.mark.parametrize('view', [ + 'pull_requests_global', + 'pull_requests_global_0', + 'pull_requests_global_1', + ]) + def test_redirect_pull_request_view(self, view, pr_util): + self.log_user() + pull_request = pr_util.create_pull_request() + pull_request_id = pull_request.pull_request_id + + response = self.app.get( + route_path(view, pull_request_id=pull_request_id), + status=302) + assert response.location.endswith( + 'pull-request/{}'.format(pull_request_id)) + + repo_name = pull_request.target_repo.repo_name + redirect_url = route_path( + 'pullrequest_show', repo_name=repo_name, + pull_request_id=pull_request.pull_request_id) + + assert redirect_url in response.location diff --git a/rhodecode/controllers/admin/admin.py b/rhodecode/apps/admin/views/audit_logs.py similarity index 57% rename from rhodecode/controllers/admin/admin.py rename to rhodecode/apps/admin/views/audit_logs.py index bbaa8dad..1ee6cf1c 100644 --- a/rhodecode/controllers/admin/admin.py +++ b/rhodecode/apps/admin/views/audit_logs.py @@ -1,6 +1,6 @@ # -*- coding: utf-8 -*- -# Copyright (C) 2010-2017 RhodeCode GmbH +# Copyright (C) 2016-2017 RhodeCode GmbH # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License, version 3 @@ -18,42 +18,41 @@ # RhodeCode Enterprise Edition, including its added features, Support services, # and proprietary license terms, please see https://rhodecode.com/licenses/ -""" -Controller for Admin panel of RhodeCode Enterprise -""" - - import logging -from pylons import request, tmpl_context as c, url -from pylons.controllers.util import redirect +from pyramid.view import view_config from sqlalchemy.orm import joinedload -from rhodecode.model.db import UserLog, PullRequest +from rhodecode.apps._base import BaseAppView +from rhodecode.model.db import UserLog from rhodecode.lib.user_log_filter import user_log_filter from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator -from rhodecode.lib.base import BaseController, render from rhodecode.lib.utils2 import safe_int from rhodecode.lib.helpers import Page - log = logging.getLogger(__name__) -class AdminController(BaseController): +class AdminAuditLogsView(BaseAppView): + def load_default_context(self): + c = self._get_local_tmpl_context() + self._register_global_c(c) + return c @LoginRequired() - def __before__(self): - super(AdminController, self).__before__() - @HasPermissionAllDecorator('hg.admin') - def index(self): + @view_config( + route_name='admin_audit_logs', request_method='GET', + renderer='rhodecode:templates/admin/admin_audit_logs.mako') + def admin_audit_logs(self): + c = self.load_default_context() + users_log = UserLog.query()\ .options(joinedload(UserLog.user))\ .options(joinedload(UserLog.repository)) # FILTERING - c.search_term = request.GET.get('filter') + c.search_term = self.request.GET.get('filter') try: users_log = user_log_filter(users_log, c.search_term) except Exception: @@ -62,28 +61,13 @@ class AdminController(BaseController): users_log = users_log.order_by(UserLog.action_date.desc()) - p = safe_int(request.GET.get('page', 1), 1) + p = safe_int(self.request.GET.get('page', 1), 1) def url_generator(**kw): - return url.current(filter=c.search_term, **kw) + if c.search_term: + kw['filter'] = c.search_term + return self.request.current_route_path(_query=kw) c.audit_logs = Page(users_log, page=p, items_per_page=10, - url=url_generator) - c.log_data = render('admin/admin_log.mako') - - if request.is_xhr: - return c.log_data - return render('admin/admin.mako') - - # global redirect doesn't need permissions - def pull_requests(self, pull_request_id): - """ - Global redirect for Pull Requests - - :param pull_request_id: id of pull requests in the system - """ - pull_request = PullRequest.get_or_404(pull_request_id) - repo_name = pull_request.target_repo.repo_name - return redirect(url( - 'pullrequest_show', repo_name=repo_name, - pull_request_id=pull_request_id)) + url=url_generator) + return self._get_template_context(c) diff --git a/rhodecode/apps/admin/views/main_views.py b/rhodecode/apps/admin/views/main_views.py new file mode 100644 index 00000000..6c014c4d --- /dev/null +++ b/rhodecode/apps/admin/views/main_views.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# 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 logging + + +from pyramid.httpexceptions import HTTPFound +from pyramid.view import view_config + +from rhodecode.apps._base import BaseAppView +from rhodecode.lib import helpers as h +from rhodecode.lib.auth import (LoginRequired, HasPermissionAllDecorator) +from rhodecode.model.db import PullRequest + + +log = logging.getLogger(__name__) + + +class AdminMainView(BaseAppView): + + @LoginRequired() + @HasPermissionAllDecorator('hg.admin') + @view_config( + route_name='admin_home', request_method='GET') + def admin_main(self): + # redirect _admin to audit logs... + raise HTTPFound(h.route_path('admin_audit_logs')) + + @LoginRequired() + @view_config(route_name='pull_requests_global_0', request_method='GET') + @view_config(route_name='pull_requests_global_1', request_method='GET') + @view_config(route_name='pull_requests_global', request_method='GET') + def pull_requests(self): + """ + Global redirect for Pull Requests + + :param pull_request_id: id of pull requests in the system + """ + + pull_request_id = self.request.matchdict.get('pull_request_id') + pull_request = PullRequest.get_or_404(pull_request_id, pyramid_exc=True) + repo_name = pull_request.target_repo.repo_name + + raise HTTPFound( + h.route_path('pullrequest_show', repo_name=repo_name, + pull_request_id=pull_request_id)) diff --git a/rhodecode/apps/repository/__init__.py b/rhodecode/apps/repository/__init__.py index 9f9bb5ab..8719555c 100644 --- a/rhodecode/apps/repository/__init__.py +++ b/rhodecode/apps/repository/__init__.py @@ -41,6 +41,12 @@ def includeme(config): name='bookmarks_home', pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True) + # Pull Requests + config.add_route( + name='pullrequest_show', + pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id}', + repo_route=True) + # Settings config.add_route( name='edit_repo', diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py index 6340201d..6cc70d76 100644 --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -561,22 +561,6 @@ def make_map(config): action='show', conditions={'method': ['GET']}, requirements=URL_NAME_REQUIREMENTS) - # ADMIN MAIN PAGES - with rmap.submapper(path_prefix=ADMIN_PREFIX, - controller='admin/admin') as m: - m.connect('admin_home', '', action='index') - m.connect('admin_add_repo', '/add_repo/{new_repo:[a-z0-9\. _-]*}', - action='add_repo') - m.connect( - 'pull_requests_global_0', '/pull_requests/{pull_request_id:[0-9]+}', - action='pull_requests') - m.connect( - 'pull_requests_global_1', '/pull-requests/{pull_request_id:[0-9]+}', - action='pull_requests') - m.connect( - 'pull_requests_global', '/pull-request/{pull_request_id:[0-9]+}', - action='pull_requests') - # USER JOURNAL rmap.connect('journal', '%s/journal' % (ADMIN_PREFIX,), controller='journal', action='index') diff --git a/rhodecode/controllers/journal.py b/rhodecode/controllers/journal.py index f49c1fa3..a88a355b 100644 --- a/rhodecode/controllers/journal.py +++ b/rhodecode/controllers/journal.py @@ -34,11 +34,11 @@ from webob.exc import HTTPBadRequest from pylons import request, tmpl_context as c, response, url from pylons.i18n.translation import _ -from rhodecode.controllers.admin.admin import user_log_filter from rhodecode.model.db import UserLog, UserFollowing, User, UserApiKeys from rhodecode.model.meta import Session import rhodecode.lib.helpers as h from rhodecode.lib.helpers import Page +from rhodecode.lib.user_log_filter import user_log_filter from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired from rhodecode.lib.base import BaseController, render from rhodecode.lib.utils2 import safe_int, AttributeDict diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js index 35b383c5..aa8e98de 100644 --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -69,6 +69,11 @@ function registerRCRoutes() { pyroutes.register('repo_integrations_create', '%(repo_name)s/settings/integrations/%(integration)s/new', ['repo_name', 'integration']); pyroutes.register('repo_integrations_edit', '%(repo_name)s/settings/integrations/%(integration)s/%(integration_id)s', ['repo_name', 'integration', 'integration_id']); pyroutes.register('ops_ping', '_admin/ops/ping', []); + pyroutes.register('admin_home', '/_admin', []); + pyroutes.register('admin_audit_logs', '_admin/audit_logs', []); + pyroutes.register('pull_requests_global_0', '_admin/pull_requests/%(pull_request_id)s', ['pull_request_id']); + pyroutes.register('pull_requests_global_1', '_admin/pull-requests/%(pull_request_id)s', ['pull_request_id']); + pyroutes.register('pull_requests_global', '_admin/pull-request/%(pull_request_id)s', ['pull_request_id']); pyroutes.register('admin_settings_open_source', '_admin/settings/open_source', []); pyroutes.register('admin_settings_vcs_svn_generate_cfg', '_admin/settings/vcs/svn_generate_cfg', []); pyroutes.register('admin_settings_system', '_admin/settings/system', []); @@ -100,6 +105,7 @@ function registerRCRoutes() { pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']); pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']); pyroutes.register('bookmarks_home', '/%(repo_name)s/bookmarks', ['repo_name']); + pyroutes.register('pullrequest_show', '/%(repo_name)s/pull-request/%(pull_request_id)s', ['repo_name', 'pull_request_id']); pyroutes.register('edit_repo', '/%(repo_name)s/settings', ['repo_name']); pyroutes.register('edit_repo_advanced', '/%(repo_name)s/settings/advanced', ['repo_name']); pyroutes.register('edit_repo_advanced_delete', '/%(repo_name)s/settings/advanced/delete', ['repo_name']); diff --git a/rhodecode/templates/admin/admin.mako b/rhodecode/templates/admin/admin_audit_logs.mako similarity index 78% rename from rhodecode/templates/admin/admin.mako rename to rhodecode/templates/admin/admin_audit_logs.mako index eea33dad..6886ccae 100644 --- a/rhodecode/templates/admin/admin.mako +++ b/rhodecode/templates/admin/admin_audit_logs.mako @@ -2,7 +2,7 @@ <%inherit file="/base/base.mako"/> <%def name="title()"> - ${_('Admin journal')} + ${_('Admin audit logs')} %if c.rhodecode_name: · ${h.branding(c.rhodecode_name)} %endif @@ -10,9 +10,9 @@ <%def name="breadcrumbs_links()"> ${h.form(None, id_="filter_form", method="get")} - + - ${_('Admin journal')} - ${ungettext('%s entry', '%s entries', c.audit_logs.item_count) % (c.audit_logs.item_count)} + ${_('Audit logs')} - ${_ungettext('%s entry', '%s entries', c.audit_logs.item_count) % (c.audit_logs.item_count)} ${h.end_form()}

    ${_('Example Queries')}

    @@ -29,7 +29,7 @@
    - ${c.log_data} + <%include file="/admin/admin_log_base.mako" />
    diff --git a/rhodecode/templates/admin/admin_log.mako b/rhodecode/templates/admin/admin_log.mako deleted file mode 100644 index 02be0e1c..00000000 --- a/rhodecode/templates/admin/admin_log.mako +++ /dev/null @@ -1,15 +0,0 @@ -## -*- coding: utf-8 -*- -<%include file="/admin/admin_log_base.mako" /> - - - diff --git a/rhodecode/templates/admin/auth/auth_settings.mako b/rhodecode/templates/admin/auth/auth_settings.mako index d9c767b4..9ccdebaa 100644 --- a/rhodecode/templates/admin/auth/auth_settings.mako +++ b/rhodecode/templates/admin/auth/auth_settings.mako @@ -9,7 +9,7 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${_('Authentication Plugins')} diff --git a/rhodecode/templates/admin/auth/plugin_settings.mako b/rhodecode/templates/admin/auth/plugin_settings.mako index ec37a790..6a84a57a 100644 --- a/rhodecode/templates/admin/auth/plugin_settings.mako +++ b/rhodecode/templates/admin/auth/plugin_settings.mako @@ -9,7 +9,7 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Authentication Plugins'),request.resource_path(resource.__parent__, route_name='auth_home'))} » diff --git a/rhodecode/templates/admin/defaults/defaults.mako b/rhodecode/templates/admin/defaults/defaults.mako index 8f47e695..78aa88ef 100644 --- a/rhodecode/templates/admin/defaults/defaults.mako +++ b/rhodecode/templates/admin/defaults/defaults.mako @@ -9,7 +9,7 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${_('Repositories defaults')} diff --git a/rhodecode/templates/admin/integrations/base.mako b/rhodecode/templates/admin/integrations/base.mako index 5db47c2c..6b5506a9 100644 --- a/rhodecode/templates/admin/integrations/base.mako +++ b/rhodecode/templates/admin/integrations/base.mako @@ -18,7 +18,7 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${_('Integrations')} diff --git a/rhodecode/templates/admin/integrations/form.mako b/rhodecode/templates/admin/integrations/form.mako index a64b4a5f..a57f0c05 100644 --- a/rhodecode/templates/admin/integrations/form.mako +++ b/rhodecode/templates/admin/integrations/form.mako @@ -12,7 +12,7 @@ repo_name=c.repo.repo_name, integration=current_IntegrationType.key))} %elif c.repo_group: - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Repository Groups'),h.url('repo_groups'))} » @@ -25,7 +25,7 @@ repo_group_name=c.repo_group.group_name, integration=current_IntegrationType.key))} %else: - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Settings'),h.url('admin_settings'))} » diff --git a/rhodecode/templates/admin/integrations/list.mako b/rhodecode/templates/admin/integrations/list.mako index 80beef2b..8a9bb1cd 100644 --- a/rhodecode/templates/admin/integrations/list.mako +++ b/rhodecode/templates/admin/integrations/list.mako @@ -5,13 +5,13 @@ %if c.repo: ${h.link_to('Settings',h.route_path('edit_repo', repo_name=c.repo.repo_name))} %elif c.repo_group: - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Repository Groups'),h.url('repo_groups'))} » ${h.link_to(c.repo_group.group_name,h.url('edit_repo_group', group_name=c.repo_group.group_name))} %else: - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Settings'),h.url('admin_settings'))} %endif diff --git a/rhodecode/templates/admin/integrations/new.mako b/rhodecode/templates/admin/integrations/new.mako index fa53a42f..65a2ed8f 100644 --- a/rhodecode/templates/admin/integrations/new.mako +++ b/rhodecode/templates/admin/integrations/new.mako @@ -8,7 +8,7 @@ » ${h.link_to(_('Integrations'),request.route_url(route_name='repo_integrations_home', repo_name=c.repo.repo_name))} %elif c.repo_group: - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Repository Groups'),h.url('repo_groups'))} » @@ -16,7 +16,7 @@ » ${h.link_to(_('Integrations'),request.route_url(route_name='repo_group_integrations_home', repo_group_name=c.repo_group.group_name))} %else: - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Settings'),h.url('admin_settings'))} » diff --git a/rhodecode/templates/admin/permissions/permissions.mako b/rhodecode/templates/admin/permissions/permissions.mako index 4afa7312..d96ecb93 100644 --- a/rhodecode/templates/admin/permissions/permissions.mako +++ b/rhodecode/templates/admin/permissions/permissions.mako @@ -9,7 +9,7 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${_('Permissions')} diff --git a/rhodecode/templates/admin/repo_groups/repo_group_add.mako b/rhodecode/templates/admin/repo_groups/repo_group_add.mako index 7bfe230c..82f836b7 100644 --- a/rhodecode/templates/admin/repo_groups/repo_group_add.mako +++ b/rhodecode/templates/admin/repo_groups/repo_group_add.mako @@ -9,7 +9,7 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Repository groups'),h.url('repo_groups'))} » diff --git a/rhodecode/templates/admin/repo_groups/repo_group_edit.mako b/rhodecode/templates/admin/repo_groups/repo_group_edit.mako index 141252d7..26ef6803 100644 --- a/rhodecode/templates/admin/repo_groups/repo_group_edit.mako +++ b/rhodecode/templates/admin/repo_groups/repo_group_edit.mako @@ -9,7 +9,7 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Repository Groups'),h.url('repo_groups'))} %if c.repo_group.parent_group: diff --git a/rhodecode/templates/admin/repo_groups/repo_groups.mako b/rhodecode/templates/admin/repo_groups/repo_groups.mako index d0cea57a..6e998c6a 100644 --- a/rhodecode/templates/admin/repo_groups/repo_groups.mako +++ b/rhodecode/templates/admin/repo_groups/repo_groups.mako @@ -10,7 +10,7 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} » 0 ${_('repository groups')} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » 0 ${_('repository groups')} <%def name="menu_bar_nav()"> diff --git a/rhodecode/templates/admin/repos/repo_add.mako b/rhodecode/templates/admin/repos/repo_add.mako index 868f27a8..3ec71cbb 100644 --- a/rhodecode/templates/admin/repos/repo_add.mako +++ b/rhodecode/templates/admin/repos/repo_add.mako @@ -10,7 +10,7 @@ <%def name="breadcrumbs_links()"> %if c.rhodecode_user.is_admin: - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Repositories'),h.url('repos'))} %else: diff --git a/rhodecode/templates/admin/repos/repos.mako b/rhodecode/templates/admin/repos/repos.mako index ce06b6b5..01debfae 100644 --- a/rhodecode/templates/admin/repos/repos.mako +++ b/rhodecode/templates/admin/repos/repos.mako @@ -10,7 +10,7 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} » 0 ${_('repositories')} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » 0 ${_('repositories')} <%def name="menu_bar_nav()"> diff --git a/rhodecode/templates/admin/settings/settings.mako b/rhodecode/templates/admin/settings/settings.mako index 071133e7..a136daa8 100644 --- a/rhodecode/templates/admin/settings/settings.mako +++ b/rhodecode/templates/admin/settings/settings.mako @@ -9,7 +9,7 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${_('Settings')} diff --git a/rhodecode/templates/admin/user_groups/user_group_add.mako b/rhodecode/templates/admin/user_groups/user_group_add.mako index b198e2de..e404d60e 100644 --- a/rhodecode/templates/admin/user_groups/user_group_add.mako +++ b/rhodecode/templates/admin/user_groups/user_group_add.mako @@ -8,7 +8,7 @@ %endif <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('User groups'),h.url('users_groups'))} » diff --git a/rhodecode/templates/admin/user_groups/user_group_edit.mako b/rhodecode/templates/admin/user_groups/user_group_edit.mako index e912b0ec..fb28b8d5 100644 --- a/rhodecode/templates/admin/user_groups/user_group_edit.mako +++ b/rhodecode/templates/admin/user_groups/user_group_edit.mako @@ -9,7 +9,7 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('User Groups'),h.url('users_groups'))} » diff --git a/rhodecode/templates/admin/user_groups/user_groups.mako b/rhodecode/templates/admin/user_groups/user_groups.mako index 441e8234..5b9709bf 100644 --- a/rhodecode/templates/admin/user_groups/user_groups.mako +++ b/rhodecode/templates/admin/user_groups/user_groups.mako @@ -10,7 +10,7 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} » 0 ${_('user groups')} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » 0 ${_('user groups')} <%def name="menu_bar_nav()"> diff --git a/rhodecode/templates/admin/users/user_add.mako b/rhodecode/templates/admin/users/user_add.mako index 21525aad..2d3e83f0 100644 --- a/rhodecode/templates/admin/users/user_add.mako +++ b/rhodecode/templates/admin/users/user_add.mako @@ -8,7 +8,7 @@ %endif <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Users'),h.route_path('users'))} » diff --git a/rhodecode/templates/admin/users/user_edit.mako b/rhodecode/templates/admin/users/user_edit.mako index d8be135b..c12b7fdf 100644 --- a/rhodecode/templates/admin/users/user_edit.mako +++ b/rhodecode/templates/admin/users/user_edit.mako @@ -9,7 +9,7 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » ${h.link_to(_('Users'),h.route_path('users'))} » diff --git a/rhodecode/templates/admin/users/users.mako b/rhodecode/templates/admin/users/users.mako index 58575eb9..d526f912 100644 --- a/rhodecode/templates/admin/users/users.mako +++ b/rhodecode/templates/admin/users/users.mako @@ -10,7 +10,7 @@ <%def name="breadcrumbs_links()"> - ${h.link_to(_('Admin'),h.url('admin_home'))} » 0 + ${h.link_to(_('Admin'),h.route_path('admin_home'))} » 0 <%def name="menu_bar_nav()"> diff --git a/rhodecode/templates/base/base.mako b/rhodecode/templates/base/base.mako index 7035c82a..256790e3 100644 --- a/rhodecode/templates/base/base.mako +++ b/rhodecode/templates/base/base.mako @@ -72,7 +72,7 @@ <%def name="admin_menu()"> + + ## REVIEW RULES + + ## REVIEWERS
    ${_('Pull request reviewers')} %if c.allowed_to_update: - ${_('Edit')} - + ${_('Edit')} %endif
    @@ -330,8 +344,8 @@ ## members goes here !
      - %for member,reasons,status in c.pull_request_reviewers: -
    • + %for member,reasons,mandatory,status in c.pull_request_reviewers: +
    • @@ -348,30 +362,43 @@ %endfor + - %if c.allowed_to_update: - - %endif + % if mandatory: +
      + +
      +
      + +
      + % else: + %if c.allowed_to_update: + + %endif + % endif
    • %endfor
    - %if not c.pull_request.is_closed(): - - %endif @@ -429,7 +456,7 @@
    % if c.allowed_to_update and not c.pull_request.is_closed(): - ${_('Update commits')} + ${_('Update commits')} % else: ${_('Update commits')} % endif @@ -615,9 +642,10 @@ versionController = new VersionController(); versionController.init(); + reviewersController = new ReviewersController(); $(function(){ - ReviewerAutoComplete('#user'); + // custom code mirror var codeMirrorInstance = initPullRequestsCodeMirror('#pr-description-input'); @@ -655,13 +683,13 @@ var ReviewersPanel = { editButton: $('#open_edit_reviewers'), closeButton: $('#close_edit_reviewers'), - addButton: $('#add_reviewer_input'), - removeButtons: $('.reviewer_member_remove'), + addButton: $('#add_reviewer'), + removeButtons: $('.reviewer_member_remove,.reviewer_member_mandatory_remove,.reviewer_member_mandatory'), init: function() { - var that = this; - this.editButton.on('click', function(e) { that.edit(); }); - this.closeButton.on('click', function(e) { that.close(); }); + var self = this; + this.editButton.on('click', function(e) { self.edit(); }); + this.closeButton.on('click', function(e) { self.close(); }); }, edit: function(event) { @@ -669,6 +697,9 @@ this.closeButton.show(); this.addButton.show(); this.removeButtons.css('visibility', 'visible'); + // review rules + reviewersController.loadReviewRules( + ${c.pull_request.reviewer_data_json | n}); }, close: function(event) { @@ -676,6 +707,8 @@ this.closeButton.hide(); this.addButton.hide(); this.removeButtons.css('visibility', 'hidden'); + // hide review rules + reviewersController.hideReviewRules() } }; @@ -774,7 +807,8 @@ $(this).attr('disabled', 'disabled'); $(this).addClass('disabled'); $(this).html(_gettext('Saving...')); - updateReviewers(undefined, "${c.repo_name}", "${c.pull_request.pull_request_id}"); + reviewersController.updateReviewers( + "${c.repo_name}", "${c.pull_request.pull_request_id}"); }); $('#update_commits').on('click', function(e){ @@ -784,14 +818,16 @@ $(e.currentTarget).removeClass('btn-primary'); $(e.currentTarget).text(_gettext('Updating...')); if(isDisabled){ - updateCommits("${c.repo_name}", "${c.pull_request.pull_request_id}"); + updateCommits( + "${c.repo_name}", "${c.pull_request.pull_request_id}"); } }); // fixing issue with caches on firefox $('#update_commits').removeAttr("disabled"); $('#close_pull_request').on('click', function(e){ - closePullRequest("${c.repo_name}", "${c.pull_request.pull_request_id}"); + closePullRequest( + "${c.repo_name}", "${c.pull_request.pull_request_id}"); }); $('.show-inline-comments').on('click', function(e){ @@ -818,6 +854,8 @@ // initial injection injectCloseAction(); + ReviewerAutoComplete('#user'); + }) diff --git a/rhodecode/tests/functional/test_login.py b/rhodecode/tests/functional/test_login.py index a7b4401b..82ded00d 100644 --- a/rhodecode/tests/functional/test_login.py +++ b/rhodecode/tests/functional/test_login.py @@ -25,7 +25,8 @@ import pytest from rhodecode.config.routing import ADMIN_PREFIX from rhodecode.tests import ( - assert_session_flash, url, HG_REPO, TEST_USER_ADMIN_LOGIN) + assert_session_flash, url, HG_REPO, TEST_USER_ADMIN_LOGIN, + no_newline_id_generator) from rhodecode.tests.fixture import Fixture from rhodecode.tests.utils import AssertResponse, get_session_from_response from rhodecode.lib.auth import check_password @@ -122,7 +123,7 @@ class TestLoginController(object): 'ftp://some.ftp.server', 'http://other.domain', '/\r\nX-Forwarded-Host: http://example.org', - ]) + ], ids=no_newline_id_generator) def test_login_bad_came_froms(self, url_came_from): _url = '{}?came_from={}'.format(login_url, url_came_from) response = self.app.post( diff --git a/rhodecode/tests/functional/test_pullrequests.py b/rhodecode/tests/functional/test_pullrequests.py index e90d9ca9..1553ba01 100644 --- a/rhodecode/tests/functional/test_pullrequests.py +++ b/rhodecode/tests/functional/test_pullrequests.py @@ -295,8 +295,8 @@ class TestPullrequestsController: def test_comment_force_close_pull_request(self, pr_util, csrf_token): pull_request = pr_util.create_pull_request() pull_request_id = pull_request.pull_request_id - reviewers_data = [(1, ['reason']), (2, ['reason2'])] - PullRequestModel().update_reviewers(pull_request_id, reviewers_data) + PullRequestModel().update_reviewers( + pull_request_id, [(1, ['reason'], False), (2, ['reason2'], False)]) author = pull_request.user_id repo = pull_request.target_repo.repo_id self.app.post( @@ -345,6 +345,7 @@ class TestPullrequestsController: ('source_ref', 'branch:default:' + commit_ids['change2']), ('target_repo', target.repo_name), ('target_ref', 'branch:default:' + commit_ids['ancestor']), + ('common_ancestor', commit_ids['ancestor']), ('pullrequest_desc', 'Description'), ('pullrequest_title', 'Title'), ('__start__', 'review_members:sequence'), @@ -353,6 +354,7 @@ class TestPullrequestsController: ('__start__', 'reasons:sequence'), ('reason', 'Some reason'), ('__end__', 'reasons:sequence'), + ('mandatory', 'False'), ('__end__', 'reviewer:mapping'), ('__end__', 'review_members:sequence'), ('__start__', 'revisions:sequence'), @@ -365,8 +367,9 @@ class TestPullrequestsController: status=302) location = response.headers['Location'] - pull_request_id = int(location.rsplit('/', 1)[1]) - pull_request = PullRequest.get(pull_request_id) + pull_request_id = location.rsplit('/', 1)[1] + assert pull_request_id != 'new' + pull_request = PullRequest.get(int(pull_request_id)) # check that we have now both revisions assert pull_request.revisions == [commit_ids['change2'], commit_ids['change']] @@ -403,6 +406,7 @@ class TestPullrequestsController: ('source_ref', 'branch:default:' + commit_ids['change']), ('target_repo', target.repo_name), ('target_ref', 'branch:default:' + commit_ids['ancestor-child']), + ('common_ancestor', commit_ids['ancestor']), ('pullrequest_desc', 'Description'), ('pullrequest_title', 'Title'), ('__start__', 'review_members:sequence'), @@ -411,6 +415,7 @@ class TestPullrequestsController: ('__start__', 'reasons:sequence'), ('reason', 'Some reason'), ('__end__', 'reasons:sequence'), + ('mandatory', 'False'), ('__end__', 'reviewer:mapping'), ('__end__', 'review_members:sequence'), ('__start__', 'revisions:sequence'), @@ -422,21 +427,22 @@ class TestPullrequestsController: status=302) location = response.headers['Location'] - pull_request_id = int(location.rsplit('/', 1)[1]) - pull_request = PullRequest.get(pull_request_id) + + pull_request_id = location.rsplit('/', 1)[1] + assert pull_request_id != 'new' + pull_request = PullRequest.get(int(pull_request_id)) # Check that a notification was made notifications = Notification.query()\ .filter(Notification.created_by == pull_request.author.user_id, Notification.type_ == Notification.TYPE_PULL_REQUEST, - Notification.subject.contains("wants you to review " - "pull request #%d" - % pull_request_id)) + Notification.subject.contains( + "wants you to review pull request #%s" % pull_request_id)) assert len(notifications.all()) == 1 # Change reviewers and check that a notification was made PullRequestModel().update_reviewers( - pull_request.pull_request_id, [(1, [])]) + pull_request.pull_request_id, [(1, [], False)]) assert len(notifications.all()) == 2 def test_create_pull_request_stores_ancestor_commit_id(self, backend, @@ -467,6 +473,7 @@ class TestPullrequestsController: ('source_ref', 'branch:default:' + commit_ids['change']), ('target_repo', target.repo_name), ('target_ref', 'branch:default:' + commit_ids['ancestor-child']), + ('common_ancestor', commit_ids['ancestor']), ('pullrequest_desc', 'Description'), ('pullrequest_title', 'Title'), ('__start__', 'review_members:sequence'), @@ -475,6 +482,7 @@ class TestPullrequestsController: ('__start__', 'reasons:sequence'), ('reason', 'Some reason'), ('__end__', 'reasons:sequence'), + ('mandatory', 'False'), ('__end__', 'reviewer:mapping'), ('__end__', 'review_members:sequence'), ('__start__', 'revisions:sequence'), @@ -486,8 +494,10 @@ class TestPullrequestsController: status=302) location = response.headers['Location'] - pull_request_id = int(location.rsplit('/', 1)[1]) - pull_request = PullRequest.get(pull_request_id) + + pull_request_id = location.rsplit('/', 1)[1] + assert pull_request_id != 'new' + pull_request = PullRequest.get(int(pull_request_id)) # target_ref has to point to the ancestor's commit_id in order to # show the correct diff @@ -954,14 +964,7 @@ class TestPullrequestsController: assert target.text.strip() == 'tag: target' assert target.getchildren() == [] - def test_description_is_escaped_on_index_page(self, backend, pr_util): - xss_description = "" - pull_request = pr_util.create_pull_request(description=xss_description) - response = self.app.get(url( - controller='pullrequests', action='show_all', - repo_name=pull_request.target_repo.repo_name)) - response.mustcontain( - "<script>alert('Hi!')</script>") + @pytest.mark.parametrize('mergeable', [True, False]) def test_shadow_repository_link( @@ -1061,23 +1064,20 @@ def assert_pull_request_status(pull_request, expected_status): assert status == expected_status -@pytest.mark.parametrize('action', ['show_all', 'index', 'create']) +@pytest.mark.parametrize('action', ['index', 'create']) @pytest.mark.usefixtures("autologin_user") -def test_redirects_to_repo_summary_for_svn_repositories( - backend_svn, app, action): - denied_actions = ['show_all', 'index', 'create'] - for action in denied_actions: - response = app.get(url( - controller='pullrequests', action=action, - repo_name=backend_svn.repo_name)) - assert response.status_int == 302 +def test_redirects_to_repo_summary_for_svn_repositories(backend_svn, app, action): + response = app.get(url( + controller='pullrequests', action=action, + repo_name=backend_svn.repo_name)) + assert response.status_int == 302 - # Not allowed, redirect to the summary - redirected = response.follow() - summary_url = url('summary_home', repo_name=backend_svn.repo_name) + # Not allowed, redirect to the summary + redirected = response.follow() + summary_url = url('summary_home', repo_name=backend_svn.repo_name) - # URL adds leading slash and path doesn't have it - assert redirected.req.path == summary_url + # URL adds leading slash and path doesn't have it + assert redirected.req.path == summary_url def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp): diff --git a/rhodecode/tests/models/test_pullrequest.py b/rhodecode/tests/models/test_pullrequest.py index a5aea0e1..54efcf35 100644 --- a/rhodecode/tests/models/test_pullrequest.py +++ b/rhodecode/tests/models/test_pullrequest.py @@ -116,7 +116,7 @@ class TestPullRequestModel: def test_get_awaiting_my_review(self, pull_request): PullRequestModel().update_reviewers( - pull_request, [(pull_request.author, ['author'])]) + pull_request, [(pull_request.author, ['author'], False)]) prs = PullRequestModel().get_awaiting_my_review( pull_request.target_repo, user_id=pull_request.author.user_id) assert isinstance(prs, list) @@ -124,7 +124,7 @@ class TestPullRequestModel: def test_count_awaiting_my_review(self, pull_request): PullRequestModel().update_reviewers( - pull_request, [(pull_request.author, ['author'])]) + pull_request, [(pull_request.author, ['author'], False)]) pr_count = PullRequestModel().count_awaiting_my_review( pull_request.target_repo, user_id=pull_request.author.user_id) assert pr_count == 1 diff --git a/rhodecode/tests/plugin.py b/rhodecode/tests/plugin.py index 9450ccb7..13852583 100644 --- a/rhodecode/tests/plugin.py +++ b/rhodecode/tests/plugin.py @@ -986,10 +986,9 @@ class PRTestUtility(object): return reference def _get_reviewers(self): - model = UserModel() return [ - model.get_by_username(TEST_USER_REGULAR_LOGIN), - model.get_by_username(TEST_USER_REGULAR2_LOGIN), + (TEST_USER_REGULAR_LOGIN, ['default1'], False), + (TEST_USER_REGULAR2_LOGIN, ['default2'], False), ] def update_source_repository(self, head=None): From a27df6ef4a2a8909815978088ba521e886cbf558 Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Tue, 30 May 2017 16:01:56 +0200 Subject: [PATCH 133/211] features: show new review rules in CE default reviewers --- .../images/ee_features/default_reviewers.png | Bin 122022 -> 160468 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/rhodecode/public/images/ee_features/default_reviewers.png b/rhodecode/public/images/ee_features/default_reviewers.png index f80db8ac2a5655b01a8a1f9651521bcec5b28288..89483e9a4aefb207931d64ab3c2c8d1f53f6906a 100644 GIT binary patch literal 160468 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 F^k2uH&i()Z From 55120caf0e837759c0699bd1cde86bf6e5da23e5 Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Mon, 5 Jun 2017 17:09:03 +0200 Subject: [PATCH 134/211] reviewers: store additional review data. G: changed rhodecode/model/db.py --- rhodecode/model/db.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py index 50a8ad8d..efd1b09b 100644 --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -3968,15 +3968,21 @@ class RepoReviewRule(Base, BaseModel): users[rule_user.user.username] = { 'user': rule_user.user, 'source': 'user', + 'source_data': {}, 'data': rule_user.rule_data() } for rule_user_group in self.rule_user_groups: + source_data = { + 'name': rule_user_group.users_group.users_group_name, + 'members': len(rule_user_group.users_group.members) + } for member in rule_user_group.users_group.members: if member.user.active: users[member.user.username] = { 'user': member.user, 'source': 'user_group', + 'source_data': source_data, 'data': rule_user_group.rule_data() } From 587e36d7458c1c9b628d2f974ce28f700bca3342 Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Thu, 1 Jun 2017 12:46:15 +0200 Subject: [PATCH 135/211] auth-plugins: updated description on auth_ldap plugin servers. --- rhodecode/authentication/plugins/auth_ldap.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/rhodecode/authentication/plugins/auth_ldap.py b/rhodecode/authentication/plugins/auth_ldap.py index 848f803c..f387031d 100644 --- a/rhodecode/authentication/plugins/auth_ldap.py +++ b/rhodecode/authentication/plugins/auth_ldap.py @@ -71,15 +71,17 @@ class LdapSettingsSchema(AuthnPluginSettingsSchemaBase): host = colander.SchemaNode( colander.String(), default='', - description=_('Host of the LDAP Server \n' - '(e.g., 192.168.2.154, or ldap-server.domain.com'), + description=_('Host[s] of the LDAP Server \n' + '(e.g., 192.168.2.154, or ldap-server.domain.com.\n ' + 'Multiple servers can be specified using commas'), preparer=strip_whitespace, title=_('LDAP Host'), widget='string') port = colander.SchemaNode( colander.Int(), default=389, - description=_('Custom port that the LDAP server is listening on. Default: 389'), + description=_('Custom port that the LDAP server is listening on. ' + 'Default value is: 389'), preparer=strip_whitespace, title=_('Port'), validator=colander.Range(min=0, max=65536), From d47af44f72b15801fb3beca5a1f7389bcda8d5e9 Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Mon, 5 Jun 2017 15:24:11 +0200 Subject: [PATCH 136/211] nix: update default.nix to be consistent with ordering with vcsserver. --- default.nix | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/default.nix b/default.nix index b7b0260d..96a917de 100644 --- a/default.nix +++ b/default.nix @@ -174,32 +174,33 @@ let ''; postInstall = '' + echo "Writing meta information for rccontrol to nix-support/rccontrol" + mkdir -p $out/nix-support/rccontrol + cp -v rhodecode/VERSION $out/nix-support/rccontrol/version + echo "DONE: Meta information for rccontrol written" + # python based programs need to be wrapped - ln -s ${self.supervisor}/bin/supervisor* $out/bin/ + ln -s ${self.pyramid}/bin/* $out/bin/ ln -s ${self.gunicorn}/bin/gunicorn $out/bin/ + ln -s ${self.supervisor}/bin/supervisor* $out/bin/ ln -s ${self.PasteScript}/bin/paster $out/bin/ ln -s ${self.channelstream}/bin/channelstream $out/bin/ - ln -s ${self.pyramid}/bin/* $out/bin/ #*/ # rhodecode-tools - # TODO: johbo: re-think this. Do the tools import anything from enterprise? ln -s ${self.rhodecode-tools}/bin/rhodecode-* $out/bin/ # note that condition should be restricted when adding further tools - for file in $out/bin/*; do #*/ + for file in $out/bin/*; + do wrapProgram $file \ - --prefix PYTHONPATH : $PYTHONPATH \ --prefix PATH : $PATH \ + --prefix PYTHONPATH : $PYTHONPATH \ --set PYTHONHASHSEED random done mkdir $out/etc cp configs/production.ini $out/etc - echo "Writing meta information for rccontrol to nix-support/rccontrol" - mkdir -p $out/nix-support/rccontrol - cp -v rhodecode/VERSION $out/nix-support/rccontrol/version - echo "DONE: Meta information for rccontrol written" # TODO: johbo: Make part of ac-tests if [ ! -f rhodecode/public/js/scripts.js ]; then From 71fee00277edc442c13ccc62f94f53925f6e98cb Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Mon, 5 Jun 2017 15:16:04 +0200 Subject: [PATCH 137/211] home: moved home and repo group views into pyramid. - fixed lots of tests related to those changes - now main and subrepo group views are handled by pyramid. --- rhodecode/api/tests/test_get_gist.py | 8 +-- rhodecode/api/tests/test_get_pull_request.py | 22 +++---- rhodecode/api/tests/test_update_repo.py | 7 ++- rhodecode/apps/__init__.py | 19 ++++++ rhodecode/apps/_base/__init__.py | 36 ++++++++++- .../home/tests}/test_home.py | 32 ++++++---- rhodecode/apps/home/views.py | 62 ++++++++++++++++++- rhodecode/apps/login/views.py | 12 ++-- .../tests/test_my_account_password.py | 3 +- rhodecode/apps/repo_group/__init__.py | 33 ++++++++++ rhodecode/apps/repository/__init__.py | 4 ++ rhodecode/config/middleware.py | 1 + rhodecode/config/routing.py | 10 --- rhodecode/controllers/admin/repo_groups.py | 6 +- rhodecode/controllers/admin/repos.py | 4 +- rhodecode/controllers/forks.py | 2 +- rhodecode/controllers/home.py | 43 ------------- rhodecode/events/base.py | 2 +- rhodecode/lib/auth.py | 22 ++++++- rhodecode/lib/base.py | 2 +- rhodecode/lib/helpers.py | 10 +-- rhodecode/lib/utils.py | 7 ++- rhodecode/lib/utils2.py | 3 +- rhodecode/model/db.py | 17 +++-- rhodecode/model/notification.py | 4 +- rhodecode/model/repo.py | 1 + rhodecode/public/js/rhodecode/routes.js | 3 + .../templates/admin/integrations/list.mako | 2 +- .../admin/repo_groups/repo_group_edit.mako | 2 +- .../templates/admin/repos/repo_creating.mako | 2 +- .../admin/users/user_edit_advanced.mako | 2 +- rhodecode/templates/base/base.mako | 4 +- rhodecode/templates/base/perms_summary.mako | 2 +- rhodecode/templates/base/root.mako | 2 +- .../templates/data_table/_dt_elements.mako | 4 +- rhodecode/templates/index_repo_group.mako | 4 +- rhodecode/templates/login.mako | 2 +- rhodecode/templates/password_reset.mako | 2 +- rhodecode/templates/register.mako | 2 +- rhodecode/tests/__init__.py | 3 +- rhodecode/tests/events/test_pullrequest.py | 12 ++-- rhodecode/tests/events/test_repo.py | 1 - .../tests/functional/test_admin_gists.py | 5 +- .../tests/functional/test_admin_my_account.py | 8 ++- .../functional/test_admin_repo_groups.py | 21 +++++-- .../tests/functional/test_delegated_admin.py | 12 +++- rhodecode/tests/functional/test_login.py | 15 +++-- rhodecode/tests/functional/test_sessions.py | 30 ++++++--- rhodecode/tests/functional/test_summary.py | 19 +++--- .../tests/lib/middleware/test_simplevcs.py | 18 ++---- rhodecode/tests/lib/test_codeblocks.py | 5 +- rhodecode/tests/lib/test_helpers.py | 3 +- rhodecode/tests/lib/test_mako_emails.py | 13 ++-- .../tests/models/test_changeset_status.py | 21 ++++--- rhodecode/tests/models/test_notifications.py | 6 +- rhodecode/tests/models/test_pullrequest.py | 20 +++--- .../tests/models/test_pullrequest_git.py | 1 + rhodecode/tests/other/test_libs.py | 14 ++++- .../tests/other/vcs_operations/conftest.py | 8 +-- .../test_vcs_calls_custom_auth_code_403.py | 4 +- .../test_vcs_calls_custom_auth_code_404.py | 4 +- ...est_vcs_calls_custom_auth_code_bad_code.py | 4 +- .../vcs_operations/test_vcs_operations.py | 2 +- .../test_vcs_operations_locking.py | 4 +- ...test_vcs_operations_locking_custom_code.py | 4 +- rhodecode/tests/plugin.py | 28 +++++++-- rhodecode/tests/scripts/test_concurency.py | 8 --- rhodecode/tests/utils.py | 31 ++++++---- .../test_hg_vcsserver_cache_invalidation.py | 5 +- 69 files changed, 457 insertions(+), 277 deletions(-) rename rhodecode/{tests/functional => apps/home/tests}/test_home.py (83%) create mode 100644 rhodecode/apps/repo_group/__init__.py diff --git a/rhodecode/api/tests/test_get_gist.py b/rhodecode/api/tests/test_get_gist.py index b919e79a..9bb4226d 100644 --- a/rhodecode/api/tests/test_get_gist.py +++ b/rhodecode/api/tests/test_get_gist.py @@ -28,7 +28,7 @@ from rhodecode.api.tests.utils import ( @pytest.mark.usefixtures("testuser_api", "app") class TestApiGetGist(object): - def test_api_get_gist(self, gist_util): + def test_api_get_gist(self, gist_util, http_host_stub): gist = gist_util.create_gist() gist_id = gist.gist_access_id gist_created_on = gist.created_on @@ -45,14 +45,14 @@ class TestApiGetGist(object): 'expires': -1.0, 'gist_id': int(gist_id), 'type': 'public', - 'url': 'http://test.example.com:80/_admin/gists/%s' % (gist_id,), + 'url': 'http://%s/_admin/gists/%s' % (http_host_stub, gist_id,), 'acl_level': Gist.ACL_LEVEL_PUBLIC, 'content': None, } assert_ok(id_, expected, given=response.body) - def test_api_get_gist_with_content(self, gist_util): + def test_api_get_gist_with_content(self, gist_util, http_host_stub): mapping = { u'filename1.txt': {'content': u'hello world'}, u'filename1ą.txt': {'content': u'hello worldę'} @@ -73,7 +73,7 @@ class TestApiGetGist(object): 'expires': -1.0, 'gist_id': int(gist_id), 'type': 'public', - 'url': 'http://test.example.com:80/_admin/gists/%s' % (gist_id,), + 'url': 'http://%s/_admin/gists/%s' % (http_host_stub, gist_id,), 'acl_level': Gist.ACL_LEVEL_PUBLIC, 'content': { u'filename1.txt': u'hello world', diff --git a/rhodecode/api/tests/test_get_pull_request.py b/rhodecode/api/tests/test_get_pull_request.py index 3018b6d5..e58c3099 100644 --- a/rhodecode/api/tests/test_get_pull_request.py +++ b/rhodecode/api/tests/test_get_pull_request.py @@ -19,13 +19,13 @@ # and proprietary license terms, please see https://rhodecode.com/licenses/ -import mock import pytest import urlobject from pylons import url from rhodecode.api.tests.utils import ( build_data, api_call, assert_error, assert_ok) +from rhodecode.lib.utils2 import safe_unicode pytestmark = pytest.mark.backends("git", "hg") @@ -33,7 +33,7 @@ pytestmark = pytest.mark.backends("git", "hg") @pytest.mark.usefixtures("testuser_api", "app") class TestGetPullRequest(object): - def test_api_get_pull_request(self, pr_util): + def test_api_get_pull_request(self, pr_util, http_host_stub, http_host_only_stub): from rhodecode.model.pull_request import PullRequestModel pull_request = pr_util.create_pull_request(mergeable=True) id_, params = build_data( @@ -50,16 +50,16 @@ class TestGetPullRequest(object): 'pullrequest_show', repo_name=pull_request.target_repo.repo_name, pull_request_id=pull_request.pull_request_id, qualified=True)) - pr_url = unicode( - url_obj.with_netloc('test.example.com:80')) - source_url = unicode( - pull_request.source_repo.clone_url() - .with_netloc('test.example.com:80')) - target_url = unicode( - pull_request.target_repo.clone_url() - .with_netloc('test.example.com:80')) - shadow_url = unicode( + + pr_url = safe_unicode( + url_obj.with_netloc(http_host_stub)) + source_url = safe_unicode( + pull_request.source_repo.clone_url().with_netloc(http_host_only_stub)) + target_url = safe_unicode( + pull_request.target_repo.clone_url().with_netloc(http_host_only_stub)) + shadow_url = safe_unicode( PullRequestModel().get_shadow_clone_url(pull_request)) + expected = { 'pull_request_id': pull_request.pull_request_id, 'url': pr_url, diff --git a/rhodecode/api/tests/test_update_repo.py b/rhodecode/api/tests/test_update_repo.py index b90380b4..16543cb5 100644 --- a/rhodecode/api/tests/test_update_repo.py +++ b/rhodecode/api/tests/test_update_repo.py @@ -26,7 +26,7 @@ from rhodecode.tests import TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN from rhodecode.api.tests.utils import ( build_data, api_call, assert_error, assert_ok, crash, jsonify) from rhodecode.tests.fixture import Fixture - +from rhodecode.tests.plugin import http_host_stub, http_host_only_stub fixture = Fixture() @@ -71,14 +71,15 @@ class TestApiUpdateRepo(object): ({'repo_name': 'new_repo_name'}, { 'repo_name': 'new_repo_name', - 'url': 'http://test.example.com:80/new_repo_name' + 'url': 'http://{}/new_repo_name'.format(http_host_only_stub()) }), ({'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME), '_group': 'test_group_for_update'}, { 'repo_name': 'test_group_for_update/{}'.format(UPDATE_REPO_NAME), - 'url': 'http://test.example.com:80/test_group_for_update/{}'.format(UPDATE_REPO_NAME) + 'url': 'http://{}/test_group_for_update/{}'.format( + http_host_only_stub(), UPDATE_REPO_NAME) }), ]) def test_api_update_repo(self, updates, expected, backend): diff --git a/rhodecode/apps/__init__.py b/rhodecode/apps/__init__.py index e69de29b..7cb7ec08 100644 --- a/rhodecode/apps/__init__.py +++ b/rhodecode/apps/__init__.py @@ -0,0 +1,19 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# 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/ diff --git a/rhodecode/apps/_base/__init__.py b/rhodecode/apps/_base/__init__.py index 3f06d5c7..79197a0d 100644 --- a/rhodecode/apps/_base/__init__.py +++ b/rhodecode/apps/_base/__init__.py @@ -29,6 +29,7 @@ from rhodecode.lib.utils2 import StrictAttributeDict, safe_int, datetime_to_time from rhodecode.lib.vcs.exceptions import RepositoryRequirementError from rhodecode.lib.ext_json import json from rhodecode.model import repo +from rhodecode.model import repo_group from rhodecode.model.db import User from rhodecode.model.scm import ScmModel @@ -39,6 +40,12 @@ ADMIN_PREFIX = '/_admin' STATIC_FILE_PREFIX = '/_static' +def add_route_with_slash(config,name, pattern, **kw): + config.add_route(name, pattern, **kw) + if not pattern.endswith('/'): + config.add_route(name + '_slash', pattern + '/', **kw) + + def get_format_ref_id(repo): """Returns a `repo` specific reference formatter function""" if h.is_svn(repo): @@ -253,8 +260,7 @@ class RepoRoutePredicate(object): repo_name = info['match']['repo_name'] repo_model = repo.RepoModel() by_name_match = repo_model.get_by_repo_name(repo_name, cache=True) - # if we match quickly from database, short circuit the operation, - # and validate repo based on the type. + if by_name_match: # register this as request object we can re-use later request.db_repo = by_name_match @@ -300,9 +306,33 @@ class RepoTypeRoutePredicate(object): return False +class RepoGroupRoutePredicate(object): + def __init__(self, val, config): + self.val = val + + def text(self): + return 'repo_group_route = %s' % self.val + + phash = text + + def __call__(self, info, request): + repo_group_name = info['match']['repo_group_name'] + repo_group_model = repo_group.RepoGroupModel() + by_name_match = repo_group_model.get_by_group_name( + repo_group_name, cache=True) + + if by_name_match: + # register this as request object we can re-use later + request.db_repo_group = by_name_match + return True + + return False + def includeme(config): config.add_route_predicate( 'repo_route', RepoRoutePredicate) config.add_route_predicate( - 'repo_accepted_types', RepoTypeRoutePredicate) \ No newline at end of file + 'repo_accepted_types', RepoTypeRoutePredicate) + config.add_route_predicate( + 'repo_group_route', RepoGroupRoutePredicate) diff --git a/rhodecode/tests/functional/test_home.py b/rhodecode/apps/home/tests/test_home.py similarity index 83% rename from rhodecode/tests/functional/test_home.py rename to rhodecode/apps/home/tests/test_home.py index dcadb243..e45aeae8 100644 --- a/rhodecode/tests/functional/test_home.py +++ b/rhodecode/apps/home/tests/test_home.py @@ -28,18 +28,25 @@ from rhodecode.model.meta import Session from rhodecode.model.repo import RepoModel from rhodecode.model.repo_group import RepoGroupModel from rhodecode.model.settings import SettingsModel -from rhodecode.tests import TestController, url +from rhodecode.tests import TestController from rhodecode.tests.fixture import Fixture fixture = Fixture() +def route_path(name, **kwargs): + return { + 'home': '/', + 'repo_group_home': '/{repo_group_name}' + }[name].format(**kwargs) + + class TestHomeController(TestController): def test_index(self): self.log_user() - response = self.app.get(url(controller='home', action='index')) + response = self.app.get(route_path('home')) # if global permission is set response.mustcontain('Add Repository') @@ -49,7 +56,7 @@ class TestHomeController(TestController): def test_index_contains_statics_with_ver(self): self.log_user() - response = self.app.get(url(controller='home', action='index')) + response = self.app.get(route_path('home')) rhodecode_version_hash = c.rhodecode_version_hash response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash)) @@ -57,7 +64,7 @@ class TestHomeController(TestController): def test_index_contains_backend_specific_details(self, backend): self.log_user() - response = self.app.get(url(controller='home', action='index')) + response = self.app.get(route_path('home')) tip = backend.repo.get_commit().raw_id # html in javascript variable: @@ -69,17 +76,16 @@ class TestHomeController(TestController): def test_index_with_anonymous_access_disabled(self): with fixture.anon_access(False): - response = self.app.get(url(controller='home', action='index'), - status=302) + response = self.app.get(route_path('home'), status=302) assert 'login' in response.location def test_index_page_on_groups(self, autologin_user, repo_group): - response = self.app.get(url('repo_group_home', group_name='gr1')) + response = self.app.get(route_path('repo_group_home', repo_group_name='gr1')) response.mustcontain("gr1/repo_in_group") def test_index_page_on_group_with_trailing_slash( self, autologin_user, repo_group): - response = self.app.get(url('repo_group_home', group_name='gr1') + '/') + response = self.app.get(route_path('repo_group_home', repo_group_name='gr1') + '/') response.mustcontain("gr1/repo_in_group") @pytest.fixture(scope='class') @@ -93,15 +99,17 @@ class TestHomeController(TestController): RepoGroupModel().delete(repo_group='gr1', force_delete=True) Session().commit() - def test_index_with_name_with_tags(self, autologin_user): - user = User.get_by_username('test_admin') + def test_index_with_name_with_tags(self, user_util, autologin_user): + user = user_util.create_user() + username = user.username user.name = '' user.lastname = ( '') Session().add(user) Session().commit() + user_util.create_repo(owner=username) - response = self.app.get(url(controller='home', action='index')) + response = self.app.get(route_path('home')) response.mustcontain( '<img src="/image1" onload="' 'alert('Hello, World!');">') @@ -122,7 +130,7 @@ class TestHomeController(TestController): Session().commit() SettingsModel().invalidate_settings_cache() - response = self.app.get(url(controller='home', action='index')) + response = self.app.get(route_path('home')) if state is True: response.mustcontain(version_string) if state is False: diff --git a/rhodecode/apps/home/views.py b/rhodecode/apps/home/views.py index 6c02e0de..ea8dab63 100644 --- a/rhodecode/apps/home/views.py +++ b/rhodecode/apps/home/views.py @@ -25,12 +25,15 @@ from pyramid.view import view_config from rhodecode.apps._base import BaseAppView from rhodecode.lib import helpers as h -from rhodecode.lib.auth import LoginRequired, NotAnonymous +from rhodecode.lib.auth import LoginRequired, NotAnonymous, \ + HasRepoGroupPermissionAnyDecorator from rhodecode.lib.index import searcher_from_config from rhodecode.lib.utils2 import safe_unicode, str2bool +from rhodecode.lib.ext_json import json from rhodecode.model.db import func, Repository, RepoGroup from rhodecode.model.repo import RepoModel -from rhodecode.model.scm import ScmModel +from rhodecode.model.repo_group import RepoGroupModel +from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList from rhodecode.model.user import UserModel from rhodecode.model.user_group import UserGroupModel @@ -143,7 +146,7 @@ class HomeView(BaseAppView): 'text': obj.group_name, 'type': 'group', 'obj': {}, - 'url': h.url('repo_group_home', group_name=obj.group_name) + 'url': h.route_path('repo_group_home', repo_group_name=obj.group_name) } for obj in repo_groups_iter] @@ -246,3 +249,56 @@ class HomeView(BaseAppView): 'results': res } return data + + def _get_groups_and_repos(self, repo_group_id=None): + # repo groups groups + repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id) + _perms = ['group.read', 'group.write', 'group.admin'] + repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms) + repo_group_data = RepoGroupModel().get_repo_groups_as_dict( + repo_group_list=repo_group_list_acl, admin=False) + + # repositories + repo_list = Repository.get_all_repos(group_id=repo_group_id) + _perms = ['repository.read', 'repository.write', 'repository.admin'] + repo_list_acl = RepoList(repo_list, perm_set=_perms) + repo_data = RepoModel().get_repos_as_dict( + repo_list=repo_list_acl, admin=False) + + return repo_data, repo_group_data + + @LoginRequired() + @view_config( + route_name='home', request_method='GET', + renderer='rhodecode:templates/index.mako') + def main_page(self): + c = self.load_default_context() + c.repo_group = None + + repo_data, repo_group_data = self._get_groups_and_repos() + # json used to render the grids + c.repos_data = json.dumps(repo_data) + c.repo_groups_data = json.dumps(repo_group_data) + + return self._get_template_context(c) + + @LoginRequired() + @HasRepoGroupPermissionAnyDecorator( + 'group.read', 'group.write', 'group.admin') + @view_config( + route_name='repo_group_home', request_method='GET', + renderer='rhodecode:templates/index_repo_group.mako') + @view_config( + route_name='repo_group_home_slash', request_method='GET', + renderer='rhodecode:templates/index_repo_group.mako') + def repo_group_main_page(self): + c = self.load_default_context() + c.repo_group = self.request.db_repo_group + repo_data, repo_group_data = self._get_groups_and_repos( + c.repo_group.group_id) + + # json used to render the grids + c.repos_data = json.dumps(repo_data) + c.repo_groups_data = json.dumps(repo_group_data) + + return self._get_template_context(c) diff --git a/rhodecode/apps/login/views.py b/rhodecode/apps/login/views.py index 64bc91a9..80429aac 100644 --- a/rhodecode/apps/login/views.py +++ b/rhodecode/apps/login/views.py @@ -25,7 +25,6 @@ import formencode import logging import urlparse -from pylons import url from pyramid.httpexceptions import HTTPFound from pyramid.view import view_config from recaptcha.client.captcha import submit @@ -91,20 +90,21 @@ def get_came_from(request): came_from = safe_str(request.GET.get('came_from', '')) parsed = urlparse.urlparse(came_from) allowed_schemes = ['http', 'https'] + default_came_from = h.route_path('home') if parsed.scheme and parsed.scheme not in allowed_schemes: log.error('Suspicious URL scheme detected %s for url %s' % (parsed.scheme, parsed)) - came_from = url('home') + came_from = default_came_from elif parsed.netloc and request.host != parsed.netloc: log.error('Suspicious NETLOC detected %s for url %s server url ' 'is: %s' % (parsed.netloc, parsed, request.host)) - came_from = url('home') + came_from = default_came_from elif any(bad_str in parsed.path for bad_str in ('\r', '\n')): log.error('Header injection detected `%s` for url %s server url ' % (parsed.path, parsed)) - came_from = url('home') + came_from = default_came_from - return came_from or url('home') + return came_from or default_came_from class LoginView(BaseAppView): @@ -215,7 +215,7 @@ class LoginView(BaseAppView): action='user.logout', action_data=action_data, user=auth_user, commit=True) self.session.delete() - return HTTPFound(url('home')) + return HTTPFound(h.route_path('home')) @HasPermissionAnyDecorator( 'hg.admin', 'hg.register.auto_activate', 'hg.register.manual_activate') diff --git a/rhodecode/apps/my_account/tests/test_my_account_password.py b/rhodecode/apps/my_account/tests/test_my_account_password.py index 8c59e707..16ebbda4 100644 --- a/rhodecode/apps/my_account/tests/test_my_account_password.py +++ b/rhodecode/apps/my_account/tests/test_my_account_password.py @@ -132,6 +132,7 @@ class TestMyAccountPassword(TestController): self.app.post(route_path('my_account_password'), form_data) response = self.app.get(route_path('home')) - new_password_hash = response.session['rhodecode_user']['password'] + session = response.get_session_from_response() + new_password_hash = session['rhodecode_user']['password'] assert old_password_hash != new_password_hash \ No newline at end of file diff --git a/rhodecode/apps/repo_group/__init__.py b/rhodecode/apps/repo_group/__init__.py new file mode 100644 index 00000000..fe0f0472 --- /dev/null +++ b/rhodecode/apps/repo_group/__init__.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2016-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# 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/ +from rhodecode.apps._base import add_route_with_slash + + +def includeme(config): + + # Summary + add_route_with_slash( + config, + name='repo_group_home', + pattern='/{repo_group_name:.*?[^/]}', repo_group_route=True) + + # Scan module for configuration decorators. + config.scan() + diff --git a/rhodecode/apps/repository/__init__.py b/rhodecode/apps/repository/__init__.py index 8d1c116f..57eac222 100644 --- a/rhodecode/apps/repository/__init__.py +++ b/rhodecode/apps/repository/__init__.py @@ -22,6 +22,10 @@ def includeme(config): # Summary + config.add_route( + name='repo_summary', + pattern='/{repo_name:.*?[^/]}', repo_route=True) + config.add_route( name='repo_summary_explicit', pattern='/{repo_name:.*?[^/]}/summary', repo_route=True) diff --git a/rhodecode/config/middleware.py b/rhodecode/config/middleware.py index 70678710..fd8bf257 100644 --- a/rhodecode/config/middleware.py +++ b/rhodecode/config/middleware.py @@ -292,6 +292,7 @@ def includeme(config): config.include('rhodecode.apps.login') config.include('rhodecode.apps.home') config.include('rhodecode.apps.repository') + config.include('rhodecode.apps.repo_group') config.include('rhodecode.apps.search') config.include('rhodecode.apps.user_profile') config.include('rhodecode.apps.my_account') diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py index bcacb1d4..341b0db5 100644 --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -184,9 +184,6 @@ def make_map(config): # CUSTOM ROUTES HERE #========================================================================== - # MAIN PAGE - rmap.connect('home', '/', controller='home', action='index') - # ping and pylons error test rmap.connect('ping', '%s/ping' % (ADMIN_PREFIX,), controller='home', action='ping') rmap.connect('error_test', '%s/error_test' % (ADMIN_PREFIX,), controller='home', action='error_test') @@ -1002,13 +999,6 @@ def make_map(config): conditions={'function': check_repo}, requirements=URL_NAME_REQUIREMENTS) - # must be here for proper group/repo catching pattern - _connect_with_slash( - rmap, 'repo_group_home', '/{group_name}', - controller='home', action='index_repo_group', - conditions={'function': check_group}, - requirements=URL_NAME_REQUIREMENTS) - # catch all, at the end _connect_with_slash( rmap, 'summary_home', '/{repo_name}', jsroute=True, diff --git a/rhodecode/controllers/admin/repo_groups.py b/rhodecode/controllers/admin/repo_groups.py index 69352ca6..ba0a40c7 100644 --- a/rhodecode/controllers/admin/repo_groups.py +++ b/rhodecode/controllers/admin/repo_groups.py @@ -193,10 +193,10 @@ class RepoGroupsController(BaseController): _new_group_name = form_result['group_name_full'] repo_group_url = h.link_to( _new_group_name, - h.url('repo_group_home', group_name=_new_group_name)) + h.route_path('repo_group_home', repo_group_name=_new_group_name)) h.flash(h.literal(_('Created repository group %s') % repo_group_url), category='success') - # TODO: in futureaction_logger(, '', '', '', self.sa) + # TODO: in future action_logger(, '', '', '', self.sa) except formencode.Invalid as errors: return htmlfill.render( render('admin/repo_groups/repo_group_add.mako'), @@ -234,7 +234,6 @@ class RepoGroupsController(BaseController): # # Or using helpers: # h.form(url('repos_group', group_name=GROUP_NAME), method='put') - # url('repo_group_home', group_name=GROUP_NAME) c.repo_group = RepoGroupModel()._get_repo_group(group_name) can_create_in_root = self._can_create_repo_group() @@ -284,7 +283,6 @@ class RepoGroupsController(BaseController): # # Or using helpers: # h.form(url('repos_group', group_name=GROUP_NAME), method='delete') - # url('repo_group_home', group_name=GROUP_NAME) gr = c.repo_group = RepoGroupModel()._get_repo_group(group_name) repos = gr.repositories.all() diff --git a/rhodecode/controllers/admin/repos.py b/rhodecode/controllers/admin/repos.py index 22f265b3..0ed88c8c 100644 --- a/rhodecode/controllers/admin/repos.py +++ b/rhodecode/controllers/admin/repos.py @@ -185,7 +185,7 @@ class ReposController(BaseRepoController): except Exception as e: msg = self._log_creation_exception(e, form_result.get('repo_name')) h.flash(msg, category='error') - return redirect(url('home')) + return redirect(h.route_path('home')) return redirect(h.url('repo_creating_home', repo_name=form_result['repo_name_full'], @@ -265,7 +265,7 @@ class ReposController(BaseRepoController): if task.failed(): msg = self._log_creation_exception(task.result, c.repo) h.flash(msg, category='error') - return redirect(url('home'), code=501) + return redirect(h.route_path('home'), code=501) repo = Repository.get_by_repo_name(repo_name) if repo and repo.repo_state == Repository.STATE_CREATED: diff --git a/rhodecode/controllers/forks.py b/rhodecode/controllers/forks.py index 42118969..4cfc60ad 100644 --- a/rhodecode/controllers/forks.py +++ b/rhodecode/controllers/forks.py @@ -139,7 +139,7 @@ class ForksController(BaseRepoController): c.repo_info = Repository.get_by_repo_name(repo_name) if not c.repo_info: h.not_mapped_error(repo_name) - return redirect(url('home')) + return redirect(h.route_path('home')) defaults = self.__load_data(repo_name) diff --git a/rhodecode/controllers/home.py b/rhodecode/controllers/home.py index fb8198e7..22db964d 100644 --- a/rhodecode/controllers/home.py +++ b/rhodecode/controllers/home.py @@ -65,46 +65,3 @@ class HomeController(BaseController): msg = ('RhodeCode Enterprise %s test exception. Generation time: %s' % (c.rhodecode_name, time.time())) raise TestException(msg) - - def _get_groups_and_repos(self, repo_group_id=None): - # repo groups groups - repo_group_list = RepoGroup.get_all_repo_groups(group_id=repo_group_id) - _perms = ['group.read', 'group.write', 'group.admin'] - repo_group_list_acl = RepoGroupList(repo_group_list, perm_set=_perms) - repo_group_data = RepoGroupModel().get_repo_groups_as_dict( - repo_group_list=repo_group_list_acl, admin=False) - - # repositories - repo_list = Repository.get_all_repos(group_id=repo_group_id) - _perms = ['repository.read', 'repository.write', 'repository.admin'] - repo_list_acl = RepoList(repo_list, perm_set=_perms) - repo_data = RepoModel().get_repos_as_dict( - repo_list=repo_list_acl, admin=False) - - return repo_data, repo_group_data - - @LoginRequired() - def index(self): - c.repo_group = None - - repo_data, repo_group_data = self._get_groups_and_repos() - # json used to render the grids - c.repos_data = json.dumps(repo_data) - c.repo_groups_data = json.dumps(repo_group_data) - - return render('/index.mako') - - @LoginRequired() - @HasRepoGroupPermissionAnyDecorator('group.read', 'group.write', - 'group.admin') - def index_repo_group(self, group_name): - """GET /repo_group_name: Show a specific item""" - c.repo_group = RepoGroupModel()._get_repo_group(group_name) - repo_data, repo_group_data = self._get_groups_and_repos( - c.repo_group.group_id) - - # json used to render the grids - c.repos_data = json.dumps(repo_data) - c.repo_groups_data = json.dumps(repo_group_data) - - return render('index_repo_group.mako') diff --git a/rhodecode/events/base.py b/rhodecode/events/base.py index 2103b66b..50e366b9 100644 --- a/rhodecode/events/base.py +++ b/rhodecode/events/base.py @@ -82,7 +82,7 @@ class RhodecodeEvent(object): if self.request: from rhodecode.lib import helpers as h try: - return h.url('home', qualified=True) + return h.route_url('home') except Exception: log.exception('Failed to fetch URL for server') return default diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py index ba6320bb..53d7fe50 100644 --- a/rhodecode/lib/auth.py +++ b/rhodecode/lib/auth.py @@ -1377,7 +1377,7 @@ class PermsDecorator(object): if anonymous: came_from = self._get_came_from() h.flash(_('You need to be signed in to view this page'), - category='warning') + category='warning') raise HTTPFound( h.route_path('login', _query={'came_from': came_from})) @@ -1429,10 +1429,16 @@ class HasRepoPermissionAllDecorator(PermsDecorator): def check_permissions(self, user): perms = user.permissions repo_name = self._get_repo_name() + try: user_perms = set([perms['repositories'][repo_name]]) except KeyError: + log.debug('cannot locate repo with name: `%s` in permissions defs', + repo_name) return False + + log.debug('checking `%s` permissions for repo `%s`', + user_perms, repo_name) if self.required_perms.issubset(user_perms): return True return False @@ -1450,11 +1456,16 @@ class HasRepoPermissionAnyDecorator(PermsDecorator): def check_permissions(self, user): perms = user.permissions repo_name = self._get_repo_name() + try: user_perms = set([perms['repositories'][repo_name]]) except KeyError: + log.debug('cannot locate repo with name: `%s` in permissions defs', + repo_name) return False + log.debug('checking `%s` permissions for repo `%s`', + user_perms, repo_name) if self.required_perms.intersection(user_perms): return True return False @@ -1476,8 +1487,12 @@ class HasRepoGroupPermissionAllDecorator(PermsDecorator): try: user_perms = set([perms['repositories_groups'][group_name]]) except KeyError: + log.debug('cannot locate repo group with name: `%s` in permissions defs', + group_name) return False + log.debug('checking `%s` permissions for repo group `%s`', + user_perms, group_name) if self.required_perms.issubset(user_perms): return True return False @@ -1496,11 +1511,16 @@ class HasRepoGroupPermissionAnyDecorator(PermsDecorator): def check_permissions(self, user): perms = user.permissions group_name = self._get_repo_group_name() + try: user_perms = set([perms['repositories_groups'][group_name]]) except KeyError: + log.debug('cannot locate repo group with name: `%s` in permissions defs', + group_name) return False + log.debug('checking `%s` permissions for repo group `%s`', + user_perms, group_name) if self.required_perms.intersection(user_perms): return True return False diff --git a/rhodecode/lib/base.py b/rhodecode/lib/base.py index 2eb86c77..463c1d8f 100644 --- a/rhodecode/lib/base.py +++ b/rhodecode/lib/base.py @@ -555,7 +555,7 @@ class BaseRepoController(BaseController): "The repository at %(repo_name)s cannot be located.") % {'repo_name': c.repo_name}, category='error', ignore_duplicate=True) - redirect(url('home')) + redirect(h.route_path('home')) # update last change according to VCS data if not missing_requirements: diff --git a/rhodecode/lib/helpers.py b/rhodecode/lib/helpers.py index a5cd2294..4918ebe0 100644 --- a/rhodecode/lib/helpers.py +++ b/rhodecode/lib/helpers.py @@ -1534,7 +1534,7 @@ def breadcrumb_repo_link(repo): """ path = [ - link_to(group.name, url('repo_group_home', group_name=group.group_name)) + link_to(group.name, route_path('repo_group_home', repo_group_name=group.group_name)) for group in repo.groups_with_parents ] + [ link_to(repo.just_name, url('summary_home', repo_name=repo.repo_name)) @@ -1960,24 +1960,24 @@ def get_last_path_part(file_node): return u'../' + path -def route_url(*args, **kwds): +def route_url(*args, **kwargs): """ Wrapper around pyramids `route_url` (fully qualified url) function. It is used to generate URLs from within pylons views or templates. This will be removed when pyramid migration if finished. """ req = get_current_request() - return req.route_url(*args, **kwds) + return req.route_url(*args, **kwargs) -def route_path(*args, **kwds): +def route_path(*args, **kwargs): """ Wrapper around pyramids `route_path` function. It is used to generate URLs from within pylons views or templates. This will be removed when pyramid migration if finished. """ req = get_current_request() - return req.route_path(*args, **kwds) + return req.route_path(*args, **kwargs) def route_path_or_none(*args, **kwargs): diff --git a/rhodecode/lib/utils.py b/rhodecode/lib/utils.py index a3f4fe3d..78240733 100644 --- a/rhodecode/lib/utils.py +++ b/rhodecode/lib/utils.py @@ -96,10 +96,11 @@ def repo_name_slug(value): # PERM DECORATOR HELPERS FOR EXTRACTING NAMES FOR PERM CHECKS #============================================================================== def get_repo_slug(request): - if isinstance(request, Request) and getattr(request, 'matchdict', None): + if isinstance(request, Request) and getattr(request, 'db_repo', None): # pyramid - _repo = request.matchdict.get('repo_name') + _repo = request.db_repo.repo_name else: + # TODO(marcink): remove after pylons migration... _repo = request.environ['pylons.routes_dict'].get('repo_name') if _repo: @@ -110,7 +111,7 @@ def get_repo_slug(request): def get_repo_group_slug(request): if isinstance(request, Request) and getattr(request, 'matchdict', None): # pyramid - _group = request.matchdict.get('group_name') + _group = request.matchdict.get('repo_group_name') else: _group = request.environ['pylons.routes_dict'].get('group_name') diff --git a/rhodecode/lib/utils2.py b/rhodecode/lib/utils2.py index b23117e5..dfc7d75c 100644 --- a/rhodecode/lib/utils2.py +++ b/rhodecode/lib/utils2.py @@ -572,7 +572,8 @@ def credentials_filter(uri): return ''.join(uri) -def get_clone_url(uri_tmpl, qualifed_home_url, repo_name, repo_id, **override): +def get_clone_url(request, uri_tmpl, repo_name, repo_id, **override): + qualifed_home_url = request.route_url('home') parsed_url = urlobject.URLObject(qualifed_home_url) decoded_path = safe_unicode(urllib.unquote(parsed_url.path.rstrip('/'))) args = { diff --git a/rhodecode/model/db.py b/rhodecode/model/db.py index efd1b09b..b3e6df51 100644 --- a/rhodecode/model/db.py +++ b/rhodecode/model/db.py @@ -44,8 +44,8 @@ from sqlalchemy.sql.expression import true from beaker.cache import cache_region from zope.cachedescriptors.property import Lazy as LazyProperty -from pylons import url from pylons.i18n.translation import lazy_ugettext as _ +from pyramid.threadlocal import get_current_request from rhodecode.lib.vcs import get_vcs_instance from rhodecode.lib.vcs.backends.base import EmptyCommit, Reference @@ -1774,7 +1774,7 @@ class Repository(Base, BaseModel): 'repo_name': repo.repo_name, 'repo_type': repo.repo_type, 'clone_uri': repo.clone_uri or '', - 'url': url('summary_home', repo_name=self.repo_name, qualified=True), + 'url': repo.home_url(), 'private': repo.private, 'created_on': repo.created_on, 'description': repo.description, @@ -1908,7 +1908,6 @@ class Repository(Base, BaseModel): return clone_uri def clone_url(self, **override): - qualified_home_url = url('home', qualified=True) uri_tmpl = None if 'with_id' in override: @@ -1930,11 +1929,16 @@ class Repository(Base, BaseModel): # ie, not having tmpl_context set up pass - return get_clone_url(uri_tmpl=uri_tmpl, - qualifed_home_url=qualified_home_url, + request = get_current_request() + return get_clone_url(request=request, + uri_tmpl=uri_tmpl, repo_name=self.repo_name, repo_id=self.repo_id, **override) + def home_url(self): + request = get_current_request() + return request.route_url('repo_summary', repo_name=self.repo_name) + def set_state(self, state): self.repo_state = state Session().add(self) @@ -3299,6 +3303,7 @@ class _PullRequestBase(BaseModel): return None def get_api_data(self): + from pylons import url from rhodecode.model.pull_request import PullRequestModel pull_request = self merge_status = PullRequestModel().merge_status(pull_request) @@ -3690,6 +3695,8 @@ class Gist(Base, BaseModel): def gist_url(self): import rhodecode + from pylons import url + alias_url = rhodecode.CONFIG.get('gist_alias_url') if alias_url: return alias_url.replace('{gistid}', self.gist_access_id) diff --git a/rhodecode/model/notification.py b/rhodecode/model/notification.py index 0872f59f..1af1b203 100644 --- a/rhodecode/model/notification.py +++ b/rhodecode/model/notification.py @@ -334,9 +334,9 @@ class EmailNotificationModel(BaseModel): """ kwargs['rhodecode_instance_name'] = self.rhodecode_instance_name - + instance_url = h.route_url('home') _kwargs = { - 'instance_url': h.url('home', qualified=True), + 'instance_url': instance_url, 'whitespace_filter': self.whitespace_filter } _kwargs.update(kwargs) diff --git a/rhodecode/model/repo.py b/rhodecode/model/repo.py index 507cc2d2..f08cbe6f 100644 --- a/rhodecode/model/repo.py +++ b/rhodecode/model/repo.py @@ -131,6 +131,7 @@ class RepoModel(BaseModel): :param repo_name: :return: repo object if matched else None """ + try: _repo_id = self._extract_id_from_repo_name(repo_name) if _repo_id: diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js index 81402391..ba7d6c96 100644 --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -99,6 +99,7 @@ function registerRCRoutes() { pyroutes.register('user_group_autocomplete_data', '/_user_groups', []); pyroutes.register('repo_list_data', '/_repos', []); pyroutes.register('goto_switcher_data', '/_goto_data', []); + pyroutes.register('repo_summary', '/%(repo_name)s', ['repo_name']); pyroutes.register('repo_summary_explicit', '/%(repo_name)s/summary', ['repo_name']); pyroutes.register('tags_home', '/%(repo_name)s/tags', ['repo_name']); pyroutes.register('branches_home', '/%(repo_name)s/branches', ['repo_name']); @@ -121,6 +122,8 @@ function registerRCRoutes() { pyroutes.register('strip', '/%(repo_name)s/settings/strip', ['repo_name']); pyroutes.register('strip_check', '/%(repo_name)s/settings/strip_check', ['repo_name']); pyroutes.register('strip_execute', '/%(repo_name)s/settings/strip_execute', ['repo_name']); + pyroutes.register('repo_group_home', '/%(repo_group_name)s', ['repo_group_name']); + pyroutes.register('repo_group_home_slash', '/%(repo_group_name)s/', ['repo_group_name']); pyroutes.register('search', '/_admin/search', []); pyroutes.register('search_repo', '/%(repo_name)s/search', ['repo_name']); pyroutes.register('user_profile', '/_profiles/%(username)s', ['username']); diff --git a/rhodecode/templates/admin/integrations/list.mako b/rhodecode/templates/admin/integrations/list.mako index 8a9bb1cd..d418c0fb 100644 --- a/rhodecode/templates/admin/integrations/list.mako +++ b/rhodecode/templates/admin/integrations/list.mako @@ -164,7 +164,7 @@ ${_('repo')}:${integration.repo.repo_name} %elif integration.repo_group: - + ${_('repogroup')}:${integration.repo_group.group_name} %if integration.child_repos_only: ${_('child repos only')} diff --git a/rhodecode/templates/admin/repo_groups/repo_group_edit.mako b/rhodecode/templates/admin/repo_groups/repo_group_edit.mako index 26ef6803..0b6f3730 100644 --- a/rhodecode/templates/admin/repo_groups/repo_group_edit.mako +++ b/rhodecode/templates/admin/repo_groups/repo_group_edit.mako @@ -13,7 +13,7 @@ » ${h.link_to(_('Repository Groups'),h.url('repo_groups'))} %if c.repo_group.parent_group: - » ${h.link_to(c.repo_group.parent_group.name,h.url('repo_group_home',group_name=c.repo_group.parent_group.group_name))} + » ${h.link_to(c.repo_group.parent_group.name, h.route_path('repo_group_home', repo_group_name=c.repo_group.parent_group.group_name))} %endif » ${c.repo_group.name} diff --git a/rhodecode/templates/admin/repos/repo_creating.mako b/rhodecode/templates/admin/repos/repo_creating.mako index af934681..3a0cd96d 100644 --- a/rhodecode/templates/admin/repos/repo_creating.mako +++ b/rhodecode/templates/admin/repos/repo_creating.mako @@ -61,7 +61,7 @@ } } else { - window.location = "${h.url('home')}"; + window.location = "${h.route_path('home')}"; } } }); diff --git a/rhodecode/templates/admin/users/user_edit_advanced.mako b/rhodecode/templates/admin/users/user_edit_advanced.mako index 31b27be1..880af28e 100644 --- a/rhodecode/templates/admin/users/user_edit_advanced.mako +++ b/rhodecode/templates/admin/users/user_edit_advanced.mako @@ -59,7 +59,7 @@ ${h.secure_form(h.url('create_personal_repo_group', user_id=c.user.user_id), method='post')} %if c.personal_repo_group: -
    ${_('Users personal repository group')} : ${h.link_to(c.personal_repo_group.group_name, url('repo_group_home', group_name=c.personal_repo_group.group_name))}
    +
    ${_('Users personal repository group')} : ${h.link_to(c.personal_repo_group.group_name, h.route_path('repo_group_home', repo_group_name=c.personal_repo_group.group_name))}
    %else:
    ${_('This user currently does not have a personal repository group')} diff --git a/rhodecode/templates/base/base.mako b/rhodecode/templates/base/base.mako index e133b722..97c3f3d0 100644 --- a/rhodecode/templates/base/base.mako +++ b/rhodecode/templates/base/base.mako @@ -7,7 +7,7 @@
    %if l.repository is not None: - ${h.link_to(l.repository.repo_name,h.url('summary_home',repo_name=l.repository.repo_name))} + ${h.link_to(l.repository.repo_name, h.route_path('repo_summary',repo_name=l.repository.repo_name))} %else: ${l.repository_name} %endif diff --git a/rhodecode/templates/admin/integrations/list.mako b/rhodecode/templates/admin/integrations/list.mako index d418c0fb..3918302b 100644 --- a/rhodecode/templates/admin/integrations/list.mako +++ b/rhodecode/templates/admin/integrations/list.mako @@ -160,7 +160,7 @@ %if integration.repo: - + ${_('repo')}:${integration.repo.repo_name} %elif integration.repo_group: diff --git a/rhodecode/templates/admin/repos/repo_creating.mako b/rhodecode/templates/admin/repos/repo_creating.mako index 3a0cd96d..0cfddf14 100644 --- a/rhodecode/templates/admin/repos/repo_creating.mako +++ b/rhodecode/templates/admin/repos/repo_creating.mako @@ -48,12 +48,12 @@ if (jsonResponse === undefined) { setTimeout(function () { // we might have a backend problem, try dashboard again - window.location = "${h.url('summary_home', repo_name = c.repo)}"; + window.location = "${h.route_path('repo_summary', repo_name = c.repo)}"; }, 3000); } else { if (skipCheck || jsonResponse.result === true) { // success, means go to dashboard - window.location = "${h.url('summary_home', repo_name = c.repo)}"; + window.location = "${h.route_path('repo_summary', repo_name = c.repo)}"; } else { // Schedule the next request when the current one's complete setTimeout(worker, 1000); diff --git a/rhodecode/templates/admin/repos/repo_edit_advanced.mako b/rhodecode/templates/admin/repos/repo_edit_advanced.mako index 529843a4..7b8cbda3 100644 --- a/rhodecode/templates/admin/repos/repo_edit_advanced.mako +++ b/rhodecode/templates/admin/repos/repo_edit_advanced.mako @@ -27,7 +27,7 @@ ${h.secure_form(h.route_path('edit_repo_advanced_fork', repo_name=c.repo_info.repo_name), method='POST')} % if c.repo_info.fork: -
    ${h.literal(_('This repository is a fork of %(repo_link)s') % {'repo_link': h.link_to_if(c.has_origin_repo_read_perm,c.repo_info.fork.repo_name, h.url('summary_home', repo_name=c.repo_info.fork.repo_name))})} +
    ${h.literal(_('This repository is a fork of %(repo_link)s') % {'repo_link': h.link_to_if(c.has_origin_repo_read_perm,c.repo_info.fork.repo_name, h.route_path('repo_summary', repo_name=c.repo_info.fork.repo_name))})} |
    % endif diff --git a/rhodecode/templates/admin/settings/settings_system.mako b/rhodecode/templates/admin/settings/settings_system.mako index 94e3e8e6..89117e3f 100644 --- a/rhodecode/templates/admin/settings/settings_system.mako +++ b/rhodecode/templates/admin/settings/settings_system.mako @@ -52,6 +52,6 @@ diff --git a/rhodecode/templates/base/base.mako b/rhodecode/templates/base/base.mako index edaf471f..be474416 100644 --- a/rhodecode/templates/base/base.mako +++ b/rhodecode/templates/base/base.mako @@ -186,7 +186,7 @@ %if repo_instance.fork:

    ${_('Fork of')} - ${repo_instance.fork.repo_name} + ${repo_instance.fork.repo_name}

    %endif @@ -225,7 +225,7 @@
    %if section == 'repositories': - ${k} + ${k} %elif section == 'repositories_groups': ${k} %elif section == 'user_groups': diff --git a/rhodecode/templates/data_table/_dt_elements.mako b/rhodecode/templates/data_table/_dt_elements.mako index f5e78613..18d5690c 100644 --- a/rhodecode/templates/data_table/_dt_elements.mako +++ b/rhodecode/templates/data_table/_dt_elements.mako @@ -9,7 +9,7 @@ - ${h.link_to(f.repo_name,h.url('summary_home',repo_name=f.repo_name))} + ${h.link_to(f.repo_name,h.route_path('repo_summary',repo_name=f.repo_name))}
    ${f.description}
    diff --git a/rhodecode/templates/journal/journal_data.mako b/rhodecode/templates/journal/journal_data.mako index 1d932053..dd140016 100644 --- a/rhodecode/templates/journal/journal_data.mako +++ b/rhodecode/templates/journal/journal_data.mako @@ -20,7 +20,7 @@ %if entry.repository is not None: ${h.link_to(entry.repository.repo_name, - h.url('summary_home',repo_name=entry.repository.repo_name))} + h.route_path('repo_summary',repo_name=entry.repository.repo_name))} %else: ${entry.repository_name} %endif diff --git a/rhodecode/templates/pullrequests/pullrequest_show.mako b/rhodecode/templates/pullrequests/pullrequest_show.mako index 588accb2..edbbe0b2 100644 --- a/rhodecode/templates/pullrequests/pullrequest_show.mako +++ b/rhodecode/templates/pullrequests/pullrequest_show.mako @@ -81,7 +81,7 @@ %endif - ${c.pull_request.source_repo.clone_url()} + ${c.pull_request.source_repo.clone_url()}
    % if c.ancestor_commit: @@ -113,7 +113,7 @@ %endif - ${c.pull_request.target_repo.clone_url()} + ${c.pull_request.target_repo.clone_url()} diff --git a/rhodecode/templates/search/search_commit.mako b/rhodecode/templates/search/search_commit.mako index 45670811..7844da46 100644 --- a/rhodecode/templates/search/search_commit.mako +++ b/rhodecode/templates/search/search_commit.mako @@ -27,7 +27,7 @@ %elif h.get_repo_type_by_name(entry.get('repository')) == 'svn': %endif - ${h.link_to(entry['repository'], h.url('summary_home',repo_name=entry['repository']))} + ${h.link_to(entry['repository'], h.route_path('repo_summary',repo_name=entry['repository']))}
    ${h.link_to(h._shorten_commit_id(entry['commit_id']), diff --git a/rhodecode/templates/search/search_content.mako b/rhodecode/templates/search/search_content.mako index aebe039d..20893279 100644 --- a/rhodecode/templates/search/search_content.mako +++ b/rhodecode/templates/search/search_content.mako @@ -49,7 +49,7 @@ ${h.code_highlight( %elif h.get_repo_type_by_name(entry.get('repository')) == 'svn': %endif - ${h.link_to(entry['repository'], h.url('summary_home',repo_name=entry['repository']))} + ${h.link_to(entry['repository'], h.route_path('repo_summary',repo_name=entry['repository']))}
    ${h.link_to(h.literal(entry['f_path']), h.url('files_home',repo_name=entry['repository'],revision=entry.get('commit_id', 'tip'),f_path=entry['f_path']))} diff --git a/rhodecode/templates/search/search_path.mako b/rhodecode/templates/search/search_path.mako index 53725db2..ff7c55cc 100644 --- a/rhodecode/templates/search/search_path.mako +++ b/rhodecode/templates/search/search_path.mako @@ -16,7 +16,7 @@ %elif h.get_repo_type_by_name(entry.get('repository')) == 'svn': %endif - ${h.link_to(entry['repository'], h.url('summary_home',repo_name=entry['repository']))} + ${h.link_to(entry['repository'], h.route_path('repo_summary',repo_name=entry['repository']))}
    ${h.link_to(h.literal(entry['f_path']), diff --git a/rhodecode/templates/summary/components.mako b/rhodecode/templates/summary/components.mako index 1f721f26..0c0353a7 100644 --- a/rhodecode/templates/summary/components.mako +++ b/rhodecode/templates/summary/components.mako @@ -1,28 +1,28 @@ <%def name="refs_counters(branches, closed_branches, tags, bookmarks)"> - ${ungettext( + ${_ungettext( '%(num)s Branch','%(num)s Branches', len(branches)) % {'num': len(branches)}} %if closed_branches: - ${ungettext( + ${_ungettext( '%(num)s Closed Branch', '%(num)s Closed Branches', len(closed_branches)) % {'num': len(closed_branches)}} %endif - ${ungettext( + ${_ungettext( '%(num)s Tag', '%(num)s Tags', len(tags)) % {'num': len(tags)}} %if bookmarks: - ${ungettext( + ${_ungettext( '%(num)s Bookmark', '%(num)s Bookmarks', len(bookmarks)) % {'num': len(bookmarks)}} %endif @@ -92,15 +92,15 @@ ## commits % if commit_rev == -1: - ${ungettext('%(num)s Commit', '%(num)s Commits', 0) % {'num': 0}}, + ${_ungettext('%(num)s Commit', '%(num)s Commits', 0) % {'num': 0}}, % else: - ${ungettext('%(num)s Commit', '%(num)s Commits', commit_rev) % {'num': commit_rev}}, + ${_ungettext('%(num)s Commit', '%(num)s Commits', commit_rev) % {'num': commit_rev}}, % endif ## forks - ${c.repository_forks} ${ungettext('Fork', 'Forks', c.repository_forks)}, + ${c.repository_forks} ${_ungettext('Fork', 'Forks', c.repository_forks)}, ## repo size % if commit_rev == -1: diff --git a/rhodecode/templates/summary/missing_requirements.mako b/rhodecode/templates/summary/missing_requirements.mako index e96cde64..6991361f 100644 --- a/rhodecode/templates/summary/missing_requirements.mako +++ b/rhodecode/templates/summary/missing_requirements.mako @@ -14,8 +14,14 @@
    Missing requirements - These commits cannot be displayed, because this repository uses the Mercurial largefiles extension, which was not enabled. - Please enable this extension in settings, or contact the repository owner for help. + Commits cannot be displayed, because this repository uses one or more extensions, which was not enabled.
    + Please enable extension in settings, or contact the repository owner for help. + Missing extensions could be: +
    +
    +- Mercurial largefiles
    +- Git LFS
    +
    diff --git a/rhodecode/templates/summary/summary.mako b/rhodecode/templates/summary/summary.mako index 2cbc4938..63f7cc9e 100644 --- a/rhodecode/templates/summary/summary.mako +++ b/rhodecode/templates/summary/summary.mako @@ -36,7 +36,7 @@ %endif
    - <%include file='../changelog/changelog_summary_data.mako'/> + <%include file='summary_commits.mako'/>
    @@ -110,8 +110,7 @@ $(document).ready(function(){ var callback = function (data) { % if c.show_stats: - showRepoStats( - 'lang_stats', data); + showRepoStats('lang_stats', data); % endif }; diff --git a/rhodecode/templates/changelog/changelog_summary_data.mako b/rhodecode/templates/summary/summary_commits.mako similarity index 100% rename from rhodecode/templates/changelog/changelog_summary_data.mako rename to rhodecode/templates/summary/summary_commits.mako diff --git a/rhodecode/tests/events/test_repo.py b/rhodecode/tests/events/test_repo.py index 8b3b83c9..66d89180 100644 --- a/rhodecode/tests/events/test_repo.py +++ b/rhodecode/tests/events/test_repo.py @@ -56,7 +56,7 @@ def scm_extras(user_regular, repo_stub): RepoPreCreateEvent, RepoCreateEvent, RepoPreDeleteEvent, RepoDeleteEvent, ]) -def test_repo_events_serialized(repo_stub, EventClass): +def test_repo_events_serialized(config_stub, repo_stub, EventClass): event = EventClass(repo_stub) data = event.as_dict() assert data['name'] == EventClass.name @@ -67,7 +67,7 @@ def test_repo_events_serialized(repo_stub, EventClass): @pytest.mark.parametrize('EventClass', [ RepoPrePullEvent, RepoPullEvent, RepoPrePushEvent ]) -def test_vcs_repo_events_serialize(repo_stub, scm_extras, EventClass): +def test_vcs_repo_events_serialize(config_stub, repo_stub, scm_extras, EventClass): event = EventClass(repo_name=repo_stub.repo_name, extras=scm_extras) data = event.as_dict() assert data['name'] == EventClass.name @@ -76,7 +76,7 @@ def test_vcs_repo_events_serialize(repo_stub, scm_extras, EventClass): @pytest.mark.parametrize('EventClass', [RepoPushEvent]) -def test_vcs_repo_push_event_serialize(repo_stub, scm_extras, EventClass): +def test_vcs_repo_push_event_serialize(config_stub, repo_stub, scm_extras, EventClass): event = EventClass(repo_name=repo_stub.repo_name, pushed_commit_ids=scm_extras['commit_ids'], extras=scm_extras) diff --git a/rhodecode/tests/functional/test_admin_repos.py b/rhodecode/tests/functional/test_admin_repos.py index 92e4629d..eefbabe1 100644 --- a/rhodecode/tests/functional/test_admin_repos.py +++ b/rhodecode/tests/functional/test_admin_repos.py @@ -24,10 +24,10 @@ import mock import pytest from rhodecode.lib import auth -from rhodecode.lib.utils2 import safe_str, str2bool, safe_unicode -from rhodecode.lib.vcs.exceptions import RepositoryRequirementError -from rhodecode.model.db import Repository, RepoGroup, UserRepoToPerm, User,\ - Permission +from rhodecode.lib.utils2 import safe_str, str2bool +from rhodecode.lib import helpers as h +from rhodecode.model.db import ( + Repository, RepoGroup, UserRepoToPerm, User, Permission) from rhodecode.model.meta import Session from rhodecode.model.repo import RepoModel from rhodecode.model.repo_group import RepoGroupModel @@ -475,7 +475,7 @@ class TestAdminRepos(object): assert new_repo.description == description # test if the repository is visible in the list ? - response = self.app.get(url('summary_home', repo_name=repo_name)) + response = self.app.get(h.route_path('repo_summary', repo_name=repo_name)) response.mustcontain(repo_name) response.mustcontain(backend.alias) diff --git a/rhodecode/tests/functional/test_files.py b/rhodecode/tests/functional/test_files.py index cbc18225..d6473c12 100644 --- a/rhodecode/tests/functional/test_files.py +++ b/rhodecode/tests/functional/test_files.py @@ -796,11 +796,12 @@ class TestChangingFiles: # Not allowed, redirect to the summary redirected = response.follow() - summary_url = url('summary_home', repo_name=repo.repo_name) + summary_url = h.route_path('repo_summary', repo_name=repo.repo_name) # As there are no commits, displays the summary page with the error of # creating a file with no filename - assert redirected.req.path == summary_url + + assert redirected.request.path == summary_url @pytest.mark.parametrize("location, filename", [ ('/abs', 'foo'), diff --git a/rhodecode/tests/functional/test_forks.py b/rhodecode/tests/functional/test_forks.py index 5bd807b4..b5f1466e 100644 --- a/rhodecode/tests/functional/test_forks.py +++ b/rhodecode/tests/functional/test_forks.py @@ -22,6 +22,7 @@ import pytest from rhodecode.tests import * from rhodecode.tests.fixture import Fixture +from rhodecode.lib import helpers as h from rhodecode.model.db import Repository from rhodecode.model.repo import RepoModel @@ -147,7 +148,7 @@ class _BaseTest(TestController): assert fork_repo.fork.repo_name == repo_name # test if the repository is visible in the list ? - response = self.app.get(url('summary_home', repo_name=fork_name_full)) + response = self.app.get(h.route_path('repo_summary', repo_name=fork_name_full)) response.mustcontain(fork_name_full) response.mustcontain(self.REPO_TYPE) @@ -193,7 +194,7 @@ class _BaseTest(TestController): assert fork_repo.fork.repo_name == repo_name # test if the repository is visible in the list ? - response = self.app.get(url('summary_home', repo_name=fork_name)) + response = self.app.get(h.route_path('repo_summary', repo_name=fork_name)) response.mustcontain(fork_name) response.mustcontain(self.REPO_TYPE) response.mustcontain('Fork of') @@ -254,7 +255,7 @@ class TestHG(_BaseTest): @pytest.mark.usefixtures('app', 'autologin_user') @pytest.mark.skip_backends('git','hg') -class TestSVNFork: +class TestSVNFork(object): def test_fork_redirects(self, backend): denied_actions = ['fork','fork_create'] @@ -266,7 +267,7 @@ class TestSVNFork: # Not allowed, redirect to the summary redirected = response.follow() - summary_url = url('summary_home', repo_name=backend.repo_name) + summary_url = h.route_path('repo_summary', repo_name=backend.repo_name) # URL adds leading slash and path doesn't have it - assert redirected.req.path == summary_url + assert redirected.request.path == summary_url diff --git a/rhodecode/tests/functional/test_login.py b/rhodecode/tests/functional/test_login.py index a3044784..242310b3 100644 --- a/rhodecode/tests/functional/test_login.py +++ b/rhodecode/tests/functional/test_login.py @@ -29,6 +29,7 @@ from rhodecode.tests import ( no_newline_id_generator) from rhodecode.tests.fixture import Fixture from rhodecode.lib.auth import check_password +from rhodecode.lib import helpers as h from rhodecode.model.auth_token import AuthTokenModel from rhodecode.model import validators from rhodecode.model.db import User, Notification, UserApiKeys @@ -105,8 +106,9 @@ class TestLoginController(object): with fixture.anon_access(False): kwargs = {'branch': 'stable'} response = self.app.get( - url('summary_home', repo_name=HG_REPO, **kwargs)) + h.route_path('repo_summary', repo_name=HG_REPO, _query=kwargs)) assert response.status == '302 Found' + response_query = urlparse.parse_qsl(response.location) assert 'branch=stable' in response_query[0][1] diff --git a/rhodecode/tests/functional/test_pullrequests.py b/rhodecode/tests/functional/test_pullrequests.py index 1553ba01..422c3e61 100644 --- a/rhodecode/tests/functional/test_pullrequests.py +++ b/rhodecode/tests/functional/test_pullrequests.py @@ -24,13 +24,13 @@ from webob.exc import HTTPNotFound import rhodecode from rhodecode.lib.vcs.nodes import FileNode +from rhodecode.lib import helpers as h from rhodecode.model.changeset_status import ChangesetStatusModel from rhodecode.model.db import ( PullRequest, ChangesetStatus, UserLog, Notification) from rhodecode.model.meta import Session from rhodecode.model.pull_request import PullRequestModel from rhodecode.model.user import UserModel -from rhodecode.model.repo import RepoModel from rhodecode.tests import ( assert_session_flash, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN) from rhodecode.tests.utils import AssertResponse @@ -38,7 +38,7 @@ from rhodecode.tests.utils import AssertResponse @pytest.mark.usefixtures('app', 'autologin_user') @pytest.mark.backends("git", "hg") -class TestPullrequestsController: +class TestPullrequestsController(object): def test_index(self, backend): self.app.get(url( @@ -47,7 +47,7 @@ class TestPullrequestsController: def test_option_menu_create_pull_request_exists(self, backend): repo_name = backend.repo_name - response = self.app.get(url('summary_home', repo_name=repo_name)) + response = self.app.get(h.route_path('repo_summary', repo_name=repo_name)) create_pr_link = 'Create Pull Request' % url( 'pullrequest', repo_name=repo_name) @@ -1074,10 +1074,10 @@ def test_redirects_to_repo_summary_for_svn_repositories(backend_svn, app, action # Not allowed, redirect to the summary redirected = response.follow() - summary_url = url('summary_home', repo_name=backend_svn.repo_name) + summary_url = h.route_path('repo_summary', repo_name=backend_svn.repo_name) # URL adds leading slash and path doesn't have it - assert redirected.req.path == summary_url + assert redirected.request.path == summary_url def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp): diff --git a/rhodecode/tests/lib/test_base.py b/rhodecode/tests/lib/test_base.py index cd3288de..c453fad2 100644 --- a/rhodecode/tests/lib/test_base.py +++ b/rhodecode/tests/lib/test_base.py @@ -20,10 +20,10 @@ import pytest from mock import Mock, patch -from pylons import url from rhodecode.lib import base from rhodecode.lib.vcs.exceptions import RepositoryRequirementError +from rhodecode.lib import helpers as h from rhodecode.model import db @@ -165,7 +165,7 @@ class TestBaseRepoControllerHandleMissingRequirements(object): context_mock.repo_name = repo_name controller._handle_missing_requirements(error) - expected_url = url('summary_home', repo_name=repo_name) + expected_url = h.route_path('repo_summary', repo_name=repo_name) if should_redirect: redirect_mock.assert_called_once_with(expected_url) else: diff --git a/rhodecode/tests/plugin.py b/rhodecode/tests/plugin.py index 9c724864..1a3155e0 100644 --- a/rhodecode/tests/plugin.py +++ b/rhodecode/tests/plugin.py @@ -1682,6 +1682,15 @@ def request_stub(): return request +@pytest.fixture +def context_stub(): + """ + Stub context object. + """ + context = pyramid.testing.DummyResource() + return context + + @pytest.fixture def config_stub(request, request_stub): """ diff --git a/rhodecode/tests/utils.py b/rhodecode/tests/utils.py index 2edc357b..d569ce57 100644 --- a/rhodecode/tests/utils.py +++ b/rhodecode/tests/utils.py @@ -123,6 +123,7 @@ def set_anonymous_access(enabled): user.active = enabled Session().add(user) Session().commit() + time.sleep(1.5) # must sleep for cache (1s to expire) log.info('anonymous access is now: %s', enabled) assert enabled == User.get_default_user().active, ( 'Cannot set anonymous access') From 53d41696e03146dcfbc614c35117434333180c99 Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Wed, 7 Jun 2017 14:35:27 +0200 Subject: [PATCH 149/211] pull-requests: expose author in creation view to be consistent with show view. --- .../templates/pullrequests/pullrequest.mako | 27 ++++++++++++++++--- .../pullrequests/pullrequest_show.mako | 2 +- 2 files changed, 25 insertions(+), 4 deletions(-) diff --git a/rhodecode/templates/pullrequests/pullrequest.mako b/rhodecode/templates/pullrequests/pullrequest.mako index 3d5cd54b..178d4a38 100644 --- a/rhodecode/templates/pullrequests/pullrequest.mako +++ b/rhodecode/templates/pullrequests/pullrequest.mako @@ -20,15 +20,22 @@
    ${self.repo_page_title(c.rhodecode_db_repo)} - ${self.breadcrumbs()}
    ${h.secure_form(url('pullrequest', repo_name=c.repo_name), method='post', id='pull_request_form')} + + ${self.breadcrumbs()} +
    -
    + +
    + ${_('Pull request summary')} +
    + +
    @@ -102,7 +109,21 @@
    - ## REIEW RULES + ## AUTHOR +
    +
    + ${_('Author of this pull request')} +
    +
    +
    +
      +
    • + ${self.gravatar_with_user(c.rhodecode_user.email, 16)} +
    • +
    +
    + + ## REVIEW RULES
    - ${h.secure_form(url('my_account_emails'),method='delete')} + ${h.secure_form(h.route_path('my_account_emails_delete'), method='POST')} ${h.hidden('del_email_id',em.email_id)} - ${h.end_form()} @@ -48,7 +48,7 @@
    - ${h.secure_form(url('my_account_emails'), method='post')} + ${h.secure_form(h.route_path('my_account_emails_add'), method='POST')}
    diff --git a/rhodecode/tests/functional/test_admin_my_account.py b/rhodecode/tests/functional/test_admin_my_account.py index 9686b2fa..385b05b4 100644 --- a/rhodecode/tests/functional/test_admin_my_account.py +++ b/rhodecode/tests/functional/test_admin_my_account.py @@ -23,8 +23,7 @@ import pytest from rhodecode.lib import helpers as h from rhodecode.model.db import User, UserFollowing, Repository from rhodecode.tests import ( - TestController, url, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL, - assert_session_flash) + TestController, url, TEST_USER_ADMIN_LOGIN, assert_session_flash) from rhodecode.tests.fixture import Fixture from rhodecode.tests.utils import AssertResponse @@ -90,56 +89,7 @@ class TestMyAccountController(TestController): response.mustcontain('"name_raw": %s' % pr.pull_request_id) response.mustcontain('TestMyAccountPR') - def test_my_account_my_emails(self): - self.log_user() - response = self.app.get(url('my_account_emails')) - response.mustcontain('No additional emails specified') - def test_my_account_my_emails_add_existing_email(self): - self.log_user() - response = self.app.get(url('my_account_emails')) - response.mustcontain('No additional emails specified') - response = self.app.post(url('my_account_emails'), - {'new_email': TEST_USER_REGULAR_EMAIL, - 'csrf_token': self.csrf_token}) - assert_session_flash(response, 'This e-mail address is already taken') - - def test_my_account_my_emails_add_mising_email_in_form(self): - self.log_user() - response = self.app.get(url('my_account_emails')) - response.mustcontain('No additional emails specified') - response = self.app.post(url('my_account_emails'), - {'csrf_token': self.csrf_token}) - assert_session_flash(response, 'Please enter an email address') - - def test_my_account_my_emails_add_remove(self): - self.log_user() - response = self.app.get(url('my_account_emails')) - response.mustcontain('No additional emails specified') - - response = self.app.post(url('my_account_emails'), - {'new_email': 'foo@barz.com', - 'csrf_token': self.csrf_token}) - - response = self.app.get(url('my_account_emails')) - - from rhodecode.model.db import UserEmailMap - email_id = UserEmailMap.query().filter( - UserEmailMap.user == User.get_by_username( - TEST_USER_ADMIN_LOGIN)).filter( - UserEmailMap.email == 'foo@barz.com').one().email_id - - response.mustcontain('foo@barz.com') - response.mustcontain('' % email_id) - - response = self.app.post( - url('my_account_emails'), { - 'del_email_id': email_id, '_method': 'delete', - 'csrf_token': self.csrf_token}) - assert_session_flash(response, 'Removed email address from user account') - response = self.app.get(url('my_account_emails')) - response.mustcontain('No additional emails specified') @pytest.mark.parametrize( "name, attrs", [ From cc531629819742e87cf057cab30b2ea2192e2133 Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Mon, 19 Jun 2017 18:15:57 +0200 Subject: [PATCH 180/211] security: use 404 instead of 403 code on permission decorator to prevent resource discovery attacks. --- rhodecode/lib/auth.py | 6 +++--- rhodecode/tests/functional/test_admin_settings.py | 6 +++--- rhodecode/tests/functional/test_forks.py | 2 +- rhodecode/tests/functional/test_integrations.py | 2 +- 4 files changed, 8 insertions(+), 8 deletions(-) diff --git a/rhodecode/lib/auth.py b/rhodecode/lib/auth.py index b9f8d968..6d237c0b 100644 --- a/rhodecode/lib/auth.py +++ b/rhodecode/lib/auth.py @@ -34,7 +34,7 @@ import traceback from functools import wraps import ipaddress -from pyramid.httpexceptions import HTTPForbidden, HTTPFound +from pyramid.httpexceptions import HTTPForbidden, HTTPFound, HTTPNotFound from pylons.i18n.translation import _ # NOTE(marcink): this has to be removed only after pyramid migration, # replace with _ = request.translate @@ -1415,8 +1415,8 @@ class PermsDecorator(object): h.route_path('login', _query={'came_from': came_from})) else: - # redirect with forbidden ret code - raise HTTPForbidden() + # redirect with 404 to prevent resource discovery + raise HTTPNotFound() def check_permissions(self, user): """Dummy function for overriding""" diff --git a/rhodecode/tests/functional/test_admin_settings.py b/rhodecode/tests/functional/test_admin_settings.py index 33847664..4d58af62 100644 --- a/rhodecode/tests/functional/test_admin_settings.py +++ b/rhodecode/tests/functional/test_admin_settings.py @@ -462,7 +462,7 @@ class TestOpenSourceLicenses(object): '.panel-heading', 'Licenses of Third Party Packages') def test_forbidden_when_normal_user(self, autologin_regular_user): - self.app.get(self._get_url(), status=403) + self.app.get(self._get_url(), status=404) @pytest.mark.usefixtures('app') @@ -475,7 +475,7 @@ class TestUserSessions(object): }[name] def test_forbidden_when_normal_user(self, autologin_regular_user): - self.app.get(self._get_url(), status=403) + self.app.get(self._get_url(), status=404) def test_show_sessions_page(self, autologin_user): response = self.app.get(self._get_url(), status=200) @@ -502,7 +502,7 @@ class TestAdminSystemInfo(object): }[name] def test_forbidden_when_normal_user(self, autologin_regular_user): - self.app.get(self._get_url(), status=403) + self.app.get(self._get_url(), status=404) def test_system_info_page(self, autologin_user): response = self.app.get(self._get_url()) diff --git a/rhodecode/tests/functional/test_forks.py b/rhodecode/tests/functional/test_forks.py index b5f1466e..58ecff1f 100644 --- a/rhodecode/tests/functional/test_forks.py +++ b/rhodecode/tests/functional/test_forks.py @@ -75,7 +75,7 @@ class _BaseTest(TestController): repo_name = self.REPO self.app.post( url(controller='forks', action='fork_create', repo_name=repo_name), - {'csrf_token': self.csrf_token}, status=403) + {'csrf_token': self.csrf_token}, status=404) def test_index_with_fork(self): self.log_user() diff --git a/rhodecode/tests/functional/test_integrations.py b/rhodecode/tests/functional/test_integrations.py index ebde5aff..aad85e41 100644 --- a/rhodecode/tests/functional/test_integrations.py +++ b/rhodecode/tests/functional/test_integrations.py @@ -214,7 +214,7 @@ def _post_integration_test_helper(app, url, csrf_token, repo, repo_group, checks if the redirect url is correct. """ - app.post(url, params={}, status=403) # missing csrf check + app.post(url, params={}, status=403) # missing csrf check response = app.post(url, params={'csrf_token': csrf_token}) assert response.status_code == 200 assert 'Errors exist' in response.body From e919908333599841f0d7505d36f3e6975bc23285 Mon Sep 17 00:00:00 2001 From: Marcin Lulek Date: Mon, 19 Jun 2017 21:15:37 +0200 Subject: [PATCH 181/211] security: make sure the admin of repo can only delete comments which are from the same repo. - fixes IDOR issue - protects against other people comment deletion by repo admins. --- rhodecode/controllers/changeset.py | 12 ++++++--- rhodecode/controllers/pullrequests.py | 27 +++++++++++-------- .../tests/functional/test_pullrequests.py | 21 ++++++++------- 3 files changed, 35 insertions(+), 25 deletions(-) diff --git a/rhodecode/controllers/changeset.py b/rhodecode/controllers/changeset.py index 42f04359..772afb72 100644 --- a/rhodecode/controllers/changeset.py +++ b/rhodecode/controllers/changeset.py @@ -40,7 +40,7 @@ from rhodecode.lib.compat import OrderedDict from rhodecode.lib.exceptions import StatusChangeOnClosedPullRequestError import rhodecode.lib.helpers as h from rhodecode.lib.utils import jsonify -from rhodecode.lib.utils2 import safe_unicode +from rhodecode.lib.utils2 import safe_unicode, safe_int from rhodecode.lib.vcs.backends.base import EmptyCommit from rhodecode.lib.vcs.exceptions import ( RepositoryError, CommitDoesNotExistError, NodeDoesNotExistError) @@ -431,15 +431,19 @@ class ChangesetController(BaseRepoController): @auth.CSRFRequired() @jsonify def delete_comment(self, repo_name, comment_id): - comment = ChangesetComment.get(comment_id) + comment = ChangesetComment.get_or_404(safe_int(comment_id)) if not comment: log.debug('Comment with id:%s not found, skipping', comment_id) # comment already deleted in another call probably return True - owner = (comment.author.user_id == c.rhodecode_user.user_id) is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name) - if h.HasPermissionAny('hg.admin')() or is_repo_admin or owner: + super_admin = h.HasPermissionAny('hg.admin')() + comment_owner = (comment.author.user_id == c.rhodecode_user.user_id) + is_repo_comment = comment.repo.repo_name == c.repo_name + comment_repo_admin = is_repo_admin and is_repo_comment + + if super_admin or comment_owner or comment_repo_admin: CommentsModel().delete(comment=comment, user=c.rhodecode_user) Session().commit() return True diff --git a/rhodecode/controllers/pullrequests.py b/rhodecode/controllers/pullrequests.py index 55a558e0..72fc717f 100644 --- a/rhodecode/controllers/pullrequests.py +++ b/rhodecode/controllers/pullrequests.py @@ -987,25 +987,30 @@ class PullrequestsController(BaseRepoController): @auth.CSRFRequired() @jsonify def delete_comment(self, repo_name, comment_id): - return self._delete_comment(comment_id) + comment = ChangesetComment.get_or_404(safe_int(comment_id)) + if not comment: + log.debug('Comment with id:%s not found, skipping', comment_id) + # comment already deleted in another call probably + return True - def _delete_comment(self, comment_id): - comment_id = safe_int(comment_id) - co = ChangesetComment.get_or_404(comment_id) - if co.pull_request.is_closed(): + if comment.pull_request.is_closed(): # don't allow deleting comments on closed pull request raise HTTPForbidden() - is_owner = co.author.user_id == c.rhodecode_user.user_id is_repo_admin = h.HasRepoPermissionAny('repository.admin')(c.repo_name) - if h.HasPermissionAny('hg.admin')() or is_repo_admin or is_owner: - old_calculated_status = co.pull_request.calculated_review_status() - CommentsModel().delete(comment=co, user=c.rhodecode_user) + super_admin = h.HasPermissionAny('hg.admin')() + comment_owner = comment.author.user_id == c.rhodecode_user.user_id + is_repo_comment = comment.repo.repo_name == c.repo_name + comment_repo_admin = is_repo_admin and is_repo_comment + + if super_admin or comment_owner or comment_repo_admin: + old_calculated_status = comment.pull_request.calculated_review_status() + CommentsModel().delete(comment=comment, user=c.rhodecode_user) Session().commit() - calculated_status = co.pull_request.calculated_review_status() + calculated_status = comment.pull_request.calculated_review_status() if old_calculated_status != calculated_status: PullRequestModel()._trigger_pull_request_hook( - co.pull_request, c.rhodecode_user, 'review_status_change') + comment.pull_request, c.rhodecode_user, 'review_status_change') return True else: raise HTTPForbidden() diff --git a/rhodecode/tests/functional/test_pullrequests.py b/rhodecode/tests/functional/test_pullrequests.py index 1a103f5f..cd4ab03c 100644 --- a/rhodecode/tests/functional/test_pullrequests.py +++ b/rhodecode/tests/functional/test_pullrequests.py @@ -1060,6 +1060,17 @@ class TestPullrequestsControllerDelete(object): response.mustcontain('id="delete_pullrequest"') response.mustcontain(no=['Confirm to delete this pull request']) + def test_delete_comment_returns_404_if_comment_does_not_exist( + self, autologin_user, pr_util, user_admin): + + pull_request = pr_util.create_pull_request( + author=user_admin.username, enable_notifications=False) + + self.app.get(url( + controller='pullrequests', action='delete_comment', + repo_name=pull_request.target_repo.scm_instance().name, + comment_id=1024404), status=404) + def assert_pull_request_status(pull_request, expected_status): status = ChangesetStatusModel().calculated_review_status( @@ -1081,13 +1092,3 @@ def test_redirects_to_repo_summary_for_svn_repositories(backend_svn, app, action # URL adds leading slash and path doesn't have it assert redirected.request.path == summary_url - - -def test_delete_comment_returns_404_if_comment_does_not_exist(pylonsapp): - # TODO: johbo: Global import not possible because models.forms blows up - from rhodecode.controllers.pullrequests import PullrequestsController - controller = PullrequestsController() - patcher = mock.patch( - 'rhodecode.model.db.BaseModel.get', return_value=None) - with pytest.raises(HTTPNotFound), patcher: - controller._delete_comment(1) From 214e9059ec066e4e5645e16361f1c477fd1d49ec Mon Sep 17 00:00:00 2001 From: Marcin Kuzminski Date: Tue, 20 Jun 2017 00:11:36 +0200 Subject: [PATCH 182/211] my-account: moved few my account views into pyramid. --- rhodecode/apps/my_account/__init__.py | 20 +++++ .../tests/test_my_account_simple_views.py | 76 ++++++++++++++++ rhodecode/apps/my_account/views.py | 88 ++++++++++++++++++- rhodecode/config/routing.py | 17 ---- rhodecode/controllers/admin/my_account.py | 60 +------------ rhodecode/public/js/rhodecode/routes.js | 8 ++ .../admin/my_account/my_account.mako | 8 +- .../my_account/my_account_notifications.mako | 2 +- .../tests/functional/test_admin_my_account.py | 22 ----- 9 files changed, 197 insertions(+), 104 deletions(-) create mode 100644 rhodecode/apps/my_account/tests/test_my_account_simple_views.py diff --git a/rhodecode/apps/my_account/__init__.py b/rhodecode/apps/my_account/__init__.py index 3b3b3fa5..388f714b 100644 --- a/rhodecode/apps/my_account/__init__.py +++ b/rhodecode/apps/my_account/__init__.py @@ -56,6 +56,26 @@ def includeme(config): name='my_account_emails_delete', pattern=ADMIN_PREFIX + '/my_account/emails/delete') + config.add_route( + name='my_account_repos', + pattern=ADMIN_PREFIX + '/my_account/repos') + + config.add_route( + name='my_account_watched', + pattern=ADMIN_PREFIX + '/my_account/watched') + + config.add_route( + name='my_account_perms', + pattern=ADMIN_PREFIX + '/my_account/perms') + + config.add_route( + name='my_account_notifications', + pattern=ADMIN_PREFIX + '/my_account/notifications') + + config.add_route( + name='my_account_notifications_toggle_visibility', + pattern=ADMIN_PREFIX + '/my_account/toggle_visibility') + # channelstream test config.add_route( name='my_account_notifications_test_channelstream', diff --git a/rhodecode/apps/my_account/tests/test_my_account_simple_views.py b/rhodecode/apps/my_account/tests/test_my_account_simple_views.py new file mode 100644 index 00000000..9620ac1e --- /dev/null +++ b/rhodecode/apps/my_account/tests/test_my_account_simple_views.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +# Copyright (C) 2010-2017 RhodeCode GmbH +# +# This program is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License, version 3 +# (only), as published by the Free Software Foundation. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +# You should have received a copy of the GNU Affero General Public License +# along with this program. If not, see . +# +# 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 pytest + +from rhodecode.apps._base import ADMIN_PREFIX +from rhodecode.model.db import User, UserEmailMap, Repository, UserFollowing +from rhodecode.tests import ( + TestController, TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_EMAIL, + assert_session_flash) +from rhodecode.tests.fixture import Fixture + +fixture = Fixture() + + +def route_path(name, **kwargs): + return { + 'my_account_repos': + ADMIN_PREFIX + '/my_account/repos', + 'my_account_watched': + ADMIN_PREFIX + '/my_account/watched', + 'my_account_perms': + ADMIN_PREFIX + '/my_account/perms', + 'my_account_notifications': + ADMIN_PREFIX + '/my_account/notifications', + }[name].format(**kwargs) + + +class TestMyAccountSimpleViews(TestController): + + def test_my_account_my_repos(self, autologin_user): + response = self.app.get(route_path('my_account_repos')) + repos = Repository.query().filter( + Repository.user == User.get_by_username( + TEST_USER_ADMIN_LOGIN)).all() + for repo in repos: + response.mustcontain('"name_raw": "%s"' % repo.repo_name) + + def test_my_account_my_watched(self, autologin_user): + response = self.app.get(route_path('my_account_watched')) + + repos = UserFollowing.query().filter( + UserFollowing.user == User.get_by_username( + TEST_USER_ADMIN_LOGIN)).all() + for repo in repos: + response.mustcontain( + '"name_raw": "%s"' % repo.follows_repository.repo_name) + + def test_my_account_perms(self, autologin_user): + response = self.app.get(route_path('my_account_perms')) + assert_response = response.assert_response() + assert assert_response.get_elements('.perm_tag.none') + assert assert_response.get_elements('.perm_tag.read') + assert assert_response.get_elements('.perm_tag.write') + assert assert_response.get_elements('.perm_tag.admin') + + def test_my_account_notifications(self, autologin_user): + response = self.app.get(route_path('my_account_notifications')) + response.mustcontain('Test flash message') diff --git a/rhodecode/apps/my_account/views.py b/rhodecode/apps/my_account/views.py index 12473d42..e2af22c6 100644 --- a/rhodecode/apps/my_account/views.py +++ b/rhodecode/apps/my_account/views.py @@ -28,14 +28,18 @@ from pyramid.view import view_config from rhodecode.apps._base import BaseAppView from rhodecode import forms from rhodecode.lib import helpers as h +from rhodecode.lib.ext_json import json from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired from rhodecode.lib.channelstream import channelstream_request, \ ChannelstreamException from rhodecode.lib.utils2 import safe_int, md5 from rhodecode.model.auth_token import AuthTokenModel -from rhodecode.model.db import UserEmailMap +from rhodecode.model.db import ( + Repository, PullRequest, UserEmailMap, User, UserFollowing, joinedload) from rhodecode.model.meta import Session +from rhodecode.model.scm import RepoList from rhodecode.model.user import UserModel +from rhodecode.model.repo import RepoModel from rhodecode.model.validation_schema.schemas import user_schema log = logging.getLogger(__name__) @@ -290,3 +294,85 @@ class MyAccountView(BaseAppView): return {"response": 'ERROR: {}'.format(e.__class__.__name__)} return {"response": 'Channelstream data sent. ' 'You should see a new live message now.'} + + def _load_my_repos_data(self, watched=False): + if watched: + admin = False + follows_repos = Session().query(UserFollowing)\ + .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\ + .options(joinedload(UserFollowing.follows_repository))\ + .all() + repo_list = [x.follows_repository for x in follows_repos] + else: + admin = True + repo_list = Repository.get_all_repos( + user_id=self._rhodecode_user.user_id) + repo_list = RepoList(repo_list, perm_set=[ + 'repository.read', 'repository.write', 'repository.admin']) + + repos_data = RepoModel().get_repos_as_dict( + repo_list=repo_list, admin=admin) + # json used to render the grid + return json.dumps(repos_data) + + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='my_account_repos', request_method='GET', + renderer='rhodecode:templates/admin/my_account/my_account.mako') + def my_account_repos(self): + c = self.load_default_context() + c.active = 'repos' + + # json used to render the grid + c.data = self._load_my_repos_data() + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='my_account_watched', request_method='GET', + renderer='rhodecode:templates/admin/my_account/my_account.mako') + def my_account_watched(self): + c = self.load_default_context() + c.active = 'watched' + + # json used to render the grid + c.data = self._load_my_repos_data(watched=True) + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='my_account_perms', request_method='GET', + renderer='rhodecode:templates/admin/my_account/my_account.mako') + def my_account_perms(self): + c = self.load_default_context() + c.active = 'perms' + + c.perm_user = c.auth_user + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @view_config( + route_name='my_account_notifications', request_method='GET', + renderer='rhodecode:templates/admin/my_account/my_account.mako') + def my_notifications(self): + c = self.load_default_context() + c.active = 'notifications' + + return self._get_template_context(c) + + @LoginRequired() + @NotAnonymous() + @CSRFRequired() + @view_config( + route_name='my_account_notifications_toggle_visibility', + request_method='POST', renderer='json_ext') + def my_notifications_toggle_visibility(self): + user = self._rhodecode_db_user + new_status = not user.user_data.get('notification_status', True) + user.update_userdata(notification_status=new_status) + Session().commit() + return user.user_data['notification_status'] \ No newline at end of file diff --git a/rhodecode/config/routing.py b/rhodecode/config/routing.py index 41a4658c..3b33cd98 100644 --- a/rhodecode/config/routing.py +++ b/rhodecode/config/routing.py @@ -485,26 +485,9 @@ def make_map(config): m.connect('my_account_password', '/my_account/password', action='my_account_password', conditions={'method': ['GET']}) - m.connect('my_account_repos', '/my_account/repos', - action='my_account_repos', conditions={'method': ['GET']}) - - m.connect('my_account_watched', '/my_account/watched', - action='my_account_watched', conditions={'method': ['GET']}) - m.connect('my_account_pullrequests', '/my_account/pull_requests', action='my_account_pullrequests', conditions={'method': ['GET']}) - m.connect('my_account_perms', '/my_account/perms', - action='my_account_perms', conditions={'method': ['GET']}) - - m.connect('my_account_notifications', '/my_account/notifications', - action='my_notifications', - conditions={'method': ['GET']}) - m.connect('my_account_notifications_toggle_visibility', - '/my_account/toggle_visibility', - action='my_notifications_toggle_visibility', - conditions={'method': ['POST']}) - # NOTIFICATION REST ROUTES with rmap.submapper(path_prefix=ADMIN_PREFIX, controller='admin/notifications') as m: diff --git a/rhodecode/controllers/admin/my_account.py b/rhodecode/controllers/admin/my_account.py index e6a1e50b..16dfcf4e 100644 --- a/rhodecode/controllers/admin/my_account.py +++ b/rhodecode/controllers/admin/my_account.py @@ -29,26 +29,22 @@ import formencode from formencode import htmlfill from pyramid.httpexceptions import HTTPFound -from pylons import request, tmpl_context as c, url +from pylons import request, tmpl_context as c from pylons.controllers.util import redirect from pylons.i18n.translation import _ -from sqlalchemy.orm import joinedload from rhodecode.lib import helpers as h from rhodecode.lib import auth from rhodecode.lib.auth import ( LoginRequired, NotAnonymous, AuthUser) from rhodecode.lib.base import BaseController, render -from rhodecode.lib.utils import jsonify from rhodecode.lib.utils2 import safe_int, str2bool from rhodecode.lib.ext_json import json from rhodecode.model.db import ( Repository, PullRequest, UserEmailMap, User, UserFollowing) from rhodecode.model.forms import UserForm -from rhodecode.model.scm import RepoList from rhodecode.model.user import UserModel -from rhodecode.model.repo import RepoModel from rhodecode.model.meta import Session from rhodecode.model.pull_request import PullRequestModel from rhodecode.model.comment import CommentsModel @@ -78,26 +74,6 @@ class MyAccountController(BaseController): c.auth_user = AuthUser( user_id=c.rhodecode_user.user_id, ip_addr=self.ip_addr) - def _load_my_repos_data(self, watched=False): - if watched: - admin = False - follows_repos = Session().query(UserFollowing)\ - .filter(UserFollowing.user_id == c.rhodecode_user.user_id)\ - .options(joinedload(UserFollowing.follows_repository))\ - .all() - repo_list = [x.follows_repository for x in follows_repos] - else: - admin = True - repo_list = Repository.get_all_repos( - user_id=c.rhodecode_user.user_id) - repo_list = RepoList(repo_list, perm_set=[ - 'repository.read', 'repository.write', 'repository.admin']) - - repos_data = RepoModel().get_repos_as_dict( - repo_list=repo_list, admin=admin) - # json used to render the grid - return json.dumps(repos_data) - @auth.CSRFRequired() def my_account_update(self): """ @@ -177,29 +153,6 @@ class MyAccountController(BaseController): force_defaults=False ) - def my_account_repos(self): - c.active = 'repos' - self.__load_data() - - # json used to render the grid - c.data = self._load_my_repos_data() - return render('admin/my_account/my_account.mako') - - def my_account_watched(self): - c.active = 'watched' - self.__load_data() - - # json used to render the grid - c.data = self._load_my_repos_data(watched=True) - return render('admin/my_account/my_account.mako') - - def my_account_perms(self): - c.active = 'perms' - self.__load_data() - c.perm_user = c.auth_user - - return render('admin/my_account/my_account.mako') - def _extract_ordering(self, request): column_index = safe_int(request.GET.get('order[0][column]')) order_dir = request.GET.get('order[0][dir]', 'desc') @@ -280,15 +233,4 @@ class MyAccountController(BaseController): else: return json.dumps(data) - def my_notifications(self): - c.active = 'notifications' - return render('admin/my_account/my_account.mako') - @auth.CSRFRequired() - @jsonify - def my_notifications_toggle_visibility(self): - user = c.rhodecode_user.get_instance() - new_status = not user.user_data.get('notification_status', True) - user.update_userdata(notification_status=new_status) - Session().commit() - return user.user_data['notification_status'] diff --git a/rhodecode/public/js/rhodecode/routes.js b/rhodecode/public/js/rhodecode/routes.js index 4d237b25..18ddcba4 100644 --- a/rhodecode/public/js/rhodecode/routes.js +++ b/rhodecode/public/js/rhodecode/routes.js @@ -134,6 +134,14 @@ function registerRCRoutes() { pyroutes.register('my_account_auth_tokens', '/_admin/my_account/auth_tokens', []); pyroutes.register('my_account_auth_tokens_add', '/_admin/my_account/auth_tokens/new', []); pyroutes.register('my_account_auth_tokens_delete', '/_admin/my_account/auth_tokens/delete', []); + pyroutes.register('my_account_emails', '/_admin/my_account/emails', []); + pyroutes.register('my_account_emails_add', '/_admin/my_account/emails/new', []); + pyroutes.register('my_account_emails_delete', '/_admin/my_account/emails/delete', []); + pyroutes.register('my_account_repos', '/_admin/my_account/repos', []); + pyroutes.register('my_account_watched', '/_admin/my_account/watched', []); + pyroutes.register('my_account_perms', '/_admin/my_account/perms', []); + pyroutes.register('my_account_notifications', '/_admin/my_account/notifications', []); + pyroutes.register('my_account_notifications_toggle_visibility', '/_admin/my_account/toggle_visibility', []); pyroutes.register('my_account_notifications_test_channelstream', '/_admin/my_account/test_channelstream', []); pyroutes.register('apiv2', '/_admin/api', []); } diff --git a/rhodecode/templates/admin/my_account/my_account.mako b/rhodecode/templates/admin/my_account/my_account.mako index f0a268ec..ff8896af 100644 --- a/rhodecode/templates/admin/my_account/my_account.mako +++ b/rhodecode/templates/admin/my_account/my_account.mako @@ -35,11 +35,11 @@
  • ${_('OAuth Identities')}
  • % endif
  • ${_('Emails')}
  • -
  • ${_('Repositories')}
  • -
  • ${_('Watched')}
  • +
  • ${_('Repositories')}
  • +
  • ${_('Watched')}
  • ${_('Pull Requests')}
  • -
  • ${_('Permissions')}
  • -
  • ${_('Live Notifications')}
  • +
  • ${_('Permissions')}
  • +
  • ${_('Live Notifications')}
  • diff --git a/rhodecode/templates/admin/my_account/my_account_notifications.mako b/rhodecode/templates/admin/my_account/my_account_notifications.mako index e71e465f..1c53e966 100644 --- a/rhodecode/templates/admin/my_account/my_account_notifications.mako +++ b/rhodecode/templates/admin/my_account/my_account_notifications.mako @@ -1,7 +1,7 @@