Package gavo :: Package web :: Module grend
[frames] | no frames]

Source Code for Module gavo.web.grend

  1  """ 
  2  Basic Code for Renderers. 
  3   
  4  Renderers are frontends for services.  They provide the glue to 
  5  somehow acquire input (typically, nevow contexts) and then format 
  6  the result for the user. 
  7  """ 
  8   
  9  #c Copyright 2008-2019, the GAVO project 
 10  #c 
 11  #c This program is free software, covered by the GNU GPL.  See the 
 12  #c COPYING file in the source distribution. 
 13   
 14   
 15  import os 
 16   
 17  from nevow import tags as T 
 18  from nevow import loaders 
 19  from nevow import inevow 
 20  from nevow import rend 
 21  from nevow import url 
 22   
 23  from twisted.internet import threads 
 24  from twisted.python import log 
 25  from zope.interface import implements 
 26   
 27  from gavo import base 
 28  from gavo import svcs 
 29  from gavo import rsc 
 30  from gavo.protocols import creds 
 31  from gavo.web import common 
 32  from gavo.web import htmltable 
 33   
 34   
 35  __docformat__ = "restructuredtext en" 
36 37 38 -class RDBlocked(Exception):
39 """is raised when a ResourceDescriptor is blocked due to maintanence 40 and caught by the root resource.. 41 """
42
43 44 ########## Useful mixins for Renderers 45 46 -class GavoRenderMixin(common.CommonRenderers):
47 """A mixin with renderers useful throughout the data center. 48 49 Rendering of meta information: 50 51 * <tag n:render="meta">METAKEY</tag> or 52 * <tag n:render="metahtml">METAKEY</tag> 53 54 Rendering the sidebar -- 55 <body n:render="withsidebar">. This will only work if the renderer 56 has a service attribute that's enough of a service (i.e., carries meta 57 and knows how to generate URLs). 58 59 Conditional rendering: 60 61 * ifmeta 62 * imownmeta 63 * ifdata 64 * ifnodata 65 * ifslot 66 * ifnoslot 67 * ifadmin 68 69 Obtaining system info 70 71 * rd <rdId> -- makes the referenced RD the current data (this is 72 not too useful right now, but it lets you check of the existence 73 of RDs already) 74 """ 75 _sidebar = svcs.loadSystemTemplate("sidebar.html") 76 _footer = svcs.loadSystemTemplate("footer.html") 77 78 # macro package to use when expanding macros. Just set this 79 # in the constructor as necessary (ServiceBasedRenderer has the 80 # service here) 81 macroPackage = None 82
83 - def _initGavoRender(self):
84 # call this to initialize this mixin. 85 # (kept for backward compatibility; don't use this any more) 86 pass
87
88 - def _doRenderMeta(self, ctx, raiseOnFail=False, plain=False, 89 carrier=None):
90 if carrier is None: 91 carrier = self.metaCarrier 92 if not hasattr(carrier, "_metaRenderCache"): 93 carrier._metaRenderCache = {} 94 95 metaKey = "(inaccessible)" 96 try: 97 metaKey = ctx.tag.children[0].strip() 98 if (metaKey, plain) in carrier._metaRenderCache: 99 rendered = carrier._metaRenderCache[(metaKey, plain)] 100 101 else: 102 htmlBuilder = common.HTMLMetaBuilder(self.macroPackage) 103 104 if plain: 105 rendered = base.getMetaText(carrier, metaKey, raiseOnFail=True, 106 macroPackage=self.macroPackage) 107 108 else: 109 rendered = T.xml(carrier.buildRepr(metaKey, htmlBuilder, 110 raiseOnFail=True)) 111 112 carrier._metaRenderCache[(metaKey, plain)] = rendered 113 114 except base.NoMetaKey: 115 if raiseOnFail: 116 raise 117 return T.comment["Meta item %s not given."%metaKey] 118 except Exception as ex: 119 msg = "Meta %s bad (%s)"%(metaKey, str(ex)) 120 base.ui.notifyError(msg) 121 return T.comment[msg] 122 123 ctx.tag.clear() 124 return ctx.tag[rendered]
125
126 - def data_meta(self, metaKey):
127 """returns the value for the meta key metaName on this service. 128 """ 129 def get(ctx, data): 130 return self.metaCarrier.getMeta(metaKey)
131 return get
132
133 - def render_meta(self, ctx, data):
134 """replaces a meta key with a plain text rendering of the metadata 135 in the service. 136 """ 137 return self._doRenderMeta(ctx, plain=True)
138
139 - def render_metahtml(self, ctx, data):
140 """replaces a meta key with an html rendering of the metadata in 141 the serivce. 142 """ 143 return self._doRenderMeta(ctx)
144
145 - def render_datameta(self, ctx, data):
146 """replaces the meta key in the contents with the corresponding 147 meta key's HTML rendering. 148 """ 149 return self._doRenderMeta(ctx, carrier=data)
150
151 - def render_ifmeta(self, metaName, propagate=True):
152 """renders its children if there is metadata for metaName. 153 """ 154 if propagate: 155 hasMeta = self.metaCarrier.getMeta(metaName) is not None 156 else: 157 hasMeta = self.metaCarrier.getMeta(metaName, propagate=False) is not None 158 159 if hasMeta: 160 return lambda ctx, data: ctx.tag 161 else: 162 return lambda ctx, data: ""
163
164 - def render_ifownmeta(self, metaName):
165 """renders its children if there is metadata for metaName in 166 the service itself. 167 """ 168 return self.render_ifmeta(metaName, propagate=False)
169
170 - def render_ifdata(self, ctx, data):
171 if data: 172 return ctx.tag 173 else: 174 return ""
175
176 - def render_ifnodata(self, ctx, data):
177 if not data: 178 return ctx.tag 179 else: 180 return ""
181
182 - def render_ifslot(self, slotName):
183 """renders the children for slotName is present and true. 184 185 This will not work properly if the slot values come from a deferred. 186 """ 187 def render(ctx, data): 188 try: 189 if ctx.locateSlotData(slotName): 190 return ctx.tag 191 else: 192 return "" 193 except KeyError: 194 return ""
195 return render 196
197 - def render_ifnoslot(self, slotName):
198 """renders if slotName is missing or not true. 199 200 This will not work properly if the slot values come from a deferred. 201 """ 202 # just repeat the code from ifslot -- this is called frequently, 203 # and additional logic just is not worth it. 204 def render(ctx, data): 205 try: 206 if not ctx.locateSlotData(slotName): 207 return ctx.tag 208 else: 209 return "" 210 except KeyError: 211 return ""
212 return render 213
214 - def render_ifadmin(self, ctx, data):
215 # NOTE: use of this renderer is *not* enough to protect critical operations 216 # since it does not check if the credentials are actually provided. 217 # Use this only hide links that will give 403s (or somesuch) for 218 # non-admins anyway (and the like). 219 if inevow.IRequest(ctx).getUser()=="gavoadmin": 220 return ctx.tag 221 else: 222 return ""
223
224 - def render_explodableMeta(self, ctx, data):
225 metaKey = ctx.tag.children[0] 226 title = ctx.tag.attributes.get("title", metaKey.capitalize()) 227 try: 228 return T.div(class_="explodable")[ 229 T.h4(class_="exploHead")[title], 230 T.div(class_="exploBody")[ 231 self._doRenderMeta(ctx, raiseOnFail=True)]] 232 except base.MetaError: 233 return ""
234
235 - def render_intro(self, ctx, data):
236 """returns something suitable for inclusion above the form. 237 238 The renderer tries, in sequence, to retrieve a meta called _intro, 239 the description meta, or nothing. 240 """ 241 for key in ["_intro", "description"]: 242 if self.service.getMeta(key, default=None) is not None: 243 introKey = key 244 break 245 else: 246 introKey = None 247 if introKey is None: 248 return ctx.tag[""] 249 else: 250 return ctx.tag[T.xml(self.metaCarrier.buildRepr(introKey, 251 common.HTMLMetaBuilder(self.macroPackage), 252 raiseOnFail=False))]
253
254 - def render_authinfo(self, ctx, data):
255 request = inevow.IRequest(ctx) 256 svc = getattr(self, "service", None) 257 258 if svc and request.getUser(): 259 anchorText = "Log out %s"%request.getUser() 260 targetURL = svc.getURL("logout", False) 261 explanation = " (give an empty user name in the dialog popping up)" 262 else: 263 targetURL = url.URL.fromString("/login").add("nextURL", 264 str(url.URL.fromContext(ctx))) 265 anchorText = "Log in" 266 explanation = "" 267 268 return ctx.tag[T.a(href=str(targetURL))[ 269 anchorText], explanation]
270
271 - def render_prependsite(self, ctx, data):
272 """prepends a site id to the body. 273 274 This is intended for titles and similar; it puts the string in 275 [web]sitename in front of anything that already is in ctx.tag. 276 """ 277 ctx.tag.children = [base.getConfig("web", "sitename")]+ctx.tag.children 278 return ctx.tag
279
280 - def render_withsidebar(self, ctx, data):
281 oldChildren = ctx.tag.children 282 ctx.tag.children = [] 283 return ctx.tag(class_="container")[ 284 self._sidebar, 285 T.div(id="body")[ 286 T.a(name="body"), 287 oldChildren, 288 self._footer, 289 ], 290 ]
291
292 - def data_rd(self, rdId):
293 """returns the RD referenced in the body (or None if the RD is not there) 294 """ 295 try: 296 return base.caches.getRD(rdId) 297 except base.NotFoundError: 298 return None
299
300 301 -class HTMLResultRenderMixin(object):
302 """is a mixin with render functions for HTML tables and associated 303 metadata within other pages. 304 305 This is primarily used for the Form renderer. 306 """ 307 result = None 308
309 - def render_resulttable(self, ctx, data):
310 if isinstance(data, rsc.BaseTable): 311 return htmltable.HTMLTableFragment(data, svcs.emptyQueryMeta) 312 313 elif hasattr(data, "child"): 314 return htmltable.HTMLTableFragment( 315 data.child(ctx, "table")(ctx, data), 316 data.queryMeta) 317 318 else: 319 # a FormError, most likely 320 return ""
321
322 - def render_resultline(self, ctx, data):
323 if hasattr(data, "child"): 324 return htmltable.HTMLKeyValueFragment( 325 data.child(ctx, "table")(ctx, data), 326 data.queryMeta) 327 else: 328 # a FormError, most likely 329 return ""
330
331 - def render_parpair(self, ctx, data):
332 if data is None or data[1] is None or "__" in data[0]: 333 return "" 334 return ctx.tag["%s: %s"%data]
335
336 - def render_ifresult(self, ctx, data):
337 if self.result.queryMeta.get("Matched", 1)!=0: 338 return ctx.tag 339 else: 340 return ""
341
342 - def render_ifnoresult(self, ctx, data):
343 if self.result.queryMeta.get("Matched", 1)==0: 344 return ctx.tag 345 else: 346 return ""
347
348 - def render_iflinkable(self, ctx, data):
349 """renders ctx.tag if we have a linkable result, nothing otherwise. 350 351 Linkable means that the result will come out as displayed through 352 a link. Currently, we only see if a file upload was part of 353 the result production -- if there was, it's not linkable. 354 355 This currently doesn't even look if a file was indeed passed in: Things 356 already are not linkable if the service takes a file upload, whether 357 that's used or not. 358 """ 359 for ik in self.service.getInputKeysFor(self): 360 if ik.type=='file': 361 return "" 362 return ctx.tag
363
364 - def render_servicestyle(self, ctx, data):
365 """enters custom service styles into ctx.tag. 366 367 They are taken from the service's customCSS property. 368 """ 369 if self.service and self.service.getProperty("customCSS", False): 370 return ctx.tag[self.service.getProperty("customCSS")] 371 return ""
372
373 - def data_result(self, ctx, data):
374 return self.result
375
376 - def _makeParPair(self, key, value, fieldDict):
377 title = key 378 if key in fieldDict: 379 title = fieldDict[key].getLabel() 380 if fieldDict[key].type=="file": 381 value = "File upload '%s'"%value[0] 382 else: 383 value = unicode(value) 384 return title, value
385 386 __suppressedParNames = set(["submit"]) 387
388 - def data_queryseq(self, ctx, data):
389 if not self.result: 390 return [] 391 392 if self.service: 393 fieldDict = dict((f.name, f) 394 for f in self.service.getInputKeysFor(self)) 395 else: 396 fieldDict = {} 397 398 s = [self._makeParPair(k, v, fieldDict) 399 for k, v in self.result.queryMeta.get("formal_data", {}).iteritems() 400 if v is not None and v!=[] 401 and k not in self.__suppressedParNames 402 and not k.startswith("_")] 403 s.sort() 404 return s
405
406 - def render_flotplot(self, ctx, data):
407 """adds an onClick attribute opening a flot plot. 408 409 This is evaluates the _plotOptions meta. This should be a javascript 410 dictionary literal with certain plot options. More on this in 411 the reference documentation on the _plotOptions meta. 412 """ 413 plotOptions = base.getMetaText(self.service, "_plotOptions") 414 if plotOptions is not None: 415 args = ", %s"%plotOptions 416 else: 417 args = "" 418 return ctx.tag(onclick="openFlotPlot($('table.results')%s)"%args)
419
420 - def render_param(self, format):
421 """returns the value of the data.getParam(content) formatted as a python 422 string. 423 424 Undefined params and NULLs give N/A. 425 """ 426 def renderer(ctx, data): 427 parName = ctx.tag.children[0].strip() 428 ctx.tag.clear() 429 try: 430 val = data.getParam(parName) 431 if val is None: 432 return ctx.tag["N/A"] 433 434 return ctx.tag[format%val] 435 except base.NotFoundError: 436 return ctx.tag["N/A"]
437 return renderer
438
439 440 -class CustomTemplateMixin(object):
441 """a mixin providing for customized templates. 442 443 This works by making docFactory a property first checking if 444 the instance has a customTemplate attribute evaluating to true. 445 If it has and it is referring to a string, its content is used 446 as a resdir-relative path to a nevow XML template. If it has and 447 it is not a string, it will be used as a template directly 448 (it's already "loaded"), else defaultDocFactory attribute of 449 the instance is used. 450 """ 451 customTemplate = None 452
453 - def getDocFactory(self):
454 if not self.customTemplate: 455 return self.defaultDocFactory 456 elif isinstance(self.customTemplate, basestring): 457 tplPath = self.rd.getAbsPath(self.customTemplate) 458 if not os.path.exists(tplPath): 459 return self.defaultDocFactory 460 return loaders.xmlfile(tplPath) 461 else: 462 return self.customTemplate
463 464 docFactory = property(getDocFactory)
465
466 467 468 ############# nevow Resource derivatives used here. 469 470 471 -class GavoPage(rend.Page, GavoRenderMixin):
472 """a base class for all "pages" (i.e. things talking to the web) within 473 DaCHS. 474 """
475
476 477 -class ResourceBasedPage(GavoPage):
478 """A base for renderers based on RDs. 479 480 It is constructed with the resource descriptor and leaves it 481 in the rd attribute. 482 483 The preferredMethod attribute is used for generation of registry records 484 and currently should be either GET or POST. urlUse should be one 485 of full, base, post, or dir, in accord with VOResource. 486 487 Renderers with fixed result types should fill out resultType. 488 489 The makeAccessURL class method is called by service.getURL; it 490 receives the service's base URL and must return a mogrified string 491 that corresponds to an endpoint this renderer will operate on (this 492 could be used to make a Form renderer into a ParamHTTP interface by 493 attaching ?__nevow_form__=genForm&, and the soap renderer does 494 nontrivial things there). 495 496 Within DaCHS, this class is mainly used as a base for ServiceBasedRenderer, 497 since almost always only services talk to the world. However, 498 we try to fudge render and data functions such that the sidebar works. 499 """ 500 implements(inevow.IResource) 501 502 preferredMethod = "GET" 503 urlUse = "full" 504 resultType = None 505 # parameterStyle is a hint for inputKeys how to transform themselves 506 # "clear" keeps types, "form" gives vizier-like expressions 507 # "vo" gives parameter-like expressions. 508 parameterStyle = "clear" 509 name = None 510
511 - def __init__(self, ctx, rd):
512 rend.Page.__init__(self) 513 self.rd = rd 514 self.metaCarrier = rd 515 self.macroPackage = rd 516 if hasattr(self.rd, "currently_blocked"): 517 raise RDBlocked() 518 self._initGavoRender()
519 520 @classmethod
521 - def isBrowseable(self, service):
522 """returns True if this renderer applied to service is usable using a 523 plain web browser. 524 """ 525 return False
526 527 @classmethod
528 - def isCacheable(self, segments, request):
529 """should return true if the content rendered will only change 530 when the associated RD changes. 531 532 request is a nevow request object. web.root.ArchiveService already 533 makes sure that you only see GET request without arguments and 534 without a user, so you do not need to check this. 535 """ 536 return False
537 538 @classmethod
539 - def makeAccessURL(cls, baseURL):
540 """returns an accessURL for a service with baseURL to this renderer. 541 """ 542 return "%s/%s"%(baseURL, cls.name)
543
544 - def data_rdId(self, ctx, data):
545 return self.rd.sourceId
546
547 - def data_serviceURL(self, type):
548 # for RD's that's simply the rdinfo. 549 return lambda ctx, data: base.makeSitePath("/browse/%s"%self.rd.sourceId)
550 551 552 _IGNORED_KEYS = set(["__nevow_form__", "_charset_", "submit", "nextURL"])
553 554 -def _formatRequestArgs(args):
555 r"""formats nevow request args for logging. 556 557 Basically, long objects (ones with len, and len>100) are truncated. 558 559 >>> _formatRequestArgs({"x": range(2), "y": [u"\u3020"], "submit": ["Ok"]}) 560 "{'x': [0,1,],'y': [u'\\u3020',],}" 561 >>> _formatRequestArgs({"hokus": ["Pokus"*300]}) 562 "{'hokus': [<data starting with 'PokusPokusPokusPokusPokusPokus'>,],}" 563 >>> _formatRequestArgs({"no": []}) 564 '{}' 565 """ 566 res = ["{"] 567 for key in sorted(args): 568 valList = args[key] 569 if not valList or key in _IGNORED_KEYS: 570 continue 571 res.append("%s: ["%repr(key)) 572 for value in valList: 573 try: 574 if len(value)>100: 575 res.append("<data starting with %s>,"%repr(value[:30])) 576 else: 577 res.append(repr(value)+",") 578 except TypeError: # no len on value 579 res.append(repr(value)+",") 580 res.append("],") 581 res.append("}") 582 return "".join(res)
583
584 585 -class ServiceBasedPage(ResourceBasedPage):
586 """the base class for renderers turning service-based info into 587 character streams. 588 589 You will need to provide some way to give rend.Page nevow templates, 590 either by supplying a docFactory or (usually preferably) mixing in 591 CustomTemplateMixin -- or just override renderHTTP to make do 592 without templates. 593 594 The class overrides nevow's child and render methods to allow the 595 service to define render_X and data_X methods, too. 596 597 You can set an attribute checkedRenderer=False for renderers that 598 are "generic" and do not need to be enumerated in the allowed 599 attribute of the underlying service ("meta renderers"). 600 601 You can set a class attribute openRenderer=True to make a renderer 602 work even on restricted services (which may make sense for stuff like 603 logout and maybe for metadata inspection). 604 """ 605 606 checkedRenderer = True 607 openRenderer = False 608
609 - def __init__(self, ctx, service):
610 ResourceBasedPage.__init__(self, ctx, service.rd) 611 612 self.service = service 613 request = inevow.IRequest(ctx) 614 if not self.openRenderer and service.limitTo: 615 if not creds.hasCredentials(request.getUser(), request.getPassword(), 616 service.limitTo): 617 raise svcs.Authenticate() 618 619 if self.checkedRenderer and self.name not in self.service.allowed: 620 raise svcs.ForbiddenURI( 621 "The renderer %s is not allowed on this service."%self.name, 622 rd=self.service.rd) 623 624 # the following attribute is checked for upstack by 625 # base.getCurrentServerURL 626 # NOTE: analogous code in svc.streaming. If you change the logic here, 627 # you'll have to change the logic there, too. 628 self.HANDLING_HTTPS = request.isSecure() 629 630 self.metaCarrier = self.service 631 self.macroPackage = self.service 632 633 # Set to true when we notice we need to fix the service's output fields 634 self.fieldsChanged = False 635 636 self._logRequestArgs(request) 637 self._fillServiceDefaults(request.args)
638
639 - def _logRequestArgs(self, request):
640 """leaves the actual arguments of a request in the log. 641 """ 642 try: 643 if request.args: 644 # even if there are args, don't log them if only boring ones 645 # were given 646 fmtArgs = _formatRequestArgs(request.args) 647 if fmtArgs!='{}': 648 log.msg("# Processing starts: %s %s"%(request.path, 649 fmtArgs)) 650 except: # don't fail because of logging problems 651 base.ui.notifyError("Formatting of request args failed.")
652
653 - def _fillServiceDefaults(self, args):
654 """a hook to enter default parameters based on the service. 655 """ 656 if self.service.core.hasProperty("defaultSortKey"): 657 if "_DBOPTIONS_ORDER" not in args: 658 args["_DBOPTIONS_ORDER"] = self.service.core.getProperty( 659 "defaultSortKey").split(",")
660
661 - def processData(self, rawData, queryMeta=None):
662 """calls the actual service. 663 664 This will run in the current thread; you will ususally 665 want to use runService from the main nevow event loop unless you know 666 the service is quick or actually works asynchronously. 667 """ 668 return self.service.run(self, rawData, queryMeta)
669
670 - def runService(self, rawData, queryMeta=None):
671 """takes raw data and returns a deferred firing the service result. 672 673 This will process everything in a thread. 674 """ 675 return threads.deferToThread(self.processData, rawData, queryMeta)
676
677 - def runServiceWithFormalData(self, rawData, context, queryMeta=None):
678 """runs the service, taking arguments from material preparsed 679 by nevow formal. 680 681 This is the entry point for the form renderer and its friends. 682 """ 683 if queryMeta is None: 684 queryMeta = svcs.QueryMeta.fromContext(context) 685 queryMeta["formal_data"] = rawData 686 687 # contextGrammar wants a dict of lists, whereas formal has direct 688 # values; accomodate to contextGrammar 689 690 if (self.service.core.outputTable.columns and 691 not self.service.getCurOutputFields(queryMeta)): 692 raise base.ValidationError("These output settings yield no" 693 " output fields", "_OUTPUT") 694 695 data = dict((k, [v] if v is not None else None) 696 for k,v in rawData.iteritems()) 697 return self.runService(svcs.PreparsedInput(data), queryMeta)
698
699 - def data_serviceURL(self, renderer):
700 """returns a relative URL for this service using the renderer. 701 702 This is ususally used like this: 703 704 <a><n:attr name="href" n:data="serviceURL info" n:render="data">x</a> 705 """ 706 def get(ctx, data): 707 return self.service.getURL(renderer, absolute="False")
708 return get
709
710 - def renderer(self, ctx, name):
711 """returns a nevow render function named name. 712 713 This overrides the method inherited from nevow's RenderFactory to 714 add a lookup in the page's service service. 715 """ 716 if name in self.service.nevowRenderers: 717 return self.service.nevowRenderers[name] 718 return rend.Page.renderer(self, ctx, name)
719
720 - def child(self, ctx, name):
721 """returns a nevow data function named name. 722 723 In addition to nevow's action, this also looks methods up in the 724 service. 725 """ 726 if name in self.service.nevowDataFunctions: 727 return self.service.nevowDataFunctions[name] 728 return rend.Page.child(self, ctx, name)
729
730 - def renderHTTP(self, ctx):
731 return rend.Page.renderHTTP(self, ctx)
732
733 - def locateChild(self, ctx, segments):
734 # By default, ServiceBasedPages have no directory-like resources. 735 # So, if some overzealous entity added a slash, just redirect. 736 # Do not upcall to this if you override locateChild. 737 if segments==("",): 738 raise svcs.WebRedirect(url.URL.fromContext(ctx)) 739 else: 740 res = ResourceBasedPage.locateChild(self, ctx, segments) 741 if res[0] is None: 742 raise svcs.UnknownURI("%s has no child resources"%repr(self.name)) 743 return res
744 745 746 if __name__=="__main__": 747 import doctest, grend 748 doctest.testmod(grend) 749