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

Source Code for Module s3.s3xforms

  1  # -*- coding: utf-8 -*- 
  2   
  3  """ S3 XForms API 
  4   
  5      @copyright: 2014-2019 (c) Sahana Software Foundation 
  6      @license: MIT 
  7   
  8      Permission is hereby granted, free of charge, to any person 
  9      obtaining a copy of this software and associated documentation 
 10      files (the "Software"), to deal in the Software without 
 11      restriction, including without limitation the rights to use, 
 12      copy, modify, merge, publish, distribute, sublicense, and/or sell 
 13      copies of the Software, and to permit persons to whom the 
 14      Software is furnished to do so, subject to the following 
 15      conditions: 
 16   
 17      The above copyright notice and this permission notice shall be 
 18      included in all copies or substantial portions of the Software. 
 19   
 20      THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 
 21      EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES 
 22      OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 
 23      NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT 
 24      HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 
 25      WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING 
 26      FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR 
 27      OTHER DEALINGS IN THE SOFTWARE. 
 28  """ 
 29   
 30  __all__ = ("S3XForms", 
 31             "S3XFormsWidget", 
 32             ) 
 33   
 34  from gluon import * 
 35  from s3rest import S3Method 
 36  from s3utils import s3_unicode 
37 38 # ============================================================================= 39 -class S3XForms(S3Method):
40 """ XForm-based CRUD Handler """ 41 42 # -------------------------------------------------------------------------
43 - def apply_method(self, r, **attr):
44 """ 45 Apply CRUD methods 46 47 @param r: the S3Request 48 @param attr: controller parameters for the request 49 50 @return: output object to send to the view 51 """ 52 53 if r.http == "GET": 54 return self.form(r, **attr) 55 else: 56 r.error(405, current.ERROR.BAD_METHOD)
57 58 # -------------------------------------------------------------------------
59 - def form(self, r, **attr):
60 """ 61 Generate an XForms form for the current resource 62 63 @param r: the S3Request 64 @param attr: controller parameters for the request 65 """ 66 67 resource = self.resource 68 69 # @todo: apply xform-setting 70 form = S3XFormsForm(resource.table) 71 72 response = current.response 73 response.headers["Content-Type"] = "application/xhtml+xml" 74 return form
75 76 # ------------------------------------------------------------------------- 77 @staticmethod
78 - def formlist():
79 """ 80 Retrieve a list of available XForms 81 82 @return: a list of tuples (url, title) of available XForms 83 """ 84 85 resources = current.deployment_settings.get_xforms_resources() 86 87 xforms = [] 88 if resources: 89 s3db = current.s3db 90 for item in resources: 91 92 # Parse item options 93 options = {} 94 if isinstance(item, (tuple, list)): 95 if len(item) == 2: 96 title, tablename = item 97 if isinstance(tablename, dict): 98 tablename, options = title, tablename 99 title = None 100 elif len(item) == 3: 101 title, tablename, options = item 102 else: 103 continue 104 else: 105 title, tablename = None, item 106 107 # Get the resource 108 try: 109 resource = s3db.resource(tablename) 110 except AttributeError: 111 current.log.warning("XForms: non-existent resource %s" % tablename) 112 continue 113 114 if title is None: 115 title = " ".join(w.capitalize() for w in resource.name.split("_")) 116 117 # Options can override target controller/function and URL vars 118 c = options.get("c", "xforms") 119 f = options.get("f", "forms") 120 url_vars = options.get("vars", {}) 121 122 config = resource.get_config("xform") 123 if config: 124 if isinstance(config, dict): 125 # Template-based XForm 126 collection = config.get("collection") 127 if not collection: 128 continue 129 table = resource.table 130 DELETED = current.xml.DELETED 131 if DELETED in table: 132 query = table[DELETED] != True 133 else: 134 query = table.id > 0 135 public = options.get("check", "public") 136 if public in table: 137 query &= table[public] == True 138 title_field = options.get("title", "name") 139 # @todo: audit 140 rows = current.db(query).select(table._id, 141 table[title_field], 142 ) 143 native = c == "xforms" 144 for row in rows: 145 if native: 146 args = [tablename, row[table._id]] 147 else: 148 args = [row[table._id], "xform.xhtml"] 149 url = URL(c = c, 150 f = f, 151 args = args, 152 vars = url_vars, 153 host = True, 154 extension = "", 155 ) 156 xforms.append((url, row[title_field])) 157 else: 158 # Custom XForm => not supported yet, skip 159 continue 160 else: 161 # Introspective XForm 162 if c == "xforms": 163 args = [tablename] 164 else: 165 args = ["xform.xhtml"] 166 url = URL(c = c, 167 f = f, 168 args = args, 169 vars = url_vars, 170 host = True, 171 extension = "", 172 ) 173 xforms.append((url, title)) 174 175 return xforms
176
177 # ============================================================================= 178 -class S3XFormsWidget(object):
179 """ XForms Form Widget (Base Class) """ 180
181 - def __init__(self, translate = True):
182 """ 183 Constructor 184 185 @param translate: enable/disable label translation 186 """ 187 188 self.translate = translate 189 self._strings = {}
190 191 # -------------------------------------------------------------------------
192 - def __call__(self, field, label, ref):
193 """ 194 Form builder entry point 195 196 @param field: the Field or a Storage with field information 197 @param label: the label 198 @param ref: the reference (string) that links the widget 199 with the data model 200 201 @return: tuple (widget, dict of i18n-strings) 202 """ 203 204 if not field: 205 raise SyntaxError("Field is required") 206 207 if not ref: 208 raise SyntaxError("Reference is required") 209 self.ref = ref 210 attr = {"_ref": ref} 211 212 self.setstr("label", label) 213 comment = field.comment 214 if comment and isinstance(comment, basestring): 215 # @todo: support LazyT, and extract hints from 216 # S3PopupLinks or other tooltip DIVs 217 self.setstr("hint", comment) 218 219 return self.widget(field, attr), self._strings
220 221 # -------------------------------------------------------------------------
222 - def widget(self, field, attr):
223 """ 224 Render the XForms Widget. 225 226 @param field: the Field or a Storage with field information 227 @param attr: dict with XML attributes for the widget, including 228 the mandatory "ref" attribute that links the widget 229 to the data model 230 """ 231 232 # To be implemented in subclass 233 raise NotImplementedError
234 235 # -------------------------------------------------------------------------
236 - def hint(self):
237 """ 238 Render the hint for this formfield 239 240 @return: a <hint> element, or an empty tag if not available 241 """ 242 243 return self.getstr("hint", "hint", default=TAG[""]())
244 245 # -------------------------------------------------------------------------
246 - def label(self):
247 """ 248 Render the label for this formfield 249 250 @return: a <label> element, or an empty tag if not available 251 """ 252 253 return self.getstr("label", "label")
254 255 # -------------------------------------------------------------------------
256 - def setstr(self, key, string=None):
257 """ 258 Add a translatable string to this widget 259 260 @param key: the key for the string 261 @param string: the string, or None to remove the key 262 """ 263 264 ref = "%s:%s" % (self.ref, key) 265 266 strings = self._strings 267 if string: 268 if hasattr(string, "flatten"): 269 string = string.flatten() 270 strings[ref] = string 271 elif key in strings: 272 del strings[ref] 273 return
274 275 # -------------------------------------------------------------------------
276 - def getstr(self, tag, key, default=None):
277 """ 278 Get a translated string reference 279 280 @param tag: the tag to wrap the string reference 281 @param key: the key for the string 282 """ 283 284 empty = False 285 ref = "%s:%s" % (self.ref, key) 286 287 translations = self._strings 288 if ref in translations: 289 string = translations[ref] 290 elif default is not None: 291 return default 292 else: 293 ref = None 294 string = "" 295 296 widget = TAG[str(tag)] 297 298 if self.translate and ref: 299 return widget(_ref="jr:itext('%s')" % ref) 300 else: 301 return widget(string)
302
303 # ============================================================================= 304 -class S3XFormsStringWidget(S3XFormsWidget):
305 """ String Input Widget for XForms """ 306
307 - def widget(self, field, attr):
308 """ Widget renderer (parameter description see base class) """ 309 310 return TAG["input"](self.label(), self.hint(), **attr)
311
312 # ============================================================================= 313 -class S3XFormsTextWidget(S3XFormsWidget):
314 """ Text (=multi-line string) Input Widget for XForms """ 315
316 - def widget(self, field, attr):
317 """ Widget renderer (parameter description see base class) """ 318 319 return TAG["input"](self.label(), self.hint(), **attr)
320
321 # ============================================================================= 322 -class S3XFormsReadonlyWidget(S3XFormsWidget):
323 """ Read-only Widget for XForms """ 324
325 - def widget(self, field, attr):
326 """ Widget renderer (parameter description see base class) """ 327 328 attr["_readonly"] = "true" 329 attr["_default"] = s3_unicode(field.default) 330 331 return TAG["input"](self.label(), **attr)
332
333 # ============================================================================= 334 -class S3XFormsOptionsWidget(S3XFormsWidget):
335 """ Options Widget for XForms """ 336
337 - def widget(self, field, attr):
338 """ Widget renderer (parameter description see base class) """ 339 340 requires = field.requires 341 if not hasattr(requires, "options"): 342 return TAG["input"](self.label(), **attr) 343 344 items = [self.label(), self.hint()] + self.items(requires.options()) 345 return TAG["select1"](items, **attr)
346 347 # -------------------------------------------------------------------------
348 - def items(self, options):
349 """ 350 Render the items for the selector 351 352 @param options: the options, list of tuples (value, text) 353 """ 354 355 items = [] 356 setstr = self.setstr 357 getstr = self.getstr 358 for index, option in enumerate(options): 359 360 value, text = option 361 key = "option%s" % index 362 if hasattr(text, "m") or hasattr(text, "flatten"): 363 setstr(key, text) 364 text = getstr("label", key) 365 else: 366 text = TAG["label"](text) 367 368 items.append(TAG["item"](text, 369 TAG["value"](value), 370 )) 371 return items
372
373 # ============================================================================= 374 -class S3XFormsMultipleOptionsWidget(S3XFormsOptionsWidget):
375 """ Multiple Options Widget for XForms """ 376
377 - def widget(self, field, attr):
378 """ Widget renderer (parameter description see base class) """ 379 380 requires = field.requires 381 if not hasattr(requires, "options"): 382 return TAG["input"](self.label(), **attr) 383 384 items = [self.label(), self.hint()] + self.items(requires.options()) 385 return TAG["select"](items, **attr)
386
387 # ============================================================================= 388 -class S3XFormsBooleanWidget(S3XFormsWidget):
389 """ Boolean Widget for XForms """ 390
391 - def widget(self, field, attr):
392 """ Widget renderer (parameter description see base class) """ 393 394 T = current.T 395 setstr = self.setstr 396 setstr("false", T("No")) # @todo: use field-represent instead 397 setstr("true", T("Yes")) # @todo: use field-represent instead 398 399 getstr = self.getstr 400 items = [self.label(), 401 self.hint(), 402 TAG["item"](getstr("label", "true"), TAG["value"](1)), 403 TAG["item"](getstr("label", "true"), TAG["value"](0)), 404 ] 405 return TAG["select1"](items, **attr)
406
407 # ============================================================================= 408 -class S3XFormsUploadWidget(S3XFormsWidget):
409 """ Upload Widget for XForms (currently only for image-upload) """ 410
411 - def widget(self, field, attr):
412 """ Widget renderer (parameter description see base class) """ 413 414 attr["_mediatype"] = "image/*" 415 return TAG["upload"](self.label(), self.hint(), **attr)
416
417 # ============================================================================= 418 -class S3XFormsField(object):
419 """ 420 Class representing an XForms form field. 421 422 After initialization, the XForms elements for the form field 423 can be accessed via lazy properties: 424 425 - model the model node for the field 426 - binding the binding tag for the field 427 - widget the form widget 428 - strings the strings for internationalization 429 430 @todo: implement specialized widgets 431 @todo: extend constraint introspection 432 """ 433 434 # Mapping field type <=> XForms widget 435 widgets = { 436 "string": S3XFormsStringWidget, 437 "text": S3XFormsTextWidget, 438 "integer": S3XFormsStringWidget, 439 "double": S3XFormsStringWidget, 440 "decimal": S3XFormsStringWidget, 441 "time": S3XFormsStringWidget, 442 "date": S3XFormsStringWidget, 443 "datetime": S3XFormsStringWidget, 444 "upload": S3XFormsUploadWidget, 445 "boolean": S3XFormsBooleanWidget, 446 "options": S3XFormsOptionsWidget, 447 "multiple": S3XFormsMultipleOptionsWidget, 448 #"radio": S3XFormsRadioWidget, 449 #"checkboxes": S3XFormsCheckboxesWidget, 450 } 451 452 # Type mapping web2py <=> XForms 453 types = {"string": "string", 454 "double": "decimal", 455 "date": "date", 456 "datetime": "datetime", 457 "integer": "int", 458 "boolean": "boolean", 459 "upload": "binary", 460 "text": "text", 461 } 462 463 # -------------------------------------------------------------------------
464 - def __init__(self, tablename, field, translate=True):
465 """ 466 Constructor 467 468 @param tablename: the table name 469 @param field: the Field or a Storage with field information 470 @param translate: enable/disable label translation 471 """ 472 473 self.tablename = tablename 474 self.field = field 475 476 self.name = field.name 477 self.ref = "/%s/%s" % (self.tablename, self.name) 478 479 self.translate = translate 480 481 # Initialize properties 482 self._model = None 483 self._binding = None 484 self._strings = None 485 self._widget = None
486 487 # ------------------------------------------------------------------------- 488 @property
489 - def model(self):
490 """ 491 The model node for this form field (lazy property) 492 """ 493 if self._model is None: 494 self._introspect() 495 return self._model
496 497 # ------------------------------------------------------------------------- 498 @property
499 - def binding(self):
500 """ 501 The binding for this form field (lazy property) 502 """ 503 504 if self._binding is None: 505 self._introspect() 506 return self._binding
507 508 # ------------------------------------------------------------------------- 509 @property
510 - def strings(self):
511 """ 512 The dict of i18n-strings for this form field (lazy property) 513 """ 514 515 if self._strings is None: 516 self._introspect() 517 return self._strings
518 519 # ------------------------------------------------------------------------- 520 @property
521 - def widget(self):
522 """ 523 The widget for this form field (lazy property) 524 """ 525 526 if self._widget is None: 527 self._introspect() 528 return self._widget
529 530 # -------------------------------------------------------------------------
531 - def _introspect(self):
532 """ 533 Introspect the field type and constraints, generate model, 534 binding and widget and extract i18n strings. The results 535 can be accessed via the lazy properties model, binding, widget, 536 and strings. 537 538 @return: nothing 539 """ 540 541 field = self.field 542 543 # Initialize i18n strings 544 self._strings = {} 545 546 # Tag for the model 547 self._model = TAG[self.name]() 548 549 # Is the field writable/required? 550 readonly = None # "false()" 551 required = None # "false()" 552 if not field.writable: 553 readonly = "true()" 554 elif self._required(field): 555 required = "true()" 556 557 # Basic binding attributes 558 attr = {"_nodeset": self.ref, 559 "_required": required, 560 "_readonly": readonly, 561 } 562 563 # Get the xforms field type 564 fieldtype = self.types.get(str(field.type), "string") 565 566 # Introspect validators 567 requires = field.requires 568 options = False 569 multiple = False 570 if requires: 571 if not isinstance(requires, list): 572 requires = [requires] 573 # Does the field have options? 574 first = requires[0] 575 if hasattr(first, "options"): 576 options = True 577 if hasattr(first, "multiple") and first.multiple: 578 multiple = True 579 # Does the field have a minimum and/or maximum constraint? 580 elif fieldtype in ("decimal", "int", "date", "datetime"): 581 constraint = self._range(requires) 582 if constraint: 583 attr["_constraint"] = constraint 584 585 # Add the field type to binding 586 if not options: 587 attr["_type"] = fieldtype 588 589 # Binding 590 self._binding = TAG["bind"](**attr) 591 592 # Determine the widget 593 if hasattr(field, "xform"): 594 # Custom widget 595 widget = field.xform 596 else: 597 # Determine the widget type 598 if not field.writable: 599 widget_type = "readonly" 600 else: 601 widget_type = str(field.type) 602 if options: 603 if multiple: 604 widget_type = "multiple" 605 else: 606 widget_type = "options" 607 608 widgets = self.widgets 609 if widget_type in widgets: 610 widget = widgets[widget_type] 611 else: 612 widget = None 613 614 if widget is not None: 615 # Instantiate the widget if necessary 616 if isinstance(widget, type): 617 widget = widget(translate=self.translate) 618 619 # Get the label 620 if hasattr(field, "label"): 621 label = field.label 622 else: 623 label = None 624 if not label: 625 label = current.T(" ".join(s.capitalize() 626 for s in self.name.split("_")).strip()) 627 628 # Render the widget and update i18n strings 629 self._widget, strings = widget(field, label, self.ref) 630 if self.translate: 631 self._strings.update(strings) 632 else: 633 # Unsupported widget type => render an empty tag 634 self._widget = TAG[""]() 635 636 return
637 638 # ------------------------------------------------------------------------- 639 @staticmethod
640 - def _range(validators):
641 """ 642 Introspect range constraints, convert to string for binding 643 644 @param validators: a sequence of validators 645 @return: a string with the range constraints 646 """ 647 648 constraints = [] 649 650 for validator in validators: 651 if hasattr(validator, "other"): 652 v = validator.other 653 else: 654 v = validator 655 if isinstance(v, (IS_INT_IN_RANGE, IS_FLOAT_IN_RANGE)): 656 maximum = v.maximum 657 if maximum is not None: 658 constraints.append(". < %s" % maximum) 659 minimum = v.minimum 660 if minimum is not None: 661 constraints.append(". > %s" % minimum) 662 if constraints: 663 return "(%s)" % " and ".join(constraints) 664 return None
665 666 # ------------------------------------------------------------------------- 667 @staticmethod
668 - def _required(field):
669 """ 670 Determine whether field is required 671 672 @param field: the Field or a Storage with field information 673 @return: True if field is required, else False 674 """ 675 676 required = False 677 validators = field.requires 678 if isinstance(validators, IS_EMPTY_OR): 679 required = False 680 else: 681 required = field.required or field.notnull 682 if not required and validators: 683 if not isinstance(validators, (list, tuple)): 684 validators = [validators] 685 for v in validators: 686 if hasattr(v, "options"): 687 if hasattr(v, "zero") and v.zero is None: 688 continue 689 if hasattr(v, "mark_required"): 690 if v.mark_required: 691 required = True 692 break 693 else: 694 continue 695 try: 696 val, error = v("") 697 except TypeError: 698 # default validator takes no args 699 pass 700 else: 701 if error: 702 required = True 703 break 704 return required
705
706 # ============================================================================= 707 -class S3XFormsForm(object):
708 """ XForms Form Generator """ 709
710 - def __init__(self, table, name=None, translate=True):
711 """ 712 Constructor 713 714 @param table: the database table 715 @param name: optional alternative form name 716 @param translate: enable/disable translation of strings 717 """ 718 719 self.table = table 720 721 if name: 722 self.name = name 723 elif hasattr(table, "_tablename"): 724 self.name = table._tablename 725 else: 726 self.name = str(table) 727 728 # Initialize the form fields 729 fields = [] 730 append = fields.append 731 for field in table: 732 if field.readable: 733 append(S3XFormsField(table, field, translate=translate)) 734 self._fields = fields 735 736 self.translate = translate
737 738 # -------------------------------------------------------------------------
739 - def xml(self):
740 """ 741 Render this form as XML string 742 743 @return: XML as string 744 """ 745 746 ns = {"_xmlns": "http://www.w3.org/2002/xforms", 747 "_xmlns:h": "http://www.w3.org/1999/xhtml", 748 "_xmlns:ev": "http://www.w3.org/2001/xml-events", 749 "_xmlns:xsd": "http://www.w3.org/2001/XMLSchema", 750 "_xmlns:jr": "http://openrosa.org/javarosa", 751 } 752 753 document = TAG["h:html"](self._head(), self._body(), **ns) 754 return document.xml()
755 756 # -------------------------------------------------------------------------
757 - def _head(self):
758 """ 759 Get the HTML head for this form 760 761 @return: an <h:head> tag 762 """ 763 764 # @todo: introspect title (in caller, then pass-in) 765 title = TAG["h:title"](self.name) 766 model = self._model() 767 768 return TAG["h:head"](title, model)
769 770 # -------------------------------------------------------------------------
771 - def _body(self):
772 """ 773 Get the HTML body for this form 774 775 @return: an <h:body> tag 776 """ 777 778 widgets = self._widgets() 779 return TAG["h:body"](widgets)
780 781 # -------------------------------------------------------------------------
782 - def _model(self):
783 """ 784 Get the model for this form 785 786 @return: a <model> tag with instance, bindings and i18n strings 787 """ 788 789 instance = self._instance() 790 bindings = self._bindings() 791 translations = self._translations() 792 793 794 return TAG["model"](instance, 795 bindings, 796 translations, 797 )
798 799 # -------------------------------------------------------------------------
800 - def _instance(self):
801 """ 802 Get the instance for this form 803 804 @return: an <instance> tag with all form field nodes 805 """ 806 807 nodes = [] 808 append = nodes.append 809 810 for field in self._fields: 811 append(field.model) 812 813 return TAG["instance"](TAG[self.name](nodes), _id=self.name)
814 815 # -------------------------------------------------------------------------
816 - def _bindings(self):
817 """ 818 Get the bindings for this form 819 820 @return: a TAG with the bindings 821 """ 822 823 bindings = [] 824 append = bindings.append 825 826 for field in self._fields: 827 append(field.binding) 828 829 return TAG[""](bindings)
830 831 # -------------------------------------------------------------------------
832 - def _widgets(self):
833 """ 834 Get the widgets for this form 835 836 @return: a TAG with the widgets 837 """ 838 839 strings = self._strings 840 841 widgets = [] 842 append = widgets.append 843 844 fields = self._fields 845 for field in self._fields: 846 append(field.widget) 847 return TAG[""](widgets)
848 849 # -------------------------------------------------------------------------
850 - def _strings(self):
851 """ 852 Get a dict with all i18n strings for this form 853 854 @return: a dict {key: string} 855 """ 856 857 strings = {} 858 if self.translate: 859 update = strings.update 860 for field in self._fields: 861 update(field.strings) 862 return strings
863 864 # -------------------------------------------------------------------------
865 - def _translations(self):
866 """ 867 Render the translations for all configured languages 868 869 @returns: translation tags 870 """ 871 872 T = current.T 873 translations = TAG[""]() 874 875 strings = self._strings() 876 if self.translate and strings: 877 append_translation = translations.append 878 879 languages = [l for l in current.response.s3.l10n_languages 880 if l != "en"] 881 languages.insert(0, "en") 882 for language in languages: 883 translation = TAG["translation"](_lang=language) 884 append_string = translation.append 885 for key, string in strings.items(): 886 tstr = T(string.m if hasattr(string, "m") else string, 887 language = language, 888 ) 889 append_string(TAG["text"](TAG["value"](tstr), _id=key)) 890 891 if len(translation): 892 append_translation(translation) 893 894 return TAG["itext"](translations)
895 896 # END ========================================================================= 897