VirtualBox

source: vbox/trunk/src/VBox/ValidationKit/testmanager/core/base.py@ 61282

Last change on this file since 61282 was 61282, checked in by vboxsync, 9 years ago

virtual test sheriff: Kick-off and the bad-testbox rebooting and disabling. (completely untested)

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 47.7 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: base.py 61282 2016-05-29 19:49:31Z vboxsync $
3# pylint: disable=C0302
4
5"""
6Test Manager Core - Base Class(es).
7"""
8
9__copyright__ = \
10"""
11Copyright (C) 2012-2015 Oracle Corporation
12
13This file is part of VirtualBox Open Source Edition (OSE), as
14available from http://www.215389.xyz. This file is free software;
15you can redistribute it and/or modify it under the terms of the GNU
16General Public License (GPL) as published by the Free Software
17Foundation, in version 2 as it comes in the "COPYING" file of the
18VirtualBox OSE distribution. VirtualBox OSE is distributed in the
19hope that it will be useful, but WITHOUT ANY WARRANTY of any kind.
20
21The contents of this file may alternatively be used under the terms
22of the Common Development and Distribution License Version 1.0
23(CDDL) only, as it comes in the "COPYING.CDDL" file of the
24VirtualBox OSE distribution, in which case the provisions of the
25CDDL are applicable instead of those of the GPL.
26
27You may elect to license modified versions of this file under the
28terms and conditions of either the GPL or the CDDL or both.
29"""
30__version__ = "$Revision: 61282 $"
31
32
33# Standard python imports.
34import copy;
35import re;
36import socket;
37import sys;
38import uuid;
39import unittest;
40
41# Validation Kit imports.
42from common import utils;
43
44# Python 3 hacks:
45if sys.version_info[0] >= 3:
46 long = int # pylint: disable=W0622,C0103
47
48
49class TMExceptionBase(Exception):
50 """
51 For exceptions raised by any TestManager component.
52 """
53 pass;
54
55
56class TMTooManyRows(TMExceptionBase):
57 """
58 Too many rows in the result.
59 Used by ModelLogicBase decendants.
60 """
61 pass;
62
63
64class TMRowNotFound(TMExceptionBase):
65 """
66 Database row not found.
67 Used by ModelLogicBase decendants.
68 """
69 pass;
70
71
72class TMRowAlreadyExists(TMExceptionBase):
73 """
74 Database row already exists (typically raised by addEntry).
75 Used by ModelLogicBase decendants.
76 """
77 pass;
78
79
80class TMInvalidData(TMExceptionBase):
81 """
82 Data validation failed.
83 Used by ModelLogicBase decendants.
84 """
85 pass;
86
87
88class TMRowInUse(TMExceptionBase):
89 """
90 Database row is in use and cannot be deleted.
91 Used by ModelLogicBase decendants.
92 """
93 pass;
94
95
96class TMInFligthCollision(TMExceptionBase):
97 """
98 Database update failed because someone else had already made changes to
99 the data there.
100 Used by ModelLogicBase decendants.
101 """
102 pass;
103
104
105class ModelBase(object): # pylint: disable=R0903
106 """
107 Something all classes in the logical model inherits from.
108
109 Not sure if 'logical model' is the right term here.
110 Will see if it has any purpose later on...
111 """
112
113 def __init__(self):
114 pass;
115
116
117class ModelDataBase(ModelBase): # pylint: disable=R0903
118 """
119 Something all classes in the data classes in the logical model inherits from.
120 """
121
122 ## Child classes can use this to list array attributes which should use
123 # an empty array ([]) instead of None as database NULL value.
124 kasAltArrayNull = [];
125
126 ## validate
127 ## @{
128 ksValidateFor_Add = 'add';
129 ksValidateFor_AddForeignId = 'add-foreign-id';
130 ksValidateFor_Edit = 'edit';
131 ksValidateFor_Other = 'other';
132 ## @}
133
134
135 def __init__(self):
136 ModelBase.__init__(self);
137
138
139 #
140 # Standard methods implemented by combining python magic and hungarian prefixes.
141 #
142
143 def getDataAttributes(self):
144 """
145 Returns a list of data attributes.
146 """
147 asRet = [];
148 asAttrs = dir(self);
149 for sAttr in asAttrs:
150 if sAttr[0] == '_' or sAttr[0] == 'k':
151 continue;
152 oValue = getattr(self, sAttr);
153 if callable(oValue):
154 continue;
155 asRet.append(sAttr);
156 return asRet;
157
158 def initFromOther(self, oOther):
159 """
160 Initialize this object with the values from another instance (child
161 class instance is accepted).
162
163 This serves as a kind of copy constructor.
164
165 Returns self. May raise exception if the type of other object differs
166 or is damaged.
167 """
168 for sAttr in self.getDataAttributes():
169 setattr(self, sAttr, getattr(oOther, sAttr));
170 return self;
171
172 @staticmethod
173 def getHungarianPrefix(sName):
174 """
175 Returns the hungarian prefix of the given name.
176 """
177 for i, _ in enumerate(sName):
178 if sName[i] not in ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
179 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']:
180 assert re.search('^[A-Z][a-zA-Z0-9]*$', sName[i:]) is not None;
181 return sName[:i];
182 return sName;
183
184 def getAttributeParamNullValues(self, sAttr):
185 """
186 Returns a list of parameter NULL values, with the preferred one being
187 the first element.
188
189 Child classes can override this to handle one or more attributes specially.
190 """
191 sPrefix = self.getHungarianPrefix(sAttr);
192 if sPrefix in ['id', 'uid', 'i', 'off', 'pct']:
193 return [-1, '', '-1',];
194 elif sPrefix in ['l', 'c',]:
195 return [long(-1), '', '-1',];
196 elif sPrefix == 'f':
197 return ['',];
198 elif sPrefix in ['enm', 'ip', 's', 'ts', 'uuid']:
199 return ['',];
200 elif sPrefix in ['ai', 'aid', 'al', 'as']:
201 return [[], '', None]; ## @todo ??
202 elif sPrefix == 'bm':
203 return ['', [],]; ## @todo bitmaps.
204 raise TMExceptionBase('Unable to classify "%s" (prefix %s)' % (sAttr, sPrefix));
205
206 def isAttributeNull(self, sAttr, oValue):
207 """
208 Checks if the specified attribute value indicates NULL.
209 Return True/False.
210
211 Note! This isn't entirely kosher actually.
212 """
213 if oValue is None:
214 return True;
215 aoNilValues = self.getAttributeParamNullValues(sAttr);
216 return oValue in aoNilValues;
217
218 def _convertAttributeFromParamNull(self, sAttr, oValue):
219 """
220 Converts an attribute from parameter NULL to database NULL value.
221 Returns the new attribute value.
222 """
223 aoNullValues = self.getAttributeParamNullValues(sAttr);
224 if oValue in aoNullValues:
225 oValue = None if sAttr not in self.kasAltArrayNull else [];
226 #
227 # Perform deep conversion on ModelDataBase object and lists of them.
228 #
229 elif isinstance(oValue, list) and len(oValue) > 0 and isinstance(oValue[0], ModelDataBase):
230 oValue = copy.copy(oValue);
231 for i, _ in enumerate(oValue):
232 assert isinstance(oValue[i], ModelDataBase);
233 oValue[i] = copy.copy(oValue[i]);
234 oValue[i].convertFromParamNull();
235
236 elif isinstance(oValue, ModelDataBase):
237 oValue = copy.copy(oValue);
238 oValue.convertFromParamNull();
239
240 return oValue;
241
242 def convertFromParamNull(self):
243 """
244 Converts from parameter NULL values to database NULL values (None).
245 Returns self.
246 """
247 for sAttr in self.getDataAttributes():
248 oValue = getattr(self, sAttr);
249 oNewValue = self._convertAttributeFromParamNull(sAttr, oValue);
250 if oValue != oNewValue:
251 setattr(self, sAttr, oNewValue);
252 return self;
253
254 def _convertAttributeToParamNull(self, sAttr, oValue):
255 """
256 Converts an attribute from database NULL to a sepcial value we can pass
257 thru parameter list.
258 Returns the new attribute value.
259 """
260 if oValue is None:
261 oValue = self.getAttributeParamNullValues(sAttr)[0];
262 #
263 # Perform deep conversion on ModelDataBase object and lists of them.
264 #
265 elif isinstance(oValue, list) and len(oValue) > 0 and isinstance(oValue[0], ModelDataBase):
266 oValue = copy.copy(oValue);
267 for i, _ in enumerate(oValue):
268 assert isinstance(oValue[i], ModelDataBase);
269 oValue[i] = copy.copy(oValue[i]);
270 oValue[i].convertToParamNull();
271
272 elif isinstance(oValue, ModelDataBase):
273 oValue = copy.copy(oValue);
274 oValue.convertToParamNull();
275
276 return oValue;
277
278 def convertToParamNull(self):
279 """
280 Converts from database NULL values (None) to special values we can
281 pass thru parameters list.
282 Returns self.
283 """
284 for sAttr in self.getDataAttributes():
285 oValue = getattr(self, sAttr);
286 oNewValue = self._convertAttributeToParamNull(sAttr, oValue);
287 if oValue != oNewValue:
288 setattr(self, sAttr, oNewValue);
289 return self;
290
291 def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb):
292 """
293 Validates and convert one attribute.
294 Returns the converted value.
295
296 Child classes can override this to handle one or more attributes specially.
297 Note! oDb can be None.
298 """
299 sPrefix = self.getHungarianPrefix(sAttr);
300
301 if sPrefix in ['id', 'uid']:
302 (oNewValue, sError) = self.validateInt( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
303 elif sPrefix in ['i', 'off', 'pct']:
304 (oNewValue, sError) = self.validateInt( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
305 iMin = getattr(self, 'kiMin_' + sAttr, 0),
306 iMax = getattr(self, 'kiMax_' + sAttr, 0x7ffffffe));
307 elif sPrefix in ['l', 'c']:
308 (oNewValue, sError) = self.validateLong(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
309 lMin = getattr(self, 'klMin_' + sAttr, 0),
310 lMax = getattr(self, 'klMax_' + sAttr, None));
311 elif sPrefix == 'f':
312 if oValue is '' and not fAllowNull: oValue = '0'; # HACK ALERT! Checkboxes are only added when checked.
313 (oNewValue, sError) = self.validateBool(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
314 elif sPrefix == 'ts':
315 (oNewValue, sError) = self.validateTs( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
316 elif sPrefix == 'ip':
317 (oNewValue, sError) = self.validateIp( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
318 elif sPrefix == 'uuid':
319 (oNewValue, sError) = self.validateUuid(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
320 elif sPrefix == 'enm':
321 (oNewValue, sError) = self.validateWord(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
322 asValid = getattr(self, 'kasValidValues_' + sAttr)); # The list is required.
323 elif sPrefix == 's':
324 (oNewValue, sError) = self.validateStr( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
325 cchMin = getattr(self, 'kcchMin_' + sAttr, 0),
326 cchMax = getattr(self, 'kcchMax_' + sAttr, 4096),
327 fAllowUnicodeSymbols = getattr(self, 'kfAllowUnicode_' + sAttr, False) );
328 ## @todo al.
329 elif sPrefix == 'aid':
330 (oNewValue, sError) = self.validateListOfInts(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
331 iMin = 1, iMax = 0x7ffffffe);
332 elif sPrefix == 'as':
333 (oNewValue, sError) = self.validateListOfStr(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
334 asValidValues = getattr(self, 'kasValidValues_' + sAttr, None),
335 cchMin = getattr(self, 'kcchMin_' + sAttr, 0 if fAllowNull else 1),
336 cchMax = getattr(self, 'kcchMax_' + sAttr, 4096));
337
338 elif sPrefix == 'bm':
339 ## @todo figure out bitfields.
340 (oNewValue, sError) = self.validateListOfStr(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
341 else:
342 raise TMExceptionBase('Unable to classify "%s" (prefix %s)' % (sAttr, sPrefix));
343
344 _ = sParam; _ = oDb;
345 return (oNewValue, sError);
346
347 def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ksValidateFor_Other):
348 """
349 Worker for implementing validateAndConvert().
350 """
351 dErrors = dict();
352 for sAttr in self.getDataAttributes():
353 oValue = getattr(self, sAttr);
354 sParam = getattr(self, 'ksParam_' + sAttr);
355 aoNilValues = self.getAttributeParamNullValues(sAttr);
356 aoNilValues.append(None);
357
358 (oNewValue, sError) = self._validateAndConvertAttribute(sAttr, sParam, oValue, aoNilValues,
359 sAttr in asAllowNullAttributes, oDb);
360 if oValue != oNewValue:
361 setattr(self, sAttr, oNewValue);
362 if sError is not None:
363 dErrors[sParam] = sError;
364
365 # Check the NULL requirements of the primary ID(s) for the 'add' and 'edit' actions.
366 if enmValidateFor == ModelDataBase.ksValidateFor_Add \
367 or enmValidateFor == ModelDataBase.ksValidateFor_AddForeignId \
368 or enmValidateFor == ModelDataBase.ksValidateFor_Edit:
369 fMustBeNull = enmValidateFor == ModelDataBase.ksValidateFor_Add;
370 sAttr = getattr(self, 'ksIdAttr', None);
371 if sAttr is not None:
372 oValue = getattr(self, sAttr);
373 if self.isAttributeNull(sAttr, oValue) != fMustBeNull:
374 sParam = getattr(self, 'ksParam_' + sAttr);
375 sErrMsg = 'Must be NULL!' if fMustBeNull else 'Must not be NULL!'
376 if sParam in dErrors:
377 dErrors[sParam] += ' ' + sErrMsg;
378 else:
379 dErrors[sParam] = sErrMsg;
380
381 return dErrors;
382
383 def validateAndConvert(self, oDb, enmValidateFor = ksValidateFor_Other):
384 """
385 Validates the input and converts valid fields to their right type.
386 Returns a dictionary with per field reports, only invalid fields will
387 be returned, so an empty dictionary means that the data is valid.
388
389 The dictionary keys are ksParam_*.
390
391 Child classes can override _validateAndConvertAttribute to handle
392 selected fields specially. There are also a few class variables that
393 can be used to advice the validation: kcchMin_sAttr, kcchMax_sAttr,
394 kiMin_iAttr, kiMax_iAttr, klMin_lAttr, klMax_lAttr,
395 kasValidValues_enmAttr, and kasAllowNullAttributes.
396 """
397 return self._validateAndConvertWorker(getattr(self, 'kasAllowNullAttributes', list()), oDb,
398 enmValidateFor = enmValidateFor);
399
400 def convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict):
401 """
402 Calculate the attribute value when initialized from a parameter.
403
404 Returns the new value, with parameter NULL values. Raises exception on
405 invalid parameter value.
406
407 Child classes can override to do special parameter conversion jobs.
408 """
409 sPrefix = self.getHungarianPrefix(sAttr);
410 asValidValues = getattr(self, 'kasValidValues_' + sAttr, None);
411 if fStrict:
412 if sPrefix == 'f':
413 # HACK ALERT! Checkboxes are only present when checked, so we always have to provide a default.
414 oNewValue = oDisp.getStringParam(sParam, asValidValues, '0');
415 elif sPrefix[0] == 'a':
416 # HACK ALERT! Lists are not present if empty.
417 oNewValue = oDisp.getListOfStrParams(sParam, []);
418 else:
419 oNewValue = oDisp.getStringParam(sParam, asValidValues, None);
420 else:
421 if sPrefix[0] == 'a':
422 oNewValue = oDisp.getListOfStrParams(sParam, []);
423 else:
424 assert oValue is not None, 'sAttr=%s' % (sAttr,);
425 oNewValue = oDisp.getStringParam(sParam, asValidValues, oValue);
426 return oNewValue;
427
428 def initFromParams(self, oDisp, fStrict = True):
429 """
430 Initialize the object from parameters.
431 The input is not validated at all, except that all parameters must be
432 present when fStrict is True.
433
434 Returns self. Raises exception on invalid parameter value.
435
436 Note! The returned object has parameter NULL values, not database ones!
437 """
438
439 self.convertToParamNull()
440 for sAttr in self.getDataAttributes():
441 oValue = getattr(self, sAttr);
442 oNewValue = self.convertParamToAttribute(sAttr, getattr(self, 'ksParam_' + sAttr), oValue, oDisp, fStrict);
443 if oNewValue != oValue:
444 setattr(self, sAttr, oNewValue);
445 return self;
446
447 def areAttributeValuesEqual(self, sAttr, sPrefix, oValue1, oValue2):
448 """
449 Called to compare two attribute values and python thinks differs.
450
451 Returns True/False.
452
453 Child classes can override this to do special compares of things like arrays.
454 """
455 # Just in case someone uses it directly.
456 if oValue1 == oValue2:
457 return True;
458
459 #
460 # Timestamps can be both string (param) and object (db)
461 # depending on the data source. Compare string values to make
462 # sure we're doing the right thing here.
463 #
464 if sPrefix == 'ts':
465 return str(oValue1) == str(oValue2);
466
467 #
468 # Some generic code handling ModelDataBase children.
469 #
470 if isinstance(oValue1, list) and isinstance(oValue2, list):
471 if len(oValue1) == len(oValue2):
472 for i, _ in enumerate(oValue1):
473 if not isinstance(oValue1[i], ModelDataBase) \
474 or type(oValue1) is not type(oValue2):
475 return False;
476 if not oValue1[i].isEqual(oValue2[i]):
477 return False;
478 return True;
479
480 elif isinstance(oValue1, ModelDataBase) \
481 and type(oValue1) is type(oValue2):
482 return oValue1[i].isEqual(oValue2[i]);
483
484 _ = sAttr;
485 return False;
486
487 def isEqual(self, oOther):
488 """ Compares two instances. """
489 for sAttr in self.getDataAttributes():
490 if getattr(self, sAttr) != getattr(oOther, sAttr):
491 # Delegate the final decision to an overridable method.
492 if not self.areAttributeValuesEqual(sAttr, self.getHungarianPrefix(sAttr),
493 getattr(self, sAttr), getattr(oOther, sAttr)):
494 return False;
495 return True;
496
497 def isEqualEx(self, oOther, asExcludeAttrs):
498 """ Compares two instances, omitting the given attributes. """
499 for sAttr in self.getDataAttributes():
500 if sAttr not in asExcludeAttrs \
501 and getattr(self, sAttr) != getattr(oOther, sAttr):
502 # Delegate the final decision to an overridable method.
503 if not self.areAttributeValuesEqual(sAttr, self.getHungarianPrefix(sAttr),
504 getattr(self, sAttr), getattr(oOther, sAttr)):
505 return False;
506 return True;
507
508 def reinitToNull(self):
509 """
510 Reinitializes the object to (database) NULL values.
511 Returns self.
512 """
513 for sAttr in self.getDataAttributes():
514 setattr(self, sAttr, None);
515 return self;
516
517 def toString(self):
518 """
519 Stringifies the object.
520 Returns string representation.
521 """
522
523 sMembers = '';
524 for sAttr in self.getDataAttributes():
525 oValue = getattr(self, sAttr);
526 sMembers += ', %s=%s' % (sAttr, oValue);
527
528 oClass = type(self);
529 if sMembers == '':
530 return '<%s>' % (oClass.__name__);
531 return '<%s: %s>' % (oClass.__name__, sMembers[2:]);
532
533 def __str__(self):
534 return self.toString();
535
536
537
538 #
539 # New validation helpers.
540 #
541 # These all return (oValue, sError), where sError is None when the value
542 # is valid and an error message when not. On success and in case of
543 # range errors, oValue is converted into the requested type.
544 #
545
546 @staticmethod
547 def validateInt(sValue, iMin = 0, iMax = 0x7ffffffe, aoNilValues = tuple([-1, None, '']), fAllowNull = True):
548 """ Validates an integer field. """
549 if sValue in aoNilValues:
550 if fAllowNull:
551 return (None if sValue is None else aoNilValues[0], None);
552 return (sValue, 'Mandatory.');
553
554 try:
555 if utils.isString(sValue):
556 iValue = int(sValue, 0);
557 else:
558 iValue = int(sValue);
559 except:
560 return (sValue, 'Not an integer');
561
562 if iValue in aoNilValues:
563 return (aoNilValues[0], None if fAllowNull else 'Mandatory.');
564
565 if iValue < iMin:
566 return (iValue, 'Value too small (min %d)' % (iMin,));
567 elif iValue > iMax:
568 return (iValue, 'Value too high (max %d)' % (iMax,));
569 return (iValue, None);
570
571 @staticmethod
572 def validateLong(sValue, lMin = 0, lMax = None, aoNilValues = tuple([long(-1), None, '']), fAllowNull = True):
573 """ Validates an long integer field. """
574 if sValue in aoNilValues:
575 if fAllowNull:
576 return (None if sValue is None else aoNilValues[0], None);
577 return (sValue, 'Mandatory.');
578 try:
579 if utils.isString(sValue):
580 lValue = long(sValue, 0);
581 else:
582 lValue = long(sValue);
583 except:
584 return (sValue, 'Not a long integer');
585
586 if lValue in aoNilValues:
587 return (aoNilValues[0], None if fAllowNull else 'Mandatory.');
588
589 if lMin is not None and lValue < lMin:
590 return (lValue, 'Value too small (min %d)' % (lMin,));
591 elif lMax is not None and lValue > lMax:
592 return (lValue, 'Value too high (max %d)' % (lMax,));
593 return (lValue, None);
594
595 @staticmethod
596 def validateTs(sValue, aoNilValues = tuple([None, '']), fAllowNull = True):
597 """ Validates a timestamp field. """
598 if sValue in aoNilValues:
599 return (sValue, None if fAllowNull else 'Mandatory.');
600 if not utils.isString(sValue):
601 return (sValue, None);
602
603 sError = None;
604 if len(sValue) == len('2012-10-08 01:54:06.364207+02:00'):
605 oRes = re.match(r'(\d{4})-([01]\d)-([0123])\d ([012]\d):[0-5]\d:([0-6]\d).\d{6}[+-](\d\d):(\d\d)', sValue);
606 if oRes is not None \
607 and ( int(oRes.group(6)) > 12 \
608 or int(oRes.group(7)) >= 60):
609 sError = 'Invalid timezone offset.';
610 elif len(sValue) == len('2012-10-08 01:54:06.00'):
611 oRes = re.match(r'(\d{4})-([01]\d)-([0123])\d ([012]\d):[0-5]\d:([0-6]\d).\d{2}', sValue);
612 elif len(sValue) == len('9999-12-31 23:59:59.999999'):
613 oRes = re.match(r'(\d{4})-([01]\d)-([0123])\d ([012]\d):[0-5]\d:([0-6]\d).\d{6}', sValue);
614 elif len(sValue) == len('999999-12-31 00:00:00.00'):
615 oRes = re.match(r'(\d{6})-([01]\d)-([0123])\d ([012]\d):[0-5]\d:([0-6]\d).\d{2}', sValue);
616 elif len(sValue) == len('9999-12-31T23:59:59.999999Z'):
617 oRes = re.match(r'(\d{4})-([01]\d)-([0123])\d[Tt]([012]\d):[0-5]\d:([0-6]\d).\d{6}[Zz]', sValue);
618 elif len(sValue) == len('9999-12-31T23:59:59.999999999Z'):
619 oRes = re.match(r'(\d{4})-([01]\d)-([0123])\d[Tt]([012]\d):[0-5]\d:([0-6]\d).\d{9}[Zz]', sValue);
620 else:
621 return (sValue, 'Invalid timestamp length.');
622
623 if oRes is None:
624 sError = 'Invalid timestamp (format: 2012-10-08 01:54:06.364207+02:00).';
625 else:
626 iYear = int(oRes.group(1));
627 if iYear % 4 == 0 and (iYear % 100 != 0 or iYear % 400 == 0):
628 acDaysOfMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
629 else:
630 acDaysOfMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
631 iMonth = int(oRes.group(2));
632 iDay = int(oRes.group(3));
633 iHour = int(oRes.group(4));
634 iSec = int(oRes.group(5));
635 if iMonth > 12:
636 sError = 'Invalid timestamp month.';
637 elif iDay > acDaysOfMonth[iMonth - 1]:
638 sError = 'Invalid timestamp day-of-month (%02d has %d days).' % (iMonth, acDaysOfMonth[iMonth - 1]);
639 elif iHour > 23:
640 sError = 'Invalid timestamp hour.'
641 elif iSec >= 61:
642 sError = 'Invalid timestamp second.'
643 elif iSec >= 60:
644 sError = 'Invalid timestamp: no leap seconds, please.'
645 return (sValue, sError);
646
647 @staticmethod
648 def validateIp(sValue, aoNilValues = tuple([None, '']), fAllowNull = True):
649 """ Validates an IP address field. """
650 if sValue in aoNilValues:
651 return (sValue, None if fAllowNull else 'Mandatory.');
652
653 if sValue == '::1':
654 return (sValue, None);
655
656 try:
657 socket.inet_pton(socket.AF_INET, sValue); # pylint: disable=E1101
658 except:
659 try:
660 socket.inet_pton(socket.AF_INET6, sValue); # pylint: disable=E1101
661 except:
662 return (sValue, 'Not a valid IP address.');
663
664 return (sValue, None);
665
666 @staticmethod
667 def validateBool(sValue, aoNilValues = tuple([None, '']), fAllowNull = True):
668 """ Validates a boolean field. """
669 if sValue in aoNilValues:
670 return (sValue, None if fAllowNull else 'Mandatory.');
671
672 if sValue in ('True', 'true', '1', True):
673 return (True, None);
674 if sValue in ('False', 'false', '0', False):
675 return (False, None);
676 return (sValue, 'Invalid boolean value.');
677
678 @staticmethod
679 def validateUuid(sValue, aoNilValues = tuple([None, '']), fAllowNull = True):
680 """ Validates an UUID field. """
681 if sValue in aoNilValues:
682 return (sValue, None if fAllowNull else 'Mandatory.');
683
684 try:
685 sValue = str(uuid.UUID(sValue));
686 except:
687 return (sValue, 'Invalid UUID value.');
688 return (sValue, None);
689
690 @staticmethod
691 def validateWord(sValue, cchMin = 1, cchMax = 64, asValid = None, aoNilValues = tuple([None, '']), fAllowNull = True):
692 """ Validates a word field. """
693 if sValue in aoNilValues:
694 return (sValue, None if fAllowNull else 'Mandatory.');
695
696 if re.search('[^a-zA-Z0-9_-]', sValue) is not None:
697 sError = 'Single word ([a-zA-Z0-9_-]), please.';
698 elif cchMin is not None and len(sValue) < cchMin:
699 sError = 'Too short, min %s chars' % (cchMin,);
700 elif cchMax is not None and len(sValue) > cchMax:
701 sError = 'Too long, max %s chars' % (cchMax,);
702 elif asValid is not None and sValue not in asValid:
703 sError = 'Invalid value "%s", must be one of: %s' % (sValue, asValid);
704 else:
705 sError = None;
706 return (sValue, sError);
707
708 @staticmethod
709 def validateStr(sValue, cchMin = 0, cchMax = 4096, aoNilValues = tuple([None, '']), fAllowNull = True,
710 fAllowUnicodeSymbols = False):
711 """ Validates a string field. """
712 if sValue in aoNilValues:
713 return (sValue, None if fAllowNull else 'Mandatory.');
714
715 if cchMin is not None and len(sValue) < cchMin:
716 sError = 'Too short, min %s chars' % (cchMin,);
717 elif cchMax is not None and len(sValue) > cchMax:
718 sError = 'Too long, max %s chars' % (cchMax,);
719 elif fAllowUnicodeSymbols is False and utils.hasNonAsciiCharacters(sValue):
720 sError = 'Non-ascii characters not allowed'
721 else:
722 sError = None;
723 return (sValue, sError);
724
725 @staticmethod
726 def validateEmail(sValue, aoNilValues = tuple([None, '']), fAllowNull = True):
727 """ Validates a email field."""
728 if sValue in aoNilValues:
729 return (sValue, None if fAllowNull else 'Mandatory.');
730
731 if re.match(r'.+@.+\..+', sValue) is None:
732 return (sValue,'Invalid e-mail format.');
733 return (sValue, None);
734
735 @staticmethod
736 def validateListOfSomething(asValues, aoNilValues = tuple([[], None]), fAllowNull = True):
737 """ Validate a list of some uniform values. Returns a copy of the list (if list it is). """
738 if asValues in aoNilValues or (len(asValues) == 0 and not fAllowNull):
739 return (asValues, None if fAllowNull else 'Mandatory.')
740
741 if not isinstance(asValues, list):
742 return (asValues, 'Invalid data type (%s).' % (type(asValues),));
743
744 asValues = list(asValues); # copy the list.
745 if len(asValues) > 0:
746 oType = type(asValues[0]);
747 for i in range(1, len(asValues)):
748 if type(asValues[i]) is not oType: # pylint: disable=unidiomatic-typecheck
749 return (asValues, 'Invalid entry data type ([0]=%s vs [%d]=%s).' % (oType, i, type(asValues[i])) );
750
751 return (asValues, None);
752
753 @staticmethod
754 def validateListOfStr(asValues, cchMin = None, cchMax = None, asValidValues = None,
755 aoNilValues = tuple([[], None]), fAllowNull = True):
756 """ Validates a list of text items."""
757 (asValues, sError) = ModelDataBase.validateListOfSomething(asValues, aoNilValues, fAllowNull);
758
759 if sError is None and asValues not in aoNilValues and len(asValues) > 0:
760 if not utils.isString(asValues[0]):
761 return (asValues, 'Invalid item data type.');
762
763 if not fAllowNull and cchMin is None:
764 cchMin = 1;
765
766 for sValue in asValues:
767 if asValidValues is not None and sValue not in asValidValues:
768 sThisErr = 'Invalid value "%s".' % (sValue,);
769 elif cchMin is not None and len(sValue) < cchMin:
770 sThisErr = 'Value "%s" is too short, min length is %u chars.' % (sValue, cchMin);
771 elif cchMax is not None and len(sValue) > cchMax:
772 sThisErr = 'Value "%s" is too long, max length is %u chars.' % (sValue, cchMax);
773 else:
774 continue;
775
776 if sError is None:
777 sError = sThisErr;
778 else:
779 sError += ' ' + sThisErr;
780
781 return (asValues, sError);
782
783 @staticmethod
784 def validateListOfInts(asValues, iMin = 0, iMax = 0x7ffffffe, aoNilValues = tuple([[], None]), fAllowNull = True):
785 """ Validates a list of integer items."""
786 (asValues, sError) = ModelDataBase.validateListOfSomething(asValues, aoNilValues, fAllowNull);
787
788 if sError is None and asValues not in aoNilValues and len(asValues) > 0:
789 for i, _ in enumerate(asValues):
790 sValue = asValues[i];
791
792 sThisErr = '';
793 try:
794 iValue = int(sValue);
795 except:
796 sThisErr = 'Invalid integer value "%s".' % (sValue,);
797 else:
798 asValues[i] = iValue;
799 if iValue < iMin:
800 sThisErr = 'Value %d is too small (min %d)' % (iValue, iMin,);
801 elif iValue > iMax:
802 sThisErr = 'Value %d is too high (max %d)' % (iValue, iMax,);
803 else:
804 continue;
805
806 if sError is None:
807 sError = sThisErr;
808 else:
809 sError += ' ' + sThisErr;
810
811 return (asValues, sError);
812
813
814
815 #
816 # Old validation helpers.
817 #
818
819 @staticmethod
820 def _validateInt(dErrors, sName, sValue, iMin = 0, iMax = 0x7ffffffe, aoNilValues = tuple([-1, None, ''])):
821 """ Validates an integer field. """
822 (sValue, sError) = ModelDataBase.validateInt(sValue, iMin, iMax, aoNilValues, fAllowNull = True);
823 if sError is not None:
824 dErrors[sName] = sError;
825 return sValue;
826
827 @staticmethod
828 def _validateIntNN(dErrors, sName, sValue, iMin = 0, iMax = 0x7ffffffe, aoNilValues = tuple([-1, None, ''])):
829 """ Validates an integer field, not null. """
830 (sValue, sError) = ModelDataBase.validateInt(sValue, iMin, iMax, aoNilValues, fAllowNull = False);
831 if sError is not None:
832 dErrors[sName] = sError;
833 return sValue;
834
835 @staticmethod
836 def _validateLong(dErrors, sName, sValue, lMin = 0, lMax = None, aoNilValues = tuple([long(-1), None, ''])):
837 """ Validates an long integer field. """
838 (sValue, sError) = ModelDataBase.validateLong(sValue, lMin, lMax, aoNilValues, fAllowNull = False);
839 if sError is not None:
840 dErrors[sName] = sError;
841 return sValue;
842
843 @staticmethod
844 def _validateLongNN(dErrors, sName, sValue, lMin = 0, lMax = None, aoNilValues = tuple([long(-1), None, ''])):
845 """ Validates an long integer field, not null. """
846 (sValue, sError) = ModelDataBase.validateLong(sValue, lMin, lMax, aoNilValues, fAllowNull = True);
847 if sError is not None:
848 dErrors[sName] = sError;
849 return sValue;
850
851 @staticmethod
852 def _validateTs(dErrors, sName, sValue):
853 """ Validates a timestamp field. """
854 (sValue, sError) = ModelDataBase.validateTs(sValue, fAllowNull = True);
855 if sError is not None:
856 dErrors[sName] = sError;
857 return sValue;
858
859 @staticmethod
860 def _validateTsNN(dErrors, sName, sValue):
861 """ Validates a timestamp field, not null. """
862 (sValue, sError) = ModelDataBase.validateTs(sValue, fAllowNull = False);
863 if sError is not None:
864 dErrors[sName] = sError;
865 return sValue;
866
867 @staticmethod
868 def _validateIp(dErrors, sName, sValue):
869 """ Validates an IP address field. """
870 (sValue, sError) = ModelDataBase.validateIp(sValue, fAllowNull = True);
871 if sError is not None:
872 dErrors[sName] = sError;
873 return sValue;
874
875 @staticmethod
876 def _validateIpNN(dErrors, sName, sValue):
877 """ Validates an IP address field, not null. """
878 (sValue, sError) = ModelDataBase.validateIp(sValue, fAllowNull = False);
879 if sError is not None:
880 dErrors[sName] = sError;
881 return sValue;
882
883 @staticmethod
884 def _validateBool(dErrors, sName, sValue):
885 """ Validates a boolean field. """
886 (sValue, sError) = ModelDataBase.validateBool(sValue, fAllowNull = True);
887 if sError is not None:
888 dErrors[sName] = sError;
889 return sValue;
890
891 @staticmethod
892 def _validateBoolNN(dErrors, sName, sValue):
893 """ Validates a boolean field, not null. """
894 (sValue, sError) = ModelDataBase.validateBool(sValue, fAllowNull = False);
895 if sError is not None:
896 dErrors[sName] = sError;
897 return sValue;
898
899 @staticmethod
900 def _validateUuid(dErrors, sName, sValue):
901 """ Validates an UUID field. """
902 (sValue, sError) = ModelDataBase.validateUuid(sValue, fAllowNull = True);
903 if sError is not None:
904 dErrors[sName] = sError;
905 return sValue;
906
907 @staticmethod
908 def _validateUuidNN(dErrors, sName, sValue):
909 """ Validates an UUID field, not null. """
910 (sValue, sError) = ModelDataBase.validateUuid(sValue, fAllowNull = False);
911 if sError is not None:
912 dErrors[sName] = sError;
913 return sValue;
914
915 @staticmethod
916 def _validateWord(dErrors, sName, sValue, cchMin = 1, cchMax = 64, asValid = None):
917 """ Validates a word field. """
918 (sValue, sError) = ModelDataBase.validateWord(sValue, cchMin, cchMax, asValid, fAllowNull = True);
919 if sError is not None:
920 dErrors[sName] = sError;
921 return sValue;
922
923 @staticmethod
924 def _validateWordNN(dErrors, sName, sValue, cchMin = 1, cchMax = 64, asValid = None):
925 """ Validates a boolean field, not null. """
926 (sValue, sError) = ModelDataBase.validateWord(sValue, cchMin, cchMax, asValid, fAllowNull = False);
927 if sError is not None:
928 dErrors[sName] = sError;
929 return sValue;
930
931 @staticmethod
932 def _validateStr(dErrors, sName, sValue, cchMin = 0, cchMax = 4096):
933 """ Validates a string field. """
934 (sValue, sError) = ModelDataBase.validateStr(sValue, cchMin, cchMax, fAllowNull = True);
935 if sError is not None:
936 dErrors[sName] = sError;
937 return sValue;
938
939 @staticmethod
940 def _validateStrNN(dErrors, sName, sValue, cchMin = 0, cchMax = 4096):
941 """ Validates a string field, not null. """
942 (sValue, sError) = ModelDataBase.validateStr(sValue, cchMin, cchMax, fAllowNull = False);
943 if sError is not None:
944 dErrors[sName] = sError;
945 return sValue;
946
947 @staticmethod
948 def _validateEmail(dErrors, sName, sValue):
949 """ Validates a email field."""
950 (sValue, sError) = ModelDataBase.validateEmail(sValue, fAllowNull = True);
951 if sError is not None:
952 dErrors[sName] = sError;
953 return sValue;
954
955 @staticmethod
956 def _validateEmailNN(dErrors, sName, sValue):
957 """ Validates a email field."""
958 (sValue, sError) = ModelDataBase.validateEmail(sValue, fAllowNull = False);
959 if sError is not None:
960 dErrors[sName] = sError;
961 return sValue;
962
963 @staticmethod
964 def _validateListOfStr(dErrors, sName, asValues, asValidValues = None):
965 """ Validates a list of text items."""
966 (sValue, sError) = ModelDataBase.validateListOfStr(asValues, asValidValues = asValidValues, fAllowNull = True);
967 if sError is not None:
968 dErrors[sName] = sError;
969 return sValue;
970
971 @staticmethod
972 def _validateListOfStrNN(dErrors, sName, asValues, asValidValues = None):
973 """ Validates a list of text items, not null and len >= 1."""
974 (sValue, sError) = ModelDataBase.validateListOfStr(asValues, asValidValues = asValidValues, fAllowNull = False);
975 if sError is not None:
976 dErrors[sName] = sError;
977 return sValue;
978
979 #
980 # Various helpers.
981 #
982
983 @staticmethod
984 def formatSimpleNowAndPeriod(oDb, tsNow = None, sPeriodBack = None,
985 sTablePrefix = '', sExpCol = 'tsExpire', sEffCol = 'tsEffective'):
986 """
987 Formats a set of tsNow and sPeriodBack arguments for a standard testmanager
988 table.
989
990 If sPeriodBack is given, the query is effective for the period
991 (tsNow - sPeriodBack) thru (tsNow).
992
993 If tsNow isn't given, it defaults to current time.
994
995 Returns the final portion of a WHERE query (start with AND) and maybe an
996 ORDER BY and LIMIT bit if sPeriodBack is given.
997 """
998 if tsNow is not None:
999 if sPeriodBack is not None:
1000 sRet = oDb.formatBindArgs(' AND ' + sTablePrefix + sExpCol + ' > (%s::timestamp - %s::interval)\n'
1001 ' AND tsEffective <= %s\n'
1002 'ORDER BY ' + sTablePrefix + sExpCol + ' DESC\n'
1003 'LIMIT 1\n'
1004 , ( tsNow, sPeriodBack, tsNow));
1005 else:
1006 sRet = oDb.formatBindArgs(' AND ' + sTablePrefix + sExpCol + ' > %s\n'
1007 ' AND ' + sTablePrefix + sEffCol + ' <= %s\n'
1008 , ( tsNow, tsNow, ));
1009 else:
1010 if sPeriodBack is not None:
1011 sRet = oDb.formatBindArgs(' AND ' + sTablePrefix + sExpCol + ' > (CURRENT_TIMESTAMP - %s::interval)\n'
1012 ' AND ' + sTablePrefix + sEffCol + ' <= CURRENT_TIMESTAMP\n'
1013 'ORDER BY ' + sTablePrefix + sExpCol + ' DESC\n'
1014 'LIMIT 1\n'
1015 , ( sPeriodBack, ));
1016 else:
1017 sRet = ' AND ' + sTablePrefix + sExpCol + ' = \'infinity\'::timestamp\n';
1018 return sRet;
1019
1020 @staticmethod
1021 def formatSimpleNowAndPeriodQuery(oDb, sQuery, aBindArgs, tsNow = None, sPeriodBack = None,
1022 sTablePrefix = '', sExpCol = 'tsExpire', sEffCol = 'tsEffective'):
1023 """
1024 Formats a simple query for a standard testmanager table with optional
1025 tsNow and sPeriodBack arguments.
1026
1027 The sQuery and sBindArgs are passed along to oDb.formatBindArgs to form
1028 the first part of the query. Must end with an open WHERE statement as
1029 we'll be adding the time part starting with 'AND something...'.
1030
1031 See formatSimpleNowAndPeriod for tsNow and sPeriodBack description.
1032
1033 Returns the final portion of a WHERE query (start with AND) and maybe an
1034 ORDER BY and LIMIT bit if sPeriodBack is given.
1035
1036 """
1037 return oDb.formatBindArgs(sQuery, aBindArgs) \
1038 + ModelDataBase.formatSimpleNowAndPeriod(oDb, tsNow, sPeriodBack, sTablePrefix, sExpCol, sEffCol);
1039
1040 #
1041 # Sub-classes.
1042 #
1043
1044 class DispWrapper(object):
1045 """Proxy object."""
1046 def __init__(self, oDisp, sAttrFmt):
1047 self.oDisp = oDisp;
1048 self.sAttrFmt = sAttrFmt;
1049 def getStringParam(self, sName, asValidValues = None, sDefault = None):
1050 """See WuiDispatcherBase.getStringParam."""
1051 return self.oDisp.getStringParam(self.sAttrFmt % (sName,), asValidValues, sDefault);
1052 def getListOfStrParams(self, sName, asDefaults = None):
1053 """See WuiDispatcherBase.getListOfStrParams."""
1054 return self.oDisp.getListOfStrParams(self.sAttrFmt % (sName,), asDefaults);
1055 def getListOfIntParams(self, sName, iMin = None, iMax = None, aiDefaults = None):
1056 """See WuiDispatcherBase.getListOfIntParams."""
1057 return self.oDisp.getListOfIntParams(self.sAttrFmt % (sName,), iMin, iMax, aiDefaults);
1058
1059
1060
1061
1062# pylint: disable=E1101,C0111,R0903
1063class ModelDataBaseTestCase(unittest.TestCase):
1064 """
1065 Base testcase for ModelDataBase decendants.
1066 Derive from this and override setUp.
1067 """
1068
1069 def setUp(self):
1070 """
1071 Override this! Don't call super!
1072 The subclasses are expected to set aoSamples to an array of instance
1073 samples. The first entry must be a default object, the subsequent ones
1074 are optional and their contents freely choosen.
1075 """
1076 self.aoSamples = [ModelDataBase(),];
1077
1078 def testEquality(self):
1079 for oSample in self.aoSamples:
1080 self.assertEqual(oSample.isEqual(copy.copy(oSample)), True);
1081 self.assertIsNotNone(oSample.isEqual(self.aoSamples[0]));
1082
1083 def testNullConversion(self):
1084 if len(self.aoSamples[0].getDataAttributes()) == 0:
1085 return;
1086 for oSample in self.aoSamples:
1087 oCopy = copy.copy(oSample);
1088 self.assertEqual(oCopy.convertToParamNull(), oCopy);
1089 self.assertEqual(oCopy.isEqual(oSample), False);
1090 self.assertEqual(oCopy.convertFromParamNull(), oCopy);
1091 self.assertEqual(oCopy.isEqual(oSample), True, '\ngot : %s\nexpected: %s' % (oCopy, oSample,));
1092
1093 oCopy = copy.copy(oSample);
1094 self.assertEqual(oCopy.convertToParamNull(), oCopy);
1095 oCopy2 = copy.copy(oCopy);
1096 self.assertEqual(oCopy.convertToParamNull(), oCopy);
1097 self.assertEqual(oCopy.isEqual(oCopy2), True);
1098 self.assertEqual(oCopy.convertToParamNull(), oCopy);
1099 self.assertEqual(oCopy.isEqual(oCopy2), True);
1100
1101 oCopy = copy.copy(oSample);
1102 self.assertEqual(oCopy.convertFromParamNull(), oCopy);
1103 oCopy2 = copy.copy(oCopy);
1104 self.assertEqual(oCopy.convertFromParamNull(), oCopy);
1105 self.assertEqual(oCopy.isEqual(oCopy2), True);
1106 self.assertEqual(oCopy.convertFromParamNull(), oCopy);
1107 self.assertEqual(oCopy.isEqual(oCopy2), True);
1108
1109 def testReinitToNull(self):
1110 oFirst = copy.copy(self.aoSamples[0]);
1111 self.assertEqual(oFirst.reinitToNull(), oFirst);
1112 for oSample in self.aoSamples:
1113 oCopy = copy.copy(oSample);
1114 self.assertEqual(oCopy.reinitToNull(), oCopy);
1115 self.assertEqual(oCopy.isEqual(oFirst), True);
1116
1117 def testValidateAndConvert(self):
1118 for oSample in self.aoSamples:
1119 oCopy = copy.copy(oSample);
1120 oCopy.convertToParamNull();
1121 dError1 = oCopy.validateAndConvert(None);
1122
1123 oCopy2 = copy.copy(oCopy);
1124 self.assertEqual(oCopy.validateAndConvert(None), dError1);
1125 self.assertEqual(oCopy.isEqual(oCopy2), True);
1126
1127 def testInitFromParams(self):
1128 class DummyDisp(object):
1129 def getStringParam(self, sName, asValidValues = None, sDefault = None):
1130 _ = sName; _ = asValidValues;
1131 return sDefault;
1132 def getListOfStrParams(self, sName, asDefaults = None):
1133 _ = sName;
1134 return asDefaults;
1135 def getListOfIntParams(self, sName, iMin = None, iMax = None, aiDefaults = None):
1136 _ = sName; _ = iMin; _ = iMax;
1137 return aiDefaults;
1138
1139 for oSample in self.aoSamples:
1140 oCopy = copy.copy(oSample);
1141 self.assertEqual(oCopy.initFromParams(DummyDisp(), fStrict = False), oCopy);
1142
1143 def testToString(self):
1144 for oSample in self.aoSamples:
1145 self.assertIsNotNone(oSample.toString());
1146
1147
1148class ModelLogicBase(ModelBase): # pylint: disable=R0903
1149 """
1150 Something all classes in the logic classes the logical model inherits from.
1151 """
1152
1153 def __init__(self, oDb):
1154 ModelBase.__init__(self);
1155
1156 #
1157 # Note! Do not create a connection here if None, we need to DB share
1158 # connection with all other logic objects so we can perform half
1159 # complex transactions involving several logic objects.
1160 #
1161 self._oDb = oDb;
1162
1163 def getDbConnection(self):
1164 """
1165 Gets the database connection.
1166 This should only be used for instantiating other ModelLogicBase children.
1167 """
1168 return self._oDb;
1169
1170 def _dbRowsToModelDataList(self, oModelDataType, aaoRows = None):
1171 """
1172 Helper for conerting a simple fetch into a list of ModelDataType python objects.
1173
1174 If aaoRows is None, we'll fetchAll from the database ourselves.
1175
1176 The oModelDataType must be a class derived from ModelDataBase and implement
1177 the initFormDbRow method.
1178
1179 Returns a list of oModelDataType instances.
1180 """
1181 assert issubclass(oModelDataType, ModelDataBase);
1182 aoRet = [];
1183 if aaoRows is None:
1184 aaoRows = self._oDb.fetchAll();
1185 for aoRow in aaoRows:
1186 aoRet.append(oModelDataType().initFromDbRow(aoRow));
1187 return aoRet;
1188
1189
1190
1191class AttributeChangeEntry(object): # pylint: disable=R0903
1192 """
1193 Data class representing the changes made to one attribute.
1194 """
1195
1196 def __init__(self, sAttr, oNewRaw, oOldRaw, sNewText, sOldText):
1197 self.sAttr = sAttr;
1198 self.oNewRaw = oNewRaw;
1199 self.oOldRaw = oOldRaw;
1200 self.sNewText = sNewText;
1201 self.sOldText = sOldText;
1202
1203class ChangeLogEntry(object): # pylint: disable=R0903
1204 """
1205 A change log entry returned by the fetchChangeLog method typically
1206 implemented by ModelLogicBase child classes.
1207 """
1208
1209 def __init__(self, uidAuthor, sAuthor, tsEffective, tsExpire, oNewRaw, oOldRaw, aoChanges):
1210 self.uidAuthor = uidAuthor;
1211 self.sAuthor = sAuthor;
1212 self.tsEffective = tsEffective;
1213 self.tsExpire = tsExpire;
1214 self.oNewRaw = oNewRaw;
1215 self.oOldRaw = oOldRaw; # Note! NULL for the last entry.
1216 self.aoChanges = aoChanges;
1217
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