VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testmanager/webui/wuicontentbase.py@ 65042

Last change on this file since 65042 was 65040, checked in by vboxsync, 8 years ago

testmanager: More details in the system wide changelog.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 44.0 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: wuicontentbase.py 65040 2016-12-31 02:29:50Z vboxsync $
3
4"""
5Test Manager Web-UI - Content Base Classes.
6"""
7
8__copyright__ = \
9"""
10Copyright (C) 2012-2016 Oracle Corporation
11
12This file is part of VirtualBox Open Source Edition (OSE), as
13available from http://www.virtualbox.org. This file is free software;
14you can redistribute it and/or modify it under the terms of the GNU
15General Public License (GPL) as published by the Free Software
16Foundation, in version 2 as it comes in the "COPYING" file of the
17VirtualBox OSE distribution. VirtualBox OSE is distributed in the
18hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
19
20The contents of this file may alternatively be used under the terms
21of the Common Development and Distribution License Version 1.0
22(CDDL) only, as it comes in the "COPYING.CDDL" file of the
23VirtualBox OSE distribution, in which case the provisions of the
24CDDL are applicable instead of those of the GPL.
25
26You may elect to license modified versions of this file under the
27terms and conditions of either the GPL or the CDDL or both.
28"""
29__version__ = "$Revision: 65040 $"
30
31
32# Standard python imports.
33import copy;
34
35# Validation Kit imports.
36from common import webutils;
37from testmanager import config;
38from testmanager.webui.wuibase import WuiDispatcherBase, WuiException;
39from testmanager.webui.wuihlpform import WuiHlpForm;
40from testmanager.core import db;
41from testmanager.core.base import AttributeChangeEntryPre;
42
43
44class WuiHtmlBase(object): # pylint: disable=R0903
45 """
46 Base class for HTML objects.
47 """
48
49 def __init__(self):
50 """Dummy init to shut up pylint."""
51 pass;
52
53 def toHtml(self):
54 """
55 Must be overridden by sub-classes.
56 """
57 assert False;
58 return '';
59
60 def __str__(self):
61 """ String representation is HTML, simplifying formatting and such. """
62 return self.toHtml();
63
64
65class WuiLinkBase(WuiHtmlBase): # pylint: disable=R0903
66 """
67 For passing links from WuiListContentBase._formatListEntry.
68 """
69
70 def __init__(self, sName, sUrlBase, dParams = None, sConfirm = None, sTitle = None,
71 sFragmentId = None, fBracketed = True, sExtraAttrs = ''):
72 WuiHtmlBase.__init__(self);
73 self.sName = sName
74 self.sUrl = sUrlBase
75 self.sConfirm = sConfirm;
76 self.sTitle = sTitle;
77 self.fBracketed = fBracketed;
78 self.sExtraAttrs = sExtraAttrs;
79
80 if dParams is not None and len(dParams) > 0:
81 # Do some massaging of None arguments.
82 dParams = dict(dParams);
83 for sKey in dParams:
84 if dParams[sKey] is None:
85 dParams[sKey] = '';
86 self.sUrl += '?' + webutils.encodeUrlParams(dParams)
87
88 if sFragmentId is not None:
89 self.sUrl += '#' + sFragmentId;
90
91 def setBracketed(self, fBracketed):
92 """Changes the bracketing style."""
93 self.fBracketed = fBracketed;
94 return True;
95
96 def toHtml(self):
97 """
98 Returns a simple HTML anchor element.
99 """
100 sExtraAttrs = self.sExtraAttrs;
101 if self.sConfirm is not None:
102 sExtraAttrs += 'onclick=\'return confirm("%s");\' ' % (webutils.escapeAttr(self.sConfirm),);
103 if self.sTitle is not None:
104 sExtraAttrs += 'title="%s" ' % (webutils.escapeAttr(self.sTitle),);
105 if len(sExtraAttrs) > 0 and sExtraAttrs[-1] != ' ':
106 sExtraAttrs += ' ';
107
108 sFmt = '[<a %shref="%s">%s</a>]';
109 if not self.fBracketed:
110 sFmt = '<a %shref="%s">%s</a>';
111 return sFmt % (sExtraAttrs, webutils.escapeAttr(self.sUrl), webutils.escapeElem(self.sName));
112
113
114class WuiTmLink(WuiLinkBase): # pylint: disable=R0903
115 """ Local link to the test manager. """
116
117 kdDbgParams = None;
118
119 def __init__(self, sName, sUrlBase, dParams = None, sConfirm = None, sTitle = None,
120 sFragmentId = None, fBracketed = True):
121
122 # Add debug parameters if necessary.
123 if self.kdDbgParams is not None and len(self.kdDbgParams) > 0:
124 if dParams is None or len(dParams) == 0:
125 dParams = dict(self.kdDbgParams);
126 else:
127 dParams = dict(dParams);
128 for sKey in self.kdDbgParams:
129 if sKey not in dParams:
130 dParams[sKey] = self.kdDbgParams[sKey];
131
132 WuiLinkBase.__init__(self, sName, sUrlBase, dParams, sConfirm, sTitle, sFragmentId, fBracketed);
133
134
135class WuiAdminLink(WuiTmLink): # pylint: disable=R0903
136 """ Local link to the test manager's admin portion. """
137
138 def __init__(self, sName, sAction, tsEffectiveDate = None, dParams = None, sConfirm = None, sTitle = None,
139 sFragmentId = None, fBracketed = True):
140 from testmanager.webui.wuiadmin import WuiAdmin;
141 if dParams is None or len(dParams) == 0:
142 dParams = dict();
143 else:
144 dParams = dict(dParams);
145 if sAction is not None:
146 dParams[WuiAdmin.ksParamAction] = sAction;
147 if tsEffectiveDate is not None:
148 dParams[WuiAdmin.ksParamEffectiveDate] = tsEffectiveDate;
149 WuiTmLink.__init__(self, sName, WuiAdmin.ksScriptName, dParams = dParams, sConfirm = sConfirm, sTitle = sTitle,
150 sFragmentId = sFragmentId, fBracketed = fBracketed);
151
152class WuiMainLink(WuiTmLink): # pylint: disable=R0903
153 """ Local link to the test manager's main portion. """
154
155 def __init__(self, sName, sAction, dParams = None, sConfirm = None, sTitle = None,
156 sFragmentId = None, fBracketed = True):
157 from testmanager.webui.wuimain import WuiMain;
158 if dParams is None or len(dParams) == 0:
159 dParams = dict();
160 else:
161 dParams = dict(dParams);
162 if sAction is not None:
163 dParams[WuiMain.ksParamAction] = sAction;
164 WuiTmLink.__init__(self, sName, WuiMain.ksScriptName, dParams = dParams, sConfirm = sConfirm, sTitle = sTitle,
165 sFragmentId = sFragmentId, fBracketed = fBracketed);
166
167class WuiSvnLink(WuiLinkBase): # pylint: disable=R0903
168 """
169 For linking to a SVN revision.
170 """
171 def __init__(self, iRevision, sName = None, fBracketed = True, sExtraAttrs = ''):
172 if sName is None:
173 sName = 'r%s' % (iRevision,);
174 WuiLinkBase.__init__(self, sName, config.g_ksTracLogUrlPrefix, { 'rev': iRevision,},
175 fBracketed = fBracketed, sExtraAttrs = sExtraAttrs);
176
177class WuiSvnLinkWithTooltip(WuiSvnLink): # pylint: disable=R0903
178 """
179 For linking to a SVN revision with changelog tooltip.
180 """
181 def __init__(self, iRevision, sRepository, sName = None, fBracketed = True):
182 sExtraAttrs = ' onmouseover="return svnHistoryTooltipShow(event,\'%s\',%s);" onmouseout="return tooltipHide();"' \
183 % ( sRepository, iRevision, );
184 WuiSvnLink.__init__(self, iRevision, sName = sName, fBracketed = fBracketed, sExtraAttrs = sExtraAttrs);
185
186class WuiBuildLogLink(WuiLinkBase):
187 """
188 For linking to a build log.
189 """
190 def __init__(self, sUrl, sName = None, fBracketed = True):
191 assert sUrl is not None; assert len(sUrl) > 0;
192 if sName is None:
193 sName = 'Build log';
194 if not webutils.hasSchema(sUrl):
195 WuiLinkBase.__init__(self, sName, config.g_ksBuildLogUrlPrefix + sUrl, fBracketed = fBracketed);
196 else:
197 WuiLinkBase.__init__(self, sName, sUrl, fBracketed = fBracketed);
198
199class WuiRawHtml(WuiHtmlBase): # pylint: disable=R0903
200 """
201 For passing raw html from WuiListContentBase._formatListEntry.
202 """
203 def __init__(self, sHtml):
204 self.sHtml = sHtml;
205 WuiHtmlBase.__init__(self);
206
207 def toHtml(self):
208 return self.sHtml;
209
210class WuiHtmlKeeper(WuiHtmlBase): # pylint: disable=R0903
211 """
212 For keeping a list of elements, concatenating their toHtml output together.
213 """
214 def __init__(self, aoInitial = None, sSep = ' '):
215 WuiHtmlBase.__init__(self);
216 self.sSep = sSep;
217 self.aoKept = [];
218 if aoInitial is not None:
219 if isinstance(aoInitial, WuiHtmlBase):
220 self.aoKept.append(aoInitial);
221 else:
222 self.aoKept.extend(aoInitial);
223
224 def append(self, oObject):
225 """ Appends one objects. """
226 self.aoKept.append(oObject);
227
228 def extend(self, aoObjects):
229 """ Appends a list of objects. """
230 self.aoKept.extend(aoObjects);
231
232 def toHtml(self):
233 return self.sSep.join(oObj.toHtml() for oObj in self.aoKept);
234
235class WuiSpanText(WuiRawHtml): # pylint: disable=R0903
236 """
237 Outputs the given text within a span of the given CSS class.
238 """
239 def __init__(self, sSpanClass, sText, sTitle = None):
240 if sTitle is None:
241 WuiRawHtml.__init__(self,
242 u'<span class="%s">%s</span>'
243 % ( webutils.escapeAttr(sSpanClass), webutils.escapeElem(sText),));
244 else:
245 WuiRawHtml.__init__(self,
246 u'<span class="%s" title="%s">%s</span>'
247 % ( webutils.escapeAttr(sSpanClass), webutils.escapeAttr(sTitle), webutils.escapeElem(sText),));
248
249class WuiElementText(WuiRawHtml): # pylint: disable=R0903
250 """
251 Outputs the given element text.
252 """
253 def __init__(self, sText):
254 WuiRawHtml.__init__(self, webutils.escapeElem(sText));
255
256
257class WuiContentBase(object): # pylint: disable=R0903
258 """
259 Base for the content classes.
260 """
261
262 ## The text/symbol for a very short add link.
263 ksShortAddLink = u'\u2795'
264 ## HTML hex entity string for ksShortAddLink.
265 ksShortAddLinkHtml = '&#x2795;;'
266 ## The text/symbol for a very short edit link.
267 ksShortEditLink = u'\u270D'
268 ## HTML hex entity string for ksShortDetailsLink.
269 ksShortEditLinkHtml = '&#x270d;'
270 ## The text/symbol for a very short details link.
271 ksShortDetailsLink = u'\u2318'
272 ## HTML hex entity string for ksShortDetailsLink.
273 ksShortDetailsLinkHtml = '&#x2318;'
274 ## The text/symbol for a very short change log / details / previous page link.
275 ksShortChangeLogLink = u'\u2397'
276 ## HTML hex entity string for ksShortDetailsLink.
277 ksShortChangeLogLinkHtml = '&#x2397;'
278 ## The text/symbol for a very short reports link.
279 ksShortReportLink = u'\u2397'
280 ## HTML hex entity string for ksShortReportLink.
281 ksShortReportLinkHtml = '&#x2397;'
282
283
284 def __init__(self, fnDPrint = None, oDisp = None):
285 self._oDisp = oDisp; # WuiDispatcherBase.
286 self._fnDPrint = fnDPrint;
287 if fnDPrint is None and oDisp is not None:
288 self._fnDPrint = oDisp.dprint;
289
290 def dprint(self, sText):
291 """ Debug printing. """
292 if self._fnDPrint:
293 self._fnDPrint(sText);
294
295 @staticmethod
296 def formatTsShort(oTs):
297 """
298 Formats a timestamp (db rep) into a short form.
299 """
300 oTsZulu = db.dbTimestampToZuluDatetime(oTs);
301 sTs = oTsZulu.strftime('%Y-%m-%d %H:%M:%SZ');
302 return unicode(sTs).replace('-', u'\u2011').replace(' ', u'\u00a0');
303
304 def getNowTs(self):
305 """ Gets a database compatible current timestamp from python. See db.dbTimestampPythonNow(). """
306 return db.dbTimestampPythonNow();
307
308 def formatIntervalShort(self, oInterval):
309 """
310 Formats an interval (db rep) into a short form.
311 """
312 # default formatting for negative intervals.
313 if oInterval.days < 0:
314 return str(oInterval);
315
316 # Figure the hour, min and sec counts.
317 cHours = oInterval.seconds / 3600;
318 cMinutes = (oInterval.seconds % 3600) / 60;
319 cSeconds = oInterval.seconds - cHours * 3600 - cMinutes * 60;
320
321 # Tailor formatting to the interval length.
322 if oInterval.days > 0:
323 if oInterval.days > 1:
324 return '%d days, %d:%02d:%02d' % (oInterval.days, cHours, cMinutes, cSeconds);
325 return '1 day, %d:%02d:%02d' % (cHours, cMinutes, cSeconds);
326 if cMinutes > 0 or cSeconds >= 30 or cHours > 0:
327 return '%d:%02d:%02d' % (cHours, cMinutes, cSeconds);
328 if cSeconds >= 10:
329 return '%d.%ds' % (cSeconds, oInterval.microseconds / 100000);
330 if cSeconds > 0:
331 return '%d.%02ds' % (cSeconds, oInterval.microseconds / 10000);
332 return '%d ms' % (oInterval.microseconds / 1000,);
333
334 @staticmethod
335 def genericPageWalker(iCurItem, cItems, sHrefFmt, cWidth = 11, iBase = 1, sItemName = 'page'):
336 """
337 Generic page walker generator.
338
339 sHrefFmt has three %s sequences:
340 1. The first is the page number link parameter (0-based).
341 2. The title text, iBase-based number or text.
342 3. The link text, iBase-based number or text.
343 """
344
345 # Calc display range.
346 iStart = 0 if iCurItem - cWidth / 2 <= cWidth / 4 else iCurItem - cWidth / 2;
347 iEnd = iStart + cWidth;
348 if iEnd > cItems:
349 iEnd = cItems;
350 if cItems > cWidth:
351 iStart = cItems - cWidth;
352
353 sHtml = u'';
354
355 # Previous page (using << >> because &laquo; and &raquo are too tiny).
356 if iCurItem > 0:
357 sHtml += '%s&nbsp;&nbsp;' % sHrefFmt % (iCurItem - 1, 'previous ' + sItemName, '&lt;&lt;');
358 else:
359 sHtml += '&lt;&lt;&nbsp;&nbsp;';
360
361 # 1 2 3 4...
362 if iStart > 0:
363 sHtml += '%s&nbsp; ... &nbsp;\n' % (sHrefFmt % (0, 'first %s' % (sItemName,), 0 + iBase),);
364
365 sHtml += '&nbsp;\n'.join(sHrefFmt % (i, '%s %d' % (sItemName, i + iBase), i + iBase) if i != iCurItem
366 else unicode(i + iBase)
367 for i in range(iStart, iEnd));
368 if iEnd < cItems:
369 sHtml += '&nbsp; ... &nbsp;%s\n' % (sHrefFmt % (cItems - 1, 'last %s' % (sItemName,), cItems - 1 + iBase));
370
371 # Next page.
372 if iCurItem + 1 < cItems:
373 sHtml += '&nbsp;&nbsp;%s' % sHrefFmt % (iCurItem + 1, 'next ' + sItemName, '&gt;&gt;');
374 else:
375 sHtml += '&nbsp;&nbsp;&gt;&gt;';
376
377 return sHtml;
378
379class WuiSingleContentBase(WuiContentBase): # pylint: disable=R0903
380 """
381 Base for the content classes working on a single data object (oData).
382 """
383 def __init__(self, oData, oDisp = None, fnDPrint = None):
384 WuiContentBase.__init__(self, oDisp = oDisp, fnDPrint = fnDPrint);
385 self._oData = oData; # Usually ModelDataBase.
386
387
388class WuiFormContentBase(WuiSingleContentBase): # pylint: disable=R0903
389 """
390 Base class for simple input form content classes (single data object).
391 """
392
393 ## @name Form mode.
394 ## @{
395 ksMode_Add = 'add';
396 ksMode_Edit = 'edit';
397 ksMode_Show = 'show';
398 ## @}
399
400 ## Default action mappings.
401 kdSubmitActionMappings = {
402 ksMode_Add: 'AddPost',
403 ksMode_Edit: 'EditPost',
404 };
405
406 def __init__(self, oData, sMode, sCoreName, oDisp, sTitle, sId = None, fEditable = True, sSubmitAction = None):
407 WuiSingleContentBase.__init__(self, copy.copy(oData), oDisp);
408 assert sMode in [self.ksMode_Add, self.ksMode_Edit, self.ksMode_Show];
409 assert len(sTitle) > 1;
410 assert sId is None or len(sId) > 0;
411
412 self._sMode = sMode;
413 self._sCoreName = sCoreName;
414 self._sActionBase = 'ksAction' + sCoreName;
415 self._sTitle = sTitle;
416 self._sId = sId if sId is not None else (type(oData).__name__.lower() + 'form');
417 self._fEditable = fEditable;
418 self._sSubmitAction = sSubmitAction;
419 if sSubmitAction is None and sMode != self.ksMode_Show:
420 self._sSubmitAction = getattr(oDisp, self._sActionBase + self.kdSubmitActionMappings[sMode]);
421 self._sRedirectTo = None;
422
423
424 def _populateForm(self, oForm, oData):
425 """
426 Populates the form. oData has parameter NULL values.
427 This must be reimplemented by the child.
428 """
429 _ = oForm; _ = oData;
430 raise Exception('Reimplement me!');
431
432 def _generatePostFormContent(self, oData):
433 """
434 Generate optional content that comes below the form.
435 Returns a list of tuples, where the first tuple element is the title
436 and the second the content. I.e. similar to show() output.
437 """
438 _ = oData;
439 return [];
440
441 @staticmethod
442 def _calcChangeLogEntryLinks(aoEntries, iEntry):
443 """
444 Returns an array of links to go with the change log entry.
445 """
446 _ = aoEntries; _ = iEntry;
447 ## @todo detect deletion and recreation.
448 ## @todo view details link.
449 ## @todo restore link (need new action)
450 ## @todo clone link.
451 return [];
452
453 @staticmethod
454 def _guessChangeLogEntryDescription(aoEntries, iEntry):
455 """
456 Guesses the action + author that caused the change log entry.
457 Returns descriptive string.
458 """
459 oEntry = aoEntries[iEntry];
460
461 # Figure the author of the change.
462 if oEntry.sAuthor is not None:
463 sAuthor = '%s (#%s)' % (oEntry.sAuthor, oEntry.uidAuthor,);
464 elif oEntry.uidAuthor is not None:
465 sAuthor = '#%d (??)' % (oEntry.uidAuthor,);
466 else:
467 sAuthor = None;
468
469 # Figure the action.
470 if oEntry.oOldRaw is None:
471 if sAuthor is None:
472 return 'Created by batch job.';
473 return 'Created by %s.' % (sAuthor,);
474
475 if sAuthor is None:
476 return 'Automatically updated.'
477 return 'Modified by %s.' % (sAuthor,);
478
479 @staticmethod
480 def formatChangeLogEntry(aoEntries, iEntry):
481 """
482 Formats one change log entry into one or more HTML table rows.
483
484 Note! The parameters are given as array + index in case someone wishes
485 to access adjacent entries later in order to generate better
486 change descriptions.
487 """
488 oEntry = aoEntries[iEntry];
489
490 # The primary row.
491 sRowClass = 'tmodd' if (iEntry + 1) & 1 else 'tmeven';
492 sContent = ' <tr class="%s">\n' \
493 ' <td rowspan="%d">%s</td>\n' \
494 ' <td rowspan="%d">%s</td>\n' \
495 ' <td colspan="3">%s%s</td>\n' \
496 ' </tr>\n' \
497 % ( sRowClass,
498 len(oEntry.aoChanges) + 1, webutils.escapeElem(WuiFormContentBase.formatTsShort(oEntry.tsEffective)),
499 len(oEntry.aoChanges) + 1, webutils.escapeElem(WuiFormContentBase.formatTsShort(oEntry.tsExpire)),
500 WuiFormContentBase._guessChangeLogEntryDescription(aoEntries, iEntry),
501 ' '.join(oLink.toHtml() for oLink in WuiFormContentBase._calcChangeLogEntryLinks(aoEntries, iEntry)),);
502
503 # Additional rows for each changed attribute.
504 j = 0;
505 for oChange in oEntry.aoChanges:
506 if isinstance(oChange, AttributeChangeEntryPre):
507 sContent += ' <tr class="%s%s"><td>%s</td>'\
508 '<td><div class="tdpre"><pre>%s</pre></div></td>' \
509 '<td><div class="tdpre"><pre>%s</pre></div></td></tr>\n' \
510 % ( sRowClass, 'odd' if j & 1 else 'even',
511 webutils.escapeElem(oChange.sAttr),
512 webutils.escapeElem(oChange.sOldText),
513 webutils.escapeElem(oChange.sNewText), );
514 else:
515 sContent += ' <tr class="%s%s"><td>%s</td><td>%s</td><td>%s</td></tr>\n' \
516 % ( sRowClass, 'odd' if j & 1 else 'even',
517 webutils.escapeElem(oChange.sAttr),
518 webutils.escapeElem(oChange.sOldText),
519 webutils.escapeElem(oChange.sNewText), );
520 j += 1;
521
522 return sContent;
523
524 def _showChangeLogNavi(self, fMoreEntries, iPageNo, cEntriesPerPage, tsNow, sWhere):
525 """
526 Returns the HTML for the change log navigator.
527 Note! See also _generateNavigation.
528 """
529 sNavigation = '<div class="tmlistnav-%s">\n' % sWhere;
530 sNavigation += ' <table class="tmlistnavtab">\n' \
531 ' <tr>\n';
532 dParams = self._oDisp.getParameters();
533 dParams[WuiDispatcherBase.ksParamChangeLogEntriesPerPage] = cEntriesPerPage;
534 dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo;
535 if tsNow is not None:
536 dParams[WuiDispatcherBase.ksParamEffectiveDate] = tsNow;
537
538 # Prev and combo box in one cell. Both inside the form for formatting reasons.
539 sNavigation += ' <td align="left">\n' \
540 ' <form name="ChangeLogEntriesPerPageForm" method="GET">\n'
541
542 # Prev
543 if iPageNo > 0:
544 dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo - 1;
545 sNavigation += '<a href="?%s#tmchangelog">Previous</a>\n' \
546 % (webutils.encodeUrlParams(dParams),);
547 dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo;
548 else:
549 sNavigation += 'Previous\n';
550
551 # Entries per page selector.
552 del dParams[WuiDispatcherBase.ksParamChangeLogEntriesPerPage];
553 sNavigation += '&nbsp; &nbsp;\n' \
554 ' <select name="%s" onchange="window.location=\'?%s&%s=\' + ' \
555 'this.options[this.selectedIndex].value + \'#tmchangelog\';" ' \
556 'title="Max change log entries per page">\n' \
557 % (WuiDispatcherBase.ksParamChangeLogEntriesPerPage,
558 webutils.encodeUrlParams(dParams),
559 WuiDispatcherBase.ksParamChangeLogEntriesPerPage);
560 dParams[WuiDispatcherBase.ksParamChangeLogEntriesPerPage] = cEntriesPerPage;
561
562 for iEntriesPerPage in [2, 4, 8, 16, 32, 64, 128, 256, 384, 512, 768, 1024, 1536, 2048, 3072, 4096, 8192]:
563 sNavigation += ' <option value="%d" %s>%d entries per page</option>\n' \
564 % ( iEntriesPerPage,
565 'selected="selected"' if iEntriesPerPage == cEntriesPerPage else '',
566 iEntriesPerPage );
567 sNavigation += ' </select>\n';
568
569 # End of cell (and form).
570 sNavigation += ' </form>\n' \
571 ' </td>\n';
572
573 # Next
574 if fMoreEntries:
575 dParams[WuiDispatcherBase.ksParamChangeLogPageNo] = iPageNo + 1;
576 sNavigation += ' <td align="right"><a href="?%s#tmchangelog">Next</a></td>\n' \
577 % (webutils.encodeUrlParams(dParams),);
578 else:
579 sNavigation += ' <td align="right">Next</td>\n';
580
581 sNavigation += ' </tr>\n' \
582 ' </table>\n' \
583 '</div>\n';
584 return sNavigation;
585
586 def setRedirectTo(self, sRedirectTo):
587 """
588 For setting the hidden redirect-to field.
589 """
590 self._sRedirectTo = sRedirectTo;
591 return True;
592
593 def showChangeLog(self, aoEntries, fMoreEntries, iPageNo, cEntriesPerPage, tsNow, fShowNavigation = True):
594 """
595 Render the change log, returning raw HTML.
596 aoEntries is an array of ChangeLogEntry.
597 """
598 sContent = '\n' \
599 '<hr>\n' \
600 '<div id="tmchangelog">\n' \
601 ' <h3>Change Log </h3>\n';
602 if fShowNavigation:
603 sContent += self._showChangeLogNavi(fMoreEntries, iPageNo, cEntriesPerPage, tsNow, 'top');
604 sContent += ' <table class="tmtable tmchangelog">\n' \
605 ' <thead class="tmheader">' \
606 ' <tr>' \
607 ' <th rowspan="2">When</th>\n' \
608 ' <th rowspan="2">Expire (excl)</th>\n' \
609 ' <th colspan="3">Changes</th>\n' \
610 ' </tr>\n' \
611 ' <tr>\n' \
612 ' <th>Attribute</th>\n' \
613 ' <th>Old value</th>\n' \
614 ' <th>New value</th>\n' \
615 ' </tr>\n' \
616 ' </thead>\n' \
617 ' <tbody>\n';
618
619 for iEntry, _ in enumerate(aoEntries):
620 sContent += self.formatChangeLogEntry(aoEntries, iEntry);
621
622 sContent += ' <tbody>\n' \
623 ' </table>\n';
624 if fShowNavigation and len(aoEntries) >= 8:
625 sContent += self._showChangeLogNavi(fMoreEntries, iPageNo, cEntriesPerPage, tsNow, 'bottom');
626 sContent += '</div>\n\n';
627 return sContent;
628
629 def _generateTopRowFormActions(self, oData):
630 """
631 Returns a list of WuiTmLinks.
632 """
633 aoActions = [];
634 if self._sMode == self.ksMode_Show and self._fEditable:
635 # Remove _idGen and effective date since we're always editing the current data,
636 # and make sure the primary ID is present.
637 dParams = self._oDisp.getParameters();
638 if hasattr(oData, 'ksIdGenAttr'):
639 sIdGenParam = getattr(oData, 'ksParam_' + oData.ksIdGenAttr);
640 if sIdGenParam in dParams:
641 del dParams[sIdGenParam];
642 if WuiDispatcherBase.ksParamEffectiveDate in dParams:
643 del dParams[WuiDispatcherBase.ksParamEffectiveDate];
644 dParams[getattr(oData, 'ksParam_' + oData.ksIdAttr)] = getattr(oData, oData.ksIdAttr);
645
646 dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Edit');
647 aoActions.append(WuiTmLink('Edit', '', dParams));
648
649 # Add clone operation if available. This uses the same data selection as for showing details.
650 if hasattr(self._oDisp, self._sActionBase + 'Clone'):
651 dParams = self._oDisp.getParameters();
652 dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Clone');
653 aoActions.append(WuiTmLink('Clone', '', dParams));
654
655 elif self._sMode == self.ksMode_Edit:
656 # Details views the details at a given time, so we need either idGen or an effecive date + regular id.
657 dParams = {};
658 if hasattr(oData, 'ksIdGenAttr'):
659 sIdGenParam = getattr(oData, 'ksParam_' + oData.ksIdGenAttr);
660 dParams[sIdGenParam] = getattr(oData, oData.ksIdGenAttr);
661 elif hasattr(oData, 'tsEffective'):
662 dParams[WuiDispatcherBase.ksParamEffectiveDate] = oData.tsEffective;
663 dParams[getattr(oData, 'ksParam_' + oData.ksIdAttr)] = getattr(oData, oData.ksIdAttr);
664 dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'Details');
665 aoActions.append(WuiTmLink('Details', '', dParams));
666
667 # Add delete operation if available.
668 if hasattr(self._oDisp, self._sActionBase + 'DoRemove'):
669 dParams = self._oDisp.getParameters();
670 dParams[WuiDispatcherBase.ksParamAction] = getattr(self._oDisp, self._sActionBase + 'DoRemove');
671 dParams[getattr(oData, 'ksParam_' + oData.ksIdAttr)] = getattr(oData, oData.ksIdAttr);
672 aoActions.append(WuiTmLink('Delete', '', dParams, sConfirm = "Are you absolutely sure?"));
673
674 return aoActions;
675
676 def showForm(self, dErrors = None, sErrorMsg = None):
677 """
678 Render the form.
679 """
680 oForm = WuiHlpForm(self._sId,
681 '?' + webutils.encodeUrlParams({WuiDispatcherBase.ksParamAction: self._sSubmitAction}),
682 dErrors if dErrors is not None else dict(),
683 fReadOnly = self._sMode == self.ksMode_Show);
684
685 self._oData.convertToParamNull();
686
687 # If form cannot be constructed due to some reason we
688 # need to show this reason
689 try:
690 self._populateForm(oForm, self._oData);
691 if self._sRedirectTo is not None:
692 oForm.addTextHidden(self._oDisp.ksParamRedirectTo, self._sRedirectTo);
693 except WuiException, oXcpt:
694 sContent = unicode(oXcpt)
695 else:
696 sContent = oForm.finalize();
697
698 # Add any post form content.
699 atPostFormContent = self._generatePostFormContent(self._oData);
700 if atPostFormContent is not None and len(atPostFormContent) > 0:
701 for iSection, tSection in enumerate(atPostFormContent):
702 (sSectionTitle, sSectionContent) = tSection;
703 sContent += u'<div id="postform-%d" class="tmformpostsection">\n' % (iSection,);
704 if sSectionTitle is not None and len(sSectionTitle) > 0:
705 sContent += '<h3 class="tmformpostheader">%s</h3>\n' % (webutils.escapeElem(sSectionTitle),);
706 sContent += u' <div id="postform-%d-content" class="tmformpostcontent">\n' % (iSection,);
707 sContent += sSectionContent;
708 sContent += u' </div>\n' \
709 u'</div>\n';
710
711 # Add action to the top.
712 aoActions = self._generateTopRowFormActions(self._oData);
713 if len(aoActions) > 0:
714 sActionLinks = '<p>%s</p>' % (' '.join(unicode(oLink) for oLink in aoActions));
715 sContent = sActionLinks + sContent;
716
717 # Add error info to the top.
718 if sErrorMsg is not None:
719 sContent = '<p class="tmerrormsg">' + webutils.escapeElem(sErrorMsg) + '</p>\n' + sContent;
720
721 return (self._sTitle, sContent);
722
723 def getListOfItems(self, asListItems = tuple(), asSelectedItems = tuple()):
724 """
725 Format generic list which should be used by HTML form
726 """
727 aoRet = []
728 for sListItem in asListItems:
729 fEnabled = True if sListItem in asSelectedItems else False
730 aoRet.append((sListItem, fEnabled, sListItem))
731 return aoRet
732
733
734class WuiListContentBase(WuiContentBase):
735 """
736 Base for the list content classes.
737 """
738
739 def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle, sId = None, fnDPrint = None, oDisp = None):
740 WuiContentBase.__init__(self, fnDPrint = fnDPrint, oDisp = oDisp);
741 self._aoEntries = aoEntries; ## @todo should replace this with a Logic object and define methods for querying.
742 self._iPage = iPage;
743 self._cItemsPerPage = cItemsPerPage;
744 self._tsEffectiveDate = tsEffectiveDate;
745 self._sTitle = sTitle; assert len(sTitle) > 1;
746 if sId is None:
747 sId = sTitle.strip().replace(' ', '').lower();
748 assert len(sId.strip()) > 0;
749 self._sId = sId;
750 self._asColumnHeaders = [];
751 self._asColumnAttribs = [];
752
753 def _formatCommentCell(self, sComment, cMaxLines = 3, cchMaxLine = 63):
754 """
755 Helper functions for formatting comment cell.
756 Returns None or WuiRawHtml instance.
757 """
758 # Nothing to do for empty comments.
759 if sComment is None:
760 return None;
761 sComment = sComment.strip();
762 if len(sComment) == 0:
763 return None;
764
765 # Restrict the text if necessary, making the whole text available thru mouse-over.
766 ## @todo this would be better done by java script or smth, so it could automatically adjust to the table size.
767 if len(sComment) > cchMaxLine or sComment.count('\n') >= cMaxLines:
768 sShortHtml = '';
769 for iLine, sLine in enumerate(sComment.split('\n')):
770 if iLine >= cMaxLines:
771 break;
772 if iLine > 0:
773 sShortHtml += '<br>\n';
774 if len(sLine) > cchMaxLine:
775 sShortHtml += webutils.escapeElem(sLine[:(cchMaxLine - 3)]);
776 sShortHtml += '...';
777 else:
778 sShortHtml += webutils.escapeElem(sLine);
779 return WuiRawHtml('<span class="tmcomment" title="%s">%s</span>' % (webutils.escapeAttr(sComment), sShortHtml,));
780
781 return WuiRawHtml('<span class="tmcomment">%s</span>' % (webutils.escapeElem(sComment).replace('\n', '<br>'),));
782
783 def _formatListEntry(self, iEntry):
784 """
785 Formats the specified list entry as a list of column values.
786 Returns HTML for a table row.
787
788 The child class really need to override this!
789 """
790 # ASSUMES ModelDataBase children.
791 asRet = [];
792 for sAttr in self._aoEntries[0].getDataAttributes():
793 asRet.append(getattr(self._aoEntries[iEntry], sAttr));
794 return asRet;
795
796 def _formatListEntryHtml(self, iEntry):
797 """
798 Formats the specified list entry as HTML.
799 Returns HTML for a table row.
800
801 The child class can override this to
802 """
803 if (iEntry + 1) & 1:
804 sRow = u' <tr class="tmodd">\n';
805 else:
806 sRow = u' <tr class="tmeven">\n';
807
808 aoValues = self._formatListEntry(iEntry);
809 assert len(aoValues) == len(self._asColumnHeaders), '%s vs %s' % (len(aoValues), len(self._asColumnHeaders));
810
811 for i, _ in enumerate(aoValues):
812 if i < len(self._asColumnAttribs) and len(self._asColumnAttribs[i]) > 0:
813 sRow += u' <td ' + self._asColumnAttribs[i] + '>';
814 else:
815 sRow += u' <td>';
816
817 if isinstance(aoValues[i], WuiHtmlBase):
818 sRow += aoValues[i].toHtml();
819 elif isinstance(aoValues[i], list):
820 if len(aoValues[i]) > 0:
821 for oElement in aoValues[i]:
822 if isinstance(oElement, WuiHtmlBase):
823 sRow += oElement.toHtml();
824 elif db.isDbTimestamp(oElement):
825 sRow += webutils.escapeElem(self.formatTsShort(oElement));
826 else:
827 sRow += webutils.escapeElem(unicode(oElement));
828 sRow += ' ';
829 elif db.isDbTimestamp(aoValues[i]):
830 sRow += webutils.escapeElem(self.formatTsShort(aoValues[i]));
831 elif db.isDbInterval(aoValues[i]):
832 sRow += webutils.escapeElem(self.formatIntervalShort(aoValues[i]));
833 elif aoValues[i] is not None:
834 sRow += webutils.escapeElem(unicode(aoValues[i]));
835
836 sRow += u'</td>\n';
837
838 return sRow + u' </tr>\n';
839
840 def _generateTimeNavigation(self, sWhere):
841 """
842 Returns HTML for time navigation.
843
844 Note! Views without a need for a timescale just stubs this method.
845 """
846 _ = sWhere;
847 sNavigation = '';
848
849 dParams = self._oDisp.getParameters();
850 dParams[WuiDispatcherBase.ksParamItemsPerPage] = self._cItemsPerPage;
851 dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage;
852
853 if WuiDispatcherBase.ksParamEffectiveDate in dParams:
854 del dParams[WuiDispatcherBase.ksParamEffectiveDate];
855 sNavigation += ' [<a href="?%s">Now</a>]' % (webutils.encodeUrlParams(dParams),);
856
857 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 01:00:00.00';
858 sNavigation += ' [<a href="?%s">1</a>' % (webutils.encodeUrlParams(dParams),);
859
860 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 02:00:00.00';
861 sNavigation += ', <a href="?%s">2</a>' % (webutils.encodeUrlParams(dParams),);
862
863 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 06:00:00.00';
864 sNavigation += ', <a href="?%s">6</a>' % (webutils.encodeUrlParams(dParams),);
865
866 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 12:00:00.00';
867 sNavigation += ', <a href="?%s">12</a>' % (webutils.encodeUrlParams(dParams),);
868
869 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-01 00:00:00.00';
870 sNavigation += ', or <a href="?%s">24</a> hours ago]' % (webutils.encodeUrlParams(dParams),);
871
872
873 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-02 00:00:00.00';
874 sNavigation += ' [<a href="?%s">2</a>' % (webutils.encodeUrlParams(dParams),);
875
876 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-03 00:00:00.00';
877 sNavigation += ', <a href="?%s">3</a>' % (webutils.encodeUrlParams(dParams),);
878
879 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-05 00:00:00.00';
880 sNavigation += ', <a href="?%s">5</a>' % (webutils.encodeUrlParams(dParams),);
881
882 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-07 00:00:00.00';
883 sNavigation += ', <a href="?%s">7</a>' % (webutils.encodeUrlParams(dParams),);
884
885 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-14 00:00:00.00';
886 sNavigation += ', <a href="?%s">14</a>' % (webutils.encodeUrlParams(dParams),);
887
888 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-21 00:00:00.00';
889 sNavigation += ', <a href="?%s">21</a>' % (webutils.encodeUrlParams(dParams),);
890
891 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-28 00:00:00.00';
892 sNavigation += ', or <a href="?%s">28</a> days ago]' % (webutils.encodeUrlParams(dParams),);
893
894 return sNavigation;
895
896
897 def _generateNavigation(self, sWhere):
898 """
899 Return HTML for navigation.
900 """
901
902 #
903 # ASSUMES the dispatcher/controller code fetches one entry more than
904 # needed to fill the page to indicate further records.
905 #
906 sNavigation = '<div class="tmlistnav-%s">\n' % sWhere;
907 sNavigation += ' <table class="tmlistnavtab">\n' \
908 ' <tr>\n';
909 dParams = self._oDisp.getParameters();
910 dParams[WuiDispatcherBase.ksParamItemsPerPage] = self._cItemsPerPage;
911 dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage;
912 if self._tsEffectiveDate is not None:
913 dParams[WuiDispatcherBase.ksParamEffectiveDate] = self._tsEffectiveDate;
914
915 # Prev
916 if self._iPage > 0:
917 dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage - 1;
918 sNavigation += ' <td align="left"><a href="?%s">Previous</a></td>\n' % (webutils.encodeUrlParams(dParams),);
919 else:
920 sNavigation += ' <td></td>\n';
921
922 # Time scale.
923 sNavigation += '<td align="center" class="tmtimenav">';
924 sNavigation += self._generateTimeNavigation(sWhere);
925 sNavigation += '</td>';
926
927 # Next
928 if len(self._aoEntries) > self._cItemsPerPage:
929 dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage + 1;
930 sNavigation += ' <td align="right"><a href="?%s">Next</a></td>\n' % (webutils.encodeUrlParams(dParams),);
931 else:
932 sNavigation += ' <td></td>\n';
933
934 sNavigation += ' </tr>\n' \
935 ' </table>\n' \
936 '</div>\n';
937 return sNavigation;
938
939 def _generateTableHeaders(self):
940 """
941 Generate table headers.
942 Returns raw html string.
943 Overridable.
944 """
945
946 sHtml = ' <thead class="tmheader"><tr>';
947 for oHeader in self._asColumnHeaders:
948 if isinstance(oHeader, WuiHtmlBase):
949 sHtml += '<th>' + oHeader.toHtml() + '</th>';
950 else:
951 sHtml += '<th>' + webutils.escapeElem(oHeader) + '</th>';
952 sHtml += '</tr><thead>\n';
953 return sHtml
954
955 def _generateTable(self):
956 """
957 show worker that just generates the table.
958 """
959
960 #
961 # Create a table.
962 # If no colum headers are provided, fall back on database field
963 # names, ASSUMING that the entries are ModelDataBase children.
964 # Note! the cellspacing is for IE8.
965 #
966 sPageBody = '<table class="tmtable" id="' + self._sId + '" cellspacing="0">\n';
967
968 if len(self._asColumnHeaders) == 0:
969 self._asColumnHeaders = self._aoEntries[0].getDataAttributes();
970
971 sPageBody += self._generateTableHeaders();
972
973 #
974 # Format the body and close the table.
975 #
976 sPageBody += ' <tbody>\n';
977 for iEntry in range(min(len(self._aoEntries), self._cItemsPerPage)):
978 sPageBody += self._formatListEntryHtml(iEntry);
979 sPageBody += ' </tbody>\n' \
980 '</table>\n';
981 return sPageBody;
982
983 def _composeTitle(self):
984 """Composes the title string (return value)."""
985 sTitle = self._sTitle;
986 if self._iPage != 0:
987 sTitle += ' (page ' + unicode(self._iPage + 1) + ')'
988 if self._tsEffectiveDate is not None:
989 sTitle += ' as per ' + unicode(self._tsEffectiveDate); ## @todo shorten this.
990 return sTitle;
991
992
993 def show(self, fShowNavigation = True):
994 """
995 Displays the list.
996 Returns (Title, HTML) on success, raises exception on error.
997 """
998
999 sPageBody = ''
1000 if fShowNavigation:
1001 sPageBody += self._generateNavigation('top');
1002
1003 if len(self._aoEntries):
1004 sPageBody += self._generateTable();
1005 if fShowNavigation:
1006 sPageBody += self._generateNavigation('bottom');
1007 else:
1008 sPageBody += '<p>No entries.</p>'
1009
1010 return (self._composeTitle(), sPageBody);
1011
1012
1013class WuiListContentWithActionBase(WuiListContentBase):
1014 """
1015 Base for the list content with action classes.
1016 """
1017
1018 def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle, sId = None, fnDPrint = None, oDisp = None):
1019 WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle,
1020 sId = sId, fnDPrint = fnDPrint, oDisp = oDisp);
1021 self._aoActions = None; # List of [ oValue, sText, sHover ] provided by the child class.
1022 self._sAction = None; # Set by the child class.
1023 self._sCheckboxName = None; # Set by the child class.
1024 self._asColumnHeaders = [ WuiRawHtml('<input type="checkbox" onClick="toggle%s(this)">'
1025 % ('' if sId is None else sId)), ];
1026 self._asColumnAttribs = [ 'align="center"', ];
1027
1028 def _getCheckBoxColumn(self, iEntry, sValue):
1029 """
1030 Used by _formatListEntry implementations, returns a WuiRawHtmlBase object.
1031 """
1032 _ = iEntry;
1033 return WuiRawHtml('<input type="checkbox" name="%s" value="%s">'
1034 % (webutils.escapeAttr(self._sCheckboxName), webutils.escapeAttr(unicode(sValue))));
1035
1036 def show(self, fShowNavigation=True):
1037 """
1038 Displays the list.
1039 Returns (Title, HTML) on success, raises exception on error.
1040 """
1041 assert self._aoActions is not None;
1042 assert self._sAction is not None;
1043
1044 sPageBody = '<script language="JavaScript">\n' \
1045 'function toggle%s(oSource) {\n' \
1046 ' aoCheckboxes = document.getElementsByName(\'%s\');\n' \
1047 ' for(var i in aoCheckboxes)\n' \
1048 ' aoCheckboxes[i].checked = oSource.checked;\n' \
1049 '}\n' \
1050 '</script>\n' \
1051 % ('' if self._sId is None else self._sId, self._sCheckboxName,);
1052 if fShowNavigation:
1053 sPageBody += self._generateNavigation('top');
1054 if len(self._aoEntries) > 0:
1055
1056 sPageBody += '<form action="?%s" method="post" class="tmlistactionform">\n' \
1057 % (webutils.encodeUrlParams({WuiDispatcherBase.ksParamAction: self._sAction,}),);
1058 sPageBody += self._generateTable();
1059
1060 sPageBody += ' <label>Actions</label>\n' \
1061 ' <select name="%s" id="%s-action-combo" class="tmlistactionform-combo">\n' \
1062 % (webutils.escapeAttr(WuiDispatcherBase.ksParamListAction), webutils.escapeAttr(self._sId),);
1063 for oValue, sText, _ in self._aoActions:
1064 sPageBody += ' <option value="%s">%s</option>\n' \
1065 % (webutils.escapeAttr(unicode(oValue)), webutils.escapeElem(sText), );
1066 sPageBody += ' </select>\n';
1067 sPageBody += ' <input type="submit"></input>\n';
1068 sPageBody += '</form>\n';
1069 if fShowNavigation:
1070 sPageBody += self._generateNavigation('bottom');
1071 else:
1072 sPageBody += '<p>No entries.</p>'
1073
1074 return (self._composeTitle(), sPageBody);
1075
Note: See TracBrowser for help on using the repository browser.

© 2025 Oracle Support Privacy / Do Not Sell My Info Terms of Use Trademark Policy Automated Access Etiquette