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 import wikiutil 11 from MoinSupport import * 12 import re 13 14 __version__ = "0.1" 15 16 form_field_regexp_str = r"<<Form(Field|Message)\((.*?)\)>>" 17 form_field_regexp = re.compile(form_field_regexp_str, re.DOTALL) 18 19 # Common action functionality. 20 21 class MoinFormHandlerAction: 22 23 "A handler action that can be specialised for individual forms." 24 25 def __init__(self, pagename, request): 26 self.pagename = pagename 27 self.request = request 28 29 def processForm(self): 30 31 """ 32 Interpret the request details and modify them according to the structure 33 of the interpreted information. 34 """ 35 36 # Get the form fields and obtain the hierarchical field structure. 37 38 form = get_form(self.request) 39 fields = getFields(form, remove=True) 40 41 # Modify, serialise and show the form. 42 43 self.modifyFields(fields) 44 self.validateFields(fields) 45 self.serialiseFields(fields, form) 46 do_show(self.pagename, self.request) 47 48 def validateFields(self, fields): 49 pass 50 51 def serialiseFields(self, fields, form, path=None): 52 53 """ 54 Serialise the given 'fields' to the given 'form', using the given 'path' 55 to name the entries. 56 """ 57 58 for key, value in fields.items(): 59 60 # Serialise sections. 61 62 if isinstance(value, dict): 63 for index, element in enumerate(getSectionElements(value)): 64 element_ref = "%s$%s" % (key, index) 65 66 self.serialiseFields(element, form, 67 path and ("%s/%s" % (path, element_ref)) or element_ref 68 ) 69 70 # Serialise fields. 71 72 else: 73 form[path and ("%s/%s" % (path, key)) or key] = value 74 75 def modifyFields(self, fields): 76 77 "Modify the given 'fields', removing and adding items." 78 79 # First, remove fields. 80 81 for key in fields.keys(): 82 if key.startswith("_remove="): 83 self.removeField(key[8:], fields) 84 85 # Then, add fields. 86 87 for key in fields.keys(): 88 if key.startswith("_add="): 89 self.addField(key[5:], fields) 90 91 def removeField(self, path, fields): 92 93 """ 94 Remove the section element indicated by the given 'path' from the 95 'fields'. 96 """ 97 98 section, (name, index) = getSectionForPath(path, fields) 99 del section[name][index] 100 101 def addField(self, path, fields): 102 103 """ 104 Add a section element indicated by the given 'path' to the 'fields'. 105 """ 106 107 section, (name, index) = getSectionForPath(path, fields) 108 placeholder = {"_new" : ""} 109 110 if section.has_key(name): 111 indexes = section[name].keys() 112 max_index = max(map(int, indexes)) 113 section[name][max_index + 1] = placeholder 114 else: 115 max_index = -1 116 section[name] = {0 : placeholder} 117 118 # Form and field information. 119 120 def getFieldArguments(field_definition): 121 122 "Return the parsed arguments from the given 'field_definition' string." 123 124 field_args = {} 125 126 for field_arg in field_definition.split(): 127 128 # Record the key-value details. 129 130 try: 131 argname, argvalue = field_arg.split("=", 1) 132 field_args[argname] = argvalue 133 134 # Single keywords are interpreted as type descriptions. 135 136 except ValueError: 137 if not field_args.has_key("type"): 138 field_args["type"] = field_arg 139 140 return field_args 141 142 # Common formatting functions. 143 144 def formatForm(text, request, fmt, attrs=None, write=None): 145 146 """ 147 Format the given 'text' using the specified 'request' and formatter 'fmt'. 148 The optional 'attrs' can be used to control the presentation of the form. 149 150 If the 'write' parameter is specified, use it to write output; otherwise, 151 write output using the request. 152 """ 153 154 write = write or request.write 155 page = request.page 156 157 fields = getFields(get_form(request)) 158 159 queryparams = [] 160 161 for argname in ["fragment", "action"]: 162 if attrs and attrs.has_key(argname): 163 queryparams.append("%s=%s" % (argname, attrs[argname])) 164 165 querystr = "&".join(queryparams) 166 167 write(fmt.rawHTML('<form method="post" action="%s">' % 168 escattr(page.url(request, querystr)) 169 )) 170 171 # Obtain page text for the form, incorporating subregions and applicable 172 # sections. 173 174 output = getFormOutput(text, fields) 175 write(formatText(output, request, fmt, inhibit_p=False)) 176 177 write(fmt.rawHTML('</form>')) 178 179 def getFormOutput(text, fields, path=None): 180 181 """ 182 Combine regions found in the given 'text' and then return them as a single 183 block. The reason for doing this, as opposed to just passing each region to 184 a suitable parser for formatting, is that form sections may break up 185 regions, and such sections may not define separate subregions but instead 186 act as a means of conditional inclusion of text into an outer region. 187 188 The given 'fields' are used to populate fields provided in forms and to 189 control whether sections are populated or not. 190 """ 191 192 output = [] 193 section = fields 194 195 for region in getRegions(text, True): 196 format, attributes, body, header, close = getFragmentFromRegion(region) 197 198 # Adjust any FormField macros to use hierarchical names. 199 200 if format is None: 201 if path: 202 adjusted_body = adjustFormFields(body, path) 203 output.append(adjusted_body) 204 else: 205 output.append(body) 206 207 # Include form sections only if fields exist for those sections. 208 209 elif format == "form": 210 section_name = attributes.get("section") 211 if section_name and section.has_key(section_name): 212 213 # Iterate over the section contents ignoring the given indexes. 214 215 for index, element in enumerate(getSectionElements(section[section_name])): 216 element_ref = "%s$%s" % (section_name, index) 217 218 # Get the output for the section. 219 220 output.append(getFormOutput(body, element, 221 path and ("%s/%s" % (path, element_ref)) or element_ref)) 222 223 # Inspect and include other regions. 224 225 else: 226 output.append(header) 227 output.append(getFormOutput(body, section, path)) 228 output.append(close) 229 230 return "".join(output) 231 232 def adjustFormFields(body, path): 233 234 """ 235 Return a version of the 'body' with the names in FormField macros updated to 236 incorporate the given 'path'. 237 """ 238 239 result = [] 240 state = None 241 type = None 242 243 for match in form_field_regexp.split(body): 244 245 # Reproduce normal text as is. 246 247 if not state: 248 result.append(match) 249 state = "TYPE" 250 251 # Capture the macro type. 252 253 elif state == "TYPE": 254 type = match 255 state = "ARGS" 256 257 # Substitute the macro and modified arguments. 258 259 else: 260 result.append("<<Form%s(%s)>>" % (type, ",".join( 261 adjustMacroArguments(parseMacroArguments(match), path) 262 ))) 263 state = None 264 265 return "".join(result) 266 267 def adjustMacroArguments(args, path): 268 269 """ 270 Adjust the given 'args' so that the path incorporates the given 271 'path', returning a new list containing the revised path and remaining 272 arguments. 273 """ 274 275 result = [] 276 old_path = None 277 278 for arg in args: 279 if arg.startswith("path="): 280 old_path = arg[5:] 281 else: 282 result.append(arg) 283 284 qualified = old_path and ("%s/%s" % (old_path, path)) or path 285 result.append("path=%s" % qualified) 286 287 return result 288 289 def parseMacroArguments(args): 290 291 """ 292 Interpret the arguments. 293 NOTE: The argument parsing should really be more powerful in order to 294 NOTE: support labels. 295 """ 296 297 try: 298 parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] 299 except AttributeError: 300 parsed_args = args.split(",") 301 302 return [arg for arg in parsed_args if arg] 303 304 def getFields(d, remove=False): 305 306 """ 307 Return the form fields hierarchy for the given dictionary 'd'. If the 308 optional 'remove' parameter is set to a true value, remove the entries for 309 the fields from 'd'. 310 """ 311 312 fields = {} 313 314 for key, value in d.items(): 315 316 # Detect modifying fields. 317 318 if key.find("=") != -1: 319 fields[key] = value 320 if remove: 321 del d[key] 322 continue 323 324 # Reproduce the original hierarchy of the fields. 325 326 section = fields 327 parts = getPathDetails(key) 328 329 for name, index in parts[:-1]: 330 331 # Add an entry for instances of the section. 332 333 if not section.has_key(name): 334 section[name] = {} 335 336 # Add an entry for the specific instance of the section. 337 338 if not section[name].has_key(index): 339 section[name][index] = {} 340 341 section = section[name][index] 342 343 section[parts[-1][0]] = value 344 345 if remove: 346 del d[key] 347 348 return fields 349 350 def getPathDetails(path): 351 352 """ 353 Return the given 'path' as a list of (name, index) tuples providing details 354 of section instances, with any specific field appearing as the last element 355 and having the form (name, None). 356 """ 357 358 parts = [] 359 360 for part in path.split("/"): 361 try: 362 name, index = part.split("$", 1) 363 index = int(index) 364 except ValueError: 365 name, index = part, None 366 367 parts.append((name, index)) 368 369 return parts 370 371 def getSectionForPath(path, fields): 372 373 """ 374 Obtain the section indicated by the given 'path' from the 'fields', 375 returning a tuple of the form (parent section, (name, index)), where the 376 parent section contains the referenced section, where name is the name of 377 the referenced section, and where index, if not None, is the index of a 378 specific section instance within the named section. 379 """ 380 381 parts = getPathDetails(path) 382 section = fields 383 384 for name, index in parts[:-1]: 385 section = fields[name][index] 386 387 return section, parts[-1] 388 389 def getSectionElements(section_elements): 390 391 "Return the given 'section_elements' as an ordered collection." 392 393 keys = map(int, section_elements.keys()) 394 keys.sort() 395 396 elements = [] 397 398 for key in keys: 399 elements.append(section_elements[key]) 400 401 return elements 402 403 def formatFormForOutputType(text, request, mimetype, attrs=None, write=None): 404 405 """ 406 Format the given 'text' using the specified 'request' for the given output 407 'mimetype'. 408 409 The optional 'attrs' can be used to control the presentation of the form. 410 411 If the 'write' parameter is specified, use it to write output; otherwise, 412 write output using the request. 413 """ 414 415 write = write or request.write 416 417 if mimetype == "text/html": 418 write('<html>') 419 write('<body>') 420 fmt = request.html_formatter 421 fmt.setPage(request.page) 422 formatForm(text, request, fmt, attrs, write) 423 write('</body>') 424 write('</html>') 425 426 # vim: tabstop=4 expandtab shiftwidth=4