Skip to content

Commit 19cb270

Browse files
Merge pull request #3140 from antgonza/admin-jobs
WIP: adding admin direct job creation
2 parents d97a99c + 67d4ea8 commit 19cb270

File tree

12 files changed

+642
-16
lines changed

12 files changed

+642
-16
lines changed

qiita_db/exceptions.py

+2-2
Original file line numberDiff line numberDiff line change
@@ -49,14 +49,14 @@ class QiitaDBArtifactCreationError(QiitaDBError):
4949
"""Exception when creating an artifact"""
5050
def __init__(self, reason):
5151
super(QiitaDBArtifactCreationError, self).__init__()
52-
self.args = ("Cannot create artifact: %s" % reason,)
52+
self.args = (f"Cannot create artifact: {reason}",)
5353

5454

5555
class QiitaDBArtifactDeletionError(QiitaDBError):
5656
"""Exception when deleting an artifact"""
5757
def __init__(self, a_id, reason):
5858
super(QiitaDBArtifactDeletionError, self).__init__()
59-
self.args = ("Cannot delete artifact %d: %s" % (a_id, reason),)
59+
self.args = (f"Cannot delete artifact {a_id}: {reason}",)
6060

6161

6262
class QiitaDBDuplicateError(QiitaDBError):

qiita_db/processing_job.py

+12-4
Original file line numberDiff line numberDiff line change
@@ -1015,11 +1015,16 @@ def release(self):
10151015
# This job is resulting from a private job
10161016
parents = None
10171017
params = None
1018-
cmd_out_id = None
10191018
name = None
10201019
data_type = a_info['data_type']
1021-
analysis = qdb.analysis.Analysis(
1022-
job.parameters.values['analysis'])
1020+
pvals = job.parameters.values
1021+
if 'analysis' in pvals:
1022+
cmd_out_id = None
1023+
analysis = qdb.analysis.Analysis(
1024+
job.parameters.values['analysis'])
1025+
else:
1026+
cmd_out_id = provenance['cmd_out_id']
1027+
analysis = None
10231028
a_info = a_info['artifact_data']
10241029
else:
10251030
# This job is resulting from a plugin job
@@ -1264,7 +1269,7 @@ def _complete_artifact_transformation(self, artifacts_data):
12641269
"is allowed, found %d" % len(templates))
12651270
elif len(templates) == 1:
12661271
template = templates.pop()
1267-
else:
1272+
elif self.input_artifacts:
12681273
# In this case we have 0 templates. What this means is that
12691274
# this artifact is being generated in the analysis pipeline
12701275
# All the artifacts included in the analysis pipeline
@@ -1297,6 +1302,9 @@ def _complete_artifact_transformation(self, artifacts_data):
12971302
'cmd_out_id': cmd_out_id,
12981303
'name': art_name}
12991304

1305+
if self.command.software.type == 'private':
1306+
provenance['data_type'] = 'Job Output Folder'
1307+
13001308
# Get the validator command for the current artifact type and
13011309
# create a new job
13021310
# see also release_validators()
+86
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
# -----------------------------------------------------------------------------
2+
# Copyright (c) 2014--, The Qiita Development Team.
3+
#
4+
# Distributed under the terms of the BSD 3-clause License.
5+
#
6+
# The full license is in the file LICENSE, distributed with this software.
7+
# -----------------------------------------------------------------------------
8+
9+
from tornado.gen import coroutine
10+
from tornado.web import HTTPError
11+
12+
from .base_handlers import BaseHandler
13+
from qiita_core.util import execute_as_transaction
14+
15+
from qiita_db.software import Software
16+
17+
from json import dumps
18+
19+
20+
class AdminProcessingJobBaseClass(BaseHandler):
21+
def _check_access(self):
22+
if self.current_user.level not in {'admin', 'dev'}:
23+
raise HTTPError(403, reason="User %s doesn't have sufficient "
24+
"privileges to view error page" %
25+
self.current_user.email)
26+
27+
return self
28+
29+
def _get_private_software(self):
30+
# skipping the internal Qiita plugin and only selecting private
31+
# commands
32+
private_software = [s for s in Software.iter()
33+
if s.name != 'Qiita' and s.type == 'private']
34+
35+
return private_software
36+
37+
38+
class AdminProcessingJob(AdminProcessingJobBaseClass):
39+
@coroutine
40+
@execute_as_transaction
41+
def get(self):
42+
self._check_access()
43+
44+
self.render("admin_processing_job.html",
45+
private_software=self._get_private_software())
46+
47+
48+
class AJAXAdminProcessingJobListing(AdminProcessingJobBaseClass):
49+
@coroutine
50+
@execute_as_transaction
51+
def get(self):
52+
self._check_access()
53+
echo = self.get_argument('sEcho')
54+
command_id = int(self.get_argument('commandId'))
55+
56+
jobs = []
57+
for ps in self._get_private_software():
58+
for cmd in ps.commands:
59+
if cmd.id != command_id:
60+
continue
61+
62+
for job in cmd.processing_jobs:
63+
msg = '' if job.status != 'error' else job.log.msg
64+
msg = msg.replace('\n', '</br>')
65+
outputs = []
66+
if job.status == 'success':
67+
outputs = [[k, v.id] for k, v in job.outputs.items()]
68+
validator_jobs = [v.id for v in job.validator_jobs]
69+
70+
if job.heartbeat is not None:
71+
heartbeat = job.heartbeat.strftime('%Y-%m-%d %H:%M:%S')
72+
else:
73+
heartbeat = 'N/A'
74+
75+
jobs.append([job.id, job.command.name, job.status, msg,
76+
outputs, validator_jobs, heartbeat,
77+
job.parameters.values])
78+
results = {
79+
"sEcho": echo,
80+
"recordsTotal": len(jobs),
81+
"recordsFiltered": len(jobs),
82+
"data": jobs
83+
}
84+
85+
# return the json in compact form to save transmit size
86+
self.write(dumps(results, separators=(',', ':')))

qiita_pet/handlers/api_proxy/processing.py

+11-3
Original file line numberDiff line numberDiff line change
@@ -135,19 +135,27 @@ def workflow_handler_post_req(user_id, command_id, params):
135135
'message': str,
136136
'workflow_id': int}
137137
"""
138-
parameters = Parameters.load(Command(command_id), json_str=params)
139-
140138
status = 'success'
141139
message = ''
142140
try:
143-
wf = ProcessingWorkflow.from_scratch(User(user_id), parameters)
141+
parameters = Parameters.load(Command(command_id), json_str=params)
144142
except Exception as exc:
145143
wf = None
146144
wf_id = None
147145
job_info = None
148146
status = 'error'
149147
message = str(exc)
150148

149+
if status == 'success':
150+
try:
151+
wf = ProcessingWorkflow.from_scratch(User(user_id), parameters)
152+
except Exception as exc:
153+
wf = None
154+
wf_id = None
155+
job_info = None
156+
status = 'error'
157+
message = str(exc)
158+
151159
if wf is not None:
152160
# this is safe as we are creating the workflow for the first time
153161
# and there is only one node. Remember networkx doesn't assure order

qiita_pet/handlers/artifact_handlers/base_handlers.py

+5-2
Original file line numberDiff line numberDiff line change
@@ -126,14 +126,17 @@ def artifact_summary_get_request(user, artifact_id):
126126
# Check if the artifact is editable by the given user
127127
study = artifact.study
128128
analysis = artifact.analysis
129-
editable = study.can_edit(user) if study else analysis.can_edit(user)
129+
if artifact_type == 'job-output-folder':
130+
editable = False
131+
else:
132+
editable = study.can_edit(user) if study else analysis.can_edit(user)
130133

131134
buttons = []
132135
btn_base = (
133136
'<button onclick="if (confirm(\'Are you sure you want to %s '
134137
'artifact id: {0}?\')) {{ set_artifact_visibility(\'%s\', {0}) }}" '
135138
'class="btn btn-primary btn-sm">%s</button>').format(artifact_id)
136-
if not analysis:
139+
if not analysis and artifact_type != 'job-output-folder':
137140
# If the artifact is part of a study, the buttons shown depend in
138141
# multiple factors (see each if statement for an explanation of those)
139142
if qiita_config.require_approval:

qiita_pet/handlers/study_handlers/processing.py

+14-2
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
# The full license is in the file LICENSE, distributed with this software.
77
# -----------------------------------------------------------------------------
88
from tornado.web import authenticated
9+
from json import loads, dumps
910

1011
from qiita_pet.handlers.base_handlers import BaseHandler
1112
from qiita_pet.handlers.api_proxy import (
@@ -29,9 +30,9 @@ class ListOptionsHandler(BaseHandler):
2930
@authenticated
3031
def get(self):
3132
command_id = self.get_argument("command_id")
32-
artifact_id = self.get_argument("artifact_id")
33+
artifact_id = self.get_argument("artifact_id", None)
3334
# if the artifact id has ':' it means that it's a job in construction
34-
if ':' in artifact_id:
35+
if artifact_id is not None and ':' in artifact_id:
3536
artifact_id = None
3637
self.write(list_options_handler_get_req(command_id, artifact_id))
3738

@@ -48,6 +49,17 @@ class WorkflowHandler(BaseHandler):
4849
def post(self):
4950
command_id = self.get_argument('command_id')
5051
params = self.get_argument('params')
52+
53+
if self.request.files:
54+
parameters = loads(params)
55+
for k, v in self.request.files.items():
56+
# [0] there is only one file -- this block is needed because
57+
# 'body' is a byte and JSON doesn't know how to translate it
58+
parameters[k] = {'body': v[0]['body'].decode("utf-8"),
59+
'filename': v[0]['filename'],
60+
'content_type': v[0]['content_type']}
61+
params = dumps(parameters)
62+
5163
self.write(workflow_handler_post_req(
5264
self.current_user.id, command_id, params))
5365

qiita_pet/handlers/study_handlers/tests/test_processing.py

+47
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,12 @@ def test_get(self):
6464
self.assertEqual(response.code, 200)
6565
self.assertEqual(loads(response.body), exp)
6666

67+
# test that it works fine with no artifact_id
68+
response = self.get('/study/process/commands/options/',
69+
{'command_id': '3'})
70+
self.assertEqual(response.code, 200)
71+
self.assertEqual(loads(response.body), exp)
72+
6773

6874
class TestJobAJAX(TestHandlerBase):
6975
def test_get(self):
@@ -137,5 +143,46 @@ def test_patch(self):
137143
self.assertEqual(loads(response.body), exp)
138144

139145

146+
class TestWorkflowHandler(TestHandlerBase):
147+
def test_post(self):
148+
# test error
149+
response = self.post('/study/process/workflow/',
150+
{'command_id': '3', 'params': '{}'})
151+
self.assertEqual(response.code, 200)
152+
exp = {'status': 'error', 'workflow_id': None, 'job': None,
153+
'message': "The provided JSON string doesn't encode a parameter"
154+
" set for command 3. Missing required parameter: "
155+
"input_data"}
156+
self.assertDictEqual(loads(response.body), exp)
157+
158+
# test success
159+
response = self.post('/study/process/workflow/',
160+
{'command_id': '3',
161+
'params': '{"input_data": 1}'})
162+
self.assertEqual(response.code, 200)
163+
obs = loads(response.body)
164+
# we are going to copy the workflow_id/job information because we only
165+
# care about the reply
166+
exp = {'status': 'success', 'workflow_id': obs['workflow_id'],
167+
'job': obs['job'], 'message': ''}
168+
self.assertEqual(obs, exp)
169+
170+
# test with files
171+
response = self.post('/study/process/workflow/',
172+
{'command_id': '3', 'params': '{"input_data": 3}',
173+
'files': '{"template": {"body": b""}}',
174+
'headers': {
175+
'Content-Type': 'application/json',
176+
'Origin': 'localhost'
177+
}, })
178+
self.assertEqual(response.code, 200)
179+
obs = loads(response.body)
180+
# we are going to copy the workflow_id/job information because we only
181+
# care about the reply
182+
exp = {'status': 'success', 'workflow_id': obs['workflow_id'],
183+
'job': obs['job'], 'message': ''}
184+
self.assertEqual(obs, exp)
185+
186+
140187
if __name__ == "__main__":
141188
main()

0 commit comments

Comments
 (0)