-
Notifications
You must be signed in to change notification settings - Fork 80
Analysis refactor GUI part1 #2076
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
3601c29
0d6788e
12406cc
958fcbe
a57ef23
2ead7a6
73a78e7
e64a22a
0dcae8b
4a5bbbc
f99975c
ed899a8
d508320
025cc1e
599bcde
d12ccfe
b33983b
b4f1b1f
62a1b93
2e36141
e006e20
c174693
131dd6a
ccb55bd
dfe2e83
15fcceb
7f97f2a
9eb9dbb
23104d7
1f1e826
19a9dda
19889f9
4e380e0
6f0dd71
ed9fc65
4b19b45
d9b41e8
5ef06ae
5e3504a
d10096a
661342f
fcd249b
f3c1216
a91a6fd
7b9fa6f
33bcbe5
5e4bd9b
8bf3d6e
7807bac
6360675
811b7a7
751d4ad
65a86df
b1817dd
18d77e1
01c656c
53188a6
1ab4e3b
1e8332e
cb67d3d
5a5127d
0033480
067f14f
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
# ----------------------------------------------------------------------------- | ||
# Copyright (c) 2014--, The Qiita Development Team. | ||
# | ||
# Distributed under the terms of the BSD 3-clause License. | ||
# | ||
# The full license is in the file LICENSE, distributed with this software. | ||
# ----------------------------------------------------------------------------- | ||
|
||
from .util import check_analysis_access | ||
from .base_handlers import (CreateAnalysisHandler, AnalysisDescriptionHandler, | ||
AnalysisGraphHandler) | ||
from .listing_handlers import (ListAnalysesHandler, AnalysisSummaryAJAX, | ||
SelectedSamplesHandler) | ||
|
||
__all__ = ['CreateAnalysisHandler', 'AnalysisDescriptionHandler', | ||
'AnalysisGraphHandler', 'ListAnalysesHandler', | ||
'AnalysisSummaryAJAX', 'SelectedSamplesHandler', | ||
'check_analysis_access'] |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,105 @@ | ||
# ----------------------------------------------------------------------------- | ||
# Copyright (c) 2014--, The Qiita Development Team. | ||
# | ||
# Distributed under the terms of the BSD 3-clause License. | ||
# | ||
# The full license is in the file LICENSE, distributed with this software. | ||
# ----------------------------------------------------------------------------- | ||
|
||
from tornado.web import authenticated | ||
|
||
from qiita_core.util import execute_as_transaction | ||
from qiita_core.qiita_settings import qiita_config | ||
from qiita_pet.handlers.base_handlers import BaseHandler | ||
from qiita_pet.handlers.analysis_handlers import check_analysis_access | ||
from qiita_pet.handlers.util import to_int | ||
from qiita_db.analysis import Analysis | ||
|
||
|
||
class CreateAnalysisHandler(BaseHandler): | ||
@authenticated | ||
@execute_as_transaction | ||
def post(self): | ||
name = self.get_argument('name') | ||
desc = self.get_argument('description') | ||
analysis = Analysis.create(self.current_user, name, desc, | ||
from_default=True) | ||
|
||
self.redirect(u"%s/analysis/description/%s/" | ||
% (qiita_config.portal_dir, analysis.id)) | ||
|
||
|
||
class AnalysisDescriptionHandler(BaseHandler): | ||
@authenticated | ||
@execute_as_transaction | ||
def get(self, analysis_id): | ||
analysis = Analysis(analysis_id) | ||
check_analysis_access(self.current_user, analysis) | ||
|
||
self.render("analysis_description.html", analysis_name=analysis.name, | ||
analysis_id=analysis_id, | ||
analysis_description=analysis.description) | ||
|
||
|
||
def analyisis_graph_handler_get_request(analysis_id, user): | ||
"""Returns the graph information of the analysis | ||
|
||
Parameters | ||
---------- | ||
analysis_id : int | ||
The analysis id | ||
user : qiita_db.user.User | ||
The user performing the request | ||
|
||
Returns | ||
------- | ||
dict with the graph information | ||
""" | ||
analysis = Analysis(analysis_id) | ||
# Check if the user actually has access to the analysis | ||
check_analysis_access(user, analysis) | ||
|
||
# A user has full access to the analysis if it is one of its private | ||
# analyses, the analysis has been shared with the user or the user is a | ||
# superuser or admin | ||
full_access = (analysis in (user.private_analyses | user.shared_analyses) | ||
or user.level in {'superuser', 'admin'}) | ||
|
||
nodes = set() | ||
edges = set() | ||
# Loop through all the initial artifacts of the analysis | ||
for a in analysis.artifacts: | ||
g = a.descendants_with_jobs | ||
# Loop through all the nodes in artifact descendants graph | ||
for n in g.nodes(): | ||
# Get if the object is an artifact or a job | ||
obj_type = n[0] | ||
# Get the actual object | ||
obj = n[1] | ||
if obj_type == 'job': | ||
name = obj.command.name | ||
elif not full_access and not obj.visibility == 'public': | ||
# The object is an artifact, it is not public and the user | ||
# doesn't have full access, so we don't include it in the | ||
# graph | ||
continue | ||
else: | ||
name = '%s - %s' % (obj.name, obj.artifact_type) | ||
nodes.add((obj_type, obj.id, name)) | ||
|
||
edges.update({(s[1].id, t[1].id) for s, t in g.edges()}) | ||
|
||
# Nodes and Edges are sets, but the set object can't be serialized using | ||
# JSON. Transforming them to lists so when this is returned to the GUI | ||
# over HTTP can be JSONized. | ||
return {'edges': list(edges), 'nodes': list(nodes)} | ||
|
||
|
||
class AnalysisGraphHandler(BaseHandler): | ||
@authenticated | ||
@execute_as_transaction | ||
def get(self): | ||
analysis_id = to_int(self.get_argument('analysis_id')) | ||
response = analyisis_graph_handler_get_request( | ||
analysis_id, self.current_user) | ||
self.write(response) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,135 @@ | ||
# ----------------------------------------------------------------------------- | ||
# Copyright (c) 2014--, The Qiita Development Team. | ||
# | ||
# Distributed under the terms of the BSD 3-clause License. | ||
# | ||
# The full license is in the file LICENSE, distributed with this software. | ||
# ----------------------------------------------------------------------------- | ||
|
||
from functools import partial | ||
from json import dumps | ||
from collections import defaultdict | ||
from future.utils import viewitems | ||
|
||
from tornado.web import authenticated | ||
|
||
from qiita_core.qiita_settings import qiita_config | ||
from qiita_core.util import execute_as_transaction | ||
from qiita_pet.handlers.base_handlers import BaseHandler | ||
from qiita_pet.handlers.util import download_link_or_path | ||
from qiita_pet.handlers.analysis_handlers import check_analysis_access | ||
from qiita_pet.util import is_localhost | ||
from qiita_db.util import get_filepath_id | ||
from qiita_db.analysis import Analysis | ||
from qiita_db.logger import LogEntry | ||
from qiita_db.reference import Reference | ||
from qiita_db.artifact import Artifact | ||
|
||
|
||
class ListAnalysesHandler(BaseHandler): | ||
@authenticated | ||
@execute_as_transaction | ||
def get(self): | ||
message = self.get_argument('message', '') | ||
level = self.get_argument('level', '') | ||
user = self.current_user | ||
|
||
analyses = user.shared_analyses | user.private_analyses | ||
|
||
is_local_request = is_localhost(self.request.headers['host']) | ||
gfi = partial(get_filepath_id, 'analysis') | ||
dlop = partial(download_link_or_path, is_local_request) | ||
mappings = {} | ||
bioms = {} | ||
tgzs = {} | ||
for analysis in analyses: | ||
_id = analysis.id | ||
# getting mapping file | ||
mapping = analysis.mapping_file | ||
if mapping is not None: | ||
mappings[_id] = dlop(mapping, gfi(mapping), 'mapping file') | ||
else: | ||
mappings[_id] = '' | ||
|
||
bioms[_id] = '' | ||
# getting tgz file | ||
tgz = analysis.tgz | ||
if tgz is not None: | ||
tgzs[_id] = dlop(tgz, gfi(tgz), 'tgz file') | ||
else: | ||
tgzs[_id] = '' | ||
|
||
self.render("list_analyses.html", analyses=analyses, message=message, | ||
level=level, is_local_request=is_local_request, | ||
mappings=mappings, bioms=bioms, tgzs=tgzs) | ||
|
||
@authenticated | ||
@execute_as_transaction | ||
def post(self): | ||
analysis_id = int(self.get_argument('analysis_id')) | ||
analysis = Analysis(analysis_id) | ||
analysis_name = analysis.name.decode('utf-8') | ||
|
||
check_analysis_access(self.current_user, analysis) | ||
|
||
try: | ||
Analysis.delete(analysis_id) | ||
msg = ("Analysis <b><i>%s</i></b> has been deleted." % ( | ||
analysis_name)) | ||
level = "success" | ||
except Exception as e: | ||
e = str(e) | ||
msg = ("Couldn't remove <b><i>%s</i></b> analysis: %s" % ( | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Is it possible to add a reason of why? If too difficult, that's fine. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The last part of the message includes the |
||
analysis_name, e)) | ||
level = "danger" | ||
LogEntry.create('Runtime', "Couldn't remove analysis ID %d: %s" % | ||
(analysis_id, e)) | ||
|
||
self.redirect(u"%s/analysis/list/?level=%s&message=%s" | ||
% (qiita_config.portal_dir, level, msg)) | ||
|
||
|
||
class AnalysisSummaryAJAX(BaseHandler): | ||
@authenticated | ||
@execute_as_transaction | ||
def get(self): | ||
info = self.current_user.default_analysis.summary_data() | ||
self.write(dumps(info)) | ||
|
||
|
||
class SelectedSamplesHandler(BaseHandler): | ||
@authenticated | ||
@execute_as_transaction | ||
def get(self): | ||
# Format sel_data to get study IDs for the processed data | ||
sel_data = defaultdict(dict) | ||
proc_data_info = {} | ||
sel_samps = self.current_user.default_analysis.samples | ||
for aid, samples in viewitems(sel_samps): | ||
a = Artifact(aid) | ||
sel_data[a.study][aid] = samples | ||
# Also get processed data info | ||
processing_parameters = a.processing_parameters | ||
if processing_parameters is None: | ||
params = None | ||
algorithm = None | ||
else: | ||
cmd = processing_parameters.command | ||
params = processing_parameters.values | ||
if 'reference' in params: | ||
ref = Reference(params['reference']) | ||
del params['reference'] | ||
|
||
params['reference_name'] = ref.name | ||
params['reference_version'] = ref.version | ||
algorithm = '%s (%s)' % (cmd.software.name, cmd.name) | ||
|
||
proc_data_info[aid] = { | ||
'processed_date': str(a.timestamp), | ||
'algorithm': algorithm, | ||
'data_type': a.data_type, | ||
'params': params | ||
} | ||
|
||
self.render("analysis_selected.html", sel_data=sel_data, | ||
proc_info=proc_data_info) |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
# ----------------------------------------------------------------------------- | ||
# Copyright (c) 2014--, The Qiita Development Team. | ||
# | ||
# Distributed under the terms of the BSD 3-clause License. | ||
# | ||
# The full license is in the file LICENSE, distributed with this software. | ||
# ----------------------------------------------------------------------------- |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,83 @@ | ||
# ----------------------------------------------------------------------------- | ||
# Copyright (c) 2014--, The Qiita Development Team. | ||
# | ||
# Distributed under the terms of the BSD 3-clause License. | ||
# | ||
# The full license is in the file LICENSE, distributed with this software. | ||
# ----------------------------------------------------------------------------- | ||
|
||
from unittest import TestCase, main | ||
from json import loads | ||
|
||
from tornado.web import HTTPError | ||
|
||
from qiita_db.user import User | ||
from qiita_db.analysis import Analysis | ||
from qiita_pet.test.tornado_test_base import TestHandlerBase | ||
from qiita_pet.handlers.analysis_handlers.base_handlers import ( | ||
analyisis_graph_handler_get_request) | ||
|
||
|
||
class TestBaseHandlersUtils(TestCase): | ||
def test_analyisis_graph_handler_get_request(self): | ||
obs = analyisis_graph_handler_get_request(1, User('test@foo.bar')) | ||
# The job id is randomly generated in the test environment. Gather | ||
# it here. There is only 1 job in the first artifact of the analysis | ||
job_id = Analysis(1).artifacts[0].jobs()[0].id | ||
exp = {'edges': [(8, job_id), (job_id, 9)], | ||
'nodes': [('job', job_id, 'Single Rarefaction'), | ||
('artifact', 9, 'noname - BIOM'), | ||
('artifact', 8, 'noname - BIOM')]} | ||
self.assertItemsEqual(obs, exp) | ||
self.assertItemsEqual(obs['edges'], exp['edges']) | ||
self.assertItemsEqual(obs['nodes'], exp['nodes']) | ||
|
||
# An admin has full access to the analysis | ||
obs = analyisis_graph_handler_get_request(1, User('admin@foo.bar')) | ||
self.assertItemsEqual(obs, exp) | ||
self.assertItemsEqual(obs['edges'], exp['edges']) | ||
self.assertItemsEqual(obs['nodes'], exp['nodes']) | ||
|
||
# If the analysis is shared with the user he also has access | ||
obs = analyisis_graph_handler_get_request(1, User('shared@foo.bar')) | ||
self.assertItemsEqual(obs, exp) | ||
self.assertItemsEqual(obs['edges'], exp['edges']) | ||
self.assertItemsEqual(obs['nodes'], exp['nodes']) | ||
|
||
# The user doesn't have access to the analysis | ||
with self.assertRaises(HTTPError): | ||
analyisis_graph_handler_get_request(1, User('demo@microbio.me')) | ||
|
||
|
||
class TestBaseHandlers(TestHandlerBase): | ||
def test_post_create_analysis_handler(self): | ||
args = {'name': 'New Test Analysis', | ||
'description': 'Test Analysis Description'} | ||
response = self.post('/analysis/create/', args) | ||
self.assertRegexpMatches( | ||
response.effective_url, | ||
r"http://localhost:\d+/analysis/description/\d+/") | ||
self.assertEqual(response.code, 200) | ||
|
||
def test_get_analysis_description_handler(self): | ||
response = self.get('/analysis/description/1/') | ||
self.assertEqual(response.code, 200) | ||
|
||
def test_get_analysis_graph_handler(self): | ||
response = self.get('/analysis/description/graph/', {'analysis_id': 1}) | ||
self.assertEqual(response.code, 200) | ||
# The job id is randomly generated in the test environment. Gather | ||
# it here. There is only 1 job in the first artifact of the analysis | ||
job_id = Analysis(1).artifacts[0].jobs()[0].id | ||
obs = loads(response.body) | ||
exp = {'edges': [[8, job_id], [job_id, 9]], | ||
'nodes': [['job', job_id, 'Single Rarefaction'], | ||
['artifact', 9, 'noname - BIOM'], | ||
['artifact', 8, 'noname - BIOM']]} | ||
self.assertItemsEqual(obs, exp) | ||
self.assertItemsEqual(obs['edges'], exp['edges']) | ||
self.assertItemsEqual(obs['nodes'], exp['nodes']) | ||
|
||
|
||
if __name__ == '__main__': | ||
main() |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,32 @@ | ||
# ----------------------------------------------------------------------------- | ||
# Copyright (c) 2014--, The Qiita Development Team. | ||
# | ||
# Distributed under the terms of the BSD 3-clause License. | ||
# | ||
# The full license is in the file LICENSE, distributed with this software. | ||
# ----------------------------------------------------------------------------- | ||
|
||
from unittest import main | ||
from json import loads | ||
|
||
from qiita_pet.test.tornado_test_base import TestHandlerBase | ||
|
||
|
||
class TestListingHandlers(TestHandlerBase): | ||
def test_get_list_analyses_handler(self): | ||
response = self.get('/analysis/list/') | ||
self.assertEqual(response.code, 200) | ||
|
||
def test_get_analysis_summary_ajax(self): | ||
response = self.get('/analysis/dflt/sumary/') | ||
self.assertEqual(response.code, 200) | ||
self.assertEqual(loads(response.body), | ||
{"artifacts": 1, "studies": 1, "samples": 4}) | ||
|
||
def test_get_selected_samples_handler(self): | ||
response = self.get('/analysis/selected/') | ||
# Make sure page response loaded sucessfully | ||
self.assertEqual(response.code, 200) | ||
|
||
if __name__ == '__main__': | ||
main() |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Does this make a desc required or do we need to put a default value?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
No it's not required (same behavior as before). If the user doesn't introduce anything, the description will be blank (empty string).