VirtualBox

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

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

pylint

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 45.6 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: wuicontentbase.py 65351 2017-01-17 15:54:47Z 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: 65351 $"
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 """
56 Must be overridden by sub-classes.
57 """
58 assert False;
59 return '';
60
61 def __str__(self):
62 """ String representation is HTML, simplifying formatting and such. """
63 return self.toHtml();
64
65
66class WuiLinkBase(WuiHtmlBase): # pylint: disable=R0903
67 """
68 For passing links from WuiListContentBase._formatListEntry.
69 """
70
71 def __init__(self, sName, sUrlBase, dParams = None, sConfirm = None, sTitle = None,
72 sFragmentId = None, fBracketed = True, sExtraAttrs = ''):
73 WuiHtmlBase.__init__(self);
74 self.sName = sName
75 self.sUrl = sUrlBase
76 self.sConfirm = sConfirm;
77 self.sTitle = sTitle;
78 self.fBracketed = fBracketed;
79 self.sExtraAttrs = sExtraAttrs;
80
81 if dParams is not None and len(dParams) > 0:
82 # Do some massaging of None arguments.
83 dParams = dict(dParams);
84 for sKey in dParams:
85 if dParams[sKey] is None:
86 dParams[sKey] = '';
87 self.sUrl += '?' + webutils.encodeUrlParams(dParams);
88
89 if sFragmentId is not None:
90 self.sUrl += '#' + sFragmentId;
91
92 def setBracketed(self, fBracketed):
93 """Changes the bracketing style."""
94 self.fBracketed = fBracketed;
95 return True;
96
97 def toHtml(self):
98 """
99 Returns a simple HTML anchor element.
100 """
101 sExtraAttrs = self.sExtraAttrs;
102 if self.sConfirm is not None:
103 sExtraAttrs += 'onclick=\'return confirm("%s");\' ' % (webutils.escapeAttr(self.sConfirm),);
104 if self.sTitle is not None:
105 sExtraAttrs += 'title="%s" ' % (webutils.escapeAttr(self.sTitle),);
106 if len(sExtraAttrs) > 0 and sExtraAttrs[-1] != ' ':
107 sExtraAttrs += ' ';
108
109 sFmt = '[<a %shref="%s">%s</a>]';
110 if not self.fBracketed:
111 sFmt = '<a %shref="%s">%s</a>';
112 return sFmt % (sExtraAttrs, webutils.escapeAttr(self.sUrl), webutils.escapeElem(self.sName));
113
114
115class WuiTmLink(WuiLinkBase): # pylint: disable=R0903
116 """ Local link to the test manager. """
117
118 kdDbgParams = None;
119
120 def __init__(self, sName, sUrlBase, dParams = None, sConfirm = None, sTitle = None,
121 sFragmentId = None, fBracketed = True):
122
123 # Add debug parameters if necessary.
124 if self.kdDbgParams is not None and len(self.kdDbgParams) > 0:
125 if dParams is None or len(dParams) == 0:
126 dParams = dict(self.kdDbgParams);
127 else:
128 dParams = dict(dParams);
129 for sKey in self.kdDbgParams:
130 if sKey not in dParams:
131 dParams[sKey] = self.kdDbgParams[sKey];
132
133 WuiLinkBase.__init__(self, sName, sUrlBase, dParams, sConfirm, sTitle, sFragmentId, fBracketed);
134
135
136class WuiAdminLink(WuiTmLink): # pylint: disable=R0903
137 """ Local link to the test manager's admin portion. """
138
139 def __init__(self, sName, sAction, tsEffectiveDate = None, dParams = None, sConfirm = None, sTitle = None,
140 sFragmentId = None, fBracketed = True):
141 from testmanager.webui.wuiadmin import WuiAdmin;
142 if dParams is None or len(dParams) == 0:
143 dParams = dict();
144 else:
145 dParams = dict(dParams);
146 if sAction is not None:
147 dParams[WuiAdmin.ksParamAction] = sAction;
148 if tsEffectiveDate is not None:
149 dParams[WuiAdmin.ksParamEffectiveDate] = tsEffectiveDate;
150 WuiTmLink.__init__(self, sName, WuiAdmin.ksScriptName, dParams = dParams, sConfirm = sConfirm, sTitle = sTitle,
151 sFragmentId = sFragmentId, fBracketed = fBracketed);
152
153class WuiMainLink(WuiTmLink): # pylint: disable=R0903
154 """ Local link to the test manager's main portion. """
155
156 def __init__(self, sName, sAction, dParams = None, sConfirm = None, sTitle = None, sFragmentId = None, fBracketed = True):
157 if dParams is None or len(dParams) == 0:
158 dParams = dict();
159 else:
160 dParams = dict(dParams);
161 from testmanager.webui.wuimain import WuiMain;
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,
740 oDisp = None, aiSelectedSortColumns = None): # pylint: disable=too-many-arguments
741 WuiContentBase.__init__(self, fnDPrint = fnDPrint, oDisp = oDisp);
742 self._aoEntries = aoEntries; ## @todo should replace this with a Logic object and define methods for querying.
743 self._iPage = iPage;
744 self._cItemsPerPage = cItemsPerPage;
745 self._tsEffectiveDate = tsEffectiveDate;
746 self._sTitle = sTitle; assert len(sTitle) > 1;
747 if sId is None:
748 sId = sTitle.strip().replace(' ', '').lower();
749 assert len(sId.strip()) > 0;
750 self._sId = sId;
751 self._asColumnHeaders = [];
752 self._asColumnAttribs = [];
753 self._aaiColumnSorting = []; ##< list of list of integers
754 self._aiSelectedSortColumns = aiSelectedSortColumns; ##< list of integers
755
756 def _formatCommentCell(self, sComment, cMaxLines = 3, cchMaxLine = 63):
757 """
758 Helper functions for formatting comment cell.
759 Returns None or WuiRawHtml instance.
760 """
761 # Nothing to do for empty comments.
762 if sComment is None:
763 return None;
764 sComment = sComment.strip();
765 if len(sComment) == 0:
766 return None;
767
768 # Restrict the text if necessary, making the whole text available thru mouse-over.
769 ## @todo this would be better done by java script or smth, so it could automatically adjust to the table size.
770 if len(sComment) > cchMaxLine or sComment.count('\n') >= cMaxLines:
771 sShortHtml = '';
772 for iLine, sLine in enumerate(sComment.split('\n')):
773 if iLine >= cMaxLines:
774 break;
775 if iLine > 0:
776 sShortHtml += '<br>\n';
777 if len(sLine) > cchMaxLine:
778 sShortHtml += webutils.escapeElem(sLine[:(cchMaxLine - 3)]);
779 sShortHtml += '...';
780 else:
781 sShortHtml += webutils.escapeElem(sLine);
782 return WuiRawHtml('<span class="tmcomment" title="%s">%s</span>' % (webutils.escapeAttr(sComment), sShortHtml,));
783
784 return WuiRawHtml('<span class="tmcomment">%s</span>' % (webutils.escapeElem(sComment).replace('\n', '<br>'),));
785
786 def _formatListEntry(self, iEntry):
787 """
788 Formats the specified list entry as a list of column values.
789 Returns HTML for a table row.
790
791 The child class really need to override this!
792 """
793 # ASSUMES ModelDataBase children.
794 asRet = [];
795 for sAttr in self._aoEntries[0].getDataAttributes():
796 asRet.append(getattr(self._aoEntries[iEntry], sAttr));
797 return asRet;
798
799 def _formatListEntryHtml(self, iEntry):
800 """
801 Formats the specified list entry as HTML.
802 Returns HTML for a table row.
803
804 The child class can override this to
805 """
806 if (iEntry + 1) & 1:
807 sRow = u' <tr class="tmodd">\n';
808 else:
809 sRow = u' <tr class="tmeven">\n';
810
811 aoValues = self._formatListEntry(iEntry);
812 assert len(aoValues) == len(self._asColumnHeaders), '%s vs %s' % (len(aoValues), len(self._asColumnHeaders));
813
814 for i, _ in enumerate(aoValues):
815 if i < len(self._asColumnAttribs) and len(self._asColumnAttribs[i]) > 0:
816 sRow += u' <td ' + self._asColumnAttribs[i] + '>';
817 else:
818 sRow += u' <td>';
819
820 if isinstance(aoValues[i], WuiHtmlBase):
821 sRow += aoValues[i].toHtml();
822 elif isinstance(aoValues[i], list):
823 if len(aoValues[i]) > 0:
824 for oElement in aoValues[i]:
825 if isinstance(oElement, WuiHtmlBase):
826 sRow += oElement.toHtml();
827 elif db.isDbTimestamp(oElement):
828 sRow += webutils.escapeElem(self.formatTsShort(oElement));
829 else:
830 sRow += webutils.escapeElem(unicode(oElement));
831 sRow += ' ';
832 elif db.isDbTimestamp(aoValues[i]):
833 sRow += webutils.escapeElem(self.formatTsShort(aoValues[i]));
834 elif db.isDbInterval(aoValues[i]):
835 sRow += webutils.escapeElem(self.formatIntervalShort(aoValues[i]));
836 elif aoValues[i] is not None:
837 sRow += webutils.escapeElem(unicode(aoValues[i]));
838
839 sRow += u'</td>\n';
840
841 return sRow + u' </tr>\n';
842
843 def _generateTimeNavigation(self, sWhere):
844 """
845 Returns HTML for time navigation.
846
847 Note! Views without a need for a timescale just stubs this method.
848 """
849 _ = sWhere;
850 sNavigation = '';
851
852 dParams = self._oDisp.getParameters();
853 dParams[WuiDispatcherBase.ksParamItemsPerPage] = self._cItemsPerPage;
854 dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage;
855
856 if WuiDispatcherBase.ksParamEffectiveDate in dParams:
857 del dParams[WuiDispatcherBase.ksParamEffectiveDate];
858 sNavigation += ' [<a href="?%s">Now</a>]' % (webutils.encodeUrlParams(dParams),);
859
860 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 01:00:00.00';
861 sNavigation += ' [<a href="?%s">1</a>' % (webutils.encodeUrlParams(dParams),);
862
863 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 02:00:00.00';
864 sNavigation += ', <a href="?%s">2</a>' % (webutils.encodeUrlParams(dParams),);
865
866 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 06:00:00.00';
867 sNavigation += ', <a href="?%s">6</a>' % (webutils.encodeUrlParams(dParams),);
868
869 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-00 12:00:00.00';
870 sNavigation += ', <a href="?%s">12</a>' % (webutils.encodeUrlParams(dParams),);
871
872 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-01 00:00:00.00';
873 sNavigation += ', or <a href="?%s">24</a> hours ago]' % (webutils.encodeUrlParams(dParams),);
874
875
876 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-02 00:00:00.00';
877 sNavigation += ' [<a href="?%s">2</a>' % (webutils.encodeUrlParams(dParams),);
878
879 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-03 00:00:00.00';
880 sNavigation += ', <a href="?%s">3</a>' % (webutils.encodeUrlParams(dParams),);
881
882 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-05 00:00:00.00';
883 sNavigation += ', <a href="?%s">5</a>' % (webutils.encodeUrlParams(dParams),);
884
885 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-07 00:00:00.00';
886 sNavigation += ', <a href="?%s">7</a>' % (webutils.encodeUrlParams(dParams),);
887
888 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-14 00:00:00.00';
889 sNavigation += ', <a href="?%s">14</a>' % (webutils.encodeUrlParams(dParams),);
890
891 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-21 00:00:00.00';
892 sNavigation += ', <a href="?%s">21</a>' % (webutils.encodeUrlParams(dParams),);
893
894 dParams[WuiDispatcherBase.ksParamEffectiveDate] = '-0000-00-28 00:00:00.00';
895 sNavigation += ', or <a href="?%s">28</a> days ago]' % (webutils.encodeUrlParams(dParams),);
896
897 return sNavigation;
898
899
900 def _generateNavigation(self, sWhere):
901 """
902 Return HTML for navigation.
903 """
904
905 #
906 # ASSUMES the dispatcher/controller code fetches one entry more than
907 # needed to fill the page to indicate further records.
908 #
909 sNavigation = '<div class="tmlistnav-%s">\n' % sWhere;
910 sNavigation += ' <table class="tmlistnavtab">\n' \
911 ' <tr>\n';
912 dParams = self._oDisp.getParameters();
913 dParams[WuiDispatcherBase.ksParamItemsPerPage] = self._cItemsPerPage;
914 dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage;
915 if self._tsEffectiveDate is not None:
916 dParams[WuiDispatcherBase.ksParamEffectiveDate] = self._tsEffectiveDate;
917
918 # Prev
919 if self._iPage > 0:
920 dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage - 1;
921 sNavigation += ' <td align="left"><a href="?%s">Previous</a></td>\n' % (webutils.encodeUrlParams(dParams),);
922 else:
923 sNavigation += ' <td></td>\n';
924
925 # Time scale.
926 sNavigation += '<td align="center" class="tmtimenav">';
927 sNavigation += self._generateTimeNavigation(sWhere);
928 sNavigation += '</td>';
929
930 # Next
931 if len(self._aoEntries) > self._cItemsPerPage:
932 dParams[WuiDispatcherBase.ksParamPageNo] = self._iPage + 1;
933 sNavigation += ' <td align="right"><a href="?%s">Next</a></td>\n' % (webutils.encodeUrlParams(dParams),);
934 else:
935 sNavigation += ' <td></td>\n';
936
937 sNavigation += ' </tr>\n' \
938 ' </table>\n' \
939 '</div>\n';
940 return sNavigation;
941
942 def _isSortingByColumnAscending(self, aiColumns):
943 """ Checks if we're already sorting by this column in ascending order """
944 # Just compare the first sorting column spec for now.
945 #if self._aiSelectedSortColumns is not None and len(aiColumns) <= len(self._aiSelectedSortColumns):
946 if len(aiColumns) <= len(self._aiSelectedSortColumns):
947 if list(aiColumns) == list(self._aiSelectedSortColumns[:len(aiColumns)]):
948 return True;
949 return False;
950
951 def _generateTableHeaders(self):
952 """
953 Generate table headers.
954 Returns raw html string.
955 Overridable.
956 """
957
958 sHtml = ' <thead class="tmheader"><tr>';
959 for iHeader, oHeader in enumerate(self._asColumnHeaders):
960 if isinstance(oHeader, WuiHtmlBase):
961 sHtml += '<th>' + oHeader.toHtml() + '</th>';
962 elif iHeader < len(self._aaiColumnSorting) and self._aaiColumnSorting[iHeader] is not None:
963 sHtml += '<th>'
964 if not self._isSortingByColumnAscending(self._aaiColumnSorting[iHeader]):
965 sSortParams = ','.join([str(i) for i in self._aaiColumnSorting[iHeader]]);
966 else:
967 sSortParams = ','.join([str(-i) for i in self._aaiColumnSorting[iHeader]]);
968 sHtml += '<a href="javascript:ahrefActionSortByColumns(\'%s\',[%s]);">' \
969 % (WuiDispatcherBase.ksParamSortColumns, sSortParams);
970 sHtml += webutils.escapeElem(oHeader) + '</a></th>';
971 else:
972 sHtml += '<th>' + webutils.escapeElem(oHeader) + '</th>';
973 sHtml += '</tr><thead>\n';
974 return sHtml
975
976 def _generateTable(self):
977 """
978 show worker that just generates the table.
979 """
980
981 #
982 # Create a table.
983 # If no colum headers are provided, fall back on database field
984 # names, ASSUMING that the entries are ModelDataBase children.
985 # Note! the cellspacing is for IE8.
986 #
987 sPageBody = '<table class="tmtable" id="' + self._sId + '" cellspacing="0">\n';
988
989 if len(self._asColumnHeaders) == 0:
990 self._asColumnHeaders = self._aoEntries[0].getDataAttributes();
991
992 sPageBody += self._generateTableHeaders();
993
994 #
995 # Format the body and close the table.
996 #
997 sPageBody += ' <tbody>\n';
998 for iEntry in range(min(len(self._aoEntries), self._cItemsPerPage)):
999 sPageBody += self._formatListEntryHtml(iEntry);
1000 sPageBody += ' </tbody>\n' \
1001 '</table>\n';
1002 return sPageBody;
1003
1004 def _composeTitle(self):
1005 """Composes the title string (return value)."""
1006 sTitle = self._sTitle;
1007 if self._iPage != 0:
1008 sTitle += ' (page ' + unicode(self._iPage + 1) + ')'
1009 if self._tsEffectiveDate is not None:
1010 sTitle += ' as per ' + unicode(self._tsEffectiveDate); ## @todo shorten this.
1011 return sTitle;
1012
1013
1014 def show(self, fShowNavigation = True):
1015 """
1016 Displays the list.
1017 Returns (Title, HTML) on success, raises exception on error.
1018 """
1019
1020 sPageBody = ''
1021 if fShowNavigation:
1022 sPageBody += self._generateNavigation('top');
1023
1024 if len(self._aoEntries):
1025 sPageBody += self._generateTable();
1026 if fShowNavigation:
1027 sPageBody += self._generateNavigation('bottom');
1028 else:
1029 sPageBody += '<p>No entries.</p>'
1030
1031 return (self._composeTitle(), sPageBody);
1032
1033
1034class WuiListContentWithActionBase(WuiListContentBase):
1035 """
1036 Base for the list content with action classes.
1037 """
1038
1039 def __init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle, sId = None, fnDPrint = None,
1040 oDisp = None, aiSelectedSortColumns = None): # pylint: disable=too-many-arguments
1041 WuiListContentBase.__init__(self, aoEntries, iPage, cItemsPerPage, tsEffectiveDate, sTitle, sId = sId,
1042 fnDPrint = fnDPrint, oDisp = oDisp, aiSelectedSortColumns = aiSelectedSortColumns);
1043 self._aoActions = None; # List of [ oValue, sText, sHover ] provided by the child class.
1044 self._sAction = None; # Set by the child class.
1045 self._sCheckboxName = None; # Set by the child class.
1046 self._asColumnHeaders = [ WuiRawHtml('<input type="checkbox" onClick="toggle%s(this)">'
1047 % ('' if sId is None else sId)), ];
1048 self._asColumnAttribs = [ 'align="center"', ];
1049 self._aaiColumnSorting = [ None, ];
1050
1051 def _getCheckBoxColumn(self, iEntry, sValue):
1052 """
1053 Used by _formatListEntry implementations, returns a WuiRawHtmlBase object.
1054 """
1055 _ = iEntry;
1056 return WuiRawHtml('<input type="checkbox" name="%s" value="%s">'
1057 % (webutils.escapeAttr(self._sCheckboxName), webutils.escapeAttr(unicode(sValue))));
1058
1059 def show(self, fShowNavigation=True):
1060 """
1061 Displays the list.
1062 Returns (Title, HTML) on success, raises exception on error.
1063 """
1064 assert self._aoActions is not None;
1065 assert self._sAction is not None;
1066
1067 sPageBody = '<script language="JavaScript">\n' \
1068 'function toggle%s(oSource) {\n' \
1069 ' aoCheckboxes = document.getElementsByName(\'%s\');\n' \
1070 ' for(var i in aoCheckboxes)\n' \
1071 ' aoCheckboxes[i].checked = oSource.checked;\n' \
1072 '}\n' \
1073 '</script>\n' \
1074 % ('' if self._sId is None else self._sId, self._sCheckboxName,);
1075 if fShowNavigation:
1076 sPageBody += self._generateNavigation('top');
1077 if len(self._aoEntries) > 0:
1078
1079 sPageBody += '<form action="?%s" method="post" class="tmlistactionform">\n' \
1080 % (webutils.encodeUrlParams({WuiDispatcherBase.ksParamAction: self._sAction,}),);
1081 sPageBody += self._generateTable();
1082
1083 sPageBody += ' <label>Actions</label>\n' \
1084 ' <select name="%s" id="%s-action-combo" class="tmlistactionform-combo">\n' \
1085 % (webutils.escapeAttr(WuiDispatcherBase.ksParamListAction), webutils.escapeAttr(self._sId),);
1086 for oValue, sText, _ in self._aoActions:
1087 sPageBody += ' <option value="%s">%s</option>\n' \
1088 % (webutils.escapeAttr(unicode(oValue)), webutils.escapeElem(sText), );
1089 sPageBody += ' </select>\n';
1090 sPageBody += ' <input type="submit"></input>\n';
1091 sPageBody += '</form>\n';
1092 if fShowNavigation:
1093 sPageBody += self._generateNavigation('bottom');
1094 else:
1095 sPageBody += '<p>No entries.</p>'
1096
1097 return (self._composeTitle(), sPageBody);
1098
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