VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testmanager/core/failurereason.py@ 78138

Last change on this file since 78138 was 76553, checked in by vboxsync, 6 years ago

scm --update-copyright-year

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 24.8 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: failurereason.py 76553 2019-01-01 01:45:53Z vboxsync $
3
4"""
5Test Manager - Failure Reasons.
6"""
7
8__copyright__ = \
9"""
10Copyright (C) 2012-2019 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: 76553 $"
30
31
32# Standard Python imports.
33import sys;
34
35# Validation Kit imports.
36from testmanager.core.base import ModelDataBase, ModelLogicBase, TMRowNotFound, TMInvalidData, TMRowInUse, \
37 AttributeChangeEntry, ChangeLogEntry;
38from testmanager.core.useraccount import UserAccountLogic;
39
40# Python 3 hacks:
41if sys.version_info[0] >= 3:
42 xrange = range; # pylint: disable=redefined-builtin,invalid-name
43
44
45class FailureReasonData(ModelDataBase):
46 """
47 Failure Reason Data.
48 """
49
50 ksIdAttr = 'idFailureReason';
51
52 ksParam_idFailureReason = 'FailureReasonData_idFailureReason'
53 ksParam_tsEffective = 'FailureReasonData_tsEffective'
54 ksParam_tsExpire = 'FailureReasonData_tsExpire'
55 ksParam_uidAuthor = 'FailureReasonData_uidAuthor'
56 ksParam_idFailureCategory = 'FailureReasonData_idFailureCategory'
57 ksParam_sShort = 'FailureReasonData_sShort'
58 ksParam_sFull = 'FailureReasonData_sFull'
59 ksParam_iTicket = 'FailureReasonData_iTicket'
60 ksParam_asUrls = 'FailureReasonData_asUrls'
61
62 kasAllowNullAttributes = [ 'idFailureReason', 'tsEffective', 'tsExpire',
63 'uidAuthor', 'iTicket', 'asUrls' ]
64
65 def __init__(self):
66 ModelDataBase.__init__(self);
67
68 #
69 # Initialize with defaults.
70 # See the database for explanations of each of these fields.
71 #
72
73 self.idFailureReason = None
74 self.tsEffective = None
75 self.tsExpire = None
76 self.uidAuthor = None
77 self.idFailureCategory = None
78 self.sShort = None
79 self.sFull = None
80 self.iTicket = None
81 self.asUrls = None
82
83 def initFromDbRow(self, aoRow):
84 """
85 Re-initializes the data with a row from a SELECT * FROM FailureReasons.
86
87 Returns self. Raises exception if the row is None or otherwise invalid.
88 """
89
90 if aoRow is None:
91 raise TMRowNotFound('Failure Reason not found.');
92
93 self.idFailureReason = aoRow[0]
94 self.tsEffective = aoRow[1]
95 self.tsExpire = aoRow[2]
96 self.uidAuthor = aoRow[3]
97 self.idFailureCategory = aoRow[4]
98 self.sShort = aoRow[5]
99 self.sFull = aoRow[6]
100 self.iTicket = aoRow[7]
101 self.asUrls = aoRow[8]
102
103 return self;
104
105 def initFromDbWithId(self, oDb, idFailureReason, tsNow = None, sPeriodBack = None):
106 """
107 Initialize from the database, given the ID of a row.
108 """
109 oDb.execute(self.formatSimpleNowAndPeriodQuery(oDb,
110 'SELECT *\n'
111 'FROM FailureReasons\n'
112 'WHERE idFailureReason = %s\n'
113 , ( idFailureReason,), tsNow, sPeriodBack));
114 aoRow = oDb.fetchOne()
115 if aoRow is None:
116 raise TMRowNotFound('idFailureReason=%s not found (tsNow=%s sPeriodBack=%s)'
117 % (idFailureReason, tsNow, sPeriodBack,));
118 return self.initFromDbRow(aoRow);
119
120
121class FailureReasonDataEx(FailureReasonData):
122 """
123 Failure Reason Data, extended version that includes the category.
124 """
125
126 def __init__(self):
127 FailureReasonData.__init__(self);
128 self.oCategory = None;
129 self.oAuthor = None;
130
131 def initFromDbRowEx(self, aoRow, oCategoryLogic, oUserAccountLogic):
132 """
133 Re-initializes the data with a row from a SELECT * FROM FailureReasons.
134
135 Returns self. Raises exception if the row is None or otherwise invalid.
136 """
137
138 self.initFromDbRow(aoRow);
139 self.oCategory = oCategoryLogic.cachedLookup(self.idFailureCategory);
140 self.oAuthor = oUserAccountLogic.cachedLookup(self.uidAuthor);
141
142 return self;
143
144
145class FailureReasonLogic(ModelLogicBase): # pylint: disable=R0903
146 """
147 Failure Reason logic.
148 """
149
150 def __init__(self, oDb):
151 ModelLogicBase.__init__(self, oDb)
152 self.dCache = None;
153 self.dCacheNameAndCat = None;
154 self.oCategoryLogic = None;
155 self.oUserAccountLogic = None;
156
157 def fetchForListing(self, iStart, cMaxRows, tsNow, aiSortColumns = None):
158 """
159 Fetches Failure Category records.
160
161 Returns an array (list) of FailureReasonDataEx items, empty list if none.
162 Raises exception on error.
163 """
164 _ = aiSortColumns;
165 self._ensureCachesPresent();
166
167 if tsNow is None:
168 self._oDb.execute('SELECT FailureReasons.*,\n'
169 ' FailureCategories.sShort AS sCategory\n'
170 'FROM FailureReasons,\n'
171 ' FailureCategories\n'
172 'WHERE FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
173 ' AND FailureCategories.idFailureCategory = FailureReasons.idFailureCategory\n'
174 ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n'
175 'ORDER BY sCategory ASC, sShort ASC\n'
176 'LIMIT %s OFFSET %s\n'
177 , (cMaxRows, iStart,));
178 else:
179 self._oDb.execute('SELECT FailureReasons.*,\n'
180 ' FailureCategories.sShort AS sCategory\n'
181 'FROM FailureReasons,\n'
182 ' FailureCategories\n'
183 'WHERE FailureReasons.tsExpire > %s\n'
184 ' AND FailureReasons.tsEffective <= %s\n'
185 ' AND FailureCategories.idFailureCategory = FailureReasons.idFailureCategory\n'
186 ' AND FailureReasons.tsExpire > %s\n'
187 ' AND FailureReasons.tsEffective <= %s\n'
188 'ORDER BY sCategory ASC, sShort ASC\n'
189 'LIMIT %s OFFSET %s\n'
190 , (tsNow, tsNow, tsNow, tsNow, cMaxRows, iStart,));
191
192 aoRows = []
193 for aoRow in self._oDb.fetchAll():
194 aoRows.append(FailureReasonDataEx().initFromDbRowEx(aoRow, self.oCategoryLogic, self.oUserAccountLogic));
195 return aoRows
196
197 def fetchForListingInCategory(self, iStart, cMaxRows, tsNow, idFailureCategory, aiSortColumns = None):
198 """
199 Fetches Failure Category records.
200
201 Returns an array (list) of FailureReasonDataEx items, empty list if none.
202 Raises exception on error.
203 """
204 _ = aiSortColumns;
205 self._ensureCachesPresent();
206
207 if tsNow is None:
208 self._oDb.execute('SELECT *\n'
209 'FROM FailureReasons\n'
210 'WHERE tsExpire = \'infinity\'::TIMESTAMP\n'
211 ' AND idFailureCategory = %s\n'
212 'ORDER BY sShort ASC\n'
213 'LIMIT %s OFFSET %s\n'
214 , ( idFailureCategory, cMaxRows, iStart,));
215 else:
216 self._oDb.execute('SELECT *\n'
217 'FROM FailureReasons\n'
218 'WHERE idFailureCategory = %s\n'
219 ' AND tsExpire > %s\n'
220 ' AND tsEffective <= %s\n'
221 'ORDER BY sShort ASC\n'
222 'LIMIT %s OFFSET %s\n'
223 , ( idFailureCategory, tsNow, tsNow, cMaxRows, iStart,));
224
225 aoRows = []
226 for aoRow in self._oDb.fetchAll():
227 aoRows.append(FailureReasonDataEx().initFromDbRowEx(aoRow, self.oCategoryLogic, self.oUserAccountLogic));
228 return aoRows
229
230
231 def fetchForSheriffByNamedCategory(self, sFailureCategory):
232 """
233 Fetches the short names of the reasons in the named category.
234
235 Returns array of strings.
236 Raises exception on error.
237 """
238 self._oDb.execute('SELECT FailureReasons.sShort\n'
239 'FROM FailureReasons,\n'
240 ' FailureCategories\n'
241 'WHERE FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
242 ' AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory\n'
243 ' AND FailureCategories.sShort = %s\n'
244 'ORDER BY FailureReasons.sShort ASC\n'
245 , ( sFailureCategory,));
246 return [aoRow[0] for aoRow in self._oDb.fetchAll()];
247
248
249 def fetchForCombo(self, sFirstEntry = 'Select a failure reason', tsEffective = None):
250 """
251 Gets the list of Failure Reasons for a combo box.
252 Returns an array of (value [idFailureReason], drop-down-name [sShort],
253 hover-text [sFull]) tuples.
254 """
255 if tsEffective is None:
256 self._oDb.execute('SELECT fr.idFailureReason, CONCAT(fc.sShort, \' / \', fr.sShort) as sComboText, fr.sFull\n'
257 'FROM FailureReasons fr,\n'
258 ' FailureCategories fc\n'
259 'WHERE fr.idFailureCategory = fc.idFailureCategory\n'
260 ' AND fr.tsExpire = \'infinity\'::TIMESTAMP\n'
261 ' AND fc.tsExpire = \'infinity\'::TIMESTAMP\n'
262 'ORDER BY sComboText')
263 else:
264 self._oDb.execute('SELECT fr.idFailureReason, CONCAT(fc.sShort, \' / \', fr.sShort) as sComboText, fr.sFull\n'
265 'FROM FailureReasons fr,\n'
266 ' FailureCategories fc\n'
267 'WHERE fr.idFailureCategory = fc.idFailureCategory\n'
268 ' AND fr.tsExpire > %s\n'
269 ' AND fr.tsEffective <= %s\n'
270 ' AND fc.tsExpire > %s\n'
271 ' AND fc.tsEffective <= %s\n'
272 'ORDER BY sComboText'
273 , (tsEffective, tsEffective, tsEffective, tsEffective));
274 aoRows = self._oDb.fetchAll();
275 return [(-1, sFirstEntry, '')] + aoRows;
276
277
278 def fetchForChangeLog(self, idFailureReason, iStart, cMaxRows, tsNow): # pylint: disable=R0914
279 """
280 Fetches change log entries for a failure reason.
281
282 Returns an array of ChangeLogEntry instance and an indicator whether
283 there are more entries.
284 Raises exception on error.
285 """
286 self._ensureCachesPresent();
287
288 if tsNow is None:
289 tsNow = self._oDb.getCurrentTimestamp();
290
291 # 1. Get a list of the relevant changes.
292 self._oDb.execute('SELECT * FROM FailureReasons WHERE idFailureReason = %s AND tsEffective <= %s\n'
293 'ORDER BY tsEffective DESC\n'
294 'LIMIT %s OFFSET %s\n'
295 , ( idFailureReason, tsNow, cMaxRows + 1, iStart, ));
296 aoRows = [];
297 for aoChange in self._oDb.fetchAll():
298 aoRows.append(FailureReasonData().initFromDbRow(aoChange));
299
300 # 2. Calculate the changes.
301 aoEntries = [];
302 for i in xrange(0, len(aoRows) - 1):
303 oNew = aoRows[i];
304 oOld = aoRows[i + 1];
305
306 aoChanges = [];
307 for sAttr in oNew.getDataAttributes():
308 if sAttr not in [ 'tsEffective', 'tsExpire', 'uidAuthor', ]:
309 oOldAttr = getattr(oOld, sAttr);
310 oNewAttr = getattr(oNew, sAttr);
311 if oOldAttr != oNewAttr:
312 if sAttr == 'idFailureCategory':
313 oCat = self.oCategoryLogic.cachedLookup(oOldAttr);
314 if oCat is not None:
315 oOldAttr = '%s (%s)' % (oOldAttr, oCat.sShort, );
316 oCat = self.oCategoryLogic.cachedLookup(oNewAttr);
317 if oCat is not None:
318 oNewAttr = '%s (%s)' % (oNewAttr, oCat.sShort, );
319 aoChanges.append(AttributeChangeEntry(sAttr, oNewAttr, oOldAttr, str(oNewAttr), str(oOldAttr)));
320
321 aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, oOld, aoChanges));
322
323 # If we're at the end of the log, add the initial entry.
324 if len(aoRows) <= cMaxRows and aoRows:
325 oNew = aoRows[-1];
326 aoEntries.append(ChangeLogEntry(oNew.uidAuthor, None, oNew.tsEffective, oNew.tsExpire, oNew, None, []));
327
328 return (UserAccountLogic(self._oDb).resolveChangeLogAuthors(aoEntries), len(aoRows) > cMaxRows);
329
330
331 def getById(self, idFailureReason):
332 """Get Failure Reason data by idFailureReason"""
333
334 self._oDb.execute('SELECT *\n'
335 'FROM FailureReasons\n'
336 'WHERE tsExpire = \'infinity\'::timestamp\n'
337 ' AND idFailureReason = %s;', (idFailureReason,))
338 aRows = self._oDb.fetchAll()
339 if len(aRows) not in (0, 1):
340 raise self._oDb.integrityException(
341 'Found more than one failure reasons with the same credentials. Database structure is corrupted.')
342 try:
343 return FailureReasonData().initFromDbRow(aRows[0])
344 except IndexError:
345 return None
346
347
348 def addEntry(self, oData, uidAuthor, fCommit = False):
349 """
350 Add a failure reason.
351 """
352 #
353 # Validate.
354 #
355 dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Add);
356 if dErrors:
357 raise TMInvalidData('addEntry invalid input: %s' % (dErrors,));
358
359 #
360 # Add the record.
361 #
362 self._readdEntry(uidAuthor, oData);
363 self._oDb.maybeCommit(fCommit);
364 return True;
365
366
367 def editEntry(self, oData, uidAuthor, fCommit = False):
368 """
369 Modifies a failure reason.
370 """
371
372 #
373 # Validate inputs and read in the old(/current) data.
374 #
375 assert isinstance(oData, FailureReasonData);
376 dErrors = oData.validateAndConvert(self._oDb, oData.ksValidateFor_Edit);
377 if dErrors:
378 raise TMInvalidData('editEntry invalid input: %s' % (dErrors,));
379
380 oOldData = FailureReasonData().initFromDbWithId(self._oDb, oData.idFailureReason);
381
382 #
383 # Update the data that needs updating.
384 #
385 if not oData.isEqualEx(oOldData, [ 'tsEffective', 'tsExpire', 'uidAuthor', ]):
386 self._historizeEntry(oData.idFailureReason);
387 self._readdEntry(uidAuthor, oData);
388 self._oDb.maybeCommit(fCommit);
389 return True;
390
391
392 def removeEntry(self, uidAuthor, idFailureReason, fCascade = False, fCommit = False):
393 """
394 Deletes a failure reason.
395 """
396 _ = fCascade; # too complicated for now.
397
398 #
399 # Check whether it's being used by other tables and bitch if it is .
400 # We currently do not implement cascading.
401 #
402 self._oDb.execute('SELECT CONCAT(idBlacklisting, \' - blacklisting\')\n'
403 'FROM BuildBlacklist\n'
404 'WHERE idFailureReason = %s\n'
405 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
406 'UNION\n'
407 'SELECT CONCAT(idTestResult, \' - test result failure reason\')\n'
408 'FROM TestResultFailures\n'
409 'WHERE idFailureReason = %s\n'
410 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
411 , (idFailureReason, idFailureReason,));
412 aaoRows = self._oDb.fetchAll();
413 if aaoRows:
414 raise TMRowInUse('Cannot remove failure reason %u because its being used by: %s'
415 % (idFailureReason, ', '.join(aoRow[0] for aoRow in aaoRows),));
416
417 #
418 # Do the job.
419 #
420 oData = FailureReasonData().initFromDbWithId(self._oDb, idFailureReason);
421 assert oData.idFailureReason == idFailureReason;
422 (tsCur, tsCurMinusOne) = self._oDb.getCurrentTimestamps();
423 if oData.tsEffective != tsCur and oData.tsEffective != tsCurMinusOne:
424 self._historizeEntry(idFailureReason, tsCurMinusOne);
425 self._readdEntry(uidAuthor, oData, tsCurMinusOne);
426 self._historizeEntry(idFailureReason);
427 self._oDb.maybeCommit(fCommit);
428 return True;
429
430
431 def cachedLookup(self, idFailureReason):
432 """
433 Looks up the most recent FailureReasonDataEx object for idFailureReason
434 via an object cache.
435
436 Returns a shared FailureReasonData object. None if not found.
437 Raises exception on DB error.
438 """
439 if self.dCache is None:
440 self.dCache = self._oDb.getCache('FailureReasonDataEx');
441 oEntry = self.dCache.get(idFailureReason, None);
442 if oEntry is None:
443 self._oDb.execute('SELECT *\n'
444 'FROM FailureReasons\n'
445 'WHERE idFailureReason = %s\n'
446 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
447 , (idFailureReason, ));
448 if self._oDb.getRowCount() == 0:
449 # Maybe it was deleted, try get the last entry.
450 self._oDb.execute('SELECT *\n'
451 'FROM FailureReasons\n'
452 'WHERE idFailureReason = %s\n'
453 'ORDER BY tsExpire DESC\n'
454 'LIMIT 1\n'
455 , (idFailureReason, ));
456 elif self._oDb.getRowCount() > 1:
457 raise self._oDb.integrityException('%s infinity rows for %s' % (self._oDb.getRowCount(), idFailureReason));
458
459 if self._oDb.getRowCount() == 1:
460 self._ensureCachesPresent();
461 oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic,
462 self.oUserAccountLogic);
463 self.dCache[idFailureReason] = oEntry;
464 return oEntry;
465
466
467 def cachedLookupByNameAndCategory(self, sName, sCategory):
468 """
469 Looks up a failure reason by it's name and category.
470
471 Should the request be ambigiuos, we'll return the oldest one.
472
473 Returns a shared FailureReasonData object. None if not found.
474 Raises exception on DB error.
475 """
476 if self.dCacheNameAndCat is None:
477 self.dCacheNameAndCat = self._oDb.getCache('FailureReasonDataEx-By-Name-And-Category');
478 sKey = '%s:::%s' % (sName, sCategory,);
479 oEntry = self.dCacheNameAndCat.get(sKey, None);
480 if oEntry is None:
481 self._oDb.execute('SELECT *\n'
482 'FROM FailureReasons,\n'
483 ' FailureCategories\n'
484 'WHERE FailureReasons.sShort = %s\n'
485 ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
486 ' AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory '
487 ' AND FailureCategories.sShort = %s\n'
488 ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n'
489 'ORDER BY FailureReasons.tsEffective\n'
490 , ( sName, sCategory));
491 if self._oDb.getRowCount() == 0:
492 sLikeSucks = self._oDb.formatBindArgs(
493 'SELECT *\n'
494 'FROM FailureReasons,\n'
495 ' FailureCategories\n'
496 'WHERE ( FailureReasons.sShort ILIKE @@@@@@@! %s !@@@@@@@\n'
497 ' OR FailureReasons.sFull ILIKE @@@@@@@! %s !@@@@@@@)\n'
498 ' AND FailureCategories.tsExpire = \'infinity\'::TIMESTAMP\n'
499 ' AND FailureReasons.idFailureCategory = FailureCategories.idFailureCategory\n'
500 ' AND ( FailureCategories.sShort = %s\n'
501 ' OR FailureCategories.sFull = %s)\n'
502 ' AND FailureReasons.tsExpire = \'infinity\'::TIMESTAMP\n'
503 'ORDER BY FailureReasons.tsEffective\n'
504 , ( sName, sName, sCategory, sCategory ));
505 sLikeSucks = sLikeSucks.replace('LIKE @@@@@@@! \'', 'LIKE \'%').replace('\' !@@@@@@@', '%\'');
506 self._oDb.execute(sLikeSucks);
507 if self._oDb.getRowCount() > 0:
508 self._ensureCachesPresent();
509 oEntry = FailureReasonDataEx().initFromDbRowEx(self._oDb.fetchOne(), self.oCategoryLogic,
510 self.oUserAccountLogic);
511 self.dCacheNameAndCat[sKey] = oEntry;
512 if sName != oEntry.sShort or sCategory != oEntry.oCategory.sShort:
513 sKey2 = '%s:::%s' % (oEntry.sShort, oEntry.oCategory.sShort,);
514 self.dCacheNameAndCat[sKey2] = oEntry;
515 return oEntry;
516
517
518 #
519 # Helpers.
520 #
521
522 def _readdEntry(self, uidAuthor, oData, tsEffective = None):
523 """
524 Re-adds the FailureReasons entry. Used by addEntry, editEntry and removeEntry.
525 """
526 if tsEffective is None:
527 tsEffective = self._oDb.getCurrentTimestamp();
528 self._oDb.execute('INSERT INTO FailureReasons (\n'
529 ' uidAuthor,\n'
530 ' tsEffective,\n'
531 ' idFailureReason,\n'
532 ' idFailureCategory,\n'
533 ' sShort,\n'
534 ' sFull,\n'
535 ' iTicket,\n'
536 ' asUrls)\n'
537 'VALUES (%s, %s, '
538 + ( 'DEFAULT' if oData.idFailureReason is None else str(oData.idFailureReason) )
539 + ', %s, %s, %s, %s, %s)\n'
540 , ( uidAuthor,
541 tsEffective,
542 oData.idFailureCategory,
543 oData.sShort,
544 oData.sFull,
545 oData.iTicket,
546 oData.asUrls,) );
547 return True;
548
549
550 def _historizeEntry(self, idFailureReason, tsExpire = None):
551 """ Historizes the current entry. """
552 if tsExpire is None:
553 tsExpire = self._oDb.getCurrentTimestamp();
554 self._oDb.execute('UPDATE FailureReasons\n'
555 'SET tsExpire = %s\n'
556 'WHERE idFailureReason = %s\n'
557 ' AND tsExpire = \'infinity\'::TIMESTAMP\n'
558 , (tsExpire, idFailureReason,));
559 return True;
560
561
562 def _ensureCachesPresent(self):
563 """ Ensures we've got the cache references resolved. """
564 if self.oCategoryLogic is None:
565 from testmanager.core.failurecategory import FailureCategoryLogic;
566 self.oCategoryLogic = FailureCategoryLogic(self._oDb);
567 if self.oUserAccountLogic is None:
568 self.oUserAccountLogic = UserAccountLogic(self._oDb);
569 return True;
570
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