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