diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 40b30942..8e912309 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -23,6 +23,7 @@ Added - ``notify_email_irods_request`` user app setting (#1939) - Assay app unit tests (#1980) - Missing assay plugin ``__init__.py`` files (#2014) + - Study plugin override via ISA-Tab comments (#1885) Changed ------- diff --git a/docs_manual/source/metadata_advanced.rst b/docs_manual/source/metadata_advanced.rst index 1f5eac6e..ebfb6e23 100644 --- a/docs_manual/source/metadata_advanced.rst +++ b/docs_manual/source/metadata_advanced.rst @@ -39,6 +39,24 @@ SODAR currently supports the following study configurations: If the configuration is not specified or is not known to SODAR, the shortcut column will not be visible. +It is possible to override the plugin to be used for a study. This allows for +e.g. having different types of studies within a single investigation. The +overriding can be done by adding a ``SODAR Study Plugin`` comment within the +``STUDY`` section of the ISA-Tab investigation file. As the value, the full +internal study plugin name (e.g. ``samplesheets_study_cancer``) should be used. +If both the study plugin comment and investigation configuration comment are +present, the former will override the latter. + +Example: + +.. code-block:: + + STUDY + Study Identifier s_small2 + Study Title Small Germline Study + Study Description + Comment[SODAR Study Plugin] samplesheets_study_cancer + Study plugins will search for the "latest" BAM and VCF files for shortcuts and IGV session generation. This is determined by file name: in case of multiple files, the last file sorted by file name is returned. Hence to ensure the most diff --git a/docs_manual/source/sodar_release_notes.rst b/docs_manual/source/sodar_release_notes.rst index 0dfeab40..524df576 100644 --- a/docs_manual/source/sodar_release_notes.rst +++ b/docs_manual/source/sodar_release_notes.rst @@ -16,6 +16,7 @@ Release for SODAR Core v1.0 upgrade, iRODS v4.3 upgrade and feature updates. - Add opt-out settings for iRODS data request and zone status update emails - Add REST API list view pagination - Add Python v3.11 support +- Add study plugin override via ISA-Tab comments - Add session control in Django settings and environment variables - Update minimum supported iRODS version to v4.3.3 - Update REST API versioning diff --git a/samplesheets/models.py b/samplesheets/models.py index 737567b8..9858fe87 100644 --- a/samplesheets/models.py +++ b/samplesheets/models.py @@ -74,7 +74,8 @@ IRODS_REQUEST_STATUS_FAILED = 'FAILED' IRODS_REQUEST_STATUS_REJECTED = 'REJECTED' -# ISA-Tab SODAR metadata comment key for assay plugin override +# ISA-Tab SODAR metadata comment keys for study and assay plugin overrides +ISA_META_STUDY_PLUGIN = 'SODAR Study Plugin' ISA_META_ASSAY_PLUGIN = 'SODAR Assay Plugin' @@ -403,9 +404,19 @@ def get_plugin(self): """Return active study app plugin or None if not found""" from samplesheets.plugins import SampleSheetStudyPluginPoint + inv_config = self.investigation.get_configuration() + study_override = self.comments.get(ISA_META_STUDY_PLUGIN) + if study_override: + try: + return SampleSheetStudyPluginPoint.get_plugin( + name=study_override + ) + except Exception: + return None for plugin in SampleSheetStudyPluginPoint.get_plugins(): - if plugin.config_name == self.investigation.get_configuration(): + if plugin.config_name == inv_config: return plugin + return None def get_url(self): """Return the URL for this study""" diff --git a/samplesheets/studyapps/cancer/plugins.py b/samplesheets/studyapps/cancer/plugins.py index 169f6f41..b36633ab 100644 --- a/samplesheets/studyapps/cancer/plugins.py +++ b/samplesheets/studyapps/cancer/plugins.py @@ -326,9 +326,6 @@ def update_cache(self, name=None, project=None, user=None): ) except Investigation.DoesNotExist: continue - # Only apply for investigations with the correct configuration - if investigation.get_configuration() != self.config_name: - continue # If a name is given, only update that specific CacheItem if name: study_uuid = name.split('/')[-1] @@ -336,6 +333,12 @@ def update_cache(self, name=None, project=None, user=None): else: studies = Study.objects.filter(investigation=investigation) for study in studies: + # Only apply for studies using this plugin + if ( + not study.get_plugin() + or study.get_plugin().__class__ != self.__class__ + ): + continue if self._has_only_ms_assays(study): continue self._update_study_cache(study, user, cache_backend) diff --git a/samplesheets/studyapps/cancer/tests/test_plugins_taskflow.py b/samplesheets/studyapps/cancer/tests/test_plugins_taskflow.py index 8634c0cf..0b5ad0f0 100644 --- a/samplesheets/studyapps/cancer/tests/test_plugins_taskflow.py +++ b/samplesheets/studyapps/cancer/tests/test_plugins_taskflow.py @@ -13,13 +13,14 @@ # Taskflowbackend dependency from taskflowbackend.tests.base import TaskflowViewTestBase +from samplesheets.models import ISA_META_STUDY_PLUGIN +from samplesheets.plugins import SampleSheetStudyPluginPoint from samplesheets.rendering import SampleSheetTableBuilder +from samplesheets.studyapps.cancer.utils import get_library_file_path +from samplesheets.studyapps.utils import get_igv_session_url from samplesheets.tests.test_io import SampleSheetIOMixin, SHEET_DIR from samplesheets.tests.test_models import SampleSheetModelMixin from samplesheets.tests.test_views_taskflow import SampleSheetTaskflowMixin -from samplesheets.plugins import SampleSheetStudyPluginPoint -from samplesheets.studyapps.cancer.utils import get_library_file_path -from samplesheets.studyapps.utils import get_igv_session_url app_settings = AppSettingAPI() @@ -560,6 +561,18 @@ def test_update_cache(self): self.assertIsNone(ci['bam'][c]) self.assertIsNone(ci['vcf'][c]) + def test_update_cache_no_config(self): + """Test update_cache() without config""" + # Clear investigation configuration comments + self.investigation.comments = {} + self.investigation.save() + self.plugin.update_cache(self.cache_name, self.project) + self.assertIsNone( + self.cache_backend.get_cache_item( + APP_NAME, self.cache_name, self.project + ) + ) + def test_update_cache_files(self): """Test update_cache() with files in iRODS""" self.irods.collections.create(self.source_path) @@ -581,6 +594,34 @@ def test_update_cache_files(self): self.assertEqual(ci['bam'][CASE_IDS[i]], None) self.assertEqual(ci['vcf'][CASE_IDS[i]], None) + def test_update_cache_files_override(self): + """Test update_cache() with files in iRODS and study override""" + # Clear investigation configuration comments and set study override + self.investigation.comments = {} + self.investigation.save() + self.study.comments = { + ISA_META_STUDY_PLUGIN: 'samplesheets_study_cancer' + } + self.study.save() + self.irods.collections.create(self.source_path) + bam_path = os.path.join( + self.source_path, '{}_test.bam'.format(SAMPLE_ID_NORMAL) + ) + vcf_path = os.path.join( + self.source_path, '{}_test.vcf.gz'.format(SAMPLE_ID_NORMAL) + ) + self.irods.data_objects.create(bam_path) + self.irods.data_objects.create(vcf_path) + self.plugin.update_cache(self.cache_name, self.project) + ci = self.cache_backend.get_cache_item( + APP_NAME, self.cache_name, self.project + ).data + self.assertEqual(ci['bam'][CASE_IDS[0]], bam_path) + self.assertEqual(ci['vcf'][CASE_IDS[0]], vcf_path) + for i in range(1, len(CASE_IDS) - 1): + self.assertEqual(ci['bam'][CASE_IDS[i]], None) + self.assertEqual(ci['vcf'][CASE_IDS[i]], None) + def test_update_cache_cram(self): """Test update_cache() with CRAM file in iRODS""" self.irods.collections.create(self.source_path) diff --git a/samplesheets/studyapps/germline/plugins.py b/samplesheets/studyapps/germline/plugins.py index cdbf3dc5..f17dae46 100644 --- a/samplesheets/studyapps/germline/plugins.py +++ b/samplesheets/studyapps/germline/plugins.py @@ -424,8 +424,6 @@ def update_cache(self, name=None, project=None, user=None): if not investigation: continue # Only apply for investigations with the correct configuration - if investigation.get_configuration() != self.config_name: - continue logger.debug( 'Updating cache for project {}..'.format( project.get_log_title() @@ -438,6 +436,12 @@ def update_cache(self, name=None, project=None, user=None): else: studies = Study.objects.filter(investigation=investigation) for study in studies: + # Only apply for studies using this plugin + if ( + not study.get_plugin() + or study.get_plugin().__class__ != self.__class__ + ): + continue logger.debug( 'Updating cache for study "{}" ({})..'.format( study.get_display_name(), study.sodar_uuid diff --git a/samplesheets/studyapps/germline/tests/test_plugins_taskflow.py b/samplesheets/studyapps/germline/tests/test_plugins_taskflow.py index ee960a0b..24280e77 100644 --- a/samplesheets/studyapps/germline/tests/test_plugins_taskflow.py +++ b/samplesheets/studyapps/germline/tests/test_plugins_taskflow.py @@ -13,14 +13,14 @@ # Taskflowbackend dependency from taskflowbackend.tests.base import TaskflowViewTestBase -from samplesheets.models import GenericMaterial +from samplesheets.models import GenericMaterial, ISA_META_STUDY_PLUGIN +from samplesheets.plugins import SampleSheetStudyPluginPoint from samplesheets.rendering import SampleSheetTableBuilder +from samplesheets.studyapps.germline.utils import get_pedigree_file_path +from samplesheets.studyapps.utils import get_igv_session_url from samplesheets.tests.test_io import SampleSheetIOMixin, SHEET_DIR from samplesheets.tests.test_models import SampleSheetModelMixin from samplesheets.tests.test_views_taskflow import SampleSheetTaskflowMixin -from samplesheets.plugins import SampleSheetStudyPluginPoint -from samplesheets.studyapps.germline.utils import get_pedigree_file_path -from samplesheets.studyapps.utils import get_igv_session_url app_settings = AppSettingAPI() @@ -594,6 +594,18 @@ def test_update_cache(self): self.assertIsNone(ci['vcf'][FAMILY_ID]) self.assertIsNone(ci['vcf'][FAMILY_ID2]) + def test_update_cache_no_config(self): + """Test update_cache() without config""" + # Clear investigation configuration comments + self.investigation.comments = {} + self.investigation.save() + self.plugin.update_cache(self.cache_name, self.project) + self.assertIsNone( + self.cache_backend.get_cache_item( + APP_NAME, self.cache_name, self.project + ) + ) + def test_update_cache_files(self): """Test update_cache() with files in iRODS""" self.irods.collections.create(self.source_path) @@ -613,6 +625,32 @@ def test_update_cache_files(self): self.assertEqual(ci['vcf'][FAMILY_ID], vcf_path) self.assertIsNone(ci['vcf'][FAMILY_ID2]) + def test_update_cache_files_override(self): + """Test update_cache() with files in iRODS and study override""" + # Clear investigation configuration comments and set study override + self.investigation.comments = {} + self.investigation.save() + self.study.comments = { + ISA_META_STUDY_PLUGIN: 'samplesheets_study_germline' + } + self.study.save() + self.irods.collections.create(self.source_path) + bam_path = os.path.join( + self.source_path, '{}_test.bam'.format(SAMPLE_ID) + ) + vcf_path = os.path.join( + self.source_path, '{}_test.vcf.gz'.format(FAMILY_ID) + ) + self.irods.data_objects.create(bam_path) + self.irods.data_objects.create(vcf_path) + self.plugin.update_cache(self.cache_name, self.project) + ci = self.cache_backend.get_cache_item( + APP_NAME, self.cache_name, self.project + ).data + self.assertEqual(ci['bam'][self.source.name], bam_path) + self.assertEqual(ci['vcf'][FAMILY_ID], vcf_path) + self.assertIsNone(ci['vcf'][FAMILY_ID2]) + def test_update_cache_cram(self): """Test update_cache() with CRAM file in iRODS""" self.irods.collections.create(self.source_path) diff --git a/samplesheets/tests/test_models.py b/samplesheets/tests/test_models.py index 22629a13..140a68f0 100644 --- a/samplesheets/tests/test_models.py +++ b/samplesheets/tests/test_models.py @@ -39,10 +39,12 @@ IrodsDataRequest, NOT_AVAILABLE_STR, CONFIG_LABEL_CREATE, + ISA_META_STUDY_PLUGIN, ISA_META_ASSAY_PLUGIN, IRODS_REQUEST_ACTION_DELETE, IRODS_REQUEST_STATUS_ACTIVE, ) +from samplesheets.plugins import SampleSheetStudyPluginPoint from samplesheets.utils import get_alt_names @@ -613,6 +615,52 @@ def test_get_url(self): ) + '#/study/{}'.format(self.study.sodar_uuid) self.assertEqual(self.study.get_url(), expected) + def test_get_plugin_unset(self): + """Test get_plugin() with no config set""" + self.assertIsNone(self.study.get_plugin()) + + def test_get_plugin_investigation(self): + """Test get_plugin() with config set in investigation""" + self.investigation.comments = {CONFIG_LABEL_CREATE: 'bih_germline'} + self.assertIsInstance( + self.study.get_plugin(), + SampleSheetStudyPluginPoint.get_plugin( + name='samplesheets_study_germline' + ).__class__, + ) + + def test_get_plugin_override(self): + """Test get_plugin() with config set in study override""" + self.study.comments = { + ISA_META_STUDY_PLUGIN: 'samplesheets_study_germline' + } + self.assertIsInstance( + self.study.get_plugin(), + SampleSheetStudyPluginPoint.get_plugin( + name='samplesheets_study_germline' + ).__class__, + ) + + def test_get_plugin_override_invalid(self): + """Test get_plugin() with invalid config name set in study override""" + self.study.comments = { + ISA_META_STUDY_PLUGIN: 'samplesheets_study_NONEXISTENT_NAME' + } + self.assertIsNone(self.study.get_plugin()) + + def test_get_plugin_both(self): + """Test get_plugin() with investigation and study configs""" + self.investigation.comments = {CONFIG_LABEL_CREATE: 'bih_germline'} + self.study.comments = { + ISA_META_STUDY_PLUGIN: 'samplesheets_study_cancer' + } + self.assertIsInstance( + self.study.get_plugin(), + SampleSheetStudyPluginPoint.get_plugin( + name='samplesheets_study_cancer' + ).__class__, + ) + class TestProtocol(SamplesheetsModelTestBase): """Tests for the Protocol model"""