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