Package gavo :: Package svcs :: Module customwidgets
[frames] | no frames]

Source Code for Module gavo.svcs.customwidgets

  1  """ 
  2  Nevow formal custom widgets used by the DC (enumerations, table options, 
  3  etc) 
  4  """ 
  5   
  6  #c Copyright 2008-2019, the GAVO project 
  7  #c 
  8  #c This program is free software, covered by the GNU GPL.  See the 
  9  #c COPYING file in the source distribution. 
 10   
 11   
 12  from twisted.python import components 
 13   
 14  from nevow import tags as T 
 15  from nevow import flat 
 16  from nevow.util import getPOSTCharset 
 17   
 18  from zope.interface import implements 
 19   
 20  from gavo import base 
 21  from gavo import rscdef 
 22  from gavo import utils 
 23  from gavo.imp import formal 
 24  from gavo.imp.formal import iformal 
 25  from gavo.imp.formal import types as formaltypes 
 26  from gavo.imp.formal import widget 
 27  from gavo.imp.formal import widgetFactory #noflake: for customWidget 
 28  from gavo.imp.formal.util import render_cssid 
 29  from gavo.imp.formal.widget import ( #noflake: exported names 
 30          TextInput, Checkbox, Password, TextArea, ChoiceBase, SelectChoice, 
 31          SelectOtherChoice, RadioChoice, CheckboxMultiChoice, FileUpload, 
 32          FileUploadWidget, Hidden) 
 33   
 34   
35 -class DBOptions(object):
36 """A widget that offers limit and sort options for db based cores. 37 38 This is for use in a formal form and goes together with the FormalDict 39 type below. 40 """ 41 implements(iformal.IWidget) 42 43 sortWidget = None 44 limitWidget = None 45
46 - def __init__(self, typeOb, service, queryMeta):
47 self.service = service 48 self.typeOb = typeOb 49 if getattr(self.service.core, "sortKey", None) is None: 50 self.sortWidget = self._makeSortWidget(service, queryMeta) 51 self.directionWidget = self._makeDirectionWidget(service, queryMeta) 52 if getattr(self.service.core, "limit", None) is None: 53 self.limitWidget = self._makeLimitWidget(service)
54
55 - def _makeSortWidget(self, service, queryMeta):
56 fields = [f for f in self.service.getCurOutputFields(queryMeta, 57 raiseOnUnknown=False)] 58 if not fields: 59 return None 60 defaultKey = service.getProperty("defaultSort", None) 61 if defaultKey: 62 return SelectChoice(formaltypes.String(), 63 options=[(field.name, field.getLabel()) 64 for field in fields if field.name!=defaultKey], 65 noneOption=(defaultKey, defaultKey)) 66 else: 67 return SelectChoice(formaltypes.String(), 68 options=[(field.name, field.getLabel()) for field in fields])
69
70 - def _makeDirectionWidget(self, service, queryMeta):
71 return SelectChoice(formaltypes.String(), 72 options=[("DESC", "DESC")], 73 noneOption=("ASC", "ASC"))
74
75 - def _makeLimitWidget(self, service):
76 keys = [(str(i), i) for i in [1000, 5000, 10000, 100000, 250000]] 77 return SelectChoice(formaltypes.Integer(), options=keys, 78 noneOption=("100", 100))
79
80 - def render(self, ctx, key, args, errors):
81 # The whole plan sucks -- these should have been two separate widgets 82 children = [] 83 if '_DBOPTIONS' in args: 84 # we're working from pre-parsed (nevow formal) arguments 85 v = [[args["_DBOPTIONS"]["order"]] or "", 86 [args["_DBOPTIONS"]["limit"] or 100], 87 [args["_DBOPTIONS"]["direction"] or "ASC"]] 88 else: 89 # args come raw from nevow contexts 90 v = [args.get("_DBOPTIONS_ORDER", ['']), 91 args.get("MAXREC", [100]), 92 args.get("_DBOPTIONS_DIR", "ASC")] 93 94 if errors: 95 args = {"_DBOPTIONS_ORDER": v[0], "MAXREC": v[1], 96 "_DBOPTIONS_DIR": v[2]} 97 else: 98 args = {"_DBOPTIONS_ORDER": v[0][0], "MAXREC": int(v[1][0]), 99 "_DBOPTIONS_DIR": v[2][0]} 100 101 if self.sortWidget: 102 children.extend(["Sort by ", 103 self.sortWidget.render(ctx, "_DBOPTIONS_ORDER", args, errors), 104 " "]) 105 children.extend([" ", self.directionWidget.render(ctx, 106 "_DBOPTIONS_DIR", args, errors)]) 107 108 if self.limitWidget: 109 children.extend([T.br, "Limit to ", 110 self.limitWidget.render(ctx, "MAXREC", args, errors), 111 " items."]) 112 return T.span(id=render_cssid(key))[children]
113 114 # XXX TODO: make this immutable. 115 renderImmutable = render 116
117 - def processInput(self, ctx, key, args, default=''):
118 order, limit, direction = None, None, "ASC" 119 if self.sortWidget: 120 order = self.sortWidget.processInput(ctx, "_DBOPTIONS_ORDER", args) 121 if self.directionWidget: 122 direction = self.directionWidget.processInput( 123 ctx, "_DBOPTIONS_DIR", args) 124 if self.limitWidget: 125 limit = self.limitWidget.processInput(ctx, "MAXREC", args) 126 return { 127 "order": order, 128 "limit": limit, 129 "direction": direction, 130 }
131 132
133 -class FormalDict(formaltypes.Type):
134 """is a formal type for dictionaries. 135 """ 136 pass
137 138 139 # I might want to specialise PairOf to have just two items; let's 140 # see if that's necessary, though. 141 PairOf = formaltypes.Sequence 142 143
144 -class SimpleSelectChoice(SelectChoice):
145 - def __init__(self, original, options, noneLabel=None):
146 if noneLabel is None: 147 noneOption = None 148 else: 149 noneOption = (noneLabel, noneLabel) 150 super(SimpleSelectChoice, self).__init__(original, 151 [(o,o) for o in options], noneOption)
152 153 154 # MultiSelectChoice is like formal's choice except you can specify a size. 155
156 -class MultiSelectChoice(SelectChoice):
157 size = 3
158 - def __init__(self, original, size=None, **kwargs):
159 if size is not None: 160 self.size=size 161 SelectChoice.__init__(self, original, **kwargs)
162
163 - def _renderTag(self, ctx, key, value, converter, disabled):
164 if not isinstance(value, (list, tuple)): 165 value = [value] 166 167 # unfortunately, I need to copy all that code from formal to let 168 # me keep multiple selections 169 def renderOptions(ctx, data): 170 if self.noneOption is not None: 171 noneVal = iformal.IKey(self.noneOption).key() 172 option = T.option(value=noneVal)[ 173 iformal.ILabel(self.noneOption).label()] 174 if value is None or value==noneVal: 175 option = option(selected='selected') 176 yield option 177 if data is None: 178 return 179 for item in data: 180 optValue = iformal.IKey(item).key() 181 optLabel = iformal.ILabel(item).label() 182 optValue = converter.fromType(optValue) 183 option = T.option(value=optValue)[optLabel] 184 if optValue in value: 185 option = option(selected='selected') 186 yield option
187 188 tag = T.select(name=key, id=render_cssid(key), data=self.options)[ 189 renderOptions] 190 if disabled: 191 tag(class_='disabled', disabled='disabled') 192 return T.span(style="white-space:nowrap")[ 193 tag(size=str(self.size), multiple="multiple"), 194 " ", 195 T.span(class_="fieldlegend")[ 196 "No selection matches all, multiple values legal."]]
197
198 - def render(self, ctx, key, args, errors):
199 converter = iformal.IStringConvertible(self.original) 200 if errors: 201 value = args.get(key, []) 202 else: 203 value = map(converter.fromType, args.get(key, []) or []) 204 return self._renderTag(ctx, key, value, converter, False)
205
206 - def processInput(self, ctx, key, args, default=''):
207 values = args.get(key, default.split()) 208 rv = [] 209 for value in values: 210 value = iformal.IStringConvertible(self.original).toType(value) 211 if self.noneOption is not None and value==iformal.IKey( 212 self.noneOption).key(): 213 # NoneOption means "any" here, don't generate a condition 214 return None 215 rv.append(self.original.validate(value)) 216 return rv
217 218
219 -def _getDisplayOptions(ik):
220 """helps EnumeratedWidget figure out the None option and the options 221 for selection. 222 """ 223 noneOption = None 224 options = [] 225 default = ik.values.default 226 if ik.value: 227 default = ik.value 228 229 if default is not None: 230 if ik.required: 231 # default given and field required: There's no noneOption but a 232 # selected default (this shouldn't happen when values.default is gone) 233 options = ik.values.options 234 else: 235 # default given and becomes the noneOption 236 for o in ik.values.options: 237 if o.content_==ik.values.default: 238 noneOption = o 239 else: 240 options.append(o) 241 else: # no default given, make up ANY option as noneOption unless 242 # ik is required. 243 options.extend(ik.values.options) 244 noneOption = None 245 if not ik.required and not ik.values.multiOk or ik.multiplicity=="multiple": 246 noneOption = base.makeStruct(rscdef.Option, title="ANY", 247 content_="__DaCHS__ANY__") 248 return noneOption, options
249 250
251 -def EnumeratedWidget(ik):
252 """is a widget factory for input keys over enumerated columns. 253 254 This probably contains a bit too much magic, but let's see. The current 255 rules are: 256 257 If values.multiOk is true, render a MultiSelectChoice, else 258 render a SelectChoice or a RadioChoice depending on how many 259 items there are. 260 261 If ik is not required, add an ANY key evaluating to None. For 262 MultiSelectChoices we don't need this since for them, you can 263 simply leave it all unselected. 264 265 If there is a default, it becomes the NoneOption. 266 """ 267 if not ik.isEnumerated(): 268 raise base.StructureError("%s is not enumerated"%ik.name) 269 noneOption, options = _getDisplayOptions(ik) 270 moreArgs = {"noneOption": noneOption} 271 if ik.values.multiOk or ik.multiplicity=="multiple": 272 if ik.showItems==-1 or len(options)<4: 273 baseWidget = CheckboxMultiChoice 274 del moreArgs["noneOption"] 275 else: 276 baseWidget = MultiSelectChoice 277 moreArgs["size"] = ik.showItems 278 moreArgs["noneOption"] = None 279 else: 280 if len(options)<4: 281 baseWidget = RadioChoice 282 else: 283 baseWidget = SelectChoice 284 res = formal.widgetFactory(baseWidget, options=options, 285 **moreArgs) 286 return res
287 288
289 -class StringFieldWithBlurb(widget.TextInput):
290 """is a text input widget with additional material at the side. 291 """ 292 additionalMaterial = "" 293
294 - def __init__(self, *args, **kwargs):
295 am = kwargs.pop("additionalMaterial", None) 296 widget.TextInput.__init__(self, *args, **kwargs) 297 if am is not None: 298 self.additionalMaterial = am
299
300 - def _renderTag(self, ctx, key, value, readonly):
301 plainTag = widget.TextInput._renderTag(self, ctx, key, value, 302 readonly) 303 return T.span(style="white-space:nowrap")[ 304 plainTag, 305 " ", 306 T.span(class_="fieldlegend")[self.additionalMaterial]]
307 308
309 -class NumericExpressionField(StringFieldWithBlurb):
310 additionalMaterial = T.a(href=base.makeSitePath( 311 "/static/help_vizier.shtml#floats"))[ 312 "[?num. expr.]"]
313 314
315 -class DateExpressionField(StringFieldWithBlurb):
316 additionalMaterial = T.a(href=base.makeSitePath( 317 "/static/help_vizier.shtml#dates"))[ 318 "[?date expr.]"]
319 320
321 -class StringExpressionField(StringFieldWithBlurb):
322 additionalMaterial = T.a(href=base.makeSitePath( 323 "/static/help_vizier.shtml#string"))[ 324 "[?char expr.]"]
325 326
327 -class ScalingTextArea(widget.TextArea):
328 """is a text area that scales with the width of the window. 329 """
330 - def _renderTag(self, ctx, key, value, readonly):
331 tag=T.textarea(name=key, id=render_cssid(key), rows=self.rows, 332 style="width:100% !important")[value or ''] 333 if readonly: 334 tag(class_='readonly', readonly='readonly') 335 return tag
336 337
338 -class Interval(object):
339 """A widget to enter an interval (lower/upper) pair of something. 340 341 As usual with formal widgets, this is constructed with the type, which 342 must be PairOf here; we're taking the widget we're supposed to pair from 343 it. 344 """
345 - def __init__(self, original):
346 self.original = original 347 self.inputType = self.original.type
348
349 - def _renderTag(self, ctx, key, values, readonly):
350 tags = [] 351 for index, seqid in enumerate(["lower", "upper"]): 352 tag = T.input(type="text", name=key+seqid, 353 id=render_cssid(key+seqid), value=values[index]) 354 if readonly: 355 tag(class_='readonly', readonly='readonly') 356 # TODO: make a placeholder in the interval 357 # if self.placeholder is not None: 358 # tag(placeholder=self.placeholder) 359 tags.append(tag) 360 361 tags[1:1] = u" \u2013 " 362 363 return T.div(class_="form-interval")[tags]
364
365 - def render(self, ctx, key, args, errors):
366 if errors: 367 values = [args.get(key+"lower", [''])[0], 368 args.get(key+"upper", [''])[0]] 369 else: 370 baseType = iformal.IStringConvertible(self.original) 371 values = [baseType.fromType(args.get(key+"lower")), 372 baseType.fromType(args.get(key+"upper"))] 373 374 return self._renderTag(ctx, key, values, False)
375
376 - def renderImmutable(self, ctx, key, args, errors):
377 baseType = iformal.IStringConvertible(self.original) 378 values = [baseType.fromType(args.get(key+"lower")), 379 baseType.fromType(args.get(key+"upper"))] 380 return self._renderTag(ctx, key, values, True)
381
382 - def processInput(self, ctx, key, args, default=["", ""]):
383 charset = getPOSTCharset(ctx) 384 values = [ 385 args.get(key+"lower", [default[0]])[0].decode(charset), 386 args.get(key+"upper", [default[0]])[0].decode(charset)] 387 baseType = iformal.IStringConvertible(self.original.type()) 388 values = [baseType.toType(values[0]), 389 baseType.toType(values[1])] 390 return self.original.validate(values)
391 392
393 -def makeWidgetFactory(code):
394 return eval(code)
395 396 397 ############# formal adapters for DaCHS objects 398 399 # column options 400 from gavo.rscdef import column 401
402 -class ToFormalAdapter(object):
403 implements(iformal.ILabel, iformal.IKey) 404
405 - def __init__(self, original):
406 self.original = original
407
408 - def label(self):
409 return unicode(self.original.title)
410
411 - def key(self):
412 return unicode(self.original.content_)
413 414 components.registerAdapter(ToFormalAdapter, column.Option, iformal.ILabel) 415 components.registerAdapter(ToFormalAdapter, column.Option, iformal.IKey) 416 417 418 flat.registerFlattener(lambda original, ctx: str(original), utils.QuotedName) 419