-
-
Notifications
You must be signed in to change notification settings - Fork 218
/
generic.py
1210 lines (1048 loc) · 40.8 KB
/
generic.py
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import datetime
import io
import json
import re
from itertools import chain
from django.http import (
Http404,
HttpResponse,
HttpResponseRedirect,
JsonResponse,
)
from django.shortcuts import render
from django.template.loader import render_to_string
from django.urls import NoReverseMatch
from django.utils.safestring import mark_safe
from django.utils.translation import gettext
from django.utils.html import conditional_escape
from celery.utils.log import get_task_logger
from memoized import memoized
from corehq.util.timezones.utils import get_timezone
from couchexport.export import export_from_tables, get_writer
from couchexport.shortcuts import export_response
from dimagi.utils.modules import to_function
from dimagi.utils.parsing import string_to_boolean
from dimagi.utils.web import json_request, json_response
from corehq.apps.domain.utils import normalize_domain_name
from corehq.apps.hqwebapp.crispy import CSS_ACTION_CLASS
from corehq.apps.hqwebapp.decorators import (
use_datatables,
use_daterangepicker,
use_jquery_ui,
use_nvd3,
)
from corehq.apps.reports.cache import request_cache
from corehq.apps.reports.datatables import DataTablesHeader
from corehq.apps.reports.filters.dates import DatespanFilter
from corehq.apps.reports.tasks import export_all_rows_task
from corehq.apps.reports.util import DatatablesParams
from corehq.apps.saved_reports.models import ReportConfig
from corehq.apps.users.models import CouchUser
from corehq.util.view_utils import absolute_reverse, request_as_dict, reverse
CHART_SPAN_MAP = {1: '10', 2: '6', 3: '4', 4: '3', 5: '2', 6: '2'}
def _sanitize_rows(rows):
return [_sanitize_row(row) for row in rows]
def _sanitize_row(row):
return [_sanitize_col(col) for col in row]
def _sanitize_col(col):
if isinstance(col, str):
return conditional_escape(col)
# HACK: dictionaries make it here. The dictionaries I've seen have an 'html' key
# which I expect to be sanitized already, but there is no guaranteee
return col
def get_filter_classes(fields, request, domain, timezone):
filters = []
fields = fields
for field in fields or []:
if isinstance(field, str):
klass = to_function(field, failhard=True)
else:
klass = field
filters.append(
klass(request, domain, timezone)
)
return filters
class GenericReportView(object):
"""
A generic report structure for viewing a report
(or pages that follow the reporting structure closely---though that seems a bit hacky)
This object is handled by the ReportDispatcher and served as a django view based on
the report maps specified in settings.py
To make the report return anything, override any or all of the following properties:
@property
template_context
- returns a dict to be inserted into self.context
- only items relevant to base_template structure should be placed here. Anything
related to report data and async templates should be done in report_context
@property
report_context
- returns a dict to be inserted into self.context
- this is where the main processing of the report data should happen
Note: In general you should not be inserting things into self.context directly, unless absolutely
necessary. Please use the structure in the above properties for updating self.context
in the relevant places.
@property
json_dict
- returns a dict to be parsed and returned as json for the json version of this report
(generally has only been useful for datatables paginated reports)
@property
export_table
- returns a multi-dimensional list formatted as export_from_tables would expect:
[ ['table_or_sheet_name', [['header'] ,['row']] ] ]
"""
# required to create a report based on this
name = None # Human-readable name to be used in the UI
slug = None # Name to be used in the URL (with lowercase and underscores)
section_name = None # string. ex: "Reports"
dispatcher = None # ReportDispatcher subclass
toggles = () # Optionally provide toggles to turn on/off the report
# whether to use caching on @request_cache methods. will ignore this if CACHE_REPORTS is set to False
is_cacheable = False
# Code can expect `fields` to be an iterable even when empty (never None)
fields = ()
# not required
description = None # Human-readable description of the report
documentation_link = None # Link to docs page if available
report_template_path = None
report_partial_path = None
asynchronous = False
hide_filters = False
emailable = False
printable = False
exportable = False
exportable_all = False # also requires overriding self.get_all_rows
export_format_override = None
icon = None
# the defaults for this should be sufficient. But if they aren't, well go for it.
base_template = None
base_template_async = None
base_template_filters = None
print_override_template = "reports/async/print_report.html"
flush_layout = False
# Todo: maybe make these a little cleaner?
show_timezone_notice = False
show_time_notice = False
is_admin_report = False
special_notice = None
# whether to ignore the permissions check that's done when rendering
# the report
override_permissions_check = False
report_title = None
report_subtitles = []
# For drilldown reports, we hide the child reports from navigation.
# For those child reports, set the parent's report class here so that we
# still include these reports in the list of reports we do access control
# against.
parent_report_class = None
is_deprecated = False
deprecation_email_message = gettext("This report has been deprecated.")
deprecation_message = gettext("This report has been deprecated.")
def __init__(self, request, base_context=None, domain=None, **kwargs):
if not self.name or not self.section_name or self.slug is None or not self.dispatcher:
raise NotImplementedError(
f'Missing a required parameter: (name: {self.name}, '
f'section_name: {self.section_name}, slug: {self.slug}, '
f'dispatcher: {self.dispatcher}'
)
from corehq.apps.reports.dispatcher import ReportDispatcher
if isinstance(self.dispatcher, ReportDispatcher):
raise ValueError("Class property dispatcher should point to a subclass of ReportDispatcher.")
self.request = request
self.request_params = json_request(self.request.GET if self.request.method == 'GET' else self.request.POST)
self.domain = normalize_domain_name(domain)
self.context = base_context or {}
self._update_initial_context()
self.is_rendered_as_email = False # setting this to true in email_response
self.is_rendered_as_export = False
self.override_template = "reports/async/email_report.html"
def __str__(self):
if self.fields:
field_lines = "\n -".join(self.fields)
fields = f"\n Report Fields: \n -{field_lines}"
else:
fields = ""
if self.description:
desc = f"\n Report Description: {self.description}"
else:
desc = ""
return (
f"{self.__class__.__name__} report named '{self.name}' with slug "
f"'{self.slug}' in section '{self.section_name}'.{desc}{fields}"
)
def __getstate__(self):
"""
For pickling the report when passing it to Celery.
"""
request = request_as_dict(self.request)
return dict(
request=request,
request_params=self.request_params,
domain=self.domain,
context={}
)
_caching = False
def __setstate__(self, state):
"""
For unpickling a pickled report.
"""
logging = get_task_logger(__name__) # logging is likely to happen within celery.
self.domain = state.get('domain')
self.context = state.get('context', {})
class FakeHttpRequest(object):
method = 'GET'
domain = ''
GET = {}
META = {}
couch_user = None
datespan = None
can_access_all_locations = None
request_data = state.get('request')
request = FakeHttpRequest()
request.domain = self.domain
request.GET = request_data.get('GET', {})
request.META = request_data.get('META', {})
request.datespan = request_data.get('datespan')
request.can_access_all_locations = request_data.get('can_access_all_locations')
try:
couch_user = CouchUser.get_by_user_id(request_data.get('couch_user'))
request.couch_user = couch_user
except Exception as e:
logging.error("Could not unpickle couch_user from request for report %s. Error: %s" %
(self.name, e))
self.request = request
self._caching = True
self.request_params = state.get('request_params')
self._update_initial_context()
@property
@memoized
def url_root(self):
path = self.request.META.get('PATH_INFO', "")
try:
root = path[0:path.index(self.slug)]
except ValueError:
root = None
return root
@property
def queried_path(self):
path = self.request.META.get('PATH_INFO')
query = self.request.META.get('QUERY_STRING')
return "%s:%s" % (path, query)
@property
@memoized
def domain_object(self):
if self.domain is not None:
from corehq.apps.domain.models import Domain
return Domain.get_by_name(self.domain)
return None
@property
@memoized
def timezone(self):
return get_timezone(self.request, self.domain)
@property
@memoized
def template_base(self):
return self.base_template
@property
@memoized
def template_async_base(self):
if self.asynchronous:
return self.base_template_async or "reports/async/bootstrap3/default.html"
return self.template_base
@property
@memoized
def template_report(self):
original_template = self.report_template_path or "reports/async/basic.html"
if self.is_rendered_as_email:
self.context.update(original_template=original_template)
return self.override_template
return original_template
@property
@memoized
def template_report_partial(self):
return self.report_partial_path
@property
@memoized
def template_filters(self):
return self.base_template_filters or "reports/async/bootstrap3/filters.html"
@property
@memoized
def rendered_report_title(self):
return gettext(self.name)
@property
@memoized
def filter_classes(self):
return get_filter_classes(self.fields, self.request, self.domain, self.timezone)
@property
@memoized
def export_format(self):
from couchexport.models import Format
return self.export_format_override or self.request.GET.get('format', Format.XLS_2007)
@property
def export_name(self):
return self.slug
@property
def export_target(self):
writer = get_writer(self.export_format)
return writer.target_app
@property
def default_report_url(self):
return "#"
@property
def breadcrumbs(self):
"""
Override this for custom breadcrumbs.
Use the format:
dict(
title="breadcrumb title",
link="url title links to"
)
This breadcrumb does not include the report title, it's only the links in between the section name
and the report title.
"""
return None
@property
def template_context(self):
"""
Intention: Override if necessary.
Update context specific to the wrapping template here.
Nothing specific to the report should go here, use report_context for that.
Must return a dict.
"""
return {
'rendered_as': self.rendered_as,
}
@property
def report_context(self):
"""
Intention: Override
!!! CRUCIAL: This is where ALL the intense processing of the report data happens.
DO NOT update self.context from here or anything that gets processed in here.
The dictionary returned by this function can get cached in memcached to optimize a report.
Must return a dict.
"""
return dict()
@property
def json_dict(self):
"""
Intention: Override
Return a json-parsable dict, as needed by your report.
"""
return {}
@property
def export_table(self):
"""
Intention: Override
Returns an export table to be parsed by export_from_tables.
"""
return [
[
'table_or_sheet_name',
[
['header'],
['row 1'],
['row 2'],
]
]
]
@property
def filter_set(self):
"""
Whether a report has any filters set. Based on whether or not there
is a query string. This gets carried to additional asynchronous calls
"""
are_filters_set = bool(self.request.META.get('QUERY_STRING'))
if "filterSet" in self.request.GET:
try:
are_filters_set = string_to_boolean(self.request.GET.get("filterSet"))
except ValueError:
# not a parseable boolean
pass
return are_filters_set
@property
def needs_filters(self):
"""
Whether a report needs filters. A shortcut for hide_filters is false and
filter_set is false.
If no filters are used, False is automatically returned.
"""
if len(self.fields) == 0:
return False
else:
return not self.hide_filters and not self.filter_set
def _validate_context_dict(self, property):
if not isinstance(property, dict):
raise TypeError("property must return a dict")
return property
def _update_initial_context(self):
"""
Intention: Don't override.
"""
report_configs = ReportConfig.by_domain_and_owner(self.domain,
self.request.couch_user._id, report_slug=self.slug)
current_config_id = self.request.GET.get('config_id', '')
default_config = ReportConfig.default()
def is_editable_datespan(field):
field_fn = to_function(field) if isinstance(field, str) else field
return issubclass(field_fn, DatespanFilter) and field_fn.is_editable
has_datespan = any([is_editable_datespan(field) for field in self.fields])
self.context.update(
report=dict(
title=self.rendered_report_title,
description=self.description,
documentation_link=self.documentation_link,
section_name=self.section_name,
slug=self.slug,
sub_slug=None,
type=self.dispatcher.prefix,
url_root=self.url_root,
is_async=self.asynchronous,
is_exportable=self.exportable,
dispatcher=self.dispatcher,
filter_set=self.filter_set,
needs_filters=self.needs_filters,
has_datespan=has_datespan,
show=(
self.override_permissions_check
or self.request.couch_user.can_view_some_reports(self.domain)
),
is_emailable=self.emailable,
is_export_all=self.exportable_all,
is_printable=self.printable,
is_admin=self.is_admin_report,
special_notice=self.special_notice,
report_title=self.report_title or self.rendered_report_title,
report_subtitles=self.report_subtitles,
export_target=self.export_target,
js_options=self.js_options,
),
current_config_id=current_config_id,
default_config=default_config,
report_configs=report_configs,
show_time_notice=self.show_time_notice,
domain=self.domain,
layout_flush_content=self.flush_layout,
)
@property
def js_options(self):
try:
async_url = self.get_url(domain=self.domain, render_as='async', relative=True)
except NoReverseMatch:
async_url = ''
return {
'async': self.asynchronous,
'domain': self.domain,
'filterSet': self.filter_set,
'isEmailable': self.emailable,
'isExportAll': self.exportable_all,
'isExportable': self.exportable,
'needsFilters': self.needs_filters,
'slug': self.slug,
'subReportSlug': None,
'emailDefaultSubject': self.rendered_report_title,
'type': self.dispatcher.prefix,
'urlRoot': self.url_root,
'asyncUrl': async_url
}
def update_filter_context(self):
"""
Intention: This probably does not need to be overridden in general.
Updates the context with filter information.
"""
self.context.update({
'report_filters': [
dict(field=f.render(), slug=f.slug) for f in self.filter_classes
],
})
def update_template_context(self):
"""
Intention: This probably does not need to be overridden in general.
Please override template_context instead.
"""
self.context.update(rendered_as=self.rendered_as)
self.context.update({
'report_filter_form_action_css_class': CSS_ACTION_CLASS,
})
self.context['report'].update(
show_filters=self.fields or not self.hide_filters,
breadcrumbs=self.breadcrumbs,
default_url=self.default_report_url,
url=self.get_url(domain=self.domain),
title=self.rendered_report_title
)
if hasattr(self, 'datespan'):
self.context.update(datespan=self.datespan)
if self.show_timezone_notice:
self.context.update(timezone={
'now': datetime.datetime.now(tz=self.timezone),
'zone': self.timezone.zone,
})
self.context.update(self._validate_context_dict(self.template_context))
def update_report_context(self):
"""
Intention: This probably does not need to be overridden in general.
Please override report_context instead.
"""
self.context.update(
report_partial=self.template_report_partial,
report_base=self.template_async_base
)
self.context['report'].update(
title=self.rendered_report_title, # overriding the default title
)
self.context.update(self._validate_context_dict(self.report_context))
@property
def deprecate_response(self):
from django.contrib import messages
messages.warning(
self.request,
self.deprecation_message
)
return HttpResponseRedirect(self.request.META.get('HTTP_REFERER', '/'))
@property
def view_response(self):
"""
Intention: Not to be overridden in general.
Renders the general view of the report template.
"""
if self.is_deprecated:
return self.deprecate_response
else:
self.update_template_context()
template = self.template_base
if not self.asynchronous:
self.update_filter_context()
self.update_report_context()
template = self.template_report
return render(self.request, template, self.context)
@property
def email_response(self):
"""
This renders a json object containing a pointer to the static html
content of the report. It is intended for use by the report scheduler.
"""
self.is_rendered_as_email = True
return self.async_response
@property
@request_cache()
def async_response(self):
"""
Intention: Not to be overridden in general.
Renders the asynchronous view of the report template, returned as json.
"""
return JsonResponse(self._async_context())
def _async_context(self):
self.update_template_context()
self.update_report_context()
rendered_filters = None
if bool(self.request.GET.get('hq_filters')):
self.update_filter_context()
rendered_filters = render_to_string(
self.template_filters, self.context, request=self.request
)
rendered_report = render_to_string(
self.template_report, self.context, request=self.request
)
report_table_js_options = {}
if 'report_table_js_options' in self.context:
report_table_js_options = self.context['report_table_js_options']
return dict(
filters=rendered_filters,
report=rendered_report,
report_table_js_options=report_table_js_options,
title=self.rendered_report_title,
slug=self.slug,
url_root=self.url_root,
)
@property
def excel_response(self):
file = io.BytesIO()
export_from_tables(self.export_table, file, self.export_format)
return file
@property
@request_cache(expiry=60 * 10)
def filters_response(self):
"""
Intention: Not to be overridden in general.
Renders just the filters for the report to be fetched asynchronously.
"""
self.update_filter_context()
rendered_filters = render_to_string(
self.template_filters, self.context, request=self.request
)
return HttpResponse(json.dumps(dict(
filters=rendered_filters,
slug=self.slug,
url_root=self.url_root
)))
@property
@request_cache()
def json_response(self):
"""
Intention: Not to be overridden in general.
Renders the json version for the report, if available.
"""
return json_response(self.json_dict)
@property
def export_response(self):
"""
Intention: Not to be overridden in general.
Returns the tabular export of the data, if available.
"""
self.is_rendered_as_export = True
if self.exportable_all:
export_all_rows_task.delay(self.__class__, self.__getstate__())
return HttpResponse()
else:
# We only want to cache the responses which serve files directly
# The response which return 200 and emails the reports should not be cached
return self._export_response_direct()
@request_cache()
def _export_response_direct(self):
temp = io.BytesIO()
export_from_tables(self.export_table, temp, self.export_format)
return export_response(temp, self.export_format, self.export_name)
@property
@request_cache()
def print_response(self):
"""
Returns the report for printing.
"""
self.is_rendered_as_email = True
self.use_datatables = False
self.override_template = self.print_override_template
return HttpResponse(self._async_context()['report'])
@property
def partial_response(self):
"""
Use this response for rendering smaller chunks of your report.
(Great if you have a giant report with annoying, complex indicators.)
"""
raise Http404
@classmethod
def get_url(cls, domain=None, render_as=None, relative=False, **kwargs):
# NOTE: I'm pretty sure this doesn't work if you ever pass in render_as
# but leaving as is for now, as it should be obvious as soon as that
# breaks something
if isinstance(cls, cls):
domain = getattr(cls, 'domain')
render_as = getattr(cls, 'rendered_as')
if render_as is not None and render_as not in cls.dispatcher.allowed_renderings():
raise ValueError('The render_as parameter is not one of the following allowed values: %s' %
', '.join(cls.dispatcher.allowed_renderings()))
url_args = [domain] if domain is not None else []
if render_as is not None:
url_args.append(render_as + '/')
if relative:
return reverse(cls.dispatcher.name(), args=url_args + [cls.slug])
return absolute_reverse(cls.dispatcher.name(), args=url_args + [cls.slug])
@classmethod
def allow_access(cls, request):
"""
Override to add additional constraints on report access on top of
what's provided by the dispatcher. For feature flags, see the toggles
attribute
"""
return True
@classmethod
def show_in_navigation(cls, domain=None, project=None, user=None):
return True
@classmethod
def show_in_user_roles(cls, domain=None, project=None, user=None):
"""
User roles can specify specific reports that users can view. Return True if this report should show in
the list of specific reports that can be viewed.
"""
return cls.show_in_navigation(domain, project, user)
@classmethod
def display_in_dropdown(cls, domain=None, project=None, user=None):
return False
@classmethod
def get_subpages(cls):
"""
List of subpages to show in sidebar navigation.
"""
return []
@use_nvd3
@use_jquery_ui
@use_datatables
@use_daterangepicker
def decorator_dispatcher(self, request, *args, **kwargs):
"""
Decorate this method in your report subclass and call super to make sure
appropriate decorators are used to render the page and its javascript
libraries.
example:
class MyNewReport(GenericReport):
...
@use_nvd3
def decorator_dispatcher(self, request, *args, **kwargs):
super(MyNewReport, self).decorator_dispatcher(request, *args, **kwargs)
"""
pass
class GenericTabularReport(GenericReportView):
"""
Override the following properties:
@property
headers
- returns a DataTablesHeader object
@property
rows
- returns a 2D list of rows.
## AJAX pagination
If you plan on using ajax pagination, take into consideration
the following properties when rendering self.rows:
self.pagination.start (skip)
self.pagination.count (limit)
Make sure you also override the following properties as necessary:
@property
total_records
- returns an integer
- the total records of what you are paginating over
@property
shared_pagination_GET_params
- this is where you select the GET parameters to pass to the paginator
- returns a list formatted like [dict(name='group', value=self.group_id)]
## Charts
To include charts in the report override the following property.
@property
charts
- returns a list of Chart objects e.g. PieChart, MultiBarChart
You can also adjust the following properties:
charts_per_row
- the number of charts to show in a row. 1, 2, 3, 4, or 6
"""
# new class properties
total_row = None
statistics_rows = None
force_page_size = False # force page size to be as the default rows
default_rows = 10
start_at_row = 0
show_all_rows = False
fix_left_col = False
disable_pagination = False
ajax_pagination = False
use_datatables = True
charts_per_row = 1
bad_request_error_text = None
exporting_as_excel = False
# Sets bSort in the datatables instance to true/false (config.dataTables.bootstrap.js)
sortable = True
# override old class properties
report_template_path = "reports/bootstrap3/tabular.html"
flush_layout = True
# set to a list of functions that take in a report object
# and return a dictionary of items that will show up in
# the report context
extra_context_providers = []
@property
def headers(self):
"""
Override this method to create a functional tabular report.
Returns a DataTablesHeader() object (or a list, but preferably the former.
"""
return DataTablesHeader()
@property
def rows(self):
"""
Override this method to create a functional tabular report.
Returns 2D list of rows.
[['row1'],[row2']]
"""
return []
@property
def get_all_rows(self):
"""
Override this method to return all records to export
"""
return []
@property
def total_records(self):
"""
Override for pagination.
Returns an integer.
"""
return 0
@property
def total_filtered_records(self):
"""
Override for pagination.
Returns an integer.
return -1 if you want total_filtered_records to equal whatever the value of total_records is.
"""
return -1
@property
def charts(self):
"""
Override to return a list of Chart objects.
"""
return []
@property
def shared_pagination_GET_params(self):
"""
Override.
Should return a list of dicts with the name and value of the GET parameters
that you'd like to pass to the server-side pagination.
ex: [dict(name='group', value=self.group_id)]
"""
return []
@property
def pagination_source(self):
return self.get_url(domain=self.domain, render_as='json')
_pagination = None
@property
def pagination(self):
if self._pagination is None:
self._pagination = DatatablesParams.from_request_dict(
self.request.POST if self.request.method == 'POST' else self.request.GET
)
return self._pagination
@property
def json_dict(self):
"""
When you implement self.rows for a paginated report,
it should take into consideration the following:
self.pagination.start (skip)
self.pagination.count (limit)
"""
rows = _sanitize_rows(self.rows)
total_records = self.total_records
if not isinstance(total_records, int):
raise ValueError("Property 'total_records' should return an int.")
total_filtered_records = self.total_filtered_records
if not isinstance(total_filtered_records, int):
raise ValueError("Property 'total_filtered_records' should return an int.")
ret = dict(
sEcho=self.pagination.echo,
iTotalRecords=total_records,
iTotalDisplayRecords=total_filtered_records if total_filtered_records >= 0 else total_records,
aaData=rows,
)
if self.total_row:
ret["total_row"] = list(self.total_row)
if self.statistics_rows:
ret["statistics_rows"] = list(self.statistics_rows)
return ret
@property
def fixed_cols_spec(self):
"""
Override
Returns a dict formatted like:
dict(num=<num_cols_to_fix>, width=<width_of_total_fixed_cols>)
"""
return dict(num=1, width=200)
@staticmethod
def _strip_tags(value):
"""
Strip HTML tags from a value
"""
# Uses regex. Regex is much faster than using an HTML parser, but will
# strip "<2 && 3>" from a value like "1<2 && 3>2". A parser will treat
# each cell like an HTML document, which might be overkill, but if
# using regex breaks values then we should use a parser instead, and
# take the knock. Assuming we won't have values with angle brackets,
# using regex for now.
if isinstance(value, str):
return re.sub('<[^>]*?>', '', value)
return value
@property
def override_export_sheet_name(self):
"""
Override the export sheet name here. Return a string.
"""
return None
_export_sheet_name = None
@property
def export_sheet_name(self):
if self._export_sheet_name is None:
override = self.override_export_sheet_name
self._export_sheet_name = override if isinstance(override, str) else self.name
return self._export_sheet_name
@property
def export_table(self):
"""
Exports the report as excel.