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 output = getFormOutput(text, fields) 172 write(formatText(output, request, fmt, inhibit_p=False)) 173 174 write(fmt.rawHTML('</form>')) 175 176 def getFormOutput(text, fields): 177 178 """ 179 Combine regions found in the given 'text' and then return them as a single 180 block. The reason for doing this, as opposed to just passing each region to 181 a suitable parser for formatting, is that form sections may break up 182 regions, and such sections may not define separate subregions but instead 183 act as a means of conditional inclusion of text into an outer region. 184 185 The given 'fields' are used to populate fields provided in forms and to 186 control whether sections are populated or not. 187 """ 188 189 output = [] 190 section = fields 191 192 for region in getRegions(text, True): 193 format, attributes, body, header, close = getFragmentFromRegion(region) 194 195 # Include bare regions as they are. 196 197 if format is None: 198 output.append(region) 199 200 # Include form sections only if fields exist for those sections. 201 202 elif format == "form": 203 section_name = attributes.get("section") 204 if section_name and section.has_key(section_name): 205 206 # Iterate over the section contents ignoring the given indexes. 207 208 for index, element in enumerate(getSectionElements(section[section_name])): 209 210 # Adjust FormField macros to use hierarchical names. 211 212 adjusted_body = adjustFormFields(body, section_name, index) 213 output.append(getFormOutput(adjusted_body, element)) 214 215 # Inspect and include other regions. 216 217 else: 218 output.append(header) 219 output.append(getFormOutput(body, section)) 220 output.append(close) 221 222 return "".join(output) 223 224 def adjustFormFields(body, section_name, index): 225 226 """ 227 Return a version of the 'body' with the names in FormField macros updated to 228 incorporate the given 'section_name' and 'index'. 229 """ 230 231 result = [] 232 state = None 233 type = None 234 235 for match in form_field_regexp.split(body): 236 237 # Reproduce normal text as is. 238 239 if not state: 240 result.append(match) 241 state = "TYPE" 242 243 # Capture the macro type. 244 245 elif state == "TYPE": 246 type = match 247 state = "ARGS" 248 249 # Substitute the macro and modified arguments. 250 251 else: 252 result.append("<<Form%s(%s)>>" % (type, ",".join( 253 adjustMacroArguments(parseMacroArguments(match), section_name, index) 254 ))) 255 state = None 256 257 return "".join(result) 258 259 def adjustMacroArguments(args, section_name, index): 260 261 """ 262 Adjust the given 'args' so that the path incorporates the given 263 'section_name' and 'index', returning a new list containing the revised 264 path and remaining arguments. 265 """ 266 267 result = [] 268 path = None 269 270 for arg in args: 271 if arg.startswith("path="): 272 path = arg[5:] 273 else: 274 result.append(arg) 275 276 qualified = "%s%s$%s" % (path and ("%s/" % path) or "", section_name, index) 277 result.append("path=%s" % qualified) 278 279 return result 280 281 def parseMacroArguments(args): 282 283 """ 284 Interpret the arguments. 285 NOTE: The argument parsing should really be more powerful in order to 286 NOTE: support labels. 287 """ 288 289 try: 290 parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] 291 except AttributeError: 292 parsed_args = args.split(",") 293 294 return [arg for arg in parsed_args if arg] 295 296 def getFields(d, remove=False): 297 298 """ 299 Return the form fields hierarchy for the given dictionary 'd'. If the 300 optional 'remove' parameter is set to a true value, remove the entries for 301 the fields from 'd'. 302 """ 303 304 fields = {} 305 306 for key, value in d.items(): 307 308 # Detect modifying fields. 309 310 if key.find("=") != -1: 311 fields[key] = value 312 if remove: 313 del d[key] 314 continue 315 316 # Reproduce the original hierarchy of the fields. 317 318 section = fields 319 parts = getPathDetails(key) 320 321 for name, index in parts[:-1]: 322 323 # Add an entry for instances of the section. 324 325 if not section.has_key(name): 326 section[name] = {} 327 328 # Add an entry for the specific instance of the section. 329 330 if not section[name].has_key(index): 331 section[name][index] = {} 332 333 section = section[name][index] 334 335 section[parts[-1][0]] = value 336 337 if remove: 338 del d[key] 339 340 return fields 341 342 def getPathDetails(path): 343 344 """ 345 Return the given 'path' as a list of (name, index) tuples providing details 346 of section instances, with any specific field appearing as the last element 347 and having the form (name, None). 348 """ 349 350 parts = [] 351 352 for part in path.split("/"): 353 try: 354 name, index = part.split("$", 1) 355 index = int(index) 356 except ValueError: 357 name, index = part, None 358 359 parts.append((name, index)) 360 361 return parts 362 363 def getSectionForPath(path, fields): 364 365 """ 366 Obtain the section indicated by the given 'path' from the 'fields', 367 returning a tuple of the form (parent section, (name, index)), where the 368 parent section contains the referenced section, where name is the name of 369 the referenced section, and where index, if not None, is the index of a 370 specific section instance within the named section. 371 """ 372 373 parts = getPathDetails(path) 374 section = fields 375 376 for name, index in parts[:-1]: 377 section = fields[name][index] 378 379 return section, parts[-1] 380 381 def getSectionElements(section_elements): 382 383 "Return the given 'section_elements' as an ordered collection." 384 385 keys = map(int, section_elements.keys()) 386 keys.sort() 387 388 elements = [] 389 390 for key in keys: 391 elements.append(section_elements[key]) 392 393 return elements 394 395 def formatFormForOutputType(text, request, mimetype, attrs=None, write=None): 396 397 """ 398 Format the given 'text' using the specified 'request' for the given output 399 'mimetype'. 400 401 The optional 'attrs' can be used to control the presentation of the form. 402 403 If the 'write' parameter is specified, use it to write output; otherwise, 404 write output using the request. 405 """ 406 407 write = write or request.write 408 409 if mimetype == "text/html": 410 write('<html>') 411 write('<body>') 412 fmt = request.html_formatter 413 fmt.setPage(request.page) 414 formatForm(text, request, fmt, attrs, write) 415 write('</body>') 416 write('</html>') 417 418 # vim: tabstop=4 expandtab shiftwidth=4