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

Source Code for Module s3.s3forms

   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 # -------------------------------------------------------------------------
64 - def __init__(self, *elements, **attributes):
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 # -------------------------------------------------------------------------
141 - def __len__(self):
142 """ 143 Support len(crud_form) 144 """ 145 146 return len(self.elements)
147 148 # -------------------------------------------------------------------------
149 - def _config(self, key, default=None):
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
165 - def _submit_buttons(readonly=False):
166 """ 167 Render submit buttons 168 169 @param readonly: render the form read-only 170 @return: list of submit buttons 171 """ 172 173 T = current.T 174 s3 = current.response.s3 175 settings = s3.crud 176 177 if settings.custom_submit: 178 submit = [(None, 179 settings.submit_button, 180 settings.submit_style)] 181 submit.extend(settings.custom_submit) 182 buttons = [] 183 for name, label, _class in submit: 184 if isinstance(label, basestring): 185 label = T(label) 186 button = INPUT(_type="submit", 187 _class="btn crud-submit-button", 188 _name=name, 189 _value=label) 190 if _class: 191 button.add_class(_class) 192 buttons.append(button) 193 else: 194 buttons = ["submit"] 195 196 # Cancel button 197 if not readonly and s3.cancel: 198 if not settings.custom_submit: 199 if settings.submit_button: 200 submit_label = T(settings.submit_button) 201 else: 202 submit_label = T("Save") 203 submit_button = INPUT(_type="submit", 204 _value=submit_label) 205 if settings.submit_style: 206 submit_button.add_class(settings.submit_style) 207 buttons = [submit_button] 208 209 cancel = s3.cancel 210 if isinstance(cancel, DIV): 211 cancel_button = cancel 212 else: 213 cancel_button = A(T("Cancel"), 214 _class="cancel-form-btn action-lnk") 215 if isinstance(cancel, dict): 216 # Script-controlled cancel button (embedded form) 217 if "script" in cancel: 218 # Custom script 219 script = cancel["script"] 220 else: 221 # Default script: hide form, show add-button 222 script = \ 223 '''$('.cancel-form-btn').click(function(){$('#%(hide)s').slideUp('medium',function(){$('#%(show)s').show()})})''' 224 s3.jquery_ready.append(script % cancel) 225 elif s3.cancel is True: 226 cancel_button.add_class("s3-cancel") 227 else: 228 cancel_button.update(_href=s3.cancel) 229 buttons.append(cancel_button) 230 231 return buttons
232 233 # ------------------------------------------------------------------------- 234 @staticmethod
235 - def _insert_subheadings(form, tablename, formstyle, subheadings):
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 # ------------------------------------------------------------------------- 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 # -------------------------------------------------------------------------
763 - def insert(self, index, element):
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 # -------------------------------------------------------------------------
791 - def append(self, element):
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 # -------------------------------------------------------------------------
1155 - def validate(self, form):
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 # -------------------------------------------------------------------------
1334 - def _extract(self, form, alias=None):
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 # -------------------------------------------------------------------------
1515 - def __init__(self, selector, **options):
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 # -------------------------------------------------------------------------
1528 - def resolve(self, resource):
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 # -------------------------------------------------------------------------
1672 - def resolve(self, resource):
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 # -------------------------------------------------------------------------
1742 - def resolve(self, resource):
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 # -------------------------------------------------------------------------
1773 - def __call__(self, field, value, **attributes):
1774 """ 1775 Widget renderer for field method values, renders a simple 1776 read-only DIV with the value 1777 """ 1778 1779 widget = DIV(value, **attributes) 1780 widget.add_class("s3-virtual-field") 1781 1782 return widget
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 # -------------------------------------------------------------------------
1793 - def resolve(self, resource):
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 # -------------------------------------------------------------------------
1817 - def __call__(self, field, value, **attributes):
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 # -------------------------------------------------------------------------
1843 - def extract(self, resource, record_id):
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 # -------------------------------------------------------------------------
1859 - def parse(self, value):
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 # -------------------------------------------------------------------------
1876 - def __call__(self, field, value, **attributes):
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 # -------------------------------------------------------------------------
1891 - def represent(self, value):
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 # -------------------------------------------------------------------------
1905 - def accept(self, form, master_id=None, format=None):
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 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
1973 - def __init__(self):
1974 """ Constructor """ 1975 1976 self.inject_script() 1977 self.columns = None 1978 self.row_actions = True
1979 1980 # -------------------------------------------------------------------------
1981 - def set_columns(self, columns, row_actions=True):
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 # -------------------------------------------------------------------------
2022 - def readonly(self, resource, data):
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
2056 - def render_list(resource, data):
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 # -------------------------------------------------------------------------
2091 - def headers(self, data, readonly=False):
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 # -------------------------------------------------------------------------
2193 - def rowstyle_read(self, form, fields, *args, **kwargs):
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 # -------------------------------------------------------------------------
2202 - def rowstyle(self, form, fields, *args, **kwargs):
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
2233 - def inject_script():
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 # -------------------------------------------------------------------------
2262 - def headers(self, data, readonly=False):
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 # -------------------------------------------------------------------------
2278 - def rowstyle_read(self, form, fields, *args, **kwargs):
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 # -------------------------------------------------------------------------
2288 - def rowstyle(self, form, fields, *args, **kwargs):
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 # -------------------------------------------------------------------------
2338 - def resolve(self, resource):
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 # -------------------------------------------------------------------------
2389 - def extract(self, resource, record_id):
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 # -------------------------------------------------------------------------
2571 - def parse(self, value):
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 # -------------------------------------------------------------------------
2600 - def __call__(self, field, value, **attributes):
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 # -------------------------------------------------------------------------
2874 - def represent(self, value):
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 # -------------------------------------------------------------------------
2913 - def accept(self, form, master_id=None, format=None):
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 # -------------------------------------------------------------------------
3188 - def _formname(self, separator=None):
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 # -------------------------------------------------------------------------
3204 - def _layout(self):
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 # -------------------------------------------------------------------------
3364 - def _filterby_query(self):
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 # -------------------------------------------------------------------------
3411 - def _filterby_defaults(self):
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 # -------------------------------------------------------------------------
3454 - def _filterby_options(self, fieldname):
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 # -------------------------------------------------------------------------
3533 - def _store_file(self, table, fieldname, rowindex):
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 4075 4076 # END ========================================================================= 4077