VirtualBox

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

Last change on this file since 82972 was 82968, checked in by vboxsync, 5 years ago

Copyright year updates by scm.

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

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