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() 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): 82 83 "Return the structure of the form being handled." 84 85 form = get_form(self.request) 86 text = Page(self.request, self.pagename).get_raw_body() 87 text = getFormForFragment(text, form.get("fragment", [None])[0]) 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 del section[name][index] 203 204 def addField(self, path, fields): 205 206 """ 207 Add a section element indicated by the given 'path' to the 'fields'. 208 """ 209 210 section, (name, index) = getSectionForPath(path, fields) 211 placeholder = {"_new" : ""} 212 213 if section.has_key(name): 214 indexes = section[name].keys() 215 max_index = max(map(int, indexes)) 216 section[name][max_index + 1] = placeholder 217 else: 218 max_index = -1 219 section[name] = {0 : placeholder} 220 221 # Form and field information. 222 223 def getFormStructure(text, request, path=None, structure=None): 224 225 """ 226 For the given form 'text' and using the 'request', return details of the 227 form for the section at the given 'path' (or the entire form if 'path' is 228 omitted), populating the given 'structure' (or populating a new structure if 229 'structure' is omitted). 230 """ 231 232 if structure is None: 233 structure = {} 234 235 for format, attributes, body in getFragments(text, True): 236 237 # Get field details at the current level. 238 239 if format is None: 240 structure.update(getFormFields(body, path, request)) 241 242 # Where a section is found, get details from within the section. 243 244 elif format == "form": 245 if attributes.has_key("section"): 246 section_name = attributes["section"] 247 section = structure[section_name] = {} 248 getFormStructure(body, request, path and ("%s/%s" % (path, section_name)) or section_name, section) 249 elif attributes.has_key("message"): 250 getFormStructure(body, request, path, structure) 251 elif attributes.has_key("not-message"): 252 getFormStructure(body, request, path, structure) 253 254 # Get field details from other kinds of region. 255 256 elif format != "form": 257 getFormStructure(body, request, path, structure) 258 259 return structure 260 261 def getFormForFragment(text, fragment=None): 262 263 """ 264 Return the form region from the given 'text' for the specified 'fragment'. 265 If no fragment is specified, the first form region is returned. 266 """ 267 268 for format, attributes, body in getFragments(text): 269 if not fragment or attributes.get("fragment") == fragment: 270 return body 271 272 return None 273 274 def getFieldArguments(field_definition): 275 276 "Return the parsed arguments from the given 'field_definition' string." 277 278 field_args = {} 279 280 for field_arg in field_definition.split(): 281 if field_arg == "required": 282 field_args[field_arg] = True 283 continue 284 285 # Record the key-value details. 286 287 try: 288 argname, argvalue = field_arg.split("=", 1) 289 field_args[argname] = argvalue 290 291 # Single keywords are interpreted as type descriptions. 292 293 except ValueError: 294 if not field_args.has_key("type"): 295 field_args["type"] = field_arg 296 297 return field_args 298 299 # Common formatting functions. 300 301 def getFormOutput(text, fields, path=None): 302 303 """ 304 Combine regions found in the given 'text' and then return them as a single 305 block. The reason for doing this, as opposed to just passing each region to 306 a suitable parser for formatting, is that form sections may break up 307 regions, and such sections may not define separate subregions but instead 308 act as a means of conditional inclusion of text into an outer region. 309 310 The given 'fields' are used to populate fields provided in forms and to 311 control whether sections are populated or not. 312 """ 313 314 output = [] 315 section = fields 316 317 for region in getRegions(text, True): 318 format, attributes, body, header, close = getFragmentFromRegion(region) 319 320 # Adjust any FormField macros to use hierarchical names. 321 322 if format is None: 323 output.append(path and adjustFormFields(body, path) or body) 324 325 # Include form sections only if fields exist for those sections. 326 327 elif format == "form": 328 section_name = attributes.get("section") 329 message_name = attributes.get("message") 330 absent_message_name = attributes.get("not-message") 331 332 # Sections are groups of fields in their own namespace. 333 334 if section_name and section.has_key(section_name): 335 336 # Iterate over the section contents ignoring the given indexes. 337 338 for index, element in enumerate(getSectionElements(section[section_name])): 339 element_ref = "%s$%s" % (section_name, index) 340 341 # Get the output for the section. 342 343 output.append(getFormOutput(body, element, 344 path and ("%s/%s" % (path, element_ref)) or element_ref)) 345 346 # Message regions are conditional on a particular field and 347 # reference the current namespace. 348 349 elif message_name and section.has_key(message_name): 350 output.append(getFormOutput(body, section, path)) 351 352 # Not-message regions are conditional on a particular field being 353 # absent. They reference the current namespace. 354 355 elif absent_message_name and not section.has_key(absent_message_name): 356 output.append(getFormOutput(body, section, path)) 357 358 # Inspect and include other regions. 359 360 else: 361 output.append(header) 362 output.append(getFormOutput(body, section, path)) 363 output.append(close) 364 365 return "".join(output) 366 367 def getFormFields(body, path, request): 368 369 "Return a dictionary of fields from the given 'body' at the given 'path'." 370 371 fields = {} 372 cache = {} 373 type = None 374 375 for i, match in enumerate(form_field_regexp.split(body)): 376 state = i % 3 377 378 if state == 1: 379 type = match 380 elif state == 2 and type == "Field": 381 args = {} 382 383 # Obtain the macro arguments, adjusted to consider the path. 384 385 name, path, dictpage, label, section = \ 386 getMacroArguments(adjustMacroArguments(parseMacroArguments(match), path)) 387 388 # Obtain field information from the cache, if possible. 389 390 cache_key = (name, dictpage) 391 392 if cache.has_key(cache_key): 393 field_args, allowed_values = cache[cache_key] 394 395 # Otherwise, obtain field information from any WikiDict. 396 397 else: 398 field_args = {} 399 allowed_values = None 400 401 if dictpage: 402 wikidict = getWikiDict(dictpage, request) 403 if wikidict: 404 field_definition = wikidict.get(name) 405 if field_definition: 406 field_args = getFieldArguments(field_definition) 407 if field_args.has_key("source"): 408 sourcedict = getWikiDict(field_args["source"], request) 409 if sourcedict: 410 allowed_values = sourcedict.keys() 411 412 cache[cache_key] = field_args, allowed_values 413 414 # Store the field information. 415 416 fields[name] = path, dictpage, label, section, field_args, allowed_values 417 418 return fields 419 420 def adjustFormFields(body, path): 421 422 """ 423 Return a version of the 'body' with the names in FormField macros updated to 424 incorporate the given 'path'. 425 """ 426 427 result = [] 428 type = None 429 430 for i, match in enumerate(form_field_regexp.split(body)): 431 state = i % 3 432 433 # Reproduce normal text as is. 434 435 if state == 0: 436 result.append(match) 437 438 # Capture the macro type. 439 440 elif state == 1: 441 type = match 442 443 # Substitute the macro and modified arguments. 444 445 else: 446 result.append("<<Form%s(%s)>>" % (type, ",".join( 447 adjustMacroArguments(parseMacroArguments(match), path) 448 ))) 449 450 return "".join(result) 451 452 def adjustMacroArguments(args, path): 453 454 """ 455 Adjust the given 'args' so that the path incorporates the given 456 'path', returning a new list containing the revised path and remaining 457 arguments. 458 """ 459 460 if not path: 461 return args 462 463 result = [] 464 old_path = None 465 466 for arg in args: 467 if arg.startswith("path="): 468 old_path = arg[5:] 469 else: 470 result.append(arg) 471 472 qualified = old_path and ("%s/%s" % (old_path, path)) or path 473 result.append("path=%s" % qualified) 474 475 return result 476 477 def parseMacroArguments(args): 478 479 """ 480 Interpret the arguments. 481 NOTE: The argument parsing should really be more powerful in order to 482 NOTE: support labels. 483 """ 484 485 try: 486 parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] 487 except AttributeError: 488 parsed_args = args.split(",") 489 490 return [arg for arg in parsed_args if arg] 491 492 def getMacroArguments(parsed_args): 493 494 "Return the macro arguments decoded from 'parsed_args'." 495 496 name = None 497 path = None 498 dictpage = None 499 label = None 500 section = None 501 502 for arg in parsed_args: 503 if arg.startswith("name="): 504 name = arg[5:] 505 506 elif arg.startswith("path="): 507 path = arg[5:] 508 509 elif arg.startswith("dict="): 510 dictpage = arg[5:] 511 512 elif arg.startswith("label="): 513 label = arg[6:] 514 515 elif arg.startswith("section="): 516 section = arg[8:] 517 518 elif name is None: 519 name = arg 520 521 elif dictpage is None: 522 dictpage = arg 523 524 return name, path, dictpage, label, section 525 526 def getFields(d, remove=False): 527 528 """ 529 Return the form fields hierarchy for the given dictionary 'd'. If the 530 optional 'remove' parameter is set to a true value, remove the entries for 531 the fields from 'd'. 532 """ 533 534 fields = {} 535 536 for key, value in d.items(): 537 538 # Detect modifying fields. 539 540 if key.find("=") != -1: 541 fields[key] = value 542 if remove: 543 del d[key] 544 continue 545 546 # Reproduce the original hierarchy of the fields. 547 548 section = fields 549 parts = getPathDetails(key) 550 551 for name, index in parts[:-1]: 552 553 # Add an entry for instances of the section. 554 555 if not section.has_key(name): 556 section[name] = {} 557 558 # Add an entry for the specific instance of the section. 559 560 if not section[name].has_key(index): 561 section[name][index] = {} 562 563 section = section[name][index] 564 565 section[parts[-1][0]] = value 566 567 if remove: 568 del d[key] 569 570 return fields 571 572 def getPathDetails(path): 573 574 """ 575 Return the given 'path' as a list of (name, index) tuples providing details 576 of section instances, with any specific field appearing as the last element 577 and having the form (name, None). 578 """ 579 580 parts = [] 581 582 for part in path.split("/"): 583 try: 584 name, index = part.split("$", 1) 585 index = int(index) 586 except ValueError: 587 name, index = part, None 588 589 parts.append((name, index)) 590 591 return parts 592 593 def getSectionForPath(path, fields): 594 595 """ 596 Obtain the section indicated by the given 'path' from the 'fields', 597 returning a tuple of the form (parent section, (name, index)), where the 598 parent section contains the referenced section, where name is the name of 599 the referenced section, and where index, if not None, is the index of a 600 specific section instance within the named section. 601 """ 602 603 parts = getPathDetails(path) 604 section = fields 605 606 for name, index in parts[:-1]: 607 section = fields[name][index] 608 609 return section, parts[-1] 610 611 def getSectionElements(section_elements): 612 613 "Return the given 'section_elements' as an ordered collection." 614 615 keys = map(int, section_elements.keys()) 616 keys.sort() 617 618 elements = [] 619 620 for key in keys: 621 elements.append(section_elements[key]) 622 623 return elements 624 625 # Parser-related formatting functions. 626 627 def formatForm(text, request, fmt, attrs=None, write=None): 628 629 """ 630 Format the given 'text' using the specified 'request' and formatter 'fmt'. 631 The optional 'attrs' can be used to control the presentation of the form. 632 633 If the 'write' parameter is specified, use it to write output; otherwise, 634 write output using the request. 635 """ 636 637 write = write or request.write 638 page = request.page 639 640 fields = getFields(get_form(request)) 641 642 queryparams = [] 643 644 for argname, default in [("fragment", None), ("action", "MoinFormHandlerAction")]: 645 if attrs and attrs.has_key(argname): 646 queryparams.append("%s=%s" % (argname, attrs[argname])) 647 elif default: 648 queryparams.append("%s=%s" % (argname, default)) 649 650 querystr = "&".join(queryparams) 651 652 write(fmt.rawHTML('<form method="post" action="%s"%s>' % ( 653 escattr(page.url(request, querystr)), 654 attrs.has_key("fragment") and (' id="%s"' % attrs["fragment"]) or "" 655 ))) 656 657 # Obtain page text for the form, incorporating subregions and applicable 658 # sections. 659 660 output = getFormOutput(text, fields) 661 write(formatText(output, request, fmt, inhibit_p=False)) 662 663 write(fmt.rawHTML('</form>')) 664 665 def formatFormForOutputType(text, request, mimetype, attrs=None, write=None): 666 667 """ 668 Format the given 'text' using the specified 'request' for the given output 669 'mimetype'. 670 671 The optional 'attrs' can be used to control the presentation of the form. 672 673 If the 'write' parameter is specified, use it to write output; otherwise, 674 write output using the request. 675 """ 676 677 write = write or request.write 678 679 if mimetype == "text/html": 680 write('<html>') 681 write('<body>') 682 fmt = request.html_formatter 683 fmt.setPage(request.page) 684 formatForm(text, request, fmt, attrs, write) 685 write('</body>') 686 write('</html>') 687 688 # vim: tabstop=4 expandtab shiftwidth=4