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