# -*- coding: utf-8 -*- # $Id: wuicontentbase.py $ """ Test Manager Web-UI - Content Base Classes. """ __copyright__ = \ """ Copyright (C) 2012-2023 Oracle and/or its affiliates. This file is part of VirtualBox base platform packages, as available from https://www.virtualbox.org. This program is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License as published by the Free Software Foundation, in version 3 of the License. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for more details. You should have received a copy of the GNU General Public License along with this program; if not, see . The contents of this file may alternatively be used under the terms of the Common Development and Distribution License Version 1.0 (CDDL), a copy of it is provided in the "COPYING.CDDL" file included in the VirtualBox distribution, in which case the provisions of the CDDL are applicable instead of those of the GPL. You may elect to license modified versions of this file under the terms and conditions of either the GPL or the CDDL or both. SPDX-License-Identifier: GPL-3.0-only OR CDDL-1.0 """ __version__ = "$Revision: 155244 $" # Standard python imports. import copy; import sys; # Validation Kit imports. from common import utils, webutils; from testmanager import config; from testmanager.webui.wuibase import WuiDispatcherBase, WuiException; from testmanager.webui.wuihlpform import WuiHlpForm; from testmanager.core import db; from testmanager.core.base import AttributeChangeEntryPre; # Python 3 hacks: if sys.version_info[0] >= 3: unicode = str; # pylint: disable=redefined-builtin,invalid-name class WuiHtmlBase(object): # pylint: disable=too-few-public-methods """ Base class for HTML objects. """ def __init__(self): """Dummy init to shut up pylint.""" pass; # pylint: disable=unnecessary-pass def toHtml(self): """ Must be overridden by sub-classes. """ assert False; return ''; def __str__(self): """ String representation is HTML, simplifying formatting and such. """ return self.toHtml(); class WuiLinkBase(WuiHtmlBase): # pylint: disable=too-few-public-methods """ For passing links from WuiListContentBase._formatListEntry. """ def __init__(self, sName, sUrlBase, dParams = None, sConfirm = None, sTitle = None, sFragmentId = None, fBracketed = True, sExtraAttrs = ''): WuiHtmlBase.__init__(self); self.sName = sName self.sUrl = sUrlBase self.sConfirm = sConfirm; self.sTitle = sTitle; self.fBracketed = fBracketed; self.sExtraAttrs = sExtraAttrs; if dParams: # Do some massaging of None arguments. dParams = dict(dParams); for sKey in dParams: if dParams[sKey] is None: dParams[sKey] = ''; self.sUrl += '?' + webutils.encodeUrlParams(dParams); if sFragmentId is not None: self.sUrl += '#' + sFragmentId; def setBracketed(self, fBracketed): """Changes the bracketing style.""" self.fBracketed = fBracketed; return True; def toHtml(self): """ Returns a simple HTML anchor element. """ sExtraAttrs = self.sExtraAttrs; if self.sConfirm is not None: sExtraAttrs += 'onclick=\'return confirm("%s");\' ' % (webutils.escapeAttr(self.sConfirm),); if self.sTitle is not None: sExtraAttrs += 'title="%s" ' % (webutils.escapeAttr(self.sTitle),); if sExtraAttrs and sExtraAttrs[-1] != ' ': sExtraAttrs += ' '; sFmt = '[%s]'; if not self.fBracketed: sFmt = '%s'; return sFmt % (sExtraAttrs, webutils.escapeAttr(self.sUrl), webutils.escapeElem(self.sName)); @staticmethod def estimateStringWidth(sString): """ Takes a string and estimate it's width so the caller can pad with U+2002 before tab in a title text. This is very very rough. """ cchWidth = 0; for ch in sString: if ch.isupper() or ch in u'wm\u2007\u2003\u2001\u3000': cchWidth += 2; else: cchWidth += 1; return cchWidth; @staticmethod def getStringWidthPaddingEx(cchWidth, cchMaxWidth): """ Works with estiamteStringWidth(). """ if cchWidth + 2 <= cchMaxWidth: return u'\u2002' * ((cchMaxWidth - cchWidth) * 2 // 3) return u''; @staticmethod def getStringWidthPadding(sString, cchMaxWidth): """ Works with estiamteStringWidth(). """ return WuiLinkBase.getStringWidthPaddingEx(WuiLinkBase.estimateStringWidth(sString), cchMaxWidth); @staticmethod def padStringToWidth(sString, cchMaxWidth): """ Works with estimateStringWidth. """ cchWidth = WuiLinkBase.estimateStringWidth(sString); if cchWidth < cchMaxWidth: return sString + WuiLinkBase.getStringWidthPaddingEx(cchWidth, cchMaxWidth); return sString; class WuiTmLink(WuiLinkBase): # pylint: disable=too-few-public-methods """ Local link to the test manager. """ kdDbgParams = []; def __init__(self, sName, sUrlBase, dParams = None, sConfirm = None, sTitle = None, sFragmentId = None, fBracketed = True): # Add debug parameters if necessary. if self.kdDbgParams: if not dParams: dParams = dict(self.kdDbgParams); else: dParams = dict(dParams); for sKey in self.kdDbgParams: if sKey not in dParams: dParams[sKey] = self.kdDbgParams[sKey]; WuiLinkBase.__init__(self, sName, sUrlBase, dParams, sConfirm, sTitle, sFragmentId, fBracketed); class WuiAdminLink(WuiTmLink): # pylint: disable=too-few-public-methods """ Local link to the test manager's admin portion. """ def __init__(self, sName, sAction, tsEffectiveDate = None, dParams = None, sConfirm = None, sTitle = None, sFragmentId = None, fBracketed = True): from testmanager.webui.wuiadmin import WuiAdmin; if not dParams: dParams = {}; else: dParams = dict(dParams); if sAction is not None: dParams[WuiAdmin.ksParamAction] = sAction; if tsEffectiveDate is not None: dParams[WuiAdmin.ksParamEffectiveDate] = tsEffectiveDate; WuiTmLink.__init__(self, sName, WuiAdmin.ksScriptName, dParams = dParams, sConfirm = sConfirm, sTitle = sTitle, sFragmentId = sFragmentId, fBracketed = fBracketed); class WuiMainLink(WuiTmLink): # pylint: disable=too-few-public-methods """ Local link to the test manager's main portion. """ def __init__(self, sName, sAction, dParams = None, sConfirm = None, sTitle = None, sFragmentId = None, fBracketed = True): if not dParams: dParams = {}; else: dParams = dict(dParams); from testmanager.webui.wuimain import WuiMain; if sAction is not None: dParams[WuiMain.ksParamAction] = sAction; WuiTmLink.__init__(self, sName, WuiMain.ksScriptName, dParams = dParams, sConfirm = sConfirm, sTitle = sTitle, sFragmentId = sFragmentId, fBracketed = fBracketed); class WuiSvnLink(WuiLinkBase): # pylint: disable=too-few-public-methods """ For linking to a SVN revision. """ def __init__(self, iRevision, sName = None, fBracketed = True, sExtraAttrs = ''): if sName is None: sName = 'r%s' % (iRevision,); WuiLinkBase.__init__(self, sName, config.g_ksTracLogUrlPrefix, { 'rev': iRevision,}, fBracketed = fBracketed, sExtraAttrs = sExtraAttrs); class WuiSvnLinkWithTooltip(WuiSvnLink): # pylint: disable=too-few-public-methods """ For linking to a SVN revision with changelog tooltip. """ def __init__(self, iRevision, sRepository, sName = None, fBracketed = True): sExtraAttrs = ' onmouseover="return svnHistoryTooltipShow(event,\'%s\',%s);" onmouseout="return tooltipHide();"' \ % ( sRepository, iRevision, ); WuiSvnLink.__init__(self, iRevision, sName = sName, fBracketed = fBracketed, sExtraAttrs = sExtraAttrs); class WuiBuildLogLink(WuiLinkBase): """ For linking to a build log. """ def __init__(self, sUrl, sName = None, fBracketed = True): assert sUrl; if sName is None: sName = 'Build log'; if not webutils.hasSchema(sUrl): WuiLinkBase.__init__(self, sName, config.g_ksBuildLogUrlPrefix + sUrl, fBracketed = fBracketed); else: WuiLinkBase.__init__(self, sName, sUrl, fBracketed = fBracketed); class WuiRawHtml(WuiHtmlBase): # pylint: disable=too-few-public-methods """ For passing raw html from WuiListContentBase._formatListEntry. """ def __init__(self, sHtml): self.sHtml = sHtml; WuiHtmlBase.__init__(self); def toHtml(self): return self.sHtml; class WuiHtmlKeeper(WuiHtmlBase): # pylint: disable=too-few-public-methods """ For keeping a list of elements, concatenating their toHtml output together. """ def __init__(self, aoInitial = None, sSep = ' '): WuiHtmlBase.__init__(self); self.sSep = sSep; self.aoKept = []; if aoInitial is not None: if isinstance(aoInitial, WuiHtmlBase): self.aoKept.append(aoInitial); else: self.aoKept.extend(aoInitial); def append(self, oObject): """ Appends one objects. """ self.aoKept.append(oObject); def extend(self, aoObjects): """ Appends a list of objects. """ self.aoKept.extend(aoObjects); def toHtml(self): return self.sSep.join(oObj.toHtml() for oObj in self.aoKept); class WuiSpanText(WuiRawHtml): # pylint: disable=too-few-public-methods """ Outputs the given text within a span of the given CSS class. """ def __init__(self, sSpanClass, sText, sTitle = None): if sTitle is None: WuiRawHtml.__init__(self, u'%s' % ( webutils.escapeAttr(sSpanClass), webutils.escapeElem(sText),)); else: WuiRawHtml.__init__(self, u'%s' % ( webutils.escapeAttr(sSpanClass), webutils.escapeAttr(sTitle), webutils.escapeElem(sText),)); class WuiElementText(WuiRawHtml): # pylint: disable=too-few-public-methods """ Outputs the given element text. """ def __init__(self, sText): WuiRawHtml.__init__(self, webutils.escapeElem(sText)); class WuiContentBase(object): # pylint: disable=too-few-public-methods """ Base for the content classes. """ ## The text/symbol for a very short add link. ksShortAddLink = u'\u2795' ## HTML hex entity string for ksShortAddLink. ksShortAddLinkHtml = '➕;' ## The text/symbol for a very short edit link. ksShortEditLink = u'\u270D' ## HTML hex entity string for ksShortDetailsLink. ksShortEditLinkHtml = '✍' ## The text/symbol for a very short details link. ksShortDetailsLink = u'\U0001f6c8\ufe0e' ## HTML hex entity string for ksShortDetailsLink. ksShortDetailsLinkHtml = '🛈;︎' ## The text/symbol for a very short change log / details / previous page link. ksShortChangeLogLink = u'\u2397' ## HTML hex entity string for ksShortDetailsLink. ksShortChangeLogLinkHtml = '⎗' ## The text/symbol for a very short reports link. ksShortReportLink = u'\U0001f4ca\ufe0e' ## HTML hex entity string for ksShortReportLink. ksShortReportLinkHtml = '📊︎' ## The text/symbol for a very short test results link. ksShortTestResultsLink = u'\U0001f5d0\ufe0e' def __init__(self, fnDPrint = None, oDisp = None): self._oDisp = oDisp; # WuiDispatcherBase. self._fnDPrint = fnDPrint; if fnDPrint is None and oDisp is not None: self._fnDPrint = oDisp.dprint; def dprint(self, sText): """ Debug printing. """ if self._fnDPrint: self._fnDPrint(sText); @staticmethod def formatTsShort(oTs): """ Formats a timestamp (db rep) into a short form. """ oTsZulu = db.dbTimestampToZuluDatetime(oTs); sTs = oTsZulu.strftime('%Y-%m-%d %H:%M:%SZ'); return unicode(sTs).replace('-', u'\u2011').replace(' ', u'\u00a0'); def getNowTs(self): """ Gets a database compatible current timestamp from python. See db.dbTimestampPythonNow(). """ return db.dbTimestampPythonNow(); def formatIntervalShort(self, oInterval): """ Formats an interval (db rep) into a short form. """ # default formatting for negative intervals. if oInterval.days < 0: return str(oInterval); # Figure the hour, min and sec counts. cHours = oInterval.seconds // 3600; cMinutes = (oInterval.seconds % 3600) // 60; cSeconds = oInterval.seconds - cHours * 3600 - cMinutes * 60; # Tailor formatting to the interval length. if oInterval.days > 0: if oInterval.days > 1: return '%d days, %d:%02d:%02d' % (oInterval.days, cHours, cMinutes, cSeconds); return '1 day, %d:%02d:%02d' % (cHours, cMinutes, cSeconds); if cMinutes > 0 or cSeconds >= 30 or cHours > 0: return '%d:%02d:%02d' % (cHours, cMinutes, cSeconds); if cSeconds >= 10: return '%d.%ds' % (cSeconds, oInterval.microseconds // 100000); if cSeconds > 0: return '%d.%02ds' % (cSeconds, oInterval.microseconds // 10000); return '%d ms' % (oInterval.microseconds // 1000,); @staticmethod def genericPageWalker(iCurItem, cItems, sHrefFmt, cWidth = 11, iBase = 1, sItemName = 'page'): """ Generic page walker generator. sHrefFmt has three %s sequences: 1. The first is the page number link parameter (0-based). 2. The title text, iBase-based number or text. 3. The link text, iBase-based number or text. """ # Calc display range. iStart = 0 if iCurItem - cWidth // 2 <= cWidth // 4 else iCurItem - cWidth // 2; iEnd = iStart + cWidth; if iEnd > cItems: iEnd = cItems; if cItems > cWidth: iStart = cItems - cWidth; sHtml = u''; # Previous page (using << >> because « and » are too tiny). if iCurItem > 0: sHtml += '%s  ' % sHrefFmt % (iCurItem - 1, 'previous ' + sItemName, '<<'); else: sHtml += '<<  '; # 1 2 3 4... if iStart > 0: sHtml += '%s  ...  \n' % (sHrefFmt % (0, 'first %s' % (sItemName,), 0 + iBase),); sHtml += ' \n'.join(sHrefFmt % (i, '%s %d' % (sItemName, i + iBase), i + iBase) if i != iCurItem else unicode(i + iBase) for i in range(iStart, iEnd)); if iEnd < cItems: sHtml += '  ...  %s\n' % (sHrefFmt % (cItems - 1, 'last %s' % (sItemName,), cItems - 1 + iBase)); # Next page. if iCurItem + 1 < cItems: sHtml += '  %s' % sHrefFmt % (iCurItem + 1, 'next ' + sItemName, '>>'); else: sHtml += '  >>'; return sHtml; class WuiSingleContentBase(WuiContentBase): # pylint: disable=too-few-public-methods """ Base for the content classes working on a single data object (oData). """ def __init__(self, oData, oDisp = None, fnDPrint = None): WuiContentBase.__init__(self, oDisp = oDisp, fnDPrint = fnDPrint); self._oData = oData; # Usually ModelDataBase. class WuiFormContentBase(WuiSingleContentBase): # pylint: disable=too-few-public-methods """ Base class for simple input form content classes (single data object). """ ## @name Form mode. ## @{ ksMode_Add = 'add'; ksMode_Edit = 'edit'; ksMode_Show = 'show'; ## @} ## Default action mappings. kdSubmitActionMappings = { ksMode_Add: 'AddPost', ksMode_Edit: 'EditPost', }; def __init__(self, oData, sMode, sCoreName, oDisp, sTitle, sId = None, fEditable = True, sSubmitAction = None): WuiSingleContentBase.__init__(self, copy.copy(oData), oDisp); assert sMode in [self.ksMode_Add, self.ksMode_Edit, self.ksMode_Show]; assert len(sTitle) > 1; assert sId is None or sId; self._sMode = sMode; self._sCoreName = sCoreName; self._sActionBase = 'ksAction' + sCoreName; self._sTitle = sTitle; self._sId = sId if sId is not None else (type(oData).__name__.lower() + 'form'); self._fEditable = fEditable and (oDisp is None or not oDisp.isReadOnlyUser()) self._sSubmitAction = sSubmitAction; if sSubmitAction is None and sMode != self.ksMode_Show: self._sSubmitAction = getattr(oDisp, self._sActionBase + self.kdSubmitActionMappings[sMode]); self._sRedirectTo = None; def _populateForm(self, oForm, oData): """ Populates the form. oData has parameter NULL values. This must be reimplemented by the child. """ _ = oForm; _ = oData; raise Exception('Reimplement me!'); def _generatePostFormContent(self, oData): """ Generate optional content that comes below the form. Returns a list of tuples, where the first tuple element is the title and the second the content. I.e. similar to show() output. """ _ = oData; return []; @staticmethod def _calcChangeLogEntryLinks(aoEntries, iEntry): """ Returns an array of links to go with the change log entry. """ _ = aoEntries; _ = iEntry; ## @todo detect deletion and recreation. ## @todo view details link. ## @todo restore link (need new action) ## @todo clone link. return []; @staticmethod def _guessChangeLogEntryDescription(aoEntries, iEntry): """ Guesses the action + author that caused the change log entry. Returns descriptive string. """ oEntry = aoEntries[iEntry]; # Figure the author of the change. if oEntry.sAuthor is not None: sAuthor = '%s (#%s)' % (oEntry.sAuthor, oEntry.uidAuthor,); elif oEntry.uidAuthor is not None: sAuthor = '#%d (??)' % (oEntry.uidAuthor,); else: sAuthor = None; # Figure the action. if oEntry.oOldRaw is None: if sAuthor is None: return 'Created by batch job.'; return 'Created by %s.' % (sAuthor,); if sAuthor is None: return 'Automatically updated.' return 'Modified by %s.' % (sAuthor,); @staticmethod def formatChangeLogEntry(aoEntries, iEntry, sUrl, dParams): """ Formats one change log entry into one or more HTML table rows. The sUrl and dParams arguments are used to construct links to historical data using the tsEffective value. If no links wanted, they'll both be None. Note! The parameters are given as array + index in case someone wishes to access adjacent entries later in order to generate better change descriptions. """ oEntry = aoEntries[iEntry]; # Turn the effective date into a URL if we can: if sUrl: dParams[WuiDispatcherBase.ksParamEffectiveDate] = oEntry.tsEffective; sEffective = WuiLinkBase(WuiFormContentBase.formatTsShort(oEntry.tsEffective), sUrl, dParams, fBracketed = False).toHtml(); else: sEffective = webutils.escapeElem(WuiFormContentBase.formatTsShort(oEntry.tsEffective)) # The primary row. sRowClass = 'tmodd' if (iEntry + 1) & 1 else 'tmeven'; sContent = ' \n' \ ' %s\n' \ ' %s\n' \ ' %s%s\n' \ ' \n' \ % ( sRowClass, len(oEntry.aoChanges) + 1, sEffective, len(oEntry.aoChanges) + 1, webutils.escapeElem(WuiFormContentBase.formatTsShort(oEntry.tsExpire)), WuiFormContentBase._guessChangeLogEntryDescription(aoEntries, iEntry), ' '.join(oLink.toHtml() for oLink in WuiFormContentBase._calcChangeLogEntryLinks(aoEntries, iEntry)),); # Additional rows for each changed attribute. j = 0; for oChange in oEntry.aoChanges: if isinstance(oChange, AttributeChangeEntryPre): sContent += ' %s'\ '
%s%s%s
' \ '
%s%s%s
\n' \ % ( sRowClass, 'odd' if j & 1 else 'even', webutils.escapeElem(oChange.sAttr), '
' if oChange.sOldText else '',
                               webutils.escapeElem(oChange.sOldText),
                              '
' if oChange.sOldText else '', '
' if oChange.sNewText else '',
                              webutils.escapeElem(oChange.sNewText),
                              '
' if oChange.sNewText else '', ); else: sContent += ' %s%s%s\n' \ % ( sRowClass, 'odd' if j & 1 else 'even', webutils.escapeElem(oChange.sAttr), webutils.escapeElem(oChange.sOldText), webutils.escapeElem(oChange.sNewText), ); j += 1; return sContent; def _showChangeLogNavi(self, fMoreEntries, iPageNo, cEntriesPerPage, tsNow, sWhere): """ Returns the HTML for the change log navigator. Note! See also _generateNavigation. """ sNavigation = '
\n' % sWhere; sNavigation += ' \n' \ ' \n'; dParams = self._oDisp.getParameters(); dParams[WuiDispatcherBase.ksParamChangeLogEntriesPerPage] = cEntriesPerPage; dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo; if tsNow is not None: dParams[WuiDispatcherBase.ksParamEffectiveDate] = tsNow; # Prev and combo box in one cell. Both inside the form for formatting reasons. sNavigation += ' \n'; # Next if fMoreEntries: dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo + 1; sNavigation += ' \n' \ % (webutils.encodeUrlParams(dParams),); else: sNavigation += ' \n'; sNavigation += ' \n' \ '
\n' \ '
\n' # Prev if iPageNo > 0: dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo - 1; sNavigation += 'Previous\n' \ % (webutils.encodeUrlParams(dParams),); dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo; else: sNavigation += 'Previous\n'; # Entries per page selector. del dParams[WuiDispatcherBase.ksParamChangeLogEntriesPerPage]; sNavigation += '   \n' \ ' \n'; # End of cell (and form). sNavigation += '
\n' \ '
NextNext
\n' \ '
\n'; return sNavigation; def setRedirectTo(self, sRedirectTo): """ For setting the hidden redirect-to field. """ self._sRedirectTo = sRedirectTo; return True; def showChangeLog(self, aoEntries, fMoreEntries, iPageNo, cEntriesPerPage, tsNow, fShowNavigation = True): """ Render the change log, returning raw HTML. aoEntries is an array of ChangeLogEntry. """ sContent = '\n' \ '
\n' \ '
\n' \ '

Change Log

\n'; if fShowNavigation: sContent += self._showChangeLogNavi(fMoreEntries, iPageNo, cEntriesPerPage, tsNow, 'top'); sContent += ' \n' \ ' ' \ ' ' \ ' \n' \ ' \n' \ ' \n' \ ' \n' \ ' \n' \ ' \n' \ ' \n' \ ' \n' \ ' \n' \ ' \n' \ ' \n'; if self._sMode == self.ksMode_Show: sUrl = self._oDisp.getUrlNoParams(); dParams = self._oDisp.getParameters(); else: sUrl = None; dParams = None; for iEntry, _ in enumerate(aoEntries): sContent += self.formatChangeLogEntry(aoEntries, iEntry, sUrl, dParams); sContent += ' \n' \ '
WhenExpire (excl)Changes
AttributeOld valueNew value
\n'; if fShowNavigation and len(aoEntries) >= 8: sContent += self._showChangeLogNavi(fMoreEntries, iPageNo, cEntriesPerPage, tsNow, 'bottom'); sContent += '
\n\n'; return sContent; def _generateTopRowFormActions(self, oData): """ Returns a list of WuiTmLinks. """ aoActions = []; if self._sMode == self.ksMode_Show and self._fEditable: # Remove _idGen and effective date since we're always editing the current data, # and make sure the primary ID is present. Also remove change log stuff. dParams = self._oDisp.getParameters(); if hasattr(oData, 'ksIdGenAttr'): sIdGenParam = getattr(oData, 'ksParam_' + oData.ksIdGenAttr); if sIdGenParam in dParams: del dParams[sIdGenParam]; for sParam in [ WuiDispatcherBase.ksParamEffectiveDate, ] + list(WuiDispatcherBase.kasChangeLogParams): if sParam in dParams: del dParams[sParam]; dParams[getattr(oData, 'ksParam_' + oData.ksIdAttr)] = getattr(oData, oData.ksIdAttr); dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Edit'); aoActions.append(WuiTmLink('Edit', '', dParams)); # Add clone operation if available. This uses the same data selection as for showing details. No change log. if hasattr(self._oDisp, self._sActionBase + 'Clone'): dParams = self._oDisp.getParameters(); for sParam in WuiDispatcherBase.kasChangeLogParams: if sParam in dParams: del dParams[sParam]; dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Clone'); aoActions.append(WuiTmLink('Clone', '', dParams)); elif self._sMode == self.ksMode_Edit: # Details views the details at a given time, so we need either idGen or an effecive date + regular id. dParams = {}; if hasattr(oData, 'ksIdGenAttr'): sIdGenParam = getattr(oData, 'ksParam_' + oData.ksIdGenAttr); dParams[sIdGenParam] = getattr(oData, oData.ksIdGenAttr); elif hasattr(oData, 'tsEffective'): dParams[WuiDispatcherBase.ksParamEffectiveDate] = oData.tsEffective; dParams[getattr(oData, 'ksParam_' + oData.ksIdAttr)] = getattr(oData, oData.ksIdAttr); dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Details'); aoActions.append(WuiTmLink('Details', '', dParams)); # Add delete operation if available. if hasattr(self._oDisp, self._sActionBase + 'DoRemove'): dParams = self._oDisp.getParameters(); dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'DoRemove'); dParams[getattr(oData, 'ksParam_' + oData.ksIdAttr)] = getattr(oData, oData.ksIdAttr); aoActions.append(WuiTmLink('Delete', '', dParams, sConfirm = "Are you absolutely sure?")); return aoActions; def showForm(self, dErrors = None, sErrorMsg = None): """ Render the form. """ oForm = WuiHlpForm(self._sId, '?' + webutils.encodeUrlParams({WuiDispatcherBase.ksParamAction: self._sSubmitAction}), dErrors if dErrors is not None else {}, fReadOnly = self._sMode == self.ksMode_Show); self._oData.convertToParamNull(); # If form cannot be constructed due to some reason we # need to show this reason try: self._populateForm(oForm, self._oData); if self._sRedirectTo is not None: oForm.addTextHidden(self._oDisp.ksParamRedirectTo, self._sRedirectTo); except WuiException as oXcpt: sContent = unicode(oXcpt) else: sContent = oForm.finalize(); # Add any post form content. atPostFormContent = self._generatePostFormContent(self._oData); if atPostFormContent: for iSection, tSection in enumerate(atPostFormContent): (sSectionTitle, sSectionContent) = tSection; sContent += u'
\n' % (iSection,); if sSectionTitle: sContent += '

%s

\n' % (webutils.escapeElem(sSectionTitle),); sContent += u'
\n' % (iSection,); sContent += sSectionContent; sContent += u'
\n' \ u'
\n'; # Add action to the top. aoActions = self._generateTopRowFormActions(self._oData); if aoActions: sActionLinks = '

%s

' % (' '.join(unicode(oLink) for oLink in aoActions)); sContent = sActionLinks + sContent; # Add error info to the top. if sErrorMsg is not None: sContent = '

' + webutils.escapeElem(sErrorMsg) + '

\n' + sContent; return (self._sTitle, sContent); def getListOfItems(self, asListItems = tuple(), asSelectedItems = tuple()): """ Format generic list which should be used by HTML form """ aoRet = [] for sListItem in asListItems: fEnabled = sListItem in asSelectedItems; aoRet.append((sListItem, fEnabled, sListItem)) return aoRet class WuiListContentBase(WuiContentBase): """ Base for the list content classes. """ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle, # pylint: disable=too-many-arguments sId = None, fnDPrint = None, oDisp = None, aiSelectedSortColumns = None, fTimeNavigation = True): WuiContentBase.__init__(self, fnDPrint = fnDPrint, oDisp = oDisp); self._aoEntries = aoEntries; ## @todo should replace this with a Logic object and define methods for querying. self._iPage = iPage; self._cItemsPerPage = cItemsPerPage; self._tsEffectiveDate = tsEffectiveDate; self._fTimeNavigation = fTimeNavigation; self._sTitle = sTitle; assert len(sTitle) > 1; if sId is None: sId = sTitle.strip().replace(' ', '').lower(); assert sId.strip(); self._sId = sId; self._asColumnHeaders = []; self._asColumnAttribs = []; self._aaiColumnSorting = []; ##< list of list of integers self._aiSelectedSortColumns = aiSelectedSortColumns; ##< list of integers def _formatCommentCell(self, sComment, cMaxLines = 3, cchMaxLine = 63): """ Helper functions for formatting comment cell. Returns None or WuiRawHtml instance. """ # Nothing to do for empty comments. if sComment is None: return None; sComment = sComment.strip(); if not sComment: return None; # Restrict the text if necessary, making the whole text available thru mouse-over. ## @todo this would be better done by java script or smth, so it could automatically adjust to the table size. if len(sComment) > cchMaxLine or sComment.count('\n') >= cMaxLines: sShortHtml = ''; for iLine, sLine in enumerate(sComment.split('\n')): if iLine >= cMaxLines: break; if iLine > 0: sShortHtml += '
\n'; if len(sLine) > cchMaxLine: sShortHtml += webutils.escapeElem(sLine[:(cchMaxLine - 3)]); sShortHtml += '...'; else: sShortHtml += webutils.escapeElem(sLine); return WuiRawHtml('%s' % (webutils.escapeAttr(sComment), sShortHtml,)); return WuiRawHtml('%s' % (webutils.escapeElem(sComment).replace('\n', '
'),)); def _formatListEntry(self, iEntry): """ Formats the specified list entry as a list of column values. Returns HTML for a table row. The child class really need to override this! """ # ASSUMES ModelDataBase children. asRet = []; for sAttr in self._aoEntries[0].getDataAttributes(): asRet.append(getattr(self._aoEntries[iEntry], sAttr)); return asRet; def _formatListEntryHtml(self, iEntry): """ Formats the specified list entry as HTML. Returns HTML for a table row. The child class can override this to """ if (iEntry + 1) & 1: sRow = u' \n'; else: sRow = u' \n'; aoValues = self._formatListEntry(iEntry); assert len(aoValues) == len(self._asColumnHeaders), '%s vs %s' % (len(aoValues), len(self._asColumnHeaders)); for i, _ in enumerate(aoValues): if i < len(self._asColumnAttribs) and self._asColumnAttribs[i]: sRow += u' '; else: sRow += u' '; if isinstance(aoValues[i], WuiHtmlBase): sRow += aoValues[i].toHtml(); elif isinstance(aoValues[i], list): if aoValues[i]: for oElement in aoValues[i]: if isinstance(oElement, WuiHtmlBase): sRow += oElement.toHtml(); elif db.isDbTimestamp(oElement): sRow += webutils.escapeElem(self.formatTsShort(oElement)); else: sRow += webutils.escapeElem(unicode(oElement)); sRow += ' '; elif db.isDbTimestamp(aoValues[i]): sRow += webutils.escapeElem(self.formatTsShort(aoValues[i])); elif db.isDbInterval(aoValues[i]): sRow += webutils.escapeElem(self.formatIntervalShort(aoValues[i])); elif aoValues[i] is not None: sRow += webutils.escapeElem(unicode(aoValues[i])); sRow += u'\n'; return sRow + u' \n'; @staticmethod def generateTimeNavigationComboBox(sWhere, dParams, tsEffective): """ Generates the HTML for the xxxx ago combo box form. """ sNavigation = '
\n' % (sWhere,); sNavigation += ' \n' \ '
\n'; return sNavigation; @staticmethod def generateTimeNavigationDateTime(sWhere, dParams, sNow): """ Generates HTML for a form with date + time input fields. Note! Modifies dParams! """ # # Date + time input fields. We use a java script helper to combine the two # into a hidden field as there is no portable datetime input field type. # sNavigation = '
' % (sWhere,); if sNow is None: sNow = utils.getIsoTimestamp(); else: sNow = utils.normalizeIsoTimestampToZulu(sNow); asSplit = sNow.split('T'); sNavigation += ' ' % (asSplit[0], sWhere, ); sNavigation += ' ' % (asSplit[1][:8], sWhere,); sNavigation += ' ' \ % (WuiDispatcherBase.ksParamEffectiveDate, webutils.escapeAttr(sNow), sWhere); for sKey in dParams: sNavigation += ' ' \ % (webutils.escapeAttr(sKey), webutils.escapeAttrToStr(dParams[sKey])); sNavigation += ' \n' \ '
\n'; return sNavigation; ## @todo move to better place! WuiMain uses it. @staticmethod def generateTimeNavigation(sWhere, dParams, tsEffectiveAbs, sPreamble = '', sPostamble = '', fKeepPageNo = False): """ Returns HTML for time navigation. Note! Modifies dParams! Note! Views without a need for a timescale just stubs this method. """ sNavigation = '
%s' % (sWhere, sPreamble,); # # Prepare the URL parameters. # if WuiDispatcherBase.ksParamPageNo in dParams: # Forget about page No when changing a period del dParams[WuiDispatcherBase.ksParamPageNo] if not fKeepPageNo and WuiDispatcherBase.ksParamEffectiveDate in dParams: tsEffectiveParam = dParams[WuiDispatcherBase.ksParamEffectiveDate]; del dParams[WuiDispatcherBase.ksParamEffectiveDate]; else: tsEffectiveParam = '' # # Generate the individual parts. # sNavigation += WuiListContentBase.generateTimeNavigationDateTime(sWhere, dParams, tsEffectiveAbs); sNavigation += WuiListContentBase.generateTimeNavigationComboBox(sWhere, dParams, tsEffectiveParam); sNavigation += '%s
' % (sPostamble,); return sNavigation; def _generateTimeNavigation(self, sWhere, sPreamble = '', sPostamble = ''): """ Returns HTML for time navigation. Note! Views without a need for a timescale just stubs this method. """ return self.generateTimeNavigation(sWhere, self._oDisp.getParameters(), self._oDisp.getEffectiveDateParam(), sPreamble, sPostamble) @staticmethod def generateItemPerPageSelector(sWhere, dParams, cCurItemsPerPage): """ Generate HTML code for items per page selector. Note! Modifies dParams! """ # Drop the current page count parameter. if WuiDispatcherBase.ksParamItemsPerPage in dParams: del dParams[WuiDispatcherBase.ksParamItemsPerPage]; # Remove the current page number. if WuiDispatcherBase.ksParamPageNo in dParams: del dParams[WuiDispatcherBase.ksParamPageNo]; sHtmlItemsPerPageSelector = '
\n'\ ' \n' \ '
\n'; return sHtmlItemsPerPageSelector def _generateNavigation(self, sWhere): """ Return HTML for navigation. """ # # ASSUMES the dispatcher/controller code fetches one entry more than # needed to fill the page to indicate further records. # sNavigation = '
\n' % sWhere; sNavigation += ' \n' \ ' \n'; dParams = self._oDisp.getParameters(); dParams[WuiDispatcherBase.ksParamItemsPerPage] = self._cItemsPerPage; dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage; if self._tsEffectiveDate is not None: dParams[WuiDispatcherBase.ksParamEffectiveDate] = self._tsEffectiveDate; # Prev if self._iPage > 0: dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage - 1; sNavigation += ' \n' % (webutils.encodeUrlParams(dParams),); else: sNavigation += ' \n'; # Time scale. if self._fTimeNavigation: sNavigation += ''; # page count and next. sNavigation += '\n'; sNavigation += ' \n' \ '
Previous'; sNavigation += self._generateTimeNavigation(sWhere); sNavigation += '\n'; if len(self._aoEntries) > self._cItemsPerPage: dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage + 1; sNavigation += ' Next\n' % (webutils.encodeUrlParams(dParams),); sNavigation += self.generateItemPerPageSelector(sWhere, dParams, self._cItemsPerPage); sNavigation += '
\n' \ '
\n'; return sNavigation; def _checkSortingByColumnAscending(self, aiColumns): """ Checks if we're sorting by this column. Returns 0 if not sorting by this, negative if descending, positive if ascending. The value indicates the priority (nearer to 0 is higher). """ if len(aiColumns) <= len(self._aiSelectedSortColumns): aiColumns = list(aiColumns); aiNegColumns = list([-i for i in aiColumns]); # pylint: disable=consider-using-generator i = 0; while i + len(aiColumns) <= len(self._aiSelectedSortColumns): aiSub = list(self._aiSelectedSortColumns[i : i + len(aiColumns)]); if aiSub == aiColumns: return 1 + i; if aiSub == aiNegColumns: return -1 - i; i += 1; return 0; def _generateTableHeaders(self): """ Generate table headers. Returns raw html string. Overridable. """ sHtml = ' '; for iHeader, oHeader in enumerate(self._asColumnHeaders): if isinstance(oHeader, WuiHtmlBase): sHtml += '' + oHeader.toHtml() + ''; elif iHeader < len(self._aaiColumnSorting) and self._aaiColumnSorting[iHeader] is not None: sHtml += '' iSorting = self._checkSortingByColumnAscending(self._aaiColumnSorting[iHeader]); if iSorting > 0: sDirection = ' ▴' if iSorting == 1 else ' ▵'; sSortParams = ','.join([str(-i) for i in self._aaiColumnSorting[iHeader]]); else: sDirection = ''; if iSorting < 0: sDirection = ' ▾' if iSorting == -1 else ' ▿' sSortParams = ','.join([str(i) for i in self._aaiColumnSorting[iHeader]]); sHtml += '' \ % (WuiDispatcherBase.ksParamSortColumns, sSortParams); sHtml += webutils.escapeElem(oHeader) + '' + sDirection + ''; else: sHtml += '' + webutils.escapeElem(oHeader) + ''; sHtml += '\n'; return sHtml def _generateTable(self): """ show worker that just generates the table. """ # # Create a table. # If no colum headers are provided, fall back on database field # names, ASSUMING that the entries are ModelDataBase children. # Note! the cellspacing is for IE8. # sPageBody = '\n'; if not self._asColumnHeaders: self._asColumnHeaders = self._aoEntries[0].getDataAttributes(); sPageBody += self._generateTableHeaders(); # # Format the body and close the table. # sPageBody += ' \n'; for iEntry in range(min(len(self._aoEntries), self._cItemsPerPage)): sPageBody += self._formatListEntryHtml(iEntry); sPageBody += ' \n' \ '
\n'; return sPageBody; def _composeTitle(self): """Composes the title string (return value).""" sTitle = self._sTitle; if self._iPage != 0: sTitle += ' (page ' + unicode(self._iPage + 1) + ')' if self._tsEffectiveDate is not None: sTitle += ' as per ' + unicode(self.formatTsShort(self._tsEffectiveDate)); return sTitle; def show(self, fShowNavigation = True): """ Displays the list. Returns (Title, HTML) on success, raises exception on error. """ sPageBody = '' if fShowNavigation: sPageBody += self._generateNavigation('top'); if self._aoEntries: sPageBody += self._generateTable(); if fShowNavigation: sPageBody += self._generateNavigation('bottom'); else: sPageBody += '

No entries.

' return (self._composeTitle(), sPageBody); class WuiListContentWithActionBase(WuiListContentBase): """ Base for the list content with action classes. """ def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle, # pylint: disable=too-many-arguments sId = None, fnDPrint = None, oDisp = None, aiSelectedSortColumns = None): WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle, sId = sId, fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns); self._aoActions = None; # List of [ oValue, sText, sHover ] provided by the child class. self._sAction = None; # Set by the child class. self._sCheckboxName = None; # Set by the child class. self._asColumnHeaders = [ WuiRawHtml('' % ('' if sId is None else sId)), ]; self._asColumnAttribs = [ 'align="center"', ]; self._aaiColumnSorting = [ None, ]; def _getCheckBoxColumn(self, iEntry, sValue): """ Used by _formatListEntry implementations, returns a WuiRawHtmlBase object. """ _ = iEntry; return WuiRawHtml('' % (webutils.escapeAttr(self._sCheckboxName), webutils.escapeAttr(unicode(sValue)))); def show(self, fShowNavigation=True): """ Displays the list. Returns (Title, HTML) on success, raises exception on error. """ assert self._aoActions is not None; assert self._sAction is not None; sPageBody = '\n' \ % ('' if self._sId is None else self._sId, self._sCheckboxName,); if fShowNavigation: sPageBody += self._generateNavigation('top'); if self._aoEntries: sPageBody += '
\n' \ % (webutils.encodeUrlParams({WuiDispatcherBase.ksParamAction: self._sAction,}),); sPageBody += self._generateTable(); sPageBody += ' \n' \ ' \n'; sPageBody += ' \n'; sPageBody += '
\n'; if fShowNavigation: sPageBody += self._generateNavigation('bottom'); else: sPageBody += '

No entries.

' return (self._composeTitle(), sPageBody);