1
2
3 """ S3 Data Views
4
5 @copyright: 2009-2019 (c) Sahana Software Foundation
6 @license: MIT
7
8 Permission is hereby granted, free of charge, to any person
9 obtaining a copy of this software and associated documentation
10 files (the "Software"), to deal in the Software without
11 restriction, including without limitation the rights to use,
12 copy, modify, merge, publish, distribute, sublicense, and/or sell
13 copies of the Software, and to permit persons to whom the
14 Software is furnished to do so, subject to the following
15 conditions:
16
17 The above copyright notice and this permission notice shall be
18 included in all copies or substantial portions of the Software.
19
20 THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
21 EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
22 OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
23 NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
24 HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
25 WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
26 FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
27 OTHER DEALINGS IN THE SOFTWARE.
28
29 @group Data Views: S3DataTable,
30 S3DataList
31 """
32
33 import re
34
35 from itertools import islice
36
37 from gluon import current
38 from gluon.html import *
39 from gluon.storage import Storage
40
41 from s3dal import Expression, S3DAL
42 from s3utils import s3_orderby_fields, s3_str, s3_unicode, s3_set_extension
46 """ Class representing a data table """
47
48
49 id_counter = 1
50
51
52
53
54 - def __init__(self,
55 rfields,
56 data,
57 start=0,
58 limit=None,
59 filterString=None,
60 orderby=None,
61 empty=False,
62 ):
63 """
64 S3DataTable constructor
65
66 @param rfields: A list of S3Resourcefield
67 @param data: A list of Storages the key is of the form table.field
68 The value is the data to be displayed in the dataTable
69 @param start: the first row to return from the data
70 @param limit: the (maximum) number of records to return
71 @param filterString: The string that was used in filtering the records
72 @param orderby: the DAL orderby construct
73 """
74
75 self.data = data
76 self.rfields = rfields
77 self.empty = empty
78
79 colnames = []
80 heading = {}
81
82 append = colnames.append
83 for rfield in rfields:
84 colname = rfield.colname
85 heading[colname] = rfield.label
86 append(colname)
87
88 self.colnames = colnames
89 self.heading = heading
90
91 data_len = len(data)
92 if start < 0:
93 start = 0
94 if start > data_len:
95 start = data_len
96 if limit == None:
97 end = data_len
98 else:
99 end = start + limit
100 if end > data_len:
101 end = data_len
102 self.start = start
103 self.end = end
104 self.filterString = filterString
105
106 if orderby:
107
108
109 orderby_dirs = {}
110 orderby_cols = []
111
112 adapter = S3DAL()
113 INVERT = adapter.INVERT
114
115 append = orderby_cols.append
116 for f in s3_orderby_fields(None, orderby, expr=True):
117 if type(f) is Expression:
118 colname = str(f.first)
119 direction = "desc" if f.op == INVERT else "asc"
120 else:
121 colname = str(f)
122 direction = "asc"
123 orderby_dirs[colname] = direction
124 append(colname)
125 pos = 0
126
127
128
129 ftuples = {}
130 def resolve_sortby(rfield):
131 colname = rfield.colname
132 if colname in ftuples:
133 return ftuples[colname]
134 ftype = rfield.ftype
135 sortby = None
136 if ftype[:9] == "reference":
137 field = rfield.field
138 if hasattr(field, "sortby") and field.sortby:
139 sortby = field.sortby
140 if not isinstance(sortby, (tuple, list)):
141 sortby = [sortby]
142 p = "%s.%%s" % ftype[10:].split(".")[0]
143 sortby = [p % fname for fname in sortby]
144 ftuples[colname] = sortby
145 return sortby
146
147 dt_ordering = []
148 append = dt_ordering.append
149
150
151 seen = set()
152 skip = seen.add
153 for i, colname in enumerate(orderby_cols):
154 if i < pos:
155
156 continue
157 direction = orderby_dirs[colname]
158 for col_idx, rfield in enumerate(rfields):
159 if col_idx in seen:
160
161 continue
162 sortby = None
163 if rfield.colname == colname:
164
165 sortby = (colname,)
166 else:
167
168
169 sortby = resolve_sortby(rfield)
170 if not sortby or \
171 sortby != orderby_cols[i:i + len(sortby)] or \
172 any(orderby_dirs[c] != direction for c in sortby):
173 sortby = None
174 if sortby:
175 append([col_idx, direction])
176 pos += len(sortby)
177 skip(col_idx)
178 break
179 else:
180 dt_ordering = [[1, "asc"]]
181
182 self.orderby = dt_ordering
183
184
185 - def html(self,
186 totalrows,
187 filteredrows,
188 id = None,
189 draw = 1,
190 **attr
191 ):
192 """
193 Method to render the dataTable into html
194
195 @param totalrows: The total rows in the unfiltered query.
196 @param filteredrows: The total rows in the filtered query.
197 @param id: The id of the table these need to be unique if more
198 than one dataTable is to be rendered on the same page.
199 If this is not passed in then a unique id will be
200 generated. Regardless the id is stored in self.id
201 so it can be easily accessed after rendering.
202 @param draw: An unaltered copy of draw sent from the client used
203 by dataTables as a draw count.
204 @param attr: dictionary of attributes which can be passed in
205 """
206
207 flist = self.colnames
208
209 if not id:
210 id = "list_%s" % self.id_counter
211 self.id_counter += 1
212 self.id = id
213
214 bulkActions = attr.get("dt_bulk_actions", None)
215 bulkCol = attr.get("dt_bulk_col", 0)
216 if bulkCol > len(flist):
217 bulkCol = len(flist)
218 action_col = attr.get("dt_action_col", 0)
219 if action_col != 0:
220 if action_col == -1 or action_col >= len(flist):
221 action_col = len(flist) -1
222 attr["dt_action_col"] = action_col
223 flist = flist[1:action_col+1] + [flist[0]] + flist[action_col+1:]
224
225
226
227
228 if bulkActions:
229 flist.insert(bulkCol, "BULK")
230 if bulkCol <= action_col:
231 action_col += 1
232
233 pagination = attr.get("dt_pagination", "true") == "true"
234 if pagination:
235 real_end = self.end
236 self.end = self.start + 1
237 table = self.table(id, flist, action_col)
238 if pagination:
239 self.end = real_end
240 aadata = self.aadata(totalrows,
241 filteredrows,
242 id,
243 draw,
244 flist,
245 action_col=action_col,
246 stringify=False,
247 **attr)
248 cache = {"cacheLower": self.start,
249 "cacheUpper": self.end if filteredrows > self.end else filteredrows,
250 "cacheLastJson": aadata,
251 }
252 else:
253 cache = None
254
255 html = self.htmlConfig(table,
256 id,
257 self.orderby,
258 self.rfields,
259 cache,
260 **attr
261 )
262 return html
263
264
265 @staticmethod
267 """
268 Return the i18n strings needed by dataTables
269 - called by views/dataTables.html
270 """
271
272 T = current.T
273 scripts = ['''i18n.sortAscending="%s"''' % T("activate to sort column ascending"),
274 '''i18n.sortDescending="%s"''' % T("activate to sort column descending"),
275 '''i18n.first="%s"''' % T("First"),
276 '''i18n.last="%s"''' % T("Last"),
277 '''i18n.next="%s"''' % T("Next"),
278 '''i18n.previous="%s"''' % T("Previous"),
279 '''i18n.emptyTable="%s"''' % T("No records found"),
280 '''i18n.info="%s"''' % T("Showing _START_ to _END_ of _TOTAL_ entries"),
281 '''i18n.infoEmpty="%s"''' % T("Showing 0 to 0 of 0 entries"),
282 '''i18n.infoFiltered="%s"''' % T("(filtered from _MAX_ total entries)"),
283 '''i18n.infoThousands="%s"''' % current.deployment_settings.get_L10n_thousands_separator(),
284 '''i18n.lengthMenu="%s"''' % (T("Show %(number)s entries") % {"number": "_MENU_"}),
285 '''i18n.loadingRecords="%s"''' % T("Loading"),
286 '''i18n.processing="%s"''' % T("Processing"),
287 '''i18n.search="%s"''' % T("Search"),
288 '''i18n.zeroRecords="%s"''' % T("No matching records found"),
289 '''i18n.selectAll="%s"''' % T("Select All")
290 ]
291 script = "\n".join(scripts)
292
293 return script
294
295
296 - def json(self,
297 totalrows,
298 displayrows,
299 id,
300 draw,
301 stringify=True,
302 **attr
303 ):
304 """
305 Method to render the data into a json object
306
307 @param totalrows: The total rows in the unfiltered query.
308 @param displayrows: The total rows in the filtered query.
309 @param id: The id of the table for which this ajax call will
310 respond to.
311 @param draw: An unaltered copy of draw sent from the client used
312 by dataTables as a draw count.
313 @param attr: dictionary of attributes which can be passed in
314 dt_action_col: The column where the action buttons will be placed
315 dt_bulk_actions: list of labels for the bulk actions.
316 dt_bulk_col: The column in which the checkboxes will appear,
317 by default it will be the column immediately
318 before the first data item
319 dt_group_totals: The number of record in each group.
320 This will be displayed in parenthesis
321 after the group title.
322 """
323
324 flist = self.colnames
325 action_col = attr.get("dt_action_col", 0)
326 if action_col != 0:
327 if action_col == -1 or action_col >= len(flist):
328 action_col = len(flist) - 1
329 flist = flist[1:action_col+1] + [flist[0]] + flist[action_col+1:]
330
331
332
333 bulkActions = attr.get("dt_bulk_actions", None)
334 bulkCol = attr.get("dt_bulk_col", 0)
335 if bulkActions:
336 if bulkCol > len(flist):
337 bulkCol = len(flist)
338 flist.insert(bulkCol, "BULK")
339 if bulkCol <= action_col:
340 action_col += 1
341
342 return self.aadata(totalrows,
343 displayrows,
344 id,
345 draw,
346 flist,
347 action_col=action_col,
348 stringify=stringify,
349 **attr)
350
351
352
353
354 @staticmethod
356 """
357 Method to extract the configuration data from S3 globals and
358 store them as an attr variable.
359 - used by Survey module
360
361 @return: dictionary of attributes which can be passed into html()
362
363 @param attr: dictionary of attributes which can be passed in
364 dt_pageLength : The default number of records that will be shown
365 dt_pagination: Enable pagination
366 dt_pagingType: type of pagination, one of:
367 simple
368 simple_numbers
369 full
370 full_numbers (default)
371 http://datatables.net/reference/option/pagingType
372 dt_searching: Enable or disable filtering of data.
373 dt_group: The colum that is used to group the data
374 dt_ajax_url: The URL to be used for the Ajax call
375 dt_action_col: The column where the action buttons will be placed
376 dt_bulk_actions: list of labels for the bulk actions.
377 dt_bulk_col: The column in which the checkboxes will appear,
378 by default it will be the column immediately
379 before the first data item
380 dt_bulk_selected: A list of selected items
381 dt_actions: dictionary of actions
382 dt_styles: dictionary of styles to be applied to a list of ids
383 for example:
384 {"warning" : [1,3,6,7,9],
385 "alert" : [2,10,13]}
386 """
387
388 s3 = current.response.s3
389
390 attr = Storage()
391 if s3.datatable_ajax_source:
392 attr.dt_ajax_url = s3.datatable_ajax_source
393 if s3.actions:
394 attr.dt_actions = s3.actions
395 if s3.dataTableBulkActions:
396 attr.dt_bulk_actions = s3.dataTableBulkActions
397 if s3.dataTable_pageLength:
398 attr.dt_pageLength = s3.dataTable_pageLength
399 attr.dt_pagination = "false" if s3.no_sspag else "true"
400
401
402
403 if s3.dataTable_group:
404 attr.dt_group = s3.dataTable_group
405
406
407
408 if s3.dataTable_dom:
409 attr.dt_dom = s3.dataTable_dom
410 if s3.dataTableDisplay:
411 attr.dt_display = s3.dataTableDisplay
412 if s3.dataTableStyleDisabled or s3.dataTableStyleWarning or s3.dataTableStyleAlert:
413 attr.dt_styles = {}
414 if s3.dataTableStyleDisabled:
415 attr.dt_styles["dtdisable"] = s3.dataTableStyleDisabled
416 if s3.dataTableStyleWarning:
417 attr.dt_styles["dtwarning"] = s3.dataTableStyleWarning
418 if s3.dataTableStyleAlert:
419 attr.dt_styles["dtalert"] = s3.dataTableStyleAlert
420 return attr
421
422
423 @staticmethod
533
534
535 @staticmethod
609
610
611 @staticmethod
612 - def htmlConfig(html,
613 id,
614 orderby,
615 rfields = None,
616 cache = None,
617 **attr
618 ):
619 """
620 Method to wrap the html for a dataTable in a form, add the export formats
621 and the config details required by dataTables
622
623 @param html: The html table
624 @param id: The id of the table
625 @param orderby: the sort details see http://datatables.net/reference/option/order
626 @param rfields: The list of resource fields
627 @param attr: dictionary of attributes which can be passed in
628 dt_lengthMenu: The menu options for the number of records to be shown
629 dt_pageLength : The default number of records that will be shown
630 dt_dom : The Datatable DOM initialisation variable, describing
631 the order in which elements are displayed.
632 See http://datatables.net/ref for more details.
633 dt_pagination : Is pagination enabled, dafault 'true'
634 dt_pagingType : How the pagination buttons are displayed
635 dt_searching: Enable or disable filtering of data.
636 dt_ajax_url: The URL to be used for the Ajax call
637 dt_action_col: The column where the action buttons will be placed
638 dt_bulk_actions: list of labels for the bulk actions.
639 dt_bulk_col: The column in which the checkboxes will appear,
640 by default it will be the column immediately
641 before the first data item
642 dt_group: The column(s) that is(are) used to group the data
643 dt_group_totals: The number of record in each group.
644 This will be displayed in parenthesis
645 after the group title.
646 dt_group_titles: The titles to be used for each group.
647 These are a list of lists with the inner list
648 consisting of two values, the repr from the
649 db and the label to display. This can be more than
650 the actual number of groups (giving an empty group).
651 dt_group_space: Insert a space between the group heading and the next group
652 dt_bulk_selected: A list of selected items
653 dt_actions: dictionary of actions
654 dt_styles: dictionary of styles to be applied to a list of ids
655 for example:
656 {"warning" : [1,3,6,7,9],
657 "alert" : [2,10,13]}
658 dt_col_widths: dictionary of columns to apply a width to
659 for example:
660 {1 : 15,
661 2 : 20}
662 dt_text_maximum_len: The maximum length of text before it is condensed
663 dt_text_condense_len: The length displayed text is condensed down to
664 dt_double_scroll: Render double scroll bars (top+bottom), only available
665 with settings.ui.datatables_responsive=False
666 dt_shrink_groups: If set then the rows within a group will be hidden
667 two types are supported, 'individual' and 'accordion'
668 dt_group_types: The type of indicator for groups that can be 'shrunk'
669 Permitted valies are: 'icon' (the default) 'text' and 'none'
670 dt_base_url: base URL to construct export format URLs, resource
671 default URL without any URL method or query part
672
673 @global current.response.s3.actions used to get the RowActions
674 """
675
676 from gluon.serializers import json as jsons
677
678 request = current.request
679 s3 = current.response.s3
680 settings = current.deployment_settings
681
682 dataTableID = s3.dataTableID
683 if not dataTableID or not isinstance(dataTableID, list):
684 dataTableID = s3.dataTableID = [id]
685 elif id not in dataTableID:
686 dataTableID.append(id)
687
688
689
690
691 config = Storage()
692 config.id = id
693 attr_get = attr.get
694 config.dom = attr_get("dt_dom", settings.get_ui_datatables_dom())
695 config.lengthMenu = attr_get("dt_lengthMenu",
696 [[25, 50, -1],
697 [25, 50, s3_str(current.T("All"))]
698 ]
699 )
700 config.pageLength = attr_get("dt_pageLength", s3.ROWSPERPAGE)
701 config.pagination = attr_get("dt_pagination", "true")
702 config.pagingType = attr_get("dt_pagingType",
703 settings.get_ui_datatables_pagingType())
704 config.searching = attr_get("dt_searching", "true")
705
706 ajaxUrl = attr_get("dt_ajax_url", None)
707 if not ajaxUrl:
708 url = URL(c=request.controller,
709 f=request.function,
710 args=request.args,
711 vars=request.get_vars,
712 )
713 ajaxUrl = s3_set_extension(url, "aadata")
714 config.ajaxUrl = ajaxUrl
715
716 config.rowStyles = attr_get("dt_styles", [])
717
718 colWidths = attr_get("dt_col_widths")
719 if colWidths is not None:
720
721
722
723 config.colWidths = colWidths
724
725 rowActions = attr_get("dt_row_actions", s3.actions)
726 if rowActions:
727 config.rowActions = rowActions
728 else:
729 config.rowActions = []
730 bulkActions = attr_get("dt_bulk_actions", None)
731 if bulkActions and not isinstance(bulkActions, list):
732 bulkActions = [bulkActions]
733 config.bulkActions = bulkActions
734 config.bulkCol = bulkCol = attr_get("dt_bulk_col", 0)
735 action_col = attr_get("dt_action_col", 0)
736 if bulkActions and bulkCol <= action_col:
737 action_col += 1
738 config.actionCol = action_col
739
740 group_list = attr_get("dt_group", [])
741 if not isinstance(group_list, list):
742 group_list = [group_list]
743 dt_group = []
744 for group in group_list:
745 if bulkActions and bulkCol <= group:
746 group += 1
747 if action_col >= group:
748 group -= 1
749 dt_group.append([group, "asc"])
750 config.group = dt_group
751 config.groupTotals = attr_get("dt_group_totals", [])
752 config.groupTitles = attr_get("dt_group_titles", [])
753 config.groupSpacing = attr_get("dt_group_space")
754 for order in orderby:
755 if bulkActions:
756 if bulkCol <= order[0]:
757 order[0] += 1
758 if action_col > 0 and action_col >= order[0]:
759 order[0] -= 1
760 config.order = orderby
761 config.textMaxLength = attr_get("dt_text_maximum_len", 80)
762 config.textShrinkLength = attr_get("dt_text_condense_len", 75)
763 config.shrinkGroupedRows = attr_get("dt_shrink_groups")
764 config.groupIcon = attr_get("dt_group_types", [])
765
766
767 if not settings.get_ui_datatables_responsive():
768 double_scroll = attr.get("dt_double_scroll")
769 if double_scroll is None:
770 double_scroll = settings.get_ui_datatables_double_scroll()
771 if double_scroll:
772 if s3.debug:
773 script = "/%s/static/scripts/jquery.doubleScroll.js" % request.application
774 else:
775 script = "/%s/static/scripts/jquery.doubleScroll.min.js" % request.application
776 if script not in s3.scripts:
777 s3.scripts.append(script)
778 html.add_class("doublescroll")
779
780
781 form = FORM(_class="dt-wrapper")
782 if not s3.no_formats:
783
784
785
786
787 permalink = attr_get("dt_permalink", None)
788 base_url = attr_get("dt_base_url", None)
789 export_formats = S3DataTable.export_formats(rfields,
790 permalink=permalink,
791 base_url=base_url)
792
793 form.append(export_formats)
794
795 form.append(html)
796
797
798 form.append(INPUT(_type="hidden",
799 _id="%s_configurations" % id,
800 _name="config",
801 _value=jsons(config)))
802
803
804 if cache:
805 form.append(INPUT(_type="hidden",
806 _id="%s_dataTable_cache" %id,
807 _name="cache",
808 _value=jsons(cache)))
809
810
811 if bulkActions:
812 form.append(INPUT(_type="hidden",
813 _id="%s_dataTable_bulkMode" % id,
814 _name="mode",
815 _value="Inclusive"))
816 bulk_selected = attr_get("dt_bulk_selected", "")
817 if isinstance(bulk_selected, list):
818 bulk_selected = ",".join(bulk_selected)
819 form.append(INPUT(_type="hidden",
820 _id="%s_dataTable_bulkSelection" % id,
821 _name="selected",
822 _value="[%s]" % bulk_selected))
823 form.append(INPUT(_type="hidden",
824 _id="%s_dataTable_filterURL" % id,
825 _class="dataTable_filterURL",
826 _name="filterURL",
827 _value="%s" % config.ajaxUrl))
828
829
830 formkey = attr_get("dt_formkey")
831 if formkey:
832 form["hidden"] = {"_formkey": formkey}
833
834
835 initComplete = settings.get_ui_datatables_initComplete()
836 if initComplete:
837
838 s3.dataTable_initComplete = initComplete
839
840 return form
841
842
843
844
845 - def table(self, id, flist=None, action_col=0):
846 """
847 Method to render the data as an html table. This is of use if
848 an html table is required without the dataTable goodness. However
849 if you want html for a dataTable then use the html() method
850
851 @param id: The id of the table
852 @param flist: The list of fields
853 @param action_col: The column where action columns will be displayed
854 (this is required by dataTables)
855 """
856
857 data = self.data
858 heading = self.heading
859 start = self.start
860 end = self.end
861 if not flist:
862 flist = self.colnames
863
864
865 header = THEAD()
866 tr = TR()
867 for field in flist:
868 if field == "BULK":
869 tr.append(TH(""))
870 else:
871 tr.append(TH(heading[field]))
872 header.append(tr)
873
874 body = TBODY()
875 if data:
876
877 rc = 0
878 for i in xrange(start, end):
879 row = data[i]
880 if rc % 2 == 0:
881 _class = "even"
882 else:
883 _class = "odd"
884 rc += 1
885 tr = TR(_class=_class)
886 for field in flist:
887
888 if field == "BULK":
889 tr.append(TD(INPUT(_type="checkbox",
890 _class="bulkcheckbox",
891 data = {"dbid": row[flist[action_col]]},
892 )))
893 else:
894 tr.append(TD(row[field]))
895 body.append(tr)
896 table = TABLE([header, body], _id=id, _class="dataTable display")
897
898 if current.deployment_settings.get_ui_datatables_responsive():
899 table.add_class("responsive")
900 return table
901
902
903 - def aadata(self,
904 totalrows,
905 displayrows,
906 id,
907 draw,
908 flist,
909 stringify=True,
910 action_col=None,
911 **attr
912 ):
913 """
914 Method to render the data into a json object
915
916 @param totalrows: The total rows in the unfiltered query.
917 @param displayrows: The total rows in the filtered query.
918 @param id: The id of the table for which this ajax call will
919 respond to.
920 @param draw: An unaltered copy of draw sent from the client used
921 by dataTables as a draw count.
922 @param flist: The list of fields
923 @param attr: dictionary of attributes which can be passed in
924 dt_action_col: The column where the action buttons will be placed
925 dt_bulk_actions: list of labels for the bulk actions.
926 dt_bulk_col: The column in which the checkboxes will appear,
927 by default it will be the column immediately
928 before the first data item
929 dt_group_totals: The number of record in each group.
930 This will be displayed in parenthesis
931 after the group title.
932 """
933
934 data = self.data
935 if not flist:
936 flist = self.colnames
937 start = self.start
938 end = self.end
939 if action_col is None:
940 action_col = attr.get("dt_action_col", 0)
941 structure = {}
942 aadata = []
943 for i in xrange(start, end):
944 row = data[i]
945 details = []
946 for field in flist:
947 if field == "BULK":
948 details.append("<INPUT type='checkbox' class='bulkcheckbox' data-dbid='%s'>" % \
949 row[flist[action_col]])
950 else:
951 details.append(s3_unicode(row[field]))
952 aadata.append(details)
953 structure["dataTable_id"] = id
954 structure["dataTable_filter"] = self.filterString
955 structure["dataTable_groupTotals"] = attr.get("dt_group_totals", [])
956 structure["dataTable_sort"] = self.orderby
957 structure["data"] = aadata
958 structure["recordsTotal"] = totalrows
959 structure["recordsFiltered"] = displayrows
960 structure["draw"] = draw
961 if stringify:
962 from gluon.serializers import json as jsons
963 return jsons(structure)
964 else:
965 return structure
966
969 """
970 Class representing a list of data cards
971 -clien-side implementation in static/scripts/S3/s3.dataLists.js
972 """
973
974
975
976
977 - def __init__(self,
978 resource,
979 list_fields,
980 records,
981 start=None,
982 limit=None,
983 total=None,
984 list_id=None,
985 layout=None,
986 row_layout=None):
987 """
988 Constructor
989
990 @param resource: the S3Resource
991 @param list_fields: the list fields
992 (list of field selector strings)
993 @param records: the records
994 @param start: index of the first item
995 @param limit: maximum number of items
996 @param total: total number of available items
997 @param list_id: the HTML ID for this list
998 @param layout: item renderer (optional) as function
999 (list_id, item_id, resource, rfields, record)
1000 @param row_layout: row renderer (optional) as
1001 function(list_id, resource, rowsize, items)
1002 """
1003
1004 self.resource = resource
1005 self.list_fields = list_fields
1006 self.records = records
1007
1008 if list_id is None:
1009 self.list_id = "datalist"
1010 else:
1011 self.list_id = list_id
1012
1013 if layout is not None:
1014 self.layout = layout
1015 else:
1016 self.layout = S3DataListLayout()
1017 self.row_layout = row_layout
1018
1019 self.start = start if start else 0
1020 self.limit = limit if limit else 0
1021 self.total = total if total else 0
1022
1023
1024 - def html(self,
1025 start=None,
1026 limit=None,
1027 pagesize=None,
1028 rowsize=None,
1029 ajaxurl=None,
1030 empty=None,
1031 popup_url=None,
1032 popup_title=None,
1033 ):
1034 """
1035 Render list data as HTML (nested DIVs)
1036
1037 @param start: index of the first item (in this page)
1038 @param limit: total number of available items
1039 @param pagesize: maximum number of items per page
1040 @param rowsize: number of items per row
1041 @param ajaxurl: the URL to Ajax-update the datalist
1042 @param empty: message to display if the list is empty
1043 @param popup_url: the URL for the modal used for the 'more'
1044 button (=> we deactivate InfiniteScroll)
1045 @param popup_title: the title for the modal
1046 """
1047
1048 T = current.T
1049 resource = self.resource
1050 list_fields = self.list_fields
1051 rfields = resource.resolve_selectors(list_fields)[0]
1052
1053 list_id = self.list_id
1054 render = self.layout
1055 render_row = self.row_layout
1056
1057 if not rowsize:
1058 rowsize = 1
1059
1060 pkey = str(resource._id)
1061
1062 records = self.records
1063 if records is not None:
1064
1065
1066 if hasattr(render, "prep"):
1067 render.prep(resource, records)
1068
1069 if current.response.s3.dl_no_header:
1070 items = []
1071 else:
1072 items = [DIV(T("Total Records: %(numrows)s") % \
1073 {"numrows": self.total},
1074 _class="dl-header",
1075 _id="%s-header" % list_id,
1076 )
1077 ]
1078
1079 if empty is None:
1080 empty = resource.crud.crud_string(resource.tablename,
1081 "msg_no_match")
1082 empty = DIV(empty, _class="dl-empty")
1083 if self.total > 0:
1084 empty.update(_style="display:none")
1085 items.append(empty)
1086
1087 row_idx = int(self.start / rowsize) + 1
1088 for group in self.groups(records, rowsize):
1089 row = []
1090 col_idx = 0
1091 for record in group:
1092
1093 if pkey in record:
1094 item_id = "%s-%s" % (list_id, record[pkey])
1095 else:
1096
1097 item_id = "%s-[id]" % list_id
1098
1099 item = render(list_id,
1100 item_id,
1101 resource,
1102 rfields,
1103 record)
1104 if hasattr(item, "add_class"):
1105 _class = "dl-item dl-%s-cols dl-col-%s" % (rowsize, col_idx)
1106 item.add_class(_class)
1107 row.append(item)
1108 col_idx += 1
1109
1110 _class = "dl-row %s" % ((row_idx % 2) and "even" or "odd")
1111 if render_row:
1112 row = render_row(list_id,
1113 resource,
1114 rowsize,
1115 row)
1116 if hasattr(row, "add_class"):
1117 row.add_class(_class)
1118 else:
1119 row = DIV(row, _class=_class)
1120
1121 items.append(row)
1122 row_idx += 1
1123 else:
1124
1125 raise NotImplementedError
1126
1127 dl = DIV(items,
1128 _class="dl",
1129 _id=list_id,
1130 )
1131
1132 dl_data = {"startindex": start,
1133 "maxitems": limit,
1134 "totalitems": self.total,
1135 "pagesize": pagesize,
1136 "rowsize": rowsize,
1137 "ajaxurl": ajaxurl,
1138 }
1139 if popup_url:
1140 input_class = "dl-pagination"
1141 a_class = "s3_modal dl-more"
1142
1143
1144 else:
1145 input_class = "dl-pagination dl-scroll"
1146 a_class = "dl-more"
1147 from gluon.serializers import json as jsons
1148 dl_data = jsons(dl_data)
1149 dl.append(DIV(INPUT(_type="hidden",
1150 _class=input_class,
1151 _value=dl_data,
1152 ),
1153 A(T("more..."),
1154 _href = popup_url or ajaxurl,
1155 _class = a_class,
1156 _title = popup_title,
1157 ),
1158 _class="dl-navigation",
1159 ))
1160
1161 return dl
1162
1163
1164 @staticmethod
1165 - def groups(iterable, length):
1166 """
1167 Iterator to group data list items into rows
1168
1169 @param iterable: the items iterable
1170 @param length: the number of items per row
1171 """
1172
1173 iterable = iter(iterable)
1174 group = list(islice(iterable, length))
1175 while group:
1176 yield group
1177 group = list(islice(iterable, length))
1178 raise StopIteration
1179
1182 """ DataList default layout """
1183
1184 item_class = "thumbnail"
1185
1186
1188 """
1189 Constructor
1190
1191 @param profile: table name of the master resource of the
1192 profile page (if used for a profile), can be
1193 used in popup URLs to indicate the master
1194 resource
1195 """
1196
1197 self.profile = profile
1198
1199
1200 - def __call__(self, list_id, item_id, resource, rfields, record):
1201 """
1202 Wrapper for render_item.
1203
1204 @param list_id: the HTML ID of the list
1205 @param item_id: the HTML ID of the item
1206 @param resource: the S3Resource to render
1207 @param rfields: the S3ResourceFields to render
1208 @param record: the record as dict
1209 """
1210
1211
1212 item = DIV(_id=item_id, _class=self.item_class)
1213
1214 header = self.render_header(list_id,
1215 item_id,
1216 resource,
1217 rfields,
1218 record)
1219 if header is not None:
1220 item.append(header)
1221
1222 body = self.render_body(list_id,
1223 item_id,
1224 resource,
1225 rfields,
1226 record)
1227 if body is not None:
1228 item.append(body)
1229
1230 return item
1231
1232
1234 """
1235 @todo: Render the card header
1236
1237 @param list_id: the HTML ID of the list
1238 @param item_id: the HTML ID of the item
1239 @param resource: the S3Resource to render
1240 @param rfields: the S3ResourceFields to render
1241 @param record: the record as dict
1242 """
1243
1244
1245
1246
1247
1248
1249
1250 return None
1251
1252
1253 - def render_body(self, list_id, item_id, resource, rfields, record):
1254 """
1255 Render the card body
1256
1257 @param list_id: the HTML ID of the list
1258 @param item_id: the HTML ID of the item
1259 @param resource: the S3Resource to render
1260 @param rfields: the S3ResourceFields to render
1261 @param record: the record as dict
1262 """
1263
1264 pkey = str(resource._id)
1265 body = DIV(_class="media-body")
1266
1267 render_column = self.render_column
1268 for rfield in rfields:
1269
1270 if not rfield.show or rfield.colname == pkey:
1271 continue
1272
1273 column = render_column(item_id, rfield, record)
1274 if column is not None:
1275 table_class = "dl-table-%s" % rfield.tname
1276 field_class = "dl-field-%s" % rfield.fname
1277 body.append(DIV(column,
1278 _class = "dl-field %s %s" % (table_class,
1279 field_class)))
1280
1281 return DIV(body, _class="media")
1282
1283
1285 """
1286 @todo: Render a body icon
1287
1288 @param list_id: the HTML ID of the list
1289 @param resource: the S3Resource to render
1290 """
1291
1292 return None
1293
1294
1305
1306
1308 """
1309 Render a data column.
1310
1311 @param item_id: the HTML element ID of the item
1312 @param rfield: the S3ResourceField for the column
1313 @param record: the record (from S3Resource.select)
1314 """
1315
1316 colname = rfield.colname
1317 if colname not in record:
1318 return None
1319
1320 value = record[colname]
1321 value_id = "%s-%s" % (item_id, rfield.colname.replace(".", "_"))
1322
1323 label = LABEL("%s:" % rfield.label,
1324 _for = value_id,
1325 _class = "dl-field-label")
1326
1327 value = SPAN(value,
1328 _id = value_id,
1329 _class = "dl-field-value")
1330
1331 return TAG[""](label, value)
1332
1333
1334