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