Skip to content

Commit 2e0e6e5

Browse files
committed
Merge pull request #1039 from squirrelo/list-proc-data-in-search
List proc data in search
2 parents b98c2db + fbcd86c commit 2e0e6e5

File tree

3 files changed

+357
-146
lines changed

3 files changed

+357
-146
lines changed

qiita_pet/handlers/study_handlers/listing_handlers.py

Lines changed: 139 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
from __future__ import division
99
from json import dumps
1010
from future.utils import viewitems
11+
from collections import defaultdict
1112

1213
from tornado.web import authenticated, HTTPError
1314
from tornado.gen import coroutine, Task
@@ -21,6 +22,7 @@
2122
from qiita_db.exceptions import QiitaDBIncompatibleDatatypeError
2223
from qiita_db.util import get_table_cols
2324
from qiita_db.data import ProcessedData
25+
from qiita_core.exceptions import IncompetentQiitaDeveloperError
2426

2527
from qiita_pet.handlers.base_handlers import BaseHandler
2628
from qiita_pet.handlers.util import study_person_linkifier, pubmed_linkifier
@@ -42,74 +44,149 @@ def _get_shared_links_for_study(study):
4244
return ", ".join(shared)
4345

4446

45-
def _build_study_info(user, results=None):
46-
"""builds list of dicts for studies table, with all html formatted"""
47+
def _build_single_study_info(study, info, study_proc, proc_samples):
48+
"""Clean up and add to the study info for HTML purposes
49+
50+
Parameters
51+
----------
52+
study : Study object
53+
The study to build information for
54+
info : dict
55+
Information from Study.get_info
56+
study_proc : dict of dict of lists
57+
Dictionary keyed on study_id that lists all processed data associated
58+
with that study. This list of processed data ids is keyed by data type
59+
proc_samples : dict of lists
60+
Dictionary keyed on proc_data_id that lists all samples associated with
61+
that processed data.
62+
63+
Returns
64+
-------
65+
dict
66+
info-information + extra information for the study,
67+
slightly HTML formatted
68+
"""
69+
PI = StudyPerson(info['principal_investigator_id'])
70+
status = study.status
71+
if info['pmid'] is not None:
72+
info['pmid'] = ", ".join([pubmed_linkifier([p])
73+
for p in info['pmid']])
74+
else:
75+
info['pmid'] = ""
76+
if info["number_samples_collected"] is None:
77+
info["number_samples_collected"] = 0
78+
info["shared"] = _get_shared_links_for_study(study)
79+
info["num_raw_data"] = len(study.raw_data())
80+
info["status"] = status
81+
info["study_id"] = study.id
82+
info["pi"] = study_person_linkifier((PI.email, PI.name))
83+
del info["principal_investigator_id"]
84+
del info["email"]
85+
# Build the proc data info list for the child row in datatable
86+
info["proc_data_info"] = []
87+
for data_type, proc_datas in viewitems(study_proc[study.id]):
88+
info["proc_data_info"].extend([
89+
_build_single_proc_data_info(pd_id, data_type, proc_samples[pd_id])
90+
for pd_id in proc_datas])
91+
return info
92+
93+
94+
def _build_single_proc_data_info(proc_data_id, data_type, samples):
95+
"""Build the proc data info list for the child row in datatable
96+
97+
Parameters
98+
----------
99+
proc_data_id : int
100+
The processed data attached to he study, in the form
101+
{study_id: [proc_data_id, proc_data_id, ...], ...}
102+
data_type : str
103+
Data type of the processed data
104+
proc_samples : dict of lists
105+
The samples available in the processed data, in the form
106+
{proc_data_id: [samp1, samp2, ...], ...}
107+
108+
Returns
109+
-------
110+
dict
111+
The information for the processed data, in the form {info: value, ...}
112+
"""
113+
proc_data = ProcessedData(proc_data_id)
114+
proc_info = proc_data.processing_info
115+
proc_info['pid'] = proc_data_id
116+
proc_info['data_type'] = data_type
117+
proc_info['samples'] = sorted(samples)
118+
proc_info['processed_date'] = str(proc_info['processed_date'])
119+
return proc_info
120+
121+
122+
def _build_study_info(user, study_proc=None, proc_samples=None):
123+
"""Builds list of dicts for studies table, with all HTML formatted
124+
125+
Parameters
126+
----------
127+
user : User object
128+
logged in user
129+
study_proc : dict of lists, optional
130+
Dictionary keyed on study_id that lists all processed data associated
131+
with that study. Required if proc_samples given.
132+
proc_samples : dict of lists, optional
133+
Dictionary keyed on proc_data_id that lists all samples associated with
134+
that processed data. Required if study_proc given.
135+
136+
Returns
137+
-------
138+
infolist: list of dict of lists and dicts
139+
study and processed data info for JSON serialiation for datatables
140+
Each dict in the list is a single study, and contains the text
141+
142+
Notes
143+
-----
144+
Both study_proc and proc_samples must be passed, or neither passed.
145+
"""
146+
build_samples = False
147+
# Logic check to make sure both needed parts passed
148+
if study_proc is not None and proc_samples is None:
149+
raise IncompetentQiitaDeveloperError(
150+
'Must pass proc_samples when study_proc given')
151+
elif proc_samples is not None and study_proc is None:
152+
raise IncompetentQiitaDeveloperError(
153+
'Must pass study_proc when proc_samples given')
154+
elif study_proc is None:
155+
build_samples = True
156+
47157
# get list of studies for table
48-
study_list = user.user_studies.union(
158+
study_set = user.user_studies.union(
49159
Study.get_by_status('public')).union(user.shared_studies)
50-
if results is not None:
51-
study_list = study_list.intersection(results)
52-
if not study_list:
160+
if study_proc is not None:
161+
study_set = study_set.intersection(study_proc)
162+
if not study_set:
53163
# No studies left so no need to continue
54164
return []
55165

56166
# get info for the studies
57167
cols = ['study_id', 'email', 'principal_investigator_id',
58168
'pmid', 'study_title', 'metadata_complete',
59169
'number_samples_collected', 'study_abstract']
60-
study_info = Study.get_info(study_list, cols)
170+
study_info = Study.get_info(study_set, cols)
61171

62172
infolist = []
63-
for row, info in enumerate(study_info):
173+
for info in study_info:
174+
# Convert DictCursor to proper dict
175+
info = dict(info)
64176
study = Study(info['study_id'])
65-
status = study.status
66-
# Just passing the email address as the name here, since
67-
# name is not a required field in qiita.qiita_user
68-
PI = StudyPerson(info['principal_investigator_id'])
69-
PI = study_person_linkifier((PI.email, PI.name))
70-
if info['pmid'] is not None:
71-
pmids = ", ".join([pubmed_linkifier([p])
72-
for p in info['pmid']])
73-
else:
74-
pmids = ""
75-
if info["number_samples_collected"] is None:
76-
info["number_samples_collected"] = "0"
77-
shared = _get_shared_links_for_study(study)
78-
meta_complete_glyph = "ok" if info["metadata_complete"] else "remove"
79-
# build the HTML elements needed for table cell
80-
title = ("<a href='#' data-toggle='modal' "
81-
"data-target='#study-abstract-modal' "
82-
"onclick='fillAbstract(\"studies-table\", {0})'>"
83-
"<span class='glyphicon glyphicon-file' "
84-
"aria-hidden='true'></span></a> | "
85-
"<a href='/study/description/{1}' "
86-
"id='study{0}-title'>{2}</a>").format(
87-
str(row), str(study.id), info["study_title"])
88-
meta_complete = "<span class='glyphicon glyphicon-%s'></span>" % \
89-
meta_complete_glyph
90-
if status == 'public':
91-
shared = "Not Available"
92-
else:
93-
shared = ("<span id='shared_html_{0}'>{1}</span><br/>"
94-
"<a class='btn btn-primary btn-xs' data-toggle='modal' "
95-
"data-target='#share-study-modal-view' "
96-
"onclick='modify_sharing({0});'>Modify</a>".format(
97-
study.id, shared))
98-
99-
infolist.append({
100-
"checkbox": "<input type='checkbox' value='%d' />" % study.id,
101-
"id": study.id,
102-
"title": title,
103-
"meta_complete": meta_complete,
104-
"num_samples": info["number_samples_collected"],
105-
"shared": shared,
106-
"num_raw_data": len(study.raw_data()),
107-
"pi": PI,
108-
"pmid": pmids,
109-
"status": status,
110-
"abstract": info["study_abstract"]
111-
112-
})
177+
# Build the processed data info for the study if none passed
178+
if build_samples:
179+
proc_data_list = study.processed_data()
180+
proc_samples = {}
181+
study_proc = {study.id: defaultdict(list)}
182+
for pid in proc_data_list:
183+
proc_data = ProcessedData(pid)
184+
study_proc[study.id][proc_data.data_type()].append(pid)
185+
proc_samples[pid] = proc_data.samples
186+
187+
study_info = _build_single_study_info(study, info, study_proc,
188+
proc_samples)
189+
infolist.append(study_info)
113190
return infolist
114191

115192

@@ -195,12 +272,12 @@ def get(self, ignore):
195272

196273
if user != self.current_user.id:
197274
raise HTTPError(403, 'Unauthorized search!')
198-
res = None
199275
if query:
200276
# Search for samples matching the query
201277
search = QiitaStudySearch()
202278
try:
203-
res, meta = search(query, self.current_user)
279+
search(query, self.current_user)
280+
study_proc, proc_samples, _ = search.filter_by_processed_data()
204281
except ParseException:
205282
self.clear()
206283
self.set_status(400)
@@ -223,9 +300,10 @@ def get(self, ignore):
223300
info={'User': self.current_user.id,
224301
'query': query})
225302
return
226-
if not res:
227-
res = {}
228-
info = _build_study_info(self.current_user, results=res)
303+
else:
304+
study_proc = proc_samples = None
305+
info = _build_study_info(self.current_user, study_proc=study_proc,
306+
proc_samples=proc_samples)
229307
# build the table json
230308
results = {
231309
"sEcho": echo,

qiita_pet/templates/list_studies.html

Lines changed: 69 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,35 +1,75 @@
11
{% extends sitebase.html %}
22
{% block head %}
33
<link rel="stylesheet" href="/static/vendor/css/jquery.dataTables.css" type="text/css">
4-
4+
<style>
5+
td.details-control {
6+
cursor: pointer;
7+
}
8+
</style>
59
<script src="/static/vendor/js/jquery.dataTables.min.js"></script>
610
<script src="/static/vendor/js/jquery.dataTables.plugin.natural.js"></script>
711

812
<script type="text/javascript">
913
var current_study;
1014
var ajaxURL = "/study/search/?&user={{current_user.id}}&sEcho=" + Math.floor(Math.random()*1001);
1115
$(document).ready(function() {
16+
17+
function format ( d ) {
18+
// `d` is the original data object for the row
19+
var proc_data_table = '<h4>Processed Data</h4><table class="table" cellpadding="5" cellspacing="0" border="0" style="padding-left:50px;"><tr><th></th><th>ID</th><th>Data type</th><th>Processed Date</th><th>Algorithm</th><th>Reference</th><th>Samples</th></tr>';
20+
for(i=0;i<d.proc_data_info.length;i++) {
21+
var proc_data = d.proc_data_info[i];
22+
proc_data_table += '<tr><td><input type="checkbox"></td><td>' + proc_data.pid + '</td><td>' + proc_data.data_type + '</td><td>' + proc_data.processed_date + '</td><td>' + proc_data.algorithm + '</td><td>' + proc_data.reference_name + ' ' + proc_data.reference_version + '</td><td>' + proc_data.samples.length + '</td></tr>';
23+
}
24+
proc_data_table += '</table>';
25+
return proc_data_table;
26+
}
27+
1228
$('#studies-table').dataTable({
1329
"columns": [
14-
{ "data": "checkbox" },
15-
{ "data": "title" },
16-
{ "data": "abstract" },
17-
{ "data": "id" },
18-
{ "data": "meta_complete" },
19-
{ "data": "num_samples" },
30+
{ "className": 'details-control', "orderable": false},
31+
{ "data": "study_title" },
32+
{ "data": "study_abstract" },
33+
{ "data": "study_id" },
34+
{ "data": "metadata_complete" },
35+
{ "data": "number_samples_collected" },
2036
{ "data": "num_raw_data" },
2137
{ "data": "shared" },
2238
{ "data": "pi" },
2339
{ "data": "pmid" },
24-
{ "data": "status" }
40+
{ "data": "status" },
41+
{"className": 'details-control', "orderable": false, "data": null, "defaultContent": '<span class="glyphicon glyphicon-chevron-down"></span>'}
2542
],
2643
order: [[10, "desc"], [ 1, "asc" ]],
27-
columnDefs: [{type:'natural', targets:[3,4,5,9]}, {"targets": [ 2 ],"visible": false}],
44+
columnDefs: [
45+
{type:'natural', targets:[3,4,5,9]},
46+
{"targets": [ 2 ],"visible": false},
47+
// render the study checkbox cell
48+
{"render": function ( data, type, row, meta ) {
49+
return "<input type='checkbox' value='"+ row.study_id +"'>";
50+
}, targets: [0]},
51+
// render the title cell
52+
{"render": function ( data, type, row, meta ) {
53+
return "<a href='#' data-toggle='modal' data-target='#study-abstract-modal' onclick=\"fillAbstract('studies-table', "+ meta.row +")\"><span class='glyphicon glyphicon-file' aria-hidden='true'></span></a> | <a href='/study/description/"+ row.study_id +"' id='study"+ meta.row +"-title'>"+ data +"</a>";
54+
}, targets: [1]},
55+
// render the metadata complete cell
56+
{"render": function ( data, type, row, meta ) {
57+
var glyph = 'remove';
58+
if(data === true) { glyph = 'ok' }
59+
return "<span class='glyphicon glyphicon-"+ glyph +"'></span>";
60+
}, targets: [4]},
61+
// render the shared with cell
62+
{"render": function ( data, type, row, meta ) {
63+
var glyph = 'remove';
64+
if(data === true) { glyph = 'ok' }
65+
return "<span id='shared_html_"+ row.study_id +"'>"+ data +"</span><br/><a class='btn btn-primary btn-xs' data-toggle='modal' data-target='#share-study-modal-view' onclick='modify_sharing("+ row.study_id +");'>Modify</a>";
66+
}, targets: [7]},
67+
],
2868
"oLanguage": {
2969
"sSearch": "Narrow search results by column data (Title, abstract, PI, etc):",
3070
"sLoadingRecords": "Loading table data",
3171
"sZeroRecords": "No studies found"
32-
},
72+
},
3373
"ajax": {
3474
"url": ajaxURL + "&query=",
3575
"error": function(jqXHR, textStatus, ex) {
@@ -39,6 +79,23 @@
3979
}
4080
}
4181
});
82+
// Add event listener for opening and closing details
83+
$('#studies-table tbody').on('click', 'td.details-control', function () {
84+
var table = $('#studies-table').DataTable();
85+
var tr = $(this).closest('tr');
86+
var row = table.row( tr );
87+
88+
if ( row.child.isShown() ) {
89+
// This row is already open - close it
90+
row.child.hide();
91+
tr.removeClass('shown');
92+
}
93+
else {
94+
// Open this row
95+
row.child( format(row.data()) ).show();
96+
tr.addClass('shown');
97+
}
98+
} );
4299

43100
$('#users_list').chosen({width: "100%"});
44101
$('#users_list').on('change', function(evt, params) {
@@ -105,7 +162,7 @@ <h1>Metadata Search</h1>
105162
<div class="row">
106163
<div class="col-sm-12" id="user-studies-div">
107164
<h1>Available Studies</h1>
108-
<table id="studies-table" class="display table-bordered table-hover">
165+
<table id="studies-table" class="table table-bordered">
109166
<thead>
110167
<tr>
111168
<th></th>
@@ -119,10 +176,9 @@ <h1>Available Studies</h1>
119176
<th>Principal Investigator</th>
120177
<th>Pubmed ID(s)</th>
121178
<th>Status</th>
179+
<th>Expand</th>
122180
</tr>
123181
</thead>
124-
<tbody>
125-
</tbody>
126182
</table>
127183
<!--Abstract Modal-->
128184
<div class="modal fade" tabindex="-1" role="dialog" aria-labelledby="myLargeModalLabel" aria-hidden="true" id="study-abstract-modal">

0 commit comments

Comments
 (0)