-
Notifications
You must be signed in to change notification settings - Fork 3
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
7 changed files
with
352 additions
and
19 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,4 +1,180 @@ | ||
"""Forms for the isatemplates app""" | ||
|
||
# TODO: Add template create/update form | ||
# TODO: Copy file uploading from samplesheets | ||
import json | ||
import logging | ||
|
||
from cubi_isa_templates import _TEMPLATES as CUBI_TEMPLATES | ||
|
||
from django import forms | ||
from django.conf import settings | ||
from django.db import transaction | ||
|
||
# Projectroles dependency | ||
from projectroles.forms import MultipleFileField | ||
|
||
# Samplesheets dependency | ||
# NOTE: Importing for generic Zip file helpers, move elsewhere? | ||
from samplesheets.io import SampleSheetIO, ARCHIVE_TYPES | ||
|
||
from isatemplates.models import ( | ||
CookiecutterISATemplate, | ||
CookiecutterISAFile, | ||
FILE_PREFIXES, | ||
) | ||
|
||
|
||
logger = logging.getLogger(__name__) | ||
|
||
|
||
# Local constants | ||
FILE_SKIP_MSG = 'Skipping unrecognized file in template archive: {file_name}' | ||
|
||
|
||
class ISATemplateForm(forms.ModelForm): | ||
"""Form for importing and updating a custom ISA-Tab template.""" | ||
|
||
@classmethod | ||
def _get_files_from_zip(cls, zip_file): | ||
""" | ||
Return template files from Zip archive. | ||
:param zip_file: ZipFile object | ||
:return: Dict | ||
""" | ||
ret = {'json': None, 'files': {}} | ||
for path in [n for n in zip_file.namelist() if not n.endswith('/')]: | ||
file_name = path.split('/')[-1] | ||
if file_name == 'cookiecutter.json': | ||
with zip_file.open(str(path), 'r') as f: | ||
ret['json'] = json.load(f) | ||
elif file_name[:2] in FILE_PREFIXES: | ||
with zip_file.open(str(path), 'r') as f: | ||
ret['files'][file_name] = f.read().decode('utf-8') | ||
else: | ||
logger.warning(FILE_SKIP_MSG.format(file_name=file_name)) | ||
return ret | ||
|
||
@classmethod | ||
def _get_files_from_multi(cls, files): | ||
""" | ||
Return template files from multi-file upload. | ||
:param files: List | ||
:return: Dict | ||
""" | ||
ret = {'json': None, 'files': {}} | ||
for file in files: | ||
if file.name == 'cookiecutter.json': | ||
ret['json'] = json.load(file) | ||
elif file.name[:2] in FILE_PREFIXES: | ||
ret['files'][file.name] = file.read().decode('utf-8') | ||
else: | ||
logger.warning(FILE_SKIP_MSG.format(file_name=file.name)) | ||
return ret | ||
|
||
file_upload = MultipleFileField( | ||
allow_empty_file=False, | ||
help_text='Zip archive or JSON/text files for an ISA-Tab template', | ||
) | ||
|
||
class Meta: | ||
model = CookiecutterISATemplate | ||
fields = ['file_upload', 'description', 'name', 'active'] | ||
|
||
def __init__(self, current_user=None, *args, **kwargs): | ||
super().__init__(*args, **kwargs) | ||
self.current_user = current_user | ||
self.isa_zip = None | ||
self.fields['name'].required = False # Will be auto-generated if empty | ||
|
||
def clean(self): | ||
self.cleaned_data = super().clean() | ||
sheet_io = SampleSheetIO() | ||
|
||
# Check file_upload | ||
files = self.files.getlist('file_upload') | ||
# Zip archive upload | ||
if len(files) == 1: | ||
file = self.cleaned_data.get('file_upload')[0] | ||
try: | ||
self.isa_zip = sheet_io.get_zip_file(file) | ||
except OSError as ex: | ||
self.add_error('file_upload', str(ex)) | ||
return self.cleaned_data | ||
# Multi-file checks | ||
else: | ||
json_found = False | ||
inv_found = False | ||
study_found = False | ||
for file in files: | ||
if file.content_type in ARCHIVE_TYPES: | ||
self.add_error( | ||
'file_upload', | ||
'You can only upload one Zip archive at a time', | ||
) | ||
return self.cleaned_data | ||
if not file.name.endswith('.json') and not file.name.endswith( | ||
'.txt' | ||
): | ||
self.add_error( | ||
'file_upload', | ||
'Only a Zip archive or template JSON/txt files allowed', | ||
) | ||
return self.cleaned_data | ||
if file.name == 'cookiecutter.json': | ||
json_found = True | ||
elif file.name.startswith('i_'): | ||
inv_found = True | ||
elif file.name.startswith('s_'): | ||
study_found = True | ||
if not json_found: | ||
self.add_error( | ||
'file_upload', 'File cookiecutter.json not found' | ||
) | ||
if not inv_found: | ||
self.add_error('file_upload', 'Investigation file not found') | ||
if not study_found: | ||
self.add_error('file_upload', 'Study file not found') | ||
|
||
# Check description | ||
# NOTE: Uniqueness within custom templates checked on model level | ||
if settings.ISATEMPLATES_ENABLE_CUBI_TEMPLATES: | ||
cubi_descs = [t.description.lower() for t in CUBI_TEMPLATES] | ||
if self.cleaned_data['description'].lower() in cubi_descs: | ||
self.add_error( | ||
'description', | ||
'Template with identical description found in CUBI ' | ||
'templates', | ||
) | ||
return self.cleaned_data | ||
|
||
@transaction.atomic | ||
def save(self, *args, **kwargs): | ||
# TODO: Add support for updating | ||
if self.isa_zip: # Zip archive | ||
file_data = self._get_files_from_zip(self.isa_zip) | ||
else: # Multi-file | ||
file_data = self._get_files_from_multi( | ||
self.files.getlist('file_upload') | ||
) | ||
logger.debug('Saving ISA template..') | ||
template = CookiecutterISATemplate.objects.create( | ||
name=self.cleaned_data['name'], | ||
description=self.cleaned_data['description'], | ||
json_data=file_data['json'], | ||
user=self.current_user, | ||
) | ||
logger.debug( | ||
'Saved ISA template: {} ({})'.format( | ||
template.name, template.sodar_uuid | ||
) | ||
) | ||
for k, v in file_data['files'].items(): | ||
file = CookiecutterISAFile.objects.create( | ||
template=template, file_name=k, content=v | ||
) | ||
logger.debug( | ||
'Saved ISA template file: {} ({})'.format(k, file.sodar_uuid) | ||
) | ||
logger.debug('Template save OK') | ||
return template |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,38 @@ | ||
{% extends 'projectroles/base.html' %} | ||
|
||
{% load rules %} | ||
{% load crispy_forms_filters %} | ||
|
||
{% block title %} | ||
{% if object.pk %}Update{% else %}Import{% endif %} ISA-Tab Template | ||
{% endblock title %} | ||
|
||
{% block projectroles %} | ||
|
||
<div class="row sodar-subtitle-container"> | ||
<h2>{% if object.pk %}Update{% else %}Import{% endif %} ISA-Tab Template</h2> | ||
</div> | ||
|
||
<div class="container-fluid sodar-page-container"> | ||
<form method="post" enctype="multipart/form-data"> | ||
{% csrf_token %} | ||
{{ form | crispy }} | ||
<div class="row"> | ||
<div class="btn-group ml-auto" role="group"> | ||
<a role="button" class="btn btn-secondary" | ||
href="{% url 'isatemplates:list' %}"> | ||
<i class="iconify" data-icon="mdi:arrow-left-circle"></i> Cancel | ||
</a> | ||
<button type="submit" class="btn btn-primary sodar-btn-submit-once"> | ||
{% if object.pk %} | ||
<i class="iconify" data-icon="mdi:check-bold"></i> Update | ||
{% else %} | ||
<i class="iconify" data-icon="mdi:upload"></i> Import | ||
{% endif %} | ||
</button> | ||
</div> | ||
</div> | ||
</form> | ||
</div> | ||
|
||
{% endblock projectroles %} |
Binary file not shown.
Oops, something went wrong.