Source code for gavo.web.formrender

"""
The form renderer is the standard renderer for web-facing services.
"""

#c Copyright 2008-2023, the GAVO project <gavo@ari.uni-heidelberg.de>
#c
#c This program is free software, covered by the GNU GPL.  See the
#c COPYING file in the source distribution.


from twisted.internet import defer
from twisted.web import server
from twisted.web import template
from twisted.web.template import tags as T

from gavo import base
from gavo import formats
from gavo import svcs
from gavo.base import typesystems
from gavo import formal
from gavo.formal import nevowc
from gavo.svcs import customwidgets
from gavo.svcs import inputdef
from gavo.svcs import streaming
from gavo.web import grend
from gavo.web import serviceresults
from gavo.web import weberrors


[docs]class ToFormalConverter(typesystems.FromSQLConverter): """is a converter from SQL types to Formal type specifications. The result of the conversion is a tuple of formal type and widget factory. """ typeSystem = "Formal" simpleMap = { "smallint": (formal.Integer, formal.TextInput), "integer": (formal.Integer, formal.TextInput), "int": (formal.Integer, formal.TextInput), "bigint": (formal.Integer, formal.TextInput), "real": (formal.Float, formal.TextInput), "float": (formal.Float, formal.TextInput), "boolean": (formal.Boolean, formal.Checkbox), "double precision": (formal.Float, formal.TextInput), "double": (formal.Float, formal.TextInput), "text": (formal.String, formal.TextInput), "unicode": (formal.String, formal.TextInput), "char": (formal.String, formal.TextInput), "date": (formal.Date, formal.widgetFactory(formal.DatePartsInput, twoCharCutoffYear=50, dayFirst=True)), "time": (formal.Time, formal.TextInput), "timestamp": (formal.Date, formal.widgetFactory(formal.DatePartsInput, twoCharCutoffYear=50, dayFirst=True)), "vexpr-float": (formal.String, customwidgets.NumericExpressionField), "vexpr-date": (formal.String, customwidgets.DateExpressionField), "vexpr-string": (formal.String, customwidgets.StringExpressionField), "vexpr-mjd": (formal.String, customwidgets.DateExpressionField), "pql-string": (formal.String, formal.TextInput), "pql-int": (formal.String, formal.TextInput), "pql-float": (formal.String, formal.TextInput), "pql-date": (formal.String, formal.TextInput), "file": (formal.File, None), "raw": (formal.String, formal.TextInput), }
[docs] def convert(self, type, xtype=None): if xtype=="interval": baseType, baseWidget = self.convert(type.rsplit('[', 1)[0]) return (lambda **kw: customwidgets.PairOf(baseType, **kw), customwidgets.Interval) try: return typesystems.FromSQLConverter.convert(self, type) except base.ConversionError: raise
[docs] def mapComplex(self, type, length): if type in self._charTypes: return formal.String, formal.TextInput if length!='1' and type in self.simpleMap: ftype, _ = self.simpleMap[type] class S(formal.Sequence): type = ftype instance = ftype() return S, formal.TextInput
sqltypeToFormal = ToFormalConverter().convert def _getFormalType(inputKey): return sqltypeToFormal(inputKey.type, xtype=inputKey.xtype )[0](required=inputKey.required) def _makeWithPlaceholder(origWidgetFactory, newPlaceholder): """helps _addPlaceholder to keep the namespaces sane. """ if newPlaceholder is None: return origWidgetFactory class widgetFactory(origWidgetFactory): placeholder = newPlaceholder return widgetFactory def _getWidgetFactory(inputKey): if not hasattr(inputKey, "_widgetFactoryCache"): widgetFactory = inputKey.widgetFactory if widgetFactory is None: if inputKey.isEnumerated(): widgetFactory = customwidgets.EnumeratedWidget(inputKey) else: widgetFactory = sqltypeToFormal(inputKey.type, inputKey.xtype)[1] if isinstance(widgetFactory, str): widgetFactory = customwidgets.makeWidgetFactory(widgetFactory) inputKey._widgetFactoryCache = _makeWithPlaceholder(widgetFactory, inputKey.getProperty("placeholder", None)) return inputKey._widgetFactoryCache
[docs]def getFieldArgsForInputKey(inputKey): """returns a dictionary of keyword arguments for gavo.formal addField from a DaCHS InputKey. """ # infer whether to show a unit and if so, which unit = "" if inputKey.type!="date": # Sigh. unit = inputKey.inputUnit or inputKey.unit or "" if unit: unit = " [%s]"%unit label = inputKey.getLabel() res = { "name": inputKey.name, "type": _getFormalType(inputKey), "widgetFactory": _getWidgetFactory(inputKey), "label": label+unit, "description": inputKey.description, "cssClass": inputKey.getProperty("cssClass", None),} if inputKey.values and inputKey.values.default: res["default"] = str(inputKey.values.default) if inputKey.value: res["default"] = str(inputKey.value) return res
[docs]class MultiField(formal.Group, template.Element): """A "widget" containing multiple InputKeys (i.e., formal Fields) in a single line. """ loader = template.TagLoader( T.div(class_=template.slot("class"), render="multifield")[ T.label(for_=template.slot('id'))[template.slot('label')], T.div(class_="multiinputs", id=template.slot('id'), render="childFields"), T.div(class_='description')[template.slot('description')], template.slot('message')])
[docs] @template.renderer def childFields(self, request, tag): formData = self.form.data formErrors = self.form.errors for field in self.items: widget = field.makeWidget() if field.type.immutable: render = widget.renderImmutable else: render = widget.render cssClass = " ".join(s for s in (field.cssClass, "inmulti") if s) if formErrors: formData = request.args tag[ T.span(class_=cssClass)[ render(request, field.key, formData, formErrors)( class_=cssClass, title=field.description or "")]] return tag
def _getMessageElement(self, request): errors = [] formErrors = self.form.errors if formErrors is not None: for field in self.items: err = formErrors.getFieldError(field.key) if err is not None: errors.append(err.message) if errors: return T.div(class_='message')["; ".join(errors)] else: return ''
[docs] @template.renderer def multifield(self, request, tag): errMsg = self._getMessageElement(request) tag.fillSlots(description=self.description, label=self.label or "", id="multigroup-"+self.key, message=errMsg) if errMsg: tag.fillSlots(**{"class":'field error'}) else: tag.fillSlots(**{"class":'field'}) return tag
[docs]class FormMixin(object): """A mixin to produce input forms for services and display errors within these forms. """ parameterStyle = "form" def _handleInputErrors(self, failure, request): """goes as an errback to form handling code to allow correction form rendering at later stages than validation. """ if isinstance(failure.value, formal.FormError): self.genForm.errors.add(failure.value) elif isinstance(failure.value, base.ValidationError) and isinstance( failure.value.colName, str): try: # Find out the formal name of the failing field... failedField = failure.value.colName # ...and make sure it exists self.genForm.items.getItemByName(failedField) self.genForm.errors.add(formal.FieldValidationError( str(failure.getErrorMessage()), failedField)) except KeyError: # Failing field cannot be determined self.genForm.errors.add(formal.FormError("Problem with input" " in the internal or generated field '%s': %s"%( failure.value.colName, failure.getErrorMessage()))) else: base.ui.notifyFailure(failure) return failure return self.genForm.errors def _addDefaults(self, request, form, additionalDefaults): """adds defaults from additionalDefaults. This is to let operators preset fields. """ if request is None: args = () else: args = request.strargs for key, value in list(additionalDefaults.items()): if not key in args: form.data[key] = value def _addInputKey(self, form, container, inputKey): """adds a form field for an inputKey to the form. """ if inputKey.values and inputKey.values.default: self._defaultsForForm[inputKey.name] = inputKey.values.default if inputKey.hasProperty("defaultForForm"): self._defaultsForForm[inputKey.name ] = inputKey.getProperty("defaultForForm") container.addField(**getFieldArgsForInputKey(inputKey)) def _groupQueryFields(self, inputTable): """returns a list of "grouped" inputKey names from inputTable. The idea here is that you can define "groups" in your input table. Each such group can contain paramRefs. When the input table is rendered in HTML, the grouped fields are created in a formal group. To make this happen, they may need to be resorted. This happens in this function. The returned list contains strings (parameter names), groups (meaning "start a new group") and None (meaning end the current group). This is understood and used by _addQueryFields. """ groupedKeys = {} for group in inputTable.groups: for ref in group.paramRefs: groupedKeys[ref.key] = group inputKeySequence, addedNames = [], set() for inputKey in inputTable.inputKeys: thisName = inputKey.name if thisName in addedNames: # part of a group and added as such continue newGroup = groupedKeys.get(thisName) if newGroup is None: # not part of a group inputKeySequence.append(thisName) addedNames.add(thisName) else: # current key is part of a group: add it and all others in the group # enclosed in group/None. inputKeySequence.append(newGroup) for ref in groupedKeys[inputKey.name].paramRefs: inputKeySequence.append(ref.key) addedNames.add(ref.key) inputKeySequence.append(None) return inputKeySequence def _addQueryFieldsForInputTable(self, form, inputTable): """generates input fields form the parameters of inputTable, taking into account grouping if necessary. """ containers = [form] for item in self._groupQueryFields(inputTable): if item is None: # end of group containers.pop() elif isinstance(item, str): # param reference self._addInputKey(form, containers[-1], inputTable.inputKeys.getColumnByName(item)) else: # It's a new group -- if the group has a "style" property and # it's "compact", use a special container form formal. if item.getProperty("style", None)=="compact": groupClass = MultiField else: groupClass = formal.Group containers.append( form.add(groupClass(item.name, description=item.description, label=item.getProperty("label", None), cssClass=item.getProperty("cssClass", None), form=form))) def _addQueryFields(self, form): """adds the inputFields of the service to form, setting proper defaults from the field or from data. """ # we have an inputTable. Handle groups and other fancy stuff self._addQueryFieldsForInputTable(form, self.service.getCoreFor(self, self.queryMeta).inputTable) # and add the service keys manually as appropriate for item in inputdef.filterInputKeys(self.service.serviceKeys, self.name, inputdef.getRendererAdaptor(self)): self._addInputKey(form, form, item) def _addMetaFields(self, form): """adds fields to choose output properties to form. """ try: if self.service.core.wantsTableWidget(): form.addField("_DBOPTIONS", svcs.FormalDict, formal.widgetFactory(svcs.DBOptions, self.service, self.queryMeta), label="Table") except AttributeError: # probably no wantsTableWidget method on core pass def _getFormLinks(self): """returns stan for widgets building GET-type strings for the current form content. """ return T.div(class_="formLinks")[ T.a(href="", class_="resultlink", onmouseover= "this.href=makeResultLink(getEnclosingForm(this))", # Ugh -- when the form is flattened, the formal.FormRenderer # is the renderFactory, and it doesn't know iflinkable. # No idea how we'd do this in an XML template. Hm. render=self.iflinkable) ["[Result link]"], " ", T.a(href="", class_="resultlink", onmouseover= "this.href=makeBookmarkLink(getEnclosingForm(this))")[ T.img(src=base.makeSitePath("/static/img/bookmark.png"), class_="silentlink", title="Link to this form", alt="[bookmark]") ], ]
[docs] def form_genForm(self, request=None, data=None): # this is an accumulator for defaultForForm items processed; this # is used below to pre-fill forms without influencing service # behaviour in the absence of parameters. self._defaultsForForm = {} form = formal.Form() self._addQueryFields(form) self._addMetaFields(form) self._addDefaults(request, form, self._defaultsForForm) if (self.name=="form" and self.service.getProperty("fixedFormat", "")!="True"): form.addField("_OUTPUT", formal.String, formal.widgetFactory(serviceresults.OutputFormat, self.service, self.queryMeta), label="Output format") form.actionURL = self.service.getURL(self.name) form.addAction(self.submitAction, label="Go") form.actionMaterial = self._getFormLinks() self.genForm = form return form
def _setResult(self, result): """sets result as the renderer's internal result This is the default callback of the submit action and is required by various data and render functions. Once the renderer is here, non-rsc.Data results have been dealt with already. """ self.result = result return result
[docs] def data_resultmeta(self, request, tag): resultmeta = { "itemsMatched": str(self.queryMeta.get("Matched", len(self.result.getPrimaryTable()))), "message": "", } return resultmeta
[docs] def data_inputRec(self, request, tag): if self.queryMeta["inputTable"]: return self.queryMeta["inputTable"].getParamDict() else: return {}
[docs] def data_table(self, request, tag): return self.result.getPrimaryTable()
[docs] def data_tableWithRole(self, role): """returns the table with role. If no such table is available, this will return an empty string. """ def _(req, tag): try: return self.result.getTableWithRole(role) except (AttributeError, base.DataError): return "" return _
def _realSubmitAction(self, request, form, data): """helps submitAction by doing the real work. It is here so we can add an error handler in submitAction. """ if self.queryMeta.get("format") in ("HTML", None): resultWriter = self else: resultWriter = serviceresults.getFormat(self.queryMeta["format"]) if resultWriter.compute: d = self.runAsyncWithFormalData(data, request) else: d = defer.succeed(None) d.addCallback(self._setResult) if resultWriter is not self: d.addCallback(resultWriter._formatOutput, request, self.queryMeta) return d
[docs] def submitAction(self, request, form, data): """executes the service. This is a callback for the formal form. """ return defer.maybeDeferred( self._realSubmitAction, request, form, data ).addErrback(self._handleInputErrors, request)
[docs]class FormRenderer(formal.ResourceWithForm, FormMixin, grend.CustomTemplateMixin, grend.HTMLResultRenderMixin, grend.ServiceBasedPage): """The "normal" renderer within DaCHS for web-facing services. It will display a form and allow outputs in various formats. It also does error reporting as long as that is possible within the form. """ name = "form" runOnEmptyInputs = False processOnGET = True compute = True defaultOutputFormat = "HTML" def __init__(self, request, service): grend.ServiceBasedPage.__init__(self, request, service) if "form" in self.service.templates: self.customTemplate = self.service.getTemplate("form") # enable special handling if I'm rendering fixed-behaviour services # (i.e., ones that never have inputs) XXX TODO: Figure out where I used this and fix that to use the fixed renderer (or whatever) if not self.service.getInputKeysFor(self): self.runOnEmptyInputs = True
[docs] @classmethod def isBrowseable(self, service): return True
[docs] @classmethod def isCacheable(self, segments, request): return segments==()
[docs] def render(self, request): if self.runOnEmptyInputs: request.args[formal.FORMS_KEY] = [b"genForm"] # we need to override the normal render callback here because we # may actually stream out completely different stuff, and # if we do, these need to finish the request. But that's # all arranged up in the submit action, so all we need to # to here is ignore things. return formal.ResourceWithForm.render(self, request, customCallback=lambda res: self._formatOutput(res, request))
[docs] def crash(self, failure, request): return weberrors.renderDCErrorPage(failure, request)
handleError = crash def _formatOutput(self, res, request): """delivers the whole document, hopefully not blocking too much. """ if res==server.NOT_DONE_YET: # _realSubmitAction has arranged for someone else to write the result, # and that's what its render method returned return res if isinstance(res, tuple): # core returned a complete document (mime and string) mime, payload = res request.setHeader("content-type", mime) request.setHeader('content-disposition', 'attachment; filename=result%s'%formats.getExtensionFor(mime)) return streaming.streamOut(lambda f: f.write(payload), request, self.queryMeta) else: if "response" in self.service.templates: self.customTemplate = self.service.getTemplate("response") nevowc.TemplatedPage.renderDocument(self, request ).addErrback(weberrors.renderDCErrorPage, request ).addCallback(request.finishCallback) return server.NOT_DONE_YET
[docs] def getChild(self, name, request): if name==b"": # redirect so we're not a directory resource raise svcs.WebRedirect(request.path.rstrip(b"/")) else: raise svcs.UnknownURI("Forms have no children")
defaultLoader = svcs.loadSystemTemplate("defaultresponse.html")