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