1
2
3 """ S3 Pivot Table Reports Method
4
5 @copyright: 2011-2019 (c) Sahana Software Foundation
6 @license: MIT
7
8 @requires: U{B{I{Python 2.6}} <http://www.python.org>}
9
10 Permission is hereby granted, free of charge, to any person
11 obtaining a copy of this software and associated documentation
12 files (the "Software"), to deal in the Software without
13 restriction, including without limitation the rights to use,
14 copy, modify, merge, publish, distribute, sublicense, and/or sell
15 copies of the Software, and to permit persons to whom the
16 Software is furnished to do so, subject to the following
17 conditions:
18
19 The above copyright notice and this permission notice shall be
20 included in all copies or substantial portions of the Software.
21
22 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
23 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
24 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
25 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
26 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
27 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
28 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
29 OTHER DEALINGS IN THE SOFTWARE.
30 """
31
32 __all__ = ("S3Report",
33 "S3PivotTable",
34 "S3ReportRepresent",
35 )
36
37 import datetime
38 import json
39 import os
40 import re
41 import sys
42
43 from itertools import product
44
45 from gluon import current
46 from gluon.contenttype import contenttype
47 from gluon.html import BUTTON, DIV, FIELDSET, FORM, INPUT, LABEL, LEGEND, TAG, XML
48 from gluon.languages import regex_translate
49 from gluon.sqlhtml import OptionsWidget
50 from gluon.storage import Storage
51 from gluon.validators import IS_IN_SET, IS_EMPTY_OR
52
53 from s3query import FS
54 from s3rest import S3Method
55 from s3utils import s3_flatlist, s3_has_foreign_key, s3_str, S3MarkupStripper, s3_represent_value
56 from s3xml import S3XMLFormat
57 from s3validators import IS_NUMBER, JSONERRORS
58
59
60 DEFAULT = lambda: None
61 SEPARATORS = (",", ":")
62
63 LAYER = re.compile(r"([a-zA-Z]+)\((.*)\)\Z")
64 FACT = re.compile(r"([a-zA-Z]+)\(([a-zA-Z0-9_.$:\,~]+)\),*(.*)\Z")
65 SELECTOR = re.compile(r"^[a-zA-Z0-9_.$:\~]+\Z")
69 """ RESTful method for pivot table reports """
70
71
73 """
74 Page-render entry point for REST interface.
75
76 @param r: the S3Request instance
77 @param attr: controller attributes for the request
78 """
79
80 if r.http == "GET":
81 if r.representation == "geojson":
82 output = self.geojson(r, **attr)
83 else:
84 output = self.report(r, **attr)
85 elif r.http == "POST":
86 if r.representation == "json":
87
88
89 output = self.explore(r, **attr)
90 else:
91 r.error(415, current.ERROR.BAD_FORMAT)
92 else:
93 r.error(405, current.ERROR.BAD_METHOD)
94 return output
95
96
98 """
99 Pivot table report page
100
101 @param r: the S3Request instance
102 @param attr: controller attributes for the request
103 """
104
105 output = {}
106
107 resource = self.resource
108 get_config = resource.get_config
109
110 show_filter_form = False
111 if r.representation in ("html", "iframe"):
112 filter_widgets = get_config("filter_widgets", None)
113 if filter_widgets and not self.hide_filter:
114
115 from s3filter import S3FilterForm
116 show_filter_form = True
117 S3FilterForm.apply_filter_defaults(r, resource)
118
119 widget_id = "pivottable"
120
121
122 maxrows = 20
123 maxcols = 20
124
125
126 report_vars = ("rows", "cols", "fact", "totals")
127 get_vars = dict((k, v) for k, v in r.get_vars.iteritems()
128 if k in report_vars)
129
130
131 report_options = get_config("report_options", {})
132 defaults = report_options.get("defaults", {})
133
134 if not any (k in get_vars for k in ("rows", "cols", "fact")):
135 get_vars = defaults
136 get_vars["chart"] = r.get_vars.get("chart",
137 defaults.get("chart", None))
138 get_vars["table"] = r.get_vars.get("table",
139 defaults.get("table", None))
140
141
142 if get_vars:
143
144 rows = get_vars.get("rows", None)
145 if type(rows) is list:
146 rows = rows[-1]
147 cols = get_vars.get("cols", None)
148 if type(cols) is list:
149 cols = cols[-1]
150
151 layer = get_vars.get("fact", "id")
152 try:
153 facts = S3PivotTableFact.parse(layer)
154 except SyntaxError:
155 current.log.error(sys.exc_info()[1])
156 facts = None
157 if not facts or not any([rows, cols]):
158 pivottable = None
159 else:
160 prefix = resource.prefix_selector
161 get_vars["rows"] = prefix(rows) if rows else None
162 get_vars["cols"] = prefix(cols) if cols else None
163 get_vars["fact"] = ",".join("%s(%s)" % (fact.method, fact.selector) for fact in facts)
164
165 pivottable = S3PivotTable(resource, rows, cols, facts,
166 precision = report_options.get("precision"),
167 )
168 else:
169 pivottable = None
170
171 representation = r.representation
172 if representation in ("html", "iframe", "json"):
173
174
175 if pivottable is not None:
176 pivotdata = pivottable.json(maxrows=maxrows, maxcols=maxcols)
177 else:
178 pivotdata = None
179
180 if r.representation in ("html", "iframe"):
181
182 tablename = resource.tablename
183
184
185 if show_filter_form:
186 advanced = False
187 for widget in filter_widgets:
188 if not widget:
189 continue
190 if "hidden" in widget.opts and widget.opts.hidden:
191 advanced = resource.get_config("report_advanced", True)
192 break
193
194 filter_formstyle = get_config("filter_formstyle", None)
195 filter_form = S3FilterForm(filter_widgets,
196 formstyle = filter_formstyle,
197 advanced = advanced,
198 submit = False,
199 _class = "filter-form",
200 _id = "%s-filter-form" % widget_id,
201 )
202 fresource = current.s3db.resource(tablename)
203 alias = resource.alias if r.component else None
204 filter_widgets = filter_form.fields(fresource,
205 r.get_vars,
206 alias = alias,
207 )
208 else:
209
210 filter_widgets = None
211
212
213 ajax_vars = Storage(r.get_vars)
214 ajax_vars.update(get_vars)
215 filter_url = r.url(method = "",
216 representation = "",
217 vars = ajax_vars.fromkeys((k for k in ajax_vars
218 if k not in report_vars)))
219 ajaxurl = attr.get("ajaxurl", r.url(method = "report",
220 representation = "json",
221 vars = ajax_vars,
222 ))
223
224 output = S3ReportForm(resource).html(pivotdata,
225 get_vars = get_vars,
226 filter_widgets = filter_widgets,
227 ajaxurl = ajaxurl,
228 filter_url = filter_url,
229 widget_id = widget_id,
230 )
231
232 output["title"] = self.crud_string(tablename, "title_report")
233 output["report_type"] = "pivottable"
234
235
236 self._view(r, "pivottable.html")
237
238
239 current.response.view = self._view(r, "report.html")
240
241 elif r.representation == "json":
242
243 output = json.dumps(pivotdata, separators=SEPARATORS)
244
245 elif r.representation == "xls":
246
247 if pivottable:
248
249
250 title = self.crud_string(r.tablename, "title_report")
251 if title is None:
252 title = current.T("Report")
253
254
255 filename = "%s_%s.xls" % (r.env.server_name,
256 s3_str(title).replace(" ", "_"),
257 )
258 disposition = "attachment; filename=\"%s\"" % filename
259
260
261 response = current.response
262 response.headers["Content-Type"] = contenttype(".xls")
263 response.headers["Content-disposition"] = disposition
264
265
266 stream = pivottable.xls(title)
267
268 output = stream.read()
269
270 else:
271 r.error(400, "No report parameters specified")
272
273 else:
274 r.error(415, current.ERROR.BAD_FORMAT)
275
276 return output
277
278
280 """
281 Render the pivot table data as a dict ready to be exported as
282 GeoJSON for display on a Map.
283
284 @param r: the S3Request instance
285 @param attr: controller attributes for the request
286 """
287
288 resource = self.resource
289 response = current.response
290 s3 = response.s3
291
292
293 response.headers["Content-Type"] = s3.content_type.get("geojson",
294 "application/json")
295 if not resource.count():
296
297 return json.dumps({})
298
299
300 get_vars = r.get_vars
301 layer_id = r.get_vars.get("layer", None)
302 level = get_vars.get("level", "L0")
303
304
305 get_config = resource.get_config
306 report_options = get_config("report_options", {})
307 defaults = report_options.get("defaults", {})
308
309
310 context = get_config("context")
311 if context and "location" in context:
312
313 rows = "(location)$%s" % level
314 else:
315
316 rows = "location_id$%s" % level
317
318
319
320
321 resource.add_filter(FS(rows) != None)
322
323
324 stylesheet = os.path.join(r.folder, r.XSLT_PATH, "geojson", "export.xsl")
325
326
327 fallback_to_points = True
328 output = None
329 if fallback_to_points:
330 if resource.count() == 0:
331
332 resource.clear_query()
333
334 resource.build_query(filter=s3.filter, vars=get_vars)
335
336
337 xmlformat = S3XMLFormat(stylesheet)
338 include, exclude = xmlformat.get_fields(resource.tablename)
339 resource.load(fields=include,
340 skip=exclude,
341 start=0,
342 limit=None,
343 orderby=None,
344 virtual=False,
345 cacheable=True)
346 gis = current.gis
347 attr_fields = []
348 style = gis.get_style(layer_id=layer_id,
349 aggregate=False)
350 popup_format = style.popup_format
351 if popup_format:
352 if "T(" in popup_format:
353
354 T = current.T
355 items = regex_translate.findall(popup_format)
356 for item in items:
357 titem = str(T(item[1:-1]))
358 popup_format = popup_format.replace("T(%s)" % item,
359 titem)
360 style.popup_format = popup_format
361
362 parts = popup_format.split("{")
363
364 parts = parts[1:]
365 for part in parts:
366 attribute = part.split("}")[0]
367 attr_fields.append(attribute)
368 attr_fields = ",".join(attr_fields)
369
370 location_data = gis.get_location_data(resource,
371 attr_fields=attr_fields)
372
373
374 current.xml.show_ids = True
375 output = resource.export_xml(fields=include,
376 mcomponents=None,
377 references=[],
378 stylesheet=stylesheet,
379 as_json=True,
380 location_data=location_data,
381 map_data=dict(style=style),
382 )
383
384 if not output:
385 r.error(400, "XSLT Transformation Error: %s " % current.xml.error)
386
387 else:
388 while resource.count() == 0:
389
390 level = int(level[1:])
391 if level == 0:
392
393 return json.dumps({})
394 resource.clear_query()
395
396 resource.build_query(filter=s3.filter, vars=get_vars)
397 level = "L%s" % (level - 1)
398 if context and "location" in context:
399
400 rows = "(location)$%s" % level
401 else:
402
403 rows = "location_id$%s" % level
404
405
406 resource.add_filter(FS(rows) != None)
407
408 if not output:
409
410 cols = None
411 layer = get_vars.get("fact", defaults.get("fact", "count(id)"))
412 facts = S3PivotTableFact.parse(layer)[:1]
413 pivottable = S3PivotTable(resource, rows, cols, facts,
414 precision = report_options.get("precision"),
415 )
416
417
418
419 style = current.gis.get_style(layer_id=layer_id,
420 aggregate=True)
421 popup_format = style.popup_format
422 if popup_format:
423 if"T(" in popup_format:
424
425 T = current.T
426 items = regex_translate.findall(popup_format)
427 for item in items:
428 titem = str(T(item[1:-1]))
429 popup_format = popup_format.replace("T(%s)" % item,
430 titem)
431 style.popup_format = popup_format
432
433
434
435
436
437
438
439
440
441
442 ids, location_data = pivottable.geojson(fact=facts[0], level=level)
443
444
445 current.xml.show_ids = True
446 gresource = current.s3db.resource("gis_location", id=ids)
447 output = gresource.export_xml(fields=[],
448 mcomponents=None,
449 references=[],
450 stylesheet=stylesheet,
451 as_json=True,
452 location_data=location_data,
453
454
455
456 map_data=dict(level=int(level[1:]),
457 style=style),
458 )
459
460 if not output:
461 r.error(400, "XSLT Transformation Error: %s " % current.xml.error)
462
463 return output
464
465
572
573
575 """
576 Ajax-lookup of representations for items contributing to the
577 aggregate value in a pivot table cell (cell explore)
578 - called with a body JSON containing the record IDs to represent,
579 and the URL params for the pivot table (rows, cols, fact)
580
581 @param r: the S3Request instance
582 @param attr: controller attributes for the request
583 """
584
585
586 s = r.body
587 s.seek(0)
588 try:
589 record_ids = json.load(s)
590 except JSONERRORS:
591 record_ids = None
592
593
594 if not isinstance(record_ids, list):
595 r.error(404, current.ERROR.BAD_RECORD)
596
597
598 resource = current.s3db.resource(self.tablename, id=record_ids)
599
600 prefix = resource.prefix_selector
601 pkey = prefix(resource._id.name)
602 pkey_colname = str(resource._id)
603
604
605 get_vars = r.get_vars
606 facts = S3PivotTableFact.parse(get_vars.get("fact"))
607
608 selectors = set()
609 ofacts = []
610
611 for fact in facts:
612 selector = prefix(fact.selector)
613 is_pkey = selector == pkey
614 if not is_pkey:
615 selectors.add(selector)
616 if not is_pkey or fact.method != "count":
617 ofacts.append(fact)
618
619
620 if len(selectors):
621 selectors.add(pkey)
622 records = resource.select(selectors,
623 raw_data = True,
624 represent = True,
625 limit = None,
626 ).rows
627 else:
628
629
630 records = []
631 for record_id in record_ids:
632 record = Storage({pkey_colname: record_id})
633 record._row = record
634 records.append(record)
635
636
637
638 represent = resource.get_config("report_represent", S3ReportRepresent)
639 if represent:
640 rows = get_vars.get("rows")
641 cols = get_vars.get("cols")
642 represent = represent(resource, rows=rows, cols=cols, facts=facts)
643
644
645
646 rfields = {}
647
648 key = None
649 aggregate = True
650 if len(ofacts) == 1:
651
652 fact = ofacts[0]
653
654 if fact.method == "count":
655
656
657
658
659
660
661 selector = prefix(fact.selector)
662 rfield = resource.resolve_selector(selector)
663 field = rfield.field
664 if field and s3_has_foreign_key(field):
665 multiple = True
666 if rfield.tname == resource.tablename or \
667 selector[:2] == "~." and "." not in selector[2:]:
668 multiple = False
669 else:
670
671 alias = selector.split("$", 1)[0].split(".", 1)[0]
672 component = resource.components.get(alias)
673 if component:
674 multiple = component.multiple
675
676 if not multiple:
677 represent = None
678 key = rfield.colname
679 aggregate = False
680 rfields[selector] = rfield
681
682
683 records_repr = represent(record_ids) if represent else None
684
685
686 output = {}
687 UNKNOWN_OPT = current.messages.UNKNOWN_OPT
688 for record in records:
689
690 raw = record._row
691 record_id = raw[pkey_colname]
692
693 values = []
694 for fact in ofacts:
695
696
697 selector = prefix(fact.selector)
698 rfield = rfields.get(selector)
699 if not rfield:
700 rfield = rfields[selector] = resource.resolve_selector(selector)
701
702
703 if aggregate:
704 value = raw[rfield.colname]
705 if type(value) is list:
706 value = fact.compute(value)
707 else:
708 value = fact.compute([value])
709 if fact.method != "count":
710 field = rfield.field
711 if field and field.represent:
712 value = field.represent(value)
713 else:
714 value = record[rfield.colname]
715
716
717 if len(values):
718 values.extend([" / ", value])
719 else:
720 values.append(value)
721
722 repr_items = [TAG[""](values)] if values else []
723
724
725 if records_repr is not None:
726 repr_items.insert(0, records_repr.get(record_id, UNKNOWN_OPT))
727 if len(repr_items) == 2:
728 repr_items.insert(1, ": ")
729
730
731
732
733 repr_str = TAG[""](repr_items).xml()
734 if key:
735
736 output[record_id] = [repr_str, s3_str(raw[key])]
737 else:
738 output[record_id] = repr_str
739
740 current.response.headers["Content-Type"] = "application/json"
741 return json.dumps(output, separators=SEPARATORS)
742
743
744 @staticmethod
746 """
747 Re-usable helper function to inject D3/NVD3 scripts
748 into the current page
749 """
750
751 appname = current.request.application
752 s3 = current.response.s3
753
754 scripts_append = s3.scripts.append
755 if s3.debug:
756 if s3.cdn:
757 scripts_append("https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.js")
758
759
760 else:
761 scripts_append("/%s/static/scripts/d3/d3.js" % appname)
762 scripts_append("/%s/static/scripts/d3/nv.d3.js" % appname)
763 else:
764 if s3.cdn:
765 scripts_append("https://cdnjs.cloudflare.com/ajax/libs/d3/3.5.17/d3.min.js")
766
767
768 else:
769 scripts_append("/%s/static/scripts/d3/d3.min.js" % appname)
770 scripts_append("/%s/static/scripts/d3/nv.d3.min.js" % appname)
771
1261
1262
1263 @staticmethod
1290
1293 """
1294 Method to represent the contributing records in a pivot table
1295 cell (cell explore)
1296
1297 The cell-explore record list will typically look like:
1298 - <record representation>: <fact value(s)>
1299 - ...
1300 This method controls the first part of each entry.
1301
1302 For customization, configure for the table as:
1303 report_represent = <subclass>
1304 """
1305
1306 - def __init__(self, resource, rows=None, cols=None, facts=None):
1307 """
1308 Constructor, initializes the method with the report context
1309 to allow it to adapt the representation (e.g. it may often
1310 be desirable to not repeat the report axes in the record list)
1311
1312 @param resource: the resource of the report
1313 @param rows: the rows-selector (can be None)
1314 @param cols: the columns-selector (can be None)
1315 @param facts: the list of S3PivotTableFacts showing in
1316 the pivot table
1317 """
1318
1319 self.resource = resource
1320 self.rows = rows
1321 self.cols = cols
1322 self.facts = facts
1323
1324
1326 """
1327 Represent record IDs, can be overloaded in subclasses
1328
1329 @param record_ids: list of record IDs
1330
1331 @returns: a JSON-serializable dict {recordID: representation},
1332 or None to suppress recordID representation in the
1333 cell explorer
1334
1335 NB default behavior is not sensitive for report axes
1336 """
1337
1338
1339 resource = self.resource
1340 table = resource.table
1341
1342 represent = self.repr_method()
1343 if represent:
1344 if hasattr(represent, "bulk"):
1345
1346 output = represent.bulk(record_ids)
1347 else:
1348
1349 output = {record_id: represent(record_id)
1350 for record_id in record_ids}
1351
1352 elif "name" in table.fields:
1353
1354 query = table._id.belongs(record_ids)
1355 rows = current.db(query).select(table._id, table.name)
1356
1357 output = {}
1358 UNKNOWN_OPT = current.messages.UNKNOWN_OPT
1359 for row in rows:
1360 name = row.name
1361 if not name:
1362 name = UNKNOWN_OPT
1363 output[row[table._id]] = s3_str(row.name)
1364 else:
1365
1366
1367
1368
1369
1370
1371
1372
1373 output = None
1374
1375 return output
1376
1377
1379 """
1380 Return a representation method for the id-field of
1381 self.resource, can be overloaded in subclasses (simpler
1382 than implementing __call__ if producing a representation
1383 method is sufficient)
1384
1385 @returns: a representation method (preferrably a S3Represent)
1386 """
1387
1388 s3db = current.s3db
1389
1390 resource = self.resource
1391 pkey = resource._id
1392
1393 represent = pkey.represent
1394 if not represent:
1395
1396
1397 if resource.tablename == "pr_person":
1398 represent = s3db.pr_PersonRepresent()
1399
1400 return represent
1401
1404 """ Class representing a fact layer """
1405
1406
1407 METHODS = {"list": "List",
1408 "count": "Count",
1409 "min": "Minimum",
1410 "max": "Maximum",
1411 "sum": "Total",
1412 "avg": "Average",
1413
1414 }
1415
1416
1417 - def __init__(self, method, selector, label=None, default_method=True):
1418 """
1419 Constructor
1420
1421 @param method: the aggregation method
1422 @param selector: the field selector
1423 @param label: the fact label
1424 @param default_method: using default method (used by parser)
1425 """
1426
1427 if method is None:
1428 method = "count"
1429 default_method = True
1430 if method not in self.METHODS:
1431 raise SyntaxError("Unsupported aggregation function: %s" % method)
1432
1433 self.method = method
1434 self.selector = selector
1435
1436 self._layer = None
1437 self.label = label
1438
1439 self.resource = None
1440 self.rfield = None
1441 self.column = selector
1442
1443 self.default_method = default_method
1444
1445
1446 @property
1448 """
1449 @todo: docstring
1450 """
1451
1452 layer = self._layer
1453 if not layer:
1454 layer = self._layer = (self.selector, self.method)
1455 return layer
1456
1457
1458 - def compute(self, values, method=DEFAULT, totals=False, precision=None):
1459 """
1460 Aggregate a list of values.
1461
1462 @param values: iterable of values
1463 @param method: the aggregation method
1464 @param totals: this call is computing row/column/grand totals
1465 @param precision: limit the precision of the computation to this
1466 number of decimals (@todo: consider a default of 6)
1467 """
1468
1469 if values is None:
1470 return None
1471
1472 if method is DEFAULT:
1473 method = self.method
1474 if totals and method == "list":
1475 method = "count"
1476
1477 if method is None or method == "list":
1478 return values if values else None
1479
1480 if method == "count":
1481
1482 return len([v for v in values if v is not None])
1483 else:
1484
1485
1486 values = [v for v in values if isinstance(v, (int, long, float))]
1487
1488 if method == "min":
1489 try:
1490 result = min(values)
1491 except (TypeError, ValueError):
1492 return None
1493
1494 elif method == "max":
1495 try:
1496 result = max(values)
1497 except (TypeError, ValueError):
1498 return None
1499
1500 elif method == "sum":
1501 try:
1502 result = sum(values)
1503 except (TypeError, ValueError):
1504 return None
1505
1506 elif method == "avg":
1507 try:
1508 number = len(values)
1509 if number:
1510 result = sum(values) / float(number)
1511 else:
1512 return 0.0
1513 except (TypeError, ValueError):
1514 return None
1515
1516
1517
1518
1519
1520
1521
1522
1523
1524
1525 if type(result) is float and precision is not None:
1526 return round(result, precision)
1527 else:
1528 return result
1529
1530 return None
1531
1532
1534 """
1535 Aggregate totals for this fact (hyper-aggregation)
1536
1537 @param totals: iterable of totals
1538 """
1539
1540 if self.method in ("list", "count"):
1541 total = self.compute(totals, method="sum")
1542 else:
1543 total = self.compute(totals)
1544 return total
1545
1546
1547 @classmethod
1549 """
1550 Parse fact expression
1551
1552 @param fact: the fact expression
1553 """
1554
1555 if isinstance(fact, tuple):
1556 label, fact = fact
1557 else:
1558 label = None
1559
1560 if isinstance(fact, list):
1561 facts = []
1562 for f in fact:
1563 facts.extend(cls.parse(f))
1564 if not facts:
1565 raise SyntaxError("Invalid fact expression: %s" % fact)
1566 return facts
1567
1568
1569 other = None
1570 default_method = False
1571 if not fact:
1572 method, parameters = "count", "id"
1573 else:
1574 match = FACT.match(fact)
1575 if match:
1576 method, parameters, other = match.groups()
1577 if other:
1578 other = cls.parse((label, other) if label else other)
1579 elif SELECTOR.match(fact):
1580 method, parameters, other = "count", fact, None
1581 default_method = True
1582 else:
1583 raise SyntaxError("Invalid fact expression: %s" % fact)
1584
1585
1586 if method not in cls.METHODS:
1587 raise SyntaxError("Unsupported aggregation method: %s" % method)
1588
1589
1590 parameters = parameters.split(",")
1591
1592 selector = parameters[0]
1593
1594 facts = [cls(method,
1595 selector,
1596 label=label,
1597 default_method=default_method,
1598 ),
1599 ]
1600 if other:
1601 facts.extend(other)
1602 return facts
1603
1604
1605 @classmethod
1607 """
1608 Get a label for a method
1609
1610 @param code: the method code
1611 @return: the label (lazyT), or None for unsupported methods
1612 """
1613
1614 methods = cls.METHODS
1615
1616 if code is None:
1617 code = "list"
1618 if code in methods:
1619 return current.T(methods[code])
1620 else:
1621 return None
1622
1623
1624 @staticmethod
1664
1665
1666 - def get_label(self, rfield, fact_options=None):
1667 """
1668 Get a label for this fact
1669
1670 @param rfield: the S3ResourceField
1671 @param fact_options: the "fact" list of the report options
1672 """
1673
1674 label = self.label
1675 if label:
1676
1677 return label
1678
1679
1680 if fact_options:
1681
1682 prefix = rfield.resource.prefix_selector
1683 for fact_option in fact_options:
1684 facts = self.parse(fact_option)
1685 for fact in facts:
1686 if fact.method == self.method and \
1687 prefix(fact.selector) == prefix(self.selector):
1688 label = fact.label
1689 break
1690 if label:
1691 break
1692
1693 if not label:
1694
1695 field_label = self._get_field_label(rfield, fact_options)
1696 method_label = self._get_method_label(self.method)
1697 label = "%s (%s)" % (field_label, method_label)
1698
1699 self.label = label
1700 return label
1701
1704 """ Class representing a pivot table of a resource """
1705
1706 - def __init__(self, resource, rows, cols, facts, strict=True, precision=None):
1707 """
1708 Constructor - extracts all unique records, generates a
1709 pivot table from them with the given dimensions and
1710 computes the aggregated values for each cell.
1711
1712 @param resource: the S3Resource
1713 @param rows: field selector for the rows dimension
1714 @param cols: field selector for the columns dimension
1715 @param facts: list of S3PivotTableFacts to compute
1716 @param strict: filter out dimension values which don't match
1717 the resource filter
1718 @param precision: maximum precision of aggregate computations,
1719 a dict {selector:number_of_decimals}
1720 """
1721
1722
1723
1724 if not rows and not cols:
1725 raise SyntaxError("No rows or columns specified for pivot table")
1726
1727 self.resource = resource
1728
1729 self.lfields = None
1730 self.dfields = None
1731 self.rfields = None
1732
1733 self.rows = rows
1734 self.cols = cols
1735 self.facts = facts
1736
1737 self.precision = precision if isinstance(precision, dict) else {}
1738
1739
1740
1741 self.records = None
1742 """ All records in the pivot table as a Storage like:
1743 {
1744 <record_id>: <Row>
1745 }
1746 """
1747
1748 self.empty = False
1749 """ Empty-flag (True if no records could be found) """
1750 self.numrows = None
1751 """ The number of rows in the pivot table """
1752 self.numcols = None
1753 """ The number of columns in the pivot table """
1754
1755 self.cell = None
1756 """ Array of pivot table cells in [rows[columns]]-order, each
1757 cell is a Storage like:
1758 {
1759 records: <list_of_record_ids>,
1760 (<fact>, <method>): <aggregated_value>, ...per layer
1761 }
1762 """
1763 self.row = None
1764 """ List of row headers, each header is a Storage like:
1765 {
1766 value: <dimension value>,
1767 records: <list_of_record_ids>,
1768 (<fact>, <method>): <total value>, ...per layer
1769 }
1770 """
1771 self.col = None
1772 """ List of column headers, each header is a Storage like:
1773 {
1774 value: <dimension value>,
1775 records: <list_of_record_ids>,
1776 (<fact>, <method>): <total value>, ...per layer
1777 }
1778 """
1779 self.totals = Storage()
1780 """ The grand total values for each layer, as a Storage like:
1781 {
1782 (<fact>, <method): <total value>, ...per layer
1783 }
1784 """
1785
1786 self.values = {}
1787
1788
1789
1790 tablename = resource.tablename
1791
1792
1793
1794
1795
1796 fields = current.s3db.get_config(tablename, "report_fields", [])
1797
1798 self._get_fields(fields=fields)
1799 rows = self.rows
1800 cols = self.cols
1801
1802
1803
1804 exclude_empty = current.s3db.get_config(tablename, "report_exclude_empty")
1805 if exclude_empty is True:
1806
1807 query = (FS(rows) != None) & (FS(cols) != None)
1808 resource.add_filter(query)
1809
1810 elif type(exclude_empty) is tuple:
1811
1812 for axis in (cols, rows):
1813 if axis in exclude_empty:
1814 resource.add_filter(FS(axis) != None)
1815
1816
1817
1818 data = resource.select(self.rfields.keys(), limit=None)
1819 drows = data["rows"]
1820 if drows:
1821
1822 key = str(resource.table._id)
1823 records = Storage([(i[key], i) for i in drows])
1824
1825
1826
1827 gfields = self.gfields
1828 pkey_colname = gfields[self.pkey]
1829 rows_colname = gfields[rows]
1830 cols_colname = gfields[cols]
1831
1832 if strict:
1833 rfields = self.rfields
1834 axes = (rfield
1835 for rfield in (rfields[rows], rfields[cols])
1836 if rfield != None)
1837 axisfilter = resource.axisfilter(axes)
1838 else:
1839 axisfilter = None
1840
1841 dataframe = []
1842 extend = dataframe.extend
1843 expand = self._expand
1844
1845 for _id in records:
1846 row = records[_id]
1847 item = {key: _id}
1848 if rows_colname:
1849 item[rows_colname] = row[rows_colname]
1850 if cols_colname:
1851 item[cols_colname] = row[cols_colname]
1852 extend(expand(item, axisfilter=axisfilter))
1853
1854 self.records = records
1855
1856
1857
1858 matrix, rnames, cnames = self._pivot(dataframe,
1859 pkey_colname,
1860 rows_colname,
1861 cols_colname)
1862
1863
1864
1865 if cols:
1866 self.col = [Storage({"value": v}) for v in cnames]
1867 self.numcols = len(self.col)
1868 else:
1869 self.col = [Storage({"value": None})]
1870 self.numcols = 1
1871
1872 if rows:
1873 self.row = [Storage({"value": v}) for v in rnames]
1874 self.numrows = len(self.row)
1875 else:
1876 self.row = [Storage({"value": None})]
1877 self.numrows = 1
1878
1879
1880
1881 add_layer = self._add_layer
1882 for fact in self.facts:
1883 add_layer(matrix, fact)
1884
1885 else:
1886
1887
1888 self.empty = True
1889
1890
1891
1892
1894 """ Total number of records in the report """
1895
1896 items = self.records
1897 if items is None:
1898 return 0
1899 else:
1900 return len(self.records)
1901
1902
1903 - def geojson(self,
1904 fact=None,
1905 level="L0"):
1906 """
1907 Render the pivot table data as a dict ready to be exported as
1908 GeoJSON for display on a Map.
1909
1910 Called by S3Report.geojson()
1911
1912 @param layer: the layer. e.g. ("id", "count")
1913 - we only support methods "count" & "sum"
1914 - @ToDo: Support density: 'per sqkm' and 'per population'
1915 @param level: the aggregation level (defaults to Country)
1916 """
1917
1918 if fact is None:
1919 fact = self.facts[0]
1920 layer = fact.layer
1921
1922
1923
1924 context = self.resource.get_config("context")
1925 if context and "location" in context:
1926 rows_dim = "(location)$%s" % level
1927 else:
1928
1929 rows_dim = "location_id$%s" % level
1930
1931
1932
1933
1934 attributes = {}
1935 geojsons = {}
1936
1937 if self.empty:
1938 location_ids = []
1939 else:
1940 numeric = lambda x: isinstance(x, (int, long, float))
1941 row_repr = s3_str
1942
1943 ids = {}
1944 irows = self.row
1945 rows = []
1946
1947
1948 is_numeric = None
1949 for i in xrange(self.numrows):
1950 irow = irows[i]
1951 total = irow[layer]
1952 if is_numeric is None:
1953 is_numeric = numeric(total)
1954 if not is_numeric:
1955 total = len(irow.records)
1956 header = Storage(value = irow.value,
1957 text = irow.text if "text" in irow
1958 else row_repr(irow.value))
1959 rows.append((i, total, header))
1960
1961 self._sortdim(rows, self.rfields[rows_dim])
1962
1963
1964 db = current.db
1965 gtable = current.s3db.gis_location
1966 query = (gtable.level == level) & (gtable.deleted == False)
1967 for _, rtotal, rtitle in rows:
1968 rval = rtitle.value
1969 if rval:
1970
1971 if rval in ids:
1972 _id = ids[rval]
1973 else:
1974 q = query & (gtable.name == rval)
1975 row = db(q).select(gtable.id,
1976 gtable.parent,
1977 limitby=(0, 1)
1978 ).first()
1979 try:
1980 _id = row.id
1981 except AttributeError:
1982 continue
1983
1984 ids[rval] = _id
1985
1986 attribute = dict(name=s3_str(rval),
1987 value=rtotal)
1988 attributes[_id] = attribute
1989
1990 location_ids = [ids[r] for r in ids]
1991 query = (gtable.id.belongs(location_ids))
1992 geojsons = current.gis.get_locations(gtable,
1993 query,
1994 join=False,
1995 geojson=True)
1996
1997
1998 location_data = {}
1999 geojsons = dict(gis_location = geojsons)
2000 location_data["geojsons"] = geojsons
2001 attributes = dict(gis_location = attributes)
2002 location_data["attributes"] = attributes
2003 return location_ids, location_data
2004
2005
2006 - def json(self, maxrows=None, maxcols=None):
2007 """
2008 Render the pivot table data as JSON-serializable dict
2009
2010 @param layer: the layer
2011 @param maxrows: maximum number of rows (None for all)
2012 @param maxcols: maximum number of columns (None for all)
2013 @param least: render the least n rows/columns rather than
2014 the top n (with maxrows/maxcols)
2015
2016 {
2017 labels: {
2018 layer:
2019 rows:
2020 cols:
2021 total:
2022 },
2023 method: <aggregation method>,
2024 cells: [rows[cols]],
2025 rows: [rows[index, value, label, total]],
2026 cols: [cols[index, value, label, total]],
2027
2028 total: <grand total>,
2029 filter: [rows selector, cols selector]
2030 }
2031 """
2032
2033 rfields = self.rfields
2034 resource = self.resource
2035
2036 T = current.T
2037 OTHER = "__other__"
2038
2039 rows_dim = self.rows
2040 cols_dim = self.cols
2041
2042
2043 orows = []
2044 rappend = orows.append
2045 ocols = []
2046 cappend = ocols.append
2047 ocells = []
2048
2049 lookups = {}
2050 facts = self.facts
2051
2052 if not self.empty:
2053
2054
2055 row_repr = self._represent_method(rows_dim)
2056 col_repr = self._represent_method(cols_dim)
2057
2058
2059 others = s3_str(T("Others"))
2060
2061
2062
2063 layers = [fact.layer for fact in facts]
2064 least = facts[0].method == "min"
2065
2066
2067 irows = self.row
2068 rows = []
2069 rtail = (None, None)
2070 for i in xrange(self.numrows):
2071 irow = irows[i]
2072 totals = [irow[layer] for layer in layers]
2073 sort_total = totals[0]
2074 header = {"value": irow.value,
2075 "text": irow.text if "text" in irow
2076 else row_repr(irow.value),
2077 }
2078 rows.append((i, sort_total, totals, header))
2079 if maxrows is not None:
2080 rtail = self._tail(rows, maxrows, least=least, facts=facts)
2081 self._sortdim(rows, rfields[rows_dim])
2082 if rtail[1] is not None:
2083 values = [irows[i]["value"] for i in rtail[0]]
2084 rows.append((OTHER,
2085 rtail[1],
2086 rtail[2],
2087 {"value": values, "text":others},
2088 ))
2089
2090
2091 icols = self.col
2092 cols = []
2093 ctail = (None, None)
2094 for i in xrange(self.numcols):
2095 icol = icols[i]
2096 totals = [icol[layer] for layer in layers]
2097 sort_total = totals[0]
2098 header = {"value": icol.value,
2099 "text": icol.text if "text" in icol
2100 else col_repr(icol.value),
2101 }
2102 cols.append((i, sort_total, totals, header))
2103 if maxcols is not None:
2104 ctail = self._tail(cols, maxcols, least=least, facts=facts)
2105 self._sortdim(cols, rfields[cols_dim])
2106 if ctail[1] is not None:
2107 values = [icols[i]["value"] for i in ctail[0]]
2108 cols.append((OTHER,
2109 ctail[1],
2110 ctail[2],
2111 {"value": values, "text": others},
2112 ))
2113
2114 rothers = rtail[0] or set()
2115 cothers = ctail[0] or set()
2116
2117
2118
2119 icell = self.cell
2120 cells = {}
2121 for i in xrange(self.numrows):
2122 irow = icell[i]
2123 ridx = (i, OTHER) if rothers and i in rothers else (i,)
2124
2125 for j in xrange(self.numcols):
2126 cell = irow[j]
2127 cidx = (j, OTHER) if cothers and j in cothers else (j,)
2128
2129 cell_records = cell["records"]
2130
2131 for layer_index, layer in enumerate(layers):
2132
2133
2134
2135 items = cell[layer]
2136
2137
2138 if isinstance(items, list):
2139 value = len(items)
2140 else:
2141 value = items
2142
2143 for ri in ridx:
2144 if ri not in cells:
2145 orow = cells[ri] = {}
2146 else:
2147 orow = cells[ri]
2148 for ci in cidx:
2149
2150 if ci not in orow:
2151
2152 ocell = orow[ci] = {"values": [],
2153 "items": [],
2154 "records": [],
2155 }
2156 else:
2157 ocell = orow[ci]
2158
2159 if layer_index == 0:
2160
2161 ocell["records"].extend(cell_records)
2162
2163 value_array = ocell["values"]
2164 items_array = ocell["items"]
2165 if len(value_array) <= layer_index:
2166 value_array.append(value)
2167 items_array.append(items)
2168 else:
2169 ovalue = value_array[layer_index]
2170 oitems = items_array[layer_index]
2171 if isinstance(ovalue, list):
2172 ovalue.append(value)
2173 oitems.append(items)
2174 else:
2175 value_array[layer_index] = [ovalue, value]
2176 items_array[layer_index] = [oitems, items]
2177
2178
2179 represents = self._represents(layers)
2180
2181
2182 add_columns = True
2183 for rindex, rtotal, rtotals, rtitle in rows:
2184
2185 orow = []
2186
2187
2188 rval = rtitle["value"]
2189 if rindex == OTHER and isinstance(rval, list):
2190 rval = ",".join(s3_str(v) for v in rval)
2191 elif rval is not None:
2192 rval = s3_str(rval)
2193
2194
2195 rappend((rindex,
2196 rindex in rothers,
2197 rtotals,
2198 rval,
2199 rtitle["text"],
2200 ))
2201
2202 for cindex, ctotal, ctotals, ctitle in cols:
2203
2204
2205 cell = cells[rindex][cindex]
2206
2207 value_array = cell["values"]
2208 items_array = cell["items"]
2209
2210
2211 ocell = {"i": [], "v": []}
2212 okeys = None
2213
2214 for layer_index, fact in enumerate(facts):
2215
2216 selector, method = fact.layer
2217
2218
2219 items = items_array[layer_index]
2220
2221
2222 value = value_array[layer_index]
2223 if type(value) is list:
2224
2225 value = fact.aggregate_totals(value)
2226 ocell["v"].append(value)
2227
2228 rfield = self.rfields[selector]
2229
2230 if method == "list":
2231
2232 if selector not in lookups:
2233 lookup = lookups[selector] = {}
2234 else:
2235 lookup = lookups[selector]
2236
2237 represent = represents[selector]
2238
2239 keys = []
2240 for record_id in cell["records"]:
2241
2242 record = self.records[record_id]
2243 try:
2244 fvalue = record[rfield.colname]
2245 except AttributeError:
2246 continue
2247 if fvalue is None:
2248 continue
2249 if type(fvalue) is not list:
2250 fvalue = [fvalue]
2251
2252 for v in fvalue:
2253 if v is None:
2254 continue
2255 if v not in keys:
2256 keys.append(v)
2257 if v not in lookup:
2258 lookup[v] = represent(v)
2259
2260
2261 keys.sort(key=lambda i: lookup[i])
2262 items = [lookup[key] for key in keys if key in lookup]
2263
2264 elif method in ("sum", "count") and okeys is None:
2265
2266
2267 okeys = []
2268 for record_id in cell["records"]:
2269 record = self.records[record_id]
2270 try:
2271 fvalue = record[rfield.colname]
2272 except AttributeError:
2273 continue
2274 if method == "sum" and \
2275 isinstance(fvalue, (int, long, float)) and fvalue:
2276 okeys.append(record_id)
2277 elif method == "count" and \
2278 fvalue is not None:
2279 okeys.append(record_id)
2280 else:
2281
2282 okeys = cell["records"]
2283
2284 ocell["i"].append(items)
2285
2286 if okeys:
2287 ocell["k"] = okeys
2288 orow.append(ocell)
2289
2290 if add_columns:
2291
2292
2293 cval = ctitle["value"]
2294 if cindex == OTHER and isinstance(cval, list):
2295 cval = ",".join(s3_str(v) for v in cval)
2296 elif cval is not None:
2297 cval = s3_str(cval)
2298
2299
2300 cappend((cindex,
2301 cindex in cothers,
2302 ctotals,
2303 cval,
2304 ctitle["text"],
2305 ))
2306
2307 add_columns = False
2308 ocells.append(orow)
2309
2310
2311 report_options = resource.get_config("report_options", {})
2312 if report_options:
2313 fact_options = report_options.get("fact")
2314 else:
2315 fact_options = ()
2316
2317
2318
2319 fact_data = []
2320 fact_labels = []
2321 for fact in facts:
2322 rfield = rfields[fact.selector]
2323 fact_label = str(fact.get_label(rfield, fact_options))
2324 fact_data.append((fact.selector, fact.method, fact_label))
2325 fact_labels.append(fact_label)
2326
2327 get_label = S3PivotTableFact._get_field_label
2328 if rows_dim:
2329 rows_label = str(get_label(rfields[rows_dim], report_options.get("rows")))
2330 else:
2331 rows_label = ""
2332
2333 if cols_dim:
2334 cols_label = str(get_label(rfields[cols_dim], report_options.get("cols")))
2335 else:
2336 cols_label = ""
2337
2338 labels = {"total": str(T("Total")),
2339 "none": str(current.messages["NONE"]),
2340 "per": str(T("per")),
2341 "breakdown": str(T("Breakdown")),
2342
2343 "layer": " / ".join(fact_labels),
2344 "rows": rows_label,
2345 "cols": cols_label,
2346 }
2347
2348
2349 output = {"rows": orows,
2350 "cols": ocols,
2351 "facts": fact_data,
2352 "cells": ocells,
2353 "total": self._totals(self.totals, [fact]),
2354 "nodata": None if not self.empty else str(T("No data available")),
2355 "labels": labels,
2356 }
2357
2358
2359 prefix = resource.prefix_selector
2360 output["filter"] = (prefix(rows_dim) if rows_dim else None,
2361 prefix(cols_dim) if cols_dim else None,
2362 )
2363
2364 return output
2365
2366
2367 - def xls(self, title):
2368 """
2369 Convert this pivot table into an XLS file
2370
2371 @param title: the title of the report
2372
2373 @returns: the XLS file as stream
2374 """
2375
2376 from s3codec import S3Codec
2377 exporter = S3Codec.get_codec("xls")
2378
2379 return exporter.encode_pt(self, title)
2380
2381
2383 """
2384 Get the representation functions per fact field
2385
2386 @param layers: the list of layers, tuples (selector, method)
2387 """
2388
2389 rfields = self.rfields
2390 represents = {}
2391
2392 values = self.values
2393
2394 for selector, method in layers:
2395 if selector in represents:
2396 continue
2397
2398
2399 rfield = rfields[selector]
2400 f = rfield.field
2401
2402
2403 if method == "list" and \
2404 f is not None and hasattr(f.represent, "bulk"):
2405 all_values = values[(selector, method)]
2406 if all_values:
2407 f.represent.bulk(list(s3_flatlist(all_values)))
2408
2409
2410 has_fk = f is not None and s3_has_foreign_key(f)
2411 if has_fk:
2412 represent = lambda v, f=f: s3_str(f.represent(v))
2413 else:
2414 m = self._represent_method(selector)
2415 represent = lambda v, m=m: s3_str(m(v))
2416
2417 represents[selector] = represent
2418
2419 return represents
2420
2421
2422 @staticmethod
2424 """
2425 Sort a dimension (sorts items in-place)
2426
2427 @param items: the items as list of tuples
2428 (index, sort-total, totals, header)
2429 @param rfield: the dimension (S3ResourceField)
2430 @param index: alternative index of the value/text dict
2431 within each item
2432 """
2433
2434 if not rfield:
2435 return
2436
2437 ftype = rfield.ftype
2438
2439 sortby = "value"
2440 key = lambda item: item[index][sortby]
2441
2442 if ftype in ("integer", "string"):
2443
2444 requires = rfield.requires
2445 if isinstance(requires, (tuple, list)):
2446 requires = requires[0]
2447 if isinstance(requires, IS_EMPTY_OR):
2448 requires = requires.other
2449 if isinstance(requires, IS_IN_SET):
2450 sortby = "text"
2451
2452 elif ftype[:9] == "reference":
2453
2454 sortby = "text"
2455
2456 elif ftype == "date":
2457
2458 mindate = datetime.date.min
2459 key = lambda item: item[index][sortby] or mindate
2460
2461 elif ftype == "datetime":
2462
2463 mindate = datetime.datetime.min
2464 key = lambda item: item[index][sortby] or mindate
2465
2466 items.sort(key=key)
2467
2468
2469 @classmethod
2470 - def _tail(cls, items, length=10, least=False, facts=None):
2471 """
2472 Find the top/least <length> items (by total)
2473
2474 @param items: the items as list of tuples
2475 (index, sort-total, totals, header)
2476 @param length: the maximum number of items
2477 @param least: find least rather than top
2478 @param facts: the facts to aggregate the tail totals
2479 """
2480
2481 try:
2482 if len(items) > length:
2483 l = list(items)
2484 l.sort(lambda x, y: int(y[1]-x[1]))
2485 if least:
2486 l.reverse()
2487 keys = [item[0] for item in l[length-1:]]
2488 totals = []
2489 for i, fact in enumerate(facts):
2490 subtotals = [item[2][i] for item in l[length-1:]]
2491 totals.append(fact.aggregate_totals(subtotals))
2492 return (keys, totals[0], totals)
2493 except (TypeError, ValueError):
2494 pass
2495 return (None, None)
2496
2497
2498 @staticmethod
2499 - def _totals(values, facts, append=None):
2500 """
2501 Get the totals of a row/column/report
2502
2503 @param values: the values dictionary
2504 @param facts: the facts
2505 @param append: callback to collect the totals for JSON data
2506 (currently only collects the first layer)
2507 """
2508
2509 totals = []
2510 number_represent = IS_NUMBER.represent
2511 for fact in facts:
2512 value = values[fact.layer]
2513
2514
2515 if not len(totals) and append is not None:
2516 append(value)
2517 totals.append(s3_str(number_represent(value)))
2518 totals = " / ".join(totals)
2519 return totals
2520
2521
2522
2523
2524 @staticmethod
2525 - def _pivot(items, pkey_colname, rows_colname, cols_colname):
2526 """
2527 2-dimensional pivoting of a list of unique items
2528
2529 @param items: list of unique items as dicts
2530 @param pkey_colname: column name of the primary key
2531 @param rows_colname: column name of the row dimension
2532 @param cols_colname: column name of the column dimension
2533
2534 @return: tuple of (cell matrix, row headers, column headers),
2535 where cell matrix is a 2-dimensional array [rows[columns]]
2536 and row headers and column headers each are lists (in the
2537 same order as the cell matrix)
2538 """
2539
2540 rvalues = Storage()
2541 cvalues = Storage()
2542 cells = Storage()
2543
2544
2545 rindex = 0
2546 cindex = 0
2547 for item in items:
2548
2549 rvalue = item[rows_colname] if rows_colname else None
2550 cvalue = item[cols_colname] if cols_colname else None
2551
2552 if rvalue not in rvalues:
2553 r = rvalues[rvalue] = rindex
2554 rindex += 1
2555 else:
2556 r = rvalues[rvalue]
2557 if cvalue not in cvalues:
2558 c = cvalues[cvalue] = cindex
2559 cindex += 1
2560 else:
2561 c = cvalues[cvalue]
2562
2563 if (r, c) not in cells:
2564 cells[(r, c)] = [item[pkey_colname]]
2565 else:
2566 cells[(r, c)].append(item[pkey_colname])
2567
2568 matrix = []
2569 for r in xrange(len(rvalues)):
2570 row = []
2571 for c in xrange(len(cvalues)):
2572 row.append(cells[(r, c)])
2573 matrix.append(row)
2574
2575 rnames = [None] * len(rvalues)
2576 for k, v in rvalues.items():
2577 rnames[v] = k
2578
2579 cnames = [None] * len(cvalues)
2580 for k, v in cvalues.items():
2581 cnames[v] = k
2582
2583 return matrix, rnames, cnames
2584
2585
2587 """
2588 Compute an aggregation layer, updates:
2589
2590 - self.cell: the aggregated values per cell
2591 - self.row: the totals per row
2592 - self.col: the totals per column
2593 - self.totals: the overall totals per layer
2594
2595 @param matrix: the cell matrix
2596 @param fact: the fact field
2597 @param method: the aggregation method
2598 """
2599
2600 rows = self.row
2601 cols = self.col
2602 records = self.records
2603 extract = self._extract
2604 resource = self.resource
2605
2606 RECORDS = "records"
2607 VALUES = "values"
2608
2609 table = resource.table
2610 pkey = table._id.name
2611
2612 layer = fact.layer
2613 precision = self.precision.get(fact.selector)
2614
2615 numcols = len(self.col)
2616 numrows = len(self.row)
2617
2618
2619 if self.cell is None:
2620 self.cell = [[Storage()
2621 for i in xrange(numcols)]
2622 for j in xrange(numrows)]
2623 cells = self.cell
2624
2625 all_values = []
2626 for r in xrange(numrows):
2627
2628
2629 row = rows[r]
2630 row[RECORDS] = []
2631 row[VALUES] = []
2632
2633 row_records = row[RECORDS]
2634 row_values = row[VALUES]
2635
2636 for c in xrange(numcols):
2637
2638
2639 col = cols[c]
2640 if RECORDS not in col:
2641 col[RECORDS] = []
2642 col_records = col[RECORDS]
2643 if VALUES not in col:
2644 col[VALUES] = []
2645 col_values = col[VALUES]
2646
2647
2648 cell = cells[r][c]
2649 if RECORDS in cell and cell[RECORDS] is not None:
2650 ids = cell[RECORDS]
2651 else:
2652 data = matrix[r][c]
2653 if data:
2654 remove = data.remove
2655 while None in data:
2656 remove(None)
2657 ids = data
2658 else:
2659 ids = []
2660 cell[RECORDS] = ids
2661 row_records.extend(ids)
2662 col_records.extend(ids)
2663
2664
2665 if fact.selector is None:
2666 fact.selector = pkey
2667 values = ids
2668 row_values = row_records
2669 col_values = row_records
2670 all_values = records.keys()
2671 else:
2672 values = []
2673 append = values.append
2674 for i in ids:
2675 value = extract(records[i], fact.selector)
2676 if value is None:
2677 continue
2678 append(value)
2679 values = list(s3_flatlist(values))
2680 if fact.method in ("list", "count"):
2681 values = list(set(values))
2682 row_values.extend(values)
2683 col_values.extend(values)
2684 all_values.extend(values)
2685
2686
2687 value = fact.compute(values, precision=precision)
2688 cell[layer] = value
2689
2690
2691 row[layer] = fact.compute(row_values,
2692 totals = True,
2693 precision = precision,
2694 )
2695 del row[VALUES]
2696
2697
2698 for c in xrange(numcols):
2699 col = cols[c]
2700 col[layer] = fact.compute(col[VALUES],
2701 totals = True,
2702 precision = precision,
2703 )
2704 del col[VALUES]
2705
2706
2707 self.totals[layer] = fact.compute(all_values,
2708 totals = True,
2709 precision = precision,
2710 )
2711 self.values[layer] = all_values
2712
2713
2715 """
2716 Determine the fields needed to generate the report
2717
2718 @param fields: fields to include in the report (all fields)
2719 """
2720
2721 resource = self.resource
2722 table = resource.table
2723
2724
2725 alias = resource.alias
2726 def prefix(s):
2727 if isinstance(s, (tuple, list)):
2728 return prefix(s[-1])
2729 if "." not in s.split("$", 1)[0]:
2730 return "%s.%s" % (alias, s)
2731 elif s[:2] == "~.":
2732 return "%s.%s" % (alias, s[2:])
2733 else:
2734 return s
2735
2736 self.pkey = pkey = prefix(table._id.name)
2737 self.rows = rows = prefix(self.rows) if self.rows else None
2738 self.cols = cols = prefix(self.cols) if self.cols else None
2739
2740 if not fields:
2741 fields = ()
2742
2743
2744 dfields = [prefix(s) for s in fields]
2745 if rows and rows not in dfields:
2746 dfields.append(rows)
2747 if cols and cols not in dfields:
2748 dfields.append(cols)
2749 if pkey not in dfields:
2750 dfields.append(pkey)
2751
2752
2753 for fact in self.facts:
2754 fact.selector = selector = prefix(fact.selector)
2755 if selector not in dfields:
2756 dfields.append(selector)
2757 self.dfields = dfields
2758
2759
2760 precision = {}
2761 for selector, decimals in self.precision.items():
2762 precision[prefix(selector)] = decimals
2763 self.precision = precision
2764
2765
2766 rfields = resource.resolve_selectors(dfields)[0]
2767 rfields = Storage([(f.selector.replace("~", alias), f) for f in rfields])
2768 self.rfields = rfields
2769
2770
2771 self.gfields = {pkey: rfields[pkey].colname,
2772 rows: rfields[rows].colname
2773 if rows and rows in rfields else None,
2774 cols: rfields[cols].colname
2775 if cols and cols in rfields else None,
2776 }
2777
2778
2780 """
2781 Get the representation method for a field in the report
2782
2783 @param field: the field selector
2784 """
2785
2786 rfields = self.rfields
2787 default = lambda value: None
2788
2789 if field and field in rfields:
2790
2791 rfield = rfields[field]
2792 if rfield.field:
2793 def repr_method(value):
2794 return s3_represent_value(rfield.field,
2795 value,
2796 strip_markup = True,
2797 )
2798 elif rfield.virtual:
2799
2800
2801 represent = rfield.represent
2802 if not represent:
2803 represent = s3_str
2804
2805
2806 stripper = S3MarkupStripper()
2807 def repr_method(val):
2808 if val is None:
2809 return "-"
2810 text = represent(val)
2811 if "<" in text:
2812 stripper.feed(text)
2813 return stripper.stripped()
2814 else:
2815 return text
2816 else:
2817 repr_method = default
2818 else:
2819 repr_method = default
2820
2821 return repr_method
2822
2823
2825 """
2826 Extract a field value from a DAL row
2827
2828 @param row: the row
2829 @param field: the fieldname (list_fields syntax)
2830 """
2831
2832 rfields = self.rfields
2833 if field not in rfields:
2834 raise KeyError("Invalid field name: %s" % field)
2835 rfield = rfields[field]
2836 try:
2837 return rfield.extract(row)
2838 except AttributeError:
2839 return None
2840
2841
2842 - def _expand(self, row, axisfilter=None):
2843 """
2844 Expand a data frame row into a list of rows for list:type values
2845
2846 @param row: the row
2847 @param field: the field to expand (None for all fields)
2848 @param axisfilter: dict of filtered field values by column names
2849 """
2850
2851 pairs = []
2852 append = pairs.append
2853 for colname in self.gfields.values():
2854 if not colname:
2855 continue
2856 value = row[colname]
2857 if type(value) is list:
2858 if not value:
2859 value = [None]
2860 if axisfilter and colname in axisfilter:
2861 p = [(colname, v) for v in value
2862 if v in axisfilter[colname]]
2863 if not p:
2864 raise RuntimeError("record does not match query")
2865 else:
2866 append(p)
2867 else:
2868 append([(colname, v) for v in value])
2869 else:
2870 append([(colname, value)])
2871 result = [dict(i) for i in product(*pairs)]
2872 return result
2873
2874
2875