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 type = None 241 242 for i, match in enumerate(form_field_regexp.split(body)): 243 state = i % 3 244 245 # Reproduce normal text as is. 246 247 if state == 0: 248 result.append(match) 249 250 # Capture the macro type. 251 252 elif state == 1: 253 type = match 254 255 # Substitute the macro and modified arguments. 256 257 else: 258 result.append("<<Form%s(%s)>>" % (type, ",".join( 259 adjustMacroArguments(parseMacroArguments(match), path) 260 ))) 261 262 return "".join(result) 263 264 def adjustMacroArguments(args, path): 265 266 """ 267 Adjust the given 'args' so that the path incorporates the given 268 'path', returning a new list containing the revised path and remaining 269 arguments. 270 """ 271 272 result = [] 273 old_path = None 274 275 for arg in args: 276 if arg.startswith("path="): 277 old_path = arg[5:] 278 else: 279 result.append(arg) 280 281 qualified = old_path and ("%s/%s" % (old_path, path)) or path 282 result.append("path=%s" % qualified) 283 284 return result 285 286 def parseMacroArguments(args): 287 288 """ 289 Interpret the arguments. 290 NOTE: The argument parsing should really be more powerful in order to 291 NOTE: support labels. 292 """ 293 294 try: 295 parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] 296 except AttributeError: 297 parsed_args = args.split(",") 298 299 return [arg for arg in parsed_args if arg] 300 301 def getFields(d, remove=False): 302 303 """ 304 Return the form fields hierarchy for the given dictionary 'd'. If the 305 optional 'remove' parameter is set to a true value, remove the entries for 306 the fields from 'd'. 307 """ 308 309 fields = {} 310 311 for key, value in d.items(): 312 313 # Detect modifying fields. 314 315 if key.find("=") != -1: 316 fields[key] = value 317 if remove: 318 del d[key] 319 continue 320 321 # Reproduce the original hierarchy of the fields. 322 323 section = fields 324 parts = getPathDetails(key) 325 326 for name, index in parts[:-1]: 327 328 # Add an entry for instances of the section. 329 330 if not section.has_key(name): 331 section[name] = {} 332 333 # Add an entry for the specific instance of the section. 334 335 if not section[name].has_key(index): 336 section[name][index] = {} 337 338 section = section[name][index] 339 340 section[parts[-1][0]] = value 341 342 if remove: 343 del d[key] 344 345 return fields 346 347 def getPathDetails(path): 348 349 """ 350 Return the given 'path' as a list of (name, index) tuples providing details 351 of section instances, with any specific field appearing as the last element 352 and having the form (name, None). 353 """ 354 355 parts = [] 356 357 for part in path.split("/"): 358 try: 359 name, index = part.split("$", 1) 360 index = int(index) 361 except ValueError: 362 name, index = part, None 363 364 parts.append((name, index)) 365 366 return parts 367 368 def getSectionForPath(path, fields): 369 370 """ 371 Obtain the section indicated by the given 'path' from the 'fields', 372 returning a tuple of the form (parent section, (name, index)), where the 373 parent section contains the referenced section, where name is the name of 374 the referenced section, and where index, if not None, is the index of a 375 specific section instance within the named section. 376 """ 377 378 parts = getPathDetails(path) 379 section = fields 380 381 for name, index in parts[:-1]: 382 section = fields[name][index] 383 384 return section, parts[-1] 385 386 def getSectionElements(section_elements): 387 388 "Return the given 'section_elements' as an ordered collection." 389 390 keys = map(int, section_elements.keys()) 391 keys.sort() 392 393 elements = [] 394 395 for key in keys: 396 elements.append(section_elements[key]) 397 398 return elements 399 400 def formatFormForOutputType(text, request, mimetype, attrs=None, write=None): 401 402 """ 403 Format the given 'text' using the specified 'request' for the given output 404 'mimetype'. 405 406 The optional 'attrs' can be used to control the presentation of the form. 407 408 If the 'write' parameter is specified, use it to write output; otherwise, 409 write output using the request. 410 """ 411 412 write = write or request.write 413 414 if mimetype == "text/html": 415 write('<html>') 416 write('<body>') 417 fmt = request.html_formatter 418 fmt.setPage(request.page) 419 formatForm(text, request, fmt, attrs, write) 420 write('</body>') 421 write('</html>') 422 423 # vim: tabstop=4 expandtab shiftwidth=4