Package vita :: Package modules :: Package s3 :: Module s3rest
[hide private]
[frames] | no frames]

Source Code for Module vita.modules.s3.s3rest

   1  # -*- coding: utf-8 -*- 
   2   
   3  """ RESTful API 
   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      @author: Dominic König <dominic[at]aidiq.com> 
  11   
  12      @copyright: 2009-2011 (c) Sahana Software Foundation 
  13      @license: MIT 
  14   
  15      Permission is hereby granted, free of charge, to any person 
  16      obtaining a copy of this software and associated documentation 
  17      files (the "Software"), to deal in the Software without 
  18      restriction, including without limitation the rights to use, 
  19      copy, modify, merge, publish, distribute, sublicense, and/or sell 
  20      copies of the Software, and to permit persons to whom the 
  21      Software is furnished to do so, subject to the following 
  22      conditions: 
  23   
  24      The above copyright notice and this permission notice shall be 
  25      included in all copies or substantial portions of the Software. 
  26   
  27      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
  28      EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
  29      OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
  30      NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
  31      HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
  32      WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
  33      FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 
  34      OTHER DEALINGS IN THE SOFTWARE. 
  35   
  36  """ 
  37   
  38  __all__ = ["S3Resource", "S3Request", "S3Method"] 
  39   
  40  import os, sys, cgi, uuid, datetime, time, urllib, StringIO, re 
  41  import gluon.contrib.simplejson as json 
  42   
  43  from gluon.storage import Storage 
  44  from gluon.sql import Row, Rows 
  45  from gluon.html import * 
  46  from gluon.http import HTTP, redirect 
  47  from gluon.sqlhtml import SQLTABLE, SQLFORM 
  48  from gluon.validators import IS_EMPTY_OR 
  49  from gluon.tools import callback 
  50   
  51  from lxml import etree 
  52  from s3tools import SQLTABLES3 
  53  from s3import import S3Importer 
54 55 # ***************************************************************************** 56 -class S3Resource(object):
57 """ 58 API for resources 59 60 """ 61
62 - def __init__(self, manager, prefix, name, 63 id=None, 64 uid=None, 65 filter=None, 66 vars=None, 67 parent=None, 68 components=None):
69 """ 70 Constructor 71 72 @param manager: the S3ResourceController 73 @param prefix: prefix of the resource name (=module name) 74 @param name: name of the resource (without prefix) 75 @param id: record ID (or list of record IDs) 76 @param uid: record UID (or list of record UIDs) 77 @param filter: filter query (DAL resources only) 78 @param vars: dictionary of URL query variables 79 @param parent: the parent resource 80 @param components: component name (or list of component names) 81 82 """ 83 84 self.manager = manager # S3ResourceController() defined in s3xrc.py 85 self.db = manager.db 86 87 self.HOOKS = manager.HOOKS # "s3" 88 self.ERROR = manager.ERROR 89 90 # Export/Import hooks 91 self.exporter = manager.exporter 92 self.importer = manager.importer 93 94 self.xml = manager.xml 95 96 # XSLT Paths 97 self.XSLT_PATH = "static/formats" 98 self.XSLT_EXTENSION = "xsl" 99 100 # Authorization hooks 101 self.permit = manager.permit 102 self.accessible_query = manager.auth.s3_accessible_query 103 104 # Audit hook 105 self.audit = manager.audit 106 107 # Basic properties 108 self.prefix = prefix 109 self.name = name 110 self.vars = None # set during build_query 111 112 # Model and Resource Manager 113 self.tablename = "%s_%s" % (self.prefix, self.name) 114 self.table = self.db.get(self.tablename, None) 115 if not self.table: 116 raise KeyError("Undefined table: %s" % self.tablename) 117 model = self.manager.model 118 119 # The Query 120 self.query_builder = manager.query_builder 121 self._query = None 122 self._multiple = True # multiple results expected by default 123 124 # The Set 125 self._rows = None 126 self._ids = [] 127 self._uids = [] 128 self._length = None 129 self._slice = False 130 131 # Request control 132 self.lastid = None 133 134 self.files = Storage() 135 136 # Attach components and build initial query 137 self.components = Storage() 138 self.parent = parent 139 140 if self.parent is None: 141 142 # Attach components as child resources 143 if components and not isinstance(components, (list, tuple)): 144 components = [components] 145 clist = model.get_components(self.prefix, self.name) 146 for i in xrange(len(clist)): 147 c, pkey, fkey = clist[i] 148 if components and c.name not in components: 149 continue 150 resource = S3Resource(self.manager, c.prefix, c.name, 151 parent=self) 152 self.components[c.name] = Storage(component=c, 153 pkey=pkey, 154 fkey=fkey, 155 resource=resource, 156 filter=None) 157 158 # Build query 159 self.build_query(id=id, uid=uid, filter=filter, vars=vars) 160 161 # Store CRUD and other method handlers 162 self.crud = self.manager.crud 163 # Get default search method for this resource 164 self.search = model.get_config(self.table, "search_method", None) 165 if not self.search: 166 if "name" in self.table: 167 T = self.manager.T 168 self.search = self.manager.search( 169 name="search_simple", 170 label=T("Name"), 171 comment=T("Enter a name to search for. You may use % as wildcard. Press 'Search' without input to list all items."), 172 field=["name"]) 173 else: 174 self.search = self.manager.search() 175 176 # Store internal handlers 177 self._handler = Storage(options=self.__get_options, 178 fields=self.__get_fields, 179 export_tree=self.__get_tree, 180 import_tree=self.__put_tree)
181 182 # Method handler configuration ============================================ 183
184 - def set_handler(self, method, handler):
185 """ 186 Set a REST method handler for this resource 187 188 @param method: the method name 189 @param handler: the handler function 190 @type handler: handler(S3Request, **attr) 191 192 """ 193 194 self._handler[method] = handler
195 196 197 # -------------------------------------------------------------------------
198 - def get_handler(self, method):
199 """ 200 Get a REST method handler for this resource 201 202 @param method: the method name 203 @returns: the handler function 204 205 """ 206 207 return self._handler.get(method, None)
208 209 210 # -------------------------------------------------------------------------
211 - def add_method(self, method, handler):
212 """ 213 Add a REST method for this resource 214 215 @param method: the method name 216 @param handler: the handler function 217 @type handler: handler(S3Request, **attr) 218 219 """ 220 221 model = self.manager.model 222 223 if self.parent: 224 model.set_method(self.parent.prefix, self.parent.name, 225 component=self.name, method=method, action=handler) 226 else: 227 model.set_method(self.prefix,self.name, 228 method=method, action=handler)
229 230 231 # Query handling ========================================================== 232
233 - def build_query(self, id=None, uid=None, filter=None, vars=None):
234 """ 235 Query builder 236 237 @param id: record ID or list of record IDs to include 238 @param uid: record UID or list of record UIDs to include 239 @param filter: filtering query (DAL only) 240 @param vars: dict of URL query variables 241 242 """ 243 244 # Reset the rows counter 245 self._length = None 246 247 # self.query_builder = manager.query_builder = S3QueryBuilder() defined in s3xrc.py 248 return self.query_builder.query(self, 249 id=id, 250 uid=uid, 251 filter=filter, 252 vars=vars)
253 254 255 # -------------------------------------------------------------------------
256 - def add_filter(self, filter=None):
257 """ 258 Extend the current query by a filter query 259 260 @param filter: a web2py Query object 261 262 """ 263 264 if filter is not None: 265 if self._query: 266 query = self._query 267 self.clear() 268 self.clear_query() 269 self._query = (query) & (filter) 270 else: 271 self.build_query(filter=filter) 272 return self._query
273 274 275 # -------------------------------------------------------------------------
276 - def add_component_filter(self, name, filter=None):
277 """ 278 Extend the filter query of a particular component 279 280 @param name: the name of the component 281 @param filter: a web2py Query object 282 283 """ 284 285 component = self.components.get(name, None) 286 if component is not None: 287 if component.filter is not None: 288 component.filter = (component.filter) & (filter) 289 else: 290 component.filter = filter
291 292 293 # -------------------------------------------------------------------------
294 - def get_query(self):
295 """ 296 Get the current query for this resource 297 298 """ 299 300 if not self._query: 301 self.build_query() 302 return self._query
303 304 305 # -------------------------------------------------------------------------
306 - def clear_query(self):
307 """ 308 Removes the current query (does not remove the set!) 309 310 """ 311 312 self._query = None 313 if self.components: 314 for c in self.components: 315 self.components[c].resource.clear_query()
316 317 318 # Data access ============================================================= 319
320 - def select(self, *fields, **attributes):
321 """ 322 Select records with the current query 323 324 @param fields: fields to select 325 @param attributes: select attributes 326 327 """ 328 329 table = self.table 330 if self._query is None: 331 self.build_query() 332 333 # Get the rows 334 rows = self.db(self._query).select(*fields, **attributes) 335 336 # Audit 337 audit = self.manager.audit 338 try: 339 # Audit "read" record by record 340 if self.tablename in rows: 341 ids = [r[str(self.table.id)] for r in rows] 342 else: 343 ids = [r.id for r in rows] 344 for i in ids: 345 audit("read", self.prefix, self.name, record=i) 346 except KeyError: 347 # Audit "list" if no IDs available 348 audit("list", self.prefix, self.name) 349 350 # Keep the rows for later access 351 self._rows = rows 352 353 return rows
354 355 356 # -------------------------------------------------------------------------
357 - def load(self, start=None, limit=None):
358 """ 359 Simplified syntax for select(): 360 - reads all fields 361 - start+limit instead of limitby 362 363 @param start: the index of the first record to load 364 @param limit: the maximum number of records to load 365 366 """ 367 368 if self._rows is not None: 369 self.clear() 370 371 if not self._query: 372 self.build_query() 373 if not self._multiple: 374 limitby = (0, 1) 375 else: 376 limitby = self.limitby(start=start, limit=limit) 377 378 if limitby: 379 rows = self.select(self.table.ALL, limitby=limitby) 380 else: 381 rows = self.select(self.table.ALL) 382 383 self._ids = [row.id for row in rows] 384 uid = self.manager.UID 385 if uid in self.table.fields: 386 self._uids = [row[uid] for row in rows] 387 388 return self
389 390 391 # -------------------------------------------------------------------------
392 - def insert(self, **fields):
393 """ 394 Insert records into this resource 395 396 @param fields: dict of fields to insert 397 398 """ 399 400 # Check permission 401 authorised = self.permit("create", self.tablename) 402 if not authorised: 403 raise IOError("Operation not permitted: INSERT INTO %s" % self.tablename) 404 405 # Insert new record 406 record_id = self.table.insert(**fields) 407 408 # Audit 409 if record_id: 410 record = Storage(fields).update(id=record_id) 411 self.audit("create", self.prefix, self.name, form=record) 412 413 return record_id
414 415 416 # -------------------------------------------------------------------------
417 - def delete(self, ondelete=None, format=None):
418 """ 419 Delete all (deletable) records in this resource 420 421 @param ondelete: on-delete callback 422 @param format: the representation format of the request (optional) 423 424 @returns: number of records deleted 425 426 """ 427 428 model = self.manager.model 429 430 settings = self.manager.s3.crud 431 archive_not_delete = settings.archive_not_delete 432 433 records = self.select(self.table.id) 434 435 numrows = 0 436 for row in records: 437 438 # Check permission to delete this row 439 if not self.permit("delete", self.table, record_id=row.id): 440 continue 441 442 # Clear session 443 if self.manager.get_session(prefix=self.prefix, name=self.name) == row.id: 444 self.manager.clear_session(prefix=self.prefix, name=self.name) 445 446 # Test row for deletability 447 try: 448 del self.table[row.id] 449 except: 450 self.manager.error = self.ERROR.INTEGRITY_ERROR 451 finally: 452 # We don't want to delete yet, so let's rollback 453 self.db.rollback() 454 455 if self.manager.error != self.ERROR.INTEGRITY_ERROR: 456 # Archive record? 457 if archive_not_delete and "deleted" in self.table: 458 fields = dict(deleted=True) 459 if "deleted_fk" in self.table: 460 # "Park" foreign keys to resolve constraints, 461 # "un-delete" will have to restore valid FKs from this field! 462 record = self.table[row.id] 463 fk = [] 464 for f in self.table.fields: 465 ftype = str(self.table[f].type) 466 if record[f] is not None and \ 467 (ftype[:9] == "reference" or \ 468 ftype[:14] == "list:reference"): 469 fk.append(dict(f=f, k=record[f])) 470 fields.update({f:None}) 471 else: 472 continue 473 if fk: 474 fields.update(deleted_fk=json.dumps(fk)) 475 self.db(self.table.id == row.id).update(**fields) 476 numrows += 1 477 self.audit("delete", self.prefix, self.name, 478 record=row.id, representation=format) 479 model.delete_super(self.table, row) 480 if ondelete: 481 callback(ondelete, row) 482 # otherwise: delete record 483 else: 484 del self.table[row.id] 485 numrows += 1 486 self.audit("delete", self.prefix, self.name, 487 record=row.id, representation=format) 488 model.delete_super(self.table, row) 489 if ondelete: 490 callback(ondelete, row) 491 492 return numrows
493 494 495 # -------------------------------------------------------------------------
496 - def update(self, **update_fields):
497 """ 498 Update all records in this resource 499 500 @todo: permission check 501 @todo: audit 502 503 @status: uncompleted 504 505 """ 506 507 if not self._query: 508 self.build_query() 509 510 success = self.db(self._query).update(**update_fields) 511 return success
512 513 514 # -------------------------------------------------------------------------
515 - def search_simple(self, fields=None, label=None, filterby=None):
516 """ 517 Simple fulltext search 518 519 @param fields: list of fields to search for the label 520 @param label: label to be found 521 @param filterby: filter query for results 522 523 """ 524 525 table = self.table 526 prefix = self.prefix 527 name = self.name 528 529 model = self.manager.model 530 531 mq = Storage() 532 search_fields = Storage() 533 534 if fields and not isinstance(fields, (list, tuple)): 535 fields = [fields] 536 elif not fields: 537 raise SyntaxError("No search fields specified.") 538 539 for f in fields: 540 _table = None 541 component = None 542 543 if f.find(".") != -1: 544 cname, f = f.split(".", 1) 545 component, pkey, fkey = model.get_component(prefix, name, cname) 546 if component: 547 _table = component.table 548 tablename = component.tablename 549 # Do not add queries for empty component tables 550 if not self.db(_table.id>0).select(_table.id, limitby=(0,1)).first(): 551 continue 552 else: 553 _table = table 554 tablename = table._tablename 555 556 if _table and tablename not in mq: 557 query = (self.accessible_query("read", _table)) 558 if "deleted" in _table.fields: 559 query = (query & (_table.deleted == "False")) 560 if component: 561 join = (table[pkey] == _table[fkey]) 562 query = (query & join) 563 mq[_table._tablename] = query 564 565 if _table and f in _table.fields: 566 if _table._tablename not in search_fields: 567 search_fields[tablename] = [_table[f]] 568 else: 569 search_fields[tablename].append(_table[f]) 570 571 if not search_fields: 572 return None 573 574 if label and isinstance(label,str): 575 labels = label.split() 576 results = [] 577 578 for l in labels: 579 wc = "%" 580 _l = "%s%s%s" % (wc, l.lower(), wc) 581 582 query = None 583 for tablename in search_fields: 584 hq = mq[tablename] 585 fq = None 586 fields = search_fields[tablename] 587 for f in fields: 588 if fq: 589 fq = (f.lower().like(_l)) | fq 590 else: 591 fq = (f.lower().like(_l)) 592 q = hq & fq 593 if query is None: 594 query = q 595 else: 596 query = query | q 597 598 if results: 599 query = (table.id.belongs(results)) & query 600 if filterby: 601 query = (filterby) & (query) 602 603 records = self.db(query).select(table.id) 604 results = [r.id for r in records] 605 if not results: 606 return None 607 608 return results 609 else: 610 return None
611 612 613 # -------------------------------------------------------------------------
614 - def count(self, left=None):
615 """ 616 Get the total number of available records in this resource 617 618 @param left: left joins, if required 619 620 """ 621 622 if not self._query: 623 self.build_query() 624 625 if self._length is None: 626 cnt = self.table[self.table.fields[0]].count() 627 row = self.db(self._query).select(cnt, left=left).first() 628 if row: 629 self._length = row[cnt] 630 631 return self._length
632 633 634 # -------------------------------------------------------------------------
635 - def clear(self):
636 """ 637 Removes the current set 638 639 """ 640 641 self._rows = None 642 self._length = None 643 self._ids = [] 644 self._uids = [] 645 self.files = Storage() 646 self._slice = False 647 648 if self.components: 649 for c in self.components: 650 self.components[c].resource.clear()
651 652 653 # -------------------------------------------------------------------------
654 - def records(self, fields=None):
655 """ 656 Get the current set 657 658 @returns: a Set or an empty list if no set is loaded 659 660 """ 661 662 if self._rows is None: 663 return Rows(self.db) 664 else: 665 if fields is not None: 666 self._rows.colnames = map(str, fields) 667 return self._rows
668 669 670 # -------------------------------------------------------------------------
671 - def __getitem__(self, key):
672 """ 673 Retrieves a record from the current set by its ID 674 675 @param key: the record ID 676 @returns: a Row 677 678 """ 679 680 if self._rows is None: 681 self.load() 682 for i in xrange(len(self._rows)): 683 row = self._rows[i] 684 if str(row.id) == str(key): 685 return row 686 687 raise IndexError
688 689 690 # -------------------------------------------------------------------------
691 - def __iter__(self):
692 """ 693 Iterate over the selected rows 694 695 """ 696 697 if self._rows is None: 698 self.load() 699 for i in xrange(len(self._rows)): 700 yield self._rows[i] 701 return
702 703 704 # -------------------------------------------------------------------------
705 - def __call__(self, key, component=None):
706 """ 707 Retrieves component records of a record in the current set 708 709 @param key: the record ID 710 @param component: the name of the component 711 (None to get the primary record) 712 @returns: a record (if component is None) or a list of records 713 714 """ 715 716 if not component: 717 return self[key] 718 else: 719 if isinstance(key, Row): 720 master = key 721 else: 722 master = self[key] 723 if component in self.components: 724 c = self.components[component] 725 r = c.resource 726 pkey, fkey = c.pkey, c.fkey 727 l = [record for record in r if master[pkey] == record[fkey]] 728 return l 729 else: 730 raise AttributeError
731 732 733 # -------------------------------------------------------------------------
734 - def get_id(self):
735 """ 736 Returns all IDs of the current set, or, if no set is loaded, 737 all IDs of the resource 738 739 @returns: a list of record IDs 740 741 """ 742 743 if not self._ids: 744 self.__load_ids() 745 if not self._ids: 746 return None 747 elif len(self._ids) == 1: 748 return self._ids[0] 749 else: 750 return self._ids
751 752 753 # -------------------------------------------------------------------------
754 - def get_uid(self):
755 """ 756 Returns all UIDs of the current set, or, if no set is loaded, 757 all UIDs of the resource 758 759 @returns: a list of record UIDs 760 761 """ 762 763 if self.manager.UID not in self.table.fields: 764 return None 765 766 if not self._uids: 767 self.__load_ids() 768 if not self._uids: 769 return None 770 elif len(self._uids) == 1: 771 return self._uids[0] 772 else: 773 return self._uids
774 775 776 # -------------------------------------------------------------------------
777 - def __load_ids(self):
778 """ 779 Loads the IDs of all records matching the master query, or, 780 if no query is given, all IDs in the primary table 781 782 """ 783 784 uid = self.manager.UID 785 786 if self._query is None: 787 self.build_query() 788 if uid in self.table.fields: 789 fields = (self.table.id, self.table[uid]) 790 else: 791 fields = (self.table.id,) 792 793 rows = self.db(self._query).select(*fields) 794 795 self._ids = [row.id for row in rows] 796 797 if uid in self.table.fields: 798 self._uids = [row[uid] for row in rows]
799 800 801 # Representation ========================================================== 802
803 - def __repr__(self):
804 """ 805 String representation of this resource 806 807 """ 808 809 if self._rows: 810 ids = [r.id for r in self] 811 return "<S3Resource %s %s>" % (self.tablename, ids) 812 else: 813 return "<S3Resource %s>" % self.tablename
814 815 816 # -------------------------------------------------------------------------
817 - def __len__(self):
818 """ 819 The number of currently loaded rows 820 821 """ 822 823 if self._rows is not None: 824 return len(self._rows) 825 else: 826 return 0
827 828 829 # -------------------------------------------------------------------------
830 - def __nonzero__(self):
831 """ 832 Boolean test of this resource 833 834 """ 835 836 return self is not None
837 838 839 # -------------------------------------------------------------------------
840 - def __contains__(self, item):
841 """ 842 Tests whether a record is currently loaded 843 844 """ 845 846 id = item.get("id", None) 847 uid = item.get(self.manager.UID, None) 848 849 if (id or uid) and not self._ids: 850 self.__load_ids() 851 if id and id in self._ids: 852 return 1 853 elif uid and uid in self._uids: 854 return 1 855 else: 856 return 0
857 858 859 # REST Interface ========================================================== 860
861 - def execute_request(self, r, **attr):
862 """ 863 Execute a HTTP request 864 865 @param r: the request to execute 866 @type r: S3Request 867 @param attr: attributes to pass to method handlers 868 869 """ 870 871 r.resource = self 872 r.next = None 873 hooks = r.response.get(self.HOOKS, None) # HOOKS = "s3" 874 bypass = False 875 output = None 876 preprocess = None 877 postprocess = None 878 879 # Enforce primary record ID 880 if not r.id and r.representation == "html": 881 if r.component or r.method in ("read", "update"): 882 count = self.count() 883 if self.vars is not None and count == 1: 884 self.load() 885 r.record = self._rows.first() 886 else: 887 model = self.manager.model 888 if self.search.interactive_search: 889 redirect(URL(r=r.request, f=self.name, args="search", 890 vars={"_next": r.same()})) 891 else: 892 r.session.error = self.ERROR.BAD_RECORD 893 redirect(URL(r=r.request, c=self.prefix, f=self.name)) 894 895 # Pre-process 896 if hooks is not None: 897 preprocess = hooks.get("prep", None) 898 if preprocess: 899 pre = preprocess(r) 900 if pre and isinstance(pre, dict): 901 bypass = pre.get("bypass", False) is True 902 output = pre.get("output", None) 903 if not bypass: 904 success = pre.get("success", True) 905 if not success: 906 if r.representation == "html" and output: 907 if isinstance(output, dict): 908 output.update(jr=r) 909 return output 910 else: 911 status = pre.get("status", 400) 912 message = pre.get("message", self.ERROR.BAD_REQUEST) 913 r.error(status, message) 914 elif not pre: 915 r.error(400, self.ERROR.BAD_REQUEST) 916 917 # Default view 918 if r.representation != "html": 919 r.response.view = "xml.html" 920 921 # Custom action? 922 if not r.custom_action: 923 model = self.manager.model 924 r.custom_action = model.get_method(r.prefix, r.name, 925 component_name=r.component_name, 926 method=r.method) 927 928 # Method handling 929 handler = None 930 if not bypass: 931 # Find the method handler 932 if r.method and r.custom_action: 933 handler = r.custom_action 934 elif r.http == "GET": 935 handler = self.__get(r) 936 elif r.http == "PUT": 937 handler = self.__put(r) 938 elif r.http == "POST": 939 handler = self.__post(r) 940 elif r.http == "DELETE": 941 handler = self.__delete(r) 942 else: 943 r.error(501, self.ERROR.BAD_METHOD) 944 # Invoke the method handler 945 if handler is not None: 946 output = handler(r, **attr) 947 elif r.method == "search": 948 output = self.search(r, **attr) 949 else: 950 # Fall back to CRUD 951 output = self.crud(r, **attr) 952 953 # Post-process 954 if hooks is not None: 955 postprocess = hooks.get("postp", None) 956 if postprocess is not None: 957 output = postprocess(r, output) 958 if output is not None and isinstance(output, dict): 959 # Put a copy of r into the output for the view to be able to make use of it 960 output.update(jr=r) 961 962 # Redirection 963 if r.next is not None and (r.http != "GET" or r.method == "clear"): 964 if isinstance(output, dict): 965 form = output.get("form", None) 966 if form: 967 if not hasattr(form, "errors"): 968 form = form[0] 969 if form.errors: 970 return output 971 r.session.flash = r.response.flash 972 r.session.confirmation = r.response.confirmation 973 r.session.error = r.response.error 974 r.session.warning = r.response.warning 975 redirect(r.next) 976 977 return output
978 979 980 # -------------------------------------------------------------------------
981 - def __get(self, r):
982 """ 983 Get the GET method handler 984 985 @param r: the S3Request 986 987 """ 988 989 method = r.method 990 model = self.manager.model 991 992 tablename = r.component and r.component.tablename or r.tablename 993 994 if method is None or method in ("read", "display"): 995 if self.__transformable(r): 996 method = "export_tree" 997 elif r.component: 998 if r.interactive and self.count() == 1: 999 # Load the record 1000 if not self._rows: 1001 self.load(start=0, limit=1) 1002 if self._rows: 1003 r.record = self._rows[0] 1004 r.id = self.get_id() 1005 r.uid = self.get_uid() 1006 if r.multiple and not r.component_id: 1007 method = "list" 1008 else: 1009 method = "read" 1010 else: 1011 if r.id or method in ("read", "display"): 1012 # Enforce single record 1013 if not self._rows: 1014 self.load(start=0, limit=1) 1015 if self._rows: 1016 r.record = self._rows[0] 1017 r.id = self.get_id() 1018 r.uid = self.get_uid() 1019 else: 1020 r.error(404, self.ERROR.BAD_RECORD) 1021 method = "read" 1022 else: 1023 method = "list" 1024 1025 elif method in ("create", "update"): 1026 if self.__transformable(r, method="import"): 1027 method = "import_tree" 1028 1029 elif method == "delete": 1030 return self.__delete(r) 1031 1032 elif method == "clear" and not r.component: 1033 self.manager.clear_session(self.prefix, self.name) 1034 if "_next" in r.request.vars: 1035 request_vars = dict(_next=r.request.vars._next) 1036 else: 1037 request_vars = {} 1038 if r.representation == "html" and self.search.interactive_search: 1039 r.next = URL(r=r.request, 1040 f=self.name, 1041 args="search", 1042 vars=request_vars) 1043 else: 1044 r.next = URL(r=r.request, f=self.name) 1045 return lambda r, **attr: None 1046 1047 return self.get_handler(method)
1048 1049 1050 # -------------------------------------------------------------------------
1051 - def __put(self, r):
1052 """ 1053 Get the PUT method handler 1054 1055 @param r: the S3Request 1056 1057 """ 1058 1059 if self.__transformable(r, method="import"): 1060 return self.get_handler("import_tree") 1061 else: 1062 r.error(501, self.ERROR.BAD_FORMAT)
1063 1064 1065 # -------------------------------------------------------------------------
1066 - def __post(self, r):
1067 """ 1068 Get the POST method handler 1069 1070 @param r: the S3Request 1071 1072 """ 1073 1074 method = r.method 1075 1076 if method == "delete": 1077 return self.__delete(r) 1078 else: 1079 if self.__transformable(r, method="import"): 1080 return self.__put(r) 1081 else: 1082 post_vars = r.request.post_vars 1083 table = r.target()[2] 1084 if "deleted" in table and \ 1085 "id" not in post_vars and "uuid" not in post_vars: 1086 original = self.manager.original(table, post_vars) 1087 if original and original.deleted: 1088 r.request.post_vars.update(id=original.id) 1089 r.request.vars.update(id=original.id) 1090 return self.__get(r)
1091 1092 1093 # -------------------------------------------------------------------------
1094 - def __delete(self, r):
1095 """ 1096 Get the DELETE method handler 1097 1098 @param r: the S3Request 1099 1100 """ 1101 1102 return self.get_handler("delete")
1103 1104 1105 # -------------------------------------------------------------------------
1106 - def __get_tree(self, r, **attr):
1107 """ 1108 Export this resource as XML 1109 1110 @param r: the request 1111 @param attr: request attributes 1112 1113 """ 1114 1115 json_formats = self.manager.json_formats 1116 content_type = self.manager.content_type 1117 1118 # Find XSLT stylesheet 1119 stylesheet = self.stylesheet(r, method="export") 1120 1121 # Slicing 1122 start = r.request.vars.get("start", None) 1123 if start is not None: 1124 try: 1125 start = int(start) 1126 except ValueError: 1127 start = None 1128 limit = r.request.vars.get("limit", None) 1129 if limit is not None: 1130 try: 1131 limit = int(limit) 1132 except ValueError: 1133 limit = None 1134 1135 # Default GIS marker 1136 marker = r.request.vars.get("marker", None) 1137 1138 # msince 1139 msince = r.request.vars.get("msince", None) 1140 if msince is not None: 1141 tfmt = "%Y-%m-%dT%H:%M:%SZ" 1142 try: 1143 (y,m,d,hh,mm,ss,t0,t1,t2) = time.strptime(msince, tfmt) 1144 msince = datetime.datetime(y,m,d,hh,mm,ss) 1145 except ValueError: 1146 msince = None 1147 1148 # Add stylesheet parameters 1149 args = Storage() 1150 if stylesheet is not None: 1151 if r.component: 1152 args.update(id=r.id, component=r.component.tablename) 1153 mode = r.request.vars.get("xsltmode", None) 1154 if mode is not None: 1155 args.update(mode=mode) 1156 1157 # Get the exporter, set response headers 1158 if r.representation in json_formats: 1159 as_json = True # convert the output into JSON 1160 r.response.headers["Content-Type"] = \ 1161 content_type.get(r.representation, "application/json") 1162 elif r.representation == "rss": 1163 as_json = False 1164 r.response.headers["Content-Type"] = \ 1165 content_type.get(r.representation, "application/rss+xml") 1166 else: 1167 as_json = False 1168 r.response.headers["Content-Type"] = \ 1169 content_type.get(r.representation, "text/xml") 1170 1171 # Export the resource 1172 exporter = self.exporter.xml 1173 output = exporter(self, 1174 stylesheet=stylesheet, 1175 as_json=as_json, 1176 start=start, 1177 limit=limit, 1178 marker=marker, 1179 msince=msince, 1180 show_urls=True, 1181 dereference=True, **args) 1182 1183 # Transformation error? 1184 if not output: 1185 r.error(400, "XSLT Transformation Error: %s " % self.xml.error) 1186 1187 return output
1188 1189 # -------------------------------------------------------------------------
1190 - def __get_options(self, r, **attr):
1191 """ 1192 Method handler to get field options in the current resource 1193 1194 @param r: the request 1195 @param attr: request attributes 1196 1197 """ 1198 1199 if "field" in r.request.get_vars: 1200 items = r.request.get_vars["field"] 1201 if not isinstance(items, (list, tuple)): 1202 items = [items] 1203 fields = [] 1204 for item in items: 1205 f = item.split(",") 1206 if f: 1207 fields.extend(f) 1208 else: 1209 fields = None 1210 1211 if r.representation == "xml": 1212 return self.options(component=r.component_name, 1213 fields=fields) 1214 elif r.representation == "s3json": 1215 return self.options(component=r.component_name, 1216 fields=fields, 1217 as_json=True) 1218 else: 1219 r.error(501, self.ERROR.BAD_FORMAT)
1220 1221 1222 # -------------------------------------------------------------------------
1223 - def __get_fields(self, r, **attr):
1224 """ 1225 Method handler to get all fields in the primary table 1226 1227 @param r: the request 1228 @param attr: the request attributes 1229 1230 """ 1231 1232 if r.representation == "xml": 1233 return self.fields(component=r.component_name) 1234 elif r.representation == "s3json": 1235 return self.fields(component=r.component_name, as_json=True) 1236 else: 1237 r.error(501, self.ERROR.BAD_FORMAT)
1238 1239 1240 # -------------------------------------------------------------------------
1241 - def __read_body(self, r):
1242 """ 1243 Read data from request body 1244 1245 @param r: the S3Request 1246 1247 """ 1248 1249 self.files = Storage() 1250 content_type = r.request.env.get("content_type", None) 1251 1252 source = [] 1253 if content_type and content_type.startswith("multipart/"): 1254 ext = ".%s" % r.representation 1255 vars = r.request.post_vars 1256 for v in vars: 1257 p = vars[v] 1258 if isinstance(p, cgi.FieldStorage) and p.filename: 1259 self.files[p.filename] = p.file 1260 if p.filename.endswith(ext): 1261 source.append(tuple(v, p.file)) 1262 elif v.endswith(ext): 1263 if isinstance(p, cgi.FieldStorage): 1264 source.append(tuple(v, p.value)) 1265 elif isinstance(p, basestring): 1266 source.append(tuple(v, StringIO.StringIO(p))) 1267 else: 1268 s = r.request.body 1269 s.seek(0) 1270 source.append(s) 1271 1272 return source
1273 1274 1275 # -------------------------------------------------------------------------
1276 - def __put_tree(self, r, **attr):
1277 """ 1278 Import data using the XML Importer (transformable XML/JSON/CSV formats) 1279 1280 @param r: the S3Request 1281 @param attr: the request attributes 1282 1283 """ 1284 1285 xml = self.xml 1286 vars = Storage(r.request.vars) 1287 auth = self.manager.auth 1288 1289 # Find all source names in the URL vars 1290 def findnames(vars, name): 1291 nlist = [] 1292 if name in vars: 1293 names = vars[name] 1294 if isinstance(names, (list, tuple)): 1295 names = ",".join(names) 1296 names = names.split(",") 1297 for n in names: 1298 if n[0] == "(" and ")" in n[1:]: 1299 nlist.append(n[1:].split(")", 1)) 1300 else: 1301 nlist.append([None, n]) 1302 return nlist
1303 1304 filenames = findnames(vars, "filename") 1305 fetchurls = findnames(vars, "fetchurl") 1306 1307 # Generate sources list 1308 json_formats = self.manager.json_formats 1309 csv_formats = self.manager.csv_formats 1310 1311 # Get the source 1312 source = [] 1313 format = r.representation 1314 if format in json_formats or \ 1315 format in csv_formats: 1316 if filenames: 1317 try: 1318 for f in filenames: 1319 source.append((f[0], open(f[1], "rb"))) 1320 except: 1321 source = [] 1322 elif fetchurls: 1323 import urllib 1324 try: 1325 for u in fetchurls: 1326 source.append((u[0], urllib.urlopen(u[1]))) 1327 except: 1328 source = [] 1329 elif r.http != "GET": 1330 source = self.__read_body(r) 1331 else: 1332 if filenames: 1333 source = filenames 1334 elif fetchurls: 1335 source = fetchurls 1336 elif r.http != "GET": 1337 source = self.__read_body(r) 1338 1339 if not source: 1340 if filenames or fetchurls: 1341 # Error: no source found 1342 r.error(400, "Invalid source") 1343 else: 1344 # GET/create without source: return the resource structure 1345 opts = vars.get("options", False) 1346 refs = vars.get("references", False) 1347 stylesheet = self.stylesheet(r) 1348 if format in json_formats: 1349 as_json = True 1350 else: 1351 as_json = False 1352 output = self.struct(options=opts, 1353 references=refs, 1354 stylesheet=stylesheet, 1355 as_json=as_json) 1356 if output is None: 1357 # Transformation error 1358 r.error(400, self.xml.error) 1359 return output 1360 1361 # Find XSLT stylesheet 1362 stylesheet = self.stylesheet(r, method="import") 1363 1364 # Target IDs 1365 if r.method == "create": 1366 id = None 1367 else: 1368 id = r.id 1369 1370 # Skip invalid records? 1371 if "ignore_errors" in r.request.vars: 1372 ignore_errors = True 1373 else: 1374 ignore_errors = False 1375 1376 # Transformation mode? 1377 if "xsltmode" in r.request.vars: 1378 args = dict(mode=request.vars["xsltmode"]) 1379 else: 1380 args = dict() 1381 1382 # Format type? 1383 if format in json_formats: 1384 format = "json" 1385 elif format in csv_formats: 1386 format = "csv" 1387 else: 1388 format = "xml" 1389 1390 # Import! 1391 importer = S3Importer(self.manager) 1392 try: 1393 return importer.xml(self, source, 1394 id=id, 1395 format=format, 1396 stylesheet=stylesheet, 1397 ignore_errors=ignore_errors, 1398 **args) 1399 except IOError: 1400 auth.permission.fail() 1401 except SyntaxError: 1402 e = sys.exc_info()[1] 1403 r.error(400, e)
1404 1405 1406 # XML functions =========================================================== 1407
1408 - def export_xml(self, 1409 stylesheet=None, 1410 as_json=False, 1411 pretty_print=False, **args):
1412 """ 1413 Export this resource as XML 1414 1415 @param stylesheet: path to the XSLT stylesheet (if not native S3-XML) 1416 @param as_json: convert the output into JSON 1417 @param pretty_print: insert newlines/indentation in the output 1418 @param args: arguments to pass to the XSLT stylesheet 1419 @returns: the XML as string 1420 1421 """ 1422 1423 exporter = self.exporter.xml 1424 1425 return exporter(self, 1426 stylesheet=stylesheet, 1427 as_json=as_json, 1428 pretty_print=pretty_print, **args)
1429 1430 1431 # -------------------------------------------------------------------------
1432 - def import_xml(self, source, 1433 files=None, 1434 id=None, 1435 stylesheet=None, 1436 as_json=False, 1437 ignore_errors=False, **args):
1438 """ 1439 Import data from an XML source into this resource 1440 1441 @param source: the XML source (or ElementTree) 1442 @param files: file attachments as {filename:file} 1443 @param id: the ID or list of IDs of records to update (None for all) 1444 @param stylesheet: the XSLT stylesheet 1445 @param as_json: the source is JSONified XML 1446 @param ignore_errors: do not stop on errors (skip invalid elements) 1447 @param args: arguments to pass to the XSLT stylesheet 1448 @returns: a JSON message as string 1449 1450 @raise SyntaxError: in case of a parser or transformation error 1451 @raise IOError: at insufficient permissions 1452 1453 @todo 2.3: deprecate? 1454 1455 """ 1456 1457 1458 importer = self.importer.xml 1459 1460 return importer(self, source, 1461 files=files, 1462 id=id, 1463 stylesheet=stylesheet, 1464 as_json=as_json, 1465 ignore_errors=ignore_errors, **args)
1466 1467 1468 # -------------------------------------------------------------------------
1469 - def push(self, url, 1470 stylesheet=None, 1471 as_json=False, 1472 xsltmode=None, 1473 start=None, 1474 limit=None, 1475 marker=None, 1476 msince=None, 1477 show_urls=True, 1478 dereference=True, 1479 content_type=None, 1480 username=None, 1481 password=None, 1482 proxy=None):
1483 """ 1484 Push (=POST) the current resource to a target URL 1485 1486 @param url: the URL to push to 1487 @param stylesheet: path to the XSLT stylesheet to be used by the exporter 1488 @param as_json: convert the output into JSON before push 1489 @param xsltmode: "mode" parameter for the XSLT stylesheet 1490 @param start: index of the first record to export (slicing) 1491 @param limit: maximum number of records to export (slicing) 1492 @param marker: default map marker URL 1493 @param msince: export only records which have been modified after 1494 this datetime 1495 @param show_urls: show URLs in the <resource> elements 1496 @param dereference: include referenced resources in the export 1497 @param content_type: content type specification for the export 1498 @param username: username to authenticate at the peer site 1499 @param password: password to authenticate at the peer site 1500 @param proxy: URL of the proxy server to use 1501 1502 @todo 2.3: error handling? 1503 1504 """ 1505 1506 args = Storage() 1507 if stylesheet and xsltmode: 1508 args.update(mode=xsltmode) 1509 1510 # Use the exporter to produce the XML 1511 exporter = self.exporter.xml 1512 data = exporter(start=start, 1513 limit=limit, 1514 marker=marker, 1515 msince=msince, 1516 show_urls=show_urls, 1517 dereference=dereference, 1518 stylesheet=stylesheet, 1519 as_json=as_json, 1520 pretty_print=False, 1521 **args) 1522 1523 if data: 1524 # Find the protocol 1525 url_split = url.split("://", 1) 1526 if len(url_split) == 2: 1527 protocol, path = url_split 1528 else: 1529 protocol, path = "http", None 1530 1531 # Generate the request 1532 import urllib2 1533 req = urllib2.Request(url=url, data=data) 1534 if content_type: 1535 req.add_header('Content-Type', content_type) 1536 1537 handlers = [] 1538 1539 # Proxy handling 1540 if proxy: 1541 proxy_handler = urllib2.ProxyHandler({protocol:proxy}) 1542 handlers.append(proxy_handler) 1543 1544 # Authentication handling 1545 if username and password: 1546 # Send auth data unsolicitedly (the only way with Eden instances): 1547 import base64 1548 base64string = base64.encodestring('%s:%s' % (username, password))[:-1] 1549 req.add_header("Authorization", "Basic %s" % base64string) 1550 # Just in case the peer does not accept that, add a 401 handler: 1551 passwd_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() 1552 passwd_manager.add_password(realm=None, 1553 uri=url, 1554 user=username, 1555 passwd=password) 1556 auth_handler = urllib2.HTTPBasicAuthHandler(passwd_manager) 1557 handlers.append(auth_handler) 1558 1559 # Install all handlers 1560 if handlers: 1561 opener = urllib2.build_opener(*handlers) 1562 urllib2.install_opener(opener) 1563 1564 # Execute the request 1565 try: 1566 f = urllib2.urlopen(req) 1567 except urllib2.HTTPError, e: 1568 # Peer error => encode as JSON message 1569 code = e.code 1570 message = e.read() 1571 try: 1572 # Sahana-Eden would send a JSON message, 1573 # try to extract the actual error message: 1574 message_json = json.loads(message) 1575 message = message_json.get("message", message) 1576 except: 1577 pass 1578 # @todo: prefix message as peer error? 1579 return xml.json_message(False, code, message) 1580 else: 1581 # Success => return what the peer returns 1582 response = f.read() 1583 return response 1584 else: 1585 # No data to send 1586 return None
1587 1588 1589 # -------------------------------------------------------------------------
1590 - def fetch(self, url, 1591 username=None, 1592 password=None, 1593 proxy=None, 1594 as_json=False, 1595 stylesheet=None, 1596 ignore_errors=False, **args):
1597 """ 1598 Fetch XML data from a remote URL into the current resource 1599 1600 @param url: the peer URL 1601 @param username: username to authenticate at the peer site 1602 @param password: password to authenticate at the peer site 1603 @param proxy: URL of the proxy server to use 1604 @param stylesheet: path to the XSLT stylesheet to transform the data 1605 @param as_json: source is JSONified XML 1606 @param ignore_errors: skip invalid records 1607 1608 """ 1609 1610 xml = self.xml 1611 1612 response = None 1613 url_split = url.split("://", 1) 1614 if len(url_split) == 2: 1615 protocol, path = url_split 1616 else: 1617 protocol, path = "http", None 1618 1619 # Prepare the request 1620 import urllib2 1621 req = urllib2.Request(url=url) 1622 handlers = [] 1623 1624 # Proxy handling 1625 if proxy: 1626 proxy_handler = urllib2.ProxyHandler({protocol:proxy}) 1627 handlers.append(proxy_handler) 1628 1629 # Authentication handling 1630 if username and password: 1631 # Send auth data unsolicitedly (the only way with Eden instances): 1632 import base64 1633 base64string = base64.encodestring('%s:%s' % (username, password))[:-1] 1634 req.add_header("Authorization", "Basic %s" % base64string) 1635 # Just in case the peer does not accept that, add a 401 handler: 1636 passwd_manager = urllib2.HTTPPasswordMgrWithDefaultRealm() 1637 passwd_manager.add_password(realm=None, 1638 uri=url, 1639 user=username, 1640 passwd=password) 1641 auth_handler = urllib2.HTTPBasicAuthHandler(passwd_manager) 1642 handlers.append(auth_handler) 1643 1644 # Install all handlers 1645 if handlers: 1646 opener = urllib2.build_opener(*handlers) 1647 urllib2.install_opener(opener) 1648 1649 # Execute the request 1650 try: 1651 f = urllib2.urlopen(req) 1652 except urllib2.HTTPError, e: 1653 # Peer error 1654 code = e.code 1655 message = e.read() 1656 try: 1657 # Sahana-Eden would send a JSON message, 1658 # try to extract the actual error message: 1659 message_json = json.loads(message) 1660 message = message_json.get("message", message) 1661 except: 1662 pass 1663 1664 # Prefix as peer error and strip XML markup from the message 1665 # @todo: better method to do this? 1666 message = "<message>PEER ERROR: %s</message>" % message 1667 try: 1668 markup = etree.XML(message) 1669 message = markup.xpath(".//text()") 1670 if message: 1671 message = " ".join(message) 1672 else: 1673 message = "" 1674 except etree.XMLSyntaxError: 1675 pass 1676 1677 # Encode as JSON message 1678 return xml.json_message(False, code, message, tree=None) 1679 else: 1680 # Successfully downloaded 1681 response = f 1682 1683 # Try to import the response 1684 try: 1685 success = self.import_xml(response, 1686 stylesheet=stylesheet, 1687 as_json=as_json, 1688 ignore_errors=ignore_errors, 1689 args=args) 1690 except IOError, e: 1691 return xml.json_message(False, 400, "LOCAL ERROR: %s" % e) 1692 if not success: 1693 error = self.manager.error 1694 return xml.json_message(False, 400, "LOCAL ERROR: %s" % error) 1695 else: 1696 return xml.json_message() # success
1697 1698 1699 # -------------------------------------------------------------------------
1700 - def options(self, component=None, fields=None, as_json=False):
1701 """ 1702 Export field options of this resource as element tree 1703 1704 @param component: name of the component which the options are 1705 requested of, None for the primary table 1706 @param fields: list of names of fields for which the options 1707 are requested, None for all fields (which have options) 1708 @param as_json: convert the output into JSON 1709 1710 """ 1711 1712 if component is not None: 1713 c = self.components.get(component, None) 1714 if c: 1715 tree = c.resource.options(fields=fields, as_json=as_json) 1716 return tree 1717 else: 1718 raise AttributeError 1719 else: 1720 tree = self.xml.get_options(self.prefix, 1721 self.name, 1722 fields=fields) 1723 tree = etree.ElementTree(tree) 1724 1725 if as_json: 1726 return self.xml.tree2json(tree, pretty_print=True) 1727 else: 1728 return self.xml.tostring(tree, pretty_print=True)
1729 1730 1731 # -------------------------------------------------------------------------
1732 - def fields(self, component=None, as_json=False):
1733 """ 1734 Export a list of fields in the resource as element tree 1735 1736 @param component: name of the component to lookup the fields 1737 (None for primary table) 1738 @param as_json: convert the output XML into JSON 1739 1740 """ 1741 1742 if component is not None: 1743 c = self.components.get(component, None) 1744 if c: 1745 tree = c.resource.fields() 1746 return tree 1747 else: 1748 raise AttributeError 1749 else: 1750 tree = self.xml.get_fields(self.prefix, self.name) 1751 1752 if as_json: 1753 return self.xml.tree2json(tree, pretty_print=True) 1754 else: 1755 return self.xml.tostring(tree, pretty_print=True)
1756 1757 1758 # -------------------------------------------------------------------------
1759 - def struct(self, 1760 options=False, 1761 references=False, 1762 stylesheet=None, 1763 as_json=False, 1764 as_tree=False):
1765 """ 1766 Get the structure of the resource 1767 1768 @param options: include option lists in option fields 1769 @param references: include option lists even for reference fields 1770 @param stylesheet: the stylesheet to use for transformation 1771 @param as_json: convert into JSON after transformation 1772 1773 """ 1774 1775 # Get the structure of the main resource 1776 root = etree.Element(self.xml.TAG.root) 1777 main = self.xml.get_struct(self.prefix, self.name, 1778 parent=root, 1779 options=options, 1780 references=references) 1781 1782 # Include the selected components 1783 for c in self.components.values(): 1784 component = c.resource 1785 prefix = component.prefix 1786 name = component.name 1787 sub = self.xml.get_struct(prefix, name, 1788 parent=main, 1789 options=options, 1790 references=references) 1791 1792 # Transformation 1793 tree = etree.ElementTree(root) 1794 if stylesheet is not None: 1795 tfmt = self.xml.ISOFORMAT 1796 args = dict(domain=self.manager.domain, 1797 base_url=self.manager.s3.base_url, 1798 prefix=self.prefix, 1799 name=self.name, 1800 utcnow=datetime.datetime.utcnow().strftime(tfmt)) 1801 1802 tree = self.xml.transform(tree, stylesheet, **args) 1803 if tree is None: 1804 return None 1805 1806 # Return tree if requested 1807 if as_tree: 1808 return tree 1809 1810 # Otherwise string-ify it 1811 if as_json: 1812 return self.xml.tree2json(tree, pretty_print=True) 1813 else: 1814 return self.xml.tostring(tree, pretty_print=True)
1815 1816 1817 # Utilities =============================================================== 1818
1819 - def readable_fields(self, subset=None):
1820 """ 1821 Get a list of all readable fields in the resource table 1822 1823 @param subset: list of fieldnames to limit the selection to 1824 1825 """ 1826 1827 fkey = None 1828 table = self.table 1829 1830 if self.parent: 1831 component = self.parent.components.get(self.name, None) 1832 if component: 1833 fkey = component.fkey 1834 1835 if subset: 1836 return [table[f] for f in subset 1837 if f in table.fields and table[f].readable and f != fkey] 1838 else: 1839 return [table[f] for f in table.fields 1840 if table[f].readable and f != fkey]
1841 1842 1843 # -------------------------------------------------------------------------
1844 - def limitby(self, start=None, limit=None):
1845 """ 1846 Convert start+limit parameters into a limitby tuple 1847 - limit without start => start = 0 1848 - start without limit => limit = ROWSPERPAGE 1849 - limit 0 (or less) => limit = 1 1850 - start less than 0 => start = 0 1851 1852 @param start: index of the first record to select 1853 @param limit: maximum number of records to select 1854 1855 """ 1856 1857 if start is None: 1858 if not limit: 1859 return None 1860 else: 1861 start = 0 1862 1863 if not limit: 1864 limit = self.manager.ROWSPERPAGE 1865 if limit is None: 1866 return None 1867 1868 if limit <= 0: 1869 limit = 1 1870 if start < 0: 1871 start = 0 1872 1873 return (start, start + limit)
1874 1875 1876 # -------------------------------------------------------------------------
1877 - def url(self, 1878 id=None, 1879 uid=None, 1880 prefix=None, 1881 format="html", 1882 method=None, 1883 vars=None):
1884 """ 1885 URL of this resource 1886 1887 @param id: record ID or list of record IDs 1888 @param uid: record UID or list of record UIDs (ignored if id is specified) 1889 @param prefix: override current controller prefix 1890 @param format: representation format 1891 @param method: URL method 1892 @param vars: override current URL query 1893 1894 """ 1895 1896 r = self.manager.request 1897 v = r.get_vars 1898 p = prefix or r.controller 1899 n = self.name 1900 x = n 1901 c = None 1902 args = [] 1903 vars = vars or v or Storage() 1904 1905 if self.parent: 1906 n = self.parent.name 1907 c = self.name 1908 if c: 1909 x = c 1910 args.append(c) 1911 if id is not None and not id: 1912 if not self._multiple: 1913 ids = self.get_id() 1914 if not isinstance(ids, (list, tuple)): 1915 args.append(str(ids)) 1916 elif self.parent: 1917 ids = self.parent.get_id() 1918 if not isinstance(ids, (list, tuple)): 1919 args.insert(0, str(ids)) 1920 elif id: 1921 if not isinstance(id, (list, tuple)): 1922 id = [id] 1923 if len(id) > 1: 1924 vars["%s.id" % x] = ",".join(map(str, id)) 1925 else: 1926 args.append(str(id[0])) 1927 elif uid: 1928 if isinstance(uid, (list, tuple)): 1929 uids = ",".join(map(str, uid)) 1930 else: 1931 uids = str(uid) 1932 vars["%s.uid" % x] = uids 1933 if method: 1934 args.append(method) 1935 if format != "html": 1936 if args: 1937 args[-1] = "%s.%s" % (args[-1], format) 1938 else: 1939 n = "%s.%s" % (n, format) 1940 url = URL(r=r, c=p, f=n, args=args, vars=vars, extension="") 1941 return url
1942 1943 1944 # -------------------------------------------------------------------------
1945 - def __transformable(self, r, method=None):
1946 """ 1947 Check the request for a transformable format 1948 1949 @param r: the S3Request 1950 @param method: "import" for import methods, else None 1951 1952 """ 1953 1954 format = r.representation 1955 stylesheet = self.stylesheet(r, method=method, skip_error=True) 1956 1957 if format != "xml" and not stylesheet: 1958 return False 1959 else: 1960 return True
1961 1962 1963 # -------------------------------------------------------------------------
1964 - def stylesheet(self, r, method=None, skip_error=False):
1965 """ 1966 Find the XSLT stylesheet for a request 1967 1968 @param r: the S3Request 1969 @param method: "import" for data imports, else None 1970 1971 """ 1972 1973 stylesheet = None 1974 format = r.representation 1975 request = r.request 1976 if r.component: 1977 resourcename = r.component.name 1978 else: 1979 resourcename = r.name 1980 1981 # Native S3XML? 1982 if format == "xml": 1983 return stylesheet 1984 1985 # External stylesheet specified? 1986 if "transform" in request.vars: 1987 return request.vars["transform"] 1988 1989 # Stylesheet attached to the request? 1990 extension = self.XSLT_EXTENSION 1991 filename = "%s.%s" % (resourcename, extension) 1992 if filename in request.post_vars: 1993 p = request.post_vars[filename] 1994 if isinstance(p, cgi.FieldStorage) and p.filename: 1995 stylesheet = p.file 1996 return stylesheet 1997 1998 # Internal stylesheet? 1999 folder = request.folder 2000 path = self.XSLT_PATH 2001 if method != "import": 2002 method = "export" 2003 filename = "%s.%s" % (method, extension) 2004 stylesheet = os.path.join(folder, path, format, filename) 2005 if not os.path.exists(stylesheet): 2006 if not skip_error: 2007 r.error(501, "%s: %s" % (self.ERROR.BAD_TEMPLATE, stylesheet)) 2008 else: 2009 stylesheet = None 2010 2011 return stylesheet
2012
2013 2014 # ***************************************************************************** 2015 -class S3Request(object):
2016 """ 2017 Class to handle HTTP requests 2018 2019 @todo: integrate into S3Resource 2020 2021 """ 2022 2023 UNAUTHORISED = "Not Authorised" # @todo: internationalization 2024 2025 DEFAULT_REPRESENTATION = "html" 2026 INTERACTIVE_FORMATS = ("html", "popup", "iframe") # @todo: read from settings 2027 2028 # -------------------------------------------------------------------------
2029 - def __init__(self, manager, prefix, name):
2030 """ 2031 Constructor 2032 2033 @param manager: the S3ResourceController 2034 @param prefix: prefix of the resource name (=module name) 2035 @param name: name of the resource (=without prefix) 2036 2037 """ 2038 2039 self.manager = manager # S3ResourceController() defined in s3xrc.py 2040 2041 # Get the environment 2042 self.session = manager.session or Storage() 2043 self.request = manager.request 2044 self.response = manager.response 2045 2046 # Main resource parameters 2047 self.prefix = prefix or self.request.controller 2048 self.name = name or self.request.function 2049 2050 # Parse the request 2051 self.http = self.request.env.request_method 2052 self.__parse() 2053 2054 self.custom_action = None 2055 2056 # Interactive representation format? 2057 self.interactive = self.representation in self.INTERACTIVE_FORMATS 2058 2059 # Append component ID to the URL query 2060 vars = Storage(self.request.get_vars) 2061 if self.component_name and self.component_id: 2062 varname = "%s.id" % self.component_name 2063 if varname in vars: 2064 var = vars[varname] 2065 if not isinstance(var, (list, tuple)): 2066 var = [var] 2067 var.append(self.component_id) 2068 vars[varname] = var 2069 else: 2070 vars[varname] = self.component_id 2071 2072 # Define the target resource 2073 self.resource = manager.define_resource(self.prefix, self.name, 2074 id=self.id, 2075 filter=self.response[manager.HOOKS].filter, # manager.HOOKS="s3" 2076 vars=vars, 2077 components=self.component_name) 2078 2079 self.tablename = self.resource.tablename 2080 self.table = self.resource.table 2081 2082 # Check for component 2083 self.component = None 2084 self.pkey = None 2085 self.fkey = None 2086 self.multiple = True 2087 if self.component_name: 2088 c = self.resource.components.get(self.component_name, None) 2089 if c: 2090 self.component = c.component 2091 self.pkey, self.fkey = c.pkey, c.fkey 2092 self.multiple = self.component.multiple 2093 else: 2094 manager.error = "%s not a component of %s" % ( 2095 self.component_name, 2096 self.resource.tablename) 2097 raise SyntaxError(manager.error) 2098 2099 # Find primary record 2100 uid = self.request.vars.get("%s.uid" % self.name, None) 2101 if self.component_name: 2102 cuid = self.request.vars.get("%s.uid" % self.component_name, None) 2103 else: 2104 cuid = None 2105 2106 # Try to load primary record, if expected 2107 self.record = None 2108 if self.id or self.component_id or \ 2109 uid and not isinstance(uid, (list, tuple)) or \ 2110 cuid and not isinstance(cuid, (list, tuple)): 2111 # Single record expected 2112 self.resource.load() 2113 if len(self.resource) == 1: 2114 self.record = self.resource.records().first() 2115 self.id = self.record.id 2116 self.manager.store_session(self.resource.prefix, 2117 self.resource.name, 2118 self.id) 2119 else: 2120 manager.error = self.manager.ERROR.BAD_RECORD 2121 if self.representation == "html": 2122 self.session.error = manager.error 2123 self.component = None # => avoid infinite loop 2124 redirect(self.there()) 2125 else: 2126 raise KeyError(manager.error)
2127 2128 2129 # -------------------------------------------------------------------------
2130 - def unauthorised(self):
2131 """ 2132 Action upon unauthorised request 2133 2134 """ 2135 2136 auth = self.manager.auth 2137 auth.permission.fail()
2138 2139 2140 # -------------------------------------------------------------------------
2141 - def error(self, status, message, tree=None, next=None):
2142 """ 2143 Action upon error 2144 2145 @param status: HTTP status code 2146 @param message: the error message 2147 @param tree: the tree causing the error 2148 2149 """ 2150 2151 xml = self.manager.xml 2152 2153 if self.representation == "html": 2154 self.session.error = message 2155 if next is not None: 2156 redirect(next) 2157 else: 2158 redirect(URL(r=self.request, f="index")) 2159 else: 2160 raise HTTP(status, 2161 body=xml.json_message(success=False, 2162 status_code=status, 2163 message=message, 2164 tree=tree))
2165 2166 2167 # Request Parser ========================================================== 2168
2169 - def __parse(self):
2170 """ 2171 Parses a web2py request for the REST interface 2172 2173 """ 2174 2175 request = self.request 2176 2177 self.id = None 2178 self.component_name = None 2179 self.component_id = None 2180 self.method = None 2181 2182 representation = request.extension 2183 2184 # Get the names of all components 2185 model = self.manager.model 2186 components = [c[0].name for c in 2187 model.get_components(self.prefix, self.name)] 2188 2189 2190 # Map request args, catch extensions 2191 f = [] 2192 args = request["args"] 2193 if len(args) > 4: 2194 args = args[:4] 2195 method = self.name 2196 for arg in args: 2197 if "." in arg: 2198 arg, representation = arg.rsplit(".", 1) 2199 if method is None: 2200 method = arg 2201 elif arg.isdigit(): 2202 f.append((method, arg)) 2203 method = None 2204 else: 2205 f.append((method, None)) 2206 method = arg 2207 if method: 2208 f.append((method, None)) 2209 2210 self.id = f[0][1] 2211 2212 # Sort out component name and method 2213 l = len(f) 2214 if l > 1: 2215 m = f[1][0].lower() 2216 i = f[1][1] 2217 if m in components: 2218 self.component_name = m 2219 self.component_id = i 2220 else: 2221 self.method = m 2222 if not self.id: 2223 self.id = i 2224 if self.component_name and l > 2: 2225 self.method = f[2][0].lower() 2226 if not self.component_id: 2227 self.component_id = f[2][1] 2228 2229 # ?format= overrides extensions 2230 if "format" in request.vars: 2231 ext = request.vars["format"] 2232 if isinstance(ext, list): 2233 ext = ext[-1] 2234 representation = ext or representation 2235 self.representation = representation.lower() 2236 2237 if not self.representation: 2238 self.representation = self.DEFAULT_REPRESENTATION
2239 2240 2241 # URL helpers ============================================================= 2242
2243 - def __next(self, id=None, method=None, representation=None, vars=None):
2244 """ 2245 Returns a URL of the current request 2246 2247 @param id: the record ID for the URL 2248 @param method: an explicit method for the URL 2249 @param representation: the representation for the URL 2250 @param vars: the URL query variables 2251 2252 """ 2253 2254 if vars is None: 2255 vars = self.request.get_vars 2256 if "format" in vars: 2257 del vars["format"] 2258 2259 args = [] 2260 read = False 2261 2262 component_id = self.component_id 2263 if id is None: 2264 id = self.id 2265 else: 2266 read = True 2267 2268 if not representation: 2269 representation = self.representation 2270 if method is None: 2271 method = self.method 2272 elif method=="": 2273 method = None 2274 if not read: 2275 if self.component: 2276 component_id = None 2277 else: 2278 id = None 2279 else: 2280 if id is None: 2281 id = self.id 2282 else: 2283 id = str(id) 2284 if len(id) == 0: 2285 id = "[id]" 2286 2287 if self.component: 2288 if id: 2289 args.append(id) 2290 args.append(self.component_name) 2291 if component_id: 2292 args.append(component_id) 2293 if method: 2294 args.append(method) 2295 else: 2296 if id: 2297 args.append(id) 2298 if method: 2299 args.append(method) 2300 2301 f = self.request.function 2302 if not representation==self.DEFAULT_REPRESENTATION: 2303 if len(args) > 0: 2304 args[-1] = "%s.%s" % (args[-1], representation) 2305 else: 2306 f = "%s.%s" % (f, representation) 2307 2308 return URL(r=self.request, 2309 c=self.request.controller, 2310 f=f, 2311 args=args, vars=vars)
2312 2313 2314 # -------------------------------------------------------------------------
2315 - def here(self, representation=None, vars=None):
2316 """ 2317 URL of the current request 2318 2319 @param representation: the representation for the URL 2320 @param vars: the URL query variables 2321 2322 """ 2323 2324 return self.__next(id=self.id, representation=representation, vars=vars)
2325 2326 2327 # -------------------------------------------------------------------------
2328 - def other(self, method=None, record_id=None, representation=None, vars=None):
2329 """ 2330 URL of a request with different method and/or record_id 2331 of the same resource 2332 2333 @param method: an explicit method for the URL 2334 @param record_id: the record ID for the URL 2335 @param representation: the representation for the URL 2336 @param vars: the URL query variables 2337 2338 """ 2339 2340 return self.__next(method=method, id=record_id, 2341 representation=representation, vars=vars)
2342 2343 2344 # -------------------------------------------------------------------------
2345 - def there(self, representation=None, vars=None):
2346 """ 2347 URL of a HTTP/list request on the same resource 2348 2349 @param representation: the representation for the URL 2350 @param vars: the URL query variables 2351 2352 """ 2353 2354 return self.__next(method="", representation=representation, vars=vars)
2355 2356 2357 # -------------------------------------------------------------------------
2358 - def same(self, representation=None, vars=None):
2359 """ 2360 URL of the same request with neutralized primary record ID 2361 2362 @param representation: the representation for the URL 2363 @param vars: the URL query variables 2364 2365 """ 2366 2367 return self.__next(id="[id]", representation=representation, vars=vars)
2368 2369 2370 # Method handler helpers ================================================== 2371
2372 - def target(self):
2373 """ 2374 Get the target table of the current request 2375 2376 @returns: a tuple of (prefix, name, table, tablename) of the target 2377 resource of this request 2378 2379 """ 2380 2381 if self.component is not None: 2382 return (self.component.prefix, 2383 self.component.name, 2384 self.component.table, 2385 self.component.tablename) 2386 else: 2387 return (self.prefix, 2388 self.name, 2389 self.table, 2390 self.tablename)
2391
2392 2393 # ***************************************************************************** 2394 -class S3Method(object):
2395 """ 2396 REST Method Handler Base Class 2397 2398 Method handler classes should inherit from this class and implement the 2399 apply_method() method. 2400 2401 """ 2402
2403 - def __call__(self, r, method=None, **attr):
2404 """ 2405 Entry point for the REST interface 2406 2407 @param r: the S3Request 2408 @param method: the method established by the REST interface 2409 @param attr: dict of parameters for the method handler 2410 2411 @returns: output object to send to the view 2412 2413 """ 2414 2415 # Environment of the request 2416 self.manager = r.manager 2417 self.session = r.session 2418 self.request = r.request 2419 self.response = r.response 2420 2421 self.T = self.manager.T 2422 self.db = self.manager.db 2423 2424 # Settings 2425 self.permit = self.manager.auth.s3_has_permission 2426 self.download_url = self.manager.s3.download_url 2427 2428 # Init 2429 self.next = None 2430 2431 # Get the right table and method 2432 self.prefix, self.name, self.table, self.tablename = r.target() 2433 2434 # Override request method 2435 if method is not None: 2436 self.method = method 2437 else: 2438 self.method = r.method 2439 if r.component: 2440 self.record = r.component_id 2441 component = r.resource.components.get(r.component_name, None) 2442 self.resource = component.resource 2443 if not self.method: 2444 if r.multiple and not r.component_id: 2445 self.method = "list" 2446 else: 2447 self.method = "read" 2448 else: 2449 self.record = r.id 2450 self.resource = r.resource 2451 if not self.method: 2452 if r.id or r.method in ("read", "display"): 2453 self.method = "read" 2454 else: 2455 self.method = "list" 2456 2457 # Apply method 2458 output = self.apply_method(r, **attr) 2459 2460 # Redirection 2461 if self.next and self.resource.lastid: 2462 self.next = str(self.next) 2463 placeholder = "%5Bid%5D" 2464 self.next = self.next.replace(placeholder, self.resource.lastid) 2465 placeholder = "[id]" 2466 self.next = self.next.replace(placeholder, self.resource.lastid) 2467 r.next = self.next 2468 2469 # Add additional view variables (e.g. rheader) 2470 self._extend_view(output, r, **attr) 2471 2472 return output
2473 2474 2475 # -------------------------------------------------------------------------
2476 - def apply_method(self, r, **attr):
2477 """ 2478 Stub for apply_method, to be implemented in subclass 2479 2480 @param r: the S3Request 2481 @param attr: dictionary of parameters for the method handler 2482 2483 @returns: output object to send to the view 2484 2485 """ 2486 2487 output = dict() 2488 return output
2489 2490 2491 # Utilities =============================================================== 2492 2493 @staticmethod
2494 - def _record_id(r):
2495 """ 2496 Get the ID of the target record of a S3Request 2497 2498 @param r: the S3Request 2499 2500 """ 2501 2502 if r.component: 2503 if r.multiple and not r.component_id: 2504 return None 2505 resource = r.resource.components.get(r.component_name).resource 2506 resource.load(start=0, limit=1) 2507 if len(resource): 2508 return resource.records().first().id 2509 else: 2510 return r.id 2511 2512 return None
2513 2514 2515 # -------------------------------------------------------------------------
2516 - def _config(self, key, default=None):
2517 """ 2518 Get a configuration setting of the current table 2519 2520 @param key: the setting key 2521 @param default: the default value 2522 2523 """ 2524 2525 return self.manager.model.get_config(self.table, key, default)
2526 2527 2528 # ------------------------------------------------------------------------- 2529 @staticmethod
2530 - def _view(r, default, format=None):
2531 """ 2532 Get the path to the view stylesheet file 2533 2534 @param r: the S3Request 2535 @param default: name of the default view stylesheet file 2536 @param format: format string (optional) 2537 2538 """ 2539 2540 request = r.request 2541 folder = request.folder 2542 prefix = request.controller 2543 2544 if r.component: 2545 view = "%s_%s_%s" % (r.name, r.component_name, default) 2546 path = os.path.join(folder, "views", prefix, view) 2547 if os.path.exists(path): 2548 return "%s/%s" % (prefix, view) 2549 else: 2550 view = "%s_%s" % (r.name, default) 2551 path = os.path.join(folder, "views", prefix, view) 2552 else: 2553 if format: 2554 view = "%s_%s_%s" % (r.name, default, format) 2555 else: 2556 view = "%s_%s" % (r.name, default) 2557 path = os.path.join(folder, "views", prefix, view) 2558 2559 if os.path.exists(path): 2560 return "%s/%s" % (prefix, view) 2561 else: 2562 if format: 2563 return default.replace(".html", "_%s.html" % format) 2564 else: 2565 return default
2566 2567 2568 # ------------------------------------------------------------------------- 2569 @staticmethod
2570 - def _extend_view(output, r, **attr):
2571 """ 2572 Add additional view variables (invokes all callables) 2573 2574 @param output: the output dict 2575 @param r: the S3Request 2576 @param attr: the view variables (e.g. 'rheader') 2577 2578 @note: overload this method in subclasses if you don't want 2579 additional view variables to be added automatically 2580 2581 """ 2582 2583 if r.interactive and isinstance(output, dict): 2584 for key in attr: 2585 handler = attr[key] 2586 if callable(handler): 2587 resolve = True 2588 try: 2589 display = handler(r) 2590 except TypeError: 2591 # Argument list failure => pass callable to the view as-is 2592 display = handler 2593 continue 2594 except: 2595 # Propagate all other errors to the caller 2596 raise 2597 else: 2598 display = handler 2599 if isinstance(display, dict) and resolve: 2600 output.update(**display) 2601 elif display is not None: 2602 output.update(**{key:display}) 2603 elif key in output: 2604 del output[key]
2605 2606 2607 # ***************************************************************************** 2608