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): 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 316 output = [] 317 section = fields 318 319 for region in getRegions(text, True): 320 format, attributes, body, header, close = getFragmentFromRegion(region) 321 322 # Adjust any FormField macros to use hierarchical names. 323 324 if format is None: 325 output.append(path and adjustFormFields(body, path) or body) 326 327 # Include form sections only if fields exist for those sections. 328 329 elif format == "form": 330 section_name = attributes.get("section") 331 message_name = attributes.get("message") 332 absent_message_name = attributes.get("not-message") 333 334 # Sections are groups of fields in their own namespace. 335 336 if section_name and section.has_key(section_name): 337 338 # Iterate over the section contents ignoring the given indexes. 339 340 for index, element in enumerate(getSectionElements(section[section_name])): 341 element_ref = "%s$%s" % (section_name, index) 342 343 # Get the output for the section. 344 345 output.append(getFormOutput(body, element, 346 path and ("%s/%s" % (path, element_ref)) or element_ref)) 347 348 # Message regions are conditional on a particular field and 349 # reference the current namespace. 350 351 elif message_name and section.has_key(message_name): 352 output.append(getFormOutput(body, section, path)) 353 354 # Not-message regions are conditional on a particular field being 355 # absent. They reference the current namespace. 356 357 elif absent_message_name and not section.has_key(absent_message_name): 358 output.append(getFormOutput(body, section, path)) 359 360 # Inspect and include other regions. 361 362 else: 363 output.append(header) 364 output.append(getFormOutput(body, section, path)) 365 output.append(close) 366 367 return "".join(output) 368 369 def getFormFields(body, path, request): 370 371 "Return a dictionary of fields from the given 'body' at the given 'path'." 372 373 fields = {} 374 cache = {} 375 type = None 376 377 for i, match in enumerate(form_field_regexp.split(body)): 378 state = i % 3 379 380 if state == 1: 381 type = match 382 elif state == 2 and type == "Field": 383 args = {} 384 385 # Obtain the macro arguments, adjusted to consider the path. 386 387 name, path, dictpage, label, section = \ 388 getMacroArguments(adjustMacroArguments(parseMacroArguments(match), path)) 389 390 # Obtain field information from the cache, if possible. 391 392 cache_key = (name, dictpage) 393 394 if cache.has_key(cache_key): 395 field_args, allowed_values = cache[cache_key] 396 397 # Otherwise, obtain field information from any WikiDict. 398 399 else: 400 field_args = {} 401 allowed_values = None 402 403 if dictpage: 404 wikidict = getWikiDict(dictpage, request) 405 if wikidict: 406 field_definition = wikidict.get(name) 407 if field_definition: 408 field_args = getFieldArguments(field_definition) 409 if field_args.has_key("source"): 410 sourcedict = getWikiDict(field_args["source"], request) 411 if sourcedict: 412 allowed_values = sourcedict.keys() 413 414 cache[cache_key] = field_args, allowed_values 415 416 # Store the field information. 417 418 fields[name] = path, dictpage, label, section, field_args, allowed_values 419 420 return fields 421 422 def adjustFormFields(body, path): 423 424 """ 425 Return a version of the 'body' with the names in FormField macros updated to 426 incorporate the given 'path'. 427 """ 428 429 result = [] 430 type = None 431 432 for i, match in enumerate(form_field_regexp.split(body)): 433 state = i % 3 434 435 # Reproduce normal text as is. 436 437 if state == 0: 438 result.append(match) 439 440 # Capture the macro type. 441 442 elif state == 1: 443 type = match 444 445 # Substitute the macro and modified arguments. 446 447 else: 448 result.append("<<Form%s(%s)>>" % (type, ",".join( 449 adjustMacroArguments(parseMacroArguments(match), path) 450 ))) 451 452 return "".join(result) 453 454 def adjustMacroArguments(args, path): 455 456 """ 457 Adjust the given 'args' so that the path incorporates the given 458 'path', returning a new list containing the revised path and remaining 459 arguments. 460 """ 461 462 if not path: 463 return args 464 465 result = [] 466 old_path = None 467 468 for arg in args: 469 if arg.startswith("path="): 470 old_path = arg[5:] 471 else: 472 result.append(arg) 473 474 qualified = old_path and ("%s/%s" % (old_path, path)) or path 475 result.append("path=%s" % qualified) 476 477 return result 478 479 def parseMacroArguments(args): 480 481 """ 482 Interpret the arguments. 483 NOTE: The argument parsing should really be more powerful in order to 484 NOTE: support labels. 485 """ 486 487 try: 488 parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] 489 except AttributeError: 490 parsed_args = args.split(",") 491 492 return [arg for arg in parsed_args if arg] 493 494 def getMacroArguments(parsed_args): 495 496 "Return the macro arguments decoded from 'parsed_args'." 497 498 name = None 499 path = None 500 dictpage = None 501 label = None 502 section = None 503 504 for arg in parsed_args: 505 if arg.startswith("name="): 506 name = arg[5:] 507 508 elif arg.startswith("path="): 509 path = arg[5:] 510 511 elif arg.startswith("dict="): 512 dictpage = arg[5:] 513 514 elif arg.startswith("label="): 515 label = arg[6:] 516 517 elif arg.startswith("section="): 518 section = arg[8:] 519 520 elif name is None: 521 name = arg 522 523 elif dictpage is None: 524 dictpage = arg 525 526 return name, path, dictpage, label, section 527 528 def getFields(d, remove=False): 529 530 """ 531 Return the form fields hierarchy for the given dictionary 'd'. If the 532 optional 'remove' parameter is set to a true value, remove the entries for 533 the fields from 'd'. 534 """ 535 536 fields = {} 537 538 for key, value in d.items(): 539 540 # Detect modifying fields. 541 542 if key.find("=") != -1: 543 fields[key] = value 544 if remove: 545 del d[key] 546 continue 547 548 # Reproduce the original hierarchy of the fields. 549 550 section = fields 551 parts = getPathDetails(key) 552 553 for name, index in parts[:-1]: 554 555 # Add an entry for instances of the section. 556 557 if not section.has_key(name): 558 section[name] = {} 559 560 # Add an entry for the specific instance of the section. 561 562 if not section[name].has_key(index): 563 section[name][index] = {} 564 565 section = section[name][index] 566 567 section[parts[-1][0]] = value 568 569 if remove: 570 del d[key] 571 572 return fields 573 574 def getPathDetails(path): 575 576 """ 577 Return the given 'path' as a list of (name, index) tuples providing details 578 of section instances, with any specific field appearing as the last element 579 and having the form (name, None). 580 """ 581 582 parts = [] 583 584 for part in path.split("/"): 585 try: 586 name, index = part.split("$", 1) 587 index = int(index) 588 except ValueError: 589 name, index = part, None 590 591 parts.append((name, index)) 592 593 return parts 594 595 def getSectionForPath(path, fields): 596 597 """ 598 Obtain the section indicated by the given 'path' from the 'fields', 599 returning a tuple of the form (parent section, (name, index)), where the 600 parent section contains the referenced section, where name is the name of 601 the referenced section, and where index, if not None, is the index of a 602 specific section instance within the named section. 603 """ 604 605 parts = getPathDetails(path) 606 section = fields 607 608 for name, index in parts[:-1]: 609 section = fields[name][index] 610 611 return section, parts[-1] 612 613 def getSectionElements(section_elements): 614 615 "Return the given 'section_elements' as an ordered collection." 616 617 keys = map(int, section_elements.keys()) 618 keys.sort() 619 620 elements = [] 621 622 for key in keys: 623 elements.append(section_elements[key]) 624 625 return elements 626 627 # Parser-related formatting functions. 628 629 def formatForm(text, request, fmt, attrs=None, write=None): 630 631 """ 632 Format the given 'text' using the specified 'request' and formatter 'fmt'. 633 The optional 'attrs' can be used to control the presentation of the form. 634 635 If the 'write' parameter is specified, use it to write output; otherwise, 636 write output using the request. 637 """ 638 639 write = write or request.write 640 page = request.page 641 642 fields = getFields(get_form(request)) 643 644 queryparams = [] 645 646 for argname, default in [("fragment", None), ("action", "MoinFormHandler")]: 647 if attrs and attrs.has_key(argname): 648 queryparams.append("%s=%s" % (argname, attrs[argname])) 649 elif default: 650 queryparams.append("%s=%s" % (argname, default)) 651 652 querystr = "&".join(queryparams) 653 fragment = attrs.get("fragment") 654 655 write(fmt.rawHTML('<form method="post" action="%s%s"%s>' % ( 656 escattr(page.url(request, querystr)), 657 fragment and ("#%s" % escattr(fragment)) or "", 658 fragment and (' id="%s"' % escattr(fragment)) or "" 659 ))) 660 661 # Obtain page text for the form, incorporating subregions and applicable 662 # sections. 663 664 output = getFormOutput(text, fields) 665 write(formatText(output, request, fmt, inhibit_p=False)) 666 667 write(fmt.rawHTML('</form>')) 668 669 def formatFormForOutputType(text, request, mimetype, attrs=None, write=None): 670 671 """ 672 Format the given 'text' using the specified 'request' for the given output 673 'mimetype'. 674 675 The optional 'attrs' can be used to control the presentation of the form. 676 677 If the 'write' parameter is specified, use it to write output; otherwise, 678 write output using the request. 679 """ 680 681 write = write or request.write 682 683 if mimetype == "text/html": 684 write('<html>') 685 write('<body>') 686 fmt = request.html_formatter 687 fmt.setPage(request.page) 688 formatForm(text, request, fmt, attrs, write) 689 write('</body>') 690 write('</html>') 691 692 # vim: tabstop=4 expandtab shiftwidth=4