diff options
Diffstat (limited to 'src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py')
-rwxr-xr-x | src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py | 1290 |
1 files changed, 1290 insertions, 0 deletions
diff --git a/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py b/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py new file mode 100755 index 00000000..c91e6036 --- /dev/null +++ b/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py @@ -0,0 +1,1290 @@ +# -*- 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 <https://www.gnu.org/licenses>. + +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 = '[<a %shref="%s">%s</a>]'; + if not self.fBracketed: + sFmt = '<a %shref="%s">%s</a>'; + 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'<span class="%s">%s</span>' + % ( webutils.escapeAttr(sSpanClass), webutils.escapeElem(sText),)); + else: + WuiRawHtml.__init__(self, + u'<span class="%s" title="%s">%s</span>' + % ( 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 = ' <tr class="%s">\n' \ + ' <td rowspan="%d">%s</td>\n' \ + ' <td rowspan="%d">%s</td>\n' \ + ' <td colspan="3">%s%s</td>\n' \ + ' </tr>\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 += ' <tr class="%s%s"><td>%s</td>'\ + '<td><div class="tdpre">%s%s%s</div></td>' \ + '<td><div class="tdpre">%s%s%s</div></td></tr>\n' \ + % ( sRowClass, 'odd' if j & 1 else 'even', + webutils.escapeElem(oChange.sAttr), + '<pre>' if oChange.sOldText else '', + webutils.escapeElem(oChange.sOldText), + '</pre>' if oChange.sOldText else '', + '<pre>' if oChange.sNewText else '', + webutils.escapeElem(oChange.sNewText), + '</pre>' if oChange.sNewText else '', ); + else: + sContent += ' <tr class="%s%s"><td>%s</td><td>%s</td><td>%s</td></tr>\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 = '<div class="tmlistnav-%s">\n' % sWhere; + sNavigation += ' <table class="tmlistnavtab">\n' \ + ' <tr>\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 += ' <td>\n' \ + ' <form name="ChangeLogEntriesPerPageForm" method="GET">\n' + + # Prev + if iPageNo > 0: + dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo - 1; + sNavigation += '<a href="?%s#tmchangelog">Previous</a>\n' \ + % (webutils.encodeUrlParams(dParams),); + dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo; + else: + sNavigation += 'Previous\n'; + + # Entries per page selector. + del dParams[WuiDispatcherBase.ksParamChangeLogEntriesPerPage]; + sNavigation += ' \n' \ + ' <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \ + 'this.options[this.selectedIndex].value + \'#tmchangelog\';" ' \ + 'title="Max change log entries per page">\n' \ + % (WuiDispatcherBase.ksParamChangeLogEntriesPerPage, + webutils.encodeUrlParams(dParams), + WuiDispatcherBase.ksParamChangeLogEntriesPerPage); + dParams[WuiDispatcherBase.ksParamChangeLogEntriesPerPage] = cEntriesPerPage; + + for iEntriesPerPage in [2, 4, 8, 16, 32, 64, 128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 8192]: + sNavigation += ' <option value="%d" %s>%d entries per page</option>\n' \ + % ( iEntriesPerPage, + 'selected="selected"' if iEntriesPerPage == cEntriesPerPage else '', + iEntriesPerPage ); + sNavigation += ' </select>\n'; + + # End of cell (and form). + sNavigation += ' </form>\n' \ + ' </td>\n'; + + # Next + if fMoreEntries: + dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo + 1; + sNavigation += ' <td><a href="?%s#tmchangelog">Next</a></td>\n' \ + % (webutils.encodeUrlParams(dParams),); + else: + sNavigation += ' <td>Next</td>\n'; + + sNavigation += ' </tr>\n' \ + ' </table>\n' \ + '</div>\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' \ + '<hr>\n' \ + '<div id="tmchangelog">\n' \ + ' <h3>Change Log </h3>\n'; + if fShowNavigation: + sContent += self._showChangeLogNavi(fMoreEntries, iPageNo, cEntriesPerPage, tsNow, 'top'); + sContent += ' <table class="tmtable tmchangelog">\n' \ + ' <thead class="tmheader">' \ + ' <tr>' \ + ' <th rowspan="2">When</th>\n' \ + ' <th rowspan="2">Expire (excl)</th>\n' \ + ' <th colspan="3">Changes</th>\n' \ + ' </tr>\n' \ + ' <tr>\n' \ + ' <th>Attribute</th>\n' \ + ' <th>Old value</th>\n' \ + ' <th>New value</th>\n' \ + ' </tr>\n' \ + ' </thead>\n' \ + ' <tbody>\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 += ' </tbody>\n' \ + ' </table>\n'; + if fShowNavigation and len(aoEntries) >= 8: + sContent += self._showChangeLogNavi(fMoreEntries, iPageNo, cEntriesPerPage, tsNow, 'bottom'); + sContent += '</div>\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'<div id="postform-%d" class="tmformpostsection">\n' % (iSection,); + if sSectionTitle: + sContent += '<h3 class="tmformpostheader">%s</h3>\n' % (webutils.escapeElem(sSectionTitle),); + sContent += u' <div id="postform-%d-content" class="tmformpostcontent">\n' % (iSection,); + sContent += sSectionContent; + sContent += u' </div>\n' \ + u'</div>\n'; + + # Add action to the top. + aoActions = self._generateTopRowFormActions(self._oData); + if aoActions: + sActionLinks = '<p>%s</p>' % (' '.join(unicode(oLink) for oLink in aoActions)); + sContent = sActionLinks + sContent; + + # Add error info to the top. + if sErrorMsg is not None: + sContent = '<p class="tmerrormsg">' + webutils.escapeElem(sErrorMsg) + '</p>\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 += '<br>\n'; + if len(sLine) > cchMaxLine: + sShortHtml += webutils.escapeElem(sLine[:(cchMaxLine - 3)]); + sShortHtml += '...'; + else: + sShortHtml += webutils.escapeElem(sLine); + return WuiRawHtml('<span class="tmcomment" title="%s">%s</span>' % (webutils.escapeAttr(sComment), sShortHtml,)); + + return WuiRawHtml('<span class="tmcomment">%s</span>' % (webutils.escapeElem(sComment).replace('\n', '<br>'),)); + + 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' <tr class="tmodd">\n'; + else: + sRow = u' <tr class="tmeven">\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' <td ' + self._asColumnAttribs[i] + '>'; + else: + sRow += u' <td>'; + + 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'</td>\n'; + + return sRow + u' </tr>\n'; + + @staticmethod + def generateTimeNavigationComboBox(sWhere, dParams, tsEffective): + """ + Generates the HTML for the xxxx ago combo box form. + """ + sNavigation = '<form name="TmTimeNavCombo-%s" method="GET">\n' % (sWhere,); + sNavigation += ' <select name="%s" onchange="window.location=' % (WuiDispatcherBase.ksParamEffectiveDate); + sNavigation += '\'?%s&%s=\' + ' % (webutils.encodeUrlParams(dParams), WuiDispatcherBase.ksParamEffectiveDate) + sNavigation += 'this.options[this.selectedIndex].value;" title="Effective date">\n'; + + aoWayBackPoints = [ + ('+0000-00-00 00:00:00.00', 'Now', ' title="Present Day. Present Time."'), # lain :) + + ('-0000-00-00 01:00:00.00', '1 hour ago', ''), + ('-0000-00-00 02:00:00.00', '2 hours ago', ''), + ('-0000-00-00 03:00:00.00', '3 hours ago', ''), + + ('-0000-00-01 00:00:00.00', '1 day ago', ''), + ('-0000-00-02 00:00:00.00', '2 days ago', ''), + ('-0000-00-03 00:00:00.00', '3 days ago', ''), + + ('-0000-00-07 00:00:00.00', '1 week ago', ''), + ('-0000-00-14 00:00:00.00', '2 weeks ago', ''), + ('-0000-00-21 00:00:00.00', '3 weeks ago', ''), + + ('-0000-01-00 00:00:00.00', '1 month ago', ''), + ('-0000-02-00 00:00:00.00', '2 months ago', ''), + ('-0000-03-00 00:00:00.00', '3 months ago', ''), + ('-0000-04-00 00:00:00.00', '4 months ago', ''), + ('-0000-05-00 00:00:00.00', '5 months ago', ''), + ('-0000-06-00 00:00:00.00', 'Half a year ago', ''), + + ('-0001-00-00 00:00:00.00', '1 year ago', ''), + ] + fSelected = False; + for sTimestamp, sWayBackPointCaption, sExtraAttrs in aoWayBackPoints: + if sTimestamp == tsEffective: + fSelected = True; + sNavigation += ' <option value="%s"%s%s>%s</option>\n' \ + % (webutils.quoteUrl(sTimestamp), + ' selected="selected"' if sTimestamp == tsEffective else '', + sExtraAttrs, sWayBackPointCaption, ); + if not fSelected and tsEffective != '': + sNavigation += ' <option value="%s" selected>%s</option>\n' \ + % (webutils.quoteUrl(tsEffective), WuiContentBase.formatTsShort(tsEffective)) + + sNavigation += ' </select>\n' \ + '</form>\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 = '<form method="get" action="?" onchange="timeNavigationUpdateHiddenEffDate(this,\'%s\')">' % (sWhere,); + if sNow is None: + sNow = utils.getIsoTimestamp(); + else: + sNow = utils.normalizeIsoTimestampToZulu(sNow); + asSplit = sNow.split('T'); + sNavigation += ' <input type="date" value="%s" id="EffDate%s"/> ' % (asSplit[0], sWhere, ); + sNavigation += ' <input type="time" value="%s" id="EffTime%s"/> ' % (asSplit[1][:8], sWhere,); + sNavigation += ' <input type="hidden" name="%s" value="%s" id="EffDateTime%s"/>' \ + % (WuiDispatcherBase.ksParamEffectiveDate, webutils.escapeAttr(sNow), sWhere); + for sKey in dParams: + sNavigation += ' <input type="hidden" name="%s" value="%s"/>' \ + % (webutils.escapeAttr(sKey), webutils.escapeAttrToStr(dParams[sKey])); + sNavigation += ' <input type="submit" value="Set"/>\n' \ + '</form>\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 = '<div class="tmtimenav-%s tmtimenav">%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</div>' % (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 = '<form name="TmItemsPerPageForm-%s" method="GET" class="tmitemsperpage-%s tmitemsperpage">\n'\ + ' <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \ + 'this.options[this.selectedIndex].value;" title="Max items per page">\n' \ + % (sWhere, WuiDispatcherBase.ksParamItemsPerPage, sWhere, + webutils.encodeUrlParams(dParams), + WuiDispatcherBase.ksParamItemsPerPage) + + acItemsPerPage = [16, 32, 64, 128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096]; + for cItemsPerPage in acItemsPerPage: + sHtmlItemsPerPageSelector += ' <option value="%d" %s>%d per page</option>\n' \ + % (cItemsPerPage, + 'selected="selected"' if cItemsPerPage == cCurItemsPerPage else '', + cItemsPerPage) + sHtmlItemsPerPageSelector += ' </select>\n' \ + '</form>\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 = '<div class="tmlistnav-%s">\n' % sWhere; + sNavigation += ' <table class="tmlistnavtab">\n' \ + ' <tr>\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 += ' <td align="left"><a href="?%s">Previous</a></td>\n' % (webutils.encodeUrlParams(dParams),); + else: + sNavigation += ' <td></td>\n'; + + # Time scale. + if self._fTimeNavigation: + sNavigation += '<td align="center" class="tmtimenav">'; + sNavigation += self._generateTimeNavigation(sWhere); + sNavigation += '</td>'; + + # page count and next. + sNavigation += '<td align="right" class="tmnextanditemsperpage">\n'; + + if len(self._aoEntries) > self._cItemsPerPage: + dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage + 1; + sNavigation += ' <a href="?%s">Next</a>\n' % (webutils.encodeUrlParams(dParams),); + sNavigation += self.generateItemPerPageSelector(sWhere, dParams, self._cItemsPerPage); + sNavigation += '</td>\n'; + sNavigation += ' </tr>\n' \ + ' </table>\n' \ + '</div>\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 = ' <thead class="tmheader"><tr>'; + for iHeader, oHeader in enumerate(self._asColumnHeaders): + if isinstance(oHeader, WuiHtmlBase): + sHtml += '<th>' + oHeader.toHtml() + '</th>'; + elif iHeader < len(self._aaiColumnSorting) and self._aaiColumnSorting[iHeader] is not None: + sHtml += '<th>' + iSorting = self._checkSortingByColumnAscending(self._aaiColumnSorting[iHeader]); + if iSorting > 0: + sDirection = ' ▴' if iSorting == 1 else '<small> ▵</small>'; + sSortParams = ','.join([str(-i) for i in self._aaiColumnSorting[iHeader]]); + else: + sDirection = ''; + if iSorting < 0: + sDirection = ' ▾' if iSorting == -1 else '<small> ▿</small>' + sSortParams = ','.join([str(i) for i in self._aaiColumnSorting[iHeader]]); + sHtml += '<a href="javascript:ahrefActionSortByColumns(\'%s\',[%s]);">' \ + % (WuiDispatcherBase.ksParamSortColumns, sSortParams); + sHtml += webutils.escapeElem(oHeader) + '</a>' + sDirection + '</th>'; + else: + sHtml += '<th>' + webutils.escapeElem(oHeader) + '</th>'; + sHtml += '</tr><thead>\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 = '<table class="tmtable" id="' + self._sId + '" cellspacing="0">\n'; + + if not self._asColumnHeaders: + self._asColumnHeaders = self._aoEntries[0].getDataAttributes(); + + sPageBody += self._generateTableHeaders(); + + # + # Format the body and close the table. + # + sPageBody += ' <tbody>\n'; + for iEntry in range(min(len(self._aoEntries), self._cItemsPerPage)): + sPageBody += self._formatListEntryHtml(iEntry); + sPageBody += ' </tbody>\n' \ + '</table>\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 += '<p>No entries.</p>' + + 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('<input type="checkbox" onClick="toggle%s(this)">' + % ('' 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('<input type="checkbox" name="%s" value="%s">' + % (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 = '<script language="JavaScript">\n' \ + 'function toggle%s(oSource) {\n' \ + ' aoCheckboxes = document.getElementsByName(\'%s\');\n' \ + ' for(var i in aoCheckboxes)\n' \ + ' aoCheckboxes[i].checked = oSource.checked;\n' \ + '}\n' \ + '</script>\n' \ + % ('' if self._sId is None else self._sId, self._sCheckboxName,); + if fShowNavigation: + sPageBody += self._generateNavigation('top'); + if self._aoEntries: + + sPageBody += '<form action="?%s" method="post" class="tmlistactionform">\n' \ + % (webutils.encodeUrlParams({WuiDispatcherBase.ksParamAction: self._sAction,}),); + sPageBody += self._generateTable(); + + sPageBody += ' <label>Actions</label>\n' \ + ' <select name="%s" id="%s-action-combo" class="tmlistactionform-combo">\n' \ + % (webutils.escapeAttr(WuiDispatcherBase.ksParamListAction), webutils.escapeAttr(self._sId),); + for oValue, sText, _ in self._aoActions: + sPageBody += ' <option value="%s">%s</option>\n' \ + % (webutils.escapeAttr(unicode(oValue)), webutils.escapeElem(sText), ); + sPageBody += ' </select>\n'; + sPageBody += ' <input type="submit"></input>\n'; + sPageBody += '</form>\n'; + if fShowNavigation: + sPageBody += self._generateNavigation('bottom'); + else: + sPageBody += '<p>No entries.</p>' + + return (self._composeTitle(), sPageBody); + |