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

Source Code for Module s3.s3rest

   1  # -*- coding: utf-8 -*-
 
   2  
 
   3  """ S3 RESTful API
 
   4  
 
   5      @copyright: 2009-2019 (c) Sahana Software Foundation
 
   6      @license: MIT
 
   7  
 
   8      Permission is hereby granted, free of charge, to any person
 
   9      obtaining a copy of this software and associated documentation
 
  10      files (the "Software"), to deal in the Software without
 
  11      restriction, including without limitation the rights to use,
 
  12      copy, modify, merge, publish, distribute, sublicense, and/or sell
 
  13      copies of the Software, and to permit persons to whom the
 
  14      Software is furnished to do so, subject to the following
 
  15      conditions:
 
  16  
 
  17      The above copyright notice and this permission notice shall be
 
  18      included in all copies or substantial portions of the Software.
 
  19  
 
  20      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
 
  21      EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
 
  22      OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
 
  23      NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
 
  24      HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
 
  25      WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 
  26      FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
 
  27      OTHER DEALINGS IN THE SOFTWARE.
 
  28  """ 
  29  
 
  30  __all__ = ("S3Request",
 
  31             "S3Method",
 
  32             "s3_request",
 
  33             ) 
  34  
 
  35  import json 
  36  import os 
  37  import re 
  38  import sys 
  39  import types 
  40  try: 
  41      from cStringIO import StringIO    # Faster, where available 
  42  except ImportError: 
  43      from StringIO import StringIO 
  44  
 
  45  from gluon import current, redirect, HTTP, URL 
  46  from gluon.storage import Storage 
  47  
 
  48  from s3datetime import s3_parse_datetime 
  49  from s3resource import S3Resource 
  50  from s3utils import s3_get_extension, s3_remove_last_record_id, s3_store_last_record_id 
  51  
 
  52  REGEX_FILTER = re.compile(r".+\..+|.*\(.+\).*") 
  53  HTTP_METHODS = ("GET", "PUT", "POST", "DELETE") 
54 55 # ============================================================================= 56 -class S3Request(object):
57 """ 58 Class to handle RESTful requests 59 """ 60 61 INTERACTIVE_FORMATS = ("html", "iframe", "popup", "dl") 62 DEFAULT_REPRESENTATION = "html" 63 64 # -------------------------------------------------------------------------
65 - def __init__(self, 66 prefix=None, 67 name=None, 68 r=None, 69 c=None, 70 f=None, 71 args=None, 72 vars=None, 73 extension=None, 74 get_vars=None, 75 post_vars=None, 76 http=None):
77 """ 78 Constructor 79 80 @param prefix: the table name prefix 81 @param name: the table name 82 @param c: the controller prefix 83 @param f: the controller function 84 @param args: list of request arguments 85 @param vars: dict of request variables 86 @param extension: the format extension (representation) 87 @param get_vars: the URL query variables (overrides vars) 88 @param post_vars: the POST variables (overrides vars) 89 @param http: the HTTP method (GET, PUT, POST, or DELETE) 90 91 @note: all parameters fall back to the attributes of the 92 current web2py request object 93 """ 94 95 auth = current.auth 96 97 # Common settings 98 99 # XSLT Paths 100 self.XSLT_PATH = "static/formats" 101 self.XSLT_EXTENSION = "xsl" 102 103 # Attached files 104 self.files = Storage() 105 106 # Allow override of controller/function 107 self.controller = c or self.controller 108 self.function = f or self.function 109 if "." in self.function: 110 self.function, ext = self.function.split(".", 1) 111 if extension is None: 112 extension = ext 113 if c or f: 114 if not auth.permission.has_permission("read", 115 c=self.controller, 116 f=self.function): 117 auth.permission.fail() 118 119 # Allow override of request args/vars 120 if args is not None: 121 if isinstance(args, (list, tuple)): 122 self.args = args 123 else: 124 self.args = [args] 125 if get_vars is not None: 126 self.get_vars = get_vars 127 self.vars = get_vars.copy() 128 if post_vars is not None: 129 self.vars.update(post_vars) 130 else: 131 self.vars.update(self.post_vars) 132 if post_vars is not None: 133 self.post_vars = post_vars 134 if get_vars is None: 135 self.vars = post_vars.copy() 136 self.vars.update(self.get_vars) 137 if get_vars is None and post_vars is None and vars is not None: 138 self.vars = vars 139 self.get_vars = vars 140 self.post_vars = Storage() 141 142 self.extension = extension or current.request.extension 143 self.http = http or current.request.env.request_method 144 145 # Main resource attributes 146 if r is not None: 147 if not prefix: 148 prefix = r.prefix 149 if not name: 150 name = r.name 151 self.prefix = prefix or self.controller 152 self.name = name or self.function 153 154 # Parse the request 155 self.__parse() 156 self.custom_action = None 157 get_vars = Storage(self.get_vars) 158 159 # Interactive representation format? 160 self.interactive = self.representation in self.INTERACTIVE_FORMATS 161 162 # Show information on deleted records? 163 include_deleted = False 164 if self.representation == "xml" and "include_deleted" in get_vars: 165 include_deleted = True 166 if "components" in get_vars: 167 cnames = get_vars["components"] 168 if isinstance(cnames, list): 169 cnames = ",".join(cnames) 170 cnames = cnames.split(",") 171 if len(cnames) == 1 and cnames[0].lower() == "none": 172 cnames = [] 173 else: 174 cnames = None 175 176 # Append component ID to the URL query 177 component_name = self.component_name 178 component_id = self.component_id 179 if component_name and component_id: 180 varname = "%s.id" % component_name 181 if varname in get_vars: 182 var = get_vars[varname] 183 if not isinstance(var, (list, tuple)): 184 var = [var] 185 var.append(component_id) 186 get_vars[varname] = var 187 else: 188 get_vars[varname] = component_id 189 190 # Define the target resource 191 _filter = current.response.s3.filter 192 components = component_name 193 if components is None: 194 components = cnames 195 196 tablename = "%s_%s" % (self.prefix, self.name) 197 198 if not current.deployment_settings.get_auth_record_approval(): 199 # Record Approval is off 200 approved, unapproved = True, False 201 elif self.method == "review": 202 approved, unapproved = False, True 203 elif auth.s3_has_permission("review", tablename, self.id): 204 # Approvers should be able to edit records during review 205 # @ToDo: deployment_setting to allow Filtering out from 206 # multi-record methods even for those with Review permission 207 approved, unapproved = True, True 208 else: 209 approved, unapproved = True, False 210 211 self.resource = S3Resource(tablename, 212 id=self.id, 213 filter=_filter, 214 vars=get_vars, 215 components=components, 216 approved=approved, 217 unapproved=unapproved, 218 include_deleted=include_deleted, 219 context=True, 220 filter_component=component_name, 221 ) 222 223 self.tablename = self.resource.tablename 224 table = self.table = self.resource.table 225 226 # Try to load the master record 227 self.record = None 228 uid = self.vars.get("%s.uid" % self.name) 229 if self.id or uid and not isinstance(uid, (list, tuple)): 230 # Single record expected 231 self.resource.load() 232 if len(self.resource) == 1: 233 self.record = self.resource.records().first() 234 _id = table._id.name 235 self.id = self.record[_id] 236 s3_store_last_record_id(self.tablename, self.id) 237 else: 238 raise KeyError(current.ERROR.BAD_RECORD) 239 240 # Identify the component 241 self.component = None 242 if self.component_name: 243 c = self.resource.components.get(self.component_name) 244 if c: 245 self.component = c 246 else: 247 error = "%s not a component of %s" % (self.component_name, 248 self.resource.tablename) 249 raise AttributeError(error) 250 251 # Identify link table and link ID 252 self.link = None 253 self.link_id = None 254 255 if self.component is not None: 256 self.link = self.component.link 257 if self.link and self.id and self.component_id: 258 self.link_id = self.link.link_id(self.id, self.component_id) 259 if self.link_id is None: 260 raise KeyError(current.ERROR.BAD_RECORD) 261 262 # Store method handlers 263 self._handlers = {} 264 set_handler = self.set_handler 265 266 set_handler("export_tree", self.get_tree, 267 http=("GET",), transform=True) 268 set_handler("import_tree", self.put_tree, 269 http=("GET", "PUT", "POST"), transform=True) 270 set_handler("fields", self.get_fields, 271 http=("GET",), transform=True) 272 set_handler("options", self.get_options, 273 http=("GET",), 274 representation = ("__transform__", "json"), 275 ) 276 277 sync = current.sync 278 set_handler("sync", sync, 279 http=("GET", "PUT", "POST",), transform=True) 280 set_handler("sync_log", sync.log, 281 http=("GET",), transform=True) 282 set_handler("sync_log", sync.log, 283 http=("GET",), transform=False) 284 285 # Initialize CRUD 286 self.resource.crud(self, method="_init") 287 if self.component is not None: 288 self.component.crud(self, method="_init")
289 290 # ------------------------------------------------------------------------- 291 # Method handler configuration 292 # -------------------------------------------------------------------------
293 - def set_handler(self, method, handler, 294 http=None, 295 representation=None, 296 transform=False):
297 """ 298 Set a method handler for this request 299 300 @param method: the method name 301 @param handler: the handler function 302 @type handler: handler(S3Request, **attr) 303 @param http: restrict to these HTTP methods, list|tuple 304 @param representation: register handler for non-transformable data 305 formats 306 @param transform: register handler for transformable data formats 307 (overrides representation) 308 """ 309 310 if http is None: 311 http = HTTP_METHODS 312 else: 313 if not isinstance(http, (tuple, list)): 314 http = (http,) 315 316 if transform: 317 representation = ("__transform__",) 318 elif not representation: 319 representation = (self.DEFAULT_REPRESENTATION,) 320 else: 321 if not isinstance(representation, (tuple, list)): 322 representation = (representation,) 323 324 if not isinstance(method, (tuple, list)): 325 method = (method,) 326 327 handlers = self._handlers 328 for h in http: 329 if h not in HTTP_METHODS: 330 continue 331 format_hooks = handlers.get(h) 332 if format_hooks is None: 333 format_hooks = handlers[h] = {} 334 for r in representation: 335 method_hooks = format_hooks.get(r) 336 if method_hooks is None: 337 method_hooks = format_hooks[r] = {} 338 for m in method: 339 method_hooks[m] = handler
340 341 # -------------------------------------------------------------------------
342 - def get_handler(self, method, transform=False):
343 """ 344 Get a method handler for this request 345 346 @param method: the method name 347 @param transform: get handler for transformable data format 348 349 @return: the method handler 350 """ 351 352 handlers = self._handlers 353 354 http_hooks = handlers.get(self.http) 355 if not http_hooks: 356 return None 357 358 DEFAULT_REPRESENTATION = self.DEFAULT_REPRESENTATION 359 hooks = http_hooks.get(DEFAULT_REPRESENTATION) 360 if hooks: 361 method_hooks = dict(hooks) 362 else: 363 method_hooks = {} 364 365 representation = "__transform__" if transform else self.representation 366 if representation and representation != DEFAULT_REPRESENTATION: 367 hooks = http_hooks.get(representation) 368 if hooks: 369 method_hooks.update(hooks) 370 371 if not method: 372 methods = (None,) 373 else: 374 methods = (method, None) 375 for m in methods: 376 handler = method_hooks.get(m) 377 if handler is not None: 378 break 379 380 if isinstance(handler, (type, types.ClassType)): 381 return handler() 382 else: 383 return handler
384 385 # -------------------------------------------------------------------------
386 - def get_widget_handler(self, method):
387 """ 388 Get the widget handler for a method 389 390 @param r: the S3Request 391 @param method: the widget method 392 """ 393 394 if self.component: 395 resource = self.component 396 if resource.link: 397 resource = resource.link 398 else: 399 resource = self.resource 400 prefix, name = self.prefix, self.name 401 component_name = self.component_name 402 403 custom_action = current.s3db.get_method(prefix, 404 name, 405 component_name=component_name, 406 method=method) 407 408 http = self.http 409 handler = None 410 411 if method and custom_action: 412 handler = custom_action 413 414 if http == "GET": 415 if not method: 416 if resource.count() == 1: 417 method = "read" 418 else: 419 method = "list" 420 transform = self.transformable() 421 handler = self.get_handler(method, transform=transform) 422 423 elif http == "PUT": 424 transform = self.transformable(method="import") 425 handler = self.get_handler(method, transform=transform) 426 427 elif http == "POST": 428 transform = self.transformable(method="import") 429 return self.get_handler(method, transform=transform) 430 431 elif http == "DELETE": 432 if method: 433 return self.get_handler(method) 434 else: 435 return self.get_handler("delete") 436 437 else: 438 return None 439 440 if handler is None: 441 handler = resource.crud 442 if isinstance(handler, (type, types.ClassType)): 443 handler = handler() 444 return handler
445 446 # ------------------------------------------------------------------------- 447 # Request Parser 448 # -------------------------------------------------------------------------
449 - def __parse(self):
450 """ Parses the web2py request object """ 451 452 self.id = None 453 self.component_name = None 454 self.component_id = None 455 self.method = None 456 457 # Get the names of all components 458 tablename = "%s_%s" % (self.prefix, self.name) 459 460 # Map request args, catch extensions 461 f = [] 462 append = f.append 463 args = self.args 464 if len(args) > 4: 465 args = args[:4] 466 method = self.name 467 for arg in args: 468 if "." in arg: 469 arg, representation = arg.rsplit(".", 1) 470 if method is None: 471 method = arg 472 elif arg.isdigit(): 473 append((method, arg)) 474 method = None 475 else: 476 append((method, None)) 477 method = arg 478 if method: 479 append((method, None)) 480 481 self.id = f[0][1] 482 483 # Sort out component name and method 484 l = len(f) 485 if l > 1: 486 m = f[1][0].lower() 487 i = f[1][1] 488 components = current.s3db.get_components(tablename, names=[m]) 489 if components and m in components: 490 self.component_name = m 491 self.component_id = i 492 else: 493 self.method = m 494 if not self.id: 495 self.id = i 496 if self.component_name and l > 2: 497 self.method = f[2][0].lower() 498 if not self.component_id: 499 self.component_id = f[2][1] 500 501 representation = s3_get_extension(self) 502 if representation: 503 self.representation = representation 504 else: 505 self.representation = self.DEFAULT_REPRESENTATION 506 507 # Check for special URL variable $search, indicating 508 # that the request body contains filter queries: 509 if self.http == "POST" and "$search" in self.get_vars: 510 self.__search()
511 512 # -------------------------------------------------------------------------
513 - def __search(self):
514 """ 515 Process filters in POST, interprets URL filter expressions 516 in POST vars (if multipart), or from JSON request body (if 517 not multipart or $search=ajax). 518 519 NB: overrides S3Request method as GET (r.http) to trigger 520 the correct method handlers, but will not change 521 current.request.env.request_method 522 """ 523 524 get_vars = self.get_vars 525 content_type = self.env.get("content_type") or "" 526 527 mode = get_vars.get("$search") 528 529 # Override request method 530 if mode: 531 self.http = "GET" 532 533 # Retrieve filters from request body 534 if content_type == "application/x-www-form-urlencoded": 535 # Read POST vars (from S3.gis.refreshLayer) 536 filters = self.post_vars 537 decode = None 538 elif mode == "ajax" or content_type[:10] != "multipart/": 539 # Read body JSON (from $.searchS3) 540 s = self.body 541 s.seek(0) 542 try: 543 filters = json.load(s) 544 except ValueError: 545 filters = {} 546 if not isinstance(filters, dict): 547 filters = {} 548 decode = None 549 else: 550 # Read POST vars JSON (from $.searchDownloadS3) 551 filters = self.post_vars 552 decode = json.loads 553 554 # Move filters into GET vars 555 get_vars = Storage(get_vars) 556 post_vars = Storage(self.post_vars) 557 558 del get_vars["$search"] 559 for k, v in filters.items(): 560 k0 = k[0] 561 if k == "$filter" or \ 562 k0 != "_" and ("." in k or k0 == "(" and ")" in k): 563 try: 564 value = decode(v) if decode else v 565 except ValueError: 566 continue 567 # Catch any non-str values 568 if type(value) is list: 569 value = [str(item) 570 if not isinstance(item, basestring) else item 571 for item in value 572 ] 573 elif not isinstance(value, basestring): 574 value = str(value) 575 get_vars[k] = value 576 # Remove filter expression from POST vars 577 if k in post_vars: 578 del post_vars[k] 579 580 # Override self.get_vars and self.post_vars 581 self.get_vars = get_vars 582 self.post_vars = post_vars 583 584 # Update combined vars 585 self.vars = get_vars.copy() 586 self.vars.update(self.post_vars)
587 588 # ------------------------------------------------------------------------- 589 # REST Interface 590 # -------------------------------------------------------------------------
591 - def __call__(self, **attr):
592 """ 593 Execute this request 594 595 @param attr: Parameters for the method handler 596 """ 597 598 response = current.response 599 s3 = response.s3 600 self.next = None 601 602 bypass = False 603 output = None 604 preprocess = None 605 postprocess = None 606 607 representation = self.representation 608 609 # Enforce primary record ID 610 if not self.id and representation == "html": 611 if self.component or self.method in ("read", "profile", "update"): 612 count = self.resource.count() 613 if self.vars is not None and count == 1: 614 self.resource.load() 615 self.record = self.resource._rows[0] 616 self.id = self.record.id 617 else: 618 #current.session.error = current.ERROR.BAD_RECORD 619 redirect(URL(r=self, c=self.prefix, f=self.name)) 620 621 # Pre-process 622 if s3 is not None: 623 preprocess = s3.get("prep") 624 if preprocess: 625 pre = preprocess(self) 626 # Re-read representation after preprocess: 627 representation = self.representation 628 if pre and isinstance(pre, dict): 629 bypass = pre.get("bypass", False) is True 630 output = pre.get("output") 631 if not bypass: 632 success = pre.get("success", True) 633 if not success: 634 if representation == "html" and output: 635 if isinstance(output, dict): 636 output["r"] = self 637 return output 638 else: 639 status = pre.get("status", 400) 640 message = pre.get("message", 641 current.ERROR.BAD_REQUEST) 642 self.error(status, message) 643 elif not pre: 644 self.error(400, current.ERROR.BAD_REQUEST) 645 646 # Default view 647 if representation not in ("html", "popup"): 648 response.view = "xml.html" 649 650 # Content type 651 response.headers["Content-Type"] = s3.content_type.get(representation, 652 "text/html") 653 654 # Custom action? 655 if not self.custom_action: 656 action = current.s3db.get_method(self.prefix, 657 self.name, 658 component_name=self.component_name, 659 method=self.method) 660 if isinstance(action, (type, types.ClassType)): 661 self.custom_action = action() 662 else: 663 self.custom_action = action 664 665 # Method handling 666 http = self.http 667 handler = None 668 if not bypass: 669 # Find the method handler 670 if self.method and self.custom_action: 671 handler = self.custom_action 672 elif http == "GET": 673 handler = self.__GET() 674 elif http == "PUT": 675 handler = self.__PUT() 676 elif http == "POST": 677 handler = self.__POST() 678 elif http == "DELETE": 679 handler = self.__DELETE() 680 else: 681 self.error(405, current.ERROR.BAD_METHOD) 682 # Invoke the method handler 683 if handler is not None: 684 output = handler(self, **attr) 685 else: 686 # Fall back to CRUD 687 output = self.resource.crud(self, **attr) 688 689 # Post-process 690 if s3 is not None: 691 postprocess = s3.get("postp") 692 if postprocess is not None: 693 output = postprocess(self, output) 694 if output is not None and isinstance(output, dict): 695 # Put a copy of r into the output for the view 696 # to be able to make use of it 697 output["r"] = self 698 699 # Redirection 700 # NB must re-read self.http/method here in case the have 701 # been changed during prep, method handling or postp 702 if self.next is not None and \ 703 (self.http != "GET" or self.method == "clear"): 704 if isinstance(output, dict): 705 form = output.get("form") 706 if form: 707 if not hasattr(form, "errors"): 708 # Form embedded in a DIV together with other components 709 form = form.elements('form', first_only=True) 710 form = form[0] if form else None 711 if form and form.errors: 712 return output 713 714 session = current.session 715 session.flash = response.flash 716 session.confirmation = response.confirmation 717 session.error = response.error 718 session.warning = response.warning 719 redirect(self.next) 720 721 return output
722 723 # -------------------------------------------------------------------------
724 - def __GET(self, resource=None):
725 """ 726 Get the GET method handler 727 """ 728 729 method = self.method 730 transform = False 731 if method is None or method in ("read", "display", "update"): 732 if self.transformable(): 733 method = "export_tree" 734 transform = True 735 elif self.component: 736 resource = self.resource 737 if self.interactive and resource.count() == 1: 738 # Load the record 739 if not resource._rows: 740 resource.load(start=0, limit=1) 741 if resource._rows: 742 self.record = resource._rows[0] 743 self.id = resource.get_id() 744 self.uid = resource.get_uid() 745 if self.component.multiple and not self.component_id: 746 method = "list" 747 else: 748 method = "read" 749 elif self.id or method in ("read", "display", "update"): 750 # Enforce single record 751 resource = self.resource 752 if not resource._rows: 753 resource.load(start=0, limit=1) 754 if resource._rows: 755 self.record = resource._rows[0] 756 self.id = resource.get_id() 757 self.uid = resource.get_uid() 758 else: 759 self.error(404, current.ERROR.BAD_RECORD) 760 method = "read" 761 else: 762 method = "list" 763 764 elif method in ("create", "update"): 765 if self.transformable(method="import"): 766 method = "import_tree" 767 transform = True 768 769 elif method == "delete": 770 return self.__DELETE() 771 772 elif method == "clear" and not self.component: 773 s3_remove_last_record_id(self.tablename) 774 self.next = URL(r=self, f=self.name) 775 return lambda r, **attr: None 776 777 elif self.transformable(): 778 transform = True 779 780 return self.get_handler(method, transform=transform)
781 782 # -------------------------------------------------------------------------
783 - def __PUT(self):
784 """ 785 Get the PUT method handler 786 """ 787 788 transform = self.transformable(method="import") 789 790 method = self.method 791 if not method and transform: 792 method = "import_tree" 793 794 return self.get_handler(method, transform=transform)
795 796 # -------------------------------------------------------------------------
797 - def __POST(self):
798 """ 799 Get the POST method handler 800 """ 801 802 if self.method == "delete": 803 return self.__DELETE() 804 else: 805 if self.transformable(method="import"): 806 return self.__PUT() 807 else: 808 post_vars = self.post_vars 809 table = self.target()[2] 810 if "deleted" in table and "id" not in post_vars: # and "uuid" not in post_vars: 811 original = S3Resource.original(table, post_vars) 812 if original and original.deleted: 813 self.post_vars["id"] = original.id 814 self.vars["id"] = original.id 815 return self.__GET()
816 817 # -------------------------------------------------------------------------
818 - def __DELETE(self):
819 """ 820 Get the DELETE method handler 821 """ 822 823 if self.method: 824 return self.get_handler(self.method) 825 else: 826 return self.get_handler("delete")
827 828 # ------------------------------------------------------------------------- 829 # Built-in method handlers 830 # ------------------------------------------------------------------------- 831 @staticmethod
832 - def get_tree(r, **attr):
833 """ 834 XML Element tree export method 835 836 @param r: the S3Request instance 837 @param attr: controller attributes 838 """ 839 840 get_vars = r.get_vars 841 args = Storage() 842 843 # Slicing 844 start = get_vars.get("start") 845 if start is not None: 846 try: 847 start = int(start) 848 except ValueError: 849 start = None 850 limit = get_vars.get("limit") 851 if limit is not None: 852 try: 853 limit = int(limit) 854 except ValueError: 855 limit = None 856 857 # msince 858 msince = get_vars.get("msince") 859 if msince is not None: 860 msince = s3_parse_datetime(msince) 861 862 # Show IDs (default: False) 863 if "show_ids" in get_vars: 864 if get_vars["show_ids"].lower() == "true": 865 current.xml.show_ids = True 866 867 # Show URLs (default: True) 868 if "show_urls" in get_vars: 869 if get_vars["show_urls"].lower() == "false": 870 current.xml.show_urls = False 871 872 # Mobile data export (default: False) 873 mdata = get_vars.get("mdata") == "1" 874 875 # Maxbounds (default: False) 876 maxbounds = False 877 if "maxbounds" in get_vars: 878 if get_vars["maxbounds"].lower() == "true": 879 maxbounds = True 880 if r.representation in ("gpx", "osm"): 881 maxbounds = True 882 883 # Components of the master resource (tablenames) 884 if "mcomponents" in get_vars: 885 mcomponents = get_vars["mcomponents"] 886 if str(mcomponents).lower() == "none": 887 mcomponents = None 888 elif not isinstance(mcomponents, list): 889 mcomponents = mcomponents.split(",") 890 else: 891 mcomponents = [] # all 892 893 # Components of referenced resources (tablenames) 894 if "rcomponents" in get_vars: 895 rcomponents = get_vars["rcomponents"] 896 if str(rcomponents).lower() == "none": 897 rcomponents = None 898 elif not isinstance(rcomponents, list): 899 rcomponents = rcomponents.split(",") 900 else: 901 rcomponents = None 902 903 # Maximum reference resolution depth 904 if "maxdepth" in get_vars: 905 try: 906 args["maxdepth"] = int(get_vars["maxdepth"]) 907 except ValueError: 908 pass 909 910 # References to resolve (field names) 911 if "references" in get_vars: 912 references = get_vars["references"] 913 if str(references).lower() == "none": 914 references = [] 915 elif not isinstance(references, list): 916 references = references.split(",") 917 else: 918 references = None # all 919 920 # Export field selection 921 if "fields" in get_vars: 922 fields = get_vars["fields"] 923 if str(fields).lower() == "none": 924 fields = [] 925 elif not isinstance(fields, list): 926 fields = fields.split(",") 927 else: 928 fields = None # all 929 930 # Find XSLT stylesheet 931 stylesheet = r.stylesheet() 932 933 # Add stylesheet parameters 934 if stylesheet is not None: 935 if r.component: 936 args["id"] = r.id 937 args["component"] = r.component.tablename 938 if r.component.alias: 939 args["alias"] = r.component.alias 940 mode = get_vars.get("xsltmode") 941 if mode is not None: 942 args["mode"] = mode 943 944 # Set response headers 945 response = current.response 946 s3 = response.s3 947 headers = response.headers 948 representation = r.representation 949 if representation in s3.json_formats: 950 as_json = True 951 default = "application/json" 952 else: 953 as_json = False 954 default = "text/xml" 955 headers["Content-Type"] = s3.content_type.get(representation, 956 default) 957 958 # Export the resource 959 resource = r.resource 960 target = r.target()[3] 961 if target == resource.tablename: 962 # Master resource targetted 963 target = None 964 output = resource.export_xml(start = start, 965 limit = limit, 966 msince = msince, 967 fields = fields, 968 dereference = True, 969 # maxdepth in args 970 references = references, 971 mdata = mdata, 972 mcomponents = mcomponents, 973 rcomponents = rcomponents, 974 stylesheet = stylesheet, 975 as_json = as_json, 976 maxbounds = maxbounds, 977 target = target, 978 **args) 979 # Transformation error? 980 if not output: 981 r.error(400, "XSLT Transformation Error: %s " % current.xml.error) 982 983 return output
984 985 # ------------------------------------------------------------------------- 986 @staticmethod
987 - def put_tree(r, **attr):
988 """ 989 XML Element tree import method 990 991 @param r: the S3Request method 992 @param attr: controller attributes 993 """ 994 995 get_vars = r.get_vars 996 997 # Skip invalid records? 998 if "ignore_errors" in get_vars: 999 ignore_errors = True 1000 else: 1001 ignore_errors = False 1002 1003 # Find all source names in the URL vars 1004 def findnames(get_vars, name): 1005 nlist = [] 1006 if name in get_vars: 1007 names = get_vars[name] 1008 if isinstance(names, (list, tuple)): 1009 names = ",".join(names) 1010 names = names.split(",") 1011 for n in names: 1012 if n[0] == "(" and ")" in n[1:]: 1013 nlist.append(n[1:].split(")", 1)) 1014 else: 1015 nlist.append([None, n]) 1016 return nlist
1017 filenames = findnames(get_vars, "filename") 1018 fetchurls = findnames(get_vars, "fetchurl") 1019 source_url = None 1020 1021 # Get the source(s) 1022 s3 = current.response.s3 1023 json_formats = s3.json_formats 1024 csv_formats = s3.csv_formats 1025 source = [] 1026 representation = r.representation 1027 if representation in json_formats or representation in csv_formats: 1028 if filenames: 1029 try: 1030 for f in filenames: 1031 source.append((f[0], open(f[1], "rb"))) 1032 except: 1033 source = [] 1034 elif fetchurls: 1035 import urllib 1036 try: 1037 for u in fetchurls: 1038 source.append((u[0], urllib.urlopen(u[1]))) 1039 except: 1040 source = [] 1041 elif r.http != "GET": 1042 source = r.read_body() 1043 else: 1044 if filenames: 1045 source = filenames 1046 elif fetchurls: 1047 source = fetchurls 1048 # Assume only 1 URL for GeoRSS feed caching 1049 source_url = fetchurls[0][1] 1050 elif r.http != "GET": 1051 source = r.read_body() 1052 if not source: 1053 if filenames or fetchurls: 1054 # Error: source not found 1055 r.error(400, "Invalid source") 1056 else: 1057 # No source specified => return resource structure 1058 return r.get_struct(r, **attr) 1059 1060 # Find XSLT stylesheet 1061 stylesheet = r.stylesheet(method="import") 1062 # Target IDs 1063 if r.method == "create": 1064 _id = None 1065 else: 1066 _id = r.id 1067 1068 # Transformation mode? 1069 if "xsltmode" in get_vars: 1070 args = dict(xsltmode=get_vars["xsltmode"]) 1071 else: 1072 args = dict() 1073 # These 3 options are called by gis.show_map() & read by the 1074 # GeoRSS Import stylesheet to populate the gis_cache table 1075 # Source URL: For GeoRSS/KML Feed caching 1076 if source_url: 1077 args["source_url"] = source_url 1078 # Data Field: For GeoRSS/KML Feed popups 1079 if "data_field" in get_vars: 1080 args["data_field"] = get_vars["data_field"] 1081 # Image Field: For GeoRSS/KML Feed popups 1082 if "image_field" in get_vars: 1083 args["image_field"] = get_vars["image_field"] 1084 1085 # Format type? 1086 if representation in json_formats: 1087 representation = "json" 1088 elif representation in csv_formats: 1089 representation = "csv" 1090 else: 1091 representation = "xml" 1092 1093 try: 1094 output = r.resource.import_xml(source, 1095 id=_id, 1096 format=representation, 1097 files=r.files, 1098 stylesheet=stylesheet, 1099 ignore_errors=ignore_errors, 1100 **args) 1101 except IOError: 1102 current.auth.permission.fail() 1103 except SyntaxError: 1104 e = sys.exc_info()[1] 1105 if hasattr(e, "message"): 1106 e = e.message 1107 r.error(400, e) 1108 1109 return output
1110 1111 # ------------------------------------------------------------------------- 1112 @staticmethod
1113 - def get_struct(r, **attr):
1114 """ 1115 Resource structure introspection method 1116 1117 @param r: the S3Request instance 1118 @param attr: controller attributes 1119 """ 1120 1121 response = current.response 1122 json_formats = response.s3.json_formats 1123 if r.representation in json_formats: 1124 as_json = True 1125 content_type = "application/json" 1126 else: 1127 as_json = False 1128 content_type = "text/xml" 1129 get_vars = r.get_vars 1130 meta = str(get_vars.get("meta", False)).lower() == "true" 1131 opts = str(get_vars.get("options", False)).lower() == "true" 1132 refs = str(get_vars.get("references", False)).lower() == "true" 1133 stylesheet = r.stylesheet() 1134 output = r.resource.export_struct(meta=meta, 1135 options=opts, 1136 references=refs, 1137 stylesheet=stylesheet, 1138 as_json=as_json) 1139 if output is None: 1140 # Transformation error 1141 r.error(400, current.xml.error) 1142 response.headers["Content-Type"] = content_type 1143 return output
1144 1145 # ------------------------------------------------------------------------- 1146 @staticmethod
1147 - def get_fields(r, **attr):
1148 """ 1149 Resource structure introspection method (single table) 1150 1151 @param r: the S3Request instance 1152 @param attr: controller attributes 1153 """ 1154 1155 representation = r.representation 1156 if representation == "xml": 1157 output = r.resource.export_fields(component=r.component_name) 1158 content_type = "text/xml" 1159 elif representation == "s3json": 1160 output = r.resource.export_fields(component=r.component_name, 1161 as_json=True) 1162 content_type = "application/json" 1163 else: 1164 r.error(415, current.ERROR.BAD_FORMAT) 1165 response = current.response 1166 response.headers["Content-Type"] = content_type 1167 return output
1168 1169 # ------------------------------------------------------------------------- 1170 @staticmethod
1171 - def get_options(r, **attr):
1172 """ 1173 Field options introspection method (single table) 1174 1175 @param r: the S3Request instance 1176 @param attr: controller attributes 1177 """ 1178 1179 get_vars = r.get_vars 1180 1181 items = get_vars.get("field") 1182 if items: 1183 if not isinstance(items, (list, tuple)): 1184 items = [items] 1185 fields = [] 1186 add_fields = fields.extend 1187 for item in items: 1188 f = item.split(",") 1189 if f: 1190 add_fields(f) 1191 else: 1192 fields = None 1193 1194 if "hierarchy" in get_vars: 1195 hierarchy = get_vars["hierarchy"].lower() not in ("false", "0") 1196 else: 1197 hierarchy = False 1198 1199 if "only_last" in get_vars: 1200 only_last = get_vars["only_last"].lower() not in ("false", "0") 1201 else: 1202 only_last = False 1203 1204 if "show_uids" in get_vars: 1205 show_uids = get_vars["show_uids"].lower() not in ("false", "0") 1206 else: 1207 show_uids = False 1208 1209 representation = r.representation 1210 flat = False 1211 if representation == "xml": 1212 only_last = False 1213 as_json = False 1214 content_type = "text/xml" 1215 elif representation == "s3json": 1216 show_uids = False 1217 as_json = True 1218 content_type = "application/json" 1219 elif representation == "json" and fields and len(fields) == 1: 1220 # JSON option supported for flat data structures only 1221 # e.g. for use by jquery.jeditable 1222 flat = True 1223 show_uids = False 1224 as_json = True 1225 content_type = "application/json" 1226 else: 1227 r.error(415, current.ERROR.BAD_FORMAT) 1228 1229 component = r.component_name 1230 output = r.resource.export_options(component=component, 1231 fields=fields, 1232 show_uids=show_uids, 1233 only_last=only_last, 1234 hierarchy=hierarchy, 1235 as_json=as_json, 1236 ) 1237 1238 if flat: 1239 s3json = json.loads(output) 1240 output = {} 1241 options = s3json.get("option") 1242 if options: 1243 for item in options: 1244 output[item.get("@value")] = item.get("$", "") 1245 output = json.dumps(output) 1246 1247 current.response.headers["Content-Type"] = content_type 1248 return output
1249 1250 # ------------------------------------------------------------------------- 1251 # Tools 1252 # -------------------------------------------------------------------------
1253 - def factory(self, **args):
1254 """ 1255 Generate a new request for the same resource 1256 1257 @param args: arguments for request constructor 1258 """ 1259 1260 return s3_request(r=self, **args)
1261 1262 # -------------------------------------------------------------------------
1263 - def __getattr__(self, key):
1264 """ 1265 Called upon S3Request.<key> - looks up the value for the <key> 1266 attribute. Falls back to current.request if the attribute is 1267 not defined in this S3Request. 1268 1269 @param key: the key to lookup 1270 """ 1271 1272 if key in self.__dict__: 1273 return self.__dict__[key] 1274 1275 sentinel = object() 1276 value = getattr(current.request, key, sentinel) 1277 if value is sentinel: 1278 raise AttributeError 1279 return value
1280 1281 # -------------------------------------------------------------------------
1282 - def transformable(self, method=None):
1283 """ 1284 Check the request for a transformable format 1285 1286 @param method: "import" for import methods, else None 1287 """ 1288 1289 if self.representation in ("html", "aadata", "popup", "iframe"): 1290 return False 1291 1292 stylesheet = self.stylesheet(method=method, skip_error=True) 1293 1294 if not stylesheet and self.representation != "xml": 1295 return False 1296 else: 1297 return True
1298 1299 # ------------------------------------------------------------------------- 1336 1337 # ------------------------------------------------------------------------- 1338 @staticmethod
1339 - def unauthorised():
1340 """ 1341 Action upon unauthorised request 1342 """ 1343 1344 current.auth.permission.fail()
1345 1346 # -------------------------------------------------------------------------
1347 - def error(self, status, message, tree=None, next=None):
1348 """ 1349 Action upon error 1350 1351 @param status: HTTP status code 1352 @param message: the error message 1353 @param tree: the tree causing the error 1354 """ 1355 1356 if self.representation == "html": 1357 current.session.error = message 1358 if next is not None: 1359 redirect(next) 1360 else: 1361 redirect(URL(r=self, f="index")) 1362 else: 1363 headers = {"Content-Type":"application/json"} 1364 current.log.error(message) 1365 raise HTTP(status, 1366 body=current.xml.json_message(success=False, 1367 statuscode=status, 1368 message=message, 1369 tree=tree), 1370 web2py_error=message, 1371 **headers)
1372 1373 # -------------------------------------------------------------------------
1374 - def url(self, 1375 id=None, 1376 component=None, 1377 component_id=None, 1378 target=None, 1379 method=None, 1380 representation=None, 1381 vars=None, 1382 host=None):
1383 """ 1384 Returns the URL of this request, use parameters to override 1385 current requests attributes: 1386 1387 - None to keep current attribute (default) 1388 - 0 or "" to set attribute to NONE 1389 - value to use explicit value 1390 1391 @param id: the master record ID 1392 @param component: the component name 1393 @param component_id: the component ID 1394 @param target: the target record ID (choose automatically) 1395 @param method: the URL method 1396 @param representation: the representation for the URL 1397 @param vars: the URL query variables 1398 @param host: string to force absolute URL with host (True means http_host) 1399 1400 Particular behavior: 1401 - changing the master record ID resets the component ID 1402 - removing the target record ID sets the method to None 1403 - removing the method sets the target record ID to None 1404 - [] as id will be replaced by the "[id]" wildcard 1405 """ 1406 1407 if vars is None: 1408 vars = self.get_vars 1409 elif vars and isinstance(vars, str): 1410 # We've come from a dataTable_vars which has the vars as 1411 # a JSON string, but with the wrong quotation marks 1412 vars = json.loads(vars.replace("'", "\"")) 1413 1414 if "format" in vars: 1415 del vars["format"] 1416 1417 args = [] 1418 1419 cname = self.component_name 1420 1421 # target 1422 if target is not None: 1423 if cname and (component is None or component == cname): 1424 component_id = target 1425 else: 1426 id = target 1427 1428 # method 1429 default_method = False 1430 if method is None: 1431 default_method = True 1432 method = self.method 1433 elif method == "": 1434 # Switch to list? (= method="" and no explicit target ID) 1435 if component_id is None: 1436 if self.component_id is not None: 1437 component_id = 0 1438 elif not self.component: 1439 if id is None: 1440 if self.id is not None: 1441 id = 0 1442 method = None 1443 1444 # id 1445 if id is None: 1446 id = self.id 1447 elif id in (0, ""): 1448 id = None 1449 elif id in ([], "[id]", "*"): 1450 id = "[id]" 1451 component_id = 0 1452 elif str(id) != str(self.id): 1453 component_id = 0 1454 1455 # component 1456 if component is None: 1457 component = cname 1458 elif component == "": 1459 component = None 1460 if cname and cname != component or not component: 1461 component_id = 0 1462 1463 # component_id 1464 if component_id is None: 1465 component_id = self.component_id 1466 elif component_id == 0: 1467 component_id = None 1468 if self.component_id and default_method: 1469 method = None 1470 1471 if id is None and self.id and \ 1472 (not component or not component_id) and default_method: 1473 method = None 1474 1475 if id: 1476 args.append(id) 1477 if component: 1478 args.append(component) 1479 if component_id: 1480 args.append(component_id) 1481 if method: 1482 args.append(method) 1483 1484 # representation 1485 if representation is None: 1486 representation = self.representation 1487 elif representation == "": 1488 representation = self.DEFAULT_REPRESENTATION 1489 f = self.function 1490 if not representation == self.DEFAULT_REPRESENTATION: 1491 if len(args) > 0: 1492 args[-1] = "%s.%s" % (args[-1], representation) 1493 else: 1494 f = "%s.%s" % (f, representation) 1495 1496 return URL(r=self, 1497 c=self.controller, 1498 f=f, 1499 args=args, 1500 vars=vars, 1501 host=host)
1502 1503 # -------------------------------------------------------------------------
1504 - def target(self):
1505 """ 1506 Get the target table of the current request 1507 1508 @return: a tuple of (prefix, name, table, tablename) of the target 1509 resource of this request 1510 1511 @todo: update for link table support 1512 """ 1513 1514 component = self.component 1515 if component is not None: 1516 link = self.component.link 1517 if link and not self.actuate_link(): 1518 return(link.prefix, 1519 link.name, 1520 link.table, 1521 link.tablename) 1522 return (component.prefix, 1523 component.name, 1524 component.table, 1525 component.tablename) 1526 else: 1527 return (self.prefix, 1528 self.name, 1529 self.table, 1530 self.tablename)
1531 1532 # -------------------------------------------------------------------------
1533 - def stylesheet(self, method=None, skip_error=False):
1534 """ 1535 Find the XSLT stylesheet for this request 1536 1537 @param method: "import" for data imports, else None 1538 @param skip_error: do not raise an HTTP error status 1539 if the stylesheet cannot be found 1540 """ 1541 1542 stylesheet = None 1543 representation = self.representation 1544 if self.component: 1545 resourcename = self.component.name 1546 else: 1547 resourcename = self.name 1548 1549 # Native S3XML? 1550 if representation == "xml": 1551 return stylesheet 1552 1553 # External stylesheet specified? 1554 if "transform" in self.vars: 1555 return self.vars["transform"] 1556 1557 # Stylesheet attached to the request? 1558 extension = self.XSLT_EXTENSION 1559 filename = "%s.%s" % (resourcename, extension) 1560 if filename in self.post_vars: 1561 p = self.post_vars[filename] 1562 import cgi 1563 if isinstance(p, cgi.FieldStorage) and p.filename: 1564 stylesheet = p.file 1565 return stylesheet 1566 1567 # Internal stylesheet? 1568 folder = self.folder 1569 path = self.XSLT_PATH 1570 if method != "import": 1571 method = "export" 1572 filename = "%s.%s" % (method, extension) 1573 stylesheet = os.path.join(folder, path, representation, filename) 1574 if not os.path.exists(stylesheet): 1575 if not skip_error: 1576 self.error(501, "%s: %s" % (current.ERROR.BAD_TEMPLATE, 1577 stylesheet)) 1578 else: 1579 stylesheet = None 1580 1581 return stylesheet
1582 1583 # -------------------------------------------------------------------------
1584 - def read_body(self):
1585 """ 1586 Read data from request body 1587 """ 1588 1589 self.files = Storage() 1590 content_type = self.env.get("content_type") 1591 1592 source = [] 1593 if content_type and content_type.startswith("multipart/"): 1594 import cgi 1595 ext = ".%s" % self.representation 1596 post_vars = self.post_vars 1597 for v in post_vars: 1598 p = post_vars[v] 1599 if isinstance(p, cgi.FieldStorage) and p.filename: 1600 self.files[p.filename] = p.file 1601 if p.filename.endswith(ext): 1602 source.append((v, p.file)) 1603 elif v.endswith(ext): 1604 if isinstance(p, cgi.FieldStorage): 1605 source.append((v, p.value)) 1606 elif isinstance(p, basestring): 1607 source.append((v, StringIO(p))) 1608 else: 1609 s = self.body 1610 s.seek(0) 1611 source.append(s) 1612 1613 return source
1614 1615 # -------------------------------------------------------------------------
1616 - def customise_resource(self, tablename=None):
1617 """ 1618 Invoke the customization callback for a resource. 1619 1620 @param tablename: the tablename of the resource; if called 1621 without tablename it will invoke the callbacks 1622 for the target resources of this request: 1623 - master 1624 - active component 1625 - active link table 1626 (in this order) 1627 1628 Resource customization functions can be defined like: 1629 1630 def customise_resource_my_table(r, tablename): 1631 1632 current.s3db.configure(tablename, 1633 my_custom_setting = "example") 1634 return 1635 1636 settings.customise_resource_my_table = \ 1637 customise_resource_my_table 1638 1639 @note: the hook itself can call r.customise_resource in order 1640 to cascade customizations as necessary 1641 @note: if a table is customised that is not currently loaded, 1642 then it will be loaded for this process 1643 """ 1644 1645 if tablename is None: 1646 customise = self.customise_resource 1647 1648 customise(self.resource.tablename) 1649 component = self.component 1650 if component: 1651 customise(component.tablename) 1652 link = self.link 1653 if link: 1654 customise(link.tablename) 1655 else: 1656 # Always load the model first (otherwise it would 1657 # override the custom settings when loaded later) 1658 db = current.db 1659 if tablename not in db: 1660 current.s3db.table(tablename) 1661 customise = current.deployment_settings.customise_resource(tablename) 1662 if customise: 1663 customise(self, tablename)
1664
1665 # ============================================================================= 1666 -class S3Method(object):
1667 """ 1668 REST Method Handler Base Class 1669 1670 Method handler classes should inherit from this class and 1671 implement the apply_method() method. 1672 1673 @note: instances of subclasses don't have any of the instance 1674 attributes available until they actually get invoked 1675 from a request - i.e. apply_method() should never be 1676 called directly. 1677 """ 1678 1679 # -------------------------------------------------------------------------
1680 - def __call__(self, r, method=None, widget_id=None, **attr):
1681 """ 1682 Entry point for the REST interface 1683 1684 @param r: the S3Request 1685 @param method: the method established by the REST interface 1686 @param widget_id: widget ID 1687 @param attr: dict of parameters for the method handler 1688 1689 @return: output object to send to the view 1690 """ 1691 1692 # Environment of the request 1693 self.request = r 1694 1695 # Settings 1696 response = current.response 1697 self.download_url = response.s3.download_url 1698 1699 # Init 1700 self.next = None 1701 1702 # Override request method 1703 if method is not None: 1704 self.method = method 1705 else: 1706 self.method = r.method 1707 1708 # Find the target resource and record 1709 if r.component: 1710 component = r.component 1711 resource = component 1712 self.record_id = self._record_id(r) 1713 if not self.method: 1714 if component.multiple and not r.component_id: 1715 self.method = "list" 1716 else: 1717 self.method = "read" 1718 if component.link: 1719 actuate_link = r.actuate_link() 1720 if not actuate_link: 1721 resource = component.link 1722 else: 1723 self.record_id = r.id 1724 resource = r.resource 1725 if not self.method: 1726 if r.id or r.method in ("read", "display"): 1727 self.method = "read" 1728 else: 1729 self.method = "list" 1730 1731 self.prefix = resource.prefix 1732 self.name = resource.name 1733 self.tablename = resource.tablename 1734 self.table = resource.table 1735 self.resource = resource 1736 1737 if self.method == "_init": 1738 return None 1739 1740 if r.interactive: 1741 # hide_filter policy: 1742 # 1743 # None show filters on master, 1744 # hide for components (default) 1745 # False show all filters (on all tabs) 1746 # True hide all filters (on all tabs) 1747 # 1748 # dict(alias=setting) setting per component, alias 1749 # None means master resource, 1750 # use special alias _default 1751 # to specify an alternative 1752 # default 1753 # 1754 hide_filter = attr.get("hide_filter") 1755 if isinstance(hide_filter, dict): 1756 component_name = r.component_name 1757 if component_name in hide_filter: 1758 hide_filter = hide_filter[component_name] 1759 elif "_default" in hide_filter: 1760 hide_filter = hide_filter["_default"] 1761 else: 1762 hide_filter = None 1763 if hide_filter is None: 1764 hide_filter = r.component is not None 1765 self.hide_filter = hide_filter 1766 else: 1767 self.hide_filter = True 1768 1769 # Apply method 1770 if widget_id and hasattr(self, "widget"): 1771 output = self.widget(r, 1772 method=self.method, 1773 widget_id=widget_id, 1774 **attr) 1775 else: 1776 output = self.apply_method(r, **attr) 1777 1778 # Redirection 1779 if self.next and resource.lastid: 1780 self.next = str(self.next) 1781 placeholder = "%5Bid%5D" 1782 self.next = self.next.replace(placeholder, resource.lastid) 1783 placeholder = "[id]" 1784 self.next = self.next.replace(placeholder, resource.lastid) 1785 if not response.error: 1786 r.next = self.next 1787 1788 # Add additional view variables (e.g. rheader) 1789 self._extend_view(output, r, **attr) 1790 1791 return output
1792 1793 # -------------------------------------------------------------------------
1794 - def apply_method(self, r, **attr):
1795 """ 1796 Stub, to be implemented in subclass. This method is used 1797 to get the results as a standalone page. 1798 1799 @param r: the S3Request 1800 @param attr: dictionary of parameters for the method handler 1801 1802 @return: output object to send to the view 1803 """ 1804 1805 output = dict() 1806 return output
1807 1808 # -------------------------------------------------------------------------
1809 - def widget(self, r, method=None, widget_id=None, visible=True, **attr):
1810 """ 1811 Stub, to be implemented in subclass. This method is used 1812 by other method handlers to embed this method as widget. 1813 1814 @note: 1815 1816 For "html" format, the widget method must return an XML 1817 component that can be embedded in a DIV. If a dict is 1818 returned, it will be rendered against the view template 1819 of the calling method - the view template selected by 1820 the widget method will be ignored. 1821 1822 For other formats, the data returned by the widget method 1823 will be rendered against the view template selected by 1824 the widget method. If no view template is set, the data 1825 will be returned as-is. 1826 1827 The widget must use the widget_id as HTML id for the element 1828 providing the Ajax-update hook and this element must be 1829 visible together with the widget. 1830 1831 The widget must include the widget_id as ?w=<widget_id> in 1832 the URL query of the Ajax-update call, and Ajax-calls should 1833 not use "html" format. 1834 1835 If visible==False, then the widget will initially be hidden, 1836 so it can be rendered empty and Ajax-load its data layer 1837 upon a separate refresh call. Otherwise, the widget should 1838 receive its data layer immediately. Widgets can ignore this 1839 parameter if delayed loading of the data layer is not 1840 all([possible, useful, supported]). 1841 1842 @param r: the S3Request 1843 @param method: the URL method 1844 @param widget_id: the widget ID 1845 @param visible: whether the widget is initially visible 1846 @param attr: dictionary of parameters for the method handler 1847 1848 @return: output 1849 """ 1850 1851 return None
1852 1853 # ------------------------------------------------------------------------- 1854 # Utility functions 1855 # -------------------------------------------------------------------------
1856 - def _permitted(self, method=None):
1857 """ 1858 Check permission for the requested resource 1859 1860 @param method: method to check, defaults to the actually 1861 requested method 1862 """ 1863 1864 auth = current.auth 1865 has_permission = auth.s3_has_permission 1866 1867 r = self.request 1868 1869 if not method: 1870 method = self.method 1871 if method in ("list", "datatable", "datalist"): 1872 # Rest handled in S3Permission.METHODS 1873 method = "read" 1874 1875 if r.component is None: 1876 table = r.table 1877 record_id = r.id 1878 else: 1879 table = r.component.table 1880 record_id = r.component_id 1881 1882 if method == "create": 1883 # Must have permission to update the master record 1884 # in order to create a new component record... 1885 master_access = has_permission("update", 1886 r.table, 1887 record_id=r.id) 1888 1889 if not master_access: 1890 return False 1891 1892 return has_permission(method, table, record_id=record_id)
1893 1894 # ------------------------------------------------------------------------- 1895 @staticmethod
1896 - def _record_id(r):
1897 """ 1898 Get the ID of the target record of a S3Request 1899 1900 @param r: the S3Request 1901 """ 1902 1903 master_id = r.id 1904 1905 if r.component: 1906 1907 component = r.component 1908 component_id = r.component_id 1909 link = r.link 1910 1911 if not component.multiple and not component_id: 1912 # Enforce first component record 1913 table = component.table 1914 pkey = table._id.name 1915 component.load(start=0, limit=1) 1916 if len(component): 1917 component_id = component.records().first()[pkey] 1918 if link and master_id: 1919 r.link_id = link.link_id(master_id, component_id) 1920 r.component_id = component_id 1921 component.add_filter(table._id == component_id) 1922 1923 if not link or r.actuate_link(): 1924 return component_id 1925 else: 1926 return r.link_id 1927 else: 1928 return master_id 1929 1930 return None
1931 1932 # -------------------------------------------------------------------------
1933 - def _config(self, key, default=None):
1934 """ 1935 Get a configuration setting of the current table 1936 1937 @param key: the setting key 1938 @param default: the default value 1939 """ 1940 1941 return current.s3db.get_config(self.tablename, key, default)
1942 1943 # ------------------------------------------------------------------------- 1944 @staticmethod
1945 - def _view(r, default):
1946 """ 1947 Get the path to the view template 1948 1949 @param r: the S3Request 1950 @param default: name of the default view template 1951 """ 1952 1953 folder = r.folder 1954 prefix = r.controller 1955 1956 exists = os.path.exists 1957 join = os.path.join 1958 1959 settings = current.deployment_settings 1960 theme = settings.get_theme() 1961 theme_layouts = settings.get_theme_layouts() 1962 1963 if theme != "default": 1964 # See if there is a Custom View for this Theme 1965 view = join(folder, "modules", "templates", theme_layouts, "views", 1966 "%s_%s_%s" % (prefix, r.name, default)) 1967 if exists(view): 1968 # There is a view specific to this page 1969 # NB This should normally include {{extend layout.html}} 1970 # Pass view as file not str to work in compiled mode 1971 return open(view, "rb") 1972 else: 1973 if "/" in default: 1974 subfolder, default_ = default.split("/", 1) 1975 else: 1976 subfolder = "" 1977 default_ = default 1978 if exists(join(folder, "modules", "templates", theme_layouts, "views", 1979 subfolder, "_%s" % default_)): 1980 # There is a general view for this page type 1981 # NB This should not include {{extend layout.html}} 1982 if subfolder: 1983 subfolder = "%s/" % subfolder 1984 # Pass this mapping to the View 1985 current.response.s3.views[default] = \ 1986 "../modules/templates/%s/views/%s_%s" % (theme_layouts, 1987 subfolder, 1988 default_, 1989 ) 1990 1991 if r.component: 1992 view = "%s_%s_%s" % (r.name, r.component_name, default) 1993 path = join(folder, "views", prefix, view) 1994 if exists(path): 1995 return "%s/%s" % (prefix, view) 1996 else: 1997 view = "%s_%s" % (r.name, default) 1998 path = join(folder, "views", prefix, view) 1999 else: 2000 view = "%s_%s" % (r.name, default) 2001 path = join(folder, "views", prefix, view) 2002 2003 if exists(path): 2004 return "%s/%s" % (prefix, view) 2005 else: 2006 return default
2007 2008 # ------------------------------------------------------------------------- 2009 @staticmethod
2010 - def _extend_view(output, r, **attr):
2011 """ 2012 Add additional view variables (invokes all callables) 2013 2014 @param output: the output dict 2015 @param r: the S3Request 2016 @param attr: the view variables (e.g. 'rheader') 2017 2018 @note: overload this method in subclasses if you don't want 2019 additional view variables to be added automatically 2020 """ 2021 2022 if r.interactive and isinstance(output, dict): 2023 for key in attr: 2024 handler = attr[key] 2025 if callable(handler): 2026 resolve = True 2027 try: 2028 display = handler(r) 2029 except TypeError: 2030 # Argument list failure 2031 # => pass callable to the view as-is 2032 display = handler 2033 continue 2034 except: 2035 # Propagate all other errors to the caller 2036 raise 2037 else: 2038 resolve = False 2039 display = handler 2040 if isinstance(display, dict) and resolve: 2041 output.update(**display) 2042 elif display is not None: 2043 output[key] = display 2044 elif key in output and callable(handler): 2045 del output[key]
2046 2047 # ------------------------------------------------------------------------- 2048 @staticmethod
2049 - def _remove_filters(get_vars):
2050 """ 2051 Remove all filters from URL vars 2052 2053 @param get_vars: the URL vars as dict 2054 """ 2055 2056 return Storage((k, v) for k, v in get_vars.iteritems() 2057 if not REGEX_FILTER.match(k))
2058 2059 # ------------------------------------------------------------------------- 2060 @staticmethod
2061 - def crud_string(tablename, name):
2062 """ 2063 Get a CRUD info string for interactive pages 2064 2065 @param tablename: the table name 2066 @param name: the name of the CRUD string 2067 """ 2068 2069 crud_strings = current.response.s3.crud_strings 2070 # CRUD strings for this table 2071 _crud_strings = crud_strings.get(tablename, crud_strings) 2072 return _crud_strings.get(name, 2073 # Default fallback 2074 crud_strings.get(name))
2075
2076 # ============================================================================= 2077 # Global functions 2078 # 2079 -def s3_request(*args, **kwargs):
2080 """ 2081 Helper function to generate S3Request instances 2082 2083 @param args: arguments for the S3Request 2084 @param kwargs: keyword arguments for the S3Request 2085 2086 @keyword catch_errors: if set to False, errors will be raised 2087 instead of returned to the client, useful 2088 for optional sub-requests, or if the caller 2089 implements fallbacks 2090 """ 2091 2092 error = None 2093 try: 2094 r = S3Request(*args, **kwargs) 2095 except (AttributeError, SyntaxError): 2096 if kwargs.get("catch_errors") is False: 2097 raise 2098 error = 400 2099 except KeyError: 2100 if kwargs.get("catch_errors") is False: 2101 raise 2102 error = 404 2103 if error: 2104 message = sys.exc_info()[1] 2105 if hasattr(message, "message"): 2106 message = message.message 2107 if current.auth.permission.format == "html": 2108 current.session.error = message 2109 redirect(URL(f="index")) 2110 else: 2111 headers = {"Content-Type":"application/json"} 2112 current.log.error(message) 2113 raise HTTP(error, 2114 body=current.xml.json_message(success=False, 2115 statuscode=error, 2116 message=message, 2117 ), 2118 web2py_error=message, 2119 **headers) 2120 return r
2121 2122 # END ========================================================================= 2123