Coverage for gws-app/gws/base/model/field.py: 79%

111 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-05 13:35 +0200

1"""Base model field.""" 

2 

3from typing import Optional, cast 

4 

5import gws 

6 

7 

8class Props(gws.Props): 

9 attributeType: gws.AttributeType 

10 geometryType: gws.GeometryType 

11 name: str 

12 title: str 

13 type: str 

14 widget: gws.ext.props.modelWidget 

15 uid: str 

16 relatedModelUids: list[str] 

17 

18 

19class Config(gws.ConfigWithAccess): 

20 """Configuration for the model field.""" 

21 

22 name: str 

23 """Field name, matching the column or attribute name in the source.""" 

24 title: Optional[str] 

25 """Field title shown in the client.""" 

26 

27 isPrimaryKey: Optional[bool] 

28 """The field is a primary key.""" 

29 isRequired: Optional[bool] 

30 """The field must not be empty.""" 

31 isUnique: Optional[bool] 

32 """The field is unique.""" 

33 isAuto: Optional[bool] 

34 """The value is set by the database and never written.""" 

35 isHidden: Optional[bool] 

36 """The field is not automatically displayed in the UI.""" 

37 

38 values: Optional[list[gws.ext.config.modelValue]] 

39 """Value sources that compute the field value on read, create or update.""" 

40 validators: Optional[list[gws.ext.config.modelValidator]] 

41 """Additional validators for the field value.""" 

42 

43 widget: Optional[gws.ext.config.modelWidget] 

44 """Widget that displays and edits the field in the client.""" 

45 

46 

47## 

48 

49 

50class Object(gws.ModelField): 

51 """Base model field. 

52 

53 Provides the field flags, value objects, validators, widget, props and validation. 

54 Subclasses implement reading and writing the field value and the conversion between 

55 record, feature and props values. 

56 """ 

57 

58 notEmptyValidator: gws.ModelValidator 

59 """Validator that checks that the value is not empty.""" 

60 formatValidator: gws.ModelValidator 

61 """Validator that checks the format of the value.""" 

62 

63 def configure(self): 

64 self.model = self.cfg('_defaultModel') 

65 self.name = self.cfg('name') 

66 self.title = self.cfg('title', default=self.name) 

67 

68 self.values = [] 

69 self.validators = [] 

70 self.widget = None 

71 

72 self.configure_flags() 

73 self.configure_values() 

74 self.configure_validators() 

75 self.configure_widget() 

76 

77 def configure_flags(self): 

78 """Set the field flags from the configuration or the source column. 

79 

80 ``isPrimaryKey``, ``isRequired`` and ``isAuto`` default to the properties of the 

81 source column with the field name. ``isUnique`` and ``isHidden`` default to False. 

82 """ 

83 col = self.describe() 

84 

85 p = self.cfg('isPrimaryKey') 

86 if p is not None: 

87 self.isPrimaryKey = p 

88 else: 

89 self.isPrimaryKey = col and col.isPrimaryKey 

90 

91 p = self.cfg('isRequired') 

92 if p is not None: 

93 self.isRequired = p 

94 else: 

95 self.isRequired = col and not col.isNullable 

96 

97 p = self.cfg('isAuto') 

98 if p is not None: 

99 self.isAuto = p 

100 else: 

101 self.isAuto = col and col.isAutoincrement 

102 

103 p = self.cfg('isUnique') 

104 if p is not None: 

105 self.isUnique = p 

106 else: 

107 self.isUnique = False 

108 

109 self.isHidden = self.cfg('isHidden', default=False) 

110 

111 def configure_values(self): 

112 """Create the configured value objects. 

113 

114 Returns: 

115 True if values are configured, None otherwise. 

116 """ 

117 p = self.cfg('values') 

118 if p: 

119 self.values = self.create_children(gws.ext.object.modelValue, p) 

120 return True 

121 

122 def configure_validators(self): 

123 """Create the configured validators. 

124 

125 Configured ``notEmpty`` and ``format`` validators replace the shared default ones, 

126 which apply to ``create`` and ``update``. 

127 

128 Returns: 

129 Always True. 

130 """ 

131 vd_not_empty = None 

132 vd_format = None 

133 

134 for p in self.cfg('validators', default=[]): 

135 vd = self.create_validator(p) 

136 if vd.extType == 'notEmpty': 

137 vd_not_empty = vd 

138 elif vd.extType == 'format': 

139 vd_format = vd 

140 else: 

141 self.validators.append(vd) 

142 

143 self.notEmptyValidator = vd_not_empty or self.root.create_shared( 

144 gws.ext.object.modelValidator, 

145 type='notEmpty', 

146 uid='gws.base.model.field.default_validator_notEmpty', 

147 forCreate=True, 

148 forUpdate=True, 

149 ) 

150 

151 self.formatValidator = vd_format or self.root.create_shared( 

152 gws.ext.object.modelValidator, 

153 type='format', 

154 uid='gws.base.model.field.default_validator_format', 

155 forCreate=True, 

156 forUpdate=True, 

157 ) 

158 

159 return True 

160 

161 def create_validator(self, cfg): 

162 """Create a validator as a child of this field. 

163 

164 Args: 

165 cfg: The validator configuration. 

166 

167 Returns: 

168 The validator object. 

169 """ 

170 return self.create_child(gws.ext.object.modelValidator, cfg) 

171 

172 def configure_widget(self): 

173 """Create the configured widget. 

174 

175 Returns: 

176 True if a widget is configured, None otherwise. 

177 """ 

178 p = self.cfg('widget') 

179 if p: 

180 self.widget = self.create_child(gws.ext.object.modelWidget, p) 

181 return True 

182 

183 ## 

184 

185 def props(self, user): 

186 wp = None 

187 if self.widget: 

188 wp = gws.props_of(self.widget, user, self) 

189 if not user.can_write(self): 

190 wp.readOnly = True 

191 

192 return Props( 

193 attributeType=self.attributeType, 

194 name=self.name, 

195 title=self.title, 

196 type=self.extType, 

197 widget=wp, 

198 uid=self.uid, 

199 relatedModelUids=[m.uid for m in self.related_models() if user.can_read(m)], 

200 ) 

201 

202 ## 

203 

204 def do_validate(self, feature, mc): 

205 # apply the 'notEmpty' validator and exit immediately if it fails 

206 # (no error message if field is not required) 

207 

208 ok = self.notEmptyValidator.validate(self, feature, mc) 

209 if not ok: 

210 if self.isRequired: 

211 feature.errors.append( 

212 gws.ModelValidationError( 

213 fieldName=self.name, 

214 message=self.notEmptyValidator.message, 

215 ) 

216 ) 

217 return 

218 

219 # apply the 'format' validator 

220 

221 ok = self.formatValidator.validate(self, feature, mc) 

222 if not ok: 

223 feature.errors.append( 

224 gws.ModelValidationError( 

225 fieldName=self.name, 

226 message=self.formatValidator.message, 

227 ) 

228 ) 

229 return 

230 

231 # apply others 

232 

233 for vd in self.validators: 

234 if mc.op in vd.ops: 

235 ok = vd.validate(self, feature, mc) 

236 if not ok: 

237 feature.errors.append( 

238 gws.ModelValidationError( 

239 fieldName=self.name, 

240 message=vd.message, 

241 ) 

242 ) 

243 return 

244 

245 def related_models(self): 

246 return [] 

247 

248 def find_relatable_features(self, search, mc): 

249 return [] 

250 

251 def raw_to_python(self, feature, value, mc): 

252 return value 

253 

254 def prop_to_python(self, feature, value, mc): 

255 return value 

256 

257 def python_to_raw(self, feature, value, mc): 

258 return value 

259 

260 def python_to_prop(self, feature, value, mc): 

261 return value 

262 

263 ## 

264 

265 def describe(self): 

266 desc = self.model.describe() 

267 if desc: 

268 return desc.columnMap.get(self.name)