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 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. To support commas in labels, the label argument 506 should be quoted. For example: 507 508 "label=No, thanks!" 509 """ 510 511 try: 512 parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] 513 except AttributeError: 514 parsed_args = args.split(",") 515 516 return [arg for arg in parsed_args if arg] 517 518 def getMacroArguments(parsed_args): 519 520 "Return the macro arguments decoded from 'parsed_args'." 521 522 name = None 523 path = None 524 dictpage = None 525 label = None 526 section = None 527 528 for arg in parsed_args: 529 if arg.startswith("name="): 530 name = arg[5:] 531 532 elif arg.startswith("path="): 533 path = arg[5:] 534 535 elif arg.startswith("dict="): 536 dictpage = arg[5:] 537 538 elif arg.startswith("label="): 539 label = arg[6:] 540 541 elif arg.startswith("section="): 542 section = arg[8:] 543 544 elif name is None: 545 name = arg 546 547 elif dictpage is None: 548 dictpage = arg 549 550 return name, path, dictpage, label, section 551 552 def getFields(d, remove=False): 553 554 """ 555 Return the form fields hierarchy for the given dictionary 'd'. If the 556 optional 'remove' parameter is set to a true value, remove the entries for 557 the fields from 'd'. 558 """ 559 560 fields = {} 561 562 for key, value in d.items(): 563 564 # Detect modifying fields. 565 566 if key.find("=") != -1: 567 fields[key] = value 568 if remove: 569 del d[key] 570 continue 571 572 # Reproduce the original hierarchy of the fields. 573 574 section = fields 575 parts = getPathDetails(key) 576 577 for name, index in parts[:-1]: 578 579 # Add an entry for instances of the section. 580 581 if not section.has_key(name): 582 section[name] = {} 583 584 # Add an entry for the specific instance of the section. 585 586 if not section[name].has_key(index): 587 section[name][index] = {} 588 589 section = section[name][index] 590 591 section[parts[-1][0]] = value 592 593 if remove: 594 del d[key] 595 596 return fields 597 598 def getPathDetails(path): 599 600 """ 601 Return the given 'path' as a list of (name, index) tuples providing details 602 of section instances, with any specific field appearing as the last element 603 and having the form (name, None). 604 """ 605 606 parts = [] 607 608 for part in path.split("/"): 609 try: 610 name, index = part.split("$", 1) 611 index = int(index) 612 except ValueError: 613 name, index = part, None 614 615 parts.append((name, index)) 616 617 return parts 618 619 def getSectionForPath(path, fields): 620 621 """ 622 Obtain the section indicated by the given 'path' from the 'fields', 623 returning a tuple of the form (parent section, (name, index)), where the 624 parent section contains the referenced section, where name is the name of 625 the referenced section, and where index, if not None, is the index of a 626 specific section instance within the named section. 627 """ 628 629 parts = getPathDetails(path) 630 section = fields 631 632 for name, index in parts[:-1]: 633 section = fields[name][index] 634 635 return section, parts[-1] 636 637 def getSectionElements(section_elements): 638 639 "Return the given 'section_elements' as an ordered collection." 640 641 keys = map(int, section_elements.keys()) 642 keys.sort() 643 644 elements = [] 645 646 for key in keys: 647 elements.append(section_elements[key]) 648 649 return elements 650 651 # Parser-related formatting functions. 652 653 def formatForm(text, request, fmt, attrs=None, write=None): 654 655 """ 656 Format the given 'text' using the specified 'request' and formatter 'fmt'. 657 The optional 'attrs' can be used to control the presentation of the form. 658 659 If the 'write' parameter is specified, use it to write output; otherwise, 660 write output using the request. 661 """ 662 663 write = write or request.write 664 page = request.page 665 666 fields = getFields(get_form(request)) 667 668 queryparams = [] 669 670 for argname, default in [("fragment", None), ("action", "MoinFormHandler")]: 671 if attrs and attrs.has_key(argname): 672 queryparams.append("%s=%s" % (argname, attrs[argname])) 673 elif default: 674 queryparams.append("%s=%s" % (argname, default)) 675 676 querystr = "&".join(queryparams) 677 fragment = attrs.get("fragment") 678 679 write(fmt.rawHTML('<form method="post" action="%s%s"%s>' % ( 680 escattr(page.url(request, querystr)), 681 fragment and ("#%s" % escattr(fragment)) or "", 682 fragment and (' id="%s"' % escattr(fragment)) or "" 683 ))) 684 685 # Obtain page text for the form, incorporating subregions and applicable 686 # sections. 687 688 output = getFormOutput(text, fields) 689 write(formatText(output, request, fmt, inhibit_p=False)) 690 691 write(fmt.rawHTML('</form>')) 692 693 def formatFormForOutputType(text, request, mimetype, attrs=None, write=None): 694 695 """ 696 Format the given 'text' using the specified 'request' for the given output 697 'mimetype'. 698 699 The optional 'attrs' can be used to control the presentation of the form. 700 701 If the 'write' parameter is specified, use it to write output; otherwise, 702 write output using the request. 703 """ 704 705 write = write or request.write 706 707 if mimetype == "text/html": 708 write('<html>') 709 write('<body>') 710 fmt = request.html_formatter 711 fmt.setPage(request.page) 712 formatForm(text, request, fmt, attrs, write) 713 write('</body>') 714 write('</html>') 715 716 # vim: tabstop=4 expandtab shiftwidth=4