From 784791cbe5dffd6ab82413fb2850162182ac7120 Mon Sep 17 00:00:00 2001 From: "Kacper Kowalik (Xarthisius)" Date: Tue, 27 Nov 2018 11:59:07 -0600 Subject: [PATCH 1/3] Add Dataverse External Tools integration --- plugin.cmake | 1 + plugin_tests/integration_test.py | 44 +++++++++++++++++++++++++++ server/__init__.py | 2 ++ server/rest/integration.py | 52 ++++++++++++++++++++++++++++++++ 4 files changed, 99 insertions(+) create mode 100644 plugin_tests/integration_test.py create mode 100644 server/rest/integration.py diff --git a/plugin.cmake b/plugin.cmake index d0bc4705..985fbbc7 100644 --- a/plugin.cmake +++ b/plugin.cmake @@ -27,5 +27,6 @@ add_python_test(dataverse plugins/wholetale/dataverse_lookup.txt plugins/wholetale/dataverse_listFiles.json ) +add_python_test(integration PLUGIN wholetale) add_python_style_test(python_static_analysis_wholetale "${PROJECT_SOURCE_DIR}/plugins/wholetale/server") diff --git a/plugin_tests/integration_test.py b/plugin_tests/integration_test.py new file mode 100644 index 00000000..ae02f5b9 --- /dev/null +++ b/plugin_tests/integration_test.py @@ -0,0 +1,44 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +from tests import base + + +def setUpModule(): + base.enabledPlugins.append('wholetale') + base.startServer() + + +def tearDownModule(): + base.stopServer() + + +class IntegrationTestCase(base.TestCase): + + def testDataverseIntegration(self): + resp = self.request( + '/integration/dataverse', method='GET', + params={'fileId': 'blah', 'siteUrl': 'https://dataverse.someplace'}) + self.assertStatus(resp, 400) + self.assertEqual(resp.json, { + 'message': 'Invalid fileId (should be integer)', + 'type': 'rest' + }) + + resp = self.request( + '/integration/dataverse', method='GET', + params={'fileId': '1234', 'siteUrl': 'definitely not a URL'}) + self.assertStatus(resp, 400) + self.assertEqual(resp.json, { + 'message': 'Not a valid URL: siteUrl', + 'type': 'rest' + }) + + resp = self.request( + '/integration/dataverse', method='GET', + params={'fileId': '1234', 'siteUrl': 'https://dataverse.someplace'}) + self.assertStatus(resp, 303) + self.assertEqual( + resp.headers['Location'], + 'https://dashboard.wholetale.org/compose?uri=' + 'https%3A%2F%2Fdataverse.someplace%2Fapi%2Faccess%2Fdatafile%2F1234' + ) diff --git a/server/__init__.py b/server/__init__.py index 53169bac..8b5adeae 100644 --- a/server/__init__.py +++ b/server/__init__.py @@ -25,6 +25,7 @@ from .rest.dataset import Dataset from .rest.recipe import Recipe from .rest.image import Image +from .rest.integration import Integration from .rest.repository import Repository from .rest.publish import Publish from .rest.harvester import listImportedData @@ -374,6 +375,7 @@ def load(info): events.bind('model.user.save.created', 'wholetale', addDefaultFolders) info['apiRoot'].repository = Repository() info['apiRoot'].publish = Publish() + info['apiRoot'].integration = Integration() info['apiRoot'].folder.route('GET', ('registered',), listImportedData) info['apiRoot'].folder.route('GET', (':id', 'listing'), listFolder) info['apiRoot'].folder.route('GET', (':id', 'dataset'), getDataSet) diff --git a/server/rest/integration.py b/server/rest/integration.py new file mode 100644 index 00000000..68cd8ce1 --- /dev/null +++ b/server/rest/integration.py @@ -0,0 +1,52 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import os +import cherrypy +import validators +from urllib.parse import urlparse, urlunparse, urlencode +from girder.api import access +from girder.api.describe import Description, autoDescribeRoute +from girder.api.rest import Resource, RestException, setResponseHeader + + +class Integration(Resource): + + def __init__(self): + super(Integration, self).__init__() + self.resourceName = 'integration' + + self.route('GET', ('dataverse',), self.dataverseExternalTools) + + @access.public + @autoDescribeRoute( + Description('Convert external tools request and bounce it to the dashboard.') + .param('fileId', 'The Dataverse database ID of a file the external tool has ' + 'been launched on.', required=True) + .param('siteUrl', 'The URL of the Dataverse installation that hosts the file ' + 'with the fileId above', required=True) + .param('apiToken', 'The Dataverse API token of the user launching the external' + ' tool, if available.', required=False) + .notes('apiToken is currently ignored.') + ) + def dataverseExternalTools(self, fileId, siteUrl, apiToken): + if not validators.url(siteUrl): + raise RestException('Not a valid URL: siteUrl') + try: + fileId = int(fileId) + except (TypeError, ValueError): + raise RestException('Invalid fileId (should be integer)') + + site = urlparse(siteUrl) + url = '{scheme}://{netloc}/api/access/datafile/{fileId}'.format( + scheme=site.scheme, netloc=site.netloc, fileId=fileId + ) + + # TODO: Make base url a plugin setting, defaulting to dashboard. + dashboard_url = os.environ.get('DASHBOARD_URL', 'https://dashboard.wholetale.org') + location = urlunparse( + urlparse(dashboard_url)._replace( + path='/compose', + query=urlencode({'uri': url})) + ) + setResponseHeader('Location', location) + cherrypy.response.status = 303 From b64eb5728968455ec966034d7f086d6d56baecf2 Mon Sep 17 00:00:00 2001 From: "Kacper Kowalik (Xarthisius)" Date: Wed, 28 Nov 2018 12:05:06 -0600 Subject: [PATCH 2/3] Fetch dataset title and pass it as query param --- server/rest/integration.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/server/rest/integration.py b/server/rest/integration.py index 68cd8ce1..250dc34c 100644 --- a/server/rest/integration.py +++ b/server/rest/integration.py @@ -4,10 +4,13 @@ import cherrypy import validators from urllib.parse import urlparse, urlunparse, urlencode +from urllib.error import HTTPError, URLError from girder.api import access from girder.api.describe import Description, autoDescribeRoute from girder.api.rest import Resource, RestException, setResponseHeader +from ..lib.dataverse.provider import DataverseImportProvider + class Integration(Resource): @@ -40,13 +43,20 @@ def dataverseExternalTools(self, fileId, siteUrl, apiToken): url = '{scheme}://{netloc}/api/access/datafile/{fileId}'.format( scheme=site.scheme, netloc=site.netloc, fileId=fileId ) + query = {'uri': url} + try: + title, _, _ = DataverseImportProvider._parse_access_url(urlparse(url)) + query['name'] = title + except (HTTPError, URLError): + # This doesn't bode well, but let's fail later when tale import happens + pass # TODO: Make base url a plugin setting, defaulting to dashboard. dashboard_url = os.environ.get('DASHBOARD_URL', 'https://dashboard.wholetale.org') location = urlunparse( urlparse(dashboard_url)._replace( path='/compose', - query=urlencode({'uri': url})) + query=urlencode(query)) ) setResponseHeader('Location', location) cherrypy.response.status = 303 From 036d5880880e99d300f3840374dce1ef041b4d8c Mon Sep 17 00:00:00 2001 From: "Kacper Kowalik (Xarthisius)" Date: Fri, 30 Nov 2018 14:06:00 -0600 Subject: [PATCH 3/3] Move dataverse integration code to lib.dataverse --- server/lib/dataverse/integration.py | 55 +++++++++++++++++++++++++++++ server/rest/integration.py | 55 ++--------------------------- 2 files changed, 58 insertions(+), 52 deletions(-) create mode 100644 server/lib/dataverse/integration.py diff --git a/server/lib/dataverse/integration.py b/server/lib/dataverse/integration.py new file mode 100644 index 00000000..c5be9f2d --- /dev/null +++ b/server/lib/dataverse/integration.py @@ -0,0 +1,55 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- +import os +import cherrypy +import validators +from urllib.parse import urlparse, urlunparse, urlencode +from urllib.error import HTTPError, URLError +from girder.api import access +from girder.api.describe import Description, autoDescribeRoute +from girder.api.rest import RestException, setResponseHeader, boundHandler + +from .provider import DataverseImportProvider + + +@access.public +@autoDescribeRoute( + Description('Convert external tools request and bounce it to the dashboard.') + .param('fileId', 'The Dataverse database ID of a file the external tool has ' + 'been launched on.', required=True) + .param('siteUrl', 'The URL of the Dataverse installation that hosts the file ' + 'with the fileId above', required=True) + .param('apiToken', 'The Dataverse API token of the user launching the external' + ' tool, if available.', required=False) + .notes('apiToken is currently ignored.') +) +@boundHandler() +def dataverseExternalTools(self, fileId, siteUrl, apiToken): + if not validators.url(siteUrl): + raise RestException('Not a valid URL: siteUrl') + try: + fileId = int(fileId) + except (TypeError, ValueError): + raise RestException('Invalid fileId (should be integer)') + + site = urlparse(siteUrl) + url = '{scheme}://{netloc}/api/access/datafile/{fileId}'.format( + scheme=site.scheme, netloc=site.netloc, fileId=fileId + ) + query = {'uri': url} + try: + title, _, _ = DataverseImportProvider._parse_access_url(urlparse(url)) + query['name'] = title + except (HTTPError, URLError): + # This doesn't bode well, but let's fail later when tale import happens + pass + + # TODO: Make base url a plugin setting, defaulting to dashboard. + dashboard_url = os.environ.get('DASHBOARD_URL', 'https://dashboard.wholetale.org') + location = urlunparse( + urlparse(dashboard_url)._replace( + path='/compose', + query=urlencode(query)) + ) + setResponseHeader('Location', location) + cherrypy.response.status = 303 diff --git a/server/rest/integration.py b/server/rest/integration.py index 250dc34c..a78593c4 100644 --- a/server/rest/integration.py +++ b/server/rest/integration.py @@ -1,15 +1,7 @@ #!/usr/bin/env python # -*- coding: utf-8 -*- -import os -import cherrypy -import validators -from urllib.parse import urlparse, urlunparse, urlencode -from urllib.error import HTTPError, URLError -from girder.api import access -from girder.api.describe import Description, autoDescribeRoute -from girder.api.rest import Resource, RestException, setResponseHeader - -from ..lib.dataverse.provider import DataverseImportProvider +from girder.api.rest import Resource +from ..lib.dataverse.integration import dataverseExternalTools class Integration(Resource): @@ -18,45 +10,4 @@ def __init__(self): super(Integration, self).__init__() self.resourceName = 'integration' - self.route('GET', ('dataverse',), self.dataverseExternalTools) - - @access.public - @autoDescribeRoute( - Description('Convert external tools request and bounce it to the dashboard.') - .param('fileId', 'The Dataverse database ID of a file the external tool has ' - 'been launched on.', required=True) - .param('siteUrl', 'The URL of the Dataverse installation that hosts the file ' - 'with the fileId above', required=True) - .param('apiToken', 'The Dataverse API token of the user launching the external' - ' tool, if available.', required=False) - .notes('apiToken is currently ignored.') - ) - def dataverseExternalTools(self, fileId, siteUrl, apiToken): - if not validators.url(siteUrl): - raise RestException('Not a valid URL: siteUrl') - try: - fileId = int(fileId) - except (TypeError, ValueError): - raise RestException('Invalid fileId (should be integer)') - - site = urlparse(siteUrl) - url = '{scheme}://{netloc}/api/access/datafile/{fileId}'.format( - scheme=site.scheme, netloc=site.netloc, fileId=fileId - ) - query = {'uri': url} - try: - title, _, _ = DataverseImportProvider._parse_access_url(urlparse(url)) - query['name'] = title - except (HTTPError, URLError): - # This doesn't bode well, but let's fail later when tale import happens - pass - - # TODO: Make base url a plugin setting, defaulting to dashboard. - dashboard_url = os.environ.get('DASHBOARD_URL', 'https://dashboard.wholetale.org') - location = urlunparse( - urlparse(dashboard_url)._replace( - path='/compose', - query=urlencode(query)) - ) - setResponseHeader('Location', location) - cherrypy.response.status = 303 + self.route('GET', ('dataverse',), dataverseExternalTools)