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

Source Code for Module s3.s3mobile

   1  # -*- coding: utf-8 -*- 
   2   
   3  """ S3 Mobile Forms API 
   4   
   5      @copyright: 2016-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      @todo: integrate S3XForms API 
  30  """ 
  31   
  32  __all__ = ("S3MobileFormList", 
  33             "S3MobileSchema", 
  34             "S3MobileForm", 
  35             "S3MobileCRUD", 
  36             ) 
  37   
  38  import json 
  39   
  40  from gluon import * 
  41  from s3datetime import s3_parse_datetime 
  42  from s3forms import S3SQLForm, S3SQLCustomForm, S3SQLDummyField, S3SQLField 
  43  from s3rest import S3Method 
  44  from s3utils import s3_get_foreign_key, s3_str 
  45  from s3validators import SEPARATORS 
  46   
  47  DEFAULT = lambda: None 
  48   
  49  # JSON-serializable table settings (SERIALIZABLE_OPTS) 
  50  # which require preprocessing before they can be passed 
  51  # to the mobile client (e.g. i18n) 
  52  #PREPROCESS_OPTS = ("subheadings", ) 
  53  PREPROCESS_OPTS = [] 
54 55 # ============================================================================= 56 -class S3MobileFormList(object):
57 """ 58 Form List Generator 59 """ 60
61 - def __init__(self):
62 """ 63 Constructor 64 """ 65 66 T = current.T 67 s3db = current.s3db 68 settings = current.deployment_settings 69 70 formlist = [] 71 formdict = {} 72 73 forms = settings.get_mobile_forms() 74 if forms: 75 keys = set() 76 for item in forms: 77 78 # Parse the configuration 79 options = {} 80 if isinstance(item, (tuple, list)): 81 if len(item) == 2: 82 title, tablename = item 83 if isinstance(tablename, dict): 84 tablename, options = title, tablename 85 title = None 86 elif len(item) == 3: 87 title, tablename, options = item 88 else: 89 continue 90 else: 91 title, tablename = None, item 92 93 # Make sure table exists 94 table = s3db.table(tablename) 95 if not table: 96 current.log.warning("Mobile forms: non-existent resource %s" % tablename) 97 continue 98 99 # Determine controller and function 100 c, f = tablename.split("_", 1) 101 c = options.get("c") or c 102 f = options.get("f") or f 103 104 # Only expose if target module is enabled 105 if not settings.has_module(c): 106 continue 107 108 # Determine the form name 109 name = options.get("name") 110 if not name: 111 name = "%s_%s" % (c, f) 112 113 # Stringify URL query vars 114 url_vars = options.get("vars") 115 if url_vars: 116 items = [] 117 for k in url_vars: 118 v = s3_str(url_vars[k]) 119 url_vars[k] = v 120 items.append("%s=%s" % (k, v)) 121 query = "&".join(sorted(items)) 122 else: 123 query = "" 124 125 # Deduplicate by target URL 126 key = (c, f, query) 127 if key in keys: 128 continue 129 keys.add(key) 130 131 # Determine form title 132 if title is None: 133 title = " ".join(w.capitalize() for w in f.split("_")) 134 if isinstance(title, basestring): 135 title = T(title) 136 137 # Provides (master-)data for download? 138 data = True if options.get("data") else False 139 140 # Exposed for data entry (or just for reference)? 141 main = False if options.get("data_only", False) else True 142 143 # Append to form list 144 url = {"c": c, "f": f} 145 if url_vars: 146 url["v"] = url_vars 147 mform = {"n": name, 148 "l": s3_str(title), 149 "t": tablename, 150 "r": url, 151 "d": data, 152 "m": main, 153 } 154 formlist.append(mform) 155 formdict[name] = mform 156 157 dynamic_tables = settings.get_mobile_dynamic_tables() 158 if dynamic_tables: 159 160 # Select all dynamic tables which have mobile_form=True 161 ttable = s3db.s3_table 162 query = (ttable.mobile_form == True) & \ 163 (ttable.deleted != True) 164 rows = current.db(query).select(ttable.name, 165 ttable.title, 166 ttable.mobile_data, 167 ) 168 for row in rows: 169 170 tablename = row.name 171 suffix = tablename.split("_", 1)[-1] 172 173 # Form title 174 title = row.title 175 if not title: 176 title = " ".join(s.capitalize() for s in suffix.split("_")) 177 178 # URL 179 # @todo: make c+f configurable? 180 url = {"c": "default", 181 "f": "table/%s" % suffix, 182 } 183 184 # Append to form list 185 mform = {"n": tablename, 186 "l": title, 187 "t": tablename, 188 "r": url, 189 "d": row.mobile_data, 190 } 191 formlist.append(mform) 192 formdict[name] = mform 193 194 self.formlist = formlist 195 self.forms = formdict
196 197 # -------------------------------------------------------------------------
198 - def json(self):
199 """ 200 Serialize the form list as JSON (EdenMobile) 201 202 @returns: a JSON string 203 """ 204 205 return json.dumps(self.formlist, separators=SEPARATORS)
206
207 # ============================================================================= 208 -class S3MobileSchema(object):
209 """ 210 Table schema for a mobile resource 211 """ 212 213 # Field types supported for mobile resources 214 SUPPORTED_FIELD_TYPES = ("string", 215 "text", 216 "integer", 217 "double", 218 "date", 219 "datetime", 220 "boolean", 221 "reference", 222 "upload", 223 ) 224 225 # -------------------------------------------------------------------------
226 - def __init__(self, resource):
227 """ 228 Constructor 229 230 @param resource - the S3Resource 231 """ 232 233 self.resource = resource 234 235 # Initialize reference map 236 self._references = {} 237 238 # Initialize the schema 239 self._schema = None 240 241 # Initialize the form description 242 self._form = None 243 244 # Initialize subheadings 245 self._subheadings = DEFAULT 246 247 # Initialize settings 248 self._settings = None 249 250 # Initialize lookup list attributes 251 self._lookup_only = None 252 self._llrepr = None
253 254 # -------------------------------------------------------------------------
255 - def serialize(self):
256 """ 257 Serialize the table schema 258 259 @return: a JSON-serializable dict containing the table schema 260 """ 261 262 schema = self._schema 263 if schema is None: 264 265 # Initialize 266 schema = {} 267 self._references = {} 268 269 if not self.lookup_only: 270 # Introspect and build schema 271 fields = self.fields() 272 for field in fields: 273 description = self.describe(field) 274 if description: 275 schema[field.name] = description 276 277 # Store schema 278 self._schema = schema 279 280 return schema
281 282 # ------------------------------------------------------------------------- 283 @property
284 - def references(self):
285 """ 286 Tables (and records) referenced in this schema (lazy property) 287 288 @return: a dict {tablename: [recordID, ...]} of all 289 referenced tables and records 290 """ 291 292 if self._references is None: 293 # Trigger introspection to gather all references 294 self.serialize() 295 296 return self._references
297 298 # ------------------------------------------------------------------------- 299 @property
300 - def form(self):
301 """ 302 The mobile form (field order) for the resource (lazy property) 303 """ 304 305 if self._form is None: 306 self.serialize() 307 308 return self._form
309 310 # ------------------------------------------------------------------------- 311 @property
312 - def subheadings(self):
313 """ 314 The subheadings for the mobile form (lazy property) 315 """ 316 317 subheadings = self._subheadings 318 319 if subheadings is DEFAULT: 320 subheadings = self._subheadings = self.resource.get_config("subheadings") 321 322 return subheadings
323 324 # ------------------------------------------------------------------------- 325 @property
326 - def settings(self):
327 """ 328 Directly-serializable settings from s3db.configure (lazy property) 329 """ 330 331 settings = self._settings 332 333 if settings is None: 334 335 settings = self._settings = {} 336 resource = self.resource 337 338 from s3model import SERIALIZABLE_OPTS 339 for key in SERIALIZABLE_OPTS: 340 if key not in PREPROCESS_OPTS: 341 setting = resource.get_config(key, DEFAULT) 342 if setting is not DEFAULT: 343 settings[key] = setting 344 345 return settings
346 347 # ------------------------------------------------------------------------- 348 # Introspection methods 349 # -------------------------------------------------------------------------
350 - def describe(self, field):
351 """ 352 Construct a field description for the schema 353 354 @param field: a Field instance 355 356 @return: the field description as JSON-serializable dict 357 """ 358 359 fieldtype = str(field.type) 360 SUPPORTED_FIELD_TYPES = set(self.SUPPORTED_FIELD_TYPES) 361 362 # Check if foreign key 363 superkey = False 364 reftype = None 365 if fieldtype[:9] == "reference": 366 367 s3db = current.s3db 368 369 is_foreign_key = True 370 371 # Get referenced table/field name 372 ktablename, key = s3_get_foreign_key(field)[:2] 373 374 # Get referenced table 375 ktable = current.s3db.table(ktablename) 376 if not ktable: 377 return None 378 379 if "instance_type" in ktable.fields: 380 # Super-key 381 382 tablename = str(field).split(".", 1)[0] 383 supertables = s3db.get_config(tablename, "super_entity") 384 if not supertables: 385 supertables = set() 386 elif not isinstance(supertables, (list, tuple)): 387 supertables = [supertables] 388 389 if ktablename in supertables and key == ktable._id.name: 390 # This is the super-id of the instance table => skip 391 return None 392 else: 393 # This is a super-entity reference 394 fieldtype = "objectkey" 395 396 # @todo: add instance types if limited in validator 397 superkey = True 398 reftype = (ktablename,) # []) 399 else: 400 # Regular foreign key 401 402 # Store schema reference 403 references = self._references 404 if ktablename not in references: 405 references[ktablename] = set() 406 else: 407 is_foreign_key = False 408 ktablename = None 409 410 # Check that field type is supported 411 if fieldtype in SUPPORTED_FIELD_TYPES or is_foreign_key: 412 supported = True 413 else: 414 supported = False 415 if not supported: 416 return None 417 418 # Create a field description 419 description = {"type": fieldtype, 420 "label": str(field.label), 421 } 422 423 # Add type for super-entity references (=object keys) 424 if reftype: 425 description["reftype"] = reftype 426 427 # Add field options to description 428 options = self.get_options(field, lookup=ktablename) 429 if options: 430 description["options"] = options 431 432 # Add default value to description 433 default = self.get_default(field, lookup=ktablename, superkey=superkey) 434 if default: 435 description["default"] = default 436 437 # Add readable/writable settings if False (True is assumed) 438 if not field.readable: 439 description["readable"] = False 440 if not field.writable: 441 description["writable"] = False 442 443 if hasattr(field.widget, "mobile"): 444 description["widget"] = field.widget.mobile 445 446 # Add required flag if True (False is assumed) 447 if self.is_required(field): 448 description["required"] = True 449 450 # @todo: add tooltip to description 451 452 # @todo: if field.represent is a base-class S3Represent 453 # (i.e. no custom lookup, no custom represent), 454 # and its field list is not just "name" => pass 455 # that field list as description["represent"] 456 457 # Add field's mobile settings to description (Dynamic Fields) 458 msettings = hasattr(field, "s3_settings") and \ 459 field.s3_settings and \ 460 field.s3_settings.get("mobile") 461 if msettings: 462 description["settings"] = msettings 463 464 return description
465 466 # ------------------------------------------------------------------------- 467 @staticmethod
468 - def is_required(field):
469 """ 470 Determine whether a value is required for a field 471 472 @param field: the Field 473 474 @return: True|False 475 """ 476 477 required = field.notnull 478 479 if not required and field.requires: 480 error = field.validate("")[1] 481 if error is not None: 482 required = True 483 484 return required
485 486 # -------------------------------------------------------------------------
487 - def get_options(self, field, lookup=None):
488 """ 489 Get the options for a field with IS_IN_SET 490 491 @param field: the Field 492 @param lookup: the name of the lookup table 493 494 @return: a list of tuples (key, label) with the field options 495 """ 496 497 requires = field.requires 498 if not requires: 499 return None 500 if isinstance(requires, (list, tuple)): 501 requires = requires[0] 502 if isinstance(requires, IS_EMPTY_OR): 503 requires = requires.other 504 505 fieldtype = str(field.type) 506 if fieldtype[:9] == "reference": 507 508 # Foreign keys have no fixed options 509 # => must expose the lookup table with data=True in order 510 # to share current field options with the mobile client; 511 # this is better done explicitly in order to run the 512 # data download through the lookup table's controller 513 # for proper authorization, customise_* and filtering 514 return None 515 516 elif fieldtype in ("string", "integer"): 517 518 # Check for IS_IN_SET, and extract the options 519 if isinstance(requires, IS_IN_SET): 520 options = [] 521 for value, label in requires.options(): 522 if value is not None: 523 options.append((value, s3_str(label))) 524 return options 525 else: 526 return None 527 528 else: 529 # @todo: add other types (may require special option key encoding) 530 return None
531 532 # -------------------------------------------------------------------------
533 - def get_default(self, field, lookup=None, superkey=False):
534 """ 535 Get the default value for a field 536 537 @param field: the Field 538 @param lookup: the name of the lookup table 539 @param superkey: lookup table is a super-entity 540 541 @returns: the default value for the field 542 """ 543 544 default = field.default 545 546 if default is not None: 547 548 fieldtype = str(field.type) 549 550 if fieldtype[:9] == "reference": 551 552 # Look up the UUID for the default 553 uuid = self.get_uuid(lookup, default) 554 if uuid: 555 556 if superkey: 557 # Get the instance record ID 558 prefix, name, record_id = current.s3db.get_instance(lookup, default) 559 if record_id: 560 tablename = "%s_%s" % (prefix, name) 561 else: 562 record_id = default 563 tablename = lookup 564 565 if record_id: 566 567 # Export the default lookup record as dependency 568 # (make sure the corresponding table schema is exported) 569 references = self.references.get(tablename) or set() 570 references.add(record_id) 571 self.references[tablename] = references 572 573 # Resolve as UUID 574 default = uuid 575 576 else: 577 default = None 578 579 else: 580 default = None 581 582 elif fieldtype in ("date", "datetime", "time"): 583 584 # @todo: implement this 585 # => typically using a dynamic default (e.g. "now"), which 586 # will need special encoding and handling on the mobile 587 # side 588 # => static defaults must be encoded in ISO-Format 589 default = None 590 591 else: 592 593 # Use field default as-is 594 default = field.default 595 596 return default
597 598 # -------------------------------------------------------------------------
599 - def fields(self):
600 """ 601 Determine which fields need to be included in the schema 602 603 @returns: a list of Field instances 604 """ 605 606 resource = self.resource 607 resolve_selector = resource.resolve_selector 608 609 tablename = resource.tablename 610 611 fields = [] 612 mobile_form = self._form = [] 613 mappend = mobile_form.append 614 615 # Prevent duplicates 616 fnames = set() 617 include = fnames.add 618 619 form = self.mobile_form(resource) 620 for element in form.elements: 621 622 if isinstance(element, S3SQLField): 623 rfield = resolve_selector(element.selector) 624 625 fname = rfield.fname 626 627 if rfield.tname == tablename and fname not in fnames: 628 fields.append(rfield.field) 629 mappend(fname) 630 include(fname) 631 632 elif isinstance(element, S3SQLDummyField): 633 field = {"type": "dummy", 634 "name": element.selector, 635 } 636 mappend(field) 637 638 if resource.parent and not resource.linktable: 639 640 # Include the parent key 641 fkey = resource.fkey 642 if fkey not in fnames: 643 fields.append(resource.table[fkey]) 644 #include(fkey) 645 646 return fields
647 648 # ------------------------------------------------------------------------- 649 @staticmethod
650 - def has_mobile_form(tablename):
651 """ 652 Check whether a table exposes a mobile form 653 654 @param tablename: the table name 655 656 @return: True|False 657 """ 658 659 from s3model import DYNAMIC_PREFIX 660 if tablename.startswith(DYNAMIC_PREFIX): 661 662 ttable = current.s3db.s3_table 663 query = (ttable.name == tablename) & \ 664 (ttable.mobile_form == True) & \ 665 (ttable.deleted != True) 666 row = current.db(query).select(ttable.id, 667 limitby = (0, 1), 668 ).first() 669 if row: 670 return True 671 else: 672 673 forms = current.deployment_settings.get_mobile_forms() 674 for spec in forms: 675 if isinstance(spec, (tuple, list)): 676 if len(spec) > 1 and not isinstance(spec[1], dict): 677 tn = spec[1] 678 else: 679 tn = spec[0] 680 else: 681 tn = spec 682 if tn == tablename: 683 return True 684 685 return False
686 687 # ------------------------------------------------------------------------- 688 @staticmethod
689 - def mobile_form(resource):
690 """ 691 Get the mobile form for a resource 692 693 @param resource: the S3Resource 694 @returns: an S3SQLForm instance 695 """ 696 697 # Get the form definition from "mobile_form" table setting 698 form = resource.get_config("mobile_form") 699 if not form or not isinstance(form, S3SQLCustomForm): 700 # Fallback 701 form = resource.get_config("crud_form") 702 703 if not form: 704 # No mobile form configured, or is a S3SQLDefaultForm 705 # => construct a custom form that includes all readable fields 706 readable_fields = resource.readable_fields() 707 fields = [field.name for field in readable_fields 708 if field.type != "id"] 709 form = S3SQLCustomForm(*fields) 710 711 return form
712 713 # ------------------------------------------------------------------------- 714 @property
715 - def lookup_only(self):
716 """ 717 Whether the resource shall be exposed as mere lookup list 718 without mobile form (lazy property) 719 """ 720 721 lookup_only = self._lookup_only 722 if lookup_only is None: 723 724 resource = self.resource 725 726 mform = resource.get_config("mobile_form") 727 if mform is False: 728 from s3fields import S3Represent 729 self._llrepr = S3Represent(lookup=resource.tablename) 730 lookup_only = True 731 elif callable(mform) and not isinstance(mform, S3SQLForm): 732 self._llrepr = mform 733 lookup_only = True 734 else: 735 lookup_only = False 736 737 self._lookup_only = lookup_only 738 739 return lookup_only
740 741 # ------------------------------------------------------------------------- 742 @property
743 - def llrepr(self):
744 """ 745 The lookup list representation method for the resource 746 """ 747 748 return self._llrepr if self.lookup_only else None
749 750 # ------------------------------------------------------------------------- 751 # Utility functions 752 # ------------------------------------------------------------------------- 753 @staticmethod
754 - def get_uuid(tablename, record_id):
755 """ 756 Look up the UUID of a record 757 758 @param tablename: the table name 759 @param record_id: the record ID 760 761 @return: the UUID of the specified record, or None if 762 the record does not exist or has no UUID 763 """ 764 765 table = current.s3db.table(tablename) 766 if not table or "uuid" not in table.fields: 767 return None 768 769 query = (table._id == record_id) 770 if "deleted" in table.fields: 771 query &= (table.deleted == False) 772 773 row = current.db(query).select(table.uuid, 774 limitby = (0, 1), 775 ).first() 776 777 return row.uuid or None if row else None
778
779 # ============================================================================= 780 -class S3MobileForm(object):
781 """ 782 Mobile representation of an S3SQLForm 783 """ 784
785 - def __init__(self, resource, form=None):
786 """ 787 Constructor 788 789 @param resource: the S3Resource 790 @param form: an S3SQLForm instance to override settings 791 """ 792 793 self.resource = resource 794 self._form = form 795 796 self._config = DEFAULT
797 798 # ------------------------------------------------------------------------- 799 @property
800 - def config(self):
801 """ 802 The mobile form configuration (lazy property) 803 804 @returns: a dict {tablename, title, options} 805 """ 806 807 config = self._config 808 if config is DEFAULT: 809 810 tablename = self.resource.tablename 811 config = {"tablename": tablename, 812 "title": None, 813 "options": {}, 814 } 815 816 forms = current.deployment_settings.get_mobile_forms() 817 if forms: 818 for form in forms: 819 options = None 820 if isinstance(form, (tuple, list)): 821 if len(form) == 2: 822 title, tablename_ = form 823 if isinstance(tablename_, dict): 824 tablename_, options = title, tablename_ 825 title = None 826 elif len(form) == 3: 827 title, tablename_, options = form 828 else: 829 # Invalid => skip 830 continue 831 else: 832 title, tablename_ = None, form 833 834 if tablename_ == tablename: 835 config["title"] = title 836 if options: 837 config["options"] = options 838 break 839 840 self._config = config 841 842 return config
843 844 # -------------------------------------------------------------------------
845 - def serialize(self, msince=None):
846 """ 847 Serialize the mobile form configuration for the target resource 848 849 @param msince: include look-up records only if modified 850 after this datetime ("modified since") 851 852 @return: a JSON-serialiable dict containing the mobile form 853 configuration for export to the mobile client 854 """ 855 856 s3db = current.s3db 857 resource = self.resource 858 tablename = resource.tablename 859 860 super_entities = self.super_entities 861 862 ms = S3MobileSchema(resource) 863 schema = ms.serialize() 864 865 main = {"tablename": tablename, 866 "schema": schema, 867 "types": super_entities(tablename), 868 "form": ms.form, 869 } 870 871 # Add CRUD strings 872 strings = self.strings() 873 if strings: 874 main["strings"] = strings 875 876 # Add subheadings 877 subheadings = ms.subheadings 878 if subheadings: 879 main["subheadings"] = subheadings 880 881 # Add directly-serializable settings 882 settings = ms.settings 883 if settings: 884 main["settings"] = settings 885 886 # Required and provided schemas 887 required = set(ms.references.keys()) 888 provided = {resource.tablename: (ms, main)} 889 890 # Add schemas for components 891 components = self.components() 892 for alias in components: 893 894 # Get the component resource 895 cresource = resource.components.get(alias) 896 if not cresource: 897 continue 898 ctablename = cresource.tablename 899 900 # Serialize the table schema 901 schema = S3MobileSchema(cresource) 902 903 # Add schema, super entities and directly-serializable settings 904 spec = components[alias] 905 spec["schema"] = schema.serialize() 906 spec["types"] = super_entities(ctablename) 907 settings = schema.settings 908 if settings: 909 spec["settings"] = settings 910 911 # If the component has a link table, add it to required schemas 912 link = spec.get("link") 913 if link: 914 required.add(link) 915 916 # Add component reference schemas 917 for tname in schema.references: 918 required.add(tname) 919 920 # Mark as provided 921 provided[tablename] = (schema, spec) 922 923 # Add schemas for referenced tables 924 references = {} 925 required = list(required) 926 while required: 927 928 # Get the referenced resource 929 ktablename = required.pop() 930 if ktablename in provided: 931 # Already provided 932 continue 933 kresource = s3db.resource(ktablename) 934 935 # Serialize the table schema 936 schema = S3MobileSchema(kresource) 937 938 # Add schema, super entities and directly-serializable settings 939 spec = {"schema": schema.serialize(), 940 "types": super_entities(ktablename), 941 } 942 settings = schema.settings 943 if settings: 944 spec["settings"] = settings 945 946 # Check for unresolved dependencies 947 for reference in schema.references: 948 if reference not in provided: 949 required.append(reference) 950 951 # Add to references 952 references[ktablename] = spec 953 954 # Mark as provided 955 provided[ktablename] = (schema, spec) 956 957 # Collect all required records (e.g. foreign key defaults) 958 required_records = {} 959 for ktablename in provided: 960 schema = provided[ktablename][0] 961 for tn, record_ids in schema.references.items(): 962 if record_ids: 963 all_ids = (required_records.get(tn) or set()) | record_ids 964 required_records[tn] = all_ids 965 966 # Export required records and add them to the specs 967 for tn, record_ids in required_records.items(): 968 kresource = s3db.resource(tn, id=list(record_ids)) 969 spec = provided[tn][1] 970 fields = spec["schema"].keys() 971 tree = kresource.export_tree(fields = fields, 972 references = fields, 973 msince = msince, 974 ) 975 if len(tree.getroot()): 976 data = current.xml.tree2json(tree, as_dict=True) 977 spec["data"] = data 978 979 # Complete the mobile schema spec 980 form = {"main": main, 981 } 982 if references: 983 form["references"] = references 984 if components: 985 form["components"] = components 986 987 return form
988 989 # -------------------------------------------------------------------------
990 - def strings(self):
991 """ 992 Add CRUD strings for mobile form 993 994 @return: a dict with CRUD strings for the resource 995 """ 996 997 tablename = self.resource.tablename 998 999 # Use the label/plural specified in deployment setting 1000 config = self.config 1001 options = config["options"] 1002 label = options.get("label") 1003 plural = options.get("plural") 1004 1005 # Fall back to CRUD title_list 1006 if not plural or not label: 1007 crud_strings = current.response.s3.crud_strings.get(tablename) 1008 if crud_strings: 1009 if not label: 1010 label = crud_strings.get("title_display") 1011 if not plural: 1012 plural = crud_strings.get("title_list") 1013 1014 # Fall back to the title specified in deployment setting 1015 if not plural: 1016 plural = config.get("title") 1017 1018 # Fall back to capitalized table name 1019 if not label: 1020 name = tablename.split("_", 1)[-1] 1021 label = " ".join(word.capitalize() for word in name.split("_")) 1022 1023 # Build strings-dict 1024 strings = {} 1025 if label: 1026 strings["label"] = s3_str(label) 1027 if plural: 1028 strings["plural"] = s3_str(plural) 1029 1030 return strings
1031 1032 # -------------------------------------------------------------------------
1033 - def components(self):
1034 """ 1035 Add component declarations to the mobile form 1036 1037 @return: a dict with component declarations for the resource 1038 """ 1039 1040 resource = self.resource 1041 tablename = resource.tablename 1042 pkey = resource._id.name 1043 1044 options = self.config.get("options") 1045 1046 aliases = set() 1047 components = {} 1048 1049 # Dynamic components, exposed if: 1050 # - "dynamic_components" is True for the master table, and 1051 # - "mobile_component" for the component key is not set to False 1052 dynamic_components = resource.get_config("dynamic_components") 1053 if dynamic_components: 1054 1055 # Dynamic components of this table and all its super-entities 1056 tablenames = [tablename] 1057 supertables = resource.get_config("super_entity") 1058 if supertables: 1059 if isinstance(supertables, (list, tuple)): 1060 tablenames.extend(supertables) 1061 elif supertables: 1062 tablenames.append(supertables) 1063 1064 # Look up corresponding component keys in s3_fields 1065 s3db = current.s3db 1066 ftable = s3db.s3_field 1067 ttable = s3db.s3_table 1068 join = ttable.on(ttable.id == ftable.table_id) 1069 query = (ftable.component_key == True) & \ 1070 (ftable.master.belongs(tablenames)) & \ 1071 (ftable.deleted == False) 1072 rows = current.db(query).select(ftable.name, 1073 ftable.component_alias, 1074 ftable.settings, 1075 ttable.name, 1076 join = join, 1077 ) 1078 1079 for row in rows: 1080 component_key = row.s3_field 1081 1082 # Skip if mobile_component is set to False 1083 settings = component_key.settings 1084 if settings and settings.get("mobile_component") is False: 1085 continue 1086 1087 alias = component_key.component_alias 1088 if not alias: 1089 # Default component alias 1090 alias = row.s3_table.name.split("_", 1)[-1] 1091 aliases.add(alias) 1092 1093 # Static components, exposed if 1094 # - configured in "components" option of settings.mobile.forms 1095 static = options.get("components") if options else None 1096 if static: 1097 aliases |= set(static) 1098 1099 # Construct component descriptions for schema export 1100 if aliases: 1101 T = current.T 1102 hooks = current.s3db.get_components(tablename, names=aliases) 1103 for alias, hook in hooks.items(): 1104 1105 description = {"table": hook.tablename, 1106 "multiple": hook.multiple, 1107 } 1108 if hook.label: 1109 description["label"] = s3_str(T(hook.label)) 1110 if hook.plural: 1111 description["plural"] = s3_str(T(hook.plural)) 1112 1113 if hook.pkey != pkey: 1114 description["pkey"] = hook.pkey 1115 1116 linktable = hook.linktable 1117 if linktable: 1118 description.update({"link": str(linktable), 1119 "joinby": hook.lkey, 1120 "key": hook.rkey, 1121 }) 1122 if hook.fkey != "id": 1123 description["fkey"] = hook.fkey 1124 else: 1125 description["joinby"] = hook.fkey 1126 1127 components[alias] = description 1128 1129 return components
1130 1131 # ------------------------------------------------------------------------- 1132 @staticmethod
1133 - def super_entities(tablename):
1134 """ 1135 Helper method to determine the super entities of a table 1136 1137 @param tablename: the table name 1138 1139 @return: a dict {super-table: super-key} 1140 """ 1141 1142 s3db = current.s3db 1143 1144 supertables = s3db.get_config(tablename, "super_entity") 1145 if not supertables: 1146 supertables = set() 1147 elif not isinstance(supertables, (tuple, list)): 1148 supertables = [supertables] 1149 1150 super_entities = {} 1151 for tablename in supertables: 1152 table = s3db.table(tablename) 1153 if table: 1154 super_entities[tablename] = table._id.name 1155 1156 return super_entities
1157
1158 # ============================================================================= 1159 -class S3MobileCRUD(S3Method):
1160 """ 1161 Mobile Data Handler 1162 1163 responds to GET /prefix/name/mform.json (Schema download) 1164 """ 1165 1166 # -------------------------------------------------------------------------
1167 - def apply_method(self, r, **attr):
1168 """ 1169 Entry point for REST interface. 1170 1171 @param r: the S3Request instance 1172 @param attr: controller attributes 1173 """ 1174 1175 http = r.http 1176 method = r.method 1177 representation = r.representation 1178 1179 output = {} 1180 1181 if method == "mform": 1182 if representation == "json": 1183 if http == "GET": 1184 output = self.mform(r, **attr) 1185 else: 1186 r.error(405, current.ERROR.BAD_METHOD) 1187 else: 1188 r.error(415, current.ERROR.BAD_FORMAT) 1189 else: 1190 r.error(405, current.ERROR.BAD_METHOD) 1191 1192 return output
1193 1194 # -------------------------------------------------------------------------
1195 - def mform(self, r, **attr):
1196 """ 1197 Get the mobile form for the target resource 1198 1199 @param r: the S3Request instance 1200 @param attr: controller attributes 1201 1202 @returns: a JSON string 1203 """ 1204 1205 resource = self.resource 1206 1207 msince = r.get_vars.get("msince") 1208 if msince: 1209 msince = s3_parse_datetime(msince) 1210 1211 # Get the mobile form 1212 mform = S3MobileForm(resource).serialize(msince=msince) 1213 1214 # Add controller and function for data exchange 1215 mform["controller"] = r.controller 1216 mform["function"] = r.function 1217 1218 # Convert to JSON 1219 output = json.dumps(mform, separators=SEPARATORS) 1220 1221 current.response.headers = {"Content-Type": "application/json"} 1222 return output
1223 1224 # END ========================================================================= 1225