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 MoinMoin.action import do_show 10 from MoinMoin.Page import Page 11 from MoinMoin import wikiutil 12 from MoinSupport import * 13 import re 14 15 __version__ = "0.1" 16 17 form_field_regexp_str = r"<<Form(Field|Message)\((.*?)\)>>" 18 form_field_regexp = re.compile(form_field_regexp_str, re.DOTALL) 19 20 # Common action functionality. 21 22 class MoinFormHandlerAction: 23 24 "A handler action that can be specialised for individual forms." 25 26 def __init__(self, pagename, request): 27 self.pagename = pagename 28 self.request = request 29 30 def processForm(self): 31 32 """ 33 Interpret the request details and modify them according to the structure 34 of the interpreted information. 35 """ 36 37 _ = self.request.getText 38 39 # Get the form fields and obtain the hierarchical field structure. 40 41 form = get_form(self.request) 42 fields = getFields(form, remove=True) 43 44 # Modify and validate the form. 45 46 self.modifyFields(fields) 47 48 # Get the form definition. 49 50 structure = self.getFormStructure(fields) 51 52 # Without any form definition, the page is probably the wrong one. 53 54 if not structure: 55 self.request.theme.add_msg(_("This page does not provide a form."), "error") 56 do_show(self.pagename, self.request) 57 return 58 59 # With a form definition, attempt to validate the fields. 60 61 if self.validateFields(fields, structure): 62 self.finished(fields, form) 63 else: 64 self.unfinished(fields, form) 65 66 def finished(self, fields, form): 67 68 "Handle the finished 'fields' and 'form'." 69 70 self.unfinished(fields, form) 71 72 def unfinished(self, fields, form): 73 74 "Handle the unfinished 'fields' and 'form'." 75 76 # Serialise and show the form. 77 78 self.serialiseFields(fields, form) 79 do_show(self.pagename, self.request) 80 81 def getFormStructure(self, fields): 82 83 "Return the structure of the form being handled." 84 85 fragment = fields.get("fragment", [None])[0] 86 text = Page(self.request, self.pagename).get_raw_body() 87 text = getFormForFragment(text, fragment) 88 return getFormStructure(text, self.request) 89 90 def validateFields(self, fields, structure): 91 92 """ 93 Validate the given 'fields' using the given form 'structure', 94 introducing error fields where the individual fields do not conform to 95 their descriptions. 96 """ 97 98 return self.validateFieldsUsingStructure(fields, structure) 99 100 def validateFieldsUsingStructure(self, fields, structure): 101 102 "Validate the given 'fields' using the given 'structure'." 103 104 _ = self.request.getText 105 valid = True 106 107 for key, definition in structure.items(): 108 value = fields.get(key) 109 110 # Enter form sections and validate them. 111 112 if isinstance(definition, dict): 113 if value: 114 for element in getSectionElements(value): 115 valid = self.validateFieldsUsingStructure(element, structure[key]) and valid 116 117 # Validate individual fields. 118 119 elif structure.has_key(key): 120 path, dictpage, label, section, field_args, allowed_values = definition 121 errors = [] 122 123 # Test for obligatory values. 124 125 if not value or not value[0]: 126 if field_args.get("required"): 127 128 # Detect new parts of the structure and avoid producing 129 # premature error messages. 130 131 if not fields.has_key("_new"): 132 errors.append(_("This field must be filled out.")) 133 else: 134 valid = False 135 else: 136 # Test for unacceptable values. 137 138 if allowed_values and set(value).difference(allowed_values): 139 errors.append(_("At least one of the choices is not acceptable.")) 140 141 # Test the number of values. 142 143 if field_args.get("type") == "select": 144 if field_args.has_key("maxselected"): 145 if len(value) > int(field_args["maxselected"]): 146 errors.append(_("Incorrect number of choices given: need %s.") % field_args["maxselected"]) 147 148 if errors: 149 fields["%s-error" % key] = errors 150 valid = False 151 152 return valid 153 154 def serialiseFields(self, fields, form, path=None): 155 156 """ 157 Serialise the given 'fields' to the given 'form', using the given 'path' 158 to name the entries. 159 """ 160 161 for key, value in fields.items(): 162 163 # Serialise sections. 164 165 if isinstance(value, dict): 166 for index, element in enumerate(getSectionElements(value)): 167 element_ref = "%s$%s" % (key, index) 168 169 self.serialiseFields(element, form, 170 path and ("%s/%s" % (path, element_ref)) or element_ref 171 ) 172 173 # Serialise fields. 174 175 else: 176 form[path and ("%s/%s" % (path, key)) or key] = value 177 178 def modifyFields(self, fields): 179 180 "Modify the given 'fields', removing and adding items." 181 182 # First, remove fields. 183 184 for key in fields.keys(): 185 if key.startswith("_remove="): 186 self.removeField(key[8:], fields) 187 188 # Then, add fields. 189 190 for key in fields.keys(): 191 if key.startswith("_add="): 192 self.addField(key[5:], fields) 193 194 def removeField(self, path, fields): 195 196 """ 197 Remove the section element indicated by the given 'path' from the 198 'fields'. 199 """ 200 201 section, (name, index) = getSectionForPath(path, fields) 202 try: 203 del section[name][index] 204 except KeyError: 205 pass 206 207 def addField(self, path, fields): 208 209 """ 210 Add a section element indicated by the given 'path' to the 'fields'. 211 """ 212 213 section, (name, index) = getSectionForPath(path, fields) 214 placeholder = {"_new" : ""} 215 216 if section.has_key(name): 217 indexes = section[name].keys() 218 max_index = max(map(int, indexes)) 219 section[name][max_index + 1] = placeholder 220 else: 221 max_index = -1 222 section[name] = {0 : placeholder} 223 224 # Form and field information. 225 226 def getFormStructure(text, request, path=None, structure=None): 227 228 """ 229 For the given form 'text' and using the 'request', return details of the 230 form for the section at the given 'path' (or the entire form if 'path' is 231 omitted), populating the given 'structure' (or populating a new structure if 232 'structure' is omitted). 233 """ 234 235 if structure is None: 236 structure = {} 237 238 for format, attributes, body in getFragments(text, True): 239 240 # Get field details at the current level. 241 242 if format is None: 243 structure.update(getFormFields(body, path, request)) 244 245 # Where a section is found, get details from within the section. 246 247 elif format == "form": 248 if attributes.has_key("section"): 249 section_name = attributes["section"] 250 section = structure[section_name] = {} 251 getFormStructure(body, request, path and ("%s/%s" % (path, section_name)) or section_name, section) 252 elif attributes.has_key("message"): 253 getFormStructure(body, request, path, structure) 254 elif attributes.has_key("not-message"): 255 getFormStructure(body, request, path, structure) 256 257 # Get field details from other kinds of region. 258 259 elif format != "form": 260 getFormStructure(body, request, path, structure) 261 262 return structure 263 264 def getFormForFragment(text, fragment=None): 265 266 """ 267 Return the form region from the given 'text' for the specified 'fragment'. 268 If no fragment is specified, the first form region is returned. 269 """ 270 271 for format, attributes, body in getFragments(text): 272 if not fragment or attributes.get("fragment") == fragment: 273 return body 274 275 return None 276 277 def getFieldArguments(field_definition): 278 279 "Return the parsed arguments from the given 'field_definition' string." 280 281 field_args = {} 282 283 for field_arg in field_definition.split(): 284 if field_arg == "required": 285 field_args[field_arg] = True 286 continue 287 288 # Record the key-value details. 289 290 try: 291 argname, argvalue = field_arg.split("=", 1) 292 field_args[argname] = argvalue 293 294 # Single keywords are interpreted as type descriptions. 295 296 except ValueError: 297 if not field_args.has_key("type"): 298 field_args["type"] = field_arg 299 300 return field_args 301 302 # Common formatting functions. 303 304 def getFormOutput(text, fields, path=None, fragment=None, repeating=None, index=None): 305 306 """ 307 Combine regions found in the given 'text' and then return them as a single 308 block. The reason for doing this, as opposed to just passing each region to 309 a suitable parser for formatting, is that form sections may break up 310 regions, and such sections may not define separate subregions but instead 311 act as a means of conditional inclusion of text into an outer region. 312 313 The given 'fields' are used to populate fields provided in forms and to 314 control whether sections are populated or not. 315 316 The optional 'path' is used to adjust form fields to refer to the correct 317 part of the form hierarchy. 318 319 The optional 'fragment' is used to indicate the form to which the fields 320 belong. 321 322 The optional 'repeating' and 'index' is used to refer to individual values 323 of a designated field. 324 """ 325 326 output = [] 327 section = fields 328 329 for region in getRegions(text, True): 330 format, attributes, body, header, close = getFragmentFromRegion(region) 331 332 # Adjust any FormField macros to use hierarchical names. 333 334 if format is None: 335 output.append((path or fragment or repeating) and 336 adjustFormFields(body, path, fragment, repeating, index) or body) 337 338 # Include form sections only if fields exist for those sections. 339 340 elif format == "form": 341 section_name = attributes.get("section") 342 message_name = attributes.get("message") 343 absent_message_name = attributes.get("not-message") 344 345 # Sections are groups of fields in their own namespace. 346 347 if section_name and section.has_key(section_name): 348 349 # Iterate over the section contents ignoring the given indexes. 350 351 for index, element in enumerate(getSectionElements(section[section_name])): 352 element_ref = "%s$%s" % (section_name, index) 353 354 # Get the output for the section. 355 356 output.append(getFormOutput(body, element, 357 path and ("%s/%s" % (path, element_ref)) or element_ref, fragment)) 358 359 # Message regions are conditional on a particular field and 360 # reference the current namespace. 361 362 elif message_name and section.has_key(message_name): 363 364 if attributes.get("repeating"): 365 for index in range(0, len(section[message_name])): 366 output.append(getFormOutput(body, section, path, fragment, message_name, index)) 367 else: 368 output.append(getFormOutput(body, section, path, fragment)) 369 370 # Not-message regions are conditional on a particular field being 371 # absent. They reference the current namespace. 372 373 elif absent_message_name and not section.has_key(absent_message_name): 374 output.append(getFormOutput(body, section, path, fragment)) 375 376 # Inspect and include other regions. 377 378 else: 379 output.append(header) 380 output.append(getFormOutput(body, section, path, fragment, repeating, index)) 381 output.append(close) 382 383 return "".join(output) 384 385 def getFormFields(body, path, request): 386 387 "Return a dictionary of fields from the given 'body' at the given 'path'." 388 389 fields = {} 390 cache = {} 391 type = None 392 393 for i, match in enumerate(form_field_regexp.split(body)): 394 state = i % 3 395 396 if state == 1: 397 type = match 398 elif state == 2 and type == "Field": 399 args = {} 400 401 # Obtain the macro arguments, adjusted to consider the path. 402 403 name, path, dictpage, label, section, fragment = \ 404 getMacroArguments(adjustMacroArguments(parseMacroArguments(match), path)) 405 406 # Obtain field information from the cache, if possible. 407 408 cache_key = (name, dictpage) 409 410 if cache.has_key(cache_key): 411 field_args, allowed_values = cache[cache_key] 412 413 # Otherwise, obtain field information from any WikiDict. 414 415 else: 416 field_args = {} 417 allowed_values = None 418 419 if dictpage: 420 wikidict = getWikiDict(dictpage, request) 421 if wikidict: 422 field_definition = wikidict.get(name) 423 if field_definition: 424 field_args = getFieldArguments(field_definition) 425 if field_args.has_key("source"): 426 sourcedict = getWikiDict(field_args["source"], request) 427 if sourcedict: 428 allowed_values = sourcedict.keys() 429 430 cache[cache_key] = field_args, allowed_values 431 432 # Store the field information. 433 434 fields[name] = path, dictpage, label, section, field_args, allowed_values 435 436 return fields 437 438 def adjustFormFields(body, path, fragment, repeating=None, index=None): 439 440 """ 441 Return a version of the 'body' with the names in FormField macros updated to 442 incorporate the given 'path' and 'fragment'. If 'repeating' is specified, 443 any field with such a name will be adjusted to reference the value with the 444 given 'index'. 445 """ 446 447 result = [] 448 type = None 449 450 for i, match in enumerate(form_field_regexp.split(body)): 451 state = i % 3 452 453 # Reproduce normal text as is. 454 455 if state == 0: 456 result.append(match) 457 458 # Capture the macro type. 459 460 elif state == 1: 461 type = match 462 463 # Substitute the macro and modified arguments. 464 465 else: 466 result.append("<<Form%s(%s)>>" % (type, ",".join( 467 adjustMacroArguments(parseMacroArguments(match), path, fragment, repeating, index) 468 ))) 469 470 return "".join(result) 471 472 def adjustMacroArguments(args, path, fragment=None, repeating=None, index=None): 473 474 """ 475 Adjust the given 'args' so that the path incorporates the given 476 'path' and 'fragment', returning a new list containing the revised path, 477 fragment and remaining arguments. If 'repeating' is specified, any field 478 with such a name will be adjusted to reference the value with the given 479 'index'. 480 """ 481 482 if not path and not fragment and not repeating: 483 return args 484 485 result = [] 486 old_path = None 487 found_name = None 488 489 for arg in args: 490 if arg.startswith("path="): 491 old_path = arg[5:] 492 elif arg.startswith("fragment=") and fragment: 493 pass 494 else: 495 result.append(arg) 496 if arg.startswith("name="): 497 found_name = arg[5:] 498 elif found_name is None: 499 found_name = arg 500 501 if path: 502 qualified = old_path and ("%s/%s" % (old_path, path)) or path 503 result.append("path=%s" % qualified) 504 505 if fragment: 506 result.append("fragment=%s" % fragment) 507 508 if repeating and repeating == found_name: 509 result.append("index=%s" % index) 510 511 return result 512 513 def parseMacroArguments(args): 514 515 """ 516 Interpret the arguments. To support commas in labels, the label argument 517 should be quoted. For example: 518 519 "label=No, thanks!" 520 """ 521 522 try: 523 parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] 524 except AttributeError: 525 parsed_args = args.split(",") 526 527 return [arg for arg in parsed_args if arg] 528 529 def getMacroArguments(parsed_args): 530 531 "Return the macro arguments decoded from 'parsed_args'." 532 533 name = None 534 path = None 535 dictpage = None 536 label = None 537 section = None 538 fragment = None 539 540 for arg in parsed_args: 541 if arg.startswith("name="): 542 name = arg[5:] 543 544 elif arg.startswith("path="): 545 path = arg[5:] 546 547 elif arg.startswith("dict="): 548 dictpage = arg[5:] 549 550 elif arg.startswith("label="): 551 label = arg[6:] 552 553 elif arg.startswith("section="): 554 section = arg[8:] 555 556 elif arg.startswith("fragment="): 557 fragment = arg[9:] 558 559 elif name is None: 560 name = arg 561 562 elif dictpage is None: 563 dictpage = arg 564 565 return name, path, dictpage, label, section, fragment 566 567 def getFields(d, remove=False): 568 569 """ 570 Return the form fields hierarchy for the given dictionary 'd'. If the 571 optional 'remove' parameter is set to a true value, remove the entries for 572 the fields from 'd'. 573 """ 574 575 fields = {} 576 577 for key, value in d.items(): 578 579 # Detect modifying fields. 580 581 if key.find("=") != -1: 582 fields[key] = value 583 if remove: 584 del d[key] 585 continue 586 587 # Reproduce the original hierarchy of the fields. 588 589 section = fields 590 parts = getPathDetails(key) 591 592 for name, index in parts[:-1]: 593 594 # Add an entry for instances of the section. 595 596 if not section.has_key(name): 597 section[name] = {} 598 599 # Add an entry for the specific instance of the section. 600 601 if not section[name].has_key(index): 602 section[name][index] = {} 603 604 section = section[name][index] 605 606 section[parts[-1][0]] = value 607 608 if remove: 609 del d[key] 610 611 return fields 612 613 def getPathDetails(path): 614 615 """ 616 Return the given 'path' as a list of (name, index) tuples providing details 617 of section instances, with any specific field appearing as the last element 618 and having the form (name, None). 619 """ 620 621 parts = [] 622 623 for part in path.split("/"): 624 try: 625 name, index = part.split("$", 1) 626 index = int(index) 627 except ValueError: 628 name, index = part, None 629 630 parts.append((name, index)) 631 632 return parts 633 634 def getSectionForPath(path, fields): 635 636 """ 637 Obtain the section indicated by the given 'path' from the 'fields', 638 returning a tuple of the form (parent section, (name, index)), where the 639 parent section contains the referenced section, where name is the name of 640 the referenced section, and where index, if not None, is the index of a 641 specific section instance within the named section. 642 """ 643 644 parts = getPathDetails(path) 645 section = fields 646 647 for name, index in parts[:-1]: 648 section = fields[name][index] 649 650 return section, parts[-1] 651 652 def getSectionElements(section_elements): 653 654 "Return the given 'section_elements' as an ordered collection." 655 656 keys = map(int, section_elements.keys()) 657 keys.sort() 658 659 elements = [] 660 661 for key in keys: 662 elements.append(section_elements[key]) 663 664 return elements 665 666 # Parser-related formatting functions. 667 668 def formatForm(text, request, fmt, attrs=None, write=None): 669 670 """ 671 Format the given 'text' using the specified 'request' and formatter 'fmt'. 672 The optional 'attrs' can be used to control the presentation of the form. 673 674 If the 'write' parameter is specified, use it to write output; otherwise, 675 write output using the request. 676 """ 677 678 write = write or request.write 679 page = request.page 680 681 fields = getFields(get_form(request)) 682 683 queryparams = [] 684 685 for argname, default in [("fragment", None), ("action", "MoinFormHandler")]: 686 if attrs and attrs.has_key(argname): 687 queryparams.append("%s=%s" % (argname, attrs[argname])) 688 elif default: 689 queryparams.append("%s=%s" % (argname, default)) 690 691 querystr = "&".join(queryparams) 692 fragment = attrs.get("fragment") 693 694 write(fmt.rawHTML('<form method="post" action="%s%s"%s>' % ( 695 escattr(page.url(request, querystr)), 696 fragment and ("#%s" % escattr(fragment)) or "", 697 fragment and (' id="%s"' % escattr(fragment)) or "" 698 ))) 699 700 # Obtain page text for the form, incorporating subregions and applicable 701 # sections. 702 703 output = getFormOutput(text, fields, fragment=fragment) 704 write(formatText(output, request, fmt, inhibit_p=False)) 705 706 write(fmt.rawHTML('</form>')) 707 708 def formatFormForOutputType(text, request, mimetype, attrs=None, write=None): 709 710 """ 711 Format the given 'text' using the specified 'request' for the given output 712 'mimetype'. 713 714 The optional 'attrs' can be used to control the presentation of the form. 715 716 If the 'write' parameter is specified, use it to write output; otherwise, 717 write output using the request. 718 """ 719 720 write = write or request.write 721 722 if mimetype == "text/html": 723 write('<html>') 724 write('<body>') 725 fmt = request.html_formatter 726 fmt.setPage(request.page) 727 formatForm(text, request, fmt, attrs, write) 728 write('</body>') 729 write('</html>') 730 731 # vim: tabstop=4 expandtab shiftwidth=4