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_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] 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))
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")