diff --git a/pyproject.toml b/pyproject.toml index d1d9384f89..9665e8e7c5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -59,7 +59,6 @@ dependencies = [ "iso8601~=2.0", "markdown~=3.4", "pypandoc~=1.11", - "requests-toolbelt~=1.0", "rules~=3.3", ] diff --git a/rdmo/projects/forms.py b/rdmo/projects/forms.py index 4ac7d42ab2..c1ff1145a1 100644 --- a/rdmo/projects/forms.py +++ b/rdmo/projects/forms.py @@ -278,8 +278,10 @@ def __init__(self, *args, **kwargs): if field.get('placeholder'): attrs = {'placeholder': field.get('placeholder')} + self.fields[field.get('key')] = forms.CharField(widget=forms.TextInput(attrs=attrs), - initial=initial, required=field.get('required', True)) + initial=initial, required=field.get('required', True), + help_text=field.get('help')) def save(self): # the the project and the provider_key diff --git a/rdmo/projects/providers.py b/rdmo/projects/providers.py index 9efa2933c8..1c11db028a 100644 --- a/rdmo/projects/providers.py +++ b/rdmo/projects/providers.py @@ -1,20 +1,10 @@ -import hmac -import json -from urllib.parse import quote - -from django.conf import settings from django.core.exceptions import ObjectDoesNotExist -from django.http import Http404, HttpResponse, HttpResponseRedirect +from django.http import HttpResponseRedirect from django.shortcuts import render from django.utils.translation import gettext_lazy as _ from rdmo.core.plugins import Plugin -from rdmo.services.providers import ( - GitHubProviderMixin, - GitLabProviderMixin, - OauthProviderMixin, - OpenProjectProviderMixin, -) +from rdmo.services.providers import OauthProviderMixin class IssueProvider(Plugin): @@ -38,7 +28,7 @@ def send_issue(self, request, issue, integration, subject, message, attachments) if url is None or data is None: return render(request, 'core/error.html', { 'title': _('Integration error'), - 'errors': [_('The Integration is not configured correctly.') % message] + 'errors': [_('The Integration is not configured correctly.')] }, status=200) return self.post(request, url, data) @@ -79,343 +69,3 @@ def get_post_data(self, request, issue, integration, subject, message, attachmen def get_issue_url(self, response): raise NotImplementedError - - -class GitHubIssueProvider(GitHubProviderMixin, OauthIssueProvider): - add_label = _('Add GitHub integration') - send_label = _('Send to GitHub') - description = _('This integration allow the creation of issues in arbitrary GitHub repositories. ' - 'The upload of attachments is not supported by GitHub.') - - def get_post_url(self, request, issue, integration, subject, message, attachments): - repo = integration.get_option_value('repo') - if repo: - return f'https://api.github.com/repos/{repo}/issues' - - def get_post_data(self, request, issue, integration, subject, message, attachments): - return { - 'title': subject, - 'body': message - } - - def get_issue_url(self, response): - return response.json().get('html_url') - - def webhook(self, request, integration): - secret = integration.get_option_value('secret') - header_signature = request.headers.get('X-Hub-Signature') - - if (secret is not None) and (header_signature is not None): - body_signature = 'sha1=' + hmac.new(secret.encode(), request.body, 'sha1').hexdigest() - - if hmac.compare_digest(header_signature, body_signature): - try: - payload = json.loads(request.body.decode()) - action = payload.get('action') - issue_url = payload.get('issue', {}).get('html_url') - - if action and issue_url: - try: - issue_resource = integration.resources.get(url=issue_url) - if action == 'closed': - issue_resource.issue.status = issue_resource.issue.ISSUE_STATUS_CLOSED - else: - issue_resource.issue.status = issue_resource.issue.ISSUE_STATUS_IN_PROGRESS - - issue_resource.issue.save() - except ObjectDoesNotExist: - pass - - return HttpResponse(status=200) - - except json.decoder.JSONDecodeError as e: - return HttpResponse(e, status=400) - - raise Http404 - - @property - def fields(self): - return [ - { - 'key': 'repo', - 'placeholder': 'user_name/repo_name', - 'help': _('The GitHub repository to send issues to.') - }, - { - 'key': 'secret', - 'placeholder': 'Secret (random) string', - 'help': _('The secret for a GitHub webhook to close a task (optional).'), - 'required': False, - 'secret': True - } - ] - - -class GitLabIssueProvider(GitLabProviderMixin, OauthIssueProvider): - add_label = _('Add GitLab integration') - send_label = _('Send to GitLab') - - @property - def description(self): - return _(f'This integration allow the creation of issues in arbitrary repositories on {self.gitlab_url}. ' - 'The upload of attachments is not supported by GitLab.') - - def get_post_url(self, request, issue, integration, subject, message, attachments): - repo = integration.get_option_value('repo') - if repo: - return '{}/api/v4/projects/{}/issues'.format(self.gitlab_url, quote(repo, safe='')) - - def get_post_data(self, request, issue, integration, subject, message, attachments): - return { - 'title': subject, - 'description': message - } - - def get_issue_url(self, response): - return response.json().get('web_url') - - def webhook(self, request, integration): - secret = integration.get_option_value('secret') - header_token = request.headers.get('X-Gitlab-Token') - - if (secret is not None) and (header_token is not None) and (header_token == secret): - try: - payload = json.loads(request.body.decode()) - state = payload.get('object_attributes', {}).get('state') - issue_url = payload.get('object_attributes', {}).get('url') - - if state and issue_url: - try: - issue_resource = integration.resources.get(url=issue_url) - if state == 'closed': - issue_resource.issue.status = issue_resource.issue.ISSUE_STATUS_CLOSED - else: - issue_resource.issue.status = issue_resource.issue.ISSUE_STATUS_IN_PROGRESS - - issue_resource.issue.save() - except ObjectDoesNotExist: - pass - - return HttpResponse(status=200) - - except json.decoder.JSONDecodeError as e: - return HttpResponse(e, status=400) - - raise Http404 - - @property - def fields(self): - return [ - { - 'key': 'repo', - 'placeholder': 'user_name/repo_name', - 'help': _('The GitLab repository to send issues to.') - }, - { - 'key': 'secret', - 'placeholder': 'Secret (random) string', - 'help': _('The secret for a GitLab webhook to close a task (optional).'), - 'required': False, - 'secret': True - } - ] - - -class OpenProjectIssueProvider(OpenProjectProviderMixin, OauthIssueProvider): - add_label = _('Add OpenProject integration') - send_label = _('Send to OpenProject') - - status_map = { - 'New': 'open', - 'To be scheduled': 'in_progress', - 'Scheduled': 'in_progress', - 'In progress': 'in_progress', - 'Closed': 'closed', - 'On hold': 'in_progress', - 'Rejected': 'closed' - } - - @property - def description(self): - return _(f'This integration allow the creation of issues on {self.openproject_url}.') - - def send_issue(self, request, issue, integration, subject, message, attachments): - self.store_in_session(request, 'issue_id', issue.id) - self.store_in_session(request, 'integration_id', integration.id) - self.store_in_session(request, 'project_name', integration.get_option_value('project_name')) - self.store_in_session(request, 'work_package_type', integration.get_option_value('work_package_type')) - self.store_in_session(request, 'subject', subject) - self.store_in_session(request, 'message', message) - self.store_in_session(request, 'attachments', attachments) - - return self.get_project_id(request) - - def get_project_id(self, request): - project_name = self.pop_from_session(request, 'project_name') - query = quote(json.dumps([{ - 'name_and_identifier': { - 'operator': '=', - 'values': [project_name] - } - }])) - url = f'{self.api_url}/projects?filters={query}' - - return self.get(request, url) - - def get_type_id(self, request): - url = f'{self.api_url}/types' - return self.get(request, url) - - def post_issue(self, request): - project_id = self.get_from_session(request, 'project_id') - type_id = self.get_from_session(request, 'type_id') - url = f'{self.api_url}/projects/{project_id}/work_packages' - data = { - '_links': { - 'type': { - 'href': f'/api/v3/types/{type_id}' - } - }, - 'subject': self.pop_from_session(request, 'subject'), - 'description': { - 'format': 'plain', - 'raw': self.pop_from_session(request, 'message'), - } - } - - return self.post(request, url, data) - - def post_attachment(self, request): - work_package_id = self.get_from_session(request, 'work_package_id') - attachments = self.pop_from_session(request, 'attachments') - - if attachments: - file_name, file_content, file_type = attachments[0] - url = f'{self.api_url}/work_packages/{work_package_id}/attachments' - multipart = { - 'metadata': json.dumps({'fileName': file_name }), - 'file': (file_name, file_content, file_type) - } - - self.store_in_session(request, 'attachments', attachments[1:]) - return self.post(request, url, multipart=multipart) - - else: - # there are no attachments left, get the url of the work_package - remote_url = self.get_work_package_url(work_package_id) - - # update the issue in rdmo - self.update_issue(request, remote_url) - - # redirect to the work package in open project - return HttpResponseRedirect(remote_url) - - def get_success(self, request, response): - if '/projects' in response.url: - try: - project_id = response.json()['_embedded']['elements'][0]['id'] - self.store_in_session(request, 'project_id', project_id) - return self.get_type_id(request) - - except (KeyError, IndexError): - return render(request, 'core/error.html', { - 'title': _('Integration error'), - 'errors': [_('OpenProject project could not be found.')] - }, status=200) - - elif '/types' in response.url: - try: - work_package_type = self.pop_from_session(request, 'work_package_type') - for element in response.json()['_embedded']['elements']: - if element['name'] == work_package_type: - self.store_in_session(request, 'type_id', element['id']) - return self.post_issue(request) - - except KeyError: - pass - - return render(request, 'core/error.html', { - 'title': _('Integration error'), - 'errors': [_('OpenProject work package type could not be found.')] - }, status=200) - - elif response.request.method == 'POST': - pass - - # return an error if everything failed - return render(request, 'core/error.html', { - 'title': _('Integration error'), - 'errors': [_('The Integration is not configured correctly.')] - }, status=200) - - def post_success(self, request, response): - if '/projects/' in response.url: - # get the upstream url of the issue - work_package_id = response.json()['id'] - self.store_in_session(request, 'work_package_id', work_package_id) - - # post the next attachment - return self.post_attachment(request) - - def get_work_package_url(self, work_package_id): - return f'{self.openproject_url}/work_packages/{work_package_id}' - - def webhook(self, request, integration): - secret = integration.get_option_value('secret') - header_signature = request.headers.get('X-Op-Signature') - - if (secret is not None) and (header_signature is not None): - body_signature = 'sha1=' + hmac.new(secret.encode(), request.body, 'sha1').hexdigest() - - if hmac.compare_digest(header_signature, body_signature): - try: - payload = json.loads(request.body.decode()) - action = payload.get('action') - work_package = payload.get('work_package') - - if action and work_package: - work_package_id = work_package.get('id') - work_package_url = self.get_work_package_url(work_package_id) - work_package_status = work_package.get('_links', {}).get('status', {}).get('title') - - try: - issue_resource = integration.resources.get(url=work_package_url) - status_map = self.status_map - status_map.update(settings.OPENPROJECT_PROVIDER.get('status_map', {})) - - if work_package_status in status_map: - print('-->' , status_map[work_package_status]) - issue_resource.issue.status = status_map[work_package_status] - issue_resource.issue.save() - - except ObjectDoesNotExist: - pass - - return HttpResponse(status=200) - - except json.decoder.JSONDecodeError as e: - return HttpResponse(e, status=400) - - raise Http404 - - @property - def fields(self): - return [ - { - 'key': 'project_name', - 'placeholder': 'Project', - 'help': _('The name of the OpenProject url') - }, - { - 'key': 'work_package_type', - 'placeholder': 'Work Package Type', - 'help': _('The type of workpackage to create, e.g. "Task"') - }, - { - 'key': 'secret', - 'placeholder': 'Secret (random) string', - 'help': _('The secret for a OpenProject webhook to close a task (optional).'), - 'required': False, - 'secret': True - } - ] diff --git a/rdmo/services/providers.py b/rdmo/services/providers.py index 8819f60860..d8262161dd 100644 --- a/rdmo/services/providers.py +++ b/rdmo/services/providers.py @@ -1,10 +1,8 @@ import logging from urllib.parse import urlencode -from django.conf import settings from django.http import HttpResponseRedirect from django.shortcuts import render -from django.urls import reverse from django.utils.crypto import get_random_string from django.utils.translation import gettext_lazy as _ @@ -168,152 +166,3 @@ def get_callback_data(self, request): def get_error_message(self, response): return response.json().get('error') - - -class GitHubProviderMixin(OauthProviderMixin): - authorize_url = 'https://github.com/login/oauth/authorize' - token_url = 'https://github.com/login/oauth/access_token' - api_url = 'https://api.github.com' - - @property - def client_id(self): - return settings.GITHUB_PROVIDER['client_id'] - - @property - def client_secret(self): - return settings.GITHUB_PROVIDER['client_secret'] - - @property - def redirect_path(self): - return reverse('oauth_callback', args=['github']) - - def get_authorization_headers(self, access_token): - return { - 'Authorization': f'token {access_token}', - 'Accept': 'application/vnd.github.v3+json' - } - - def get_authorize_params(self, request, state): - return { - 'authorize_url': self.authorize_url, - 'client_id': self.client_id, - 'redirect_uri': request.build_absolute_uri(self.redirect_path), - 'scope': 'repo', - 'state': state, - } - - def get_callback_params(self, request): - return { - 'token_url': self.token_url, - 'client_id': self.client_id, - 'client_secret': self.client_secret, - 'code': request.GET.get('code') - } - - def get_error_message(self, response): - return response.json().get('message') - - -class GitLabProviderMixin(OauthProviderMixin): - - @property - def gitlab_url(self): - return settings.GITLAB_PROVIDER['gitlab_url'].strip('/') - - @property - def authorize_url(self): - return f'{self.gitlab_url}/oauth/authorize' - - @property - def token_url(self): - return f'{self.gitlab_url}/oauth/token' - - @property - def api_url(self): - return f'{self.gitlab_url}/api/v4' - - @property - def client_id(self): - return settings.GITLAB_PROVIDER['client_id'] - - @property - def client_secret(self): - return settings.GITLAB_PROVIDER['client_secret'] - - @property - def redirect_path(self): - return reverse('oauth_callback', args=['gitlab']) - - def get_authorize_params(self, request, state): - return { - 'authorize_url': self.authorize_url, - 'client_id': self.client_id, - 'redirect_uri': request.build_absolute_uri(self.redirect_path), - 'response_type': 'code', - 'scope': 'api', - 'state': state, - } - - def get_callback_params(self, request): - return { - 'token_url': self.token_url, - 'client_id': self.client_id, - 'client_secret': self.client_secret, - 'code': request.GET.get('code'), - 'grant_type': 'authorization_code', - 'redirect_uri': request.build_absolute_uri(self.redirect_path) - } - - -class OpenProjectProviderMixin(OauthProviderMixin): - - @property - def openproject_url(self): - return settings.OPENPROJECT_PROVIDER['openproject_url'].strip('/') - - @property - def authorize_url(self): - return f'{self.openproject_url}/oauth/authorize' - - @property - def token_url(self): - return f'{self.openproject_url}/oauth/token' - - @property - def api_url(self): - return f'{self.openproject_url}/api/v3' - - @property - def client_id(self): - return settings.OPENPROJECT_PROVIDER['client_id'] - - @property - def client_secret(self): - return settings.OPENPROJECT_PROVIDER['client_secret'] - - @property - def redirect_path(self): - return reverse('oauth_callback', args=['openproject']) - - def get_authorize_params(self, request, state): - return { - 'authorize_url': self.authorize_url, - 'client_id': self.client_id, - 'redirect_uri': request.build_absolute_uri(self.redirect_path), - 'response_type': 'code', - 'scope': 'api_v3', - 'state': state, - } - - def get_callback_data(self, request): - return { - 'token_url': self.token_url, - 'client_id': self.client_id, - 'client_secret': self.client_secret, - 'code': request.GET.get('code'), - 'grant_type': 'authorization_code', - 'redirect_uri': request.build_absolute_uri(self.redirect_path) - } - - def get_error_message(self, response): - return response.json().get('message')