views.py
385 lines
| 13.9 KiB
| text/x-python
|
PythonLexer
r411 | # -*- coding: utf-8 -*- | |||
# Copyright (C) 2012-2016 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 <http://www.gnu.org/licenses/>. | ||||
# | ||||
# 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 pylons | ||||
r518 | import deform | |||
r731 | import logging | |||
import colander | ||||
import peppercorn | ||||
import webhelpers.paginate | ||||
r411 | ||||
r731 | from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPBadRequest | |||
r411 | from pyramid.renderers import render | |||
from pyramid.response import Response | ||||
from rhodecode.lib import auth | ||||
from rhodecode.lib.auth import LoginRequired, HasPermissionAllDecorator | ||||
r731 | from rhodecode.lib.utils2 import safe_int | |||
from rhodecode.lib.helpers import Page | ||||
r667 | from rhodecode.model.db import Repository, RepoGroup, Session, Integration | |||
r411 | from rhodecode.model.scm import ScmModel | |||
from rhodecode.model.integration import IntegrationModel | ||||
from rhodecode.admin.navigation import navigation_list | ||||
from rhodecode.translation import _ | ||||
from rhodecode.integrations import integration_type_registry | ||||
r731 | from rhodecode.model.validation_schema.schemas.integration_schema import ( | |||
make_integration_schema) | ||||
r411 | ||||
log = logging.getLogger(__name__) | ||||
class IntegrationSettingsViewBase(object): | ||||
""" Base Integration settings view used by both repo / global settings """ | ||||
def __init__(self, context, request): | ||||
self.context = context | ||||
self.request = request | ||||
self._load_general_context() | ||||
if not self.perm_check(request.user): | ||||
raise HTTPForbidden() | ||||
def _load_general_context(self): | ||||
""" | ||||
This avoids boilerplate for repo/global+list/edit+views/templates | ||||
by doing all possible contexts at the same time however it should | ||||
be split up into separate functions once more "contexts" exist | ||||
""" | ||||
self.IntegrationType = None | ||||
self.repo = None | ||||
r667 | self.repo_group = None | |||
r411 | self.integration = None | |||
self.integrations = {} | ||||
request = self.request | ||||
r731 | if 'repo_name' in request.matchdict: # in repo settings context | |||
r411 | repo_name = request.matchdict['repo_name'] | |||
self.repo = Repository.get_by_repo_name(repo_name) | ||||
r731 | if 'repo_group_name' in request.matchdict: # in group settings context | |||
r667 | repo_group_name = request.matchdict['repo_group_name'] | |||
self.repo_group = RepoGroup.get_by_group_name(repo_group_name) | ||||
r731 | ||||
if 'integration' in request.matchdict: # integration type context | ||||
r411 | integration_type = request.matchdict['integration'] | |||
self.IntegrationType = integration_type_registry[integration_type] | ||||
if 'integration_id' in request.matchdict: # single integration context | ||||
integration_id = request.matchdict['integration_id'] | ||||
self.integration = Integration.get(integration_id) | ||||
r667 | ||||
r731 | # extra perms check just in case | |||
if not self._has_perms_for_integration(self.integration): | ||||
raise HTTPForbidden() | ||||
r411 | ||||
self.settings = self.integration and self.integration.settings or {} | ||||
r731 | self.admin_view = not (self.repo or self.repo_group) | |||
def _has_perms_for_integration(self, integration): | ||||
perms = self.request.user.permissions | ||||
if 'hg.admin' in perms['global']: | ||||
return True | ||||
if integration.repo: | ||||
return perms['repositories'].get( | ||||
integration.repo.repo_name) == 'repository.admin' | ||||
if integration.repo_group: | ||||
return perms['repositories_groups'].get( | ||||
integration.repo_group.group_name) == 'group.admin' | ||||
return False | ||||
r411 | ||||
def _template_c_context(self): | ||||
# TODO: dan: this is a stopgap in order to inherit from current pylons | ||||
# based admin/repo settings templates - this should be removed entirely | ||||
# after port to pyramid | ||||
c = pylons.tmpl_context | ||||
c.active = 'integrations' | ||||
c.rhodecode_user = self.request.user | ||||
c.repo = self.repo | ||||
r667 | c.repo_group = self.repo_group | |||
r411 | c.repo_name = self.repo and self.repo.repo_name or None | |||
r667 | c.repo_group_name = self.repo_group and self.repo_group.group_name or None | |||
r731 | ||||
r411 | if self.repo: | |||
c.repo_info = self.repo | ||||
c.rhodecode_db_repo = self.repo | ||||
c.repository_pull_requests = ScmModel().get_pull_requests(self.repo) | ||||
else: | ||||
c.navlist = navigation_list(self.request) | ||||
return c | ||||
def _form_schema(self): | ||||
r731 | schema = make_integration_schema(IntegrationType=self.IntegrationType, | |||
settings=self.settings) | ||||
r411 | ||||
r731 | # returns a clone, important if mutating the schema later | |||
return schema.bind( | ||||
permissions=self.request.user.permissions, | ||||
no_scope=not self.admin_view) | ||||
def _form_defaults(self): | ||||
defaults = {} | ||||
r411 | ||||
r518 | if self.integration: | |||
r731 | defaults['settings'] = self.integration.settings or {} | |||
defaults['options'] = { | ||||
'name': self.integration.name, | ||||
'enabled': self.integration.enabled, | ||||
'scope': self.integration.scope, | ||||
} | ||||
r518 | else: | |||
if self.repo: | ||||
r667 | scope = _('{repo_name} repository').format( | |||
repo_name=self.repo.repo_name) | ||||
elif self.repo_group: | ||||
scope = _('{repo_group_name} repo group').format( | ||||
repo_group_name=self.repo_group.group_name) | ||||
r411 | else: | |||
r518 | scope = _('Global') | |||
r731 | defaults['options'] = { | |||
'enabled': True, | ||||
'name': _('{name} integration').format( | ||||
name=self.IntegrationType.display_name), | ||||
} | ||||
if self.repo: | ||||
defaults['options']['scope'] = self.repo | ||||
elif self.repo_group: | ||||
defaults['options']['scope'] = self.repo_group | ||||
return defaults | ||||
r411 | ||||
r731 | def _delete_integration(self, integration): | |||
Session().delete(self.integration) | ||||
Session().commit() | ||||
self.request.session.flash( | ||||
_('Integration {integration_name} deleted successfully.').format( | ||||
integration_name=self.integration.name), | ||||
queue='success') | ||||
if self.repo: | ||||
redirect_to = self.request.route_url( | ||||
'repo_integrations_home', repo_name=self.repo.repo_name) | ||||
elif self.repo_group: | ||||
redirect_to = self.request.route_url( | ||||
'repo_group_integrations_home', | ||||
repo_group_name=self.repo_group.group_name) | ||||
else: | ||||
redirect_to = self.request.route_url('global_integrations_home') | ||||
raise HTTPFound(redirect_to) | ||||
def settings_get(self, defaults=None, form=None): | ||||
""" | ||||
View that displays the integration settings as a form. | ||||
""" | ||||
defaults = defaults or self._form_defaults() | ||||
schema = self._form_schema() | ||||
r518 | ||||
if self.integration: | ||||
buttons = ('submit', 'delete') | ||||
else: | ||||
buttons = ('submit',) | ||||
form = form or deform.Form(schema, appstruct=defaults, buttons=buttons) | ||||
r411 | ||||
template_context = { | ||||
r518 | 'form': form, | |||
r411 | 'current_IntegrationType': self.IntegrationType, | |||
'integration': self.integration, | ||||
'c': self._template_c_context(), | ||||
} | ||||
return template_context | ||||
@auth.CSRFRequired() | ||||
def settings_post(self): | ||||
""" | ||||
r731 | View that validates and stores the integration settings. | |||
r411 | """ | |||
r731 | controls = self.request.POST.items() | |||
pstruct = peppercorn.parse(controls) | ||||
if self.integration and pstruct.get('delete'): | ||||
return self._delete_integration(self.integration) | ||||
schema = self._form_schema() | ||||
skip_settings_validation = False | ||||
if self.integration and 'enabled' not in pstruct.get('options', {}): | ||||
skip_settings_validation = True | ||||
schema['settings'].validator = None | ||||
for field in schema['settings'].children: | ||||
field.validator = None | ||||
field.missing = '' | ||||
r411 | ||||
r731 | if self.integration: | |||
buttons = ('submit', 'delete') | ||||
else: | ||||
buttons = ('submit',) | ||||
r518 | ||||
r731 | form = deform.Form(schema, buttons=buttons) | |||
r411 | ||||
r731 | if not self.admin_view: | |||
# scope is read only field in these cases, and has to be added | ||||
options = pstruct.setdefault('options', {}) | ||||
if 'scope' not in options: | ||||
if self.repo: | ||||
options['scope'] = 'repo:{}'.format(self.repo.repo_name) | ||||
elif self.repo_group: | ||||
options['scope'] = 'repogroup:{}'.format( | ||||
self.repo_group.group_name) | ||||
r411 | ||||
try: | ||||
r731 | valid_data = form.validate_pstruct(pstruct) | |||
r518 | except deform.ValidationFailure as e: | |||
r411 | self.request.session.flash( | |||
r518 | _('Errors exist when saving integration settings. ' | |||
r411 | 'Please check the form inputs.'), | |||
queue='error') | ||||
r731 | return self.settings_get(form=e) | |||
r411 | ||||
if not self.integration: | ||||
r448 | self.integration = Integration() | |||
self.integration.integration_type = self.IntegrationType.key | ||||
Session().add(self.integration) | ||||
r411 | ||||
r731 | scope = valid_data['options']['scope'] | |||
r411 | ||||
r731 | IntegrationModel().update_integration(self.integration, | |||
name=valid_data['options']['name'], | ||||
enabled=valid_data['options']['enabled'], | ||||
settings=valid_data['settings'], | ||||
scope=scope) | ||||
self.integration.settings = valid_data['settings'] | ||||
r448 | Session().commit() | |||
r411 | # Display success message and redirect. | |||
self.request.session.flash( | ||||
_('Integration {integration_name} updated successfully.').format( | ||||
r424 | integration_name=self.IntegrationType.display_name), | |||
queue='success') | ||||
r731 | ||||
# if integration scope changes, we must redirect to the right place | ||||
# keeping in mind if the original view was for /repo/ or /_admin/ | ||||
admin_view = not (self.repo or self.repo_group) | ||||
if isinstance(self.integration.scope, Repository) and not admin_view: | ||||
redirect_to = self.request.route_path( | ||||
'repo_integrations_edit', | ||||
repo_name=self.integration.scope.repo_name, | ||||
r411 | integration=self.integration.integration_type, | |||
integration_id=self.integration.integration_id) | ||||
r731 | elif isinstance(self.integration.scope, RepoGroup) and not admin_view: | |||
redirect_to = self.request.route_path( | ||||
r667 | 'repo_group_integrations_edit', | |||
r731 | repo_group_name=self.integration.scope.group_name, | |||
r667 | integration=self.integration.integration_type, | |||
integration_id=self.integration.integration_id) | ||||
r411 | else: | |||
r731 | redirect_to = self.request.route_path( | |||
r411 | 'global_integrations_edit', | |||
integration=self.integration.integration_type, | ||||
integration_id=self.integration.integration_id) | ||||
return HTTPFound(redirect_to) | ||||
def index(self): | ||||
r731 | """ List integrations """ | |||
if self.repo: | ||||
scope = self.repo | ||||
elif self.repo_group: | ||||
scope = self.repo_group | ||||
else: | ||||
scope = 'all' | ||||
integrations = [] | ||||
for integration in IntegrationModel().get_integrations( | ||||
scope=scope, IntegrationType=self.IntegrationType): | ||||
# extra permissions check *just in case* | ||||
if not self._has_perms_for_integration(integration): | ||||
continue | ||||
integrations.append(integration) | ||||
sort_arg = self.request.GET.get('sort', 'name:asc') | ||||
if ':' in sort_arg: | ||||
sort_field, sort_dir = sort_arg.split(':') | ||||
else: | ||||
sort_field = sort_arg, 'asc' | ||||
assert sort_field in ('name', 'integration_type', 'enabled', 'scope') | ||||
integrations.sort( | ||||
key=lambda x: getattr(x[1], sort_field), reverse=(sort_dir=='desc')) | ||||
page_url = webhelpers.paginate.PageURL( | ||||
self.request.path, self.request.GET) | ||||
page = safe_int(self.request.GET.get('page', 1), 1) | ||||
integrations = Page(integrations, page=page, items_per_page=10, | ||||
url=page_url) | ||||
r411 | ||||
template_context = { | ||||
r731 | 'sort_field': sort_field, | |||
'rev_sort_dir': sort_dir != 'desc' and 'desc' or 'asc', | ||||
r411 | 'current_IntegrationType': self.IntegrationType, | |||
r731 | 'integrations_list': integrations, | |||
r411 | 'available_integrations': integration_type_registry, | |||
r731 | 'c': self._template_c_context(), | |||
'request': self.request, | ||||
r411 | } | |||
r731 | return template_context | |||
r411 | ||||
r731 | def new_integration(self): | |||
template_context = { | ||||
'available_integrations': integration_type_registry, | ||||
'c': self._template_c_context(), | ||||
} | ||||
return template_context | ||||
r411 | ||||
class GlobalIntegrationsView(IntegrationSettingsViewBase): | ||||
def perm_check(self, user): | ||||
return auth.HasPermissionAll('hg.admin').check_permissions(user=user) | ||||
class RepoIntegrationsView(IntegrationSettingsViewBase): | ||||
def perm_check(self, user): | ||||
return auth.HasRepoPermissionAll('repository.admin' | ||||
)(repo_name=self.repo.repo_name, user=user) | ||||
r667 | ||||
r731 | ||||
r667 | class RepoGroupIntegrationsView(IntegrationSettingsViewBase): | |||
def perm_check(self, user): | ||||
return auth.HasRepoGroupPermissionAll('group.admin' | ||||
)(group_name=self.repo_group.group_name, user=user) | ||||
r731 | ||||