diff --git a/qiita_pet/handlers/analysis_handlers/__init__.py b/qiita_pet/handlers/analysis_handlers/__init__.py new file mode 100644 index 000000000..366c5bb23 --- /dev/null +++ b/qiita_pet/handlers/analysis_handlers/__init__.py @@ -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'] diff --git a/qiita_pet/handlers/analysis_handlers/base_handlers.py b/qiita_pet/handlers/analysis_handlers/base_handlers.py new file mode 100644 index 000000000..bc5de4568 --- /dev/null +++ b/qiita_pet/handlers/analysis_handlers/base_handlers.py @@ -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) diff --git a/qiita_pet/handlers/analysis_handlers/listing_handlers.py b/qiita_pet/handlers/analysis_handlers/listing_handlers.py new file mode 100644 index 000000000..fde0a6237 --- /dev/null +++ b/qiita_pet/handlers/analysis_handlers/listing_handlers.py @@ -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 %s has been deleted." % ( + analysis_name)) + level = "success" + except Exception as e: + e = str(e) + msg = ("Couldn't remove %s analysis: %s" % ( + 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) diff --git a/qiita_pet/handlers/analysis_handlers/tests/__init__.py b/qiita_pet/handlers/analysis_handlers/tests/__init__.py new file mode 100644 index 000000000..e0aff71d9 --- /dev/null +++ b/qiita_pet/handlers/analysis_handlers/tests/__init__.py @@ -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. +# ----------------------------------------------------------------------------- diff --git a/qiita_pet/handlers/analysis_handlers/tests/test_base_handlers.py b/qiita_pet/handlers/analysis_handlers/tests/test_base_handlers.py new file mode 100644 index 000000000..485174878 --- /dev/null +++ b/qiita_pet/handlers/analysis_handlers/tests/test_base_handlers.py @@ -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() diff --git a/qiita_pet/handlers/analysis_handlers/tests/test_listing_handlers.py b/qiita_pet/handlers/analysis_handlers/tests/test_listing_handlers.py new file mode 100644 index 000000000..f4e5742b5 --- /dev/null +++ b/qiita_pet/handlers/analysis_handlers/tests/test_listing_handlers.py @@ -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() diff --git a/qiita_pet/handlers/analysis_handlers/tests/test_util.py b/qiita_pet/handlers/analysis_handlers/tests/test_util.py new file mode 100644 index 000000000..93d5016ee --- /dev/null +++ b/qiita_pet/handlers/analysis_handlers/tests/test_util.py @@ -0,0 +1,36 @@ +# ----------------------------------------------------------------------------- +# 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, TestCase + +from tornado.web import HTTPError + +from qiita_db.user import User +from qiita_db.analysis import Analysis +from qiita_pet.handlers.analysis_handlers import check_analysis_access + + +class UtilTests(TestCase): + def test_check_analysis_access(self): + # Has access, so it allows execution + u = User('test@foo.bar') + a = Analysis(1) + check_analysis_access(u, a) + + # Admin has access to everything + u = User('admin@foo.bar') + check_analysis_access(u, a) + + # Raises an error because it doesn't have access + u = User('demo@microbio.me') + with self.assertRaises(HTTPError): + check_analysis_access(u, a) + + +if __name__ == '__main__': + main() diff --git a/qiita_pet/handlers/analysis_handlers/util.py b/qiita_pet/handlers/analysis_handlers/util.py new file mode 100644 index 000000000..37417968d --- /dev/null +++ b/qiita_pet/handlers/analysis_handlers/util.py @@ -0,0 +1,28 @@ +# ----------------------------------------------------------------------------- +# 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 HTTPError + + +def check_analysis_access(user, analysis): + """Checks whether user has access to an analysis + + Parameters + ---------- + user : User object + User to check + analysis : Analysis object + Analysis to check access for + + Raises + ------ + RuntimeError + Tried to access analysis that user does not have access to + """ + if not analysis.has_access(user): + raise HTTPError(403, "Analysis access denied to %s" % (analysis.id)) diff --git a/qiita_pet/static/js/qiita.js b/qiita_pet/static/js/qiita.js index f9d555fc2..2847f6506 100644 --- a/qiita_pet/static/js/qiita.js +++ b/qiita_pet/static/js/qiita.js @@ -108,3 +108,104 @@ function show_hide_process_list() { $("#qiita-processing").hide(); } } + +/** + * Draw the artifact + jobs processing graph + * + * Draws a vis.Network graph in the given target div with the network + * information stored in nodes and and edges + * + * @param nodes: list of {id: str, label: str, group: {'artifact', 'job'}} + * The node information. Id is the unique id of the node (artifact or job), + * label is the name to show under the node and group is the type of node + * @param edges: list of {from: str, to: str, arrows: 'to'} + * The connectivity information in the graph. from and to are the nodes of + * origin and destination of the edge, respectivelly. + * @param target: str. The id of the target div to draw the graph + * @param artifactFunc: function. The function to execute when the user + * clicks on a node of group 'artifact'. It should accept only 1 parameter + * which is the artifact (node) id + * @param jobFunc: function. The function to execute when the user clicks on + * a node of group 'job'. It should accept only 1 parameter which is the + * job (node) id + * + */ +function draw_processing_graph(nodes, edges, target, artifactFunc, jobFunc) { + var container = document.getElementById(target); + container.innerHTML = ""; + + var nodes = new vis.DataSet(nodes); + var edges = new vis.DataSet(edges); + var data = { + nodes: nodes, + edges: edges + }; + var options = { + nodes: { + shape: 'dot', + font: { + size: 16, + color: '#000000' + }, + size: 13, + borderWidth: 2, + }, + edges: { + color: 'grey' + }, + layout: { + hierarchical: { + direction: "LR", + sortMethod: "directed", + levelSeparation: 260 + } + }, + interaction: { + dragNodes: false, + dragView: true, + zoomView: true, + selectConnectedEdges: true, + navigationButtons: true, + keyboard: true + }, + groups: { + jobs: { + color: '#FF9152' + }, + artifact: { + color: '#FFFFFF' + } + } + }; + + var network = new vis.Network(container, data, options); + network.on("click", function (properties) { + var ids = properties.nodes; + if (ids.length == 0) { + return + } + // [0] cause only users can only select 1 node + var clickedNode = nodes.get(ids)[0]; + var element_id = ids[0]; + if (clickedNode.group == 'artifact') { + artifactFunc(element_id); + } else { + jobFunc(element_id); + } + }); +}; + +/** + * + * Function to show the loading gif in a given div + * + * @param portal_dir: string. The portal that qiita is running under + * @param target: string. The id of the div to populate with the loading gif + * + * This function replaces the content of the given div with the + * gif to show that the section of page is loading + * + */ +function show_loading(portal_dir, target) { + $("#" + target).html(""); +} diff --git a/qiita_pet/templates/analysis_description.html b/qiita_pet/templates/analysis_description.html new file mode 100644 index 000000000..8d850248a --- /dev/null +++ b/qiita_pet/templates/analysis_description.html @@ -0,0 +1,143 @@ +{% extends sitebase.html %} +{% block head %} + + +{% end %} +{% block content %} + +
+
+

{{analysis_name}} - ID {{analysis_id}}

+

{{analysis_description}}

+
+
+
+
+

- Processing network

+ (Click nodes for more information, blue are jobs) +
+
+
+
+
+
+
+
+
+
+ +{% end %} diff --git a/qiita_pet/templates/analysis_selected.html b/qiita_pet/templates/analysis_selected.html index f9095d10c..74ff6df01 100644 --- a/qiita_pet/templates/analysis_selected.html +++ b/qiita_pet/templates/analysis_selected.html @@ -133,7 +133,7 @@

Processed Data

-
+ - +