| Home | Trees | Indices | Help |
|
|---|
|
|
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 # -------------------------------------------------------------------------
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 # -------------------------------------------------------------------------
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
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
182 """
183 Constructor
184
185 @param translate: enable/disable label translation
186 """
187
188 self.translate = translate
189 self._strings = {}
190
191 # -------------------------------------------------------------------------
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 # -------------------------------------------------------------------------
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 # -------------------------------------------------------------------------
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 # -------------------------------------------------------------------------
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 # -------------------------------------------------------------------------
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 # -------------------------------------------------------------------------
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):
311
312 # =============================================================================
313 -class S3XFormsTextWidget(S3XFormsWidget):
320
321 # =============================================================================
322 -class S3XFormsReadonlyWidget(S3XFormsWidget):
323 """ Read-only Widget for XForms """
324
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
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 # -------------------------------------------------------------------------
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
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
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):
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 # -------------------------------------------------------------------------
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
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
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
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
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 # -------------------------------------------------------------------------
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
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
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
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 # -------------------------------------------------------------------------
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 # -------------------------------------------------------------------------
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 # -------------------------------------------------------------------------
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 # -------------------------------------------------------------------------
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 # -------------------------------------------------------------------------
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 # -------------------------------------------------------------------------
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 # -------------------------------------------------------------------------
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 # -------------------------------------------------------------------------
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 # -------------------------------------------------------------------------
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
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Fri Mar 15 08:51:53 2019 | http://epydoc.sourceforge.net |