Coverage for /home/runner/work/viur-core/viur-core/viur/src/viur/core/bones/boolean.py: 75%

51 statements  

« prev     ^ index     » next       coverage.py v7.16.1, created at 2026-09-25 15:02 +0000

1import typing as t 

2 

3from viur.core import conf, db, utils 

4from viur.core.bones.base import BaseBone, ReadFromClientError, ReadFromClientErrorSeverity 

5 

6DEFAULT_VALUE_T: t.TypeAlias = bool | None | list[bool] | dict[str, list[bool] | bool] 

7 

8 

9class BooleanBone(BaseBone): 

10 """ 

11 Represents a boolean data type, which can have two possible values: `True` or `False`. 

12 It also allows for `None` to specify the "not yet set"-state. 

13 BooleanBones cannot be defined as `multiple=True`. 

14 

15 :param defaultValue: The default value of the `BooleanBone` instance. Defaults to `None` (unset). 

16 :raises ValueError: If the `defaultValue` is not either a boolean value (`True` or `False`) or `None`. 

17 """ 

18 type = "bool" 

19 

20 def __init__( 

21 self, 

22 *, 

23 defaultValue: DEFAULT_VALUE_T | t.Callable[[t.Self, "SkeletonInstance"], DEFAULT_VALUE_T] = None, 

24 **kwargs 

25 ): 

26 if defaultValue is not None: 

27 # We have given an explicit defaultValue and maybe a complex structure 

28 if not kwargs.get("languages") and not (isinstance(defaultValue, bool) or callable(defaultValue)): 

29 raise TypeError("Only True, False, None or callable can be provided as BooleanBone defaultValue") 

30 # TODO: missing validation for complex types, but in other bones too 

31 

32 super().__init__(defaultValue=defaultValue, **kwargs) 

33 

34 # Disallow creation of BooleanBone(multiple=True) 

35 if self.multiple: 

36 raise ValueError("BooleanBone cannot be multiple") 

37 

38 def singleValueFromClient(self, value, skel, bone_name, client_data): 

39 value = utils.parse.bool(value, conf.bone_boolean_str2true) 

40 

41 if err := self.isInvalid(value): 41 ↛ 42line 41 didn't jump to line 42 because the condition on line 41 was never true

42 return value, [ReadFromClientError(ReadFromClientErrorSeverity.Invalid, err)] 

43 

44 return value, None 

45 

46 def getEmptyValue(self): 

47 """ 

48 Returns the empty value of the `BooleanBone` class, which is `False`. 

49 

50 :return: The empty value of the `BooleanBone` class (`False`). 

51 :rtype: bool 

52 """ 

53 return False 

54 

55 def isEmpty(self, value: t.Any): 

56 """ 

57 Checks if the given boolean value is empty. 

58 

59 :param value: The boolean value to be checked. 

60 :return: `True` if the boolean value is empty (i.e., equal to the empty value of the `BooleanBone` class), \ 

61 `False` otherwise. 

62 :rtype: bool 

63 """ 

64 if value is self.getEmptyValue(): 

65 return True 

66 return not bool(value) 

67 

68 def refresh(self, skel: 'viur.core.skeleton.SkeletonInstance', name: str) -> None: 

69 """ 

70 Inverse of serialize. Evaluates whats 

71 read from the datastore and populates 

72 this bone accordingly. 

73 

74 :param name: The property-name this bone has in its Skeleton (not the description!) 

75 """ 

76 if self.languages: 76 ↛ 90line 76 didn't jump to line 90 because the condition on line 76 was always true

77 # The raw datastore value can be None (entity written before this bone existed) and 

78 # getDefaultValue() answers with a dict for a language-aware bone, so neither may be 

79 # indexed blindly. 

80 values = skel[name] or {} 

81 defaults = self.getDefaultValue(skel) 

82 if not isinstance(defaults, dict): 82 ↛ 83line 82 didn't jump to line 83 because the condition on line 82 was never true

83 defaults = {lang: defaults for lang in self.languages} 

84 

85 skel[name] = { 

86 lang: utils.parse.bool(values[lang], conf.bone_boolean_str2true) 

87 if lang in values else defaults.get(lang) 

88 for lang in self.languages 

89 } 

90 elif skel[name] != self.getEmptyValue(): 

91 # Enforce a boolean if the bone is not empty (Maybe the empty value is explicit set to None). 

92 # So in this case we keep the empty value (e.g. the None) as is. 

93 skel[name] = utils.parse.bool(skel[name], conf.bone_boolean_str2true) 

94 

95 def setBoneValue( 

96 self, 

97 skel: 'SkeletonInstance', 

98 boneName: str, 

99 value: t.Any, 

100 append: bool, 

101 language: None | str = None 

102 ) -> bool: 

103 """ 

104 Sets the value of the bone to the provided 'value'. 

105 Sanity checks are performed; if the value is invalid, the bone value will revert to its original 

106 (default) value and the function will return False. 

107 

108 :param skel: Dictionary with the current values from the skeleton the bone belongs to 

109 :param boneName: The name of the bone that should be modified 

110 :param value: The value that should be assigned. Its type depends on the type of the bone 

111 :param append: If True, the given value will be appended to the existing bone values instead of 

112 replacing them. Only supported on bones with multiple=True 

113 :param language: Optional, the language of the value if the bone is language-aware 

114 :return: A boolean indicating whether the operation succeeded or not 

115 :rtype: bool 

116 """ 

117 if append: 

118 raise ValueError(f"append is not possible on {self.type} bones") 

119 

120 if language: 

121 if not self.languages or language not in self.languages: 121 ↛ 122line 121 didn't jump to line 122 because the condition on line 121 was never true

122 return False 

123 

124 skel[boneName][language] = utils.parse.bool(value, conf.bone_boolean_str2true) 

125 else: 

126 skel[boneName] = utils.parse.bool(value, conf.bone_boolean_str2true) 

127 

128 return True 

129 

130 def singleValueSerialize(self, value, skel: 'SkeletonInstance', name: str, parentIndexed: bool): 

131 """ 

132 Serializes a single value of the bone for storage in the database. 

133 

134 Derived bone classes should overwrite this method to implement their own logic for serializing single 

135 values. 

136 The serialized value should be suitable for storage in the database. 

137 """ 

138 if value == self.getEmptyValue(): 138 ↛ 141line 138 didn't jump to line 141 because the condition on line 138 was always true

139 # Keep the bones empty, maybe the empty value is explicit set to None 

140 return value 

141 return utils.parse.bool(value, conf.bone_boolean_str2true) 

142 

143 def buildDBFilter( 

144 self, 

145 name: str, 

146 skel: 'viur.core.skeleton.SkeletonInstance', 

147 dbFilter: db.Query, 

148 rawFilter: dict, 

149 prefix: t.Optional[str] = None 

150 ) -> db.Query: 

151 """ 

152 Builds a database filter based on the boolean value. 

153 

154 :param name: The name of the `BooleanBone` instance. 

155 :param skel: The `SkeletonInstance` object representing the data of the current entity. 

156 :param dbFilter: The `Query` object representing the current database filter. 

157 :param rawFilter: The dictionary representing the raw filter data received from the client. 

158 :param prefix: A prefix to be added to the property name in the database filter. 

159 :return: The updated `Query` object representing the updated database filter. 

160 :rtype: google.cloud.ndb.query.Query 

161 """ 

162 if name in rawFilter: 

163 val = utils.parse.bool(rawFilter[name], conf.bone_boolean_str2true) 

164 return super().buildDBFilter(name, skel, dbFilter, {name: val}, prefix=prefix) 

165 

166 return dbFilter