Skip to content
Merged
61 changes: 61 additions & 0 deletions qiita_db/artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@
from future.utils import viewitems
from itertools import chain
from datetime import datetime
from os import remove

import networkx as nx

Expand Down Expand Up @@ -775,6 +776,66 @@ def filepaths(self):
return qdb.util.retrieve_filepaths(
"artifact_filepath", "artifact_id", self.id, sort='ascending')

@property
def html_summary_fp(self):
"""Returns the HTML summary filepath

Returns
-------
tuple of (int, str)
The filepath id and the path to the HTML summary
"""
fps = qdb.util.retrieve_filepaths("artifact_filepath", "artifact_id",
self.id, fp_type='html_summary')
if fps:
# If fps is not the empty list, then we have exactly one file
# retrieve_filepaths returns a list of lists of 3 values: the
# filepath id, the filepath and the filepath type. We don't want
# to return the filepath type here, so just grabbing the first and
# second element of the list
res = (fps[0][0], fps[0][1])
else:
res = None

return res

@html_summary_fp.setter
def html_summary_fp(self, value):
"""Sets the HTML summary of the artifact

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Can you describe what value is?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nix that, didn't see the parameter below.

Parameters
----------
value : str
Path to the new HTML summary
"""
with qdb.sql_connection.TRN:
current = self.html_summary_fp
if current:
# Delete the current HTML summary
fp_id = current[0]
fp = current[1]
# From the artifact_filepath table
sql = """DELETE FROM qiita.artifact_filepath
WHERE filepath_id = %s"""
qdb.sql_connection.TRN.add(sql, [fp_id])
# From the filepath table
sql = "DELETE FROM qiita.filepath WHERE filepath_id=%s"
qdb.sql_connection.TRN.add(sql, [fp_id])
# And from the filesystem only after the transaction is
# successfully completed (after commit)
qdb.sql_connection.TRN.add_post_commit_func(remove, fp)

# Add the new HTML summary
fp_ids = qdb.util.insert_filepaths(
[(value, 'html_summary')], self.id, self.artifact_type,
"filepath")
sql = """INSERT INTO qiita.artifact_filepath
(artifact_id, filepath_id)
VALUES (%s, %s)"""
# We only inserted a single filepath, so using index 0
qdb.sql_connection.TRN.add(sql, [self.id, fp_ids[0]])
qdb.sql_connection.TRN.execute()

@property
def parents(self):
"""Returns the parents of the artifact
Expand Down
40 changes: 40 additions & 0 deletions qiita_db/handlers/artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -69,6 +69,46 @@ def get(self, artifact_id):

self.write(response)

@authenticate_oauth
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This needs a test, right?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just noticed comment below.

def patch(self, artifact_id):
"""Patches the filepaths of the artifact
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

filepaths or filepath? AFAIK there is only one, no?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "PATCH" request will allow us to update the filepaths attribute of the artifact. Currently, I'm only allowing to add a new filepath, which should be the html-summary, by using op = add and `path = /html_summary/". However, this may change if we need more functionality to the patch request.


Parameter
---------
artifact_id : str
The id of the artifact whole filepaths information is being updated
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whole?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

whose oops!


Returns
-------
dict
Format:
{'success': bool,
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

On previous return dictionaries, we were using a slightly different scheme, is there a good motivation to change that? I'm referring to the dictionaries that have a "status" and a "message" key, etc.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. The difference between here and the other dictionaries is that this is part of the REST api that we put in place for the plugins, while the other ones are used in the GUI. It turns out that we did not catch those differences when we reviewed the GUI code adding this structure.

If you feel strong about it I can change but I will need to change all the qiita_db handlers plus the plugins consuming that API. I would prefer to not change it right now, given that we should end up changing this entire API to not return the success keyword but rather raising an HTTP error in case the call is not successful. Does it make sense?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we are going to change the API regardless, I'm fine and I think we already have an issue open, I've added a note there to make sure we don't forget about this.

'error': str}
- success: whether the request is successful or not
- error: in case that success is false, it contains the error msg
"""
req_op = self.get_argument('op')
req_path = self.get_argument('path')
req_value = self.get_argument('value')

if req_op == 'add':
req_path = [v for v in req_path.split('/') if v]
if len(req_path) != 1 or req_path[0] != 'html_summary':
success = False
error_msg = 'Incorrect path parameter value'
else:
artifact, success, error_msg = _get_artifact(artifact_id)
if success:
artifact.html_summary_fp = req_value
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does this mean that we are going to store the full path?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nope, the setter of that property calls internally "insert_filepaths" which moves the file to the correct location in the file system and inserts the correct relative path to the DB

else:
success = False
error_msg = ('Operation "%s" not supported. Current supported '
'operations: add' % req_op)

response = {'success': success, 'error': error_msg}

self.write(response)


class ArtifactMappingHandler(OauthBaseHandler):
@authenticate_oauth
Expand Down
4 changes: 4 additions & 0 deletions qiita_db/handlers/tests/test_artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,10 @@ def test_get_no_header(self):
obs = self.get('/qiita_db/artifacts/1/filepaths/')
self.assertEqual(obs.code, 400)

def test_patch(self):
# TODO: issue #1682
pass


class ArtifactMappingHandlerTests(OauthTestingBase):
def test_get_artifact_does_not_exist(self):
Expand Down
6 changes: 6 additions & 0 deletions qiita_db/support_files/patches/40.sql
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,12 @@
-- types if they already exist. This way, multiple plugins can share the same
-- type of artifacts without depending in another "processing" plugin.

-- Add the type HTML summary to the list of supported filepath types
-- Note that we are not linking this filepath type with any specific artifact
-- type. The reason is that all artifacts should have it and users are not
-- allowed to upload this file, since it is internally generated
INSERT INTO qiita.filepath_type (filepath_type) VALUES ('html_summary');

-- Create the new table to hold the software types
CREATE TABLE qiita.software_type (
software_type_id bigserial NOT NULL,
Expand Down
28 changes: 28 additions & 0 deletions qiita_db/test/test_artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -854,5 +854,33 @@ def test_is_submitted_to_vamps_setter(self):
a.is_submitted_to_vamps = True
self.assertTrue(a.is_submitted_to_vamps)

def test_html_summary_setter(self):
a = qdb.artifact.Artifact(1)

# Check that returns None when it doesn't exist
self.assertIsNone(a.html_summary_fp)

fd, fp = mkstemp(suffix=".html")
close(fd)
self._clean_up_files.append(fp)

db_fastq_dir = qdb.util.get_mountpoint('FASTQ')[0][1]
path_builder = partial(join, db_fastq_dir, str(a.id))

# Check the setter works when the artifact does not have the summary
a.html_summary_fp = fp
exp1 = path_builder(basename(fp))
self.assertEqual(a.html_summary_fp[1], exp1)

fd, fp = mkstemp(suffix=".html")
close(fd)
self._clean_up_files.append(fp)

# Check the setter works when the artifact already has a summary
a.html_summary_fp = fp
exp2 = path_builder(basename(fp))
self.assertEqual(a.html_summary_fp[1], exp2)
self.assertFalse(exists(exp1))

if __name__ == '__main__':
main()
2 changes: 1 addition & 1 deletion qiita_db/test/test_setup.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ def test_filepath(self):
self.assertEqual(get_count("qiita.filepath"), 19)

def test_filepath_type(self):
self.assertEqual(get_count("qiita.filepath_type"), 19)
self.assertEqual(get_count("qiita.filepath_type"), 20)

def test_study_prep_template(self):
self.assertEqual(get_count("qiita.study_prep_template"), 1)
Expand Down
24 changes: 24 additions & 0 deletions qiita_db/test/test_util.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,6 +251,30 @@ def test_retrieve_filepaths_sort(self):
"raw_forward_seqs")]
self.assertEqual(obs, exp)

def test_retrieve_filepaths_type(self):
obs = qdb.util.retrieve_filepaths(
'artifact_filepath', 'artifact_id', 1, sort='descending',
fp_type='raw_barcodes')
path_builder = partial(
join, qdb.util.get_db_files_base_dir(), "raw_data")
exp = [(2, path_builder("1_s_G1_L001_sequences_barcodes.fastq.gz"),
"raw_barcodes")]
self.assertEqual(obs, exp)

obs = qdb.util.retrieve_filepaths(
'artifact_filepath', 'artifact_id', 1, fp_type='raw_barcodes')
path_builder = partial(
join, qdb.util.get_db_files_base_dir(), "raw_data")
exp = [(2, path_builder("1_s_G1_L001_sequences_barcodes.fastq.gz"),
"raw_barcodes")]
self.assertEqual(obs, exp)

obs = qdb.util.retrieve_filepaths(
'artifact_filepath', 'artifact_id', 1, fp_type='biom')
path_builder = partial(
join, qdb.util.get_db_files_base_dir(), "raw_data")
self.assertEqual(obs, [])

def test_retrieve_filepaths_error(self):
with self.assertRaises(qdb.exceptions.QiitaDBError):
qdb.util.retrieve_filepaths('artifact_filepath', 'artifact_id', 1,
Expand Down
22 changes: 16 additions & 6 deletions qiita_db/util.py
Original file line number Diff line number Diff line change
Expand Up @@ -647,7 +647,8 @@ def str_to_id(x):
chain.from_iterable(qdb.sql_connection.TRN.execute()[idx:])))


def retrieve_filepaths(obj_fp_table, obj_id_column, obj_id, sort=None):
def retrieve_filepaths(obj_fp_table, obj_id_column, obj_id, sort=None,
fp_type=None):
"""Retrieves the filepaths for the given object id

Parameters
Expand All @@ -661,6 +662,8 @@ def retrieve_filepaths(obj_fp_table, obj_id_column, obj_id, sort=None):
sort : {'ascending', 'descending'}, optional
The direction in which the results are sorted, using the filepath id
as sorting key. Default: None, no sorting is applied
fp_type: str, optional
Retrieve only the filepaths of the matching filepath type

Returns
-------
Expand All @@ -685,21 +688,28 @@ def path_builder(db_dir, filepath, mountpoint, subdirectory, obj_id):
"Unknown sorting direction: %s. Please choose from 'ascending' or "
"'descending'" % sort)

sql_args = [obj_id]

sql_type = ""
if fp_type:
sql_type = " AND filepath_type=%s"
sql_args.append(fp_type)

with qdb.sql_connection.TRN:
sql = """SELECT filepath_id, filepath, filepath_type, mountpoint,
subdirectory
FROM qiita.filepath
JOIN qiita.filepath_type USING (filepath_type_id)
JOIN qiita.data_directory USING (data_directory_id)
JOIN qiita.{0} USING (filepath_id)
WHERE {1} = %s{2}""".format(obj_fp_table, obj_id_column,
sql_sort)
qdb.sql_connection.TRN.add(sql, [obj_id])
WHERE {1} = %s{2}{3}""".format(obj_fp_table, obj_id_column,
sql_type, sql_sort)
qdb.sql_connection.TRN.add(sql, sql_args)
results = qdb.sql_connection.TRN.execute_fetchindex()
db_dir = get_db_files_base_dir()

return [(fpid, path_builder(db_dir, fp, m, s, obj_id), fp_type)
for fpid, fp, fp_type, m, s in results]
return [(fpid, path_builder(db_dir, fp, m, s, obj_id), fp_type_)
for fpid, fp, fp_type_, m, s in results]


def purge_filepaths():
Expand Down