# HG changeset patch # User Paul Boddie # Date 1354390715 -3600 # Node ID fc84b1f78f1e86a58e129c0f3797b658bf09d12e # Parent 17a538df78259f9d45e7d571bb508890dee226c5 Added support for form sections, hierarchical naming, and modification of forms. Fixed values retrieved by the macro for fields in form sections. Added support for select/option fields. diff -r 17a538df7825 -r fc84b1f78f1e MoinForms.py --- a/MoinForms.py Thu Nov 29 00:53:51 2012 +0100 +++ b/MoinForms.py Sat Dec 01 20:38:35 2012 +0100 @@ -7,12 +7,14 @@ """ from MoinMoin import wikiutil -from StringIO import StringIO from MoinSupport import * import re __version__ = "0.1" +form_field_regexp_str = r"<>" +form_field_regexp = re.compile(form_field_regexp_str, re.DOTALL) + # Common formatting functions. def formatForm(text, request, fmt, attrs=None, write=None): @@ -30,7 +32,13 @@ fields = getFields(get_form(request)) - querystr = attrs and attrs.has_key("action") and ("action=%s" % attrs["action"]) or None + queryparams = [] + + for argname in ["fragment", "action"]: + if attrs and attrs.has_key(argname): + queryparams.append("%s=%s" % (argname, attrs[argname])) + + querystr = "&".join(queryparams) write(fmt.rawHTML('
' % escattr(page.url(request, querystr)) @@ -60,8 +68,6 @@ for region in getRegions(text, True): format, attributes, body, header, close = getFragmentFromRegion(region) - # NOTE: Need to adjust FormField macros to use hierarchical names. - # Include bare regions as they are. if format is None: @@ -72,9 +78,15 @@ elif format == "form": section_name = attributes.get("section") if section_name and section.has_key(section_name): - output.append(header) - output.append(getFormOutput(body, section[section_name])) - output.append(close) + + # Iterate over the section contents ignoring the given indexes. + + for index, element in enumerate(getSectionElements(section[section_name])): + + # Adjust FormField macros to use hierarchical names. + + adjusted_body = adjustFormFields(body, section_name, index) + output.append(getFormOutput(adjusted_body, element)) # Inspect and include other regions. @@ -85,40 +97,157 @@ return "".join(output) -def getFields(d): +def adjustFormFields(body, section_name, index): + + """ + Return a version of the 'body' with the names in FormField macros updated to + incorporate the given 'section_name' and 'index'. + """ + + result = [] + in_macro = False + + for match in form_field_regexp.split(body): + if not in_macro: + result.append(match) + else: + result.append("<>" % ",".join( + adjustMacroArguments(parseMacroArguments(match), section_name, index) + )) + + in_macro = not in_macro + + return "".join(result) + +def adjustMacroArguments(args, section_name, index): """ - Return the form fields hierarchy for the given dictionary 'd'. + Adjust the given 'args' so that the path incorporates the given + 'section_name' and 'index', returning a new list containing the revised + path and remaining arguments. + """ + + result = [] + path = None + + for arg in args: + if arg.startswith("path="): + path = arg[5:] + else: + result.append(arg) + + qualified = "%s%s$%s" % (path and ("%s/" % path) or "", section_name, index) + result.append("path=%s" % qualified) + + return result + +def parseMacroArguments(args): + + """ + Interpret the arguments. + NOTE: The argument parsing should really be more powerful in order to + NOTE: support labels. + """ + + try: + parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] + except AttributeError: + parsed_args = args.split(",") + + return [arg for arg in parsed_args if arg] + +def getFields(d, remove=False): + + """ + Return the form fields hierarchy for the given dictionary 'd'. If the + optional 'remove' parameter is set to a true value, remove the entries for + the fields from 'd'. """ fields = {} for key, value in d.items(): + # Detect modifying fields. + + if key.find("=") != -1: + fields[key] = value + if remove: + del d[key] + continue + # Reproduce the original hierarchy of the fields. section = fields - parts = key.split("/") + parts = getPathDetails(key) - for part in parts[:-1]: - try: - name, index = part.split("$", 1) - index = int(index) - except ValueError: - name, index = part, None + for name, index in parts[:-1]: + + # Add an entry for instances of the section. if not section.has_key(name): section[name] = {} + # Add an entry for the specific instance of the section. + if not section[name].has_key(index): section[name][index] = {} section = section[name][index] - section[parts[-1]] = value + section[parts[-1][0]] = value + + if remove: + del d[key] return fields +def getPathDetails(path): + parts = [] + + for part in path.split("/"): + try: + name, index = part.split("$", 1) + index = int(index) + except ValueError: + name, index = part, None + + parts.append((name, index)) + + return parts + +def getSectionForPath(path, fields): + + """ + Obtain the section indicated by the given 'path' from the 'fields', + returning a tuple of the form (parent section, (name, index)), where the + parent section contains the referenced section, where name is the name of + the referenced section, and where index, if not None, is the index of a + specific section instance within the named section. + """ + + parts = getPathDetails(path) + section = fields + + for name, index in parts[:-1]: + section = fields[name][index] + + return section, parts[-1] + +def getSectionElements(section_elements): + + "Return the given 'section_elements' as an ordered collection." + + keys = map(int, section_elements.keys()) + keys.sort() + + elements = [] + + for key in keys: + elements.append(section_elements[key]) + + return elements + def formatFormForOutputType(text, request, mimetype, attrs=None, write=None): """ diff -r 17a538df7825 -r fc84b1f78f1e actions/MoinFormHandler.py --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/actions/MoinFormHandler.py Sat Dec 01 20:38:35 2012 +0100 @@ -0,0 +1,113 @@ +# -*- coding: iso-8859-1 -*- +""" + MoinMoin - MoinFormHandler Action + + @copyright: 2012 by Paul Boddie + @license: GNU GPL (v2 or later), see COPYING.txt for details. +""" + +from MoinMoin.action import do_show +from MoinMoin import config +from MoinForms import * + +Dependencies = ['pages'] + +class MoinFormHandlerAction: + + "A handler action that can be specialised for individual forms." + + def __init__(self, pagename): + self.pagename = pagename + + def processForm(self, request): + + """ + Interpret the given 'request' details and modify them according to the + structure of the interpreted information. + """ + + # Get the form fields and obtain the hierarchical field structure. + + form = get_form(request) + fields = getFields(form, remove=True) + + # Process modification operations. + + self.modifyFields(fields) + self.serialiseFields(fields, form) + + do_show(self.pagename, request) + + def serialiseFields(self, fields, form, path=None): + + """ + Serialise the given 'fields' to the given 'form', using the given 'path' + to name the entries. + """ + + for key, value in fields.items(): + + # Serialise sections. + + if isinstance(value, dict): + for index, element in enumerate(getSectionElements(value)): + element_ref = "%s$%s" % (key, index) + + self.serialiseFields(element, form, + path and ("%s/%s" % (path, element_ref)) or element_ref + ) + + # Serialise fields. + + else: + form[path and ("%s/%s" % (path, key)) or key] = value + + def modifyFields(self, fields): + + "Modify the given 'fields', removing and adding items." + + # First, remove fields. + + for key in fields.keys(): + if key.startswith("_remove="): + self.removeField(key[8:], fields) + + # Then, add fields. + + for key in fields.keys(): + if key.startswith("_add="): + self.addField(key[5:], fields) + + def removeField(self, path, fields): + + """ + Remove the section element indicated by the given 'path' from the + 'fields'. + """ + + section, (name, index) = getSectionForPath(path, fields) + del section[name][index] + + def addField(self, path, fields): + + """ + Add a section element indicated by the given 'path' to the 'fields'. + """ + + section, (name, index) = getSectionForPath(path, fields) + placeholder = {"_new" : ""} + + if section.has_key(name): + indexes = section[name].keys() + max_index = max(map(int, indexes)) + section[name][max_index + 1] = placeholder + else: + max_index = -1 + section[name] = {0 : placeholder} + +# Action function. + +def execute(pagename, request): + MoinFormHandlerAction(pagename).processForm(request) + +# vim: tabstop=4 expandtab shiftwidth=4 diff -r 17a538df7825 -r fc84b1f78f1e macros/FormField.py --- a/macros/FormField.py Thu Nov 29 00:53:51 2012 +0100 +++ b/macros/FormField.py Sat Dec 01 20:38:35 2012 +0100 @@ -8,6 +8,7 @@ from MoinMoin import wikiutil from MoinSupport import * +from MoinForms import parseMacroArguments Dependencies = ['pages'] @@ -26,6 +27,7 @@ The following optional named arguments are also supported: label=TEXT The label employed by button-like fields + path=PATH The location of the field in the form section hierarchy The nature of each field is described by a WikiDict entry for the given field name. @@ -37,32 +39,33 @@ _ = request.getText # Interpret the arguments. - # NOTE: The argument parsing should really be more powerful in order to - # NOTE: support labels. - try: - parsed_args = args and wikiutil.parse_quoted_separated(args, name_value=False) or [] - except AttributeError: - parsed_args = args.split(",") - - parsed_args = [arg for arg in parsed_args if arg] + parsed_args = parseMacroArguments(args) # Get special arguments. name = None + path = None dictpage = None label = None + section = None for arg in parsed_args: if arg.startswith("name="): name = arg[5:] + elif arg.startswith("path="): + path = arg[5:] + elif arg.startswith("dict="): dictpage = arg[5:] elif arg.startswith("label="): label = arg[6:] + elif arg.startswith("section="): + section = arg[8:] + elif name is None: name = arg @@ -72,41 +75,55 @@ if not name: return showError(_("No field name specified."), request) - if not dictpage: - return showError(_("No WikiDict specified."), request) + # Detect special modification fields. + + if name in ("_add", "_remove"): + field_args = {"type" : "submit"} + + # The field name is a combination of the name, path and section. + + ref = "%s=%s%s%s" % (name, path or "", path and section and "/" or "", section or "") # Get the WikiDict and the field's definition. - wikidict = getWikiDict(dictpage, request) - - if not wikidict: - return showError(_("WikiDict %s cannot be loaded.") % dictpage, request) + elif dictpage: + wikidict = getWikiDict(dictpage, request) - try: - field_definition = wikidict[name] - except KeyError: - return showError(_("No entry for %s in %s.") % (name, dictpage), request) - - field_args = {} - - for field_arg in field_definition.split(): - - # Record the key-value details. + if not wikidict: + return showError(_("WikiDict %s cannot be loaded for %s.") % (dictpage, name), request) try: - argname, argvalue = field_arg.split("=", 1) - field_args[argname] = argvalue + field_definition = wikidict[name] + except KeyError: + return showError(_("No entry for %s in %s.") % (name, dictpage), request) + + field_args = {} + + for field_arg in field_definition.split(): + + # Record the key-value details. - # Single keywords are interpreted as type descriptions. + try: + argname, argvalue = field_arg.split("=", 1) + field_args[argname] = argvalue + + # Single keywords are interpreted as type descriptions. - except ValueError: - if not field_args.has_key("type"): - field_args["type"] = field_arg + except ValueError: + if not field_args.has_key("type"): + field_args["type"] = field_arg + + # The field name is a combination of the path and the name. + + ref = "%s%s" % (path and ("%s/" % path) or "", name) + + else: + return showError(_("No WikiDict specified for %s.") % name, request) # Obtain any request parameters corresponding to the field. form = get_form(request) - value = form.get(name, [""])[0] + value = form.get(ref, [""])[0] # Render the field. @@ -114,23 +131,42 @@ if type == "text": return fmt.rawHTML('' % ( - escattr(name), escattr(type), escattr(field_args.get("size", "10")), escattr(value) + escattr(ref), escattr(type), escattr(field_args.get("size", "10")), escattr(value) )) elif type == "textarea": return fmt.rawHTML('' % ( - escattr(name), escattr(field_args.get("cols", "60")), escattr(field_args.get("rows", "5")), escape(value) + escattr(ref), escattr(field_args.get("cols", "60")), escattr(field_args.get("rows", "5")), escape(value) )) elif type == "submit": return fmt.rawHTML('' % ( - escattr(name), escattr(_(label or name)) + escattr(ref), escattr(_(label or name)) )) - # NOTE: Support select fields at the very least. + elif type == "select": + + if not field_args.has_key("source"): + return showError(_("No source dictionary given for %s.") % name, request) + + sourcedict = getWikiDict(field_args["source"], request) + + if not sourcedict: + return showError(_("WikiDict %s cannot be loaded for %s.") % (sourcedict, name), request) + + output = [] + output.append(fmt.rawHTML('')) + return u''.join(output) else: - return '' + return u'' def showError(text, request): fmt = request.formatter diff -r 17a538df7825 -r fc84b1f78f1e parsers/form.py --- a/parsers/form.py Thu Nov 29 00:53:51 2012 +0100 +++ b/parsers/form.py Sat Dec 01 20:38:35 2012 +0100 @@ -33,9 +33,7 @@ self.raw = raw self.request = request - attrs = parseAttributes(kw.get("format_args", ""), False) - - self.fragment = attrs.get("fragment") + self.attrs = parseAttributes(kw.get("format_args", ""), False) def format(self, fmt, write=None): @@ -45,7 +43,7 @@ using the request. """ - formatForm(self.raw, self.request, fmt, write=write) + formatForm(self.raw, self.request, fmt, self.attrs, write=write) # Extra API methods. @@ -57,7 +55,7 @@ request. """ - formatFormForOutputType(self.raw, self.request, mimetype, write=write) + formatFormForOutputType(self.raw, self.request, mimetype, self.attrs, write=write) # Class methods.