Source code for gavo.svcs.customwidgets

"""
gavo.formal custom widgets used by the DC (enumerations, table options,
etc)
"""

#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.python import components

from twisted.web.template import tags as T

from zope.interface import implementer

from gavo import base
from gavo import formal
from gavo import rscdef
from gavo import utils
from gavo.formal import iformal
from gavo.formal import types as formaltypes
from gavo.formal import widget
from gavo.formal import widgetFactory #noflake: for customWidget
from gavo.formal.util import render_cssid
from gavo.formal.widget import ( #noflake: exported names
	TextInput, Checkbox, Password, TextArea, ChoiceBase, SelectChoice,
	SelectOtherChoice, RadioChoice, CheckboxMultiChoice, FileUpload,
	Hidden)


[docs]@implementer(iformal.IWidget) class DBOptions(object): """A widget that offers limit and sort options for db based cores. This is for use in a formal form and goes together with the FormalDict type below. """ sortWidget = None limitWidget = None def __init__(self, typeOb, service, queryMeta): self.service = service self.typeOb = typeOb if getattr(self.service.core, "sortKey", None) is None: self.sortWidget = self._makeSortWidget(service, queryMeta) self.directionWidget = self._makeDirectionWidget(service, queryMeta) if getattr(self.service.core, "limit", None) is None: self.limitWidget = self._makeLimitWidget(service) def _makeSortWidget(self, service, queryMeta): fields = [f for f in self.service.getCurOutputFields(queryMeta, raiseOnUnknown=False)] if not fields: return None defaultKey = service.core.getProperty("defaultSortKey", None) if defaultKey: return SelectChoice(formaltypes.String(), options=[(field.name, field.getLabel()) for field in fields if field.name!=defaultKey], noneOption=(defaultKey, defaultKey)) else: return SelectChoice(formaltypes.String(), options=[(field.name, field.getLabel()) for field in fields]) def _makeDirectionWidget(self, service, queryMeta): return SelectChoice(formaltypes.String(), options=[("DESC", "DESC")], noneOption=("ASC", "ASC")) def _makeLimitWidget(self, service): keys = [(i, str(i)) for i in [1000, 5000, 10000, 100000, 250000]] return SelectChoice(formaltypes.Integer(), options=keys, noneOption=(100, "100"))
[docs] def render(self, request, key, args, errors): # The whole plan sucks -- these should have been two separate widgets children = [] if '_DBOPTIONS' in args: # we're working from pre-parsed (nevow formal) arguments v = [[args["_DBOPTIONS"]["order"]] or "", [args["_DBOPTIONS"]["limit"] or "100"], [args["_DBOPTIONS"]["direction"] or "ASC"]] else: # args come raw from t.w.Request v = [args.get(b"_DBOPTIONS_ORDER", [b'']), args.get(b"MAXREC", [b"100"]), args.get(b"_DBOPTIONS_DIR", b"ASC")] if errors: # emulate t.w.Request no matter what v = [args.get(b"_DBOPTIONS_ORDER", [b'']), args.get(b"MAXREC", [b"100"]), args.get(b"_DBOPTIONS_DIR", b"ASC")] else: args = {"_DBOPTIONS_ORDER": utils.debytify(v[0][0]), "MAXREC": int(v[1][0]), "_DBOPTIONS_DIR": v[2][0]} if self.sortWidget: children.extend(["Sort by ", self.sortWidget.render(request, "_DBOPTIONS_ORDER", args, errors), " "]) children.extend([" ", self.directionWidget.render(request, "_DBOPTIONS_DIR", args, errors)]) if self.limitWidget: children.extend([T.br, "Limit to ", self.limitWidget.render(request, "MAXREC", args, errors), " items."]) return T.span(id=render_cssid(key))[children]
# XXX TODO: make this immutable. renderImmutable = render
[docs] def processInput(self, request, key, args, default=''): order, limit, direction = None, None, "ASC" if self.sortWidget: order = self.sortWidget.processInput(request, "_DBOPTIONS_ORDER", args) if self.directionWidget: direction = self.directionWidget.processInput( request, "_DBOPTIONS_DIR", args) if self.limitWidget: limit = self.limitWidget.processInput(request, "MAXREC", args) return { "order": order, "limit": limit, "direction": direction, }
[docs]class FormalDict(formaltypes.Type): """is a formal type for dictionaries. """ pass
# I might want to specialise PairOf to have just two items; let's # see if that's necessary, though. PairOf = formaltypes.Sequence
[docs]class SimpleSelectChoice(SelectChoice): def __init__(self, original, options, noneLabel=None): if noneLabel is None: noneOption = None else: noneOption = (noneLabel, noneLabel) super(SimpleSelectChoice, self).__init__(original, [(o,o) for o in options], noneOption)
# MultiSelectChoice is like formal's choice except you can specify a size.
[docs]class MultiSelectChoice(SelectChoice): size = 3 def __init__(self, original, size=None, **kwargs): if size is not None: self.size=size SelectChoice.__init__(self, original, **kwargs) def _renderTag(self, request, key, value, converter, disabled): if not isinstance(value, (list, tuple)): value = [value] tag = T.select(name=key, id=render_cssid(key)) if self.noneOption is not None: noneVal = iformal.IKey(self.noneOption).key() option = T.option(value=str(noneVal))[ iformal.ILabel(self.noneOption).label()] if value is None or value==noneVal: option = option(selected='selected') tag[option] for item in self.options or []: optValue = iformal.IKey(item).key() optLabel = iformal.ILabel(item).label() optValue = converter.fromType(optValue) option = T.option(value=widget.stringifyOptionValue(optValue))[ str(optLabel)] if optValue in value: option = option(selected='selected') tag[option] if disabled: tag(class_='disabled', disabled='disabled') return T.span(style="white-space:nowrap")[ tag(size=str(self.size), multiple="multiple"), " ", T.span(class_="fieldlegend")[ "No selection matches all, multiple values legal."]]
[docs] def render(self, request, key, args, errors): converter = iformal.IStringConvertible(self.original) if errors: value = args.get(key, []) else: value = [converter.fromType(v) for v in args.get(key, [])] return self._renderTag(request, key, value, converter, False)
[docs] def processInput(self, request, key, args, default=''): values = args.get(key, default.split()) rv = [] for value in values: value = iformal.IStringConvertible(self.original).toType(value) if self.noneOption is not None and value==iformal.IKey( self.noneOption).key(): # NoneOption means "any" here, don't generate a condition return None rv.append(self.original.validate(value)) return rv
def _getDisplayOptions(ik): """helps EnumeratedWidget figure out the None option and the options for selection. """ noneOption = None options = [] default = ik.values.default if ik.value: default = ik.value if default is not None: if ik.required: # default given and field required: There's no noneOption but a # selected default (this shouldn't happen when values.default is gone) options = ik.values.options else: # default given and becomes the noneOption for o in ik.values.options: if o.content_==ik.values.default: noneOption = o else: options.append(o) else: # no default given, make up ANY option as noneOption unless # ik is required. options.extend(ik.values.options) noneOption = None if not ik.required and not ik.values.multiOk or ik.multiplicity=="multiple": noneOption = base.makeStruct(rscdef.Option, title="ANY", content_="__DaCHS__ANY__") return noneOption, options
[docs]def EnumeratedWidget(ik): """a widget factory for input keys over enumerated columns. This probably contains a bit too much magic, but let's see. The current rules are: If values.multiOk is true, render a MultiSelectChoice, else render a SelectChoice or a RadioChoice depending on how many items there are. If ik is not required, add an ANY key evaluating to None. For MultiSelectChoices we don't need this since for them, you can simply leave it all unselected. If there is a default, it becomes the NoneOption. """ if not ik.isEnumerated(): raise base.StructureError("%s is not enumerated"%ik.name) noneOption, options = _getDisplayOptions(ik) moreArgs = {"noneOption": noneOption} if ik.values.multiOk or ik.multiplicity=="multiple": if ik.showItems==-1 or len(options)<4: baseWidget = CheckboxMultiChoice del moreArgs["noneOption"] else: baseWidget = MultiSelectChoice moreArgs["size"] = ik.showItems moreArgs["noneOption"] = None else: if len(options)<4: baseWidget = RadioChoice else: baseWidget = SelectChoice res = formal.widgetFactory(baseWidget, options=options, **moreArgs) return res
[docs]class StringFieldWithBlurb(widget.TextInput): """is a text input widget with additional material at the side. """ additionalMaterial = "" def __init__(self, *args, **kwargs): am = kwargs.pop("additionalMaterial", None) widget.TextInput.__init__(self, *args, **kwargs) if am is not None: self.additionalMaterial = am def _renderTag(self, request, key, value, readonly): plainTag = widget.TextInput._renderTag(self, request, key, value, readonly) return T.span(style="white-space:nowrap")[ plainTag, " ", T.span(class_="fieldlegend")[self.additionalMaterial]]
[docs]class NumericExpressionField(StringFieldWithBlurb): additionalMaterial = T.a(href=base.makeSitePath( "/static/help_vizier.shtml#floats"))[ "[?num. expr.]"]
[docs]class DateExpressionField(StringFieldWithBlurb): additionalMaterial = T.a(href=base.makeSitePath( "/static/help_vizier.shtml#dates"))[ "[?date expr.]"]
[docs]class StringExpressionField(StringFieldWithBlurb): additionalMaterial = T.a(href=base.makeSitePath( "/static/help_vizier.shtml#string"))[ "[?char expr.]"]
[docs]class ScalingTextArea(widget.TextArea): """is a text area that scales with the width of the window. """ def _renderTag(self, request, key, value, readonly): tag=T.textarea(name=key, id=render_cssid(key), rows=str(self.rows), style="width:100% !important")[str(value or '')] if readonly: tag(class_='readonly', readonly='readonly') return tag
[docs]class Interval(object): """A widget to enter an interval (lower/upper) pair of something. As usual with formal widgets, this is constructed with the type, which must be PairOf here; we're taking the widget we're supposed to pair from it. """ def __init__(self, original): self.original = original self.inputType = self.original.type def _renderTag(self, request, key, values, readonly): tags = [] for index, seqid in enumerate(["lower", "upper"]): tag = T.input(type="text", name=key+seqid, id=render_cssid(key+seqid), value=values[index]) if readonly: tag(class_='readonly', readonly='readonly') # TODO: make a placeholder in the interval # if self.placeholder is not None: # tag(placeholder=self.placeholder) tags.append(tag) tags[1:1] = " \u2013 " return T.div(class_="form-interval")[tags]
[docs] def render(self, request, key, args, errors): if errors: values = [args.get(key+"lower", [''])[0], args.get(key+"upper", [''])[0]] else: baseType = iformal.IStringConvertible(self.original) values = [baseType.fromType(args.get(key+"lower")), baseType.fromType(args.get(key+"upper"))] return self._renderTag(request, key, values, False)
[docs] def renderImmutable(self, request, key, args, errors): baseType = iformal.IStringConvertible(self.original) values = [baseType.fromType(args.get(key+"lower")), baseType.fromType(args.get(key+"upper"))] return self._renderTag(request, key, values, True)
[docs] def processInput(self, request, key, args, default=["", ""]): charset = "utf-8" values = [ args.get(key+"lower", [default[0]])[0].decode(charset), args.get(key+"upper", [default[0]])[0].decode(charset)] baseType = iformal.IStringConvertible(self.original.type()) values = [baseType.toType(values[0]), baseType.toType(values[1])] return self.original.validate(values)
[docs]def makeWidgetFactory(code): return eval(code)
############# formal adapters for DaCHS objects # column options from gavo.rscdef import column
[docs]@implementer(iformal.ILabel, iformal.IKey) class ToFormalAdapter(object): def __init__(self, original): self.original = original
[docs] def label(self): return str(self.original.title)
[docs] def key(self): return str(self.original.content_)
components.registerAdapter(ToFormalAdapter, column.Option, iformal.ILabel) components.registerAdapter(ToFormalAdapter, column.Option, iformal.IKey)