"""
A renderer for Data to HTML/stan
"""
#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.
import itertools
import os
import re
import urllib.parse
from twisted.web import template
from twisted.web.template import tags as T
from gavo import base
from gavo import formal
from gavo import formats
from gavo import rsc
from gavo import svcs
from gavo import utils
from gavo.base import valuemappers
from gavo.formal import nevowc
from gavo.formats import texttable
from gavo.protocols import products
from gavo.rscdef import rmkfuncs
from gavo.utils import serializers
from gavo.utils import typeconversions
from gavo.web import common
_htmlMFRegistry = texttable.displayMFRegistry.clone()
_registerHTMLMF = _htmlMFRegistry.registerFactory
NT = nevowc.addNevowAttributes
def _barMapperFactory(colDesc):
if colDesc["displayHint"].get("type")!="bar":
return
def coder(val):
if val:
return T.hr(style="width: %dpx"%int(val), title="%.2f"%val,
class_="scoreBar")
return ""
return coder
_registerHTMLMF(_barMapperFactory)
def _productMapperFactory(colDesc):
if colDesc["displayHint"].get("type")!="product":
return
if colDesc["displayHint"].get("nopreview"):
mouseoverHandler = None
else:
mouseoverHandler = "insertPreview(this, null)"
fixedArgs = ""
baseURL = base.getCurrentServerURL()
def coder(val):
if val:
anchor = re.sub(r"\?.*", "",
os.path.basename(urllib.parse.unquote_plus(str(val)[4:])))
if not anchor:
anchor = "File"
if isinstance(val, str) and utils.looksLikeURLPat.match(val):
# readily inserted URLs probably can't do previews, and
# we must not direct them through the product table anyway.
return T.a(href=val)[anchor]
else:
return T.a(
href=products.makeProductLink(val, useHost=baseURL)+fixedArgs,
onmouseover=mouseoverHandler,
class_="productlink")[anchor]
else:
return ""
return coder
_registerHTMLMF(_productMapperFactory)
def _simbadMapperFactory(colDesc):
"""is a mapper yielding links to simbad.
To make this work, you need to furnish the OutputField with a
select="array[alphaFloat, deltaFloat]" or similar.
You can give a coneMins displayHint to specify the search radius in
minutes.
"""
if colDesc["displayHint"].get("type")!="simbadlink":
return
radius = float(colDesc["displayHint"].get("coneMins", "1"))
def coder(data):
if data is None:
return ""
alpha, delta = data[0], data[1]
if alpha and delta:
return T.a(href="http://simbad.u-strasbg.fr/simbad/sim-coo?Coord=%s"
"&Radius=%f"%(urllib.parse.quote("%.5fd%+.5fd"%(alpha, delta)),
radius))["[Simbad]"]
return coder
_registerHTMLMF(_simbadMapperFactory)
def _bibcodeMapperFactory(colDesc):
if colDesc["displayHint"].get("type")!="bibcode":
return
def coder(data):
if data:
for item in data.split(","):
urlBibcode = urllib.parse.quote(item.strip())
adsLink = f"https://ui.adsabs.harvard.edu/abs/{urlBibcode}/abstract"
yield T.a(href=adsLink)[item.strip()]
yield " "
else:
yield ""
return coder
_registerHTMLMF(_bibcodeMapperFactory)
def _keepHTMLMapperFactory(colDesc):
if colDesc["displayHint"].get("type")!="keephtml":
return
def coder(data):
if data:
return T.xml(data)
return ""
return coder
_registerHTMLMF(_keepHTMLMapperFactory)
def _imageURLMapperFactory(colDesc):
if colDesc["displayHint"].get("type")!="imageURL":
return
width = colDesc["displayHint"].get("width")
def coder(data):
if data:
res = T.img(src=data, alt="Image at %s"%data)
if width:
res(width=width)
return res
return ""
return coder
_registerHTMLMF(_imageURLMapperFactory)
def _urlMapperFactory(colDesc):
if colDesc["displayHint"].get("type")!="url":
return
anchorText = colDesc.original.getProperty("anchorText", None)
if anchorText:
def makeAnchor(data):
return anchorText
else:
def makeAnchor(data): #noflake: conditional definition
return urllib.parse.unquote(
urllib.parse.urlparse(data)[2].split("/")[-1])
def coder(data):
if data:
return T.a(href=data)[makeAnchor(data)]
return ""
return coder
_registerHTMLMF(_urlMapperFactory)
def _booleanCheckmarkFactory(colDesc):
"""inserts mappers for values with displayHint type=checkmark.
These render a check mark if the value is python-true, else nothing.
"""
if colDesc["displayHint"].get("type")!="checkmark":
return
def coder(data):
if data:
return "\u2713"
return ""
return coder
_registerHTMLMF(_booleanCheckmarkFactory)
def _pgSphereMapperFactory(colDesc):
"""do a reasonable representation of arrays in HTML:
"""
if not colDesc["dbtype"] in serializers.GEOMETRY_TYPES:
return
def mapper(val):
if val is None:
return None
return T.span(class_="array")["[%s]"%" ".join(
"%s"%v for v in val.asDALI())]
colDesc["datatype"], colDesc["arraysize"], colDesc["xtype"
] = typeconversions.sqltypeToVOTable(colDesc["dbtype"])
return mapper
_registerHTMLMF(_pgSphereMapperFactory)
# Insert new, more specific factories here
[docs]class HeadCellsMixin(nevowc.CommonRenderers):
"""A mixin providing renders for table headings.
The class mixing in must give the SerManager used in a serManager
attribute.
"""
[docs] def data_fielddefs(self, request, tag):
return self.serManager.table.tableDef.columns
[docs] @template.renderer
def headCell(self, request, tag):
colDef = tag.slotData
# work with OutputFields, too
if hasattr(colDef, "key"):
colDef = self.serManager.getColumnByName(colDef.key)
cont = colDef.original.getLabel()
desc = colDef["description"]
if not desc:
desc = cont
tag = tag(title=desc)[T.xml(cont)]
if colDef["unit"]:
tag[T.br, "[%s]"%colDef["unit"]]
note = colDef["note"]
if note:
noteURL = "#note-%s"%note.tag
tag[T.sup[T.a(href=noteURL)[note.tag]]]
return tag
[docs]class HeadCells(template.Element, HeadCellsMixin):
def __init__(self, serManager):
self.serManager = serManager
loader = nevowc.XMLString("""
<tr xmlns:n="http://nevow.com/ns/nevow/0.1"
n:render="sequence" n:data="fielddefs">
<th n:pattern="item" n:render="headCell" class="thVertical"/>
</tr>""")
_htmlMetaBuilder = common.HTMLMetaBuilder()
def _compileRenderer(source, queryMeta, rd):
"""returns a function object from source.
Source must be the function body of a renderer. The variable data
contains the entire row, and the thing must return a string or at
least stan (it can use T.tag).
"""
code = ("def format(data):\n"+
utils.fixIndentation(source, " ")+"\n")
return rmkfuncs.makeProc("format", code, "", None,
queryMeta=queryMeta, source=source, T=T, rd=rd)
[docs]class HTMLDataRenderer(formal.NevowcElement):
"""A base class for rendering tables and table lines.
Both HTMLTableFragment (for complete tables) and HTMLKeyValueFragment
(for single rows) inherit from this.
"""
def __init__(self, table, queryMeta):
self.table, self.queryMeta = table, queryMeta
super(HTMLDataRenderer, self).__init__()
self._computeSerializationRules()
self._makeSerializer()
def _computeSerializationRules(self):
"""creates the serialization manager and the formatter sequence.
These are in the attributes serManager and formatterSeq, respectively.
formatterSeq consists of triples of (name, formatter, fullRow), where
fullRow is true if the formatter wants to be passed the full row rather
than just the column value.
"""
self.serManager = valuemappers.SerManager(self.table, withRanges=False,
mfRegistry=_htmlMFRegistry, acquireSamples=False)
self.formatterSeq = []
for index, (desc, field) in enumerate(
zip(self.serManager, self.table.tableDef)):
formatter = self.serManager.mappers[index]
if isinstance(field, svcs.OutputField):
if field.wantsRow:
desc["wantsRow"] = True
if field.formatter:
formatter = _compileRenderer(
field.formatter,
self.queryMeta,
self.table.tableDef.rd)
self.formatterSeq.append(
(desc["name"], formatter, desc.get("wantsRow", False)))
def _makeSerializer(self):
"""adds a serialiseRow attribute containing a function that turns
a table row into a sequence embeddable into stan trees.
"""
source = [
"def serializeRow(row):",
" res = []",]
for index, (name, _, wantsRow) in enumerate(self.formatterSeq):
if wantsRow:
source.append(" val = formatters[%d](row)"%index)
else:
source.append(" val = formatters[%d](row[%s])"%(index, repr(name)))
source.append(
" res.append('N/A' if val is None else val)")
source.append(" return res")
self.serializeRow = utils.compileFunction(
"\n".join(source), "serializeRow", {
"formatters": [p[1] for p in self.formatterSeq]})
[docs] def data_fielddefs(self, request, tag):
return self.table.tableDef.columns
[docs]class HTMLTableFragment(HTMLDataRenderer):
"""A nevow renderer for result tables.
"""
rowsPerDivision = 25
def __init__(self, table, queryMeta):
HTMLDataRenderer.__init__(self, table, queryMeta)
self._computeHeadCellsStan()
def _computeHeadCellsStan(self):
rendered = HeadCells(self.serManager)
# We're caching the computed head cells in hopes that things are
# a bit faster.
self.headCellsStan = T.xml(nevowc.flattenSync(rendered))
[docs] @template.renderer
def headCells(self, request, tag):
"""returns the header line for this table as an XML string.
"""
return tag[self.headCellsStan]
def _formatRow(self, row, rowAttrs):
"""returns row HTML-rendered.
"""
res = ['<tr%s>'%rowAttrs]
for val in self.serializeRow(row):
if isinstance(val, (str, bytes)):
serFct = common.escapeForHTML
else:
serFct = nevowc.flattenSyncToString
res.append('<td>%s</td>'%serFct(val))
res.append('</tr>')
return ''.join(res)
[docs] @template.renderer
def rowSet(self, request, tag):
# slow, rather use tableBody
return tag(render="sequence")[
NT(T.td, pattern="item")(render="unicode")]
[docs] @template.renderer
def tableBody(self, request, tag):
"""returns HTML-rendered table rows in chunks of rowsPerDivision.
We don't use stan here since we can concat all those tr/td much faster
ourselves.
"""
rowAttrsIterator = itertools.cycle([' class="data"', ' class="data even"'])
rendered = []
yield T.xml("<tbody>")
for row in self.table:
rendered.append(self._formatRow(row, next(rowAttrsIterator)))
if len(rendered)>=self.rowsPerDivision:
yield T.xml("\n".join(rendered))
yield self.headCellsStan
rendered = []
yield T.xml("\n".join(rendered)+"\n</tbody>")
loader = template.TagLoader(T.div(class_="tablewrap")[
T.div(render="meta", class_="warning")["_warning"],
T.table(class_="results") [
T.thead(render="headCells"),
T.tbody(render="tableBody")],
T.transparent(render="footnotes"),
]
)
[docs]class HTMLKeyValueFragment(HTMLDataRenderer, HeadCellsMixin):
"""A nevow renderer for single-row result tables.
"""
[docs] def data_firstrow(self, request, tag):
"""returns a sequence for (colDef, serialised value) for the first row
of the result table.
"""
return list(zip(self.serManager, self.serializeRow(self.table.rows[0])))
[docs] @template.renderer
def coldefdesc(self, request, tag):
"""returns the description of a colDev in a firstrow sequence.
"""
return tag[tag.slotData[0].original.description]
[docs] def makeLoader(self):
return template.TagLoader([
T.div(render="meta", class_="warning")["_warning"],
NT(T.table, data="firstrow")(class_="keyvalue", render="sequence")[
NT(T.transparent, pattern="item")[
T.tr[
NT(T.th, data="0")(render="headCell", class_="thHorizontal"),
NT(T.td, data="1")(class_="data", render="passthrough")
],
T.tr(class_="keyvaluedesc")[
T.td(colspan="2", render="coldefdesc")]
]],
T.transparent(render="footnotes"),
])
loader = property(makeLoader)
[docs]def writeDataAsHTML(data, outputFile, acquireSamples=False):
"""writes data's primary table to outputFile.
(acquireSamples is actually ignored; it is just present for compatibility
with the other writers until I rip out the samples stuff altogether).
"""
if isinstance(data, rsc.Data):
data = data.getPrimaryTable()
fragment = HTMLTableFragment(data, svcs.emptyQueryMeta)
outputFile.write(nevowc.flattenSync(fragment))
formats.registerDataWriter("html", writeDataAsHTML, "text/html", "HTML",
".html")