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