Skip to content

Commit e951daa

Browse files
authored
Merge 53450db into ebdddd8
2 parents ebdddd8 + 53450db commit e951daa

File tree

11 files changed

+263
-8
lines changed

11 files changed

+263
-8
lines changed

qiita_db/metadata_template/prep_template.py

Lines changed: 189 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -713,3 +713,192 @@ def modification_timestamp(self):
713713
@staticmethod
714714
def max_samples():
715715
return qdb.util.max_preparation_samples()
716+
717+
def add_default_workflow(self, user):
718+
"""The modification timestamp of the prep information
719+
720+
721+
Parameters
722+
----------
723+
user : The user that requested to add the default workflows
724+
725+
Returns
726+
-------
727+
ProcessingWorkflow
728+
The workflow created
729+
730+
Raises
731+
------
732+
ValueError
733+
If this preparation doesn't have valid workflows
734+
If this preparation has been fully processed
735+
"""
736+
# helper functions to avoid duplication of code
737+
738+
def _get_node_info(node):
739+
# retrieves the merging scheme of a node
740+
parent = list(wk.graph.predecessors(node))
741+
if parent:
742+
parent = parent.pop()
743+
pdp = parent.default_parameter
744+
pcmd = pdp.command
745+
pparams = pdp.values
746+
else:
747+
pcmd = None
748+
pparams = {}
749+
750+
dp = node.default_parameter
751+
cparams = dp.values
752+
ccmd = dp.command
753+
754+
parent_cmd_name = None
755+
parent_merging_scheme = None
756+
if pcmd is not None:
757+
parent_cmd_name = pcmd.name
758+
parent_merging_scheme = pcmd.merging_scheme
759+
760+
return qdb.util.human_merging_scheme(
761+
ccmd.name, ccmd.merging_scheme, parent_cmd_name,
762+
parent_merging_scheme, cparams, [], pparams)
763+
764+
def _get_predecessors(node):
765+
# recursive method to get predecessors of a given node
766+
for pnode in wk.graph.predecessors(node):
767+
pred = _get_predecessors(pnode)
768+
cxns = {x[0]: x[2]
769+
for x in wk.graph.get_edge_data(
770+
pnode, node)['connections'].connections}
771+
data = [pnode, node, cxns]
772+
if pred is None:
773+
pred = [data]
774+
else:
775+
pred.append(data)
776+
return pred
777+
778+
# Note: we are going to use the final BIOMs to figure out which
779+
# processing is missing from the back/end to the front, as this
780+
# will prevent generating unnecessary steps (AKA already provided
781+
# by another command), like "Split Library of Demuxed",
782+
# when "Split per Sample" is alrady generated
783+
#
784+
# The steps to generate the default workflow are as follow:
785+
# 1. retrieve all valid merging schemes from valid jobs in the
786+
# current preparation
787+
# 2. retrive all the valid workflows for the preparation data type and
788+
# find the final BIOM missing from the valid available merging
789+
# schemes
790+
# 3. loop over the missing merging schemes and create the commands
791+
# missing to get to those processed samples and add them to a new
792+
# workflow
793+
# 4.
794+
795+
# 1.
796+
prep_jobs = [j for c in self.artifact.descendants.nodes()
797+
for j in c.jobs(show_hidden=True)
798+
if j.command.software.type == 'artifact transformation']
799+
merging_schemes = {
800+
qdb.archive.Archive.get_merging_scheme_from_job(j): {
801+
x: y.id for x, y in j.outputs.items()}
802+
for j in prep_jobs if j.status == 'success' and not j.hidden}
803+
804+
# 2.
805+
pt_dt = self.data_type()
806+
workflows = [wk for wk in qdb.software.DefaultWorkflow.iter()
807+
if pt_dt in wk.data_type]
808+
if not workflows:
809+
raise ValueError(f'This preparation data type: "{pt_dt}" does not '
810+
'have valid workflows')
811+
missing_artifacts = dict()
812+
for wk in workflows:
813+
missing_artifacts[wk] = dict()
814+
for node, degree in wk.graph.out_degree():
815+
if degree != 0:
816+
continue
817+
mscheme = _get_node_info(node)
818+
if mscheme not in merging_schemes:
819+
missing_artifacts[wk][mscheme] = node
820+
if not missing_artifacts[wk]:
821+
del missing_artifacts[wk]
822+
if not missing_artifacts:
823+
raise ValueError('This preparation is complete')
824+
825+
# 3.
826+
workflow = None
827+
for wk, wk_data in missing_artifacts.items():
828+
previous_jobs = dict()
829+
for ma, node in wk_data.items():
830+
predecessors = _get_predecessors(node)
831+
predecessors.reverse()
832+
cmds_to_create = []
833+
init_artifacts = None
834+
for i, (pnode, cnode, cxns) in enumerate(predecessors):
835+
cdp = cnode.default_parameter
836+
cdp_cmd = cdp.command
837+
params = cdp.values.copy()
838+
839+
icxns = {y: x for x, y in cxns.items()}
840+
reqp = {x: icxns[y[1][0]]
841+
for x, y in cdp_cmd.required_parameters.items()}
842+
cmds_to_create.append([cdp_cmd, params, reqp])
843+
844+
info = _get_node_info(pnode)
845+
if info in merging_schemes:
846+
if set(merging_schemes[info]) >= set(cxns):
847+
init_artifacts = merging_schemes[info]
848+
break
849+
if init_artifacts is None:
850+
pdp = pnode.default_parameter
851+
pdp_cmd = pdp.command
852+
params = pdp.values.copy()
853+
reqp = {x: y[1][0]
854+
for x, y in pdp_cmd.required_parameters.items()}
855+
cmds_to_create.append([pdp_cmd, params, reqp])
856+
857+
init_artifacts = {
858+
self.artifact.artifact_type: self.artifact.id}
859+
860+
cmds_to_create.reverse()
861+
current_job = None
862+
for i, (cmd, params, rp) in enumerate(cmds_to_create):
863+
previous_job = current_job
864+
if previous_job is None:
865+
req_params = dict()
866+
for iname, dname in rp.items():
867+
if dname not in init_artifacts:
868+
msg = (f'Missing Artifact type: "{dname}" in '
869+
'this preparation; are you missing a '
870+
'step to start?')
871+
raise ValueError(msg)
872+
req_params[iname] = init_artifacts[dname]
873+
else:
874+
req_params = dict()
875+
connections = dict()
876+
for iname, dname in rp.items():
877+
req_params[iname] = f'{previous_job.id}{dname}'
878+
connections[dname] = iname
879+
params.update(req_params)
880+
job_params = qdb.software.Parameters.load(
881+
cmd, values_dict=params)
882+
883+
if job_params in previous_jobs.values():
884+
for x, y in previous_jobs.items():
885+
if job_params == y:
886+
current_job = x
887+
continue
888+
889+
if workflow is None:
890+
PW = qdb.processing_job.ProcessingWorkflow
891+
workflow = PW.from_scratch(user, job_params)
892+
current_job = [j for j in workflow.graph.nodes()][0]
893+
else:
894+
if previous_job is None:
895+
current_job = workflow.add(
896+
job_params, req_params=req_params)
897+
else:
898+
current_job = workflow.add(
899+
job_params, req_params=req_params,
900+
connections={previous_job: connections})
901+
902+
previous_jobs[current_job] = job_params
903+
904+
return workflow

qiita_db/metadata_template/test/test_prep_template.py

Lines changed: 16 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1340,11 +1340,26 @@ def test_artifact_setter_error(self):
13401340

13411341
def test_artifact_setter(self):
13421342
pt = qdb.metadata_template.prep_template.PrepTemplate.create(
1343-
self.metadata, self.test_study, self.data_type_id)
1343+
self.metadata, self.test_study, '16S')
13441344
self.assertEqual(pt.artifact, None)
13451345
artifact = qdb.artifact.Artifact.create(
13461346
self.filepaths, "FASTQ", prep_template=pt)
13471347
self.assertEqual(pt.artifact, artifact)
1348+
1349+
# here we can test that we can properly create a workflow
1350+
wk = pt.add_default_workflow(qdb.user.User('test@foo.bar'))
1351+
self.assertEqual(len(wk.graph.nodes), 2)
1352+
self.assertEqual(len(wk.graph.edges), 1)
1353+
self.assertEqual(
1354+
[x.command.name for x in wk.graph.nodes],
1355+
['Split libraries FASTQ', 'Pick closed-reference OTUs'])
1356+
1357+
# now let's try to generate again and it should fail cause the jobs
1358+
# are alrady created
1359+
with self.assertRaisesRegex(ValueError, "Cannot create job because "
1360+
"the parameters are the same as jobs"):
1361+
pt.add_default_workflow(qdb.user.User('test@foo.bar'))
1362+
13481363
# cleaning
13491364
qdb.artifact.Artifact.delete(artifact.id)
13501365
qdb.metadata_template.prep_template.PrepTemplate.delete(pt.id)

qiita_pet/handlers/api_proxy/studies.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -236,6 +236,9 @@ def study_prep_get_req(study_id, user_id):
236236
info['start_artifact'] = None
237237
info['start_artifact_id'] = None
238238
info['youngest_artifact'] = None
239+
info['num_artifact_children'] = 0
240+
info['youngest_artifact_name'] = None
241+
info['youngest_artifact_type'] = None
239242
info['ebi_experiment'] = 0
240243

241244
dtype_infos.append(info)

qiita_pet/handlers/api_proxy/tests/test_studies.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -238,6 +238,9 @@ def test_study_prep_get_req_failed_EBI(self):
238238
'start_artifact_id': None,
239239
'creation_timestamp': pt.creation_timestamp,
240240
'modification_timestamp': pt.modification_timestamp,
241+
'num_artifact_children': 0,
242+
'youngest_artifact_name': None,
243+
'youngest_artifact_type': None,
241244
'total_samples': 3}]
242245

243246
exp = {
@@ -577,6 +580,9 @@ def test_study_prep_get_req(self):
577580
'start_artifact_id': None,
578581
'start_artifact': None,
579582
'youngest_artifact': None,
583+
'num_artifact_children': 0,
584+
'youngest_artifact_name': None,
585+
'youngest_artifact_type': None,
580586
'ebi_experiment': 0}]
581587
exp = {'status': 'success',
582588
'message': '',

qiita_pet/handlers/study_handlers/__init__.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
DataTypesMenuAJAX, StudyFilesAJAX, StudyGetTags, StudyTags,
1717
Study)
1818
from .prep_template import (
19-
PrepTemplateAJAX, PrepFilesHandler,
19+
PrepTemplateAJAX, PrepFilesHandler, AddDefaultWorkflowHandler,
2020
NewPrepTemplateAjax, PrepTemplateSummaryAJAX)
2121
from .processing import (ListCommandsHandler, ListOptionsHandler,
2222
WorkflowHandler, WorkflowRunHandler, JobAJAX)
@@ -31,7 +31,7 @@
3131
'VAMPSHandler', 'ListStudiesAJAX', 'ArtifactGraphAJAX',
3232
'ArtifactAdminAJAX', 'StudyIndexHandler', 'StudyBaseInfoAJAX',
3333
'SampleTemplateHandler', 'SampleTemplateOverviewHandler',
34-
'SampleTemplateColumnsHandler',
34+
'SampleTemplateColumnsHandler', 'AddDefaultWorkflowHandler',
3535
'PrepTemplateAJAX', 'NewArtifactHandler', 'PrepFilesHandler',
3636
'ListCommandsHandler', 'ListOptionsHandler', 'SampleAJAX',
3737
'StudyDeleteAjax', 'NewPrepTemplateAjax',

qiita_pet/handlers/study_handlers/prep_template.py

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
from qiita_pet.handlers.base_handlers import BaseHandler
1616
from qiita_db.util import (get_files_from_uploads_folders, get_mountpoint,
1717
supported_filepath_types)
18+
from qiita_db.metadata_template.prep_template import PrepTemplate
1819
from qiita_pet.handlers.api_proxy import (
1920
prep_template_ajax_get_req, new_prep_template_get_req,
2021
prep_template_summary_get_req)
@@ -32,6 +33,22 @@ def get(self):
3233
study_id=study_id)
3334

3435

36+
class AddDefaultWorkflowHandler(BaseHandler):
37+
@authenticated
38+
def post(self):
39+
prep_id = self.get_argument('prep_id')
40+
msg_error = None
41+
data = None
42+
try:
43+
workflow = PrepTemplate(prep_id).add_default_workflow(
44+
self.current_user)
45+
data = workflow.id
46+
except Exception as error:
47+
msg_error = str(error)
48+
49+
self.write({'data': data, 'msg_error': msg_error})
50+
51+
3552
class PrepTemplateSummaryAJAX(BaseHandler):
3653
@authenticated
3754
def get(self):

qiita_pet/static/js/networkVue.js

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -110,7 +110,7 @@ Vue.component('processing-graph', {
110110
'</div>' +
111111
'</div>' +
112112
'</div>',
113-
props: ['portal', 'graph-endpoint', 'jobs-endpoint', 'no-init-jobs-callback', 'is-analysis-pipeline'],
113+
props: ['portal', 'graph-endpoint', 'jobs-endpoint', 'no-init-jobs-callback', 'is-analysis-pipeline', 'element-id'],
114114
methods: {
115115
/**
116116
*
@@ -997,6 +997,11 @@ Vue.component('processing-graph', {
997997
$("#processing-network-instructions-div").show();
998998
$("#show-hide-network-btn").show();
999999
$("#processing-job-div").hide();
1000+
if (vm.workflowId === null && vm.isAnalysisPipeline === false) {
1001+
$("#add-default-workflow").show();
1002+
} else {
1003+
$("#add-default-workflow").hide();
1004+
}
10001005
}
10011006
})
10021007
.fail(function(object, status, error_msg) {
@@ -1138,6 +1143,10 @@ Vue.component('processing-graph', {
11381143
'<tr>' +
11391144
'<td><small>Job status (circles):</small></td>' +
11401145
'<td>' + circle_statuses.join('') + '</td>' +
1146+
'<td rowspan="2" width="20px">&nbsp;</td>' +
1147+
'<td rowspan="2">' +
1148+
'<a class="btn btn-success form-control" id="add-default-workflow"><span class="glyphicon glyphicon-flash"></span> Add Default Workflow</a>' +
1149+
'</td>' +
11411150
'</tr>' +
11421151
'<tr>' +
11431152
'<td><small>Artifact status (triangles):</small>' +
@@ -1146,6 +1155,20 @@ Vue.component('processing-graph', {
11461155
'</table>';
11471156
$('#circle-explanation').html(full_text);
11481157

1158+
$('#add-default-workflow').on('click', function () {
1159+
$('#add-default-workflow').attr('disabled', true);
1160+
document.getElementById('add-default-workflow').innerHTML = 'Submitting!';
1161+
$.post(vm.portal + '/study/process/workflow/default/', {prep_id: vm.elementId}, function(data) {
1162+
if (data['msg_error'] !== null){
1163+
$('#add-default-workflow').attr('disabled', false);
1164+
bootstrapAlert('Error generating workflow: ' + data['msg_error'].replace("\n", "<br/>"));
1165+
} else {
1166+
vm.updateGraph();
1167+
}
1168+
});
1169+
document.getElementById('add-default-workflow').innerHTML = ' Add Default Workflow';
1170+
});
1171+
11491172
// This call to udpate graph will take care of updating the jobs
11501173
// if the graph is not available
11511174
vm.updateGraph();

qiita_pet/templates/analysis_description.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ <h2>
9191
<hr/>
9292
</div>
9393
<div id='analysis-graph-vue' style="margin-left: 15px">
94-
<processing-graph v-bind:is-analysis-pipeline='true' ref="procGraph" portal="{% raw qiita_config.portal_dir %}" graph-endpoint="/analysis/description/{{analysis_id}}/graph/" jobs-endpoint="/analysis/description/{{analysis_id}}/jobs/"></processing-graph>
94+
<processing-graph v-bind:is-analysis-pipeline='true' ref="procGraph" portal="{% raw qiita_config.portal_dir %}" graph-endpoint="/analysis/description/{{analysis_id}}/graph/" jobs-endpoint="/analysis/description/{{analysis_id}}/jobs/" element-id="{{analysis_id}}"></processing-graph>
9595
</div>
9696
<div class="row" id='processing-content-div'></div>
9797

qiita_pet/templates/study_ajax/data_type_menu.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,7 @@ <h4 class="panel-title">
4545
<span id="prep-header-{{prep['id']}}">
4646
{{prep['name']}} - ID: {{prep['id']}} - {{prep['status']}}
4747
</span><br/>
48-
Raw files {% if prep['start_artifact'] == 'FASTQ' %}<i class="fa fa-check green"></i>{% else %}<i class="fa fa-times red"></i>{% end %}, processed {% if prep['num_artifact_children'] > 0 %}<i class="fa fa-check green"></i> {% if prep['num_artifact_children'] > 1 %} <i class="fa fa-check green"></i>{% end%}{% else %}<i class="fa fa-times red"></i>{% end %}, BIOM {% if prep['youngest_artifact_type'] == 'BIOM' %}<i class="fa fa-check green"></i>{% else %}<i class="fa fa-times red"></i>{% end %}
48+
Raw files {% if prep['start_artifact'] in ('per_sample_FASTQ', 'FASTA', 'FASTQ') %}<i class="fa fa-check green"></i>{% else %}<i class="fa fa-times red"></i>{% end %}, processed {% if prep['num_artifact_children'] > 0 %}<i class="fa fa-check green"></i> {% if prep['num_artifact_children'] > 1 %} <i class="fa fa-check green"></i>{% end%}{% else %}<i class="fa fa-times red"></i>{% end %}, BIOM {% if prep['youngest_artifact_type'] == 'BIOM' %}<i class="fa fa-check green"></i>{% else %}<i class="fa fa-times red"></i>{% end %}
4949
<br />
5050
Created: {{prep['creation_timestamp'].strftime('%B %-d, %Y')}}, last updated: {{prep['modification_timestamp'].strftime('%B %-d, %Y')}}
5151
</a>

qiita_pet/templates/study_ajax/prep_summary.html

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -557,7 +557,7 @@ <h5>
557557
</div>
558558
</div>
559559
<div id="processing-graph-vue" class="tab-pane fade in active">
560-
<processing-graph ref="procGraph" v-bind:is-analysis-pipeline='false' v-bind:no-init-jobs-callback="load_new_artifact" portal="{% raw qiita_config.portal_dir %}" graph-endpoint="/prep_template/{{prep_id}}/graph/" jobs-endpoint="/prep_template/{{prep_id}}/jobs/"></processing-graph>
560+
<processing-graph ref="procGraph" v-bind:is-analysis-pipeline='false' v-bind:no-init-jobs-callback="load_new_artifact" portal="{% raw qiita_config.portal_dir %}" graph-endpoint="/prep_template/{{prep_id}}/graph/" jobs-endpoint="/prep_template/{{prep_id}}/jobs/" element-id="{{prep_id}}"></processing-graph>
561561
</div>
562562
</div>
563563
</div>

0 commit comments

Comments
 (0)