views.py
467 lines
| 16.2 KiB
| text/x-python
|
PythonLexer
r5608 | # Copyright (C) 2012-2024 RhodeCode GmbH | |||
r411 | # | |||
# 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/ | ||||
r518 | import deform | |||
r731 | import logging | |||
import peppercorn | ||||
r411 | ||||
r2138 | from pyramid.httpexceptions import HTTPFound, HTTPForbidden, HTTPNotFound | |||
r411 | ||||
r3238 | from rhodecode.integrations import integration_type_registry | |||
r1990 | from rhodecode.apps._base import BaseAppView | |||
r3238 | from rhodecode.apps._base.navigation import navigation_list | |||
r1990 | from rhodecode.lib.auth import ( | |||
LoginRequired, CSRFRequired, HasPermissionAnyDecorator, | ||||
HasRepoPermissionAnyDecorator, HasRepoGroupPermissionAnyDecorator) | ||||
r731 | from rhodecode.lib.utils2 import safe_int | |||
r4091 | from rhodecode.lib.helpers import Page | |||
r2366 | from rhodecode.lib import helpers as h | |||
r667 | from rhodecode.model.db import Repository, RepoGroup, Session, Integration | |||
r411 | from rhodecode.model.scm import ScmModel | |||
from rhodecode.model.integration import IntegrationModel | ||||
r731 | from rhodecode.model.validation_schema.schemas.integration_schema import ( | |||
r793 | make_integration_schema, IntegrationScopeType) | |||
r411 | ||||
log = logging.getLogger(__name__) | ||||
r1990 | class IntegrationSettingsViewBase(BaseAppView): | |||
""" | ||||
Base Integration settings view used by both repo / global settings | ||||
""" | ||||
r411 | ||||
def __init__(self, context, request): | ||||
r5095 | super().__init__(context, request) | |||
r1990 | self._load_view_context() | |||
r411 | ||||
r1990 | def _load_view_context(self): | |||
r411 | """ | |||
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) | ||||
r1503 | 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'] | |||
r2138 | if integration_type not in integration_type_registry: | |||
raise HTTPNotFound() | ||||
r411 | self.IntegrationType = integration_type_registry[integration_type] | |||
r2138 | if self.IntegrationType.is_dummy: | |||
raise HTTPNotFound() | ||||
r411 | ||||
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 | ||||
r2351 | def _get_local_tmpl_context(self, include_app_defaults=True): | |||
r1990 | _ = self.request.translate | |||
r5095 | c = super()._get_local_tmpl_context( | |||
r1990 | include_app_defaults=include_app_defaults) | |||
r411 | c.active = 'integrations' | |||
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): | ||||
r1990 | _ = self.request.translate | |||
r731 | defaults = {} | |||
r411 | ||||
r518 | if self.integration: | |||
r731 | defaults['settings'] = self.integration.settings or {} | |||
defaults['options'] = { | ||||
'name': self.integration.name, | ||||
'enabled': self.integration.enabled, | ||||
r793 | 'scope': { | |||
'repo': self.integration.repo, | ||||
'repo_group': self.integration.repo_group, | ||||
'child_repos_only': self.integration.child_repos_only, | ||||
}, | ||||
r731 | } | |||
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), | ||||
} | ||||
r793 | defaults['options']['scope'] = { | |||
'repo': self.repo, | ||||
'repo_group': self.repo_group, | ||||
} | ||||
r731 | ||||
return defaults | ||||
r411 | ||||
r731 | def _delete_integration(self, integration): | |||
r1990 | _ = self.request.translate | |||
Session().delete(integration) | ||||
r731 | Session().commit() | |||
r2366 | h.flash( | |||
r731 | _('Integration {integration_name} deleted successfully.').format( | |||
r1990 | integration_name=integration.name), | |||
r2366 | category='success') | |||
r731 | ||||
if self.repo: | ||||
r1990 | redirect_to = self.request.route_path( | |||
r731 | 'repo_integrations_home', repo_name=self.repo.repo_name) | |||
elif self.repo_group: | ||||
r1990 | redirect_to = self.request.route_path( | |||
r731 | 'repo_group_integrations_home', | |||
repo_group_name=self.repo_group.group_name) | ||||
else: | ||||
r1990 | redirect_to = self.request.route_path('global_integrations_home') | |||
r731 | raise HTTPFound(redirect_to) | |||
r1990 | def _integration_list(self): | |||
""" List integrations """ | ||||
c = self.load_default_context() | ||||
if self.repo: | ||||
scope = self.repo | ||||
elif self.repo_group: | ||||
scope = self.repo_group | ||||
else: | ||||
scope = 'all' | ||||
integrations = [] | ||||
for IntType, 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((IntType, integration)) | ||||
sort_arg = self.request.GET.get('sort', 'name:asc') | ||||
r2366 | sort_dir = 'asc' | |||
r1990 | 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')) | ||||
r4091 | def url_generator(page_num): | |||
query_params = { | ||||
'page': page_num | ||||
} | ||||
return self.request.current_route_path(_query=query_params) | ||||
r1990 | page = safe_int(self.request.GET.get('page', 1), 1) | |||
r4091 | integrations = Page( | |||
integrations, page=page, items_per_page=10, url_maker=url_generator) | ||||
r1990 | ||||
c.rev_sort_dir = sort_dir != 'desc' and 'desc' or 'asc' | ||||
c.current_IntegrationType = self.IntegrationType | ||||
c.integrations_list = integrations | ||||
c.available_integrations = integration_type_registry | ||||
return self._get_template_context(c) | ||||
def _settings_get(self, defaults=None, form=None): | ||||
r731 | """ | |||
View that displays the integration settings as a form. | ||||
""" | ||||
r1990 | c = self.load_default_context() | |||
r731 | ||||
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 | ||||
r1990 | c.form = form | |||
c.current_IntegrationType = self.IntegrationType | ||||
c.integration = self.integration | ||||
r411 | ||||
r1990 | return self._get_template_context(c) | |||
r411 | ||||
r1990 | def _settings_post(self): | |||
r411 | """ | |||
r731 | View that validates and stores the integration settings. | |||
r411 | """ | |||
r1990 | _ = self.request.translate | |||
r5064 | controls = list(self.request.POST.items()) | |||
r731 | 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: | ||||
r793 | options['scope'] = IntegrationScopeType().serialize(None, { | |||
'repo': self.repo, | ||||
'repo_group': self.repo_group, | ||||
}) | ||||
r411 | ||||
try: | ||||
r731 | valid_data = form.validate_pstruct(pstruct) | |||
r518 | except deform.ValidationFailure as e: | |||
r2366 | h.flash( | |||
r518 | _('Errors exist when saving integration settings. ' | |||
r411 | 'Please check the form inputs.'), | |||
r2366 | category='error') | |||
r1990 | 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'], | ||||
r793 | repo=scope['repo'], | |||
repo_group=scope['repo_group'], | ||||
child_repos_only=scope['child_repos_only'], | ||||
) | ||||
r731 | self.integration.settings = valid_data['settings'] | |||
r448 | Session().commit() | |||
r411 | # Display success message and redirect. | |||
r2366 | h.flash( | |||
r411 | _('Integration {integration_name} updated successfully.').format( | |||
r424 | integration_name=self.IntegrationType.display_name), | |||
r2366 | category='success') | |||
r424 | ||||
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) | ||||
r793 | if self.integration.repo and not admin_view: | |||
r731 | redirect_to = self.request.route_path( | |||
'repo_integrations_edit', | ||||
r793 | repo_name=self.integration.repo.repo_name, | |||
r411 | integration=self.integration.integration_type, | |||
integration_id=self.integration.integration_id) | ||||
r793 | elif self.integration.repo_group and not admin_view: | |||
r731 | redirect_to = self.request.route_path( | |||
r667 | 'repo_group_integrations_edit', | |||
r793 | repo_group_name=self.integration.repo_group.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) | ||||
r1990 | def _new_integration(self): | |||
c = self.load_default_context() | ||||
c.available_integrations = integration_type_registry | ||||
return self._get_template_context(c) | ||||
r731 | ||||
r1990 | def load_default_context(self): | |||
raise NotImplementedError() | ||||
r411 | ||||
r1384 | ||||
r411 | class GlobalIntegrationsView(IntegrationSettingsViewBase): | |||
r1990 | def load_default_context(self): | |||
c = self._get_local_tmpl_context() | ||||
c.repo = self.repo | ||||
c.repo_group = self.repo_group | ||||
c.navlist = navigation_list(self.request) | ||||
r2351 | ||||
r1990 | return c | |||
@LoginRequired() | ||||
@HasPermissionAnyDecorator('hg.admin') | ||||
def integration_list(self): | ||||
return self._integration_list() | ||||
@LoginRequired() | ||||
@HasPermissionAnyDecorator('hg.admin') | ||||
def settings_get(self): | ||||
return self._settings_get() | ||||
@LoginRequired() | ||||
@HasPermissionAnyDecorator('hg.admin') | ||||
@CSRFRequired() | ||||
def settings_post(self): | ||||
return self._settings_post() | ||||
@LoginRequired() | ||||
@HasPermissionAnyDecorator('hg.admin') | ||||
def new_integration(self): | ||||
return self._new_integration() | ||||
r411 | ||||
class RepoIntegrationsView(IntegrationSettingsViewBase): | ||||
r1990 | def load_default_context(self): | |||
c = self._get_local_tmpl_context() | ||||
c.repo = self.repo | ||||
c.repo_group = self.repo_group | ||||
r2081 | self.db_repo = self.repo | |||
r1990 | c.rhodecode_db_repo = self.repo | |||
c.repo_name = self.db_repo.repo_name | ||||
c.repository_pull_requests = ScmModel().get_pull_requests(self.repo) | ||||
r3984 | c.repository_artifacts = ScmModel().get_artifacts(self.repo) | |||
r3739 | c.repository_is_user_following = ScmModel().is_following_repo( | |||
c.repo_name, self._rhodecode_user.user_id) | ||||
r3611 | c.has_origin_repo_read_perm = False | |||
if self.db_repo.fork: | ||||
c.has_origin_repo_read_perm = h.HasRepoPermissionAny( | ||||
'repository.write', 'repository.read', 'repository.admin')( | ||||
self.db_repo.fork.repo_name, 'summary fork link') | ||||
r1990 | return c | |||
@LoginRequired() | ||||
@HasRepoPermissionAnyDecorator('repository.admin') | ||||
def integration_list(self): | ||||
return self._integration_list() | ||||
@LoginRequired() | ||||
@HasRepoPermissionAnyDecorator('repository.admin') | ||||
def settings_get(self): | ||||
return self._settings_get() | ||||
@LoginRequired() | ||||
@HasRepoPermissionAnyDecorator('repository.admin') | ||||
@CSRFRequired() | ||||
def settings_post(self): | ||||
return self._settings_post() | ||||
@LoginRequired() | ||||
@HasRepoPermissionAnyDecorator('repository.admin') | ||||
def new_integration(self): | ||||
return self._new_integration() | ||||
r667 | ||||
r731 | ||||
r667 | class RepoGroupIntegrationsView(IntegrationSettingsViewBase): | |||
r1990 | def load_default_context(self): | |||
c = self._get_local_tmpl_context() | ||||
c.repo = self.repo | ||||
c.repo_group = self.repo_group | ||||
c.navlist = navigation_list(self.request) | ||||
r2351 | ||||
r1990 | return c | |||
@LoginRequired() | ||||
@HasRepoGroupPermissionAnyDecorator('group.admin') | ||||
def integration_list(self): | ||||
return self._integration_list() | ||||
r731 | ||||
r1990 | @LoginRequired() | |||
@HasRepoGroupPermissionAnyDecorator('group.admin') | ||||
def settings_get(self): | ||||
return self._settings_get() | ||||
@LoginRequired() | ||||
@HasRepoGroupPermissionAnyDecorator('group.admin') | ||||
@CSRFRequired() | ||||
def settings_post(self): | ||||
return self._settings_post() | ||||
@LoginRequired() | ||||
@HasRepoGroupPermissionAnyDecorator('group.admin') | ||||
def new_integration(self): | ||||
return self._new_integration() | ||||