Source code for gavo.web.metarender

# -*- coding: utf-8 -*-
Renderers that take services "as arguments".

#c Copyright 2008-2022, the GAVO project <>
#c This program is free software, covered by the GNU GPL.  See the
#c COPYING file in the source distribution.

import re
import urllib.request, urllib.parse, urllib.error

from twisted.web import resource
from twisted.web import template
from twisted.web.template import tags as T

from gavo import base
from gavo import formal
from gavo import registry
from gavo import svcs
from gavo import utils
from gavo.web import common
from gavo.web import grend

def _protectForBibTeX(tx):
	"""returns tx in a way that hopefully prevents larger disasters
	when used with BibTeX.

	Among others, this just looks for multiple uppercase characters within
	one word and protects the respective word with curly braces; for now,
	this is ASCII only.

	This is also where we escape for TeX.
	return re.sub(r"([#$_&\\%])", r"\\\1",
		re.sub(r"(\w*[A-Z]\w+[A-Z]\w*)", r"{\1}", tx))

[docs]def makeBibTeXForMetaCarrier(mc): """returns a (unicode) best-effort BibTeX record for something mixing in mc. This needs, at least, a creationDate, a creator, a referenceURL, and a title to work; if these aren't there, you'll get a MetaError. """ creationDate = utils.parseISODT( base.getMetaText(mc, "creationDate", raiseOnFail=True)) year = creationDate.year # for the BibTeX tag, prefer the short name if given... label = base.getMetaText(mc, "shortName", None) if label is None: # ... else hope it's a table... try: label = mc.getQName() except AttributeError: # fall back to something made from the creation date # (which at least won't change between invocations) label = utils.intToFunnyWord( int(re.sub("[^\d]+", "", creationDate.isoformat()))) label = "vo:"+re.sub("[^\w]", "_", label) authors = " and ".join(m.getContent() for m in mc.iterMeta("", propagate=True)) res = [ "@MISC{%s"%label, " year=%s"%year, " title={%s}"%_protectForBibTeX( base.getMetaText(mc, "title", "Untitled Resource")), " author={%s}"%_protectForBibTeX(authors), " url={%s}"%base.getMetaText(mc, "referenceURL", raiseOnFail=True), " howpublished={{VO} resource provided by the %s}"% _protectForBibTeX(base.getConfig("web", "sitename")), ] doi = base.getMetaText(mc, "doi", None) if doi: res.append( " doi = {%s}"%doi) return ",\n".join(res)+"\n}\n"
[docs]class MetaRenderer(grend.CustomTemplateMixin, grend.ServiceBasedPage): """Renderers that are allowed on all services. """ checkedRenderer = False
[docs] @classmethod def isCacheable(self, segments, request): return True
[docs] def data_otherServices(self, request, tag): """returns a list of dicts describing other services provided by the the describing RD. The class mixing this in needs to provide a describingRD attribute for this to work. This may be the same as self.service.rd, and the current service will be excluded from the list in this case. """ res = [] for svc in if svc is not self.service: res.append({"infoURL": svc.getURL("info"), "title": base.getMetaText(svc, "title")}) return res
[docs] @template.renderer def sortOrder(self, request, tag): if "alphaOrder" in request.strargs: return tag["Sorted alphabetically. ", T.a(href=request.path)[ "[Sort by DB column index]"]] else: return tag["Sorted by DB column index. ", T.a(href=request.path+b"?alphaOrder=True")[ "[Sort alphabetically]"]]
[docs] @template.renderer def ifkey(self, keyName): def render(request, tag): if keyName in tag.slotData: return tag return "" return render
[docs] @template.renderer def ifbibcode(self, request, tag): """renders its children if the source metadata looks like a bibcode. """ source = base.getMetaText(self.metaCarrier, "source", "") if utils.couldBeABibcode(source): return tag else: return ""
[docs] def data_bibtexentry(self, request, tag): """returns BibTeX for the current record. This will return None if no BibTeX can be made. """ try: return makeBibTeXForMetaCarrier(self.metaCarrier) except base.MetaError: return None
[docs]class RendExplainer(object): """is a container for various functions having to do with explaining renderers on services. Use the explain(renderer, service) class method. """ @classmethod def _explain_form(cls, service): return T.transparent["an interface for web browsers through an ", T.a(href=service.getURL("form"))["HTML form"]] @classmethod def _explain_fixed(cls, service): return T.transparent["a ", T.a(href=service.getURL("fixed"))["custom page"], ", possibly with dynamic content"] @classmethod def _explain_volatile(cls, service): return T.transparent["a ", T.a(href=service.getURL("volatile"))["custom page"], ", possibly with dynamic content"] @classmethod def _explain_soap(cls, service): def generateArguments(): # Slightly obfuscated -- I need to get ", " in between the items. fieldIter = iter(service.getInputKeysFor("soap")) try: nextItem = next(fieldIter) while True: desc = "%s/%s"%(, nextItem.type) if nextItem.required: desc = T.strong[desc] yield desc nextItem = next(fieldIter) yield ', ' except StopIteration: pass return T.transparent["enables remote procedure calls through the" " slightly aged SOAP mechanism; to use it," " feed the WSDL URL "+ service.getURL("soap")+"/go?wsdl"+ " to your SOAP library; the function signature is" " useService(", generateArguments(), "). See also our ", T.a(render="rootlink", href="/static/doc/soaplocal.shtml")[ "local soap hints"]] @classmethod def _explain_custom(cls, service): res = T.transparent["a custom rendering of the service, typically" " for interactive web applications."] if svcs.getRenderer("custom").isBrowseable(service): res[" See also the ", T.a(href=service.getURL("custom"))["entry page"], "."] return res @classmethod def _explain_static(cls, service): return T.transparent["static (i.e. prepared) data or custom client-side" " code; probably used to access ancillary files here"] @classmethod def _explain_text(cls, service): return T.transparent["a text interface not intended for user" " applications"] @classmethod def _explain_siap_xml(cls, service): return T.transparent["a standard SIAP interface as defined by the" " IVOA to access collections of celestial images; SIAP clients" " use ", service.getURL("siap.xml"), " to access the service", T.transparent(render="ifadmin")[" – ", T.a(href="" "spec=Simple+Image+Access+1.0" "&format=XHTML&batch=yes" +"&serviceURL=%s"%urllib.parse.quote(service.getURL("siap.xml")) +"&POS=%s%%2C%s&SIZE=%s%%2C%s&FORMAT=ALL"%( base.getMetaText(service, "testQuery.pos.ra", default="180"), base.getMetaText(service, "testQuery.pos.dec", default="60"), base.getMetaText(service, "testQuery.size.ra", default="3"), base.getMetaText(service, "testQuery.size.dec", default="3")))[ "Validate"]]] @classmethod def _explain_scs_xml(cls, service): return T.transparent["a standard SCS interface as defined by the" " IVOA to access catalog-type data; SCS clients" " use ", service.getURL("scs.xml"), " to access the service", T.transparent(render="ifadmin")[" – ", T.a(href="" "spec=Simple+Cone+Search+1.03" "&format=XHTML&batch=yes" +"&serviceURL=%s"%urllib.parse.quote(service.getURL("scs.xml")) +"&RA=%s&DEC=%s&SR=%s&VERB=3"%( base.getMetaText(service, "testQuery.ra", default="180"), base.getMetaText(service, "testQuery.dec", default="60"), base.getMetaText(service, "", default="1")))[ "Validate"]]] @classmethod def _explain_ssap_xml(cls, service): return T.transparent["a standard SSAP interface as defined by the" " IVOA to access spectral or time-series data; SSAP clients" " use ", service.getURL("ssap.xml"), " to access the service", T.transparent(render="ifadmin")[" – ", T.a(href="" "REQUEST=queryData&POS=&SIZE=&TIME=&BAND=&FORMAT=ALL" "&VERSION=1.1&APERTURE=&SPECRP=&SPATRES=&TIMERES=&SNR=" "&REDSHIFT=&VARAMPL=&TARGETNAME=&TARGETCLASS=&FLUXCALIB=any" "&WAVECALIB=any&PUBDID=&CREATORDID=&COLLECTION=&TOP=&MAXREC=1" "&MTIME=&COMPRESS=true&RUNID=&spec=Simple+Spectral+Access+1.1" "&format=XHTML" +"&serviceURL=%s"%urllib.parse.quote(service.getURL("ssap.xml")))[ "Validate"]]] @classmethod def _explain_slap_xml(cls, service): return T.transparent["a standard SLAP interface as defined by the" " IVOA to access spectral line data; SLAP clients (usually" " spectral analysis programs)" " use ", service.getURL("slap.xml"), " to access the service", T.transparent(render="ifadmin")[" – ", T.a(href="" "REQUEST=queryData&spec=Simple+Line+Access+1.0" "&format=XHTML" "&addparams=MAXREC%3D1%26" +"&serviceURL=%s"%urllib.parse.quote(service.getURL("slap.xml")))[ "Validate"]]] @classmethod def _explain_dali(cls, service): return T.transparent["a complex interface consisting of multiple" " endpoints (sync, async, perhaps tables or examples, and possibly" " more). This is used, in particular, for TAP. You will usually need" " a client programme to use this." " In such clients, you can find this service by its" " IVOID, ", T.code(render="meta")["identifier"], ", or access it by entering its base URL ", T.code[service.getURL("dali")], " directly. Using an XSL-enabled web browser you can, in a pinch," " also operate ", T.a(href=service.getURL("async"))["the service"], " asynchronously without a specialized client."] @classmethod def _explain_uws_xml(cls, service): return T.transparent["a user-defined UWS service." " This service is best accessed using specialized clients" " or libraries. Give those its base URL ", T.a(href=service.getURL("uws.xml"))[service.getURL("uws.xml")], ". Using an XSL-enabled web browser you can" " also click on the link above and operate the service 'manually'." " For parameters and the output schema, see below."] @classmethod def _explain_pubreg_xml(cls, service): return T.transparent["an interface for the OAI-PMH protocol, typically" " this site's publishing registry (but possibly some other" " registry-like thing). This endpoint is usually accessed" " by harvesters, but with an XML-enabled browser you can" " also try the access URL at ", T.a(href=service.getURL("pubreg.xml"))[service.getURL("pubreg.xml")], "."] @classmethod def _explain_qp(cls, service): return T.transparent["an interface that uses the last path element" " to query the column %s in the underlying table."% service.getProperty("queryField", "defunct")] @classmethod def _explain_upload(cls, service): return T.transparent["a ", T.a(href=service.getURL("upload"))["form-based interface"], " for uploading data"] @classmethod def _explain_dlget(cls, service): return T.transparent["a datalink interface letting specialized clients" " retrieve parts of datasets or discover related data. You" " use this kind of service exclusively in combination with" " a pubdid, usually via a direct link."] @classmethod def _explain_dlmeta(cls, service): return T.transparent["a datalink interface for discovering access" " options (processed data, related datasets...) for a dataset." " You usually need a publisherDID to use this kind of service." " For special applications, the base URL of this service might" " still come handy: %s"%service.getURL("dlmeta")] @classmethod def _explain_dlasync(cls, service): return T.transparent["an asynchronous interface to retrieving" " processed data. This needs a special client that presumably" " would first look at the dlmeta endpoint to discover what" " processing options are available."] @classmethod def _explain_api(cls, service): return T.transparent["an interface for operation with curl and" " similar low-level tools. The endpoint is at ", T.a(href=service.getURL("api"))[service.getURL("api")], "; as usual for DALI-conforming services, parameters" " an response structure is available by ", T.a(href=service.getURL("api")+"MAXREC=0")["querying with" " MAXREC=0"], "."] @classmethod def _explain_coverage(cls, service): return T.transparent["an interface to retrieve the spatial coverage" " of this service. By default, this will return a FITS MOC," " but browsers and similar clients declaring they accept PNGs" " will get a sky plot showing the coverage."] @classmethod def _explainEverything(cls, service): return T.transparent["a renderer with some custom access method that" " should be mentioned in the service description"]
[docs] @classmethod def explain(cls, renderer, service): return getattr(cls, "_explain_"+renderer.replace(".", "_"), cls._explainEverything)(service)
[docs]class ServiceInfoRenderer(MetaRenderer, utils.IdManagerMixin): """A renderer showing all kinds of metadata on a service. This renderer produces the default referenceURL page. To change its appearance, override the serviceinfo.html template. """ name = "info" customTemplate = svcs.loadSystemTemplate("serviceinfo.html") def __init__(self, *args, **kwargs): grend.ServiceBasedPage.__init__(self, *args, **kwargs) self.describingRD = self.service.rd self.rawFootnotes = set()
[docs] @template.renderer def title(self, request, tag): return tag["Information on Service '%s'"% base.getMetaText(self.service, "title")]
[docs] @template.renderer def notebubble(self, request, tag): if not tag.slotData["note"]: return "" id = tag.slotData["note"].tag self.rawFootnotes.add(tag.slotData["note"]) return tag(href="#note-%s"%id)["Note %s"%id]
[docs] @template.renderer def footnotes(self, request, tag): """renders the footnotes as a definition list. """ return T.dl(class_="footnotes")[[ T.xml(note.getContent(targetFormat="html")) for note in sorted(self.rawFootnotes, key=lambda n: n.tag)]]
[docs] def data_internalpath(self, request, tag): return "%s/%s"%(self.service.rd.sourceId,
[docs] def data_inputFields(self, request, tag): res = [f.asInfoDict() for f in self.service.getInputKeysFor("info")] res.sort(key=lambda val: val["name"].lower()) return res
[docs] def data_htmlOutputFields(self, request, tag): res = [f.asInfoDict() for f in self.service.getCurOutputFields()] res.sort(key=lambda val: val["name"].lower()) return res
[docs] def data_votableOutputFields(self, request, tag): queryMeta = svcs.QueryMeta({"_FORMAT": "VOTable", "_VERB": 3}) res = [f.asInfoDict() for f in self.service.getCurOutputFields(queryMeta)] res.sort(key=lambda val: val["verbLevel"]) return res
[docs] def data_rendAvail(self, request, tag): return [{"rendName": rend, "rendExpl": RendExplainer.explain(rend, self.service)} for rend in self.service.allowed]
[docs] def data_publications(self, request, tag): res = [{"sets": ",".join(p.sets), "render": p.render} for p in self.service.publications if p.sets and p.render not in registry.HIDDEN_RENDERERS] return sorted(res, key=lambda v: v["render"])
[docs] def data_browserURL(self, request, tag): return self.service.getBrowserURL()
[docs] def data_service(self, request, tag): return self.service
defaultLoader = common.doctypedStan( T.html[ T.head[ T.title["Missing Template"]], T.body[ T.p["Infos are only available with a serviceinfo.html template"]] ])
[docs]class TableInfoRenderer(MetaRenderer): """A renderer for displaying table information. Since tables don't necessarily have associated services, this renderer cannot use a service to sit on. Instead, the table is being passed in as as an argument. There's a built-in vanity tableinfo that sits on //dc_tables#show using this renderer (it could really sit anywhere else). """ name = "tableinfo" customTemplate = svcs.loadSystemTemplate("tableinfo.html")
[docs] def render(self, request): if not hasattr(self, "table"): # _retrieveTableDef did not run, i.e., no tableName was given raise svcs.UnknownURI( "You must provide a table name to this renderer.") self.macroPackage = self.table self.metaCarrier = self.table return super(TableInfoRenderer, self).render(request)
def _retrieveTableDef(self, tableName): try: self.tableName = tableName self.table = registry.getTableDef(tableName) self.describingRD = self.table.rd except base.NotFoundError as msg: raise base.ui.logOldExc(svcs.UnknownURI(str(msg)))
[docs] def data_forADQL(self, request, tag): return self.table.adql
[docs] def data_fields(self, request, tag): res = [f.asInfoDict() for f in self.table] for d in res: if d["note"]: d["noteKey"] = d["note"].tag if "alphaOrder" in request.strargs: res.sort(key=lambda item: item["name"].lower()) return res
[docs] def data_internalpath(self, request, tag): return "%s/%s"%(self.table.rd.sourceId,
[docs] @template.renderer def title(self, request, tag): return tag["Table information for '%s'"%self.tableName]
[docs] @template.renderer def rdmeta(self, request, tag): # rdmeta: Meta info at the table's rd (since there's ownmeta) metaKey = tag.children[0] tag.clear() htmlBuilder = common.HTMLMetaBuilder(self.describingRD) try: return tag[self.describingRD.buildRepr(metaKey, htmlBuilder)] except base.NoMetaKey: return ""
[docs] @template.renderer def ifrdmeta(self, metaName): if self.describingRD.getMeta(metaName, propagate=False): return lambda request, tag: tag else: return lambda request, tag: ""
[docs] @template.renderer def iftapinfo(self, request, tag): """renders the content if there was a tapinfo key somewhere in the query string. """ if "tapinfo" in request.strargs: return tag else: return ""
[docs] def data_tableDef(self, request, tag): return self.table
[docs] def getChild(self, name, request): self._retrieveTableDef(name.decode("utf-8")) return self
defaultLoader = common.doctypedStan( T.html[ T.head[ T.title["Missing Template"]], T.body[ T.p["Infos are only available with a tableinfo.html template"]] ])
[docs]class TableNoteRenderer(MetaRenderer): """A renderer for displaying table notes. It takes a schema-qualified table name and a note tag in the segments. This does not use the underlying service, so it could and will run on any service. However, you really should run it on __system__/dc_tables/show, and there's a built-in vanity name tablenote for this. """ name = "tablenote"
[docs] def render(self, request): if not hasattr(self, "noteTag"): # _retrieveTableDef did not run, i.e., no tableName was given raise svcs.UnknownURI( "You must provide table name and note tag to this renderer.") return super(TableNoteRenderer, self).render(request)
def _retrieveNote(self, tableName, noteTag): try: table = registry.getTableDef(tableName) self.metaCarrier = table self.renderedNote = table.getNote(noteTag ).getContent(targetFormat="html", macroPackage=table) except base.NotFoundError as msg: raise base.ui.logOldExc(svcs.UnknownURI(msg)) self.noteTag = noteTag self.tableName = tableName
[docs] def getChild(self, name, request): segments = request.popSegments(name) if len(segments)==2: self._retrieveNote(segments[0], segments[1]) elif len(segments)==3: # segments[0] may be anything, # but conventionally "inner" self._retrieveNote(segments[1], segments[2]) self.loader = self.innerLoader else: raise svcs.UnknownURI("No such table note") return self
[docs] def data_tableName(self, request, tag): return self.tableName
[docs] def data_noteTag(self, request, tag): return self.noteTag
[docs] @template.renderer def noteHTML(self, request, tag): return T.xml(self.renderedNote)
loader = formal.XMLString(""" <html xmlns="xmlns=" xmlns:n=""> <head> <title>\getConfig{web}{sitename} – Note for table <n:invisible n:render="unicode" n:data="tableName"/></title> <n:invisible n:render="commonhead"/> <style> {font-size: 180%;font-weight:bold} </style> </head> <body> <n:invisible n:render="noteHTML"/> </body> </html>""") innerLoader = template.TagLoader( T.transparent(render="noteHTML"))
[docs]class HowToCiteRenderer(MetaRenderer): """A renderer that lets you format citation instructions. """ name = "howtocite" customTemplate = svcs.loadSystemTemplate("howtocite.html")
[docs]class CoverageRenderer(MetaRenderer): """A renderer returning various forms of a service's spatial coverage. This will return a 404 if the service doesn't have a coverage.spatial meta (and will bomb out if that isn't a SMoc). Based on the accept header, it will return a PNG if the client indicates it's interested in that or if it accepts text/html, in which case we assume it's a browser; otherwise, it will produce a MOC in FITS format. """ name = "coverage"
[docs] def render(self, request): from gavo.utils import pgsphere try: moc = pgsphere.SMoc.fromASCII( self.service.getMeta( "coverage.spatial", raiseOnFail=True).getContent()) except base.NoMetaKey: raise svcs.UnknownURI( "No spatial coverage available for this service.") if self.returnAPNG(request): request.setHeader("content-type", "image/png") return moc.getPlot(xsize=400) else: request.setHeader("content-type", "application/fits") return moc.asFITS()
[docs] @classmethod def returnAPNG(self, request): """returns true if request indicates we're being retrieved by a web browser. """ acceptDict = utils.parseAccept(request.getHeader("accept")) return ("image/png" in acceptDict or "image/webp" in acceptDict # firefox ~78 hack or "image/*" in acceptDict or "text/html" in acceptDict)
[docs] @classmethod def isCacheable(cls, segments, request): """only cache this if we return a PNG. Our caching system doesn't support content negotiation, so we can't keep the FITS (and it's fast to generate, so that doesn't matter so much. """ return cls.returnAPNG(request)
[docs]class EditionRenderer(MetaRenderer): """A renderer representing a (tutorial-like) text document. Not sure yet what I'll do when people actually retrieve these. This must have a meta accessURL with the document URI. It may have a sourceURL meta giving the VCS URI. """ name = "edition"
[docs]class ExternalRenderer(grend.ServiceBasedPage): """A renderer redirecting to an external resource. These try to access an external publication on the parent service and ask it for an accessURL. If it doesn't define one, this will lead to a redirect loop. In the DC, external renderers are mainly used for registration of third-party browser-based services. """ name = "external"
[docs] @classmethod def isBrowseable(self, service): return True # we probably need some way to say when that's wrong...
[docs] def render(self, request): # look for a matching publication in the parent service... for pub in self.service.publications: if break else: # no publication, 404 raise svcs.UnknownURI("No publication for an external service here.") raise svcs.WebRedirect(base.getMetaText(pub, "accessURL", macroPackage=self.service))
[docs]class RDInfoPage(grend.CustomTemplateMixin, grend.ResourceBasedPage): """A page giving infos about an RD. This is not a renderer but a helper for RDInfoRenderer. """ customTemplate = svcs.loadSystemTemplate("rdinfo.html")
[docs] def data_services(self, request, tag): return sorted(, key=lambda s: base.getMetaText(s, "title",
[docs] def data_tables(self, request, tag): return sorted((t for t in self.rd.tables if t.onDisk and not t.temporary and not t.hasProperty("internal")), key=lambda t:
[docs] def data_clientRdId(self, request, tag): return self.rd.sourceId
def _getDescriptionHTML(self, descItem): """returns stan for the "description" of a service or a table. The RD's description is not picked up. """ iDesc = descItem.getMeta("description", propagate=False) if iDesc is None: return "" else: return T.div(class_="lidescription")[ T.xml(iDesc.getContent("blockhtml", macroPackage=descItem))]
[docs] @template.renderer def rdsvc(self, request, tag): service = tag.slotData return tag[ T.a(href=service.getURL("info"))[ base.getMetaText(service, "title",], self._getDescriptionHTML(service)]
[docs] @template.renderer def rdtable(self, request, tag): tableDef = tag.slotData qName = tableDef.getQName() adqlNote = "" if tableDef.adql: adqlNote = T.span(class_="adqlnote")[" ", "–", " queryable through ", T.a(href="/tap")["TAP"], " and ", T.a(href="/adql")["ADQL"], " "] return tag[ T.a(href="/tableinfo/%s"%qName)[qName], adqlNote, self._getDescriptionHTML(tableDef)]
[docs] @classmethod def makePageTitle(cls, rd): """returns a suitable title for the rd info page. This is a class method to allow other renderers to generate titles for link anchors. """ return "Information on resource '%s'"%base.getMetaText( rd, "title", default="%s"%rd.sourceId)
[docs] @template.renderer def title(self, request, tag): return tag[self.makePageTitle(self.rd)]
defaultLoader = common.doctypedStan( T.html[ T.head[ T.title["Missing Template"]], T.body[ T.p["RD infos are only available with an rdinfo.html template"]] ])
[docs]class RDInfoRenderer(grend.CustomTemplateMixin, grend.ServiceBasedPage): """A renderer for displaying various properties about a resource descriptor. This renderer could really be attached to any service since it does not call it, but it usually lives on //services/overview. By virtue of builtin vanity, you can reach the rdinfo renderer at /browse, and thus you can access /browse/foo/q to view the RD infos. This is the form used by table registrations. In addition to all services, this renderer also links tableinfos for all non-temporary, on-disk tables defined in the RD. When you actually want to hide some internal on-disk tables, you can set a property ``internal`` on the table (the value is ignored). """ name = "rdinfo" customTemplate = svcs.loadSystemTemplate("rdlist.html")
[docs] def data_publishedRDs(self, request, tag): with base.getTableConn() as conn: return [row[0] for row in conn.query( """SELECT DISTINCT sourceRD FROM ( SELECT sourceRD FROM dc.resources WHERE NOT deleted) as q ORDER BY sourceRD""")]
[docs] def getChild(self, name, request): rdId = "/".join(request.popSegments(name)) if not rdId: raise svcs.WebRedirect("browse") clientRD = base.caches.getRD(rdId) return RDInfoPage(request, clientRD)
defaultLoader = common.doctypedStan( T.html[ T.head[ T.title["Missing Template"]], T.body[ T.p["The RD list is only available with an rdlist.html template"]] ])
[docs]class ResourceRecordMaker(resource.Resource): """A page that returns resource records for internal services. This is basically like OAI-PMH getRecord, except we're using rd/id/svcid from our path. Also (and that's fairly important for purx), this will use the _metadataUpdated meta for a modified header. """
[docs] def render(self, request): raise svcs.UnknownURI("What resource record do you want?")
[docs] def getChild(self, name, request): from gavo.registry import builders segments = request.popSegments(name) rdParts, svcId = segments[:-1], segments[-1] rdId = "/".join(rdParts) try: rd = base.caches.getRD(rdId) resob = rd.getById(svcId) except base.NotFoundError: raise svcs.UnknownURI("The resource %s#%s is unknown at this site."%( rdId, svcId)) timestampUpdated = utils.parseISODT( resob.getMeta("_metadataUpdated").getContent("text")).timestamp() return common.TypedData( utils.xmlrender( builders.getVORMetadataElement(resob), prolog="<?xml version='1.0'?>" "<?xml-stylesheet href='/static/xsl/oai.xsl' type='text/xsl'?>", ), "application/xml", timestampUpdated)