diff --git a/qiita_db/analysis.py b/qiita_db/analysis.py index 662098076..048d1f5ce 100644 --- a/qiita_db/analysis.py +++ b/qiita_db/analysis.py @@ -514,6 +514,26 @@ def pmid(self, pmid): qdb.sql_connection.TRN.add(sql, [pmid, self._id]) qdb.sql_connection.TRN.execute() + @property + def can_be_publicized(self): + """Returns whether the analysis can be made public + + Returns + ------- + bool + Whether the analysis can be publicized or not + """ + # The analysis can be made public if all the artifacts used + # to get the samples from are public + with qdb.sql_connection.TRN: + sql = """SELECT DISTINCT artifact_id + FROM qiita.analysis_sample + WHERE analysis_id = %s""" + qdb.sql_connection.TRN.add(sql, [self.id]) + return all( + [qdb.artifact.Artifact(aid).visibility == 'public' + for aid in qdb.sql_connection.TRN.execute_fetchflatten()]) + def add_artifact(self, artifact): """Adds an artifact to the analysis @@ -570,6 +590,24 @@ def has_access(self, user): return self in Analysis.get_by_status('public') | \ user.private_analyses | user.shared_analyses + def can_edit(self, user): + """Returns whether the given user can edit the analysis + + Parameters + ---------- + user : User object + User we are checking edit permissions for + + Returns + ------- + bool + Whether user can edit the study or not + """ + # The analysis is editable only if the user is the owner, is in the + # shared list or the user is an admin + return (user.level in {'superuser', 'admin'} or self.owner == user or + user in self.shared_with) + def summary_data(self): """Return number of studies, artifacts, and samples selected diff --git a/qiita_db/handlers/artifact.py b/qiita_db/handlers/artifact.py index 82732863f..45c4b387d 100644 --- a/qiita_db/handlers/artifact.py +++ b/qiita_db/handlers/artifact.py @@ -79,6 +79,7 @@ def get(self, artifact_id): """ with qdb.sql_connection.TRN: artifact = _get_artifact(artifact_id) + study = artifact.study response = { 'name': artifact.name, 'timestamp': str(artifact.timestamp), @@ -89,7 +90,7 @@ def get(self, artifact_id): 'can_be_submitted_to_vamps': artifact.can_be_submitted_to_vamps, 'prep_information': [p.id for p in artifact.prep_templates], - 'study': artifact.study.id} + 'study': study.id if study else None} params = artifact.processing_parameters response['processing_parameters'] = ( params.values if params is not None else None) diff --git a/qiita_db/test/test_analysis.py b/qiita_db/test/test_analysis.py index 2fbce2187..356acc13a 100644 --- a/qiita_db/test/test_analysis.py +++ b/qiita_db/test/test_analysis.py @@ -125,6 +125,27 @@ def test_get_by_status(self): self.assertEqual( qdb.analysis.Analysis.get_by_status('public'), set([])) + def test_can_be_publicized(self): + analysis = qdb.analysis.Analysis(1) + self.assertFalse(analysis.can_be_publicized) + a4 = qdb.artifact.Artifact(4) + a5 = qdb.artifact.Artifact(5) + a6 = qdb.artifact.Artifact(6) + + a4.visibility = 'public' + self.assertFalse(analysis.can_be_publicized) + + a5.visibility = 'public' + self.assertFalse(analysis.can_be_publicized) + + a6.visibility = 'public' + self.assertTrue(analysis.can_be_publicized) + + a4.visibility = 'private' + a5.visibility = 'private' + a6.visibility = 'private' + self.assertFalse(analysis.can_be_publicized) + def test_add_artifact(self): obs = self._create_analyses_with_samples() exp = qdb.artifact.Artifact(4) @@ -162,6 +183,13 @@ def test_has_access_no_access(self): self.assertFalse( self.analysis.has_access(qdb.user.User("demo@microbio.me"))) + def test_can_edit(self): + a = qdb.analysis.Analysis(1) + self.assertTrue(a.can_edit(qdb.user.User('test@foo.bar'))) + self.assertTrue(a.can_edit(qdb.user.User('shared@foo.bar'))) + self.assertTrue(a.can_edit(qdb.user.User('admin@foo.bar'))) + self.assertFalse(a.can_edit(qdb.user.User('demo@microbio.me'))) + def test_create_nonqiita_portal(self): qiita_config.portal = "EMP" obs = qdb.analysis.Analysis.create( diff --git a/qiita_pet/exceptions.py b/qiita_pet/exceptions.py index 01fcc8afc..745ba1270 100644 --- a/qiita_pet/exceptions.py +++ b/qiita_pet/exceptions.py @@ -7,9 +7,28 @@ # ----------------------------------------------------------------------------- from __future__ import division + +from tornado.web import HTTPError + from qiita_core.exceptions import QiitaError +class QiitaHTTPError(HTTPError): + def __init__(self, status_code=500, log_message=None, *args, **kwargs): + super(QiitaHTTPError, self).__init__( + status_code, log_message, *args, **kwargs) + # The HTTPError has an attribute named "reason" that will get send to + # the requester if specified. However, the developer need to + # specifically pass the keyword "reason" when raising the exception. + # The vast majority of our code it is not using the keyword "reason" + # but we are using "log_message". By setting up the attribute reason + # with the value in log_message, we make sure that when the answer + # is sent to the requester, it will contain a useful error message, + # rather than a generic error message. + if not self.reason: + self.reason = log_message + + class QiitaPetAuthorizationError(QiitaError): """When a user tries to access a resource without proper authorization""" def __init__(self, user_id, resource_name_str): diff --git a/qiita_pet/handlers/analysis_handlers/tests/test_base_handlers.py b/qiita_pet/handlers/analysis_handlers/tests/test_base_handlers.py index 1f0d31eb3..9203f79dd 100644 --- a/qiita_pet/handlers/analysis_handlers/tests/test_base_handlers.py +++ b/qiita_pet/handlers/analysis_handlers/tests/test_base_handlers.py @@ -11,6 +11,7 @@ from tornado.web import HTTPError +from qiita_core.util import qiita_test_checker from qiita_db.user import User from qiita_db.analysis import Analysis from qiita_pet.test.tornado_test_base import TestHandlerBase @@ -18,6 +19,7 @@ analyisis_graph_handler_get_request) +@qiita_test_checker() class TestBaseHandlersUtils(TestCase): def test_analyisis_graph_handler_get_request(self): obs = analyisis_graph_handler_get_request(1, User('test@foo.bar')) diff --git a/qiita_pet/handlers/artifact_handlers/__init__.py b/qiita_pet/handlers/artifact_handlers/__init__.py new file mode 100644 index 000000000..859fbcca1 --- /dev/null +++ b/qiita_pet/handlers/artifact_handlers/__init__.py @@ -0,0 +1,11 @@ +# ----------------------------------------------------------------------------- +# 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 .base_handlers import ArtifactSummaryAJAX, ArtifactAJAX + +__all__ = ['ArtifactSummaryAJAX', 'ArtifactAJAX'] diff --git a/qiita_pet/handlers/artifact_handlers/base_handlers.py b/qiita_pet/handlers/artifact_handlers/base_handlers.py new file mode 100644 index 000000000..b93e75f89 --- /dev/null +++ b/qiita_pet/handlers/artifact_handlers/base_handlers.py @@ -0,0 +1,340 @@ +# ----------------------------------------------------------------------------- +# 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 os.path import basename + +from tornado.web import authenticated + +from qiita_core.qiita_settings import qiita_config +from qiita_pet.handlers.base_handlers import BaseHandler +from qiita_pet.handlers.util import safe_execution +from qiita_pet.exceptions import QiitaHTTPError +from qiita_db.artifact import Artifact +from qiita_db.software import Command, Parameters +from qiita_db.processing_job import ProcessingJob + + +def check_artifact_access(user, artifact): + """Checks whether user has access to an artifact + + Parameters + ---------- + user : qiita_db.user.User object + User to check + artifact : qiita_db.artifact.Artifact + Artifact to check access for + + Raises + ------ + QiitaHTTPError + If the user doesn't have access to the given artifact + """ + if user.level == 'admin': + return + if artifact.visibility != 'public': + study = artifact.study + analysis = artifact.analysis + if study: + if not study.has_access(user): + raise QiitaHTTPError(403, "Access denied to study %s" + % artifact.id) + elif analysis: + if not analysis.has_access(user): + raise QiitaHTTPError(403, "Access denied to artifact %s" + % artifact.id) + else: + # This can't happen but worth adding a check + raise QiitaHTTPError(500, "Error accessing artifact %s" + % artifact.id) + + +def artifact_summary_get_request(user, artifact_id): + """Returns the information for the artifact summary page + + Parameters + ---------- + user : qiita_db.user.User + The user making the request + artifact_id : int or str + The artifact id + + Returns + ------- + dict of objects + A dictionary containing the artifact summary information + {'name': str, + 'artifact_id': int, + 'visibility': str, + 'editable': bool, + 'buttons': str, + 'processing_parameters': dict of {str: object}, + 'files': list of (int, str), + 'processing_jobs': list of [str, str, str, str, str], + 'summary': str or None, + 'job': [str, str, str], + 'errored_jobs': list of [str, str]} + """ + artifact_id = int(artifact_id) + artifact = Artifact(artifact_id) + + check_artifact_access(user, artifact) + + visibility = artifact.visibility + summary = artifact.html_summary_fp + job_info = None + errored_jobs = [] + processing_jobs = [] + for j in artifact.jobs(): + if j.command.software.type == "artifact transformation": + status = j.status + if status == 'success': + continue + j_msg = j.log.msg if status == 'error' else None + processing_jobs.append( + [j.id, j.command.name, j.status, j.step, j_msg]) + + # Check if the HTML summary exists + if summary: + with open(summary[1]) as f: + summary = f.read() + else: + # Check if the summary is being generated + command = Command.get_html_generator(artifact.artifact_type) + all_jobs = set(artifact.jobs(cmd=command)) + jobs = [j for j in all_jobs if j.status in ['queued', 'running']] + errored_jobs = [(j.id, j.log.msg) + for j in all_jobs if j.status in ['error']] + if jobs: + # There is already a job generating the HTML. Also, there should be + # at most one job, because we are not allowing here to start more + # than one + job = jobs[0] + job_info = [job.id, job.status, job.step] + + # Check if the artifact is editable by the given user + study = artifact.study + analysis = artifact.analysis + editable = study.can_edit(user) if study else analysis.can_edit(user) + + buttons = [] + btn_base = ( + '').format(artifact_id) + + if analysis: + # If the artifact is part of an analysis, we don't require admin + # approval, and the artifact can be made public only if all the + # artifacts used to create the initial artifact set are public + if analysis.can_be_publicized: + buttons.append(btn_base % ('make public', 'public', 'Make public')) + + else: + # If the artifact is part of a study, the buttons shown depend in + # multiple factors (see each if statement for an explanation of those) + if qiita_config.require_approval: + if visibility == 'sandbox': + # The request approval button only appears if the artifact is + # sandboxed and the qiita_config specifies that the approval + # should be requested + buttons.append( + btn_base % ('request approval for', 'awaiting_approval', + 'Request approval')) + + elif user.level == 'admin' and visibility == 'awaiting_approval': + # The approve artifact button only appears if the user is an admin + # the artifact is waiting to be approvaed and the qiita config + # requires artifact approval + buttons.append(btn_base % ('approve', 'private', + 'Approve artifact')) + + if visibility == 'private': + # The make public button only appears if the artifact is private + buttons.append(btn_base % ('make public', 'public', 'Make public')) + + # The revert to sandbox button only appears if the artifact is not + # sandboxed nor public + if visibility not in {'sandbox', 'public'}: + buttons.append(btn_base % ('revert to sandbox', 'sandbox', + 'Revert to sandbox')) + + if user.level == 'admin': + if artifact.can_be_submitted_to_ebi: + if not artifact.is_submitted_to_ebi: + buttons.append( + '' + '' + ' Submit to EBI' % artifact_id) + if artifact.can_be_submitted_to_vamps: + if not artifact.is_submitted_to_vamps: + buttons.append( + '' + '' + ' Submit to VAMPS' % artifact_id) + + files = [(f_id, "%s (%s)" % (basename(fp), f_type.replace('_', ' '))) + for f_id, fp, f_type in artifact.filepaths + if f_type != 'directory'] + + # TODO: https://github.com/biocore/qiita/issues/1724 Remove this hardcoded + # values to actually get the information from the database once it stores + # the information + if artifact.artifact_type in ['SFF', 'FASTQ', 'FASTA', 'FASTA_Sanger', + 'per_sample_FASTQ']: + # If the artifact is one of the "raw" types, only the owner of the + # study and users that has been shared with can see the files + if not artifact.study.has_access(user, no_public=True): + files = [] + + processing_parameters = (artifact.processing_parameters.values + if artifact.processing_parameters is not None + else {}) + + return {'name': artifact.name, + 'artifact_id': artifact_id, + 'visibility': visibility, + 'editable': editable, + 'buttons': ' '.join(buttons), + 'processing_parameters': processing_parameters, + 'files': files, + 'processing_jobs': processing_jobs, + 'summary': summary, + 'job': job_info, + 'errored_jobs': errored_jobs + } + + +def artifact_summary_post_request(user, artifact_id): + """Launches the HTML summary generation and returns the job information + + Parameters + ---------- + user : qiita_db.user.User + The user making the request + artifact_id : int or str + The artifact id + + Returns + ------- + dict of objects + A dictionary containing the job summary information + {'job': [str, str, str]} + """ + artifact_id = int(artifact_id) + artifact = Artifact(artifact_id) + + check_artifact_access(user, artifact) + + # Check if the summary is being generated or has been already generated + command = Command.get_html_generator(artifact.artifact_type) + jobs = artifact.jobs(cmd=command) + jobs = [j for j in jobs if j.status in ['queued', 'running', 'success']] + if jobs: + # The HTML summary is either being generated or already generated. + # Return the information of that job so we only generate the HTML + # once - Magic number 0 -> we are ensuring that there is only one + # job generating the summary, so we can use the index 0 to access to + # that job + job = jobs[0] + else: + # Create a new job to generate the HTML summary and return the newly + # created job information + job = ProcessingJob.create(user, Parameters.load( + command, values_dict={'input_data': artifact_id})) + job.submit() + + return {'job': [job.id, job.status, job.step]} + + +class ArtifactSummaryAJAX(BaseHandler): + @authenticated + def get(self, artifact_id): + with safe_execution(): + res = artifact_summary_get_request(self.current_user, artifact_id) + + self.render("artifact_ajax/artifact_summary.html", **res) + + @authenticated + def post(self, artifact_id): + with safe_execution(): + res = artifact_summary_post_request(self.current_user, artifact_id) + self.write(res) + + +def artifact_patch_request(user, artifact_id, req_op, req_path, req_value=None, + req_from=None): + """Modifies an attribute of the artifact + + Parameters + ---------- + user : qiita_db.user.User + The user performing the patch operation + artifact_id : int + Id of the artifact in which the patch operation is being performed + req_op : str + The operation to perform on the artifact + req_path : str + The prep information and attribute to patch + req_value : str, optional + The value that needs to be modified + req_from : str, optional + The original path of the element + + Raises + ------ + QiitaHTTPError + If `req_op` != 'replace' + If the path parameter is incorrect + If missing req_value + If the attribute to replace is not known + """ + if req_op == 'replace': + req_path = [v for v in req_path.split('/') if v] + if len(req_path) != 1: + raise QiitaHTTPError(404, 'Incorrect path parameter') + + attribute = req_path[0] + + # Check if the user actually has access to the artifact + artifact = Artifact(artifact_id) + check_artifact_access(user, artifact) + + if not req_value: + raise QiitaHTTPError(404, 'Missing value to replace') + + if attribute == 'name': + artifact.name = req_value + return + else: + # We don't understand the attribute so return an error + raise QiitaHTTPError(404, 'Attribute "%s" not found. Please, ' + 'check the path parameter' % attribute) + else: + raise QiitaHTTPError(400, 'Operation "%s" not supported. Current ' + 'supported operations: replace' % req_op) + + +class ArtifactAJAX(BaseHandler): + @authenticated + def patch(self, artifact_id): + """Patches a prep template in the system + + Follows the JSON PATCH specification: + https://tools.ietf.org/html/rfc6902 + """ + req_op = self.get_argument('op') + req_path = self.get_argument('path') + req_value = self.get_argument('value', None) + req_from = self.get_argument('from', None) + + with safe_execution(): + artifact_patch_request(self.current_user, artifact_id, req_op, + req_path, req_value, req_from) + + self.finish() diff --git a/qiita_pet/handlers/artifact_handlers/tests/__init__.py b/qiita_pet/handlers/artifact_handlers/tests/__init__.py new file mode 100644 index 000000000..e0aff71d9 --- /dev/null +++ b/qiita_pet/handlers/artifact_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/artifact_handlers/tests/test_base_handlers.py b/qiita_pet/handlers/artifact_handlers/tests/test_base_handlers.py new file mode 100644 index 000000000..7c5473d35 --- /dev/null +++ b/qiita_pet/handlers/artifact_handlers/tests/test_base_handlers.py @@ -0,0 +1,306 @@ +# ----------------------------------------------------------------------------- +# 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 tempfile import mkstemp +from os import close, remove +from os.path import basename, exists + +from tornado.web import HTTPError + +from qiita_core.util import qiita_test_checker +from qiita_db.user import User +from qiita_db.artifact import Artifact +from qiita_db.processing_job import ProcessingJob +from qiita_db.software import Parameters, Command +from qiita_pet.exceptions import QiitaHTTPError +from qiita_pet.test.tornado_test_base import TestHandlerBase +from qiita_pet.handlers.artifact_handlers.base_handlers import ( + check_artifact_access, artifact_summary_get_request, + artifact_summary_post_request, artifact_patch_request) + + +@qiita_test_checker() +class TestBaseHandlersUtils(TestCase): + def setUp(self): + self._files_to_remove = [] + + def tearDown(self): + for fp in self._files_to_remove: + if exists(fp): + remove(fp) + + def test_check_artifact_access(self): + # "Study" artifact + a = Artifact(1) + # The user has access + u = User('test@foo.bar') + check_artifact_access(u, a) + + # Admin has access to everything + admin = User('admin@foo.bar') + check_artifact_access(admin, a) + + # Demo user doesn't have access + demo_u = User('demo@microbio.me') + with self.assertRaises(HTTPError): + check_artifact_access(demo_u, a) + + # "Analysis" artifact + a = Artifact(8) + a.visibility = 'private' + check_artifact_access(u, a) + check_artifact_access(admin, a) + with self.assertRaises(HTTPError): + check_artifact_access(demo_u, a) + check_artifact_access(User('shared@foo.bar'), a) + a.visibility = 'public' + check_artifact_access(demo_u, a) + + def test_artifact_summary_get_request(self): + user = User('test@foo.bar') + # Artifact w/o summary + obs = artifact_summary_get_request(user, 1) + exp_p_jobs = [ + ['063e553b-327c-4818-ab4a-adfe58e49860', 'Split libraries FASTQ', + 'queued', None, None], + ['bcc7ebcd-39c1-43e4-af2d-822e3589f14d', 'Split libraries', + 'running', 'demultiplexing', None]] + exp_files = [ + (1L, '1_s_G1_L001_sequences.fastq.gz (raw forward seqs)'), + (2L, '1_s_G1_L001_sequences_barcodes.fastq.gz (raw barcodes)')] + exp = {'name': 'Raw data 1', + 'artifact_id': 1, + 'visibility': 'private', + 'editable': True, + 'buttons': (' '), + 'processing_parameters': {}, + 'files': exp_files, + 'summary': None, + 'job': None, + 'processing_jobs': exp_p_jobs, + 'errored_jobs': []} + self.assertEqual(obs, exp) + + # Artifact with summary being generated + job = ProcessingJob.create( + User('test@foo.bar'), + Parameters.load(Command(7), values_dict={'input_data': 1}) + ) + job._set_status('queued') + obs = artifact_summary_get_request(user, 1) + exp = {'name': 'Raw data 1', + 'artifact_id': 1, + 'visibility': 'private', + 'editable': True, + 'buttons': (' '), + 'processing_parameters': {}, + 'files': exp_files, + 'summary': None, + 'job': [job.id, 'queued', None], + 'processing_jobs': exp_p_jobs, + 'errored_jobs': []} + self.assertEqual(obs, exp) + + # Artifact with summary + fd, fp = mkstemp(suffix=".html") + close(fd) + with open(fp, 'w') as f: + f.write('HTML TEST - not important\n') + a = Artifact(1) + a.html_summary_fp = fp + self._files_to_remove.extend([fp, a.html_summary_fp[1]]) + exp_files.append( + (a.html_summary_fp[0], + '%s (html summary)' % basename(a.html_summary_fp[1]))) + obs = artifact_summary_get_request(user, 1) + exp = {'name': 'Raw data 1', + 'artifact_id': 1, + 'visibility': 'private', + 'editable': True, + 'buttons': (' '), + 'processing_parameters': {}, + 'files': exp_files, + 'summary': 'HTML TEST - not important\n', + 'job': None, + 'processing_jobs': exp_p_jobs, + 'errored_jobs': []} + self.assertEqual(obs, exp) + + # No access + demo_u = User('demo@microbio.me') + with self.assertRaises(QiitaHTTPError): + obs = artifact_summary_get_request(demo_u, 1) + + # A non-owner/share user can't see the files + a.visibility = 'public' + obs = artifact_summary_get_request(demo_u, 1) + exp = {'name': 'Raw data 1', + 'artifact_id': 1, + 'visibility': 'public', + 'editable': False, + 'buttons': '', + 'processing_parameters': {}, + 'files': [], + 'summary': 'HTML TEST - not important\n', + 'job': None, + 'processing_jobs': exp_p_jobs, + 'errored_jobs': []} + self.assertEqual(obs, exp) + + # returnig to private + a.visibility = 'sandbox' + + # admin gets buttons + obs = artifact_summary_get_request(User('admin@foo.bar'), 2) + exp_p_jobs = [ + ['d19f76ee-274e-4c1b-b3a2-a12d73507c55', + 'Pick closed-reference OTUs', 'error', 'generating demux file', + 'Error message']] + exp_files = [ + (3L, '1_seqs.fna (preprocessed fasta)'), + (4L, '1_seqs.qual (preprocessed fastq)'), + (5L, '1_seqs.demux (preprocessed demux)')] + exp = {'name': 'Demultiplexed 1', + 'artifact_id': 2, + 'visibility': 'private', + 'editable': True, + 'buttons': (' Submit to VAMPS'), + 'processing_parameters': { + 'max_barcode_errors': 1.5, 'sequence_max_n': 0, + 'max_bad_run_length': 3, 'phred_offset': u'auto', + 'rev_comp': False, 'phred_quality_threshold': 3, + 'input_data': 1, 'rev_comp_barcode': False, + 'rev_comp_mapping_barcodes': False, + 'min_per_read_length_fraction': 0.75, + 'barcode_type': u'golay_12'}, + 'files': exp_files, + 'summary': None, + 'job': None, + 'processing_jobs': exp_p_jobs, + 'errored_jobs': []} + self.assertEqual(obs, exp) + + # analysis artifact + obs = artifact_summary_get_request(user, 8) + exp = {'name': 'noname', + 'artifact_id': 8, + 'visibility': 'sandbox', + 'editable': True, + 'buttons': '', + 'processing_parameters': {}, + 'files': [(22, 'biom_table.biom (biom)')], + 'summary': None, + 'job': None, + 'processing_jobs': [], + 'errored_jobs': []} + self.assertEqual(obs, exp) + + def test_artifact_summary_post_request(self): + # No access + with self.assertRaises(QiitaHTTPError): + artifact_summary_post_request(User('demo@microbio.me'), 1) + + # Returns already existing job + job = ProcessingJob.create( + User('test@foo.bar'), + Parameters.load(Command(7), values_dict={'input_data': 2}) + ) + job._set_status('queued') + obs = artifact_summary_post_request(User('test@foo.bar'), 2) + exp = {'job': [job.id, 'queued', None]} + self.assertEqual(obs, exp) + + def test_artifact_patch_request(self): + a = Artifact(1) + self.assertEqual(a.name, 'Raw data 1') + + artifact_patch_request(User('test@foo.bar'), 1, 'replace', '/name/', + req_value='NEW_NAME') + self.assertEqual(a.name, 'NEW_NAME') + + # Reset the name + a.name = 'Raw data 1' + + # No access + with self.assertRaises(QiitaHTTPError): + artifact_patch_request(User('demo@microbio.me'), 1, 'replace', + '/name/', req_value='NEW_NAME') + + # Incorrect path parameter + with self.assertRaises(QiitaHTTPError): + artifact_patch_request(User('test@foo.bar'), 1, 'replace', + '/name/wrong/', req_value='NEW_NAME') + + # Missing value + with self.assertRaises(QiitaHTTPError): + artifact_patch_request(User('test@foo.bar'), 1, 'replace', + '/name/') + + # Wrong attribute + with self.assertRaises(QiitaHTTPError): + artifact_patch_request(User('test@foo.bar'), 1, 'replace', + '/wrong/', req_value='NEW_NAME') + + # Wrong operation + with self.assertRaises(QiitaHTTPError): + artifact_patch_request(User('test@foo.bar'), 1, 'add', '/name/', + req_value='NEW_NAME') + + +class TestBaseHandlers(TestHandlerBase): + def test_get_artifact_summary_ajax_handler(self): + response = self.get('/artifact/1/summary/') + self.assertEqual(response.code, 200) + + def test_patch_artifact_ajax_handler(self): + a = Artifact(1) + self.assertEqual(a.name, 'Raw data 1') + arguments = {'op': 'replace', 'path': '/name/', 'value': 'NEW_NAME'} + response = self.patch('/artifact/1/', data=arguments) + self.assertEqual(response.code, 200) + self.assertEqual(a.name, 'NEW_NAME') + a.name = 'Raw data 1' + + +if __name__ == '__main__': + main() diff --git a/qiita_pet/handlers/util.py b/qiita_pet/handlers/util.py index 2ff3891ce..4eafafbf2 100644 --- a/qiita_pet/handlers/util.py +++ b/qiita_pet/handlers/util.py @@ -7,13 +7,29 @@ # ----------------------------------------------------------------------------- from __future__ import division from functools import partial +from contextlib import contextmanager from tornado.web import HTTPError from qiita_pet.util import linkify +from qiita_pet.exceptions import QiitaHTTPError from qiita_core.util import execute_as_transaction +@contextmanager +def safe_execution(): + try: + yield + except HTTPError: + # The HTTPError is already handled nicely by tornado, just re-raise + raise + except Exception as e: + # Any other error we need to catch and re-raise as a QiitaHTTPError + # so we can make sure that tornado will handle it gracefully and send + # a useful error message to the user + raise QiitaHTTPError(500, str(e)) + + @execute_as_transaction def check_access(user, study, no_public=False, raise_error=False): """make sure user has access to the study requested""" diff --git a/qiita_pet/static/js/qiita.js b/qiita_pet/static/js/qiita.js index 2847f6506..48e8ab5b5 100644 --- a/qiita_pet/static/js/qiita.js +++ b/qiita_pet/static/js/qiita.js @@ -209,3 +209,27 @@ function draw_processing_graph(nodes, edges, target, artifactFunc, jobFunc) { function show_loading(portal_dir, target) { $("#" + target).html(""); } + +/** + * + * Function to update the name of an artifact + * + * @param portal_dir: string. The portal that qiita is running under + * @param artifact_id: int. The artifact to be changed + * @param new_name: string. The new artifact name + * @param on_success_func: function. Function to execute when the name has been + * successfully updated + * + */ +function change_artifact_name(portal_dir, artifact_id, new_name, on_success_func) { + $.ajax({ + url: portal_dir + '/artifact/' + artifact_id + '/', + type: 'PATCH', + data: {'op': 'replace', 'path': '/name/', 'value': new_name}, + success: on_success_func, + error: function(object, status, error_msg) { + // Something went wrong, show the message + bootstrapAlert("Error changing artifact name: " + error_msg, "danger"); + } + }); +} diff --git a/qiita_pet/templates/analysis_description.html b/qiita_pet/templates/analysis_description.html index cf859e78d..ff2e628c0 100644 --- a/qiita_pet/templates/analysis_description.html +++ b/qiita_pet/templates/analysis_description.html @@ -34,11 +34,12 @@ function populateContentArtifact(artifactId) { // Put the loading gif in the div show_loading('{% raw qiita_config.portal_dir %}', 'analysis-results'); - $.get('{% raw qiita_config.portal_dir %}/study/description/artifact_summary/', {'artifact_id': artifactId}, function(data){ + $.get('{% raw qiita_config.portal_dir %}/artifact/' + artifactId + '/summary/', function(data){ $("#analysis-results").html(data); }) .fail(function(object, status, error_msg) { - $("#analysis-results").html("Error loading artifact information: " + status + " " + error_msg); + // $("#analysis-results").html(object.responseText); + $("#analysis-results").html("Error loading artifact information: " + status + " " + object.statusText); } ); }; @@ -84,7 +85,7 @@ props: ['nodes', 'edges'] }); - new Vue({ + var vueGraph = new Vue({ el: "#analysis-graph-vue", data: { nodes: [], @@ -101,6 +102,8 @@ vm.update_jobs(); } else { + vm.nodes = []; + vm.edges = []; // The initial set of artifacts has been created! Format the graph // data in a way that Vis.Network likes it // Format edge list data @@ -163,7 +166,10 @@ } }, 5000); } - }) + }); + + // Add the vue object to the div, so we avoid to have global variables + $("#analysis-network-div").data('data-graph-vue', vueGraph); });