~launchpad-p-s/sofastatistics/main

« back to all changes in this revision

Viewing changes to dimtables.py

  • Committer: Grant Paton-Simpson
  • Date: 2009-05-19 04:21:43 UTC
  • Revision ID: g@ubuntu-20090519042143-p561mbokz3inefvd
Initial import

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
import tree
 
2
import numpy
 
3
from operator import itemgetter
 
4
import pprint
 
5
import getdata
 
6
 
 
7
"""
 
8
v1.1 - shifted getHTML settings for header and footer into separate methods
 
9
    to simplify interface when more complex setup.
 
10
    Can now select css on a table-by-table basis.
 
11
"""
 
12
ROWDIM = "row" #double as labels
 
13
COLDIM = "column"
 
14
#actual options selected ...
 
15
SORT_NONE = "None" #double as labels
 
16
SORT_LABEL = "By Label"
 
17
SORT_FREQ_ASC = "By Freq (Asc)"
 
18
SORT_FREQ_DESC = "By Freq (Desc)"
 
19
 
 
20
# can use content of constant as a short label
 
21
FREQ = "Freq"
 
22
ROWPCT = "Row %"
 
23
COLPCT = "Col %"
 
24
SUM = "Sum"
 
25
MEAN = "Mean"
 
26
MEDIAN = "Median"
 
27
SUMM_N = "N" # N used in Summary tables
 
28
STD_DEV = "Std Dev"
 
29
measures_long_label_dic = {FREQ: "Frequency", 
 
30
                           ROWPCT: "Row %",
 
31
                           COLPCT: "Column %",
 
32
                           SUM: "Sum", 
 
33
                           MEAN: "Mean",
 
34
                           MEDIAN: "Median", 
 
35
                           SUMM_N: "N",
 
36
                           STD_DEV: "Standard Deviation"}
 
37
# content of constant and constant (ready to include in exported script)
 
38
# e.g. "dimtables.%s" "ROWPCT"
 
39
script_export_measures_dic = {FREQ: "FREQ", 
 
40
                              ROWPCT: "ROWPCT",
 
41
                              COLPCT: "COLPCT",
 
42
                              SUM: "SUM", 
 
43
                              MEAN: "MEAN",
 
44
                              MEDIAN: "MEDIAN", 
 
45
                              SUMM_N: "SUMM_N",
 
46
                              STD_DEV: "STD_DEV"}
 
47
#NOTNULL = " NOT ISNULL(%s) "
 
48
NOTNULL = " %s IS NOT NULL "
 
49
 
 
50
DEF_CSS = r"c:\mypy\tbl_css\css_default.txt"
 
51
 
 
52
def pct_1_dec(num):
 
53
    return "%s%%" % round(num,1)
 
54
def pct_2_dec(num):
 
55
    return "%s%%" % round(num,2)
 
56
data_format_dic = {FREQ: str, ROWPCT: pct_1_dec, COLPCT: pct_1_dec}
 
57
 
 
58
class DimNodeTree(tree.NodeTree):
 
59
    """
 
60
    A specialist tree for storing dimension nodes.
 
61
    Sets the root node up as a DimNode.
 
62
    """    
 
63
    def __init__(self, measures=None):
 
64
        ""
 
65
        self.root_node = DimNode(label="Root", measures=measures)
 
66
        self.root_node.level = 0
 
67
 
 
68
    def addChild(self, child_node):
 
69
        "Update filt_flds to cover all fields in ancestral line"
 
70
        #super(tree.NodeTree, self).addChild(child_node)
 
71
        tree.NodeTree.addChild(self, child_node)
 
72
        child_node.filt_flds = [child_node.fld] #may be None
 
73
 
 
74
class LabelNodeTree(tree.NodeTree):
 
75
    """
 
76
    A specialist tree for storing label nodes.
 
77
    Sets the root node up as a LabelNode.
 
78
    """    
 
79
    def __init__(self):
 
80
        ""
 
81
        self.root_node = LabelNode(label="Root")
 
82
        self.root_node.level = 0
 
83
        
 
84
class DimNode(tree.Node):
 
85
    """
 
86
    A specialist node for recording table dimension (row or column)
 
87
    data.
 
88
    fld is optional for use in columns because sometimes we just want 
 
89
        measures there e.g. freq, or summary measures such as mean, 
 
90
        median etc.
 
91
    label - will use fld if no label supplied (and fld available) - e.g.
 
92
        fld=gender, fld.title() = Gender.
 
93
    labels - a dictionary of labels e.g. {"1": "Male", "2": "Female"}
 
94
    measures - e.g. FREQ
 
95
    has_tot - boolean
 
96
    sort_order - dimtables.SORT_NONE, dimtables.SORT_LABEL, 
 
97
        dimtables.SORT_FREQ_ASC, dimtables.SORT_FREQ_DESC
 
98
    bolnumeric - so can set up filters correctly e.g. gender = "1" or 
 
99
        gender = 1 as appropriate
 
100
    """
 
101
    def __init__(self, fld=None, label="", labels=None, measures=None, 
 
102
                 has_tot=False, sort_order=SORT_NONE, bolnumeric=False):
 
103
        ""
 
104
        self.fld = fld
 
105
        self.filt_flds = [] #only built up when added as a child to another DimNode
 
106
        if not label and fld != None:
 
107
            self.label = fld.title()
 
108
        else:
 
109
            self.label = label
 
110
        if not labels:
 
111
            self.labels = {}
 
112
        else:
 
113
            self.labels = labels
 
114
        if not measures:
 
115
            self.measures = []
 
116
        else:
 
117
            self.measures = measures
 
118
        self.has_tot = has_tot
 
119
        self.sort_order = sort_order
 
120
        self.bolnumeric = bolnumeric
 
121
        tree.Node.__init__(self, dets_dic=None, label=self.label)
 
122
 
 
123
    def addChild(self, child_node):
 
124
        "Update filt_flds to cover all fields in ancestral line"
 
125
        #super(tree.Node, self).addChild(child_node)
 
126
        tree.Node.addChild(self, child_node)
 
127
        child_node.filt_flds = self.filt_flds + [child_node.fld]
 
128
 
 
129
class LabelNode(tree.Node):
 
130
    """
 
131
    A specialist node for recording table label data for a given dimension 
 
132
    (row or column).
 
133
    label - the most important data of all - what to display for this node
 
134
    filts - a list of all the filter clauses inherited from the ancestral 
 
135
        line e.g. gender=1, eth=3
 
136
    measure - if this is a terminal node, a single measure must be 
 
137
        specified e.g. FREQ
 
138
    is_coltot - used for calculations of data values
 
139
    """
 
140
    
 
141
    def __init__(self, label="", filts=None, measure=None, 
 
142
                 is_coltot=False):
 
143
        ""
 
144
        """filt_flds is only filled if this is a terminal node.  
 
145
        It is filled when the label nodes tree is being built 
 
146
        from the dim node tree node (which is where we get it from)"""
 
147
        self.filt_flds = [] 
 
148
        if not filts:
 
149
            self.filts = []
 
150
        else:
 
151
            self.filts = filts
 
152
        self.measure = measure
 
153
        self.is_coltot = is_coltot
 
154
        #super(tree.Node, self).__init__(dets_dic=None, label=self.label)
 
155
        tree.Node.__init__(self, dets_dic=None, label=label)
 
156
 
 
157
    def __str__(self):
 
158
        return self.level*2*" " + "Level: " + str(self.level) + \
 
159
            "; Label: " + self.label + \
 
160
            "; Measure: " + (self.measure if self.measure else "None") + \
 
161
            "; Col Total?: " + ("Yes" if self.is_coltot else "No") + \
 
162
            "; Child labels: " + ", ".join([x.label for x in self.children])
 
163
 
 
164
class DimTable(object):
 
165
    """
 
166
    Functionality that applies to both demo and live tables
 
167
    """
 
168
    def processHdrTree(self, tree_col_labels, row_label_cols_n):
 
169
        """
 
170
        Set up titles, subtitles, and col labels into table header.
 
171
        """
 
172
        #print tree_col_labels #debug
 
173
        col_label_rows_n = tree_col_labels.getDepth()
 
174
        col_label_rows_lst = [["<tr>"] for x in range(col_label_rows_n)]
 
175
        #title/subtitle etc share their own row
 
176
        titles_html = "\n<p class='tbltitle'>"
 
177
        for title in self.titles:
 
178
            titles_html += "%s<br>" % title
 
179
        titles_html += "</p>"
 
180
        if self.subtitles != [""]:
 
181
            subtitles_html = "\n<p class='tblsubtitle'>"
 
182
            for subtitle in self.subtitles:
 
183
                subtitles_html += "%s<br>" % subtitle
 
184
            subtitles_html += "</p>"
 
185
        else:
 
186
            subtitles_html = ""
 
187
        title_dets_html = titles_html + subtitles_html
 
188
        col_label_rows_lst[0].append("<th class='tbltitlecell' " + \
 
189
                                     "colspan='%s'>%s</th>" % \
 
190
            (len(tree_col_labels.getTerminalNodes()) + row_label_cols_n, 
 
191
             title_dets_html))
 
192
        #start off with spaceholder heading cell
 
193
        col_label_rows_lst[1].append("<th class='firstcolvar' rowspan='%s' " % \
 
194
            (tree_col_labels.getDepth() - 1) + "colspan='%s'>&nbsp;&nbsp;</th>" % \
 
195
            row_label_cols_n)
 
196
        col_label_rows_lst = self.colLabelRowBuilder(\
 
197
                        node=tree_col_labels.root_node,
 
198
                        col_label_rows_lst=col_label_rows_lst, 
 
199
                        col_label_rows_n=col_label_rows_n, row_offset=0)
 
200
        
 
201
        hdr_html = "\n<thead>"
 
202
        for row in col_label_rows_lst:
 
203
            #flatten row list
 
204
            hdr_html += "\n" + "".join(row) + "</tr>"
 
205
        hdr_html += "\n</thead>"
 
206
        #print tree_col_labels
 
207
        return (tree_col_labels, hdr_html)
 
208
      
 
209
    def processRowTree(self, tree_row_labels):
 
210
        "Turn row label tree into labels"
 
211
        #print tree_row_labels #debug
 
212
        row_label_cols_n = tree_row_labels.getDepth() - 1 #exclude root node
 
213
        row_label_rows_n = len(tree_row_labels.getTerminalNodes())
 
214
        row_label_rows_lst = [["<tr>"] for x in range(row_label_rows_n)]
 
215
        row_offset_dic = {}
 
216
        for i in range(row_label_cols_n):
 
217
            row_offset_dic[i]=0
 
218
        row_label_rows_lst = self.rowLabelRowBuilder(\
 
219
                        node=tree_row_labels.root_node,
 
220
                        row_label_rows_lst=row_label_rows_lst, 
 
221
                        row_label_cols_n=row_label_cols_n, 
 
222
                        row_offset_dic=row_offset_dic, col_offset=0)
 
223
        return (row_label_rows_lst, tree_row_labels, row_label_cols_n)       
 
224
 
 
225
    def rowLabelRowBuilder(self, node, row_label_rows_lst, row_label_cols_n, 
 
226
                           row_offset_dic, col_offset=0):
 
227
        """
 
228
        Adds cells to the row label rows list as it goes through all nodes.
 
229
            NB nodes are not processed level by level but from from 
 
230
            parent to child.
 
231
        Which row do we add a cell to?  It depends entirely on the 
 
232
            row offset for the level concerned.  (NB colspanning doesn't 
 
233
            affect the which row a cell goes in, or in which order it appears 
 
234
            in the row.)
 
235
        So we need a row_offset_dic with a key for each level and a value
 
236
            which represents the offset (which is updated as we pass through 
 
237
            siblings).  If a cell for level X needs to span Y rows
 
238
            we add Y to the value for row_offset_dic[X].
 
239
        As for colspanning, we need to know how many cols have been
 
240
            filled already, and how many cols there are to come to the right.
 
241
        If there is a gap, colspan the cell to cover it, and increase the
 
242
            col_offset being passed down the subtree.
 
243
        node - the node we are adding a cell to the table based upon.
 
244
        row_label_rows_lst - one row per row in row label section        
 
245
        row_label_cols_n - number of cols in row label section        
 
246
        row_offset_dic - keeps track of row position for sibling cells
 
247
            according to how much its previous siblings have spanned.
 
248
            Zero-based index with as many items as the depth of tree 
 
249
            (including root).  Index 0 is never used.
 
250
        col_offset - amount of colspanning which has occurred prior
 
251
            to the cell.  Need to know so terminal nodes all appear
 
252
            at same rightwards position regardless of subtree depth.
 
253
        Format cells according to whether variable or value.  Even level
 
254
            = value, odd level = variable.
 
255
        """
 
256
        #print node #debug
 
257
        level = node.level
 
258
        if level > 0: # skip adding cells for root node itself
 
259
            row_offset = level - 1 # e.g. first row level is 0
 
260
            row_idx = row_offset_dic[row_offset]
 
261
            rowspan_n = len(node.getTerminalNodes())
 
262
            row_offset_dic[row_offset] = row_idx + rowspan_n # leave for next sibling
 
263
            # cell dimensions
 
264
            if rowspan_n > 1:
 
265
                rowspan = " rowspan='%s' " % rowspan_n
 
266
            else:
 
267
                rowspan = "" 
 
268
            cols_filled = level + col_offset
 
269
            cols_to_fill = row_label_cols_n - cols_filled
 
270
            cols_to_right = node.getDepth() - 1 # exclude self
 
271
            gap = cols_to_fill - cols_to_right            
 
272
            col_offset += gap
 
273
            if gap > 0:
 
274
                colspan = " colspan='%s' " % (1 + gap,)
 
275
            else:
 
276
                colspan = ""
 
277
            # styling
 
278
            if cols_to_right % 2 > 0: #odd
 
279
                if cols_filled == 1:
 
280
                    cellclass="class='firstrowvar'"
 
281
                else:
 
282
                    cellclass="class='rowvar'"
 
283
            else:
 
284
                cellclass="class='rowval'"
 
285
            row_label_rows_lst[row_idx].append("<td %s %s %s>%s</td>" % \
 
286
                                (cellclass, rowspan, colspan, node.label))
 
287
        for child in node.children:
 
288
            row_label_rows_lst = self.rowLabelRowBuilder(child, 
 
289
                                    row_label_rows_lst, row_label_cols_n, 
 
290
                                    row_offset_dic, col_offset)
 
291
        # finish level, set all child levels to start with this one's final offset
 
292
        # Otherwise Gender, Gender->Asst problem (whereas Gender->Asst, Gender is fine)
 
293
        if level > 0: # don't do this on the root
 
294
            for i in range(row_offset + 1, row_label_cols_n):
 
295
                row_offset_dic[i] = row_offset_dic[row_offset]
 
296
        return row_label_rows_lst
 
297
    
 
298
    def colLabelRowBuilder(self, node, col_label_rows_lst, col_label_rows_n, 
 
299
                           row_offset=0):
 
300
        """
 
301
        Adds cells to the column label rows list as it goes through all nodes.
 
302
        Add cells to the correct row which means that the first cell
 
303
        in a subtree which is shorter than the maximum for the table
 
304
        must have an increased rowspan + pass on a row offset to all its
 
305
        children.
 
306
        
 
307
        node - the node we are adding a cell to the table based upon.
 
308
        col_label_rows_lst - one row per row in column label header        
 
309
        col_label_rows_n - number of rows in column label header        
 
310
        row_offset - number of rows downwards to be put so terminal nodes
 
311
            all appear at same level regardless of subtree depth.
 
312
 
 
313
        Add cell for node.
 
314
        Any gap between rows in table header below (which we are filling)
 
315
        and depth of nodes below (with which we fill the table header)?
 
316
        If so, increase rowspan of this cell + increase row offset by 
 
317
        appropriate amount so that the subsequent cells are added
 
318
        to the correct col label row.
 
319
        
 
320
        Format cells according to whether variable or value.  
 
321
        For General Tables, odd number of levels below = value, 
 
322
        even = variable.  For Summary Tables, vv.
 
323
        """
 
324
        rows_filled = node.level + 1 + row_offset
 
325
        rows_to_fill = col_label_rows_n - rows_filled
 
326
        rows_below = node.getDepth() - 1 # exclude self
 
327
        gap = rows_to_fill - rows_below
 
328
        # styling
 
329
        if self.has_col_measures:
 
330
            if rows_below == 0:
 
331
                cellclass="class='measure'"
 
332
            elif rows_below % 2 > 0: # odd
 
333
                cellclass="class='colval'"
 
334
            else:
 
335
                if rows_filled == 2:
 
336
                    cellclass="class='firstcolvar'"
 
337
                else:
 
338
                    cellclass="class='colvar'"
 
339
        else:
 
340
            if rows_below % 2 == 0: # even
 
341
                cellclass="class='colval'"
 
342
            else:
 
343
                if rows_filled == 2:
 
344
                    cellclass="class='firstcolvar'"
 
345
                else:
 
346
                    cellclass="class='colvar'"
 
347
        # cell dimensions
 
348
        if gap > 0:
 
349
            rowspan = " rowspan='%s' " % (1 + gap,)
 
350
        else:
 
351
            rowspan = ""
 
352
        colspan_n = len(node.getTerminalNodes())
 
353
        if colspan_n > 1:
 
354
            colspan = " colspan='%s' " % colspan_n
 
355
        else:
 
356
            colspan = ""
 
357
        if node.level > 0: # skip root (we use that row for the title
 
358
            col_label_rows_lst[rows_filled - 1].append(\
 
359
                "<th %s %s %s>%s</th>" % (cellclass, rowspan, colspan, 
 
360
                                          node.label))
 
361
        row_offset += gap
 
362
        for child in node.children:
 
363
            col_label_rows_lst = self.colLabelRowBuilder(child, 
 
364
                                col_label_rows_lst, col_label_rows_n, 
 
365
                                row_offset)
 
366
        return col_label_rows_lst
 
367
    
 
368
    
 
369
class LiveTable(DimTable):
 
370
    """
 
371
    A Table with the ability to nest rows and columns, add totals to any 
 
372
    node, have multiple measures per terminal node e.g. freq, rowpct, 
 
373
    and colpct, etc etc.
 
374
    """
 
375
    
 
376
    def __init__(self, titles, dbe, datasource, cur, tree_rows, tree_cols, 
 
377
                 subtitles=None):
 
378
        """
 
379
        cur - must return tuples, not dictionaries
 
380
        """
 
381
        self.titles = titles
 
382
        if subtitles:
 
383
            self.subtitles = subtitles
 
384
        else:
 
385
            self.subtitles = []
 
386
        self.dbe = dbe
 
387
        self.if_clause, self.abs_wrapper_l, self.abs_wrapper_r = \
 
388
            getdata.getDbeSyntaxElements(self.dbe)
 
389
        self.datasource = datasource
 
390
        self.cur = cur
 
391
        self.tree_rows = tree_rows
 
392
        self.tree_cols = tree_cols
 
393
    
 
394
    def getDataCellN(self, tree_col_labels, tree_row_labels):
 
395
        ""
 
396
        col_term_nodes = tree_col_labels.getTerminalNodes()
 
397
        row_term_nodes = tree_row_labels.getTerminalNodes()
 
398
        data_cell_n = len(row_term_nodes) * len(col_term_nodes)
 
399
        return data_cell_n
 
400
    
 
401
    def prepTable(self):
 
402
        "Prepare table setup information towards generation of final html."
 
403
        (self.row_label_rows_lst, self.tree_row_labels, row_label_cols_n) = \
 
404
            self.getRowDets()
 
405
        self.tree_col_labels, self.hdr_html = self.getHdrDets(row_label_cols_n)
 
406
    
 
407
    def getCellNOk(self, max_cells=5000):
 
408
        """
 
409
        Returns False if too many cells to proceed (according to max_cells).
 
410
        Used to determine whether to proceed with table or not.
 
411
        """
 
412
        data_cell_n = self.getDataCellN(self.tree_col_labels, 
 
413
                                        self.tree_row_labels)
 
414
        return max_cells >= data_cell_n
 
415
    
 
416
    def getHTML(self, page_break_after=False):
 
417
        """
 
418
        Get HTML for table.
 
419
        """
 
420
        html = ""
 
421
        html += "<table cellspacing='0'>\n" # IE6 doesn't support CSS borderspacing
 
422
        (row_label_rows_lst, tree_row_labels, row_label_cols_n) = \
 
423
            self.getRowDets()
 
424
        (tree_col_dets, hdr_html) = self.getHdrDets(row_label_cols_n)
 
425
        row_label_rows_lst = self.getBodyHtmlRows(row_label_rows_lst,
 
426
                                                  tree_row_labels, 
 
427
                                                  tree_col_dets)
 
428
        body_html = "\n\n<tbody>"
 
429
        for row in row_label_rows_lst:
 
430
            #flatten row list
 
431
            body_html += "\n" + "".join(row) + "</tr>"
 
432
        body_html += "\n</tbody>"
 
433
        html += hdr_html
 
434
        html += body_html
 
435
        html += "\n</table>"
 
436
        return html
 
437
    
 
438
    def getRowDets(self):
 
439
        """
 
440
        Return row_label_rows_lst - need combination of row and col filters
 
441
            to add the data cells to the table body rows.
 
442
        tree_row_labels - we collect row filters from this.
 
443
        row_label_cols_n - needed to set up header (need to span the 
 
444
            row labels).
 
445
        """
 
446
        tree_row_labels = LabelNodeTree()
 
447
        for child in self.tree_rows.root_node.children:
 
448
            self.addSubtreeToLabelTree(tree_dims_node=child, 
 
449
                                tree_labels_node=tree_row_labels.root_node,
 
450
                                dim=ROWDIM, 
 
451
                                oth_dim_root=self.tree_cols.root_node)
 
452
        return self.processRowTree(tree_row_labels)        
 
453
    
 
454
    def addSubtreesToColLabelTree(self, tree_col_labels):
 
455
        """
 
456
        Add subtrees to column label tree.
 
457
        If coltree has no children, must add a subtree underneath.
 
458
        """
 
459
        if self.tree_cols.root_node.children:
 
460
            for child in self.tree_cols.root_node.children:
 
461
                self.addSubtreeToLabelTree(tree_dims_node=child, 
 
462
                            tree_labels_node=tree_col_labels.root_node,
 
463
                            dim=COLDIM, 
 
464
                            oth_dim_root=self.tree_rows.root_node)
 
465
        else:
 
466
            self.addSubtreeToLabelTree(tree_dims_node=\
 
467
                               self.tree_cols.root_node, 
 
468
                               tree_labels_node=tree_col_labels.root_node,
 
469
                               dim=COLDIM, 
 
470
                               oth_dim_root=self.tree_rows.root_node)
 
471
        return tree_col_labels
 
472
          
 
473
    def addSubtreeToLabelTree(self, tree_dims_node, tree_labels_node, 
 
474
                              dim, oth_dim_root):
 
475
        """
 
476
        Based on information from the variable node, add a subtree
 
477
        to the node supplied from the labels tree (if appropriate).
 
478
        """
 
479
        has_fld = tree_dims_node.fld #None or a string        
 
480
        filt_flds = tree_dims_node.filt_flds
 
481
        if dim == ROWDIM:
 
482
            if not has_fld:
 
483
                raise Exception, "All row nodes must have a variable " + \
 
484
                    "field specified"
 
485
            if self.has_row_vals:
 
486
                self.addSubtreeIfVals(tree_dims_node, tree_labels_node, 
 
487
                                  oth_dim_root, dim, filt_flds)
 
488
            else:
 
489
                self.addSubtreeMeasuresOnly(tree_dims_node, 
 
490
                                            tree_labels_node, 
 
491
                                            filt_flds)            
 
492
        elif dim == COLDIM:
 
493
            if has_fld:
 
494
                self.addSubtreeIfVals(tree_dims_node, tree_labels_node, 
 
495
                                  oth_dim_root, dim, filt_flds)            
 
496
            else:
 
497
                if self.has_col_measures:
 
498
                    self.addColMeasuresSubtreeIfNoFld(tree_dims_node, 
 
499
                                                  tree_labels_node)                
 
500
 
 
501
    def addSubtreeIfVals(self, tree_dims_node, tree_labels_node, 
 
502
                         oth_dim_root, dim, filt_flds):
 
503
        """
 
504
        If the var node has values to display (if any found in data)
 
505
        (i.e. must have a field not be a summary table row), 
 
506
        the subtree will have two initial 
 
507
        levels - 1) a node for the variable itself
 
508
        (storing labels in its dets_dic),
 
509
        and 2) a set of values nodes - one for each value plus
 
510
        one for the total (if appropriate).
 
511
        Then we need to follow the subtree down a level below each 
 
512
        of the values nodes (assuming the tree_dims_node has any children).        
 
513
        
 
514
        To display a cell, we must know that there will be at least 
 
515
        one descendant cell to show underneath it.              
 
516
        We do this by filtering the raw data by the appropriate row 
 
517
        and column filters.  If any records remain, we can show the 
 
518
        cell.
 
519
        """
 
520
        val_labels = tree_dims_node.labels
 
521
        bolnumeric = tree_dims_node.bolnumeric
 
522
        fld = tree_dims_node.fld
 
523
        #print tree_dims_node #debug        
 
524
        final_filt_clause = self.getValsFiltClause(tree_dims_node, 
 
525
                                                   tree_labels_node,
 
526
                                                   oth_dim_root)
 
527
        SQL_get_vals = "SELECT " + fld + ", COUNT(*)" + \
 
528
            " FROM " + self.datasource + \
 
529
            " WHERE " + final_filt_clause + \
 
530
            " GROUP BY " + fld
 
531
        #print SQL_get_vals #debug
 
532
        self.cur.execute(SQL_get_vals)
 
533
        #get vals and their frequency (across all the other dimension)
 
534
        val_freq_label_lst = [(val, val_freq, \
 
535
                              val_labels.get(val, str(val))) \
 
536
                              for (val, val_freq) in self.cur.fetchall()]
 
537
        # [(val, freq, val_label), ...]  
 
538
        #http://www.python.org/dev/peps/pep-0265/
 
539
        if tree_dims_node.sort_order == SORT_FREQ_ASC:
 
540
            val_freq_label_lst.sort(key=itemgetter(1)) #sort asc by freq
 
541
        elif tree_dims_node.sort_order == SORT_FREQ_DESC:
 
542
            val_freq_label_lst.sort(key=itemgetter(1), reverse=True) #desc
 
543
        elif tree_dims_node.sort_order == SORT_LABEL:
 
544
            val_freq_label_lst.sort(key=itemgetter(2)) #sort by label
 
545
        if not val_freq_label_lst:
 
546
            return #do not add subtree - no values
 
547
        else:
 
548
            #add level 1 to data tree - the var
 
549
            node_lev1 = tree_labels_node.addChild(LabelNode(label=\
 
550
                                                tree_dims_node.label))
 
551
            if tree_dims_node.has_tot:
 
552
                #freq not needed now that sorting has already occurred
 
553
                val_freq_label_lst.append(("_tot_", 0, "TOTAL"))
 
554
            terminal_var = not tree_dims_node.children
 
555
            if terminal_var:
 
556
                var_measures = tree_dims_node.measures
 
557
                if not var_measures:
 
558
                    var_measures = [FREQ]
 
559
            for val, val_freq, val_label in val_freq_label_lst:
 
560
                """add level 2 to the data tree - the value nodes 
 
561
                (plus total?); pass on and extend filtering from 
 
562
                higher level in data tree"""
 
563
                val_node_filts = tree_labels_node.filts[:]
 
564
                is_tot = (val == "_tot_")
 
565
                if is_tot:
 
566
                    val_node_filts.append(NOTNULL % fld)
 
567
                else:
 
568
                    if bolnumeric:
 
569
                        val_node_filts.append("%s = %s" % (fld, val))
 
570
                    else:
 
571
                        val_node_filts.append("%s = \"%s\"" % (fld, val))
 
572
                is_coltot=(is_tot and dim == COLDIM)
 
573
                val_node = \
 
574
                    node_lev1.addChild(LabelNode(label = val_label,
 
575
                        filts=val_node_filts))
 
576
                #if tree_dims_node has children, send through again 
 
577
                # to add further subtree
 
578
                if terminal_var: #a terminal node - add measures
 
579
                    #only gen table cols and summ table rows can 
 
580
                    #  have measures
 
581
                    if (dim == COLDIM and self.has_col_measures) or \
 
582
                            (dim == ROWDIM and self.has_row_measures):
 
583
                        self.addMeasures(label_node=val_node, 
 
584
                                         measures=var_measures, 
 
585
                                         is_coltot=is_coltot, 
 
586
                                         filt_flds=filt_flds,
 
587
                                         filts=val_node_filts) 
 
588
                    else:
 
589
                        val_node.filt_flds = filt_flds
 
590
                else:
 
591
                    for child in tree_dims_node.children:
 
592
                        self.addSubtreeToLabelTree(\
 
593
                                        tree_dims_node=child, 
 
594
                                        tree_labels_node=val_node,
 
595
                                        dim=dim, 
 
596
                                        oth_dim_root=oth_dim_root)
 
597
                                    
 
598
    def addSubtreeMeasuresOnly(self, tree_dims_node, tree_labels_node, 
 
599
                               filt_flds):
 
600
        """
 
601
        For summary table row trees (NB no nesting) we always 
 
602
        display data cells so there is no need to evaluate
 
603
        values etc.  The row will be shown even if they are all 
 
604
        missing symbols. 
 
605
        Instead of value nodes there is a node per measure.
 
606
        """
 
607
        #add level 1 to data tree - the var
 
608
        node_lev1 = tree_labels_node.addChild(LabelNode(label=\
 
609
                                            tree_dims_node.label))
 
610
        self.addMeasures(label_node=node_lev1, 
 
611
                         measures=tree_dims_node.measures, 
 
612
                         is_coltot=False, filt_flds=filt_flds,
 
613
                         filts=[])
 
614
    
 
615
    def addColMeasuresSubtreeIfNoFld(self, tree_dims_node, 
 
616
                                     tree_labels_node):
 
617
        """
 
618
        Add subtree in case where no field.
 
619
        First check that it is OK to add.
 
620
        """
 
621
        if tree_dims_node.level > 1:
 
622
            raise Exception, "If the col field has not " + \
 
623
                "been set, a node without a field specified " + \
 
624
                "must be immediately under the root node"
 
625
        self.addMeasures(label_node=tree_labels_node, 
 
626
                     measures=tree_dims_node.measures, 
 
627
                     is_coltot=False, filt_flds=[], filts=[])
 
628
 
 
629
    def addMeasures(self, label_node, measures, is_coltot, filt_flds, 
 
630
                    filts):
 
631
        "Add measure label nodes under label node"
 
632
        for measure in measures:
 
633
            measure_node = LabelNode(label=measure, 
 
634
                                  filts=filts,
 
635
                                  measure=measure,
 
636
                                  is_coltot=is_coltot)
 
637
            measure_node.filt_flds = filt_flds
 
638
            label_node.addChild(measure_node)
 
639
    
 
640
    def getValsFiltClause(self, tree_dims_node, tree_labels_node, 
 
641
                          oth_dim_root):
 
642
        """
 
643
        To display a cell, we must know that there will be at least 
 
644
        one descendant cell to show underneath it. We do this by 
 
645
        filtering the raw data by the appropriate row 
 
646
        and column filters.  If any records remain, we can show the 
 
647
        cell. As to showing the values beneath the variable, we should 
 
648
        work from the same filtered dataset. For the cell, we only look 
 
649
        at variable subtrees under the cell and all variable subtrees 
 
650
        under the root of the other dimension.
 
651
        
 
652
        E.g. cols:
 
653
                          gender
 
654
           eth                            agegp
 
655
                                nation            religion
 
656
                                region
 
657
                                
 
658
        and rows:
 
659
                year                    year
 
660
                month
 
661
       
 
662
        Should we show gender? E.g.
 
663
        SELECT gender
 
664
        FROM datasource
 
665
        WHERE NOT ISNULL(gender)
 
666
            AND ( 
 
667
            (NOT ISNULL(agegp) AND NOT ISNULL(nation) AND NOT ISNULL(region))
 
668
                OR
 
669
            (NOT ISNULL(agegp) AND NOT ISNULL(religion))
 
670
            )
 
671
            AND (
 
672
            (NOT ISNULL(year) AND NOT ISNULL(month)) 
 
673
                OR
 
674
            (NOT ISNULL(year))
 
675
            )
 
676
        GROUP BY gender                
 
677
        1) parent filters must all be true (none in example above)
 
678
        2) self field cannot be null
 
679
        3) for each subtree, no fields in subtree can be null
 
680
        4) In the other dimension, for each subtree, 
 
681
        none of the fields can have a Null value.
 
682
        """
 
683
        #1) e.g. []
 
684
        if tree_labels_node.filts:
 
685
            parent_filts = " AND ".join(tree_labels_node.filts)
 
686
        else:
 
687
            parent_filts = ""
 
688
        #2) e.g. " NOT ISNULL(gender) "
 
689
        self_filt = NOTNULL % tree_dims_node.fld
 
690
        #3 Identify fields already filtered in 1) or 2) already
 
691
        #we will remove them from field lists of subtree term nodes
 
692
        flds_done = len(tree_dims_node.filt_flds)
 
693
        #get subtree term node field lists (with already done fields sliced out)
 
694
        #e.g. gender>eth, gender>agegp>nation>region, agegp>religion
 
695
        # becomes [[eth],[agegp,nation,region],[agegp,religion]]
 
696
        subtree_term_nodes = []
 
697
        for child in tree_dims_node.children:            
 
698
            subtree_term_nodes += child.getTerminalNodes()
 
699
        if subtree_term_nodes:
 
700
            subtree_filt_fld_lsts = [x.filt_flds[flds_done:] for x \
 
701
                                     in subtree_term_nodes]
 
702
            dim_clause = self.treeFldLstsToClause(tree_fld_lsts=\
 
703
                                             subtree_filt_fld_lsts)
 
704
        else:
 
705
            dim_clause = ""
 
706
        #4 - get all subtree term node field lists (no slicing this time)
 
707
        #  e.g. year>month, year becomes [[year,month],[year]]
 
708
        oth_subtree_term_nodes = []
 
709
        for child in oth_dim_root.children:
 
710
            oth_subtree_term_nodes += child.getTerminalNodes()
 
711
        if oth_subtree_term_nodes:
 
712
            #NB the other dimension could be fieldless e.g. 
 
713
            #  we are a row dim and the oth dim has col measures
 
714
            #  and no field set
 
715
            oth_subtree_filt_fld_lsts = [x.filt_flds for x \
 
716
                                     in oth_subtree_term_nodes \
 
717
                                     if x.filt_flds != [None]]
 
718
            oth_dim_clause = self.treeFldLstsToClause(tree_fld_lsts=\
 
719
                                             oth_subtree_filt_fld_lsts)
 
720
        else:
 
721
            oth_dim_clause = ""
 
722
        #assemble
 
723
        main_clauses = []
 
724
        for clause in [parent_filts, self_filt, dim_clause, 
 
725
                       oth_dim_clause]:
 
726
            if clause:
 
727
                main_clauses.append(clause)
 
728
        final_filt_clause = " AND ".join(main_clauses)
 
729
        return final_filt_clause
 
730
    
 
731
    def treeFldLstsToClause(self, tree_fld_lsts):
 
732
        """
 
733
        [[eth],[agegp,nation,region],[agegp,religion]]
 
734
        becomes
 
735
        "((NOT ISNULL(eth)) 
 
736
            OR (NOT ISNULL(agegp) AND NOT ISNULL(nation) AND NOT ISNULL(region)) 
 
737
            OR (NOT ISNULL(agegp) AND NOT ISNULL(religion)))"
 
738
        """
 
739
        if not tree_fld_lsts:
 
740
            return None
 
741
        else:
 
742
            subtree_clauses_lst = [] #each subtree needs a parenthesised clause
 
743
            #e.g. "( NOT ISNULL(agegp) AND NOT ISNULL(religion) )"
 
744
            for subtree_lst in tree_fld_lsts:
 
745
                subtree_clauses = [NOTNULL % fld for fld \
 
746
                                            in subtree_lst]
 
747
                #e.g. " NOT ISNULL(agegp) ", " NOT ISNULL(religion) "
 
748
                #use AND within subtrees because every field must be filled
 
749
                subtree_clauses_lst.append("(" + \
 
750
                                           " AND ".join(subtree_clauses) + \
 
751
                                           ")")
 
752
            #join subtree clauses with OR because a value in any is enough to
 
753
            #  retain label 
 
754
            clause = "(" + " OR ".join(subtree_clauses_lst) + ")"
 
755
            #e.g. see method documentation at top
 
756
            return clause
 
757
 
 
758
        
 
759
class GenTable(LiveTable):
 
760
    "A general table (not a summary table)"
 
761
 
 
762
    has_row_measures = False
 
763
    has_row_vals = True
 
764
    has_col_measures = True
 
765
 
 
766
    def getHdrDets(self, row_label_cols_n):
 
767
        """
 
768
        Return tree_col_labels and the table header HTML.
 
769
        For HTML provide everything from <thead> to </thead>.
 
770
        """
 
771
        tree_col_labels = LabelNodeTree()
 
772
        tree_col_labels = self.addSubtreesToColLabelTree(tree_col_labels)
 
773
        return self.processHdrTree(tree_col_labels, row_label_cols_n)
 
774
        
 
775
    def getBodyHtmlRows(self, row_label_rows_lst, tree_row_labels,
 
776
                        tree_col_labels):
 
777
        """
 
778
        Make table body rows based on contents of row_label_rows_lst:
 
779
        e.g. [["<tr>", "<td class='firstrowvar' rowspan='8'>Gender</td>" ...],
 
780
        ...]
 
781
        It already contains row label data - we need to add the data cells 
 
782
        into the appropriate row list within row_label_rows_lst before
 
783
        concatenating and appending "</tr>".
 
784
        """
 
785
        col_term_nodes = tree_col_labels.getTerminalNodes()
 
786
        row_term_nodes = tree_row_labels.getTerminalNodes()
 
787
        col_filters_lst = [x.filts for x in col_term_nodes]
 
788
        col_filt_flds_lst = [x.filt_flds for x in col_term_nodes]
 
789
        col_tots_lst = [x.is_coltot for x in col_term_nodes]
 
790
        col_measures_lst = [x.measure for x in col_term_nodes]
 
791
        row_filters_lst = [x.filts for x in row_term_nodes]
 
792
        row_filt_flds_lst = [x.filt_flds for x in row_term_nodes]
 
793
        data_cells_n = len(row_term_nodes) * len(col_term_nodes)
 
794
        print "%s data cells in table" % data_cells_n
 
795
        row_label_rows_lst = self.getRowLabelsRowLst(row_filters_lst, 
 
796
            row_filt_flds_lst, col_measures_lst, col_filters_lst, 
 
797
            col_tots_lst, col_filt_flds_lst, row_label_rows_lst, 
 
798
            data_cells_n, col_term_nodes)
 
799
        return row_label_rows_lst
 
800
                
 
801
    def getRowLabelsRowLst(self, row_filters_lst, row_filt_flds_lst, 
 
802
                           col_measures_lst, col_filters_lst, 
 
803
                           col_tots_lst, col_filt_flds_lst, 
 
804
                           row_label_rows_lst, data_cells_n,
 
805
                           col_term_nodes):
 
806
        """
 
807
        Get list of row data.  Each row in the list is represented
 
808
        by a row of strings to concatenate, one per data point.
 
809
        """
 
810
        
 
811
        """Build lists of data item HTML (data_item_presn_lst)
 
812
            and data item values (results) ready to combine.
 
813
        data_item_presn_lst is a list of tuples with left and right HTML 
 
814
            wrappers for data ("<td class='%s'>" % cellclass, "</td>").  
 
815
            As each data point is processed, a tuple is added to the list.
 
816
        results is built once per batch of data points for database 
 
817
            efficiency reasons.  Each call returns multiple values."""
 
818
        i=0
 
819
        data_item_presn_lst = []
 
820
        results = ()
 
821
        SQL_table_select_clauses_lst = []
 
822
        max_select_vars = 50 #same speed between about 30 and 100 but
 
823
        #twice as slow if much smaller or larger
 
824
        for (row_filter, row_filt_flds) in zip(row_filters_lst,
 
825
                                               row_filt_flds_lst):
 
826
            #row-derived inputs for clause function
 
827
            if len(row_filter) == 1:
 
828
                all_but_last_row_filters_lst = []
 
829
            elif len(row_filter) > 1:
 
830
                all_but_last_row_filters_lst = row_filter[:]
 
831
                del all_but_last_row_filters_lst[-1] #all but the last row 
 
832
            last_row_filter = NOTNULL % row_filt_flds[-1] #for colpct
 
833
            #for styling
 
834
            first = True
 
835
            for (colmeasure, col_filter, coltot, col_filt_flds) in \
 
836
                        zip(col_measures_lst, col_filters_lst, 
 
837
                            col_tots_lst, col_filt_flds_lst):
 
838
                #get column-derived clause function inputs
 
839
                cols_not_null_lst = [NOTNULL % x for x in \
 
840
                                     col_filt_flds]
 
841
                if len(col_filter) <= 1:
 
842
                    all_but_last_col_filters_lst = []
 
843
                elif len(col_filter) > 1:
 
844
                    all_but_last_col_filters_lst = col_filter[:]
 
845
                    del all_but_last_col_filters_lst[-1] #all but the last col
 
846
                if col_filt_flds:
 
847
                    last_col_filter = NOTNULL % col_filt_flds[-1] #for rowpct
 
848
                else:
 
849
                    last_col_filter = ""
 
850
                #styling
 
851
                if first:
 
852
                    cellclass = "firstdatacell"
 
853
                    first = False
 
854
                else:
 
855
                    cellclass = "datacell"
 
856
                #build data row list
 
857
                data_item_presn_lst.append(("<td class='%s'>" % cellclass, 
 
858
                                           colmeasure, "</td>"))
 
859
                #build SQL clauses for next SQL query
 
860
                clause = self.getFuncClause(measure=colmeasure,
 
861
                          row_filters_lst=row_filter, 
 
862
                          col_filters_lst=col_filter, 
 
863
                          all_but_last_row_filters_lst=\
 
864
                              all_but_last_row_filters_lst, #needed for colpct
 
865
                          last_row_filter=last_row_filter, #needed for colpct
 
866
                          all_but_last_col_filters_lst=\
 
867
                              all_but_last_col_filters_lst, #needed for rowpct
 
868
                          last_col_filter=last_col_filter, #needed for colpct
 
869
                          cols_not_null_lst=cols_not_null_lst, #needed for rowpct
 
870
                          is_coltot=coltot
 
871
                          )
 
872
                SQL_table_select_clauses_lst.append(clause)
 
873
                #process SQL queries when number of clauses reaches certain threshold
 
874
                if len(SQL_table_select_clauses_lst) == max_select_vars \
 
875
                    or i == data_cells_n - 1:
 
876
                    SQL_select_results = "SELECT " + \
 
877
                             ", ".join(SQL_table_select_clauses_lst) + \
 
878
                             " FROM " + self.datasource
 
879
                    #print SQL_select_results #debug but reset max_select... low first
 
880
                    self.cur.execute(SQL_select_results)
 
881
                    results += self.cur.fetchone()
 
882
                    SQL_table_select_clauses_lst = []
 
883
                i=i+1
 
884
        i=0
 
885
        #using the data item HTML tuples and the results data, 
 
886
        # build the body row html
 
887
        for row in row_label_rows_lst:
 
888
            for j in range(len(col_term_nodes)):
 
889
                data_format = data_format_dic[data_item_presn_lst[i][1]]
 
890
                data_val = data_format(results[i])
 
891
                row.append(data_item_presn_lst[i][0] + \
 
892
                           data_val + data_item_presn_lst[i][2])
 
893
                i=i+1
 
894
        return row_label_rows_lst
 
895
    
 
896
    def getFuncClause(self, measure, row_filters_lst, col_filters_lst, 
 
897
                      all_but_last_row_filters_lst, last_row_filter,
 
898
                      all_but_last_col_filters_lst, last_col_filter,
 
899
                      cols_not_null_lst, is_coltot):
 
900
        """
 
901
        measure - e.g. FREQ
 
902
        row_filters_lst - effectively applies filtering to
 
903
            the total source data by setting some values to 0 or NULL
 
904
            if not a datapoint defined by the row.
 
905
        col_filters_lst - effectively applies filtering to
 
906
            the total source data by setting some values to 0 or NULL
 
907
            if not a datapoint defined by the column(s).
 
908
        all_but_last_row_filters_lst - used for colpct filtering to get 
 
909
            denominator
 
910
        last_row_filter - last row filter used for colpct filtering
 
911
            to get denominator
 
912
        all_but_last_col_filters_lst - used for rowpct filtering to get 
 
913
            denominator
 
914
        last_col_filter - last col filter used for rowpct filtering
 
915
            to get denominator
 
916
        cols_not_null_lst - used for rowpct filtering
 
917
        is_coltot - boolean
 
918
        """
 
919
        #To get freq, evaluate matching values to 1 (otherwise 0) then sum
 
920
        # 
 
921
        freq = self.abs_wrapper_l + "SUM(" + " AND ".join(\
 
922
                                row_filters_lst + col_filters_lst) + ")" + \
 
923
                                self.abs_wrapper_r
 
924
        col_freq = self.abs_wrapper_l + "SUM(" + " AND ".join(\
 
925
                    row_filters_lst + all_but_last_col_filters_lst) + ")" + \
 
926
                    self.abs_wrapper_r
 
927
        #pprint.pprint(freq) # debug
 
928
        if measure == FREQ:
 
929
            if not is_coltot:
 
930
                return freq
 
931
            else:
 
932
                return col_freq
 
933
        elif measure == COLPCT:
 
934
            if not is_coltot:
 
935
                numerator = freq
 
936
                #we want to divide by all values where all the rows but the last match.
 
937
                #the last row cannot be null.  And all column values must match.
 
938
                denom_filters_lst = []
 
939
                colpct_filter_lst = []
 
940
                colpct_filter_lst.append(" AND ".join(all_but_last_row_filters_lst))
 
941
                colpct_filter_lst.append(last_row_filter)
 
942
                colpct_filter_lst.append(" AND ".join(col_filters_lst))
 
943
            else:
 
944
                numerator = col_freq
 
945
                #we want to divide by all values where all the rows but the last match.
 
946
                #the last row cannot be null. And the col values cannot be null.
 
947
                denom_filters_lst = []
 
948
                colpct_filter_lst = []
 
949
                colpct_filter_lst.append(" AND ".join(all_but_last_row_filters_lst))
 
950
                colpct_filter_lst.append(last_row_filter)
 
951
                colpct_filter_lst.append(" AND ".join(all_but_last_col_filters_lst))                
 
952
            for filter in colpct_filter_lst:
 
953
                if filter != "":
 
954
                    denom_filters_lst.append(filter)
 
955
            denominator = self.abs_wrapper_l + "SUM(" + \
 
956
                " AND ".join(denom_filters_lst) + ")" + self.abs_wrapper_r
 
957
            perc = "100*(%s)/%s" % (numerator, denominator)
 
958
            template = self.if_clause % (NOTNULL % perc, perc, 0)
 
959
            #print template #debug
 
960
            return template
 
961
        elif measure == ROWPCT:
 
962
            if not is_coltot:
 
963
                numerator = freq
 
964
                #we want to divide by all values where all the rows match
 
965
                # and all the cols but the last one match.  The last column 
 
966
                # cannot be null.
 
967
                denom_filters_lst = []
 
968
                rowpct_filter_lst = []
 
969
                rowpct_filter_lst.append(" AND ".join(row_filters_lst))
 
970
                rowpct_filter_lst.append(" AND ".join(all_but_last_col_filters_lst))
 
971
                rowpct_filter_lst.append(last_col_filter) 
 
972
                for filter in rowpct_filter_lst:
 
973
                    if filter != "":
 
974
                        denom_filters_lst.append(filter)
 
975
                denominator = self.abs_wrapper_l + "SUM(" + \
 
976
                    " AND ".join(denom_filters_lst) + ")" + self.abs_wrapper_r
 
977
                perc = "100*(%s)/%s" % (numerator, denominator)
 
978
                template = self.if_clause % (NOTNULL % perc, perc, 0)
 
979
                #print numerator, denominator
 
980
                return template
 
981
            else:
 
982
                return "100"
 
983
        else:
 
984
            raise Exception, "Measure %s not available" % measure
 
985
        
 
986
                    
 
987
class SummTable(LiveTable):
 
988
    "A summary table - e.g. Median, Mean etc"
 
989
    
 
990
    has_row_measures = True
 
991
    has_row_vals = False
 
992
    has_col_measures = False
 
993
 
 
994
    def getHdrDets(self, row_label_cols_n):
 
995
        """
 
996
        Return tree_col_labels and the table header HTML.
 
997
        For HTML provide everything from <thead> to </thead>.
 
998
        If no column variables, make a special column node.
 
999
        """
 
1000
        tree_col_labels = LabelNodeTree()
 
1001
        tree_col_labels = self.addSubtreesToColLabelTree(tree_col_labels)
 
1002
        if tree_col_labels.getDepth() == 1:
 
1003
            tree_col_labels.addChild(LabelNode(label="Measures"))
 
1004
        return self.processHdrTree(tree_col_labels, row_label_cols_n)
 
1005
        
 
1006
    def getBodyHtmlRows(self, row_label_rows_lst, tree_row_labels,
 
1007
                        tree_col_labels):
 
1008
        """
 
1009
        Make table body rows based on contents of row_label_rows_lst:
 
1010
        e.g. [["<tr>", "<td class='firstrowvar' rowspan='8'>Gender</td>" ...],
 
1011
        ...]
 
1012
        It already contains row label data - we need to add the data cells 
 
1013
        into the appropriate row list within row_label_rows_lst before
 
1014
        concatenating and appending "</tr>".
 
1015
        """
 
1016
        col_term_nodes = tree_col_labels.getTerminalNodes()
 
1017
        row_term_nodes = tree_row_labels.getTerminalNodes()
 
1018
        row_measures_lst = [x.measure for x in row_term_nodes]
 
1019
        col_filters_lst = [x.filts for x in col_term_nodes] #can be [[],[],[], ...]
 
1020
        row_filt_flds_lst = [x.filt_flds for x in row_term_nodes]
 
1021
        col_filt_flds_lst = [x.filt_flds for x in col_term_nodes]
 
1022
        col_tots_lst = [x.is_coltot for x in col_term_nodes]
 
1023
        data_cells_n = len(row_term_nodes) * len(col_term_nodes)
 
1024
        print "%s data cells in table" % data_cells_n
 
1025
        row_label_rows_lst = self.getRowLabelsRowLst(row_filt_flds_lst, 
 
1026
                                row_measures_lst, col_filters_lst, 
 
1027
                                row_label_rows_lst, col_term_nodes)
 
1028
        return row_label_rows_lst
 
1029
    
 
1030
    def getRowLabelsRowLst(self, row_flds_lst,  
 
1031
                           row_measures_lst, col_filters_lst, 
 
1032
                           row_label_rows_lst, col_term_nodes):
 
1033
        """
 
1034
        Get list of row data.  Each row in the list is represented
 
1035
        by a row of strings to concatenate, one per data point.
 
1036
        Get data values one at a time (no batches unlike Gen Tables).
 
1037
        """
 
1038
        data_item_lst = []
 
1039
        for (rowmeasure, row_fld_lst) in zip(row_measures_lst, 
 
1040
                                             row_flds_lst):
 
1041
            first = True
 
1042
            for col_filter_lst in col_filters_lst:
 
1043
                #styling
 
1044
                if first:
 
1045
                    cellclass = "firstdatacell"
 
1046
                    first = False
 
1047
                else:
 
1048
                    cellclass = "datacell"
 
1049
                data_val = self.getDataVal(rowmeasure, row_fld_lst[0], 
 
1050
                                             col_filter_lst)
 
1051
                data_item_lst.append("<td class='%s'>%s</td>" % \
 
1052
                                     (cellclass, data_val))
 
1053
        i=0
 
1054
        for row in row_label_rows_lst:
 
1055
            for j in range(len(col_term_nodes)):
 
1056
                row.append(data_item_lst[i])
 
1057
                i=i+1
 
1058
        return row_label_rows_lst
 
1059
    
 
1060
    def getDataVal(self, measure, row_fld, col_filter_lst):
 
1061
        """
 
1062
        measure - e.g. MEAN
 
1063
        row_fld - the numeric field we are calculating the summary of.
 
1064
        col_filter - so we only look at values in the column.
 
1065
        """
 
1066
        col_filt_clause = " AND ".join(col_filter_lst)
 
1067
        if col_filt_clause:
 
1068
            filter = " WHERE " + col_filt_clause
 
1069
        else: 
 
1070
            filter = ""
 
1071
        sql_for_raw_only = [MEDIAN, STD_DEV]
 
1072
        if measure in sql_for_raw_only:
 
1073
            SQL_get_raw_vals = "SELECT " + row_fld + """
 
1074
                FROM """ + self.datasource + filter
 
1075
            self.cur.execute(SQL_get_raw_vals)
 
1076
            data = [x[0] for x in self.cur.fetchall() if x[0]]
 
1077
            #print data #debug
 
1078
        if measure == SUM:
 
1079
            SQL_get_sum = "SELECT SUM(%s) " % row_fld + \
 
1080
                "FROM " + self.datasource + filter
 
1081
            self.cur.execute(SQL_get_sum)
 
1082
            data_val = self.cur.fetchone()[0]
 
1083
        elif measure == MEAN:
 
1084
            SQL_get_mean = "SELECT AVG(" + row_fld + """
 
1085
                ) FROM """ + self.datasource + filter
 
1086
            self.cur.execute(SQL_get_mean)
 
1087
            data_val =  round(self.cur.fetchone()[0],2)
 
1088
        elif measure == MEDIAN:
 
1089
            data_val =  round(numpy.median(data),2)
 
1090
        elif measure == SUMM_N:
 
1091
            SQL_get_n = "SELECT COUNT(" + row_fld + """)
 
1092
                FROM """ + self.datasource + filter
 
1093
            self.cur.execute(SQL_get_n)
 
1094
            data_val =  "N=%s" % self.cur.fetchone()[0]
 
1095
        elif measure == STD_DEV:
 
1096
            data_val =  round(numpy.std(data),2)
 
1097
        else:
 
1098
            raise Exception, "Measure not available"
 
1099
        return data_val
 
1100
        
 
 
b'\\ No newline at end of file'