From cb67d3d5e7db6988dcfa90ec6730de5b1b8a2686 Mon Sep 17 00:00:00 2001 From: Jose Navas Date: Thu, 9 Feb 2017 09:15:58 -0800 Subject: [PATCH 01/82] Removing qiita ware code that will not be used anymore --- qiita_ware/analysis_pipeline.py | 188 ---------------------- qiita_ware/dispatchable.py | 12 -- qiita_ware/test/test_analysis_pipeline.py | 90 ----------- 3 files changed, 290 deletions(-) delete mode 100644 qiita_ware/analysis_pipeline.py delete mode 100644 qiita_ware/test/test_analysis_pipeline.py diff --git a/qiita_ware/analysis_pipeline.py b/qiita_ware/analysis_pipeline.py deleted file mode 100644 index 629ec3e4a..000000000 --- a/qiita_ware/analysis_pipeline.py +++ /dev/null @@ -1,188 +0,0 @@ -#!/usr/bin/env python -from __future__ import division -from os.path import join -from sys import stderr - -from future.utils import viewitems -from biom import load_table - -from qiita_db.job import Job -from qiita_db.reference import Reference -from qiita_db.software import Command -from qiita_db.logger import LogEntry -from qiita_db.util import get_db_files_base_dir -from qiita_ware.wrapper import ParallelWrapper, system_call_from_job - - -# ----------------------------------------------------------------------------- -# 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. -# ----------------------------------------------------------------------------- - - -def _finish_analysis(analysis, **kwargs): - """Checks job statuses and finalized analysis and redis communication - - Parameters - ---------- - analysis: Analysis - Analysis to finalize. - kwargs : ignored - Necessary to have in parameters to support execution via moi. - """ - # check job exit statuses for analysis result status - all_good = True - for job in analysis.jobs: - if job.status == "error": - all_good = False - break - - # set final analysis status - if all_good: - analysis.status = "completed" - else: - analysis.status = "error" - - -def _generate_analysis_tgz(analysis, **kwargs): - """Generates the analysis tgz - - Parameters - ---------- - analysis: Analysis - Analysis to generate the tgz command from - kwargs : ignored - Necessary to have in parameters to support execution via moi. - """ - return analysis.generate_tgz() - - -class RunAnalysis(ParallelWrapper): - def _construct_job_graph(self, analysis, commands, comm_opts=None, - rarefaction_depth=None, - merge_duplicated_sample_ids=False): - """Builds the job graph for running an analysis - - Parameters - ---------- - analysis: Analysis object - Analysis to finalize. - commands : list of tuples - Commands to add as jobs in the analysis. - Format [(data_type, command name), ...] - comm_opts : dict of dicts, optional - Options for commands. Format {command name: {opt1: value,...},...} - Default None (use default options). - rarefaction_depth : int, optional - Rarefaction depth for analysis' biom tables. Default None. - merge_duplicated_sample_ids : bool, optional - If the duplicated sample ids in the selected studies should be - merged or prepended with the artifact ids. False (default) prepends - the artifact id - """ - self._logger = stderr - self.analysis = analysis - analysis_id = analysis.id - - # Add jobs to analysis - if comm_opts is None: - comm_opts = {} - - analysis.status = "running" - # creating bioms at this point cause all this section runs on a worker - # node, currently an ipython job - analysis.build_files(rarefaction_depth, merge_duplicated_sample_ids) - mapping_file = analysis.mapping_file - - tree_commands = ["Beta Diversity", "Alpha Rarefaction"] - for data_type, biom_fp in viewitems(analysis.biom_tables): - biom_table = load_table(biom_fp) - # getting reference_id and software_command_id from the first - # sample of the biom. This decision was discussed on the qiita - # meeting on 02/24/16 - metadata = biom_table.metadata(biom_table.ids()[0]) - rid = metadata['reference_id'] - sci = metadata['command_id'] - - if rid != 'na': - reference = Reference(rid) - tree = reference.tree_fp - else: - reference = None - tree = '' - - cmd = Command(sci) if sci != 'na' else None - - for cmd_data_type, command in commands: - if data_type != cmd_data_type: - continue - - # get opts set by user, else make it empty dict - opts = comm_opts.get(command, {}) - opts["--otu_table_fp"] = biom_fp - opts["--mapping_fp"] = mapping_file - - if command in tree_commands: - if tree != '': - opts["--tree_fp"] = tree - else: - opts["--parameter_fp"] = join( - get_db_files_base_dir(), "reference", - "params_qiime.txt") - - if command == "Alpha Rarefaction": - opts["-n"] = 4 - - Job.create(data_type, command, opts, analysis, reference, cmd, - return_existing=True) - - # Add the jobs - job_nodes = [] - for job in analysis.jobs: - node_name = "%d_JOB_%d" % (analysis_id, job.id) - job_nodes.append(node_name) - job_name = "%s: %s" % (job.datatype, job.command[0]) - self._job_graph.add_node(node_name, - func=system_call_from_job, - args=(job.id,), - job_name=job_name, - requires_deps=False) - - # tgz-ing the analysis results - tgz_node_name = "TGZ_ANALYSIS_%d" % (analysis_id) - job_name = "tgz_analysis_%d" % (analysis_id) - self._job_graph.add_node(tgz_node_name, - func=_generate_analysis_tgz, - args=(analysis,), - job_name=job_name, - requires_deps=False) - # Adding the dependency edges to the graph - for job_node_name in job_nodes: - self._job_graph.add_edge(job_node_name, tgz_node_name) - - # Finalize the analysis. - node_name = "FINISH_ANALYSIS_%d" % analysis.id - self._job_graph.add_node(node_name, - func=_finish_analysis, - args=(analysis,), - job_name='Finalize analysis', - requires_deps=False) - self._job_graph.add_edge(tgz_node_name, node_name) - - def _failure_callback(self, msg=None): - """Executed if something fails""" - # set the analysis to errored - self.analysis.status = 'error' - - if self._update_status is not None: - self._update_status("Failed") - - # set any jobs to errored if they didn't execute - for job in self.analysis.jobs: - if job.status not in {'error', 'completed'}: - job.status = 'error' - - LogEntry.create('Runtime', msg, info={'analysis': self.analysis.id}) diff --git a/qiita_ware/dispatchable.py b/qiita_ware/dispatchable.py index c0b667b25..2aaafaa8b 100644 --- a/qiita_ware/dispatchable.py +++ b/qiita_ware/dispatchable.py @@ -5,9 +5,7 @@ # # The full license is in the file LICENSE, distributed with this software. # ----------------------------------------------------------------------------- -from .analysis_pipeline import RunAnalysis from qiita_ware.commands import submit_EBI, submit_VAMPS -from qiita_db.analysis import Analysis def submit_to_ebi(preprocessed_data_id, submission_type): @@ -20,16 +18,6 @@ def submit_to_VAMPS(preprocessed_data_id): return submit_VAMPS(preprocessed_data_id) -def run_analysis(analysis_id, commands, comm_opts=None, - rarefaction_depth=None, merge_duplicated_sample_ids=False, - **kwargs): - """Run an analysis""" - analysis = Analysis(analysis_id) - ar = RunAnalysis(**kwargs) - return ar(analysis, commands, comm_opts, rarefaction_depth, - merge_duplicated_sample_ids) - - def create_raw_data(artifact_type, prep_template, filepaths, name=None): """Creates a new raw data diff --git a/qiita_ware/test/test_analysis_pipeline.py b/qiita_ware/test/test_analysis_pipeline.py deleted file mode 100644 index 479adf5e2..000000000 --- a/qiita_ware/test/test_analysis_pipeline.py +++ /dev/null @@ -1,90 +0,0 @@ -from unittest import TestCase, main -from os.path import join -from os import remove, rename -from shutil import copy -import numpy.testing as npt - -from moi.group import get_id_from_user -from moi import ctx_default - -from qiita_core.util import qiita_test_checker -from qiita_db.analysis import Analysis -from qiita_db.job import Job -from qiita_db.util import get_db_files_base_dir -from qiita_db.exceptions import QiitaDBWarning -from qiita_ware.analysis_pipeline import RunAnalysis, _generate_analysis_tgz - - -# ----------------------------------------------------------------------------- -# 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. -# ----------------------------------------------------------------------------- - - -@qiita_test_checker() -class TestRun(TestCase): - def setUp(self): - self._del_files = [] - - def tearDown(self): - for delfile in self._del_files: - remove(delfile) - - def test_failure_callback(self): - """Make sure failure at file creation step doesn't hang everything""" - # rename a needed file for creating the biom table - base = get_db_files_base_dir() - copy(join(base, "processed_data", - "1_study_1001_closed_reference_otu_table.biom"), - join(base, "processed_data", "1_study_1001.bak")) - analysis = Analysis(2) - group = get_id_from_user("demo@microbio.me") - try: - app = RunAnalysis(moi_context=ctx_default, - moi_parent_id=group) - app(analysis, [], rarefaction_depth=100) - self.assertEqual(analysis.status, 'error') - for job in analysis.jobs: - self.assertEqual(job.status, 'error') - finally: - rename(join(base, "processed_data", "1_study_1001.bak"), - join(base, "processed_data", - "1_study_1001_closed_reference_otu_table.biom")) - - def test_add_jobs_in_construct_job_graphs(self): - analysis = Analysis(2) - npt.assert_warns(QiitaDBWarning, analysis.build_files) - RunAnalysis()._construct_job_graph( - analysis, [('18S', 'Summarize Taxa')], - comm_opts={'Summarize Taxa': {'opt1': 5}}) - self.assertEqual(analysis.jobs, [Job(3), Job(4)]) - job = Job(4) - self.assertEqual(job.datatype, '18S') - self.assertEqual(job.command, - ['Summarize Taxa', 'summarize_taxa_through_plots.py']) - expopts = { - '--mapping_fp': join( - get_db_files_base_dir(), 'analysis/2_analysis_mapping.txt'), - '--otu_table_fp': join( - get_db_files_base_dir(), - 'analysis/2_analysis_dt-18S_r-1_c-3.biom'), - '--output_dir': join( - get_db_files_base_dir(), 'job', - '4_summarize_taxa_through_plots.py_output_dir'), - 'opt1': 5} - self.assertEqual(job.options, expopts) - - def test_generate_analysis_tgz(self): - obs_sout, obs_serr, obs_return = _generate_analysis_tgz(Analysis(1)) - - # not testing obs_serr as it will change depending on the system's tar - # version - self.assertEqual(obs_sout, "") - self.assertEqual(obs_return, 0) - - -if __name__ == "__main__": - main() From 0033480aa5925e689b8465e77532947a57cc3466 Mon Sep 17 00:00:00 2001 From: Jose Navas Date: Thu, 9 Feb 2017 09:56:40 -0800 Subject: [PATCH 02/82] Organizing the handlers and new analysis description page --- .../handlers/analysis_handlers/__init__.py | 18 +++ .../analysis_handlers/base_handlers.py | 101 +++++++++++++ .../analysis_handlers/listing_handlers.py | 135 +++++++++++++++++ .../analysis_handlers/tests/__init__.py | 7 + .../tests/test_base_handlers.py | 83 ++++++++++ .../tests/test_listing_handlers.py | 32 ++++ .../analysis_handlers/tests/test_util.py | 36 +++++ qiita_pet/handlers/analysis_handlers/util.py | 28 ++++ qiita_pet/static/js/qiita.js | 101 +++++++++++++ qiita_pet/templates/analysis_description.html | 143 ++++++++++++++++++ qiita_pet/templates/analysis_selected.html | 4 +- ...{show_analyses.html => list_analyses.html} | 32 +--- qiita_pet/templates/sitebase.html | 3 + qiita_pet/webserver.py | 21 +-- 14 files changed, 706 insertions(+), 38 deletions(-) create mode 100644 qiita_pet/handlers/analysis_handlers/__init__.py create mode 100644 qiita_pet/handlers/analysis_handlers/base_handlers.py create mode 100644 qiita_pet/handlers/analysis_handlers/listing_handlers.py create mode 100644 qiita_pet/handlers/analysis_handlers/tests/__init__.py create mode 100644 qiita_pet/handlers/analysis_handlers/tests/test_base_handlers.py create mode 100644 qiita_pet/handlers/analysis_handlers/tests/test_listing_handlers.py create mode 100644 qiita_pet/handlers/analysis_handlers/tests/test_util.py create mode 100644 qiita_pet/handlers/analysis_handlers/util.py create mode 100644 qiita_pet/templates/analysis_description.html rename qiita_pet/templates/{show_analyses.html => list_analyses.html} (50%) 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..8d54ffabd --- /dev/null +++ b/qiita_pet/handlers/analysis_handlers/base_handlers.py @@ -0,0 +1,101 @@ +# ----------------------------------------------------------------------------- +# 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 + else: + if full_access or obj.visibility == 'public': + name = '%s - %s' % (obj.name, obj.artifact_type) + else: + continue + nodes.add((obj_type, obj.id, name)) + + edges.update({(s[1].id, t[1].id) for s, t in g.edges()}) + + # Transforming to lists so they are JSON serializable + 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

-
+ - +