Skip to content

Commit

Permalink
add general irods access ticket support (#804), refactor tests
Browse files Browse the repository at this point in the history
  • Loading branch information
mikkonie committed Jun 14, 2023
1 parent 5fb9633 commit cb2a74c
Show file tree
Hide file tree
Showing 15 changed files with 988 additions and 605 deletions.
2 changes: 2 additions & 0 deletions CHANGELOG.rst
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ Added
- Enable ``stem_cell_core_bulk`` ISA-Tab template (#1697)
- Enable ``stem_cell_core_sc`` ISA-Tab template (#1697)
- Enable ``tumor_normal_dna`` ISA-Tab template (#1697)
- General iRODS access ticket management for assay collections (#804)
- **Taskflowbackend**
- ``BatchCalculateChecksumTask`` iRODS task (#1634)
- Automated generation of missing checksums in ``zone_move`` flow (#1634)
Expand Down Expand Up @@ -54,6 +55,7 @@ Removed

- **Samplesheets**
- ``SHEETS_TABLE_HEIGHT`` Django setting (#1283)
- Duplicate ``IrodsAccessTicketMixin`` from ``test_views_ajax`` (#1703)


v0.13.4 (2023-05-15)
Expand Down
1 change: 1 addition & 0 deletions docs_manual/source/sodar_release_notes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ v0.14.0 (WIP)

Major feature update.

- Add general read-only iRODS access tickets for assay collections
- Add support for additional sample sheet templates
- Add landing zone updating
- Add automated checksum calculation in landing zone validation and moving
Expand Down
118 changes: 63 additions & 55 deletions samplesheets/forms.py
Original file line number Diff line number Diff line change
Expand Up @@ -279,48 +279,24 @@ def save(self):


class IrodsAccessTicketForm(forms.ModelForm):
"""Form for the irods access ticket creation and editing."""
"""Form for the irods access ticket creation and editing"""

class Meta:
model = IrodsAccessTicket
fields = ('path', 'label', 'date_expires')

def __init__(self, *args, **kwargs):
def __init__(self, project=None, *args, **kwargs):
super().__init__(*args, **kwargs)
from samplesheets.views import TRACK_HUBS_COLL

# Add selection to path field
irods_backend = get_backend_api('omics_irods')
assays = Assay.objects.filter(
study__investigation__project=kwargs['initial']['project']
)
if irods_backend:
with irods_backend.get_session() as irods:
choices = [
(
track_hub.path,
"{} / {}".format(
assay.get_display_name(), track_hub.name
),
)
for assay in assays
for track_hub in irods_backend.get_child_colls(
irods,
os.path.join(
irods_backend.get_path(assay), TRACK_HUBS_COLL
),
)
]
if self.instance.pk:
self.project = self.instance.get_project()
else:
choices = []

# Hide path in update or make it a dropdown with available track hubs
# on creation
self.project = project
# Update path help and hide in update
path_help = 'Full path to iRODS collection within an assay'
self.fields['path'].help_text = path_help
if self.instance.path:
self.fields['path'].widget = forms.widgets.HiddenInput()
else:
self.fields['path'].widget = forms.widgets.Select(choices=choices)

self.fields['path'].required = False
# Add date input widget to expiry date field
self.fields['date_expires'].label = 'Expiry date'
self.fields['date_expires'].widget = forms.widgets.DateInput(
Expand All @@ -329,6 +305,51 @@ def __init__(self, *args, **kwargs):

def clean(self):
irods_backend = get_backend_api('omics_irods')
# Validate path (only if creating)
if not self.instance.pk:
try:
self.cleaned_data['path'] = irods_backend.sanitize_path(
self.cleaned_data['path']
)
except Exception as ex:
self.add_error('path', 'Invalid iRODS path: {}'.format(ex))
return self.cleaned_data
# Ensure path is within project
if not self.cleaned_data['path'].startswith(
irods_backend.get_path(self.project)
):
self.add_error('path', 'Path is not within the project')
return self.cleaned_data
# Ensure path is within a project assay
match = re.search(
r'/assay_([0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})',
self.cleaned_data['path'],
)
if not match:
self.add_error('path', 'Not a valid assay path')
return self.cleaned_data
else:
try:
# Set assay if successful
self.cleaned_data['assay'] = Assay.objects.get(
study__investigation__project=self.project,
sodar_uuid=match.group(1),
)
except ObjectDoesNotExist:
self.add_error('path', 'Assay not found in project')
return self.cleaned_data
# Ensure path is a collection
with irods_backend.get_session() as irods:
if not irods.collections.exists(self.cleaned_data['path']):
self.add_error(
'path',
'Path does not point to a collection or the collection '
'doesn\'t exist',
)
return self.cleaned_data
else: # Do not allow editing path
self.cleaned_data['path'] = self.instance.path

# Check if expiry date is in the past
if (
self.cleaned_data.get('date_expires')
Expand All @@ -337,28 +358,15 @@ def clean(self):
self.add_error(
'date_expires', 'Expiry date in the past not allowed'
)
# Check path validity
try:
self.cleaned_data['path'] = irods_backend.sanitize_path(
self.cleaned_data['path']
)
except Exception as ex:
self.add_error('path', 'Invalid iRODS path: {}'.format(ex))
return self.cleaned_data
# Check if path corresponds to a track hub path
match = re.search(
r'/assay_([0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12})/',
self.cleaned_data['path'],
)
if not match:
self.add_error('path', 'Not a valid TrackHubs path')
else:
try:
self.cleaned_data['assay'] = Assay.objects.get(
sodar_uuid=match.group(1)
)
except ObjectDoesNotExist:
self.add_error('path', 'Assay not found')

# Check if unexpired ticket already exists for path
if (
not self.instance.pk
and IrodsAccessTicket.objects.filter(
path=self.cleaned_data['path']
).first()
):
self.add_error('path', 'Ticket for path already exists')
return self.cleaned_data


Expand Down
47 changes: 47 additions & 0 deletions samplesheets/migrations/0020_update_irodsaccessticket.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
# Generated by Django 3.2.19 on 2023-06-13 10:29

from django.conf import settings
from django.db import migrations, models
import django.db.models.deletion
import django.db.models.manager


class Migration(migrations.Migration):

dependencies = [
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
('samplesheets', '0019_alter_isatab_date_created'),
]

operations = [
migrations.AlterModelManagers(
name='irodsaccessticket',
managers=[
('active_objects', django.db.models.manager.Manager()),
],
),
migrations.RemoveField(
model_name='irodsaccessticket',
name='project',
),
migrations.AlterField(
model_name='irodsaccessticket',
name='assay',
field=models.ForeignKey(blank=True, help_text='Assay in which the ticket belongs (optional)', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='irods_access_tickets', to='samplesheets.assay'),
),
migrations.AlterField(
model_name='irodsaccessticket',
name='date_expires',
field=models.DateTimeField(blank=True, help_text='DateTime of ticket expiration (leave unset to never expire)', null=True),
),
migrations.AlterField(
model_name='irodsaccessticket',
name='study',
field=models.ForeignKey(help_text='Study in which the ticket belongs', on_delete=django.db.models.deletion.CASCADE, related_name='irods_access_tickets', to='samplesheets.study'),
),
migrations.AlterField(
model_name='irodsaccessticket',
name='user',
field=models.ForeignKey(help_text='User that created the ticket', null=True, on_delete=django.db.models.deletion.CASCADE, related_name='irods_access_tickets', to=settings.AUTH_USER_MODEL),
),
]
101 changes: 50 additions & 51 deletions samplesheets/models.py
Original file line number Diff line number Diff line change
Expand Up @@ -1116,44 +1116,23 @@ def get_queryset(self):


class IrodsAccessTicket(models.Model):
"""
Model for managing tickets in irods
"""

class Meta:
ordering = ['-date_created']

objects = models.Manager()
active_objects = IrodsAccessTicketActiveManager()

#: Internal UUID for the object
sodar_uuid = models.UUIDField(
default=uuid.uuid4, unique=True, help_text='SODAR UUID for the object'
)

#: Project the ticket belongs to
project = models.ForeignKey(
Project,
related_name='irods_access_ticket',
help_text='Project the ticket belongs to',
on_delete=models.CASCADE,
)
"""Model for managing access tickets in iRODS"""

#: Study the ticket belongs to
study = models.ForeignKey(
Study,
related_name='irods_access_ticket',
help_text='Study the ticket belongs to',
related_name='irods_access_tickets',
help_text='Study in which the ticket belongs',
on_delete=models.CASCADE,
)

#: Assay the ticket belongs to (optional)
assay = models.ForeignKey(
Assay,
related_name='irods_access_ticket',
related_name='irods_access_tickets',
null=True,
blank=True,
help_text='Assay the ticket belongs to (optional)',
help_text='Assay in which the ticket belongs (optional)',
on_delete=models.CASCADE,
)

Expand All @@ -1178,7 +1157,7 @@ class Meta:
#: User that created the ticket
user = models.ForeignKey(
AUTH_USER_MODEL,
related_name='irods_access_ticket',
related_name='irods_access_tickets',
null=True,
help_text='User that created the ticket',
on_delete=models.CASCADE,
Expand All @@ -1193,11 +1172,50 @@ class Meta:
date_expires = models.DateTimeField(
null=True,
blank=True,
help_text='DateTime of ticket expiration (leave unset to never '
'expire; click x on righthand-side of field to unset)',
help_text='DateTime of ticket expiration (leave unset to never expire)',
)

#: SODAR UUID for the object
sodar_uuid = models.UUIDField(
default=uuid.uuid4, unique=True, help_text='SODAR UUID for the object'
)

def get_track_hub_name(self):
#: Standard manager
objects = models.Manager()

#: Active objects manager
active_objects = IrodsAccessTicketActiveManager()

class Meta:
ordering = ['-date_created']

def __str__(self):
return '{} / {} / {} / {}'.format(
self.study.investigation.project.title,
self.assay.get_display_name(),
self.get_coll_name(),
self.get_label(),
)

def __repr__(self):
values = (
self.study.investigation.project.title,
self.assay.get_display_name(),
self.get_coll_name(),
self.get_label(),
)
return 'IrodsAccessTicket({})'.format(
', '.join(repr(v) for v in values)
)

def get_project(self):
return self.study.investigation.project

@classmethod
def get_project_filter_key(cls):
return 'study__investigation__project'

def get_coll_name(self):
return os.path.basename(self.path)

def get_date_created(self):
Expand All @@ -1215,13 +1233,13 @@ def get_display_name(self):
assay_name = ''
if (
Assay.objects.filter(
study__investigation__project=self.project
study__investigation__project=self.study.investigation.project
).count()
> 1
):
assay_name = '{} / '.format(self.assay.get_display_name())
return '{}{} / {}'.format(
assay_name, self.get_track_hub_name(), self.get_label()
assay_name, self.get_coll_name(), self.get_label()
)

def get_webdav_link(self):
Expand All @@ -1234,25 +1252,6 @@ def get_webdav_link(self):
def is_active(self):
return self.date_expires is None or self.date_expires >= timezone.now()

def __str__(self):
return '{} / {} / {} / {}'.format(
self.project.title,
self.assay.get_display_name(),
self.get_track_hub_name(),
self.get_label(),
)

def __repr__(self):
values = (
self.project.title,
self.assay.get_display_name(),
self.get_track_hub_name(),
self.get_label(),
)
return 'IrodsAccessTicket({})'.format(
', '.join(repr(v) for v in values)
)


class IrodsDataRequest(models.Model):
"""
Expand Down
Loading

0 comments on commit cb2a74c

Please sign in to comment.