1 # -*- coding: iso-8859-1 -*- 2 """ 3 MoinMoin - MoinForms library 4 5 @copyright: 2012, 2013 by Paul Boddie <paul@boddie.org.uk> 6 @license: GNU GPL (v2 or later), see COPYING.txt for details. 7 """ 8 9 from compiler import parse 10 from compiler.ast import Const, Dict, Discard, List, Module, Stmt 11 from MoinMoin.action import do_show 12 from MoinMoin.Page import Page 13 from MoinMoin import security, wikiutil 14 from MoinSupport import * 15 import re 16 17 __version__ = "0.1" 18 19 form_field_regexp_str = r"<<Form(Field|Message)\((.*?)\)>>" 20 form_field_regexp = re.compile(form_field_regexp_str, re.DOTALL) 21 22 class MoinFormDataError(Exception): 23 24 "An exception indicating a problem with form data." 25 26 pass 27 28 # Common action functionality. 29 30 class MoinFormHandlerAction: 31 32 "A handler action that can be specialised for individual forms." 33 34 def __init__(self, pagename, request): 35 self.pagename = pagename 36 self.request = request 37 self.access_handler = None 38 self.attributes = None 39 40 def getAccessHandler(self): 41 42 """ 43 Return an access handler for the form whose attributes have been 44 obtained and stored in this instance. 45 """ 46 47 return FormAccess(self.pagename, self.request, self.attributes) 48 49 def processForm(self): 50 51 """ 52 Interpret the request details and modify them according to the structure 53 of the interpreted information. 54 """ 55 56 _ = self.request.getText 57 58 # Get the form fields and obtain the hierarchical field structure. 59 60 form = get_form(self.request) 61 fields = getFields(form, remove=True) 62 63 # Detect any request to load data. 64 65 if fields.has_key("load"): 66 try: 67 number = int(fields["load"][0]) 68 except ValueError: 69 fields = {} 70 else: 71 self.attributes, text = self.getFormForFragment(fields) 72 self.access_handler = self.getAccessHandler() 73 74 # Attempt to load the form. 75 76 try: 77 fields = self.loadFields(number) 78 79 # Absent or inaccessible forms will result in an IndexError. 80 81 except IndexError: 82 self.request.theme.add_msg(_("The stored data for this form cannot be accessed."), "error") 83 do_show(self.pagename, self.request) 84 return 85 86 # Bad data will result in a MoinFormDataError. 87 88 except MoinFormDataError: 89 self.request.theme.add_msg(_("The stored data for this form is in the wrong format."), "error") 90 do_show(self.pagename, self.request) 91 return 92 93 self.unfinished(fields, form) 94 95 # Otherwise, process any supplied data. 96 97 else: 98 # Modify and validate the form. 99 100 self.modifyFields(fields) 101 102 # Get the form definition. 103 104 self.attributes, text = self.getFormForFragment(fields) 105 self.access_handler = self.getAccessHandler() 106 structure = getFormStructure(text, self.request) 107 108 # Check the permissions on the form. 109 110 if not self.checkPermissions("write"): 111 self.request.theme.add_msg(_("You do not appear to have access to this form."), "error") 112 do_show(self.pagename, self.request) 113 return 114 115 # Without any form definition, the page is probably the wrong one. 116 117 if not structure: 118 self.request.theme.add_msg(_("This page does not provide a form."), "error") 119 do_show(self.pagename, self.request) 120 return 121 122 # With a form definition, attempt to validate the fields. 123 124 if self.validateFields(fields, structure): 125 if self.shouldFinish(fields): 126 self.finished(fields, form) 127 return 128 129 self.unfinished(fields, form) 130 131 def finished(self, fields, form): 132 133 "Handle the finished 'fields' and 'form'." 134 135 self.storeFields(fields) 136 self.unfinished(fields, form) 137 138 def unfinished(self, fields, form): 139 140 "Handle the unfinished 'fields' and 'form'." 141 142 # Serialise and show the form. 143 144 self.serialiseFields(fields, form) 145 do_show(self.pagename, self.request) 146 147 def shouldFinish(self, fields): 148 149 """ 150 Subject to the attributes stored for the form in this instance, return 151 whether any field referenced by the "finishing" attribute is present 152 and thus indicate whether the form handling should finish. 153 """ 154 155 finishing = self.attributes.has_key("finishing") and self.attributes["finishing"].split(",") 156 157 if finishing: 158 for name in finishing: 159 if fields.has_key(name): 160 return True 161 162 return False 163 164 def getFormForFragment(self, fields): 165 166 "Return the attributes and text of the form being handled." 167 168 fragment = fields.get("fragment", [None])[0] 169 text = Page(self.request, self.pagename).get_raw_body() 170 return getFormForFragment(text, fragment) 171 172 def checkPermissions(self, action): 173 174 """ 175 Check the permissions of the user against any restrictions specified in 176 the form's 'attributes'. 177 """ 178 179 return self.access_handler.checkPermissions(action) 180 181 def validateFields(self, fields, structure): 182 183 """ 184 Validate the given 'fields' using the given form 'structure', 185 introducing error fields where the individual fields do not conform to 186 their descriptions. 187 """ 188 189 return self.validateFieldsUsingStructure(fields, structure) 190 191 def validateFieldsUsingStructure(self, fields, structure): 192 193 "Validate the given 'fields' using the given 'structure'." 194 195 _ = self.request.getText 196 valid = True 197 198 for key, definition in structure.items(): 199 value = fields.get(key) 200 201 # Enter form sections and validate them. 202 203 if isinstance(definition, dict): 204 if value: 205 for element in getSectionElements(value): 206 valid = self.validateFieldsUsingStructure(element, structure[key]) and valid 207 208 # Validate individual fields. 209 210 elif structure.has_key(key): 211 path, dictpage, label, section, field_args, allowed_values = definition 212 errors = [] 213 214 # Test for obligatory values. 215 216 if not value or not value[0]: 217 if field_args.get("required"): 218 219 # Detect new parts of the structure and avoid producing 220 # premature error messages. 221 222 if not fields.has_key("_new"): 223 errors.append(_("This field must be filled out.")) 224 else: 225 valid = False 226 else: 227 # Test for unacceptable values. 228 229 if allowed_values and set(value).difference(allowed_values): 230 errors.append(_("At least one of the choices is not acceptable.")) 231 232 # Test the number of values. 233 234 if field_args.get("type") == "select": 235 if field_args.has_key("maxselected"): 236 if len(value) > int(field_args["maxselected"]): 237 errors.append(_("Incorrect number of choices given: need %s.") % field_args["maxselected"]) 238 239 if errors: 240 fields["%s-error" % key] = errors 241 valid = False 242 243 return valid 244 245 def serialiseFields(self, fields, form, path=None): 246 247 """ 248 Serialise the given 'fields' to the given 'form', using the given 'path' 249 to name the entries. 250 """ 251 252 for key, value in fields.items(): 253 254 # Serialise sections. 255 256 if isinstance(value, dict): 257 for index, element in enumerate(getSectionElements(value)): 258 element_ref = "%s$%s" % (key, index) 259 260 self.serialiseFields(element, form, 261 path and ("%s/%s" % (path, element_ref)) or element_ref 262 ) 263 264 # Serialise fields. 265 266 else: 267 form[path and ("%s/%s" % (path, key)) or key] = value 268 269 def modifyFields(self, fields): 270 271 "Modify the given 'fields', removing and adding items." 272 273 # First, remove fields. 274 275 for key in fields.keys(): 276 if key.startswith("_remove="): 277 self.removeField(key[8:], fields) 278 279 # Then, add fields. 280 281 for key in fields.keys(): 282 if key.startswith("_add="): 283 self.addField(key[5:], fields) 284 285 def removeField(self, path, fields): 286 287 """ 288 Remove the section element indicated by the given 'path' from the 289 'fields'. 290 """ 291 292 section, (name, index) = getSectionForPath(path, fields) 293 try: 294 del section[name][index] 295 except KeyError: 296 pass 297 298 def addField(self, path, fields): 299 300 """ 301 Add a section element indicated by the given 'path' to the 'fields'. 302 """ 303 304 section, (name, index) = getSectionForPath(path, fields) 305 placeholder = {"_new" : ""} 306 307 if section.has_key(name): 308 indexes = section[name].keys() 309 max_index = max(map(int, indexes)) 310 section[name][max_index + 1] = placeholder 311 else: 312 max_index = -1 313 section[name] = {0 : placeholder} 314 315 # Storage of form submissions. 316 317 def storeFields(self, fields): 318 319 "Store the given 'fields' as a Python object representation." 320 321 store = FormStore(self.access_handler) 322 store.append(repr(fields)) 323 324 def loadFields(self, number): 325 326 "Load the fields associated with the given submission 'number'." 327 328 store = FormStore(self.access_handler) 329 return loadFields(store, number) 330 331 def loadFields(store, number): 332 333 """ 334 Load the fields from the 'store' that are associated with the given 335 submission 'number'. 336 """ 337 338 text = store[number] 339 module = parse(text) 340 if checkStoredFormData(module): 341 return eval(text) 342 else: 343 raise MoinFormDataError, text 344 345 def checkStoredFormData(node): 346 347 """ 348 Check the syntax 'node' and its descendants for suitability as parts of 349 a field definition. 350 """ 351 352 for child in node.getChildNodes(): 353 if isinstance(child, Const): 354 pass 355 elif not isinstance(child, (Dict, Discard, List, Module, Stmt)) or not checkStoredFormData(child): 356 return False 357 358 return True 359 360 class FormAccess: 361 362 "A means of checking access to form data." 363 364 def __init__(self, pagename, request, attributes): 365 self.pagename = pagename 366 self.request = request 367 self.attributes = attributes 368 369 def checkPermissions(self, action): 370 371 """ 372 Check the permissions of the user against any restrictions specified in 373 the form's 'attributes'. 374 """ 375 376 user = self.request.user 377 378 # Use the access definition if one is given. 379 380 if self.attributes.has_key("access"): 381 access = self.attributes["access"] 382 acl = security.AccessControlList(self.request.cfg, [access]) 383 policy = lambda request, pagename, username, action: acl.may(request, username, action) 384 385 # Otherwise, use the page permissions. 386 387 else: 388 policy = security._check 389 390 # The "read" action is only satisfied by the "admin" role. 391 392 return user and ( 393 action != "read" and policy(self.request, self.pagename, user.name, action) or 394 action == "read" and policy(self.request, self.pagename, user.name, "admin") 395 ) 396 397 class FormStore(ItemStore): 398 399 "A form-specific storage mechanism." 400 401 def __init__(self, handler): 402 403 "Initialise the store with the form 'handler'." 404 405 self.handler = handler 406 page = Page(handler.request, handler.pagename) 407 ItemStore.__init__(self, page, "forms", "form-locks") 408 409 def can_write(self): 410 411 """ 412 Permit writing of form data using the form attributes or page 413 permissions. 414 """ 415 416 return self.handler.checkPermissions("write") 417 418 def can_read(self): 419 420 """ 421 Permit reading of form data using the form attributes or page 422 permissions. 423 """ 424 425 return self.handler.checkPermissions("read") 426 427 # Form and field information. 428 429 def getFormStructure(text, request, path=None, structure=None): 430 431 """ 432 For the given form 'text' and using the 'request', return details of the 433 form for the section at the given 'path' (or the entire form if 'path' is 434 omitted), populating the given 'structure' (or populating a new structure if 435 'structure' is omitted). 436 """ 437 438 if structure is None: 439 structure = {} 440 441 for format, attributes, body in getFragments(text, True): 442 443 # Get field details at the current level. 444 445 if format is None: 446 structure.update(getFormFields(body, path, request)) 447 448 # Where a section is found, get details from within the section. 449 450 elif format == "form": 451 if attributes.has_key("section"): 452 section_name = attributes["section"] 453 section = structure[section_name] = {} 454 getFormStructure(body, request, path and ("%s/%s" % (path, section_name)) or section_name, section) 455 elif attributes.has_key("message"): 456 getFormStructure(body, request, path, structure) 457 elif attributes.has_key("not-message"): 458 getFormStructure(body, request, path, structure) 459 460 # Get field details from other kinds of region. 461 462 elif format != "form": 463 getFormStructure(body, request, path, structure) 464 465 return structure 466 467 def getFormForFragment(text, fragment=None): 468 469 """ 470 Return the form region from the given 'text' for the specified 'fragment'. 471 If no fragment is specified, the first form region is returned. The form 472 region is described using a tuple containing the attributes for the form 473 and the body text of the form. 474 """ 475 476 for format, attributes, body in getFragments(text): 477 if format == "form" and (not fragment or attributes.get("fragment") == fragment): 478 return attributes, body 479 480 return {}, None 481 482 def getFieldArguments(field_definition): 483 484 "Return the parsed arguments from the given 'field_definition' string." 485 486 field_args = {} 487 488 for field_arg in field_definition.split(): 489 if field_arg == "required": 490 field_args[field_arg] = True 491 continue 492 493 # Record the key-value details. 494 495 try: 496 argname, argvalue = field_arg.split("=", 1) 497 field_args[argname] = argvalue 498 499 # Single keywords are interpreted as type descriptions. 500 501 except ValueError: 502 if not field_args.has_key("type"): 503 field_args["type"] = field_arg 504 505 return field_args 506 507 # Common formatting functions. 508 509 def getFormOutput(text, fields, form_fragment=None, path=None, fragment=None, repeating=None, index=None): 510 511 """ 512 Combine regions found in the given 'text' and then return them as a single 513 block. The reason for doing this, as opposed to just passing each region to 514 a suitable parser for formatting, is that form sections may break up 515 regions, and such sections may not define separate subregions but instead 516 act as a means of conditional inclusion of text into an outer region. 517 518 The given 'fields' are used to populate fields provided in forms and to 519 control whether sections are populated or not. 520 521 The optional 'form_fragment' is used to indicate the form to which the 522 fields belong. 523 524 The optional 'path' is used to adjust form fields to refer to the correct 525 part of the form hierarchy. 526 527 The optional 'fragment' is used to indicate the form being output. If this 528 value is different to 'form_fragment', the structure of the form should not 529 be influenced by the 'fields'. 530 531 The optional 'repeating' and 'index' is used to refer to individual values 532 of a designated field. 533 """ 534 535 this_form = fragment and form_fragment == fragment or not fragment and not form_fragment 536 537 output = [] 538 section = fields 539 540 for region in getRegions(text, True): 541 format, attributes, body, header, close = getFragmentFromRegion(region) 542 543 # Adjust any FormField macros to use hierarchical names. 544 545 if format is None: 546 output.append((path or fragment or repeating) and 547 adjustFormFields(body, path, fragment, repeating, index) or body) 548 549 # Include form sections only if fields exist for those sections. 550 551 elif format == "form": 552 section_name = attributes.get("section") 553 message_name = attributes.get("message") 554 absent_message_name = attributes.get("not-message") 555 556 # Ignore sections not related to the supplied field data. 557 558 if not this_form: 559 pass 560 561 # Sections are groups of fields in their own namespace. 562 563 elif section_name and section.has_key(section_name): 564 565 # Iterate over the section contents ignoring the given indexes. 566 567 for index, element in enumerate(getSectionElements(section[section_name])): 568 element_ref = "%s$%s" % (section_name, index) 569 570 # Get the output for the section. 571 572 output.append(getFormOutput(body, element, form_fragment, 573 path and ("%s/%s" % (path, element_ref)) or element_ref, fragment)) 574 575 # Message regions are conditional on a particular field and 576 # reference the current namespace. 577 578 elif message_name and section.has_key(message_name): 579 580 if attributes.get("repeating"): 581 for index in range(0, len(section[message_name])): 582 output.append(getFormOutput(body, section, form_fragment, path, fragment, message_name, index)) 583 else: 584 output.append(getFormOutput(body, section, form_fragment, path, fragment)) 585 586 # Not-message regions are conditional on a particular field being 587 # absent. They reference the current namespace. 588 589 elif absent_message_name and not section.has_key(absent_message_name): 590 output.append(getFormOutput(body, section, form_fragment, path, fragment)) 591 592 # Inspect and include other regions. 593 594 else: 595 output.append(header) 596 output.append(getFormOutput(body, section, form_fragment, path, fragment, repeating, index)) 597 output.append(close) 598 599 return "".join(output) 600 601 def getFormFields(body, path, request): 602 603 "Return a dictionary of fields from the given 'body' at the given 'path'." 604 605 fields = {} 606 cache = {} 607 type = None 608 609 for i, match in enumerate(form_field_regexp.split(body)): 610 state = i % 3 611 612 if state == 1: 613 type = match 614 elif state == 2 and type == "Field": 615 args = {} 616 617 # Obtain the macro arguments, adjusted to consider the path. 618 619 name, path, dictpage, label, section, fragment = \ 620 getMacroArguments(adjustMacroArguments(parseMacroArguments(match), path)) 621 622 # Obtain field information from the cache, if possible. 623 624 cache_key = (name, dictpage) 625 626 if cache.has_key(cache_key): 627 field_args, allowed_values = cache[cache_key] 628 629 # Otherwise, obtain field information from any WikiDict. 630 631 else: 632 field_args = {} 633 allowed_values = None 634 635 if dictpage: 636 wikidict = getWikiDict(dictpage, request) 637 if wikidict: 638 field_definition = wikidict.get(name) 639 if field_definition: 640 field_args = getFieldArguments(field_definition) 641 if field_args.has_key("source"): 642 sourcedict = getWikiDict(field_args["source"], request) 643 if sourcedict: 644 allowed_values = sourcedict.keys() 645 646 cache[cache_key] = field_args, allowed_values 647 648 # Store the field information. 649 650 fields[name] = path, dictpage, label, section, field_args, allowed_values 651 652 return fields 653 654 def adjustFormFields(body, path, fragment, repeating=None, index=None): 655 656 """ 657 Return a version of the 'body' with the names in FormField macros updated to 658 incorporate the given 'path' and 'fragment'. If 'repeating' is specified, 659 any field with such a name will be adjusted to reference the value with the 660 given 'index'. 661 """ 662 663 result = [] 664 type = None 665 666 for i, match in enumerate(form_field_regexp.split(body)): 667 state = i % 3 668 669 # Reproduce normal text as is. 670 671 if state == 0: 672 result.append(match) 673 674 # Capture the macro type. 675 676 elif state == 1: 677 type = match 678 679 # Substitute the macro and modified arguments. 680 681 else: 682 result.append("<<Form%s(%s)>>" % (type, ",".join( 683 adjustMacroArguments(parseMacroArguments(match), path, fragment, repeating, index) 684 ))) 685 686 return "".join(result) 687 688 def adjustMacroArguments(args, path, fragment=None, repeating=None, index=None): 689 690 """ 691 Adjust the given 'args' so that the path incorporates the given 692 'path' and 'fragment', returning a new list containing the revised path, 693 fragment and remaining arguments. If 'repeating' is specified, any field 694 with such a name will be adjusted to reference the value with the given 695 'index'. 696 """ 697 698 if not path and not fragment and not repeating: 699 return args 700 701 result = [] 702 old_path = None 703 found_name = None 704 705 for arg in args: 706 if arg.startswith("path="): 707 old_path = arg[5:] 708 elif arg.startswith("fragment=") and fragment: 709 pass 710 else: 711 result.append(arg) 712 if arg.startswith("name="): 713 found_name = arg[5:] 714 elif found_name is None: 715 found_name = arg 716 717 if path: 718 qualified = old_path and ("%s/%s" % (old_path, path)) or path 719 result.append("path=%s" % qualified) 720 721 if fragment: 722 result.append("fragment=%s" % fragment) 723 724 if repeating and repeating == found_name: 725 result.append("index=%s" % index) 726 727 return result 728 729 def parseMacroArguments(args): 730 731 """ 732 Interpret the arguments. To support commas in labels, the label argument 733 should be quoted. For example: 734 735 "label=No, thanks!" 736 """ 737 738 try: 739 parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] 740 except AttributeError: 741 parsed_args = args.split(",") 742 743 return [arg for arg in parsed_args if arg] 744 745 def getMacroArguments(parsed_args): 746 747 "Return the macro arguments decoded from 'parsed_args'." 748 749 name = None 750 path = None 751 dictpage = None 752 label = None 753 section = None 754 fragment = None 755 756 for arg in parsed_args: 757 if arg.startswith("name="): 758 name = arg[5:] 759 760 elif arg.startswith("path="): 761 path = arg[5:] 762 763 elif arg.startswith("dict="): 764 dictpage = arg[5:] 765 766 elif arg.startswith("label="): 767 label = arg[6:] 768 769 elif arg.startswith("section="): 770 section = arg[8:] 771 772 elif arg.startswith("fragment="): 773 fragment = arg[9:] 774 775 elif name is None: 776 name = arg 777 778 elif dictpage is None: 779 dictpage = arg 780 781 return name, path, dictpage, label, section, fragment 782 783 def getFields(d, remove=False): 784 785 """ 786 Return the form fields hierarchy for the given dictionary 'd'. If the 787 optional 'remove' parameter is set to a true value, remove the entries for 788 the fields from 'd'. 789 """ 790 791 fields = {} 792 793 for key, value in d.items(): 794 795 # Detect modifying fields. 796 797 if key.find("=") != -1: 798 fields[key] = value 799 if remove: 800 del d[key] 801 continue 802 803 # Reproduce the original hierarchy of the fields. 804 805 section = fields 806 parts = getPathDetails(key) 807 808 for name, index in parts[:-1]: 809 810 # Add an entry for instances of the section. 811 812 if not section.has_key(name): 813 section[name] = {} 814 815 # Add an entry for the specific instance of the section. 816 817 if not section[name].has_key(index): 818 section[name][index] = {} 819 820 section = section[name][index] 821 822 section[parts[-1][0]] = value 823 824 if remove: 825 del d[key] 826 827 return fields 828 829 def getPathDetails(path): 830 831 """ 832 Return the given 'path' as a list of (name, index) tuples providing details 833 of section instances, with any specific field appearing as the last element 834 and having the form (name, None). 835 """ 836 837 parts = [] 838 839 for part in path.split("/"): 840 try: 841 name, index = part.split("$", 1) 842 index = int(index) 843 except ValueError: 844 name, index = part, None 845 846 parts.append((name, index)) 847 848 return parts 849 850 def getSectionForPath(path, fields): 851 852 """ 853 Obtain the section indicated by the given 'path' from the 'fields', 854 returning a tuple of the form (parent section, (name, index)), where the 855 parent section contains the referenced section, where name is the name of 856 the referenced section, and where index, if not None, is the index of a 857 specific section instance within the named section. 858 """ 859 860 parts = getPathDetails(path) 861 section = fields 862 863 for name, index in parts[:-1]: 864 section = fields[name][index] 865 866 return section, parts[-1] 867 868 def getSectionElements(section_elements): 869 870 "Return the given 'section_elements' as an ordered collection." 871 872 keys = map(int, section_elements.keys()) 873 keys.sort() 874 875 elements = [] 876 877 for key in keys: 878 elements.append(section_elements[key]) 879 880 return elements 881 882 # Parser-related formatting functions. 883 884 def formatForm(text, request, fmt, attrs=None, write=None): 885 886 """ 887 Format the given 'text' using the specified 'request' and formatter 'fmt'. 888 The optional 'attrs' can be used to control the presentation of the form. 889 890 If the 'write' parameter is specified, use it to write output; otherwise, 891 write output using the request. 892 """ 893 894 write = write or request.write 895 page = request.page 896 897 form = get_form(request) 898 form_fragment = form.get("fragment", [None])[0] 899 fields = getFields(form) 900 901 # Prepare the query string for the form action URL. 902 903 queryparams = [] 904 905 for argname, default in [("fragment", None), ("action", "MoinFormHandler")]: 906 if attrs and attrs.has_key(argname): 907 queryparams.append("%s=%s" % (argname, attrs[argname])) 908 elif default: 909 queryparams.append("%s=%s" % (argname, default)) 910 911 querystr = "&".join(queryparams) 912 fragment = attrs.get("fragment") 913 914 write(fmt.rawHTML('<form method="post" action="%s%s"%s>' % ( 915 escattr(page.url(request, querystr)), 916 fragment and ("#%s" % escattr(fragment)) or "", 917 fragment and (' id="%s"' % escattr(fragment)) or "" 918 ))) 919 920 # Obtain page text for the form, incorporating subregions and applicable 921 # sections. 922 923 output = getFormOutput(text, fields, form_fragment=form_fragment, fragment=fragment) 924 write(formatText(output, request, fmt, inhibit_p=False)) 925 926 write(fmt.rawHTML('</form>')) 927 928 def formatFormForOutputType(text, request, mimetype, attrs=None, write=None): 929 930 """ 931 Format the given 'text' using the specified 'request' for the given output 932 'mimetype'. 933 934 The optional 'attrs' can be used to control the presentation of the form. 935 936 If the 'write' parameter is specified, use it to write output; otherwise, 937 write output using the request. 938 """ 939 940 write = write or request.write 941 942 if mimetype == "text/html": 943 write('<html>') 944 write('<body>') 945 fmt = request.html_formatter 946 fmt.setPage(request.page) 947 formatForm(text, request, fmt, attrs, write) 948 write('</body>') 949 write('</html>') 950 951 # vim: tabstop=4 expandtab shiftwidth=4