Skip to content

Commit

Permalink
update IrodsDataRequest handling (#1706), refactor tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mikkonie committed Aug 1, 2023
1 parent 13ec5c1 commit 3982feb
Show file tree
Hide file tree
Showing 23 changed files with 3,851 additions and 3,262 deletions.
15 changes: 15 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,12 @@ Added
- **Irodsbackend**
- ``get_trash_path()`` helper (#1658)
- iRODS trash statistics for siteinfo (#1658)
- **Irodsinfo**
- ``IrodsEnvRetrieveAPIView`` for retrieving iRODS environment (#1685)
- **Landingzones**
- Landing zone updating (#1267)
- "Nothing to do" check for landing zone validation and moving (#339)
- iRODS path clipboard copying button in iRODS collection list modal (#1282)
- **Samplesheets**
- User setting for study and assay table height (#1283)
- Study table cache disabling (#1639)
Expand All @@ -27,6 +31,9 @@ Added
- Enable ``tumor_normal_dna`` ISA-Tab template (#1697)
- General iRODS access ticket management for assay collections (#804, #1717)
- Disabled row delete button tooltips (#1731)
- ``IrodsDataRequest`` REST API views (#1588, #1706, #1734, #1735, #1736)
- Davrods links in iRODS delete request list (#1339)
- Batch accepting and rejecting for iRODS delete requests (#1340)
- **Taskflowbackend**
- ``BatchCalculateChecksumTask`` iRODS task (#1634)
- Automated generation of missing checksums in ``zone_move`` flow (#1634)
Expand All @@ -41,10 +48,14 @@ Changed
- Upgrade to python-irodsclient v1.1.8 (#1538)
- **Landingzones**
- Move iRODS object helpers to ``TaskflowTestMixin`` (#1699)
- Enable superuser landing zone controls for locked zones (#1607)
- Add ``DELETING`` to locked states in UI (#1657)
- Query for landing zone status in batch (#1684)
- **Samplesheets**
- Sample sheet table viewport background color (#1692)
- Contract sheet table height to fit content (#1693)
- Hide additional internal fields from ISA-Tab templates (#1698)
- Refactor ``IrodsDataRequest`` model and tests (#1706)
- **Taskflowbackend**
- Move iRODS object helpers from ``LandingZoneTaskflowMixin`` (#1699)
- Move iRODS test cleanup to ``TaskflowTestMixin.clear_irods_test_data()`` (#1722)
Expand All @@ -57,13 +68,17 @@ Fixed
- Batch import tests failing from forbidden obolibrary access (#1694)
- **Samplesheets**
- ``perform_project_sync()`` crash with no iRODS collections created (#1687)
- iRODS delete request modification UI view permission checks failing for non-creator contributors (#1737)

Removed
-------

- **Landingzones**
- Unused ``data_tables`` references from templates (#1710)
- **Samplesheets**
- ``SHEETS_TABLE_HEIGHT`` Django setting (#1283)
- Duplicate ``IrodsAccessTicketMixin`` from ``test_views_ajax`` (#1703)
- ``IRODS_DATA_REQUEST_STATUS_CHOICES`` constant (#1706)


v0.13.4 (2023-05-15)
Expand Down
12 changes: 7 additions & 5 deletions docs_manual/source/api_samplesheets.rst
Original file line number Diff line number Diff line change
Expand Up @@ -15,17 +15,19 @@ API Views

.. autoclass:: IrodsCollsCreateAPIView

.. autoclass:: IrodsDataRequestRetrieveAPIView

.. autoclass:: IrodsDataRequestListAPIView

.. autoclass:: IrodsRequestCreateAPIView
.. autoclass:: IrodsDataRequestCreateAPIView

.. autoclass:: IrodsRequestUpdateAPIView
.. autoclass:: IrodsDataRequestUpdateAPIView

.. autoclass:: IrodsRequestDeleteAPIView
.. autoclass:: IrodsDataRequestDeleteAPIView

.. autoclass:: IrodsRequestAcceptAPIView
.. autoclass:: IrodsDataRequestAcceptAPIView

.. autoclass:: IrodsRequestRejectAPIView
.. autoclass:: IrodsDataRequestRejectAPIView

.. autoclass:: SheetImportAPIView

Expand Down
153 changes: 87 additions & 66 deletions samplesheets/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@
ISATab,
IrodsAccessTicket,
IrodsDataRequest,
IRODS_REQUEST_STATUS_ACTIVE,
IRODS_REQUEST_STATUS_FAILED,
)


Expand All @@ -35,6 +37,9 @@
TPL_DIR_FIELD = '__output_dir'


# Mixins and Helpers -----------------------------------------------------------


class MultipleFileInput(forms.ClearableFileInput):
allow_multiple_selected = True

Expand All @@ -53,6 +58,72 @@ def clean(self, data, initial=None):
return result


class IrodsDataRequestValidateMixin:
"""Validation helpers for iRODS data requests"""

def validate_request_path(self, irods_backend, project, instance, path):
"""
Validate path for IrodsAccessRequest.
:param irods_backend: IrodsAPI object
:param project: Project object
:param instance: Existing instance in case of update or None
:param path: Full iRODS path to a collection or a data object (string)
:raises: ValueError if path is incorrect
"""
old_request = IrodsDataRequest.objects.filter(
path=path,
status__in=[
IRODS_REQUEST_STATUS_ACTIVE,
IRODS_REQUEST_STATUS_FAILED,
],
).first()
if old_request and old_request != instance:
raise ValueError(ERROR_MSG_EXISTING)

path_re = re.compile(
'^' + irods_backend.get_projects_path() + '/[0-9a-f]{2}/'
'(?P<project_uuid>[0-9a-f-]{36})/'
+ settings.IRODS_SAMPLE_COLL
+ '/study_(?P<study_uuid>[0-9a-f-]{36})/'
'assay_(?P<assay_uuid>[0-9a-f-]{36})/.+$'
)
match = re.search(path_re, path)
if not match:
raise ValueError(ERROR_MSG_INVALID_PATH)

project_path = irods_backend.get_path(project)
if not path.startswith(project_path):
raise ValueError('Path does not belong in project')

try:
Study.objects.get(
sodar_uuid=match.group('study_uuid'),
investigation__project__sodar_uuid=match.group('project_uuid'),
)
except Study.DoesNotExist:
raise ValueError('Study not found in project with UUID')
try:
Assay.objects.get(
sodar_uuid=match.group('assay_uuid'),
study__sodar_uuid=match.group('study_uuid'),
)
except Assay.DoesNotExist:
raise ValueError('Assay not found in this project with UUID')

with irods_backend.get_session() as irods:
if path and not (
irods.data_objects.exists(path)
or irods.collections.exists(path)
):
raise ValueError(
'Path to collection or data object doesn\'t exist in iRODS',
)


# Forms ------------------------------------------------------------------------


class SheetImportForm(forms.Form):
"""
Form for importing an ISA investigation from an ISA-Tab archive or
Expand Down Expand Up @@ -154,7 +225,7 @@ def save(self, *args, **kwargs):


class SheetTemplateCreateForm(forms.Form):
"""Form for creating sample sheets from an ISA-Tab template."""
"""Form for creating sample sheets from an ISA-Tab template"""

@classmethod
def _get_tsv_data(cls, path, file_names):
Expand Down Expand Up @@ -378,84 +449,34 @@ def clean(self):
return self.cleaned_data


class IrodsRequestForm(forms.ModelForm):
"""Form for the iRODS delete request creation and editing"""
class IrodsDataRequestForm(IrodsDataRequestValidateMixin, forms.ModelForm):
"""Form for iRODS data request creation and editing"""

class Meta:
model = IrodsDataRequest
fields = ['path', 'description']

def __init__(self, *args, **kwargs):
def __init__(self, project=None, *args, **kwargs):
super().__init__(*args, **kwargs)
if project:
self.project = Project.objects.filter(sodar_uuid=project).first()
self.fields['description'].required = False

def clean(self):
cleaned_data = super().clean()
irods_backend = get_backend_api('omics_irods')
# Remove trailing slashes as irodspython client does not recognize
# this as a collection
cleaned_data['path'] = cleaned_data['path'].rstrip('/')

old_request = IrodsDataRequest.objects.filter(
path=cleaned_data['path'], status__in=['ACTIVE', 'FAILED']
).first()
if old_request and old_request != self.instance:
self.add_error('path', ERROR_MSG_EXISTING)
return cleaned_data

path_re = re.compile(
'^' + irods_backend.get_projects_path() + '/[0-9a-f]{2}/'
'(?P<project_uuid>[0-9a-f-]{36})/'
+ settings.IRODS_SAMPLE_COLL
+ '/study_(?P<study_uuid>[0-9a-f-]{36})/'
'assay_(?P<assay_uuid>[0-9a-f-]{36})/.+$'
)
match = re.search(
path_re,
cleaned_data['path'],
)
if not match:
self.add_error('path', ERROR_MSG_INVALID_PATH)
else:
try:
cleaned_data['project'] = Project.objects.get(
sodar_uuid=match.group('project_uuid')
)
except Project.DoesNotExist:
self.add_error('path', 'Project not found')
try:
Study.objects.get(
sodar_uuid=match.group('study_uuid'),
investigation__project__sodar_uuid=match.group(
'project_uuid'
),
)
except Study.DoesNotExist:
self.add_error('path', 'Study not found in project with UUID')
try:
Assay.objects.get(
sodar_uuid=match.group('assay_uuid'),
study__sodar_uuid=match.group('study_uuid'),
)
except Assay.DoesNotExist:
self.add_error(
'path', 'Assay not found in this project with UUID'
)

with irods_backend.get_session() as irods:
if 'path' in cleaned_data and not (
irods.data_objects.exists(cleaned_data['path'])
or irods.collections.exists(cleaned_data['path'])
):
self.add_error(
'path',
'Path to collection or data object doesn\'t exist in iRODS',
)
cleaned_data['path'] = irods_backend.sanitize_path(cleaned_data['path'])
try:
self.validate_request_path(
irods_backend, self.project, self.instance, cleaned_data['path']
)
except Exception as ex:
self.add_error('path', str(ex))
return cleaned_data


class IrodsRequestAcceptForm(forms.Form):
"""Form accepting an iRODS delete request."""
class IrodsDataRequestAcceptForm(forms.Form):
"""Form for accepting an iRODS data request"""

confirm = forms.BooleanField(required=True)

Expand All @@ -468,7 +489,7 @@ def __init__(self, *args, **kwargs):


class SheetVersionEditForm(forms.ModelForm):
"""Form for editing a saved ISA-Tab version of the sample sheets."""
"""Form for editing a saved ISA-Tab version of sample sheets"""

class Meta:
model = ISATab
Expand Down
40 changes: 40 additions & 0 deletions samplesheets/migrations/0021_update_irodsdatarequest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Generated by Django 3.2.19 on 2023-07-27 13:52

from django.db import migrations, models
import django.db.models.deletion


class Migration(migrations.Migration):

dependencies = [
('projectroles', '0028_populate_finder_role'),
('samplesheets', '0020_update_irodsaccessticket'),
]

operations = [
migrations.AlterModelManagers(
name='irodsaccessticket',
managers=[
],
),
migrations.AlterField(
model_name='irodsdatarequest',
name='action',
field=models.CharField(default='DELETE', help_text='Action to be performed', max_length=64),
),
migrations.AlterField(
model_name='irodsdatarequest',
name='description',
field=models.CharField(blank=True, help_text='Request description (optional)', max_length=1024, null=True),
),
migrations.AlterField(
model_name='irodsdatarequest',
name='project',
field=models.ForeignKey(help_text='Project to which the iRODS data request belongs', on_delete=django.db.models.deletion.CASCADE, related_name='irods_data_request', to='projectroles.project'),
),
migrations.AlterField(
model_name='irodsdatarequest',
name='status',
field=models.CharField(default='ACTIVE', help_text='Status of the request', max_length=16),
),
]
Loading

0 comments on commit 3982feb

Please sign in to comment.