| Home | Trees | Indices | Help |
|
|---|
|
|
1 # -*- coding: utf-8 -*-
2
3 """ S3 SQL Forms
4
5 @copyright: 2012-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__ = ("S3SQLCustomForm",
31 "S3SQLDefaultForm",
32 "S3SQLDummyField",
33 "S3SQLVirtualField",
34 "S3SQLSubFormLayout",
35 "S3SQLVerticalSubFormLayout",
36 "S3SQLInlineComponent",
37 "S3SQLInlineLink",
38 )
39
40 import json
41
42 from itertools import chain
43
44 from gluon import *
45 from gluon.storage import Storage
46 from gluon.sqlhtml import StringWidget
47 from gluon.tools import callback
48 from gluon.validators import Validator
49
50 from s3dal import original_tablename
51 from s3query import FS
52 from s3utils import s3_mark_required, s3_store_last_record_id, s3_str, s3_validate
53 from s3widgets import S3Selector, S3UploadWidget
54
55 # Compact JSON encoding
56 SEPARATORS = (",", ":")
57 DEFAULT = lambda: None
58
59 # =============================================================================
60 -class S3SQLForm(object):
61 """ SQL Form Base Class"""
62
63 # -------------------------------------------------------------------------
65 """
66 Constructor to define the form and its elements.
67
68 @param elements: the form elements
69 @param attributes: form attributes
70 """
71
72 self.elements = []
73 append = self.elements.append
74
75 debug = current.deployment_settings.get_base_debug()
76 for element in elements:
77 if not element:
78 continue
79 if isinstance(element, S3SQLFormElement):
80 append(element)
81 elif isinstance(element, str):
82 append(S3SQLField(element))
83 elif isinstance(element, tuple):
84 l = len(element)
85 if l > 1:
86 label, selector = element[:2]
87 widget = element[2] if l > 2 else DEFAULT
88 else:
89 selector = element[0]
90 label = widget = DEFAULT
91 append(S3SQLField(selector, label=label, widget=widget))
92 else:
93 msg = "Invalid form element: %s" % str(element)
94 if debug:
95 raise SyntaxError(msg)
96 else:
97 current.log.error(msg)
98
99 opts = {}
100 attr = {}
101 for k in attributes:
102 value = attributes[k]
103 if k[:1] == "_":
104 attr[k] = value
105 else:
106 opts[k] = value
107
108 self.attr = attr
109 self.opts = opts
110
111 # -------------------------------------------------------------------------
112 # Rendering/Processing
113 # -------------------------------------------------------------------------
114 - def __call__(self,
115 request=None,
116 resource=None,
117 record_id=None,
118 readonly=False,
119 message="Record created/updated",
120 format=None,
121 **options):
122 """
123 Render/process the form. To be implemented in subclass.
124
125 @param request: the S3Request
126 @param resource: the target S3Resource
127 @param record_id: the record ID
128 @param readonly: render the form read-only
129 @param message: message upon successful form submission
130 @param format: data format extension (for audit)
131 @param options: keyword options for the form
132
133 @return: a FORM instance
134 """
135
136 return None
137
138 # -------------------------------------------------------------------------
139 # Utility functions
140 # -------------------------------------------------------------------------
147
148 # -------------------------------------------------------------------------
150 """
151 Get a configuration setting for the current table
152
153 @param key: the setting key
154 @param default: fallback value if the setting is not available
155 """
156
157 tablename = self.tablename
158 if tablename:
159 return current.s3db.get_config(tablename, key, default)
160 else:
161 return default
162
163 # -------------------------------------------------------------------------
164 @staticmethod
232
233 # -------------------------------------------------------------------------
234 @staticmethod
236 """
237 Insert subheadings into forms
238
239 @param form: the form
240 @param tablename: the tablename
241 @param formstyle: the formstyle
242 @param subheadings:
243 {"fieldname": "Heading"} or {"fieldname": ["Heading1", "Heading2"]}
244 """
245
246 if not subheadings:
247 return
248 if tablename in subheadings:
249 subheadings = subheadings.get(tablename)
250 if formstyle.__name__ in ("formstyle_table",
251 "formstyle_table_inline",
252 ):
253 def create_subheading(represent, tablename, f, level=""):
254 return TR(TD(represent, _colspan=3,
255 _class="subheading",
256 ),
257 _class = "subheading",
258 _id = "%s_%s__subheading%s" % (tablename, f, level),
259 )
260 else:
261 def create_subheading(represent, tablename, f, level=""):
262 return DIV(represent,
263 _class = "subheading",
264 _id = "%s_%s__subheading%s" % (tablename, f, level),
265 )
266
267 form_rows = iter(form[0])
268 tr = form_rows.next()
269 i = 0
270 while tr:
271 # @ToDo: We need a better way of working than this!
272 f = tr.attributes.get("_id", None)
273 if not f:
274 try:
275 # DIV-based form-style
276 f = tr[0][0].attributes.get("_id", None)
277 if not f:
278 # DRRPP formstyle
279 f = tr[0][0][1][0].attributes.get("_id", None)
280 if not f:
281 # Date fields are inside an extra TAG()
282 f = tr[0][0][1][0][0].attributes.get("_id", None)
283 except:
284 # Something else
285 f = None
286 if f:
287 if f.endswith("__row"):
288 f = f[:-5]
289 if f.startswith(tablename):
290 f = f[len(tablename) + 1:] # : -6
291 if f.startswith("sub_"):
292 # Component
293 f = f[4:]
294 elif f.startswith("sub-default"):
295 # S3SQLInlineComponent[CheckBox]
296 f = f[11:]
297 elif f.startswith("sub_"):
298 # S3GroupedOptionsWidget
299 f = f[4:]
300 headings = subheadings.get(f)
301 if not headings:
302 try:
303 tr = form_rows.next()
304 except StopIteration:
305 break
306 else:
307 i += 1
308 continue
309 if not isinstance(headings, list):
310 headings = [headings]
311 inserted = 0
312 for heading in headings:
313 subheading = create_subheading(heading, tablename, f, inserted if inserted else "")
314 form[0].insert(i, subheading)
315 i += 1
316 inserted += 1
317 if inserted:
318 tr.attributes.update(_class="%s after_subheading" % tr.attributes["_class"])
319 for _i in range(0, inserted):
320 # Iterate over the rows we just created
321 tr = form_rows.next()
322 try:
323 tr = form_rows.next()
324 except StopIteration:
325 break
326 else:
327 i += 1
328
329 # =============================================================================
330 -class S3SQLDefaultForm(S3SQLForm):
331 """ Standard SQL form """
332
333 # -------------------------------------------------------------------------
334 # Rendering/Processing
335 # -------------------------------------------------------------------------
336 - def __call__(self,
337 request=None,
338 resource=None,
339 record_id=None,
340 readonly=False,
341 message="Record created/updated",
342 format=None,
343 **options):
344 """
345 Render/process the form.
346
347 @param request: the S3Request
348 @param resource: the target S3Resource
349 @param record_id: the record ID
350 @param readonly: render the form read-only
351 @param message: message upon successful form submission
352 @param format: data format extension (for audit)
353 @param options: keyword options for the form
354
355 @todo: describe keyword arguments
356
357 @return: a FORM instance
358 """
359
360 if resource is None:
361 self.resource = request.resource
362 self.prefix, self.name, self.table, self.tablename = \
363 request.target()
364 else:
365 self.resource = resource
366 self.prefix = resource.prefix
367 self.name = resource.name
368
369 self.tablename = resource.tablename
370 self.table = resource.table
371
372 response = current.response
373 s3 = response.s3
374 settings = s3.crud
375
376 prefix = self.prefix
377 name = self.name
378 tablename = self.tablename
379 table = self.table
380
381 record = None
382 labels = None
383
384 self.record_id = record_id
385
386 if not readonly:
387 _get = options.get
388
389 # Pre-populate create-form?
390 if record_id is None:
391 data = _get("data", None)
392 from_table = _get("from_table", None)
393 from_record = _get("from_record", None)
394 map_fields = _get("map_fields", None)
395 record = self.prepopulate(from_table=from_table,
396 from_record=from_record,
397 map_fields=map_fields,
398 data=data,
399 format=format)
400
401 # De-duplicate link table entries
402 self.record_id = record_id = self.deduplicate_link(request, record_id)
403
404 # Add asterisk to labels of required fields
405 mark_required = self._config("mark_required", default=[])
406 labels, required = s3_mark_required(table, mark_required)
407 if required:
408 # Show the key if there are any required fields.
409 s3.has_required = True
410 else:
411 s3.has_required = False
412
413 # Determine form style
414 if format == "plain":
415 # Default formstyle works best when we have no formatting
416 formstyle = "table3cols"
417 elif readonly:
418 formstyle = settings.formstyle_read
419 else:
420 formstyle = settings.formstyle
421
422 # Submit buttons
423 buttons = self._submit_buttons(readonly)
424
425 # Generate the form
426 if record is None:
427 record = record_id
428 response.form_label_separator = ""
429 form = SQLFORM(table,
430 record = record,
431 record_id = record_id,
432 readonly = readonly,
433 comments = not readonly,
434 deletable = False,
435 showid = False,
436 upload = s3.download_url,
437 labels = labels,
438 formstyle = formstyle,
439 separator = "",
440 submit_button = settings.submit_button,
441 buttons = buttons)
442
443 # Style the Submit button, if-requested
444 if settings.submit_style and not settings.custom_submit:
445 try:
446 form[0][-1][0][0]["_class"] = settings.submit_style
447 except:
448 # Submit button has been removed or a different formstyle,
449 # such as Bootstrap (which is already styled anyway)
450 pass
451
452 # Subheadings
453 subheadings = options.get("subheadings", None)
454 if subheadings:
455 self._insert_subheadings(form, tablename, formstyle, subheadings)
456
457 # Process the form
458 logged = False
459 if not readonly:
460 link = _get("link")
461 hierarchy = _get("hierarchy")
462 onvalidation = _get("onvalidation")
463 onaccept = _get("onaccept")
464 success, error = self.process(form,
465 request.post_vars,
466 onvalidation = onvalidation,
467 onaccept = onaccept,
468 hierarchy = hierarchy,
469 link = link,
470 http = request.http,
471 format = format,
472 )
473 if success:
474 response.confirmation = message
475 logged = True
476 elif error:
477 response.error = error
478
479 # Audit read
480 if not logged and not form.errors:
481 current.audit("read", prefix, name,
482 record=record_id, representation=format)
483
484 return form
485
486 # -------------------------------------------------------------------------
487 - def prepopulate(self,
488 from_table=None,
489 from_record=None,
490 map_fields=None,
491 data=None,
492 format=None):
493 """
494 Pre-populate the form with values from a previous record or
495 controller-submitted data
496
497 @param from_table: the table to copy the data from
498 @param from_record: the record to copy the data from
499 @param map_fields: field selection/mapping
500 @param data: the data to prepopulate the form with
501 @param format: the request format extension
502 """
503
504 table = self.table
505 record = None
506
507 # Pre-populate from a previous record?
508 if from_table is not None:
509
510 # Field mapping
511 if map_fields:
512 if isinstance(map_fields, dict):
513 # Map fields with other names
514 fields = [from_table[map_fields[f]]
515 for f in map_fields
516 if f in table.fields and
517 map_fields[f] in from_table.fields and
518 table[f].writable]
519
520 elif isinstance(map_fields, (list, tuple)):
521 # Only use a subset of the fields
522 fields = [from_table[f]
523 for f in map_fields
524 if f in table.fields and
525 f in from_table.fields and
526 table[f].writable]
527 else:
528 raise TypeError
529 else:
530 # Use all writable fields
531 fields = [from_table[f]
532 for f in table.fields
533 if f in from_table.fields and
534 table[f].writable]
535
536 # Audit read => this is a read method, after all
537 prefix, name = from_table._tablename.split("_", 1)
538 current.audit("read", prefix, name,
539 record=from_record, representation=format)
540
541 # Get original record
542 query = (from_table.id == from_record)
543 row = current.db(query).select(limitby=(0, 1), *fields).first()
544 if row:
545 if isinstance(map_fields, dict):
546 record = Storage([(f, row[map_fields[f]])
547 for f in map_fields])
548 else:
549 record = Storage(row)
550
551 # Pre-populate from call?
552 elif isinstance(data, dict):
553 record = Storage([(f, data[f])
554 for f in data
555 if f in table.fields and
556 table[f].writable])
557
558 # Add missing fields to pre-populated record
559 if record:
560 missing_fields = Storage()
561 for f in table.fields:
562 if f not in record and table[f].writable:
563 missing_fields[f] = table[f].default
564 record.update(missing_fields)
565 record[table._id.name] = None
566
567 return record
568
569 # -------------------------------------------------------------------------
571 """
572 Change to update if this request attempts to create a
573 duplicate entry in a link table
574
575 @param request: the request
576 @param record_id: the record ID
577 """
578
579 linked = self.resource.linked
580 table = self.table
581
582 session = current.session
583
584 if request.env.request_method == "POST" and linked is not None:
585 pkey = table._id.name
586 post_vars = request.post_vars
587 if not post_vars[pkey]:
588
589 lkey = linked.lkey
590 rkey = linked.rkey
591
592 def parse_key(value):
593 key = s3_str(value)
594 if key.startswith("{"):
595 # JSON-based selector (e.g. S3LocationSelector)
596 return json.loads(key).get("id")
597 else:
598 # Normal selector (e.g. OptionsWidget)
599 return value
600
601 try:
602 lkey_ = parse_key(post_vars[lkey])
603 rkey_ = parse_key(post_vars[rkey])
604 except Exception:
605 return record_id
606
607 query = (table[lkey] == lkey_) & (table[rkey] == rkey_)
608 row = current.db(query).select(table._id, limitby=(0, 1)).first()
609 if row is not None:
610 tablename = self.tablename
611 record_id = row[pkey]
612 formkey = session.get("_formkey[%s/None]" % tablename)
613 formname = "%s/%s" % (tablename, record_id)
614 session["_formkey[%s]" % formname] = formkey
615 post_vars["_formname"] = formname
616 post_vars[pkey] = record_id
617
618 return record_id
619
620 # -------------------------------------------------------------------------
621 - def process(self, form, vars,
622 onvalidation = None,
623 onaccept = None,
624 hierarchy = None,
625 link = None,
626 http = "POST",
627 format = None,
628 ):
629 """
630 Process the form
631
632 @param form: FORM instance
633 @param vars: request POST variables
634 @param onvalidation: callback(function) upon successful form validation
635 @param onaccept: callback(function) upon successful form acceptance
636 @param hierarchy: the data for the hierarchy link to create
637 @param link: component link
638 @param http: HTTP method
639 @param format: request extension
640
641 """
642
643 table = self.table
644 tablename = self.tablename
645
646 # Get the proper onvalidation routine
647 if isinstance(onvalidation, dict):
648 onvalidation = onvalidation.get(tablename, [])
649
650 # Append link.postprocess to onvalidation
651 if link and link.postprocess:
652 postprocess = link.postprocess
653 if isinstance(onvalidation, list):
654 onvalidation.insert(0, postprocess)
655 elif onvalidation is not None:
656 onvalidation = [postprocess, onvalidation]
657 else:
658 onvalidation = [postprocess]
659
660 success = True
661 error = None
662
663 record_id = self.record_id
664 formname = "%s/%s" % (tablename, record_id)
665 if form.accepts(vars,
666 current.session,
667 formname=formname,
668 onvalidation=onvalidation,
669 keepvalues=False,
670 hideerror=False):
671
672 # Undelete?
673 if vars.get("_undelete"):
674 undelete = form.vars.get("deleted") is False
675 else:
676 undelete = False
677
678 # Audit
679 prefix = self.prefix
680 name = self.name
681 if record_id is None or undelete:
682 current.audit("create", prefix, name, form=form,
683 representation=format)
684 else:
685 current.audit("update", prefix, name, form=form,
686 record=record_id, representation=format)
687
688 form_vars = form.vars
689
690 # Update super entity links
691 s3db = current.s3db
692 s3db.update_super(table, form_vars)
693
694 # Update component link
695 if link and link.postprocess is None:
696 resource = link.resource
697 master = link.master
698 resource.update_link(master, form_vars)
699
700 if form_vars.id:
701 if record_id is None or undelete:
702 # Create hierarchy link
703 if hierarchy:
704 from s3hierarchy import S3Hierarchy
705 h = S3Hierarchy(tablename)
706 if h.config:
707 h.postprocess_create_node(hierarchy, form_vars)
708 # Set record owner
709 auth = current.auth
710 auth.s3_set_record_owner(table, form_vars.id)
711 auth.s3_make_session_owner(table, form_vars.id)
712 else:
713 # Update realm
714 update_realm = s3db.get_config(table, "update_realm")
715 if update_realm:
716 current.auth.set_realm_entity(table, form_vars,
717 force_update=True)
718 # Store session vars
719 self.resource.lastid = str(form_vars.id)
720 s3_store_last_record_id(tablename, form_vars.id)
721
722 # Execute onaccept
723 try:
724 callback(onaccept, form, tablename=tablename)
725 except:
726 error = "onaccept failed: %s" % str(onaccept)
727 current.log.error(error)
728 # This is getting swallowed
729 raise
730
731 else:
732 success = False
733
734 if form.errors:
735
736 # Revert any records created within widgets/validators
737 current.db.rollback()
738
739 # IS_LIST_OF validation errors need special handling
740 errors = []
741 for fieldname in form.errors:
742 if fieldname in table:
743 if isinstance(table[fieldname].requires, IS_LIST_OF):
744 errors.append("%s: %s" % (fieldname,
745 form.errors[fieldname]))
746 else:
747 errors.append(str(form.errors[fieldname]))
748 if errors:
749 error = "\n".join(errors)
750
751 elif http == "POST":
752
753 # Invalid form
754 error = current.T("Invalid form (re-opened in another window?)")
755
756 return success, error
757
758 # =============================================================================
759 -class S3SQLCustomForm(S3SQLForm):
760 """ Custom SQL Form """
761
762 # -------------------------------------------------------------------------
764 """
765 S.insert(index, object) -- insert object before index
766 """
767
768 if not element:
769 return
770 if isinstance(element, S3SQLFormElement):
771 self.elements.insert(index, element)
772 elif isinstance(element, str):
773 self.elements.insert(index, S3SQLField(element))
774 elif isinstance(element, tuple):
775 l = len(element)
776 if l > 1:
777 label, selector = element[:2]
778 widget = element[2] if l > 2 else DEFAULT
779 else:
780 selector = element[0]
781 label = widget = DEFAULT
782 self.elements.insert(index, S3SQLField(selector, label=label, widget=widget))
783 else:
784 msg = "Invalid form element: %s" % str(element)
785 if current.deployment_settings.get_base_debug():
786 raise SyntaxError(msg)
787 else:
788 current.log.error(msg)
789
790 # -------------------------------------------------------------------------
792 """
793 S.append(object) -- append object to the end of the sequence
794 """
795
796 self.insert(len(self), element)
797
798 # -------------------------------------------------------------------------
799 # Rendering/Processing
800 # -------------------------------------------------------------------------
801 - def __call__(self,
802 request=None,
803 resource=None,
804 record_id=None,
805 readonly=False,
806 message="Record created/updated",
807 format=None,
808 **options):
809 """
810 Render/process the form.
811
812 @param request: the S3Request
813 @param resource: the target S3Resource
814 @param record_id: the record ID
815 @param readonly: render the form read-only
816 @param message: message upon successful form submission
817 @param format: data format extension (for audit)
818 @param options: keyword options for the form
819
820 @return: a FORM instance
821 """
822
823 db = current.db
824 response = current.response
825 s3 = response.s3
826
827 # Determine the target resource
828 if resource is None:
829 resource = request.resource
830 self.prefix, self.name, self.table, self.tablename = \
831 request.target()
832 else:
833 self.prefix = resource.prefix
834 self.name = resource.name
835 self.tablename = resource.tablename
836 self.table = resource.table
837 self.resource = resource
838
839 # Resolve all form elements against the resource
840 subtables = set()
841 subtable_fields = {}
842 fields = []
843 components = []
844
845 for element in self.elements:
846 alias, name, field = element.resolve(resource)
847
848 if isinstance(alias, str):
849 subtables.add(alias)
850
851 if field is not None:
852 fields_ = subtable_fields.get(alias)
853 if fields_ is None:
854 fields_ = []
855 fields_.append((name, field))
856 subtable_fields[alias] = fields_
857
858 elif isinstance(alias, S3SQLFormElement):
859 components.append(alias)
860
861 if field is not None:
862 fields.append((alias, name, field))
863
864 self.subtables = subtables
865 self.components = components
866
867 rcomponents = resource.components
868
869 # Customise subtables
870 if subtables:
871 if not request:
872 # Create dummy S3Request
873 from s3rest import S3Request
874 r = S3Request(resource.prefix,
875 resource.name,
876 # Current request args/vars could be in a different
877 # resource context, so must override them here:
878 args = [],
879 get_vars = {},
880 )
881 else:
882 r = request
883
884 customise_resource = current.deployment_settings.customise_resource
885 for alias in subtables:
886
887 # Get tablename
888 component = rcomponents.get(alias)
889 if not component:
890 continue
891 tablename = component.tablename
892
893 # Run customise_resource
894 customise = customise_resource(tablename)
895 if customise:
896 customise(r, tablename)
897
898 # Apply customised attributes to renamed fields
899 # => except default, label, requires and widget, which can be overridden
900 # in S3SQLField.resolve instead
901 renamed_fields = subtable_fields.get(alias)
902 if renamed_fields:
903 table = component.table
904 for name, renamed_field in renamed_fields:
905 original_field = table[name]
906 for attr in ("comment",
907 "default",
908 "readable",
909 "represent",
910 "requires",
911 "update",
912 "writable",
913 ):
914 setattr(renamed_field,
915 attr,
916 getattr(original_field, attr),
917 )
918
919 # Mark required fields with asterisk
920 if not readonly:
921 mark_required = self._config("mark_required", default=[])
922 labels, required = s3_mark_required(self.table, mark_required)
923 if required:
924 # Show the key if there are any required fields.
925 s3.has_required = True
926 else:
927 s3.has_required = False
928 else:
929 labels = None
930
931 # Choose formstyle
932 settings = s3.crud
933 if format == "plain":
934 # Simple formstyle works best when we have no formatting
935 formstyle = "table3cols"
936 elif readonly:
937 formstyle = settings.formstyle_read
938 else:
939 formstyle = settings.formstyle
940
941 # Retrieve the record
942 record = None
943 if record_id is not None:
944 query = (self.table._id == record_id)
945 # @ToDo: limit fields (at least not meta)
946 record = db(query).select(limitby=(0, 1)).first()
947 self.record_id = record_id
948 self.subrows = Storage()
949
950 # Populate the form
951 data = None
952 noupdate = []
953 forbidden = []
954 has_permission = current.auth.s3_has_permission
955
956 if record is not None:
957
958 # Retrieve the subrows
959 subrows = self.subrows
960 for alias in subtables:
961
962 # Get the join for this subtable
963 component = rcomponents.get(alias)
964 if not component or component.multiple:
965 continue
966 join = component.get_join()
967 q = query & join
968
969 # Retrieve the row
970 # @todo: Should not need .ALL here
971 row = db(q).select(component.table.ALL,
972 limitby = (0, 1),
973 ).first()
974
975 # Check permission for this subrow
976 ctname = component.tablename
977 if not row:
978 permitted = has_permission("create", ctname)
979 if not permitted:
980 forbidden.append(alias)
981 continue
982 else:
983 cid = row[component.table._id]
984 permitted = has_permission("read", ctname, cid)
985 if not permitted:
986 forbidden.append(alias)
987 continue
988 permitted = has_permission("update", ctname, cid)
989 if not permitted:
990 noupdate.append(alias)
991
992 # Add the row to the subrows
993 subrows[alias] = row
994
995 # Build the data Storage for the form
996 pkey = self.table._id
997 data = Storage({pkey.name:record[pkey]})
998 for alias, name, field in fields:
999
1000 if alias is None:
1001 # Field in the master table
1002 if name in record:
1003 value = record[name]
1004 # Field Method?
1005 if callable(value):
1006 value = value()
1007 data[field.name] = value
1008
1009 elif alias in subtables:
1010 # Field in a subtable
1011 if alias in subrows and \
1012 subrows[alias] is not None and \
1013 name in subrows[alias]:
1014 data[field.name] = subrows[alias][name]
1015
1016 elif hasattr(alias, "extract"):
1017 # Form element with custom extraction method
1018 data[field.name] = alias.extract(resource, record_id)
1019
1020 else:
1021 # Record does not exist
1022 self.record_id = record_id = None
1023
1024 # Check create-permission for subtables
1025 for alias in subtables:
1026 component = rcomponents.get(alias)
1027 if not component:
1028 continue
1029 permitted = has_permission("create", component.tablename)
1030 if not permitted:
1031 forbidden.append(alias)
1032
1033 # Apply permissions for subtables
1034 fields = [f for f in fields if f[0] not in forbidden]
1035 for a, n, f in fields:
1036 if a:
1037 if a in noupdate:
1038 f.writable = False
1039 if labels is not None and f.name not in labels:
1040 if f.required:
1041 flabels = s3_mark_required([f], mark_required=[f])[0]
1042 labels[f.name] = flabels[f.name]
1043 elif f.label:
1044 labels[f.name] = "%s:" % f.label
1045 else:
1046 labels[f.name] = ""
1047
1048 if readonly:
1049 # Strip all comments
1050 for a, n, f in fields:
1051 f.comment = None
1052 else:
1053 # Mark required subtable-fields (retaining override-labels)
1054 for alias in subtables:
1055 component = rcomponents.get(alias)
1056 if not component:
1057 continue
1058 mark_required = component.get_config("mark_required", [])
1059 ctable = component.table
1060 sfields = dict((n, (f.name, f.label))
1061 for a, n, f in fields
1062 if a == alias and n in ctable)
1063 slabels = s3_mark_required([ctable[n] for n in sfields],
1064 mark_required=mark_required,
1065 map_names=sfields)[0]
1066 if labels:
1067 labels.update(slabels)
1068 else:
1069 labels = slabels
1070
1071 self.subtables = [s for s in self.subtables if s not in forbidden]
1072
1073 # Aggregate the form fields
1074 formfields = [f[-1] for f in fields]
1075
1076 # Submit buttons
1077 buttons = self._submit_buttons(readonly)
1078
1079 # Render the form
1080 tablename = self.tablename
1081 response.form_label_separator = ""
1082 form = SQLFORM.factory(record = data,
1083 showid = False,
1084 labels = labels,
1085 formstyle = formstyle,
1086 table_name = tablename,
1087 upload = s3.download_url,
1088 readonly = readonly,
1089 separator = "",
1090 submit_button = settings.submit_button,
1091 buttons = buttons,
1092 *formfields)
1093
1094 # Style the Submit button, if-requested
1095 if settings.submit_style and not settings.custom_submit:
1096 try:
1097 form[0][-1][0][0]["_class"] = settings.submit_style
1098 except:
1099 # Submit button has been removed or a different formstyle,
1100 # such as Bootstrap (which is already styled anyway)
1101 pass
1102
1103 # Subheadings
1104 subheadings = options.get("subheadings", None)
1105 if subheadings:
1106 self._insert_subheadings(form, tablename, formstyle, subheadings)
1107
1108 # Process the form
1109 formname = "%s/%s" % (tablename, record_id)
1110 post_vars = request.post_vars
1111 if form.accepts(post_vars,
1112 current.session,
1113 onvalidation = self.validate,
1114 formname = formname,
1115 keepvalues = False,
1116 hideerror = False,
1117 ):
1118
1119 # Undelete?
1120 if post_vars.get("_undelete"):
1121 undelete = post_vars.get("deleted") is False
1122 else:
1123 undelete = False
1124
1125 link = options.get("link")
1126 hierarchy = options.get("hierarchy")
1127 self.accept(form,
1128 format = format,
1129 link = link,
1130 hierarchy = hierarchy,
1131 undelete = undelete,
1132 )
1133 # Post-process the form submission after all records have
1134 # been accepted and linked together (self.accept() has
1135 # already updated the form data with any new keys here):
1136 postprocess = self.opts.get("postprocess", None)
1137 if postprocess:
1138 try:
1139 callback(postprocess, form, tablename=tablename)
1140 except:
1141 error = "postprocess failed: %s" % postprocess
1142 current.log.error(error)
1143 raise
1144 response.confirmation = message
1145
1146 if form.errors:
1147 # Revert any records created within widgets/validators
1148 db.rollback()
1149
1150 response.error = current.T("There are errors in the form, please check your input")
1151
1152 return form
1153
1154 # -------------------------------------------------------------------------
1156 """
1157 Run the onvalidation callbacks for the master table
1158 and all subtables in the form, and store any errors
1159 in the form.
1160
1161 @param form: the form
1162 """
1163
1164 s3db = current.s3db
1165 config = self._config
1166
1167 # Validate against the main table
1168 if self.record_id:
1169 onvalidation = config("update_onvalidation",
1170 config("onvalidation", None))
1171 else:
1172 onvalidation = config("create_onvalidation",
1173 config("onvalidation", None))
1174 if onvalidation is not None:
1175 try:
1176 callback(onvalidation, form, tablename=self.tablename)
1177 except:
1178 error = "onvalidation failed: %s" % str(onvalidation)
1179 current.log.error(error)
1180 raise
1181
1182 # Validate against all subtables
1183 get_config = s3db.get_config
1184 for alias in self.subtables:
1185
1186 # Extract the subtable data
1187 subdata = self._extract(form, alias)
1188 if not subdata:
1189 continue
1190
1191 # Get the onvalidation callback for this subtable
1192 subtable = self.resource.components[alias].table
1193 subform = Storage(vars=subdata, errors=Storage())
1194
1195 rows = self.subrows
1196 if alias in rows and rows[alias] is not None:
1197 # Add the record ID for update-onvalidation
1198 pkey = subtable._id
1199 subform.vars[pkey.name] = rows[alias][pkey]
1200 subonvalidation = get_config(subtable._tablename,
1201 "update_onvalidation",
1202 get_config(subtable._tablename,
1203 "onvalidation", None))
1204 else:
1205 subonvalidation = get_config(subtable._tablename,
1206 "create_onvalidation",
1207 get_config(subtable._tablename,
1208 "onvalidation", None))
1209
1210 # Validate against the subtable, store errors in form
1211 if subonvalidation is not None:
1212 try:
1213 callback(subonvalidation, subform,
1214 tablename = subtable._tablename)
1215 except:
1216 error = "onvalidation failed: %s" % str(subonvalidation)
1217 current.log.error(error)
1218 raise
1219 for fn in subform.errors:
1220 dummy = "sub_%s_%s" % (alias, fn)
1221 form.errors[dummy] = subform.errors[fn]
1222
1223 # Validate components (e.g. Inline-Forms)
1224 for component in self.components:
1225 if hasattr(component, "validate"):
1226 component.validate(form)
1227
1228 return
1229
1230 # -------------------------------------------------------------------------
1231 - def accept(self,
1232 form,
1233 format=None,
1234 link=None,
1235 hierarchy=None,
1236 undelete=False):
1237 """
1238 Create/update all records from the form.
1239
1240 @param form: the form
1241 @param format: data format extension (for audit)
1242 @param link: resource.link for linktable components
1243 @param hierarchy: the data for the hierarchy link to create
1244 @param undelete: reinstate a previously deleted record
1245 """
1246
1247 db = current.db
1248 table = self.table
1249
1250 # Create/update the main record
1251 main_data = self._extract(form)
1252 master_id, master_form_vars = self._accept(self.record_id,
1253 main_data,
1254 format = format,
1255 link = link,
1256 hierarchy = hierarchy,
1257 undelete = undelete,
1258 )
1259 if not master_id:
1260 return
1261 else:
1262 main_data[table._id.name] = master_id
1263 # Make sure lastid is set even if master has no data
1264 # (otherwise *_next redirection will fail)
1265 self.resource.lastid = str(master_id)
1266
1267 # Create or update the subtables
1268 for alias in self.subtables:
1269
1270 subdata = self._extract(form, alias=alias)
1271 if not subdata:
1272 continue
1273
1274 component = self.resource.components[alias]
1275 subtable = component.table
1276
1277 # Get the key (pkey) of the master record to link the
1278 # subtable record to, and update the subdata with it
1279 pkey = component.pkey
1280 if pkey != table._id.name and pkey not in main_data:
1281 row = db(table._id == master_id).select(table[pkey],
1282 limitby=(0, 1)).first()
1283 if not row:
1284 return
1285 main_data[pkey] = row[table[pkey]]
1286 if component.link:
1287 link = Storage(resource = component.link,
1288 master = main_data,
1289 )
1290 else:
1291 link = None
1292 subdata[component.fkey] = main_data[pkey]
1293
1294 # Do we already have a record for this component?
1295 rows = self.subrows
1296 if alias in rows and rows[alias] is not None:
1297 # Yes => get the subrecord ID
1298 subid = rows[alias][subtable._id]
1299 else:
1300 # No => apply component defaults
1301 subid = None
1302 subdata = component.get_defaults(main_data,
1303 data = subdata,
1304 )
1305 # Accept the subrecord
1306 self._accept(subid,
1307 subdata,
1308 alias = alias,
1309 link = link,
1310 format = format,
1311 )
1312
1313 # Accept components (e.g. Inline-Forms)
1314 for item in self.components:
1315 if hasattr(item, "accept"):
1316 item.accept(form,
1317 master_id = master_id,
1318 format = format,
1319 )
1320
1321 # Update form with master form_vars
1322 form_vars = form.vars
1323 # ID
1324 form_vars[table._id.name] = master_id
1325 # Super entities (& anything added manually in table's onaccept)
1326 for var in master_form_vars:
1327 if var not in form_vars:
1328 form_vars[var] = master_form_vars[var]
1329 return
1330
1331 # -------------------------------------------------------------------------
1332 # Utility functions
1333 # -------------------------------------------------------------------------
1335 """
1336 Extract data for a subtable from the form
1337
1338 @param form: the form
1339 @param alias: the component alias of the subtable
1340 """
1341
1342 if alias is None:
1343 return self.table._filter_fields(form.vars)
1344 else:
1345 subform = Storage()
1346 alias_length = len(alias)
1347 form_vars = form.vars
1348 for k in form_vars:
1349 if k[:4] == "sub_" and \
1350 k[4:4 + alias_length + 1] == "%s_" % alias:
1351 fn = k[4 + alias_length + 1:]
1352 subform[fn] = form_vars[k]
1353 return subform
1354
1355 # -------------------------------------------------------------------------
1356 - def _accept(self,
1357 record_id,
1358 data,
1359 alias=None,
1360 format=None,
1361 hierarchy=None,
1362 link=None,
1363 undelete=False):
1364 """
1365 Create or update a record
1366
1367 @param record_id: the record ID
1368 @param data: the data
1369 @param alias: the component alias
1370 @param format: the request format (for audit)
1371 @param hierarchy: the data for the hierarchy link to create
1372 @param link: resource.link for linktable components
1373 @param undelete: reinstate a previously deleted record
1374 """
1375
1376 if alias is not None:
1377 # Subtable
1378 if not data or \
1379 not record_id and all(value is None for value in data.values()):
1380 # No data => skip
1381 return None, Storage()
1382 elif record_id and not data:
1383 # Existing master record, no data => skip, but return
1384 # record_id to allow update of inline-components:
1385 return record_id, Storage()
1386
1387 s3db = current.s3db
1388
1389 if alias is None:
1390 component = self.resource
1391 else:
1392 component = self.resource.components[alias]
1393
1394 # Get the DB table (without alias)
1395 table = component.table
1396 tablename = component.tablename
1397 if component._alias != tablename:
1398 unaliased = s3db.table(component.tablename)
1399 # Must retain custom defaults of the aliased component:
1400 for field in table:
1401 field_ = unaliased[field.name]
1402 field_.default = field.default
1403 field_.update = field.update
1404 table = unaliased
1405
1406 get_config = s3db.get_config
1407
1408 oldrecord = None
1409 if record_id:
1410 # Update existing record
1411 accept_id = record_id
1412 db = current.db
1413 onaccept = get_config(tablename, "update_onaccept",
1414 get_config(tablename, "onaccept", None))
1415
1416 table_fields = table.fields
1417 query = (table._id == record_id)
1418 if onaccept:
1419 # Get oldrecord in full to save in form
1420 oldrecord = db(query).select(limitby=(0, 1)).first()
1421 elif "deleted" in table_fields:
1422 oldrecord = db(query).select(table.deleted,
1423 limitby=(0, 1)).first()
1424 else:
1425 oldrecord = None
1426
1427 if undelete:
1428 # Restoring a previously deleted record
1429 if "deleted" in table_fields:
1430 data["deleted"] = False
1431 if "created_by" in table_fields and current.auth.user:
1432 data["created_by"] = current.auth.user.id
1433 if "created_on" in table_fields:
1434 data["created_on"] = current.request.utcnow
1435 elif oldrecord and "deleted" in oldrecord and oldrecord.deleted:
1436 # Do not (ever) update a deleted record that we don't
1437 # want to restore, otherwise this may set foreign keys
1438 # in a deleted record!
1439 return accept_id
1440 db(table._id == record_id).update(**data)
1441 else:
1442 # Insert new record
1443 accept_id = table.insert(**data)
1444 if not accept_id:
1445 raise RuntimeError("Could not create record")
1446 onaccept = get_config(tablename, "create_onaccept",
1447 get_config(tablename, "onaccept", None))
1448
1449 data[table._id.name] = accept_id
1450 prefix, name = tablename.split("_", 1)
1451 form_vars = Storage(data)
1452 form = Storage(vars=form_vars, record=oldrecord)
1453
1454 # Audit
1455 if record_id is None or undelete:
1456 current.audit("create", prefix, name, form=form,
1457 representation=format)
1458 else:
1459 current.audit("update", prefix, name, form=form,
1460 record=accept_id, representation=format)
1461
1462 # Update super entity links
1463 s3db.update_super(table, form_vars)
1464
1465 # Update component link
1466 if link and link.postprocess is None:
1467 resource = link.resource
1468 master = link.master
1469 resource.update_link(master, form_vars)
1470
1471 if accept_id:
1472 if record_id is None or undelete:
1473 # Create hierarchy link
1474 if hierarchy:
1475 from s3hierarchy import S3Hierarchy
1476 h = S3Hierarchy(tablename)
1477 if h.config:
1478 h.postprocess_create_node(hierarchy, form_vars)
1479 # Set record owner
1480 auth = current.auth
1481 auth.s3_set_record_owner(table, accept_id)
1482 auth.s3_make_session_owner(table, accept_id)
1483 else:
1484 # Update realm
1485 update_realm = get_config(table, "update_realm")
1486 if update_realm:
1487 current.auth.set_realm_entity(table, form_vars,
1488 force_update = True,
1489 )
1490
1491 # Store session vars
1492 component.lastid = str(accept_id)
1493 s3_store_last_record_id(tablename, accept_id)
1494
1495 # Execute onaccept
1496 try:
1497 callback(onaccept, form, tablename=tablename)
1498 except:
1499 error = "onaccept failed: %s" % str(onaccept)
1500 current.log.error(error)
1501 # This is getting swallowed
1502 raise
1503
1504 if alias is None:
1505 # Return master_form_vars
1506 return accept_id, form.vars
1507 else:
1508 return accept_id
1509
1510 # =============================================================================
1511 -class S3SQLFormElement(object):
1512 """ SQL Form Element Base Class """
1513
1514 # -------------------------------------------------------------------------
1516 """
1517 Constructor to define the form element, to be extended
1518 in subclass.
1519
1520 @param selector: the data object selector
1521 @param options: options for the form element
1522 """
1523
1524 self.selector = selector
1525 self.options = Storage(options)
1526
1527 # -------------------------------------------------------------------------
1529 """
1530 Method to resolve this form element against the calling resource.
1531 To be implemented in subclass.
1532
1533 @param resource: the resource
1534 @return: a tuple
1535 (
1536 form element,
1537 original field name,
1538 Field instance for the form renderer
1539 )
1540
1541 The form element can be None for the main table, the component
1542 alias for a subtable, or this form element instance for a
1543 subform.
1544
1545 If None is returned as Field instance, this form element will
1546 not be rendered at all. Besides setting readable/writable
1547 in the Field instance, this can be another mechanism to
1548 control access to form elements.
1549 """
1550
1551 return None, None, None
1552
1553 # -------------------------------------------------------------------------
1554 # Utility methods
1555 # -------------------------------------------------------------------------
1556 @staticmethod
1557 - def _rename_field(field, name,
1558 comments=True,
1559 label=DEFAULT,
1560 popup=None,
1561 skip_post_validation=False,
1562 widget=DEFAULT):
1563 """
1564 Rename a field (actually: create a new Field instance with the
1565 same attributes as the given Field, but a different field name).
1566
1567 @param field: the original Field instance
1568 @param name: the new name
1569 @param comments: render comments - if set to False, only
1570 navigation items with an inline() renderer
1571 method will be rendered (unless popup is None)
1572 @param label: override option for the original field label
1573 @param popup: only if comments=False, additional vars for comment
1574 navigation items (e.g. AddResourceLink), None prevents
1575 rendering of navigation items
1576 @param skip_post_validation: skip field validation during POST,
1577 useful for client-side processed
1578 dummy fields.
1579 @param widget: override option for the original field widget
1580 """
1581
1582 if label is DEFAULT:
1583 label = field.label
1584 if widget is DEFAULT:
1585 # Some widgets may need disabling during POST
1586 widget = field.widget
1587
1588 if not hasattr(field, "type"):
1589 # Virtual Field
1590 field = Storage(comment=None,
1591 type="string",
1592 length=255,
1593 unique=False,
1594 uploadfolder=None,
1595 autodelete=False,
1596 label="",
1597 writable=False,
1598 readable=True,
1599 default=None,
1600 update=None,
1601 compute=None,
1602 represent=lambda v: v or "",
1603 )
1604 requires = None
1605 required = False
1606 notnull = False
1607 elif skip_post_validation and \
1608 current.request.env.request_method == "POST":
1609 requires = SKIP_POST_VALIDATION(field.requires)
1610 required = False
1611 notnull = False
1612 else:
1613 requires = field.requires
1614 required = field.required
1615 notnull = field.notnull
1616
1617 if not comments:
1618 if popup:
1619 comment = field.comment
1620 if hasattr(comment, "clone"):
1621 comment = comment.clone()
1622 if hasattr(comment, "renderer") and \
1623 hasattr(comment, "inline") and \
1624 isinstance(popup, dict):
1625 comment.vars.update(popup)
1626 comment.renderer = comment.inline
1627 else:
1628 comment = None
1629 else:
1630 comment = None
1631 else:
1632 comment = field.comment
1633
1634 f = Field(str(name),
1635 type = field.type,
1636 length = field.length,
1637
1638 required = required,
1639 notnull = notnull,
1640 unique = field.unique,
1641
1642 uploadfolder = field.uploadfolder,
1643 autodelete = field.autodelete,
1644
1645 comment = comment,
1646 label = label,
1647 widget = widget,
1648
1649 default = field.default,
1650
1651 writable = field.writable,
1652 readable = field.readable,
1653
1654 update = field.update,
1655 compute = field.compute,
1656
1657 represent = field.represent,
1658 requires = requires)
1659
1660 return f
1661
1662 # =============================================================================
1663 -class S3SQLField(S3SQLFormElement):
1664 """
1665 Base class for regular form fields
1666
1667 A regular form field is a field in the main form, which can be
1668 fields in the main record or in a subtable (single-record-component).
1669 """
1670
1671 # -------------------------------------------------------------------------
1673 """
1674 Method to resolve this form element against the calling resource.
1675
1676 @param resource: the resource
1677 @return: a tuple
1678 (
1679 subtable alias (or None for main table),
1680 original field name,
1681 Field instance for the form renderer
1682 )
1683 """
1684
1685 # Import S3ResourceField only here, to avoid circular dependency
1686 from s3query import S3ResourceField
1687
1688 rfield = S3ResourceField(resource, self.selector)
1689
1690 field = rfield.field
1691 if field is None:
1692 raise SyntaxError("Invalid selector: %s" % self.selector)
1693
1694 tname = rfield.tname
1695
1696 options_get = self.options.get
1697 label = options_get("label", DEFAULT)
1698 widget = options_get("widget", DEFAULT)
1699
1700 if resource._alias:
1701 tablename = resource._alias
1702 else:
1703 tablename = resource.tablename
1704
1705 if tname == tablename:
1706 # Field in the main table
1707
1708 if label is not DEFAULT:
1709 field.label = label
1710 if widget is not DEFAULT:
1711 field.widget = widget
1712
1713 return None, field.name, field
1714
1715 else:
1716 for alias, component in resource.components.loaded.items():
1717 if component.multiple:
1718 continue
1719 if component._alias:
1720 tablename = component._alias
1721 else:
1722 tablename = component.tablename
1723 if tablename == tname:
1724 name = "sub_%s_%s" % (alias, rfield.fname)
1725 renamed_field = self._rename_field(field,
1726 name,
1727 label = label,
1728 widget = widget,
1729 )
1730 return alias, field.name, renamed_field
1731
1732 raise SyntaxError("Invalid subtable: %s" % tname)
1733
1734 # =============================================================================
1735 -class S3SQLVirtualField(S3SQLFormElement):
1736 """
1737 A form element to embed values of field methods (virtual fields),
1738 always read-only
1739 """
1740
1741 # -------------------------------------------------------------------------
1743 """
1744 Method to resolve this form element against the calling resource.
1745
1746 @param resource: the resource
1747 @return: a tuple
1748 (
1749 subtable alias (or None for main table),
1750 original field name,
1751 Field instance for the form renderer
1752 )
1753 """
1754
1755 table = resource.table
1756 selector = self.selector
1757
1758 if not hasattr(table, selector):
1759 raise SyntaxError("Undefined virtual field: %s" % selector)
1760
1761 label = self.options.label
1762 if not label:
1763 label = " ".join(s.capitalize() for s in selector.split("_"))
1764
1765 field = Field(selector,
1766 label = label,
1767 widget = self,
1768 )
1769
1770 return None, selector, field
1771
1772 # -------------------------------------------------------------------------
1783
1784 # =============================================================================
1785 -class S3SQLDummyField(S3SQLFormElement):
1786 """
1787 A Dummy Field
1788
1789 A simple DIV which can then be acted upon with JavaScript
1790 """
1791
1792 # -------------------------------------------------------------------------
1794 """
1795 Method to resolve this form element against the calling resource.
1796
1797 @param resource: the resource
1798 @return: a tuple
1799 (
1800 subtable alias (or None for main table),
1801 original field name,
1802 Field instance for the form renderer
1803 )
1804 """
1805
1806 selector = self.selector
1807
1808 field = Field(selector,
1809 default = "",
1810 label = "",
1811 widget = self,
1812 )
1813
1814 return None, selector, field
1815
1816 # -------------------------------------------------------------------------
1818 """
1819 Widget renderer for the input field. To be implemented in
1820 subclass (if required) and to be set as widget=self for the
1821 field returned by the resolve()-method of this form element.
1822
1823 @param field: the input field
1824 @param value: the value to populate the widget
1825 @param attributes: attributes for the widget
1826 @return: the widget for this form element as HTML helper
1827 """
1828
1829 return DIV(_class="s3-dummy-field",
1830 )
1831
1832 # =============================================================================
1833 -class S3SQLSubForm(S3SQLFormElement):
1834 """
1835 Base class for subforms
1836
1837 A subform is a form element to be processed after the main
1838 form. Subforms render a single (usually hidden) input field
1839 and a client-side controlled widget to manipulate its contents.
1840 """
1841
1842 # -------------------------------------------------------------------------
1844 """
1845 Initialize this form element for a particular record. This
1846 method will be called by the form renderer to populate the
1847 form for an existing record. To be implemented in subclass.
1848
1849 @param resource: the resource the record belongs to
1850 @param record_id: the record ID
1851
1852 @return: the value for the input field that corresponds
1853 to the specified record.
1854 """
1855
1856 return None
1857
1858 # -------------------------------------------------------------------------
1860 """
1861 Validator method for the input field, used to extract the
1862 data from the input field and prepare them for further
1863 processing by the accept()-method. To be implemented in
1864 subclass and set as requires=self.parse for the input field
1865 in the resolve()-method of this form element.
1866
1867 @param value: the value returned from the input field
1868 @return: tuple of (value, error) where value is the
1869 pre-processed field value and error an error
1870 message in case of invalid data, or None.
1871 """
1872
1873 return (value, None)
1874
1875 # -------------------------------------------------------------------------
1877 """
1878 Widget renderer for the input field. To be implemented in
1879 subclass (if required) and to be set as widget=self for the
1880 field returned by the resolve()-method of this form element.
1881
1882 @param field: the input field
1883 @param value: the value to populate the widget
1884 @param attributes: attributes for the widget
1885 @return: the widget for this form element as HTML helper
1886 """
1887
1888 raise NotImplementedError
1889
1890 # -------------------------------------------------------------------------
1892 """
1893 Read-only representation of this form element. This will be
1894 used instead of the __call__() method when the form element
1895 is to be rendered read-only.
1896
1897 @param value: the value as returned from extract()
1898 @return: the read-only representation of this element as
1899 string or HTML helper
1900 """
1901
1902 return ""
1903
1904 # -------------------------------------------------------------------------
1906 """
1907 Post-process this form element and perform the related
1908 transactions. This method will be called after the main
1909 form has been accepted, where the master record ID will
1910 be provided.
1911
1912 @param form: the form
1913 @param master_id: the master record ID
1914 @param format: the data format extension
1915 @return: True on success, False on error
1916 """
1917
1918 return True
1919
1920 # =============================================================================
1921 -class SKIP_POST_VALIDATION(Validator):
1922 """
1923 Pseudo-validator that allows introspection of field options
1924 during GET, but does nothing during POST. Used for Ajax-validated
1925 inline-components to prevent them from throwing validation errors
1926 when the outer form gets submitted.
1927 """
1928
1930 """
1931 Constructor, used like:
1932 field.requires = SKIP_POST_VALIDATION(field.requires)
1933
1934 @param other: the actual field validator
1935 """
1936
1937 if other and isinstance(other, (list, tuple)):
1938 other = other[0]
1939 self.other = other
1940 if other:
1941 if hasattr(other, "multiple"):
1942 self.multiple = other.multiple
1943 if hasattr(other, "options"):
1944 self.options = other.options
1945 if hasattr(other, "formatter"):
1946 self.formatter = other.formatter
1947
1949 """
1950 Validation
1951
1952 @param value: the value
1953 """
1954
1955 other = self.other
1956 if current.request.env.request_method == "POST" or not other:
1957 return value, None
1958 if not isinstance(other, (list, tuple)):
1959 other = [other]
1960 for r in other:
1961 value, error = r(value)
1962 if error:
1963 return value, error
1964 return value, None
1965
1966 # =============================================================================
1967 -class S3SQLSubFormLayout(object):
1968 """ Layout for S3SQLInlineComponent (Base Class) """
1969
1970 # Layout-specific CSS class for the inline component
1971 layout_class = "subform-default"
1972
1974 """ Constructor """
1975
1976 self.inject_script()
1977 self.columns = None
1978 self.row_actions = True
1979
1980 # -------------------------------------------------------------------------
1982 """
1983 Set column widths for inline-widgets, can be used by subclasses
1984 to render CSS classes for grid-width
1985
1986 @param columns: iterable of column widths
1987 @param actions: whether the subform contains an action column
1988 """
1989
1990 self.columns = columns
1991 self.row_actions = row_actions
1992
1993 # -------------------------------------------------------------------------
1994 - def subform(self,
1995 data,
1996 item_rows,
1997 action_rows,
1998 empty=False,
1999 readonly=False):
2000 """
2001 Outer container for the subform
2002
2003 @param data: the data dict (as returned from extract())
2004 @param item_rows: the item rows
2005 @param action_rows: the (hidden) action rows
2006 @param empty: no data in this component
2007 @param readonly: render read-only
2008 """
2009
2010 if empty:
2011 subform = current.T("No entries currently available")
2012 else:
2013 headers = self.headers(data, readonly=readonly)
2014 subform = TABLE(headers,
2015 TBODY(item_rows),
2016 TFOOT(action_rows),
2017 _class= " ".join(("embeddedComponent", self.layout_class)),
2018 )
2019 return subform
2020
2021 # -------------------------------------------------------------------------
2023 """
2024 Render this component read-only (table-style)
2025
2026 @param resource: the S3Resource
2027 @param data: the data dict (as returned from extract())
2028 """
2029
2030 audit = current.audit
2031 prefix, name = resource.prefix, resource.name
2032
2033 xml_decode = current.xml.xml_decode
2034
2035 items = data["data"]
2036 fields = data["fields"]
2037
2038 trs = []
2039 for item in items:
2040 if "_id" in item:
2041 record_id = item["_id"]
2042 else:
2043 continue
2044 audit("read", prefix, name,
2045 record=record_id, representation="html")
2046 trow = TR(_class="read-row")
2047 for f in fields:
2048 text = xml_decode(item[f["name"]]["text"])
2049 trow.append(XML(xml_decode(text)))
2050 trs.append(trow)
2051
2052 return self.subform(data, trs, [], empty=False, readonly=True)
2053
2054 # -------------------------------------------------------------------------
2055 @staticmethod
2057 """
2058 Render this component read-only (list-style)
2059
2060 @param resource: the S3Resource
2061 @param data: the data dict (as returned from extract())
2062 """
2063
2064 audit = current.audit
2065 prefix, name = resource.prefix, resource.name
2066
2067 xml_decode = current.xml.xml_decode
2068
2069 items = data["data"]
2070 fields = data["fields"]
2071
2072 # Render as comma-separated list of values (no header)
2073 elements = []
2074 for item in items:
2075 if "_id" in item:
2076 record_id = item["_id"]
2077 else:
2078 continue
2079 audit("read", prefix, name,
2080 record=record_id, representation="html")
2081 t = []
2082 for f in fields:
2083 t.append([XML(xml_decode(item[f["name"]]["text"])), " "])
2084 elements.append([TAG[""](list(chain.from_iterable(t))[:-1]), ", "])
2085
2086 return DIV(list(chain.from_iterable(elements))[:-1],
2087 _class="embeddedComponent",
2088 )
2089
2090 # -------------------------------------------------------------------------
2092 """
2093 Render the header row with field labels
2094
2095 @param data: the input field data as Python object
2096 @param readonly: whether the form is read-only
2097 """
2098
2099 fields = data["fields"]
2100
2101 # Don't render a header row if there are no labels
2102 render_header = False
2103 header_row = TR(_class="label-row static")
2104 happend = header_row.append
2105 for f in fields:
2106 label = f["label"]
2107 if label:
2108 render_header = True
2109 label = TD(LABEL(label))
2110 happend(label)
2111
2112 if render_header:
2113 if not readonly:
2114 # Add columns for the Controls
2115 happend(TD())
2116 happend(TD())
2117 return THEAD(header_row)
2118 else:
2119 return THEAD(_class="hide")
2120
2121 # -------------------------------------------------------------------------
2122 @staticmethod
2123 - def actions(subform,
2124 formname,
2125 index,
2126 item = None,
2127 readonly=True,
2128 editable=True,
2129 deletable=True):
2130 """
2131 Render subform row actions into the row
2132
2133 @param subform: the subform row
2134 @param formname: the form name
2135 @param index: the row index
2136 @param item: the row data
2137 @param readonly: this is a read-row
2138 @param editable: this row is editable
2139 @param deletable: this row is deletable
2140 """
2141
2142 T = current.T
2143 action_id = "%s-%s" % (formname, index)
2144
2145 # Action button helper
2146 def action(title, name, throbber=False):
2147 btn = DIV(_id="%s-%s" % (name, action_id),
2148 _class="inline-%s" % name)
2149 if throbber:
2150 return DIV(btn,
2151 DIV(_class="inline-throbber hide",
2152 _id="throbber-%s" % action_id))
2153 else:
2154 return DIV(btn)
2155
2156
2157 # CSS class for action-columns
2158 _class = "subform-action"
2159
2160 # Render the action icons for this row
2161 append = subform.append
2162 if readonly:
2163 if editable:
2164 append(TD(action(T("Edit this entry"), "edt"),
2165 _class = _class,
2166 ))
2167 else:
2168 append(TD(_class=_class))
2169
2170 if deletable:
2171 append(TD(action(T("Remove this entry"), "rmv"),
2172 _class = _class,
2173 ))
2174 else:
2175 append(TD(_class=_class))
2176 else:
2177 if index != "none" or item:
2178 append(TD(action(T("Update this entry"), "rdy", throbber=True),
2179 _class = _class,
2180 ))
2181 append(TD(action(T("Cancel editing"), "cnc"),
2182 _class = _class,
2183 ))
2184 else:
2185 append(TD(action(T("Discard this entry"), "dsc"),
2186 _class=_class,
2187 ))
2188 append(TD(action(T("Add this entry"), "add", throbber=True),
2189 _class = _class,
2190 ))
2191
2192 # -------------------------------------------------------------------------
2194 """
2195 Formstyle for subform read-rows, normally identical
2196 to rowstyle, but can be different in certain layouts
2197 """
2198
2199 return self.rowstyle(form, fields, *args, **kwargs)
2200
2201 # -------------------------------------------------------------------------
2203 """
2204 Formstyle for subform action-rows
2205 """
2206
2207 def render_col(col_id, label, widget, comment, hidden=False):
2208
2209 if col_id == "submit_record__row":
2210 if hasattr(widget, "add_class"):
2211 widget.add_class("inline-row-actions")
2212 col = TD(widget)
2213 elif comment:
2214 col = TD(DIV(widget, comment), _id=col_id)
2215 else:
2216 col = TD(widget, _id=col_id)
2217 return col
2218
2219 if args:
2220 col_id = form
2221 label = fields
2222 widget, comment = args
2223 hidden = kwargs.get("hidden", False)
2224 return render_col(col_id, label, widget, comment, hidden)
2225 else:
2226 parent = TR()
2227 for col_id, label, widget, comment in fields:
2228 parent.append(render_col(col_id, label, widget, comment))
2229 return parent
2230
2231 # -------------------------------------------------------------------------
2232 @staticmethod
2234 """ Inject custom JS to render new read-rows """
2235
2236 # Example:
2237
2238 #appname = current.request.application
2239 #scripts = current.response.s3.scripts
2240
2241 #script = "/%s/static/themes/CRMT/js/inlinecomponent.layout.js" % appname
2242 #if script not in scripts:
2243 #scripts.append(script)
2244
2245 # No custom JS in the default layout
2246 return
2247
2248 # =============================================================================
2249 -class S3SQLVerticalSubFormLayout(S3SQLSubFormLayout):
2250 """
2251 Vertical layout for inline-components
2252
2253 - renders an vertical layout for edit-rows
2254 - standard horizontal layout for read-rows
2255 - hiding header row if there are no visible read-rows
2256 """
2257
2258 # Layout-specific CSS class for the inline component
2259 layout_class = "subform-vertical"
2260
2261 # -------------------------------------------------------------------------
2263 """
2264 Header-row layout: same as default, but non-static (i.e. hiding
2265 if there are no visible read-rows, because edit-rows have their
2266 own labels)
2267 """
2268
2269 headers = super(S3SQLVerticalSubFormLayout, self).headers
2270
2271 header_row = headers(data, readonly = readonly)
2272 element = header_row.element('tr')
2273 if hasattr(element, "remove_class"):
2274 element.remove_class("static")
2275 return header_row
2276
2277 # -------------------------------------------------------------------------
2279 """
2280 Formstyle for subform read-rows, same as standard
2281 horizontal layout.
2282 """
2283
2284 rowstyle = super(S3SQLVerticalSubFormLayout, self).rowstyle
2285 return rowstyle(form, fields, *args, **kwargs)
2286
2287 # -------------------------------------------------------------------------
2289 """
2290 Formstyle for subform edit-rows, using a vertical
2291 formstyle because multiple fields combined with
2292 location-selector are too complex for horizontal
2293 layout.
2294 """
2295
2296 # Use standard foundation formstyle
2297 from s3theme import formstyle_foundation as formstyle
2298 if args:
2299 col_id = form
2300 label = fields
2301 widget, comment = args
2302 hidden = kwargs.get("hidden", False)
2303 return formstyle(col_id, label, widget, comment, hidden)
2304 else:
2305 parent = TD(_colspan = len(fields))
2306 for col_id, label, widget, comment in fields:
2307 parent.append(formstyle(col_id, label, widget, comment))
2308 return TR(parent)
2309
2310 # =============================================================================
2311 -class S3SQLInlineComponent(S3SQLSubForm):
2312 """
2313 Form element for an inline-component-form
2314
2315 This form element allows CRUD of multi-record-components within
2316 the main record form. It renders a single hidden text field with a
2317 JSON representation of the component records, and a widget which
2318 facilitates client-side manipulation of this JSON.
2319 This widget is a row of fields per component record.
2320
2321 The widget uses the s3.ui.inline_component.js script for client-side
2322 manipulation of the JSON data. Changes made by the script will be
2323 validated through Ajax-calls to the CRUD.validate() method.
2324 During accept(), the component gets updated according to the JSON
2325 returned.
2326
2327 @ToDo: Support filtering of field options
2328 Usecase is inline project_organisation for IFRC
2329 PartnerNS needs to be filtered differently from Partners/Donors,
2330 so can't just set a global requires for the field in the controller
2331 - needs to be inside the widget.
2332 See private/templates/IFRC/config.py
2333 """
2334
2335 prefix = "sub"
2336
2337 # -------------------------------------------------------------------------
2339 """
2340 Method to resolve this form element against the calling resource.
2341
2342 @param resource: the resource
2343 @return: a tuple (self, None, Field instance)
2344 """
2345
2346 selector = self.selector
2347
2348 # Check selector
2349 try:
2350 component = resource.components[selector]
2351 except KeyError:
2352 raise SyntaxError("Undefined component: %s" % selector)
2353
2354 # Check permission
2355 permitted = current.auth.s3_has_permission("read",
2356 component.tablename,
2357 )
2358 if not permitted:
2359 return (None, None, None)
2360
2361 options = self.options
2362
2363 if "name" in options:
2364 self.alias = options["name"]
2365 label = self.alias
2366 else:
2367 self.alias = "default"
2368 label = self.selector
2369
2370 if "label" in options:
2371 label = options["label"]
2372 else:
2373 label = " ".join([s.capitalize() for s in label.split("_")])
2374
2375 fname = self._formname(separator = "_")
2376 field = Field(fname, "text",
2377 comment = options.get("comment", None),
2378 default = self.extract(resource, None),
2379 label = label,
2380 represent = self.represent,
2381 required = options.get("required", False),
2382 requires = self.parse,
2383 widget = self,
2384 )
2385
2386 return (self, None, field)
2387
2388 # -------------------------------------------------------------------------
2390 """
2391 Initialize this form element for a particular record. Retrieves
2392 the component data for this record from the database and
2393 converts them into a JSON string to populate the input field with.
2394
2395 @param resource: the resource the record belongs to
2396 @param record_id: the record ID
2397
2398 @return: the JSON for the input field.
2399 """
2400
2401 self.resource = resource
2402
2403 component_name = self.selector
2404 try:
2405 component = resource.components[component_name]
2406 except KeyError:
2407 raise AttributeError("Undefined component")
2408
2409 options = self.options
2410
2411 if component.link:
2412 link = options.get("link", True)
2413 if link:
2414 # For link-table components, embed the link
2415 # table rather than the component
2416 component = component.link
2417
2418 table = component.table
2419 tablename = component.tablename
2420
2421 pkey = table._id.name
2422
2423 fields_opt = options.get("fields", None)
2424 labels = {}
2425 if fields_opt:
2426 fields = []
2427 for f in fields_opt:
2428 if isinstance(f, tuple):
2429 label, f = f
2430 labels[f] = label
2431 if f in table.fields:
2432 fields.append(f)
2433 else:
2434 # Really?
2435 fields = [f.name for f in table if f.readable or f.writable]
2436
2437 if pkey not in fields:
2438 fields.insert(0, pkey)
2439
2440 # Support read-only Virtual Fields
2441 if "virtual_fields" in options:
2442 virtual_fields = options["virtual_fields"]
2443 else:
2444 virtual_fields = []
2445
2446 if "orderby" in options:
2447 orderby = options["orderby"]
2448 else:
2449 orderby = component.get_config("orderby")
2450
2451 if record_id:
2452 if "filterby" in options:
2453 # Filter
2454 f = self._filterby_query()
2455 if f is not None:
2456 component.build_query(filter=f)
2457
2458 if "extra_fields" in options:
2459 extra_fields = options["extra_fields"]
2460 else:
2461 extra_fields = []
2462 all_fields = fields + virtual_fields + extra_fields
2463 start = 0
2464 limit = 1 if options.multiple is False else None
2465 data = component.select(all_fields,
2466 start = start,
2467 limit = limit,
2468 represent = True,
2469 raw_data = True,
2470 show_links = False,
2471 orderby = orderby,
2472 )
2473
2474 records = data["rows"]
2475 rfields = data["rfields"]
2476
2477 for f in rfields:
2478 if f.fname in extra_fields:
2479 rfields.remove(f)
2480 else:
2481 s = f.selector
2482 if s.startswith("~."):
2483 s = s[2:]
2484 label = labels.get(s, None)
2485 if label is not None:
2486 f.label = label
2487
2488 else:
2489 records = []
2490 rfields = []
2491 for s in fields:
2492 rfield = component.resolve_selector(s)
2493 label = labels.get(s, None)
2494 if label is not None:
2495 rfield.label = label
2496 rfields.append(rfield)
2497 for f in virtual_fields:
2498 rfield = component.resolve_selector(f[1])
2499 rfield.label = f[0]
2500 rfields.append(rfield)
2501
2502 headers = [{"name": rfield.fname,
2503 "label": s3_str(rfield.label),
2504 }
2505 for rfield in rfields if rfield.fname != pkey]
2506
2507 items = []
2508 has_permission = current.auth.s3_has_permission
2509 for record in records:
2510
2511 row = record["_row"]
2512 row_id = row[str(table._id)]
2513
2514 item = {"_id": row_id}
2515
2516 permitted = has_permission("update", tablename, row_id)
2517 if not permitted:
2518 item["_readonly"] = True
2519
2520 for rfield in rfields:
2521
2522 fname = rfield.fname
2523 if fname == pkey:
2524 continue
2525
2526 colname = rfield.colname
2527 field = rfield.field
2528
2529 widget = field.widget
2530 if isinstance(widget, S3Selector):
2531 # Use the widget extraction/serialization method
2532 value = widget.serialize(widget.extract(row[colname]))
2533 elif hasattr(field, "formatter"):
2534 value = field.formatter(row[colname])
2535 else:
2536 # Virtual Field
2537 value = row[colname]
2538
2539 text = s3_str(record[colname])
2540 # Text representation is only used in read-forms where
2541 # representation markup cannot interfere with the inline
2542 # form logic - so stripping the markup should not be
2543 # necessary here:
2544 #if "<" in text:
2545 # text = s3_strip_markup(text)
2546
2547 item[fname] = {"value": value, "text": text}
2548
2549 items.append(item)
2550
2551 validate = options.get("validate", None)
2552 if not validate or \
2553 not isinstance(validate, tuple) or \
2554 not len(validate) == 2:
2555 request = current.request
2556 validate = (request.controller, request.function)
2557 c, f = validate
2558
2559 data = {"controller": c,
2560 "function": f,
2561 "resource": resource.tablename,
2562 "component": component_name,
2563 "fields": headers,
2564 "defaults": self._filterby_defaults(),
2565 "data": items
2566 }
2567
2568 return json.dumps(data, separators=SEPARATORS)
2569
2570 # -------------------------------------------------------------------------
2572 """
2573 Validator method, converts the JSON returned from the input
2574 field into a Python object.
2575
2576 @param value: the JSON from the input field.
2577 @return: tuple of (value, error), where value is the converted
2578 JSON, and error the error message if the decoding
2579 fails, otherwise None
2580 """
2581
2582 # @todo: catch uploads during validation errors
2583 if isinstance(value, basestring):
2584 try:
2585 value = json.loads(value)
2586 except:
2587 import sys
2588 error = sys.exc_info()[1]
2589 if hasattr(error, "message"):
2590 error = error.message
2591 else:
2592 error = None
2593 else:
2594 value = None
2595 error = None
2596
2597 return (value, error)
2598
2599 # -------------------------------------------------------------------------
2601 """
2602 Widget method for this form element. Renders a table with
2603 read-rows for existing entries, a variable edit-row to update
2604 existing entries, and an add-row to add new entries. This widget
2605 uses s3.inline_component.js to facilitate manipulation of the
2606 entries.
2607
2608 @param field: the Field for this form element
2609 @param value: the current value for this field
2610 @param attributes: keyword attributes for this widget
2611 """
2612
2613 T = current.T
2614 settings = current.deployment_settings
2615
2616 options = self.options
2617 if options.readonly is True:
2618 # Render read-only
2619 return self.represent(value)
2620
2621 if value is None:
2622 value = field.default
2623 if isinstance(value, basestring):
2624 data = json.loads(value)
2625 else:
2626 data = value
2627 value = json.dumps(value, separators=SEPARATORS)
2628 if data is None:
2629 raise SyntaxError("No resource structure information")
2630
2631 self.upload = Storage()
2632
2633 if options.multiple is False:
2634 multiple = False
2635 else:
2636 multiple = True
2637
2638 required = options.get("required", False)
2639
2640 # Get the table
2641 resource = self.resource
2642 component_name = data["component"]
2643 component = resource.components[component_name]
2644 table = component.table
2645
2646 # @ToDo: Hide completely if the user is not permitted to read this
2647 # component
2648
2649 formname = self._formname()
2650
2651 fields = data["fields"]
2652 items = data["data"]
2653
2654 # Flag whether there are any rows (at least an add-row) in the widget
2655 has_rows = False
2656
2657 # Add the item rows
2658 item_rows = []
2659 prefix = component.prefix
2660 name = component.name
2661 audit = current.audit
2662 has_permission = current.auth.s3_has_permission
2663 tablename = component.tablename
2664
2665 # Configure the layout
2666 layout = self._layout()
2667 columns = self.options.get("columns")
2668 if columns:
2669 layout.set_columns(columns, row_actions = multiple)
2670
2671 get_config = current.s3db.get_config
2672 _editable = get_config(tablename, "editable")
2673 if _editable is None:
2674 _editable = True
2675 _deletable = get_config(tablename, "deletable")
2676 if _deletable is None:
2677 _deletable = True
2678 _class = "read-row inline-form"
2679 if not multiple:
2680 # Mark to client-side JS that we should open Edit Row
2681 _class = "%s single" % _class
2682 item = None
2683 for i in xrange(len(items)):
2684 has_rows = True
2685 item = items[i]
2686 # Get the item record ID
2687 if "_delete" in item and item["_delete"]:
2688 continue
2689 elif "_id" in item:
2690 record_id = item["_id"]
2691 # Check permissions to edit this item
2692 if _editable:
2693 editable = has_permission("update", tablename, record_id)
2694 else:
2695 editable = False
2696 if _deletable:
2697 deletable = has_permission("delete", tablename, record_id)
2698 else:
2699 deletable = False
2700 else:
2701 record_id = None
2702 if _editable:
2703 editable = True
2704 else:
2705 editable = False
2706 if _deletable:
2707 deletable = True
2708 else:
2709 deletable = False
2710
2711 # Render read-row accordingly
2712 rowname = "%s-%s" % (formname, i)
2713 read_row = self._render_item(table, item, fields,
2714 editable = editable,
2715 deletable = deletable,
2716 readonly = True,
2717 multiple = multiple,
2718 index = i,
2719 layout = layout,
2720 _id = "read-row-%s" % rowname,
2721 _class = _class,
2722 )
2723 if record_id:
2724 audit("read", prefix, name,
2725 record = record_id,
2726 representation = "html",
2727 )
2728 item_rows.append(read_row)
2729
2730 # Add the action rows
2731 action_rows = []
2732
2733 # Edit-row
2734 _class = "edit-row inline-form hide"
2735 if required and has_rows:
2736 _class = "%s required" % _class
2737 if not multiple:
2738 _class = "%s single" % _class
2739 edit_row = self._render_item(table, item, fields,
2740 editable = _editable,
2741 deletable = _deletable,
2742 readonly = False,
2743 multiple = multiple,
2744 index = 0,
2745 layout = layout,
2746 _id = "edit-row-%s" % formname,
2747 _class = _class,
2748 )
2749 action_rows.append(edit_row)
2750
2751 # Add-row
2752 inline_open_add = ""
2753 insertable = get_config(tablename, "insertable")
2754 if insertable is None:
2755 insertable = True
2756 if insertable:
2757 insertable = has_permission("create", tablename)
2758 if insertable:
2759 _class = "add-row inline-form"
2760 explicit_add = options.explicit_add
2761 if not multiple:
2762 explicit_add = False
2763 if has_rows:
2764 # Add Rows not relevant
2765 _class = "%s hide" % _class
2766 else:
2767 # Mark to client-side JS that we should always validate
2768 _class = "%s single" % _class
2769 if required and not has_rows:
2770 explicit_add = False
2771 _class = "%s required" % _class
2772 # Explicit open-action for add-row (optional)
2773 if explicit_add:
2774 # Hide add-row for explicit open-action
2775 _class = "%s hide" % _class
2776 if explicit_add is True:
2777 label = T("Add another")
2778 else:
2779 label = explicit_add
2780 inline_open_add = A(label,
2781 _class="inline-open-add action-lnk",
2782 )
2783 has_rows = True
2784 add_row = self._render_item(table, None, fields,
2785 editable = True,
2786 deletable = True,
2787 readonly = False,
2788 multiple = multiple,
2789 layout = layout,
2790 _id = "add-row-%s" % formname,
2791 _class = _class,
2792 )
2793 action_rows.append(add_row)
2794
2795 # Empty edit row
2796 empty_row = self._render_item(table, None, fields,
2797 editable = _editable,
2798 deletable = _deletable,
2799 readonly = False,
2800 multiple = multiple,
2801 index = "default",
2802 layout = layout,
2803 _id = "empty-edit-row-%s" % formname,
2804 _class = "empty-row inline-form hide",
2805 )
2806 action_rows.append(empty_row)
2807
2808 # Empty read row
2809 empty_row = self._render_item(table, None, fields,
2810 editable = _editable,
2811 deletable = _deletable,
2812 readonly = True,
2813 multiple = multiple,
2814 index = "none",
2815 layout = layout,
2816 _id = "empty-read-row-%s" % formname,
2817 _class = "empty-row inline-form hide",
2818 )
2819 action_rows.append(empty_row)
2820
2821 # Real input: a hidden text field to store the JSON data
2822 real_input = "%s_%s" % (resource.tablename, field.name)
2823 default = {"_type": "hidden",
2824 "_value": value,
2825 "requires": lambda v: (v, None),
2826 }
2827 attr = StringWidget._attributes(field, default, **attributes)
2828 attr["_class"] = "%s hide" % attr["_class"]
2829 attr["_id"] = real_input
2830
2831 widget = layout.subform(data,
2832 item_rows,
2833 action_rows,
2834 empty = not has_rows,
2835 )
2836
2837 if self.upload:
2838 hidden = DIV(_class="hidden", _style="display:none")
2839 for k, v in self.upload.items():
2840 hidden.append(INPUT(_type = "text",
2841 _id = k,
2842 _name = k,
2843 _value = v,
2844 _style = "display:none",
2845 ))
2846 else:
2847 hidden = ""
2848
2849 # Render output HTML
2850 output = DIV(INPUT(**attr),
2851 hidden,
2852 widget,
2853 inline_open_add,
2854 _id = self._formname(separator="-"),
2855 _field = real_input,
2856 _class = "inline-component",
2857 )
2858
2859 # Reset the layout
2860 layout.set_columns(None)
2861
2862 # Script options
2863 js_opts = {"implicitCancelEdit": settings.get_ui_inline_cancel_edit(),
2864 "confirmCancelEdit": s3_str(T("Discard changes?")),
2865 }
2866 script = '''S3.inlineComponentsOpts=%s''' % json.dumps(js_opts)
2867 js_global = current.response.s3.js_global
2868 if script not in js_global:
2869 js_global.append(script)
2870
2871 return output
2872
2873 # -------------------------------------------------------------------------
2875 """
2876 Read-only representation of this sub-form
2877
2878 @param value: the value returned from extract()
2879 """
2880
2881 if isinstance(value, basestring):
2882 data = json.loads(value)
2883 else:
2884 data = value
2885
2886 if data["data"] == []:
2887 # Don't render a subform for NONE
2888 return current.messages["NONE"]
2889
2890 resource = self.resource
2891 component = resource.components[data["component"]]
2892
2893 layout = self._layout()
2894 columns = self.options.get("columns")
2895 if columns:
2896 layout.set_columns(columns, row_actions=False)
2897
2898 fields = data["fields"]
2899 if len(fields) == 1 and self.options.get("render_list", False):
2900 output = layout.render_list(component, data)
2901 else:
2902 output = layout.readonly(component, data)
2903
2904 # Reset the layout
2905 layout.set_columns(None)
2906
2907 return DIV(output,
2908 _id = self._formname(separator="-"),
2909 _class = "inline-component readonly",
2910 )
2911
2912 # -------------------------------------------------------------------------
2914 """
2915 Post-processes this form element against the POST data of the
2916 request, and create/update/delete any related records.
2917
2918 @param form: the form
2919 @param master_id: the ID of the master record in the form
2920 @param format: the data format extension (for audit)
2921 """
2922
2923 # Name of the real input field
2924 fname = self._formname(separator="_")
2925
2926 options = self.options
2927 multiple = options.get("multiple", True)
2928 defaults = options.get("default", {})
2929
2930 if fname in form.vars:
2931
2932 # Retrieve the data
2933 try:
2934 data = json.loads(form.vars[fname])
2935 except ValueError:
2936 return
2937 component_name = data.get("component", None)
2938 if not component_name:
2939 return
2940 data = data.get("data", None)
2941 if not data:
2942 return
2943
2944 # Get the component
2945 resource = self.resource
2946 component = resource.components.get(component_name)
2947 if not component:
2948 return
2949
2950 # Link table handling
2951 link = component.link
2952 if link and options.get("link", True):
2953 # Data are for the link table
2954 actuate_link = False
2955 component = link
2956 else:
2957 # Data are for the component
2958 actuate_link = True
2959
2960 # Table, tablename, prefix and name of the component
2961 prefix = component.prefix
2962 name = component.name
2963 tablename = component.tablename
2964
2965 db = current.db
2966 table = db[tablename]
2967
2968 s3db = current.s3db
2969 auth = current.auth
2970
2971 # Process each item
2972 has_permission = auth.s3_has_permission
2973 audit = current.audit
2974 onaccept = s3db.onaccept
2975
2976 for item in data:
2977
2978 if not "_changed" in item and not "_delete" in item:
2979 # No changes made to this item - skip
2980 continue
2981
2982 delete = item.get("_delete")
2983 values = Storage()
2984 valid = True
2985
2986 if not delete:
2987 # Get the values
2988 for f, d in item.iteritems():
2989 if f[0] != "_" and d and isinstance(d, dict):
2990
2991 field = table[f]
2992 widget = field.widget
2993 if not hasattr(field, "type"):
2994 # Virtual Field
2995 continue
2996 elif field.type == "upload":
2997 # Find, rename and store the uploaded file
2998 rowindex = item.get("_index", None)
2999 if rowindex is not None:
3000 filename = self._store_file(table, f, rowindex)
3001 if filename:
3002 values[f] = filename
3003 elif isinstance(widget, S3Selector):
3004 # Value must be processed by widget post-process
3005 value, error = widget.postprocess(d["value"])
3006 if not error:
3007 values[f] = value
3008 else:
3009 valid = False
3010 break
3011 else:
3012 # Must run through validator again (despite pre-validation)
3013 # in order to post-process widget output properly (e.g. UTC
3014 # offset subtraction)
3015 try:
3016 value, error = s3_validate(table, f, d["value"])
3017 except AttributeError:
3018 continue
3019 if not error:
3020 values[f] = value
3021 else:
3022 valid = False
3023 break
3024
3025 if not valid:
3026 # Skip invalid items
3027 continue
3028
3029 record_id = item.get("_id")
3030
3031 if not record_id:
3032 if delete:
3033 # Item has been added and then removed again,
3034 # so just ignore it
3035 continue
3036
3037 elif not component.multiple or not multiple:
3038 # Do not create a second record in this component
3039 query = (resource._id == master_id) & \
3040 component.get_join()
3041 f = self._filterby_query()
3042 if f is not None:
3043 query &= f
3044 DELETED = current.xml.DELETED
3045 if DELETED in table.fields:
3046 query &= table[DELETED] != True
3047 row = db(query).select(table._id, limitby=(0, 1)).first()
3048 if row:
3049 record_id = row[table._id]
3050
3051 if record_id:
3052 # Delete..?
3053 if delete:
3054 authorized = has_permission("delete", tablename, record_id)
3055 if not authorized:
3056 continue
3057 c = s3db.resource(tablename, id=record_id)
3058 # Audit happens inside .delete()
3059 # Use cascade=True so that the deletion gets
3060 # rolled back in case subsequent items fail:
3061 success = c.delete(cascade=True, format="html")
3062
3063 # ...or update?
3064 else:
3065 authorized = has_permission("update", tablename, record_id)
3066 if not authorized:
3067 continue
3068 query = (table._id == record_id)
3069 success = db(query).update(**values)
3070 values[table._id.name] = record_id
3071
3072 # Post-process update
3073 if success:
3074 audit("update", prefix, name,
3075 record=record_id, representation=format)
3076 # Update super entity links
3077 s3db.update_super(table, values)
3078 # Update realm
3079 update_realm = s3db.get_config(table, "update_realm")
3080 if update_realm:
3081 auth.set_realm_entity(table, values,
3082 force_update=True)
3083 # Onaccept
3084 onaccept(table, Storage(vars=values), method="update")
3085 else:
3086 # Create a new record
3087 authorized = has_permission("create", tablename)
3088 if not authorized:
3089 continue
3090
3091 # Get master record ID
3092 pkey = component.pkey
3093 mastertable = resource.table
3094 if pkey != mastertable._id.name:
3095 query = (mastertable._id == master_id)
3096 master = db(query).select(mastertable._id,
3097 mastertable[pkey],
3098 limitby=(0, 1)).first()
3099 if not master:
3100 return
3101 else:
3102 master = Storage({pkey: master_id})
3103
3104 if actuate_link:
3105 # Data are for component => apply component defaults
3106 values = component.get_defaults(master,
3107 defaults = defaults,
3108 data = values,
3109 )
3110
3111 if not actuate_link or not link:
3112 # Add master record ID as linked directly
3113 values[component.fkey] = master[pkey]
3114 else:
3115 # Check whether the component is a link table and
3116 # we're linking to that via something like pr_person
3117 # from hrm_human_resource
3118 fkey = component.fkey
3119 if fkey != "id" and fkey in component.fields and fkey not in values:
3120 if fkey == "pe_id" and pkey == "person_id":
3121 # Need to lookup the pe_id manually (bad that we need this
3122 # special case, must be a better way but this works for now)
3123 ptable = s3db.pr_person
3124 query = (ptable.id == master[pkey])
3125 person = db(query).select(ptable.pe_id,
3126 limitby=(0, 1)
3127 ).first()
3128 if person:
3129 values["pe_id"] = person.pe_id
3130 else:
3131 current.log.debug("S3Forms: Cannot find person with ID: %s" % master[pkey])
3132 elif resource.tablename == "pr_person" and \
3133 fkey == "case_id" and pkey == "id":
3134 # Using dvr_case as a link between pr_person & e.g. project_activity
3135 # @ToDo: Work out generalisation & move to option if-possible
3136 ltable = component.link.table
3137 query = (ltable.person_id == master[pkey])
3138 link_record = db(query).select(ltable.id,
3139 limitby=(0, 1)
3140 ).first()
3141 if link_record:
3142 values[fkey] = link_record[pkey]
3143 else:
3144 current.log.debug("S3Forms: Cannot find case for person ID: %s" % master[pkey])
3145
3146 else:
3147 values[fkey] = master[pkey]
3148
3149 # Create the new record
3150 # use _table in case we are using an alias
3151 try:
3152 record_id = component._table.insert(**values)
3153 except:
3154 current.log.debug("S3Forms: Cannot insert values %s into table: %s" % (values, component._table))
3155 raise
3156
3157 # Post-process create
3158 if record_id:
3159 # Ensure we're using the real table, not an alias
3160 table = db[tablename]
3161 # Audit
3162 audit("create", prefix, name,
3163 record = record_id,
3164 representation = format,
3165 )
3166 # Add record_id
3167 values[table._id.name] = record_id
3168 # Update super entity link
3169 s3db.update_super(table, values)
3170 # Update link table
3171 if link and actuate_link and \
3172 options.get("update_link", True):
3173 link.update_link(master, values)
3174 # Set record owner
3175 auth.s3_set_record_owner(table, record_id)
3176 # onaccept
3177 subform = Storage(vars=Storage(values))
3178 onaccept(table, subform, method="create")
3179
3180 # Success
3181 return True
3182 else:
3183 return False
3184
3185 # -------------------------------------------------------------------------
3186 # Utility methods
3187 # -------------------------------------------------------------------------
3189 """
3190 Generate a string representing the formname
3191
3192 @param separator: separator to prepend a prefix
3193 """
3194
3195 if separator:
3196 return "%s%s%s%s" % (self.prefix,
3197 separator,
3198 self.alias,
3199 self.selector)
3200 else:
3201 return "%s%s" % (self.alias, self.selector)
3202
3203 # -------------------------------------------------------------------------
3205 """ Get the current layout """
3206
3207 layout = self.options.layout
3208 if not layout:
3209 layout = current.deployment_settings.get_ui_inline_component_layout()
3210 elif isinstance(layout, type):
3211 layout = layout()
3212 return layout
3213
3214 # -------------------------------------------------------------------------
3215 - def _render_item(self,
3216 table,
3217 item,
3218 fields,
3219 readonly=True,
3220 editable=False,
3221 deletable=False,
3222 multiple=True,
3223 index="none",
3224 layout=None,
3225 **attributes):
3226 """
3227 Render a read- or edit-row.
3228
3229 @param table: the database table
3230 @param item: the data
3231 @param fields: the fields to render (list of strings)
3232 @param readonly: render a read-row (otherwise edit-row)
3233 @param editable: whether the record can be edited
3234 @param deletable: whether the record can be deleted
3235 @param multiple: whether multiple records can be added
3236 @param index: the row index
3237 @param layout: the subform layout (S3SQLSubFormLayout)
3238 @param attributes: HTML attributes for the row
3239 """
3240
3241 s3 = current.response.s3
3242
3243 rowtype = readonly and "read" or "edit"
3244 pkey = table._id.name
3245
3246 data = {}
3247 formfields = []
3248 formname = self._formname()
3249 for f in fields:
3250
3251 # Construct a row-specific field name
3252 fname = f["name"]
3253 idxname = "%s_i_%s_%s_%s" % (formname, fname, rowtype, index)
3254
3255 # Parent and caller for add-popup
3256 if not readonly:
3257 # Use unaliased name to avoid need to create additional controllers
3258 parent = original_tablename(table).split("_", 1)[1]
3259 caller = "sub_%s_%s" % (formname, idxname)
3260 popup = Storage(parent=parent, caller=caller)
3261 else:
3262 popup = None
3263
3264 # Custom label
3265 label = f.get("label", DEFAULT)
3266
3267 # Use S3UploadWidget for upload fields
3268 if str(table[fname].type) == "upload":
3269 widget = S3UploadWidget.widget
3270 else:
3271 widget = DEFAULT
3272
3273 # Get a Field instance for SQLFORM.factory
3274 formfield = self._rename_field(table[fname],
3275 idxname,
3276 comments = False,
3277 label = label,
3278 popup = popup,
3279 skip_post_validation = True,
3280 widget = widget,
3281 )
3282
3283 # Reduced options set?
3284 if "filterby" in self.options:
3285 options = self._filterby_options(fname)
3286 if options:
3287 if len(options) < 2:
3288 requires = IS_IN_SET(options, zero=None)
3289 else:
3290 requires = IS_IN_SET(options)
3291 formfield.requires = SKIP_POST_VALIDATION(requires)
3292
3293 # Get filterby-default
3294 filterby_defaults = self._filterby_defaults()
3295 if filterby_defaults and fname in filterby_defaults:
3296 default = filterby_defaults[fname]["value"]
3297 formfield.default = default
3298
3299 # Add the data for this field (for existing rows)
3300 if index is not None and item and fname in item:
3301 if formfield.type == "upload":
3302 filename = item[fname]["value"]
3303 if current.request.env.request_method == "POST":
3304 if "_index" in item and item.get("_changed", False):
3305 rowindex = item["_index"]
3306 filename = self._store_file(table, fname, rowindex)
3307 data[idxname] = filename
3308 else:
3309 value = item[fname]["value"]
3310 if type(value) is unicode:
3311 value = value.encode("utf-8")
3312 widget = formfield.widget
3313 if isinstance(widget, S3Selector):
3314 # Use the widget parser to get at the selected ID
3315 value, error = widget.parse(value).get("id"), None
3316 else:
3317 # Use the validator to get at the original value
3318 value, error = s3_validate(table, fname, value)
3319 if error:
3320 value = None
3321 data[idxname] = value
3322 formfields.append(formfield)
3323
3324 if not data:
3325 data = None
3326 elif pkey not in data:
3327 data[pkey] = None
3328
3329 # Render the subform
3330 subform_name = "sub_%s" % formname
3331 rowstyle = layout.rowstyle_read if readonly else layout.rowstyle
3332 subform = SQLFORM.factory(*formfields,
3333 record=data,
3334 showid=False,
3335 formstyle=rowstyle,
3336 upload = s3.download_url,
3337 readonly=readonly,
3338 table_name=subform_name,
3339 separator = ":",
3340 submit = False,
3341 buttons = [])
3342 subform = subform[0]
3343
3344 # Retain any CSS classes added by the layout
3345 subform_class = subform["_class"]
3346 subform.update(**attributes)
3347 if subform_class:
3348 subform.add_class(subform_class)
3349
3350 if multiple:
3351 # Render row actions
3352 layout.actions(subform,
3353 formname,
3354 index,
3355 item = item,
3356 readonly = readonly,
3357 editable = editable,
3358 deletable = deletable,
3359 )
3360
3361 return subform
3362
3363 # -------------------------------------------------------------------------
3365 """
3366 Render the filterby-options as Query to apply when retrieving
3367 the existing rows in this inline-component
3368 """
3369
3370 filterby = self.options["filterby"]
3371 if not filterby:
3372 return
3373 if not isinstance(filterby, (list, tuple)):
3374 filterby = [filterby]
3375
3376 component = self.resource.components[self.selector]
3377 table = component.table
3378
3379 query = None
3380 for f in filterby:
3381 fieldname = f["field"]
3382 if fieldname not in table.fields:
3383 continue
3384 field = table[fieldname]
3385 if "options" in f:
3386 options = f["options"]
3387 else:
3388 continue
3389 if "invert" in f:
3390 invert = f["invert"]
3391 else:
3392 invert = False
3393 if not isinstance(options, (list, tuple)):
3394 if invert:
3395 q = (field != options)
3396 else:
3397 q = (field == options)
3398 else:
3399 if invert:
3400 q = (~(field.belongs(options)))
3401 else:
3402 q = (field.belongs(options))
3403 if query is None:
3404 query = q
3405 else:
3406 query &= q
3407
3408 return query
3409
3410 # -------------------------------------------------------------------------
3412 """
3413 Render the defaults for this inline-component as a dict
3414 for the real-input JSON
3415 """
3416
3417 filterby = self.options.get("filterby")
3418 if filterby is None:
3419 return None
3420
3421 if not isinstance(filterby, (list, tuple)):
3422 filterby = [filterby]
3423
3424 component = self.resource.components[self.selector]
3425 table = component.table
3426
3427 defaults = dict()
3428 for f in filterby:
3429 fieldname = f["field"]
3430 if fieldname not in table.fields:
3431 continue
3432 if "default" in f:
3433 default = f["default"]
3434 elif "options" in f:
3435 options = f["options"]
3436 if "invert" in f and f["invert"]:
3437 continue
3438 if isinstance(options, (list, tuple)):
3439 if len(options) != 1:
3440 continue
3441 else:
3442 default = options[0]
3443 else:
3444 default = options
3445 else:
3446 continue
3447
3448 if default is not None:
3449 defaults[fieldname] = {"value": default}
3450
3451 return defaults
3452
3453 # -------------------------------------------------------------------------
3455 """
3456 Re-render the options list for a field if there is a
3457 filterby-restriction.
3458
3459 @param fieldname: the name of the field
3460 """
3461
3462 component = self.resource.components[self.selector]
3463 table = component.table
3464
3465 if fieldname not in table.fields:
3466 return None
3467 field = table[fieldname]
3468
3469 filterby = self.options["filterby"]
3470 if filterby is None:
3471 return None
3472 if not isinstance(filterby, (list, tuple)):
3473 filterby = [filterby]
3474
3475 filter_fields = dict((f["field"], f) for f in filterby)
3476 if fieldname not in filter_fields:
3477 return None
3478
3479 filterby = filter_fields[fieldname]
3480 if "options" not in filterby:
3481 return None
3482
3483 # Get the options list for the original validator
3484 requires = field.requires
3485 if not isinstance(requires, (list, tuple)):
3486 requires = [requires]
3487 if requires:
3488 r = requires[0]
3489 if isinstance(r, IS_EMPTY_OR):
3490 #empty = True
3491 r = r.other
3492 # Currently only supporting IS_IN_SET
3493 if not isinstance(r, IS_IN_SET):
3494 return None
3495 else:
3496 return None
3497 r_opts = r.options()
3498
3499 # Get the filter options
3500 options = filterby["options"]
3501 if not isinstance(options, (list, tuple)):
3502 options = [options]
3503 subset = []
3504 if "invert" in filterby:
3505 invert = filterby["invert"]
3506 else:
3507 invert = False
3508
3509 # Compute reduced options list
3510 for o in r_opts:
3511 if invert:
3512 if isinstance(o, (list, tuple)):
3513 if o[0] not in options:
3514 subset.append(o)
3515 elif isinstance(r_opts, dict):
3516 if o not in options:
3517 subset.append((o, r_opts[o]))
3518 elif o not in options:
3519 subset.append(o)
3520 else:
3521 if isinstance(o, (list, tuple)):
3522 if o[0] in options:
3523 subset.append(o)
3524 elif isinstance(r_opts, dict):
3525 if o in options:
3526 subset.append((o, r_opts[o]))
3527 elif o in options:
3528 subset.append(o)
3529
3530 return subset
3531
3532 # -------------------------------------------------------------------------
3534 """
3535 Find, rename and store an uploaded file and return it's
3536 new pathname
3537 """
3538
3539 field = table[fieldname]
3540
3541 formname = self._formname()
3542 upload = "upload_%s_%s_%s" % (formname, fieldname, rowindex)
3543
3544 post_vars = current.request.post_vars
3545 if upload in post_vars:
3546
3547 f = post_vars[upload]
3548
3549 if hasattr(f, "file"):
3550 # Newly uploaded file (FieldStorage)
3551 (sfile, ofilename) = (f.file, f.filename)
3552 nfilename = field.store(sfile,
3553 ofilename,
3554 field.uploadfolder)
3555 self.upload[upload] = nfilename
3556 return nfilename
3557
3558 elif isinstance(f, basestring):
3559 # Previously uploaded file
3560 return f
3561
3562 return None
3563
3564 # =============================================================================
3565 -class S3SQLInlineLink(S3SQLInlineComponent):
3566 """
3567 Subform to edit link table entries for the master record
3568
3569 Constructor options:
3570
3571 ** Common options:
3572
3573 readonly..........True|False......render read-only always
3574 multiple..........True|False......allow selection of multiple
3575 options (default True)
3576 widget............string..........which widget to use, one of:
3577 - multiselect (default)
3578 - groupedopts (default when cols is specified)
3579 - hierarchy (requires hierarchical lookup-table)
3580 - cascade (requires hierarchical lookup-table)
3581 render_list.......True|False......in read-only mode, render HTML
3582 list rather than comma-separated
3583 strings (default False)
3584
3585 ** Options for groupedopts widget:
3586
3587 cols..............integer.........number of columns for grouped
3588 options (default: None)
3589 orientation.......string..........orientation for grouped options
3590 order, one of:
3591 - cols
3592 - rows
3593 size..............integer.........maximum number of items per group
3594 in grouped options, None to disable
3595 grouping
3596 sort..............True|False......sort grouped options (always True
3597 when grouping, i.e. size!=None)
3598 help_field........string..........additional field in the look-up
3599 table to render as tooltip for
3600 grouped options
3601 table.............True|False......render grouped options as HTML
3602 TABLE rather than nested DIVs
3603 (default True)
3604
3605 ** Options for multi-select widget:
3606
3607 header............True|False......multi-select to show a header with
3608 bulk-select options and optional
3609 search-field
3610 search............True|False......show the search-field in the header
3611 selectedList......integer.........how many items to show on multi-select
3612 button before collapsing into number
3613 noneSelectedText..string..........placeholder text on multi-select button
3614 columns...........integer.........Foundation column-width for the
3615 widget (for custom forms)
3616
3617 ** Options-filtering:
3618 - multiselect and groupedopts only
3619 - for hierarchy and cascade widgets, use the "filter" option
3620
3621 requires..........Validator.......validator to determine the
3622 selectable options (defaults to
3623 field validator)
3624 filterby..........field selector..filter look-up options by this field
3625 (can be a field in the look-up table
3626 itself or in another table linked to it)
3627 options...........value|list......filter for these values, or:
3628 match.............field selector..lookup the filter value from this
3629 field (can be a field in the master
3630 table, or in linked table)
3631
3632 ** Options for hierarchy and cascade widgets:
3633
3634 levels............list............ordered list of labels for hierarchy
3635 levels (top-down order), to override
3636 the lookup-table's "hierarchy_levels"
3637 setting, cascade-widget only
3638 represent.........callback........representation method for hierarchy
3639 nodes (defaults to field represent)
3640 leafonly..........True|False......only leaf nodes can be selected
3641 cascade...........True|False......automatically select the entire branch
3642 when a parent node is newly selected;
3643 with multiple=False, this will
3644 auto-select single child options
3645 (default True when leafonly=True)
3646 filter............resource query..filter expression to filter the
3647 selectable options
3648 """
3649
3650 prefix = "link"
3651
3652 # -------------------------------------------------------------------------
3654 """
3655 Get all existing links for record_id.
3656
3657 @param resource: the resource the record belongs to
3658 @param record_id: the record ID
3659
3660 @return: list of component record IDs this record is
3661 linked to via the link table
3662 """
3663
3664 self.resource = resource
3665 component, link = self.get_link()
3666
3667 # Customise resources
3668 from s3rest import S3Request
3669 r = S3Request(resource.prefix,
3670 resource.name,
3671 # Current request args/vars could be in a different
3672 # resource context, so must override them here:
3673 args = [],
3674 get_vars = {},
3675 )
3676 customise_resource = current.deployment_settings.customise_resource
3677 for tablename in (component.tablename, link.tablename):
3678 customise = customise_resource(tablename)
3679 if customise:
3680 customise(r, tablename)
3681
3682 self.initialized = True
3683 if record_id:
3684 rkey = component.rkey
3685 rows = link.select([rkey], as_rows=True)
3686 if rows:
3687 rkey = str(link.table[rkey])
3688 values = [row[rkey] for row in rows]
3689 else:
3690 values = []
3691 else:
3692 # Use default
3693 values = [link.table[self.options.field].default]
3694
3695 return values
3696
3697 # -------------------------------------------------------------------------
3699 """
3700 Widget renderer, currently supports multiselect (default),
3701 hierarchy and groupedopts widgets.
3702
3703 @param field: the input field
3704 @param value: the value to populate the widget
3705 @param attributes: attributes for the widget
3706
3707 @return: the widget
3708 """
3709
3710 options = self.options
3711 component, link = self.get_link()
3712
3713 has_permission = current.auth.s3_has_permission
3714 ltablename = link.tablename
3715
3716 # User must have permission to create and delete
3717 # link table entries (which is what this widget is about):
3718 if options.readonly is True or \
3719 not has_permission("create", ltablename) or \
3720 not has_permission("delete", ltablename):
3721 # Render read-only
3722 return self.represent(value)
3723
3724 multiple = options.get("multiple", True)
3725 options["multiple"] = multiple
3726
3727 # Field dummy
3728 kfield = link.table[component.rkey]
3729 dummy_field = Storage(name = field.name,
3730 type = kfield.type,
3731 label = options.label or kfield.label,
3732 represent = kfield.represent,
3733 )
3734
3735 # Widget type
3736 widget = options.get("widget")
3737 if widget not in ("hierarchy", "cascade"):
3738 requires = options.get("requires")
3739 if requires is None:
3740 # Get the selectable entries for the widget and construct
3741 # a validator from it
3742 zero = None if multiple else options.get("zero", XML(" "))
3743 opts = self.get_options()
3744 if zero is None:
3745 # Remove the empty option
3746 opts = dict((k, v) for k, v in opts.items() if k != "")
3747 requires = IS_IN_SET(opts,
3748 multiple=multiple,
3749 zero=zero,
3750 sort=options.get("sort", True))
3751 if zero is not None:
3752 requires = IS_EMPTY_OR(requires)
3753 dummy_field.requires = requires
3754
3755 # Helper to extract widget options
3756 widget_opts = lambda keys: dict((k, v)
3757 for k, v in options.items()
3758 if k in keys)
3759
3760 # Instantiate the widget
3761 if widget == "groupedopts" or not widget and "cols" in options:
3762 from s3widgets import S3GroupedOptionsWidget
3763 w_opts = widget_opts(("cols",
3764 "help_field",
3765 "multiple",
3766 "orientation",
3767 "size",
3768 "sort",
3769 "table",
3770 ))
3771 w = S3GroupedOptionsWidget(**w_opts)
3772 elif widget == "hierarchy":
3773 from s3widgets import S3HierarchyWidget
3774 w_opts = widget_opts(("multiple",
3775 "filter",
3776 "leafonly",
3777 "cascade",
3778 "represent",
3779 ))
3780 w_opts["lookup"] = component.tablename
3781 w = S3HierarchyWidget(**w_opts)
3782 elif widget == "cascade":
3783 from s3widgets import S3CascadeSelectWidget
3784 w_opts = widget_opts(("levels",
3785 "multiple",
3786 "filter",
3787 "leafonly",
3788 "cascade",
3789 "represent",
3790 ))
3791 w_opts["lookup"] = component.tablename
3792 w = S3CascadeSelectWidget(**w_opts)
3793 else:
3794 # Default to multiselect
3795 from s3widgets import S3MultiSelectWidget
3796 w_opts = widget_opts(("multiple",
3797 "search",
3798 "header",
3799 "selectedList",
3800 "noneSelectedText",
3801 "columns",
3802 ))
3803 w = S3MultiSelectWidget(**w_opts)
3804
3805 # Render the widget
3806 attr = dict(attributes)
3807 attr["_id"] = field.name
3808 if not link.table[options.field].writable:
3809 _class = attr.get("_class", None)
3810 if _class:
3811 attr["_class"] = "%s hide" % _class
3812 else:
3813 attr["_class"] = "hide"
3814 widget = w(dummy_field, value, **attr)
3815 if hasattr(widget, "add_class"):
3816 widget.add_class("inline-link")
3817
3818 # Append the attached script to jquery_ready
3819 script = options.get("script")
3820 if script:
3821 current.response.s3.jquery_ready.append(script)
3822
3823 return widget
3824
3825 # -------------------------------------------------------------------------
3827 """
3828 Validate this link, currently only checking whether it has
3829 a value when required=True
3830
3831 @param form: the form
3832 """
3833
3834 required = self.options.required
3835 if not required:
3836 return
3837
3838 fname = self._formname(separator="_")
3839 values = form.vars.get(fname)
3840
3841 if not values:
3842 error = current.T("Value Required") \
3843 if required is True else required
3844 form.errors[fname] = error
3845
3846 # -------------------------------------------------------------------------
3848 """
3849 Post-processes this subform element against the POST data,
3850 and create/update/delete any related records.
3851
3852 @param form: the master form
3853 @param master_id: the ID of the master record in the form
3854 @param format: the data format extension (for audit)
3855
3856 @todo: implement audit
3857 """
3858
3859 s3db = current.s3db
3860
3861 # Name of the real input field
3862 fname = self._formname(separator="_")
3863 resource = self.resource
3864
3865 success = False
3866
3867 if fname in form.vars:
3868
3869 # Extract the new values from the form
3870 values = form.vars[fname]
3871 if values is None:
3872 values = []
3873 elif not isinstance(values, (list, tuple, set)):
3874 values = [values]
3875 values = set(str(v) for v in values)
3876
3877 # Get the link table
3878 component, link = self.get_link()
3879
3880 # Get the master identity (pkey)
3881 pkey = component.pkey
3882 if pkey == resource._id.name:
3883 master = {pkey: master_id}
3884 else:
3885 # Different pkey (e.g. super-key) => reload the master
3886 query = (resource._id == master_id)
3887 master = current.db(query).select(resource.table[pkey],
3888 limitby=(0, 1)).first()
3889
3890 if master:
3891 # Find existing links
3892 query = FS(component.lkey) == master[pkey]
3893 lresource = s3db.resource(link.tablename, filter = query)
3894 rows = lresource.select([component.rkey], as_rows=True)
3895
3896 # Determine which to delete and which to add
3897 if rows:
3898 rkey = link.table[component.rkey]
3899 current_ids = set(str(row[rkey]) for row in rows)
3900 delete = current_ids - values
3901 insert = values - current_ids
3902 else:
3903 delete = None
3904 insert = values
3905
3906 # Delete links which are no longer used
3907 # @todo: apply filterby to only delete within the subset?
3908 if delete:
3909 query &= FS(component.rkey).belongs(delete)
3910 lresource = s3db.resource(link.tablename, filter = query)
3911 lresource.delete()
3912
3913 # Insert new links
3914 insert.discard("")
3915 if insert:
3916 # Insert new links
3917 for record_id in insert:
3918 record = {component.fkey: record_id}
3919 link.update_link(master, record)
3920
3921 success = True
3922
3923 return success
3924
3925 # -------------------------------------------------------------------------
3927 """
3928 Read-only representation of this subform.
3929
3930 @param value: the value as returned from extract()
3931 @return: the read-only representation
3932 """
3933
3934 component, link = self.get_link()
3935
3936 # Use the represent of rkey if it supports bulk, otherwise
3937 # instantiate an S3Represent from scratch:
3938 rkey = link.table[component.rkey]
3939 represent = rkey.represent
3940 if not hasattr(represent, "bulk"):
3941 # Pick the first field from the list that is available:
3942 lookup_field = None
3943 for fname in ("name", "tag"):
3944 if fname in component.fields:
3945 lookup_field = fname
3946 break
3947 from s3fields import S3Represent
3948 represent = S3Represent(lookup = component.tablename,
3949 fields = [lookup_field],
3950 )
3951
3952 # Represent all values
3953 if isinstance(value, (list, tuple, set)):
3954 result = represent.bulk(list(value))
3955 if None not in value:
3956 result.pop(None, None)
3957 else:
3958 result = represent.bulk([value])
3959
3960 # Sort them
3961 labels = result.values()
3962 labels.sort()
3963
3964 if self.options.get("render_list"):
3965 if value is None or value == [None]:
3966 # Don't render as list if empty
3967 return current.messages.NONE
3968 else:
3969 # Render as HTML list
3970 return UL([LI(l) for l in labels],
3971 _class = "s3-inline-link",
3972 )
3973 else:
3974 # Render as comma-separated list of strings
3975 # (using TAG rather than join() to support HTML labels)
3976 return TAG[""](list(chain.from_iterable([[l, ", "]
3977 for l in labels]))[:-1])
3978
3979 # -------------------------------------------------------------------------
3981 """
3982 Get the options for the widget
3983
3984 @return: dict {value: representation} of options
3985 """
3986
3987 resource = self.resource
3988 component, link = self.get_link()
3989
3990 rkey = link.table[component.rkey]
3991
3992 # Lookup rkey options from rkey validator
3993 opts = []
3994 requires = rkey.requires
3995 if not isinstance(requires, (list, tuple)):
3996 requires = [requires]
3997 if requires:
3998 validator = requires[0]
3999 if isinstance(validator, IS_EMPTY_OR):
4000 validator = validator.other
4001 try:
4002 opts = validator.options()
4003 except:
4004 pass
4005
4006 # Filter these options?
4007 widget_opts = self.options
4008 filterby = widget_opts.get("filterby")
4009 filteropts = widget_opts.get("options")
4010 filterexpr = widget_opts.get("match")
4011
4012 if filterby and \
4013 (filteropts is not None or filterexpr and resource._rows):
4014
4015 # filterby is a field selector for the component
4016 # that shall match certain conditions
4017 filter_selector = FS(filterby)
4018 filter_query = None
4019
4020 if filteropts is not None:
4021 # filterby-field shall match one of the given filteropts
4022 if isinstance(filteropts, (list, tuple, set)):
4023 filter_query = (filter_selector.belongs(list(filteropts)))
4024 else:
4025 filter_query = (filter_selector == filteropts)
4026
4027 elif filterexpr:
4028 # filterby-field shall match one of the values for the
4029 # filterexpr-field of the master record
4030 rfield = resource.resolve_selector(filterexpr)
4031 colname = rfield.colname
4032
4033 rows = resource.select([filterexpr], as_rows=True)
4034 values = set(row[colname] for row in rows)
4035 values.discard(None)
4036
4037 if values:
4038 filter_query = (filter_selector.belongs(values)) | \
4039 (filter_selector == None)
4040
4041 # Select the filtered component rows
4042 filter_resource = current.s3db.resource(component.tablename,
4043 filter = filter_query)
4044 rows = filter_resource.select(["id"], as_rows=True)
4045
4046 filtered_opts = []
4047 values = set(str(row[component.table._id]) for row in rows)
4048 for opt in opts:
4049 if str(opt[0]) in values:
4050 filtered_opts.append(opt)
4051 opts = filtered_opts
4052
4053 return dict(opts)
4054
4055 # -------------------------------------------------------------------------
4057 """
4058 Find the target component and its linktable
4059
4060 @return: tuple of S3Resource instances (component, link)
4061 """
4062
4063 selector = self.selector
4064 try:
4065 component = self.resource.components[selector]
4066 except KeyError:
4067 raise SyntaxError("Undefined component: %s" % selector)
4068
4069 link = component.link
4070 if not link:
4071 # @todo: better error message
4072 raise SyntaxError("No linktable for %s" % selector)
4073
4074 return (component, link)
4075
4076 # END =========================================================================
4077
| Home | Trees | Indices | Help |
|
|---|
| Generated by Epydoc 3.0.1 on Fri Mar 15 08:51:51 2019 | http://epydoc.sourceforge.net |