0
|
1 import os.path
|
|
2 import time
|
|
3 import csv
|
|
4 import collections
|
|
5 import re
|
|
6
|
|
7 HTML_REPORT_HEADER_FILE = 'html_report_header.html'
|
|
8
|
|
9 class HTMLReport(object):
|
|
10
|
|
11 """ This receives and sets up the general meta-data fields available to the html rendering engine
|
|
12
|
|
13 """
|
|
14 def __init__(self, tagGroup, options, query_stats = []):
|
|
15
|
|
16 self.columns = tagGroup.columns
|
|
17 self.display_columns = [field for field in self.columns if field['group']=='column']
|
|
18 self.row_limit = options.row_limit
|
|
19 self.section_bins = {}
|
|
20 self.todo = collections.deque([]) # stack of things to do
|
|
21 self.query_stats = query_stats
|
|
22 self.empty_queries = [query['id'] for query in query_stats if query['filtered_rows'] == 0]
|
|
23 self.initialized = False
|
|
24 self.errorNotice = ''
|
|
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':'',
|
|
42 'section_counter':1
|
|
43 }
|
|
44
|
|
45 self.initialized = True
|
|
46
|
|
47 #else:
|
|
48 # add error notice for non-initialized template:
|
|
49 # self.errorNotice = '<div style="width:400px;margin:auto"><h3>This HTML Report could not be initialized ...</h3></div>'
|
|
50
|
|
51
|
|
52 """
|
|
53 _processTagStack()
|
|
54 In the production of html, start tags and template bits are added to the outgoing html when designated section columns of data change value. the self.todo stack keeps track of all the tag closings that have to occur when a section or table section comes to an end (and new one begins or end of document occurs).
|
|
55
|
|
56 This dynamically executes any functions listed in stack that are greater than given tag depth.
|
|
57 Generally the functions return html closing content.
|
|
58
|
|
59 @param depth integer >= 0
|
|
60 @uses self.todo stack of [depth, function_name] items
|
|
61 """
|
|
62 def _processTagStack(self, depth = 0):
|
|
63 html = ''
|
|
64 while len(self.todo) and self.todo[0][0] >= depth:
|
|
65 html += getattr(self, self.todo.popleft()[1] )()
|
|
66 return html
|
|
67
|
|
68
|
|
69
|
|
70 ############################### HTML REPORT RENDERING ##############################
|
|
71 """ render() produces the html. Takes in tabular data + metainformation about that file, and iterates through rows. This approach depends on detecting changes in stated report section columns and table section columns, and triggers appropriate section start and end, and table / table section start and end tags.
|
|
72
|
|
73 @param in_file string Full file path
|
|
74 @param out_html_file string Full output html data file path to write to.
|
|
75 """
|
|
76 def render (self, in_file, out_html_file):
|
|
77
|
|
78 try:
|
|
79
|
|
80 fp_in = open(in_file, "rb")
|
|
81 fp_out = open(out_html_file, 'w')
|
|
82
|
|
83 fp_out.write( self._header(HTML_REPORT_HEADER_FILE) )
|
|
84
|
|
85 if self.initialized:
|
|
86
|
|
87 fp_out.write( self._bodyStart() )
|
|
88 self.todo.appendleft([0,'_bodyEnd'])
|
|
89
|
|
90
|
|
91 reader = csv.reader(fp_in, delimiter="\t")
|
|
92
|
|
93 for row in reader:
|
|
94
|
|
95 html = ''
|
|
96 self.rowdata = []
|
|
97 row_bins = []
|
|
98 section_reset = False
|
|
99
|
|
100 for (idx, field) in enumerate(self.columns):
|
|
101
|
|
102 value = field['value'] = row[idx]
|
|
103 depth = idx + 1
|
|
104
|
|
105 # If a bin is mentioned on this row, its put into self.selection_bins.
|
|
106 if field['type'] == 'bin' and value != '':
|
|
107 row_bins.append(value)
|
|
108 if not value in self.section_bins:
|
|
109 self.section_bins[value] = field['label']
|
|
110
|
|
111 grouping = field['group']
|
|
112 # Section or table grouping here:
|
|
113 if grouping == 'section' or grouping == 'table':
|
|
114
|
|
115 # Check to see if a new section or table section is triggered by change in field's value:
|
|
116 if section_reset or (not 'valueOld' in field) or value != field['valueOld']:
|
|
117
|
|
118 self.lookup['value'] = value
|
|
119 self.lookup['label'] = field['label']
|
|
120
|
|
121 html += self._processTagStack(depth)
|
|
122
|
|
123 if grouping == 'section':
|
|
124 section_reset = True
|
|
125 self.lookup['section_depth'] = depth
|
|
126 self.lookup['section_counter'] += 1
|
|
127 self.lookup['table_rows'] = 0
|
|
128 self.section_bins = {}
|
|
129
|
|
130 html += self._sectionStart()
|
|
131
|
|
132 html += self._sectionFormStart()
|
|
133 self.todo.appendleft([depth,'_sectionFormEnd'])
|
|
134
|
|
135 self.todo.appendleft([depth,'_sectionEnd'])
|
|
136
|
|
137
|
|
138 elif grouping == 'table':
|
|
139
|
|
140 lastToDo = self.todo[0]
|
|
141 if lastToDo[1] == '_sectionEnd':
|
|
142 html += self._tableStart()
|
|
143 self.todo.appendleft([lastToDo[0]+1,'_tableEnd'])
|
|
144
|
|
145 html += self._tbodyHeader() + self._tbodyStart()
|
|
146 self.todo.appendleft([lastToDo[0]+2,'_tbodyEnd'])
|
|
147
|
|
148 field['valueOld'] = value
|
|
149
|
|
150 else:
|
|
151
|
|
152 if grouping == 'column': self.rowdata.append(row[idx])
|
|
153
|
|
154 lastToDo = self.todo[0]
|
|
155 # No table level, instead going right from section to column field:
|
|
156 if lastToDo[1] == '_sectionEnd':
|
|
157 html += self._tableStart() + self._tbodyStart()
|
|
158 self.todo.appendleft([lastToDo[0]+1,'_tableEnd'])
|
|
159 self.todo.appendleft([lastToDo[0]+2,'_tbodyEnd'])
|
|
160
|
|
161 self.lookup['row_bins'] = ",".join(row_bins)
|
|
162 fp_out.write(html)
|
|
163 self.lookup['table_rows'] += 1
|
|
164 # Now output table row of data:
|
|
165 fp_out.write( self._tableRow() )
|
|
166
|
|
167
|
|
168 #Not initialized here, so just write created error notice.
|
|
169 else:
|
|
170
|
|
171 fp_out.write('<body><h3>' + self.errorNotice + '</h3></body></html>')
|
|
172
|
|
173 fp_out.write( self._processTagStack() )
|
|
174
|
|
175 except IOError as e:
|
|
176 print 'Operation failed: %s' % e.strerror
|
|
177
|
|
178 fp_in.close()
|
|
179 fp_out.close()
|
|
180
|
|
181
|
|
182 ############################### HTML REPORT PART TEMPLATES ##############################
|
|
183 def _header(self, filename):
|
|
184
|
|
185 with open(os.path.join(os.path.dirname(__file__), filename), "r") as fphtml:
|
|
186 data = fphtml.read()
|
|
187
|
|
188 return data
|
|
189
|
|
190
|
|
191 def _bodyStart(self):
|
|
192 # 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.
|
|
193 html = """
|
|
194 """
|
|
195 if len(self.empty_queries):
|
|
196 qnames = ''
|
|
197 for name in self.empty_queries: qnames += '<li>' + name + '</li>\n'
|
|
198 html += """
|
|
199 <div class="headerMessage">The following queries yielded 0 results (check filters):
|
|
200 <ul>
|
|
201 %s
|
|
202 </ul>
|
|
203 </div>""" % qnames
|
|
204
|
|
205 return html % self.lookup
|
|
206
|
|
207 # Repeated for each grouped section table display
|
|
208 def _sectionStart(self):
|
|
209 self.lookup['select_row'] +=1
|
|
210 return """
|
|
211 <div class="section section_depth%(section_depth)s">
|
|
212 <div class="section_title">%(label)s: %(value)s</div>
|
|
213 """ % self.lookup
|
|
214
|
|
215
|
|
216
|
|
217 def _sectionFormStart (self):
|
|
218 # This sets up the selection form #../../../tool_runner/index
|
|
219 return ""
|
|
220
|
|
221 def _tableStart (self):
|
|
222
|
|
223 return """
|
|
224 <table class="report">
|
|
225 %(table_header)s
|
|
226 """ % self.lookup
|
|
227
|
|
228
|
|
229 def _tableHeader(self):
|
|
230
|
|
231 colTags = '' # Style numeric fields
|
|
232 thTags = ''
|
|
233
|
|
234 for field in self.columns:
|
|
235 if field['group'] == 'column':
|
|
236 colTags += ('<col />' if field['type'] == 'text' else '<col class="numeric" />')
|
|
237 thTags += '<th>' + field['label'] + '</th>'
|
|
238
|
|
239 return """
|
|
240 <colgroup>
|
|
241 %s
|
|
242 </colgroup>
|
|
243 <thead class="top">
|
|
244 <tr>%s</tr>
|
|
245 </thead>""" % (colTags, thTags)
|
|
246
|
|
247 def _tbodyHeader (self):
|
|
248 if self.lookup['value'] == '': self.lookup['value'] = '(no match)'
|
|
249 return """
|
|
250 <thead class="inside">
|
|
251 <tr>
|
|
252 <th colspan="%(column_count)s">%(label)s: %(value)s</th>
|
|
253 </tr>
|
|
254 </thead>""" % self.lookup
|
|
255
|
|
256 def _tbodyStart (self):
|
|
257 return """
|
|
258 <tbody>""" % self.lookup
|
|
259
|
|
260
|
|
261 def _tableRow(self):
|
|
262 self.lookup['select_row'] +=1
|
|
263
|
|
264 tdTags = ''
|
|
265 for (col, field) in enumerate(self.display_columns):
|
|
266 value = self.rowdata[col]
|
|
267 self.lookup['value'] = value
|
|
268 self.lookup['cssClass'] = ' class="numeric"' if field['type'] == 'numeric' else ''
|
|
269 accessionID = re.search(r'[a-z]+[0-9]+(.[0-9]+)*' ,value, re.I)
|
|
270 if (accessionID) :
|
|
271 self.lookup['link'] = '<a href="https://google.ca/#q=%s+gene" target="search">%s</a>' % (accessionID.group(), value)
|
|
272 else:
|
|
273 self.lookup['link'] = value
|
|
274 # First column optionally gets bin indicator as well as row checkbox selector
|
|
275 if (col == 0):
|
|
276 tdTags += '<td%(cssClass)s>%(link)s<span class="super">%(row_bins)s</span></td>' % self.lookup
|
|
277 else:
|
|
278 tdTags += '<td%(cssClass)s>%(value)s</td>' % self.lookup
|
|
279
|
|
280 return """\n\t\t\t<tr>%s</tr>""" % tdTags
|
|
281
|
|
282 def _tbodyEnd (self):
|
|
283 return """
|
|
284 </tbody>"""
|
|
285
|
|
286 def _tableEnd (self):
|
|
287 if len(self.section_bins):
|
|
288 bins = []
|
|
289 for key in sorted(self.section_bins):
|
|
290 bins.append( '<span class="super">(%s)</span>%s' % (key, self.section_bins[key]) )
|
|
291 self.lookup['section_bins'] = 'Bins: ' + ', '.join(bins)
|
|
292 else:
|
|
293 self.lookup['section_bins'] = ''
|
|
294
|
|
295 return """
|
|
296 <tfoot>
|
|
297 <tr>
|
|
298 <td colspan="%(column_count)s">
|
|
299 <div class="footerCenter">
|
|
300 %(filters)s.
|
|
301 </div>
|
|
302 <div class="footerLeft">
|
|
303 <span class="rowViewer0"></span> %(table_rows)s results.
|
|
304 <span class="rowViewer1 nonprintable"></span>
|
|
305 %(section_bins)s
|
|
306 </div>
|
|
307 <div class="footerRight">
|
|
308 Report produced on %(timestamp)s
|
|
309 </div>
|
|
310
|
|
311 </td>
|
|
312 </tr>
|
|
313 </tfoot>
|
|
314 </table>""" % self.lookup
|
|
315
|
|
316 def _sectionFormEnd (self):
|
|
317 return """
|
|
318
|
|
319 """
|
|
320
|
|
321 def _sectionEnd (self):
|
|
322 return """
|
|
323 </div>"""
|
|
324
|
|
325
|
|
326 def _bodyEnd (self):
|
|
327
|
|
328 return """\n\t</body>\n</html>"""
|
|
329
|