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