VirtualBox

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

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

TestManager/SchedQueue: Fixed various validation issues with the testbox-in-sched-group stuff. Added change log to the scheduling queue details page.

  • Property svn:eol-style set to native
  • Property svn:keywords set to Author Date Id Revision
File size: 57.3 KB
Line 
1# -*- coding: utf-8 -*-
2# $Id: base.py 83384 2020-03-24 14:46:06Z vboxsync $
3# pylint: disable=too-many-lines
4
5"""
6Test Manager Core - Base Class(es).
7"""
8
9__copyright__ = \
10"""
11Copyright (C) 2012-2020 Oracle Corporation
12
13This file is part of VirtualBox Open Source Edition (OSE), as
14available from http://www.virtualbox.org. 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: 83384 $"
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=redefined-builtin,invalid-name
47
48
49class TMExceptionBase(Exception):
50 """
51 For exceptions raised by any TestManager component.
52 """
53 pass; # pylint: disable=unnecessary-pass
54
55
56class TMTooManyRows(TMExceptionBase):
57 """
58 Too many rows in the result.
59 Used by ModelLogicBase decendants.
60 """
61 pass; # pylint: disable=unnecessary-pass
62
63
64class TMRowNotFound(TMExceptionBase):
65 """
66 Database row not found.
67 Used by ModelLogicBase decendants.
68 """
69 pass; # pylint: disable=unnecessary-pass
70
71
72class TMRowAlreadyExists(TMExceptionBase):
73 """
74 Database row already exists (typically raised by addEntry).
75 Used by ModelLogicBase decendants.
76 """
77 pass; # pylint: disable=unnecessary-pass
78
79
80class TMInvalidData(TMExceptionBase):
81 """
82 Data validation failed.
83 Used by ModelLogicBase decendants.
84 """
85 pass; # pylint: disable=unnecessary-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; # pylint: disable=unnecessary-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; # pylint: disable=unnecessary-pass
103
104
105class ModelBase(object): # pylint: disable=too-few-public-methods
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=too-few-public-methods
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 ## List of internal attributes which should be ignored by
136 ## getDataAttributes and related machinery
137 kasInternalAttributes = [];
138
139 def __init__(self):
140 ModelBase.__init__(self);
141
142
143 #
144 # Standard methods implemented by combining python magic and hungarian prefixes.
145 #
146
147 def getDataAttributes(self):
148 """
149 Returns a list of data attributes.
150 """
151 asRet = [];
152 asAttrs = dir(self);
153 for sAttr in asAttrs:
154 if sAttr[0] == '_' or sAttr[0] == 'k':
155 continue;
156 if sAttr in self.kasInternalAttributes:
157 continue;
158 oValue = getattr(self, sAttr);
159 if callable(oValue):
160 continue;
161 asRet.append(sAttr);
162 return asRet;
163
164 def initFromOther(self, oOther):
165 """
166 Initialize this object with the values from another instance (child
167 class instance is accepted).
168
169 This serves as a kind of copy constructor.
170
171 Returns self. May raise exception if the type of other object differs
172 or is damaged.
173 """
174 for sAttr in self.getDataAttributes():
175 setattr(self, sAttr, getattr(oOther, sAttr));
176 return self;
177
178 @staticmethod
179 def getHungarianPrefix(sName):
180 """
181 Returns the hungarian prefix of the given name.
182 """
183 for i, _ in enumerate(sName):
184 if sName[i] not in ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
185 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z']:
186 assert re.search('^[A-Z][a-zA-Z0-9]*$', sName[i:]) is not None;
187 return sName[:i];
188 return sName;
189
190 def getAttributeParamNullValues(self, sAttr):
191 """
192 Returns a list of parameter NULL values, with the preferred one being
193 the first element.
194
195 Child classes can override this to handle one or more attributes specially.
196 """
197 sPrefix = self.getHungarianPrefix(sAttr);
198 if sPrefix in ['id', 'uid', 'i', 'off', 'pct']:
199 return [-1, '', '-1',];
200 if sPrefix in ['l', 'c',]:
201 return [long(-1), '', '-1',];
202 if sPrefix == 'f':
203 return ['',];
204 if sPrefix in ['enm', 'ip', 's', 'ts', 'uuid']:
205 return ['',];
206 if sPrefix in ['ai', 'aid', 'al', 'as']:
207 return [[], '', None]; ## @todo ??
208 if sPrefix == 'bm':
209 return ['', [],]; ## @todo bitmaps.
210 raise TMExceptionBase('Unable to classify "%s" (prefix %s)' % (sAttr, sPrefix));
211
212 def isAttributeNull(self, sAttr, oValue):
213 """
214 Checks if the specified attribute value indicates NULL.
215 Return True/False.
216
217 Note! This isn't entirely kosher actually.
218 """
219 if oValue is None:
220 return True;
221 aoNilValues = self.getAttributeParamNullValues(sAttr);
222 return oValue in aoNilValues;
223
224 def _convertAttributeFromParamNull(self, sAttr, oValue):
225 """
226 Converts an attribute from parameter NULL to database NULL value.
227 Returns the new attribute value.
228 """
229 aoNullValues = self.getAttributeParamNullValues(sAttr);
230 if oValue in aoNullValues:
231 oValue = None if sAttr not in self.kasAltArrayNull else [];
232 #
233 # Perform deep conversion on ModelDataBase object and lists of them.
234 #
235 elif isinstance(oValue, list) and oValue and isinstance(oValue[0], ModelDataBase):
236 oValue = copy.copy(oValue);
237 for i, _ in enumerate(oValue):
238 assert isinstance(oValue[i], ModelDataBase);
239 oValue[i] = copy.copy(oValue[i]);
240 oValue[i].convertFromParamNull();
241
242 elif isinstance(oValue, ModelDataBase):
243 oValue = copy.copy(oValue);
244 oValue.convertFromParamNull();
245
246 return oValue;
247
248 def convertFromParamNull(self):
249 """
250 Converts from parameter NULL values to database NULL values (None).
251 Returns self.
252 """
253 for sAttr in self.getDataAttributes():
254 oValue = getattr(self, sAttr);
255 oNewValue = self._convertAttributeFromParamNull(sAttr, oValue);
256 if oValue != oNewValue:
257 setattr(self, sAttr, oNewValue);
258 return self;
259
260 def _convertAttributeToParamNull(self, sAttr, oValue):
261 """
262 Converts an attribute from database NULL to a sepcial value we can pass
263 thru parameter list.
264 Returns the new attribute value.
265 """
266 if oValue is None:
267 oValue = self.getAttributeParamNullValues(sAttr)[0];
268 #
269 # Perform deep conversion on ModelDataBase object and lists of them.
270 #
271 elif isinstance(oValue, list) and oValue and isinstance(oValue[0], ModelDataBase):
272 oValue = copy.copy(oValue);
273 for i, _ in enumerate(oValue):
274 assert isinstance(oValue[i], ModelDataBase);
275 oValue[i] = copy.copy(oValue[i]);
276 oValue[i].convertToParamNull();
277
278 elif isinstance(oValue, ModelDataBase):
279 oValue = copy.copy(oValue);
280 oValue.convertToParamNull();
281
282 return oValue;
283
284 def convertToParamNull(self):
285 """
286 Converts from database NULL values (None) to special values we can
287 pass thru parameters list.
288 Returns self.
289 """
290 for sAttr in self.getDataAttributes():
291 oValue = getattr(self, sAttr);
292 oNewValue = self._convertAttributeToParamNull(sAttr, oValue);
293 if oValue != oNewValue:
294 setattr(self, sAttr, oNewValue);
295 return self;
296
297 def _validateAndConvertAttribute(self, sAttr, sParam, oValue, aoNilValues, fAllowNull, oDb):
298 """
299 Validates and convert one attribute.
300 Returns the converted value.
301
302 Child classes can override this to handle one or more attributes specially.
303 Note! oDb can be None.
304 """
305 sPrefix = self.getHungarianPrefix(sAttr);
306
307 if sPrefix in ['id', 'uid']:
308 (oNewValue, sError) = self.validateInt( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
309 elif sPrefix in ['i', 'off', 'pct']:
310 (oNewValue, sError) = self.validateInt( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
311 iMin = getattr(self, 'kiMin_' + sAttr, 0),
312 iMax = getattr(self, 'kiMax_' + sAttr, 0x7ffffffe));
313 elif sPrefix in ['l', 'c']:
314 (oNewValue, sError) = self.validateLong(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
315 lMin = getattr(self, 'klMin_' + sAttr, 0),
316 lMax = getattr(self, 'klMax_' + sAttr, None));
317 elif sPrefix == 'f':
318 if not oValue and not fAllowNull: oValue = '0'; # HACK ALERT! Checkboxes are only added when checked.
319 (oNewValue, sError) = self.validateBool(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
320 elif sPrefix == 'ts':
321 (oNewValue, sError) = self.validateTs( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
322 elif sPrefix == 'ip':
323 (oNewValue, sError) = self.validateIp( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
324 elif sPrefix == 'uuid':
325 (oNewValue, sError) = self.validateUuid(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
326 elif sPrefix == 'enm':
327 (oNewValue, sError) = self.validateWord(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
328 asValid = getattr(self, 'kasValidValues_' + sAttr)); # The list is required.
329 elif sPrefix == 's':
330 (oNewValue, sError) = self.validateStr( oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
331 cchMin = getattr(self, 'kcchMin_' + sAttr, 0),
332 cchMax = getattr(self, 'kcchMax_' + sAttr, 4096),
333 fAllowUnicodeSymbols = getattr(self, 'kfAllowUnicode_' + sAttr, False) );
334 ## @todo al.
335 elif sPrefix == 'aid':
336 (oNewValue, sError) = self.validateListOfInts(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
337 iMin = 1, iMax = 0x7ffffffe);
338 elif sPrefix == 'as':
339 (oNewValue, sError) = self.validateListOfStr(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull,
340 asValidValues = getattr(self, 'kasValidValues_' + sAttr, None),
341 cchMin = getattr(self, 'kcchMin_' + sAttr, 0 if fAllowNull else 1),
342 cchMax = getattr(self, 'kcchMax_' + sAttr, 4096));
343
344 elif sPrefix == 'bm':
345 ## @todo figure out bitfields.
346 (oNewValue, sError) = self.validateListOfStr(oValue, aoNilValues = aoNilValues, fAllowNull = fAllowNull);
347 else:
348 raise TMExceptionBase('Unable to classify "%s" (prefix %s)' % (sAttr, sPrefix));
349
350 _ = sParam; _ = oDb;
351 return (oNewValue, sError);
352
353 def _validateAndConvertWorker(self, asAllowNullAttributes, oDb, enmValidateFor = ksValidateFor_Other):
354 """
355 Worker for implementing validateAndConvert().
356 """
357 dErrors = dict();
358 for sAttr in self.getDataAttributes():
359 oValue = getattr(self, sAttr);
360 sParam = getattr(self, 'ksParam_' + sAttr);
361 aoNilValues = self.getAttributeParamNullValues(sAttr);
362 aoNilValues.append(None);
363
364 (oNewValue, sError) = self._validateAndConvertAttribute(sAttr, sParam, oValue, aoNilValues,
365 sAttr in asAllowNullAttributes, oDb);
366 if oValue != oNewValue:
367 setattr(self, sAttr, oNewValue);
368 if sError is not None:
369 dErrors[sParam] = sError;
370
371 # Check the NULL requirements of the primary ID(s) for the 'add' and 'edit' actions.
372 if enmValidateFor in (ModelDataBase.ksValidateFor_Add,
373 ModelDataBase.ksValidateFor_AddForeignId,
374 ModelDataBase.ksValidateFor_Edit,):
375 fMustBeNull = enmValidateFor == ModelDataBase.ksValidateFor_Add;
376 sAttr = getattr(self, 'ksIdAttr', None);
377 if sAttr is not None:
378 oValue = getattr(self, sAttr);
379 if self.isAttributeNull(sAttr, oValue) != fMustBeNull:
380 sParam = getattr(self, 'ksParam_' + sAttr);
381 sErrMsg = 'Must be NULL!' if fMustBeNull else 'Must not be NULL!'
382 if sParam in dErrors:
383 dErrors[sParam] += ' ' + sErrMsg;
384 else:
385 dErrors[sParam] = sErrMsg;
386
387 return dErrors;
388
389 def validateAndConvert(self, oDb, enmValidateFor = ksValidateFor_Other):
390 """
391 Validates the input and converts valid fields to their right type.
392 Returns a dictionary with per field reports, only invalid fields will
393 be returned, so an empty dictionary means that the data is valid.
394
395 The dictionary keys are ksParam_*.
396
397 Child classes can override _validateAndConvertAttribute to handle
398 selected fields specially. There are also a few class variables that
399 can be used to advice the validation: kcchMin_sAttr, kcchMax_sAttr,
400 kiMin_iAttr, kiMax_iAttr, klMin_lAttr, klMax_lAttr,
401 kasValidValues_enmAttr, and kasAllowNullAttributes.
402 """
403 return self._validateAndConvertWorker(getattr(self, 'kasAllowNullAttributes', list()), oDb,
404 enmValidateFor = enmValidateFor);
405
406 def validateAndConvertEx(self, asAllowNullAttributes, oDb, enmValidateFor = ksValidateFor_Other):
407 """
408 Same as validateAndConvert but with custom allow-null list.
409 """
410 return self._validateAndConvertWorker(asAllowNullAttributes, oDb, enmValidateFor = enmValidateFor);
411
412 def convertParamToAttribute(self, sAttr, sParam, oValue, oDisp, fStrict):
413 """
414 Calculate the attribute value when initialized from a parameter.
415
416 Returns the new value, with parameter NULL values. Raises exception on
417 invalid parameter value.
418
419 Child classes can override to do special parameter conversion jobs.
420 """
421 sPrefix = self.getHungarianPrefix(sAttr);
422 asValidValues = getattr(self, 'kasValidValues_' + sAttr, None);
423 fAllowNull = sAttr in getattr(self, 'kasAllowNullAttributes', list());
424 if fStrict:
425 if sPrefix == 'f':
426 # HACK ALERT! Checkboxes are only present when checked, so we always have to provide a default.
427 oNewValue = oDisp.getStringParam(sParam, asValidValues, '0');
428 elif sPrefix[0] == 'a':
429 # HACK ALERT! Lists are not present if empty.
430 oNewValue = oDisp.getListOfStrParams(sParam, []);
431 else:
432 oNewValue = oDisp.getStringParam(sParam, asValidValues, None, fAllowNull = fAllowNull);
433 else:
434 if sPrefix[0] == 'a':
435 oNewValue = oDisp.getListOfStrParams(sParam, []);
436 else:
437 assert oValue is not None, 'sAttr=%s' % (sAttr,);
438 oNewValue = oDisp.getStringParam(sParam, asValidValues, oValue, fAllowNull = fAllowNull);
439 return oNewValue;
440
441 def initFromParams(self, oDisp, fStrict = True):
442 """
443 Initialize the object from parameters.
444 The input is not validated at all, except that all parameters must be
445 present when fStrict is True.
446
447 Returns self. Raises exception on invalid parameter value.
448
449 Note! The returned object has parameter NULL values, not database ones!
450 """
451
452 self.convertToParamNull()
453 for sAttr in self.getDataAttributes():
454 oValue = getattr(self, sAttr);
455 oNewValue = self.convertParamToAttribute(sAttr, getattr(self, 'ksParam_' + sAttr), oValue, oDisp, fStrict);
456 if oNewValue != oValue:
457 setattr(self, sAttr, oNewValue);
458 return self;
459
460 def areAttributeValuesEqual(self, sAttr, sPrefix, oValue1, oValue2):
461 """
462 Called to compare two attribute values and python thinks differs.
463
464 Returns True/False.
465
466 Child classes can override this to do special compares of things like arrays.
467 """
468 # Just in case someone uses it directly.
469 if oValue1 == oValue2:
470 return True;
471
472 #
473 # Timestamps can be both string (param) and object (db)
474 # depending on the data source. Compare string values to make
475 # sure we're doing the right thing here.
476 #
477 if sPrefix == 'ts':
478 return str(oValue1) == str(oValue2);
479
480 #
481 # Some generic code handling ModelDataBase children.
482 #
483 if isinstance(oValue1, list) and isinstance(oValue2, list):
484 if len(oValue1) == len(oValue2):
485 for i, _ in enumerate(oValue1):
486 if not isinstance(oValue1[i], ModelDataBase) \
487 or type(oValue1) is not type(oValue2):
488 return False;
489 if not oValue1[i].isEqual(oValue2[i]):
490 return False;
491 return True;
492
493 elif isinstance(oValue1, ModelDataBase) \
494 and type(oValue1) is type(oValue2):
495 return oValue1[i].isEqual(oValue2[i]);
496
497 _ = sAttr;
498 return False;
499
500 def isEqual(self, oOther):
501 """ Compares two instances. """
502 for sAttr in self.getDataAttributes():
503 if getattr(self, sAttr) != getattr(oOther, sAttr):
504 # Delegate the final decision to an overridable method.
505 if not self.areAttributeValuesEqual(sAttr, self.getHungarianPrefix(sAttr),
506 getattr(self, sAttr), getattr(oOther, sAttr)):
507 return False;
508 return True;
509
510 def isEqualEx(self, oOther, asExcludeAttrs):
511 """ Compares two instances, omitting the given attributes. """
512 for sAttr in self.getDataAttributes():
513 if sAttr not in asExcludeAttrs \
514 and getattr(self, sAttr) != getattr(oOther, sAttr):
515 # Delegate the final decision to an overridable method.
516 if not self.areAttributeValuesEqual(sAttr, self.getHungarianPrefix(sAttr),
517 getattr(self, sAttr), getattr(oOther, sAttr)):
518 return False;
519 return True;
520
521 def reinitToNull(self):
522 """
523 Reinitializes the object to (database) NULL values.
524 Returns self.
525 """
526 for sAttr in self.getDataAttributes():
527 setattr(self, sAttr, None);
528 return self;
529
530 def toString(self):
531 """
532 Stringifies the object.
533 Returns string representation.
534 """
535
536 sMembers = '';
537 for sAttr in self.getDataAttributes():
538 oValue = getattr(self, sAttr);
539 sMembers += ', %s=%s' % (sAttr, oValue);
540
541 oClass = type(self);
542 if sMembers == '':
543 return '<%s>' % (oClass.__name__);
544 return '<%s: %s>' % (oClass.__name__, sMembers[2:]);
545
546 def __str__(self):
547 return self.toString();
548
549
550
551 #
552 # New validation helpers.
553 #
554 # These all return (oValue, sError), where sError is None when the value
555 # is valid and an error message when not. On success and in case of
556 # range errors, oValue is converted into the requested type.
557 #
558
559 @staticmethod
560 def validateInt(sValue, iMin = 0, iMax = 0x7ffffffe, aoNilValues = tuple([-1, None, '']), fAllowNull = True):
561 """ Validates an integer field. """
562 if sValue in aoNilValues:
563 if fAllowNull:
564 return (None if sValue is None else aoNilValues[0], None);
565 return (sValue, 'Mandatory.');
566
567 try:
568 if utils.isString(sValue):
569 iValue = int(sValue, 0);
570 else:
571 iValue = int(sValue);
572 except:
573 return (sValue, 'Not an integer');
574
575 if iValue in aoNilValues:
576 return (aoNilValues[0], None if fAllowNull else 'Mandatory.');
577
578 if iValue < iMin:
579 return (iValue, 'Value too small (min %d)' % (iMin,));
580 if iValue > iMax:
581 return (iValue, 'Value too high (max %d)' % (iMax,));
582 return (iValue, None);
583
584 @staticmethod
585 def validateLong(sValue, lMin = 0, lMax = None, aoNilValues = tuple([long(-1), None, '']), fAllowNull = True):
586 """ Validates an long integer field. """
587 if sValue in aoNilValues:
588 if fAllowNull:
589 return (None if sValue is None else aoNilValues[0], None);
590 return (sValue, 'Mandatory.');
591 try:
592 if utils.isString(sValue):
593 lValue = long(sValue, 0);
594 else:
595 lValue = long(sValue);
596 except:
597 return (sValue, 'Not a long integer');
598
599 if lValue in aoNilValues:
600 return (aoNilValues[0], None if fAllowNull else 'Mandatory.');
601
602 if lMin is not None and lValue < lMin:
603 return (lValue, 'Value too small (min %d)' % (lMin,));
604 if lMax is not None and lValue > lMax:
605 return (lValue, 'Value too high (max %d)' % (lMax,));
606 return (lValue, None);
607
608 @staticmethod
609 def validateTs(sValue, aoNilValues = tuple([None, '']), fAllowNull = True):
610 """ Validates a timestamp field. """
611 if sValue in aoNilValues:
612 return (sValue, None if fAllowNull else 'Mandatory.');
613 if not utils.isString(sValue):
614 return (sValue, None);
615
616 sError = None;
617 if len(sValue) == len('2012-10-08 01:54:06.364207+02:00'):
618 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);
619 if oRes is not None \
620 and ( int(oRes.group(6)) > 12 \
621 or int(oRes.group(7)) >= 60):
622 sError = 'Invalid timezone offset.';
623 elif len(sValue) == len('2012-10-08 01:54:06.00'):
624 oRes = re.match(r'(\d{4})-([01]\d)-([0123])\d[ Tt]([012]\d):[0-5]\d:([0-6]\d).\d{2}', sValue);
625 elif len(sValue) == len('2012-10-08 01:54:06.00Z'):
626 oRes = re.match(r'(\d{4})-([01]\d)-([0123])\d[ Tt]([012]\d):[0-5]\d:([0-6]\d).\d{2}[Zz]', sValue);
627 elif len(sValue) == len('9999-12-31 23:59:59.999999'):
628 oRes = re.match(r'(\d{4})-([01]\d)-([0123])\d[ Tt]([012]\d):[0-5]\d:([0-6]\d).\d{6}', sValue);
629 elif len(sValue) == len('9999-12-31 23:59:59.999999Z'):
630 oRes = re.match(r'(\d{4})-([01]\d)-([0123])\d[ Tt]([012]\d):[0-5]\d:([0-6]\d).\d{6}[Zz]', sValue);
631 elif len(sValue) == len('999999-12-31 00:00:00.00'):
632 oRes = re.match(r'(\d{6})-([01]\d)-([0123])\d[ Tt]([012]\d):[0-5]\d:([0-6]\d).\d{2}', sValue);
633 elif len(sValue) == len('9999-12-31T23:59:59.999999Z'):
634 oRes = re.match(r'(\d{4})-([01]\d)-([0123])\d[Tt]([012]\d):[0-5]\d:([0-6]\d).\d{6}[Zz]', sValue);
635 elif len(sValue) == len('9999-12-31T23:59:59.999999999Z'):
636 oRes = re.match(r'(\d{4})-([01]\d)-([0123])\d[Tt]([012]\d):[0-5]\d:([0-6]\d).\d{9}[Zz]', sValue);
637 else:
638 return (sValue, 'Invalid timestamp length.');
639
640 if oRes is None:
641 sError = 'Invalid timestamp (format: 2012-10-08 01:54:06.364207+02:00).';
642 else:
643 iYear = int(oRes.group(1));
644 if iYear % 4 == 0 and (iYear % 100 != 0 or iYear % 400 == 0):
645 acDaysOfMonth = [31, 29, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
646 else:
647 acDaysOfMonth = [31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31];
648 iMonth = int(oRes.group(2));
649 iDay = int(oRes.group(3));
650 iHour = int(oRes.group(4));
651 iSec = int(oRes.group(5));
652 if iMonth > 12:
653 sError = 'Invalid timestamp month.';
654 elif iDay > acDaysOfMonth[iMonth - 1]:
655 sError = 'Invalid timestamp day-of-month (%02d has %d days).' % (iMonth, acDaysOfMonth[iMonth - 1]);
656 elif iHour > 23:
657 sError = 'Invalid timestamp hour.'
658 elif iSec >= 61:
659 sError = 'Invalid timestamp second.'
660 elif iSec >= 60:
661 sError = 'Invalid timestamp: no leap seconds, please.'
662 return (sValue, sError);
663
664 @staticmethod
665 def validateIp(sValue, aoNilValues = tuple([None, '']), fAllowNull = True):
666 """ Validates an IP address field. """
667 if sValue in aoNilValues:
668 return (sValue, None if fAllowNull else 'Mandatory.');
669
670 if sValue == '::1':
671 return (sValue, None);
672
673 try:
674 socket.inet_pton(socket.AF_INET, sValue); # pylint: disable=no-member
675 except:
676 try:
677 socket.inet_pton(socket.AF_INET6, sValue); # pylint: disable=no-member
678 except:
679 return (sValue, 'Not a valid IP address.');
680
681 return (sValue, None);
682
683 @staticmethod
684 def validateBool(sValue, aoNilValues = tuple([None, '']), fAllowNull = True):
685 """ Validates a boolean field. """
686 if sValue in aoNilValues:
687 return (sValue, None if fAllowNull else 'Mandatory.');
688
689 if sValue in ('True', 'true', '1', True):
690 return (True, None);
691 if sValue in ('False', 'false', '0', False):
692 return (False, None);
693 return (sValue, 'Invalid boolean value.');
694
695 @staticmethod
696 def validateUuid(sValue, aoNilValues = tuple([None, '']), fAllowNull = True):
697 """ Validates an UUID field. """
698 if sValue in aoNilValues:
699 return (sValue, None if fAllowNull else 'Mandatory.');
700
701 try:
702 sValue = str(uuid.UUID(sValue));
703 except:
704 return (sValue, 'Invalid UUID value.');
705 return (sValue, None);
706
707 @staticmethod
708 def validateWord(sValue, cchMin = 1, cchMax = 64, asValid = None, aoNilValues = tuple([None, '']), fAllowNull = True):
709 """ Validates a word field. """
710 if sValue in aoNilValues:
711 return (sValue, None if fAllowNull else 'Mandatory.');
712
713 if re.search('[^a-zA-Z0-9_-]', sValue) is not None:
714 sError = 'Single word ([a-zA-Z0-9_-]), please.';
715 elif 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 asValid is not None and sValue not in asValid:
720 sError = 'Invalid value "%s", must be one of: %s' % (sValue, asValid);
721 else:
722 sError = None;
723 return (sValue, sError);
724
725 @staticmethod
726 def validateStr(sValue, cchMin = 0, cchMax = 4096, aoNilValues = tuple([None, '']), fAllowNull = True,
727 fAllowUnicodeSymbols = False):
728 """ Validates a string field. """
729 if sValue in aoNilValues:
730 return (sValue, None if fAllowNull else 'Mandatory.');
731
732 if cchMin is not None and len(sValue) < cchMin:
733 sError = 'Too short, min %s chars' % (cchMin,);
734 elif cchMax is not None and len(sValue) > cchMax:
735 sError = 'Too long, max %s chars' % (cchMax,);
736 elif fAllowUnicodeSymbols is False and utils.hasNonAsciiCharacters(sValue):
737 sError = 'Non-ascii characters not allowed'
738 else:
739 sError = None;
740 return (sValue, sError);
741
742 @staticmethod
743 def validateEmail(sValue, aoNilValues = tuple([None, '']), fAllowNull = True):
744 """ Validates a email field."""
745 if sValue in aoNilValues:
746 return (sValue, None if fAllowNull else 'Mandatory.');
747
748 if re.match(r'.+@.+\..+', sValue) is None:
749 return (sValue,'Invalid e-mail format.');
750 return (sValue, None);
751
752 @staticmethod
753 def validateListOfSomething(asValues, aoNilValues = tuple([[], None]), fAllowNull = True):
754 """ Validate a list of some uniform values. Returns a copy of the list (if list it is). """
755 if asValues in aoNilValues or (not asValues and not fAllowNull):
756 return (asValues, None if fAllowNull else 'Mandatory.')
757
758 if not isinstance(asValues, list):
759 return (asValues, 'Invalid data type (%s).' % (type(asValues),));
760
761 asValues = list(asValues); # copy the list.
762 if asValues:
763 oType = type(asValues[0]);
764 for i in range(1, len(asValues)):
765 if type(asValues[i]) is not oType: # pylint: disable=unidiomatic-typecheck
766 return (asValues, 'Invalid entry data type ([0]=%s vs [%d]=%s).' % (oType, i, type(asValues[i])) );
767
768 return (asValues, None);
769
770 @staticmethod
771 def validateListOfStr(asValues, cchMin = None, cchMax = None, asValidValues = None,
772 aoNilValues = tuple([[], None]), fAllowNull = True):
773 """ Validates a list of text items."""
774 (asValues, sError) = ModelDataBase.validateListOfSomething(asValues, aoNilValues, fAllowNull);
775
776 if sError is None and asValues not in aoNilValues and asValues:
777 if not utils.isString(asValues[0]):
778 return (asValues, 'Invalid item data type.');
779
780 if not fAllowNull and cchMin is None:
781 cchMin = 1;
782
783 for sValue in asValues:
784 if asValidValues is not None and sValue not in asValidValues:
785 sThisErr = 'Invalid value "%s".' % (sValue,);
786 elif cchMin is not None and len(sValue) < cchMin:
787 sThisErr = 'Value "%s" is too short, min length is %u chars.' % (sValue, cchMin);
788 elif cchMax is not None and len(sValue) > cchMax:
789 sThisErr = 'Value "%s" is too long, max length is %u chars.' % (sValue, cchMax);
790 else:
791 continue;
792
793 if sError is None:
794 sError = sThisErr;
795 else:
796 sError += ' ' + sThisErr;
797
798 return (asValues, sError);
799
800 @staticmethod
801 def validateListOfInts(asValues, iMin = 0, iMax = 0x7ffffffe, aoNilValues = tuple([[], None]), fAllowNull = True):
802 """ Validates a list of integer items."""
803 (asValues, sError) = ModelDataBase.validateListOfSomething(asValues, aoNilValues, fAllowNull);
804
805 if sError is None and asValues not in aoNilValues and asValues:
806 for i, _ in enumerate(asValues):
807 sValue = asValues[i];
808
809 sThisErr = '';
810 try:
811 iValue = int(sValue);
812 except:
813 sThisErr = 'Invalid integer value "%s".' % (sValue,);
814 else:
815 asValues[i] = iValue;
816 if iValue < iMin:
817 sThisErr = 'Value %d is too small (min %d)' % (iValue, iMin,);
818 elif iValue > iMax:
819 sThisErr = 'Value %d is too high (max %d)' % (iValue, iMax,);
820 else:
821 continue;
822
823 if sError is None:
824 sError = sThisErr;
825 else:
826 sError += ' ' + sThisErr;
827
828 return (asValues, sError);
829
830
831
832 #
833 # Old validation helpers.
834 #
835
836 @staticmethod
837 def _validateInt(dErrors, sName, sValue, iMin = 0, iMax = 0x7ffffffe, aoNilValues = tuple([-1, None, ''])):
838 """ Validates an integer field. """
839 (sValue, sError) = ModelDataBase.validateInt(sValue, iMin, iMax, aoNilValues, fAllowNull = True);
840 if sError is not None:
841 dErrors[sName] = sError;
842 return sValue;
843
844 @staticmethod
845 def _validateIntNN(dErrors, sName, sValue, iMin = 0, iMax = 0x7ffffffe, aoNilValues = tuple([-1, None, ''])):
846 """ Validates an integer field, not null. """
847 (sValue, sError) = ModelDataBase.validateInt(sValue, iMin, iMax, aoNilValues, fAllowNull = False);
848 if sError is not None:
849 dErrors[sName] = sError;
850 return sValue;
851
852 @staticmethod
853 def _validateLong(dErrors, sName, sValue, lMin = 0, lMax = None, aoNilValues = tuple([long(-1), None, ''])):
854 """ Validates an long integer field. """
855 (sValue, sError) = ModelDataBase.validateLong(sValue, lMin, lMax, aoNilValues, fAllowNull = False);
856 if sError is not None:
857 dErrors[sName] = sError;
858 return sValue;
859
860 @staticmethod
861 def _validateLongNN(dErrors, sName, sValue, lMin = 0, lMax = None, aoNilValues = tuple([long(-1), None, ''])):
862 """ Validates an long integer field, not null. """
863 (sValue, sError) = ModelDataBase.validateLong(sValue, lMin, lMax, aoNilValues, fAllowNull = True);
864 if sError is not None:
865 dErrors[sName] = sError;
866 return sValue;
867
868 @staticmethod
869 def _validateTs(dErrors, sName, sValue):
870 """ Validates a timestamp field. """
871 (sValue, sError) = ModelDataBase.validateTs(sValue, fAllowNull = True);
872 if sError is not None:
873 dErrors[sName] = sError;
874 return sValue;
875
876 @staticmethod
877 def _validateTsNN(dErrors, sName, sValue):
878 """ Validates a timestamp field, not null. """
879 (sValue, sError) = ModelDataBase.validateTs(sValue, fAllowNull = False);
880 if sError is not None:
881 dErrors[sName] = sError;
882 return sValue;
883
884 @staticmethod
885 def _validateIp(dErrors, sName, sValue):
886 """ Validates an IP address field. """
887 (sValue, sError) = ModelDataBase.validateIp(sValue, fAllowNull = True);
888 if sError is not None:
889 dErrors[sName] = sError;
890 return sValue;
891
892 @staticmethod
893 def _validateIpNN(dErrors, sName, sValue):
894 """ Validates an IP address field, not null. """
895 (sValue, sError) = ModelDataBase.validateIp(sValue, fAllowNull = False);
896 if sError is not None:
897 dErrors[sName] = sError;
898 return sValue;
899
900 @staticmethod
901 def _validateBool(dErrors, sName, sValue):
902 """ Validates a boolean field. """
903 (sValue, sError) = ModelDataBase.validateBool(sValue, fAllowNull = True);
904 if sError is not None:
905 dErrors[sName] = sError;
906 return sValue;
907
908 @staticmethod
909 def _validateBoolNN(dErrors, sName, sValue):
910 """ Validates a boolean field, not null. """
911 (sValue, sError) = ModelDataBase.validateBool(sValue, fAllowNull = False);
912 if sError is not None:
913 dErrors[sName] = sError;
914 return sValue;
915
916 @staticmethod
917 def _validateUuid(dErrors, sName, sValue):
918 """ Validates an UUID field. """
919 (sValue, sError) = ModelDataBase.validateUuid(sValue, fAllowNull = True);
920 if sError is not None:
921 dErrors[sName] = sError;
922 return sValue;
923
924 @staticmethod
925 def _validateUuidNN(dErrors, sName, sValue):
926 """ Validates an UUID field, not null. """
927 (sValue, sError) = ModelDataBase.validateUuid(sValue, fAllowNull = False);
928 if sError is not None:
929 dErrors[sName] = sError;
930 return sValue;
931
932 @staticmethod
933 def _validateWord(dErrors, sName, sValue, cchMin = 1, cchMax = 64, asValid = None):
934 """ Validates a word field. """
935 (sValue, sError) = ModelDataBase.validateWord(sValue, cchMin, cchMax, asValid, fAllowNull = True);
936 if sError is not None:
937 dErrors[sName] = sError;
938 return sValue;
939
940 @staticmethod
941 def _validateWordNN(dErrors, sName, sValue, cchMin = 1, cchMax = 64, asValid = None):
942 """ Validates a boolean field, not null. """
943 (sValue, sError) = ModelDataBase.validateWord(sValue, cchMin, cchMax, asValid, fAllowNull = False);
944 if sError is not None:
945 dErrors[sName] = sError;
946 return sValue;
947
948 @staticmethod
949 def _validateStr(dErrors, sName, sValue, cchMin = 0, cchMax = 4096):
950 """ Validates a string field. """
951 (sValue, sError) = ModelDataBase.validateStr(sValue, cchMin, cchMax, fAllowNull = True);
952 if sError is not None:
953 dErrors[sName] = sError;
954 return sValue;
955
956 @staticmethod
957 def _validateStrNN(dErrors, sName, sValue, cchMin = 0, cchMax = 4096):
958 """ Validates a string field, not null. """
959 (sValue, sError) = ModelDataBase.validateStr(sValue, cchMin, cchMax, fAllowNull = False);
960 if sError is not None:
961 dErrors[sName] = sError;
962 return sValue;
963
964 @staticmethod
965 def _validateEmail(dErrors, sName, sValue):
966 """ Validates a email field."""
967 (sValue, sError) = ModelDataBase.validateEmail(sValue, fAllowNull = True);
968 if sError is not None:
969 dErrors[sName] = sError;
970 return sValue;
971
972 @staticmethod
973 def _validateEmailNN(dErrors, sName, sValue):
974 """ Validates a email field."""
975 (sValue, sError) = ModelDataBase.validateEmail(sValue, fAllowNull = False);
976 if sError is not None:
977 dErrors[sName] = sError;
978 return sValue;
979
980 @staticmethod
981 def _validateListOfStr(dErrors, sName, asValues, asValidValues = None):
982 """ Validates a list of text items."""
983 (sValue, sError) = ModelDataBase.validateListOfStr(asValues, asValidValues = asValidValues, fAllowNull = True);
984 if sError is not None:
985 dErrors[sName] = sError;
986 return sValue;
987
988 @staticmethod
989 def _validateListOfStrNN(dErrors, sName, asValues, asValidValues = None):
990 """ Validates a list of text items, not null and len >= 1."""
991 (sValue, sError) = ModelDataBase.validateListOfStr(asValues, asValidValues = asValidValues, fAllowNull = False);
992 if sError is not None:
993 dErrors[sName] = sError;
994 return sValue;
995
996 #
997 # Various helpers.
998 #
999
1000 @staticmethod
1001 def formatSimpleNowAndPeriod(oDb, tsNow = None, sPeriodBack = None,
1002 sTablePrefix = '', sExpCol = 'tsExpire', sEffCol = 'tsEffective'):
1003 """
1004 Formats a set of tsNow and sPeriodBack arguments for a standard testmanager
1005 table.
1006
1007 If sPeriodBack is given, the query is effective for the period
1008 (tsNow - sPeriodBack) thru (tsNow).
1009
1010 If tsNow isn't given, it defaults to current time.
1011
1012 Returns the final portion of a WHERE query (start with AND) and maybe an
1013 ORDER BY and LIMIT bit if sPeriodBack is given.
1014 """
1015 if tsNow is not None:
1016 if sPeriodBack is not None:
1017 sRet = oDb.formatBindArgs(' AND ' + sTablePrefix + sExpCol + ' > (%s::timestamp - %s::interval)\n'
1018 ' AND tsEffective <= %s\n'
1019 'ORDER BY ' + sTablePrefix + sExpCol + ' DESC\n'
1020 'LIMIT 1\n'
1021 , ( tsNow, sPeriodBack, tsNow));
1022 else:
1023 sRet = oDb.formatBindArgs(' AND ' + sTablePrefix + sExpCol + ' > %s\n'
1024 ' AND ' + sTablePrefix + sEffCol + ' <= %s\n'
1025 , ( tsNow, tsNow, ));
1026 else:
1027 if sPeriodBack is not None:
1028 sRet = oDb.formatBindArgs(' AND ' + sTablePrefix + sExpCol + ' > (CURRENT_TIMESTAMP - %s::interval)\n'
1029 ' AND ' + sTablePrefix + sEffCol + ' <= CURRENT_TIMESTAMP\n'
1030 'ORDER BY ' + sTablePrefix + sExpCol + ' DESC\n'
1031 'LIMIT 1\n'
1032 , ( sPeriodBack, ));
1033 else:
1034 sRet = ' AND ' + sTablePrefix + sExpCol + ' = \'infinity\'::timestamp\n';
1035 return sRet;
1036
1037 @staticmethod
1038 def formatSimpleNowAndPeriodQuery(oDb, sQuery, aBindArgs, tsNow = None, sPeriodBack = None,
1039 sTablePrefix = '', sExpCol = 'tsExpire', sEffCol = 'tsEffective'):
1040 """
1041 Formats a simple query for a standard testmanager table with optional
1042 tsNow and sPeriodBack arguments.
1043
1044 The sQuery and sBindArgs are passed along to oDb.formatBindArgs to form
1045 the first part of the query. Must end with an open WHERE statement as
1046 we'll be adding the time part starting with 'AND something...'.
1047
1048 See formatSimpleNowAndPeriod for tsNow and sPeriodBack description.
1049
1050 Returns the final portion of a WHERE query (start with AND) and maybe an
1051 ORDER BY and LIMIT bit if sPeriodBack is given.
1052
1053 """
1054 return oDb.formatBindArgs(sQuery, aBindArgs) \
1055 + ModelDataBase.formatSimpleNowAndPeriod(oDb, tsNow, sPeriodBack, sTablePrefix, sExpCol, sEffCol);
1056
1057 #
1058 # Sub-classes.
1059 #
1060
1061 class DispWrapper(object):
1062 """Proxy object."""
1063 def __init__(self, oDisp, sAttrFmt):
1064 self.oDisp = oDisp;
1065 self.sAttrFmt = sAttrFmt;
1066 def getStringParam(self, sName, asValidValues = None, sDefault = None, fAllowNull = False):
1067 """See WuiDispatcherBase.getStringParam."""
1068 return self.oDisp.getStringParam(self.sAttrFmt % (sName,), asValidValues, sDefault, fAllowNull = fAllowNull);
1069 def getListOfStrParams(self, sName, asDefaults = None):
1070 """See WuiDispatcherBase.getListOfStrParams."""
1071 return self.oDisp.getListOfStrParams(self.sAttrFmt % (sName,), asDefaults);
1072 def getListOfIntParams(self, sName, iMin = None, iMax = None, aiDefaults = None):
1073 """See WuiDispatcherBase.getListOfIntParams."""
1074 return self.oDisp.getListOfIntParams(self.sAttrFmt % (sName,), iMin, iMax, aiDefaults);
1075
1076
1077
1078
1079# pylint: disable=no-member,missing-docstring,too-few-public-methods
1080class ModelDataBaseTestCase(unittest.TestCase):
1081 """
1082 Base testcase for ModelDataBase decendants.
1083 Derive from this and override setUp.
1084 """
1085
1086 def setUp(self):
1087 """
1088 Override this! Don't call super!
1089 The subclasses are expected to set aoSamples to an array of instance
1090 samples. The first entry must be a default object, the subsequent ones
1091 are optional and their contents freely choosen.
1092 """
1093 self.aoSamples = [ModelDataBase(),];
1094
1095 def testEquality(self):
1096 for oSample in self.aoSamples:
1097 self.assertEqual(oSample.isEqual(copy.copy(oSample)), True);
1098 self.assertIsNotNone(oSample.isEqual(self.aoSamples[0]));
1099
1100 def testNullConversion(self):
1101 if not self.aoSamples[0].getDataAttributes():
1102 return;
1103 for oSample in self.aoSamples:
1104 oCopy = copy.copy(oSample);
1105 self.assertEqual(oCopy.convertToParamNull(), oCopy);
1106 self.assertEqual(oCopy.isEqual(oSample), False);
1107 self.assertEqual(oCopy.convertFromParamNull(), oCopy);
1108 self.assertEqual(oCopy.isEqual(oSample), True, '\ngot : %s\nexpected: %s' % (oCopy, oSample,));
1109
1110 oCopy = copy.copy(oSample);
1111 self.assertEqual(oCopy.convertToParamNull(), oCopy);
1112 oCopy2 = copy.copy(oCopy);
1113 self.assertEqual(oCopy.convertToParamNull(), oCopy);
1114 self.assertEqual(oCopy.isEqual(oCopy2), True);
1115 self.assertEqual(oCopy.convertToParamNull(), oCopy);
1116 self.assertEqual(oCopy.isEqual(oCopy2), True);
1117
1118 oCopy = copy.copy(oSample);
1119 self.assertEqual(oCopy.convertFromParamNull(), oCopy);
1120 oCopy2 = copy.copy(oCopy);
1121 self.assertEqual(oCopy.convertFromParamNull(), oCopy);
1122 self.assertEqual(oCopy.isEqual(oCopy2), True);
1123 self.assertEqual(oCopy.convertFromParamNull(), oCopy);
1124 self.assertEqual(oCopy.isEqual(oCopy2), True);
1125
1126 def testReinitToNull(self):
1127 oFirst = copy.copy(self.aoSamples[0]);
1128 self.assertEqual(oFirst.reinitToNull(), oFirst);
1129 for oSample in self.aoSamples:
1130 oCopy = copy.copy(oSample);
1131 self.assertEqual(oCopy.reinitToNull(), oCopy);
1132 self.assertEqual(oCopy.isEqual(oFirst), True);
1133
1134 def testValidateAndConvert(self):
1135 for oSample in self.aoSamples:
1136 oCopy = copy.copy(oSample);
1137 oCopy.convertToParamNull();
1138 dError1 = oCopy.validateAndConvert(None);
1139
1140 oCopy2 = copy.copy(oCopy);
1141 self.assertEqual(oCopy.validateAndConvert(None), dError1);
1142 self.assertEqual(oCopy.isEqual(oCopy2), True);
1143
1144 def testInitFromParams(self):
1145 class DummyDisp(object):
1146 def getStringParam(self, sName, asValidValues = None, sDefault = None, fAllowNull = False):
1147 _ = sName; _ = asValidValues; _ = fAllowNull;
1148 return sDefault;
1149 def getListOfStrParams(self, sName, asDefaults = None):
1150 _ = sName;
1151 return asDefaults;
1152 def getListOfIntParams(self, sName, iMin = None, iMax = None, aiDefaults = None):
1153 _ = sName; _ = iMin; _ = iMax;
1154 return aiDefaults;
1155
1156 for oSample in self.aoSamples:
1157 oCopy = copy.copy(oSample);
1158 self.assertEqual(oCopy.initFromParams(DummyDisp(), fStrict = False), oCopy);
1159
1160 def testToString(self):
1161 for oSample in self.aoSamples:
1162 self.assertIsNotNone(oSample.toString());
1163
1164
1165class FilterCriterionValueAndDescription(object):
1166 """
1167 A filter criterion value and its description.
1168 """
1169
1170 def __init__(self, oValue, sDesc, cTimes = None, sHover = None, fIrrelevant = False):
1171 self.oValue = oValue; ##< Typically the ID of something in the database.
1172 self.sDesc = sDesc; ##< What to display.
1173 self.cTimes = cTimes; ##< Number of times the value occurs in the result set. None if not given.
1174 self.sHover = sHover; ##< Optional hover/title string.
1175 self.fIrrelevant = fIrrelevant; ##< Irrelevant filter option, only present because it's selected
1176 self.aoSubs = []; ##< References to FilterCriterion.oSub.aoPossible.
1177
1178
1179class FilterCriterion(object):
1180 """
1181 A filter criterion.
1182 """
1183
1184 ## @name The state.
1185 ## @{
1186 ksState_NotSelected = 'not-selected';
1187 ksState_Selected = 'selected';
1188 ## @}
1189
1190 ## @name The kind of filtering.
1191 ## @{
1192 ## 'Element of' by default, 'not an element of' when fInverted is False.
1193 ksKind_ElementOfOrNot = 'element-of-or-not';
1194 ## The criterion is a special one and cannot be inverted.
1195 ksKind_Special = 'special';
1196 ## @}
1197
1198 ## @name The value type.
1199 ## @{
1200 ksType_UInt = 'uint'; ##< unsigned integer value.
1201 ksType_UIntNil = 'uint-nil'; ##< unsigned integer value, with nil.
1202 ksType_String = 'string'; ##< string value.
1203 ksType_Ranges = 'ranges'; ##< List of (unsigned) integer ranges.
1204 ## @}
1205
1206 def __init__(self, sName, sVarNm = None, sType = ksType_UInt, # pylint: disable=too-many-arguments
1207 sState = ksState_NotSelected, sKind = ksKind_ElementOfOrNot,
1208 sTable = None, sColumn = None, asTables = None, oSub = None):
1209 assert len(sVarNm) == 2; # required by wuimain.py for filtering.
1210 self.sName = sName;
1211 self.sState = sState;
1212 self.sType = sType;
1213 self.sKind = sKind;
1214 self.sVarNm = sVarNm;
1215 self.aoSelected = []; ##< User input from sVarNm. Single value, type according to sType.
1216 self.sInvVarNm = 'i' + sVarNm if sKind == self.ksKind_ElementOfOrNot else None;
1217 self.fInverted = False; ##< User input from sInvVarNm. Inverts the operation (-> not an element of).
1218 self.aoPossible = []; ##< type: list[FilterCriterionValueAndDescription]
1219 assert (sTable is None and asTables is None) or ((sTable is not None) != (asTables is not None)), \
1220 '%s %s' % (sTable, asTables);
1221 self.asTables = [sTable,] if sTable is not None else asTables;
1222 assert sColumn is None or len(self.asTables) == 1, '%s %s' % (self.asTables, sColumn);
1223 self.sColumn = sColumn; ##< Normally only applicable if one table.
1224 self.fExpanded = None; ##< Tristate (None, False, True)
1225 self.oSub = oSub; ##< type: FilterCriterion
1226
1227
1228class ModelFilterBase(ModelBase):
1229 """
1230 Base class for filters.
1231
1232 Filters are used to narrow down data that is displayed in a list or
1233 report. This class differs a little from ModelDataBase in that it is not
1234 tied to a database table, but one or more database queries that are
1235 typically rather complicated.
1236
1237 The filter object has two roles:
1238
1239 1. It is used by a ModelLogicBase descendant to store the available
1240 filtering options for data begin displayed.
1241
1242 2. It decodes and stores the filtering options submitted by the user so
1243 a ModeLogicBase descendant can use it to construct WHERE statements.
1244
1245 The ModelFilterBase class is related to the ModelDataBase class in that it
1246 decodes user parameters and stores data, however it is not a descendant.
1247
1248 Note! In order to reduce URL lengths, we use very very brief parameter
1249 names for the filters.
1250 """
1251
1252 def __init__(self):
1253 ModelBase.__init__(self);
1254 self.aCriteria = [] # type: list[FilterCriterion]
1255
1256 def _initFromParamsWorker(self, oDisp, oCriterion): # (,FilterCriterion)
1257 """ Worker for initFromParams. """
1258 if oCriterion.sType == FilterCriterion.ksType_UInt:
1259 oCriterion.aoSelected = oDisp.getListOfIntParams(oCriterion.sVarNm, iMin = 0, aiDefaults = []);
1260 elif oCriterion.sType == FilterCriterion.ksType_UIntNil:
1261 oCriterion.aoSelected = oDisp.getListOfIntParams(oCriterion.sVarNm, iMin = -1, aiDefaults = []);
1262 elif oCriterion.sType == FilterCriterion.ksType_String:
1263 oCriterion.aoSelected = oDisp.getListOfStrParams(oCriterion.sVarNm, asDefaults = []);
1264 if len(oCriterion.aoSelected) > 100:
1265 raise TMExceptionBase('Variable %s has %u value, max allowed is 100!'
1266 % (oCriterion.sVarNm, len(oCriterion.aoSelected)));
1267 for sValue in oCriterion.aoSelected:
1268 if len(sValue) > 64 \
1269 or '\'' in sValue \
1270 or sValue[-1] == '\\':
1271 raise TMExceptionBase('Variable %s has an illegal value "%s"!' % (oCriterion.sVarNm, sValue));
1272 elif oCriterion.sType == FilterCriterion.ksType_Ranges:
1273 def convertRangeNumber(sValue):
1274 """ Helper """
1275 sValue = sValue.strip();
1276 if sValue and sValue not in ('inf', 'Inf', 'INf', 'INF', 'InF', 'iNf', 'iNF', 'inF',):
1277 try: return int(sValue);
1278 except: pass;
1279 return None;
1280
1281 for sRange in oDisp.getStringParam(oCriterion.sVarNm, sDefault = '').split(','):
1282 sRange = sRange.strip();
1283 if sRange and sRange != '-' and any(ch.isdigit() for ch in sRange):
1284 asValues = sRange.split('-');
1285 if len(asValues) == 1:
1286 asValues = [asValues[0], asValues[0]];
1287 elif len(asValues) > 2:
1288 asValues = [asValues[0], asValues[-1]];
1289 tTuple = (convertRangeNumber(asValues[0]), convertRangeNumber(asValues[1]));
1290 if tTuple[0] is not None and tTuple[1] is not None and tTuple[0] > tTuple[1]:
1291 tTuple = (tTuple[1], tTuple[0]);
1292 oCriterion.aoSelected.append(tTuple);
1293 else:
1294 assert False;
1295 if oCriterion.aoSelected:
1296 oCriterion.sState = FilterCriterion.ksState_Selected;
1297 else:
1298 oCriterion.sState = FilterCriterion.ksState_NotSelected;
1299
1300 if oCriterion.sKind == FilterCriterion.ksKind_ElementOfOrNot:
1301 oCriterion.fInverted = oDisp.getBoolParam(oCriterion.sInvVarNm, fDefault = False);
1302
1303 if oCriterion.oSub is not None:
1304 self._initFromParamsWorker(oDisp, oCriterion.oSub);
1305 return;
1306
1307 def initFromParams(self, oDisp): # type: (WuiDispatcherBase) -> self
1308 """
1309 Initialize the object from parameters.
1310
1311 Returns self. Raises exception on invalid parameter value.
1312 """
1313
1314 for oCriterion in self.aCriteria:
1315 self._initFromParamsWorker(oDisp, oCriterion);
1316 return self;
1317
1318 def strainParameters(self, dParams, aAdditionalParams = None):
1319 """ Filters just the parameters relevant to this filter, returning a copy. """
1320
1321 # Collect the parameter names.
1322 dWanted = dict();
1323 for oCrit in self.aCriteria:
1324 dWanted[oCrit.sVarNm] = 1;
1325 if oCrit.sInvVarNm:
1326 dWanted[oCrit.sInvVarNm] = 1;
1327
1328 # Add additional stuff.
1329 if aAdditionalParams:
1330 for sParam in aAdditionalParams:
1331 dWanted[sParam] = 1;
1332
1333 # To the straining.
1334 dRet = dict();
1335 for sKey in dParams:
1336 if sKey in dWanted:
1337 dRet[sKey] = dParams[sKey];
1338 return dRet;
1339
1340
1341class ModelLogicBase(ModelBase): # pylint: disable=too-few-public-methods
1342 """
1343 Something all classes in the logic classes the logical model inherits from.
1344 """
1345
1346 def __init__(self, oDb):
1347 ModelBase.__init__(self);
1348
1349 #
1350 # Note! Do not create a connection here if None, we need to DB share
1351 # connection with all other logic objects so we can perform half
1352 # complex transactions involving several logic objects.
1353 #
1354 self._oDb = oDb;
1355
1356 def getDbConnection(self):
1357 """
1358 Gets the database connection.
1359 This should only be used for instantiating other ModelLogicBase children.
1360 """
1361 return self._oDb;
1362
1363 def _dbRowsToModelDataList(self, oModelDataType, aaoRows = None):
1364 """
1365 Helper for conerting a simple fetch into a list of ModelDataType python objects.
1366
1367 If aaoRows is None, we'll fetchAll from the database ourselves.
1368
1369 The oModelDataType must be a class derived from ModelDataBase and implement
1370 the initFormDbRow method.
1371
1372 Returns a list of oModelDataType instances.
1373 """
1374 assert issubclass(oModelDataType, ModelDataBase);
1375 aoRet = [];
1376 if aaoRows is None:
1377 aaoRows = self._oDb.fetchAll();
1378 for aoRow in aaoRows:
1379 aoRet.append(oModelDataType().initFromDbRow(aoRow));
1380 return aoRet;
1381
1382
1383
1384class AttributeChangeEntry(object): # pylint: disable=too-few-public-methods
1385 """
1386 Data class representing the changes made to one attribute.
1387 """
1388
1389 def __init__(self, sAttr, oNewRaw, oOldRaw, sNewText, sOldText):
1390 self.sAttr = sAttr;
1391 self.oNewRaw = oNewRaw;
1392 self.oOldRaw = oOldRaw;
1393 self.sNewText = sNewText;
1394 self.sOldText = sOldText;
1395
1396class AttributeChangeEntryPre(AttributeChangeEntry): # pylint: disable=too-few-public-methods
1397 """
1398 AttributeChangeEntry for preformatted values.
1399 """
1400
1401 def __init__(self, sAttr, oNewRaw, oOldRaw, sNewText, sOldText):
1402 AttributeChangeEntry.__init__(self, sAttr, oNewRaw, oOldRaw, sNewText, sOldText);
1403
1404class ChangeLogEntry(object): # pylint: disable=too-few-public-methods
1405 """
1406 A change log entry returned by the fetchChangeLog method typically
1407 implemented by ModelLogicBase child classes.
1408 """
1409
1410 def __init__(self, uidAuthor, sAuthor, tsEffective, tsExpire, oNewRaw, oOldRaw, aoChanges):
1411 self.uidAuthor = uidAuthor;
1412 self.sAuthor = sAuthor;
1413 self.tsEffective = tsEffective;
1414 self.tsExpire = tsExpire;
1415 self.oNewRaw = oNewRaw;
1416 self.oOldRaw = oOldRaw; # Note! NULL for the last entry.
1417 self.aoChanges = aoChanges;
1418
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