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