Skip to content
Merged
Show file tree
Hide file tree
Changes from 6 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 16 additions & 0 deletions qiita_db/test/test_user.py
Original file line number Diff line number Diff line change
Expand Up @@ -418,5 +418,21 @@ def test_delete_messages(self):
obs = self.conn_handler.execute_fetchall(sql)
self.assertItemsEqual(obs, [[1], [3]])

def test_user_artifacts(self):
user = qdb.user.User('test@foo.bar')
obs = user.user_artifacts()
exp = {qdb.study.Study(1): [qdb.artifact.Artifact(1),
qdb.artifact.Artifact(2),
qdb.artifact.Artifact(3),
qdb.artifact.Artifact(4),
qdb.artifact.Artifact(5),
qdb.artifact.Artifact(6)]}
self.assertEqual(obs, exp)
obs = user.user_artifacts(artifact_type='BIOM')
exp = {qdb.study.Study(1): [qdb.artifact.Artifact(4),
qdb.artifact.Artifact(5),
qdb.artifact.Artifact(6)]}
self.assertEqual(obs, exp)

if __name__ == "__main__":
main()
42 changes: 42 additions & 0 deletions qiita_db/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@
from re import sub
from datetime import datetime

from future.utils import viewitems

from qiita_core.exceptions import (IncorrectEmailError, IncorrectPasswordError,
IncompetentQiitaDeveloperError)
from qiita_core.qiita_settings import qiita_config
Expand Down Expand Up @@ -475,6 +477,46 @@ def unread_messages(self):
return qdb.sql_connection.TRN.execute_fetchindex()

# ------- methods ---------
def user_artifacts(self, artifact_type=None):
"""Returns the artifacts owned by the user, grouped by study

Parameters
----------
artifact_type : str, optional
The artifact type to retrieve. Default: retrieve all artfact types

Returns
-------
dict of {qiita_db.study.Study: list of qiita_db.artifact.Artifact}
The artifacts owned by the user
"""
with qdb.sql_connection.TRN:
sql_args = [self.id, qiita_config.portal]
sql_a_type = ""
if artifact_type:
Copy link
Contributor

Choose a reason for hiding this comment

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

Question: incorrect artifact types here will go unnoticed i.e. >>> .....user_artifacts(artifact_type='B10M'), this will return an empty dictionary, would we want to alert consumers that this is actually an artifact type not recognized by the system or is that not at all expected/supported.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I see your point. This has been added mainly for the needs in the interface. I don't think though that an empty dict is a bad answer. What do others think?

Copy link
Member

Choose a reason for hiding this comment

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

I think is fine to return an empty dict. This is also happening in other sections of the code.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sounds good.

On (Mar-28-16|16:22), Antonio Gonzalez wrote:

  •    """Returns the artifacts owned by the user, grouped by study
    
  •    Parameters
    

  •    artifact_type : str, optional
    
  •        The artifact type to retrieve. Default: retrieve all artfact types
    
  •    Returns
    

  •    dict of {qiita_db.study.Study: list of qiita_db.artifact.Artifact}
    
  •        The artifacts owned by the user
    
  •    """
    
  •    with qdb.sql_connection.TRN:
    
  •        sql_args = [self.id, qiita_config.portal]
    
  •        sql_a_type = ""
    
  •        if artifact_type:
    

I think is fine to return an empty dict. This is also happening in other sections of the code.


You are receiving this because you commented.
Reply to this email directly or view it on GitHub:
https://github.com/biocore/qiita/pull/1716/files/8365dceee043c29c50ea3a58e61887e96b3f99a2#r57650695

sql_a_type = " AND artifact_type = %s"
sql_args.append(artifact_type)

sql = """SELECT study_id, array_agg(
artifact_id ORDER BY artifact_id)
FROM qiita.study
JOIN qiita.study_portal USING (study_id)
JOIN qiita.portal_type USING (portal_type_id)
JOIN qiita.study_artifact USING (study_id)
JOIN qiita.artifact USING (artifact_id)
JOIN qiita.artifact_type USING (artifact_type_id)
WHERE email = %s AND portal = %s{0}
GROUP BY study_id
ORDER BY study_id""".format(sql_a_type)
qdb.sql_connection.TRN.add(sql, sql_args)
db_res = dict(qdb.sql_connection.TRN.execute_fetchindex())
res = {}
for s_id, artifact_ids in viewitems(db_res):
res[qdb.study.Study(s_id)] = [
qdb.artifact.Artifact(a_id) for a_id in artifact_ids]

return res

def change_password(self, oldpass, newpass):
"""Changes the password from oldpass to newpass

Expand Down
86 changes: 51 additions & 35 deletions qiita_pet/handlers/api_proxy/artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -234,7 +234,7 @@ def artifact_get_req(user_id, artifact_id):

@execute_as_transaction
def artifact_post_req(user_id, filepaths, artifact_type, name,
prep_template_id):
prep_template_id, artifact_id=None):
"""Creates the initial artifact for the prep template

Parameters
Expand All @@ -250,6 +250,8 @@ def artifact_post_req(user_id, filepaths, artifact_type, name,
Name to give the artifact
prep_template_id : int or str castable to int
Prep template to attach the artifact to
artifact_id : str, optional
Copy link
Member

Choose a reason for hiding this comment

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

Is this really a str vs an int?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

This comes from the handler so it actually comes as a string.

Copy link
Member

Choose a reason for hiding this comment

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

OK but note that prep_template_id also does and it can be int or str.

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 id of the imported artifact

Returns
-------
Expand All @@ -267,41 +269,55 @@ def artifact_post_req(user_id, filepaths, artifact_type, name,
if access_error:
return access_error

uploads_path = get_mountpoint('uploads')[0][1]
path_builder = partial(join, uploads_path, str(study_id))
cleaned_filepaths = []
for ftype, file_list in viewitems(filepaths):
# JavaScript sends us this list as a comma-separated list
for fp in file_list.split(','):
# JavaScript will send this value as an empty string if the
# list of files was empty. In such case, the split will generate
# a single element containing the empty string. Check for that case
# here and, if fp is not the empty string, proceed to check if
# the file exists
if fp:
# Check if filepath being passed exists for study
full_fp = path_builder(fp)
exists = check_fp(study_id, full_fp)
if exists['status'] != 'success':
return {'status': 'error',
'message': 'File does not exist: %s' % fp}
cleaned_filepaths.append((full_fp, ftype))

# This should never happen, but it doesn't hurt to actually have
# a explicit check, in case there is something odd with the JS
if not cleaned_filepaths:
return {'status': 'error',
'message': "Can't create artifact, no files provided."}
if artifact_id:
# if the artifact id has been provided, import the artifact
try:
artifact = Artifact.copy(Artifact(artifact_id), prep)
except Exception as e:
Copy link
Contributor

Choose a reason for hiding this comment

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

This pattern seems to be replicated in two different locations, the reality of it is that if anything goes wrong, we want the interface to know about that, so perhaps the thing to do here is to follow the decorator pattern and decorate these methods to catch a general exception and return the appropriate dictionary with the encoded information we need? Not blocking, but I thought I would bring this up now.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

That is actually a good point - however I think it shouldn't be blocking now and we can fix later. Can you open an issue?

Copy link
Contributor

Choose a reason for hiding this comment

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

See: #1717

On (Mar-28-16|16:22), Jose Navas wrote:

  •            exists = check_fp(study_id, full_fp)
    
  •            if exists['status'] != 'success':
    
  •                return {'status': 'error',
    
  •                        'message': 'File does not exist: %s' % fp}
    

- cleaned_filepaths.append((full_fp, ftype))

  • This should never happen, but it doesn't hurt to actually have

  • a explicit check, in case there is something odd with the JS

  • if not cleaned_filepaths:
  •    return {'status': 'error',
    
  •            'message': "Can't create artifact, no files provided."}
    
  • if artifact_id:
  •    # if the artifact id has been provided, import the artifact
    
  •    try:
    
  •        artifact = Artifact.copy(Artifact(artifact_id), prep)
    
  •    except Exception as e:
    

That is actually a good point - however I think it shouldn't be blocking now and we can fix later. Can you open an issue?


You are receiving this because you commented.
Reply to this email directly or view it on GitHub:
https://github.com/biocore/qiita/pull/1716/files/8365dceee043c29c50ea3a58e61887e96b3f99a2#r57650625

# We should hit this exception rarely (that's why it is an
# exception) since at this point we have done multiple checks.
# However, it can occur in weird cases, so better let the GUI know
# that this failed
return {'status': 'error',
'message': "Error creating artifact: %s" % str(e)}

try:
artifact = Artifact.create(cleaned_filepaths, artifact_type, name=name,
prep_template=prep)
except Exception as e:
# We should hit this exception rarely (that's why it is an exception)
# since at this point we have done multiple checks. However, it can
# occur in weird cases, so better let the GUI know that this failed
return {'status': 'error',
'message': "Error creating artifact: %s" % str(e)}
else:
Copy link
Member

Choose a reason for hiding this comment

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

Just want to confirm that there are tests for the try/except blocks for this section.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

I had for the one below, not for the one above - just added.

uploads_path = get_mountpoint('uploads')[0][1]
path_builder = partial(join, uploads_path, str(study_id))
cleaned_filepaths = []
for ftype, file_list in viewitems(filepaths):
# JavaScript sends us this list as a comma-separated list
for fp in file_list.split(','):
# JavaScript will send this value as an empty string if the
# list of files was empty. In such case, the split will
# generate a single element containing the empty string. Check
# for that case here and, if fp is not the empty string,
# proceed to check if the file exists
if fp:
# Check if filepath being passed exists for study
full_fp = path_builder(fp)
exists = check_fp(study_id, full_fp)
if exists['status'] != 'success':
return {'status': 'error',
'message': 'File does not exist: %s' % fp}
cleaned_filepaths.append((full_fp, ftype))

# This should never happen, but it doesn't hurt to actually have
# a explicit check, in case there is something odd with the JS
if not cleaned_filepaths:
return {'status': 'error',
'message': "Can't create artifact, no files provided."}

try:
artifact = Artifact.create(cleaned_filepaths, artifact_type,
name=name, prep_template=prep)
except Exception as e:
# We should hit this exception rarely (that's why it is an
# exception) since at this point we have done multiple checks.
# However, it can occur in weird cases, so better let the GUI know
# that this failed
return {'status': 'error',
'message': "Error creating artifact: %s" % str(e)}

return {'status': 'success',
'message': '',
Expand Down
20 changes: 18 additions & 2 deletions qiita_pet/handlers/api_proxy/studies.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@
from __future__ import division
from collections import defaultdict

from future.utils import viewitems

from qiita_db.user import User
from qiita_db.study import Study
from qiita_db.metadata_template.prep_template import PrepTemplate
Expand Down Expand Up @@ -180,7 +182,7 @@ def study_prep_get_req(study_id, user_id):
'info': prep_info}


def study_files_get_req(study_id, prep_template_id, artifact_type):
def study_files_get_req(user_id, study_id, prep_template_id, artifact_type):
"""Returns the uploaded files for the study id categorized by artifact_type

It retrieves the files uploaded for the given study and tries to do a
Expand All @@ -189,6 +191,8 @@ def study_files_get_req(study_id, prep_template_id, artifact_type):

Parameters
----------
user_id : str
The id of the user making the request
study_id : int
The study id
prep_template_id : int
Expand Down Expand Up @@ -242,8 +246,20 @@ def study_files_get_req(study_id, prep_template_id, artifact_type):
# because selected is initialized to the empty list
file_types.insert(0, (first[0], first[1], selected))

# Create a list of artifacts that the user has access to, in case that
# he wants to import the files from another artifact
user = User(user_id)
artifact_options = []
for study, artifacts in viewitems(user.user_artifacts(
Copy link
Member

Choose a reason for hiding this comment

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

This makes sense for most users but for an admin you will like to see not only your artifacts but also the artifacts that belong to the owner of the study you are checking. For example, let's say that you are helping a user and you added an artifact in prep X, then you want to reuse it for prep Y, with this structure you can't.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Just to confirm - you wanna add to the list the artifacts of the current study - correct?

artifact_type=artifact_type)):
study_label = "%s (%d)" % (study.title, study.id)
for a in artifacts:
artifact_options.append(
(a.id, "%s - %s (%d)" % (study_label, a.name, a.id)))

return {'status': 'success',
'message': '',
'remaining': remaining,
'file_types': file_types,
'num_prefixes': num_prefixes}
'num_prefixes': num_prefixes,
'artifacts': artifact_options}
20 changes: 20 additions & 0 deletions qiita_pet/handlers/api_proxy/tests/test_artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -326,6 +326,26 @@ def test_artifact_post_req(self):
a = Artifact(new_artifact_id)
self._files_to_remove.extend([fp for _, fp, _ in a.filepaths])

# Test importing an artifact
# Create new prep template to attach artifact to
pt = npt.assert_warns(
QiitaDBWarning, PrepTemplate.create,
pd.DataFrame({'new_col': {'1.SKD6.640190': 1}}), Study(1), '16S')
self._files_to_remove.extend([fp for _, fp in pt.get_filepaths()])

new_artifact_id_2 = get_count('qiita.artifact') + 1
obs = artifact_post_req(
'test@foo.bar', {}, 'FASTQ', 'New Test Artifact 2', pt.id,
new_artifact_id)
exp = {'status': 'success',
'message': '',
'artifact': new_artifact_id_2}
self.assertEqual(obs, exp)
# Instantiate the artifact to make sure it was made and
# to clean the environment
a = Artifact(new_artifact_id)
self._files_to_remove.extend([fp for _, fp, _ in a.filepaths])

def test_artifact_post_req_error(self):
# Create a new prep template to attach the artifact to
pt = npt.assert_warns(
Expand Down
6 changes: 4 additions & 2 deletions qiita_pet/handlers/api_proxy/tests/test_studies.py
Original file line number Diff line number Diff line change
Expand Up @@ -247,14 +247,16 @@ def test_study_delete_req_no_exists(self):
self.assertEqual(obs, exp)

def test_study_files_get_req(self):
obs = study_files_get_req(1, 1, 'FASTQ')
obs = study_files_get_req('test@foo.bar', 1, 1, 'FASTQ')
exp = {'status': 'success',
'message': '',
'remaining': ['uploaded_file.txt'],
'file_types': [('raw_barcodes', True, []),
('raw_forward_seqs', True, []),
('raw_reverse_seqs', False, [])],
'num_prefixes': 1}
'num_prefixes': 1,
'artifacts': [(1, 'Identification of the Microbiomes for '
'Cannabis Soils (1) - Raw data 1 (1)')]}
self.assertEqual(obs, exp)

if __name__ == '__main__':
Expand Down
7 changes: 5 additions & 2 deletions qiita_pet/handlers/study_handlers/artifact.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,13 +47,16 @@ def post(self):
artifact_type = self.get_argument('artifact-type')
name = self.get_argument('name')
prep_id = self.get_argument('prep-template-id')
artifact_id = self.get_argument('import-artifact')

# Request the rest of the arguments, which will be the files
files = {arg: self.get_argument(arg) for arg in self.request.arguments
if arg not in ['name', 'prep-template-id', 'artifact-type']}
if arg not in ['name', 'prep-template-id', 'artifact-type',
'import-artifact']}

artifact = artifact_post_req(
self.current_user.id, files, artifact_type, name, prep_id)
self.current_user.id, files, artifact_type, name, prep_id,
artifact_id)
self.write(artifact)


Expand Down
7 changes: 2 additions & 5 deletions qiita_pet/handlers/study_handlers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -78,9 +78,6 @@ def get(self):
atype = self.get_argument('artifact_type')
pt_id = self.get_argument('prep_template_id')

res = study_files_get_req(study_id, pt_id, atype)
res = study_files_get_req(self.current_user.id, study_id, pt_id, atype)
Copy link
Member

Choose a reason for hiding this comment

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

I was wondering about this, why send the user.id vs. the user object?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

For consistency with the other calls - when these calls where put in place, it was assumed that they'll be replaced by REST api calls, hence the idea on just passing the ids. I don't think that is gonna happen, b/c this calls are specialized for the interface but I want to be consistent at this point.


self.render('study_ajax/artifact_file_selector.html',
remaining=res['remaining'],
file_types=res['file_types'],
num_prefixes=res['num_prefixes'])
self.render('study_ajax/artifact_file_selector.html', **res)
2 changes: 1 addition & 1 deletion qiita_pet/templates/study_ajax/add_artifact.html
Original file line number Diff line number Diff line change
Expand Up @@ -82,7 +82,7 @@

<div class="row">
<div class="col-md-12">
<h4><i>Add files</i></h4>
<h4><i>No files attached to this preparation</i></h4>
</div>
</div>

Expand Down
32 changes: 31 additions & 1 deletion qiita_pet/templates/study_ajax/artifact_file_selector.html
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
<link rel="stylesheet" href="/static/vendor/css/chosen.css" type="text/css">
<script type="text/javascript>">
/**
*
Expand Down Expand Up @@ -131,6 +132,23 @@
// Add the event listener to all the lists that can be checked
$(".checkable").on( "sortupdate", on_change_validation );

// When the artifact importer changes, check if we need to show the import
// button
$("#import-artifact").change(function(event) {
if( $("#import-artifact").val() !== "") {
$("#artifact-import-btn").show();
$("#drag-files-div").hide();
$("#submit-div").hide();
}
else {
$("#artifact-import-btn").hide();
$("#drag-files-div").show();
allow_submission();
}
});

$("#artifact-import-btn").hide()

// Perform the first check to all the lists, given that we may be
// prepopulating some of them
check_files_lists();
Expand Down Expand Up @@ -164,7 +182,19 @@
</style>
<div class="row">
<div class="col-md-12">
<b>Click and drag your files to the correct file type:</b><br/>
<b>Import files from other studies:</b>
<select id="import-artifact" name="import-artifact">
<option value="">Choose an artfact to import...</option>
{% for a_id, label in artifacts %}
<option value="{{a_id}}">{{label}}</option>
{% end %}
</select>
<button id="artifact-import-btn" class="btn btn-default btn-sm" onclick="console.log('clicked')"><span class="glyphicon glyphicon-import"></span> Import</button>
</div>
</div>
<div id="drag-files-div" class="row">
<div class="col-md-12">
<b>Or click and drag your uploaded files to the correct file type:</b><br/>
</div>
<div class="col-md-3" id="files-div">
<p style="text-align: center;"><i>Available Files</i></p>
Expand Down
2 changes: 2 additions & 0 deletions qiita_pet/templates/study_ajax/sample_summary.html
Original file line number Diff line number Diff line change
Expand Up @@ -27,9 +27,11 @@
populate_data_type_menu_div();
if (act == 'delete') {
$("#sample-summary-btn").hide();
$("#add-new-preparation-btn").hide();
}
else if(act == 'create') {
$("#sample-summary-btn").show();
$("#add-new-preparation-btn").show();
}
}
})
Expand Down
Loading