Package s3 :: Module s3data
[frames] | no frames]

Source Code for Module s3.s3data

   1  # -*- coding: utf-8 -*- 
   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 
43 44 # ============================================================================= 45 -class S3DataTable(object):
46 """ Class representing a data table """ 47 48 # The dataTable id if no explicit value has been provided 49 id_counter = 1 50 51 # ------------------------------------------------------------------------- 52 # Standard API 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 # Resolve orderby expression into column names 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 # Helper function to resolve a reference's "sortby" into 128 # a list of column names 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 = [] # order expression for datatable 148 append = dt_ordering.append 149 150 # Match orderby-fields against table columns (=rfields) 151 seen = set() 152 skip = seen.add 153 for i, colname in enumerate(orderby_cols): 154 if i < pos: 155 # Already consumed by sortby-tuple 156 continue 157 direction = orderby_dirs[colname] 158 for col_idx, rfield in enumerate(rfields): 159 if col_idx in seen: 160 # Column already in dt_ordering 161 continue 162 sortby = None 163 if rfield.colname == colname: 164 # Match a single orderby-field 165 sortby = (colname,) 166 else: 167 # Match between sortby and the orderby-field tuple 168 # (must appear in same order and sorting direction) 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 # Get the details for any bulk actions. If we have at least one bulk 226 # action then a column will be added, either at the start or in the 227 # column identified by dt_bulk_col 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
266 - def i18n():
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"), #T("No data available in table"), 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 # Get the details for any bulk actions. If we have at least one bulk 331 # action then a column will be added, either at the start or in the 332 # column identified by dt_bulk_col 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 # Extended API 353 # ------------------------------------------------------------------------- 354 @staticmethod
355 - def getConfigData():
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 # Nothing using currently 401 #if s3.dataTable_pagingType: 402 # attr.dt_pagingType = s3.dataTable_pagingType 403 if s3.dataTable_group: 404 attr.dt_group = s3.dataTable_group 405 # Nothing using currently 406 #if s3.dataTable_NoSearch: 407 # attr.dt_searching = not s3.dataTable_NoSearch 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
424 - def export_formats(rfields=None, permalink=None, base_url=None):
425 """ 426 Calculate the export formats that can be added to the table 427 428 @param rfields: optional list of field selectors for exports 429 @param permalink: search result URL 430 @param base_url: the base URL of the datatable (without 431 method or query vars) to construct format URLs 432 """ 433 434 T = current.T 435 s3 = current.response.s3 436 request = current.request 437 438 if base_url is None: 439 base_url = request.url 440 441 # @todo: other data formats could have other list_fields, 442 # so position-based datatable sorting/filters may 443 # be applied wrongly 444 if s3.datatable_ajax_source: 445 default_url = s3.datatable_ajax_source 446 else: 447 default_url = base_url 448 449 # Strip format extensions (e.g. .aadata or .iframe) 450 default_url = re.sub(r"(\/[a-zA-Z0-9_]*)(\.[a-zA-Z]*)", r"\g<1>", default_url) 451 452 # Keep any URL filters 453 get_vars = request.get_vars 454 if get_vars: 455 query = "&".join("%s=%s" % (k, v) for k, v in get_vars.items()) 456 default_url = "%s?%s" % (default_url, query) 457 458 # Construct row of export icons 459 # @note: icons appear in reverse order due to float-right 460 icons = SPAN(_class = "list_formats") 461 462 settings = current.deployment_settings 463 export_formats = settings.get_ui_export_formats() 464 if export_formats: 465 466 icons.append("%s:" % T("Export as")) 467 468 formats = dict(s3.formats) 469 470 # Auto-detect KML fields 471 if "kml" not in formats and rfields: 472 kml_fields = set(["location_id", "site_id"]) 473 if any(rfield.fname in kml_fields for rfield in rfields): 474 formats["kml"] = default_url 475 476 default_formats = ("xml", "rss", "xls", "pdf") 477 EXPORT = T("Export in %(format)s format") 478 479 append_icon = icons.append 480 for fmt in export_formats: 481 482 # CSS classes and on-hover title 483 title = None 484 if isinstance(fmt, tuple): 485 if len(fmt) >= 3: 486 title = fmt[2] 487 fmt, css = fmt[:2] if len(fmt) >= 2 else (fmt[0], "") 488 else: 489 css = "" 490 491 class_ = "dt-export export_%s" % fmt 492 if css: 493 class_ = "%s %s" % (class_, css) 494 495 if title is None: 496 if fmt == "map": 497 title = T("Show on Map") 498 else: 499 title = EXPORT % dict(format=fmt.upper()) 500 501 # Export format URL 502 if fmt in default_formats: 503 url = formats.get(fmt, default_url) 504 else: 505 url = formats.get(fmt) 506 if not url: 507 continue 508 509 append_icon(DIV(_class = class_, 510 _title = title, 511 data = {"url": url, 512 "extension": fmt.split(".")[-1], 513 }, 514 )) 515 516 export_options = DIV(_class="dt-export-options") 517 518 # Append the permalink (if any) 519 if permalink is not None: 520 label = settings.get_ui_label_permalink() 521 if label: 522 link = A(T(label), 523 _href=permalink, 524 _class="permalink") 525 export_options.append(link) 526 if len(icons): 527 export_options.append(" | ") 528 529 # Append the icons 530 export_options.append(icons) 531 532 return export_options
533 534 # ------------------------------------------------------------------------- 535 @staticmethod
536 - def defaultActionButtons(resource, 537 custom_actions=None, 538 r=None 539 ):
540 """ 541 Configure default action buttons 542 543 @param resource: the resource 544 @param r: the request, if specified, all action buttons will 545 be linked to the controller/function of this request 546 rather than to prefix/name of the resource 547 @param custom_actions: custom actions as list of dicts like 548 {"label":label, "url":url, "_class":class}, 549 will be appended to the default actions 550 551 @ToDo: DRY with S3CRUD.action_buttons() 552 """ 553 554 from s3crud import S3CRUD 555 556 s3 = current.response.s3 557 auth = current.auth 558 actions = s3.actions = None 559 560 table = resource.table 561 has_permission = auth.s3_has_permission 562 ownership_required = auth.permission.ownership_required 563 564 labels = s3.crud_labels 565 args = ["[id]"] 566 567 # Choose controller/function to link to 568 if r is not None: 569 c = r.controller 570 f = r.function 571 else: 572 c = resource.prefix 573 f = resource.name 574 575 tablename = resource.tablename 576 get_config = current.s3db.get_config 577 578 # "Open" button 579 editable = get_config(tablename, "editable", True) 580 if editable and has_permission("update", table) and \ 581 not ownership_required("update", table): 582 update_url = URL(c=c, f=f, args=args + ["update"]) 583 S3CRUD.action_button(labels.UPDATE, update_url, 584 icon = "edit", 585 _class="action-btn edit") 586 else: 587 read_url = URL(c=c, f=f, args=args) 588 S3CRUD.action_button(labels.READ, read_url, 589 icon = "file", 590 _class="action-btn read") 591 592 # Delete button 593 # @todo: does not apply selective action (renders DELETE for 594 # all items even if the user is only permitted to delete 595 # some of them) => should implement "restrict", see 596 # S3CRUD.action_buttons 597 deletable = get_config(tablename, "deletable", True) 598 if deletable and \ 599 has_permission("delete", table) and \ 600 not ownership_required("delete", table): 601 delete_url = URL(c=c, f=f, args=args + ["delete"]) 602 S3CRUD.action_button(labels.DELETE, delete_url, 603 icon = "delete", 604 _class="delete-btn") 605 606 # Append custom actions 607 if custom_actions: 608 actions = actions + custom_actions if actions else custom_actions
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 # The configuration parameter from the server to the client will be 689 # sent in a json object stored in an hidden input field. This object 690 # will then be parsed by s3.dataTable.js and the values used. 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 # NB This requires "table-layout:fixed" in your CSS 721 # You will likely need to specify all column widths if you do this 722 # & won't have responsiveness 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 # Activate double scroll and inject jQuery plugin 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 # Wrap the table in a form and add some data in hidden fields 781 form = FORM(_class="dt-wrapper") 782 if not s3.no_formats: 783 # @todo: move export-format update into drawCallback() 784 # @todo: poor UX with onclick-JS, better to render real 785 # links which can be bookmarked, and then update them 786 # in drawCallback() 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 # Nb These can be moved around in initComplete() 793 form.append(export_formats) 794 795 form.append(html) 796 797 # Add the configuration details for this dataTable 798 form.append(INPUT(_type="hidden", 799 _id="%s_configurations" % id, 800 _name="config", 801 _value=jsons(config))) 802 803 # If we have a cache set up then pass it in 804 if cache: 805 form.append(INPUT(_type="hidden", 806 _id="%s_dataTable_cache" %id, 807 _name="cache", 808 _value=jsons(cache))) 809 810 # If we have bulk actions then add the hidden fields 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 # Form key (CSRF protection for Ajax actions) 830 formkey = attr_get("dt_formkey") 831 if formkey: 832 form["hidden"] = {"_formkey": formkey} 833 834 # Set callback? 835 initComplete = settings.get_ui_datatables_initComplete() 836 if initComplete: 837 # Processed in views/dataTables.html 838 s3.dataTable_initComplete = initComplete 839 840 return form
841 842 # ------------------------------------------------------------------------- 843 # Helper methods 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 # Build the header row 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 # Build the body rows (the actual data) 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 # Insert a checkbox for bulk select 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 # Is this used anywhere? Can't see it used, so could be removed? 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
967 # ============================================================================= 968 -class S3DataList(object):
969 """ 970 Class representing a list of data cards 971 -clien-side implementation in static/scripts/S3/s3.dataLists.js 972 """ 973 974 # ------------------------------------------------------------------------- 975 # Standard API 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 # Call prep if present 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 # template 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 # template 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 #dl_data["popup_url"] = popup_url 1143 #dl_data["popup_title"] = popup_title 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
1180 # ============================================================================= 1181 -class S3DataListLayout(object):
1182 """ DataList default layout """ 1183 1184 item_class = "thumbnail" 1185 1186 # ---------------------------------------------------------------------
1187 - def __init__(self, profile=None):
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 # Render the item 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 # ---------------------------------------------------------------------
1233 - def render_header(self, list_id, item_id, resource, rfields, record):
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 #DIV( 1245 #ICON("icon"), 1246 #SPAN(" %s" % title, _class="card-title"), 1247 #toolbox, 1248 #_class="card-header", 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 # ---------------------------------------------------------------------
1284 - def render_icon(self, list_id, resource):
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 # ---------------------------------------------------------------------
1295 - def render_toolbox(self, list_id, resource, record):
1296 """ 1297 @todo: Render the toolbox 1298 1299 @param list_id: the HTML ID of the list 1300 @param resource: the S3Resource to render 1301 @param record: the record as dict 1302 """ 1303 1304 return None
1305 1306 # ---------------------------------------------------------------------
1307 - def render_column(self, item_id, rfield, record):
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 # END ========================================================================= 1334