VirtualBox

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

Last change on this file since 83343 was 83338, checked in by vboxsync, 5 years ago

TestManager: Scheduling queue patch #4 with fEnable sorting adjustment. bugref:9657

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 46.6 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: wuicontentbase.py 83338 2020-03-19 17:51:47Z 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: 83338 $"
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, fTimeNavigation = True):
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._fTimeNavigation = fTimeNavigation;
754 self._sTitle = sTitle; assert len(sTitle) > 1;
755 if sId is None:
756 sId = sTitle.strip().replace(' ', '').lower();
757 assert sId.strip();
758 self._sId = sId;
759 self._asColumnHeaders = [];
760 self._asColumnAttribs = [];
761 self._aaiColumnSorting = []; ##< list of list of integers
762 self._aiSelectedSortColumns = aiSelectedSortColumns; ##< list of integers
763
764 def _formatCommentCell(self, sComment, cMaxLines = 3, cchMaxLine = 63):
765 """
766 Helper functions for formatting comment cell.
767 Returns None or WuiRawHtml instance.
768 """
769 # Nothing to do for empty comments.
770 if sComment is None:
771 return None;
772 sComment = sComment.strip();
773 if not sComment:
774 return None;
775
776 # Restrict the text if necessary, making the whole text available thru mouse-over.
777 ## @todo this would be better done by java script or smth, so it could automatically adjust to the table size.
778 if len(sComment) > cchMaxLine or sComment.count('\n') >= cMaxLines:
779 sShortHtml = '';
780 for iLine, sLine in enumerate(sComment.split('\n')):
781 if iLine >= cMaxLines:
782 break;
783 if iLine > 0:
784 sShortHtml += '<br>\n';
785 if len(sLine) > cchMaxLine:
786 sShortHtml += webutils.escapeElem(sLine[:(cchMaxLine - 3)]);
787 sShortHtml += '...';
788 else:
789 sShortHtml += webutils.escapeElem(sLine);
790 return WuiRawHtml('<span class="tmcomment" title="%s">%s</span>' % (webutils.escapeAttr(sComment), sShortHtml,));
791
792 return WuiRawHtml('<span class="tmcomment">%s</span>' % (webutils.escapeElem(sComment).replace('\n', '<br>'),));
793
794 def _formatListEntry(self, iEntry):
795 """
796 Formats the specified list entry as a list of column values.
797 Returns HTML for a table row.
798
799 The child class really need to override this!
800 """
801 # ASSUMES ModelDataBase children.
802 asRet = [];
803 for sAttr in self._aoEntries[0].getDataAttributes():
804 asRet.append(getattr(self._aoEntries[iEntry], sAttr));
805 return asRet;
806
807 def _formatListEntryHtml(self, iEntry):
808 """
809 Formats the specified list entry as HTML.
810 Returns HTML for a table row.
811
812 The child class can override this to
813 """
814 if (iEntry + 1) & 1:
815 sRow = u' <tr class="tmodd">\n';
816 else:
817 sRow = u' <tr class="tmeven">\n';
818
819 aoValues = self._formatListEntry(iEntry);
820 assert len(aoValues) == len(self._asColumnHeaders), '%s vs %s' % (len(aoValues), len(self._asColumnHeaders));
821
822 for i, _ in enumerate(aoValues):
823 if i < len(self._asColumnAttribs) and self._asColumnAttribs[i]:
824 sRow += u' <td ' + self._asColumnAttribs[i] + '>';
825 else:
826 sRow += u' <td>';
827
828 if isinstance(aoValues[i], WuiHtmlBase):
829 sRow += aoValues[i].toHtml();
830 elif isinstance(aoValues[i], list):
831 if aoValues[i]:
832 for oElement in aoValues[i]:
833 if isinstance(oElement, WuiHtmlBase):
834 sRow += oElement.toHtml();
835 elif db.isDbTimestamp(oElement):
836 sRow += webutils.escapeElem(self.formatTsShort(oElement));
837 else:
838 sRow += webutils.escapeElem(unicode(oElement));
839 sRow += ' ';
840 elif db.isDbTimestamp(aoValues[i]):
841 sRow += webutils.escapeElem(self.formatTsShort(aoValues[i]));
842 elif db.isDbInterval(aoValues[i]):
843 sRow += webutils.escapeElem(self.formatIntervalShort(aoValues[i]));
844 elif aoValues[i] is not None:
845 sRow += webutils.escapeElem(unicode(aoValues[i]));
846
847 sRow += u'</td>\n';
848
849 return sRow + u' </tr>\n';
850
851 def _generateTimeNavigation(self, sWhere):
852 """
853 Returns HTML for time navigation.
854
855 Note! Views without a need for a timescale just stubs this method.
856 """
857 _ = sWhere;
858 sNavigation = '';
859
860 dParams = self._oDisp.getParameters();
861 dParams[WuiDispatcherBase.ksParamItemsPerPage] = self._cItemsPerPage;
862 dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage;
863
864 if WuiDispatcherBase.ksParamEffectiveDate in dParams:
865 del dParams[WuiDispatcherBase.ksParamEffectiveDate];
866 sNavigation += ' [<a href="?%s">Now</a>]' % (webutils.encodeUrlParams(dParams),);
867
868 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 01:00:00.00';
869 sNavigation += ' [<a href="?%s">1</a>' % (webutils.encodeUrlParams(dParams),);
870
871 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 02:00:00.00';
872 sNavigation += ', <a href="?%s">2</a>' % (webutils.encodeUrlParams(dParams),);
873
874 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 06:00:00.00';
875 sNavigation += ', <a href="?%s">6</a>' % (webutils.encodeUrlParams(dParams),);
876
877 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 12:00:00.00';
878 sNavigation += ', <a href="?%s">12</a>' % (webutils.encodeUrlParams(dParams),);
879
880 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-01 00:00:00.00';
881 sNavigation += ', or <a href="?%s">24</a> hours ago]' % (webutils.encodeUrlParams(dParams),);
882
883
884 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-02 00:00:00.00';
885 sNavigation += ' [<a href="?%s">2</a>' % (webutils.encodeUrlParams(dParams),);
886
887 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-03 00:00:00.00';
888 sNavigation += ', <a href="?%s">3</a>' % (webutils.encodeUrlParams(dParams),);
889
890 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-05 00:00:00.00';
891 sNavigation += ', <a href="?%s">5</a>' % (webutils.encodeUrlParams(dParams),);
892
893 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-07 00:00:00.00';
894 sNavigation += ', <a href="?%s">7</a>' % (webutils.encodeUrlParams(dParams),);
895
896 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-14 00:00:00.00';
897 sNavigation += ', <a href="?%s">14</a>' % (webutils.encodeUrlParams(dParams),);
898
899 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-21 00:00:00.00';
900 sNavigation += ', <a href="?%s">21</a>' % (webutils.encodeUrlParams(dParams),);
901
902 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-28 00:00:00.00';
903 sNavigation += ', or <a href="?%s">28</a> days ago]' % (webutils.encodeUrlParams(dParams),);
904
905 return sNavigation;
906
907
908 def _generateNavigation(self, sWhere):
909 """
910 Return HTML for navigation.
911 """
912
913 #
914 # ASSUMES the dispatcher/controller code fetches one entry more than
915 # needed to fill the page to indicate further records.
916 #
917 sNavigation = '<div class="tmlistnav-%s">\n' % sWhere;
918 sNavigation += ' <table class="tmlistnavtab">\n' \
919 ' <tr>\n';
920 dParams = self._oDisp.getParameters();
921 dParams[WuiDispatcherBase.ksParamItemsPerPage] = self._cItemsPerPage;
922 dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage;
923 if self._tsEffectiveDate is not None:
924 dParams[WuiDispatcherBase.ksParamEffectiveDate] = self._tsEffectiveDate;
925
926 # Prev
927 if self._iPage > 0:
928 dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage - 1;
929 sNavigation += ' <td align="left"><a href="?%s">Previous</a></td>\n' % (webutils.encodeUrlParams(dParams),);
930 else:
931 sNavigation += ' <td></td>\n';
932
933 # Time scale.
934 if self._fTimeNavigation:
935 sNavigation += '<td align="center" class="tmtimenav">';
936 sNavigation += self._generateTimeNavigation(sWhere);
937 sNavigation += '</td>';
938
939 # Next
940 if len(self._aoEntries) > self._cItemsPerPage:
941 dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage + 1;
942 sNavigation += ' <td align="right"><a href="?%s">Next</a></td>\n' % (webutils.encodeUrlParams(dParams),);
943 else:
944 sNavigation += ' <td></td>\n';
945
946 sNavigation += ' </tr>\n' \
947 ' </table>\n' \
948 '</div>\n';
949 return sNavigation;
950
951 def _checkSortingByColumnAscending(self, aiColumns):
952 """
953 Checks if we're sorting by this column.
954
955 Returns 0 if not sorting by this, negative if descending, positive if ascending. The
956 value indicates the priority (nearer to 0 is higher).
957 """
958 if len(aiColumns) <= len(self._aiSelectedSortColumns):
959 aiColumns = list(aiColumns);
960 aiNegColumns = list([-i for i in aiColumns]);
961 i = 0;
962 while i + len(aiColumns) <= len(self._aiSelectedSortColumns):
963 aiSub = list(self._aiSelectedSortColumns[i : i + len(aiColumns)]);
964 if aiSub == aiColumns:
965 return 1 + i;
966 if aiSub == aiNegColumns:
967 return -1 - i;
968 i += 1;
969 return 0;
970
971 def _generateTableHeaders(self):
972 """
973 Generate table headers.
974 Returns raw html string.
975 Overridable.
976 """
977
978 sHtml = ' <thead class="tmheader"><tr>';
979 for iHeader, oHeader in enumerate(self._asColumnHeaders):
980 if isinstance(oHeader, WuiHtmlBase):
981 sHtml += '<th>' + oHeader.toHtml() + '</th>';
982 elif iHeader < len(self._aaiColumnSorting) and self._aaiColumnSorting[iHeader] is not None:
983 sHtml += '<th>'
984 iSorting = self._checkSortingByColumnAscending(self._aaiColumnSorting[iHeader]);
985 if iSorting > 0:
986 sDirection = '&nbsp;&#x25b4;' if iSorting == 1 else '<small>&nbsp;&#x25b5;</small>';
987 sSortParams = ','.join([str(-i) for i in self._aaiColumnSorting[iHeader]]);
988 else:
989 sDirection = '';
990 if iSorting < 0:
991 sDirection = '&nbsp;&#x25be;' if iSorting == -1 else '<small>&nbsp;&#x25bf;</small>'
992 sSortParams = ','.join([str(i) for i in self._aaiColumnSorting[iHeader]]);
993 sHtml += '<a href="javascript:ahrefActionSortByColumns(\'%s\',[%s]);">' \
994 % (WuiDispatcherBase.ksParamSortColumns, sSortParams);
995 sHtml += webutils.escapeElem(oHeader) + '</a>' + sDirection + '</th>';
996 else:
997 sHtml += '<th>' + webutils.escapeElem(oHeader) + '</th>';
998 sHtml += '</tr><thead>\n';
999 return sHtml
1000
1001 def _generateTable(self):
1002 """
1003 show worker that just generates the table.
1004 """
1005
1006 #
1007 # Create a table.
1008 # If no colum headers are provided, fall back on database field
1009 # names, ASSUMING that the entries are ModelDataBase children.
1010 # Note! the cellspacing is for IE8.
1011 #
1012 sPageBody = '<table class="tmtable" id="' + self._sId + '" cellspacing="0">\n';
1013
1014 if not self._asColumnHeaders:
1015 self._asColumnHeaders = self._aoEntries[0].getDataAttributes();
1016
1017 sPageBody += self._generateTableHeaders();
1018
1019 #
1020 # Format the body and close the table.
1021 #
1022 sPageBody += ' <tbody>\n';
1023 for iEntry in range(min(len(self._aoEntries), self._cItemsPerPage)):
1024 sPageBody += self._formatListEntryHtml(iEntry);
1025 sPageBody += ' </tbody>\n' \
1026 '</table>\n';
1027 return sPageBody;
1028
1029 def _composeTitle(self):
1030 """Composes the title string (return value)."""
1031 sTitle = self._sTitle;
1032 if self._iPage != 0:
1033 sTitle += ' (page ' + unicode(self._iPage + 1) + ')'
1034 if self._tsEffectiveDate is not None:
1035 sTitle += ' as per ' + unicode(self._tsEffectiveDate); ## @todo shorten this.
1036 return sTitle;
1037
1038
1039 def show(self, fShowNavigation = True):
1040 """
1041 Displays the list.
1042 Returns (Title, HTML) on success, raises exception on error.
1043 """
1044
1045 sPageBody = ''
1046 if fShowNavigation:
1047 sPageBody += self._generateNavigation('top');
1048
1049 if self._aoEntries:
1050 sPageBody += self._generateTable();
1051 if fShowNavigation:
1052 sPageBody += self._generateNavigation('bottom');
1053 else:
1054 sPageBody += '<p>No entries.</p>'
1055
1056 return (self._composeTitle(), sPageBody);
1057
1058
1059class WuiListContentWithActionBase(WuiListContentBase):
1060 """
1061 Base for the list content with action classes.
1062 """
1063
1064 def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle, # pylint: disable=too-many-arguments
1065 sId = None, fnDPrint = None, oDisp = None, aiSelectedSortColumns = None):
1066 WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle, sId = sId,
1067 fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
1068 self._aoActions = None; # List of [ oValue, sText, sHover ] provided by the child class.
1069 self._sAction = None; # Set by the child class.
1070 self._sCheckboxName = None; # Set by the child class.
1071 self._asColumnHeaders = [ WuiRawHtml('<input type="checkbox" onClick="toggle%s(this)">'
1072 % ('' if sId is None else sId)), ];
1073 self._asColumnAttribs = [ 'align="center"', ];
1074 self._aaiColumnSorting = [ None, ];
1075
1076 def _getCheckBoxColumn(self, iEntry, sValue):
1077 """
1078 Used by _formatListEntry implementations, returns a WuiRawHtmlBase object.
1079 """
1080 _ = iEntry;
1081 return WuiRawHtml('<input type="checkbox" name="%s" value="%s">'
1082 % (webutils.escapeAttr(self._sCheckboxName), webutils.escapeAttr(unicode(sValue))));
1083
1084 def show(self, fShowNavigation=True):
1085 """
1086 Displays the list.
1087 Returns (Title, HTML) on success, raises exception on error.
1088 """
1089 assert self._aoActions is not None;
1090 assert self._sAction is not None;
1091
1092 sPageBody = '<script language="JavaScript">\n' \
1093 'function toggle%s(oSource) {\n' \
1094 ' aoCheckboxes = document.getElementsByName(\'%s\');\n' \
1095 ' for(var i in aoCheckboxes)\n' \
1096 ' aoCheckboxes[i].checked = oSource.checked;\n' \
1097 '}\n' \
1098 '</script>\n' \
1099 % ('' if self._sId is None else self._sId, self._sCheckboxName,);
1100 if fShowNavigation:
1101 sPageBody += self._generateNavigation('top');
1102 if self._aoEntries:
1103
1104 sPageBody += '<form action="?%s" method="post" class="tmlistactionform">\n' \
1105 % (webutils.encodeUrlParams({WuiDispatcherBase.ksParamAction: self._sAction,}),);
1106 sPageBody += self._generateTable();
1107
1108 sPageBody += ' <label>Actions</label>\n' \
1109 ' <select name="%s" id="%s-action-combo" class="tmlistactionform-combo">\n' \
1110 % (webutils.escapeAttr(WuiDispatcherBase.ksParamListAction), webutils.escapeAttr(self._sId),);
1111 for oValue, sText, _ in self._aoActions:
1112 sPageBody += ' <option value="%s">%s</option>\n' \
1113 % (webutils.escapeAttr(unicode(oValue)), webutils.escapeElem(sText), );
1114 sPageBody += ' </select>\n';
1115 sPageBody += ' <input type="submit"></input>\n';
1116 sPageBody += '</form>\n';
1117 if fShowNavigation:
1118 sPageBody += self._generateNavigation('bottom');
1119 else:
1120 sPageBody += '<p>No entries.</p>'
1121
1122 return (self._composeTitle(), sPageBody);
1123
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