# -*- 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 = '
%s
%s
\n' \ ' \n' \ ' | \n'; # Next if fMoreEntries: dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo + 1; sNavigation += 'Next | \n' \ % (webutils.encodeUrlParams(dParams),); else: sNavigation += 'Next | \n'; sNavigation += '
When | \n' \ 'Expire (excl) | \n' \ 'Changes | \n' \ '||
---|---|---|---|---|
Attribute | \n' \ 'Old value | \n' \ 'New value | \n' \ '
%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 += 'Previous | \n' % (webutils.encodeUrlParams(dParams),); else: sNavigation += '\n'; # Time scale. sNavigation += ' | '; sNavigation += self._generateTimeNavigation(sWhere); sNavigation += ' | '; # Next if len(self._aoEntries) > self._cItemsPerPage: dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage + 1; sNavigation += 'Next | \n' % (webutils.encodeUrlParams(dParams),); else: sNavigation += '\n'; sNavigation += ' |
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'; if fShowNavigation: sPageBody += self._generateNavigation('bottom'); else: sPageBody += 'No entries.
' return (self._composeTitle(), sPageBody);