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);
});