Coverage for gws-app/gws/base/model/core.py: 92%

212 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-24 12:46 +0200

1"""Base model.""" 

2 

3from typing import Optional, cast 

4 

5import gws 

6import gws.base.feature 

7import gws.base.shape 

8import gws.config.util 

9 

10DEFAULT_UID_NAME = 'uid' 

11DEFAULT_GEOMETRY_NAME = 'geometry' 

12 

13 

14class TableViewColumn(gws.Data): 

15 """Table view column configuration.""" 

16 

17 name: str 

18 """Column name.""" 

19 width: Optional[int] 

20 """Column width in pixels. If not set, the column will be auto-sized.""" 

21 

22 

23class ClientOptions(gws.Data): 

24 """Client options for a model""" 

25 

26 keepFormOpen: bool = False 

27 """Keep the edit form open after save""" 

28 

29 

30class Config(gws.ConfigWithAccess): 

31 """Model configuration""" 

32 

33 fields: Optional[list[gws.ext.config.modelField]] 

34 """Model fields.""" 

35 loadingStrategy: Optional[gws.FeatureLoadingStrategy] 

36 """Loading strategy for features.""" 

37 exportStrategy: Optional[gws.FeatureExportStrategy] 

38 """Export strategy for features.""" 

39 title: str = '' 

40 """Model title.""" 

41 isEditable: bool = False 

42 """This model is editable.""" 

43 withAutoFields: bool = False 

44 """Autoload non-configured model fields from the source.""" 

45 excludeColumns: Optional[list[str]] 

46 """Exclude columns names from autoload.""" 

47 withTableView: bool = True 

48 """Enable table view for this model.""" 

49 tableViewColumns: Optional[list[TableViewColumn]] 

50 """Fields to include in the table view.""" 

51 templates: Optional[list[gws.ext.config.template]] 

52 """Feature templates.""" 

53 sort: Optional[list[gws.SortOptions]] 

54 """Default sorting.""" 

55 clientOptions: Optional[ClientOptions] 

56 """Client options for a model..""" 

57 

58 

59class Props(gws.Props): 

60 clientOptions: gws.ModelClientOptions 

61 canCreate: bool 

62 canDelete: bool 

63 canRead: bool 

64 canWrite: bool 

65 isEditable: bool 

66 fields: list[gws.ext.props.modelField] 

67 geometryCrs: Optional[str] 

68 geometryName: Optional[str] 

69 geometryType: Optional[gws.GeometryType] 

70 layerUid: Optional[str] 

71 loadingStrategy: gws.FeatureLoadingStrategy 

72 supportsGeometrySearch: bool 

73 supportsKeywordSearch: bool 

74 tableViewColumns: list[TableViewColumn] 

75 title: str 

76 uid: str 

77 uidName: Optional[str] 

78 

79 

80class Object(gws.Model): 

81 def configure(self): 

82 self.isEditable = self.cfg('isEditable', default=False) 

83 self.withTableView = self.cfg('withTableView', default=True) 

84 self.fields = [] 

85 self.geometryCrs = None 

86 self.geometryName = '' 

87 self.geometryType = None 

88 self.uidName = '' 

89 self.loadingStrategy = self.cfg('loadingStrategy') 

90 self.exportStrategy = self.cfg('exportStrategy', default=gws.FeatureExportStrategy.load) 

91 self.title = self.cfg('title') 

92 self.clientOptions = self.cfg('clientOptions') or gws.Data() 

93 

94 def post_configure(self): 

95 if self.isEditable and not self.uidName: 

96 raise gws.ConfigurationError(f'no primary key found for editable model {self}') 

97 

98 def configure_model(self): 

99 """Model configuration protocol.""" 

100 

101 self.configure_provider() 

102 self.configure_sources() 

103 self.configure_fields() 

104 self.configure_uid() 

105 self.configure_geometry() 

106 self.configure_sort() 

107 self.configure_templates() 

108 

109 def configure_provider(self): 

110 return False 

111 

112 def configure_sources(self): 

113 return False 

114 

115 def configure_fields(self): 

116 has_conf = False 

117 has_auto = False 

118 

119 p = self.cfg('fields') 

120 if p: 

121 self.fields = self.create_children(gws.ext.object.modelField, p, _defaultModel=self) 

122 has_conf = True 

123 if not has_conf or self.cfg('withAutoFields'): 

124 has_auto = self.configure_auto_fields() 

125 

126 return has_conf or has_auto 

127 

128 def configure_auto_fields(self): 

129 desc = self.describe() 

130 if not desc: 

131 return False 

132 

133 exclude = set(self.cfg('excludeColumns', default=[])) 

134 exclude.update(fld.name for fld in self.fields) 

135 

136 for col in desc.columns: 

137 if col.name in exclude: 

138 continue 

139 

140 typ = _DEFAULT_FIELD_TYPES.get(col.type) 

141 if not typ: 

142 # gws.log.warning(f'cannot find suitable field type for column {desc.fullName}.{col.name} ({col.type})') 

143 continue 

144 

145 cfg = gws.Config( 

146 type=typ, 

147 name=col.name, 

148 isPrimaryKey=col.isPrimaryKey, 

149 isRequired=not col.isNullable, 

150 ) 

151 fld = self.create_child(gws.ext.object.modelField, cfg, _defaultModel=self) 

152 if fld: 

153 self.fields.append(fld) 

154 exclude.add(fld.name) 

155 

156 return True 

157 

158 def configure_uid(self): 

159 if self.uidName: 

160 return True 

161 uids = [] 

162 for fld in self.fields: 

163 if fld.isPrimaryKey: 

164 uids.append(fld.name) 

165 if len(uids) == 1: 

166 self.uidName = uids[0] 

167 return True 

168 

169 def configure_geometry(self): 

170 for fld in self.fields: 

171 if getattr(fld, 'geometryType', None): 

172 self.geometryName = fld.name 

173 self.geometryType = getattr(fld, 'geometryType') 

174 self.geometryCrs = getattr(fld, 'geometryCrs') 

175 return True 

176 

177 def configure_sort(self): 

178 p = self.cfg('sort') 

179 if p: 

180 self.defaultSort = [gws.SearchSort(c) for c in p] 

181 return True 

182 if self.uidName: 

183 self.defaultSort = [gws.SearchSort(fieldName=self.uidName, reversed=False)] 

184 return False 

185 self.defaultSort = [] 

186 return False 

187 

188 def configure_templates(self): 

189 return gws.config.util.configure_templates_for(self) 

190 

191 ## 

192 

193 def props(self, user): 

194 layer = cast(gws.Layer, self.find_closest(gws.ext.object.layer)) 

195 

196 return gws.Props( 

197 clientOptions=self.clientOptions, 

198 canCreate=user.can_create(self), 

199 canDelete=user.can_delete(self), 

200 canRead=user.can_read(self), 

201 canWrite=user.can_write(self), 

202 fields=self.fields, 

203 geometryCrs=self.geometryCrs.epsg if self.geometryCrs else None, 

204 geometryName=self.geometryName, 

205 geometryType=self.geometryType, 

206 isEditable=self.isEditable, 

207 layerUid=layer.uid if layer else None, 

208 loadingStrategy=self.loadingStrategy or (layer.loadingStrategy if layer else gws.FeatureLoadingStrategy.all), 

209 supportsGeometrySearch=any(fld.supportsGeometrySearch for fld in self.fields), 

210 supportsKeywordSearch=any(fld.supportsKeywordSearch for fld in self.fields), 

211 tableViewColumns=self.table_view_columns(user), 

212 title=self.title or (layer.title if layer else ''), 

213 uid=self.uid, 

214 uidName=self.uidName, 

215 ) 

216 

217 ## 

218 

219 def table_view_columns(self, user): 

220 if not self.withTableView: 

221 return [] 

222 

223 cols = [] 

224 

225 p = self.cfg('tableViewColumns') 

226 if p: 

227 fmap = {fld.name: fld for fld in self.fields} 

228 for c in p: 

229 fld = fmap.get(c.name) 

230 if fld and user.can_use(fld) and fld.widget and fld.widget.supportsTableView: 

231 cols.append(TableViewColumn(name=c.name, width=c.width or 0)) 

232 else: 

233 for fld in self.fields: 

234 if fld and user.can_use(fld) and fld.widget and fld.widget.supportsTableView: 

235 cols.append(TableViewColumn(name=fld.name, width=0)) 

236 

237 return cols 

238 

239 def field(self, name): 

240 for fld in self.fields: 

241 if fld.name == name: 

242 return fld 

243 

244 def validate_feature(self, feature, mc): 

245 feature.errors = [] 

246 for fld in self.fields: 

247 fld.do_validate(feature, mc) 

248 return len(feature.errors) == 0 

249 

250 def related_models(self): 

251 d = {} 

252 

253 for fld in self.fields: 

254 for model in fld.related_models(): 

255 d[model.uid] = model 

256 

257 return list(d.values()) 

258 

259 ## 

260 

261 def get_features(self, uids, mc): 

262 if not uids: 

263 return [] 

264 search = gws.SearchQuery(uids=set(uids)) 

265 return self.find_features(search, mc) 

266 

267 def get_feature(self, uid, mc): 

268 features = self.get_features([uid], mc) 

269 if features: 

270 return features[0] 

271 

272 def find_features(self, search, mc): 

273 return [] 

274 

275 ## 

276 

277 def feature_from_props(self, props, mc): 

278 props = cast(gws.FeatureProps, gws.u.to_data_object(props)) 

279 feature = gws.base.feature.new(model=self, props=props) 

280 feature.cssSelector = props.cssSelector or '' 

281 feature.isNew = props.isNew or False 

282 feature.views = props.views or {} 

283 

284 for fld in self.fields: 

285 fld.from_props(feature, mc) 

286 

287 return feature 

288 

289 def feature_to_props(self, feature, mc): 

290 feature.props = gws.FeatureProps( 

291 attributes={}, 

292 cssSelector=feature.cssSelector, 

293 category=feature.category or '', 

294 errors=feature.errors or [], 

295 isNew=feature.isNew, 

296 modelUid=self.uid, 

297 uid=feature.uid(), 

298 views=feature.views, 

299 ) 

300 

301 for fld in self.fields: 

302 fld.to_props(feature, mc) 

303 

304 return feature.props 

305 

306 def feature_to_view_props(self, feature, mc): 

307 props = self.feature_to_props(feature, mc) 

308 

309 a = {} 

310 

311 if self.uidName: 

312 a[DEFAULT_UID_NAME] = props.attributes.get(self.uidName) 

313 if self.geometryName: 

314 a[DEFAULT_GEOMETRY_NAME] = props.attributes.get(self.geometryName) 

315 

316 # only provide "uid" and "geometry" attributes for the view props 

317 props.attributes = a 

318 

319 return props 

320 

321 

322## 

323 

324# @TODO this should be populated dynamically from available gws.ext.object.modelField types 

325 

326_DEFAULT_FIELD_TYPES = { 

327 gws.AttributeType.str: 'text', 

328 gws.AttributeType.int: 'integer', 

329 gws.AttributeType.date: 'date', 

330 gws.AttributeType.bool: 'bool', 

331 # gws.AttributeType.bytes: 'bytea', 

332 gws.AttributeType.datetime: 'datetime', 

333 # gws.AttributeType.feature: 'feature', 

334 # gws.AttributeType.featurelist: 'featurelist', 

335 gws.AttributeType.float: 'float', 

336 # gws.AttributeType.floatlist: 'floatlist', 

337 gws.AttributeType.geometry: 'geometry', 

338 # gws.AttributeType.intlist: 'intlist', 

339 # gws.AttributeType.strlist: 'strlist', 

340 gws.AttributeType.time: 'time', 

341}