From d03e6d50f122dc1a29195fea8abab037befdbdc6 Mon Sep 17 00:00:00 2001 From: Mikko Nieminen Date: Thu, 31 Oct 2024 13:46:47 +0100 Subject: [PATCH] add contact field list support (#1789, #2033) --- CHANGELOG.rst | 3 +- docs_manual/source/app_samplesheets_edit.rst | 4 +- docs_manual/source/sodar_release_notes.rst | 1 + samplesheets/rendering.py | 18 ++++++--- samplesheets/tests/test_views_ajax.py | 29 +++++++++++++++ samplesheets/views_ajax.py | 5 ++- .../components/renderers/DataCellRenderer.vue | 37 +++++++++++++++---- .../tests/unit/DataCellRenderer.spec.js | 19 ++++++++++ 8 files changed, 99 insertions(+), 17 deletions(-) diff --git a/CHANGELOG.rst b/CHANGELOG.rst index 86c2be2e1..3c6b76df5 100644 --- a/CHANGELOG.rst +++ b/CHANGELOG.rst @@ -31,6 +31,7 @@ Added - Study plugin override via ISA-Tab comments (#1885) - Token auth support in study plugin IGV XML serving views (#1999, #2021) - Support for newlines in altamISA error messages (#2033) + - Support for performer and contact field values as list (#1789, #2033) - **Taskflowbackend** - ``TaskflowAPI.raise_submit_api_exception()`` helper (#1847) @@ -65,7 +66,7 @@ Changed - Refactor ``SampleSheetAssayPluginPoint.get_assay_path()`` (#2016) - Return ``503`` in ``IrodsCollsCreateAPIView`` if project is locked (#1847) - Return ``503`` in ``IrodsDataRequestAcceptAPIView`` if project is locked (#1847) - - Remove length limitation from ``Process.performer`` (#1789, #1942, #2033) + - Remove length limit from ``Process.performer`` (#1789, #1942, #2033) - **Taskflowbackend** - Refactor task tests (#2002) - Unify user name parameter naming in flows (#1653) diff --git a/docs_manual/source/app_samplesheets_edit.rst b/docs_manual/source/app_samplesheets_edit.rst index e8cc4a5ae..8c981cd00 100644 --- a/docs_manual/source/app_samplesheets_edit.rst +++ b/docs_manual/source/app_samplesheets_edit.rst @@ -103,7 +103,9 @@ Node Names orphaned files. Contacts Contact cells act as string cells with the following expected syntax: - ``Contact Name ``. The email can be omitted. + ``Contact Name ``. The email can be omitted. Multiple + contacts can be provided using the semicolon character as a delimter. For + example: ``Contact1 ;Contact1 ``. Dates Date cells also provide standard string editing but enforce the ISO 8601 ``YYYY-MM-DD`` syntax. diff --git a/docs_manual/source/sodar_release_notes.rst b/docs_manual/source/sodar_release_notes.rst index ffb9e1832..de625279f 100644 --- a/docs_manual/source/sodar_release_notes.rst +++ b/docs_manual/source/sodar_release_notes.rst @@ -19,6 +19,7 @@ Release for SODAR Core v1.0 upgrade, iRODS v4.3 upgrade and feature updates. - Add study plugin override via ISA-Tab comments - Add session control in Django settings and environment variables - Add token-based iRODS/IGV basic auth support for OIDC users +- Add support for performer and contact field values as list - Update minimum supported iRODS version to v4.3.3 - Update REST API versioning - Update REST API views for OpenAPI support diff --git a/samplesheets/rendering.py b/samplesheets/rendering.py index 63dc56651..40f9af439 100644 --- a/samplesheets/rendering.py +++ b/samplesheets/rendering.py @@ -527,16 +527,22 @@ def _is_num(value): col_type = self._field_header[i]['col_type'] if col_type == 'CONTACT': + contact_vals = [] + for x in self._table_data: + if not x[i].get('value'): + contact_vals.append('') + elif isinstance(x[i]['value'], list): + contact_vals.append('; '.join(x[i]['value'])) + else: + contact_vals.append(x[i]['value']) max_cell_len = max( [ ( - _get_length( - re.findall(link_re, x[i]['value'])[0][0] - ) - if re.findall(link_re, x[i].get('value')) - else len(x[i].get('value') or '') + _get_length(re.findall(link_re, x)[0][0]) + if re.findall(link_re, x) + else len(x or '') ) - for x in self._table_data + for x in contact_vals ] ) elif col_type == 'EXTERNAL_LINKS': # Special case, count elements diff --git a/samplesheets/tests/test_views_ajax.py b/samplesheets/tests/test_views_ajax.py index e17be5e18..677cc5de3 100644 --- a/samplesheets/tests/test_views_ajax.py +++ b/samplesheets/tests/test_views_ajax.py @@ -774,6 +774,35 @@ def test_post_performer(self): obj.refresh_from_db() self.assertEqual(obj.performer, value) + def test_post_performer_list(self): + """Test POST with process performer and list value""" + obj = Process.objects.filter(study=self.study, assay=None).first() + value = [ + 'Alice Example ', + 'Bob Example ', + ] + self.values['updated_cells'].append( + { + 'uuid': str(obj.sodar_uuid), + 'header_name': 'Performer', + 'header_type': 'performer', + 'obj_cls': 'Process', + 'value': value, + } + ) + with self.login(self.user): + response = self.client.post( + reverse( + 'samplesheets:ajax_edit_cell', + kwargs={'project': self.project.sodar_uuid}, + ), + json.dumps(self.values), + content_type='application/json', + ) + self.assertEqual(response.status_code, 200) + obj.refresh_from_db() + self.assertEqual(obj.performer, ';'.join(value)) + def test_post_perform_date(self): """Test POST with process perform date""" obj = Process.objects.filter(study=self.study, assay=None).first() diff --git a/samplesheets/views_ajax.py b/samplesheets/views_ajax.py index 2d9541b6c..3fbba2cef 100644 --- a/samplesheets/views_ajax.py +++ b/samplesheets/views_ajax.py @@ -913,7 +913,10 @@ def _update_cell(self, node_obj, cell, save=False): # Performer (special case) elif header_type == 'performer': - node_obj.performer = cell['value'] + if isinstance(cell['value'], list): + node_obj.performer = ';'.join(cell['value']) + else: + node_obj.performer = cell['value'] # Perform date (special case) elif header_type == 'perform_date': diff --git a/samplesheets/vueapp/src/components/renderers/DataCellRenderer.vue b/samplesheets/vueapp/src/components/renderers/DataCellRenderer.vue index cf533e9f7..c5f348c35 100644 --- a/samplesheets/vueapp/src/components/renderers/DataCellRenderer.vue +++ b/samplesheets/vueapp/src/components/renderers/DataCellRenderer.vue @@ -22,16 +22,23 @@ {{ term.name }}; + target="_blank">{{ term.name }}; - {{ term.name }}; + {{ term.name }}; - {{ renderData.name }} + + + {{ contact.name }}; + + + {{ contact.name }}; + + @@ -121,11 +128,25 @@ export default Vue.extend({ return this.params.colDef.headerName.toLowerCase() }, getContact () { - // Return contact name and email - if (contactRegex.test(this.value.value) === true) { - const contactGroup = contactRegex.exec(this.value.value) - return { name: contactGroup[1], email: contactGroup[2] } - } else this.colType = null // Fall back to standard field + // Return contact name(s) and email(s) + const ret = [] + if (this.value.value) { + // console.debug('value type = ' + typeof this.value.value) + let splitVal + if (typeof this.value.value === 'string') { + splitVal = this.value.value.split(';') + } else { + splitVal = this.value.value + } + for (let i = 0; i < splitVal.length; i++) { + if (contactRegex.test(splitVal[i]) === true) { + const contactGroup = contactRegex.exec(splitVal[i]) + ret.push({ name: contactGroup[1].trim(), email: contactGroup[2] }) + } else ret.push({ name: splitVal[i].trim(), email: null }) + } + } + if (ret.length === 0) this.colType = null // Fall back to standard field + return ret }, getExternalLinks () { // Return external links diff --git a/samplesheets/vueapp/tests/unit/DataCellRenderer.spec.js b/samplesheets/vueapp/tests/unit/DataCellRenderer.spec.js index 4208554e5..ba80a9fa9 100644 --- a/samplesheets/vueapp/tests/unit/DataCellRenderer.spec.js +++ b/samplesheets/vueapp/tests/unit/DataCellRenderer.spec.js @@ -296,6 +296,25 @@ describe('DataCellRenderer.vue', () => { expect(cell.classes()).not.toContain('text-muted') }) + it('renders contact cell with list value', async () => { + const table = copy(studyTablesOneCol).tables.study + table.field_header[0] = studyTables.tables.study.field_header[5] + table.table_data[0][0] = studyTables.tables.study.table_data[0][5] + table.table_data[0][0].value = 'John Doe ;Jane Doe' + const wrapper = mountSheetTable({ table: table }) + await waitAG(wrapper) + await waitRAF() + + const cell = wrapper.find('.sodar-ss-data-cell') + const cellData = cell.find('.sodar-ss-data') + expect(cellData.text()).toContain('John Doe;') + expect(cellData.text()).toContain('Jane Doe') + expect(cellData.find('a').exists()).toBe(true) + expect(cellData.find('a').attributes().href).toBe('mailto:john@example.com') + expect(cell.classes()).not.toContain('text-right') + expect(cell.classes()).not.toContain('text-muted') + }) + it('renders contact cell with bracket syntax', async () => { const table = copy(studyTablesOneCol).tables.study table.field_header[0] = studyTables.tables.study.field_header[5]