# -*- coding: utf-8 -*- # $Id: wuicontentbase.py 76553 2019-01-01 01:45:53Z vboxsync $ """ Test Manager Web-UI - Content Base Classes. """ __copyright__ = \ """ Copyright (C) 2012-2019 Oracle Corporation This file is part of VirtualBox Open Source Edition (OSE), as available from http://www.virtualbox.org. This file is free software; you can redistribute it and/or modify it under the terms of the GNU General Public License (GPL) as published by the Free Software Foundation, in version 2 as it comes in the "COPYING" file of the VirtualBox OSE distribution. VirtualBox OSE is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY of any kind. The contents of this file may alternatively be used under the terms of the Common Development and Distribution License Version 1.0 (CDDL) only, as it comes in the "COPYING.CDDL" file of the VirtualBox OSE 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. """ __version__ = "$Revision: 76553 $" # Standard python imports. import copy; import sys; # Validation Kit imports. from common import 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=R0903 """ Base class for HTML objects. """ def __init__(self): """Dummy init to shut up pylint.""" 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=R0903 """ 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)); class WuiTmLink(WuiLinkBase): # pylint: disable=R0903 """ Local link to the test manager. """ kdDbgParams = None; 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=R0903 """ 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 = dict(); 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=R0903 """ 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 = dict(); 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=R0903 """ 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=R0903 """ 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=R0903 """ 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=R0903 """ 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=R0903 """ 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=R0903 """ Outputs the given element text. """ def __init__(self, sText): WuiRawHtml.__init__(self, webutils.escapeElem(sText)); class WuiContentBase(object): # pylint: disable=R0903 """ 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'\u2318' ## 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'\u2397' ## HTML hex entity string for ksShortReportLink. ksShortReportLinkHtml = '⎗' 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=R0903 """ 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=R0903 """ 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): """ Formats one change log entry into one or more HTML table rows. 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]; # 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, webutils.escapeElem(WuiFormContentBase.formatTsShort(oEntry.tsEffective)), 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
\n' \ % ( sRowClass, 'odd' if j & 1 else 'even', webutils.escapeElem(oChange.sAttr), webutils.escapeElem(oChange.sOldText), webutils.escapeElem(oChange.sNewText), ); 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'; for iEntry, _ in enumerate(aoEntries): sContent += self.formatChangeLogEntry(aoEntries, iEntry); 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. dParams = self._oDisp.getParameters(); if hasattr(oData, 'ksIdGenAttr'): sIdGenParam = getattr(oData, 'ksParam_' + oData.ksIdGenAttr); if sIdGenParam in dParams: del dParams[sIdGenParam]; if WuiDispatcherBase.ksParamEffectiveDate in dParams: del dParams[WuiDispatcherBase.ksParamEffectiveDate]; 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. if hasattr(self._oDisp, self._sActionBase + 'Clone'): dParams = self._oDisp.getParameters(); 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 dict(), 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 = True if sListItem in asSelectedItems else False 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): 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._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'; def _generateTimeNavigation(self, sWhere): """ Returns HTML for time navigation. Note! Views without a need for a timescale just stubs this method. """ _ = sWhere; sNavigation = ''; dParams = self._oDisp.getParameters(); dParams[WuiDispatcherBase.ksParamItemsPerPage] = self._cItemsPerPage; dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage; if WuiDispatcherBase.ksParamEffectiveDate in dParams: del dParams[WuiDispatcherBase.ksParamEffectiveDate]; sNavigation += ' [Now]' % (webutils.encodeUrlParams(dParams),); dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 01:00:00.00'; sNavigation += ' [1' % (webutils.encodeUrlParams(dParams),); dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 02:00:00.00'; sNavigation += ', 2' % (webutils.encodeUrlParams(dParams),); dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 06:00:00.00'; sNavigation += ', 6' % (webutils.encodeUrlParams(dParams),); dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 12:00:00.00'; sNavigation += ', 12' % (webutils.encodeUrlParams(dParams),); dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-01 00:00:00.00'; sNavigation += ', or 24 hours ago]' % (webutils.encodeUrlParams(dParams),); dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-02 00:00:00.00'; sNavigation += ' [2' % (webutils.encodeUrlParams(dParams),); dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-03 00:00:00.00'; sNavigation += ', 3' % (webutils.encodeUrlParams(dParams),); dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-05 00:00:00.00'; sNavigation += ', 5' % (webutils.encodeUrlParams(dParams),); dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-07 00:00:00.00'; sNavigation += ', 7' % (webutils.encodeUrlParams(dParams),); dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-14 00:00:00.00'; sNavigation += ', 14' % (webutils.encodeUrlParams(dParams),); dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-21 00:00:00.00'; sNavigation += ', 21' % (webutils.encodeUrlParams(dParams),); dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-28 00:00:00.00'; sNavigation += ', or 28 days ago]' % (webutils.encodeUrlParams(dParams),); return sNavigation; 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. sNavigation += ''; # Next if len(self._aoEntries) > self._cItemsPerPage: dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage + 1; sNavigation += ' \n' % (webutils.encodeUrlParams(dParams),); else: sNavigation += ' \n'; sNavigation += ' \n' \ '
Previous'; sNavigation += self._generateTimeNavigation(sWhere); sNavigation += 'Next
\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]); 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._tsEffectiveDate); ## @todo shorten this. 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);