+ }
+ error : null
+ """
+
+ if not has_superadmin_permission(apiuser):
+ raise JSONRPCForbidden()
+
+ path = VcsSettingsModel().get_repos_location()
+ return {"path": path}
+
+
+@jsonrpc_method()
def get_ip(request, apiuser, userid=Optional(OAttr('apiuser'))):
"""
Displays the IP Address as seen from the |RCE| server.
diff --git a/rhodecode/api/views/user_group_api.py b/rhodecode/api/views/user_group_api.py
--- a/rhodecode/api/views/user_group_api.py
+++ b/rhodecode/api/views/user_group_api.py
@@ -464,6 +464,7 @@ def add_user_to_user_group(request, apiu
raise JSONRPCError('user group `%s` does not exist' % (
usergroupid,))
+ old_values = user_group.get_api_data()
try:
ugm = UserGroupModel().add_user_to_group(user_group, user)
success = True if ugm is not True else False
@@ -474,7 +475,8 @@ def add_user_to_user_group(request, apiu
if success:
user_data = user.get_api_data()
audit_logger.store_api(
- 'user_group.edit.member.add', action_data={'user': user_data},
+ 'user_group.edit.member.add',
+ action_data={'user': user_data, 'old_data': old_values},
user=apiuser)
Session().commit()
@@ -534,6 +536,7 @@ def remove_user_from_user_group(request,
raise JSONRPCError(
'user group `%s` does not exist' % (usergroupid,))
+ old_values = user_group.get_api_data()
try:
success = UserGroupModel().remove_user_from_group(user_group, user)
msg = 'removed member `%s` from user group `%s`' % (
@@ -543,7 +546,8 @@ def remove_user_from_user_group(request,
if success:
user_data = user.get_api_data()
audit_logger.store_api(
- 'user_group.edit.member.delete', action_data={'user': user_data},
+ 'user_group.edit.member.delete',
+ action_data={'user': user_data, 'old_data': old_values},
user=apiuser)
Session().commit()
diff --git a/rhodecode/apps/_base/__init__.py b/rhodecode/apps/_base/__init__.py
--- a/rhodecode/apps/_base/__init__.py
+++ b/rhodecode/apps/_base/__init__.py
@@ -20,16 +20,17 @@
import time
import logging
-from pylons import tmpl_context as c
+import operator
+
from pyramid.httpexceptions import HTTPFound
from rhodecode.lib import helpers as h
-from rhodecode.lib.utils import PartialRenderer
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 import user_group
+from rhodecode.model import user
from rhodecode.model.db import User
from rhodecode.model.scm import ScmModel
@@ -39,6 +40,19 @@ log = logging.getLogger(__name__)
ADMIN_PREFIX = '/_admin'
STATIC_FILE_PREFIX = '/_static'
+URL_NAME_REQUIREMENTS = {
+ # group name can have a slash in them, but they must not end with a slash
+ 'group_name': r'.*?[^/]',
+ 'repo_group_name': r'.*?[^/]',
+ # repo names can have a slash in them, but they must not end with a slash
+ 'repo_name': r'.*?[^/]',
+ # file path eats up everything at the end
+ 'f_path': r'.*',
+ # reference types
+ 'source_ref_type': '(branch|book|tag|rev|\%\(source_ref_type\)s)',
+ 'target_ref_type': '(branch|book|tag|rev|\%\(target_ref_type\)s)',
+}
+
def add_route_with_slash(config,name, pattern, **kw):
config.add_route(name, pattern, **kw)
@@ -46,6 +60,17 @@ def add_route_with_slash(config,name, pa
config.add_route(name + '_slash', pattern + '/', **kw)
+def add_route_requirements(route_path, requirements=URL_NAME_REQUIREMENTS):
+ """
+ Adds regex requirements to pyramid routes using a mapping dict
+ e.g::
+ add_route_requirements('{repo_name}/settings')
+ """
+ for key, regex in requirements.items():
+ route_path = route_path.replace('{%s}' % key, '{%s:%s}' % (key, regex))
+ return route_path
+
+
def get_format_ref_id(repo):
"""Returns a `repo` specific reference formatter function"""
if h.is_svn(repo):
@@ -105,23 +130,50 @@ class BaseAppView(object):
raise HTTPFound(
self.request.route_path('my_account_password'))
+ def _log_creation_exception(self, e, repo_name):
+ _ = self.request.translate
+ reason = None
+ if len(e.args) == 2:
+ reason = e.args[1]
+
+ if reason == 'INVALID_CERTIFICATE':
+ log.exception(
+ 'Exception creating a repository: invalid certificate')
+ msg = (_('Error creating repository %s: invalid certificate')
+ % repo_name)
+ else:
+ log.exception("Exception creating a repository")
+ msg = (_('Error creating repository %s')
+ % repo_name)
+ return msg
+
def _get_local_tmpl_context(self, include_app_defaults=False):
c = TemplateArgs()
c.auth_user = self.request.user
+ # TODO(marcink): migrate the usage of c.rhodecode_user to c.auth_user
+ c.rhodecode_user = self.request.user
+
if include_app_defaults:
# NOTE(marcink): after full pyramid migration include_app_defaults
# should be turned on by default
from rhodecode.lib.base import attach_context_attributes
attach_context_attributes(c, self.request, self.request.user.user_id)
+
return c
def _register_global_c(self, tmpl_args):
"""
Registers attributes to pylons global `c`
"""
+
# TODO(marcink): remove once pyramid migration is finished
- for k, v in tmpl_args.items():
- setattr(c, k, v)
+ from pylons import tmpl_context as c
+ try:
+ for k, v in tmpl_args.items():
+ setattr(c, k, v)
+ except TypeError:
+ log.exception('Failed to register pylons C')
+ pass
def _get_template_context(self, tmpl_args):
self._register_global_c(tmpl_args)
@@ -129,6 +181,10 @@ class BaseAppView(object):
local_tmpl_args = {
'defaults': {},
'errors': {},
+ # register a fake 'c' to be used in templates instead of global
+ # pylons c, after migration to pyramid we should rename it to 'c'
+ # make sure we replace usage of _c in templates too
+ '_c': tmpl_args
}
local_tmpl_args.update(tmpl_args)
return local_tmpl_args
@@ -160,6 +216,7 @@ class RepoAppView(BaseAppView):
self.db_repo_name, error.message)
def _get_local_tmpl_context(self, include_app_defaults=False):
+ _ = self.request.translate
c = super(RepoAppView, self)._get_local_tmpl_context(
include_app_defaults=include_app_defaults)
@@ -174,9 +231,70 @@ class RepoAppView(BaseAppView):
except RepositoryRequirementError as e:
c.repository_requirements_missing = True
self._handle_missing_requirements(e)
+ self.rhodecode_vcs_repo = None
+
+ if (not c.repository_requirements_missing
+ and self.rhodecode_vcs_repo is None):
+ # unable to fetch this repo as vcs instance, report back to user
+ h.flash(_(
+ "The repository `%(repo_name)s` cannot be loaded in filesystem. "
+ "Please check if it exist, or is not damaged.") %
+ {'repo_name': c.repo_name},
+ category='error', ignore_duplicate=True)
+ raise HTTPFound(h.route_path('home'))
return c
+ def _get_f_path(self, matchdict, default=None):
+ f_path = matchdict.get('f_path')
+ if f_path:
+ # fix for multiple initial slashes that causes errors for GIT
+ return f_path.lstrip('/')
+
+ return default
+
+
+class RepoGroupAppView(BaseAppView):
+ def __init__(self, context, request):
+ super(RepoGroupAppView, self).__init__(context, request)
+ self.db_repo_group = request.db_repo_group
+ self.db_repo_group_name = self.db_repo_group.group_name
+
+ def _revoke_perms_on_yourself(self, form_result):
+ _updates = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
+ form_result['perm_updates'])
+ _additions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
+ form_result['perm_additions'])
+ _deletions = filter(lambda u: self._rhodecode_user.user_id == int(u[0]),
+ form_result['perm_deletions'])
+ admin_perm = 'group.admin'
+ if _updates and _updates[0][1] != admin_perm or \
+ _additions and _additions[0][1] != admin_perm or \
+ _deletions and _deletions[0][1] != admin_perm:
+ return True
+ return False
+
+
+class UserGroupAppView(BaseAppView):
+ def __init__(self, context, request):
+ super(UserGroupAppView, self).__init__(context, request)
+ self.db_user_group = request.db_user_group
+ self.db_user_group_name = self.db_user_group.users_group_name
+
+
+class UserAppView(BaseAppView):
+ def __init__(self, context, request):
+ super(UserAppView, self).__init__(context, request)
+ self.db_user = request.db_user
+ self.db_user_id = self.db_user.user_id
+
+ _ = self.request.translate
+ if not request.db_user_supports_default:
+ if self.db_user.username == User.DEFAULT_USER:
+ h.flash(_("Editing user `{}` is disabled.".format(
+ User.DEFAULT_USER)), category='warning')
+ raise HTTPFound(h.route_path('users'))
+
class DataGridAppView(object):
"""
@@ -203,6 +321,15 @@ class DataGridAppView(object):
draw = safe_int(request.GET.get('draw'))
return draw, start, length
+ def _get_order_col(self, order_by, model):
+ if isinstance(order_by, basestring):
+ try:
+ return operator.attrgetter(order_by)(model)
+ except AttributeError:
+ return None
+ else:
+ return order_by
+
class BaseReferencesView(RepoAppView):
"""
@@ -211,35 +338,48 @@ class BaseReferencesView(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
def load_refs_context(self, ref_items, partials_template):
- _render = PartialRenderer(partials_template)
- _data = []
+ _render = self.request.get_partial_renderer(partials_template)
pre_load = ["author", "date", "message"]
is_svn = h.is_svn(self.rhodecode_vcs_repo)
+ is_hg = h.is_hg(self.rhodecode_vcs_repo)
+
format_ref_id = get_format_ref_id(self.rhodecode_vcs_repo)
+ closed_refs = {}
+ if is_hg:
+ closed_refs = self.rhodecode_vcs_repo.branches_closed
+
+ data = []
for ref_name, commit_id in ref_items:
commit = self.rhodecode_vcs_repo.get_commit(
commit_id=commit_id, pre_load=pre_load)
+ closed = ref_name in closed_refs
# TODO: johbo: Unify generation of reference links
use_commit_id = '/' in ref_name or is_svn
- files_url = h.url(
- 'files_home',
- repo_name=c.repo_name,
- f_path=ref_name if is_svn else '',
- revision=commit_id if use_commit_id else ref_name,
- at=ref_name)
+
+ if use_commit_id:
+ files_url = h.route_path(
+ 'repo_files',
+ repo_name=self.db_repo_name,
+ f_path=ref_name if is_svn else '',
+ commit_id=commit_id)
- _data.append({
- "name": _render('name', ref_name, files_url),
+ else:
+ files_url = h.route_path(
+ 'repo_files',
+ repo_name=self.db_repo_name,
+ f_path=ref_name if is_svn else '',
+ commit_id=ref_name,
+ _query=dict(at=ref_name))
+
+ data.append({
+ "name": _render('name', ref_name, files_url, closed),
"name_raw": ref_name,
"date": _render('date', commit.date),
"date_raw": datetime_to_time(commit.date),
@@ -250,8 +390,8 @@ class BaseReferencesView(RepoAppView):
"compare": _render(
'compare', format_ref_id(ref_name, commit.raw_id)),
})
- c.has_references = bool(_data)
- c.data = json.dumps(_data)
+
+ return data
class RepoRoutePredicate(object):
@@ -273,14 +413,22 @@ class RepoRoutePredicate(object):
repo_model = repo.RepoModel()
by_name_match = repo_model.get_by_repo_name(repo_name, cache=True)
+ def redirect_if_creating(db_repo):
+ if db_repo.repo_state in [repo.Repository.STATE_PENDING]:
+ raise HTTPFound(
+ request.route_path('repo_creating',
+ repo_name=db_repo.repo_name))
+
if by_name_match:
# register this as request object we can re-use later
request.db_repo = by_name_match
+ redirect_if_creating(by_name_match)
return True
by_id_match = repo_model.get_repo_by_id(repo_name)
if by_id_match:
request.db_repo = by_id_match
+ redirect_if_creating(by_id_match)
return True
return False
@@ -348,6 +496,79 @@ class RepoGroupRoutePredicate(object):
return False
+class UserGroupRoutePredicate(object):
+ def __init__(self, val, config):
+ self.val = val
+
+ def text(self):
+ return 'user_group_route = %s' % self.val
+
+ phash = text
+
+ def __call__(self, info, request):
+ if hasattr(request, 'vcs_call'):
+ # skip vcs calls
+ return
+
+ user_group_id = info['match']['user_group_id']
+ user_group_model = user_group.UserGroup()
+ by_id_match = user_group_model.get(
+ user_group_id, cache=True)
+
+ if by_id_match:
+ # register this as request object we can re-use later
+ request.db_user_group = by_id_match
+ return True
+
+ return False
+
+
+class UserRoutePredicateBase(object):
+ supports_default = None
+
+ def __init__(self, val, config):
+ self.val = val
+
+ def text(self):
+ raise NotImplementedError()
+
+ def __call__(self, info, request):
+ if hasattr(request, 'vcs_call'):
+ # skip vcs calls
+ return
+
+ user_id = info['match']['user_id']
+ user_model = user.User()
+ by_id_match = user_model.get(
+ user_id, cache=True)
+
+ if by_id_match:
+ # register this as request object we can re-use later
+ request.db_user = by_id_match
+ request.db_user_supports_default = self.supports_default
+ return True
+
+ return False
+
+
+class UserRoutePredicate(UserRoutePredicateBase):
+ supports_default = False
+
+ def text(self):
+ return 'user_route = %s' % self.val
+
+ phash = text
+
+
+class UserRouteWithDefaultPredicate(UserRoutePredicateBase):
+ supports_default = True
+
+ def text(self):
+ return 'user_with_default_route = %s' % self.val
+
+ phash = text
+
+
def includeme(config):
config.add_route_predicate(
'repo_route', RepoRoutePredicate)
@@ -355,3 +576,9 @@ def includeme(config):
'repo_accepted_types', RepoTypeRoutePredicate)
config.add_route_predicate(
'repo_group_route', RepoGroupRoutePredicate)
+ config.add_route_predicate(
+ 'user_group_route', UserGroupRoutePredicate)
+ config.add_route_predicate(
+ 'user_route_with_default', UserRouteWithDefaultPredicate)
+ config.add_route_predicate(
+ 'user_route', UserRoutePredicate)
\ No newline at end of file
diff --git a/rhodecode/apps/admin/__init__.py b/rhodecode/apps/admin/__init__.py
--- a/rhodecode/apps/admin/__init__.py
+++ b/rhodecode/apps/admin/__init__.py
@@ -34,14 +34,18 @@ def admin_routes(config):
pattern='/audit_logs')
config.add_route(
+ name='admin_audit_log_entry',
+ pattern='/audit_logs/{audit_log_id}')
+
+ config.add_route(
name='pull_requests_global_0', # backward compat
- pattern='/pull_requests/{pull_request_id:[0-9]+}')
+ pattern='/pull_requests/{pull_request_id:\d+}')
config.add_route(
name='pull_requests_global_1', # backward compat
- pattern='/pull-requests/{pull_request_id:[0-9]+}')
+ pattern='/pull-requests/{pull_request_id:\d+}')
config.add_route(
name='pull_requests_global',
- pattern='/pull-request/{pull_request_id:[0-9]+}')
+ pattern='/pull-request/{pull_request_id:\d+}')
config.add_route(
name='admin_settings_open_source',
@@ -64,11 +68,66 @@ def admin_routes(config):
name='admin_settings_sessions_cleanup',
pattern='/settings/sessions/cleanup')
+ config.add_route(
+ name='admin_settings_process_management',
+ pattern='/settings/process_management')
+ config.add_route(
+ name='admin_settings_process_management_signal',
+ pattern='/settings/process_management/signal')
+
+ # default settings
+ config.add_route(
+ name='admin_defaults_repositories',
+ pattern='/defaults/repositories')
+ config.add_route(
+ name='admin_defaults_repositories_update',
+ pattern='/defaults/repositories/update')
+
# global permissions
+
+ config.add_route(
+ name='admin_permissions_application',
+ pattern='/permissions/application')
+ config.add_route(
+ name='admin_permissions_application_update',
+ pattern='/permissions/application/update')
+
+ config.add_route(
+ name='admin_permissions_global',
+ pattern='/permissions/global')
+ config.add_route(
+ name='admin_permissions_global_update',
+ pattern='/permissions/global/update')
+
+ config.add_route(
+ name='admin_permissions_object',
+ pattern='/permissions/object')
+ config.add_route(
+ name='admin_permissions_object_update',
+ pattern='/permissions/object/update')
+
config.add_route(
name='admin_permissions_ips',
pattern='/permissions/ips')
+ config.add_route(
+ name='admin_permissions_overview',
+ pattern='/permissions/overview')
+
+ config.add_route(
+ name='admin_permissions_auth_token_access',
+ pattern='/permissions/auth_token_access')
+
+ config.add_route(
+ name='admin_permissions_ssh_keys',
+ pattern='/permissions/ssh_keys')
+ config.add_route(
+ name='admin_permissions_ssh_keys_data',
+ pattern='/permissions/ssh_keys/data')
+ config.add_route(
+ name='admin_permissions_ssh_keys_update',
+ pattern='/permissions/ssh_keys/update')
+
# users admin
config.add_route(
name='users',
@@ -78,52 +137,176 @@ def admin_routes(config):
name='users_data',
pattern='/users_data')
+ config.add_route(
+ name='users_create',
+ pattern='/users/create')
+
+ config.add_route(
+ name='users_new',
+ pattern='/users/new')
+
+ # user management
+ config.add_route(
+ name='user_edit',
+ pattern='/users/{user_id:\d+}/edit',
+ user_route=True)
+ config.add_route(
+ name='user_edit_advanced',
+ pattern='/users/{user_id:\d+}/edit/advanced',
+ user_route=True)
+ config.add_route(
+ name='user_edit_global_perms',
+ pattern='/users/{user_id:\d+}/edit/global_permissions',
+ user_route=True)
+ config.add_route(
+ name='user_edit_global_perms_update',
+ pattern='/users/{user_id:\d+}/edit/global_permissions/update',
+ user_route=True)
+ config.add_route(
+ name='user_update',
+ pattern='/users/{user_id:\d+}/update',
+ user_route=True)
+ config.add_route(
+ name='user_delete',
+ pattern='/users/{user_id:\d+}/delete',
+ user_route=True)
+ config.add_route(
+ name='user_force_password_reset',
+ pattern='/users/{user_id:\d+}/password_reset',
+ user_route=True)
+ config.add_route(
+ name='user_create_personal_repo_group',
+ pattern='/users/{user_id:\d+}/create_repo_group',
+ user_route=True)
+
# user auth tokens
config.add_route(
name='edit_user_auth_tokens',
- pattern='/users/{user_id:\d+}/edit/auth_tokens')
+ pattern='/users/{user_id:\d+}/edit/auth_tokens',
+ user_route=True)
config.add_route(
name='edit_user_auth_tokens_add',
- pattern='/users/{user_id:\d+}/edit/auth_tokens/new')
+ pattern='/users/{user_id:\d+}/edit/auth_tokens/new',
+ user_route=True)
config.add_route(
name='edit_user_auth_tokens_delete',
- pattern='/users/{user_id:\d+}/edit/auth_tokens/delete')
+ pattern='/users/{user_id:\d+}/edit/auth_tokens/delete',
+ user_route=True)
+
+ # user ssh keys
+ config.add_route(
+ name='edit_user_ssh_keys',
+ pattern='/users/{user_id:\d+}/edit/ssh_keys',
+ user_route=True)
+ config.add_route(
+ name='edit_user_ssh_keys_generate_keypair',
+ pattern='/users/{user_id:\d+}/edit/ssh_keys/generate',
+ user_route=True)
+ config.add_route(
+ name='edit_user_ssh_keys_add',
+ pattern='/users/{user_id:\d+}/edit/ssh_keys/new',
+ user_route=True)
+ config.add_route(
+ name='edit_user_ssh_keys_delete',
+ pattern='/users/{user_id:\d+}/edit/ssh_keys/delete',
+ user_route=True)
# user emails
config.add_route(
name='edit_user_emails',
- pattern='/users/{user_id:\d+}/edit/emails')
+ pattern='/users/{user_id:\d+}/edit/emails',
+ user_route=True)
config.add_route(
name='edit_user_emails_add',
- pattern='/users/{user_id:\d+}/edit/emails/new')
+ pattern='/users/{user_id:\d+}/edit/emails/new',
+ user_route=True)
config.add_route(
name='edit_user_emails_delete',
- pattern='/users/{user_id:\d+}/edit/emails/delete')
+ pattern='/users/{user_id:\d+}/edit/emails/delete',
+ user_route=True)
# user IPs
config.add_route(
name='edit_user_ips',
- pattern='/users/{user_id:\d+}/edit/ips')
+ pattern='/users/{user_id:\d+}/edit/ips',
+ user_route=True)
config.add_route(
name='edit_user_ips_add',
- pattern='/users/{user_id:\d+}/edit/ips/new')
+ pattern='/users/{user_id:\d+}/edit/ips/new',
+ user_route_with_default=True) # enabled for default user too
config.add_route(
name='edit_user_ips_delete',
- pattern='/users/{user_id:\d+}/edit/ips/delete')
+ pattern='/users/{user_id:\d+}/edit/ips/delete',
+ user_route_with_default=True) # enabled for default user too
- # user groups management
+ # user perms
+ config.add_route(
+ name='edit_user_perms_summary',
+ pattern='/users/{user_id:\d+}/edit/permissions_summary',
+ user_route=True)
+ config.add_route(
+ name='edit_user_perms_summary_json',
+ pattern='/users/{user_id:\d+}/edit/permissions_summary/json',
+ user_route=True)
+
+ # user user groups management
config.add_route(
name='edit_user_groups_management',
- pattern='/users/{user_id:\d+}/edit/groups_management')
+ pattern='/users/{user_id:\d+}/edit/groups_management',
+ user_route=True)
config.add_route(
name='edit_user_groups_management_updates',
- pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates')
+ pattern='/users/{user_id:\d+}/edit/edit_user_groups_management/updates',
+ user_route=True)
# user audit logs
config.add_route(
name='edit_user_audit_logs',
- pattern='/users/{user_id:\d+}/edit/audit')
+ pattern='/users/{user_id:\d+}/edit/audit', user_route=True)
+
+ # user-groups admin
+ config.add_route(
+ name='user_groups',
+ pattern='/user_groups')
+
+ config.add_route(
+ name='user_groups_data',
+ pattern='/user_groups_data')
+
+ config.add_route(
+ name='user_groups_new',
+ pattern='/user_groups/new')
+
+ config.add_route(
+ name='user_groups_create',
+ pattern='/user_groups/create')
+
+ # repos admin
+ config.add_route(
+ name='repos',
+ pattern='/repos')
+
+ config.add_route(
+ name='repo_new',
+ pattern='/repos/new')
+
+ config.add_route(
+ name='repo_create',
+ pattern='/repos/create')
+
+ # repo groups admin
+ config.add_route(
+ name='repo_groups',
+ pattern='/repo_groups')
+
+ config.add_route(
+ name='repo_group_new',
+ pattern='/repo_group/new')
+
+ config.add_route(
+ name='repo_group_create',
+ pattern='/repo_group/create')
def includeme(config):
@@ -139,4 +322,4 @@ def includeme(config):
config.include(admin_routes, route_prefix=ADMIN_PREFIX)
# Scan module for configuration decorators.
- config.scan()
+ config.scan('.views', ignore='.tests')
diff --git a/rhodecode/apps/admin/navigation.py b/rhodecode/apps/admin/navigation.py
--- a/rhodecode/apps/admin/navigation.py
+++ b/rhodecode/apps/admin/navigation.py
@@ -22,7 +22,6 @@
import logging
import collections
-from pylons import url
from zope.interface import implementer
from rhodecode.apps.admin.interfaces import IAdminNavigationRegistry
@@ -64,6 +63,7 @@ class NavEntry(object):
pyramid_request = get_current_request()
return pyramid_request.route_path(self.view_name)
else:
+ from pylons import url
return url(self.view_name)
def get_localized_name(self, request):
@@ -94,6 +94,8 @@ class NavigationRegistry(object):
'global_integrations_home', pyramid=True),
NavEntry('system', _('System Info'),
'admin_settings_system', pyramid=True),
+ NavEntry('process_management', _('Processes'),
+ 'admin_settings_process_management', pyramid=True),
NavEntry('sessions', _('User Sessions'),
'admin_settings_sessions', pyramid=True),
NavEntry('open_source', _('Open Source Licenses'),
diff --git a/rhodecode/tests/functional/test_admin_defaults.py b/rhodecode/apps/admin/tests/test_admin_defaults.py
rename from rhodecode/tests/functional/test_admin_defaults.py
rename to rhodecode/apps/admin/tests/test_admin_defaults.py
--- a/rhodecode/tests/functional/test_admin_defaults.py
+++ b/rhodecode/apps/admin/tests/test_admin_defaults.py
@@ -20,15 +20,31 @@
import pytest
-from rhodecode.tests import assert_session_flash, url
+from rhodecode.tests import assert_session_flash
from rhodecode.model.settings import SettingsModel
+def route_path(name, params=None, **kwargs):
+ import urllib
+ from rhodecode.apps._base import ADMIN_PREFIX
+
+ base_url = {
+ 'admin_defaults_repositories':
+ ADMIN_PREFIX + '/defaults/repositories',
+ 'admin_defaults_repositories_update':
+ ADMIN_PREFIX + '/defaults/repositories/update',
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
+
+
@pytest.mark.usefixtures("app")
-class TestDefaultsController:
+class TestDefaultsView(object):
def test_index(self, autologin_user):
- response = self.app.get(url('admin_defaults_repositories'))
+ response = self.app.get(route_path('admin_defaults_repositories'))
response.mustcontain('default_repo_private')
response.mustcontain('default_repo_enable_statistics')
response.mustcontain('default_repo_enable_downloads')
@@ -44,7 +60,7 @@ class TestDefaultsController:
'csrf_token': csrf_token,
}
response = self.app.post(
- url('admin_defaults_repositories'), params=params)
+ route_path('admin_defaults_repositories_update'), params=params)
assert_session_flash(response, 'Default settings updated successfully')
defs = SettingsModel().get_default_repo_settings()
@@ -61,8 +77,9 @@ class TestDefaultsController:
'csrf_token': csrf_token,
}
response = self.app.post(
- url('admin_defaults_repositories'), params=params)
+ route_path('admin_defaults_repositories_update'), params=params)
assert_session_flash(response, 'Default settings updated successfully')
+
defs = SettingsModel().get_default_repo_settings()
del params['csrf_token']
assert params == defs
diff --git a/rhodecode/tests/functional/test_admin_permissions.py b/rhodecode/apps/admin/tests/test_admin_permissions.py
rename from rhodecode/tests/functional/test_admin_permissions.py
rename to rhodecode/apps/admin/tests/test_admin_permissions.py
--- a/rhodecode/tests/functional/test_admin_permissions.py
+++ b/rhodecode/apps/admin/tests/test_admin_permissions.py
@@ -18,11 +18,14 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
+import mock
import pytest
from rhodecode.model.db import User, UserIpMap
+from rhodecode.model.meta import Session
from rhodecode.model.permission import PermissionModel
+from rhodecode.model.ssh_key import SshKeyModel
from rhodecode.tests import (
- TestController, url, clear_all_caches, assert_session_flash)
+ TestController, clear_all_caches, assert_session_flash)
def route_path(name, params=None, **kwargs):
@@ -36,6 +39,34 @@ def route_path(name, params=None, **kwar
ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
'edit_user_ips_delete':
ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
+
+ 'admin_permissions_application':
+ ADMIN_PREFIX + '/permissions/application',
+ 'admin_permissions_application_update':
+ ADMIN_PREFIX + '/permissions/application/update',
+
+ 'admin_permissions_global':
+ ADMIN_PREFIX + '/permissions/global',
+ 'admin_permissions_global_update':
+ ADMIN_PREFIX + '/permissions/global/update',
+
+ 'admin_permissions_object':
+ ADMIN_PREFIX + '/permissions/object',
+ 'admin_permissions_object_update':
+ ADMIN_PREFIX + '/permissions/object/update',
+
+ 'admin_permissions_ips':
+ ADMIN_PREFIX + '/permissions/ips',
+ 'admin_permissions_overview':
+ ADMIN_PREFIX + '/permissions/overview',
+
+ 'admin_permissions_ssh_keys':
+ ADMIN_PREFIX + '/permissions/ssh_keys',
+ 'admin_permissions_ssh_keys_data':
+ ADMIN_PREFIX + '/permissions/ssh_keys/data',
+ 'admin_permissions_ssh_keys_update':
+ ADMIN_PREFIX + '/permissions/ssh_keys/update'
+
}[name].format(**kwargs)
if params:
@@ -55,7 +86,7 @@ class TestAdminPermissionsController(Tes
def test_index_application(self):
self.log_user()
- self.app.get(url('admin_permissions_application'))
+ self.app.get(route_path('admin_permissions_application'))
@pytest.mark.parametrize(
'anonymous, default_register, default_register_message, default_password_reset,'
@@ -87,7 +118,7 @@ class TestAdminPermissionsController(Tes
'default_password_reset': default_password_reset,
'default_extern_activate': default_extern_activate,
}
- response = self.app.post(url('admin_permissions_application'),
+ response = self.app.post(route_path('admin_permissions_application_update'),
params=params)
if expect_form_error:
assert response.status_int == 200
@@ -101,7 +132,7 @@ class TestAdminPermissionsController(Tes
def test_index_object(self):
self.log_user()
- self.app.get(url('admin_permissions_object'))
+ self.app.get(route_path('admin_permissions_object'))
@pytest.mark.parametrize(
'repo, repo_group, user_group, expect_error, expect_form_error', [
@@ -127,7 +158,7 @@ class TestAdminPermissionsController(Tes
'default_user_group_perm': user_group,
'overwrite_default_user_group': False,
}
- response = self.app.post(url('admin_permissions_object'),
+ response = self.app.post(route_path('admin_permissions_object_update'),
params=params)
if expect_form_error:
assert response.status_int == 200
@@ -141,7 +172,7 @@ class TestAdminPermissionsController(Tes
def test_index_global(self):
self.log_user()
- self.app.get(url('admin_permissions_global'))
+ self.app.get(route_path('admin_permissions_global'))
@pytest.mark.parametrize(
'repo_create, repo_create_write, user_group_create, repo_group_create,'
@@ -175,7 +206,7 @@ class TestAdminPermissionsController(Tes
'default_fork_create': fork_create,
'default_inherit_default_permissions': inherit_default_permissions
}
- response = self.app.post(url('admin_permissions_global'),
+ response = self.app.post(route_path('admin_permissions_global_update'),
params=params)
if expect_form_error:
assert response.status_int == 200
@@ -189,7 +220,7 @@ class TestAdminPermissionsController(Tes
def test_index_ips(self):
self.log_user()
- response = self.app.get(url('admin_permissions_ips'))
+ response = self.app.get(route_path('admin_permissions_ips'))
# TODO: Test response...
response.mustcontain('All IP addresses are allowed')
@@ -203,7 +234,7 @@ class TestAdminPermissionsController(Tes
route_path('edit_user_ips_add', user_id=default_user_id),
params={'new_ip': '127.0.0.0/24', 'csrf_token': self.csrf_token})
- response = self.app.get(url('admin_permissions_ips'))
+ response = self.app.get(route_path('admin_permissions_ips'))
response.mustcontain('127.0.0.0/24')
response.mustcontain('127.0.0.0 - 127.0.0.255')
@@ -219,11 +250,51 @@ class TestAdminPermissionsController(Tes
assert_session_flash(response, 'Removed ip address from user whitelist')
clear_all_caches()
- response = self.app.get(url('admin_permissions_ips'))
+ response = self.app.get(route_path('admin_permissions_ips'))
response.mustcontain('All IP addresses are allowed')
response.mustcontain(no=['127.0.0.0/24'])
response.mustcontain(no=['127.0.0.0 - 127.0.0.255'])
def test_index_overview(self):
self.log_user()
- self.app.get(url('admin_permissions_overview'))
+ self.app.get(route_path('admin_permissions_overview'))
+
+ def test_ssh_keys(self):
+ self.log_user()
+ self.app.get(route_path('admin_permissions_ssh_keys'), status=200)
+
+ def test_ssh_keys_data(self, user_util, xhr_header):
+ self.log_user()
+ response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
+ extra_environ=xhr_header)
+ assert response.json == {u'data': [], u'draw': None,
+ u'recordsFiltered': 0, u'recordsTotal': 0}
+
+ dummy_user = user_util.create_user()
+ SshKeyModel().create(dummy_user, 'ab:cd:ef', 'KEYKEY', 'test_key')
+ Session().commit()
+ response = self.app.get(route_path('admin_permissions_ssh_keys_data'),
+ extra_environ=xhr_header)
+ assert response.json['data'][0]['fingerprint'] == 'ab:cd:ef'
+
+ def test_ssh_keys_update(self):
+ self.log_user()
+ response = self.app.post(
+ route_path('admin_permissions_ssh_keys_update'),
+ dict(csrf_token=self.csrf_token), status=302)
+
+ assert_session_flash(
+ response, 'Updated SSH keys file')
+
+ def test_ssh_keys_update_disabled(self):
+ self.log_user()
+
+ from rhodecode.apps.admin.views.permissions import AdminPermissionsView
+ with mock.patch.object(AdminPermissionsView, 'ssh_enabled',
+ return_value=False):
+ response = self.app.post(
+ route_path('admin_permissions_ssh_keys_update'),
+ dict(csrf_token=self.csrf_token), status=302)
+
+ assert_session_flash(
+ response, 'SSH key support is disabled in .ini file')
\ No newline at end of file
diff --git a/rhodecode/tests/functional/test_admin_repos.py b/rhodecode/apps/admin/tests/test_admin_repos.py
rename from rhodecode/tests/functional/test_admin_repos.py
rename to rhodecode/apps/admin/tests/test_admin_repos.py
--- a/rhodecode/tests/functional/test_admin_repos.py
+++ b/rhodecode/apps/admin/tests/test_admin_repos.py
@@ -23,55 +23,83 @@ import urllib
import mock
import pytest
+from rhodecode.apps._base import ADMIN_PREFIX
from rhodecode.lib import auth
-from rhodecode.lib.utils2 import safe_str, str2bool
+from rhodecode.lib.utils2 import safe_str
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
-from rhodecode.model.settings import SettingsModel, VcsSettingsModel
from rhodecode.model.user import UserModel
from rhodecode.tests import (
- login_user_session, url, assert_session_flash, TEST_USER_ADMIN_LOGIN,
- TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS, HG_REPO, GIT_REPO,
- logout_user_session)
+ login_user_session, assert_session_flash, TEST_USER_ADMIN_LOGIN,
+ TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
from rhodecode.tests.fixture import Fixture, error_function
from rhodecode.tests.utils import AssertResponse, repo_on_filesystem
fixture = Fixture()
+def route_path(name, params=None, **kwargs):
+ import urllib
+
+ base_url = {
+ 'repos': ADMIN_PREFIX + '/repos',
+ 'repo_new': ADMIN_PREFIX + '/repos/new',
+ 'repo_create': ADMIN_PREFIX + '/repos/create',
+
+ 'repo_creating_check': '/{repo_name}/repo_creating_check',
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
+
+
+def _get_permission_for_user(user, repo):
+ perm = UserRepoToPerm.query()\
+ .filter(UserRepoToPerm.repository ==
+ Repository.get_by_repo_name(repo))\
+ .filter(UserRepoToPerm.user == User.get_by_username(user))\
+ .all()
+ return perm
+
+
@pytest.mark.usefixtures("app")
class TestAdminRepos(object):
- def test_index(self):
- self.app.get(url('repos'))
+ def test_repo_list(self, autologin_user, user_util):
+ repo = user_util.create_repo()
+ response = self.app.get(
+ route_path('repos'), status=200)
- def test_create_page_restricted(self, autologin_user, backend):
+ response.mustcontain(repo.repo_name)
+
+ def test_create_page_restricted_to_single_backend(self, autologin_user, backend):
with mock.patch('rhodecode.BACKENDS', {'git': 'git'}):
- response = self.app.get(url('new_repo'), status=200)
+ response = self.app.get(route_path('repo_new'), status=200)
assert_response = AssertResponse(response)
element = assert_response.get_element('#repo_type')
assert element.text_content() == '\ngit\n'
- def test_create_page_non_restricted(self, autologin_user, backend):
- response = self.app.get(url('new_repo'), status=200)
+ def test_create_page_non_restricted_backends(self, autologin_user, backend):
+ response = self.app.get(route_path('repo_new'), status=200)
assert_response = AssertResponse(response)
assert_response.element_contains('#repo_type', 'git')
assert_response.element_contains('#repo_type', 'svn')
assert_response.element_contains('#repo_type', 'hg')
- @pytest.mark.parametrize("suffix",
- [u'', u'xxa'], ids=['', 'non-ascii'])
+ @pytest.mark.parametrize(
+ "suffix", [u'', u'xxa'], ids=['', 'non-ascii'])
def test_create(self, autologin_user, backend, suffix, csrf_token):
repo_name_unicode = backend.new_repo_name(suffix=suffix)
repo_name = repo_name_unicode.encode('utf8')
description_unicode = u'description for newly created repo' + suffix
description = description_unicode.encode('utf8')
response = self.app.post(
- url('repos'),
+ route_path('repo_create'),
fixture._get_repo_create_params(
repo_private=False,
repo_name=repo_name,
@@ -83,12 +111,12 @@ class TestAdminRepos(object):
self.assert_repository_is_created_correctly(
repo_name, description, backend)
- def test_create_numeric(self, autologin_user, backend, csrf_token):
+ def test_create_numeric_name(self, autologin_user, backend, csrf_token):
numeric_repo = '1234'
repo_name = numeric_repo
description = 'description for newly created repo' + numeric_repo
self.app.post(
- url('repos'),
+ route_path('repo_create'),
fixture._get_repo_create_params(
repo_private=False,
repo_name=repo_name,
@@ -114,7 +142,7 @@ class TestAdminRepos(object):
[group_name, repo_name])
description = u'description for newly created repo'
self.app.post(
- url('repos'),
+ route_path('repo_create'),
fixture._get_repo_create_params(
repo_private=False,
repo_name=safe_str(repo_name),
@@ -137,7 +165,7 @@ class TestAdminRepos(object):
RepoGroupModel().delete(group_name)
Session().commit()
- def test_create_in_group_numeric(
+ def test_create_in_group_numeric_name(
self, autologin_user, backend, csrf_token):
# create GROUP
group_name = 'sometest_%s' % backend.alias
@@ -150,7 +178,7 @@ class TestAdminRepos(object):
repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
description = 'description for newly created repo'
self.app.post(
- url('repos'),
+ route_path('repo_create'),
fixture._get_repo_create_params(
repo_private=False,
repo_name=repo_name,
@@ -209,7 +237,7 @@ class TestAdminRepos(object):
repo_name = 'ingroup'
description = 'description for newly created repo'
response = self.app.post(
- url('repos'),
+ route_path('repo_create'),
fixture._get_repo_create_params(
repo_private=False,
repo_name=repo_name,
@@ -226,7 +254,7 @@ class TestAdminRepos(object):
[group_name_allowed, repo_name])
description = 'description for newly created repo'
response = self.app.post(
- url('repos'),
+ route_path('repo_create'),
fixture._get_repo_create_params(
repo_private=False,
repo_name=repo_name,
@@ -270,7 +298,7 @@ class TestAdminRepos(object):
repo_name_full = RepoGroup.url_sep().join([group_name, repo_name])
description = 'description for newly created repo'
self.app.post(
- url('repos'),
+ route_path('repo_create'),
fixture._get_repo_create_params(
repo_private=False,
repo_name=repo_name,
@@ -314,7 +342,7 @@ class TestAdminRepos(object):
repo_name = backend.new_repo_name()
response = self.app.post(
- url('repos'),
+ route_path('repo_create'),
fixture._get_repo_create_params(
repo_private=False,
repo_name=repo_name,
@@ -342,7 +370,7 @@ class TestAdminRepos(object):
repo_name = backend.new_repo_name()
description = 'description for newly created repo'
response = self.app.post(
- url('repos'),
+ route_path('repo_create'),
fixture._get_repo_create_params(
repo_private=False,
repo_name=repo_name,
@@ -358,7 +386,7 @@ class TestAdminRepos(object):
repo_name = backend.new_repo_name()
description = 'description for newly created repo'
response = self.app.post(
- url('repos'),
+ route_path('repo_create'),
fixture._get_repo_create_params(
repo_private=False,
repo_name=repo_name,
@@ -373,7 +401,7 @@ class TestAdminRepos(object):
repo_name = backend.new_repo_name() + ".git"
description = 'description for newly created repo'
response = self.app.post(
- url('repos'),
+ route_path('repo_create'),
fixture._get_repo_create_params(
repo_private=False,
repo_name=repo_name,
@@ -382,11 +410,8 @@ class TestAdminRepos(object):
csrf_token=csrf_token))
response.mustcontain('Repository name cannot end with .git')
- def test_show(self, autologin_user, backend):
- self.app.get(url('repo', repo_name=backend.repo_name))
-
def test_default_user_cannot_access_private_repo_in_a_group(
- self, autologin_user, user_util, backend, csrf_token):
+ self, autologin_user, user_util, backend):
group = user_util.create_repo_group()
@@ -422,7 +447,7 @@ class TestAdminRepos(object):
repo_name = backend.new_repo_name()
description = 'description for newly created repo'
response = self.app.post(
- url('repos'),
+ route_path('repo_create'),
fixture._get_repo_create_params(
repo_private=False,
repo_name=repo_name,
@@ -441,7 +466,7 @@ class TestAdminRepos(object):
description = 'description for newly created repo'
response = self.app.post(
- url('repos'),
+ route_path('repo_create'),
fixture._get_repo_create_params(
repo_private=False,
repo_name=repo_name,
@@ -461,7 +486,8 @@ class TestAdminRepos(object):
repo_name_utf8 = safe_str(repo_name)
# run the check page that triggers the flash message
- response = self.app.get(url('repo_check_home', repo_name=repo_name))
+ response = self.app.get(
+ route_path('repo_creating_check', repo_name=safe_str(repo_name)))
assert response.json == {u'result': True}
flash_msg = u'Created repository {} '.format(
@@ -475,643 +501,9 @@ class TestAdminRepos(object):
assert new_repo.description == description
# test if the repository is visible in the list ?
- response = self.app.get(h.route_path('repo_summary', repo_name=repo_name))
+ response = self.app.get(
+ h.route_path('repo_summary', repo_name=safe_str(repo_name)))
response.mustcontain(repo_name)
response.mustcontain(backend.alias)
assert repo_on_filesystem(repo_name)
-
-
-@pytest.mark.usefixtures("app")
-class TestVcsSettings(object):
- FORM_DATA = {
- 'inherit_global_settings': False,
- 'hooks_changegroup_repo_size': False,
- 'hooks_changegroup_push_logger': False,
- 'hooks_outgoing_pull_logger': False,
- 'extensions_largefiles': False,
- 'extensions_evolve': False,
- 'phases_publish': 'False',
- 'rhodecode_pr_merge_enabled': False,
- 'rhodecode_use_outdated_comments': False,
- 'new_svn_branch': '',
- 'new_svn_tag': ''
- }
-
- @pytest.mark.skip_backends('svn')
- def test_global_settings_initial_values(self, autologin_user, backend):
- repo_name = backend.repo_name
- response = self.app.get(url('repo_vcs_settings', repo_name=repo_name))
-
- expected_settings = (
- 'rhodecode_use_outdated_comments', 'rhodecode_pr_merge_enabled',
- 'hooks_changegroup_repo_size', 'hooks_changegroup_push_logger',
- 'hooks_outgoing_pull_logger'
- )
- for setting in expected_settings:
- self.assert_repo_value_equals_global_value(response, setting)
-
- def test_show_settings_requires_repo_admin_permission(
- self, backend, user_util, settings_util):
- repo = backend.create_repo()
- repo_name = repo.repo_name
- user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
- user_util.grant_user_permission_to_repo(repo, user, 'repository.admin')
- login_user_session(
- self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
- self.app.get(url('repo_vcs_settings', repo_name=repo_name), status=200)
-
- def test_inherit_global_settings_flag_is_true_by_default(
- self, autologin_user, backend):
- repo_name = backend.repo_name
- response = self.app.get(url('repo_vcs_settings', repo_name=repo_name))
-
- assert_response = AssertResponse(response)
- element = assert_response.get_element('#inherit_global_settings')
- assert element.checked
-
- @pytest.mark.parametrize('checked_value', [True, False])
- def test_inherit_global_settings_value(
- self, autologin_user, backend, checked_value, settings_util):
- repo = backend.create_repo()
- repo_name = repo.repo_name
- settings_util.create_repo_rhodecode_setting(
- repo, 'inherit_vcs_settings', checked_value, 'bool')
- response = self.app.get(url('repo_vcs_settings', repo_name=repo_name))
-
- assert_response = AssertResponse(response)
- element = assert_response.get_element('#inherit_global_settings')
- assert element.checked == checked_value
-
- @pytest.mark.skip_backends('svn')
- def test_hooks_settings_are_created(
- self, autologin_user, backend, csrf_token):
- repo_name = backend.repo_name
- data = self.FORM_DATA.copy()
- data['csrf_token'] = csrf_token
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data, status=302)
- settings = SettingsModel(repo=repo_name)
- try:
- for section, key in VcsSettingsModel.HOOKS_SETTINGS:
- ui = settings.get_ui_by_section_and_key(section, key)
- assert ui.ui_active is False
- finally:
- self._cleanup_repo_settings(settings)
-
- def test_hooks_settings_are_not_created_for_svn(
- self, autologin_user, backend_svn, csrf_token):
- repo_name = backend_svn.repo_name
- data = self.FORM_DATA.copy()
- data['csrf_token'] = csrf_token
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data, status=302)
- settings = SettingsModel(repo=repo_name)
- try:
- for section, key in VcsSettingsModel.HOOKS_SETTINGS:
- ui = settings.get_ui_by_section_and_key(section, key)
- assert ui is None
- finally:
- self._cleanup_repo_settings(settings)
-
- @pytest.mark.skip_backends('svn')
- def test_hooks_settings_are_updated(
- self, autologin_user, backend, csrf_token):
- repo_name = backend.repo_name
- settings = SettingsModel(repo=repo_name)
- for section, key in VcsSettingsModel.HOOKS_SETTINGS:
- settings.create_ui_section_value(section, '', key=key, active=True)
-
- data = self.FORM_DATA.copy()
- data['csrf_token'] = csrf_token
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data, status=302)
- try:
- for section, key in VcsSettingsModel.HOOKS_SETTINGS:
- ui = settings.get_ui_by_section_and_key(section, key)
- assert ui.ui_active is False
- finally:
- self._cleanup_repo_settings(settings)
-
- def test_hooks_settings_are_not_updated_for_svn(
- self, autologin_user, backend_svn, csrf_token):
- repo_name = backend_svn.repo_name
- settings = SettingsModel(repo=repo_name)
- for section, key in VcsSettingsModel.HOOKS_SETTINGS:
- settings.create_ui_section_value(section, '', key=key, active=True)
-
- data = self.FORM_DATA.copy()
- data['csrf_token'] = csrf_token
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data, status=302)
- try:
- for section, key in VcsSettingsModel.HOOKS_SETTINGS:
- ui = settings.get_ui_by_section_and_key(section, key)
- assert ui.ui_active is True
- finally:
- self._cleanup_repo_settings(settings)
-
- @pytest.mark.skip_backends('svn')
- def test_pr_settings_are_created(
- self, autologin_user, backend, csrf_token):
- repo_name = backend.repo_name
- data = self.FORM_DATA.copy()
- data['csrf_token'] = csrf_token
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data, status=302)
- settings = SettingsModel(repo=repo_name)
- try:
- for name in VcsSettingsModel.GENERAL_SETTINGS:
- setting = settings.get_setting_by_name(name)
- assert setting.app_settings_value is False
- finally:
- self._cleanup_repo_settings(settings)
-
- def test_pr_settings_are_not_created_for_svn(
- self, autologin_user, backend_svn, csrf_token):
- repo_name = backend_svn.repo_name
- data = self.FORM_DATA.copy()
- data['csrf_token'] = csrf_token
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data, status=302)
- settings = SettingsModel(repo=repo_name)
- try:
- for name in VcsSettingsModel.GENERAL_SETTINGS:
- setting = settings.get_setting_by_name(name)
- assert setting is None
- finally:
- self._cleanup_repo_settings(settings)
-
- def test_pr_settings_creation_requires_repo_admin_permission(
- self, backend, user_util, settings_util, csrf_token):
- repo = backend.create_repo()
- repo_name = repo.repo_name
-
- logout_user_session(self.app, csrf_token)
- session = login_user_session(
- self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
- new_csrf_token = auth.get_csrf_token(session)
-
- user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
- repo = Repository.get_by_repo_name(repo_name)
- user_util.grant_user_permission_to_repo(repo, user, 'repository.admin')
- data = self.FORM_DATA.copy()
- data['csrf_token'] = new_csrf_token
- settings = SettingsModel(repo=repo_name)
-
- try:
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data,
- status=302)
- finally:
- self._cleanup_repo_settings(settings)
-
- @pytest.mark.skip_backends('svn')
- def test_pr_settings_are_updated(
- self, autologin_user, backend, csrf_token):
- repo_name = backend.repo_name
- settings = SettingsModel(repo=repo_name)
- for name in VcsSettingsModel.GENERAL_SETTINGS:
- settings.create_or_update_setting(name, True, 'bool')
-
- data = self.FORM_DATA.copy()
- data['csrf_token'] = csrf_token
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data, status=302)
- try:
- for name in VcsSettingsModel.GENERAL_SETTINGS:
- setting = settings.get_setting_by_name(name)
- assert setting.app_settings_value is False
- finally:
- self._cleanup_repo_settings(settings)
-
- def test_pr_settings_are_not_updated_for_svn(
- self, autologin_user, backend_svn, csrf_token):
- repo_name = backend_svn.repo_name
- settings = SettingsModel(repo=repo_name)
- for name in VcsSettingsModel.GENERAL_SETTINGS:
- settings.create_or_update_setting(name, True, 'bool')
-
- data = self.FORM_DATA.copy()
- data['csrf_token'] = csrf_token
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data, status=302)
- try:
- for name in VcsSettingsModel.GENERAL_SETTINGS:
- setting = settings.get_setting_by_name(name)
- assert setting.app_settings_value is True
- finally:
- self._cleanup_repo_settings(settings)
-
- def test_svn_settings_are_created(
- self, autologin_user, backend_svn, csrf_token, settings_util):
- repo_name = backend_svn.repo_name
- data = self.FORM_DATA.copy()
- data['new_svn_tag'] = 'svn-tag'
- data['new_svn_branch'] = 'svn-branch'
- data['csrf_token'] = csrf_token
-
- # Create few global settings to make sure that uniqueness validators
- # are not triggered
- settings_util.create_rhodecode_ui(
- VcsSettingsModel.SVN_BRANCH_SECTION, 'svn-branch')
- settings_util.create_rhodecode_ui(
- VcsSettingsModel.SVN_TAG_SECTION, 'svn-tag')
-
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data, status=302)
- settings = SettingsModel(repo=repo_name)
- try:
- svn_branches = settings.get_ui_by_section(
- VcsSettingsModel.SVN_BRANCH_SECTION)
- svn_branch_names = [b.ui_value for b in svn_branches]
- svn_tags = settings.get_ui_by_section(
- VcsSettingsModel.SVN_TAG_SECTION)
- svn_tag_names = [b.ui_value for b in svn_tags]
- assert 'svn-branch' in svn_branch_names
- assert 'svn-tag' in svn_tag_names
- finally:
- self._cleanup_repo_settings(settings)
-
- def test_svn_settings_are_unique(
- self, autologin_user, backend_svn, csrf_token, settings_util):
- repo = backend_svn.repo
- repo_name = repo.repo_name
- data = self.FORM_DATA.copy()
- data['new_svn_tag'] = 'test_tag'
- data['new_svn_branch'] = 'test_branch'
- data['csrf_token'] = csrf_token
- settings_util.create_repo_rhodecode_ui(
- repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'test_branch')
- settings_util.create_repo_rhodecode_ui(
- repo, VcsSettingsModel.SVN_TAG_SECTION, 'test_tag')
-
- response = self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data, status=200)
- response.mustcontain('Pattern already exists')
-
- def test_svn_settings_with_empty_values_are_not_created(
- self, autologin_user, backend_svn, csrf_token):
- repo_name = backend_svn.repo_name
- data = self.FORM_DATA.copy()
- data['csrf_token'] = csrf_token
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data, status=302)
- settings = SettingsModel(repo=repo_name)
- try:
- svn_branches = settings.get_ui_by_section(
- VcsSettingsModel.SVN_BRANCH_SECTION)
- svn_tags = settings.get_ui_by_section(
- VcsSettingsModel.SVN_TAG_SECTION)
- assert len(svn_branches) == 0
- assert len(svn_tags) == 0
- finally:
- self._cleanup_repo_settings(settings)
-
- def test_svn_settings_are_shown_for_svn_repository(
- self, autologin_user, backend_svn, csrf_token):
- repo_name = backend_svn.repo_name
- response = self.app.get(
- url('repo_vcs_settings', repo_name=repo_name), status=200)
- response.mustcontain('Subversion Settings')
-
- @pytest.mark.skip_backends('svn')
- def test_svn_settings_are_not_created_for_not_svn_repository(
- self, autologin_user, backend, csrf_token):
- repo_name = backend.repo_name
- data = self.FORM_DATA.copy()
- data['csrf_token'] = csrf_token
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data, status=302)
- settings = SettingsModel(repo=repo_name)
- try:
- svn_branches = settings.get_ui_by_section(
- VcsSettingsModel.SVN_BRANCH_SECTION)
- svn_tags = settings.get_ui_by_section(
- VcsSettingsModel.SVN_TAG_SECTION)
- assert len(svn_branches) == 0
- assert len(svn_tags) == 0
- finally:
- self._cleanup_repo_settings(settings)
-
- @pytest.mark.skip_backends('svn')
- def test_svn_settings_are_shown_only_for_svn_repository(
- self, autologin_user, backend, csrf_token):
- repo_name = backend.repo_name
- response = self.app.get(
- url('repo_vcs_settings', repo_name=repo_name), status=200)
- response.mustcontain(no='Subversion Settings')
-
- def test_hg_settings_are_created(
- self, autologin_user, backend_hg, csrf_token):
- repo_name = backend_hg.repo_name
- data = self.FORM_DATA.copy()
- data['new_svn_tag'] = 'svn-tag'
- data['new_svn_branch'] = 'svn-branch'
- data['csrf_token'] = csrf_token
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data, status=302)
- settings = SettingsModel(repo=repo_name)
- try:
- largefiles_ui = settings.get_ui_by_section_and_key(
- 'extensions', 'largefiles')
- assert largefiles_ui.ui_active is False
- phases_ui = settings.get_ui_by_section_and_key(
- 'phases', 'publish')
- assert str2bool(phases_ui.ui_value) is False
- finally:
- self._cleanup_repo_settings(settings)
-
- def test_hg_settings_are_updated(
- self, autologin_user, backend_hg, csrf_token):
- repo_name = backend_hg.repo_name
- settings = SettingsModel(repo=repo_name)
- settings.create_ui_section_value(
- 'extensions', '', key='largefiles', active=True)
- settings.create_ui_section_value(
- 'phases', '1', key='publish', active=True)
-
- data = self.FORM_DATA.copy()
- data['csrf_token'] = csrf_token
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data, status=302)
- try:
- largefiles_ui = settings.get_ui_by_section_and_key(
- 'extensions', 'largefiles')
- assert largefiles_ui.ui_active is False
- phases_ui = settings.get_ui_by_section_and_key(
- 'phases', 'publish')
- assert str2bool(phases_ui.ui_value) is False
- finally:
- self._cleanup_repo_settings(settings)
-
- def test_hg_settings_are_shown_for_hg_repository(
- self, autologin_user, backend_hg, csrf_token):
- repo_name = backend_hg.repo_name
- response = self.app.get(
- url('repo_vcs_settings', repo_name=repo_name), status=200)
- response.mustcontain('Mercurial Settings')
-
- @pytest.mark.skip_backends('hg')
- def test_hg_settings_are_created_only_for_hg_repository(
- self, autologin_user, backend, csrf_token):
- repo_name = backend.repo_name
- data = self.FORM_DATA.copy()
- data['csrf_token'] = csrf_token
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data, status=302)
- settings = SettingsModel(repo=repo_name)
- try:
- largefiles_ui = settings.get_ui_by_section_and_key(
- 'extensions', 'largefiles')
- assert largefiles_ui is None
- phases_ui = settings.get_ui_by_section_and_key(
- 'phases', 'publish')
- assert phases_ui is None
- finally:
- self._cleanup_repo_settings(settings)
-
- @pytest.mark.skip_backends('hg')
- def test_hg_settings_are_shown_only_for_hg_repository(
- self, autologin_user, backend, csrf_token):
- repo_name = backend.repo_name
- response = self.app.get(
- url('repo_vcs_settings', repo_name=repo_name), status=200)
- response.mustcontain(no='Mercurial Settings')
-
- @pytest.mark.skip_backends('hg')
- def test_hg_settings_are_updated_only_for_hg_repository(
- self, autologin_user, backend, csrf_token):
- repo_name = backend.repo_name
- settings = SettingsModel(repo=repo_name)
- settings.create_ui_section_value(
- 'extensions', '', key='largefiles', active=True)
- settings.create_ui_section_value(
- 'phases', '1', key='publish', active=True)
-
- data = self.FORM_DATA.copy()
- data['csrf_token'] = csrf_token
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data, status=302)
- try:
- largefiles_ui = settings.get_ui_by_section_and_key(
- 'extensions', 'largefiles')
- assert largefiles_ui.ui_active is True
- phases_ui = settings.get_ui_by_section_and_key(
- 'phases', 'publish')
- assert phases_ui.ui_value == '1'
- finally:
- self._cleanup_repo_settings(settings)
-
- def test_per_repo_svn_settings_are_displayed(
- self, autologin_user, backend_svn, settings_util):
- repo = backend_svn.create_repo()
- repo_name = repo.repo_name
- branches = [
- settings_util.create_repo_rhodecode_ui(
- repo, VcsSettingsModel.SVN_BRANCH_SECTION,
- 'branch_{}'.format(i))
- for i in range(10)]
- tags = [
- settings_util.create_repo_rhodecode_ui(
- repo, VcsSettingsModel.SVN_TAG_SECTION, 'tag_{}'.format(i))
- for i in range(10)]
-
- response = self.app.get(
- url('repo_vcs_settings', repo_name=repo_name), status=200)
- assert_response = AssertResponse(response)
- for branch in branches:
- css_selector = '[name=branch_value_{}]'.format(branch.ui_id)
- element = assert_response.get_element(css_selector)
- assert element.value == branch.ui_value
- for tag in tags:
- css_selector = '[name=tag_ui_value_new_{}]'.format(tag.ui_id)
- element = assert_response.get_element(css_selector)
- assert element.value == tag.ui_value
-
- def test_per_repo_hg_and_pr_settings_are_not_displayed_for_svn(
- self, autologin_user, backend_svn, settings_util):
- repo = backend_svn.create_repo()
- repo_name = repo.repo_name
- response = self.app.get(
- url('repo_vcs_settings', repo_name=repo_name), status=200)
- response.mustcontain(no='Hooks: ')
- response.mustcontain(no='Pull Request Settings: ')
-
- def test_inherit_global_settings_value_is_saved(
- self, autologin_user, backend, csrf_token):
- repo_name = backend.repo_name
- data = self.FORM_DATA.copy()
- data['csrf_token'] = csrf_token
- data['inherit_global_settings'] = True
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data, status=302)
-
- settings = SettingsModel(repo=repo_name)
- vcs_settings = VcsSettingsModel(repo=repo_name)
- try:
- assert vcs_settings.inherit_global_settings is True
- finally:
- self._cleanup_repo_settings(settings)
-
- def test_repo_cache_is_invalidated_when_settings_are_updated(
- self, autologin_user, backend, csrf_token):
- repo_name = backend.repo_name
- data = self.FORM_DATA.copy()
- data['csrf_token'] = csrf_token
- data['inherit_global_settings'] = True
- settings = SettingsModel(repo=repo_name)
-
- invalidation_patcher = mock.patch(
- 'rhodecode.controllers.admin.repos.ScmModel.mark_for_invalidation')
- with invalidation_patcher as invalidation_mock:
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data,
- status=302)
- try:
- invalidation_mock.assert_called_once_with(repo_name, delete=True)
- finally:
- self._cleanup_repo_settings(settings)
-
- def test_other_settings_not_saved_inherit_global_settings_is_true(
- self, autologin_user, backend, csrf_token):
- repo_name = backend.repo_name
- data = self.FORM_DATA.copy()
- data['csrf_token'] = csrf_token
- data['inherit_global_settings'] = True
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data, status=302)
-
- settings = SettingsModel(repo=repo_name)
- ui_settings = (
- VcsSettingsModel.HOOKS_SETTINGS + VcsSettingsModel.HG_SETTINGS)
-
- vcs_settings = []
- try:
- for section, key in ui_settings:
- ui = settings.get_ui_by_section_and_key(section, key)
- if ui:
- vcs_settings.append(ui)
- vcs_settings.extend(settings.get_ui_by_section(
- VcsSettingsModel.SVN_BRANCH_SECTION))
- vcs_settings.extend(settings.get_ui_by_section(
- VcsSettingsModel.SVN_TAG_SECTION))
- for name in VcsSettingsModel.GENERAL_SETTINGS:
- setting = settings.get_setting_by_name(name)
- if setting:
- vcs_settings.append(setting)
- assert vcs_settings == []
- finally:
- self._cleanup_repo_settings(settings)
-
- def test_delete_svn_branch_and_tag_patterns(
- self, autologin_user, backend_svn, settings_util, csrf_token):
- repo = backend_svn.create_repo()
- repo_name = repo.repo_name
- branch = settings_util.create_repo_rhodecode_ui(
- repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'test_branch',
- cleanup=False)
- tag = settings_util.create_repo_rhodecode_ui(
- repo, VcsSettingsModel.SVN_TAG_SECTION, 'test_tag', cleanup=False)
- data = {
- '_method': 'delete',
- 'csrf_token': csrf_token
- }
- for id_ in (branch.ui_id, tag.ui_id):
- data['delete_svn_pattern'] = id_,
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data,
- headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200)
- settings = VcsSettingsModel(repo=repo_name)
- assert settings.get_repo_svn_branch_patterns() == []
-
- def test_delete_svn_branch_requires_repo_admin_permission(
- self, backend_svn, user_util, settings_util, csrf_token):
- repo = backend_svn.create_repo()
- repo_name = repo.repo_name
-
- logout_user_session(self.app, csrf_token)
- session = login_user_session(
- self.app, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
- csrf_token = auth.get_csrf_token(session)
-
- repo = Repository.get_by_repo_name(repo_name)
- user = UserModel().get_by_username(TEST_USER_REGULAR_LOGIN)
- user_util.grant_user_permission_to_repo(repo, user, 'repository.admin')
- branch = settings_util.create_repo_rhodecode_ui(
- repo, VcsSettingsModel.SVN_BRANCH_SECTION, 'test_branch',
- cleanup=False)
- data = {
- '_method': 'delete',
- 'csrf_token': csrf_token,
- 'delete_svn_pattern': branch.ui_id
- }
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data,
- headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=200)
-
- def test_delete_svn_branch_raises_400_when_not_found(
- self, autologin_user, backend_svn, settings_util, csrf_token):
- repo_name = backend_svn.repo_name
- data = {
- '_method': 'delete',
- 'delete_svn_pattern': 123,
- 'csrf_token': csrf_token
- }
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data,
- headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=400)
-
- def test_delete_svn_branch_raises_400_when_no_id_specified(
- self, autologin_user, backend_svn, settings_util, csrf_token):
- repo_name = backend_svn.repo_name
- data = {
- '_method': 'delete',
- 'csrf_token': csrf_token
- }
- self.app.post(
- url('repo_vcs_settings', repo_name=repo_name), data,
- headers={'X-REQUESTED-WITH': 'XMLHttpRequest', }, status=400)
-
- def _cleanup_repo_settings(self, settings_model):
- cleanup = []
- ui_settings = (
- VcsSettingsModel.HOOKS_SETTINGS + VcsSettingsModel.HG_SETTINGS)
-
- for section, key in ui_settings:
- ui = settings_model.get_ui_by_section_and_key(section, key)
- if ui:
- cleanup.append(ui)
-
- cleanup.extend(settings_model.get_ui_by_section(
- VcsSettingsModel.INHERIT_SETTINGS))
- cleanup.extend(settings_model.get_ui_by_section(
- VcsSettingsModel.SVN_BRANCH_SECTION))
- cleanup.extend(settings_model.get_ui_by_section(
- VcsSettingsModel.SVN_TAG_SECTION))
-
- for name in VcsSettingsModel.GENERAL_SETTINGS:
- setting = settings_model.get_setting_by_name(name)
- if setting:
- cleanup.append(setting)
-
- for object_ in cleanup:
- Session().delete(object_)
- Session().commit()
-
- def assert_repo_value_equals_global_value(self, response, setting):
- assert_response = AssertResponse(response)
- global_css_selector = '[name={}_inherited]'.format(setting)
- repo_css_selector = '[name={}]'.format(setting)
- repo_element = assert_response.get_element(repo_css_selector)
- global_element = assert_response.get_element(global_css_selector)
- assert repo_element.value == global_element.value
-
-
-def _get_permission_for_user(user, repo):
- perm = UserRepoToPerm.query()\
- .filter(UserRepoToPerm.repository ==
- Repository.get_by_repo_name(repo))\
- .filter(UserRepoToPerm.user == User.get_by_username(user))\
- .all()
- return perm
diff --git a/rhodecode/apps/admin/tests/test_admin_repository_groups.py b/rhodecode/apps/admin/tests/test_admin_repository_groups.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/admin/tests/test_admin_repository_groups.py
@@ -0,0 +1,176 @@
+# -*- 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 os
+import pytest
+
+from rhodecode.apps._base import ADMIN_PREFIX
+from rhodecode.lib import helpers as h
+from rhodecode.model.db import Repository, UserRepoToPerm, User
+from rhodecode.model.meta import Session
+from rhodecode.model.repo_group import RepoGroupModel
+from rhodecode.tests import (
+ assert_session_flash, TEST_USER_REGULAR_LOGIN, TESTS_TMP_PATH, TestController)
+from rhodecode.tests.fixture import Fixture
+
+fixture = Fixture()
+
+
+def route_path(name, params=None, **kwargs):
+ import urllib
+
+ base_url = {
+ 'repo_groups': ADMIN_PREFIX + '/repo_groups',
+ 'repo_group_new': ADMIN_PREFIX + '/repo_group/new',
+ 'repo_group_create': ADMIN_PREFIX + '/repo_group/create',
+
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
+
+
+def _get_permission_for_user(user, repo):
+ perm = UserRepoToPerm.query()\
+ .filter(UserRepoToPerm.repository ==
+ Repository.get_by_repo_name(repo))\
+ .filter(UserRepoToPerm.user == User.get_by_username(user))\
+ .all()
+ return perm
+
+
+@pytest.mark.usefixtures("app")
+class TestAdminRepositoryGroups(object):
+ def test_show_repo_groups(self, autologin_user):
+ response = self.app.get(route_path('repo_groups'))
+ response.mustcontain('data: []')
+
+ def test_show_repo_groups_after_creating_group(self, autologin_user):
+ fixture.create_repo_group('test_repo_group')
+ response = self.app.get(route_path('repo_groups'))
+ response.mustcontain('"name_raw": "test_repo_group"')
+ fixture.destroy_repo_group('test_repo_group')
+
+ def test_new(self, autologin_user):
+ self.app.get(route_path('repo_group_new'))
+
+ def test_new_with_parent_group(self, autologin_user, user_util):
+ gr = user_util.create_repo_group()
+
+ self.app.get(route_path('repo_group_new'),
+ params=dict(parent_group=gr.group_name))
+
+ def test_new_by_regular_user_no_permission(self, autologin_regular_user):
+ self.app.get(route_path('repo_group_new'), status=403)
+
+ @pytest.mark.parametrize('repo_group_name', [
+ 'git_repo',
+ 'git_repo_ąć',
+ 'hg_repo',
+ '12345',
+ 'hg_repo_ąć',
+ ])
+ def test_create(self, autologin_user, repo_group_name, csrf_token):
+ repo_group_name_unicode = repo_group_name.decode('utf8')
+ description = 'description for newly created repo group'
+
+ response = self.app.post(
+ route_path('repo_group_create'),
+ fixture._get_group_create_params(
+ group_name=repo_group_name,
+ group_description=description,
+ csrf_token=csrf_token))
+
+ # run the check page that triggers the flash message
+ repo_gr_url = h.route_path(
+ 'repo_group_home', repo_group_name=repo_group_name)
+
+ assert_session_flash(
+ response,
+ 'Created repository group %s ' % (
+ repo_gr_url, repo_group_name_unicode))
+
+ # # test if the repo group was created in the database
+ new_repo_group = RepoGroupModel()._get_repo_group(
+ repo_group_name_unicode)
+ assert new_repo_group is not None
+
+ assert new_repo_group.group_name == repo_group_name_unicode
+ assert new_repo_group.group_description == description
+
+ # test if the repository is visible in the list ?
+ response = self.app.get(repo_gr_url)
+ response.mustcontain(repo_group_name)
+
+ # test if the repository group was created on filesystem
+ is_on_filesystem = os.path.isdir(
+ os.path.join(TESTS_TMP_PATH, repo_group_name))
+ if not is_on_filesystem:
+ self.fail('no repo group %s in filesystem' % repo_group_name)
+
+ RepoGroupModel().delete(repo_group_name_unicode)
+ Session().commit()
+
+ @pytest.mark.parametrize('repo_group_name', [
+ 'git_repo',
+ 'git_repo_ąć',
+ 'hg_repo',
+ '12345',
+ 'hg_repo_ąć',
+ ])
+ def test_create_subgroup(self, autologin_user, user_util, repo_group_name, csrf_token):
+ parent_group = user_util.create_repo_group()
+ parent_group_name = parent_group.group_name
+
+ expected_group_name = '{}/{}'.format(
+ parent_group_name, repo_group_name)
+ expected_group_name_unicode = expected_group_name.decode('utf8')
+
+ try:
+ response = self.app.post(
+ route_path('repo_group_create'),
+ fixture._get_group_create_params(
+ group_name=repo_group_name,
+ group_parent_id=parent_group.group_id,
+ group_description='Test desciption',
+ csrf_token=csrf_token))
+
+ assert_session_flash(
+ response,
+ u'Created repository group %s ' % (
+ h.route_path('repo_group_home',
+ repo_group_name=expected_group_name),
+ expected_group_name_unicode))
+ finally:
+ RepoGroupModel().delete(expected_group_name_unicode)
+ Session().commit()
+
+ def test_user_with_creation_permissions_cannot_create_subgroups(
+ self, autologin_regular_user, user_util):
+
+ user_util.grant_user_permission(
+ TEST_USER_REGULAR_LOGIN, 'hg.repogroup.create.true')
+ parent_group = user_util.create_repo_group()
+ parent_group_id = parent_group.group_id
+ self.app.get(
+ route_path('repo_group_new',
+ params=dict(parent_group=parent_group_id), ),
+ status=403)
diff --git a/rhodecode/apps/admin/tests/test_admin_user_groups.py b/rhodecode/apps/admin/tests/test_admin_user_groups.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/admin/tests/test_admin_user_groups.py
@@ -0,0 +1,170 @@
+# -*- 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.model.db import UserGroup, User
+from rhodecode.model.meta import Session
+
+from rhodecode.tests import (
+ TestController, TEST_USER_REGULAR_LOGIN, assert_session_flash)
+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 = {
+ 'user_groups': ADMIN_PREFIX + '/user_groups',
+ 'user_groups_data': ADMIN_PREFIX + '/user_groups_data',
+ 'user_group_members_data': ADMIN_PREFIX + '/user_groups/{user_group_id}/members',
+ 'user_groups_new': ADMIN_PREFIX + '/user_groups/new',
+ 'user_groups_create': ADMIN_PREFIX + '/user_groups/create',
+ 'edit_user_group': ADMIN_PREFIX + '/user_groups/{user_group_id}/edit',
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
+
+
+class TestAdminUserGroupsView(TestController):
+
+ def test_show_users(self):
+ self.log_user()
+ self.app.get(route_path('user_groups'))
+
+ def test_show_user_groups_data(self, xhr_header):
+ self.log_user()
+ response = self.app.get(route_path(
+ 'user_groups_data'), extra_environ=xhr_header)
+
+ all_user_groups = UserGroup.query().count()
+ assert response.json['recordsTotal'] == all_user_groups
+
+ def test_show_user_groups_data_filtered(self, xhr_header):
+ self.log_user()
+ response = self.app.get(route_path(
+ 'user_groups_data', params={'search[value]': 'empty_search'}),
+ extra_environ=xhr_header)
+
+ all_user_groups = UserGroup.query().count()
+ assert response.json['recordsTotal'] == all_user_groups
+ assert response.json['recordsFiltered'] == 0
+
+ def test_usergroup_escape(self, user_util, xhr_header):
+ self.log_user()
+
+ xss_img = ' '
+ user = user_util.create_user()
+ user.name = xss_img
+ user.lastname = xss_img
+ Session().add(user)
+ Session().commit()
+
+ user_group = user_util.create_user_group()
+
+ user_group.users_group_name = xss_img
+ user_group.user_group_description = 'DESC '
+
+ response = self.app.get(
+ route_path('user_groups_data'), extra_environ=xhr_header)
+
+ response.mustcontain(
+ '<strong onload="alert();">DESC</strong>')
+ response.mustcontain(
+ '<img src="/image1" onload="'
+ 'alert('Hello, World!');">')
+
+ def test_edit_user_group_autocomplete_empty_members(self, xhr_header, user_util):
+ self.log_user()
+ ug = user_util.create_user_group()
+ response = self.app.get(
+ route_path('user_group_members_data', user_group_id=ug.users_group_id),
+ extra_environ=xhr_header)
+
+ assert response.json == {'members': []}
+
+ def test_edit_user_group_autocomplete_members(self, xhr_header, user_util):
+ self.log_user()
+ members = [u.user_id for u in User.get_all()]
+ ug = user_util.create_user_group(members=members)
+ response = self.app.get(
+ route_path('user_group_members_data',
+ user_group_id=ug.users_group_id),
+ extra_environ=xhr_header)
+
+ assert len(response.json['members']) == len(members)
+
+ def test_creation_page(self):
+ self.log_user()
+ self.app.get(route_path('user_groups_new'), status=200)
+
+ def test_create(self):
+ from rhodecode.lib import helpers as h
+
+ self.log_user()
+ users_group_name = 'test_user_group'
+ response = self.app.post(route_path('user_groups_create'), {
+ 'users_group_name': users_group_name,
+ 'user_group_description': 'DESC',
+ 'active': True,
+ 'csrf_token': self.csrf_token})
+
+ user_group_id = UserGroup.get_by_group_name(
+ users_group_name).users_group_id
+
+ user_group_link = h.link_to(
+ users_group_name,
+ route_path('edit_user_group', user_group_id=user_group_id))
+
+ assert_session_flash(
+ response,
+ 'Created user group %s' % user_group_link)
+
+ fixture.destroy_user_group(users_group_name)
+
+ def test_create_with_empty_name(self):
+ self.log_user()
+
+ response = self.app.post(route_path('user_groups_create'), {
+ 'users_group_name': '',
+ 'user_group_description': 'DESC',
+ 'active': True,
+ 'csrf_token': self.csrf_token}, status=200)
+
+ response.mustcontain('Please enter a value')
+
+ def test_create_duplicate(self, user_util):
+ self.log_user()
+
+ user_group = user_util.create_user_group()
+ duplicate_name = user_group.users_group_name
+ response = self.app.post(route_path('user_groups_create'), {
+ 'users_group_name': duplicate_name,
+ 'user_group_description': 'DESC',
+ 'active': True,
+ 'csrf_token': self.csrf_token}, status=200)
+
+ response.mustcontain(
+ 'User group `{}` already exists'.format(user_group.users_group_name))
diff --git a/rhodecode/apps/admin/tests/test_admin_users.py b/rhodecode/apps/admin/tests/test_admin_users.py
--- a/rhodecode/apps/admin/tests/test_admin_users.py
+++ b/rhodecode/apps/admin/tests/test_admin_users.py
@@ -19,8 +19,12 @@
# and proprietary license terms, please see https://rhodecode.com/licenses/
import pytest
+from sqlalchemy.orm.exc import NoResultFound
-from rhodecode.model.db import User, UserApiKeys, UserEmailMap
+from rhodecode.lib import auth
+from rhodecode.lib import helpers as h
+from rhodecode.model import validators
+from rhodecode.model.db import User, UserApiKeys, UserEmailMap, Repository
from rhodecode.model.meta import Session
from rhodecode.model.user import UserModel
@@ -40,6 +44,27 @@ def route_path(name, params=None, **kwar
ADMIN_PREFIX + '/users',
'users_data':
ADMIN_PREFIX + '/users_data',
+ 'users_create':
+ ADMIN_PREFIX + '/users/create',
+ 'users_new':
+ ADMIN_PREFIX + '/users/new',
+ 'user_edit':
+ ADMIN_PREFIX + '/users/{user_id}/edit',
+ 'user_edit_advanced':
+ ADMIN_PREFIX + '/users/{user_id}/edit/advanced',
+ 'user_edit_global_perms':
+ ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions',
+ 'user_edit_global_perms_update':
+ ADMIN_PREFIX + '/users/{user_id}/edit/global_permissions/update',
+ 'user_update':
+ ADMIN_PREFIX + '/users/{user_id}/update',
+ 'user_delete':
+ ADMIN_PREFIX + '/users/{user_id}/delete',
+ 'user_force_password_reset':
+ ADMIN_PREFIX + '/users/{user_id}/password_reset',
+ 'user_create_personal_repo_group':
+ ADMIN_PREFIX + '/users/{user_id}/create_repo_group',
+
'edit_user_auth_tokens':
ADMIN_PREFIX + '/users/{user_id}/edit/auth_tokens',
'edit_user_auth_tokens_add':
@@ -60,6 +85,15 @@ def route_path(name, params=None, **kwar
ADMIN_PREFIX + '/users/{user_id}/edit/ips/new',
'edit_user_ips_delete':
ADMIN_PREFIX + '/users/{user_id}/edit/ips/delete',
+
+ 'edit_user_perms_summary':
+ ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary',
+ 'edit_user_perms_summary_json':
+ ADMIN_PREFIX + '/users/{user_id}/edit/permissions_summary/json',
+
+ 'edit_user_audit_logs':
+ ADMIN_PREFIX + '/users/{user_id}/edit/audit',
+
}[name].format(**kwargs)
if params:
@@ -135,7 +169,7 @@ class TestAdminUsersView(TestController)
self.log_user()
user = user_util.create_user()
user_id = user.user_id
- keys = user.extra_auth_tokens
+ keys = user.auth_tokens
assert 2 == len(keys)
response = self.app.post(
@@ -220,7 +254,8 @@ class TestAdminUsersView(TestController)
def test_emails(self):
self.log_user()
user = User.get_by_username(TEST_USER_REGULAR_LOGIN)
- response = self.app.get(route_path('edit_user_emails', user_id=user.user_id))
+ response = self.app.get(
+ route_path('edit_user_emails', user_id=user.user_id))
response.mustcontain('No additional emails specified')
def test_emails_add(self, user_util):
@@ -233,7 +268,8 @@ class TestAdminUsersView(TestController)
params={'new_email': 'example@rhodecode.com',
'csrf_token': self.csrf_token})
- response = self.app.get(route_path('edit_user_emails', user_id=user_id))
+ response = self.app.get(
+ route_path('edit_user_emails', user_id=user_id))
response.mustcontain('example@rhodecode.com')
def test_emails_add_existing_email(self, user_util, user_regular):
@@ -250,7 +286,8 @@ class TestAdminUsersView(TestController)
assert_session_flash(
response, 'This e-mail address is already taken')
- response = self.app.get(route_path('edit_user_emails', user_id=user_id))
+ response = self.app.get(
+ route_path('edit_user_emails', user_id=user_id))
response.mustcontain(no=[existing_email])
def test_emails_delete(self, user_util):
@@ -263,7 +300,8 @@ class TestAdminUsersView(TestController)
params={'new_email': 'example@rhodecode.com',
'csrf_token': self.csrf_token})
- response = self.app.get(route_path('edit_user_emails', user_id=user_id))
+ response = self.app.get(
+ route_path('edit_user_emails', user_id=user_id))
response.mustcontain('example@rhodecode.com')
user_email = UserEmailMap.query()\
@@ -277,5 +315,469 @@ class TestAdminUsersView(TestController)
params={'del_email_id': del_email_id,
'csrf_token': self.csrf_token})
- response = self.app.get(route_path('edit_user_emails', user_id=user_id))
- response.mustcontain(no=['example@rhodecode.com'])
\ No newline at end of file
+ response = self.app.get(
+ route_path('edit_user_emails', user_id=user_id))
+ response.mustcontain(no=['example@rhodecode.com'])
+
+
+ def test_create(self, request, xhr_header):
+ self.log_user()
+ username = 'newtestuser'
+ password = 'test12'
+ password_confirmation = password
+ name = 'name'
+ lastname = 'lastname'
+ email = 'mail@mail.com'
+
+ self.app.get(route_path('users_new'))
+
+ response = self.app.post(route_path('users_create'), params={
+ 'username': username,
+ 'password': password,
+ 'password_confirmation': password_confirmation,
+ 'firstname': name,
+ 'active': True,
+ 'lastname': lastname,
+ 'extern_name': 'rhodecode',
+ 'extern_type': 'rhodecode',
+ 'email': email,
+ 'csrf_token': self.csrf_token,
+ })
+ user_link = h.link_to(
+ username,
+ route_path(
+ 'user_edit', user_id=User.get_by_username(username).user_id))
+ assert_session_flash(response, 'Created user %s' % (user_link,))
+
+ @request.addfinalizer
+ def cleanup():
+ fixture.destroy_user(username)
+ Session().commit()
+
+ new_user = User.query().filter(User.username == username).one()
+
+ assert new_user.username == username
+ assert auth.check_password(password, new_user.password)
+ assert new_user.name == name
+ assert new_user.lastname == lastname
+ assert new_user.email == email
+
+ response = self.app.get(route_path('users_data'),
+ extra_environ=xhr_header)
+ response.mustcontain(username)
+
+ def test_create_err(self):
+ self.log_user()
+ username = 'new_user'
+ password = ''
+ name = 'name'
+ lastname = 'lastname'
+ email = 'errmail.com'
+
+ self.app.get(route_path('users_new'))
+
+ response = self.app.post(route_path('users_create'), params={
+ 'username': username,
+ 'password': password,
+ 'name': name,
+ 'active': False,
+ 'lastname': lastname,
+ 'email': email,
+ 'csrf_token': self.csrf_token,
+ })
+
+ msg = validators.ValidUsername(
+ False, {})._messages['system_invalid_username']
+ msg = h.html_escape(msg % {'username': 'new_user'})
+ response.mustcontain('%s ' % msg)
+ response.mustcontain(
+ 'Please enter a value ')
+ response.mustcontain(
+ 'An email address must contain a'
+ ' single @ ')
+
+ def get_user():
+ Session().query(User).filter(User.username == username).one()
+
+ with pytest.raises(NoResultFound):
+ get_user()
+
+ def test_new(self):
+ self.log_user()
+ self.app.get(route_path('users_new'))
+
+ @pytest.mark.parametrize("name, attrs", [
+ ('firstname', {'firstname': 'new_username'}),
+ ('lastname', {'lastname': 'new_username'}),
+ ('admin', {'admin': True}),
+ ('admin', {'admin': False}),
+ ('extern_type', {'extern_type': 'ldap'}),
+ ('extern_type', {'extern_type': None}),
+ ('extern_name', {'extern_name': 'test'}),
+ ('extern_name', {'extern_name': None}),
+ ('active', {'active': False}),
+ ('active', {'active': True}),
+ ('email', {'email': 'some@email.com'}),
+ ('language', {'language': 'de'}),
+ ('language', {'language': 'en'}),
+ # ('new_password', {'new_password': 'foobar123',
+ # 'password_confirmation': 'foobar123'})
+ ])
+ def test_update(self, name, attrs, user_util):
+ self.log_user()
+ usr = user_util.create_user(
+ password='qweqwe',
+ email='testme@rhodecode.org',
+ extern_type='rhodecode',
+ extern_name='xxx',
+ )
+ user_id = usr.user_id
+ Session().commit()
+
+ params = usr.get_api_data()
+ cur_lang = params['language'] or 'en'
+ params.update({
+ 'password_confirmation': '',
+ 'new_password': '',
+ 'language': cur_lang,
+ 'csrf_token': self.csrf_token,
+ })
+ params.update({'new_password': ''})
+ params.update(attrs)
+ if name == 'email':
+ params['emails'] = [attrs['email']]
+ elif name == 'extern_type':
+ # cannot update this via form, expected value is original one
+ params['extern_type'] = "rhodecode"
+ elif name == 'extern_name':
+ # cannot update this via form, expected value is original one
+ params['extern_name'] = 'xxx'
+ # special case since this user is not
+ # logged in yet his data is not filled
+ # so we use creation data
+
+ response = self.app.post(
+ route_path('user_update', user_id=usr.user_id), params)
+ assert response.status_int == 302
+ assert_session_flash(response, 'User updated successfully')
+
+ updated_user = User.get(user_id)
+ updated_params = updated_user.get_api_data()
+ updated_params.update({'password_confirmation': ''})
+ updated_params.update({'new_password': ''})
+
+ del params['csrf_token']
+ assert params == updated_params
+
+ def test_update_and_migrate_password(
+ self, autologin_user, real_crypto_backend, user_util):
+
+ user = user_util.create_user()
+ temp_user = user.username
+ user.password = auth._RhodeCodeCryptoSha256().hash_create(
+ b'test123')
+ Session().add(user)
+ Session().commit()
+
+ params = user.get_api_data()
+
+ params.update({
+ 'password_confirmation': 'qweqwe123',
+ 'new_password': 'qweqwe123',
+ 'language': 'en',
+ 'csrf_token': autologin_user.csrf_token,
+ })
+
+ response = self.app.post(
+ route_path('user_update', user_id=user.user_id), params)
+ assert response.status_int == 302
+ assert_session_flash(response, 'User updated successfully')
+
+ # new password should be bcrypted, after log-in and transfer
+ user = User.get_by_username(temp_user)
+ assert user.password.startswith('$')
+
+ updated_user = User.get_by_username(temp_user)
+ updated_params = updated_user.get_api_data()
+ updated_params.update({'password_confirmation': 'qweqwe123'})
+ updated_params.update({'new_password': 'qweqwe123'})
+
+ del params['csrf_token']
+ assert params == updated_params
+
+ def test_delete(self):
+ self.log_user()
+ username = 'newtestuserdeleteme'
+
+ fixture.create_user(name=username)
+
+ new_user = Session().query(User)\
+ .filter(User.username == username).one()
+ response = self.app.post(
+ route_path('user_delete', user_id=new_user.user_id),
+ params={'csrf_token': self.csrf_token})
+
+ assert_session_flash(response, 'Successfully deleted user')
+
+ def test_delete_owner_of_repository(self, request, user_util):
+ self.log_user()
+ obj_name = 'test_repo'
+ usr = user_util.create_user()
+ username = usr.username
+ fixture.create_repo(obj_name, cur_user=usr.username)
+
+ new_user = Session().query(User)\
+ .filter(User.username == username).one()
+ response = self.app.post(
+ route_path('user_delete', user_id=new_user.user_id),
+ params={'csrf_token': self.csrf_token})
+
+ msg = 'user "%s" still owns 1 repositories and cannot be removed. ' \
+ 'Switch owners or remove those repositories:%s' % (username,
+ obj_name)
+ assert_session_flash(response, msg)
+ fixture.destroy_repo(obj_name)
+
+ def test_delete_owner_of_repository_detaching(self, request, user_util):
+ self.log_user()
+ obj_name = 'test_repo'
+ usr = user_util.create_user(auto_cleanup=False)
+ username = usr.username
+ fixture.create_repo(obj_name, cur_user=usr.username)
+
+ new_user = Session().query(User)\
+ .filter(User.username == username).one()
+ response = self.app.post(
+ route_path('user_delete', user_id=new_user.user_id),
+ params={'user_repos': 'detach', 'csrf_token': self.csrf_token})
+
+ msg = 'Detached 1 repositories'
+ assert_session_flash(response, msg)
+ fixture.destroy_repo(obj_name)
+
+ def test_delete_owner_of_repository_deleting(self, request, user_util):
+ self.log_user()
+ obj_name = 'test_repo'
+ usr = user_util.create_user(auto_cleanup=False)
+ username = usr.username
+ fixture.create_repo(obj_name, cur_user=usr.username)
+
+ new_user = Session().query(User)\
+ .filter(User.username == username).one()
+ response = self.app.post(
+ route_path('user_delete', user_id=new_user.user_id),
+ params={'user_repos': 'delete', 'csrf_token': self.csrf_token})
+
+ msg = 'Deleted 1 repositories'
+ assert_session_flash(response, msg)
+
+ def test_delete_owner_of_repository_group(self, request, user_util):
+ self.log_user()
+ obj_name = 'test_group'
+ usr = user_util.create_user()
+ username = usr.username
+ fixture.create_repo_group(obj_name, cur_user=usr.username)
+
+ new_user = Session().query(User)\
+ .filter(User.username == username).one()
+ response = self.app.post(
+ route_path('user_delete', user_id=new_user.user_id),
+ params={'csrf_token': self.csrf_token})
+
+ msg = 'user "%s" still owns 1 repository groups and cannot be removed. ' \
+ 'Switch owners or remove those repository groups:%s' % (username,
+ obj_name)
+ assert_session_flash(response, msg)
+ fixture.destroy_repo_group(obj_name)
+
+ def test_delete_owner_of_repository_group_detaching(self, request, user_util):
+ self.log_user()
+ obj_name = 'test_group'
+ usr = user_util.create_user(auto_cleanup=False)
+ username = usr.username
+ fixture.create_repo_group(obj_name, cur_user=usr.username)
+
+ new_user = Session().query(User)\
+ .filter(User.username == username).one()
+ response = self.app.post(
+ route_path('user_delete', user_id=new_user.user_id),
+ params={'user_repo_groups': 'delete', 'csrf_token': self.csrf_token})
+
+ msg = 'Deleted 1 repository groups'
+ assert_session_flash(response, msg)
+
+ def test_delete_owner_of_repository_group_deleting(self, request, user_util):
+ self.log_user()
+ obj_name = 'test_group'
+ usr = user_util.create_user(auto_cleanup=False)
+ username = usr.username
+ fixture.create_repo_group(obj_name, cur_user=usr.username)
+
+ new_user = Session().query(User)\
+ .filter(User.username == username).one()
+ response = self.app.post(
+ route_path('user_delete', user_id=new_user.user_id),
+ params={'user_repo_groups': 'detach', 'csrf_token': self.csrf_token})
+
+ msg = 'Detached 1 repository groups'
+ assert_session_flash(response, msg)
+ fixture.destroy_repo_group(obj_name)
+
+ def test_delete_owner_of_user_group(self, request, user_util):
+ self.log_user()
+ obj_name = 'test_user_group'
+ usr = user_util.create_user()
+ username = usr.username
+ fixture.create_user_group(obj_name, cur_user=usr.username)
+
+ new_user = Session().query(User)\
+ .filter(User.username == username).one()
+ response = self.app.post(
+ route_path('user_delete', user_id=new_user.user_id),
+ params={'csrf_token': self.csrf_token})
+
+ msg = 'user "%s" still owns 1 user groups and cannot be removed. ' \
+ 'Switch owners or remove those user groups:%s' % (username,
+ obj_name)
+ assert_session_flash(response, msg)
+ fixture.destroy_user_group(obj_name)
+
+ def test_delete_owner_of_user_group_detaching(self, request, user_util):
+ self.log_user()
+ obj_name = 'test_user_group'
+ usr = user_util.create_user(auto_cleanup=False)
+ username = usr.username
+ fixture.create_user_group(obj_name, cur_user=usr.username)
+
+ new_user = Session().query(User)\
+ .filter(User.username == username).one()
+ try:
+ response = self.app.post(
+ route_path('user_delete', user_id=new_user.user_id),
+ params={'user_user_groups': 'detach',
+ 'csrf_token': self.csrf_token})
+
+ msg = 'Detached 1 user groups'
+ assert_session_flash(response, msg)
+ finally:
+ fixture.destroy_user_group(obj_name)
+
+ def test_delete_owner_of_user_group_deleting(self, request, user_util):
+ self.log_user()
+ obj_name = 'test_user_group'
+ usr = user_util.create_user(auto_cleanup=False)
+ username = usr.username
+ fixture.create_user_group(obj_name, cur_user=usr.username)
+
+ new_user = Session().query(User)\
+ .filter(User.username == username).one()
+ response = self.app.post(
+ route_path('user_delete', user_id=new_user.user_id),
+ params={'user_user_groups': 'delete', 'csrf_token': self.csrf_token})
+
+ msg = 'Deleted 1 user groups'
+ assert_session_flash(response, msg)
+
+ def test_edit(self, user_util):
+ self.log_user()
+ user = user_util.create_user()
+ self.app.get(route_path('user_edit', user_id=user.user_id))
+
+ def test_edit_default_user_redirect(self):
+ self.log_user()
+ user = User.get_default_user()
+ self.app.get(route_path('user_edit', user_id=user.user_id), status=302)
+
+ @pytest.mark.parametrize(
+ 'repo_create, repo_create_write, user_group_create, repo_group_create,'
+ 'fork_create, inherit_default_permissions, expect_error,'
+ 'expect_form_error', [
+ ('hg.create.none', 'hg.create.write_on_repogroup.false',
+ 'hg.usergroup.create.false', 'hg.repogroup.create.false',
+ 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
+ ('hg.create.repository', 'hg.create.write_on_repogroup.false',
+ 'hg.usergroup.create.false', 'hg.repogroup.create.false',
+ 'hg.fork.none', 'hg.inherit_default_perms.false', False, False),
+ ('hg.create.repository', 'hg.create.write_on_repogroup.true',
+ 'hg.usergroup.create.true', 'hg.repogroup.create.true',
+ 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
+ False),
+ ('hg.create.XXX', 'hg.create.write_on_repogroup.true',
+ 'hg.usergroup.create.true', 'hg.repogroup.create.true',
+ 'hg.fork.repository', 'hg.inherit_default_perms.false', False,
+ True),
+ ('', '', '', '', '', '', True, False),
+ ])
+ def test_global_perms_on_user(
+ self, repo_create, repo_create_write, user_group_create,
+ repo_group_create, fork_create, expect_error, expect_form_error,
+ inherit_default_permissions, user_util):
+ self.log_user()
+ user = user_util.create_user()
+ uid = user.user_id
+
+ # ENABLE REPO CREATE ON A GROUP
+ perm_params = {
+ 'inherit_default_permissions': False,
+ 'default_repo_create': repo_create,
+ 'default_repo_create_on_write': repo_create_write,
+ 'default_user_group_create': user_group_create,
+ 'default_repo_group_create': repo_group_create,
+ 'default_fork_create': fork_create,
+ 'default_inherit_default_permissions': inherit_default_permissions,
+ 'csrf_token': self.csrf_token,
+ }
+ response = self.app.post(
+ route_path('user_edit_global_perms_update', user_id=uid),
+ params=perm_params)
+
+ if expect_form_error:
+ assert response.status_int == 200
+ response.mustcontain('Value must be one of')
+ else:
+ if expect_error:
+ msg = 'An error occurred during permissions saving'
+ else:
+ msg = 'User global permissions updated successfully'
+ ug = User.get(uid)
+ del perm_params['inherit_default_permissions']
+ del perm_params['csrf_token']
+ assert perm_params == ug.get_default_perms()
+ assert_session_flash(response, msg)
+
+ def test_global_permissions_initial_values(self, user_util):
+ self.log_user()
+ user = user_util.create_user()
+ uid = user.user_id
+ response = self.app.get(
+ route_path('user_edit_global_perms', user_id=uid))
+ default_user = User.get_default_user()
+ default_permissions = default_user.get_default_perms()
+ assert_response = response.assert_response()
+ expected_permissions = (
+ 'default_repo_create', 'default_repo_create_on_write',
+ 'default_fork_create', 'default_repo_group_create',
+ 'default_user_group_create', 'default_inherit_default_permissions')
+ for permission in expected_permissions:
+ css_selector = '[name={}][checked=checked]'.format(permission)
+ element = assert_response.get_element(css_selector)
+ assert element.value == default_permissions[permission]
+
+ def test_perms_summary_page(self):
+ user = self.log_user()
+ response = self.app.get(
+ route_path('edit_user_perms_summary', user_id=user['user_id']))
+ for repo in Repository.query().all():
+ response.mustcontain(repo.repo_name)
+
+ def test_perms_summary_page_json(self):
+ user = self.log_user()
+ response = self.app.get(
+ route_path('edit_user_perms_summary_json', user_id=user['user_id']))
+ for repo in Repository.query().all():
+ response.mustcontain(repo.repo_name)
+
+ def test_audit_log_page(self):
+ user = self.log_user()
+ self.app.get(
+ route_path('edit_user_audit_logs', user_id=user['user_id']))
diff --git a/rhodecode/apps/admin/tests/test_admin_users_ssh_keys.py b/rhodecode/apps/admin/tests/test_admin_users_ssh_keys.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/admin/tests/test_admin_users_ssh_keys.py
@@ -0,0 +1,173 @@
+# -*- 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.model.db import User, UserSshKeys
+
+from rhodecode.tests import TestController, assert_session_flash
+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 = {
+ 'edit_user_ssh_keys':
+ ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys',
+ 'edit_user_ssh_keys_generate_keypair':
+ ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/generate',
+ 'edit_user_ssh_keys_add':
+ ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/new',
+ 'edit_user_ssh_keys_delete':
+ ADMIN_PREFIX + '/users/{user_id}/edit/ssh_keys/delete',
+
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
+
+
+class TestAdminUsersSshKeysView(TestController):
+ INVALID_KEY = """\
+ ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vevJsuZds1iNU5
+ LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSykfR1D1TdluyIpQLrwgH5kb
+ n8FkVI8zBMCKakxowvN67B0R7b1BT4PPzW2JlOXei/m9W12ZY484VTow6/B+kf2Q8
+ cP8tmCJmKWZma5Em7OTUhvjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6
+ jvdphZTc30I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zP
+ qPFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL
+ your_email@example.com
+ """
+ VALID_KEY = 'ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABAQDk+77sjDzVeB6vev' \
+ 'JsuZds1iNU5LANOa5CU5G/9JYIA6RYsWWMO7mbsR82IUckdqOHmxSy' \
+ 'kfR1D1TdluyIpQLrwgH5kbn8FkVI8zBMCKakxowvN67B0R7b1BT4PP' \
+ 'zW2JlOXei/m9W12ZY484VTow6/B+kf2Q8cP8tmCJmKWZma5Em7OTUh' \
+ 'vjyQVNz3v7HfeY5Hq0Ci4ECJ59hepFDabJvtAXg9XrI6jvdphZTc30' \
+ 'I4fG8+hBHzpeFxUGvSGNtXPUbwaAY8j/oHYrTpMgkj6pUEFsiKfC5zPq' \
+ 'PFR5HyKTCHW0nFUJnZsbyFT5hMiF/hZkJc9A0ZbdSvJwCRQ/g3bmdL ' \
+ 'your_email@example.com'
+
+ def test_ssh_keys_default_user(self):
+ self.log_user()
+ user = User.get_default_user()
+ self.app.get(
+ route_path('edit_user_ssh_keys', user_id=user.user_id),
+ status=302)
+
+ def test_add_ssh_key_error(self, user_util):
+ self.log_user()
+ user = user_util.create_user()
+ user_id = user.user_id
+
+ key_data = self.INVALID_KEY
+
+ desc = 'MY SSH KEY'
+ response = self.app.post(
+ route_path('edit_user_ssh_keys_add', user_id=user_id),
+ {'description': desc, 'key_data': key_data,
+ 'csrf_token': self.csrf_token})
+ assert_session_flash(response, 'An error occurred during ssh '
+ 'key saving: Unable to decode the key')
+
+ def test_ssh_key_duplicate(self, user_util):
+ self.log_user()
+ user = user_util.create_user()
+ user_id = user.user_id
+
+ key_data = self.VALID_KEY
+
+ desc = 'MY SSH KEY'
+ response = self.app.post(
+ route_path('edit_user_ssh_keys_add', user_id=user_id),
+ {'description': desc, 'key_data': key_data,
+ 'csrf_token': self.csrf_token})
+ assert_session_flash(response, 'Ssh Key successfully created')
+ response.follow() # flush session flash
+
+ # add the same key AGAIN
+ desc = 'MY SSH KEY'
+ response = self.app.post(
+ route_path('edit_user_ssh_keys_add', user_id=user_id),
+ {'description': desc, 'key_data': key_data,
+ 'csrf_token': self.csrf_token})
+ assert_session_flash(response, 'An error occurred during ssh key '
+ 'saving: Such key already exists, '
+ 'please use a different one')
+
+ def test_add_ssh_key(self, user_util):
+ self.log_user()
+ user = user_util.create_user()
+ user_id = user.user_id
+
+ key_data = self.VALID_KEY
+
+ desc = 'MY SSH KEY'
+ response = self.app.post(
+ route_path('edit_user_ssh_keys_add', user_id=user_id),
+ {'description': desc, 'key_data': key_data,
+ 'csrf_token': self.csrf_token})
+ assert_session_flash(response, 'Ssh Key successfully created')
+
+ response = response.follow()
+ response.mustcontain(desc)
+
+ def test_delete_ssh_key(self, user_util):
+ self.log_user()
+ user = user_util.create_user()
+ user_id = user.user_id
+
+ key_data = self.VALID_KEY
+
+ desc = 'MY SSH KEY'
+ response = self.app.post(
+ route_path('edit_user_ssh_keys_add', user_id=user_id),
+ {'description': desc, 'key_data': key_data,
+ 'csrf_token': self.csrf_token})
+ assert_session_flash(response, 'Ssh Key successfully created')
+ response = response.follow() # flush the Session flash
+
+ # now delete our key
+ keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
+ assert 1 == len(keys)
+
+ response = self.app.post(
+ route_path('edit_user_ssh_keys_delete', user_id=user_id),
+ {'del_ssh_key': keys[0].ssh_key_id,
+ 'csrf_token': self.csrf_token})
+
+ assert_session_flash(response, 'Ssh key successfully deleted')
+ keys = UserSshKeys.query().filter(UserSshKeys.user_id == user_id).all()
+ assert 0 == len(keys)
+
+ def test_generate_keypair(self, user_util):
+ self.log_user()
+ user = user_util.create_user()
+ user_id = user.user_id
+
+ response = self.app.get(
+ route_path('edit_user_ssh_keys_generate_keypair', user_id=user_id))
+
+ response.mustcontain('Private key')
+ response.mustcontain('Public key')
+ response.mustcontain('-----BEGIN RSA PRIVATE KEY-----')
diff --git a/rhodecode/apps/admin/views/audit_logs.py b/rhodecode/apps/admin/views/audit_logs.py
--- a/rhodecode/apps/admin/views/audit_logs.py
+++ b/rhodecode/apps/admin/views/audit_logs.py
@@ -20,11 +20,11 @@
import logging
+from pyramid.httpexceptions import HTTPNotFound
from pyramid.view import view_config
-from sqlalchemy.orm import joinedload
from rhodecode.apps._base import BaseAppView
-from rhodecode.model.db import UserLog
+from rhodecode.model.db import joinedload, UserLog
from rhodecode.lib.user_log_filter import user_log_filter
from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator
from rhodecode.lib.utils2 import safe_int
@@ -71,3 +71,21 @@ class AdminAuditLogsView(BaseAppView):
c.audit_logs = Page(users_log, page=p, items_per_page=10,
url=url_generator)
return self._get_template_context(c)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='admin_audit_log_entry', request_method='GET',
+ renderer='rhodecode:templates/admin/admin_audit_log_entry.mako')
+ def admin_audit_log_entry(self):
+ c = self.load_default_context()
+ audit_log_id = self.request.matchdict['audit_log_id']
+
+ c.audit_log_entry = UserLog.query()\
+ .options(joinedload(UserLog.user))\
+ .options(joinedload(UserLog.repository))\
+ .filter(UserLog.user_log_id == audit_log_id).scalar()
+ if not c.audit_log_entry:
+ raise HTTPNotFound()
+
+ return self._get_template_context(c)
diff --git a/rhodecode/apps/admin/views/defaults.py b/rhodecode/apps/admin/views/defaults.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/admin/views/defaults.py
@@ -0,0 +1,111 @@
+# -*- 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
+
+import formencode
+import formencode.htmlfill
+
+from pyramid.view import view_config
+from pyramid.httpexceptions import HTTPFound
+from pyramid.renderers import render
+from pyramid.response import Response
+
+from rhodecode.apps._base import BaseAppView
+from rhodecode.lib.auth import (
+ LoginRequired, HasPermissionAllDecorator, CSRFRequired)
+from rhodecode.lib import helpers as h
+from rhodecode.model.forms import DefaultsForm
+from rhodecode.model.meta import Session
+from rhodecode import BACKENDS
+from rhodecode.model.settings import SettingsModel
+
+log = logging.getLogger(__name__)
+
+
+class AdminDefaultSettingsView(BaseAppView):
+ def load_default_context(self):
+ c = self._get_local_tmpl_context()
+
+ self._register_global_c(c)
+ return c
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='admin_defaults_repositories', request_method='GET',
+ renderer='rhodecode:templates/admin/defaults/defaults.mako')
+ def defaults_repository_show(self):
+ c = self.load_default_context()
+ c.backends = BACKENDS.keys()
+ c.active = 'repositories'
+ defaults = SettingsModel().get_default_repo_settings()
+
+ data = render(
+ 'rhodecode:templates/admin/defaults/defaults.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=defaults,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='admin_defaults_repositories_update', request_method='POST',
+ renderer='rhodecode:templates/admin/defaults/defaults.mako')
+ def defaults_repository_update(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+ c.active = 'repositories'
+ form = DefaultsForm()()
+
+ try:
+ form_result = form.to_python(dict(self.request.POST))
+ for k, v in form_result.iteritems():
+ setting = SettingsModel().create_or_update_setting(k, v)
+ Session().add(setting)
+ Session().commit()
+ h.flash(_('Default settings updated successfully'),
+ category='success')
+
+ except formencode.Invalid as errors:
+ data = render(
+ 'rhodecode:templates/admin/defaults/defaults.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=errors.value,
+ errors=errors.error_dict or {},
+ prefix_error=False,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+ except Exception:
+ log.exception('Exception in update action')
+ h.flash(_('Error occurred during update of default values'),
+ category='error')
+
+ raise HTTPFound(h.route_path('admin_defaults_repositories'))
diff --git a/rhodecode/apps/admin/views/main_views.py b/rhodecode/apps/admin/views/main_views.py
--- a/rhodecode/apps/admin/views/main_views.py
+++ b/rhodecode/apps/admin/views/main_views.py
@@ -20,7 +20,6 @@
import logging
-
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
@@ -54,8 +53,10 @@ class AdminMainView(BaseAppView):
: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)
+ pull_request = PullRequest.get_or_404(
+ self.request.matchdict['pull_request_id'])
+ pull_request_id = pull_request.pull_request_id
+
repo_name = pull_request.target_repo.repo_name
raise HTTPFound(
diff --git a/rhodecode/apps/admin/views/open_source_licenses.py b/rhodecode/apps/admin/views/open_source_licenses.py
--- a/rhodecode/apps/admin/views/open_source_licenses.py
+++ b/rhodecode/apps/admin/views/open_source_licenses.py
@@ -21,7 +21,6 @@
import collections
import logging
-
from pyramid.view import view_config
from rhodecode.apps._base import BaseAppView
@@ -48,7 +47,7 @@ class OpenSourceLicensesAdminSettingsVie
c = self.load_default_context()
c.active = 'open_source'
c.navlist = navigation_list(self.request)
- c.opensource_licenses = collections.OrderedDict(
- sorted(read_opensource_licenses().items(), key=lambda t: t[0]))
+ items = sorted(read_opensource_licenses().items(), key=lambda t: t[0])
+ c.opensource_licenses = collections.OrderedDict(items)
return self._get_template_context(c)
diff --git a/rhodecode/apps/admin/views/permissions.py b/rhodecode/apps/admin/views/permissions.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/admin/views/permissions.py
@@ -0,0 +1,482 @@
+# -*- 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 re
+import logging
+import formencode
+import formencode.htmlfill
+import datetime
+from pyramid.interfaces import IRoutesMapper
+
+from pyramid.view import view_config
+from pyramid.httpexceptions import HTTPFound
+from pyramid.renderers import render
+from pyramid.response import Response
+
+from rhodecode.apps._base import BaseAppView, DataGridAppView
+from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
+from rhodecode.events import trigger
+
+from rhodecode.lib import helpers as h
+from rhodecode.lib.auth import (
+ LoginRequired, HasPermissionAllDecorator, CSRFRequired)
+from rhodecode.lib.utils2 import aslist, safe_unicode
+from rhodecode.model.db import (
+ or_, coalesce, User, UserIpMap, UserSshKeys)
+from rhodecode.model.forms import (
+ ApplicationPermissionsForm, ObjectPermissionsForm, UserPermissionsForm)
+from rhodecode.model.meta import Session
+from rhodecode.model.permission import PermissionModel
+from rhodecode.model.settings import SettingsModel
+
+
+log = logging.getLogger(__name__)
+
+
+class AdminPermissionsView(BaseAppView, DataGridAppView):
+ def load_default_context(self):
+ c = self._get_local_tmpl_context()
+
+ self._register_global_c(c)
+ PermissionModel().set_global_permission_choices(
+ c, gettext_translator=self.request.translate)
+ return c
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='admin_permissions_application', request_method='GET',
+ renderer='rhodecode:templates/admin/permissions/permissions.mako')
+ def permissions_application(self):
+ c = self.load_default_context()
+ c.active = 'application'
+
+ c.user = User.get_default_user(refresh=True)
+
+ app_settings = SettingsModel().get_all_settings()
+ defaults = {
+ 'anonymous': c.user.active,
+ 'default_register_message': app_settings.get(
+ 'rhodecode_register_message')
+ }
+ defaults.update(c.user.get_default_perms())
+
+ data = render('rhodecode:templates/admin/permissions/permissions.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=defaults,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='admin_permissions_application_update', request_method='POST',
+ renderer='rhodecode:templates/admin/permissions/permissions.mako')
+ def permissions_application_update(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+ c.active = 'application'
+
+ _form = ApplicationPermissionsForm(
+ [x[0] for x in c.register_choices],
+ [x[0] for x in c.password_reset_choices],
+ [x[0] for x in c.extern_activate_choices])()
+
+ try:
+ form_result = _form.to_python(dict(self.request.POST))
+ form_result.update({'perm_user_name': User.DEFAULT_USER})
+ PermissionModel().update_application_permissions(form_result)
+
+ settings = [
+ ('register_message', 'default_register_message'),
+ ]
+ for setting, form_key in settings:
+ sett = SettingsModel().create_or_update_setting(
+ setting, form_result[form_key])
+ Session().add(sett)
+
+ Session().commit()
+ h.flash(_('Application permissions updated successfully'),
+ category='success')
+
+ except formencode.Invalid as errors:
+ defaults = errors.value
+
+ data = render(
+ 'rhodecode:templates/admin/permissions/permissions.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=defaults,
+ errors=errors.error_dict or {},
+ prefix_error=False,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+
+ except Exception:
+ log.exception("Exception during update of permissions")
+ h.flash(_('Error occurred during update of permissions'),
+ category='error')
+
+ raise HTTPFound(h.route_path('admin_permissions_application'))
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='admin_permissions_object', request_method='GET',
+ renderer='rhodecode:templates/admin/permissions/permissions.mako')
+ def permissions_objects(self):
+ c = self.load_default_context()
+ c.active = 'objects'
+
+ c.user = User.get_default_user(refresh=True)
+ defaults = {}
+ defaults.update(c.user.get_default_perms())
+
+ data = render(
+ 'rhodecode:templates/admin/permissions/permissions.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=defaults,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='admin_permissions_object_update', request_method='POST',
+ renderer='rhodecode:templates/admin/permissions/permissions.mako')
+ def permissions_objects_update(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+ c.active = 'objects'
+
+ _form = ObjectPermissionsForm(
+ [x[0] for x in c.repo_perms_choices],
+ [x[0] for x in c.group_perms_choices],
+ [x[0] for x in c.user_group_perms_choices])()
+
+ try:
+ form_result = _form.to_python(dict(self.request.POST))
+ form_result.update({'perm_user_name': User.DEFAULT_USER})
+ PermissionModel().update_object_permissions(form_result)
+
+ Session().commit()
+ h.flash(_('Object permissions updated successfully'),
+ category='success')
+
+ except formencode.Invalid as errors:
+ defaults = errors.value
+
+ data = render(
+ 'rhodecode:templates/admin/permissions/permissions.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=defaults,
+ errors=errors.error_dict or {},
+ prefix_error=False,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+ except Exception:
+ log.exception("Exception during update of permissions")
+ h.flash(_('Error occurred during update of permissions'),
+ category='error')
+
+ raise HTTPFound(h.route_path('admin_permissions_object'))
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='admin_permissions_global', request_method='GET',
+ renderer='rhodecode:templates/admin/permissions/permissions.mako')
+ def permissions_global(self):
+ c = self.load_default_context()
+ c.active = 'global'
+
+ c.user = User.get_default_user(refresh=True)
+ defaults = {}
+ defaults.update(c.user.get_default_perms())
+
+ data = render(
+ 'rhodecode:templates/admin/permissions/permissions.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=defaults,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='admin_permissions_global_update', request_method='POST',
+ renderer='rhodecode:templates/admin/permissions/permissions.mako')
+ def permissions_global_update(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+ c.active = 'global'
+
+ _form = UserPermissionsForm(
+ [x[0] for x in c.repo_create_choices],
+ [x[0] for x in c.repo_create_on_write_choices],
+ [x[0] for x in c.repo_group_create_choices],
+ [x[0] for x in c.user_group_create_choices],
+ [x[0] for x in c.fork_choices],
+ [x[0] for x in c.inherit_default_permission_choices])()
+
+ try:
+ form_result = _form.to_python(dict(self.request.POST))
+ form_result.update({'perm_user_name': User.DEFAULT_USER})
+ PermissionModel().update_user_permissions(form_result)
+
+ Session().commit()
+ h.flash(_('Global permissions updated successfully'),
+ category='success')
+
+ except formencode.Invalid as errors:
+ defaults = errors.value
+
+ data = render(
+ 'rhodecode:templates/admin/permissions/permissions.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=defaults,
+ errors=errors.error_dict or {},
+ prefix_error=False,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+ except Exception:
+ log.exception("Exception during update of permissions")
+ h.flash(_('Error occurred during update of permissions'),
+ category='error')
+
+ raise HTTPFound(h.route_path('admin_permissions_global'))
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='admin_permissions_ips', request_method='GET',
+ renderer='rhodecode:templates/admin/permissions/permissions.mako')
+ def permissions_ips(self):
+ c = self.load_default_context()
+ c.active = 'ips'
+
+ c.user = User.get_default_user(refresh=True)
+ c.user_ip_map = (
+ UserIpMap.query().filter(UserIpMap.user == c.user).all())
+
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='admin_permissions_overview', request_method='GET',
+ renderer='rhodecode:templates/admin/permissions/permissions.mako')
+ def permissions_overview(self):
+ c = self.load_default_context()
+ c.active = 'perms'
+
+ c.user = User.get_default_user(refresh=True)
+ c.perm_user = c.user.AuthUser()
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='admin_permissions_auth_token_access', request_method='GET',
+ renderer='rhodecode:templates/admin/permissions/permissions.mako')
+ def auth_token_access(self):
+ from rhodecode import CONFIG
+
+ c = self.load_default_context()
+ c.active = 'auth_token_access'
+
+ c.user = User.get_default_user(refresh=True)
+ c.perm_user = c.user.AuthUser()
+
+ mapper = self.request.registry.queryUtility(IRoutesMapper)
+ c.view_data = []
+
+ _argument_prog = re.compile('\{(.*?)\}|:\((.*)\)')
+ introspector = self.request.registry.introspector
+
+ view_intr = {}
+ for view_data in introspector.get_category('views'):
+ intr = view_data['introspectable']
+
+ if 'route_name' in intr and intr['attr']:
+ view_intr[intr['route_name']] = '{}:{}'.format(
+ str(intr['derived_callable'].func_name), intr['attr']
+ )
+
+ c.whitelist_key = 'api_access_controllers_whitelist'
+ c.whitelist_file = CONFIG.get('__file__')
+ whitelist_views = aslist(
+ CONFIG.get(c.whitelist_key), sep=',')
+
+ for route_info in mapper.get_routes():
+ if not route_info.name.startswith('__'):
+ routepath = route_info.pattern
+
+ def replace(matchobj):
+ if matchobj.group(1):
+ return "{%s}" % matchobj.group(1).split(':')[0]
+ else:
+ return "{%s}" % matchobj.group(2)
+
+ routepath = _argument_prog.sub(replace, routepath)
+
+ if not routepath.startswith('/'):
+ routepath = '/' + routepath
+
+ view_fqn = view_intr.get(route_info.name, 'NOT AVAILABLE')
+ active = view_fqn in whitelist_views
+ c.view_data.append((route_info.name, view_fqn, routepath, active))
+
+ c.whitelist_views = whitelist_views
+ return self._get_template_context(c)
+
+ def ssh_enabled(self):
+ return self.request.registry.settings.get(
+ 'ssh.generate_authorized_keyfile')
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='admin_permissions_ssh_keys', request_method='GET',
+ renderer='rhodecode:templates/admin/permissions/permissions.mako')
+ def ssh_keys(self):
+ c = self.load_default_context()
+ c.active = 'ssh_keys'
+ c.ssh_enabled = self.ssh_enabled()
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='admin_permissions_ssh_keys_data', request_method='GET',
+ renderer='json_ext', xhr=True)
+ def ssh_keys_data(self):
+ _ = self.request.translate
+ column_map = {
+ 'fingerprint': 'ssh_key_fingerprint',
+ 'username': User.username
+ }
+ draw, start, limit = self._extract_chunk(self.request)
+ search_q, order_by, order_dir = self._extract_ordering(
+ self.request, column_map=column_map)
+
+ ssh_keys_data_total_count = UserSshKeys.query()\
+ .count()
+
+ # json generate
+ base_q = UserSshKeys.query().join(UserSshKeys.user)
+
+ if search_q:
+ like_expression = u'%{}%'.format(safe_unicode(search_q))
+ base_q = base_q.filter(or_(
+ User.username.ilike(like_expression),
+ UserSshKeys.ssh_key_fingerprint.ilike(like_expression),
+ ))
+
+ users_data_total_filtered_count = base_q.count()
+
+ sort_col = self._get_order_col(order_by, UserSshKeys)
+ if sort_col:
+ if order_dir == 'asc':
+ # handle null values properly to order by NULL last
+ if order_by in ['created_on']:
+ sort_col = coalesce(sort_col, datetime.date.max)
+ sort_col = sort_col.asc()
+ else:
+ # handle null values properly to order by NULL last
+ if order_by in ['created_on']:
+ sort_col = coalesce(sort_col, datetime.date.min)
+ sort_col = sort_col.desc()
+
+ base_q = base_q.order_by(sort_col)
+ base_q = base_q.offset(start).limit(limit)
+
+ ssh_keys = base_q.all()
+
+ ssh_keys_data = []
+ for ssh_key in ssh_keys:
+ ssh_keys_data.append({
+ "username": h.gravatar_with_user(self.request, ssh_key.user.username),
+ "fingerprint": ssh_key.ssh_key_fingerprint,
+ "description": ssh_key.description,
+ "created_on": h.format_date(ssh_key.created_on),
+ "accessed_on": h.format_date(ssh_key.accessed_on),
+ "action": h.link_to(
+ _('Edit'), h.route_path('edit_user_ssh_keys',
+ user_id=ssh_key.user.user_id))
+ })
+
+ data = ({
+ 'draw': draw,
+ 'data': ssh_keys_data,
+ 'recordsTotal': ssh_keys_data_total_count,
+ 'recordsFiltered': users_data_total_filtered_count,
+ })
+
+ return data
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='admin_permissions_ssh_keys_update', request_method='POST',
+ renderer='rhodecode:templates/admin/permissions/permissions.mako')
+ def ssh_keys_update(self):
+ _ = self.request.translate
+ self.load_default_context()
+
+ ssh_enabled = self.ssh_enabled()
+ key_file = self.request.registry.settings.get(
+ 'ssh.authorized_keys_file_path')
+ if ssh_enabled:
+ trigger(SshKeyFileChangeEvent(), self.request.registry)
+ h.flash(_('Updated SSH keys file: {}').format(key_file),
+ category='success')
+ else:
+ h.flash(_('SSH key support is disabled in .ini file'),
+ category='warning')
+
+ raise HTTPFound(h.route_path('admin_permissions_ssh_keys'))
diff --git a/rhodecode/apps/admin/views/process_management.py b/rhodecode/apps/admin/views/process_management.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/admin/views/process_management.py
@@ -0,0 +1,91 @@
+# -*- 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
+
+import psutil
+from pyramid.view import view_config
+
+from rhodecode.apps._base import BaseAppView
+from rhodecode.apps.admin.navigation import navigation_list
+from rhodecode.lib.auth import (
+ LoginRequired, HasPermissionAllDecorator, CSRFRequired)
+from rhodecode.lib.utils2 import safe_int
+
+log = logging.getLogger(__name__)
+
+
+class AdminProcessManagementView(BaseAppView):
+ def load_default_context(self):
+ c = self._get_local_tmpl_context()
+ self._register_global_c(c)
+ return c
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='admin_settings_process_management', request_method='GET',
+ renderer='rhodecode:templates/admin/settings/settings.mako')
+ def process_management(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+
+ c.active = 'process_management'
+ c.navlist = navigation_list(self.request)
+ c.gunicorn_processes = (
+ p for p in psutil.process_iter() if 'gunicorn' in p.name())
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='admin_settings_process_management_signal',
+ request_method='POST', renderer='json_ext')
+ def process_management_signal(self):
+ pids = self.request.json.get('pids', [])
+ result = []
+ def on_terminate(proc):
+ msg = "process `PID:{}` terminated with exit code {}".format(
+ proc.pid, proc.returncode)
+ result.append(msg)
+
+ procs = []
+ for pid in pids:
+ pid = safe_int(pid)
+ if pid:
+ try:
+ proc = psutil.Process(pid)
+ except psutil.NoSuchProcess:
+ continue
+
+ children = proc.children(recursive=True)
+ if children:
+ print('Wont kill Master Process')
+ else:
+ procs.append(proc)
+
+ for p in procs:
+ p.terminate()
+ gone, alive = psutil.wait_procs(procs, timeout=10, callback=on_terminate)
+ for p in alive:
+ p.kill()
+
+ return {'result': result}
diff --git a/rhodecode/apps/admin/views/repo_groups.py b/rhodecode/apps/admin/views/repo_groups.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/admin/views/repo_groups.py
@@ -0,0 +1,204 @@
+# -*- 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
+import formencode
+import formencode.htmlfill
+
+from pyramid.httpexceptions import HTTPFound, HTTPForbidden
+from pyramid.view import view_config
+from pyramid.renderers import render
+from pyramid.response import Response
+
+from rhodecode.apps._base import BaseAppView, DataGridAppView
+
+from rhodecode.lib.ext_json import json
+from rhodecode.lib.auth import (
+ LoginRequired, CSRFRequired, NotAnonymous,
+ HasPermissionAny, HasRepoGroupPermissionAny)
+from rhodecode.lib import helpers as h, audit_logger
+from rhodecode.lib.utils2 import safe_int, safe_unicode
+from rhodecode.model.forms import RepoGroupForm
+from rhodecode.model.repo_group import RepoGroupModel
+from rhodecode.model.scm import RepoGroupList
+from rhodecode.model.db import Session, RepoGroup
+
+log = logging.getLogger(__name__)
+
+
+class AdminRepoGroupsView(BaseAppView, DataGridAppView):
+
+ def load_default_context(self):
+ c = self._get_local_tmpl_context()
+ self._register_global_c(c)
+ return c
+
+ def _load_form_data(self, c):
+ allow_empty_group = False
+
+ if self._can_create_repo_group():
+ # we're global admin, we're ok and we can create TOP level groups
+ allow_empty_group = True
+
+ # override the choices for this form, we need to filter choices
+ # and display only those we have ADMIN right
+ groups_with_admin_rights = RepoGroupList(
+ RepoGroup.query().all(),
+ perm_set=['group.admin'])
+ c.repo_groups = RepoGroup.groups_choices(
+ groups=groups_with_admin_rights,
+ show_empty_group=allow_empty_group)
+
+ def _can_create_repo_group(self, parent_group_id=None):
+ is_admin = HasPermissionAny('hg.admin')('group create controller')
+ create_repo_group = HasPermissionAny(
+ 'hg.repogroup.create.true')('group create controller')
+ if is_admin or (create_repo_group and not parent_group_id):
+ # we're global admin, or we have global repo group create
+ # permission
+ # we're ok and we can create TOP level groups
+ return True
+ elif parent_group_id:
+ # we check the permission if we can write to parent group
+ group = RepoGroup.get(parent_group_id)
+ group_name = group.group_name if group else None
+ if HasRepoGroupPermissionAny('group.admin')(
+ group_name, 'check if user is an admin of group'):
+ # we're an admin of passed in group, we're ok.
+ return True
+ else:
+ return False
+ return False
+
+ @LoginRequired()
+ @NotAnonymous()
+ # perms check inside
+ @view_config(
+ route_name='repo_groups', request_method='GET',
+ renderer='rhodecode:templates/admin/repo_groups/repo_groups.mako')
+ def repo_group_list(self):
+ c = self.load_default_context()
+
+ repo_group_list = RepoGroup.get_all_repo_groups()
+ repo_group_list_acl = RepoGroupList(
+ repo_group_list, perm_set=['group.admin'])
+ repo_group_data = RepoGroupModel().get_repo_groups_as_dict(
+ repo_group_list=repo_group_list_acl, admin=True)
+ c.data = json.dumps(repo_group_data)
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @NotAnonymous()
+ # perm checks inside
+ @view_config(
+ route_name='repo_group_new', request_method='GET',
+ renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako')
+ def repo_group_new(self):
+ c = self.load_default_context()
+
+ # perm check for admin, create_group perm or admin of parent_group
+ parent_group_id = safe_int(self.request.GET.get('parent_group'))
+ if not self._can_create_repo_group(parent_group_id):
+ raise HTTPForbidden()
+
+ self._load_form_data(c)
+
+ defaults = {} # Future proof for default of repo group
+ data = render(
+ 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=defaults,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+
+ @LoginRequired()
+ @NotAnonymous()
+ @CSRFRequired()
+ # perm checks inside
+ @view_config(
+ route_name='repo_group_create', request_method='POST',
+ renderer='rhodecode:templates/admin/repo_groups/repo_group_add.mako')
+ def repo_group_create(self):
+ c = self.load_default_context()
+ _ = self.request.translate
+
+ parent_group_id = safe_int(self.request.POST.get('group_parent_id'))
+ can_create = self._can_create_repo_group(parent_group_id)
+
+ self._load_form_data(c)
+ # permissions for can create group based on parent_id are checked
+ # here in the Form
+ available_groups = map(lambda k: safe_unicode(k[0]), c.repo_groups)
+ repo_group_form = RepoGroupForm(available_groups=available_groups,
+ can_create_in_root=can_create)()
+
+ repo_group_name = self.request.POST.get('group_name')
+ try:
+ owner = self._rhodecode_user
+ form_result = repo_group_form.to_python(dict(self.request.POST))
+ repo_group = RepoGroupModel().create(
+ group_name=form_result['group_name_full'],
+ group_description=form_result['group_description'],
+ owner=owner.user_id,
+ copy_permissions=form_result['group_copy_permissions']
+ )
+ Session().flush()
+
+ repo_group_data = repo_group.get_api_data()
+ audit_logger.store_web(
+ 'repo_group.create', action_data={'data': repo_group_data},
+ user=self._rhodecode_user)
+
+ Session().commit()
+
+ _new_group_name = form_result['group_name_full']
+
+ repo_group_url = h.link_to(
+ _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')
+
+ except formencode.Invalid as errors:
+ data = render(
+ 'rhodecode:templates/admin/repo_groups/repo_group_add.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=errors.value,
+ errors=errors.error_dict or {},
+ prefix_error=False,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+ except Exception:
+ log.exception("Exception during creation of repository group")
+ h.flash(_('Error occurred during creation of repository group %s')
+ % repo_group_name, category='error')
+ raise HTTPFound(h.route_path('home'))
+
+ raise HTTPFound(
+ h.route_path('repo_group_home',
+ repo_group_name=form_result['group_name_full']))
diff --git a/rhodecode/apps/admin/views/repositories.py b/rhodecode/apps/admin/views/repositories.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/admin/views/repositories.py
@@ -0,0 +1,182 @@
+# -*- 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
+import formencode
+import formencode.htmlfill
+
+from pyramid.httpexceptions import HTTPFound, HTTPForbidden
+from pyramid.view import view_config
+from pyramid.renderers import render
+from pyramid.response import Response
+
+from rhodecode.apps._base import BaseAppView, DataGridAppView
+
+from rhodecode.lib.ext_json import json
+from rhodecode.lib.auth import (
+ LoginRequired, CSRFRequired, NotAnonymous,
+ HasPermissionAny, HasRepoGroupPermissionAny)
+from rhodecode.lib import helpers as h
+from rhodecode.lib.utils import repo_name_slug
+from rhodecode.lib.utils2 import safe_int, safe_unicode
+from rhodecode.model.forms import RepoForm
+from rhodecode.model.repo import RepoModel
+from rhodecode.model.scm import RepoList, RepoGroupList, ScmModel
+from rhodecode.model.settings import SettingsModel
+from rhodecode.model.db import Repository, RepoGroup
+
+log = logging.getLogger(__name__)
+
+
+class AdminReposView(BaseAppView, DataGridAppView):
+
+ def load_default_context(self):
+ c = self._get_local_tmpl_context()
+ self._register_global_c(c)
+ return c
+
+ def _load_form_data(self, c):
+ acl_groups = RepoGroupList(RepoGroup.query().all(),
+ perm_set=['group.write', 'group.admin'])
+ c.repo_groups = RepoGroup.groups_choices(groups=acl_groups)
+ c.repo_groups_choices = map(lambda k: safe_unicode(k[0]), c.repo_groups)
+ c.landing_revs_choices, c.landing_revs = \
+ ScmModel().get_repo_landing_revs()
+ c.personal_repo_group = self._rhodecode_user.personal_repo_group
+
+ @LoginRequired()
+ @NotAnonymous()
+ # perms check inside
+ @view_config(
+ route_name='repos', request_method='GET',
+ renderer='rhodecode:templates/admin/repos/repos.mako')
+ def repository_list(self):
+ c = self.load_default_context()
+
+ repo_list = Repository.get_all_repos()
+ c.repo_list = RepoList(repo_list, perm_set=['repository.admin'])
+ repos_data = RepoModel().get_repos_as_dict(
+ repo_list=c.repo_list, admin=True, super_user_actions=True)
+ # json used to render the grid
+ c.data = json.dumps(repos_data)
+
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @NotAnonymous()
+ # perms check inside
+ @view_config(
+ route_name='repo_new', request_method='GET',
+ renderer='rhodecode:templates/admin/repos/repo_add.mako')
+ def repository_new(self):
+ c = self.load_default_context()
+
+ new_repo = self.request.GET.get('repo', '')
+ parent_group = safe_int(self.request.GET.get('parent_group'))
+ _gr = RepoGroup.get(parent_group)
+
+ if not HasPermissionAny('hg.admin', 'hg.create.repository')():
+ # you're not super admin nor have global create permissions,
+ # but maybe you have at least write permission to a parent group ?
+
+ gr_name = _gr.group_name if _gr else None
+ # create repositories with write permission on group is set to true
+ create_on_write = HasPermissionAny('hg.create.write_on_repogroup.true')()
+ group_admin = HasRepoGroupPermissionAny('group.admin')(group_name=gr_name)
+ group_write = HasRepoGroupPermissionAny('group.write')(group_name=gr_name)
+ if not (group_admin or (group_write and create_on_write)):
+ raise HTTPForbidden()
+
+ self._load_form_data(c)
+ c.new_repo = repo_name_slug(new_repo)
+
+ # apply the defaults from defaults page
+ defaults = SettingsModel().get_default_repo_settings(strip_prefix=True)
+ # set checkbox to autochecked
+ defaults['repo_copy_permissions'] = True
+
+ parent_group_choice = '-1'
+ if not self._rhodecode_user.is_admin and self._rhodecode_user.personal_repo_group:
+ parent_group_choice = self._rhodecode_user.personal_repo_group
+
+ if parent_group and _gr:
+ if parent_group in [x[0] for x in c.repo_groups]:
+ parent_group_choice = safe_unicode(parent_group)
+
+ defaults.update({'repo_group': parent_group_choice})
+
+ data = render('rhodecode:templates/admin/repos/repo_add.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=defaults,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+
+ @LoginRequired()
+ @NotAnonymous()
+ @CSRFRequired()
+ # perms check inside
+ @view_config(
+ route_name='repo_create', request_method='POST',
+ renderer='rhodecode:templates/admin/repos/repos.mako')
+ def repository_create(self):
+ c = self.load_default_context()
+
+ form_result = {}
+ task_id = None
+ self._load_form_data(c)
+
+ try:
+ # CanWriteToGroup validators checks permissions of this POST
+ form_result = RepoForm(repo_groups=c.repo_groups_choices,
+ landing_revs=c.landing_revs_choices)()\
+ .to_python(dict(self.request.POST))
+
+ # create is done sometimes async on celery, db transaction
+ # management is handled there.
+ task = RepoModel().create(form_result, self._rhodecode_user.user_id)
+ from celery.result import BaseAsyncResult
+ if isinstance(task, BaseAsyncResult):
+ task_id = task.task_id
+ except formencode.Invalid as errors:
+ data = render('rhodecode:templates/admin/repos/repo_add.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=errors.value,
+ errors=errors.error_dict or {},
+ prefix_error=False,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+
+ except Exception as e:
+ msg = self._log_creation_exception(e, form_result.get('repo_name'))
+ h.flash(msg, category='error')
+ raise HTTPFound(h.route_path('home'))
+
+ raise HTTPFound(
+ h.route_path('repo_creating',
+ repo_name=form_result['repo_name_full'],
+ _query=dict(task_id=task_id)))
diff --git a/rhodecode/apps/admin/views/user_groups.py b/rhodecode/apps/admin/views/user_groups.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/admin/views/user_groups.py
@@ -0,0 +1,247 @@
+# -*- 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
+
+import formencode
+import formencode.htmlfill
+
+from pyramid.httpexceptions import HTTPFound
+from pyramid.view import view_config
+from pyramid.response import Response
+from pyramid.renderers import render
+
+from rhodecode.apps._base import BaseAppView, DataGridAppView
+from rhodecode.lib.auth import (
+ LoginRequired, NotAnonymous, CSRFRequired, HasPermissionAnyDecorator)
+from rhodecode.lib import helpers as h, audit_logger
+from rhodecode.lib.utils2 import safe_unicode
+
+from rhodecode.model.forms import UserGroupForm
+from rhodecode.model.permission import PermissionModel
+from rhodecode.model.scm import UserGroupList
+from rhodecode.model.db import (
+ or_, count, User, UserGroup, UserGroupMember)
+from rhodecode.model.meta import Session
+from rhodecode.model.user_group import UserGroupModel
+
+log = logging.getLogger(__name__)
+
+
+class AdminUserGroupsView(BaseAppView, DataGridAppView):
+
+ def load_default_context(self):
+ c = self._get_local_tmpl_context()
+
+ PermissionModel().set_global_permission_choices(
+ c, gettext_translator=self.request.translate)
+
+ self._register_global_c(c)
+ return c
+
+ # permission check in data loading of
+ # `user_groups_list_data` via UserGroupList
+ @LoginRequired()
+ @NotAnonymous()
+ @view_config(
+ route_name='user_groups', request_method='GET',
+ renderer='rhodecode:templates/admin/user_groups/user_groups.mako')
+ def user_groups_list(self):
+ c = self.load_default_context()
+ return self._get_template_context(c)
+
+ # permission check inside
+ @LoginRequired()
+ @NotAnonymous()
+ @view_config(
+ route_name='user_groups_data', request_method='GET',
+ renderer='json_ext', xhr=True)
+ def user_groups_list_data(self):
+ column_map = {
+ 'active': 'users_group_active',
+ 'description': 'user_group_description',
+ 'members': 'members_total',
+ 'owner': 'user_username',
+ 'sync': 'group_data'
+ }
+ draw, start, limit = self._extract_chunk(self.request)
+ search_q, order_by, order_dir = self._extract_ordering(
+ self.request, column_map=column_map)
+
+ _render = self.request.get_partial_renderer(
+ 'data_table/_dt_elements.mako')
+
+ def user_group_name(user_group_id, user_group_name):
+ return _render("user_group_name", user_group_id, user_group_name)
+
+ def user_group_actions(user_group_id, user_group_name):
+ return _render("user_group_actions", user_group_id, user_group_name)
+
+ def user_profile(username):
+ return _render('user_profile', username)
+
+ auth_user_group_list = UserGroupList(
+ UserGroup.query().all(), perm_set=['usergroup.admin'])
+
+ allowed_ids = [-1]
+ for user_group in auth_user_group_list:
+ allowed_ids.append(user_group.users_group_id)
+
+ user_groups_data_total_count = UserGroup.query()\
+ .filter(UserGroup.users_group_id.in_(allowed_ids))\
+ .count()
+
+ member_count = count(UserGroupMember.user_id)
+ base_q = Session.query(
+ UserGroup.users_group_name,
+ UserGroup.user_group_description,
+ UserGroup.users_group_active,
+ UserGroup.users_group_id,
+ UserGroup.group_data,
+ User,
+ member_count.label('member_count')
+ ) \
+ .filter(UserGroup.users_group_id.in_(allowed_ids)) \
+ .outerjoin(UserGroupMember) \
+ .join(User, User.user_id == UserGroup.user_id) \
+ .group_by(UserGroup, User)
+
+ if search_q:
+ like_expression = u'%{}%'.format(safe_unicode(search_q))
+ base_q = base_q.filter(or_(
+ UserGroup.users_group_name.ilike(like_expression),
+ ))
+
+ user_groups_data_total_filtered_count = base_q.count()
+
+ if order_by == 'members_total':
+ sort_col = member_count
+ elif order_by == 'user_username':
+ sort_col = User.username
+ else:
+ sort_col = getattr(UserGroup, order_by, None)
+
+ if isinstance(sort_col, count) or sort_col:
+ if order_dir == 'asc':
+ sort_col = sort_col.asc()
+ else:
+ sort_col = sort_col.desc()
+
+ base_q = base_q.order_by(sort_col)
+ base_q = base_q.offset(start).limit(limit)
+
+ # authenticated access to user groups
+ auth_user_group_list = base_q.all()
+
+ user_groups_data = []
+ for user_gr in auth_user_group_list:
+ user_groups_data.append({
+ "users_group_name": user_group_name(
+ user_gr.users_group_id, h.escape(user_gr.users_group_name)),
+ "name_raw": h.escape(user_gr.users_group_name),
+ "description": h.escape(user_gr.user_group_description),
+ "members": user_gr.member_count,
+ # NOTE(marcink): because of advanced query we
+ # need to load it like that
+ "sync": UserGroup._load_group_data(
+ user_gr.group_data).get('extern_type'),
+ "active": h.bool2icon(user_gr.users_group_active),
+ "owner": user_profile(user_gr.User.username),
+ "action": user_group_actions(
+ user_gr.users_group_id, user_gr.users_group_name)
+ })
+
+ data = ({
+ 'draw': draw,
+ 'data': user_groups_data,
+ 'recordsTotal': user_groups_data_total_count,
+ 'recordsFiltered': user_groups_data_total_filtered_count,
+ })
+
+ return data
+
+ @LoginRequired()
+ @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
+ @view_config(
+ route_name='user_groups_new', request_method='GET',
+ renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
+ def user_groups_new(self):
+ c = self.load_default_context()
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @HasPermissionAnyDecorator('hg.admin', 'hg.usergroup.create.true')
+ @CSRFRequired()
+ @view_config(
+ route_name='user_groups_create', request_method='POST',
+ renderer='rhodecode:templates/admin/user_groups/user_group_add.mako')
+ def user_groups_create(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+ users_group_form = UserGroupForm()()
+
+ user_group_name = self.request.POST.get('users_group_name')
+ try:
+ form_result = users_group_form.to_python(dict(self.request.POST))
+ user_group = UserGroupModel().create(
+ name=form_result['users_group_name'],
+ description=form_result['user_group_description'],
+ owner=self._rhodecode_user.user_id,
+ active=form_result['users_group_active'])
+ Session().flush()
+ creation_data = user_group.get_api_data()
+ user_group_name = form_result['users_group_name']
+
+ audit_logger.store_web(
+ 'user_group.create', action_data={'data': creation_data},
+ user=self._rhodecode_user)
+
+ user_group_link = h.link_to(
+ h.escape(user_group_name),
+ h.route_path(
+ 'edit_user_group', user_group_id=user_group.users_group_id))
+ h.flash(h.literal(_('Created user group %(user_group_link)s')
+ % {'user_group_link': user_group_link}),
+ category='success')
+ Session().commit()
+ user_group_id = user_group.users_group_id
+ except formencode.Invalid as errors:
+
+ data = render(
+ 'rhodecode:templates/admin/user_groups/user_group_add.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=errors.value,
+ errors=errors.error_dict or {},
+ prefix_error=False,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+
+ except Exception:
+ log.exception("Exception creating user group")
+ h.flash(_('Error occurred during creation of user group %s') \
+ % user_group_name, category='error')
+ raise HTTPFound(h.route_path('user_groups_new'))
+
+ raise HTTPFound(
+ h.route_path('edit_user_group', user_group_id=user_group_id))
diff --git a/rhodecode/apps/admin/views/users.py b/rhodecode/apps/admin/views/users.py
--- a/rhodecode/apps/admin/views/users.py
+++ b/rhodecode/apps/admin/views/users.py
@@ -21,50 +21,51 @@
import logging
import datetime
import formencode
+import formencode.htmlfill
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
-from sqlalchemy.sql.functions import coalesce
+from pyramid.renderers import render
+from pyramid.response import Response
-from rhodecode.apps._base import BaseAppView, DataGridAppView
+from rhodecode.apps._base import BaseAppView, DataGridAppView, UserAppView
+from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
+from rhodecode.authentication.plugins import auth_rhodecode
+from rhodecode.events import trigger
from rhodecode.lib import audit_logger
+from rhodecode.lib.exceptions import (
+ UserCreationError, UserOwnsReposException, UserOwnsRepoGroupsException,
+ UserOwnsUserGroupsException, DefaultUserException)
from rhodecode.lib.ext_json import json
from rhodecode.lib.auth import (
LoginRequired, HasPermissionAllDecorator, CSRFRequired)
from rhodecode.lib import helpers as h
-from rhodecode.lib.utils import PartialRenderer
-from rhodecode.lib.utils2 import safe_int, safe_unicode
+from rhodecode.lib.utils2 import safe_int, safe_unicode, AttributeDict
from rhodecode.model.auth_token import AuthTokenModel
+from rhodecode.model.forms import (
+ UserForm, UserIndividualPermissionsForm, UserPermissionsForm)
+from rhodecode.model.permission import PermissionModel
+from rhodecode.model.repo_group import RepoGroupModel
+from rhodecode.model.ssh_key import SshKeyModel
from rhodecode.model.user import UserModel
from rhodecode.model.user_group import UserGroupModel
-from rhodecode.model.db import User, or_, UserIpMap, UserEmailMap, UserApiKeys
+from rhodecode.model.db import (
+ or_, coalesce,IntegrityError, User, UserGroup, UserIpMap, UserEmailMap,
+ UserApiKeys, UserSshKeys, RepoGroup)
from rhodecode.model.meta import Session
log = logging.getLogger(__name__)
class AdminUsersView(BaseAppView, DataGridAppView):
- ALLOW_SCOPED_TOKENS = False
- """
- This view has alternative version inside EE, if modified please take a look
- in there as well.
- """
def load_default_context(self):
c = self._get_local_tmpl_context()
- c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
self._register_global_c(c)
return c
- def _redirect_for_default_user(self, username):
- _ = self.request.translate
- if username == User.DEFAULT_USER:
- h.flash(_("You can't edit this user"), category='warning')
- # TODO(marcink): redirect to 'users' admin panel once this
- # is a pyramid view
- raise HTTPFound('/')
-
+ @LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@view_config(
route_name='users', request_method='GET',
@@ -73,16 +74,23 @@ class AdminUsersView(BaseAppView, DataGr
c = self.load_default_context()
return self._get_template_context(c)
+ @LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@view_config(
# renderer defined below
route_name='users_data', request_method='GET',
renderer='json_ext', xhr=True)
def users_list_data(self):
+ column_map = {
+ 'first_name': 'name',
+ 'last_name': 'lastname',
+ }
draw, start, limit = self._extract_chunk(self.request)
- search_q, order_by, order_dir = self._extract_ordering(self.request)
+ search_q, order_by, order_dir = self._extract_ordering(
+ self.request, column_map=column_map)
- _render = PartialRenderer('data_table/_dt_elements.mako')
+ _render = self.request.get_partial_renderer(
+ 'data_table/_dt_elements.mako')
def user_actions(user_id, username):
return _render("user_actions", user_id, username)
@@ -126,7 +134,7 @@ class AdminUsersView(BaseAppView, DataGr
users_data = []
for user in users_list:
users_data.append({
- "username": h.gravatar_with_user(user.username),
+ "username": h.gravatar_with_user(self.request, user.username),
"email": user.email,
"first_name": user.first_name,
"last_name": user.last_name,
@@ -149,6 +157,529 @@ class AdminUsersView(BaseAppView, DataGr
return data
+ def _set_personal_repo_group_template_vars(self, c_obj):
+ DummyUser = AttributeDict({
+ 'username': '${username}',
+ 'user_id': '${user_id}',
+ })
+ c_obj.default_create_repo_group = RepoGroupModel() \
+ .get_default_create_personal_repo_group()
+ c_obj.personal_repo_group_name = RepoGroupModel() \
+ .get_personal_group_name(DummyUser)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='users_new', request_method='GET',
+ renderer='rhodecode:templates/admin/users/user_add.mako')
+ def users_new(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+ c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
+ self._set_personal_repo_group_template_vars(c)
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='users_create', request_method='POST',
+ renderer='rhodecode:templates/admin/users/user_add.mako')
+ def users_create(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+ c.default_extern_type = auth_rhodecode.RhodeCodeAuthPlugin.name
+ user_model = UserModel()
+ user_form = UserForm()()
+ try:
+ form_result = user_form.to_python(dict(self.request.POST))
+ user = user_model.create(form_result)
+ Session().flush()
+ creation_data = user.get_api_data()
+ username = form_result['username']
+
+ audit_logger.store_web(
+ 'user.create', action_data={'data': creation_data},
+ user=c.rhodecode_user)
+
+ user_link = h.link_to(
+ h.escape(username),
+ h.route_path('user_edit', user_id=user.user_id))
+ h.flash(h.literal(_('Created user %(user_link)s')
+ % {'user_link': user_link}), category='success')
+ Session().commit()
+ except formencode.Invalid as errors:
+ self._set_personal_repo_group_template_vars(c)
+ data = render(
+ 'rhodecode:templates/admin/users/user_add.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=errors.value,
+ errors=errors.error_dict or {},
+ prefix_error=False,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+ except UserCreationError as e:
+ h.flash(e, 'error')
+ except Exception:
+ log.exception("Exception creation of user")
+ h.flash(_('Error occurred during creation of user %s')
+ % self.request.POST.get('username'), category='error')
+ raise HTTPFound(h.route_path('users'))
+
+
+class UsersView(UserAppView):
+ ALLOW_SCOPED_TOKENS = False
+ """
+ This view has alternative version inside EE, if modified please take a look
+ in there as well.
+ """
+
+ def load_default_context(self):
+ c = self._get_local_tmpl_context()
+ c.allow_scoped_tokens = self.ALLOW_SCOPED_TOKENS
+ c.allowed_languages = [
+ ('en', 'English (en)'),
+ ('de', 'German (de)'),
+ ('fr', 'French (fr)'),
+ ('it', 'Italian (it)'),
+ ('ja', 'Japanese (ja)'),
+ ('pl', 'Polish (pl)'),
+ ('pt', 'Portuguese (pt)'),
+ ('ru', 'Russian (ru)'),
+ ('zh', 'Chinese (zh)'),
+ ]
+ req = self.request
+
+ c.available_permissions = req.registry.settings['available_permissions']
+ PermissionModel().set_global_permission_choices(
+ c, gettext_translator=req.translate)
+
+ self._register_global_c(c)
+ return c
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='user_update', request_method='POST',
+ renderer='rhodecode:templates/admin/users/user_edit.mako')
+ def user_update(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+
+ user_id = self.db_user_id
+ c.user = self.db_user
+
+ c.active = 'profile'
+ c.extern_type = c.user.extern_type
+ c.extern_name = c.user.extern_name
+ c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
+ available_languages = [x[0] for x in c.allowed_languages]
+ _form = UserForm(edit=True, available_languages=available_languages,
+ old_data={'user_id': user_id,
+ 'email': c.user.email})()
+ form_result = {}
+ old_values = c.user.get_api_data()
+ try:
+ form_result = _form.to_python(dict(self.request.POST))
+ skip_attrs = ['extern_type', 'extern_name']
+ # TODO: plugin should define if username can be updated
+ if c.extern_type != "rhodecode":
+ # forbid updating username for external accounts
+ skip_attrs.append('username')
+
+ UserModel().update_user(
+ user_id, skip_attrs=skip_attrs, **form_result)
+
+ audit_logger.store_web(
+ 'user.edit', action_data={'old_data': old_values},
+ user=c.rhodecode_user)
+
+ Session().commit()
+ h.flash(_('User updated successfully'), category='success')
+ except formencode.Invalid as errors:
+ data = render(
+ 'rhodecode:templates/admin/users/user_edit.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=errors.value,
+ errors=errors.error_dict or {},
+ prefix_error=False,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+ except UserCreationError as e:
+ h.flash(e, 'error')
+ except Exception:
+ log.exception("Exception updating user")
+ h.flash(_('Error occurred during update of user %s')
+ % form_result.get('username'), category='error')
+ raise HTTPFound(h.route_path('user_edit', user_id=user_id))
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='user_delete', request_method='POST',
+ renderer='rhodecode:templates/admin/users/user_edit.mako')
+ def user_delete(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+ c.user = self.db_user
+
+ _repos = c.user.repositories
+ _repo_groups = c.user.repository_groups
+ _user_groups = c.user.user_groups
+
+ handle_repos = None
+ handle_repo_groups = None
+ handle_user_groups = None
+ # dummy call for flash of handle
+ set_handle_flash_repos = lambda: None
+ set_handle_flash_repo_groups = lambda: None
+ set_handle_flash_user_groups = lambda: None
+
+ if _repos and self.request.POST.get('user_repos'):
+ do = self.request.POST['user_repos']
+ if do == 'detach':
+ handle_repos = 'detach'
+ set_handle_flash_repos = lambda: h.flash(
+ _('Detached %s repositories') % len(_repos),
+ category='success')
+ elif do == 'delete':
+ handle_repos = 'delete'
+ set_handle_flash_repos = lambda: h.flash(
+ _('Deleted %s repositories') % len(_repos),
+ category='success')
+
+ if _repo_groups and self.request.POST.get('user_repo_groups'):
+ do = self.request.POST['user_repo_groups']
+ if do == 'detach':
+ handle_repo_groups = 'detach'
+ set_handle_flash_repo_groups = lambda: h.flash(
+ _('Detached %s repository groups') % len(_repo_groups),
+ category='success')
+ elif do == 'delete':
+ handle_repo_groups = 'delete'
+ set_handle_flash_repo_groups = lambda: h.flash(
+ _('Deleted %s repository groups') % len(_repo_groups),
+ category='success')
+
+ if _user_groups and self.request.POST.get('user_user_groups'):
+ do = self.request.POST['user_user_groups']
+ if do == 'detach':
+ handle_user_groups = 'detach'
+ set_handle_flash_user_groups = lambda: h.flash(
+ _('Detached %s user groups') % len(_user_groups),
+ category='success')
+ elif do == 'delete':
+ handle_user_groups = 'delete'
+ set_handle_flash_user_groups = lambda: h.flash(
+ _('Deleted %s user groups') % len(_user_groups),
+ category='success')
+
+ old_values = c.user.get_api_data()
+ try:
+ UserModel().delete(c.user, handle_repos=handle_repos,
+ handle_repo_groups=handle_repo_groups,
+ handle_user_groups=handle_user_groups)
+
+ audit_logger.store_web(
+ 'user.delete', action_data={'old_data': old_values},
+ user=c.rhodecode_user)
+
+ Session().commit()
+ set_handle_flash_repos()
+ set_handle_flash_repo_groups()
+ set_handle_flash_user_groups()
+ h.flash(_('Successfully deleted user'), category='success')
+ except (UserOwnsReposException, UserOwnsRepoGroupsException,
+ UserOwnsUserGroupsException, DefaultUserException) as e:
+ h.flash(e, category='warning')
+ except Exception:
+ log.exception("Exception during deletion of user")
+ h.flash(_('An error occurred during deletion of user'),
+ category='error')
+ raise HTTPFound(h.route_path('users'))
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='user_edit', request_method='GET',
+ renderer='rhodecode:templates/admin/users/user_edit.mako')
+ def user_edit(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+ c.user = self.db_user
+
+ c.active = 'profile'
+ c.extern_type = c.user.extern_type
+ c.extern_name = c.user.extern_name
+ c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
+
+ defaults = c.user.get_dict()
+ defaults.update({'language': c.user.user_data.get('language')})
+
+ data = render(
+ 'rhodecode:templates/admin/users/user_edit.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=defaults,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='user_edit_advanced', request_method='GET',
+ renderer='rhodecode:templates/admin/users/user_edit.mako')
+ def user_edit_advanced(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+
+ user_id = self.db_user_id
+ c.user = self.db_user
+
+ c.active = 'advanced'
+ c.personal_repo_group = RepoGroup.get_user_personal_repo_group(user_id)
+ c.personal_repo_group_name = RepoGroupModel()\
+ .get_personal_group_name(c.user)
+
+ c.user_to_review_rules = sorted(
+ (x.user for x in c.user.user_review_rules),
+ key=lambda u: u.username.lower())
+
+ c.first_admin = User.get_first_super_admin()
+ defaults = c.user.get_dict()
+
+ # Interim workaround if the user participated on any pull requests as a
+ # reviewer.
+ has_review = len(c.user.reviewer_pull_requests)
+ c.can_delete_user = not has_review
+ c.can_delete_user_message = ''
+ inactive_link = h.link_to(
+ 'inactive', h.route_path('user_edit', user_id=user_id, _anchor='active'))
+ if has_review == 1:
+ c.can_delete_user_message = h.literal(_(
+ 'The user participates as reviewer in {} pull request and '
+ 'cannot be deleted. \nYou can set the user to '
+ '"{}" instead of deleting it.').format(
+ has_review, inactive_link))
+ elif has_review:
+ c.can_delete_user_message = h.literal(_(
+ 'The user participates as reviewer in {} pull requests and '
+ 'cannot be deleted. \nYou can set the user to '
+ '"{}" instead of deleting it.').format(
+ has_review, inactive_link))
+
+ data = render(
+ 'rhodecode:templates/admin/users/user_edit.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=defaults,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='user_edit_global_perms', request_method='GET',
+ renderer='rhodecode:templates/admin/users/user_edit.mako')
+ def user_edit_global_perms(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+ c.user = self.db_user
+
+ c.active = 'global_perms'
+
+ c.default_user = User.get_default_user()
+ defaults = c.user.get_dict()
+ defaults.update(c.default_user.get_default_perms(suffix='_inherited'))
+ defaults.update(c.default_user.get_default_perms())
+ defaults.update(c.user.get_default_perms())
+
+ data = render(
+ 'rhodecode:templates/admin/users/user_edit.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=defaults,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='user_edit_global_perms_update', request_method='POST',
+ renderer='rhodecode:templates/admin/users/user_edit.mako')
+ def user_edit_global_perms_update(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+
+ user_id = self.db_user_id
+ c.user = self.db_user
+
+ c.active = 'global_perms'
+ try:
+ # first stage that verifies the checkbox
+ _form = UserIndividualPermissionsForm()
+ form_result = _form.to_python(dict(self.request.POST))
+ inherit_perms = form_result['inherit_default_permissions']
+ c.user.inherit_default_permissions = inherit_perms
+ Session().add(c.user)
+
+ if not inherit_perms:
+ # only update the individual ones if we un check the flag
+ _form = UserPermissionsForm(
+ [x[0] for x in c.repo_create_choices],
+ [x[0] for x in c.repo_create_on_write_choices],
+ [x[0] for x in c.repo_group_create_choices],
+ [x[0] for x in c.user_group_create_choices],
+ [x[0] for x in c.fork_choices],
+ [x[0] for x in c.inherit_default_permission_choices])()
+
+ form_result = _form.to_python(dict(self.request.POST))
+ form_result.update({'perm_user_id': c.user.user_id})
+
+ PermissionModel().update_user_permissions(form_result)
+
+ # TODO(marcink): implement global permissions
+ # audit_log.store_web('user.edit.permissions')
+
+ Session().commit()
+ h.flash(_('User global permissions updated successfully'),
+ category='success')
+
+ except formencode.Invalid as errors:
+ data = render(
+ 'rhodecode:templates/admin/users/user_edit.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=errors.value,
+ errors=errors.error_dict or {},
+ prefix_error=False,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+ except Exception:
+ log.exception("Exception during permissions saving")
+ h.flash(_('An error occurred during permissions saving'),
+ category='error')
+ raise HTTPFound(h.route_path('user_edit_global_perms', user_id=user_id))
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='user_force_password_reset', request_method='POST',
+ renderer='rhodecode:templates/admin/users/user_edit.mako')
+ def user_force_password_reset(self):
+ """
+ toggle reset password flag for this user
+ """
+ _ = self.request.translate
+ c = self.load_default_context()
+
+ user_id = self.db_user_id
+ c.user = self.db_user
+
+ try:
+ old_value = c.user.user_data.get('force_password_change')
+ c.user.update_userdata(force_password_change=not old_value)
+
+ if old_value:
+ msg = _('Force password change disabled for user')
+ audit_logger.store_web(
+ 'user.edit.password_reset.disabled',
+ user=c.rhodecode_user)
+ else:
+ msg = _('Force password change enabled for user')
+ audit_logger.store_web(
+ 'user.edit.password_reset.enabled',
+ user=c.rhodecode_user)
+
+ Session().commit()
+ h.flash(msg, category='success')
+ except Exception:
+ log.exception("Exception during password reset for user")
+ h.flash(_('An error occurred during password reset for user'),
+ category='error')
+
+ raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='user_create_personal_repo_group', request_method='POST',
+ renderer='rhodecode:templates/admin/users/user_edit.mako')
+ def user_create_personal_repo_group(self):
+ """
+ Create personal repository group for this user
+ """
+ from rhodecode.model.repo_group import RepoGroupModel
+
+ _ = self.request.translate
+ c = self.load_default_context()
+
+ user_id = self.db_user_id
+ c.user = self.db_user
+
+ personal_repo_group = RepoGroup.get_user_personal_repo_group(
+ c.user.user_id)
+ if personal_repo_group:
+ raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
+
+ personal_repo_group_name = RepoGroupModel().get_personal_group_name(
+ c.user)
+ named_personal_group = RepoGroup.get_by_group_name(
+ personal_repo_group_name)
+ try:
+
+ if named_personal_group and named_personal_group.user_id == c.user.user_id:
+ # migrate the same named group, and mark it as personal
+ named_personal_group.personal = True
+ Session().add(named_personal_group)
+ Session().commit()
+ msg = _('Linked repository group `%s` as personal' % (
+ personal_repo_group_name,))
+ h.flash(msg, category='success')
+ elif not named_personal_group:
+ RepoGroupModel().create_personal_repo_group(c.user)
+
+ msg = _('Created repository group `%s`' % (
+ personal_repo_group_name,))
+ h.flash(msg, category='success')
+ else:
+ msg = _('Repository group `%s` is already taken' % (
+ personal_repo_group_name,))
+ h.flash(msg, category='warning')
+ except Exception:
+ log.exception("Exception during repository group creation")
+ msg = _(
+ 'An error occurred during repository group creation for user')
+ h.flash(msg, category='error')
+ Session().rollback()
+
+ raise HTTPFound(h.route_path('user_edit_advanced', user_id=user_id))
+
@LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@view_config(
@@ -157,27 +688,18 @@ class AdminUsersView(BaseAppView, DataGr
def auth_tokens(self):
_ = self.request.translate
c = self.load_default_context()
-
- user_id = self.request.matchdict.get('user_id')
- c.user = User.get_or_404(user_id, pyramid_exc=True)
- self._redirect_for_default_user(c.user.username)
+ c.user = self.db_user
c.active = 'auth_tokens'
- c.lifetime_values = [
- (str(-1), _('forever')),
- (str(5), _('5 minutes')),
- (str(60), _('1 hour')),
- (str(60 * 24), _('1 day')),
- (str(60 * 24 * 30), _('1 month')),
- ]
- c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
+ c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
c.role_values = [
(x, AuthTokenModel.cls._get_role_name(x))
for x in AuthTokenModel.cls.ROLES]
c.role_options = [(c.role_values, _("Role"))]
c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
c.user.user_id, show_expired=True)
+ c.role_vcs = AuthTokenModel.cls.ROLE_VCS
return self._get_template_context(c)
def maybe_attach_token_scope(self, token):
@@ -193,10 +715,8 @@ class AdminUsersView(BaseAppView, DataGr
_ = self.request.translate
c = self.load_default_context()
- user_id = self.request.matchdict.get('user_id')
- c.user = User.get_or_404(user_id, pyramid_exc=True)
-
- self._redirect_for_default_user(c.user.username)
+ user_id = self.db_user_id
+ c.user = self.db_user
user_data = c.user.get_api_data()
lifetime = safe_int(self.request.POST.get('lifetime'), -1)
@@ -226,15 +746,15 @@ class AdminUsersView(BaseAppView, DataGr
_ = self.request.translate
c = self.load_default_context()
- user_id = self.request.matchdict.get('user_id')
- c.user = User.get_or_404(user_id, pyramid_exc=True)
- self._redirect_for_default_user(c.user.username)
+ user_id = self.db_user_id
+ c.user = self.db_user
+
user_data = c.user.get_api_data()
del_auth_token = self.request.POST.get('del_auth_token')
if del_auth_token:
- token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True)
+ token = UserApiKeys.get_or_404(del_auth_token)
token_data = token.get_api_data()
AuthTokenModel().delete(del_auth_token, c.user.user_id)
@@ -250,15 +770,127 @@ class AdminUsersView(BaseAppView, DataGr
@LoginRequired()
@HasPermissionAllDecorator('hg.admin')
@view_config(
+ route_name='edit_user_ssh_keys', request_method='GET',
+ renderer='rhodecode:templates/admin/users/user_edit.mako')
+ def ssh_keys(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+ c.user = self.db_user
+
+ c.active = 'ssh_keys'
+ c.default_key = self.request.GET.get('default_key')
+ c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='edit_user_ssh_keys_generate_keypair', request_method='GET',
+ renderer='rhodecode:templates/admin/users/user_edit.mako')
+ def ssh_keys_generate_keypair(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+
+ c.user = self.db_user
+
+ c.active = 'ssh_keys_generate'
+ comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
+ c.private, c.public = SshKeyModel().generate_keypair(comment=comment)
+
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='edit_user_ssh_keys_add', request_method='POST')
+ def ssh_keys_add(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+
+ user_id = self.db_user_id
+ c.user = self.db_user
+
+ user_data = c.user.get_api_data()
+ key_data = self.request.POST.get('key_data')
+ description = self.request.POST.get('description')
+
+ try:
+ if not key_data:
+ raise ValueError('Please add a valid public key')
+
+ key = SshKeyModel().parse_key(key_data.strip())
+ fingerprint = key.hash_md5()
+
+ ssh_key = SshKeyModel().create(
+ c.user.user_id, fingerprint, key_data, description)
+ ssh_key_data = ssh_key.get_api_data()
+
+ audit_logger.store_web(
+ 'user.edit.ssh_key.add', action_data={
+ 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
+ user=self._rhodecode_user, )
+ Session().commit()
+
+ # Trigger an event on change of keys.
+ trigger(SshKeyFileChangeEvent(), self.request.registry)
+
+ h.flash(_("Ssh Key successfully created"), category='success')
+
+ except IntegrityError:
+ log.exception("Exception during ssh key saving")
+ h.flash(_('An error occurred during ssh key saving: {}').format(
+ 'Such key already exists, please use a different one'),
+ category='error')
+ except Exception as e:
+ log.exception("Exception during ssh key saving")
+ h.flash(_('An error occurred during ssh key saving: {}').format(e),
+ category='error')
+
+ return HTTPFound(
+ h.route_path('edit_user_ssh_keys', user_id=user_id))
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='edit_user_ssh_keys_delete', request_method='POST')
+ def ssh_keys_delete(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+
+ user_id = self.db_user_id
+ c.user = self.db_user
+
+ user_data = c.user.get_api_data()
+
+ del_ssh_key = self.request.POST.get('del_ssh_key')
+
+ if del_ssh_key:
+ ssh_key = UserSshKeys.get_or_404(del_ssh_key)
+ ssh_key_data = ssh_key.get_api_data()
+
+ SshKeyModel().delete(del_ssh_key, c.user.user_id)
+ audit_logger.store_web(
+ 'user.edit.ssh_key.delete', action_data={
+ 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
+ user=self._rhodecode_user,)
+ Session().commit()
+ # Trigger an event on change of keys.
+ trigger(SshKeyFileChangeEvent(), self.request.registry)
+ h.flash(_("Ssh key successfully deleted"), category='success')
+
+ return HTTPFound(h.route_path('edit_user_ssh_keys', user_id=user_id))
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
route_name='edit_user_emails', request_method='GET',
renderer='rhodecode:templates/admin/users/user_edit.mako')
def emails(self):
_ = self.request.translate
c = self.load_default_context()
-
- user_id = self.request.matchdict.get('user_id')
- c.user = User.get_or_404(user_id, pyramid_exc=True)
- self._redirect_for_default_user(c.user.username)
+ c.user = self.db_user
c.active = 'emails'
c.user_email_map = UserEmailMap.query() \
@@ -275,22 +907,26 @@ class AdminUsersView(BaseAppView, DataGr
_ = self.request.translate
c = self.load_default_context()
- user_id = self.request.matchdict.get('user_id')
- c.user = User.get_or_404(user_id, pyramid_exc=True)
- self._redirect_for_default_user(c.user.username)
+ user_id = self.db_user_id
+ c.user = self.db_user
email = self.request.POST.get('new_email')
user_data = c.user.get_api_data()
try:
UserModel().add_extra_email(c.user.user_id, email)
audit_logger.store_web(
- 'user.edit.email.add', action_data={'email': email, 'user': user_data},
+ 'user.edit.email.add',
+ action_data={'email': email, 'user': user_data},
user=self._rhodecode_user)
Session().commit()
h.flash(_("Added new email address `%s` for user account") % email,
category='success')
except formencode.Invalid as error:
h.flash(h.escape(error.error_dict['email']), category='error')
+ except IntegrityError:
+ log.warning("Email %s already exists", email)
+ h.flash(_('Email `{}` is already registered for another user.').format(email),
+ category='error')
except Exception:
log.exception("Exception during email saving")
h.flash(_('An error occurred during email saving'),
@@ -306,9 +942,8 @@ class AdminUsersView(BaseAppView, DataGr
_ = self.request.translate
c = self.load_default_context()
- user_id = self.request.matchdict.get('user_id')
- c.user = User.get_or_404(user_id, pyramid_exc=True)
- self._redirect_for_default_user(c.user.username)
+ user_id = self.db_user_id
+ c.user = self.db_user
email_id = self.request.POST.get('del_email_id')
user_model = UserModel()
@@ -317,7 +952,8 @@ class AdminUsersView(BaseAppView, DataGr
user_data = c.user.get_api_data()
user_model.delete_extra_email(c.user.user_id, email_id)
audit_logger.store_web(
- 'user.edit.email.delete', action_data={'email': email, 'user': user_data},
+ 'user.edit.email.delete',
+ action_data={'email': email, 'user': user_data},
user=self._rhodecode_user)
Session().commit()
h.flash(_("Removed email address from user account"),
@@ -332,10 +968,7 @@ class AdminUsersView(BaseAppView, DataGr
def ips(self):
_ = self.request.translate
c = self.load_default_context()
-
- user_id = self.request.matchdict.get('user_id')
- c.user = User.get_or_404(user_id, pyramid_exc=True)
- self._redirect_for_default_user(c.user.username)
+ c.user = self.db_user
c.active = 'ips'
c.user_ip_map = UserIpMap.query() \
@@ -352,14 +985,14 @@ class AdminUsersView(BaseAppView, DataGr
@CSRFRequired()
@view_config(
route_name='edit_user_ips_add', request_method='POST')
+ # NOTE(marcink): this view is allowed for default users, as we can
+ # edit their IP white list
def ips_add(self):
_ = self.request.translate
c = self.load_default_context()
- user_id = self.request.matchdict.get('user_id')
- c.user = User.get_or_404(user_id, pyramid_exc=True)
- # NOTE(marcink): this view is allowed for default users, as we can
- # edit their IP white list
+ user_id = self.db_user_id
+ c.user = self.db_user
user_model = UserModel()
desc = self.request.POST.get('description')
@@ -377,7 +1010,8 @@ class AdminUsersView(BaseAppView, DataGr
try:
user_model.add_extra_ip(c.user.user_id, ip, desc)
audit_logger.store_web(
- 'user.edit.ip.add', action_data={'ip': ip, 'user': user_data},
+ 'user.edit.ip.add',
+ action_data={'ip': ip, 'user': user_data},
user=self._rhodecode_user)
Session().commit()
added.append(ip)
@@ -402,14 +1036,14 @@ class AdminUsersView(BaseAppView, DataGr
@CSRFRequired()
@view_config(
route_name='edit_user_ips_delete', request_method='POST')
+ # NOTE(marcink): this view is allowed for default users, as we can
+ # edit their IP white list
def ips_delete(self):
_ = self.request.translate
c = self.load_default_context()
- user_id = self.request.matchdict.get('user_id')
- c.user = User.get_or_404(user_id, pyramid_exc=True)
- # NOTE(marcink): this view is allowed for default users, as we can
- # edit their IP white list
+ user_id = self.db_user_id
+ c.user = self.db_user
ip_id = self.request.POST.get('del_ip_id')
user_model = UserModel()
@@ -434,11 +1068,9 @@ class AdminUsersView(BaseAppView, DataGr
renderer='rhodecode:templates/admin/users/user_edit.mako')
def groups_management(self):
c = self.load_default_context()
+ c.user = self.db_user
+ c.data = c.user.group_member
- user_id = self.request.matchdict.get('user_id')
- c.user = User.get_or_404(user_id, pyramid_exc=True)
- c.data = c.user.group_member
- self._redirect_for_default_user(c.user.username)
groups = [UserGroupModel.get_user_groups_as_dict(group.users_group)
for group in c.user.group_member]
c.groups = json.dumps(groups)
@@ -455,17 +1087,35 @@ class AdminUsersView(BaseAppView, DataGr
_ = self.request.translate
c = self.load_default_context()
- user_id = self.request.matchdict.get('user_id')
- c.user = User.get_or_404(user_id, pyramid_exc=True)
- self._redirect_for_default_user(c.user.username)
+ user_id = self.db_user_id
+ c.user = self.db_user
+
+ user_groups = set(self.request.POST.getall('users_group_id'))
+ user_groups_objects = []
+
+ for ugid in user_groups:
+ user_groups_objects.append(
+ UserGroupModel().get_group(safe_int(ugid)))
+ user_group_model = UserGroupModel()
+ added_to_groups, removed_from_groups = \
+ user_group_model.change_groups(c.user, user_groups_objects)
- users_groups = set(self.request.POST.getall('users_group_id'))
- users_groups_model = []
+ user_data = c.user.get_api_data()
+ for user_group_id in added_to_groups:
+ user_group = UserGroup.get(user_group_id)
+ old_values = user_group.get_api_data()
+ audit_logger.store_web(
+ 'user_group.edit.member.add',
+ action_data={'user': user_data, 'old_data': old_values},
+ user=self._rhodecode_user)
- for ugid in users_groups:
- users_groups_model.append(UserGroupModel().get_group(safe_int(ugid)))
- user_group_model = UserGroupModel()
- user_group_model.change_groups(c.user, users_groups_model)
+ for user_group_id in removed_from_groups:
+ user_group = UserGroup.get(user_group_id)
+ old_values = user_group.get_api_data()
+ audit_logger.store_web(
+ 'user_group.edit.member.delete',
+ action_data={'user': user_data, 'old_data': old_values},
+ user=self._rhodecode_user)
Session().commit()
c.active = 'user_groups_management'
@@ -482,10 +1132,8 @@ class AdminUsersView(BaseAppView, DataGr
def user_audit_logs(self):
_ = self.request.translate
c = self.load_default_context()
+ c.user = self.db_user
- user_id = self.request.matchdict.get('user_id')
- c.user = User.get_or_404(user_id, pyramid_exc=True)
- self._redirect_for_default_user(c.user.username)
c.active = 'audit'
p = safe_int(self.request.GET.get('page', 1), 1)
@@ -503,3 +1151,28 @@ class AdminUsersView(BaseAppView, DataGr
c.filter_term = filter_term
return self._get_template_context(c)
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='edit_user_perms_summary', request_method='GET',
+ renderer='rhodecode:templates/admin/users/user_edit.mako')
+ def user_perms_summary(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+ c.user = self.db_user
+
+ c.active = 'perms_summary'
+ c.perm_user = c.user.AuthUser(ip_addr=self.request.remote_addr)
+
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @HasPermissionAllDecorator('hg.admin')
+ @view_config(
+ route_name='edit_user_perms_summary_json', request_method='GET',
+ renderer='json_ext')
+ def user_perms_summary_json(self):
+ self.load_default_context()
+ perm_user = self.db_user.AuthUser(ip_addr=self.request.remote_addr)
+
+ return perm_user.permissions
diff --git a/rhodecode/apps/channelstream/__init__.py b/rhodecode/apps/channelstream/__init__.py
--- a/rhodecode/apps/channelstream/__init__.py
+++ b/rhodecode/apps/channelstream/__init__.py
@@ -87,4 +87,4 @@ def includeme(config):
pattern=settings.get('channelstream.proxy_path') or '/_channelstream')
# Scan module for configuration decorators.
- config.scan()
+ config.scan('.views', ignore='.tests')
diff --git a/rhodecode/apps/channelstream/views.py b/rhodecode/apps/channelstream/views.py
--- a/rhodecode/apps/channelstream/views.py
+++ b/rhodecode/apps/channelstream/views.py
@@ -18,20 +18,11 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
-"""
-Channel Stream controller for rhodecode
-
-:created_on: Oct 10, 2015
-:author: marcinl
-:copyright: (c) 2013-2015 RhodeCode GmbH.
-:license: Commercial License, see LICENSE for more details.
-"""
-
import logging
import uuid
from pyramid.view import view_config
-from webob.exc import HTTPBadRequest, HTTPForbidden, HTTPBadGateway
+from pyramid.httpexceptions import HTTPBadRequest, HTTPForbidden, HTTPBadGateway
from rhodecode.lib.channelstream import (
channelstream_request,
diff --git a/rhodecode/apps/debug_style/__init__.py b/rhodecode/apps/debug_style/__init__.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/debug_style/__init__.py
@@ -0,0 +1,42 @@
+# -*- 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 ADMIN_PREFIX
+from rhodecode.lib.utils2 import str2bool
+
+
+def debug_style_enabled(info, request):
+ return str2bool(request.registry.settings.get('debug_style'))
+
+
+def includeme(config):
+ config.add_route(
+ name='debug_style_home',
+ pattern=ADMIN_PREFIX + '/debug_style',
+ custom_predicates=(debug_style_enabled,))
+ config.add_route(
+ name='debug_style_template',
+ pattern=ADMIN_PREFIX + '/debug_style/t/{t_path}',
+ custom_predicates=(debug_style_enabled,))
+
+ # Scan module for configuration decorators.
+ config.scan('.views', ignore='.tests')
+
+
+
diff --git a/rhodecode/apps/debug_style/views.py b/rhodecode/apps/debug_style/views.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/debug_style/views.py
@@ -0,0 +1,59 @@
+# -*- 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 os
+import logging
+
+from pyramid.view import view_config
+from pyramid.renderers import render_to_response
+from rhodecode.apps._base import BaseAppView
+
+log = logging.getLogger(__name__)
+
+
+class DebugStyleView(BaseAppView):
+ def load_default_context(self):
+ c = self._get_local_tmpl_context()
+ self._register_global_c(c)
+ return c
+
+ @view_config(
+ route_name='debug_style_home', request_method='GET',
+ renderer=None)
+ def index(self):
+ c = self.load_default_context()
+ c.active = 'index'
+
+ return render_to_response(
+ 'debug_style/index.html', self._get_template_context(c),
+ request=self.request)
+
+ @view_config(
+ route_name='debug_style_template', request_method='GET',
+ renderer=None)
+ def template(self):
+ t_path = self.request.matchdict['t_path']
+ c = self.load_default_context()
+ c.active = os.path.splitext(t_path)[0]
+ c.came_from = ''
+
+ return render_to_response(
+ 'debug_style/' + t_path, self._get_template_context(c),
+ request=self.request)
\ No newline at end of file
diff --git a/rhodecode/apps/gist/__init__.py b/rhodecode/apps/gist/__init__.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/gist/__init__.py
@@ -0,0 +1,62 @@
+# -*- 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 ADMIN_PREFIX
+
+
+def admin_routes(config):
+ config.add_route(
+ name='gists_show', pattern='/gists')
+ config.add_route(
+ name='gists_new', pattern='/gists/new')
+ config.add_route(
+ name='gists_create', pattern='/gists/create')
+
+ config.add_route(
+ name='gist_show', pattern='/gists/{gist_id}')
+
+ config.add_route(
+ name='gist_delete', pattern='/gists/{gist_id}/delete')
+
+ config.add_route(
+ name='gist_edit', pattern='/gists/{gist_id}/edit')
+
+ config.add_route(
+ name='gist_edit_check_revision',
+ pattern='/gists/{gist_id}/edit/check_revision')
+
+ config.add_route(
+ name='gist_update', pattern='/gists/{gist_id}/update')
+
+ config.add_route(
+ name='gist_show_rev',
+ pattern='/gists/{gist_id}/{revision}')
+ config.add_route(
+ name='gist_show_formatted',
+ pattern='/gists/{gist_id}/{revision}/{format}')
+
+ config.add_route(
+ name='gist_show_formatted_path',
+ pattern='/gists/{gist_id}/{revision}/{format}/{f_path:.*}')
+
+
+def includeme(config):
+ config.include(admin_routes, route_prefix=ADMIN_PREFIX)
+ # Scan module for configuration decorators.
+ config.scan('.views', ignore='.tests')
diff --git a/rhodecode/apps/gist/tests/__init__.py b/rhodecode/apps/gist/tests/__init__.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/gist/tests/__init__.py
@@ -0,0 +1,20 @@
+# -*- 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/tests/functional/test_admin_gists.py b/rhodecode/apps/gist/tests/test_admin_gists.py
rename from rhodecode/tests/functional/test_admin_gists.py
rename to rhodecode/apps/gist/tests/test_admin_gists.py
--- a/rhodecode/tests/functional/test_admin_gists.py
+++ b/rhodecode/apps/gist/tests/test_admin_gists.py
@@ -27,7 +27,31 @@ from rhodecode.model.gist import GistMod
from rhodecode.model.meta import Session
from rhodecode.tests import (
TEST_USER_ADMIN_LOGIN, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
- TestController, assert_session_flash, url)
+ TestController, assert_session_flash)
+
+
+def route_path(name, params=None, **kwargs):
+ import urllib
+ from rhodecode.apps._base import ADMIN_PREFIX
+
+ base_url = {
+ 'gists_show': ADMIN_PREFIX + '/gists',
+ 'gists_new': ADMIN_PREFIX + '/gists/new',
+ 'gists_create': ADMIN_PREFIX + '/gists/create',
+ 'gist_show': ADMIN_PREFIX + '/gists/{gist_id}',
+ 'gist_delete': ADMIN_PREFIX + '/gists/{gist_id}/delete',
+ 'gist_edit': ADMIN_PREFIX + '/gists/{gist_id}/edit',
+ 'gist_edit_check_revision': ADMIN_PREFIX + '/gists/{gist_id}/edit/check_revision',
+ 'gist_update': ADMIN_PREFIX + '/gists/{gist_id}/update',
+ 'gist_show_rev': ADMIN_PREFIX + '/gists/{gist_id}/{revision}',
+ 'gist_show_formatted': ADMIN_PREFIX + '/gists/{gist_id}/{revision}/{format}',
+ 'gist_show_formatted_path': ADMIN_PREFIX + '/gists/{gist_id}/{revision}/{format}/{f_path}',
+
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
class GistUtility(object):
@@ -70,7 +94,7 @@ class TestGistsController(TestController
def test_index_empty(self, create_gist):
self.log_user()
- response = self.app.get(url('gists'))
+ response = self.app.get(route_path('gists_show'))
response.mustcontain('data: [],')
def test_index(self, create_gist):
@@ -79,7 +103,7 @@ class TestGistsController(TestController
g2 = create_gist('gist2', lifetime=1400)
g3 = create_gist('gist3', description='gist3-desc')
g4 = create_gist('gist4', gist_type='private').gist_access_id
- response = self.app.get(url('gists'))
+ response = self.app.get(route_path('gists_show'))
response.mustcontain('gist: %s' % g1.gist_access_id)
response.mustcontain('gist: %s' % g2.gist_access_id)
@@ -95,7 +119,7 @@ class TestGistsController(TestController
def test_index_private_gists(self, create_gist):
self.log_user()
gist = create_gist('gist5', gist_type='private')
- response = self.app.get(url('gists', private=1))
+ response = self.app.get(route_path('gists_show', params=dict(private=1)))
# and privates
response.mustcontain('gist: %s' % gist.gist_access_id)
@@ -107,7 +131,7 @@ class TestGistsController(TestController
create_gist('gist3', description='gist3-desc')
create_gist('gist4', gist_type='private')
- response = self.app.get(url('gists', all=1))
+ response = self.app.get(route_path('gists_show', params=dict(all=1)))
assert len(GistModel.get_all()) == 4
# and privates
@@ -120,7 +144,7 @@ class TestGistsController(TestController
create_gist('gist3', gist_type='private')
create_gist('gist4', gist_type='private')
- response = self.app.get(url('gists', all=1))
+ response = self.app.get(route_path('gists_show', params=dict(all=1)))
assert len(GistModel.get_all()) == 3
# since we don't have access to private in this view, we
@@ -131,7 +155,7 @@ class TestGistsController(TestController
def test_create(self):
self.log_user()
response = self.app.post(
- url('gists'),
+ route_path('gists_create'),
params={'lifetime': -1,
'content': 'gist test',
'filename': 'foo',
@@ -146,7 +170,7 @@ class TestGistsController(TestController
def test_create_with_path_with_dirs(self):
self.log_user()
response = self.app.post(
- url('gists'),
+ route_path('gists_create'),
params={'lifetime': -1,
'content': 'gist test',
'filename': '/home/foo',
@@ -163,12 +187,13 @@ class TestGistsController(TestController
Session().add(gist)
Session().commit()
- self.app.get(url('gist', gist_id=gist.gist_access_id), status=404)
+ self.app.get(route_path('gist_show', gist_id=gist.gist_access_id),
+ status=404)
def test_create_private(self):
self.log_user()
response = self.app.post(
- url('gists'),
+ route_path('gists_create'),
params={'lifetime': -1,
'content': 'private gist test',
'filename': 'private-foo',
@@ -187,7 +212,7 @@ class TestGistsController(TestController
def test_create_private_acl_private(self):
self.log_user()
response = self.app.post(
- url('gists'),
+ route_path('gists_create'),
params={'lifetime': -1,
'content': 'private gist test',
'filename': 'private-foo',
@@ -206,7 +231,7 @@ class TestGistsController(TestController
def test_create_with_description(self):
self.log_user()
response = self.app.post(
- url('gists'),
+ route_path('gists_create'),
params={'lifetime': -1,
'content': 'gist test',
'filename': 'foo-desc',
@@ -231,7 +256,8 @@ class TestGistsController(TestController
'gist_acl_level': Gist.ACL_LEVEL_PUBLIC,
'csrf_token': self.csrf_token
}
- response = self.app.post(url('gists'), params=params, status=302)
+ response = self.app.post(
+ route_path('gists_create'), params=params, status=302)
self.logout_user()
response = response.follow()
response.mustcontain('added file: foo-desc')
@@ -240,35 +266,36 @@ class TestGistsController(TestController
def test_new(self):
self.log_user()
- self.app.get(url('new_gist'))
+ self.app.get(route_path('gists_new'))
def test_delete(self, create_gist):
self.log_user()
gist = create_gist('delete-me')
response = self.app.post(
- url('gist', gist_id=gist.gist_id),
- params={'_method': 'delete', 'csrf_token': self.csrf_token})
+ route_path('gist_delete', gist_id=gist.gist_id),
+ params={'csrf_token': self.csrf_token})
assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
def test_delete_normal_user_his_gist(self, create_gist):
self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
gist = create_gist('delete-me', owner=TEST_USER_REGULAR_LOGIN)
+
response = self.app.post(
- url('gist', gist_id=gist.gist_id),
- params={'_method': 'delete', 'csrf_token': self.csrf_token})
+ route_path('gist_delete', gist_id=gist.gist_id),
+ params={'csrf_token': self.csrf_token})
assert_session_flash(response, 'Deleted gist %s' % gist.gist_id)
def test_delete_normal_user_not_his_own_gist(self, create_gist):
self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
- gist = create_gist('delete-me')
+ gist = create_gist('delete-me-2')
+
self.app.post(
- url('gist', gist_id=gist.gist_id),
- params={'_method': 'delete', 'csrf_token': self.csrf_token},
- status=403)
+ route_path('gist_delete', gist_id=gist.gist_id),
+ params={'csrf_token': self.csrf_token}, status=404)
def test_show(self, create_gist):
gist = create_gist('gist-show-me')
- response = self.app.get(url('gist', gist_id=gist.gist_access_id))
+ response = self.app.get(route_path('gist_show', gist_id=gist.gist_access_id))
response.mustcontain('added file: gist-show-me<')
@@ -283,16 +310,19 @@ class TestGistsController(TestController
with mock.patch(
'rhodecode.lib.vcs.settings.ALIASES', ['git']):
gist = create_gist('gist-show-me-again')
- self.app.get(url('gist', gist_id=gist.gist_access_id), status=200)
+ self.app.get(
+ route_path('gist_show', gist_id=gist.gist_access_id), status=200)
def test_show_acl_private(self, create_gist):
gist = create_gist('gist-show-me-only-when-im-logged-in',
acl_level=Gist.ACL_LEVEL_PRIVATE)
- self.app.get(url('gist', gist_id=gist.gist_access_id), status=404)
+ self.app.get(
+ route_path('gist_show', gist_id=gist.gist_access_id), status=404)
# now we log-in we should see thi gist
self.log_user()
- response = self.app.get(url('gist', gist_id=gist.gist_access_id))
+ response = self.app.get(
+ route_path('gist_show', gist_id=gist.gist_access_id))
response.mustcontain('added file: gist-show-me-only-when-im-logged-in')
assert_response = response.assert_response()
@@ -303,36 +333,42 @@ class TestGistsController(TestController
def test_show_as_raw(self, create_gist):
gist = create_gist('gist-show-me', content='GIST CONTENT')
- response = self.app.get(url('formatted_gist',
- gist_id=gist.gist_access_id, format='raw'))
+ response = self.app.get(
+ route_path('gist_show_formatted',
+ gist_id=gist.gist_access_id, revision='tip',
+ format='raw'))
assert response.body == 'GIST CONTENT'
def test_show_as_raw_individual_file(self, create_gist):
gist = create_gist('gist-show-me-raw', content='GIST BODY')
- response = self.app.get(url('formatted_gist_file',
- gist_id=gist.gist_access_id, format='raw',
- revision='tip', f_path='gist-show-me-raw'))
+ response = self.app.get(
+ route_path('gist_show_formatted_path',
+ gist_id=gist.gist_access_id, format='raw',
+ revision='tip', f_path='gist-show-me-raw'))
assert response.body == 'GIST BODY'
def test_edit_page(self, create_gist):
self.log_user()
gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
- response = self.app.get(url('edit_gist', gist_id=gist.gist_access_id))
+ response = self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id))
response.mustcontain('GIST EDIT BODY')
def test_edit_page_non_logged_user(self, create_gist):
gist = create_gist('gist-for-edit', content='GIST EDIT BODY')
- self.app.get(url('edit_gist', gist_id=gist.gist_access_id), status=302)
+ self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
+ status=302)
def test_edit_normal_user_his_gist(self, create_gist):
self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
gist = create_gist('gist-for-edit', owner=TEST_USER_REGULAR_LOGIN)
- self.app.get(url('edit_gist', gist_id=gist.gist_access_id, status=200))
+ self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id,
+ status=200))
def test_edit_normal_user_not_his_own_gist(self, create_gist):
self.log_user(TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS)
gist = create_gist('delete-me')
- self.app.get(url('edit_gist', gist_id=gist.gist_access_id), status=403)
+ self.app.get(route_path('gist_edit', gist_id=gist.gist_access_id),
+ status=404)
def test_user_first_name_is_escaped(self, user_util, create_gist):
xss_atack_string = '">'
@@ -341,7 +377,7 @@ class TestGistsController(TestController
user = user_util.create_user(
firstname=xss_atack_string, password=password)
create_gist('gist', gist_type='public', owner=user.username)
- response = self.app.get(url('gists'))
+ response = self.app.get(route_path('gists_show'))
response.mustcontain(xss_escaped_string)
def test_user_last_name_is_escaped(self, user_util, create_gist):
@@ -351,5 +387,5 @@ class TestGistsController(TestController
user = user_util.create_user(
lastname=xss_atack_string, password=password)
create_gist('gist', gist_type='public', owner=user.username)
- response = self.app.get(url('gists'))
+ response = self.app.get(route_path('gists_show'))
response.mustcontain(xss_escaped_string)
diff --git a/rhodecode/apps/gist/views.py b/rhodecode/apps/gist/views.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/gist/views.py
@@ -0,0 +1,413 @@
+# -*- coding: utf-8 -*-
+
+# Copyright (C) 2013-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 time
+import logging
+
+import formencode
+import formencode.htmlfill
+import peppercorn
+
+from pyramid.httpexceptions import HTTPNotFound, HTTPFound
+from pyramid.view import view_config
+from pyramid.renderers import render
+from pyramid.response import Response
+
+from rhodecode.apps._base import BaseAppView
+from rhodecode.lib import helpers as h
+from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
+from rhodecode.lib.utils2 import time_to_datetime
+from rhodecode.lib.ext_json import json
+from rhodecode.lib.vcs.exceptions import VCSError, NodeNotChangedError
+from rhodecode.model.gist import GistModel
+from rhodecode.model.meta import Session
+from rhodecode.model.db import Gist, User, or_
+from rhodecode.model import validation_schema
+from rhodecode.model.validation_schema.schemas import gist_schema
+
+
+log = logging.getLogger(__name__)
+
+
+class GistView(BaseAppView):
+
+ def load_default_context(self):
+ _ = self.request.translate
+ c = self._get_local_tmpl_context()
+ c.user = c.auth_user.get_instance()
+
+ c.lifetime_values = [
+ (-1, _('forever')),
+ (5, _('5 minutes')),
+ (60, _('1 hour')),
+ (60 * 24, _('1 day')),
+ (60 * 24 * 30, _('1 month')),
+ ]
+
+ c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
+ c.acl_options = [
+ (Gist.ACL_LEVEL_PRIVATE, _("Requires registered account")),
+ (Gist.ACL_LEVEL_PUBLIC, _("Can be accessed by anonymous users"))
+ ]
+
+ self._register_global_c(c)
+ return c
+
+ @LoginRequired()
+ @view_config(
+ route_name='gists_show', request_method='GET',
+ renderer='rhodecode:templates/admin/gists/index.mako')
+ def gist_show_all(self):
+ c = self.load_default_context()
+
+ not_default_user = self._rhodecode_user.username != User.DEFAULT_USER
+ c.show_private = self.request.GET.get('private') and not_default_user
+ c.show_public = self.request.GET.get('public') and not_default_user
+ c.show_all = self.request.GET.get('all') and self._rhodecode_user.admin
+
+ gists = _gists = Gist().query()\
+ .filter(or_(Gist.gist_expires == -1, Gist.gist_expires >= time.time()))\
+ .order_by(Gist.created_on.desc())
+
+ c.active = 'public'
+ # MY private
+ if c.show_private and not c.show_public:
+ gists = _gists.filter(Gist.gist_type == Gist.GIST_PRIVATE)\
+ .filter(Gist.gist_owner == self._rhodecode_user.user_id)
+ c.active = 'my_private'
+ # MY public
+ elif c.show_public and not c.show_private:
+ gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)\
+ .filter(Gist.gist_owner == self._rhodecode_user.user_id)
+ c.active = 'my_public'
+ # MY public+private
+ elif c.show_private and c.show_public:
+ gists = _gists.filter(or_(Gist.gist_type == Gist.GIST_PUBLIC,
+ Gist.gist_type == Gist.GIST_PRIVATE))\
+ .filter(Gist.gist_owner == self._rhodecode_user.user_id)
+ c.active = 'my_all'
+ # Show all by super-admin
+ elif c.show_all:
+ c.active = 'all'
+ gists = _gists
+
+ # default show ALL public gists
+ if not c.show_public and not c.show_private and not c.show_all:
+ gists = _gists.filter(Gist.gist_type == Gist.GIST_PUBLIC)
+ c.active = 'public'
+
+ _render = self.request.get_partial_renderer(
+ 'data_table/_dt_elements.mako')
+
+ data = []
+
+ for gist in gists:
+ data.append({
+ 'created_on': _render('gist_created', gist.created_on),
+ 'created_on_raw': gist.created_on,
+ 'type': _render('gist_type', gist.gist_type),
+ 'access_id': _render('gist_access_id', gist.gist_access_id, gist.owner.full_contact),
+ 'author': _render('gist_author', gist.owner.full_contact, gist.created_on, gist.gist_expires),
+ 'author_raw': h.escape(gist.owner.full_contact),
+ 'expires': _render('gist_expires', gist.gist_expires),
+ 'description': _render('gist_description', gist.gist_description)
+ })
+ c.data = json.dumps(data)
+
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @NotAnonymous()
+ @view_config(
+ route_name='gists_new', request_method='GET',
+ renderer='rhodecode:templates/admin/gists/new.mako')
+ def gist_new(self):
+ c = self.load_default_context()
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @NotAnonymous()
+ @CSRFRequired()
+ @view_config(
+ route_name='gists_create', request_method='POST',
+ renderer='rhodecode:templates/admin/gists/new.mako')
+ def gist_create(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+
+ data = dict(self.request.POST)
+ data['filename'] = data.get('filename') or Gist.DEFAULT_FILENAME
+ data['nodes'] = [{
+ 'filename': data['filename'],
+ 'content': data.get('content'),
+ 'mimetype': data.get('mimetype') # None is autodetect
+ }]
+
+ data['gist_type'] = (
+ Gist.GIST_PUBLIC if data.get('public') else Gist.GIST_PRIVATE)
+ data['gist_acl_level'] = (
+ data.get('gist_acl_level') or Gist.ACL_LEVEL_PRIVATE)
+
+ schema = gist_schema.GistSchema().bind(
+ lifetime_options=[x[0] for x in c.lifetime_values])
+
+ try:
+
+ schema_data = schema.deserialize(data)
+ # convert to safer format with just KEYs so we sure no duplicates
+ schema_data['nodes'] = gist_schema.sequence_to_nodes(
+ schema_data['nodes'])
+
+ gist = GistModel().create(
+ gist_id=schema_data['gistid'], # custom access id not real ID
+ description=schema_data['description'],
+ owner=self._rhodecode_user.user_id,
+ gist_mapping=schema_data['nodes'],
+ gist_type=schema_data['gist_type'],
+ lifetime=schema_data['lifetime'],
+ gist_acl_level=schema_data['gist_acl_level']
+ )
+ Session().commit()
+ new_gist_id = gist.gist_access_id
+ except validation_schema.Invalid as errors:
+ defaults = data
+ errors = errors.asdict()
+
+ if 'nodes.0.content' in errors:
+ errors['content'] = errors['nodes.0.content']
+ del errors['nodes.0.content']
+ if 'nodes.0.filename' in errors:
+ errors['filename'] = errors['nodes.0.filename']
+ del errors['nodes.0.filename']
+
+ data = render('rhodecode:templates/admin/gists/new.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=defaults,
+ errors=errors,
+ prefix_error=False,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+
+ except Exception:
+ log.exception("Exception while trying to create a gist")
+ h.flash(_('Error occurred during gist creation'), category='error')
+ raise HTTPFound(h.route_url('gists_new'))
+ raise HTTPFound(h.route_url('gist_show', gist_id=new_gist_id))
+
+ @LoginRequired()
+ @NotAnonymous()
+ @CSRFRequired()
+ @view_config(
+ route_name='gist_delete', request_method='POST')
+ def gist_delete(self):
+ _ = self.request.translate
+ gist_id = self.request.matchdict['gist_id']
+
+ c = self.load_default_context()
+ c.gist = Gist.get_or_404(gist_id)
+
+ owner = c.gist.gist_owner == self._rhodecode_user.user_id
+ if not (h.HasPermissionAny('hg.admin')() or owner):
+ log.warning('Deletion of Gist was forbidden '
+ 'by unauthorized user: `%s`', self._rhodecode_user)
+ raise HTTPNotFound()
+
+ GistModel().delete(c.gist)
+ Session().commit()
+ h.flash(_('Deleted gist %s') % c.gist.gist_access_id, category='success')
+
+ raise HTTPFound(h.route_url('gists_show'))
+
+ def _get_gist(self, gist_id):
+
+ gist = Gist.get_or_404(gist_id)
+
+ # Check if this gist is expired
+ if gist.gist_expires != -1:
+ if time.time() > gist.gist_expires:
+ log.error(
+ 'Gist expired at %s', time_to_datetime(gist.gist_expires))
+ raise HTTPNotFound()
+
+ # check if this gist requires a login
+ is_default_user = self._rhodecode_user.username == User.DEFAULT_USER
+ if gist.acl_level == Gist.ACL_LEVEL_PRIVATE and is_default_user:
+ log.error("Anonymous user %s tried to access protected gist `%s`",
+ self._rhodecode_user, gist_id)
+ raise HTTPNotFound()
+ return gist
+
+ @LoginRequired()
+ @view_config(
+ route_name='gist_show', request_method='GET',
+ renderer='rhodecode:templates/admin/gists/show.mako')
+ @view_config(
+ route_name='gist_show_rev', request_method='GET',
+ renderer='rhodecode:templates/admin/gists/show.mako')
+ @view_config(
+ route_name='gist_show_formatted', request_method='GET',
+ renderer=None)
+ @view_config(
+ route_name='gist_show_formatted_path', request_method='GET',
+ renderer=None)
+ def gist_show(self):
+ gist_id = self.request.matchdict['gist_id']
+
+ # TODO(marcink): expose those via matching dict
+ revision = self.request.matchdict.get('revision', 'tip')
+ f_path = self.request.matchdict.get('f_path', None)
+ return_format = self.request.matchdict.get('format')
+
+ c = self.load_default_context()
+ c.gist = self._get_gist(gist_id)
+ c.render = not self.request.GET.get('no-render', False)
+
+ try:
+ c.file_last_commit, c.files = GistModel().get_gist_files(
+ gist_id, revision=revision)
+ except VCSError:
+ log.exception("Exception in gist show")
+ raise HTTPNotFound()
+
+ if return_format == 'raw':
+ content = '\n\n'.join([f.content for f in c.files
+ if (f_path is None or f.path == f_path)])
+ response = Response(content)
+ response.content_type = 'text/plain'
+ return response
+
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @NotAnonymous()
+ @view_config(
+ route_name='gist_edit', request_method='GET',
+ renderer='rhodecode:templates/admin/gists/edit.mako')
+ def gist_edit(self):
+ _ = self.request.translate
+ gist_id = self.request.matchdict['gist_id']
+ c = self.load_default_context()
+ c.gist = self._get_gist(gist_id)
+
+ owner = c.gist.gist_owner == self._rhodecode_user.user_id
+ if not (h.HasPermissionAny('hg.admin')() or owner):
+ raise HTTPNotFound()
+
+ try:
+ c.file_last_commit, c.files = GistModel().get_gist_files(gist_id)
+ except VCSError:
+ log.exception("Exception in gist edit")
+ raise HTTPNotFound()
+
+ if c.gist.gist_expires == -1:
+ expiry = _('never')
+ else:
+ # this cannot use timeago, since it's used in select2 as a value
+ expiry = h.age(h.time_to_datetime(c.gist.gist_expires))
+
+ c.lifetime_values.append(
+ (0, _('%(expiry)s - current value') % {'expiry': _(expiry)})
+ )
+
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @NotAnonymous()
+ @CSRFRequired()
+ @view_config(
+ route_name='gist_update', request_method='POST',
+ renderer='rhodecode:templates/admin/gists/edit.mako')
+ def gist_update(self):
+ _ = self.request.translate
+ gist_id = self.request.matchdict['gist_id']
+ c = self.load_default_context()
+ c.gist = self._get_gist(gist_id)
+
+ owner = c.gist.gist_owner == self._rhodecode_user.user_id
+ if not (h.HasPermissionAny('hg.admin')() or owner):
+ raise HTTPNotFound()
+
+ data = peppercorn.parse(self.request.POST.items())
+
+ schema = gist_schema.GistSchema()
+ schema = schema.bind(
+ # '0' is special value to leave lifetime untouched
+ lifetime_options=[x[0] for x in c.lifetime_values] + [0],
+ )
+
+ try:
+ schema_data = schema.deserialize(data)
+ # convert to safer format with just KEYs so we sure no duplicates
+ schema_data['nodes'] = gist_schema.sequence_to_nodes(
+ schema_data['nodes'])
+
+ GistModel().update(
+ gist=c.gist,
+ description=schema_data['description'],
+ owner=c.gist.owner,
+ gist_mapping=schema_data['nodes'],
+ lifetime=schema_data['lifetime'],
+ gist_acl_level=schema_data['gist_acl_level']
+ )
+
+ Session().commit()
+ h.flash(_('Successfully updated gist content'), category='success')
+ except NodeNotChangedError:
+ # raised if nothing was changed in repo itself. We anyway then
+ # store only DB stuff for gist
+ Session().commit()
+ h.flash(_('Successfully updated gist data'), category='success')
+ except validation_schema.Invalid as errors:
+ errors = h.escape(errors.asdict())
+ h.flash(_('Error occurred during update of gist {}: {}').format(
+ gist_id, errors), category='error')
+ except Exception:
+ log.exception("Exception in gist edit")
+ h.flash(_('Error occurred during update of gist %s') % gist_id,
+ category='error')
+
+ raise HTTPFound(h.route_url('gist_show', gist_id=gist_id))
+
+ @LoginRequired()
+ @NotAnonymous()
+ @view_config(
+ route_name='gist_edit_check_revision', request_method='GET',
+ renderer='json_ext')
+ def gist_edit_check_revision(self):
+ _ = self.request.translate
+ gist_id = self.request.matchdict['gist_id']
+ c = self.load_default_context()
+ c.gist = self._get_gist(gist_id)
+
+ last_rev = c.gist.scm_instance().get_commit()
+ success = True
+ revision = self.request.GET.get('revision')
+
+ if revision != last_rev.raw_id:
+ log.error('Last revision %s is different then submitted %s'
+ % (revision, last_rev))
+ # our gist has newer version than we
+ success = False
+
+ return {'success': success}
diff --git a/rhodecode/apps/home/__init__.py b/rhodecode/apps/home/__init__.py
--- a/rhodecode/apps/home/__init__.py
+++ b/rhodecode/apps/home/__init__.py
@@ -46,4 +46,4 @@ def includeme(config):
routing_links.connect_redirection_links(config)
# Scan module for configuration decorators.
- config.scan()
+ config.scan('.views', ignore='.tests')
diff --git a/rhodecode/apps/home/tests/test_home.py b/rhodecode/apps/home/tests/test_home.py
--- a/rhodecode/apps/home/tests/test_home.py
+++ b/rhodecode/apps/home/tests/test_home.py
@@ -54,14 +54,16 @@ class TestHomeController(TestController)
response.mustcontain('"name_raw": "%s"' % repo.repo_name)
def test_index_contains_statics_with_ver(self):
- from pylons import tmpl_context as c
+ from rhodecode.lib.base import calculate_version_hash
self.log_user()
response = self.app.get(route_path('home'))
- rhodecode_version_hash = c.rhodecode_version_hash
+ rhodecode_version_hash = calculate_version_hash(
+ {'beaker.session.secret':'test-rc-uytcxaz'})
response.mustcontain('style.css?ver={0}'.format(rhodecode_version_hash))
- response.mustcontain('rhodecode-components.js?ver={0}'.format(rhodecode_version_hash))
+ response.mustcontain('rhodecode-components.js?ver={0}'.format(
+ rhodecode_version_hash))
def test_index_contains_backend_specific_details(self, backend):
self.log_user()
@@ -132,3 +134,9 @@ class TestHomeController(TestController)
response.mustcontain(version_string)
if state is False:
response.mustcontain(no=[version_string])
+
+ def test_logout_form_contains_csrf(self, autologin_user, csrf_token):
+ response = self.app.get(route_path('home'))
+ assert_response = response.assert_response()
+ element = assert_response.get_element('.logout #csrf_token')
+ assert element.value == csrf_token
diff --git a/rhodecode/apps/home/views.py b/rhodecode/apps/home/views.py
--- a/rhodecode/apps/home/views.py
+++ b/rhodecode/apps/home/views.py
@@ -25,15 +25,16 @@ 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, \
- HasRepoGroupPermissionAnyDecorator
+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.db import (
+ func, or_, in_filter_generator, Repository, RepoGroup)
from rhodecode.model.repo import RepoModel
from rhodecode.model.repo_group import RepoGroupModel
-from rhodecode.model.scm import ScmModel, RepoGroupList, RepoList
+from rhodecode.model.scm import RepoGroupList, RepoList
from rhodecode.model.user import UserModel
from rhodecode.model.user_group import UserGroupModel
@@ -101,9 +102,17 @@ class HomeView(BaseAppView):
return {'suggestions': _user_groups}
def _get_repo_list(self, name_contains=None, repo_type=None, limit=20):
+ allowed_ids = self._rhodecode_user.repo_acl_ids(
+ ['repository.read', 'repository.write', 'repository.admin'],
+ cache=False, name_filter=name_contains) or [-1]
+
query = Repository.query()\
.order_by(func.length(Repository.repo_name))\
- .order_by(Repository.repo_name)
+ .order_by(Repository.repo_name)\
+ .filter(or_(
+ # generate multiple IN to fix limitation problems
+ *in_filter_generator(Repository.repo_id, allowed_ids)
+ ))
if repo_type:
query = query.filter(Repository.repo_type == repo_type)
@@ -114,23 +123,31 @@ class HomeView(BaseAppView):
Repository.repo_name.ilike(ilike_expression))
query = query.limit(limit)
- all_repos = query.all()
- # permission checks are inside this function
- repo_iter = ScmModel().get_repos(all_repos)
+ acl_repo_iter = query
+
return [
{
- 'id': obj['name'],
- 'text': obj['name'],
+ 'id': obj.repo_name,
+ 'text': obj.repo_name,
'type': 'repo',
- 'obj': obj['dbrepo'],
- 'url': h.route_path('repo_summary', repo_name=obj['name'])
+ 'obj': {'repo_type': obj.repo_type, 'private': obj.private,
+ 'repo_id': obj.repo_id},
+ 'url': h.route_path('repo_summary', repo_name=obj.repo_name)
}
- for obj in repo_iter]
+ for obj in acl_repo_iter]
def _get_repo_group_list(self, name_contains=None, limit=20):
+ allowed_ids = self._rhodecode_user.repo_group_acl_ids(
+ ['group.read', 'group.write', 'group.admin'],
+ cache=False, name_filter=name_contains) or [-1]
+
query = RepoGroup.query()\
.order_by(func.length(RepoGroup.group_name))\
- .order_by(RepoGroup.group_name)
+ .order_by(RepoGroup.group_name) \
+ .filter(or_(
+ # generate multiple IN to fix limitation problems
+ *in_filter_generator(RepoGroup.group_id, allowed_ids)
+ ))
if name_contains:
ilike_expression = u'%{}%'.format(safe_unicode(name_contains))
@@ -138,23 +155,24 @@ class HomeView(BaseAppView):
RepoGroup.group_name.ilike(ilike_expression))
query = query.limit(limit)
- all_groups = query.all()
- repo_groups_iter = ScmModel().get_repo_groups(all_groups)
+ acl_repo_iter = query
+
return [
{
'id': obj.group_name,
'text': obj.group_name,
'type': 'group',
'obj': {},
- 'url': h.route_path('repo_group_home', repo_group_name=obj.group_name)
+ 'url': h.route_path(
+ 'repo_group_home', repo_group_name=obj.group_name)
}
- for obj in repo_groups_iter]
+ for obj in acl_repo_iter]
- def _get_hash_commit_list(self, auth_user, hash_starts_with=None):
- if not hash_starts_with or len(hash_starts_with) < 3:
+ def _get_hash_commit_list(self, auth_user, query=None):
+ if not query or len(query) < 3:
return []
- commit_hashes = re.compile('([0-9a-f]{2,40})').findall(hash_starts_with)
+ commit_hashes = re.compile('(?:commit:)([0-9a-f]{2,40})').findall(query)
if len(commit_hashes) != 1:
return []
@@ -172,9 +190,9 @@ class HomeView(BaseAppView):
'text': entry['commit_id'],
'type': 'commit',
'obj': {'repo': entry['repository']},
- 'url': h.url('changeset_home',
- repo_name=entry['repository'],
- revision=entry['commit_id'])
+ 'url': h.route_path(
+ 'repo_commit',
+ repo_name=entry['repository'], commit_id=entry['commit_id'])
}
for entry in result['results']]
diff --git a/rhodecode/apps/journal/__init__.py b/rhodecode/apps/journal/__init__.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/journal/__init__.py
@@ -0,0 +1,53 @@
+# -*- 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 ADMIN_PREFIX
+
+
+def admin_routes(config):
+
+ config.add_route(
+ name='journal', pattern='/journal')
+ config.add_route(
+ name='journal_rss', pattern='/journal/rss')
+ config.add_route(
+ name='journal_atom', pattern='/journal/atom')
+
+ config.add_route(
+ name='journal_public', pattern='/public_journal')
+ config.add_route(
+ name='journal_public_atom', pattern='/public_journal/atom')
+ config.add_route(
+ name='journal_public_atom_old', pattern='/public_journal_atom')
+
+ config.add_route(
+ name='journal_public_rss', pattern='/public_journal/rss')
+ config.add_route(
+ name='journal_public_rss_old', pattern='/public_journal_rss')
+
+ config.add_route(
+ name='toggle_following', pattern='/toggle_following')
+
+
+def includeme(config):
+ config.include(admin_routes, route_prefix=ADMIN_PREFIX)
+ # Scan module for configuration decorators.
+ config.scan('.views', ignore='.tests')
diff --git a/rhodecode/apps/journal/tests/__init__.py b/rhodecode/apps/journal/tests/__init__.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/journal/tests/__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/tests/functional/test_journal.py b/rhodecode/apps/journal/tests/test_journal.py
rename from rhodecode/tests/functional/test_journal.py
rename to rhodecode/apps/journal/tests/test_journal.py
--- a/rhodecode/tests/functional/test_journal.py
+++ b/rhodecode/apps/journal/tests/test_journal.py
@@ -19,24 +19,62 @@
# and proprietary license terms, please see https://rhodecode.com/licenses/
import datetime
-from rhodecode.tests import TestController, url
+
+import pytest
+
+from rhodecode.apps._base import ADMIN_PREFIX
+from rhodecode.tests import TestController
from rhodecode.model.db import UserFollowing, Repository
-class TestJournalController(TestController):
+def route_path(name, params=None, **kwargs):
+ import urllib
- def test_index(self):
+ base_url = {
+ 'journal': ADMIN_PREFIX + '/journal',
+ 'journal_rss': ADMIN_PREFIX + '/journal/rss',
+ 'journal_atom': ADMIN_PREFIX + '/journal/atom',
+ 'journal_public': ADMIN_PREFIX + '/public_journal',
+ 'journal_public_atom': ADMIN_PREFIX + '/public_journal/atom',
+ 'journal_public_atom_old': ADMIN_PREFIX + '/public_journal_atom',
+ 'journal_public_rss': ADMIN_PREFIX + '/public_journal/rss',
+ 'journal_public_rss_old': ADMIN_PREFIX + '/public_journal_rss',
+ 'toggle_following': ADMIN_PREFIX + '/toggle_following',
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
+
+
+class TestJournalViews(TestController):
+
+ def test_journal(self):
self.log_user()
- response = self.app.get(url(controller='journal', action='index'))
- response.mustcontain(
- """%s
""" % datetime.date.today())
+ response = self.app.get(route_path('journal'))
+ # response.mustcontain(
+ # """%s
""" % datetime.date.today())
+
+ @pytest.mark.parametrize("feed_type, content_type", [
+ ('rss', "application/rss+xml"),
+ ('atom', "application/atom+xml")
+ ])
+ def test_journal_feed(self, feed_type, content_type):
+ self.log_user()
+ response = self.app.get(
+ route_path(
+ 'journal_{}'.format(feed_type)),
+ status=200)
+
+ assert response.content_type == content_type
def test_toggle_following_repository(self, backend):
user = self.log_user()
repo = Repository.get_by_repo_name(backend.repo_name)
repo_id = repo.repo_id
- self.app.post(url('toggle_following'), {'follows_repo_id': repo_id,
- 'csrf_token': self.csrf_token})
+ self.app.post(
+ route_path('toggle_following'), {'follows_repo_id': repo_id,
+ 'csrf_token': self.csrf_token})
followings = UserFollowing.query()\
.filter(UserFollowing.user_id == user['user_id'])\
@@ -44,8 +82,9 @@ class TestJournalController(TestControll
assert len(followings) == 0
- self.app.post(url('toggle_following'), {'follows_repo_id': repo_id,
- 'csrf_token': self.csrf_token})
+ self.app.post(
+ route_path('toggle_following'), {'follows_repo_id': repo_id,
+ 'csrf_token': self.csrf_token})
followings = UserFollowing.query()\
.filter(UserFollowing.user_id == user['user_id'])\
@@ -53,12 +92,15 @@ class TestJournalController(TestControll
assert len(followings) == 1
- def test_public_journal_atom(self):
+ @pytest.mark.parametrize("feed_type, content_type", [
+ ('rss', "application/rss+xml"),
+ ('atom', "application/atom+xml")
+ ])
+ def test_public_journal_feed(self, feed_type, content_type):
self.log_user()
- response = self.app.get(url(controller='journal',
- action='public_journal_atom'),)
+ response = self.app.get(
+ route_path(
+ 'journal_public_{}'.format(feed_type)),
+ status=200)
- def test_public_journal_rss(self):
- self.log_user()
- response = self.app.get(url(controller='journal',
- action='public_journal_rss'),)
+ assert response.content_type == content_type
diff --git a/rhodecode/apps/journal/views.py b/rhodecode/apps/journal/views.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/journal/views.py
@@ -0,0 +1,384 @@
+# -*- 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 logging
+import itertools
+
+from webhelpers.feedgenerator import Atom1Feed, Rss201rev2Feed
+
+from pyramid.view import view_config
+from pyramid.httpexceptions import HTTPBadRequest
+from pyramid.response import Response
+from pyramid.renderers import render
+
+from rhodecode.apps._base import BaseAppView
+from rhodecode.model.db import (
+ or_, joinedload, 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.utils2 import safe_int, AttributeDict, md5_safe
+from rhodecode.model.scm import ScmModel
+
+log = logging.getLogger(__name__)
+
+
+class JournalView(BaseAppView):
+
+ def load_default_context(self):
+ c = self._get_local_tmpl_context(include_app_defaults=True)
+ self._register_global_c(c)
+ self._load_defaults(c.rhodecode_name)
+
+ # TODO(marcink): what is this, why we need a global register ?
+ c.search_term = self.request.GET.get('filter') or ''
+ return c
+
+ def _get_config(self, rhodecode_name):
+ import rhodecode
+ config = rhodecode.CONFIG
+
+ return {
+ 'language': 'en-us',
+ 'feed_ttl': '5', # TTL of feed,
+ 'feed_items_per_page':
+ safe_int(config.get('rss_items_per_page', 20)),
+ 'rhodecode_name': rhodecode_name
+ }
+
+ def _load_defaults(self, rhodecode_name):
+ config = self._get_config(rhodecode_name)
+ # common values for feeds
+ self.language = config["language"]
+ self.ttl = config["feed_ttl"]
+ self.feed_items_per_page = config['feed_items_per_page']
+ self.rhodecode_name = config['rhodecode_name']
+
+ def _get_daily_aggregate(self, journal):
+ groups = []
+ for k, g in itertools.groupby(journal, lambda x: x.action_as_day):
+ user_group = []
+ # groupby username if it's a present value, else
+ # fallback to journal username
+ for _, g2 in itertools.groupby(
+ list(g), lambda x: x.user.username if x.user else x.username):
+ l = list(g2)
+ user_group.append((l[0].user, l))
+
+ groups.append((k, user_group,))
+
+ return groups
+
+ def _get_journal_data(self, following_repos, search_term):
+ repo_ids = [x.follows_repository.repo_id for x in following_repos
+ if x.follows_repository is not None]
+ user_ids = [x.follows_user.user_id for x in following_repos
+ if x.follows_user is not None]
+
+ filtering_criterion = None
+
+ if repo_ids and user_ids:
+ filtering_criterion = or_(UserLog.repository_id.in_(repo_ids),
+ UserLog.user_id.in_(user_ids))
+ if repo_ids and not user_ids:
+ filtering_criterion = UserLog.repository_id.in_(repo_ids)
+ if not repo_ids and user_ids:
+ filtering_criterion = UserLog.user_id.in_(user_ids)
+ if filtering_criterion is not None:
+ journal = Session().query(UserLog)\
+ .options(joinedload(UserLog.user))\
+ .options(joinedload(UserLog.repository))
+ # filter
+ try:
+ journal = user_log_filter(journal, search_term)
+ except Exception:
+ # we want this to crash for now
+ raise
+ journal = journal.filter(filtering_criterion)\
+ .order_by(UserLog.action_date.desc())
+ else:
+ journal = []
+
+ return journal
+
+ def feed_uid(self, entry_id):
+ return '{}:{}'.format('journal', md5_safe(entry_id))
+
+ def _atom_feed(self, repos, search_term, public=True):
+ _ = self.request.translate
+ journal = self._get_journal_data(repos, search_term)
+ if public:
+ _link = h.route_url('journal_public_atom')
+ _desc = '%s %s %s' % (self.rhodecode_name, _('public journal'),
+ 'atom feed')
+ else:
+ _link = h.route_url('journal_atom')
+ _desc = '%s %s %s' % (self.rhodecode_name, _('journal'), 'atom feed')
+
+ feed = Atom1Feed(
+ title=_desc, link=_link, description=_desc,
+ language=self.language, ttl=self.ttl)
+
+ for entry in journal[:self.feed_items_per_page]:
+ user = entry.user
+ if user is None:
+ # fix deleted users
+ user = AttributeDict({'short_contact': entry.username,
+ 'email': '',
+ 'full_contact': ''})
+ action, action_extra, ico = h.action_parser(entry, feed=True)
+ title = "%s - %s %s" % (user.short_contact, action(),
+ entry.repository.repo_name)
+ desc = action_extra()
+ _url = h.route_url('home')
+ if entry.repository is not None:
+ _url = h.route_url('repo_changelog',
+ repo_name=entry.repository.repo_name)
+
+ feed.add_item(
+ unique_id=self.feed_uid(entry.user_log_id),
+ title=title,
+ pubdate=entry.action_date,
+ link=_url,
+ author_email=user.email,
+ author_name=user.full_contact,
+ description=desc)
+
+ response = Response(feed.writeString('utf-8'))
+ response.content_type = feed.mime_type
+ return response
+
+ def _rss_feed(self, repos, search_term, public=True):
+ _ = self.request.translate
+ journal = self._get_journal_data(repos, search_term)
+ if public:
+ _link = h.route_url('journal_public_atom')
+ _desc = '%s %s %s' % (
+ self.rhodecode_name, _('public journal'), 'rss feed')
+ else:
+ _link = h.route_url('journal_atom')
+ _desc = '%s %s %s' % (
+ self.rhodecode_name, _('journal'), 'rss feed')
+
+ feed = Rss201rev2Feed(
+ title=_desc, link=_link, description=_desc,
+ language=self.language, ttl=self.ttl)
+
+ for entry in journal[:self.feed_items_per_page]:
+ user = entry.user
+ if user is None:
+ # fix deleted users
+ user = AttributeDict({'short_contact': entry.username,
+ 'email': '',
+ 'full_contact': ''})
+ action, action_extra, ico = h.action_parser(entry, feed=True)
+ title = "%s - %s %s" % (user.short_contact, action(),
+ entry.repository.repo_name)
+ desc = action_extra()
+ _url = h.route_url('home')
+ if entry.repository is not None:
+ _url = h.route_url('repo_changelog',
+ repo_name=entry.repository.repo_name)
+
+ feed.add_item(
+ unique_id=self.feed_uid(entry.user_log_id),
+ title=title,
+ pubdate=entry.action_date,
+ link=_url,
+ author_email=user.email,
+ author_name=user.full_contact,
+ description=desc)
+
+ response = Response(feed.writeString('utf-8'))
+ response.content_type = feed.mime_type
+ return response
+
+ @LoginRequired()
+ @NotAnonymous()
+ @view_config(
+ route_name='journal', request_method='GET',
+ renderer=None)
+ def journal(self):
+ c = self.load_default_context()
+
+ p = safe_int(self.request.GET.get('page', 1), 1)
+ c.user = User.get(self._rhodecode_user.user_id)
+ following = Session().query(UserFollowing)\
+ .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
+ .options(joinedload(UserFollowing.follows_repository))\
+ .all()
+
+ journal = self._get_journal_data(following, c.search_term)
+
+ def url_generator(**kw):
+ query_params = {
+ 'filter': c.search_term
+ }
+ query_params.update(kw)
+ return self.request.current_route_path(_query=query_params)
+
+ c.journal_pager = Page(
+ journal, page=p, items_per_page=20, url=url_generator)
+ c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
+
+ c.journal_data = render(
+ 'rhodecode:templates/journal/journal_data.mako',
+ self._get_template_context(c), self.request)
+
+ if self.request.is_xhr:
+ return Response(c.journal_data)
+
+ html = render(
+ 'rhodecode:templates/journal/journal.mako',
+ self._get_template_context(c), self.request)
+ return Response(html)
+
+ @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
+ @NotAnonymous()
+ @view_config(
+ route_name='journal_atom', request_method='GET',
+ renderer=None)
+ def journal_atom(self):
+ """
+ Produce an atom-1.0 feed via feedgenerator module
+ """
+ c = self.load_default_context()
+ following_repos = Session().query(UserFollowing)\
+ .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
+ .options(joinedload(UserFollowing.follows_repository))\
+ .all()
+ return self._atom_feed(following_repos, c.search_term, public=False)
+
+ @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
+ @NotAnonymous()
+ @view_config(
+ route_name='journal_rss', request_method='GET',
+ renderer=None)
+ def journal_rss(self):
+ """
+ Produce an rss feed via feedgenerator module
+ """
+ c = self.load_default_context()
+ following_repos = Session().query(UserFollowing)\
+ .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
+ .options(joinedload(UserFollowing.follows_repository))\
+ .all()
+ return self._rss_feed(following_repos, c.search_term, public=False)
+
+ @LoginRequired()
+ @NotAnonymous()
+ @CSRFRequired()
+ @view_config(
+ route_name='toggle_following', request_method='POST',
+ renderer='json_ext')
+ def toggle_following(self):
+ user_id = self.request.POST.get('follows_user_id')
+ if user_id:
+ try:
+ ScmModel().toggle_following_user(
+ user_id, self._rhodecode_user.user_id)
+ Session().commit()
+ return 'ok'
+ except Exception:
+ raise HTTPBadRequest()
+
+ repo_id = self.request.POST.get('follows_repo_id')
+ if repo_id:
+ try:
+ ScmModel().toggle_following_repo(
+ repo_id, self._rhodecode_user.user_id)
+ Session().commit()
+ return 'ok'
+ except Exception:
+ raise HTTPBadRequest()
+
+ raise HTTPBadRequest()
+
+ @LoginRequired()
+ @view_config(
+ route_name='journal_public', request_method='GET',
+ renderer=None)
+ def journal_public(self):
+ c = self.load_default_context()
+ # Return a rendered template
+ p = safe_int(self.request.GET.get('page', 1), 1)
+
+ c.following = Session().query(UserFollowing)\
+ .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
+ .options(joinedload(UserFollowing.follows_repository))\
+ .all()
+
+ journal = self._get_journal_data(c.following, c.search_term)
+
+ def url_generator(**kw):
+ query_params = {}
+ query_params.update(kw)
+ return self.request.current_route_path(_query=query_params)
+
+ c.journal_pager = Page(
+ journal, page=p, items_per_page=20, url=url_generator)
+ c.journal_day_aggreagate = self._get_daily_aggregate(c.journal_pager)
+
+ c.journal_data = render(
+ 'rhodecode:templates/journal/journal_data.mako',
+ self._get_template_context(c), self.request)
+
+ if self.request.is_xhr:
+ return Response(c.journal_data)
+
+ html = render(
+ 'rhodecode:templates/journal/public_journal.mako',
+ self._get_template_context(c), self.request)
+ return Response(html)
+
+ @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
+ @view_config(
+ route_name='journal_public_atom', request_method='GET',
+ renderer=None)
+ def journal_public_atom(self):
+ """
+ Produce an atom-1.0 feed via feedgenerator module
+ """
+ c = self.load_default_context()
+ following_repos = Session().query(UserFollowing)\
+ .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
+ .options(joinedload(UserFollowing.follows_repository))\
+ .all()
+
+ return self._atom_feed(following_repos, c.search_term)
+
+ @LoginRequired(auth_token_access=[UserApiKeys.ROLE_FEED])
+ @view_config(
+ route_name='journal_public_rss', request_method='GET',
+ renderer=None)
+ def journal_public_rss(self):
+ """
+ Produce an rss2 feed via feedgenerator module
+ """
+ c = self.load_default_context()
+ following_repos = Session().query(UserFollowing)\
+ .filter(UserFollowing.user_id == self._rhodecode_user.user_id)\
+ .options(joinedload(UserFollowing.follows_repository))\
+ .all()
+
+ return self._rss_feed(following_repos, c.search_term)
diff --git a/rhodecode/apps/login/__init__.py b/rhodecode/apps/login/__init__.py
--- a/rhodecode/apps/login/__init__.py
+++ b/rhodecode/apps/login/__init__.py
@@ -41,4 +41,4 @@ def includeme(config):
pattern=ADMIN_PREFIX + '/password_reset_confirmation')
# Scan module for configuration decorators.
- config.scan()
+ config.scan('.views', ignore='.tests')
diff --git a/rhodecode/tests/functional/test_login.py b/rhodecode/apps/login/tests/test_login.py
rename from rhodecode/tests/functional/test_login.py
rename to rhodecode/apps/login/tests/test_login.py
--- a/rhodecode/tests/functional/test_login.py
+++ b/rhodecode/apps/login/tests/test_login.py
@@ -23,9 +23,8 @@ import urlparse
import mock
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, HG_REPO, TEST_USER_ADMIN_LOGIN,
no_newline_id_generator)
from rhodecode.tests.fixture import Fixture
from rhodecode.lib.auth import check_password
@@ -37,14 +36,34 @@ from rhodecode.model.meta import Session
fixture = Fixture()
-# Hardcode URLs because we don't have a request object to use
-# pyramids URL generation methods.
-index_url = '/'
-login_url = ADMIN_PREFIX + '/login'
-logut_url = ADMIN_PREFIX + '/logout'
-register_url = ADMIN_PREFIX + '/register'
-pwd_reset_url = ADMIN_PREFIX + '/password_reset'
-pwd_reset_confirm_url = ADMIN_PREFIX + '/password_reset_confirmation'
+whitelist_view = ['RepoCommitsView:repo_commit_raw']
+
+
+def route_path(name, params=None, **kwargs):
+ import urllib
+ from rhodecode.apps._base import ADMIN_PREFIX
+
+ base_url = {
+ 'login': ADMIN_PREFIX + '/login',
+ 'logout': ADMIN_PREFIX + '/logout',
+ 'register': ADMIN_PREFIX + '/register',
+ 'reset_password':
+ ADMIN_PREFIX + '/password_reset',
+ 'reset_password_confirmation':
+ ADMIN_PREFIX + '/password_reset_confirmation',
+
+ 'admin_permissions_application':
+ ADMIN_PREFIX + '/permissions/application',
+ 'admin_permissions_application_update':
+ ADMIN_PREFIX + '/permissions/application/update',
+
+ 'repo_commit_raw': '/{repo_name}/raw-changeset/{commit_id}'
+
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
@pytest.mark.usefixtures('app')
@@ -63,12 +82,12 @@ class TestLoginController(object):
assert Notification.query().all() == []
def test_index(self):
- response = self.app.get(login_url)
+ response = self.app.get(route_path('login'))
assert response.status == '200 OK'
# Test response...
def test_login_admin_ok(self):
- response = self.app.post(login_url,
+ response = self.app.post(route_path('login'),
{'username': 'test_admin',
'password': 'test12'})
assert response.status == '302 Found'
@@ -79,7 +98,7 @@ class TestLoginController(object):
response.mustcontain('/%s' % HG_REPO)
def test_login_regular_ok(self):
- response = self.app.post(login_url,
+ response = self.app.post(route_path('login'),
{'username': 'test_regular',
'password': 'test12'})
@@ -92,7 +111,7 @@ class TestLoginController(object):
def test_login_ok_came_from(self):
test_came_from = '/_admin/users?branch=stable'
- _url = '{}?came_from={}'.format(login_url, test_came_from)
+ _url = '{}?came_from={}'.format(route_path('login'), test_came_from)
response = self.app.post(
_url, {'username': 'test_admin', 'password': 'test12'})
assert response.status == '302 Found'
@@ -113,7 +132,7 @@ class TestLoginController(object):
assert 'branch=stable' in response_query[0][1]
def test_login_form_with_get_args(self):
- _url = '{}?came_from=/_admin/users,branch=stable'.format(login_url)
+ _url = '{}?came_from=/_admin/users,branch=stable'.format(route_path('login'))
response = self.app.get(_url)
assert 'branch%3Dstable' in response.form.action
@@ -126,7 +145,7 @@ class TestLoginController(object):
'/\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)
+ _url = '{}?came_from={}'.format(route_path('login'), url_came_from)
response = self.app.post(
_url,
{'username': 'test_admin', 'password': 'test12'})
@@ -136,7 +155,7 @@ class TestLoginController(object):
assert response.request.path == '/'
def test_login_short_password(self):
- response = self.app.post(login_url,
+ response = self.app.post(route_path('login'),
{'username': 'test_admin',
'password': 'as'})
assert response.status == '200 OK'
@@ -145,7 +164,7 @@ class TestLoginController(object):
def test_login_wrong_non_ascii_password(self, user_regular):
response = self.app.post(
- login_url,
+ route_path('login'),
{'username': user_regular.username,
'password': u'invalid-non-asci\xe4'.encode('utf8')})
@@ -156,13 +175,13 @@ class TestLoginController(object):
password = u'valid-non-ascii\xe4'
user = user_util.create_user(password=password)
response = self.app.post(
- login_url,
+ route_path('login'),
{'username': user.username,
'password': password.encode('utf-8')})
assert response.status_code == 302
def test_login_wrong_username_password(self):
- response = self.app.post(login_url,
+ response = self.app.post(route_path('login'),
{'username': 'error',
'password': 'test12'})
@@ -180,7 +199,7 @@ class TestLoginController(object):
Session().add(user)
Session().commit()
self.destroy_users.add(temp_user)
- response = self.app.post(login_url,
+ response = self.app.post(route_path('login'),
{'username': temp_user,
'password': 'test123'})
@@ -197,13 +216,13 @@ class TestLoginController(object):
# REGISTRATIONS
def test_register(self):
- response = self.app.get(register_url)
+ response = self.app.get(route_path('register'))
response.mustcontain('Create an Account')
def test_register_err_same_username(self):
uname = 'test_admin'
response = self.app.post(
- register_url,
+ route_path('register'),
{
'username': uname,
'password': 'test12',
@@ -221,7 +240,7 @@ class TestLoginController(object):
def test_register_err_same_email(self):
response = self.app.post(
- register_url,
+ route_path('register'),
{
'username': 'test_admin_0',
'password': 'test12',
@@ -238,7 +257,7 @@ class TestLoginController(object):
def test_register_err_same_email_case_sensitive(self):
response = self.app.post(
- register_url,
+ route_path('register'),
{
'username': 'test_admin_1',
'password': 'test12',
@@ -254,7 +273,7 @@ class TestLoginController(object):
def test_register_err_wrong_data(self):
response = self.app.post(
- register_url,
+ route_path('register'),
{
'username': 'xs',
'password': 'test',
@@ -270,7 +289,7 @@ class TestLoginController(object):
def test_register_err_username(self):
response = self.app.post(
- register_url,
+ route_path('register'),
{
'username': 'error user',
'password': 'test12',
@@ -291,7 +310,7 @@ class TestLoginController(object):
def test_register_err_case_sensitive(self):
usr = 'Test_Admin'
response = self.app.post(
- register_url,
+ route_path('register'),
{
'username': usr,
'password': 'test12',
@@ -309,7 +328,7 @@ class TestLoginController(object):
def test_register_special_chars(self):
response = self.app.post(
- register_url,
+ route_path('register'),
{
'username': 'xxxaxn',
'password': 'ąćźżąśśśś',
@@ -325,7 +344,7 @@ class TestLoginController(object):
def test_register_password_mismatch(self):
response = self.app.post(
- register_url,
+ route_path('register'),
{
'username': 'xs',
'password': '123qwe',
@@ -346,7 +365,7 @@ class TestLoginController(object):
lastname = 'testlastname'
response = self.app.post(
- register_url,
+ route_path('register'),
{
'username': username,
'password': password,
@@ -374,29 +393,29 @@ class TestLoginController(object):
def test_forgot_password_wrong_mail(self):
bad_email = 'marcin@wrongmail.org'
response = self.app.post(
- pwd_reset_url, {'email': bad_email, }
+ route_path('reset_password'), {'email': bad_email, }
)
assert_session_flash(response,
'If such email exists, a password reset link was sent to it.')
def test_forgot_password(self, user_util):
- response = self.app.get(pwd_reset_url)
+ response = self.app.get(route_path('reset_password'))
assert response.status == '200 OK'
user = user_util.create_user()
user_id = user.user_id
email = user.email
- response = self.app.post(pwd_reset_url, {'email': email, })
+ response = self.app.post(route_path('reset_password'), {'email': email, })
assert_session_flash(response,
'If such email exists, a password reset link was sent to it.')
# BAD KEY
- confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, 'badkey')
+ confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), 'badkey')
response = self.app.get(confirm_url)
assert response.status == '302 Found'
- assert response.location.endswith(pwd_reset_url)
+ assert response.location.endswith(route_path('reset_password'))
assert_session_flash(response, 'Given reset token is invalid')
response.follow() # cleanup flash
@@ -409,10 +428,10 @@ class TestLoginController(object):
assert key
- confirm_url = '{}?key={}'.format(pwd_reset_confirm_url, key.api_key)
+ confirm_url = '{}?key={}'.format(route_path('reset_password_confirmation'), key.api_key)
response = self.app.get(confirm_url)
assert response.status == '302 Found'
- assert response.location.endswith(login_url)
+ assert response.location.endswith(route_path('login'))
assert_session_flash(
response,
@@ -442,11 +461,11 @@ class TestLoginController(object):
auth_token = user_admin.api_key
with fixture.anon_access(False):
- self.app.get(url(controller='changeset',
- action='changeset_raw',
- repo_name=HG_REPO, revision='tip',
- api_key=auth_token),
- status=302)
+ self.app.get(
+ route_path('repo_commit_raw',
+ repo_name=HG_REPO, commit_id='tip',
+ params=dict(api_key=auth_token)),
+ status=302)
@pytest.mark.parametrize("test_name, auth_token, code", [
('none', None, 302),
@@ -457,45 +476,67 @@ class TestLoginController(object):
def test_access_whitelisted_page_via_auth_token(
self, test_name, auth_token, code, user_admin):
- whitelist_entry = ['ChangesetController:changeset_raw']
- whitelist = self._get_api_whitelist(whitelist_entry)
+ whitelist = self._get_api_whitelist(whitelist_view)
with mock.patch.dict('rhodecode.CONFIG', whitelist):
- assert whitelist_entry == whitelist['api_access_controllers_whitelist']
+ assert whitelist_view == whitelist['api_access_controllers_whitelist']
if test_name == 'proper_auth_token':
auth_token = user_admin.api_key
assert auth_token
with fixture.anon_access(False):
- self.app.get(url(controller='changeset',
- action='changeset_raw',
- repo_name=HG_REPO, revision='tip',
- api_key=auth_token),
- status=code)
+ self.app.get(
+ route_path('repo_commit_raw',
+ repo_name=HG_REPO, commit_id='tip',
+ params=dict(api_key=auth_token)),
+ status=code)
+
+ @pytest.mark.parametrize("test_name, auth_token, code", [
+ ('proper_auth_token', None, 200),
+ ('wrong_auth_token', '123456', 302),
+ ])
+ def test_access_whitelisted_page_via_auth_token_bound_to_token(
+ self, test_name, auth_token, code, user_admin):
+
+ expected_token = auth_token
+ if test_name == 'proper_auth_token':
+ auth_token = user_admin.api_key
+ expected_token = auth_token
+ assert auth_token
+
+ whitelist = self._get_api_whitelist([
+ 'RepoCommitsView:repo_commit_raw@{}'.format(expected_token)])
+
+ with mock.patch.dict('rhodecode.CONFIG', whitelist):
+
+ with fixture.anon_access(False):
+ self.app.get(
+ route_path('repo_commit_raw',
+ repo_name=HG_REPO, commit_id='tip',
+ params=dict(api_key=auth_token)),
+ status=code)
def test_access_page_via_extra_auth_token(self):
- whitelist = self._get_api_whitelist(
- ['ChangesetController:changeset_raw'])
+ whitelist = self._get_api_whitelist(whitelist_view)
with mock.patch.dict('rhodecode.CONFIG', whitelist):
- assert ['ChangesetController:changeset_raw'] == \
+ assert whitelist_view == \
whitelist['api_access_controllers_whitelist']
new_auth_token = AuthTokenModel().create(
TEST_USER_ADMIN_LOGIN, 'test')
Session().commit()
with fixture.anon_access(False):
- self.app.get(url(controller='changeset',
- action='changeset_raw',
- repo_name=HG_REPO, revision='tip',
- api_key=new_auth_token.api_key),
- status=200)
+ self.app.get(
+ route_path('repo_commit_raw',
+ repo_name=HG_REPO, commit_id='tip',
+ params=dict(api_key=new_auth_token.api_key)),
+ status=200)
def test_access_page_via_expired_auth_token(self):
- whitelist = self._get_api_whitelist(
- ['ChangesetController:changeset_raw'])
+ whitelist = self._get_api_whitelist(whitelist_view)
with mock.patch.dict('rhodecode.CONFIG', whitelist):
- assert ['ChangesetController:changeset_raw'] == \
+ assert whitelist_view == \
whitelist['api_access_controllers_whitelist']
new_auth_token = AuthTokenModel().create(
@@ -506,8 +547,8 @@ class TestLoginController(object):
Session().add(new_auth_token)
Session().commit()
with fixture.anon_access(False):
- self.app.get(url(controller='changeset',
- action='changeset_raw',
- repo_name=HG_REPO, revision='tip',
- api_key=new_auth_token.api_key),
- status=302)
+ self.app.get(
+ route_path('repo_commit_raw',
+ repo_name=HG_REPO, commit_id='tip',
+ params=dict(api_key=new_auth_token.api_key)),
+ status=302)
diff --git a/rhodecode/tests/functional/test_password_reset.py b/rhodecode/apps/login/tests/test_password_reset.py
rename from rhodecode/tests/functional/test_password_reset.py
rename to rhodecode/apps/login/tests/test_password_reset.py
--- a/rhodecode/tests/functional/test_password_reset.py
+++ b/rhodecode/apps/login/tests/test_password_reset.py
@@ -20,23 +20,38 @@
import pytest
-from rhodecode.config.routing import ADMIN_PREFIX
+from rhodecode.lib import helpers as h
from rhodecode.tests import (
- TestController, clear_all_caches, url,
+ TestController, clear_all_caches,
TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
from rhodecode.tests.fixture import Fixture
from rhodecode.tests.utils import AssertResponse
fixture = Fixture()
-# Hardcode URLs because we don't have a request object to use
-# pyramids URL generation methods.
-index_url = '/'
-login_url = ADMIN_PREFIX + '/login'
-logut_url = ADMIN_PREFIX + '/logout'
-register_url = ADMIN_PREFIX + '/register'
-pwd_reset_url = ADMIN_PREFIX + '/password_reset'
-pwd_reset_confirm_url = ADMIN_PREFIX + '/password_reset_confirmation'
+
+def route_path(name, params=None, **kwargs):
+ import urllib
+ from rhodecode.apps._base import ADMIN_PREFIX
+
+ base_url = {
+ 'login': ADMIN_PREFIX + '/login',
+ 'logout': ADMIN_PREFIX + '/logout',
+ 'register': ADMIN_PREFIX + '/register',
+ 'reset_password':
+ ADMIN_PREFIX + '/password_reset',
+ 'reset_password_confirmation':
+ ADMIN_PREFIX + '/password_reset_confirmation',
+
+ 'admin_permissions_application':
+ ADMIN_PREFIX + '/permissions/application',
+ 'admin_permissions_application_update':
+ ADMIN_PREFIX + '/permissions/application/update',
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
class TestPasswordReset(TestController):
@@ -59,12 +74,12 @@ class TestPasswordReset(TestController):
'default_password_reset': pwd_reset_setting,
'default_extern_activate': 'hg.extern_activate.auto',
}
- resp = self.app.post(url('admin_permissions_application'), params=params)
+ resp = self.app.post(route_path('admin_permissions_application_update'), params=params)
self.logout_user()
- login_page = self.app.get(login_url)
+ login_page = self.app.get(route_path('login'))
asr_login = AssertResponse(login_page)
- index_page = self.app.get(index_url)
+ index_page = self.app.get(h.route_path('home'))
asr_index = AssertResponse(index_page)
if show_link:
@@ -74,7 +89,7 @@ class TestPasswordReset(TestController):
asr_login.no_element_exists('a.pwd_reset')
asr_index.no_element_exists('a.pwd_reset')
- response = self.app.get(pwd_reset_url)
+ response = self.app.get(route_path('reset_password'))
assert_response = AssertResponse(response)
if show_reset:
@@ -96,11 +111,11 @@ class TestPasswordReset(TestController):
'default_password_reset': 'hg.password_reset.disabled',
'default_extern_activate': 'hg.extern_activate.auto',
}
- self.app.post(url('admin_permissions_application'), params=params)
+ self.app.post(route_path('admin_permissions_application_update'), params=params)
self.logout_user()
response = self.app.post(
- pwd_reset_url, {'email': 'lisa@rhodecode.com',}
+ route_path('reset_password'), {'email': 'lisa@rhodecode.com',}
)
response = response.follow()
response.mustcontain('Password reset is disabled.')
diff --git a/rhodecode/apps/login/tests/test_register_captcha.py b/rhodecode/apps/login/tests/test_register_captcha.py
--- a/rhodecode/apps/login/tests/test_register_captcha.py
+++ b/rhodecode/apps/login/tests/test_register_captcha.py
@@ -60,7 +60,7 @@ class TestRegisterCaptcha(object):
])
def test_get_captcha_data(self, private_key, public_key, expected, db,
request_stub, user_util):
- request_stub.user = user_util.create_user().AuthUser
+ request_stub.user = user_util.create_user().AuthUser()
request_stub.matched_route = AttributeDict({'name': 'login'})
login_view = LoginView(mock.Mock(), request_stub)
diff --git a/rhodecode/apps/login/views.py b/rhodecode/apps/login/views.py
--- a/rhodecode/apps/login/views.py
+++ b/rhodecode/apps/login/views.py
@@ -22,6 +22,7 @@ import time
import collections
import datetime
import formencode
+import formencode.htmlfill
import logging
import urlparse
@@ -160,7 +161,7 @@ class LoginView(BaseAppView):
try:
self.session.invalidate()
- form_result = login_form.to_python(self.request.params)
+ form_result = login_form.to_python(self.request.POST)
# form checks for username/password, now we're authenticated
headers = _store_user_in_session(
self.session,
@@ -169,7 +170,7 @@ class LoginView(BaseAppView):
log.debug('Redirecting to "%s" after login.', c.came_from)
audit_user = audit_logger.UserWrap(
- username=self.request.params.get('username'),
+ username=self.request.POST.get('username'),
ip_addr=self.request.remote_addr)
action_data = {'user_agent': self.request.user_agent}
audit_logger.store_web(
@@ -188,7 +189,7 @@ class LoginView(BaseAppView):
})
audit_user = audit_logger.UserWrap(
- username=self.request.params.get('username'),
+ username=self.request.POST.get('username'),
ip_addr=self.request.remote_addr)
action_data = {'user_agent': self.request.user_agent}
audit_logger.store_web(
@@ -231,7 +232,7 @@ class LoginView(BaseAppView):
register_message = settings.get('rhodecode_register_message') or ''
captcha = self._get_captcha_data()
auto_active = 'hg.register.auto_activate' in User.get_default_user()\
- .AuthUser.permissions['global']
+ .AuthUser().permissions['global']
render_ctx = self._get_template_context(c)
render_ctx.update({
@@ -252,17 +253,18 @@ class LoginView(BaseAppView):
def register_post(self):
captcha = self._get_captcha_data()
auto_active = 'hg.register.auto_activate' in User.get_default_user()\
- .AuthUser.permissions['global']
+ .AuthUser().permissions['global']
register_form = RegisterForm()()
try:
- form_result = register_form.to_python(self.request.params)
+
+ form_result = register_form.to_python(self.request.POST)
form_result['active'] = auto_active
if captcha.active:
response = submit(
- self.request.params.get('recaptcha_challenge_field'),
- self.request.params.get('recaptcha_response_field'),
+ self.request.POST.get('recaptcha_challenge_field'),
+ self.request.POST.get('recaptcha_response_field'),
private_key=captcha.private_key,
remoteip=get_ip_addr(self.request.environ))
if not response.is_valid:
@@ -325,13 +327,13 @@ class LoginView(BaseAppView):
password_reset_form = PasswordResetForm()()
try:
form_result = password_reset_form.to_python(
- self.request.params)
+ self.request.POST)
user_email = form_result['email']
if captcha.active:
response = submit(
- self.request.params.get('recaptcha_challenge_field'),
- self.request.params.get('recaptcha_response_field'),
+ self.request.POST.get('recaptcha_challenge_field'),
+ self.request.POST.get('recaptcha_response_field'),
private_key=captcha.private_key,
remoteip=get_ip_addr(self.request.environ))
if not response.is_valid:
@@ -374,7 +376,7 @@ class LoginView(BaseAppView):
'defaults': errors.value,
'errors': errors.error_dict,
})
- if not self.request.params.get('email'):
+ if not self.request.POST.get('email'):
# case of empty email, we want to report that
return render_ctx
diff --git a/rhodecode/apps/my_account/__init__.py b/rhodecode/apps/my_account/__init__.py
--- a/rhodecode/apps/my_account/__init__.py
+++ b/rhodecode/apps/my_account/__init__.py
@@ -28,14 +28,24 @@ def includeme(config):
name='my_account_profile',
pattern=ADMIN_PREFIX + '/my_account/profile')
+ # my account edit details
+ config.add_route(
+ name='my_account_edit',
+ pattern=ADMIN_PREFIX + '/my_account/edit')
+ config.add_route(
+ name='my_account_update',
+ pattern=ADMIN_PREFIX + '/my_account/update')
+
+ # my account password
config.add_route(
name='my_account_password',
pattern=ADMIN_PREFIX + '/my_account/password')
config.add_route(
name='my_account_password_update',
- pattern=ADMIN_PREFIX + '/my_account/password')
+ pattern=ADMIN_PREFIX + '/my_account/password/update')
+ # my account tokens
config.add_route(
name='my_account_auth_tokens',
pattern=ADMIN_PREFIX + '/my_account/auth_tokens')
@@ -46,6 +56,21 @@ def includeme(config):
name='my_account_auth_tokens_delete',
pattern=ADMIN_PREFIX + '/my_account/auth_tokens/delete')
+ # my account ssh keys
+ config.add_route(
+ name='my_account_ssh_keys',
+ pattern=ADMIN_PREFIX + '/my_account/ssh_keys')
+ config.add_route(
+ name='my_account_ssh_keys_generate',
+ pattern=ADMIN_PREFIX + '/my_account/ssh_keys/generate')
+ config.add_route(
+ name='my_account_ssh_keys_add',
+ pattern=ADMIN_PREFIX + '/my_account/ssh_keys/new')
+ config.add_route(
+ name='my_account_ssh_keys_delete',
+ pattern=ADMIN_PREFIX + '/my_account/ssh_keys/delete')
+
+ # my account emails
config.add_route(
name='my_account_emails',
pattern=ADMIN_PREFIX + '/my_account/emails')
@@ -76,10 +101,40 @@ def includeme(config):
name='my_account_notifications_toggle_visibility',
pattern=ADMIN_PREFIX + '/my_account/toggle_visibility')
+ # my account pull requests
+ config.add_route(
+ name='my_account_pullrequests',
+ pattern=ADMIN_PREFIX + '/my_account/pull_requests')
+ config.add_route(
+ name='my_account_pullrequests_data',
+ pattern=ADMIN_PREFIX + '/my_account/pull_requests/data')
+
+ # notifications
+ config.add_route(
+ name='notifications_show_all',
+ pattern=ADMIN_PREFIX + '/notifications')
+
+ # notifications
+ config.add_route(
+ name='notifications_mark_all_read',
+ pattern=ADMIN_PREFIX + '/notifications/mark_all_read')
+
+ config.add_route(
+ name='notifications_show',
+ pattern=ADMIN_PREFIX + '/notifications/{notification_id}')
+
+ config.add_route(
+ name='notifications_update',
+ pattern=ADMIN_PREFIX + '/notifications/{notification_id}/update')
+
+ config.add_route(
+ name='notifications_delete',
+ pattern=ADMIN_PREFIX + '/notifications/{notification_id}/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()
+ config.scan('.views', ignore='.tests')
diff --git a/rhodecode/apps/my_account/tests/test_my_account_auth_tokens.py b/rhodecode/apps/my_account/tests/test_my_account_auth_tokens.py
--- a/rhodecode/apps/my_account/tests/test_my_account_auth_tokens.py
+++ b/rhodecode/apps/my_account/tests/test_my_account_auth_tokens.py
@@ -87,7 +87,7 @@ class TestMyAccountAuthTokens(TestContro
self.log_user(user.username, 'qweqwe')
user = User.get(user_id)
- keys = user.extra_auth_tokens
+ keys = user.get_auth_tokens()
assert 2 == len(keys)
response = self.app.post(
@@ -98,7 +98,7 @@ class TestMyAccountAuthTokens(TestContro
response.follow()
user = User.get(user_id)
- keys = user.extra_auth_tokens
+ keys = user.get_auth_tokens()
assert 3 == len(keys)
response = self.app.post(
@@ -107,5 +107,5 @@ class TestMyAccountAuthTokens(TestContro
assert_session_flash(response, 'Auth token successfully deleted')
user = User.get(user_id)
- keys = user.extra_auth_tokens
+ keys = user.auth_tokens
assert 2 == len(keys)
diff --git a/rhodecode/apps/my_account/tests/test_my_account_edit.py b/rhodecode/apps/my_account/tests/test_my_account_edit.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/my_account/tests/test_my_account_edit.py
@@ -0,0 +1,205 @@
+# -*- 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/
+# -*- 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 pytest
+
+from rhodecode.model.db import User
+from rhodecode.tests import TestController, assert_session_flash
+from rhodecode.lib import helpers as h
+
+
+def route_path(name, params=None, **kwargs):
+ import urllib
+ from rhodecode.apps._base import ADMIN_PREFIX
+
+ base_url = {
+ 'my_account_edit': ADMIN_PREFIX + '/my_account/edit',
+ 'my_account_update': ADMIN_PREFIX + '/my_account/update',
+ 'my_account_pullrequests': ADMIN_PREFIX + '/my_account/pull_requests',
+ 'my_account_pullrequests_data': ADMIN_PREFIX + '/my_account/pull_requests/data',
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
+
+
+class TestMyAccountEdit(TestController):
+
+ def test_my_account_edit(self):
+ self.log_user()
+ response = self.app.get(route_path('my_account_edit'))
+
+ response.mustcontain('value="test_admin')
+
+ @pytest.mark.backends("git", "hg")
+ def test_my_account_my_pullrequests(self, pr_util):
+ self.log_user()
+ response = self.app.get(route_path('my_account_pullrequests'))
+ response.mustcontain('There are currently no open pull '
+ 'requests requiring your participation.')
+
+ @pytest.mark.backends("git", "hg")
+ def test_my_account_my_pullrequests_data(self, pr_util, xhr_header):
+ self.log_user()
+ response = self.app.get(route_path('my_account_pullrequests_data'),
+ extra_environ=xhr_header)
+ assert response.json == {
+ u'data': [], u'draw': None,
+ u'recordsFiltered': 0, u'recordsTotal': 0}
+
+ pr = pr_util.create_pull_request(title='TestMyAccountPR')
+ expected = {
+ 'author_raw': 'RhodeCode Admin',
+ 'name_raw': pr.pull_request_id
+ }
+ response = self.app.get(route_path('my_account_pullrequests_data'),
+ extra_environ=xhr_header)
+ assert response.json['recordsTotal'] == 1
+ assert response.json['data'][0]['author_raw'] == expected['author_raw']
+
+ assert response.json['data'][0]['author_raw'] == expected['author_raw']
+ assert response.json['data'][0]['name_raw'] == expected['name_raw']
+
+ @pytest.mark.parametrize(
+ "name, attrs", [
+ ('firstname', {'firstname': 'new_username'}),
+ ('lastname', {'lastname': 'new_username'}),
+ ('admin', {'admin': True}),
+ ('admin', {'admin': False}),
+ ('extern_type', {'extern_type': 'ldap'}),
+ ('extern_type', {'extern_type': None}),
+ # ('extern_name', {'extern_name': 'test'}),
+ # ('extern_name', {'extern_name': None}),
+ ('active', {'active': False}),
+ ('active', {'active': True}),
+ ('email', {'email': 'some@email.com'}),
+ ])
+ def test_my_account_update(self, name, attrs, user_util):
+ usr = user_util.create_user(password='qweqwe')
+ params = usr.get_api_data() # current user data
+ user_id = usr.user_id
+ self.log_user(
+ username=usr.username, password='qweqwe')
+
+ params.update({'password_confirmation': ''})
+ params.update({'new_password': ''})
+ params.update({'extern_type': 'rhodecode'})
+ params.update({'extern_name': 'rhodecode'})
+ params.update({'csrf_token': self.csrf_token})
+
+ params.update(attrs)
+ # my account page cannot set language param yet, only for admins
+ del params['language']
+ response = self.app.post(route_path('my_account_update'), params)
+
+ assert_session_flash(
+ response, 'Your account was updated successfully')
+
+ del params['csrf_token']
+
+ updated_user = User.get(user_id)
+ updated_params = updated_user.get_api_data()
+ updated_params.update({'password_confirmation': ''})
+ updated_params.update({'new_password': ''})
+
+ params['last_login'] = updated_params['last_login']
+ params['last_activity'] = updated_params['last_activity']
+ # my account page cannot set language param yet, only for admins
+ # but we get this info from API anyway
+ params['language'] = updated_params['language']
+
+ if name == 'email':
+ params['emails'] = [attrs['email']]
+ if name == 'extern_type':
+ # cannot update this via form, expected value is original one
+ params['extern_type'] = "rhodecode"
+ if name == 'extern_name':
+ # cannot update this via form, expected value is original one
+ params['extern_name'] = str(user_id)
+ if name == 'active':
+ # my account cannot deactivate account
+ params['active'] = True
+ if name == 'admin':
+ # my account cannot make you an admin !
+ params['admin'] = False
+
+ assert params == updated_params
+
+ def test_my_account_update_err_email_exists(self):
+ self.log_user()
+
+ new_email = 'test_regular@mail.com' # already existing email
+ params = {
+ 'username': 'test_admin',
+ 'new_password': 'test12',
+ 'password_confirmation': 'test122',
+ 'firstname': 'NewName',
+ 'lastname': 'NewLastname',
+ 'email': new_email,
+ 'csrf_token': self.csrf_token,
+ }
+
+ response = self.app.post(route_path('my_account_update'),
+ params=params)
+
+ response.mustcontain('This e-mail address is already taken')
+
+ def test_my_account_update_bad_email_address(self):
+ self.log_user('test_regular2', 'test12')
+
+ new_email = 'newmail.pl'
+ params = {
+ 'username': 'test_admin',
+ 'new_password': 'test12',
+ 'password_confirmation': 'test122',
+ 'firstname': 'NewName',
+ 'lastname': 'NewLastname',
+ 'email': new_email,
+ 'csrf_token': self.csrf_token,
+ }
+ response = self.app.post(route_path('my_account_update'),
+ params=params)
+
+ response.mustcontain('An email address must contain a single @')
+ from rhodecode.model import validators
+ msg = validators.ValidUsername(
+ edit=False, old_data={})._messages['username_exists']
+ msg = h.html_escape(msg % {'username': 'test_admin'})
+ response.mustcontain(u"%s" % msg)
diff --git a/rhodecode/tests/functional/test_admin_notifications.py b/rhodecode/apps/my_account/tests/test_my_account_notifications.py
rename from rhodecode/tests/functional/test_admin_notifications.py
rename to rhodecode/apps/my_account/tests/test_my_account_notifications.py
--- a/rhodecode/tests/functional/test_admin_notifications.py
+++ b/rhodecode/apps/my_account/tests/test_my_account_notifications.py
@@ -20,7 +20,10 @@
import pytest
-from rhodecode.tests import *
+from rhodecode.apps._base import ADMIN_PREFIX
+from rhodecode.tests import (
+ TestController, TEST_USER_REGULAR_LOGIN, TEST_USER_REGULAR_PASS,
+ TEST_USER_ADMIN_LOGIN, TEST_USER_ADMIN_PASS)
from rhodecode.tests.fixture import Fixture
from rhodecode.model.db import Notification, User
@@ -31,12 +34,25 @@ from rhodecode.model.meta import Session
fixture = Fixture()
-class TestNotificationsController(TestController):
- destroy_users = set()
+def route_path(name, params=None, **kwargs):
+ import urllib
+ from rhodecode.apps._base import ADMIN_PREFIX
- @classmethod
- def teardown_class(cls):
- fixture.destroy_users(cls.destroy_users)
+ base_url = {
+ 'notifications_show_all': ADMIN_PREFIX + '/notifications',
+ 'notifications_mark_all_read': ADMIN_PREFIX + '/notifications/mark_all_read',
+ 'notifications_show': ADMIN_PREFIX + '/notifications/{notification_id}',
+ 'notifications_update': ADMIN_PREFIX + '/notifications/{notification_id}/update',
+ 'notifications_delete': ADMIN_PREFIX + '/notifications/{notification_id}/delete',
+
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
+
+
+class TestNotificationsController(TestController):
def teardown_method(self, method):
for n in Notification.query().all():
@@ -44,43 +60,61 @@ class TestNotificationsController(TestCo
Session().delete(inst)
Session().commit()
- def test_index(self):
- u1 = UserModel().create_or_update(
- username='u1', password='qweqwe', email='u1@rhodecode.org',
- firstname='u1', lastname='u1')
- u1 = u1.user_id
- self.destroy_users.add('u1')
+ def test_show_all(self, user_util):
+ user = user_util.create_user(password='qweqwe')
+ user_id = user.user_id
+ self.log_user(user.username, 'qweqwe')
- self.log_user('u1', 'qweqwe')
-
- response = self.app.get(url('notifications'))
+ response = self.app.get(
+ route_path('notifications_show_all', params={'type': 'all'}))
response.mustcontain(
'No notifications here yet
')
- cur_user = self._get_logged_user()
- notif = NotificationModel().create(
- created_by=u1, notification_subject=u'test_notification_1',
- notification_body=u'notification_1', recipients=[cur_user])
+ notification = NotificationModel().create(
+ created_by=user_id, notification_subject=u'test_notification_1',
+ notification_body=u'notification_1', recipients=[user_id])
Session().commit()
- response = self.app.get(url('notifications'))
- response.mustcontain('id="notification_%s"' % notif.notification_id)
+ notification_id = notification.notification_id
+
+ response = self.app.get(route_path('notifications_show_all',
+ params={'type': 'all'}))
+ response.mustcontain('id="notification_%s"' % notification_id)
+
+ def test_show_unread(self, user_util):
+ user = user_util.create_user(password='qweqwe')
+ user_id = user.user_id
+ self.log_user(user.username, 'qweqwe')
+
+ response = self.app.get(route_path('notifications_show_all'))
+ response.mustcontain(
+ 'No notifications here yet
')
+
+ notification = NotificationModel().create(
+ created_by=user_id, notification_subject=u'test_notification_1',
+ notification_body=u'notification_1', recipients=[user_id])
+
+ # mark the USER notification as unread
+ user_notification = NotificationModel().get_user_notification(
+ user_id, notification)
+ user_notification.read = False
+
+ Session().commit()
+ notification_id = notification.notification_id
+
+ response = self.app.get(route_path('notifications_show_all'))
+ response.mustcontain('id="notification_%s"' % notification_id)
+ response.mustcontain('
.
+#
+# 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/my_account/views.py b/rhodecode/apps/my_account/views/my_account.py
rename from rhodecode/apps/my_account/views.py
rename to rhodecode/apps/my_account/views/my_account.py
--- a/rhodecode/apps/my_account/views.py
+++ b/rhodecode/apps/my_account/views/my_account.py
@@ -22,22 +22,29 @@ import logging
import datetime
import formencode
+import formencode.htmlfill
from pyramid.httpexceptions import HTTPFound
from pyramid.view import view_config
+from pyramid.renderers import render
+from pyramid.response import Response
-from rhodecode.apps._base import BaseAppView
+from rhodecode.apps._base import BaseAppView, DataGridAppView
from rhodecode import forms
from rhodecode.lib import helpers as h
from rhodecode.lib import audit_logger
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.lib.channelstream import (
+ channelstream_request, ChannelstreamException)
+from rhodecode.lib.utils2 import safe_int, md5, str2bool
from rhodecode.model.auth_token import AuthTokenModel
+from rhodecode.model.comment import CommentsModel
from rhodecode.model.db import (
- Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload)
+ Repository, UserEmailMap, UserApiKeys, UserFollowing, joinedload,
+ PullRequest)
+from rhodecode.model.forms import UserForm
from rhodecode.model.meta import Session
+from rhodecode.model.pull_request import PullRequestModel
from rhodecode.model.scm import RepoList
from rhodecode.model.user import UserModel
from rhodecode.model.repo import RepoModel
@@ -46,7 +53,7 @@ from rhodecode.model.validation_schema.s
log = logging.getLogger(__name__)
-class MyAccountView(BaseAppView):
+class MyAccountView(BaseAppView, DataGridAppView):
ALLOW_SCOPED_TOKENS = False
"""
This view has alternative version inside EE, if modified please take a look
@@ -84,7 +91,9 @@ class MyAccountView(BaseAppView):
username=c.user.username)
form = forms.Form(
- schema, buttons=(forms.buttons.save, forms.buttons.reset))
+ schema,
+ action=h.route_path('my_account_password_update'),
+ buttons=(forms.buttons.save, forms.buttons.reset))
c.form = form
return self._get_template_context(c)
@@ -93,7 +102,7 @@ class MyAccountView(BaseAppView):
@NotAnonymous()
@CSRFRequired()
@view_config(
- route_name='my_account_password', request_method='POST',
+ route_name='my_account_password_update', request_method='POST',
renderer='rhodecode:templates/admin/my_account/my_account.mako')
def my_account_password_update(self):
_ = self.request.translate
@@ -143,21 +152,14 @@ class MyAccountView(BaseAppView):
c = self.load_default_context()
c.active = 'auth_tokens'
-
- c.lifetime_values = [
- (str(-1), _('forever')),
- (str(5), _('5 minutes')),
- (str(60), _('1 hour')),
- (str(60 * 24), _('1 day')),
- (str(60 * 24 * 30), _('1 month')),
- ]
- c.lifetime_options = [(c.lifetime_values, _("Lifetime"))]
+ c.lifetime_values = AuthTokenModel.get_lifetime_values(translator=_)
c.role_values = [
(x, AuthTokenModel.cls._get_role_name(x))
for x in AuthTokenModel.cls.ROLES]
c.role_options = [(c.role_values, _("Role"))]
c.user_auth_tokens = AuthTokenModel().get_auth_tokens(
c.user.user_id, show_expired=True)
+ c.role_vcs = AuthTokenModel.cls.ROLE_VCS
return self._get_template_context(c)
def maybe_attach_token_scope(self, token):
@@ -203,7 +205,7 @@ class MyAccountView(BaseAppView):
del_auth_token = self.request.POST.get('del_auth_token')
if del_auth_token:
- token = UserApiKeys.get_or_404(del_auth_token, pyramid_exc=True)
+ token = UserApiKeys.get_or_404(del_auth_token)
token_data = token.get_api_data()
AuthTokenModel().delete(del_auth_token, c.user.user_id)
@@ -271,7 +273,7 @@ class MyAccountView(BaseAppView):
del_email_id = self.request.POST.get('del_email_id')
if del_email_id:
- email = UserEmailMap.get_or_404(del_email_id, pyramid_exc=True).email
+ email = UserEmailMap.get_or_404(del_email_id).email
UserModel().delete_extra_email(c.user.user_id, del_email_id)
audit_logger.store_web(
'user.edit.email.delete', action_data={
@@ -396,4 +398,183 @@ class MyAccountView(BaseAppView):
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
+ return user.user_data['notification_status']
+
+ @LoginRequired()
+ @NotAnonymous()
+ @view_config(
+ route_name='my_account_edit',
+ request_method='GET',
+ renderer='rhodecode:templates/admin/my_account/my_account.mako')
+ def my_account_edit(self):
+ c = self.load_default_context()
+ c.active = 'profile_edit'
+
+ c.perm_user = c.auth_user
+ c.extern_type = c.user.extern_type
+ c.extern_name = c.user.extern_name
+
+ defaults = c.user.get_dict()
+
+ data = render('rhodecode:templates/admin/my_account/my_account.mako',
+ self._get_template_context(c), self.request)
+ html = formencode.htmlfill.render(
+ data,
+ defaults=defaults,
+ encoding="UTF-8",
+ force_defaults=False
+ )
+ return Response(html)
+
+ @LoginRequired()
+ @NotAnonymous()
+ @CSRFRequired()
+ @view_config(
+ route_name='my_account_update',
+ request_method='POST',
+ renderer='rhodecode:templates/admin/my_account/my_account.mako')
+ def my_account_update(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+ c.active = 'profile_edit'
+
+ c.perm_user = c.auth_user
+ c.extern_type = c.user.extern_type
+ c.extern_name = c.user.extern_name
+
+ _form = UserForm(edit=True,
+ old_data={'user_id': self._rhodecode_user.user_id,
+ 'email': self._rhodecode_user.email})()
+ form_result = {}
+ try:
+ post_data = dict(self.request.POST)
+ post_data['new_password'] = ''
+ post_data['password_confirmation'] = ''
+ form_result = _form.to_python(post_data)
+ # skip updating those attrs for my account
+ skip_attrs = ['admin', 'active', 'extern_type', 'extern_name',
+ 'new_password', 'password_confirmation']
+ # TODO: plugin should define if username can be updated
+ if c.extern_type != "rhodecode":
+ # forbid updating username for external accounts
+ skip_attrs.append('username')
+
+ UserModel().update_user(
+ self._rhodecode_user.user_id, skip_attrs=skip_attrs,
+ **form_result)
+ h.flash(_('Your account was updated successfully'),
+ category='success')
+ Session().commit()
+
+ except formencode.Invalid as errors:
+ data = render(
+ 'rhodecode:templates/admin/my_account/my_account.mako',
+ self._get_template_context(c), self.request)
+
+ html = formencode.htmlfill.render(
+ data,
+ defaults=errors.value,
+ errors=errors.error_dict or {},
+ prefix_error=False,
+ encoding="UTF-8",
+ force_defaults=False)
+ return Response(html)
+
+ except Exception:
+ log.exception("Exception updating user")
+ h.flash(_('Error occurred during update of user %s')
+ % form_result.get('username'), category='error')
+ raise HTTPFound(h.route_path('my_account_profile'))
+
+ raise HTTPFound(h.route_path('my_account_profile'))
+
+ def _get_pull_requests_list(self, statuses):
+ draw, start, limit = self._extract_chunk(self.request)
+ search_q, order_by, order_dir = self._extract_ordering(self.request)
+ _render = self.request.get_partial_renderer(
+ 'data_table/_dt_elements.mako')
+
+ pull_requests = PullRequestModel().get_im_participating_in(
+ user_id=self._rhodecode_user.user_id,
+ statuses=statuses,
+ offset=start, length=limit, order_by=order_by,
+ order_dir=order_dir)
+
+ pull_requests_total_count = PullRequestModel().count_im_participating_in(
+ user_id=self._rhodecode_user.user_id, statuses=statuses)
+
+ data = []
+ comments_model = CommentsModel()
+ for pr in pull_requests:
+ repo_id = pr.target_repo_id
+ comments = comments_model.get_all_comments(
+ repo_id, pull_request=pr)
+ owned = pr.user_id == self._rhodecode_user.user_id
+
+ data.append({
+ 'target_repo': _render('pullrequest_target_repo',
+ pr.target_repo.repo_name),
+ 'name': _render('pullrequest_name',
+ pr.pull_request_id, pr.target_repo.repo_name,
+ short=True),
+ 'name_raw': pr.pull_request_id,
+ 'status': _render('pullrequest_status',
+ pr.calculated_review_status()),
+ 'title': _render(
+ 'pullrequest_title', pr.title, pr.description),
+ 'description': h.escape(pr.description),
+ 'updated_on': _render('pullrequest_updated_on',
+ h.datetime_to_time(pr.updated_on)),
+ 'updated_on_raw': h.datetime_to_time(pr.updated_on),
+ 'created_on': _render('pullrequest_updated_on',
+ h.datetime_to_time(pr.created_on)),
+ 'created_on_raw': h.datetime_to_time(pr.created_on),
+ 'author': _render('pullrequest_author',
+ pr.author.full_contact, ),
+ 'author_raw': pr.author.full_name,
+ 'comments': _render('pullrequest_comments', len(comments)),
+ 'comments_raw': len(comments),
+ 'closed': pr.is_closed(),
+ 'owned': owned
+ })
+
+ # json used to render the grid
+ data = ({
+ 'draw': draw,
+ 'data': data,
+ 'recordsTotal': pull_requests_total_count,
+ 'recordsFiltered': pull_requests_total_count,
+ })
+ return data
+
+ @LoginRequired()
+ @NotAnonymous()
+ @view_config(
+ route_name='my_account_pullrequests',
+ request_method='GET',
+ renderer='rhodecode:templates/admin/my_account/my_account.mako')
+ def my_account_pullrequests(self):
+ c = self.load_default_context()
+ c.active = 'pullrequests'
+ req_get = self.request.GET
+
+ c.closed = str2bool(req_get.get('pr_show_closed'))
+
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @NotAnonymous()
+ @view_config(
+ route_name='my_account_pullrequests_data',
+ request_method='GET', renderer='json_ext')
+ def my_account_pullrequests_data(self):
+ req_get = self.request.GET
+ closed = str2bool(req_get.get('closed'))
+
+ statuses = [PullRequest.STATUS_NEW, PullRequest.STATUS_OPEN]
+ if closed:
+ statuses += [PullRequest.STATUS_CLOSED]
+
+ data = self._get_pull_requests_list(statuses=statuses)
+ return data
+
diff --git a/rhodecode/apps/my_account/views/my_account_notifications.py b/rhodecode/apps/my_account/views/my_account_notifications.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/my_account/views/my_account_notifications.py
@@ -0,0 +1,198 @@
+# -*- 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 logging
+
+from pyramid.httpexceptions import (
+ HTTPFound, HTTPNotFound, HTTPInternalServerError)
+from pyramid.view import view_config
+
+from rhodecode.apps._base import BaseAppView
+from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
+
+from rhodecode.lib import helpers as h
+from rhodecode.lib.helpers import Page
+from rhodecode.lib.utils2 import safe_int
+from rhodecode.model.db import Notification
+from rhodecode.model.notification import NotificationModel
+from rhodecode.model.meta import Session
+
+
+log = logging.getLogger(__name__)
+
+
+class MyAccountNotificationsView(BaseAppView):
+
+ def load_default_context(self):
+ c = self._get_local_tmpl_context()
+ c.user = c.auth_user.get_instance()
+ self._register_global_c(c)
+ return c
+
+ def _has_permissions(self, notification):
+ def is_owner():
+ user_id = self._rhodecode_db_user.user_id
+ for user_notification in notification.notifications_to_users:
+ if user_notification.user.user_id == user_id:
+ return True
+ return False
+ return h.HasPermissionAny('hg.admin')() or is_owner()
+
+ @LoginRequired()
+ @NotAnonymous()
+ @view_config(
+ route_name='notifications_show_all', request_method='GET',
+ renderer='rhodecode:templates/admin/notifications/notifications_show_all.mako')
+ def notifications_show_all(self):
+ c = self.load_default_context()
+
+ c.unread_count = NotificationModel().get_unread_cnt_for_user(
+ self._rhodecode_db_user.user_id)
+
+ _current_filter = self.request.GET.getall('type') or ['unread']
+
+ notifications = NotificationModel().get_for_user(
+ self._rhodecode_db_user.user_id,
+ filter_=_current_filter)
+
+ p = safe_int(self.request.GET.get('page', 1), 1)
+
+ def url_generator(**kw):
+ _query = self.request.GET.mixed()
+ _query.update(kw)
+ return self.request.current_route_path(_query=_query)
+
+ c.notifications = Page(notifications, page=p, items_per_page=10,
+ url=url_generator)
+
+ c.unread_type = 'unread'
+ c.all_type = 'all'
+ c.pull_request_type = Notification.TYPE_PULL_REQUEST
+ c.comment_type = [Notification.TYPE_CHANGESET_COMMENT,
+ Notification.TYPE_PULL_REQUEST_COMMENT]
+
+ c.current_filter = 'unread' # default filter
+
+ if _current_filter == [c.pull_request_type]:
+ c.current_filter = 'pull_request'
+ elif _current_filter == c.comment_type:
+ c.current_filter = 'comment'
+ elif _current_filter == [c.unread_type]:
+ c.current_filter = 'unread'
+ elif _current_filter == [c.all_type]:
+ c.current_filter = 'all'
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @NotAnonymous()
+ @CSRFRequired()
+ @view_config(
+ route_name='notifications_mark_all_read', request_method='POST',
+ renderer='rhodecode:templates/admin/notifications/notifications_show_all.mako')
+ def notifications_mark_all_read(self):
+ NotificationModel().mark_all_read_for_user(
+ self._rhodecode_db_user.user_id,
+ filter_=self.request.GET.getall('type'))
+ Session().commit()
+ raise HTTPFound(h.route_path('notifications_show_all'))
+
+ @LoginRequired()
+ @NotAnonymous()
+ @view_config(
+ route_name='notifications_show', request_method='GET',
+ renderer='rhodecode:templates/admin/notifications/notifications_show.mako')
+ def notifications_show(self):
+ c = self.load_default_context()
+ notification_id = self.request.matchdict['notification_id']
+ notification = Notification.get_or_404(notification_id)
+
+ if not self._has_permissions(notification):
+ log.debug('User %s does not have permission to access notification',
+ self._rhodecode_user)
+ raise HTTPNotFound()
+
+ u_notification = NotificationModel().get_user_notification(
+ self._rhodecode_db_user.user_id, notification)
+ if not u_notification:
+ log.debug('User %s notification does not exist',
+ self._rhodecode_user)
+ raise HTTPNotFound()
+
+ # when opening this notification, mark it as read for this use
+ if not u_notification.read:
+ u_notification.mark_as_read()
+ Session().commit()
+
+ c.notification = notification
+
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @NotAnonymous()
+ @CSRFRequired()
+ @view_config(
+ route_name='notifications_update', request_method='POST',
+ renderer='json_ext')
+ def notification_update(self):
+ notification_id = self.request.matchdict['notification_id']
+ notification = Notification.get_or_404(notification_id)
+
+ if not self._has_permissions(notification):
+ log.debug('User %s does not have permission to access notification',
+ self._rhodecode_user)
+ raise HTTPNotFound()
+
+ try:
+ # updates notification read flag
+ NotificationModel().mark_read(
+ self._rhodecode_user.user_id, notification)
+ Session().commit()
+ return 'ok'
+ except Exception:
+ Session().rollback()
+ log.exception("Exception updating a notification item")
+
+ raise HTTPInternalServerError()
+
+ @LoginRequired()
+ @NotAnonymous()
+ @CSRFRequired()
+ @view_config(
+ route_name='notifications_delete', request_method='POST',
+ renderer='json_ext')
+ def notification_delete(self):
+ notification_id = self.request.matchdict['notification_id']
+ notification = Notification.get_or_404(notification_id)
+ if not self._has_permissions(notification):
+ log.debug('User %s does not have permission to access notification',
+ self._rhodecode_user)
+ raise HTTPNotFound()
+
+ try:
+ # deletes only notification2user
+ NotificationModel().delete(
+ self._rhodecode_user.user_id, notification)
+ Session().commit()
+ return 'ok'
+ except Exception:
+ Session().rollback()
+ log.exception("Exception deleting a notification item")
+
+ raise HTTPInternalServerError()
diff --git a/rhodecode/apps/my_account/views/my_account_ssh_keys.py b/rhodecode/apps/my_account/views/my_account_ssh_keys.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/my_account/views/my_account_ssh_keys.py
@@ -0,0 +1,154 @@
+# -*- 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, DataGridAppView
+from rhodecode.apps.ssh_support import SshKeyFileChangeEvent
+from rhodecode.events import trigger
+from rhodecode.lib import helpers as h
+from rhodecode.lib import audit_logger
+from rhodecode.lib.auth import LoginRequired, NotAnonymous, CSRFRequired
+from rhodecode.model.db import IntegrityError, UserSshKeys
+from rhodecode.model.meta import Session
+from rhodecode.model.ssh_key import SshKeyModel
+
+log = logging.getLogger(__name__)
+
+
+class MyAccountSshKeysView(BaseAppView, DataGridAppView):
+
+ def load_default_context(self):
+ c = self._get_local_tmpl_context()
+ c.user = c.auth_user.get_instance()
+
+ c.ssh_enabled = self.request.registry.settings.get(
+ 'ssh.generate_authorized_keyfile')
+ self._register_global_c(c)
+ return c
+
+ @LoginRequired()
+ @NotAnonymous()
+ @view_config(
+ route_name='my_account_ssh_keys', request_method='GET',
+ renderer='rhodecode:templates/admin/my_account/my_account.mako')
+ def my_account_ssh_keys(self):
+ _ = self.request.translate
+
+ c = self.load_default_context()
+ c.active = 'ssh_keys'
+ c.default_key = self.request.GET.get('default_key')
+ c.user_ssh_keys = SshKeyModel().get_ssh_keys(c.user.user_id)
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @NotAnonymous()
+ @view_config(
+ route_name='my_account_ssh_keys_generate', request_method='GET',
+ renderer='rhodecode:templates/admin/my_account/my_account.mako')
+ def ssh_keys_generate_keypair(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+
+ c.active = 'ssh_keys_generate'
+ comment = 'RhodeCode-SSH {}'.format(c.user.email or '')
+ c.private, c.public = SshKeyModel().generate_keypair(comment=comment)
+ c.target_form_url = h.route_path(
+ 'my_account_ssh_keys', _query=dict(default_key=c.public))
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @NotAnonymous()
+ @CSRFRequired()
+ @view_config(
+ route_name='my_account_ssh_keys_add', request_method='POST',)
+ def my_account_ssh_keys_add(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+
+ user_data = c.user.get_api_data()
+ key_data = self.request.POST.get('key_data')
+ description = self.request.POST.get('description')
+
+ try:
+ if not key_data:
+ raise ValueError('Please add a valid public key')
+
+ key = SshKeyModel().parse_key(key_data.strip())
+ fingerprint = key.hash_md5()
+
+ ssh_key = SshKeyModel().create(
+ c.user.user_id, fingerprint, key_data, description)
+ ssh_key_data = ssh_key.get_api_data()
+
+ audit_logger.store_web(
+ 'user.edit.ssh_key.add', action_data={
+ 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
+ user=self._rhodecode_user, )
+ Session().commit()
+
+ # Trigger an event on change of keys.
+ trigger(SshKeyFileChangeEvent(), self.request.registry)
+
+ h.flash(_("Ssh Key successfully created"), category='success')
+
+ except IntegrityError:
+ log.exception("Exception during ssh key saving")
+ h.flash(_('An error occurred during ssh key saving: {}').format(
+ 'Such key already exists, please use a different one'),
+ category='error')
+ except Exception as e:
+ log.exception("Exception during ssh key saving")
+ h.flash(_('An error occurred during ssh key saving: {}').format(e),
+ category='error')
+
+ return HTTPFound(h.route_path('my_account_ssh_keys'))
+
+ @LoginRequired()
+ @NotAnonymous()
+ @CSRFRequired()
+ @view_config(
+ route_name='my_account_ssh_keys_delete', request_method='POST')
+ def my_account_ssh_keys_delete(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+
+ user_data = c.user.get_api_data()
+
+ del_ssh_key = self.request.POST.get('del_ssh_key')
+
+ if del_ssh_key:
+ ssh_key = UserSshKeys.get_or_404(del_ssh_key)
+ ssh_key_data = ssh_key.get_api_data()
+
+ SshKeyModel().delete(del_ssh_key, c.user.user_id)
+ audit_logger.store_web(
+ 'user.edit.ssh_key.delete', action_data={
+ 'data': {'ssh_key': ssh_key_data, 'user': user_data}},
+ user=self._rhodecode_user,)
+ Session().commit()
+ # Trigger an event on change of keys.
+ trigger(SshKeyFileChangeEvent(), self.request.registry)
+ h.flash(_("Ssh key successfully deleted"), category='success')
+
+ return HTTPFound(h.route_path('my_account_ssh_keys'))
diff --git a/rhodecode/apps/ops/__init__.py b/rhodecode/apps/ops/__init__.py
--- a/rhodecode/apps/ops/__init__.py
+++ b/rhodecode/apps/ops/__init__.py
@@ -25,11 +25,22 @@ def admin_routes(config):
config.add_route(
name='ops_ping',
pattern='/ping')
+ config.add_route(
+ name='ops_error_test',
+ pattern='/error')
+ config.add_route(
+ name='ops_redirect_test',
+ pattern='/redirect')
def includeme(config):
config.include(admin_routes, route_prefix=ADMIN_PREFIX + '/ops')
+ # make OLD entries from pylons work
+ config.add_route(
+ name='ops_ping_legacy', pattern=ADMIN_PREFIX + '/ping')
+ config.add_route(
+ name='ops_error_test_legacy', pattern=ADMIN_PREFIX + '/error_test')
# Scan module for configuration decorators.
- config.scan()
+ config.scan('.views', ignore='.tests')
diff --git a/rhodecode/apps/ops/tests/__init__.py b/rhodecode/apps/ops/tests/__init__.py
new file mode 100644
diff --git a/rhodecode/apps/ops/views.py b/rhodecode/apps/ops/views.py
--- a/rhodecode/apps/ops/views.py
+++ b/rhodecode/apps/ops/views.py
@@ -18,12 +18,14 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
+import time
import logging
from pyramid.view import view_config
+from pyramid.httpexceptions import HTTPFound
from rhodecode.apps._base import BaseAppView
-
+from rhodecode.lib import helpers as h
log = logging.getLogger(__name__)
@@ -39,6 +41,9 @@ class OpsView(BaseAppView):
@view_config(
route_name='ops_ping', request_method='GET',
renderer='json_ext')
+ @view_config(
+ route_name='ops_ping_legacy', request_method='GET',
+ renderer='json_ext')
def ops_ping(self):
data = {
'instance': self.request.registry.settings.get('instance_id'),
@@ -50,5 +55,29 @@ class OpsView(BaseAppView):
})
return {'ok': data}
+ @view_config(
+ route_name='ops_error_test', request_method='GET',
+ renderer='json_ext')
+ @view_config(
+ route_name='ops_error_test_legacy', request_method='GET',
+ renderer='json_ext')
+ def ops_error_test(self):
+ """
+ Test exception handling and emails on errors
+ """
+ class TestException(Exception):
+ pass
+ msg = ('RhodeCode Enterprise test exception. '
+ 'Generation time: {}'.format(time.time()))
+ raise TestException(msg)
+ @view_config(
+ route_name='ops_redirect_test', request_method='GET',
+ renderer='json_ext')
+ def ops_redirect_test(self):
+ """
+ Test redirect handling
+ """
+ redirect_to = self.request.GET.get('to') or h.route_path('home')
+ raise HTTPFound(redirect_to)
diff --git a/rhodecode/apps/repo_group/__init__.py b/rhodecode/apps/repo_group/__init__.py
--- a/rhodecode/apps/repo_group/__init__.py
+++ b/rhodecode/apps/repo_group/__init__.py
@@ -22,12 +22,40 @@ from rhodecode.apps._base import add_rou
def includeme(config):
- # Summary
+ # Settings
+ config.add_route(
+ name='edit_repo_group',
+ pattern='/{repo_group_name:.*?[^/]}/_edit',
+ repo_group_route=True)
+ # update is POST on edit_repo_group
+
+ # Settings advanced
+ config.add_route(
+ name='edit_repo_group_advanced',
+ pattern='/{repo_group_name:.*?[^/]}/_settings/advanced',
+ repo_group_route=True)
+
+ config.add_route(
+ name='edit_repo_group_advanced_delete',
+ pattern='/{repo_group_name:.*?[^/]}/_settings/advanced/delete',
+ repo_group_route=True)
+
+ # settings permissions
+ config.add_route(
+ name='edit_repo_group_perms',
+ pattern='/{repo_group_name:.*?[^/]}/_settings/permissions',
+ repo_group_route=True)
+
+ config.add_route(
+ name='edit_repo_group_perms_update',
+ pattern='/{repo_group_name:.*?[^/]}/_settings/permissions/update',
+ repo_group_route=True)
+
+ # Summary, NOTE(marcink): needs to be at the end for catch-all
add_route_with_slash(
config,
name='repo_group_home',
pattern='/{repo_group_name:.*?[^/]}', repo_group_route=True)
# Scan module for configuration decorators.
- config.scan()
-
+ config.scan('.views', ignore='.tests')
diff --git a/rhodecode/apps/repo_group/tests/__init__.py b/rhodecode/apps/repo_group/tests/__init__.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/repo_group/tests/__init__.py
@@ -0,0 +1,19 @@
+# -*- 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/
diff --git a/rhodecode/apps/repo_group/tests/test_repo_groups_advanced.py b/rhodecode/apps/repo_group/tests/test_repo_groups_advanced.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/repo_group/tests/test_repo_groups_advanced.py
@@ -0,0 +1,89 @@
+# -*- 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 assert_session_flash
+
+
+def route_path(name, params=None, **kwargs):
+ import urllib
+
+ base_url = {
+ 'edit_repo_group_advanced':
+ '/{repo_group_name}/_settings/advanced',
+ 'edit_repo_group_advanced_delete':
+ '/{repo_group_name}/_settings/advanced/delete',
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
+
+
+@pytest.mark.usefixtures("app")
+class TestRepoGroupsAdvancedView(object):
+
+ @pytest.mark.parametrize('repo_group_name', [
+ 'gro',
+ '12345',
+ ])
+ def test_show_advanced_settings(self, autologin_user, user_util, repo_group_name):
+ user_util._test_name = repo_group_name
+ gr = user_util.create_repo_group()
+ self.app.get(
+ route_path('edit_repo_group_advanced',
+ repo_group_name=gr.group_name))
+
+ def test_show_advanced_settings_delete(self, autologin_user, user_util,
+ csrf_token):
+ gr = user_util.create_repo_group(auto_cleanup=False)
+ repo_group_name = gr.group_name
+
+ params = dict(
+ csrf_token=csrf_token
+ )
+ response = self.app.post(
+ route_path('edit_repo_group_advanced_delete',
+ repo_group_name=repo_group_name), params=params)
+ assert_session_flash(
+ response, 'Removed repository group `{}`'.format(repo_group_name))
+
+ def test_delete_not_possible_with_objects_inside(self, autologin_user,
+ repo_groups, csrf_token):
+ zombie_group, parent_group, child_group = repo_groups
+
+ response = self.app.get(
+ route_path('edit_repo_group_advanced',
+ repo_group_name=parent_group.group_name))
+
+ response.mustcontain(
+ 'This repository group includes 1 children repository group')
+
+ params = dict(
+ csrf_token=csrf_token
+ )
+ response = self.app.post(
+ route_path('edit_repo_group_advanced_delete',
+ repo_group_name=parent_group.group_name), params=params)
+
+ assert_session_flash(
+ response, 'This repository group contains 1 subgroup '
+ 'and cannot be deleted')
diff --git a/rhodecode/apps/repo_group/tests/test_repo_groups_permissions.py b/rhodecode/apps/repo_group/tests/test_repo_groups_permissions.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/repo_group/tests/test_repo_groups_permissions.py
@@ -0,0 +1,49 @@
+# -*- 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
+
+
+def route_path(name, params=None, **kwargs):
+ import urllib
+
+ base_url = {
+ 'edit_repo_group_perms':
+ '/{repo_group_name:}/_settings/permissions',
+ 'edit_repo_group_perms_update':
+ '/{repo_group_name}/_settings/permissions/update',
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
+
+
+@pytest.mark.usefixtures("app")
+class TestRepoGroupsPermissionsView(object):
+
+ def test_edit_repo_group_perms(self, user_util, autologin_user):
+ repo_group = user_util.create_repo_group()
+ self.app.get(
+ route_path('edit_repo_group_perms',
+ repo_group_name=repo_group.group_name), status=200)
+
+ def test_update_permissions(self):
+ pass
diff --git a/rhodecode/apps/repo_group/tests/test_repo_groups_settings.py b/rhodecode/apps/repo_group/tests/test_repo_groups_settings.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/repo_group/tests/test_repo_groups_settings.py
@@ -0,0 +1,90 @@
+# -*- 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 assert_session_flash
+
+
+def route_path(name, params=None, **kwargs):
+ import urllib
+
+ base_url = {
+ 'edit_repo_group': '/{repo_group_name}/_edit',
+ # Update is POST to the above url
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
+
+
+@pytest.mark.usefixtures("app")
+class TestRepoGroupsSettingsView(object):
+
+ @pytest.mark.parametrize('repo_group_name', [
+ 'gro',
+ u'12345',
+ ])
+ def test_edit(self, user_util, autologin_user, repo_group_name):
+ user_util._test_name = repo_group_name
+ repo_group = user_util.create_repo_group()
+
+ self.app.get(
+ route_path('edit_repo_group', repo_group_name=repo_group.group_name),
+ status=200)
+
+ def test_update(self, csrf_token, autologin_user, user_util, rc_fixture):
+ repo_group = user_util.create_repo_group()
+ repo_group_name = repo_group.group_name
+
+ description = 'description for newly created repo group'
+ form_data = rc_fixture._get_group_create_params(
+ group_name=repo_group.group_name,
+ group_description=description,
+ csrf_token=csrf_token,
+ repo_group_name=repo_group.group_name,
+ repo_group_owner=repo_group.user.username)
+
+ response = self.app.post(
+ route_path('edit_repo_group',
+ repo_group_name=repo_group.group_name),
+ form_data,
+ status=302)
+
+ assert_session_flash(
+ response, 'Repository Group `{}` updated successfully'.format(
+ repo_group_name))
+
+ def test_update_fails_when_parent_pointing_to_self(
+ self, csrf_token, user_util, autologin_user, rc_fixture):
+ group = user_util.create_repo_group()
+ response = self.app.post(
+ route_path('edit_repo_group', repo_group_name=group.group_name),
+ rc_fixture._get_group_create_params(
+ repo_group_name=group.group_name,
+ repo_group_owner=group.user.username,
+ repo_group=group.group_id,
+ csrf_token=csrf_token),
+ status=200
+ )
+ response.mustcontain(
+ '"{}" is not one of -1'.format(
+ group.group_id))
diff --git a/rhodecode/apps/repo_group/views/__init__.py b/rhodecode/apps/repo_group/views/__init__.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/repo_group/views/__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/
\ No newline at end of file
diff --git a/rhodecode/apps/repo_group/views/repo_group_advanced.py b/rhodecode/apps/repo_group/views/repo_group_advanced.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/repo_group/views/repo_group_advanced.py
@@ -0,0 +1,105 @@
+# -*- 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 RepoGroupAppView
+from rhodecode.lib import helpers as h
+from rhodecode.lib import audit_logger
+from rhodecode.lib.auth import (
+ LoginRequired, CSRFRequired, HasRepoGroupPermissionAnyDecorator)
+from rhodecode.model.repo_group import RepoGroupModel
+from rhodecode.model.meta import Session
+
+log = logging.getLogger(__name__)
+
+
+class RepoGroupSettingsView(RepoGroupAppView):
+ def load_default_context(self):
+ c = self._get_local_tmpl_context()
+ self._register_global_c(c)
+ return c
+
+ @LoginRequired()
+ @HasRepoGroupPermissionAnyDecorator('group.admin')
+ @view_config(
+ route_name='edit_repo_group_advanced', request_method='GET',
+ renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
+ def edit_repo_group_advanced(self):
+ c = self.load_default_context()
+ c.active = 'advanced'
+ c.repo_group = self.db_repo_group
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @HasRepoGroupPermissionAnyDecorator('group.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='edit_repo_group_advanced_delete', request_method='POST',
+ renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
+ def edit_repo_group_delete(self):
+ _ = self.request.translate
+ _ungettext = self.request.plularize
+ c = self.load_default_context()
+ c.repo_group = self.db_repo_group
+
+ repos = c.repo_group.repositories.all()
+ if repos:
+ msg = _ungettext(
+ 'This repository group contains %(num)d repository and cannot be deleted',
+ 'This repository group contains %(num)d repositories and cannot be'
+ ' deleted',
+ len(repos)) % {'num': len(repos)}
+ h.flash(msg, category='warning')
+ raise HTTPFound(
+ h.route_path('edit_repo_group_advanced',
+ repo_group_name=self.db_repo_group_name))
+
+ children = c.repo_group.children.all()
+ if children:
+ msg = _ungettext(
+ 'This repository group contains %(num)d subgroup and cannot be deleted',
+ 'This repository group contains %(num)d subgroups and cannot be deleted',
+ len(children)) % {'num': len(children)}
+ h.flash(msg, category='warning')
+ raise HTTPFound(
+ h.route_path('edit_repo_group_advanced',
+ repo_group_name=self.db_repo_group_name))
+
+ try:
+ old_values = c.repo_group.get_api_data()
+ RepoGroupModel().delete(self.db_repo_group_name)
+
+ audit_logger.store_web(
+ 'repo_group.delete', action_data={'old_data': old_values},
+ user=c.rhodecode_user)
+
+ Session().commit()
+ h.flash(_('Removed repository group `%s`') % self.db_repo_group_name,
+ category='success')
+ except Exception:
+ log.exception("Exception during deletion of repository group")
+ h.flash(_('Error occurred during deletion of repository group %s')
+ % self.db_repo_group_name, category='error')
+
+ raise HTTPFound(h.route_path('repo_groups'))
diff --git a/rhodecode/apps/repo_group/views/repo_group_permissions.py b/rhodecode/apps/repo_group/views/repo_group_permissions.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/repo_group/views/repo_group_permissions.py
@@ -0,0 +1,100 @@
+# -*- 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 RepoGroupAppView
+from rhodecode.lib import helpers as h
+from rhodecode.lib import audit_logger
+from rhodecode.lib.auth import (
+ LoginRequired, HasRepoGroupPermissionAnyDecorator, CSRFRequired)
+from rhodecode.model.repo_group import RepoGroupModel
+from rhodecode.model.forms import RepoGroupPermsForm
+from rhodecode.model.meta import Session
+
+log = logging.getLogger(__name__)
+
+
+class RepoGroupPermissionsView(RepoGroupAppView):
+ def load_default_context(self):
+ c = self._get_local_tmpl_context()
+ self._register_global_c(c)
+ return c
+
+ @LoginRequired()
+ @HasRepoGroupPermissionAnyDecorator('group.admin')
+ @view_config(
+ route_name='edit_repo_group_perms', request_method='GET',
+ renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
+ def edit_repo_group_permissions(self):
+ c = self.load_default_context()
+ c.active = 'permissions'
+ c.repo_group = self.db_repo_group
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @HasRepoGroupPermissionAnyDecorator('group.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='edit_repo_group_perms_update', request_method='POST',
+ renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
+ def edit_repo_groups_permissions_update(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+ c.active = 'perms'
+ c.repo_group = self.db_repo_group
+
+ valid_recursive_choices = ['none', 'repos', 'groups', 'all']
+ form = RepoGroupPermsForm(valid_recursive_choices)()\
+ .to_python(self.request.POST)
+
+ if not c.rhodecode_user.is_admin:
+ if self._revoke_perms_on_yourself(form):
+ msg = _('Cannot change permission for yourself as admin')
+ h.flash(msg, category='warning')
+ raise HTTPFound(
+ h.route_path('edit_repo_group_perms',
+ group_name=self.db_repo_group_name))
+
+ # iterate over all members(if in recursive mode) of this groups and
+ # set the permissions !
+ # this can be potentially heavy operation
+ changes = RepoGroupModel().update_permissions(
+ c.repo_group,
+ form['perm_additions'], form['perm_updates'], form['perm_deletions'],
+ form['recursive'])
+
+ action_data = {
+ 'added': changes['added'],
+ 'updated': changes['updated'],
+ 'deleted': changes['deleted'],
+ }
+ audit_logger.store_web(
+ 'repo_group.edit.permissions', action_data=action_data,
+ user=c.rhodecode_user)
+
+ Session().commit()
+ h.flash(_('Repository Group permissions updated'), category='success')
+ raise HTTPFound(
+ h.route_path('edit_repo_group_perms',
+ repo_group_name=self.db_repo_group_name))
diff --git a/rhodecode/apps/repo_group/views/repo_group_settings.py b/rhodecode/apps/repo_group/views/repo_group_settings.py
new file mode 100644
--- /dev/null
+++ b/rhodecode/apps/repo_group/views/repo_group_settings.py
@@ -0,0 +1,183 @@
+# -*- 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
+import deform
+
+from pyramid.view import view_config
+from pyramid.httpexceptions import HTTPFound
+
+from rhodecode.apps._base import RepoGroupAppView
+from rhodecode.forms import RcForm
+from rhodecode.lib import helpers as h
+from rhodecode.lib import audit_logger
+from rhodecode.lib.auth import (
+ LoginRequired, HasPermissionAll,
+ HasRepoGroupPermissionAny, HasRepoGroupPermissionAnyDecorator, CSRFRequired)
+from rhodecode.model.db import Session, RepoGroup
+from rhodecode.model.scm import RepoGroupList
+from rhodecode.model.repo_group import RepoGroupModel
+from rhodecode.model.validation_schema.schemas import repo_group_schema
+
+log = logging.getLogger(__name__)
+
+
+class RepoGroupSettingsView(RepoGroupAppView):
+ def load_default_context(self):
+ c = self._get_local_tmpl_context()
+ c.repo_group = self.db_repo_group
+ no_parrent = not c.repo_group.parent_group
+ can_create_in_root = self._can_create_repo_group()
+
+ show_root_location = False
+ if no_parrent or can_create_in_root:
+ # we're global admin, we're ok and we can create TOP level groups
+ # or in case this group is already at top-level we also allow
+ # creation in root
+ show_root_location = True
+
+ acl_groups = RepoGroupList(
+ RepoGroup.query().all(),
+ perm_set=['group.admin'])
+ c.repo_groups = RepoGroup.groups_choices(
+ groups=acl_groups,
+ show_empty_group=show_root_location)
+ # filter out current repo group
+ exclude_group_ids = [c.repo_group.group_id]
+ c.repo_groups = filter(lambda x: x[0] not in exclude_group_ids,
+ c.repo_groups)
+ c.repo_groups_choices = map(lambda k: k[0], c.repo_groups)
+
+ parent_group = c.repo_group.parent_group
+
+ add_parent_group = (parent_group and (
+ parent_group.group_id not in c.repo_groups_choices))
+ if add_parent_group:
+ c.repo_groups_choices.append(parent_group.group_id)
+ c.repo_groups.append(RepoGroup._generate_choice(parent_group))
+
+ self._register_global_c(c)
+ return c
+
+ def _can_create_repo_group(self, parent_group_id=None):
+ is_admin = HasPermissionAll('hg.admin')('group create controller')
+ create_repo_group = HasPermissionAll(
+ 'hg.repogroup.create.true')('group create controller')
+ if is_admin or (create_repo_group and not parent_group_id):
+ # we're global admin, or we have global repo group create
+ # permission
+ # we're ok and we can create TOP level groups
+ return True
+ elif parent_group_id:
+ # we check the permission if we can write to parent group
+ group = RepoGroup.get(parent_group_id)
+ group_name = group.group_name if group else None
+ if HasRepoGroupPermissionAny('group.admin')(
+ group_name, 'check if user is an admin of group'):
+ # we're an admin of passed in group, we're ok.
+ return True
+ else:
+ return False
+ return False
+
+ def _get_schema(self, c, old_values=None):
+ return repo_group_schema.RepoGroupSettingsSchema().bind(
+ repo_group_repo_group_options=c.repo_groups_choices,
+ repo_group_repo_group_items=c.repo_groups,
+
+ # user caller
+ user=self._rhodecode_user,
+ old_values=old_values
+ )
+
+ @LoginRequired()
+ @HasRepoGroupPermissionAnyDecorator('group.admin')
+ @view_config(
+ route_name='edit_repo_group', request_method='GET',
+ renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
+ def edit_settings(self):
+ c = self.load_default_context()
+ c.active = 'settings'
+
+ defaults = RepoGroupModel()._get_defaults(self.db_repo_group_name)
+ defaults['repo_group_owner'] = defaults['user']
+
+ schema = self._get_schema(c)
+ c.form = RcForm(schema, appstruct=defaults)
+ return self._get_template_context(c)
+
+ @LoginRequired()
+ @HasRepoGroupPermissionAnyDecorator('group.admin')
+ @CSRFRequired()
+ @view_config(
+ route_name='edit_repo_group', request_method='POST',
+ renderer='rhodecode:templates/admin/repo_groups/repo_group_edit.mako')
+ def edit_settings_update(self):
+ _ = self.request.translate
+ c = self.load_default_context()
+ c.active = 'settings'
+
+ old_repo_group_name = self.db_repo_group_name
+ new_repo_group_name = old_repo_group_name
+
+ old_values = RepoGroupModel()._get_defaults(self.db_repo_group_name)
+ schema = self._get_schema(c, old_values=old_values)
+
+ c.form = RcForm(schema)
+ pstruct = self.request.POST.items()
+
+ try:
+ schema_data = c.form.validate(pstruct)
+ except deform.ValidationFailure as err_form:
+ return self._get_template_context(c)
+
+ # data is now VALID, proceed with updates
+ # save validated data back into the updates dict
+ validated_updates = dict(
+ group_name=schema_data['repo_group']['repo_group_name_without_group'],
+ group_parent_id=schema_data['repo_group']['repo_group_id'],
+ user=schema_data['repo_group_owner'],
+ group_description=schema_data['repo_group_description'],
+ enable_locking=schema_data['repo_group_enable_locking'],
+ )
+
+ try:
+ RepoGroupModel().update(self.db_repo_group, validated_updates)
+
+ audit_logger.store_web(
+ 'repo_group.edit', action_data={'old_data': old_values},
+ user=c.rhodecode_user)
+
+ Session().commit()
+
+ # use the new full name for redirect once we know we updated
+ # the name on filesystem and in DB
+ new_repo_group_name = schema_data['repo_group_name']
+
+ h.flash(_('Repository Group `{}` updated successfully').format(
+ old_repo_group_name), category='success')
+
+ except Exception:
+ log.exception("Exception during update or repository group")
+ h.flash(_('Error occurred during update of repository group %s')
+ % old_repo_group_name, category='error')
+
+ raise HTTPFound(
+ h.route_path('edit_repo_group', repo_group_name=new_repo_group_name))
diff --git a/rhodecode/apps/repository/__init__.py b/rhodecode/apps/repository/__init__.py
--- a/rhodecode/apps/repository/__init__.py
+++ b/rhodecode/apps/repository/__init__.py
@@ -22,6 +22,15 @@ from rhodecode.apps._base import add_rou
def includeme(config):
+ # repo creating checks, special cases that aren't repo routes
+ config.add_route(
+ name='repo_creating',
+ pattern='/{repo_name:.*?[^/]}/repo_creating')
+
+ config.add_route(
+ name='repo_creating_check',
+ pattern='/{repo_name:.*?[^/]}/repo_creating_check')
+
# Summary
# NOTE(marcink): one additional route is defined in very bottom, catch
# all pattern
@@ -32,12 +41,142 @@ def includeme(config):
name='repo_summary_commits',
pattern='/{repo_name:.*?[^/]}/summary-commits', repo_route=True)
- # repo commits
+ # Commits
config.add_route(
name='repo_commit',
pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}', repo_route=True)
- # refs data
+ config.add_route(
+ name='repo_commit_children',
+ pattern='/{repo_name:.*?[^/]}/changeset_children/{commit_id}', repo_route=True)
+
+ config.add_route(
+ name='repo_commit_parents',
+ pattern='/{repo_name:.*?[^/]}/changeset_parents/{commit_id}', repo_route=True)
+
+ config.add_route(
+ name='repo_commit_raw',
+ pattern='/{repo_name:.*?[^/]}/changeset-diff/{commit_id}', repo_route=True)
+
+ config.add_route(
+ name='repo_commit_patch',
+ pattern='/{repo_name:.*?[^/]}/changeset-patch/{commit_id}', repo_route=True)
+
+ config.add_route(
+ name='repo_commit_download',
+ pattern='/{repo_name:.*?[^/]}/changeset-download/{commit_id}', repo_route=True)
+
+ config.add_route(
+ name='repo_commit_data',
+ pattern='/{repo_name:.*?[^/]}/changeset-data/{commit_id}', repo_route=True)
+
+ config.add_route(
+ name='repo_commit_comment_create',
+ pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/create', repo_route=True)
+
+ config.add_route(
+ name='repo_commit_comment_preview',
+ pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/preview', repo_route=True)
+
+ config.add_route(
+ name='repo_commit_comment_delete',
+ pattern='/{repo_name:.*?[^/]}/changeset/{commit_id}/comment/{comment_id}/delete', repo_route=True)
+
+ # still working url for backward compat.
+ config.add_route(
+ name='repo_commit_raw_deprecated',
+ pattern='/{repo_name:.*?[^/]}/raw-changeset/{commit_id}', repo_route=True)
+
+ # Files
+ config.add_route(
+ name='repo_archivefile',
+ pattern='/{repo_name:.*?[^/]}/archive/{fname}', repo_route=True)
+
+ config.add_route(
+ name='repo_files_diff',
+ pattern='/{repo_name:.*?[^/]}/diff/{f_path:.*}', repo_route=True)
+ config.add_route( # legacy route to make old links work
+ name='repo_files_diff_2way_redirect',
+ pattern='/{repo_name:.*?[^/]}/diff-2way/{f_path:.*}', repo_route=True)
+
+ config.add_route(
+ name='repo_files',
+ pattern='/{repo_name:.*?[^/]}/files/{commit_id}/{f_path:.*}', repo_route=True)
+ config.add_route(
+ name='repo_files:default_path',
+ pattern='/{repo_name:.*?[^/]}/files/{commit_id}/', repo_route=True)
+ config.add_route(
+ name='repo_files:default_commit',
+ pattern='/{repo_name:.*?[^/]}/files', repo_route=True)
+
+ config.add_route(
+ name='repo_files:rendered',
+ pattern='/{repo_name:.*?[^/]}/render/{commit_id}/{f_path:.*}', repo_route=True)
+
+ config.add_route(
+ name='repo_files:annotated',
+ pattern='/{repo_name:.*?[^/]}/annotate/{commit_id}/{f_path:.*}', repo_route=True)
+ config.add_route(
+ name='repo_files:annotated_previous',
+ pattern='/{repo_name:.*?[^/]}/annotate-previous/{commit_id}/{f_path:.*}', repo_route=True)
+
+ config.add_route(
+ name='repo_nodetree_full',
+ pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/{f_path:.*}', repo_route=True)
+ config.add_route(
+ name='repo_nodetree_full:default_path',
+ pattern='/{repo_name:.*?[^/]}/nodetree_full/{commit_id}/', repo_route=True)
+
+ config.add_route(
+ name='repo_files_nodelist',
+ pattern='/{repo_name:.*?[^/]}/nodelist/{commit_id}/{f_path:.*}', repo_route=True)
+
+ config.add_route(
+ name='repo_file_raw',
+ pattern='/{repo_name:.*?[^/]}/raw/{commit_id}/{f_path:.*}', repo_route=True)
+
+ config.add_route(
+ name='repo_file_download',
+ pattern='/{repo_name:.*?[^/]}/download/{commit_id}/{f_path:.*}', repo_route=True)
+ config.add_route( # backward compat to keep old links working
+ name='repo_file_download:legacy',
+ pattern='/{repo_name:.*?[^/]}/rawfile/{commit_id}/{f_path:.*}',
+ repo_route=True)
+
+ config.add_route(
+ name='repo_file_history',
+ pattern='/{repo_name:.*?[^/]}/history/{commit_id}/{f_path:.*}', repo_route=True)
+
+ config.add_route(
+ name='repo_file_authors',
+ pattern='/{repo_name:.*?[^/]}/authors/{commit_id}/{f_path:.*}', repo_route=True)
+
+ config.add_route(
+ name='repo_files_remove_file',
+ pattern='/{repo_name:.*?[^/]}/remove_file/{commit_id}/{f_path:.*}',
+ repo_route=True)
+ config.add_route(
+ name='repo_files_delete_file',
+ pattern='/{repo_name:.*?[^/]}/delete_file/{commit_id}/{f_path:.*}',
+ repo_route=True)
+ config.add_route(
+ name='repo_files_edit_file',
+ pattern='/{repo_name:.*?[^/]}/edit_file/{commit_id}/{f_path:.*}',
+ repo_route=True)
+ config.add_route(
+ name='repo_files_update_file',
+ pattern='/{repo_name:.*?[^/]}/update_file/{commit_id}/{f_path:.*}',
+ repo_route=True)
+ config.add_route(
+ name='repo_files_add_file',
+ pattern='/{repo_name:.*?[^/]}/add_file/{commit_id}/{f_path:.*}',
+ repo_route=True)
+ config.add_route(
+ name='repo_files_create_file',
+ pattern='/{repo_name:.*?[^/]}/create_file/{commit_id}/{f_path:.*}',
+ repo_route=True)
+
+ # Refs data
config.add_route(
name='repo_refs_data',
pattern='/{repo_name:.*?[^/]}/refs-data', repo_route=True)
@@ -50,6 +189,29 @@ def includeme(config):
name='repo_stats',
pattern='/{repo_name:.*?[^/]}/repo_stats/{commit_id}', repo_route=True)
+ # Changelog
+ config.add_route(
+ name='repo_changelog',
+ pattern='/{repo_name:.*?[^/]}/changelog', repo_route=True)
+ config.add_route(
+ name='repo_changelog_file',
+ pattern='/{repo_name:.*?[^/]}/changelog/{commit_id}/{f_path:.*}', repo_route=True)
+ config.add_route(
+ name='repo_changelog_elements',
+ pattern='/{repo_name:.*?[^/]}/changelog_elements', repo_route=True)
+ config.add_route(
+ name='repo_changelog_elements_file',
+ pattern='/{repo_name:.*?[^/]}/changelog_elements/{commit_id}/{f_path:.*}', repo_route=True)
+
+ # Compare
+ config.add_route(
+ name='repo_compare_select',
+ pattern='/{repo_name:.*?[^/]}/compare', repo_route=True)
+
+ config.add_route(
+ name='repo_compare',
+ pattern='/{repo_name:.*?[^/]}/compare/{source_ref_type}@{source_ref:.*?}...{target_ref_type}@{target_ref:.*?}', repo_route=True)
+
# Tags
config.add_route(
name='tags_home',
@@ -60,14 +222,35 @@ def includeme(config):
name='branches_home',
pattern='/{repo_name:.*?[^/]}/branches', repo_route=True)
+ # Bookmarks
config.add_route(
name='bookmarks_home',
pattern='/{repo_name:.*?[^/]}/bookmarks', repo_route=True)
+ # Forks
+ config.add_route(
+ name='repo_fork_new',
+ pattern='/{repo_name:.*?[^/]}/fork', repo_route=True,
+ repo_accepted_types=['hg', 'git'])
+
+ config.add_route(
+ name='repo_fork_create',
+ pattern='/{repo_name:.*?[^/]}/fork/create', repo_route=True,
+ repo_accepted_types=['hg', 'git'])
+
+ config.add_route(
+ name='repo_forks_show_all',
+ pattern='/{repo_name:.*?[^/]}/forks', repo_route=True,
+ repo_accepted_types=['hg', 'git'])
+ config.add_route(
+ name='repo_forks_data',
+ pattern='/{repo_name:.*?[^/]}/forks/data', repo_route=True,
+ repo_accepted_types=['hg', 'git'])
+
# Pull Requests
config.add_route(
name='pullrequest_show',
- pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id}',
+ pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}',
repo_route=True)
config.add_route(
@@ -80,10 +263,56 @@ def includeme(config):
pattern='/{repo_name:.*?[^/]}/pull-request-data',
repo_route=True, repo_accepted_types=['hg', 'git'])
+ config.add_route(
+ name='pullrequest_repo_refs',
+ pattern='/{repo_name:.*?[^/]}/pull-request/refs/{target_repo_name:.*?[^/]}',
+ repo_route=True)
+
+ config.add_route(
+ name='pullrequest_repo_destinations',
+ pattern='/{repo_name:.*?[^/]}/pull-request/repo-destinations',
+ repo_route=True)
+
+ config.add_route(
+ name='pullrequest_new',
+ pattern='/{repo_name:.*?[^/]}/pull-request/new',
+ repo_route=True, repo_accepted_types=['hg', 'git'])
+
+ config.add_route(
+ name='pullrequest_create',
+ pattern='/{repo_name:.*?[^/]}/pull-request/create',
+ repo_route=True, repo_accepted_types=['hg', 'git'])
+
+ config.add_route(
+ name='pullrequest_update',
+ pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/update',
+ repo_route=True)
+
+ config.add_route(
+ name='pullrequest_merge',
+ pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/merge',
+ repo_route=True)
+
+ config.add_route(
+ name='pullrequest_delete',
+ pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/delete',
+ repo_route=True)
+
+ config.add_route(
+ name='pullrequest_comment_create',
+ pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment',
+ repo_route=True)
+
+ config.add_route(
+ name='pullrequest_comment_delete',
+ pattern='/{repo_name:.*?[^/]}/pull-request/{pull_request_id:\d+}/comment/{comment_id}/delete',
+ repo_route=True, repo_accepted_types=['hg', 'git'])
+
# Settings
config.add_route(
name='edit_repo',
pattern='/{repo_name:.*?[^/]}/settings', repo_route=True)
+ # update is POST on edit_repo
# Settings advanced
config.add_route(
@@ -112,7 +341,75 @@ def includeme(config):
name='edit_repo_perms',
pattern='/{repo_name:.*?[^/]}/settings/permissions', repo_route=True)
- # Repo Review Rules
+ # Maintenance
+ config.add_route(
+ name='edit_repo_maintenance',
+ pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
+
+ config.add_route(
+ name='edit_repo_maintenance_execute',
+ pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
+
+ # Fields
+ config.add_route(
+ name='edit_repo_fields',
+ pattern='/{repo_name:.*?[^/]}/settings/fields', repo_route=True)
+ config.add_route(
+ name='edit_repo_fields_create',
+ pattern='/{repo_name:.*?[^/]}/settings/fields/create', repo_route=True)
+ config.add_route(
+ name='edit_repo_fields_delete',
+ pattern='/{repo_name:.*?[^/]}/settings/fields/{field_id}/delete', repo_route=True)
+
+ # Locking
+ config.add_route(
+ name='repo_edit_toggle_locking',
+ pattern='/{repo_name:.*?[^/]}/settings/toggle_locking', repo_route=True)
+
+ # Remote
+ config.add_route(
+ name='edit_repo_remote',
+ pattern='/{repo_name:.*?[^/]}/settings/remote', repo_route=True)
+ config.add_route(
+ name='edit_repo_remote_pull',
+ pattern='/{repo_name:.*?[^/]}/settings/remote/pull', repo_route=True)
+
+ # Statistics
+ config.add_route(
+ name='edit_repo_statistics',
+ pattern='/{repo_name:.*?[^/]}/settings/statistics', repo_route=True)
+ config.add_route(
+ name='edit_repo_statistics_reset',
+ pattern='/{repo_name:.*?[^/]}/settings/statistics/update', repo_route=True)
+
+ # Issue trackers
+ config.add_route(
+ name='edit_repo_issuetracker',
+ pattern='/{repo_name:.*?[^/]}/settings/issue_trackers', repo_route=True)
+ config.add_route(
+ name='edit_repo_issuetracker_test',
+ pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/test', repo_route=True)
+ config.add_route(
+ name='edit_repo_issuetracker_delete',
+ pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/delete', repo_route=True)
+ config.add_route(
+ name='edit_repo_issuetracker_update',
+ pattern='/{repo_name:.*?[^/]}/settings/issue_trackers/update', repo_route=True)
+
+ # VCS Settings
+ config.add_route(
+ name='edit_repo_vcs',
+ pattern='/{repo_name:.*?[^/]}/settings/vcs', repo_route=True)
+ config.add_route(
+ name='edit_repo_vcs_update',
+ pattern='/{repo_name:.*?[^/]}/settings/vcs/update', repo_route=True)
+
+ # svn pattern
+ config.add_route(
+ name='edit_repo_vcs_svn_pattern_delete',
+ pattern='/{repo_name:.*?[^/]}/settings/vcs/svn_pattern/delete', repo_route=True)
+
+ # Repo Review Rules (EE feature)
config.add_route(
name='repo_reviewers',
pattern='/{repo_name:.*?[^/]}/settings/review/rules', repo_route=True)
@@ -121,18 +418,9 @@ def includeme(config):
name='repo_default_reviewers_data',
pattern='/{repo_name:.*?[^/]}/settings/review/default-reviewers', repo_route=True)
- # Maintenance
- config.add_route(
- name='repo_maintenance',
- pattern='/{repo_name:.*?[^/]}/settings/maintenance', repo_route=True)
-
- config.add_route(
- name='repo_maintenance_execute',
- pattern='/{repo_name:.*?[^/]}/settings/maintenance/execute', repo_route=True)
-
# Strip
config.add_route(
- name='strip',
+ name='edit_repo_strip',
pattern='/{repo_name:.*?[^/]}/settings/strip', repo_route=True)
config.add_route(
@@ -143,6 +431,20 @@ def includeme(config):
name='strip_execute',
pattern='/{repo_name:.*?[^/]}/settings/strip_execute', repo_route=True)
+ # Audit logs
+ config.add_route(
+ name='edit_repo_audit_logs',
+ pattern='/{repo_name:.*?[^/]}/settings/audit_logs', repo_route=True)
+
+ # ATOM/RSS Feed
+ config.add_route(
+ name='rss_feed_home',
+ pattern='/{repo_name:.*?[^/]}/feed/rss', repo_route=True)
+
+ config.add_route(
+ name='atom_feed_home',
+ pattern='/{repo_name:.*?[^/]}/feed/atom', repo_route=True)
+
# NOTE(marcink): needs to be at the end for catch-all
add_route_with_slash(
config,
@@ -150,4 +452,4 @@ def includeme(config):
pattern='/{repo_name:.*?[^/]}', repo_route=True)
# Scan module for configuration decorators.
- config.scan()
+ config.scan('.views', ignore='.tests')
diff --git a/rhodecode/apps/repository/tests/__init__.py b/rhodecode/apps/repository/tests/__init__.py
--- a/rhodecode/apps/repository/tests/__init__.py
+++ b/rhodecode/apps/repository/tests/__init__.py
@@ -0,0 +1,19 @@
+# -*- 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/
diff --git a/rhodecode/tests/functional/test_changelog.py b/rhodecode/apps/repository/tests/test_repo_changelog.py
rename from rhodecode/tests/functional/test_changelog.py
rename to rhodecode/apps/repository/tests/test_repo_changelog.py
--- a/rhodecode/tests/functional/test_changelog.py
+++ b/rhodecode/apps/repository/tests/test_repo_changelog.py
@@ -22,20 +22,32 @@ import re
import pytest
-from rhodecode.controllers.changelog import DEFAULT_CHANGELOG_SIZE
-from rhodecode.tests import url, TestController
-from rhodecode.tests.utils import AssertResponse
-
+from rhodecode.apps.repository.views.repo_changelog import DEFAULT_CHANGELOG_SIZE
+from rhodecode.tests import TestController
MATCH_HASH = re.compile(r'r(\d+):[\da-f]+ ')
+def route_path(name, params=None, **kwargs):
+ import urllib
+
+ base_url = {
+ 'repo_changelog':'/{repo_name}/changelog',
+ 'repo_changelog_file':'/{repo_name}/changelog/{commit_id}/{f_path}',
+ 'repo_changelog_elements':'/{repo_name}/changelog_elements',
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
+
+
class TestChangelogController(TestController):
- def test_index(self, backend):
+ def test_changelog(self, backend):
self.log_user()
- response = self.app.get(url(controller='changelog', action='index',
- repo_name=backend.repo_name))
+ response = self.app.get(
+ route_path('repo_changelog', repo_name=backend.repo_name))
first_idx = -1
last_idx = -DEFAULT_CHANGELOG_SIZE
@@ -43,39 +55,30 @@ class TestChangelogController(TestContro
response, first_idx, last_idx, backend)
@pytest.mark.backends("hg", "git")
- def test_index_filtered_by_branch(self, backend):
+ def test_changelog_filtered_by_branch(self, backend):
self.log_user()
self.app.get(
- url(
- controller='changelog',
- action='index',
- repo_name=backend.repo_name,
- branch=backend.default_branch_name),
+ route_path('repo_changelog', repo_name=backend.repo_name,
+ params=dict(branch=backend.default_branch_name)),
status=200)
@pytest.mark.backends("svn")
- def test_index_filtered_by_branch_svn(self, autologin_user, backend):
+ def test_changelog_filtered_by_branch_svn(self, autologin_user, backend):
repo = backend['svn-simple-layout']
response = self.app.get(
- url(
- controller='changelog',
- action='index',
- repo_name=repo.repo_name,
- branch='trunk'),
+ route_path('repo_changelog', repo_name=repo.repo_name,
+ params=dict(branch='trunk')),
status=200)
self.assert_commits_on_page(
response, indexes=[15, 12, 7, 3, 2, 1])
- def test_index_filtered_by_wrong_branch(self, backend):
+ def test_changelog_filtered_by_wrong_branch(self, backend):
self.log_user()
branch = 'wrong-branch-name'
response = self.app.get(
- url(
- controller='changelog',
- action='index',
- repo_name=backend.repo_name,
- branch=branch),
+ route_path('repo_changelog', repo_name=backend.repo_name,
+ params=dict(branch=branch)),
status=302)
expected_url = '/{repo}/changelog/{branch}'.format(
repo=backend.repo_name, branch=branch)
@@ -89,7 +92,7 @@ class TestChangelogController(TestContro
assert found_indexes == indexes
@pytest.mark.xfail_backends("svn", reason="Depends on branch support")
- def test_index_filtered_by_branch_with_merges(
+ def test_changelog_filtered_by_branch_with_merges(
self, autologin_user, backend):
# Note: The changelog of branch "b" does not contain the commit "a1"
@@ -104,33 +107,27 @@ class TestChangelogController(TestContro
backend.create_repo(commits)
self.app.get(
- url('changelog_home',
- controller='changelog',
- action='index',
- repo_name=backend.repo_name,
- branch='b'),
+ route_path('repo_changelog', repo_name=backend.repo_name,
+ params=dict(branch='b')),
status=200)
@pytest.mark.backends("hg")
- def test_index_closed_branches(self, autologin_user, backend):
+ def test_changelog_closed_branches(self, autologin_user, backend):
repo = backend['closed_branch']
response = self.app.get(
- url(
- controller='changelog',
- action='index',
- repo_name=repo.repo_name,
- branch='experimental'),
+ route_path('repo_changelog', repo_name=repo.repo_name,
+ params=dict(branch='experimental')),
status=200)
self.assert_commits_on_page(
response, indexes=[3, 1])
- def test_index_pagination(self, backend):
+ def test_changelog_pagination(self, backend):
self.log_user()
# pagination, walk up to page 6
- changelog_url = url(
- controller='changelog', action='index',
- repo_name=backend.repo_name)
+ changelog_url = route_path(
+ 'repo_changelog', repo_name=backend.repo_name)
+
for page in range(1, 7):
response = self.app.get(changelog_url, {'page': page})
@@ -166,27 +163,33 @@ class TestChangelogController(TestContro
first_commit_of_next_page.idx, first_commit_of_next_page.short_id)
assert first_span_of_next_page not in response
- def test_index_with_filenode(self, backend):
+ @pytest.mark.parametrize('test_path', [
+ 'vcs/exceptions.py',
+ '/vcs/exceptions.py',
+ '//vcs/exceptions.py'
+ ])
+ def test_changelog_with_filenode(self, backend, test_path):
self.log_user()
- response = self.app.get(url(
- controller='changelog', action='index', revision='tip',
- f_path='/vcs/exceptions.py', repo_name=backend.repo_name))
+ response = self.app.get(
+ route_path('repo_changelog_file', repo_name=backend.repo_name,
+ commit_id='tip', f_path=test_path),
+ )
# history commits messages
response.mustcontain('Added exceptions module, this time for real')
response.mustcontain('Added not implemented hg backend test case')
response.mustcontain('Added BaseChangeset class')
- def test_index_with_filenode_that_is_dirnode(self, backend):
+ def test_changelog_with_filenode_that_is_dirnode(self, backend):
self.log_user()
- response = self.app.get(url(controller='changelog', action='index',
- revision='tip', f_path='/tests',
- repo_name=backend.repo_name))
- assert response.status == '302 Found'
+ self.app.get(
+ route_path('repo_changelog_file', repo_name=backend.repo_name,
+ commit_id='tip', f_path='/tests'),
+ status=302)
- def test_index_with_filenode_not_existing(self, backend):
+ def test_changelog_with_filenode_not_existing(self, backend):
self.log_user()
- response = self.app.get(url(controller='changelog', action='index',
- revision='tip', f_path='/wrong_path',
- repo_name=backend.repo_name))
- assert response.status == '302 Found'
+ self.app.get(
+ route_path('repo_changelog_file', repo_name=backend.repo_name,
+ commit_id='tip', f_path='wrong_path'),
+ status=302)
diff --git a/rhodecode/tests/functional/test_commit_comments.py b/rhodecode/apps/repository/tests/test_repo_commit_comments.py
rename from rhodecode/tests/functional/test_commit_comments.py
rename to rhodecode/apps/repository/tests/test_repo_commit_comments.py
--- a/rhodecode/tests/functional/test_commit_comments.py
+++ b/rhodecode/apps/repository/tests/test_repo_commit_comments.py
@@ -18,18 +18,33 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
-from pylons.i18n import ungettext
import pytest
-from rhodecode.tests import *
+from rhodecode.tests import TestController
+
from rhodecode.model.db import (
ChangesetComment, Notification, UserNotification)
from rhodecode.model.meta import Session
from rhodecode.lib import helpers as h
+def route_path(name, params=None, **kwargs):
+ import urllib
+
+ base_url = {
+ 'repo_commit': '/{repo_name}/changeset/{commit_id}',
+ 'repo_commit_comment_create': '/{repo_name}/changeset/{commit_id}/comment/create',
+ 'repo_commit_comment_preview': '/{repo_name}/changeset/{commit_id}/comment/preview',
+ 'repo_commit_comment_delete': '/{repo_name}/changeset/{commit_id}/comment/{comment_id}/delete',
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
+
+
@pytest.mark.backends("git", "hg", "svn")
-class TestCommitCommentsController(TestController):
+class TestRepoCommitCommentsView(TestController):
@pytest.fixture(autouse=True)
def prepare(self, request, pylonsapp):
@@ -62,12 +77,13 @@ class TestCommitCommentsController(TestC
params = {'text': text, 'csrf_token': self.csrf_token,
'comment_type': comment_type}
self.app.post(
- url(controller='changeset', action='comment',
- repo_name=backend.repo_name, revision=commit_id), params=params)
+ route_path('repo_commit_comment_create',
+ repo_name=backend.repo_name, commit_id=commit_id),
+ params=params)
response = self.app.get(
- url(controller='changeset', action='index',
- repo_name=backend.repo_name, revision=commit_id))
+ route_path('repo_commit',
+ repo_name=backend.repo_name, commit_id=commit_id))
# test DB
assert ChangesetComment.query().count() == 1
@@ -103,12 +119,13 @@ class TestCommitCommentsController(TestC
'csrf_token': self.csrf_token}
self.app.post(
- url(controller='changeset', action='comment',
- repo_name=backend.repo_name, revision=commit_id), params=params)
+ route_path('repo_commit_comment_create',
+ repo_name=backend.repo_name, commit_id=commit_id),
+ params=params)
response = self.app.get(
- url(controller='changeset', action='index',
- repo_name=backend.repo_name, revision=commit_id))
+ route_path('repo_commit',
+ repo_name=backend.repo_name, commit_id=commit_id))
# test DB
assert ChangesetComment.query().count() == 1
@@ -153,12 +170,13 @@ class TestCommitCommentsController(TestC
params = {'text': text, 'csrf_token': self.csrf_token}
self.app.post(
- url(controller='changeset', action='comment',
- repo_name=backend.repo_name, revision=commit_id), params=params)
+ route_path('repo_commit_comment_create',
+ repo_name=backend.repo_name, commit_id=commit_id),
+ params=params)
response = self.app.get(
- url(controller='changeset', action='index',
- repo_name=backend.repo_name, revision=commit_id))
+ route_path('repo_commit',
+ repo_name=backend.repo_name, commit_id=commit_id))
# test DB
assert ChangesetComment.query().count() == 1
assert_comment_links(response, ChangesetComment.query().count(), 0)
@@ -183,12 +201,14 @@ class TestCommitCommentsController(TestC
'csrf_token': self.csrf_token}
self.app.post(
- url(controller='changeset', action='comment',
- repo_name=backend.repo_name, revision=commit_id), params=params)
+ route_path(
+ 'repo_commit_comment_create',
+ repo_name=backend.repo_name, commit_id=commit_id),
+ params=params)
response = self.app.get(
- url(controller='changeset', action='index',
- repo_name=backend.repo_name, revision=commit_id))
+ route_path('repo_commit',
+ repo_name=backend.repo_name, commit_id=commit_id))
# test DB
assert ChangesetComment.query().count() == 1
@@ -218,9 +238,9 @@ class TestCommitCommentsController(TestC
params = {'text': text, 'csrf_token': self.csrf_token}
self.app.post(
- url(
- controller='changeset', action='comment',
- repo_name=backend.repo_name, revision=commit_id),
+ route_path(
+ 'repo_commit_comment_create',
+ repo_name=backend.repo_name, commit_id=commit_id),
params=params)
comments = ChangesetComment.query().all()
@@ -228,16 +248,18 @@ class TestCommitCommentsController(TestC
comment_id = comments[0].comment_id
self.app.post(
- url(controller='changeset', action='delete_comment',
- repo_name=backend.repo_name, comment_id=comment_id),
- params={'_method': 'delete', 'csrf_token': self.csrf_token})
+ route_path('repo_commit_comment_delete',
+ repo_name=backend.repo_name,
+ commit_id=commit_id,
+ comment_id=comment_id),
+ params={'csrf_token': self.csrf_token})
comments = ChangesetComment.query().all()
assert len(comments) == 0
response = self.app.get(
- url(controller='changeset', action='index',
- repo_name=backend.repo_name, revision=commit_id))
+ route_path('repo_commit',
+ repo_name=backend.repo_name, commit_id=commit_id))
assert_comment_links(response, 0, 0)
@pytest.mark.parametrize('renderer, input, output', [
@@ -251,36 +273,39 @@ class TestCommitCommentsController(TestC
('markdown', '**bold**', 'bold '),
], ids=['rst-plain', 'rst-header', 'rst-italics', 'rst-bold', 'md-plain',
'md-header', 'md-italics', 'md-bold', ])
- def test_preview(self, renderer, input, output, backend):
+ def test_preview(self, renderer, input, output, backend, xhr_header):
self.log_user()
params = {
'renderer': renderer,
'text': input,
'csrf_token': self.csrf_token
}
- environ = {
- 'HTTP_X_PARTIAL_XHR': 'true'
- }
+ commit_id = '0' * 16 # fake this for tests
response = self.app.post(
- url(controller='changeset',
- action='preview_comment',
- repo_name=backend.repo_name),
+ route_path('repo_commit_comment_preview',
+ repo_name=backend.repo_name, commit_id=commit_id,),
params=params,
- extra_environ=environ)
+ extra_environ=xhr_header)
response.mustcontain(output)
def assert_comment_links(response, comments, inline_comments):
- comments_text = ungettext("%d Commit comment",
- "%d Commit comments", comments) % comments
+ if comments == 1:
+ comments_text = "%d Commit comment" % comments
+ else:
+ comments_text = "%d Commit comments" % comments
+
+ if inline_comments == 1:
+ inline_comments_text = "%d Inline Comment" % inline_comments
+ else:
+ inline_comments_text = "%d Inline Comments" % inline_comments
+
if comments:
response.mustcontain('%s ,' % comments_text)
else:
response.mustcontain(comments_text)
- inline_comments_text = ungettext("%d Inline Comment", "%d Inline Comments",
- inline_comments) % inline_comments
if inline_comments:
response.mustcontain(
'id="inline-comments-counter">%s' % inline_comments_text)
diff --git a/rhodecode/tests/functional/test_changeset.py b/rhodecode/apps/repository/tests/test_repo_commits.py
rename from rhodecode/tests/functional/test_changeset.py
rename to rhodecode/apps/repository/tests/test_repo_commits.py
--- a/rhodecode/tests/functional/test_changeset.py
+++ b/rhodecode/apps/repository/tests/test_repo_commits.py
@@ -21,40 +21,56 @@
import pytest
from rhodecode.lib.helpers import _shorten_commit_id
-from rhodecode.tests import url
+
+
+def route_path(name, params=None, **kwargs):
+ import urllib
+
+ base_url = {
+ 'repo_commit': '/{repo_name}/changeset/{commit_id}',
+ 'repo_commit_children': '/{repo_name}/changeset_children/{commit_id}',
+ 'repo_commit_parents': '/{repo_name}/changeset_parents/{commit_id}',
+ 'repo_commit_raw': '/{repo_name}/changeset-diff/{commit_id}',
+ 'repo_commit_patch': '/{repo_name}/changeset-patch/{commit_id}',
+ 'repo_commit_download': '/{repo_name}/changeset-download/{commit_id}',
+ 'repo_commit_data': '/{repo_name}/changeset-data/{commit_id}',
+ 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
@pytest.mark.usefixtures("app")
-class TestChangesetController(object):
+class TestRepoCommitView(object):
- def test_index(self, backend):
+ def test_show_commit(self, backend):
commit_id = self.commit_id[backend.alias]
- response = self.app.get(url(
- controller='changeset', action='index',
- repo_name=backend.repo_name, revision=commit_id))
+ response = self.app.get(route_path(
+ 'repo_commit', repo_name=backend.repo_name, commit_id=commit_id))
response.mustcontain('Added a symlink')
response.mustcontain(commit_id)
response.mustcontain('No newline at end of file')
- def test_index_raw(self, backend):
+ def test_show_raw(self, backend):
commit_id = self.commit_id[backend.alias]
- response = self.app.get(url(
- controller='changeset', action='changeset_raw',
- repo_name=backend.repo_name, revision=commit_id))
+ response = self.app.get(route_path(
+ 'repo_commit_raw',
+ repo_name=backend.repo_name, commit_id=commit_id))
assert response.body == self.diffs[backend.alias]
- def test_index_raw_patch(self, backend):
- response = self.app.get(url(
- controller='changeset', action='changeset_patch',
- repo_name=backend.repo_name,
- revision=self.commit_id[backend.alias]))
+ def test_show_raw_patch(self, backend):
+ response = self.app.get(route_path(
+ 'repo_commit_patch', repo_name=backend.repo_name,
+ commit_id=self.commit_id[backend.alias]))
assert response.body == self.patches[backend.alias]
- def test_index_changeset_download(self, backend):
- response = self.app.get(url(
- controller='changeset', action='changeset_download',
+ def test_commit_download(self, backend):
+ response = self.app.get(route_path(
+ 'repo_commit_download',
repo_name=backend.repo_name,
- revision=self.commit_id[backend.alias]))
+ commit_id=self.commit_id[backend.alias]))
assert response.body == self.diffs[backend.alias]
def test_single_commit_page_different_ops(self, backend):
@@ -64,9 +80,9 @@ class TestChangesetController(object):
'svn': '337',
}
commit_id = commit_id[backend.alias]
- response = self.app.get(url(
- controller='changeset', action='index',
- repo_name=backend.repo_name, revision=commit_id))
+ response = self.app.get(route_path(
+ 'repo_commit',
+ repo_name=backend.repo_name, commit_id=commit_id))
response.mustcontain(_shorten_commit_id(commit_id))
response.mustcontain('21 files changed: 943 inserted, 288 deleted')
@@ -98,9 +114,9 @@ class TestChangesetController(object):
}
commit_ids = commit_id_range[backend.alias]
commit_id = '%s...%s' % (commit_ids[0], commit_ids[1])
- response = self.app.get(url(
- controller='changeset', action='index',
- repo_name=backend.repo_name, revision=commit_id))
+ response = self.app.get(route_path(
+ 'repo_commit',
+ repo_name=backend.repo_name, commit_id=commit_id))
response.mustcontain(_shorten_commit_id(commit_ids[0]))
response.mustcontain(_shorten_commit_id(commit_ids[1]))
@@ -137,8 +153,8 @@ class TestChangesetController(object):
'337'),
}
commit_ids = commit_id_range[backend.alias]
- response = self.app.get(url(
- controller='compare', action='compare',
+ response = self.app.get(route_path(
+ 'repo_compare',
repo_name=backend.repo_name,
source_ref_type='rev', source_ref=commit_ids[0],
target_ref_type='rev', target_ref=commit_ids[1], ))
@@ -188,9 +204,10 @@ class TestChangesetController(object):
def _check_changeset_range(
self, backend, commit_id_ranges, commit_id_range_result):
response = self.app.get(
- url(controller='changeset', action='index',
- repo_name=backend.repo_name,
- revision=commit_id_ranges[backend.alias]))
+ route_path('repo_commit',
+ repo_name=backend.repo_name,
+ commit_id=commit_id_ranges[backend.alias]))
+
expected_result = commit_id_range_result[backend.alias]
response.mustcontain('{} commits'.format(len(expected_result)))
for commit_id in expected_result:
diff --git a/rhodecode/tests/functional/test_compare.py b/rhodecode/apps/repository/tests/test_repo_compare.py
rename from rhodecode/tests/functional/test_compare.py
rename to rhodecode/apps/repository/tests/test_repo_compare.py
--- a/rhodecode/tests/functional/test_compare.py
+++ b/rhodecode/apps/repository/tests/test_repo_compare.py
@@ -23,12 +23,30 @@ import pytest
import lxml.html
from rhodecode.lib.vcs.exceptions import RepositoryRequirementError
-from rhodecode.tests import url, assert_session_flash
+from rhodecode.tests import assert_session_flash
from rhodecode.tests.utils import AssertResponse, commit_change
+def route_path(name, params=None, **kwargs):
+ import urllib
+
+ base_url = {
+ 'repo_compare_select': '/{repo_name}/compare',
+ 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
+
+
@pytest.mark.usefixtures("autologin_user", "app")
-class TestCompareController(object):
+class TestCompareView(object):
+
+ def test_compare_index_is_reached_at_least_once(self, backend):
+ repo = backend.repo
+ self.app.get(
+ route_path('repo_compare_select', repo_name=repo.repo_name))
@pytest.mark.xfail_backends("svn", reason="Requires pull")
def test_compare_remote_with_different_commit_indexes(self, backend):
@@ -85,14 +103,14 @@ class TestCompareController(object):
# Comparing the revisions
response = self.app.get(
- url('compare_url',
+ route_path('repo_compare',
repo_name=origin.repo_name,
source_ref_type="rev",
source_ref=commit3.raw_id,
- target_repo=fork.repo_name,
target_ref_type="rev",
target_ref=commit4.raw_id,
- merge='1',))
+ params=dict(merge='1', target_repo=fork.repo_name)
+ ))
compare_page = ComparePage(response)
compare_page.contains_commits([commit4])
@@ -123,14 +141,14 @@ class TestCompareController(object):
commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
response = self.app.get(
- url('compare_url',
+ route_path('repo_compare',
repo_name=repo1.repo_name,
source_ref_type="branch",
source_ref=commit_id2,
- target_repo=repo2.repo_name,
target_ref_type="branch",
target_ref=commit_id1,
- merge='1',))
+ params=dict(merge='1', target_repo=repo2.repo_name)
+ ))
response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
@@ -180,14 +198,14 @@ class TestCompareController(object):
commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
response = self.app.get(
- url('compare_url',
+ route_path('repo_compare',
repo_name=repo1.repo_name,
source_ref_type="branch",
source_ref=commit_id2,
- target_repo=repo2.repo_name,
target_ref_type="branch",
target_ref=commit_id1,
- merge='1'))
+ params=dict(merge='1', target_repo=repo2.repo_name),
+ ))
response.mustcontain('%s@%s' % (repo1.repo_name, commit_id2))
response.mustcontain('%s@%s' % (repo2.repo_name, commit_id1))
@@ -211,17 +229,16 @@ class TestCompareController(object):
fork = backend.create_repo(number_of_commits=1)
response = self.app.get(
- url('compare_url',
+ route_path('repo_compare',
repo_name=orig.repo_name,
- action="compare",
source_ref_type="rev",
source_ref="tip",
target_ref_type="rev",
target_ref="tip",
- merge='1',
- target_repo=fork.repo_name),
- status=400)
-
+ params=dict(merge='1', target_repo=fork.repo_name),
+ ),
+ status=302)
+ response = response.follow()
response.mustcontain("Repositories unrelated.")
@pytest.mark.xfail_backends("svn")
@@ -271,15 +288,15 @@ class TestCompareController(object):
message='commit6', vcs_type=backend.alias, parent=commit4)
response = self.app.get(
- url('compare_url',
+ route_path('repo_compare',
repo_name=repo2.repo_name,
source_ref_type="rev",
# parent of commit2, in target repo2
source_ref=commit1.raw_id,
- target_repo=repo1.repo_name,
target_ref_type="rev",
target_ref=commit4.raw_id,
- merge='1',))
+ params=dict(merge='1', target_repo=repo1.repo_name),
+ ))
response.mustcontain('%s@%s' % (repo2.repo_name, commit1.short_id))
response.mustcontain('%s@%s' % (repo1.repo_name, commit4.short_id))
@@ -337,14 +354,15 @@ class TestCompareController(object):
message='commit6', vcs_type=backend.alias, parent=commit4)
response = self.app.get(
- url('compare_url',
+ route_path('repo_compare',
repo_name=repo1.repo_name,
source_ref_type="rev",
# parent of commit3, not in source repo2
source_ref=commit2.raw_id,
target_ref_type="rev",
target_ref=commit5.raw_id,
- merge='1',))
+ params=dict(merge='1'),
+ ))
response.mustcontain('%s@%s' % (repo1.repo_name, commit2.short_id))
response.mustcontain('%s@%s' % (repo1.repo_name, commit5.short_id))
@@ -367,14 +385,14 @@ class TestCompareController(object):
commit_id2 = repo1.get_commit(commit_idx=6).raw_id
response = self.app.get(
- url('compare_url',
+ route_path('repo_compare',
repo_name=repo1.repo_name,
source_ref_type="rev",
source_ref=commit_id1,
target_ref_type="rev",
target_ref=commit_id2,
- target_repo=repo2.repo_name,
- merge='1',))
+ params=dict(merge='1', target_repo=repo2.repo_name),
+ ))
response.mustcontain('%s@%s' % (repo1.repo_name, commit_id1))
response.mustcontain('%s@%s' % (repo2.repo_name, commit_id2))
@@ -432,14 +450,14 @@ class TestCompareController(object):
commit_id2 = repo2.scm_instance().DEFAULT_BRANCH_NAME
response = self.app.get(
- url('compare_url',
+ route_path('repo_compare',
repo_name=r2_name,
source_ref_type="branch",
source_ref=commit_id1,
target_ref_type="branch",
target_ref=commit_id2,
- target_repo=r1_name,
- merge='1',))
+ params=dict(merge='1', target_repo=r1_name),
+ ))
response.mustcontain('%s@%s' % (r2_name, commit_id1))
response.mustcontain('%s@%s' % (r1_name, commit_id2))
@@ -453,14 +471,14 @@ class TestCompareController(object):
# compare !
response = self.app.get(
- url('compare_url',
+ route_path('repo_compare',
repo_name=r2_name,
source_ref_type="branch",
source_ref=commit_id1,
target_ref_type="branch",
target_ref=commit_id2,
- target_repo=r1_name,
- merge='1',))
+ params=dict(merge='1', target_repo=r1_name),
+ ))
response.mustcontain('%s@%s' % (r2_name, commit_id1))
response.mustcontain('%s@%s' % (r1_name, commit_id2))
@@ -471,19 +489,20 @@ class TestCompareController(object):
compare_page.contains_change_summary(1, 1, 0)
@pytest.mark.xfail_backends("svn")
- def test_compare_commits(self, backend):
+ def test_compare_commits(self, backend, xhr_header):
commit0 = backend.repo.get_commit(commit_idx=0)
commit1 = backend.repo.get_commit(commit_idx=1)
response = self.app.get(
- url('compare_url',
+ route_path('repo_compare',
repo_name=backend.repo_name,
source_ref_type="rev",
source_ref=commit0.raw_id,
target_ref_type="rev",
target_ref=commit1.raw_id,
- merge='1',),
- extra_environ={'HTTP_X_PARTIAL_XHR': '1'},)
+ params=dict(merge='1')
+ ),
+ extra_environ=xhr_header,)
# outgoing commits between those commits
compare_page = ComparePage(response)
@@ -494,14 +513,14 @@ class TestCompareController(object):
badrepo = 'badrepo'
response = self.app.get(
- url('compare_url',
+ route_path('repo_compare',
repo_name=badrepo,
source_ref_type="rev",
source_ref='tip',
target_ref_type="rev",
target_ref='tip',
- target_repo=repo.repo_name,
- merge='1',),
+ params=dict(merge='1', target_repo=repo.repo_name)
+ ),
status=404)
def test_errors_when_comparing_unknown_target_repo(self, backend):
@@ -509,14 +528,14 @@ class TestCompareController(object):
badrepo = 'badrepo'
response = self.app.get(
- url('compare_url',
+ route_path('repo_compare',
repo_name=repo.repo_name,
source_ref_type="rev",
source_ref='tip',
target_ref_type="rev",
target_ref='tip',
- target_repo=badrepo,
- merge='1',),
+ params=dict(merge='1', target_repo=badrepo),
+ ),
status=302)
redirected = response.follow()
redirected.mustcontain(
@@ -526,13 +545,14 @@ class TestCompareController(object):
commit0 = backend_stub.repo.get_commit(commit_idx=0)
commit1 = backend_stub.repo.get_commit(commit_idx=1)
- response = self.app.get(url('compare_url',
- repo_name=backend_stub.repo_name,
- source_ref_type="rev",
- source_ref=commit0.raw_id,
- target_ref_type="rev",
- target_ref=commit1.raw_id,
- ),)
+ response = self.app.get(
+ route_path('repo_compare',
+ repo_name=backend_stub.repo_name,
+ source_ref_type="rev",
+ source_ref=commit0.raw_id,
+ target_ref_type="rev",
+ target_ref=commit1.raw_id,
+ ))
# outgoing commits between those commits
compare_page = ComparePage(response)
@@ -554,15 +574,14 @@ class TestCompareController(object):
compare_mock.side_effect = RepositoryRequirementError()
response = self.app.get(
- url('compare_url',
+ route_path('repo_compare',
repo_name=orig.repo_name,
- action="compare",
source_ref_type="rev",
source_ref="tip",
target_ref_type="rev",
target_ref="tip",
- merge='1',
- target_repo=fork.repo_name),
+ params=dict(merge='1', target_repo=fork.repo_name),
+ ),
status=302)
assert_session_flash(
@@ -577,13 +596,14 @@ class TestCompareControllerSvn(object):
repo = backend_svn['svn-simple-layout']
commit_id = repo.get_commit(commit_idx=-1).raw_id
response = app.get(
- url('compare_url',
+ route_path('repo_compare',
repo_name=repo.repo_name,
source_ref_type="tag",
source_ref="%s@%s" % ('tags/v0.1', commit_id),
target_ref_type="tag",
target_ref="%s@%s" % ('tags/v0.2', commit_id),
- merge='1',),
+ params=dict(merge='1'),
+ ),
status=200)
# Expecting no commits, since both paths are at the same revision
@@ -599,13 +619,14 @@ class TestCompareControllerSvn(object):
source_id = repo.get_commit(commit_idx=-6).raw_id
target_id = repo.get_commit(commit_idx=-1).raw_id
response = app.get(
- url('compare_url',
+ route_path('repo_compare',
repo_name=repo.repo_name,
source_ref_type="tag",
source_ref="%s@%s" % ('tags/v0.1', source_id),
target_ref_type="tag",
target_ref="%s@%s" % ('tags/v0.2', target_id),
- merge='1',),
+ params=dict(merge='1')
+ ),
status=200)
# It should show commits
@@ -673,3 +694,4 @@ class ComparePage(AssertResponse):
def target_source_are_enabled(self):
response = self.response
response.mustcontain("var enable_fields = true;")
+
diff --git a/rhodecode/tests/functional/test_compare_local.py b/rhodecode/apps/repository/tests/test_repo_compare_local.py
rename from rhodecode/tests/functional/test_compare_local.py
rename to rhodecode/apps/repository/tests/test_repo_compare_local.py
--- a/rhodecode/tests/functional/test_compare_local.py
+++ b/rhodecode/apps/repository/tests/test_repo_compare_local.py
@@ -20,20 +20,32 @@
import pytest
-from rhodecode.tests import url
-from rhodecode.tests.functional.test_compare import ComparePage
+from .test_repo_compare import ComparePage
+
+
+def route_path(name, params=None, **kwargs):
+ import urllib
+
+ base_url = {
+ 'repo_compare_select': '/{repo_name}/compare',
+ 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
@pytest.mark.usefixtures("autologin_user", "app")
-class TestCompareController:
+class TestCompareView(object):
@pytest.mark.xfail_backends("svn", msg="Depends on branch and tag support")
def test_compare_tag(self, backend):
tag1 = 'v0.1.2'
tag2 = 'v0.1.3'
response = self.app.get(
- url(
- 'compare_url',
+ route_path(
+ 'repo_compare',
repo_name=backend.repo_name,
source_ref_type="tag",
source_ref=tag1,
@@ -90,8 +102,9 @@ class TestCompareController:
# functional tests.
data = revisions[backend.alias]
- response = self.app.get(url(
- 'compare_url',
+ response = self.app.get(
+ route_path(
+ 'repo_compare',
repo_name=backend.repo_name,
source_ref_type='branch',
source_ref=data['branch'],
@@ -106,8 +119,9 @@ class TestCompareController:
def test_index_branch(self, backend):
head_id = backend.default_head_id
- response = self.app.get(url(
- 'compare_url',
+ response = self.app.get(
+ route_path(
+ 'repo_compare',
repo_name=backend.repo_name,
source_ref_type="branch",
source_ref=head_id,
@@ -126,8 +140,9 @@ class TestCompareController:
commit1 = repo.get_commit(commit_idx=0)
commit2 = repo.get_commit(commit_idx=1)
- response = self.app.get(url(
- 'compare_url',
+ response = self.app.get(
+ route_path(
+ 'repo_compare',
repo_name=backend.repo_name,
source_ref_type="rev",
source_ref=commit1.raw_id,
diff --git a/rhodecode/tests/functional/test_compare_on_single_file.py b/rhodecode/apps/repository/tests/test_repo_compare_on_single_file.py
rename from rhodecode/tests/functional/test_compare_on_single_file.py
rename to rhodecode/apps/repository/tests/test_repo_compare_on_single_file.py
--- a/rhodecode/tests/functional/test_compare_on_single_file.py
+++ b/rhodecode/apps/repository/tests/test_repo_compare_on_single_file.py
@@ -18,29 +18,28 @@
# RhodeCode Enterprise Edition, including its added features, Support services,
# and proprietary license terms, please see https://rhodecode.com/licenses/
-import os
-
-import mock
import pytest
-from rhodecode.controllers.files import FilesController
-from rhodecode.lib import helpers as h
-from rhodecode.lib.compat import OrderedDict
-from rhodecode.lib.ext_json import json
from rhodecode.lib.vcs import nodes
-from rhodecode.lib.vcs.backends.base import EmptyCommit
-from rhodecode.lib.vcs.conf import settings
-from rhodecode.lib.vcs.nodes import FileNode
-from rhodecode.model.db import Repository
-from rhodecode.model.scm import ScmModel
-from rhodecode.tests import (
- url, TEST_USER_ADMIN_LOGIN, assert_session_flash, assert_not_in_session_flash)
from rhodecode.tests.fixture import Fixture
from rhodecode.tests.utils import commit_change
fixture = Fixture()
+def route_path(name, params=None, **kwargs):
+ import urllib
+
+ base_url = {
+ 'repo_compare_select': '/{repo_name}/compare',
+ 'repo_compare': '/{repo_name}/compare/{source_ref_type}@{source_ref}...{target_ref_type}@{target_ref}',
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
+
+
@pytest.mark.usefixtures("autologin_user", "app")
class TestSideBySideDiff(object):
@@ -58,18 +57,15 @@ class TestSideBySideDiff(object):
repo.repo_name, filename=f_path, content=commit2_content,
message='B, child of A', vcs_type=backend.alias, parent=commit1)
- compare_url = url(
- 'compare_url',
+ response = self.app.get(route_path(
+ 'repo_compare',
repo_name=repo.repo_name,
source_ref_type='rev',
source_ref=commit1.raw_id,
- target_repo=repo.repo_name,
target_ref_type='rev',
target_ref=commit2.raw_id,
- f_path=f_path,
- diffmode='sidebyside')
-
- response = self.app.get(compare_url)
+ params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
+ ))
response.mustcontain('Expand 1 commit')
response.mustcontain('1 file changed')
@@ -91,18 +87,15 @@ class TestSideBySideDiff(object):
commit1 = repo.get_commit(commit_idx=0)
commit2 = repo.get_commit(commit_idx=1)
- compare_url = url(
- 'compare_url',
+ response = self.app.get(route_path(
+ 'repo_compare',
repo_name=repo.repo_name,
source_ref_type='rev',
source_ref=commit1.raw_id,
- target_repo=repo.repo_name,
target_ref_type='rev',
target_ref=commit2.raw_id,
- f_path=f_path,
- diffmode='sidebyside')
-
- response = self.app.get(compare_url)
+ params=dict(f_path=f_path, target_repo=repo.repo_name, diffmode='sidebyside')
+ ))
response.mustcontain('Expand 1 commit')
response.mustcontain('1 file changed')
@@ -137,16 +130,16 @@ class TestSideBySideDiff(object):
commit2, commit1 = commit_info['commits']
file_changes = commit_info['changes']
- compare_url = url(
- 'compare_url',
+ response = self.app.get(route_path(
+ 'repo_compare',
repo_name=backend.repo_name,
source_ref_type='rev',
source_ref=commit2,
target_repo=backend.repo_name,
target_ref_type='rev',
target_ref=commit1,
- diffmode='sidebyside')
- response = self.app.get(compare_url)
+ params=dict(target_repo=backend.repo_name, diffmode='sidebyside')
+ ))
response.mustcontain('Expand 1 commit')
response.mustcontain(file_changes)
@@ -176,17 +169,15 @@ class TestSideBySideDiff(object):
commit2, commit1 = commit_info['commits']
file_changes = commit_info['changes']
- compare_url = url(
- 'compare_url',
+ response = self.app.get(route_path(
+ 'repo_compare',
repo_name=backend.repo_name,
source_ref_type='rev',
source_ref=commit2,
- target_repo=backend.repo_name,
target_ref_type='rev',
target_ref=commit1,
- f_path=f_path,
- diffmode='sidebyside')
- response = self.app.get(compare_url)
+ params=dict(f_path=f_path, target_repo=backend.repo_name, diffmode='sidebyside')
+ ))
response.mustcontain('Expand 1 commit')
response.mustcontain(file_changes)
diff --git a/rhodecode/tests/functional/test_feed.py b/rhodecode/apps/repository/tests/test_repo_feed.py
rename from rhodecode/tests/functional/test_feed.py
rename to rhodecode/apps/repository/tests/test_repo_feed.py
--- a/rhodecode/tests/functional/test_feed.py
+++ b/rhodecode/apps/repository/tests/test_repo_feed.py
@@ -17,59 +17,83 @@
# 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.model.auth_token import AuthTokenModel
-from rhodecode.model.db import User
-from rhodecode.tests import *
+from rhodecode.tests import TestController
+
+
+def route_path(name, params=None, **kwargs):
+ import urllib
+
+ base_url = {
+ 'rss_feed_home': '/{repo_name}/feed/rss',
+ 'atom_feed_home': '/{repo_name}/feed/atom',
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
-class TestFeedController(TestController):
+class TestFeedView(TestController):
- def test_rss(self, backend):
+ @pytest.mark.parametrize("feed_type,response_types,content_type",[
+ ('rss', [''],
+ "application/rss+xml"),
+ ('atom', ['xmlns="http://www.w3.org/2005/Atom"', 'xml:lang="en-us"'],
+ "application/atom+xml"),
+ ])
+ def test_feed(self, backend, feed_type, response_types, content_type):
self.log_user()
- response = self.app.get(url(controller='feed', action='rss',
- repo_name=backend.repo_name))
+ response = self.app.get(
+ route_path('{}_feed_home'.format(feed_type),
+ repo_name=backend.repo_name))
+
+ for content in response_types:
+ response.mustcontain(content)
- assert response.content_type == "application/rss+xml"
- assert """""" in response
+ assert response.content_type == content_type
- def test_rss_with_auth_token(self, backend, user_admin):
+ @pytest.mark.parametrize("feed_type, content_type", [
+ ('rss', "application/rss+xml"),
+ ('atom', "application/atom+xml")
+ ])
+ def test_feed_with_auth_token(
+ self, backend, user_admin, feed_type, content_type):
auth_token = user_admin.feed_token
assert auth_token != ''
- response = self.app.get(
- url(controller='feed', action='rss',
- repo_name=backend.repo_name, auth_token=auth_token,
- status=200))
- assert response.content_type == "application/rss+xml"
- assert """""" in response
+ response = self.app.get(
+ route_path(
+ '{}_feed_home'.format(feed_type),
+ repo_name=backend.repo_name,
+ params=dict(auth_token=auth_token)),
+ status=200)
- def test_rss_with_auth_token_of_wrong_type(self, backend, user_util):
+ assert response.content_type == content_type
+
+ @pytest.mark.parametrize("feed_type", ['rss', 'atom'])
+ def test_feed_with_auth_token_of_wrong_type(
+ self, backend, user_util, feed_type):
user = user_util.create_user()
auth_token = AuthTokenModel().create(
user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_API)
auth_token = auth_token.api_key
self.app.get(
- url(controller='feed', action='rss',
- repo_name=backend.repo_name, auth_token=auth_token),
+ route_path(
+ '{}_feed_home'.format(feed_type),
+ repo_name=backend.repo_name,
+ params=dict(auth_token=auth_token)),
status=302)
auth_token = AuthTokenModel().create(
user.user_id, 'test-token', -1, AuthTokenModel.cls.ROLE_FEED)
auth_token = auth_token.api_key
self.app.get(
- url(controller='feed', action='rss',
- repo_name=backend.repo_name, auth_token=auth_token),
+ route_path(
+ '{}_feed_home'.format(feed_type),
+ repo_name=backend.repo_name,
+ params=dict(auth_token=auth_token)),
status=200)
-
- def test_atom(self, backend):
- self.log_user()
- response = self.app.get(url(controller='feed', action='atom',
- repo_name=backend.repo_name))
-
- assert response.content_type == """application/atom+xml"""
- assert """""" in response
-
- tag1 = ''
- tag2 = ''
- assert tag1 in response or tag2 in response
diff --git a/rhodecode/tests/functional/test_files.py b/rhodecode/apps/repository/tests/test_repo_files.py
rename from rhodecode/tests/functional/test_files.py
rename to rhodecode/apps/repository/tests/test_repo_files.py
--- a/rhodecode/tests/functional/test_files.py
+++ b/rhodecode/apps/repository/tests/test_repo_files.py
@@ -23,34 +23,92 @@ import os
import mock
import pytest
-from rhodecode.controllers.files import FilesController
+from rhodecode.apps.repository.views.repo_files import RepoFilesView
from rhodecode.lib import helpers as h
from rhodecode.lib.compat import OrderedDict
from rhodecode.lib.ext_json import json
from rhodecode.lib.vcs import nodes
from rhodecode.lib.vcs.conf import settings
-from rhodecode.tests import (
- url, assert_session_flash, assert_not_in_session_flash)
+from rhodecode.tests import assert_session_flash
from rhodecode.tests.fixture import Fixture
fixture = Fixture()
-NODE_HISTORY = {
- 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
- 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
- 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
-}
+
+def get_node_history(backend_type):
+ return {
+ 'hg': json.loads(fixture.load_resource('hg_node_history_response.json')),
+ 'git': json.loads(fixture.load_resource('git_node_history_response.json')),
+ 'svn': json.loads(fixture.load_resource('svn_node_history_response.json')),
+ }[backend_type]
+
+
+def route_path(name, params=None, **kwargs):
+ import urllib
+ base_url = {
+ 'repo_archivefile': '/{repo_name}/archive/{fname}',
+ 'repo_files_diff': '/{repo_name}/diff/{f_path}',
+ 'repo_files_diff_2way_redirect': '/{repo_name}/diff-2way/{f_path}',
+ 'repo_files': '/{repo_name}/files/{commit_id}/{f_path}',
+ 'repo_files:default_path': '/{repo_name}/files/{commit_id}/',
+ 'repo_files:default_commit': '/{repo_name}/files',
+ 'repo_files:rendered': '/{repo_name}/render/{commit_id}/{f_path}',
+ 'repo_files:annotated': '/{repo_name}/annotate/{commit_id}/{f_path}',
+ 'repo_files:annotated_previous': '/{repo_name}/annotate-previous/{commit_id}/{f_path}',
+ 'repo_files_nodelist': '/{repo_name}/nodelist/{commit_id}/{f_path}',
+ 'repo_file_raw': '/{repo_name}/raw/{commit_id}/{f_path}',
+ 'repo_file_download': '/{repo_name}/download/{commit_id}/{f_path}',
+ 'repo_file_history': '/{repo_name}/history/{commit_id}/{f_path}',
+ 'repo_file_authors': '/{repo_name}/authors/{commit_id}/{f_path}',
+ 'repo_files_remove_file': '/{repo_name}/remove_file/{commit_id}/{f_path}',
+ 'repo_files_delete_file': '/{repo_name}/delete_file/{commit_id}/{f_path}',
+ 'repo_files_edit_file': '/{repo_name}/edit_file/{commit_id}/{f_path}',
+ 'repo_files_update_file': '/{repo_name}/update_file/{commit_id}/{f_path}',
+ 'repo_files_add_file': '/{repo_name}/add_file/{commit_id}/{f_path}',
+ 'repo_files_create_file': '/{repo_name}/create_file/{commit_id}/{f_path}',
+ 'repo_nodetree_full': '/{repo_name}/nodetree_full/{commit_id}/{f_path}',
+ 'repo_nodetree_full:default_path': '/{repo_name}/nodetree_full/{commit_id}/',
+ }[name].format(**kwargs)
+
+ if params:
+ base_url = '{}?{}'.format(base_url, urllib.urlencode(params))
+ return base_url
+
+
+def assert_files_in_response(response, files, params):
+ template = (
+ 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
+ _assert_items_in_response(response, files, template, params)
+
+
+def assert_dirs_in_response(response, dirs, params):
+ template = (
+ 'href="/%(repo_name)s/files/%(commit_id)s/%(name)s"')
+ _assert_items_in_response(response, dirs, template, params)
+
+
+def _assert_items_in_response(response, items, template, params):
+ for item in items:
+ item_params = {'name': item}
+ item_params.update(params)
+ response.mustcontain(template % item_params)
+
+
+def assert_timeago_in_response(response, items, params):
+ for item in items:
+ response.mustcontain(h.age_component(params['date']))
@pytest.mark.usefixtures("app")
-class TestFilesController:
+class TestFilesViews(object):
- def test_index(self, backend):
- response = self.app.get(url(
- controller='files', action='index',
- repo_name=backend.repo_name, revision='tip', f_path='/'))
+ def test_show_files(self, backend):
+ response = self.app.get(
+ route_path('repo_files',
+ repo_name=backend.repo_name,
+ commit_id='tip', f_path='/'))
commit = backend.repo.get_commit()
params = {
@@ -77,21 +135,23 @@ class TestFilesController:
assert_files_in_response(response, files, params)
assert_timeago_in_response(response, files, params)
- def test_index_links_submodules_with_absolute_url(self, backend_hg):
+ def test_show_files_links_submodules_with_absolute_url(self, backend_hg):
repo = backend_hg['subrepos']
- response = self.app.get(url(
- controller='files', action='index',
- repo_name=repo.repo_name, revision='tip', f_path='/'))
+ response = self.app.get(
+ route_path('repo_files',
+ repo_name=repo.repo_name,
+ commit_id='tip', f_path='/'))
assert_response = response.assert_response()
assert_response.contains_one_link(
'absolute-path @ 000000000000', 'http://example.com/absolute-path')
- def test_index_links_submodules_with_absolute_url_subpaths(
+ def test_show_files_links_submodules_with_absolute_url_subpaths(
self, backend_hg):
repo = backend_hg['subrepos']
- response = self.app.get(url(
- controller='files', action='index',
- repo_name=repo.repo_name, revision='tip', f_path='/'))
+ response = self.app.get(
+ route_path('repo_files',
+ repo_name=repo.repo_name,
+ commit_id='tip', f_path='/'))
assert_response = response.assert_response()
assert_response.contains_one_link(
'subpaths-path @ 000000000000',
@@ -108,29 +168,29 @@ class TestFilesController:
backend.repo.landing_rev = "branch:%s" % new_branch
- # get response based on tip and not new revision
- response = self.app.get(url(
- controller='files', action='index',
- repo_name=backend.repo_name, revision='tip', f_path='/'),
- status=200)
+ # get response based on tip and not new commit
+ response = self.app.get(
+ route_path('repo_files',
+ repo_name=backend.repo_name,
+ commit_id='tip', f_path='/'))
- # make sure Files menu url is not tip but new revision
+ # make sure Files menu url is not tip but new commit
landing_rev = backend.repo.landing_rev[1]
- files_url = url('files_home', repo_name=backend.repo_name,
- revision=landing_rev)
+ files_url = route_path('repo_files:default_path',
+ repo_name=backend.repo_name,
+ commit_id=landing_rev)
assert landing_rev != 'tip'
- response.mustcontain('