0
|
1 import os.path
|
|
2 import time
|
|
3 import csv
|
|
4 import collections
|
|
5 import re
|
|
6 import common
|
|
7
|
|
8 HTML_REPORT_HEADER_FILE = 'html_report_header.html'
|
|
9
|
|
10 class HTMLReport(object):
|
|
11
|
|
12 """ This receives and sets up the general meta-data fields available to the html rendering engine
|
|
13
|
|
14 """
|
|
15 def __init__(self, tagGroup, options, query_stats = []):
|
|
16
|
|
17 self.columns = tagGroup.columns
|
|
18 self.display_columns = [field for field in self.columns if field['group']=='column']
|
|
19 self.row_limit = options.row_limit
|
|
20 self.section_bins = {}
|
|
21 self.todo = collections.deque([]) # stack of things to do
|
|
22 self.query_stats = query_stats
|
|
23 self.empty_queries = [query['id'] for query in query_stats if query['filtered_rows'] == 0]
|
|
24 self.initialized = False
|
|
25
|
|
26 # These items are available for display in html generation via dictionary string replacement: [string ... %(filters)s ...] % self.lookup
|
|
27 self.lookup = {
|
|
28 'depth': 0,
|
|
29 'filters': 'Filters: ' + options.filters_HTML if len(options.filters_HTML) else '',
|
|
30 'timestamp': time.strftime('%Y/%m/%d'),
|
|
31 'visible_limit': 20,
|
|
32 'column_count': str(len([field for field in self.columns if field['group']=='column'])),
|
|
33 'table_rows':0,
|
|
34 'select_row':0,
|
|
35 'row_limit': self.row_limit,
|
|
36 'label':'',
|
|
37 'value':'',
|
|
38 'link':'',
|
|
39 'cssClass':'',
|
|
40 'table_header': self._tableHeader(),
|
|
41 'section_bins':'',
|
1
|
42 'section_counter':1,
|
|
43 'target_form':'select_subsets'
|
0
|
44 }
|
|
45
|
|
46 if hasattr(options,'dataset_selection_id'):
|
|
47 self.lookup['dataset_selection_id'] = options.dataset_selection_id
|
|
48 self.initialized = True
|
|
49
|
|
50 else:
|
|
51 self.lookup['dataset_selection_id'] = 0
|
|
52 self.errorNotice = '<div style="width:400px;margin:auto"><h3>This Selectable HTML Report needs the "Basic Report Field Output" to include the qseq and sseq sequence fields (aligned part of query sequence and subject sequence). Add these fields or try a selection that has more than 12 columns.</h3></div>'
|
|
53
|
|
54 """
|
|
55 _processTagStack()
|
|
56 In the production of html, start tags and template bits are added to the outgoing html when
|
|
57 designated section columns of data change value. the self.todo stack keeps track of all the
|
|
58 tag closings that have to occur when a section or table section comes to an end (and new one
|
|
59 begins or end of document occurs).
|
|
60
|
|
61 This dynamically executes any functions listed in stack that are greater than given tag depth.
|
|
62 Generally the functions return html closing content.
|
|
63
|
|
64 @param depth integer >= 0
|
|
65 @uses self.todo stack of [depth, function_name] items
|
|
66 """
|
|
67 def _processTagStack(self, depth = 0):
|
|
68 html = ''
|
|
69 while len(self.todo) and self.todo[0][0] >= depth:
|
|
70 html += getattr(self, self.todo.popleft()[1] )()
|
|
71 return html
|
|
72
|
|
73
|
|
74
|
|
75 ############################### HTML REPORT RENDERING ##############################
|
|
76 """ render() produces the html. Takes in tabular data + metainformation about that file,
|
|
77 and iterates through rows. This approach depends on detecting changes in stated report
|
|
78 section columns and table section columns, and triggers appropriate section start and end,
|
|
79 and table / table section start and end tags.
|
|
80
|
|
81 @param in_file string Full file path
|
|
82 @param out_html_file string Full output html data file path to write to.
|
|
83 """
|
|
84 def render (self, in_file, out_html_file):
|
|
85
|
|
86 try:
|
|
87
|
|
88 fp_in = open(in_file, "rb")
|
|
89 fp_out = open(out_html_file, 'w')
|
|
90
|
|
91 fp_out.write( self._header(HTML_REPORT_HEADER_FILE) )
|
|
92
|
|
93 if self.initialized:
|
|
94
|
|
95 fp_out.write( self._bodyStart() )
|
|
96 self.todo.appendleft([0,'_bodyEnd'])
|
|
97
|
|
98 reader = csv.reader(fp_in, delimiter="\t")
|
|
99
|
|
100 for row in reader:
|
|
101
|
|
102 html = ''
|
|
103 self.rowdata = []
|
|
104 row_bins = []
|
|
105 section_reset = False
|
|
106
|
|
107 for (idx, field) in enumerate(self.columns):
|
|
108
|
|
109 value = field['value'] = row[idx]
|
|
110 depth = idx + 1
|
|
111
|
|
112 # If a bin is mentioned on this row, its put into self.selection_bins.
|
|
113 if field['type'] == 'bin' and value != '':
|
|
114 row_bins.append(value)
|
|
115 if not value in self.section_bins:
|
|
116 self.section_bins[value] = field['label']
|
|
117
|
|
118 grouping = field['group']
|
|
119 # Section or table grouping here:
|
|
120 if grouping == 'section' or grouping == 'table':
|
|
121
|
|
122 # Check to see if a new section or table section is triggered by change in field's value:
|
|
123 if section_reset or (not 'valueOld' in field) or value != field['valueOld']:
|
|
124
|
|
125 self.lookup['value'] = value
|
|
126 self.lookup['label'] = field['label']
|
|
127
|
|
128 html += self._processTagStack(depth)
|
|
129
|
|
130 if grouping == 'section':
|
|
131 section_reset = True
|
|
132 self.lookup['section_depth'] = depth
|
|
133 self.lookup['section_counter'] += 1
|
|
134 self.lookup['table_rows'] = 0
|
|
135 self.section_bins = {}
|
|
136
|
|
137 html += self._sectionStart()
|
|
138
|
|
139 html += self._sectionFormStart()
|
|
140 self.todo.appendleft([depth,'_sectionFormEnd'])
|
|
141
|
|
142 self.todo.appendleft([depth,'_sectionEnd'])
|
|
143
|
|
144 elif grouping == 'table':
|
|
145
|
|
146 lastToDo = self.todo[0]
|
|
147 if lastToDo[1] == '_sectionEnd': #Just started a section
|
|
148 html += self._tableStart()
|
|
149 self.todo.appendleft([lastToDo[0]+1,'_tableEnd'])
|
|
150
|
|
151 html += self._tbodyHeader() + self._tbodyStart()
|
|
152 self.todo.appendleft([lastToDo[0]+2,'_tbodyEnd'])
|
|
153
|
|
154 field['valueOld'] = value
|
|
155
|
|
156 else:
|
|
157
|
|
158 if grouping == 'column': self.rowdata.append(row[idx])
|
|
159
|
|
160 lastToDo = self.todo[0]
|
|
161 # No table level, instead going right from section to column field:
|
|
162 if lastToDo[1] == '_sectionEnd':
|
|
163 html += self._tableStart() + self._tbodyStart()
|
|
164 self.todo.appendleft([lastToDo[0]+1,'_tableEnd'])
|
|
165 self.todo.appendleft([lastToDo[0]+2,'_tbodyEnd'])
|
|
166
|
|
167 self.lookup['row_bins'] = ",".join(row_bins)
|
|
168
|
|
169 fp_out.write(html)
|
|
170
|
|
171 self.lookup['table_rows'] += 1
|
|
172
|
|
173 # Now output table row of data:
|
|
174 fp_out.write( self._tableRow() )
|
|
175
|
|
176
|
|
177 #Not initialized here, so just write created error notice.
|
|
178 else:
|
|
179
|
|
180 fp_out.write('<h3>' + self.errorNotice + '</body></html>')
|
|
181
|
|
182 fp_out.write( self._processTagStack() )
|
|
183
|
|
184 except IOError as e:
|
|
185 print 'Operation failed: %s' % e.strerror
|
|
186
|
|
187 fp_in.close()
|
|
188 fp_out.close()
|
|
189
|
|
190
|
|
191 ############################### HTML REPORT PART TEMPLATES ##############################
|
|
192 def _header(self, filename):
|
|
193
|
|
194 with open(os.path.join(os.path.dirname(__file__), filename), "r") as fphtml:
|
|
195 data = fphtml.read()
|
|
196
|
|
197 return data
|
|
198
|
|
199
|
|
200 def _bodyStart(self):
|
|
201 # The form enables the creation of a dataset from selected entries. It passes selections (based on a certain column's value) to the "Select tabular rows" tool, which then creates a dataset from the selected rows.
|
|
202 with open(os.path.join(os.path.dirname(__file__), "html_selectable_report_tool_state.txt"), "r") as form_code_file:
|
|
203 self.lookup['form_state'] = form_code_file.readline().strip()
|
|
204
|
|
205 html = """
|
|
206 <script>
|
|
207 // Toggle checkbox inputs of given table according to state of checkbox
|
|
208 function toggleInputs(mycheckbox, table) {
|
|
209 var checkboxes = table.getElementsByTagName('INPUT');
|
|
210 // Loop skips first toggler checkbox
|
|
211 for (var ptr = 1; ptr < checkboxes.length; ptr ++ ) {
|
|
212 checkboxes[ptr].checked = mycheckbox.checked;
|
|
213 }
|
|
214 return true; // prevents click processing
|
|
215 }
|
|
216
|
|
217 //Drops hidden section id from form submission when no section items were selected
|
|
218 function formCheck() {
|
|
219 var tables = document.getElementsByTagName('TABLE');
|
|
220 for (var ptr = 0; ptr < tables.length; ptr ++) {
|
|
221 var inputs = tables[ptr].getElementsByTagName('INPUT');
|
|
222 var selected = 0;
|
|
223 // 1st item in table is select-all toggle
|
|
224 for (var ptr2 = 1; ptr2 < inputs.length; ptr2 ++) {
|
|
225 if (inputs[ptr2].getAttribute('name') == 'select' && inputs[ptr2].checked) selected ++;
|
|
226 }
|
|
227 if (selected==0) {
|
|
228 id = tables[ptr].getAttribute("id");
|
|
229 secInput = document.getElementById(id + '_start');
|
|
230 secInput.disabled = true;
|
|
231 secInput.setAttribute('disabled','disabled');
|
|
232 secInput.value='';
|
|
233 }
|
|
234
|
|
235 }
|
|
236 return true;
|
|
237 }
|
|
238 </script>
|
|
239
|
|
240 <form id="tool_form" name="tool_form" action="../../../tool_runner" target="galaxy_main" method="post" enctype="application/x-www-form-urlencoded">
|
|
241 <input type="hidden" name="refresh" value="refresh"/>
|
1
|
242 <input type="hidden" name="tool_id" value="%(target_form)s"/>
|
0
|
243 <input type="hidden" name="tool_state" value="%(form_state)s">
|
|
244 <input type="hidden" name="input" value="%(dataset_selection_id)s"/>
|
|
245 <input type="hidden" name="incl_excl" value="1"/>
|
|
246 """ % self.lookup
|
|
247
|
|
248 if len(self.empty_queries):
|
|
249 qnames = ''
|
|
250 for name in self.empty_queries: qnames += '<li>' + name + '</li>\n'
|
|
251 html += """
|
|
252 <div class="headerMessage">The following %s queries yielded 0 results (check filters):
|
|
253 <ul>
|
|
254 %s
|
|
255 </ul>
|
|
256 </div>""" % (len(self.empty_queries), qnames)
|
|
257
|
|
258 return html % self.lookup
|
|
259
|
|
260 # Repeated for each grouped section table display
|
|
261 def _sectionStart(self):
|
|
262 self.lookup['select_row'] +=1
|
|
263 return """
|
|
264 <div class="section section_depth%(section_depth)s">
|
|
265 <div class="section_title">%(label)s: %(value)s</div>
|
|
266 """ % self.lookup
|
|
267
|
|
268
|
|
269
|
|
270 def _sectionFormStart (self):
|
|
271 # This sets up the selection form with the minimal necessary parameters #
|
|
272 return """
|
|
273 <input type="checkbox" name="select" value="%(select_row)s" id="secId_%(select_row)s_start" checked="checked" style="display:none">
|
|
274 <input type="submit" class="btn btn-primary nonprintable" name="runtool_btn" value="Submit" onclick="formCheck()">
|
|
275 """ % self.lookup
|
|
276
|
|
277
|
|
278 def _tableStart (self):
|
|
279
|
|
280 return """
|
|
281 <div class="checkUncheckAllPlaceholder"></div>
|
|
282 <table class="report" id="secId_%(select_row)s">
|
|
283 %(table_header)s
|
|
284 """ % self.lookup
|
|
285
|
|
286
|
|
287 def _tableHeader(self):
|
|
288
|
|
289 colTags = ''
|
|
290 thTags = ''
|
|
291 firstColumnFlag = True
|
|
292 # Style numeric fields
|
|
293 for field in self.columns:
|
|
294 if field['group'] == 'column':
|
|
295 colTags += ('<col />' if field['type'] == 'text' else '<col class="numeric" />')
|
|
296 if firstColumnFlag == True:
|
|
297 thTags += '''<th>
|
|
298 <input type="checkbox" class="sectionCheckbox nonprintable" value="1"/>
|
|
299 ''' + field['label'] + '</th>'
|
|
300 firstColumnFlag = False
|
|
301 else:
|
|
302 thTags += '<th>' + field['label'] + '</th>'
|
|
303
|
|
304 return """
|
|
305 <colgroup>
|
|
306 %s
|
|
307 </colgroup>
|
|
308 <thead class="top">
|
|
309 <tr>%s</tr>
|
|
310 </thead>""" % (colTags, thTags)
|
|
311
|
|
312 def _tbodyHeader (self):
|
|
313 if self.lookup['value'] == '': self.lookup['value'] = '(no match)'
|
|
314 return """
|
|
315 <thead class="inside">
|
|
316 <tr>
|
|
317 <th colspan="%(column_count)s">%(label)s: %(value)s</th>
|
|
318 </tr>
|
|
319 </thead>""" % self.lookup
|
|
320
|
|
321 def _tbodyStart (self):
|
|
322 return """
|
|
323 <tbody>""" % self.lookup
|
|
324
|
|
325
|
|
326 def _tableRow(self):
|
|
327 self.lookup['select_row'] +=1
|
|
328
|
|
329 tdTags = ''
|
|
330 for (col, field) in enumerate(self.display_columns):
|
|
331 value = self.rowdata[col]
|
|
332 self.lookup['value'] = value
|
|
333 self.lookup['cssClass'] = ' class="numeric"' if field['type'] == 'numeric' else ''
|
|
334 #See http://www.ncbi.nlm.nih.gov/books/NBK21091/table/ch18.T.refseq_accession_numbers_and_mole/?report=objectonly
|
|
335 #See http://www.ncbi.nlm.nih.gov/Sequin/acc.html
|
|
336 accessionID = re.search(r'[a-z]+[_]?[0-9]+(.[0-9]+)*' ,value, re.I)
|
|
337 if (accessionID) :
|
|
338 self.lookup['link'] = '<a href="https://google.com/#q=%s+gene" target="search">%s</a>' % (accessionID.group(), value)
|
|
339 else:
|
|
340 self.lookup['link'] = value
|
|
341 # First column optionally gets bin indicator as well as row checkbox selector
|
|
342 if (col == 0):
|
|
343 tdTags += '<td%(cssClass)s><input type="checkbox" name="select" value="%(select_row)s" />%(link)s<span class="super">%(row_bins)s</span></td>' % self.lookup
|
|
344 else:
|
|
345 tdTags += '<td%(cssClass)s>%(value)s</td>' % self.lookup
|
|
346
|
|
347 return """\n\t\t\t<tr>%s</tr>""" % tdTags
|
|
348
|
|
349
|
|
350 def _tbodyEnd (self):
|
|
351 return """
|
|
352 </tbody>"""
|
|
353
|
|
354 def _tableEnd (self):
|
|
355 if len(self.section_bins):
|
|
356 bins = []
|
|
357 for key in sorted(self.section_bins):
|
|
358 bins.append( '<span class="super">(%s)</span>%s' % (key, self.section_bins[key]) )
|
|
359 self.lookup['section_bins'] = 'Bins: ' + ', '.join(bins)
|
|
360 else:
|
|
361 self.lookup['section_bins'] = ''
|
|
362
|
|
363 return """
|
|
364 <tfoot>
|
|
365 <tr>
|
|
366 <td colspan="%(column_count)s">
|
|
367 <div class="footerCenter">
|
|
368 %(filters)s.
|
|
369 </div>
|
|
370 <div class="footerLeft">
|
|
371 <span class="rowViewer0"></span> %(table_rows)s results.
|
|
372 <span class="rowViewer1 nonprintable"></span>
|
|
373 %(section_bins)s
|
|
374 </div>
|
|
375 <div class="footerRight">
|
|
376 Report produced on %(timestamp)s
|
|
377 </div>
|
|
378
|
|
379 </td>
|
|
380 </tr>
|
|
381 </tfoot>
|
|
382 </table>""" % self.lookup
|
|
383
|
|
384 def _sectionFormEnd (self):
|
|
385 return """
|
|
386
|
|
387 """
|
|
388
|
|
389 def _sectionEnd (self):
|
|
390 return """
|
|
391
|
|
392 </div>"""
|
|
393
|
|
394
|
|
395 def _bodyEnd (self):
|
|
396
|
|
397 return """
|
|
398 </form>\n\t</body>\n</html>"""
|
|
399
|
|
400
|