From 005d948bb44f4adcc17b319814399661db589ea2 Mon Sep 17 00:00:00 2001 From: Antonio Gonzalez Date: Thu, 20 Nov 2025 07:50:57 -0700 Subject: [PATCH 1/6] allow multiple standalone steps in workflows --- qiita_pet/handlers/software.py | 20 +++++++++++++++++--- 1 file changed, 17 insertions(+), 3 deletions(-) diff --git a/qiita_pet/handlers/software.py b/qiita_pet/handlers/software.py index 54526a3d0..b7f0496be 100644 --- a/qiita_pet/handlers/software.py +++ b/qiita_pet/handlers/software.py @@ -158,12 +158,19 @@ def _default_parameters_parsing(node): # note that this block is similar but not identical to adding connected # nodes + standalone_input = None for i, (_, x) in enumerate(not_used_nodes.items()): vals_x, input_x, output_x = _default_parameters_parsing(x) - if at in input_x[0][1]: + if input_x and at in input_x[0][1]: input_x[0][1] = at - else: + elif input_x: input_x[0][1] = '** WARNING, NOT DEFINED **' + else: + # if we get to this point it means that these are "standalone" + # commands, thus is fine to link them to the same raw data + if standalone_input is None: + standalone_input = vals_x[0] + input_x = [['', at]] name_x = vals_x[0] if vals_x not in (nodes): @@ -173,7 +180,14 @@ def _default_parameters_parsing(node): name = inputs[b] else: name = 'input_%s_%s' % (name_x, b) - nodes.append([name, a, b]) + # if standalone_input == name_x then this is the first time + # we are adding an standalone command and we need to add the node + # (only once) and store the name of the node for future usage + if standalone_input == name_x: + nodes.append([name, a, b]) + standalone_input = name + else: + name = standalone_input edges.append([name, vals_x[0]]) for a, b in output_x: name = 'output_%s_%s' % (name_x, b) From 5e88735235e37bfcdf7d397becad37b16fb57c6d Mon Sep 17 00:00:00 2001 From: Antonio Gonzalez Date: Thu, 20 Nov 2025 08:56:55 -0700 Subject: [PATCH 2/6] flake8 --- qiita_pet/handlers/software.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/qiita_pet/handlers/software.py b/qiita_pet/handlers/software.py index b7f0496be..f352b7102 100644 --- a/qiita_pet/handlers/software.py +++ b/qiita_pet/handlers/software.py @@ -74,6 +74,7 @@ def _default_parameters_parsing(node): # for easy look up and merge of output_names main_nodes = dict() not_used_nodes = {n.id: n for n in graph.nodes} + standalone_input = None for i, (x, y) in enumerate(graph.edges): if x.id in not_used_nodes: del not_used_nodes[x.id] @@ -89,10 +90,16 @@ def _default_parameters_parsing(node): if i == 0: # we are in the first element so we can specifically select # the type we are looking for - if at in input_x[0][1]: + if input_x and at in input_x[0][1]: input_x[0][1] = at - else: + elif input_x: input_x[0][1] = '** WARNING, NOT DEFINED **' + else: + # if we get to this point it means that the workflow has a + # multiple commands starting from the main single input, + # thus is fine to link them to the same raw data + standalone_input = vals_x[0] + input_x = [['', at]] name_x = vals_x[0] name_y = vals_y[0] @@ -106,6 +113,8 @@ def _default_parameters_parsing(node): name = inputs[b] else: name = 'input_%s_%s' % (name_x, b) + if standalone_input is not None: + standalone_input = name vals = [name, a, b] if vals not in nodes: inputs[b] = name @@ -152,13 +161,12 @@ def _default_parameters_parsing(node): # adding nodes without edges # as a first step if not_used_nodes is not empty we'll confirm that # nodes/edges are empty; in theory we should never hit this - if not_used_nodes and (nodes or edges): + if not_used_nodes and (nodes or edges) and standalone_input is None: raise ValueError( 'Error, please check your workflow configuration') # note that this block is similar but not identical to adding connected # nodes - standalone_input = None for i, (_, x) in enumerate(not_used_nodes.items()): vals_x, input_x, output_x = _default_parameters_parsing(x) if input_x and at in input_x[0][1]: @@ -181,8 +189,8 @@ def _default_parameters_parsing(node): else: name = 'input_%s_%s' % (name_x, b) # if standalone_input == name_x then this is the first time - # we are adding an standalone command and we need to add the node - # (only once) and store the name of the node for future usage + # we are processing a standalone command so we need to add + # the node and store the name of the node for future usage if standalone_input == name_x: nodes.append([name, a, b]) standalone_input = name From d13be53fd4e4d787d4a4ae4934fe8a7eff6cda9f Mon Sep 17 00:00:00 2001 From: Antonio Gonzalez Date: Thu, 20 Nov 2025 10:02:18 -0700 Subject: [PATCH 3/6] adding a test --- qiita_pet/test/test_software.py | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/qiita_pet/test/test_software.py b/qiita_pet/test/test_software.py index c9baecd67..1cf6f1467 100644 --- a/qiita_pet/test/test_software.py +++ b/qiita_pet/test/test_software.py @@ -57,6 +57,37 @@ def test_get(self): self.assertIn('FASTA upstream workflow', body) DefaultWorkflow(2).active = True + def test_retrive_workflows_standalone(self): + # let's create a new workflow, add 2 commands, and make parameters not + # required: two standalone commands + with TRN: + # 5 per_sample_FASTQ + sql = """INSERT INTO qiita.default_workflow + (name, artifact_type_id, description, parameters) + VALUES ('', 5, '', '{"prep": {}, "sample": {}}') + RETURNING default_workflow_id""" + TRN.add(sql) + wid = TRN.execute_fetchlast() + # 11 & 12 are per-sample-FASTQ split libraries commands + sql = """INSERT INTO qiita.default_workflow_node + (default_workflow_id, default_parameter_set_id) + VALUES (%s, 11), (%s, 12) + RETURNING default_workflow_node_id""" + TRN.add(sql, [wid, wid]) + nid = TRN.execute_fetchflatten() + sql = """UPDATE qiita.command_parameter SET required = false""" + TRN.add(sql) + TRN.execute() + + obs = _retrive_workflows(True)[-1] + exp_value = f'input_params_{nid[0]}_per_sample_FASTQ' + # there should be a single "input" node + self.assertEqual(1, len( + [x for x in obs['nodes'] if x[0] == exp_value])) + # and 2 edges + self.assertEqual(2, len( + [x for x in obs['edges'] if x[0] == exp_value])) + def test_retrive_workflows(self): # we should see all 3 workflows DefaultWorkflow(2).active = False From 0f27dda4140510d845b68009fe3f6a2bfda493f6 Mon Sep 17 00:00:00 2001 From: Antonio Gonzalez Date: Thu, 20 Nov 2025 11:02:47 -0700 Subject: [PATCH 4/6] 2 commands from start & extra commands --- qiita_pet/handlers/software.py | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/qiita_pet/handlers/software.py b/qiita_pet/handlers/software.py index f352b7102..673c23000 100644 --- a/qiita_pet/handlers/software.py +++ b/qiita_pet/handlers/software.py @@ -158,12 +158,10 @@ def _default_parameters_parsing(node): wparams = w.parameters - # adding nodes without edges - # as a first step if not_used_nodes is not empty we'll confirm that - # nodes/edges are empty; in theory we should never hit this + # This case happens when a workflow has 2 commands from the initial + # artifact and one of them has more processing after if not_used_nodes and (nodes or edges) and standalone_input is None: - raise ValueError( - 'Error, please check your workflow configuration') + standalone_input = edges[0][0] # note that this block is similar but not identical to adding connected # nodes From d3d89c7b0f0cc90ea2da1e1c78b00508e4b8e3f1 Mon Sep 17 00:00:00 2001 From: Antonio Gonzalez Date: Thu, 20 Nov 2025 11:55:22 -0700 Subject: [PATCH 5/6] more tests --- qiita_pet/handlers/software.py | 4 +++- qiita_pet/test/test_software.py | 41 ++++++++++++++++++++++----------- 2 files changed, 31 insertions(+), 14 deletions(-) diff --git a/qiita_pet/handlers/software.py b/qiita_pet/handlers/software.py index 673c23000..67030c9e0 100644 --- a/qiita_pet/handlers/software.py +++ b/qiita_pet/handlers/software.py @@ -189,7 +189,9 @@ def _default_parameters_parsing(node): # if standalone_input == name_x then this is the first time # we are processing a standalone command so we need to add # the node and store the name of the node for future usage - if standalone_input == name_x: + if standalone_input is None: + nodes.append([name, a, b]) + elif standalone_input == name_x: nodes.append([name, a, b]) standalone_input = name else: diff --git a/qiita_pet/test/test_software.py b/qiita_pet/test/test_software.py index 1cf6f1467..cc3fd01f3 100644 --- a/qiita_pet/test/test_software.py +++ b/qiita_pet/test/test_software.py @@ -58,8 +58,8 @@ def test_get(self): DefaultWorkflow(2).active = True def test_retrive_workflows_standalone(self): - # let's create a new workflow, add 2 commands, and make parameters not - # required: two standalone commands + # let's create a new workflow, add 1 commands, and make parameters not + # required to make sure the stanalone is "active" with TRN: # 5 per_sample_FASTQ sql = """INSERT INTO qiita.default_workflow @@ -68,25 +68,40 @@ def test_retrive_workflows_standalone(self): RETURNING default_workflow_id""" TRN.add(sql) wid = TRN.execute_fetchlast() - # 11 & 12 are per-sample-FASTQ split libraries commands + # 11 is per-sample-FASTQ split libraries commands sql = """INSERT INTO qiita.default_workflow_node (default_workflow_id, default_parameter_set_id) - VALUES (%s, 11), (%s, 12) + VALUES (%s, 11) RETURNING default_workflow_node_id""" - TRN.add(sql, [wid, wid]) + TRN.add(sql, [wid]) nid = TRN.execute_fetchflatten() sql = """UPDATE qiita.command_parameter SET required = false""" TRN.add(sql) TRN.execute() - obs = _retrive_workflows(True)[-1] - exp_value = f'input_params_{nid[0]}_per_sample_FASTQ' - # there should be a single "input" node - self.assertEqual(1, len( - [x for x in obs['nodes'] if x[0] == exp_value])) - # and 2 edges - self.assertEqual(2, len( - [x for x in obs['edges'] if x[0] == exp_value])) + # here we expect 1 input node and 1 edge + obs = _retrive_workflows(True)[-1] + exp_value = f'input_params_{nid[0]}_per_sample_FASTQ' + self.assertEqual(1, len( + [x for x in obs['nodes'] if x[0] == exp_value])) + self.assertEqual(1, len( + [x for x in obs['edges'] if x[0] == exp_value])) + + # now let's insert another command using the same input + with TRN: + # 12 is per-sample-FASTQ split libraries commands + sql = """INSERT INTO qiita.default_workflow_node + (default_workflow_id, default_parameter_set_id) + VALUES (%s, 12)""" + TRN.add(sql, [wid]) + TRN.execute() + + # we should still have 1 node but now with 2 edges + obs = _retrive_workflows(True)[-1] + self.assertEqual(1, len( + [x for x in obs['nodes'] if x[0] == exp_value])) + self.assertEqual(2, len( + [x for x in obs['edges'] if x[0] == exp_value])) def test_retrive_workflows(self): # we should see all 3 workflows From 3d620b3bdce9994c58e15881f524e555fc065a13 Mon Sep 17 00:00:00 2001 From: Antonio Gonzalez Date: Thu, 20 Nov 2025 12:49:35 -0700 Subject: [PATCH 6/6] artifact.parents --- qiita_pet/handlers/artifact_handlers/base_handlers.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/qiita_pet/handlers/artifact_handlers/base_handlers.py b/qiita_pet/handlers/artifact_handlers/base_handlers.py index a1f5f9463..f57a4c6b7 100644 --- a/qiita_pet/handlers/artifact_handlers/base_handlers.py +++ b/qiita_pet/handlers/artifact_handlers/base_handlers.py @@ -134,7 +134,9 @@ def artifact_summary_get_request(user, artifact_id): # Check if the artifact is editable by the given user study = artifact.study analysis = artifact.analysis - if artifact_type == 'job-output-folder': + # if is a folder and has no parents, it means that is an SPP job and + # nobody should be able to change anything about it + if artifact_type == 'job-output-folder' and not artifact.parents: editable = False else: editable = study.can_edit(user) if study else analysis.can_edit(user)