Source code for gavo.web.htmltable

"""
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] @template.renderer def footnotes(self, request, tag): """renders the footnotes as a definition list. """ if self.serManager.notes: yield T.hr(class_="footsep") yield T.dl(class_="footnotes")[[ T.xml(note.getContent(targetFormat="html", macroPackage=self.serManager.table.tableDef)) for t, note in sorted(self.serManager.notes.items())]]
[docs] def data_fielddefs(self, request, tag): return self.table.tableDef.columns
[docs] @template.renderer def meta(self, request, tag): metaKey = tag.children[0] if self.table.getMeta(metaKey, propagate=False): tag.clear() _htmlMetaBuilder.clear() return tag[self.table.buildRepr(metaKey, _htmlMetaBuilder)] else: return ""
[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")