diff --git a/mfr/extensions/jamovi/README.md b/mfr/extensions/jamovi/README.md new file mode 100644 index 000000000..fb6e52c8c --- /dev/null +++ b/mfr/extensions/jamovi/README.md @@ -0,0 +1,5 @@ +# jamovi .omv file renderer + +`.omv` is the native file format for the free and open source [jamovi statistical spreadsheet](https://www.jamovi.org). An `.omv` file is a 'compound' file format, containing data, analyses, and results. + +`.omv` files created by recent versions of jamovi contain an `index.html` file which represents the results of the analyses performed. The jamovi `.omv` file renderer extracts the contents of `index.html` from the archive and replaces image paths from the archive with equivalent data URIs. This then serves as the rendered content. diff --git a/mfr/extensions/jamovi/__init__.py b/mfr/extensions/jamovi/__init__.py new file mode 100644 index 000000000..63706055e --- /dev/null +++ b/mfr/extensions/jamovi/__init__.py @@ -0,0 +1 @@ +from .render import JamoviRenderer # noqa diff --git a/mfr/extensions/jamovi/exceptions.py b/mfr/extensions/jamovi/exceptions.py new file mode 100644 index 000000000..e1744d3c3 --- /dev/null +++ b/mfr/extensions/jamovi/exceptions.py @@ -0,0 +1,45 @@ +from mfr.core.exceptions import RendererError + + +class JamoviRendererError(RendererError): + + def __init__(self, message, *args, **kwargs): + super().__init__(message, *args, renderer_class='jamovi', **kwargs) + + +class JamoviVersionError(JamoviRendererError): + """The jamovi related errors raised from a :class:`mfr.extentions.jamovi` and relating to minimum + data archive version should throw or subclass JamoviVersionError. + """ + + __TYPE = 'jamovi_version' + + def __init__(self, message, *args, code: int=400, created_by: str='', + actual_version: str='', required_version: str='', **kwargs): + super().__init__(message, *args, code=code, **kwargs) + self.created_by = created_by + self.actual_version = actual_version + self.required_version = required_version + self.attr_stack.append([self.__TYPE, { + 'created_by': self.created_by, + 'actual_version': self.actual_version, + 'required_version': self.required_version, + }]) + + +class JamoviFileCorruptError(JamoviRendererError): + """The jamovi related errors raised from a :class:`mfr.extentions.jamovi` and relating to failure + while consuming jamovi files should inherit from JamoviFileCorruptError + """ + + __TYPE = 'jamovi_file_corrupt' + + def __init__(self, message, *args, code: int=400, corruption_type: str='', + reason: str='', **kwargs): + super().__init__(message, *args, code=code, **kwargs) + self.corruption_type = corruption_type + self.reason = reason + self.attr_stack.append([self.__TYPE, { + 'corruption_type': self.corruption_type, + 'reason': self.reason, + }]) diff --git a/mfr/extensions/jamovi/html_processor.py b/mfr/extensions/jamovi/html_processor.py new file mode 100644 index 000000000..1e6da2c4f --- /dev/null +++ b/mfr/extensions/jamovi/html_processor.py @@ -0,0 +1,71 @@ +import base64 +from html.parser import HTMLParser +from io import StringIO + + +class HTMLProcessor(HTMLParser): + + # The HTMLProcessor replaces the src attribute in tags with the base64 equivalent. + # The image content comes from the zip_file (specified with set_src_source()). + # It also strips + diff --git a/mfr/extensions/jasp/templates/viewer.mako b/mfr/extensions/jasp/templates/viewer.mako index 517bc4693..a0637ea35 100644 --- a/mfr/extensions/jasp/templates/viewer.mako +++ b/mfr/extensions/jasp/templates/viewer.mako @@ -1,4 +1,4 @@ -
+
${body}
diff --git a/setup.py b/setup.py index 028de2808..4fa7a9663 100755 --- a/setup.py +++ b/setup.py @@ -795,6 +795,9 @@ def parse_requirements(requirements): #'.wmv = mfr.extensions.video:VideoRenderer', '.webm = mfr.extensions.video:VideoRenderer', + # jamovi + '.omv = mfr.extensions.jamovi:JamoviRenderer', + # JASP '.jasp = mfr.extensions.jasp:JASPRenderer', diff --git a/supportedextensions.md b/supportedextensions.md index 016885ec5..d61225a20 100644 --- a/supportedextensions.md +++ b/supportedextensions.md @@ -35,6 +35,9 @@ Some file types may not be in the correct list. Please search for the file type ## JASP * .jasp +## Jamovi +* .omv + ## Google Documents * .gdoc * .gsheet diff --git a/tests/extensions/jamovi/__init__.py b/tests/extensions/jamovi/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/tests/extensions/jamovi/files/contains-malicious-script.omv b/tests/extensions/jamovi/files/contains-malicious-script.omv new file mode 100644 index 000000000..04f322a6d Binary files /dev/null and b/tests/extensions/jamovi/files/contains-malicious-script.omv differ diff --git a/tests/extensions/jamovi/files/data-archive-version-is-too-old.omv b/tests/extensions/jamovi/files/data-archive-version-is-too-old.omv new file mode 100644 index 000000000..31e32b3da Binary files /dev/null and b/tests/extensions/jamovi/files/data-archive-version-is-too-old.omv differ diff --git a/tests/extensions/jamovi/files/no-data-archive-version-in-manifest.omv b/tests/extensions/jamovi/files/no-data-archive-version-in-manifest.omv new file mode 100644 index 000000000..8877f4b7d Binary files /dev/null and b/tests/extensions/jamovi/files/no-data-archive-version-in-manifest.omv differ diff --git a/tests/extensions/jamovi/files/no-index_html.omv b/tests/extensions/jamovi/files/no-index_html.omv new file mode 100644 index 000000000..1e2236f18 Binary files /dev/null and b/tests/extensions/jamovi/files/no-index_html.omv differ diff --git a/tests/extensions/jamovi/files/no-manifest.omv b/tests/extensions/jamovi/files/no-manifest.omv new file mode 100644 index 000000000..ec98fa44f Binary files /dev/null and b/tests/extensions/jamovi/files/no-manifest.omv differ diff --git a/tests/extensions/jamovi/files/not-a-zip-file.omv b/tests/extensions/jamovi/files/not-a-zip-file.omv new file mode 100644 index 000000000..8704c4379 --- /dev/null +++ b/tests/extensions/jamovi/files/not-a-zip-file.omv @@ -0,0 +1 @@ +this really isn't a zip file diff --git a/tests/extensions/jamovi/files/ok.omv b/tests/extensions/jamovi/files/ok.omv new file mode 100644 index 000000000..7677ce2d8 Binary files /dev/null and b/tests/extensions/jamovi/files/ok.omv differ diff --git a/tests/extensions/jamovi/test_renderer.py b/tests/extensions/jamovi/test_renderer.py new file mode 100644 index 000000000..7ed81da60 --- /dev/null +++ b/tests/extensions/jamovi/test_renderer.py @@ -0,0 +1,138 @@ +import os + +import pytest + +from mfr.core.provider import ProviderMetadata +from mfr.core.exceptions import RendererError +from mfr.extensions.jamovi import JamoviRenderer + + +BASE_PATH = os.path.join(os.path.dirname(os.path.abspath(__file__)), 'files') + +@pytest.fixture +def metadata(): + return ProviderMetadata('jamovi', '.omv', 'application/octet-stream', '1337', + 'http://wb.osf.io/file/jamovi.omv?token=1337') + +@pytest.fixture +def ok_path(): + return os.path.join(BASE_PATH, 'ok.omv') + +@pytest.fixture +def not_a_zip_file_path(): + return os.path.join(BASE_PATH, 'not-a-zip-file.omv') + +@pytest.fixture +def no_manifest_path(): + return os.path.join(BASE_PATH, 'no-manifest.omv') + +@pytest.fixture +def no_archive_version_in_manifest_path(): + return os.path.join(BASE_PATH, 'no-data-archive-version-in-manifest.omv') + +@pytest.fixture +def archive_version_is_too_old_path(): + return os.path.join(BASE_PATH, 'data-archive-version-is-too-old.omv') + +@pytest.fixture +def no_index_html_path(): + return os.path.join(BASE_PATH, 'no-index_html.omv') + +@pytest.fixture +def contains_malicious_script_path(): + return os.path.join(BASE_PATH, 'contains-malicious-script.omv') + +@pytest.fixture +def url(): + return 'http://wb.osf.io/file/jamovi.omv' + +@pytest.fixture +def assets_url(): + return 'http://mfr.osf.io/assets' + +@pytest.fixture +def export_url(): + return 'http://mfr.osf.io/export?url=' + url() + +@pytest.fixture +def extension(): + return '.omv' + +@pytest.fixture +def renderer(metadata, ok_path, url, assets_url, export_url): + return JamoviRenderer(metadata, ok_path, url, assets_url, export_url) + + +class TestCodeJamoviRenderer: + + def test_render_jamovi(self, renderer): + body = renderer.render() + assert '
' in body + + def test_render_jamovi_not_a_zip_file(self, metadata, not_a_zip_file_path, url, assets_url, + export_url): + try: + renderer = JamoviRenderer(metadata, not_a_zip_file_path, url, assets_url, export_url) + renderer.render() + except RendererError: + return + + assert False # should not get here + + def test_render_jamovi_no_manifest(self, metadata, no_manifest_path, url, assets_url, + export_url): + try: + renderer = JamoviRenderer(metadata, no_manifest_path, url, assets_url, export_url) + renderer.render() + except RendererError: + return + + assert False # should not get here + + def test_render_jamovi_no_archive_version_in_manifest(self, metadata, + no_archive_version_in_manifest_path, + url, assets_url, export_url): + try: + renderer = JamoviRenderer(metadata, no_archive_version_in_manifest_path, url, + assets_url, export_url) + renderer.render() + except RendererError: + return + + assert False # should not get here + + def test_render_jamovi_archive_is_too_old(self, metadata, archive_version_is_too_old_path, url, + assets_url, export_url): + try: + renderer = JamoviRenderer(metadata, archive_version_is_too_old_path, url, assets_url, + export_url) + renderer.render() + except RendererError: + return + + assert False # should not get here + + def test_render_jamovi_no_index_html(self, metadata, no_index_html_path, url, assets_url, + export_url): + try: + renderer = JamoviRenderer(metadata, no_index_html_path, url, assets_url, export_url) + renderer.render() + except RendererError: + return + + assert False # should not get here + + def test_render_jamovi_contains_malicious_script(self, metadata, contains_malicious_script_path, + url, assets_url, export_url): + renderer = JamoviRenderer(metadata, contains_malicious_script_path, url, assets_url, + export_url) + body = renderer.render() + + assert '