"""
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
# 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)
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]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)
############# formal adapters for DaCHS objects
# column options
from gavo.rscdef import column
components.registerAdapter(ToFormalAdapter, column.Option, iformal.ILabel)
components.registerAdapter(ToFormalAdapter, column.Option, iformal.IKey)