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

Source Code for Module s3.s3crud

   1  # -*- coding: utf-8 -*- 
   2   
   3  """ S3 RESTful CRUD Methods 
   4   
   5      @see: U{B{I{S3XRC}} <http://eden.sahanafoundation.org/wiki/S3XRC>} 
   6   
   7      @requires: U{B{I{gluon}} <http://web2py.com>} 
   8      @requires: U{B{I{lxml}} <http://codespeak.net/lxml>} 
   9   
  10      @copyright: 2009-2019 (c) Sahana Software Foundation 
  11      @license: MIT 
  12   
  13      Permission is hereby granted, free of charge, to any person 
  14      obtaining a copy of this software and associated documentation 
  15      files (the "Software"), to deal in the Software without 
  16      restriction, including without limitation the rights to use, 
  17      copy, modify, merge, publish, distribute, sublicense, and/or sell 
  18      copies of the Software, and to permit persons to whom the 
  19      Software is furnished to do so, subject to the following 
  20      conditions: 
  21   
  22      The above copyright notice and this permission notice shall be 
  23      included in all copies or substantial portions of the Software. 
  24   
  25      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
  26      EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
  27      OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
  28      NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
  29      HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
  30      WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
  31      FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 
  32      OTHER DEALINGS IN THE SOFTWARE. 
  33  """ 
  34   
  35  __all__ = ("S3CRUD",) 
  36   
  37  import json 
  38   
  39  try: 
  40      from lxml import etree 
  41  except ImportError: 
  42      import sys 
  43      sys.stderr.write("ERROR: lxml module needed for XML handling\n") 
  44      raise 
  45   
  46  from gluon import current, redirect, HTTP, URL, \ 
  47                    A, DIV, FORM, INPUT, TABLE, TD, TR, XML 
  48  from gluon.contenttype import contenttype 
  49  from gluon.languages import lazyT 
  50  from gluon.storage import Storage 
  51  from gluon.tools import callback 
  52   
  53  from s3datetime import S3DateTime, s3_decode_iso_datetime 
  54  from s3export import S3Exporter 
  55  from s3forms import S3SQLDefaultForm 
  56  from s3rest import S3Method 
  57  from s3utils import s3_str, s3_unicode, s3_validate, s3_represent_value, s3_set_extension 
  58  from s3widgets import S3EmbeddedComponentWidget, S3Selector, ICON 
  59   
  60  # Compact JSON encoding 
  61  SEPARATORS = (",", ":") 
62 63 # ============================================================================= 64 -class S3CRUD(S3Method):
65 """ 66 Interactive CRUD Method Handler 67 """ 68 69 # -------------------------------------------------------------------------
70 - def apply_method(self, r, **attr):
71 """ 72 Apply CRUD methods 73 74 @param r: the S3Request 75 @param attr: dictionary of parameters for the method handler 76 77 @return: output object to send to the view 78 """ 79 80 self.settings = current.response.s3.crud 81 sqlform = self._config("crud_form") 82 self.sqlform = sqlform if sqlform else S3SQLDefaultForm() 83 84 # Pre-populate create-form? 85 self.data = None 86 if r.http == "GET" and not self.record_id: 87 populate = attr.pop("populate", None) 88 if callable(populate): 89 try: 90 self.data = populate(r, **attr) 91 except TypeError: 92 self.data = None 93 except: 94 raise 95 elif isinstance(populate, dict): 96 self.data = populate 97 98 method = self.method 99 100 if r.http == "DELETE" or self.method == "delete": 101 output = self.delete(r, **attr) 102 elif method == "create": 103 output = self.create(r, **attr) 104 elif method == "read": 105 output = self.read(r, **attr) 106 elif method == "update": 107 output = self.update(r, **attr) 108 109 # Standard list view: list-type and hide-filter set by controller 110 # (default: list_type="datatable", hide_filter=None) 111 elif method == "list": 112 output = self.select(r, **attr) 113 114 # URL Methods to explicitly choose list-type and hide-filter in the URL 115 elif method in ("datatable", "datatable_f"): 116 _attr = Storage(attr) 117 _attr["list_type"] = "datatable" 118 if method == "datatable_f": 119 self.hide_filter = False 120 output = self.select(r, **_attr) 121 elif method in ("datalist", "datalist_f"): 122 _attr = Storage(attr) 123 _attr["list_type"] = "datalist" 124 if method == "datalist_f": 125 self.hide_filter = False 126 output = self.select(r, **_attr) 127 128 elif method == "validate": 129 output = self.validate(r, **attr) 130 elif method == "review": 131 if r.record: 132 output = self.review(r, **attr) 133 else: 134 output = self.unapproved(r, **attr) 135 else: 136 r.error(405, current.ERROR.BAD_METHOD) 137 138 return output
139 140 # -------------------------------------------------------------------------
141 - def widget(self, r, method=None, widget_id=None, visible=True, **attr):
142 """ 143 Entry point for other method handlers to embed this 144 method as widget 145 146 @param r: the S3Request 147 @param method: the widget method 148 @param widget_id: the widget ID 149 @param visible: whether the widget is initially visible 150 @param attr: controller attributes 151 """ 152 153 # Settings 154 self.settings = current.response.s3.crud 155 sqlform = self._config("crud_form") 156 self.sqlform = sqlform if sqlform else S3SQLDefaultForm() 157 158 _attr = Storage(attr) 159 _attr["list_id"] = widget_id 160 161 if method == "datatable": 162 output = self._datatable(r, **_attr) 163 if isinstance(output, dict): 164 output = DIV(output["items"], _id="table-container") 165 return output 166 elif method == "datalist": 167 output = self._datalist(r, **_attr) 168 if isinstance(output, dict) and "items" in output: 169 output = DIV(output["items"], _id="list-container") 170 return output 171 elif method == "create": 172 return self._widget_create(r, **_attr) 173 else: 174 return None
175 176 # -------------------------------------------------------------------------
177 - def create(self, r, **attr):
178 """ 179 Create new records 180 181 @param r: the S3Request 182 @param attr: dictionary of parameters for the method handler 183 """ 184 185 session = current.session 186 request = self.request 187 response = current.response 188 189 resource = self.resource 190 table = resource.table 191 tablename = resource.tablename 192 193 representation = r.representation 194 195 output = {} 196 197 native = r.method == "create" 198 199 # Get table configuration 200 _config = self._config 201 insertable = _config("insertable", True) 202 if not insertable: 203 if native: 204 r.error(405, current.ERROR.METHOD_DISABLED) 205 else: 206 return {"form": None} 207 208 authorised = self._permitted(method="create") 209 if not authorised: 210 if native: 211 r.unauthorised() 212 else: 213 return {"form": None} 214 215 # Get callbacks 216 onvalidation = _config("create_onvalidation") or \ 217 _config("onvalidation") 218 onaccept = _config("create_onaccept") or \ 219 _config("onaccept") 220 221 if r.interactive: 222 223 crud_string = self.crud_string 224 225 # Page details 226 if native: 227 228 # Set view 229 if representation in ("popup", "iframe"): 230 response.view = self._view(r, "popup.html") 231 output["caller"] = request.vars.caller 232 else: 233 response.view = self._view(r, "create.html") 234 235 # Title 236 if r.component: 237 title = crud_string(r.tablename, "title_display") 238 output["title"] = title 239 else: 240 title = crud_string(tablename, "label_create") 241 output["title"] = title 242 output["title_list"] = crud_string(tablename, "title_list") 243 244 # Buttons 245 buttons = self.render_buttons(r, ["list"], **attr) 246 if buttons: 247 output["buttons"] = buttons 248 249 # Component defaults and linking 250 link = None 251 if r.component: 252 253 defaults = r.component.get_defaults(r.record) 254 255 if resource.link is None: 256 # Apply component defaults 257 linked = resource.linked 258 ctable = linked.table if linked else table 259 for (k, v) in defaults.items(): 260 ctable[k].default = v 261 262 # Configure post-process for S3EmbeddedComponentWidget 263 link = self._embed_component(resource, record=r.id) 264 265 # Set default value for parent key (fkey) 266 pkey = resource.pkey 267 fkey = resource.fkey 268 field = table[fkey] 269 value = r.record[pkey] 270 field.default = field.update = value 271 272 # Add parent key to POST vars so that callbacks can see it 273 if r.http == "POST": 274 r.post_vars.update({fkey: value}) 275 276 # Hide the parent link in component forms 277 field.comment = None 278 field.readable = False 279 field.writable = False 280 281 else: 282 # Apply component defaults 283 for (k, v) in defaults.items(): 284 table[k].default = v 285 286 # Configure post-process to add a link table entry 287 link = Storage(resource=resource.link, master=r.record) 288 289 get_vars = r.get_vars 290 291 # Hierarchy parent 292 hierarchy = None 293 link_to_parent = get_vars.get("link_to_parent") 294 if link_to_parent: 295 try: 296 parent = long(link_to_parent) 297 except ValueError: 298 r.error(400, "Invalid parent record ID: %s" % link_to_parent) 299 else: 300 from s3hierarchy import S3Hierarchy 301 h = S3Hierarchy(tablename) 302 if h.config: 303 try: 304 hierarchy = h.preprocess_create_node(r, parent) 305 except KeyError: 306 import sys 307 r.error(404, sys.exc_info()[1]) 308 309 # Organizer 310 organizer = get_vars.get("organizer") 311 if organizer: 312 self._set_organizer_dates(organizer) 313 314 # Copy record 315 from_table = None 316 from_record = get_vars.get("from_record") 317 map_fields = get_vars.get("from_fields") 318 319 if from_record: 320 del get_vars["from_record"] # forget it 321 if from_record.find(".") != -1: 322 from_table, from_record = from_record.split(".", 1) 323 from_table = current.db.get(from_table, None) 324 if not from_table: 325 r.error(404, current.ERROR.BAD_RESOURCE) 326 else: 327 from_table = table 328 try: 329 from_record = long(from_record) 330 except ValueError: 331 r.error(404, current.ERROR.BAD_RECORD) 332 authorised = current.auth.s3_has_permission("read", 333 from_table._tablename, 334 from_record) 335 if not authorised: 336 r.unauthorised() 337 if map_fields: 338 del r.get_vars["from_fields"] 339 if map_fields.find("$") != -1: 340 mf = map_fields.split(",") 341 mf = [f.find("$") != -1 and f.split("$") or \ 342 [f, f] for f in mf] 343 map_fields = Storage(mf) 344 else: 345 map_fields = map_fields.split(",") 346 347 # Success message 348 message = crud_string(self.tablename, "msg_record_created") 349 350 # Copy formkey if un-deleting a duplicate 351 if "id" in request.post_vars: 352 post_vars = request.post_vars 353 original = str(post_vars.id) 354 formkey = session.get("_formkey[%s/None]" % tablename) 355 formname = "%s/%s" % (tablename, original) 356 session["_formkey[%s]" % formname] = formkey 357 if "deleted" in table: 358 table.deleted.writable = True 359 post_vars["deleted"] = False 360 if "created_on" in table: 361 table.created_on.writable = True 362 post_vars["created_on"] = request.utcnow 363 if "created_by" in table: 364 table.created_by.writable = True 365 if current.auth.user: 366 post_vars["created_by"] = current.auth.user.id 367 else: 368 post_vars["created_by"] = None 369 post_vars["_undelete"] = True 370 post_vars["_formname"] = formname 371 post_vars["id"] = original 372 request.vars.update(**post_vars) 373 else: 374 original = None 375 376 subheadings = _config("subheadings") 377 378 # Interim save button 379 self._interim_save_button() 380 381 # Default Cancel Button 382 if r.representation == "html" and r.method == "create": 383 self._default_cancel_button(r) 384 385 # Get the form 386 output["form"] = self.sqlform(request=request, 387 resource=resource, 388 data=self.data, 389 record_id=original, 390 from_table=from_table, 391 from_record=from_record, 392 map_fields=map_fields, 393 onvalidation=onvalidation, 394 onaccept=onaccept, 395 link=link, 396 hierarchy=hierarchy, 397 message=message, 398 subheadings=subheadings, 399 format=representation, 400 ) 401 402 # Navigate-away confirmation 403 if self.settings.navigate_away_confirm: 404 response.s3.jquery_ready.append("S3EnableNavigateAwayConfirm()") 405 406 # Redirection 407 if representation in ("popup", "iframe", "plain", "dl"): 408 self.next = None 409 else: 410 if r.http == "POST" and "interim_save" in r.post_vars: 411 next_vars = self._remove_filters(r.get_vars) 412 create_next = r.url(target="[id]", method="update", 413 vars=next_vars) 414 elif r.http == "POST" and "save_close" in r.post_vars: 415 create_next = _config("create_next_close") 416 elif session.s3.rapid_data_entry and not r.component: 417 if "w" in r.get_vars: 418 # Don't redirect to form tab from summary page 419 w = r.get_vars.pop("w") 420 else: 421 w = None 422 create_next = r.url() 423 if w: 424 r.get_vars["w"] = w 425 else: 426 create_next = _config("create_next") 427 428 if not create_next: 429 next_vars = self._remove_filters(r.get_vars) 430 if r.component: 431 self.next = r.url(method="", 432 vars=next_vars) 433 else: 434 self.next = r.url(id="[id]", 435 method="read", 436 vars=next_vars) 437 elif callable(create_next): 438 self.next = create_next(r) 439 else: 440 self.next = create_next 441 442 elif representation == "plain": 443 # NB formstyle will be "table3cols" so widgets need to support that 444 # or else we need to be able to override this 445 response.view = self._view(r, "plain.html") 446 crud_string = self.crud_string 447 message = crud_string(tablename, "msg_record_created") 448 subheadings = _config("subheadings") 449 output["title"] = crud_string(tablename, "label_create") 450 output["details_btn"] = "" 451 output["item"] = self.sqlform(request=request, 452 resource=resource, 453 data=self.data, 454 onvalidation=onvalidation, 455 onaccept=onaccept, 456 #link=link, 457 message=message, 458 subheadings=subheadings, 459 format=representation) 460 461 elif representation == "csv": 462 import cgi 463 import csv 464 csv.field_size_limit(1000000000) 465 infile = request.vars.filename 466 if isinstance(infile, cgi.FieldStorage) and infile.filename: 467 infile = infile.file 468 else: 469 try: 470 infile = open(infile, "rb") 471 except IOError: 472 session.error = current.T("Cannot read from file: %(filename)s") % \ 473 {"filename": infile} 474 redirect(r.url(method="", representation="html")) 475 try: 476 self.import_csv(infile, table=table) 477 except: 478 session.error = current.T("Unable to parse CSV file or file contains invalid data") 479 else: 480 session.confirmation = current.T("Data uploaded") 481 482 elif representation == "pdf": 483 from s3pdf import S3PDF 484 exporter = S3PDF() 485 return exporter(r, **attr) 486 487 elif representation == "url": 488 results = self.import_url(r) 489 return results 490 491 else: 492 r.error(415, current.ERROR.BAD_FORMAT) 493 494 return output
495 496 # -------------------------------------------------------------------------
497 - def _widget_create(self, r, **attr):
498 """ 499 Create-buttons/form in summary views, both GET and POST 500 501 @param r: the S3Request 502 @param attr: dictionary of parameters for the method handler 503 """ 504 505 response = current.response 506 507 resource = self.resource 508 get_config = resource.get_config 509 tablename = resource.tablename 510 511 output = {} 512 insertable = get_config("insertable", True) 513 if insertable: 514 515 listadd = get_config("listadd", True) 516 addbtn = get_config("addbtn", False) 517 518 if listadd: 519 # Hidden form + Add-button to activate it 520 self.data = None 521 if r.http == "GET" and not self.record_id: 522 populate = attr.pop("populate", None) 523 if callable(populate): 524 try: 525 self.data = populate(r, **attr) 526 except TypeError: 527 self.data = None 528 except: 529 raise 530 elif isinstance(populate, dict): 531 self.data = populate 532 533 view = response.view 534 535 # JS Cancel (no redirect with embedded form) 536 s3 = response.s3 537 cancel = s3.cancel 538 s3.cancel = {"hide": "list-add", "show": "show-add-btn"} 539 540 form = self.create(r, **attr).get("form", None) 541 if form and form.accepted and self.next: 542 # Tell the summary handler that we're done 543 # and supposed to redirect to another view 544 return {"success": True, "next": self.next} 545 546 # Restore standard view and cancel-config 547 response.view = view 548 s3.cancel = cancel 549 550 if form is not None: 551 form_postp = r.resource.get_config("form_postp") 552 if form_postp: 553 form_postp(form) 554 output["form"] = form 555 output["showadd_btn"] = self.crud_button(tablename=tablename, 556 name="label_create", 557 icon="add", 558 _id="show-add-btn") 559 addtitle = self.crud_string(tablename, "label_create") 560 output["addtitle"] = addtitle 561 if r.http == "POST": 562 # Always show the form if there was a form error 563 script = '''$('#list-add').show();$('#show-add-btn').hide()''' 564 s3.jquery_ready.append(script) 565 566 # Add-button script 567 # - now in S3.js 568 #script = '''$('#show-add-btn').click(function(){$('#show-add-btn').hide(10, function(){$('#list-add').slideDown('medium')})})''' 569 #s3.jquery_ready.append(script) 570 571 elif addbtn: 572 # No form, just Add-button linked to create-view 573 add_btn = self.crud_button( 574 tablename=tablename, 575 name="label_create", 576 icon="add", 577 _id="add-btn") 578 output["buttons"] = {"add_btn": add_btn} 579 580 view = self._view(r, "listadd.html") 581 output = XML(response.render(view, output)) 582 return output
583 584 # -------------------------------------------------------------------------
585 - def read(self, r, **attr):
586 """ 587 Read a single record 588 589 @param r: the S3Request 590 @param attr: dictionary of parameters for the method handler 591 """ 592 593 # Check authorization to read the record 594 authorised = self._permitted() 595 if not authorised: 596 r.unauthorised() 597 598 request = self.request 599 response = current.response 600 601 resource = self.resource 602 table = resource.table 603 tablename = resource.tablename 604 605 representation = r.representation 606 607 output = {} 608 609 _config = self._config 610 editable = _config("editable", True) 611 612 # Get the target record ID 613 record_id = self.record_id 614 615 if r.interactive: 616 617 component = r.component 618 619 # If this is a single-component and no record exists, 620 # try to create one if the user is permitted 621 if not record_id and component and not component.multiple: 622 empty = True 623 authorised = self._permitted(method="create") 624 if authorised and _config("insertable", True): 625 # This should become Native 626 r.method = "create" 627 return self.create(r, **attr) 628 else: 629 empty = False 630 631 # Redirect to update if user has permission unless 632 # a method has been specified in the URL 633 # MH: Is this really desirable? Many users would prefer to open as read 634 if not r.method: #or r.method == "review": 635 authorised = self._permitted("update") 636 if authorised and representation == "html" and editable: 637 return self.update(r, **attr) 638 639 # Form configuration 640 subheadings = _config("subheadings") 641 642 # Title and subtitle 643 crud_string = self.crud_string 644 title = crud_string(r.tablename, "title_display") 645 output["title"] = title 646 if component and not empty: 647 subtitle = crud_string(tablename, "title_display") 648 output["subtitle"] = subtitle 649 output["title_list"] = crud_string(tablename, "title_list") 650 651 # Hide component key when on tab 652 if component and resource.link is None: 653 try: 654 field = table[resource.fkey] 655 except (AttributeError, KeyError): 656 pass 657 else: 658 field.readable = field.writable = False 659 660 # Item 661 if record_id: 662 try: 663 item = self.sqlform(request = request, 664 resource = resource, 665 record_id = record_id, 666 readonly = True, 667 subheadings = subheadings, 668 format = representation, 669 ) 670 except HTTP, e: 671 message = current.ERROR.BAD_RECORD \ 672 if e.status == 404 else e.message 673 r.error(e.status, message) 674 else: 675 item = DIV(crud_string(tablename, "msg_list_empty"), 676 _class = "empty", 677 ) 678 679 # View 680 if representation == "html": 681 response.view = self._view(r, "display.html") 682 output["item"] = item 683 elif representation == "popup": 684 response.view = self._view(r, "popup.html") 685 output["form"] = item 686 caller = attr.get("caller", None) 687 output["caller"] = caller 688 elif representation == "iframe": 689 response.view = self._view(r, "iframe.html") 690 output["form"] = item 691 692 # Buttons 693 buttons = self.render_buttons(r, 694 ["edit", "delete", "list", "summary"], 695 record_id = record_id, 696 **attr) 697 if buttons: 698 output["buttons"] = buttons 699 700 # Last update 701 last_update = self.last_update() 702 if last_update: 703 try: 704 output["modified_on"] = last_update["modified_on"] 705 except KeyError: 706 # Field not in table 707 pass 708 try: 709 output["modified_by"] = last_update["modified_by"] 710 except KeyError: 711 # Field not in table, such as auth_user 712 pass 713 714 # De-duplication 715 from s3merge import S3Merge 716 output["deduplicate"] = S3Merge.bookmark(r, tablename, record_id) 717 718 elif representation == "plain": 719 # e.g. Map Popup 720 T = current.T 721 fields = [f for f in table if f.readable] 722 if r.component: 723 if record_id: 724 record = current.db(table._id == record_id).select(limitby=(0, 1), 725 *fields 726 ).first() 727 else: 728 record = None 729 else: 730 record = r.record 731 if record: 732 # Hide empty fields from popups on map 733 for field in fields: 734 try: 735 value = record[field] 736 except KeyError: 737 # e.g. gis_location.wkt 738 value = None 739 if value is None or value == "" or value == []: 740 field.readable = False 741 item = self.sqlform(request=request, 742 resource=resource, 743 record_id=record_id, 744 readonly=True, 745 format=representation) 746 747 # Link to Open record 748 popup_edit_url = _config("popup_edit_url", None) 749 if popup_edit_url and \ 750 current.auth.s3_has_permission("update", table, record_id): 751 # Open edit form in iframe 752 details_btn = A(T("Edit"), 753 _href=popup_edit_url, 754 _class="btn iframe", 755 ) 756 output["details_btn"] = details_btn 757 else: 758 # Open read view in new tab 759 # Set popup_url to "" to have no button present 760 popup_url = _config("popup_url", None) 761 if popup_url is None: 762 popup_url = r.url(method="read", representation="html") 763 if popup_url: 764 popup_url = popup_url.replace("%5Bid%5D", str(record_id)) 765 details_btn = A(T("Open"), 766 _href = popup_url, 767 _class = "btn", 768 _target = "_blank", 769 ) 770 output["details_btn"] = details_btn 771 772 # Title and subtitle 773 title = self.crud_string(r.tablename, "title_display") 774 output["title"] = title 775 776 else: 777 item = T("Record not found") 778 779 output["item"] = item 780 response.view = self._view(r, "plain.html") 781 782 elif representation == "csv": 783 exporter = S3Exporter().csv 784 output = exporter(resource) 785 786 #elif representation == "map": 787 # exporter = S3Map() 788 # output = exporter(r, **attr) 789 790 elif representation == "pdf": 791 exporter = S3Exporter().pdf 792 output = exporter(resource, request=r, **attr) 793 794 elif representation == "shp": 795 list_fields = resource.list_fields() 796 exporter = S3Exporter().shp 797 output = exporter(resource, list_fields=list_fields, **attr) 798 799 elif representation == "svg": 800 list_fields = resource.list_fields() 801 exporter = S3Exporter().svg 802 output = exporter(resource, list_fields=list_fields, **attr) 803 804 elif representation == "xls": 805 list_fields = resource.list_fields() 806 exporter = S3Exporter().xls 807 output = exporter(resource, list_fields=list_fields) 808 809 elif representation == "json": 810 exporter = S3Exporter().json 811 812 # Render extra "_tooltip" field for each row? 813 get_vars = request.get_vars 814 if "tooltip" in get_vars: 815 tooltip = get_vars["tooltip"] 816 else: 817 tooltip = None 818 819 output = exporter(resource, tooltip=tooltip) 820 821 elif representation == "card": 822 823 if not resource.get_config("pdf_card_layout"): 824 # This format is not supported for this resource 825 r.error(415, current.ERROR.BAD_FORMAT) 826 827 pagesize = resource.get_config("pdf_card_pagesize") 828 output = S3Exporter().pdfcard(resource, 829 pagesize = pagesize, 830 ) 831 832 disposition = "attachment; filename=\"%s_card.pdf\"" % resource.name 833 response.headers["Content-Type"] = contenttype(".pdf") 834 response.headers["Content-disposition"] = disposition 835 836 else: 837 r.error(415, current.ERROR.BAD_FORMAT) 838 839 return output
840 841 # -------------------------------------------------------------------------
842 - def update(self, r, **attr):
843 """ 844 Update a record 845 846 @param r: the S3Request 847 @param attr: dictionary of parameters for the method handler 848 """ 849 850 resource = self.resource 851 table = resource.table 852 tablename = resource.tablename 853 854 representation = r.representation 855 856 output = {} 857 858 # Get table configuration 859 _config = self._config 860 editable = _config("editable", True) 861 862 # Get callbacks 863 onvalidation = _config("update_onvalidation") or \ 864 _config("onvalidation") 865 onaccept = _config("update_onaccept") or \ 866 _config("onaccept") 867 868 # Get the target record ID 869 record_id = self.record_id 870 if r.interactive and not record_id: 871 r.error(404, current.ERROR.BAD_RECORD) 872 873 # Check if editable 874 if not editable: 875 if r.interactive: 876 return self.read(r, **attr) 877 else: 878 r.error(405, current.ERROR.METHOD_DISABLED) 879 880 # Check permission for update 881 authorised = self._permitted(method="update") 882 if not authorised: 883 r.unauthorised() 884 885 if r.interactive or representation == "plain": 886 887 response = current.response 888 s3 = response.s3 889 890 # Form configuration 891 subheadings = _config("subheadings") 892 893 # Set view 894 if representation == "html": 895 response.view = self._view(r, "update.html") 896 elif representation in "popup": 897 response.view = self._view(r, "popup.html") 898 elif representation == "plain": 899 response.view = self._view(r, "plain.html") 900 elif representation == "iframe": 901 response.view = self._view(r, "iframe.html") 902 903 # Title and subtitle 904 crud_string = self.crud_string 905 if r.component: 906 title = crud_string(r.tablename, "title_display") 907 subtitle = crud_string(self.tablename, "title_update") 908 output["title"] = title 909 output["subtitle"] = subtitle 910 else: 911 title = crud_string(self.tablename, "title_update") 912 output["title"] = title 913 output["title_list"] = crud_string(tablename, "title_list") 914 915 # Component join 916 link = None 917 if r.component: 918 if resource.link is None: 919 link = self._embed_component(resource, record=r.id) 920 pkey = resource.pkey 921 fkey = resource.fkey 922 field = table[fkey] 923 value = r.record[pkey] 924 field.comment = None 925 field.default = value 926 field.update = value 927 if r.http == "POST": 928 r.post_vars.update({fkey: value}) 929 field.readable = False 930 field.writable = False 931 else: 932 link = Storage(resource=resource.link, master=r.record) 933 934 # Success message 935 message = crud_string(self.tablename, "msg_record_modified") 936 937 # Interim save button 938 self._interim_save_button() 939 940 # Default Cancel Button 941 if r.representation == "html" and \ 942 (r.method == "update" or not r.method): 943 self._default_cancel_button(r) 944 945 # Get the form 946 try: 947 form = self.sqlform(request=self.request, 948 resource=resource, 949 record_id=record_id, 950 onvalidation=onvalidation, 951 onaccept=onaccept, 952 message=message, 953 link=link, 954 subheadings=subheadings, 955 format=representation) 956 except HTTP, e: 957 message = current.ERROR.BAD_RECORD \ 958 if e.status == 404 else e.message 959 r.error(e.status, message) 960 961 # Navigate-away confirmation 962 if self.settings.navigate_away_confirm: 963 s3.jquery_ready.append("S3EnableNavigateAwayConfirm()") 964 965 # Put form into output 966 output["form"] = form 967 if representation == "plain": 968 output["item"] = form 969 output["title"] = "" 970 971 # Add delete and list buttons 972 buttons = self.render_buttons(r, 973 ["delete"], 974 record_id=record_id, 975 **attr) 976 if buttons: 977 output["buttons"] = buttons 978 979 # Last update 980 last_update = self.last_update() 981 if last_update: 982 try: 983 output["modified_on"] = last_update["modified_on"] 984 except KeyError: 985 # Field not in table 986 pass 987 try: 988 output["modified_by"] = last_update["modified_by"] 989 except KeyError: 990 # Field not in table, such as auth_user 991 pass 992 993 # De-duplication 994 from s3merge import S3Merge 995 output["deduplicate"] = S3Merge.bookmark(r, tablename, record_id) 996 997 # Redirection 998 if r.http == "POST" and "interim_save" in r.post_vars: 999 next_vars = self._remove_filters(r.get_vars) 1000 self.next = r.url(target="[id]", method="update", 1001 vars=next_vars) 1002 else: 1003 update_next = _config("update_next") 1004 if representation in ("popup", "iframe", "plain", "dl"): 1005 self.next = None 1006 elif not update_next: 1007 next_vars = self._remove_filters(r.get_vars) 1008 if r.component: 1009 self.next = r.url(method="", vars=next_vars) 1010 else: 1011 self.next = r.url(id="[id]", 1012 method="read", 1013 vars=next_vars) 1014 else: 1015 try: 1016 self.next = update_next(self) 1017 except TypeError: 1018 self.next = update_next 1019 1020 elif representation == "url": 1021 return self.import_url(r) 1022 1023 else: 1024 r.error(415, current.ERROR.BAD_FORMAT) 1025 1026 return output
1027 1028 # -------------------------------------------------------------------------
1029 - def delete(self, r, **attr):
1030 """ 1031 Delete record(s) 1032 1033 @param r: the S3Request 1034 @param attr: dictionary of parameters for the method handler 1035 1036 @todo: update for link table components 1037 """ 1038 1039 output = {} 1040 1041 # Get table-specific parameters 1042 config = self._config 1043 deletable = config("deletable", True) 1044 delete_next = config("delete_next", None) 1045 1046 # Check if deletable 1047 if not deletable: 1048 r.error(403, current.ERROR.NOT_PERMITTED, 1049 next=r.url(method="")) 1050 1051 # Get the target record ID 1052 record_id = self.record_id 1053 1054 # Check permission to delete 1055 authorised = self._permitted() 1056 if not authorised: 1057 r.unauthorised() 1058 1059 elif (r.interactive or r.representation == "aadata") and \ 1060 r.http == "GET" and not record_id: 1061 output = self._datatable(r, **attr) 1062 if isinstance(output, dict): 1063 # Provide a confirmation form and a record list 1064 form = FORM(TABLE(TR(TD(self.settings.confirm_delete, 1065 _style="color:red"), 1066 TD(INPUT(_type="submit", 1067 _value=current.T("Delete"), 1068 _style="margin-left:10px"))))) 1069 output["form"] = form 1070 current.response.view = self._view(r, "delete.html") 1071 else: 1072 # @todo: sorting not working yet 1073 return output 1074 1075 elif r.interactive and (r.http == "POST" or 1076 r.http == "GET" and record_id): 1077 # Delete the records, notify success and redirect to the next view 1078 numrows = self.resource.delete(format=r.representation) 1079 if numrows > 1: 1080 message = "%s %s" % (numrows, current.T("records deleted")) 1081 elif numrows == 1: 1082 message = self.crud_string(self.tablename, 1083 "msg_record_deleted") 1084 else: 1085 r.error(404, self.resource.error, next=r.url(method="")) 1086 current.response.confirmation = message 1087 r.http = "DELETE" # must be set for immediate redirect 1088 self.next = delete_next or r.url(method="") 1089 1090 elif r.http == "DELETE" or \ 1091 r.representation == "json" and r.http == "POST" and record_id: 1092 1093 numrows = None 1094 resource = self.resource 1095 1096 recursive = r.get_vars.get("recursive", False) 1097 if recursive and recursive.lower() in ("1", "true"): 1098 # Try recursive deletion of the whole hierarchy branch 1099 # => falls back to normal delete if no hierarchy configured 1100 from s3hierarchy import S3Hierarchy 1101 h = S3Hierarchy(resource.tablename) 1102 if h.config: 1103 node_ids = None 1104 pkey = h.pkey 1105 if str(pkey) == str(resource._id): 1106 node_ids = [record_id] 1107 else: 1108 # Need to lookup the hierarchy node key 1109 query = (resource._id == record_id) 1110 row = current.db(query).select(pkey, 1111 limitby=(0, 1)).first() 1112 if row: 1113 node_ids = [row[pkey]] 1114 numrows = h.delete(node_ids) if node_ids else 0 1115 if not numrows: 1116 # Cascade failed, do not fall back 1117 # @todo: propagate the original error 1118 resource.error = current.T("Deletion failed") 1119 numrows = 0 1120 1121 if numrows is None: 1122 # Delete the records and return a JSON message 1123 numrows = resource.delete(format=r.representation) 1124 1125 if numrows > 1: 1126 message = "%s %s" % (numrows, current.T("records deleted")) 1127 elif numrows == 1: 1128 message = self.crud_string(self.tablename, 1129 "msg_record_deleted") 1130 else: 1131 r.error(404, resource.error, next=r.url(method="")) 1132 1133 item = current.xml.json_message(message=message) 1134 current.response.view = "xml.html" 1135 output.update(item=item) 1136 1137 else: 1138 r.error(405, current.ERROR.BAD_METHOD) 1139 1140 return output
1141 1142 # -------------------------------------------------------------------------
1143 - def select(self, r, **attr):
1144 """ 1145 Filterable datatable/datalist 1146 1147 @param r: the S3Request 1148 @param attr: dictionary of parameters for the method handler 1149 """ 1150 1151 resource = self.resource 1152 1153 tablename = resource.tablename 1154 get_config = resource.get_config 1155 1156 list_fields = get_config("list_fields", None) 1157 1158 representation = r.representation 1159 if representation in ("html", "iframe", "aadata", "dl", "popup"): 1160 1161 hide_filter = self.hide_filter 1162 filter_widgets = get_config("filter_widgets", None) 1163 1164 show_filter_form = False 1165 if filter_widgets and not hide_filter and \ 1166 representation not in ("aadata", "dl"): 1167 show_filter_form = True 1168 # Apply filter defaults (before rendering the data!) 1169 from s3filter import S3FilterForm 1170 default_filters = S3FilterForm.apply_filter_defaults(r, resource) 1171 else: 1172 default_filters = None 1173 1174 get_vars = r.get_vars 1175 attr = dict(attr) 1176 1177 # Data 1178 list_type = attr.get("list_type", "datatable") 1179 if list_type == "datalist": 1180 filter_ajax = True 1181 target = "datalist" 1182 output = self._datalist(r, **attr) 1183 else: 1184 if filter_widgets and not hide_filter: 1185 dtargs = attr.get("dtargs", {}) 1186 # Hide datatable filter box if we have a filter form 1187 if "dt_searching" not in dtargs: 1188 dtargs["dt_searching"] = False 1189 # Override default ajax URL if we have default filters 1190 if default_filters: 1191 ajax_vars = dict(get_vars) 1192 ajax_vars.update(default_filters) 1193 ajax_url = r.url(representation = "aadata", 1194 vars = ajax_vars, 1195 ) 1196 dtargs["dt_ajax_url"] = ajax_url 1197 attr["dtargs"] = dtargs 1198 filter_ajax = True 1199 target = "datatable" 1200 output = self._datatable(r, **attr) 1201 1202 if representation in ("aadata", "dl"): 1203 return output 1204 1205 output["list_type"] = list_type 1206 1207 crud_string = self.crud_string 1208 1209 # Page title 1210 if representation != "iframe": 1211 if r.component: 1212 title = crud_string(r.tablename, "title_display") 1213 else: 1214 title = crud_string(self.tablename, "title_list") 1215 output["title"] = title 1216 1217 # Filter-form 1218 if show_filter_form: 1219 1220 # Where to retrieve filtered data from: 1221 filter_submit_url = attr.get("filter_submit_url") 1222 if not filter_submit_url: 1223 get_vars_ = self._remove_filters(get_vars) 1224 filter_submit_url = r.url(vars=get_vars_) 1225 1226 # Where to retrieve updated filter options from: 1227 filter_ajax_url = attr.get("filter_ajax_url") 1228 if filter_ajax_url is None: 1229 filter_ajax_url = r.url(method = "filter", 1230 vars = {}, 1231 representation = "options", 1232 ) 1233 filter_clear = get_config("filter_clear", 1234 current.deployment_settings.get_ui_filter_clear()) 1235 filter_formstyle = get_config("filter_formstyle", None) 1236 filter_submit = get_config("filter_submit", True) 1237 filter_form = S3FilterForm(filter_widgets, 1238 clear = filter_clear, 1239 formstyle = filter_formstyle, 1240 submit = filter_submit, 1241 ajax = filter_ajax, 1242 url = filter_submit_url, 1243 ajaxurl = filter_ajax_url, 1244 _class = "filter-form", 1245 _id = "%s-filter-form" % target 1246 ) 1247 fresource = current.s3db.resource(resource.tablename) # Use a clean resource 1248 alias = resource.alias if r.component else None 1249 output["list_filter_form"] = filter_form.html(fresource, 1250 get_vars, 1251 target = target, 1252 alias = alias 1253 ) 1254 else: 1255 # Render as empty string to avoid the exception in the view 1256 output["list_filter_form"] = "" 1257 1258 # Add-form or -button 1259 insertable = get_config("insertable", True) 1260 if insertable: 1261 1262 addbtn = get_config("addbtn", False) 1263 listadd = get_config("listadd", True) 1264 1265 if listadd: 1266 # Save the view 1267 response = current.response 1268 view = response.view 1269 1270 # JS Cancel (no redirect with embedded form) 1271 s3 = response.s3 1272 cancel = s3.cancel 1273 s3.cancel = {"hide": "list-add", "show": "show-add-btn"} 1274 1275 # Add a hidden add-form and a button to activate it 1276 form = self.create(r, **attr).get("form", None) 1277 if form is not None: 1278 output["form"] = form 1279 addtitle = self.crud_string(tablename, "label_create") 1280 output["addtitle"] = addtitle 1281 showadd_btn = self.crud_button(None, 1282 tablename = tablename, 1283 name = "label_create", 1284 icon = "add", 1285 _id = "show-add-btn", 1286 ) 1287 output["showadd_btn"] = showadd_btn 1288 1289 # Restore the view 1290 response.view = view 1291 s3.cancel = cancel 1292 1293 elif addbtn: 1294 # Add an action-button linked to the create view 1295 buttons = self.render_buttons(r, ["add"], **attr) 1296 if buttons: 1297 output["buttons"] = buttons 1298 1299 return output 1300 1301 elif representation == "plain": 1302 1303 if resource.count() == 1: 1304 # Provide the record 1305 # (used by Map's Layer Properties window) 1306 resource.load() 1307 r.record = resource.records().first() 1308 if r.record: 1309 r.id = r.record.id 1310 self.record_id = self._record_id(r) 1311 if "update" in r.get_vars and \ 1312 self._permitted(method="update"): 1313 items = self.update(r, **attr).get("form", None) 1314 else: 1315 items = self.sqlform(request = self.request, 1316 resource = self.resource, 1317 record_id = r.id, 1318 readonly = True, 1319 format = representation, 1320 ) 1321 else: 1322 raise HTTP(404, body="Record not Found") 1323 else: 1324 rows = resource.select(list_fields, 1325 limit = None, 1326 as_rows = True, 1327 ) 1328 if rows: 1329 items = rows.as_list() 1330 else: 1331 items = [] 1332 1333 current.response.view = "plain.html" 1334 return {"item": items} 1335 1336 elif representation == "csv": 1337 1338 exporter = S3Exporter().csv 1339 return exporter(resource) 1340 1341 elif representation == "json": 1342 1343 get_vars = self.request.get_vars 1344 1345 # Start/limit (no default limit) 1346 start, limit = self._limits(get_vars, default_limit=None) 1347 1348 # Render extra "_tooltip" field for each row? 1349 tooltip = get_vars.get("tooltip", None) 1350 1351 # Represent? 1352 represent = get_vars.get("represent", False) 1353 if represent and represent != "0": 1354 represent = True 1355 1356 exporter = S3Exporter().json 1357 return exporter(resource, 1358 start = start, 1359 limit = limit, 1360 represent = represent, 1361 tooltip = tooltip, 1362 ) 1363 1364 elif representation == "pdf": 1365 1366 report_hide_comments = get_config("report_hide_comments", None) 1367 report_filename = get_config("report_filename", None) 1368 report_formname = get_config("report_formname", None) 1369 1370 exporter = S3Exporter().pdf 1371 return exporter(resource, 1372 request = r, 1373 list_fields = list_fields, 1374 report_hide_comments = report_hide_comments, 1375 report_filename = report_filename, 1376 report_formname = report_formname, 1377 **attr) 1378 1379 elif representation == "shp": 1380 exporter = S3Exporter().shp 1381 return exporter(resource, 1382 list_fields = list_fields, 1383 **attr) 1384 1385 elif representation == "svg": 1386 exporter = S3Exporter().svg 1387 return exporter(resource, 1388 list_fields = list_fields, 1389 **attr) 1390 1391 elif representation == "xls": 1392 report_groupby = get_config("report_groupby", None) 1393 exporter = S3Exporter().xls 1394 return exporter(resource, 1395 list_fields = list_fields, 1396 report_groupby = report_groupby, 1397 **attr) 1398 1399 elif representation == "msg": 1400 if r.http == "POST": 1401 from s3notify import S3Notifications 1402 return S3Notifications.send(r, resource) 1403 else: 1404 r.error(405, current.ERROR.BAD_METHOD) 1405 1406 elif representation == "card": 1407 if not resource.get_config("pdf_card_layout"): 1408 # This format is not supported for this resource 1409 r.error(415, current.ERROR.BAD_FORMAT) 1410 1411 pagesize = resource.get_config("pdf_card_pagesize") 1412 output = S3Exporter().pdfcard(resource, 1413 pagesize = pagesize, 1414 ) 1415 1416 response = current.response 1417 disposition = "attachment; filename=\"%s_cards.pdf\"" % resource.name 1418 response.headers["Content-Type"] = contenttype(".pdf") 1419 response.headers["Content-disposition"] = disposition 1420 1421 return output 1422 1423 else: 1424 r.error(415, current.ERROR.BAD_FORMAT)
1425 1426 # -------------------------------------------------------------------------
1427 - def _datatable(self, r, **attr):
1428 """ 1429 Get a data table 1430 1431 @param r: the S3Request 1432 @param attr: parameters for the method handler 1433 """ 1434 1435 # Check permission to read in this table 1436 authorised = self._permitted() 1437 if not authorised: 1438 r.unauthorised() 1439 1440 resource = self.resource 1441 get_config = resource.get_config 1442 1443 # Get table-specific parameters 1444 linkto = get_config("linkto", None) 1445 1446 # List ID 1447 list_id = attr.get("list_id", "datatable") 1448 1449 # List fields 1450 list_fields = resource.list_fields() 1451 1452 # Default orderby 1453 orderby = get_config("orderby", None) 1454 1455 response = current.response 1456 s3 = response.s3 1457 representation = r.representation 1458 1459 # Pagination 1460 get_vars = self.request.get_vars 1461 if representation == "aadata": 1462 start, limit = self._limits(get_vars) 1463 else: 1464 # Initial page request always uses defaults (otherwise 1465 # filtering and pagination would have to be relative to 1466 # the initial limits, but there is no use-case for that) 1467 start = None 1468 limit = None if s3.no_sspag else 0 1469 1470 # Initialize output 1471 output = {} 1472 1473 # Linkto 1474 if not linkto: 1475 linkto = self._linkto(r) 1476 1477 left = [] 1478 distinct = False 1479 dtargs = attr.get("dtargs", {}) 1480 1481 if r.interactive: 1482 1483 # How many records per page? 1484 if s3.dataTable_pageLength: 1485 display_length = s3.dataTable_pageLength 1486 else: 1487 display_length = 25 1488 1489 # Server-side pagination? 1490 if not s3.no_sspag: 1491 dt_pagination = "true" 1492 if not limit: 1493 limit = 2 * display_length 1494 current.session.s3.filter = get_vars 1495 if orderby is None: 1496 dt_sorting = {"iSortingCols": "1", 1497 "sSortDir_0": "asc" 1498 } 1499 1500 if len(list_fields) > 1: 1501 dt_sorting["bSortable_0"] = "false" 1502 dt_sorting["iSortCol_0"] = "1" 1503 else: 1504 dt_sorting["bSortable_0"] = "true" 1505 dt_sorting["iSortCol_0"] = "0" 1506 1507 orderby, left = resource.datatable_filter(list_fields, 1508 dt_sorting, 1509 )[1:3] 1510 else: 1511 dt_pagination = "false" 1512 1513 # Get the data table 1514 dt, totalrows = resource.datatable(fields = list_fields, 1515 start = start, 1516 limit = limit, 1517 left = left, 1518 orderby = orderby, 1519 distinct = distinct, 1520 ) 1521 displayrows = totalrows 1522 1523 if not dt.data: 1524 # Empty table - or just no match? 1525 #if dt.empty: 1526 # datatable = DIV(self.crud_string(resource.tablename, 1527 # "msg_list_empty"), 1528 # _class="empty") 1529 #else: 1530 # #datatable = DIV(self.crud_string(resource.tablename, 1531 # "msg_no_match"), 1532 # _class="empty") 1533 1534 # Must include export formats to allow subsequent unhiding 1535 # when Ajax (un-)filtering produces exportable table contents: 1536 #s3.no_formats = True 1537 1538 if r.component and "showadd_btn" in output: 1539 # Hide the list and show the form by default 1540 del output["showadd_btn"] 1541 datatable = "" 1542 1543 # Always show table, otherwise it can't be Ajax-filtered 1544 # @todo: need a better algorithm to determine total_rows 1545 # (which excludes URL filters), so that datatables 1546 # shows the right empty-message (ZeroRecords instead 1547 # of EmptyTable) 1548 dtargs["dt_pagination"] = dt_pagination 1549 dtargs["dt_pageLength"] = display_length 1550 dtargs["dt_base_url"] = r.url(method="", vars={}) 1551 dtargs["dt_permalink"] = r.url() 1552 datatable = dt.html(totalrows, 1553 displayrows, 1554 id=list_id, 1555 **dtargs) 1556 1557 # View + data 1558 response.view = self._view(r, "list_filter.html") 1559 output["items"] = datatable 1560 1561 elif representation == "aadata": 1562 1563 # Apply datatable filters 1564 searchq, orderby, left = resource.datatable_filter(list_fields, 1565 get_vars) 1566 if searchq is not None: 1567 totalrows = resource.count() 1568 resource.add_filter(searchq) 1569 else: 1570 totalrows = None 1571 1572 # Orderby fallbacks 1573 if orderby is None: 1574 orderby = get_config("orderby", None) 1575 1576 # Get a data table 1577 if totalrows != 0: 1578 dt, displayrows = resource.datatable(fields = list_fields, 1579 start = start, 1580 limit = limit, 1581 left = left, 1582 orderby = orderby, 1583 distinct = distinct, 1584 ) 1585 else: 1586 dt, displayrows = None, 0 1587 if totalrows is None: 1588 totalrows = displayrows 1589 1590 # Echo 1591 draw = int(get_vars.get("draw", 0)) 1592 1593 # Representation 1594 if dt is not None: 1595 output = dt.json(totalrows, 1596 displayrows, 1597 list_id, 1598 draw, 1599 **dtargs) 1600 else: 1601 output = '{"recordsTotal":%s,' \ 1602 '"recordsFiltered":0,' \ 1603 '"dataTable_id":"%s",' \ 1604 '"draw":%s,' \ 1605 '"data":[]}' % (totalrows, list_id, draw) 1606 1607 else: 1608 r.error(415, current.ERROR.BAD_FORMAT) 1609 1610 return output
1611 1612 # -------------------------------------------------------------------------
1613 - def _datalist(self, r, **attr):
1614 """ 1615 Get a data list 1616 1617 @param r: the S3Request 1618 @param attr: parameters for the method handler 1619 """ 1620 1621 # Check permission to read in this table 1622 authorised = self._permitted() 1623 if not authorised: 1624 r.unauthorised() 1625 1626 resource = self.resource 1627 get_config = resource.get_config 1628 get_vars = self.request.get_vars 1629 1630 # Get table-specific parameters 1631 layout = get_config("list_layout", None) 1632 1633 # List ID 1634 list_id = get_vars.get("list_id", # Could we check for refresh here? (Saves extra get_var) 1635 attr.get("list_id", "datalist")) 1636 1637 # List fields 1638 if hasattr(layout, "list_fields"): 1639 list_fields = layout.list_fields 1640 else: 1641 list_fields = resource.list_fields() 1642 1643 # Default orderby 1644 orderby = get_config("list_orderby", 1645 get_config("orderby", None)) 1646 if orderby is None: 1647 if "created_on" in resource.fields: 1648 default_orderby = ~(resource.table["created_on"]) 1649 else: 1650 for f in list_fields: 1651 rfield = resource.resolve_selector(f) 1652 if rfield.field and rfield.colname != str(resource._id): 1653 default_orderby = rfield.field 1654 break 1655 else: 1656 default_orderby = None 1657 1658 # Pagination 1659 response = current.response 1660 s3 = response.s3 1661 1662 # Pagelength = number of items per page 1663 if "dl_pagelength" in attr: 1664 pagelength = attr["dl_pagelength"] 1665 elif s3.dl_pagelength: 1666 pagelength = s3.dl_pagelength 1667 else: 1668 pagelength = 10 1669 1670 # Rowsize = number of items per row 1671 if "dl_rowsize" in attr: 1672 rowsize = attr["dl_rowsize"] 1673 elif s3.dl_rowsize: 1674 rowsize = s3.dl_rowsize 1675 else: 1676 rowsize = 1 1677 1678 # Make sure that pagelength is a multiple of rowsize 1679 if pagelength % rowsize: 1680 pagelength = (int(pagelength / rowsize) + 1) * rowsize 1681 1682 record_id = get_vars.get("record", None) 1683 if record_id is not None: 1684 # Ajax-reload of a single record 1685 from s3query import FS 1686 resource.add_filter(FS("id") == record_id) 1687 start = 0 1688 limit = 1 1689 else: 1690 start, limit = self._limits(get_vars) 1691 1692 # Initialize output 1693 output = {} 1694 1695 # Prepare data list 1696 representation = r.representation 1697 1698 # Ajax-delete items? 1699 if representation == "dl" and r.http in ("DELETE", "POST"): 1700 if "delete" in get_vars: 1701 return {"item": self._dl_ajax_delete(r, resource)} 1702 else: 1703 r.error(405, current.ERROR.BAD_METHOD) 1704 1705 if representation in ("html", "dl", "popup"): 1706 1707 # Retrieve the data 1708 if limit: 1709 initial_limit = min(limit, pagelength) 1710 else: 1711 initial_limit = pagelength 1712 1713 # We don't have client-side sorting yet to override 1714 # default-orderby, so fall back unconditionally here: 1715 if not orderby: 1716 orderby = default_orderby 1717 1718 datalist, numrows = resource.datalist(fields = list_fields, 1719 start = start, 1720 limit = initial_limit, 1721 orderby = orderby, 1722 list_id = list_id, 1723 layout = layout, 1724 ) 1725 1726 if numrows == 0: 1727 s3.no_formats = True 1728 if r.component and "showadd_btn" in output: 1729 # Hide the list and show the form by default 1730 del output["showadd_btn"] 1731 1732 # Allow customization of the datalist Ajax-URL 1733 # Note: the Ajax-URL must use the .dl representation and 1734 # plain.html view for pagination to work properly! 1735 ajax_url = attr.get("list_ajaxurl", None) 1736 if not ajax_url: 1737 ajax_vars = dict((k,v) for k, v in r.get_vars.iteritems() 1738 if k not in ("start", "limit")) 1739 ajax_url = r.url(representation="dl", vars=ajax_vars) 1740 1741 # Render the list (even if empty => Ajax-section is required 1742 # in any case to be able to Ajax-refresh e.g. after adding 1743 # new records or changing the filter) 1744 if representation == "dl" or not limit: 1745 limit = numrows 1746 dl = datalist.html(start = start if start else 0, 1747 limit = limit, 1748 pagesize = pagelength, 1749 rowsize = rowsize, 1750 ajaxurl = ajax_url) 1751 data = dl 1752 else: 1753 r.error(415, current.ERROR.BAD_FORMAT) 1754 1755 1756 if representation == "html": 1757 1758 # View + data 1759 response.view = self._view(r, "list_filter.html") 1760 output["items"] = data 1761 1762 elif representation == "dl": 1763 1764 # View + data 1765 response.view = "plain.html" 1766 output["item"] = data 1767 1768 elif representation == "popup": 1769 1770 # View + data 1771 response.view = "popup.html" 1772 output["items"] = data 1773 # Pagination Support (normally added by views/dataLists.html) 1774 if s3.debug: 1775 appname = current.request.application 1776 sappend = s3.scripts.append 1777 sappend("/%s/static/scripts/jquery.infinitescroll.js" % appname) 1778 sappend("/%s/static/scripts/jquery.viewport.js" % appname) 1779 sappend("/%s/static/scripts/S3/s3.dataLists.js" % appname) 1780 else: 1781 s3.scripts.append("/%s/static/scripts/S3/s3.dataLists.min.js" % current.request.application) 1782 1783 return output
1784 1785 # -------------------------------------------------------------------------
1786 - def unapproved(self, r, **attr):
1787 """ 1788 Get a list of unapproved records in this resource 1789 1790 @param r: the S3Request 1791 @param attr: dictionary of parameters for the method handler 1792 """ 1793 1794 session = current.session 1795 response = current.response 1796 s3 = response.s3 1797 1798 resource = self.resource 1799 table = self.table 1800 1801 representation = r.representation 1802 1803 output = {} 1804 1805 # Get table-specific parameters 1806 _config = self._config 1807 orderby = _config("orderby", None) 1808 linkto = _config("linkto", None) 1809 list_fields = _config("list_fields") 1810 1811 list_id = "datatable" 1812 1813 # Check permission to read in this table 1814 authorised = self._permitted() 1815 if not authorised: 1816 r.unauthorised() 1817 1818 # Pagination 1819 get_vars = self.request.get_vars 1820 if representation == "aadata": 1821 start, limit = self._limits(get_vars) 1822 else: 1823 start = None 1824 limit = None if s3.no_sspag else 0 1825 1826 # Linkto 1827 if not linkto: 1828 linkto = self._linkto(r) 1829 1830 # List fields 1831 if not list_fields: 1832 fields = resource.readable_fields() 1833 list_fields = [f.name for f in fields] 1834 else: 1835 fields = [table[f] for f in list_fields if f in table.fields] 1836 if not fields: 1837 fields = [] 1838 1839 if not fields or \ 1840 fields[0].name != table.fields[0]: 1841 fields.insert(0, table[table.fields[0]]) 1842 if list_fields[0] != table.fields[0]: 1843 list_fields.insert(0, table.fields[0]) 1844 1845 left = [] 1846 distinct = False 1847 1848 if r.interactive: 1849 1850 resource.build_query(filter=s3.filter) 1851 1852 # View 1853 response.view = self._view(r, "list.html") 1854 1855 # Page title 1856 crud_string = self.crud_string 1857 if r.component: 1858 title = crud_string(r.tablename, "title_display") 1859 else: 1860 title = crud_string(self.tablename, "title_list") 1861 output["title"] = title 1862 1863 # How many records per page? 1864 if s3.dataTable_pageLength: 1865 display_length = s3.dataTable_pageLength 1866 else: 1867 display_length = 25 1868 1869 # Server-side pagination? 1870 if not s3.no_sspag: 1871 dt_pagination = "true" 1872 if not limit: 1873 limit = 2 * display_length 1874 session.s3.filter = get_vars 1875 if orderby is None: 1876 # Default initial sorting 1877 scol = len(list_fields) > 1 and "1" or "0" 1878 get_vars.update(iSortingCols="1", 1879 iSortCol_0=scol, 1880 sSortDir_0="asc") 1881 orderby, left = resource.datatable_filter(list_fields, 1882 get_vars, 1883 )[1:3] 1884 del get_vars["iSortingCols"] 1885 del get_vars["iSortCol_0"] 1886 del get_vars["sSortDir_0"] 1887 else: 1888 dt_pagination = "false" 1889 1890 # Get the data table 1891 dt, totalrows = resource.datatable(fields = list_fields, 1892 start = start, 1893 limit = limit, 1894 left = left, 1895 orderby = orderby, 1896 distinct = distinct, 1897 ) 1898 displayrows = totalrows 1899 1900 # No records? 1901 if dt is None: 1902 s3.no_formats = True 1903 datatable = current.T("No records to review") 1904 else: 1905 dt_dom = s3.get("dataTable_dom", 1906 current.deployment_settings.get_ui_datatables_dom()) 1907 datatable = dt.html(totalrows, displayrows, list_id, 1908 dt_pagination=dt_pagination, 1909 dt_pageLength=display_length, 1910 dt_dom = dt_dom, 1911 ) 1912 s3.actions = [{"label": s3_str(current.T("Review")), 1913 "url": r.url(id="[id]", method="review"), 1914 "_class": "action-btn"}] 1915 1916 # Add items to output 1917 output["items"] = datatable 1918 1919 elif r.representation == "aadata": 1920 1921 resource.build_query(filter=s3.filter, vars=session.s3.filter) 1922 1923 # Apply datatable filters 1924 searchq, orderby, left = resource.datatable_filter(list_fields, get_vars) 1925 if searchq is not None: 1926 totalrows = resource.count() 1927 resource.add_filter(searchq) 1928 else: 1929 totalrows = None 1930 1931 # Orderby fallbacks 1932 if orderby is None: 1933 orderby = _config("orderby", None) 1934 1935 # Get a data table 1936 if totalrows != 0: 1937 dt, displayrows = resource.datatable(fields = list_fields, 1938 start = start, 1939 limit = limit, 1940 left = left, 1941 orderby = orderby, 1942 distinct = distinct, 1943 ) 1944 else: 1945 dt, displayrows = None, 0 1946 if totalrows is None: 1947 totalrows = displayrows 1948 1949 # Echo 1950 draw = int(get_vars.draw or 0) 1951 1952 # Representation 1953 if dt is not None: 1954 output = dt.json(totalrows, 1955 displayrows, 1956 list_id, 1957 draw) 1958 else: 1959 output = '{"recordsTotal": %s, ' \ 1960 '"recordsFiltered": 0,' \ 1961 '"dataTable_id": "%s", ' \ 1962 '"draw": %s, ' \ 1963 '"data": []}' % (totalrows, list_id, draw) 1964 1965 else: 1966 r.error(415, current.ERROR.BAD_FORMAT) 1967 1968 return output
1969 1970 # -------------------------------------------------------------------------
1971 - def review(self, r, **attr):
1972 """ 1973 Review/approve/reject an unapproved record. 1974 1975 @param r: the S3Request 1976 @param attr: dictionary of parameters for the method handler 1977 """ 1978 1979 if not self._permitted("review"): 1980 r.unauthorized() 1981 1982 T = current.T 1983 1984 session = current.session 1985 response = current.response 1986 1987 output = Storage() 1988 if r.interactive: 1989 1990 _next = r.url(id="[id]", method="review") 1991 1992 if self._permitted("approve"): 1993 1994 approve = FORM(INPUT(_value=T("Approve"), 1995 _type="submit", 1996 _name="approve-btn", 1997 _id="approve-btn", 1998 _class="action-btn")) 1999 2000 reject = FORM(INPUT(_value=T("Reject"), 2001 _type="submit", 2002 _name="reject-btn", 2003 _id="reject-btn", 2004 _class="action-btn")) 2005 2006 edit = A(T("Edit"), 2007 _href=r.url(id=r.id, method="update", 2008 vars={"_next": r.url(id=r.id, method="review")}), 2009 _class="action-btn") 2010 2011 cancel = A(T("Cancel"), 2012 _href=r.url(id=0), 2013 _class="action-lnk") 2014 2015 output["approve_form"] = DIV(TABLE(TR(approve, reject, edit, cancel)), 2016 _id="approve_form") 2017 2018 reviewing = False 2019 if approve.accepts(r.post_vars, session, formname="approve"): 2020 resource = current.s3db.resource(r.tablename, r.id, 2021 approved=False, 2022 unapproved=True) 2023 try: 2024 success = resource.approve() 2025 except: 2026 success = False 2027 if success: 2028 confirmation = response.confirmation 2029 if confirmation: 2030 response.confirmation = "%s, %s" % (T("Record approved"), 2031 confirmation, 2032 ) 2033 else: 2034 response.confirmation = T("Record approved") 2035 output["approve_form"] = "" 2036 else: 2037 response.warning = T("Record could not be approved.") 2038 2039 r.http = "GET" 2040 _next = r.url(id=0, method="review") 2041 2042 elif reject.accepts(r.post_vars, session, formname="reject"): 2043 resource = current.s3db.resource(r.tablename, r.id, 2044 approved=False, 2045 unapproved=True) 2046 try: 2047 success = resource.reject() 2048 except: 2049 success = False 2050 if success: 2051 response.confirmation = T("Record deleted") 2052 output["approve_form"] = "" 2053 else: 2054 response.warning = T("Record could not be deleted.") 2055 2056 r.http = "GET" 2057 _next = r.url(id=0, method="review") 2058 2059 else: 2060 reviewing = True 2061 2062 if reviewing: 2063 output.update(self.read(r, **attr)) 2064 self.next = _next 2065 r.http = r.env.request_method 2066 current.response.view = "review.html" 2067 2068 else: 2069 r.error(415, current.ERROR.BAD_FORMAT) 2070 2071 return output
2072 2073 # -------------------------------------------------------------------------
2074 - def validate(self, r, **attr):
2075 """ 2076 Validate records (AJAX). This method reads a JSON object from 2077 the request body, validates it against the current resource, 2078 and returns a JSON object with either the validation errors or 2079 the text representations of the data. 2080 2081 @param r: the S3Request 2082 @param attr: dictionary of parameters for the method handler 2083 2084 Input JSON format: 2085 2086 {"<fieldname>":"<value>", "<fieldname>":"<value>"} 2087 2088 Output JSON format: 2089 2090 {"<fieldname>": {"value":"<value>", 2091 "text":"<representation>", 2092 "_error":"<error message>"}} 2093 2094 The input JSON can also be a list of multiple records. This 2095 will return a list of results accordingly. Note that "text" 2096 is not provided if there was a validation error, and vice 2097 versa. 2098 2099 The record ID should always be present in the JSON to 2100 avoid false duplicate errors. 2101 2102 Non-existent fields will return "invalid field" as _error. 2103 2104 Representations will be URL-escaped and any markup stripped. 2105 2106 The ?component=<alias> URL query can be used to specify a 2107 component of the current resource rather than the main table. 2108 2109 This method does only accept .json format. 2110 """ 2111 2112 if r.representation != "json": 2113 r.error(415, current.ERROR.BAD_FORMAT) 2114 2115 resource = self.resource 2116 2117 get_vars = r.get_vars 2118 if "component" in get_vars: 2119 alias = get_vars["component"] 2120 else: 2121 alias = None 2122 if "resource" in get_vars: 2123 tablename = get_vars["resource"] 2124 2125 if tablename != "%s_%s" % (r.controller, r.function): 2126 # Customise the resource 2127 customise = current.deployment_settings.customise_resource(tablename) 2128 if customise: 2129 customise(r, tablename) 2130 2131 components = [alias] if alias else None 2132 try: 2133 resource = current.s3db.resource(tablename, 2134 components = components, 2135 ) 2136 except (AttributeError, SyntaxError): 2137 r.error(404, current.ERROR.BAD_RESOURCE) 2138 2139 if alias: 2140 try: 2141 component = resource.components[alias] 2142 except KeyError: 2143 r.error(404, current.ERROR.BAD_RESOURCE) 2144 else: 2145 component = resource 2146 2147 source = r.body 2148 source.seek(0) 2149 2150 try: 2151 data = json.load(source) 2152 except ValueError: 2153 r.error(501, current.ERROR.BAD_SOURCE) 2154 2155 if not isinstance(data, list): 2156 single = True 2157 data = [data] 2158 else: 2159 single = False 2160 2161 table = component.table 2162 pkey = table._id.name 2163 2164 get_config = current.s3db.get_config 2165 tablename = component.tablename 2166 onvalidation = get_config(tablename, "onvalidation") 2167 update_onvalidation = get_config(tablename, "update_onvalidation", 2168 onvalidation) 2169 create_onvalidation = get_config(tablename, "create_onvalidation", 2170 onvalidation) 2171 2172 output = [] 2173 for record in data: 2174 2175 has_errors = False 2176 2177 # Retrieve the record ID 2178 if pkey in record: 2179 original = {pkey: record[pkey]} 2180 elif "_id" in record: 2181 original = {pkey: record["_id"]} 2182 else: 2183 original = None 2184 2185 # Field validation 2186 fields = Storage() 2187 for fname in record: 2188 2189 # We do not validate primary keys 2190 # (because we don't update them) 2191 if fname in (pkey, "_id"): 2192 continue 2193 2194 error = None 2195 validated = fields[fname] = Storage() 2196 2197 skip_validation = False 2198 skip_formatting = False 2199 2200 value = record[fname] 2201 2202 if fname not in table.fields: 2203 validated["value"] = value 2204 validated["_error"] = "invalid field" 2205 continue 2206 else: 2207 field = table[fname] 2208 2209 # Convert numeric types (does not always happen in the widget) 2210 widget = field.widget 2211 if widget and hasattr(widget, "s3_parse"): 2212 parser = widget.s3_parse 2213 else: 2214 parser = None 2215 ftype = field.type 2216 if ftype == "integer": 2217 if value not in (None, ""): 2218 if not callable(parser): 2219 parser = int 2220 try: 2221 value = parser(value) 2222 except ValueError: 2223 value = 0 2224 else: 2225 value = None 2226 elif ftype == "double": 2227 if value not in (None, ""): 2228 if not callable(parser): 2229 parser = float 2230 try: 2231 value = parser(value) 2232 except ValueError: 2233 value = 0.0 2234 else: 2235 value = None 2236 2237 # Catch upload fields 2238 if ftype == "upload" and value: 2239 2240 # We cannot Ajax-validate the file (it's not uploaded yet) 2241 skip_validation = True 2242 2243 # We cannot render a link/preview of the file 2244 # (unless it's already uploaded) 2245 try: 2246 fullname = field.retrieve(value, nameonly=True)[1] 2247 except Exception: 2248 skip_formatting = True 2249 else: 2250 import os 2251 skip_formatting = not isinstance(fullname, basestring) or \ 2252 not os.path.isfile(fullname) 2253 2254 # Validate and serialize the value 2255 if isinstance(widget, S3Selector): 2256 # Use widget-validator instead of field-validator 2257 if not skip_validation: 2258 value, error = widget.validate(value, 2259 requires=field.requires, 2260 ) 2261 validated["value"] = widget.serialize(value) \ 2262 if not error else value 2263 # Use widget-represent instead of standard represent 2264 widget_represent = widget.represent 2265 else: 2266 # Validate and format the value 2267 if not skip_validation: 2268 try: 2269 value, error = s3_validate(table, fname, value, original) 2270 except AttributeError: 2271 error = "invalid field" 2272 validated["value"] = field.formatter(value) \ 2273 if not error else value 2274 widget_represent = None 2275 2276 # Handle errors, update the validated item 2277 if error: 2278 has_errors = True 2279 validated["_error"] = s3_unicode(error) 2280 elif skip_formatting: 2281 validated["text"] = s3_unicode(value) 2282 elif widget_represent: 2283 try: 2284 text = widget_represent(value) 2285 except: 2286 text = s3_unicode(value) 2287 validated["text"] = text 2288 else: 2289 try: 2290 text = s3_represent_value(field, value = value) 2291 except: 2292 text = s3_unicode(value) 2293 validated["text"] = text 2294 2295 # Form validation (=onvalidation) 2296 if not has_errors: 2297 if original is not None: 2298 onvalidation = update_onvalidation 2299 else: 2300 onvalidation = create_onvalidation 2301 form = Storage(vars=Storage(record), errors=Storage()) 2302 if onvalidation is not None: 2303 callback(onvalidation, form, tablename=tablename) 2304 for fn in form.errors: 2305 msg = s3_unicode(form.errors[fn]) 2306 if fn in fields: 2307 validated = fields[fn] 2308 has_errors = True 2309 validated._error = msg 2310 if "text" in validated: 2311 del validated["text"] 2312 else: 2313 msg = "%s: %s" % (fn, msg) 2314 if "_error" in fields: 2315 fields["_error"] = "\n".join([msg, 2316 fields["_error"]]) 2317 else: 2318 fields["_error"] = msg 2319 2320 output.append(fields) 2321 2322 if single and len(output) == 1: 2323 output = output[0] 2324 2325 return json.dumps(output, separators=SEPARATORS)
2326 2327 # ------------------------------------------------------------------------- 2328 # Utility functions 2329 # ------------------------------------------------------------------------- 2330 @staticmethod
2331 - def crud_button(label=None, 2332 tablename=None, 2333 name=None, 2334 icon=None, 2335 _href=None, 2336 _id=None, 2337 _class=None, 2338 _title=None, 2339 _target=None, 2340 **attr):
2341 """ 2342 Generate a CRUD action button 2343 2344 @param label: the link label (None if using CRUD string) 2345 @param tablename: the name of table for CRUD string selection 2346 @param name: name of CRUD string for the button label 2347 @param icon: name of the icon (e.g. "add") 2348 @param _href: the target URL 2349 @param _id: the HTML id of the link 2350 @param _class: the HTML class of the link 2351 @param _title: the HTML title of the link 2352 @param _target: the HTML target of the link 2353 2354 @keyword custom: custom CRUD button (just add classes) 2355 """ 2356 2357 settings = current.deployment_settings 2358 2359 # If using Bootstrap then we need to amend our core HTML markup 2360 bootstrap = settings.ui.formstyle == "bootstrap" 2361 2362 # Custom button? 2363 if "custom" in attr: 2364 custom = attr["custom"] 2365 if custom is None: 2366 custom = "" 2367 elif bootstrap and hasattr(custom, "add_class"): 2368 custom.add_class("btn btn-primary") 2369 return custom 2370 2371 # Default class 2372 if _class is None and not bootstrap: 2373 _class = "action-btn" 2374 2375 # Default label 2376 if name: 2377 labelstr = S3CRUD.crud_string(tablename, name) 2378 else: 2379 labelstr = str(label) 2380 2381 # Show icon on button? 2382 if icon and settings.get_ui_use_button_icons(): 2383 button = A(ICON(icon), labelstr, _id=_id, _class=_class) 2384 else: 2385 button = A(labelstr, _id=_id, _class=_class) 2386 2387 # Button attributes 2388 if _href: 2389 button["_href"] = _href 2390 if _title: 2391 button["_title"] = _title 2392 if _target: 2393 button["_target"] = _target 2394 2395 # Additional classes? 2396 if bootstrap: 2397 button.add_class("btn btn-primary") 2398 return button
2399 2400 # -------------------------------------------------------------------------
2401 - def last_update(self):
2402 """ 2403 Get the last update meta-data of the current record 2404 2405 @return: a dict {modified_by: <user>, modified_on: <datestr>}, 2406 depending on which of these attributes are available 2407 in the current record 2408 """ 2409 2410 output = {} 2411 record_id = self.record_id 2412 if record_id: 2413 record = None 2414 fields = [] 2415 table = self.table 2416 if "modified_on" in table.fields: 2417 fields.append(table.modified_on) 2418 if "modified_by" in table.fields: 2419 fields.append(table.modified_by) 2420 2421 if fields: 2422 query = (table._id == record_id) 2423 record = current.db(query).select(limitby=(0, 1), 2424 *fields).first() 2425 if record: 2426 T = current.T 2427 if "modified_by" in record: 2428 if not record.modified_by: 2429 modified_by = T("anonymous user") 2430 else: 2431 modified_by = \ 2432 table.modified_by.represent(record.modified_by) 2433 output["modified_by"] = T("by %(person)s") % \ 2434 {"person": modified_by} 2435 if "modified_on" in record: 2436 modified_on = \ 2437 S3DateTime.datetime_represent(record.modified_on, 2438 utc=True, 2439 ) 2440 output["modified_on"] = T("on %(date)s") % \ 2441 {"date": modified_on} 2442 return output
2443 2444 # -------------------------------------------------------------------------
2445 - def render_buttons(self, r, buttons, record_id=None, **attr):
2446 """ 2447 Render CRUD buttons 2448 2449 @param r: the S3Request 2450 @param buttons: list of button names, any of: 2451 "add", "edit", "delete", "list", "summary" 2452 @param record_id: the record ID 2453 @param attr: the controller attributes 2454 2455 @return: a dict of buttons for the view 2456 """ 2457 2458 output = {} 2459 custom_crud_buttons = attr.get("custom_crud_buttons", {}) 2460 2461 tablename = self.tablename 2462 representation = r.representation 2463 2464 url = r.url 2465 2466 remove_filters = self._remove_filters 2467 crud_string = self.crud_string 2468 config = self._config 2469 crud_button = self.crud_button 2470 2471 # Add button 2472 if "add" in buttons and config("insertable", True): 2473 ADD_BTN = "add_btn" 2474 authorised = self._permitted(method="create") 2475 if authorised: 2476 if ADD_BTN in custom_crud_buttons: 2477 btn = crud_button(custom=custom_crud_buttons[ADD_BTN]) 2478 else: 2479 label = crud_string(tablename, "label_create") 2480 _href = url(method="create", 2481 representation=representation) 2482 btn = crud_button(label=label, 2483 icon="add", 2484 _href=_href, 2485 _id="add-btn") 2486 output[ADD_BTN] = btn 2487 2488 # List button 2489 if "list" in buttons: 2490 LIST_BTN = "list_btn" 2491 if not r.component or r.component.multiple: 2492 if LIST_BTN in custom_crud_buttons: 2493 btn = crud_button(custom=custom_crud_buttons[LIST_BTN]) 2494 else: 2495 label = crud_string(tablename, "label_list_button") 2496 _href = url(method="", 2497 id=r.id if r.component else 0, 2498 vars=remove_filters(r.get_vars), 2499 representation=representation) 2500 btn = crud_button(label=label, 2501 icon="list", 2502 _href=_href, 2503 _id="list-btn") 2504 output[LIST_BTN] = btn 2505 2506 # Summary button 2507 if "summary" in buttons: 2508 SUMMARY_BTN = "summary_btn" 2509 if not r.component or r.component.multiple: 2510 if SUMMARY_BTN in custom_crud_buttons: 2511 btn = crud_button(custom=custom_crud_buttons[SUMMARY_BTN]) 2512 else: 2513 label = crud_string(tablename, "label_list_button") 2514 _href = url(method="summary", 2515 id=0, 2516 vars=remove_filters(r.get_vars), 2517 representation=representation) 2518 btn = crud_button(label=label, 2519 icon="list", 2520 _href=_href, 2521 _id="summary-btn") 2522 output[SUMMARY_BTN] = btn 2523 2524 if not record_id: 2525 return output 2526 2527 # Edit button 2528 if "edit" in buttons and config("editable", True): 2529 EDIT_BTN = "edit_btn" 2530 authorised = self._permitted(method="update") 2531 if authorised: 2532 if EDIT_BTN in custom_crud_buttons: 2533 btn = crud_button(custom=custom_crud_buttons[EDIT_BTN]) 2534 else: 2535 label = current.messages.UPDATE 2536 _href = url(method="update", 2537 representation=representation) 2538 btn = crud_button(label=label, 2539 icon="edit", 2540 _href=_href, 2541 _id="edit-btn") 2542 output[EDIT_BTN] = btn 2543 2544 # Delete button 2545 if "delete" in buttons and config("deletable", True): 2546 DELETE_BTN = "delete_btn" 2547 authorised = self._permitted(method="delete") 2548 if authorised: 2549 if DELETE_BTN in custom_crud_buttons: 2550 btn = crud_button(custom=custom_crud_buttons[DELETE_BTN]) 2551 else: 2552 label = crud_string(tablename, "label_delete_button") 2553 _href = url(method="delete", 2554 representation=representation) 2555 btn = crud_button(label=label, 2556 icon="delete", 2557 _href=_href, 2558 _id="delete-btn", 2559 _class="delete-btn") 2560 output[DELETE_BTN] = btn 2561 2562 return output
2563 2564 # ------------------------------------------------------------------------- 2565 @staticmethod
2566 - def action_button(label, url, icon=None, **attr):
2567 """ 2568 Add a link to response.s3.actions 2569 2570 @param label: the link label 2571 @param url: the target URL 2572 @param attr: attributes for the link (default: {"_class":"action-btn"}) 2573 """ 2574 2575 link = dict(attr) 2576 link["label"] = s3_str(label) 2577 link["url"] = url if url else "" 2578 if icon and current.deployment_settings.get_ui_use_button_icons(): 2579 link["icon"] = ICON.css_class(icon) 2580 if "_class" not in link: 2581 link["_class"] = "action-btn" 2582 2583 s3 = current.response.s3 2584 if s3.actions is None: 2585 s3.actions = [link] 2586 else: 2587 s3.actions.append(link)
2588 2589 # ------------------------------------------------------------------------- 2590 @classmethod
2591 - def action_buttons(cls, 2592 r, 2593 deletable = True, 2594 editable = None, 2595 copyable = False, 2596 read_url = None, 2597 delete_url = None, 2598 update_url = None, 2599 copy_url = None):
2600 """ 2601 Provide the usual action buttons in list views. 2602 Allow customizing the urls, since this overwrites anything 2603 that would be inserted by CRUD/select via linkto. The resource 2604 id should be represented by "[id]". 2605 2606 @param r: the S3Request 2607 @param deletable: records can be deleted 2608 @param editable: records can be modified 2609 @param copyable: record data can be copied into new record 2610 @param read_url: URL to read a record 2611 @param delete_url: URL to delete a record 2612 @param update_url: URL to update a record 2613 @param copy_url: URL to copy record data 2614 2615 @note: If custom actions are already configured at this point, 2616 they will appear AFTER the standard action buttons 2617 """ 2618 2619 s3crud = S3CRUD 2620 s3 = current.response.s3 2621 labels = s3.crud_labels 2622 2623 custom_actions = s3.actions 2624 s3.actions = None 2625 2626 auth = current.auth 2627 has_permission = auth.s3_has_permission 2628 ownership_required = auth.permission.ownership_required 2629 2630 if r.component: 2631 table = r.component.table 2632 args = [r.id, r.component.alias, "[id]"] 2633 else: 2634 table = r.table 2635 args = ["[id]"] 2636 2637 get_vars = cls._linkto_vars(r) 2638 2639 settings = current.deployment_settings 2640 2641 # If this request is in iframe-format, action URLs should be in 2642 # iframe-format as well 2643 if r.representation == "iframe": 2644 if settings.get_ui_iframe_opens_full(): 2645 iframe_safe = lambda url: url 2646 # This is processed client-side in s3.ui.datatable.js 2647 target = {"_target": "_blank"} 2648 else: 2649 iframe_safe = lambda url: s3_set_extension(url, "iframe") 2650 target = {} 2651 else: 2652 iframe_safe = lambda url: url 2653 target = {} 2654 2655 if editable is None: 2656 # Fall back to settings if caller didn't override 2657 editable = False if settings.get_ui_open_read_first() else \ 2658 "auto" if settings.get_ui_auto_open_update() else True 2659 2660 # Open-action (Update or Read) 2661 authorised = has_permission("update", table) 2662 if editable and authorised and not ownership_required("update", table): 2663 # User has permission to edit all records, and caller allows edit 2664 if not update_url: 2665 update_url = iframe_safe(URL(args = args + ["update"], #.popup to use modals 2666 vars = get_vars, 2667 )) 2668 s3crud.action_button(labels.UPDATE, update_url, 2669 # To use modals 2670 #_class="action-btn s3_modal" 2671 _class="action-btn edit", 2672 icon = "edit", 2673 **target 2674 ) 2675 else: 2676 # User is not permitted to edit at least some of the records, 2677 # or caller doesn't allow edit 2678 if not read_url: 2679 method = ["read"] if not editable or not authorised else [] 2680 read_url = iframe_safe(URL(args = args + method, #.popup to use modals 2681 vars = get_vars, 2682 )) 2683 s3crud.action_button(labels.READ, read_url, 2684 # To use modals 2685 #_class="action-btn s3_modal" 2686 _class="action-btn read", 2687 icon = "file", 2688 **target 2689 ) 2690 2691 # Delete-action 2692 if deletable and has_permission("delete", table): 2693 icon = "delete" 2694 if not delete_url: 2695 delete_url = iframe_safe(URL(args = args + ["delete"], 2696 vars = get_vars)) 2697 if ownership_required("delete", table): 2698 # Check which records can be deleted 2699 query = auth.s3_accessible_query("delete", table) 2700 rows = current.db(query).select(table._id) 2701 restrict = [] 2702 rappend = restrict.append 2703 for row in rows: 2704 row_id = row.get("id", None) 2705 if row_id: 2706 rappend(str(row_id)) 2707 s3crud.action_button(labels.DELETE, delete_url, 2708 _class="delete-btn", 2709 icon=icon, 2710 restrict=restrict, 2711 **target 2712 ) 2713 else: 2714 s3crud.action_button(labels.DELETE, delete_url, 2715 _class="delete-btn", 2716 icon=icon, 2717 **target 2718 ) 2719 2720 # Copy-action 2721 if copyable and has_permission("create", table): 2722 if not copy_url: 2723 copy_url = iframe_safe(URL(args = args + ["copy"])) 2724 s3crud.action_button(labels.COPY, 2725 copy_url, 2726 icon="icon-copy", 2727 **target 2728 ) 2729 2730 # Append custom actions 2731 if custom_actions: 2732 s3.actions = s3.actions + custom_actions
2733 2734 # -------------------------------------------------------------------------
2735 - def _default_cancel_button(self, r):
2736 """ 2737 Show a default cancel button in standalone create/update forms. 2738 Individual controllers can override this by setting 2739 response.s3.cancel = False. 2740 2741 @param r: the S3Request 2742 """ 2743 2744 if r.representation != "html": 2745 return False 2746 2747 s3 = current.response.s3 2748 2749 cancel = s3.cancel 2750 if cancel is False or isinstance(cancel, dict): 2751 success = False 2752 elif cancel is True or \ 2753 current.deployment_settings.get_ui_default_cancel_button(): 2754 2755 if isinstance(cancel, basestring): 2756 default_url = cancel 2757 else: 2758 method = r.method 2759 if method == "create": 2760 if r.component: 2761 default_url = r.url(method = "", 2762 component_id= "", 2763 vars = {}, 2764 ) 2765 else: 2766 config = self._config("summary") 2767 if config or \ 2768 current.deployment_settings.get_ui_summary(): 2769 default_url = r.url(method="summary", id=0) 2770 else: 2771 default_url = r.url(method="", id=0) 2772 elif method == "update" or not method: 2773 if r.component: 2774 default_url = r.url(method = "", 2775 component_id= "", 2776 vars = {}, 2777 ) 2778 else: 2779 default_url = r.url(method="read") 2780 if default_url: 2781 script = '''$.cancelButtonS3('%s')''' % default_url 2782 else: 2783 script = '''$.cancelButtonS3()''' 2784 jquery_ready = current.response.s3.jquery_ready 2785 if script not in jquery_ready: 2786 jquery_ready.append(script) 2787 success = s3.cancel = True 2788 else: 2789 success = False 2790 2791 return success
2792 2793 # -------------------------------------------------------------------------
2794 - def import_csv(self, stream, table=None):
2795 """ 2796 Import CSV file into database 2797 2798 @param stream: file handle 2799 @param table: the table to import to 2800 """ 2801 2802 if table: 2803 table.import_from_csv_file(stream) 2804 else: 2805 db = current.db 2806 # This is the preferred method as it updates reference fields 2807 db.import_from_csv_file(stream) 2808 db.commit()
2809 2810 # ------------------------------------------------------------------------- 2811 @staticmethod
2812 - def import_url(r):
2813 """ 2814 Import data from vars in URL query 2815 2816 @param r: the S3Request 2817 @note: can only update single records (no mass-update) 2818 2819 @todo: update for link table components 2820 @todo: re-integrate into S3Importer 2821 """ 2822 2823 xml = current.xml 2824 2825 table = r.target()[2] 2826 2827 record = r.record 2828 resource = r.resource 2829 2830 # Handle components 2831 if record and r.component: 2832 resource = resource.components[r.component_name] 2833 resource.load() 2834 if len(resource) == 1: 2835 record = resource.records()[0] 2836 else: 2837 record = None 2838 r.vars.update({resource.fkey: r.record[resource.pkey]}) 2839 elif not record and r.component: 2840 item = xml.json_message(False, 400, "Invalid Request!") 2841 return {"item": item} 2842 2843 # Check for update 2844 if record and xml.UID in table.fields: 2845 r.vars.update({xml.UID: xml.export_uid(record[xml.UID])}) 2846 2847 # Build tree 2848 element = etree.Element(xml.TAG.resource) 2849 element.set(xml.ATTRIBUTE.name, resource.tablename) 2850 for var in r.vars: 2851 if var.find(".") != -1: 2852 continue 2853 elif var in table.fields: 2854 field = table[var] 2855 value = str(r.vars[var]).decode("utf-8") 2856 if var in xml.FIELDS_TO_ATTRIBUTES: 2857 element.set(var, value) 2858 else: 2859 data = etree.Element(xml.TAG.data) 2860 data.set(xml.ATTRIBUTE.field, var) 2861 if field.type == "upload": 2862 data.set(xml.ATTRIBUTE.filename, value) 2863 else: 2864 data.text = value 2865 element.append(data) 2866 tree = xml.tree([element], domain=xml.domain) 2867 2868 # Import data 2869 result = Storage(committed=False) 2870 def log(item): 2871 result["item"] = item
2872 resource.configure(oncommit_import_item = log) 2873 try: 2874 success = resource.import_xml(tree) 2875 except SyntaxError: 2876 pass 2877 2878 # Check result 2879 if result.item: 2880 result = result.item 2881 2882 # Build response 2883 if success and result.committed: 2884 r.id = result.id 2885 method = result.method 2886 if method == result.METHOD.CREATE: 2887 item = xml.json_message(True, 201, "Created as %s?%s.id=%s" % 2888 (str(r.url(method="", 2889 representation="html", 2890 vars={}, 2891 ) 2892 ), 2893 r.name, result.id) 2894 ) 2895 else: 2896 item = xml.json_message(True, 200, "Record updated") 2897 else: 2898 item = xml.json_message(False, 403, 2899 "Could not create/update record: %s" % 2900 resource.error or xml.error, 2901 tree=xml.tree2json(tree)) 2902 2903 return {"item": item}
2904 2905 2906 # -------------------------------------------------------------------------
2907 - def _embed_component(self, resource, record=None):
2908 """ 2909 Renders the right key constraint in a link table as 2910 S3EmbeddedComponentWidget and stores the postprocess hook. 2911 2912 @param resource: the link table resource 2913 """ 2914 2915 link = None 2916 2917 component = resource.linked 2918 if component is not None and component.actuate == "embed": 2919 2920 ctablename = component.tablename 2921 attr = {"link": resource.tablename, 2922 "component": ctablename, 2923 } 2924 2925 autocomplete = component.autocomplete 2926 if autocomplete and autocomplete in component.table: 2927 attr["autocomplete"] = autocomplete 2928 2929 if record is not None: 2930 attr["link_filter"] = "%s.%s.%s.%s.%s" % ( 2931 resource.tablename, 2932 component.lkey, 2933 record, 2934 component.rkey, 2935 component.fkey) 2936 2937 rkey = component.rkey 2938 if rkey in resource.table: 2939 field = resource.table[rkey] 2940 field.widget = S3EmbeddedComponentWidget(**attr) 2941 field.comment = None 2942 2943 callback = self._postprocess_embedded 2944 postprocess = lambda form, key=rkey, component=ctablename: \ 2945 callback(form, key=key, component=component) 2946 link = Storage(postprocess=postprocess) 2947 2948 return link
2949 2950 # -------------------------------------------------------------------------
2951 - def _postprocess_embedded(self, form, component=None, key=None):
2952 """ 2953 Post-processes a form with an S3EmbeddedComponentWidget and 2954 created/updates the component record. 2955 2956 @param form: the form 2957 @param component: the component tablename 2958 @param key: the field name of the foreign key for the component 2959 in the link table 2960 """ 2961 2962 s3db = current.s3db 2963 request = current.request 2964 2965 get_config = lambda key, tablename=component: \ 2966 s3db.get_config(tablename, key, None) 2967 try: 2968 selected = form.vars[key] 2969 except (AttributeError, KeyError): 2970 selected = None 2971 2972 if request.env.request_method == "POST": 2973 db = current.db 2974 table = db[component] 2975 2976 # Extract data for embedded form from post_vars 2977 post_vars = request.post_vars 2978 form_vars = Storage(table._filter_fields(post_vars)) 2979 2980 # Pass values through validator to convert them into db-format 2981 for k in form_vars: 2982 value, error = s3_validate(table, k, form_vars[k]) 2983 if not error: 2984 form_vars[k] = value 2985 2986 _form = Storage(vars = form_vars, errors = Storage()) 2987 if _form.vars: 2988 if selected: 2989 form_vars[table._id.name] = selected 2990 # Onvalidation 2991 onvalidation = get_config("update_onvalidation") or \ 2992 get_config("onvalidation") 2993 callback(onvalidation, _form, tablename=component) 2994 # Update the record if no errors 2995 if not _form.errors: 2996 db(table._id == selected).update(**_form.vars) 2997 else: 2998 form.errors.update(_form.errors) 2999 return 3000 # Update super-entity links 3001 s3db.update_super(table, {"id": selected}) 3002 # Update realm 3003 update_realm = s3db.get_config(table, "update_realm") 3004 if update_realm: 3005 current.auth.set_realm_entity(table, selected, 3006 force_update=True) 3007 # Onaccept 3008 onaccept = get_config("update_onaccept") or \ 3009 get_config("onaccept") 3010 callback(onaccept, _form, tablename=component) 3011 else: 3012 form_vars.pop(table._id.name, None) 3013 # Onvalidation 3014 onvalidation = get_config("create_onvalidation") or \ 3015 get_config("onvalidation") 3016 callback(onvalidation, _form, tablename=component) 3017 # Insert the record if no errors 3018 if not _form.errors: 3019 selected = table.insert(**_form.vars) 3020 else: 3021 form.errors.update(_form.errors) 3022 return 3023 if selected: 3024 # Update post_vars and form.vars 3025 post_vars[key] = str(selected) 3026 form.request_vars[key] = str(selected) 3027 form.vars[key] = selected 3028 # Update super-entity links 3029 s3db.update_super(table, {"id": selected}) 3030 # Set record owner 3031 auth = current.auth 3032 auth.s3_set_record_owner(table, selected) 3033 auth.s3_make_session_owner(table, selected) 3034 # Onaccept 3035 onaccept = get_config("create_onaccept") or \ 3036 get_config("onaccept") 3037 callback(onaccept, _form, tablename=component) 3038 else: 3039 form.errors[key] = current.T("Could not create record.") 3040 return
3041 3042 # -------------------------------------------------------------------------
3043 - def _linkto(self, r, authorised=None, update=None, native=False):
3044 """ 3045 Returns a linker function for the record ID column in list views 3046 3047 @param r: the S3Request 3048 @param authorised: user authorised for update 3049 (override internal check) 3050 @param update: provide link to update rather than to read 3051 @param native: link to the native controller rather than to 3052 component controller 3053 """ 3054 3055 c = None 3056 f = None 3057 3058 s3db = current.s3db 3059 3060 prefix, name, _, tablename = r.target() 3061 permit = current.auth.s3_has_permission 3062 3063 if authorised is None: 3064 authorised = permit("update", tablename) 3065 3066 if authorised and update: 3067 linkto = s3db.get_config(tablename, "linkto_update", None) 3068 else: 3069 linkto = s3db.get_config(tablename, "linkto", None) 3070 3071 if r.component and native: 3072 # link to native component controller (be sure that you have one) 3073 c = prefix 3074 f = name 3075 3076 if r.representation == "iframe": 3077 if current.deployment_settings.get_ui_iframe_opens_full(): 3078 iframe_safe = lambda url: s3_set_extension(url, "html") 3079 else: 3080 iframe_safe = lambda url: s3_set_extension(url, "iframe") 3081 else: 3082 iframe_safe = False 3083 3084 def list_linkto(record_id, r=r, c=c, f=f, 3085 linkto=linkto, 3086 update=authorised and update): 3087 3088 if linkto: 3089 try: 3090 url = str(linkto(record_id)) 3091 except TypeError: 3092 url = linkto % record_id 3093 else: 3094 get_vars = self._linkto_vars(r) 3095 3096 if r.component: 3097 if r.link and not r.actuate_link(): 3098 # We're rendering a link table here, but must 3099 # however link to the component record IDs 3100 if str(record_id).isdigit(): 3101 # dataTables uses the value in the ID column 3102 # to render action buttons, so we replace that 3103 # value by the component record ID using .represent 3104 _id = r.link.table._id 3105 _id.represent = lambda opt, \ 3106 link=r.link, master=r.id: \ 3107 link.component_id(master, opt) 3108 # The native link behind the action buttons uses 3109 # record_id, so we replace that too just in case 3110 # the action button cannot be displayed 3111 record_id = r.link.component_id(r.id, record_id) 3112 3113 if c and f: 3114 args = [record_id] 3115 else: 3116 c = r.controller 3117 f = r.function 3118 args = [r.id, r.component_name, record_id] 3119 else: 3120 args = [record_id] 3121 3122 # Add explicit open-method if required 3123 if update != "auto": 3124 if update: 3125 args = args + ["update"] 3126 else: 3127 args = args + ["read"] 3128 3129 url = str(URL(r=r, c=c, f=f, args=args, vars=get_vars)) 3130 3131 if iframe_safe: 3132 url = iframe_safe(url) 3133 return url
3134 3135 return list_linkto 3136 3137 # ------------------------------------------------------------------------- 3138 @staticmethod
3139 - def _linkto_vars(r):
3140 """ 3141 Retain certain GET vars of the request in action links 3142 3143 @param r: the S3Request 3144 3145 @return: Storage with GET vars 3146 """ 3147 3148 get_vars = r.get_vars 3149 linkto_vars = Storage() 3150 3151 # Retain "viewing" 3152 if not r.component and "viewing" in get_vars: 3153 linkto_vars.viewing = get_vars["viewing"] 3154 3155 keep_vars = current.response.s3.crud.keep_vars 3156 if keep_vars: 3157 for key in keep_vars: 3158 if key in get_vars: 3159 linkto_vars[key] = get_vars[key] 3160 3161 return linkto_vars
3162 3163 # ------------------------------------------------------------------------- 3164 @staticmethod
3165 - def _interim_save_button():
3166 """ 3167 Render an additional custom submit button for interim save, 3168 which overrides the default _next to returns to an update 3169 form for the same record after create/update 3170 """ 3171 3172 label = current.deployment_settings.get_ui_interim_save() 3173 if label: 3174 _class = "interim-save" 3175 if isinstance(label, basestring): 3176 label = current.T(label) 3177 elif isinstance(label, (tuple, list)) and len(label) > 1: 3178 label, _class = label[:2] 3179 elif not isinstance(label, lazyT): 3180 label = current.T("Save and Continue Editing") 3181 item = ("interim_save", label, _class) 3182 else: 3183 return 3184 settings = current.response.s3.crud 3185 custom_submit = [item] 3186 if settings.custom_submit: 3187 custom_submit.extend(settings.custom_submit) 3188 settings.custom_submit = custom_submit 3189 return
3190 3191 # ------------------------------------------------------------------------- 3192 @classmethod
3193 - def _dl_ajax_delete(cls, r, resource):
3194 3195 UID = current.xml.UID 3196 3197 delete = r.get_vars.get("delete", None) 3198 if delete is not None: 3199 3200 dresource = current.s3db.resource(resource, id=delete) 3201 3202 # Deleting in this resource allowed at all? 3203 deletable = dresource.get_config("deletable", True) 3204 if not deletable: 3205 r.error(403, current.ERROR.NOT_PERMITTED) 3206 3207 # Permitted to delete this record? 3208 authorised = current.auth.s3_has_permission("delete", 3209 dresource.table, 3210 record_id=delete) 3211 if not authorised: 3212 r.unauthorised() 3213 3214 # Delete it 3215 uid = None 3216 if UID in dresource.table: 3217 rows = dresource.select([UID], 3218 start=0, 3219 limit=1, 3220 as_rows=True) 3221 if rows: 3222 uid = rows[0][UID] 3223 numrows = dresource.delete(format=r.representation) 3224 if numrows > 1: 3225 message = "%s %s" % (numrows, 3226 current.T("records deleted")) 3227 elif numrows == 1: 3228 message = cls.crud_string(dresource.tablename, 3229 "msg_record_deleted") 3230 else: 3231 r.error(404, dresource.error) 3232 3233 # Return a JSON message 3234 # @note: make sure the view doesn't get overridden afterwards! 3235 current.response.view = "xml.html" 3236 return current.xml.json_message(message=message, uuid=uid) 3237 else: 3238 r.error(404, current.ERROR.BAD_RECORD)
3239 3240 # -------------------------------------------------------------------------
3241 - def _set_organizer_dates(self, dates):
3242 """ 3243 Set default dates for organizer resources 3244 3245 @param dates: a string with two ISO dates separated by --, like: 3246 "2010-11-29T23:00:00.000Z--2010-11-29T23:59:59.000Z" 3247 """ 3248 3249 resource = self.resource 3250 3251 if dates: 3252 dates = dates.split("--") 3253 if len(dates) != 2: 3254 return 3255 3256 from s3organizer import S3Organizer 3257 3258 try: 3259 config = S3Organizer.parse_config(resource) 3260 except AttributeError: 3261 return 3262 3263 start = config["start"] 3264 if start and start.field: 3265 try: 3266 start.field.default = s3_decode_iso_datetime(dates[0]) 3267 except ValueError: 3268 pass 3269 3270 end = config["end"] 3271 if end and end.field: 3272 try: 3273 end.field.default = s3_decode_iso_datetime(dates[1]) 3274 except ValueError: 3275 pass
3276 3277 # ------------------------------------------------------------------------- 3278 @staticmethod
3279 - def _limits(get_vars, default_limit=0):
3280 """ 3281 Extract page limits (start and limit) from GET vars 3282 3283 @param get_vars: the GET vars 3284 @param default_limit: the default limit, explicit value or: 3285 0 => response.s3.ROWSPERPAGE 3286 None => no default limit 3287 """ 3288 3289 start = get_vars.get("start", None) 3290 limit = get_vars.get("limit", default_limit) 3291 3292 # Deal with overrides (pagination limits come last) 3293 if isinstance(start, list): 3294 start = start[-1] 3295 if isinstance(limit, list): 3296 limit = limit[-1] 3297 3298 if limit: 3299 # Ability to override default limit to "Show All" 3300 if isinstance(limit, basestring) and limit.lower() == "none": 3301 #start = None # needed? 3302 limit = None 3303 else: 3304 try: 3305 start = int(start) if start is not None else None 3306 limit = int(limit) 3307 except (ValueError, TypeError): 3308 # Fall back to defaults 3309 start, limit = None, default_limit 3310 3311 else: 3312 # Use defaults, assume sspag because this is a 3313 # pagination request by definition 3314 start = None 3315 limit = default_limit 3316 3317 return start, limit
3318 3319 # END ========================================================================= 3320