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

205 statements  

« prev     ^ index     » next       coverage.py v7.11.0, created at 2025-10-16 22:59 +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 title: str = '' 

38 """Model title.""" 

39 isEditable: bool = False 

40 """This model is editable.""" 

41 withAutoFields: bool = False 

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

43 excludeColumns: Optional[list[str]] 

44 """Exclude columns names from autoload.""" 

45 withTableView: bool = True 

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

47 tableViewColumns: Optional[list[TableViewColumn]] 

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

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

50 """Feature templates.""" 

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

52 """Default sorting.""" 

53 clientOptions: Optional[ClientOptions] 

54 """Client options for a model..""" 

55 

56 

57class Props(gws.Props): 

58 clientOptions: gws.ModelClientOptions 

59 canCreate: bool 

60 canDelete: bool 

61 canRead: bool 

62 canWrite: bool 

63 isEditable: bool 

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

65 geometryCrs: Optional[str] 

66 geometryName: Optional[str] 

67 geometryType: Optional[gws.GeometryType] 

68 layerUid: Optional[str] 

69 loadingStrategy: gws.FeatureLoadingStrategy 

70 supportsGeometrySearch: bool 

71 supportsKeywordSearch: bool 

72 tableViewColumns: list[TableViewColumn] 

73 title: str 

74 uid: str 

75 uidName: Optional[str] 

76 

77 

78class Object(gws.Model): 

79 def configure(self): 

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

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

82 self.fields = [] 

83 self.geometryCrs = None 

84 self.geometryName = '' 

85 self.geometryType = None 

86 self.uidName = '' 

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

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

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

90 

91 def post_configure(self): 

92 if self.isEditable and not self.uidName: 

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

94 

95 def configure_model(self): 

96 """Model configuration protocol.""" 

97 

98 self.configure_provider() 

99 self.configure_sources() 

100 self.configure_fields() 

101 self.configure_uid() 

102 self.configure_geometry() 

103 self.configure_sort() 

104 self.configure_templates() 

105 

106 def configure_provider(self): 

107 return False 

108 

109 def configure_sources(self): 

110 return False 

111 

112 def configure_fields(self): 

113 has_conf = False 

114 has_auto = False 

115 

116 p = self.cfg('fields') 

117 if p: 

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

119 has_conf = True 

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

121 has_auto = self.configure_auto_fields() 

122 

123 return has_conf or has_auto 

124 

125 def configure_auto_fields(self): 

126 desc = self.describe() 

127 if not desc: 

128 return False 

129 

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

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

132 

133 for col in desc.columns: 

134 if col.name in exclude: 

135 continue 

136 

137 typ = _DEFAULT_FIELD_TYPES.get(col.type) 

138 if not typ: 

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

140 continue 

141 

142 cfg = gws.Config( 

143 type=typ, 

144 name=col.name, 

145 isPrimaryKey=col.isPrimaryKey, 

146 isRequired=not col.isNullable, 

147 ) 

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

149 if fld: 

150 self.fields.append(fld) 

151 exclude.add(fld.name) 

152 

153 return True 

154 

155 def configure_uid(self): 

156 if self.uidName: 

157 return True 

158 uids = [] 

159 for fld in self.fields: 

160 if fld.isPrimaryKey: 

161 uids.append(fld.name) 

162 if len(uids) == 1: 

163 self.uidName = uids[0] 

164 return True 

165 

166 def configure_geometry(self): 

167 for fld in self.fields: 

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

169 self.geometryName = fld.name 

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

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

172 return True 

173 

174 def configure_sort(self): 

175 p = self.cfg('sort') 

176 if p: 

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

178 return True 

179 if self.uidName: 

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

181 return False 

182 self.defaultSort = [] 

183 return False 

184 

185 def configure_templates(self): 

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

187 

188 ## 

189 

190 def props(self, user): 

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

192 

193 return gws.Props( 

194 clientOptions=self.clientOptions, 

195 canCreate=user.can_create(self), 

196 canDelete=user.can_delete(self), 

197 canRead=user.can_read(self), 

198 canWrite=user.can_write(self), 

199 fields=self.fields, 

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

201 geometryName=self.geometryName, 

202 geometryType=self.geometryType, 

203 isEditable=self.isEditable, 

204 layerUid=layer.uid if layer else None, 

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

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

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

208 tableViewColumns=self.table_view_columns(user), 

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

210 uid=self.uid, 

211 uidName=self.uidName, 

212 ) 

213 

214 ## 

215 

216 def table_view_columns(self, user): 

217 if not self.withTableView: 

218 return [] 

219 

220 cols = [] 

221 

222 p = self.cfg('tableViewColumns') 

223 if p: 

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

225 for c in p: 

226 fld = fmap.get(c.name) 

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

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

229 else: 

230 for fld in self.fields: 

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

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

233 

234 return cols 

235 

236 def field(self, name): 

237 for fld in self.fields: 

238 if fld.name == name: 

239 return fld 

240 

241 def validate_feature(self, feature, mc): 

242 feature.errors = [] 

243 for fld in self.fields: 

244 fld.do_validate(feature, mc) 

245 return len(feature.errors) == 0 

246 

247 def related_models(self): 

248 d = {} 

249 

250 for fld in self.fields: 

251 for model in fld.related_models(): 

252 d[model.uid] = model 

253 

254 return list(d.values()) 

255 

256 ## 

257 

258 def get_features(self, uids, mc): 

259 if not uids: 

260 return [] 

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

262 return self.find_features(search, mc) 

263 

264 def find_features(self, search, mc): 

265 return [] 

266 

267 ## 

268 

269 def feature_from_props(self, props, mc): 

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

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

272 feature.cssSelector = props.cssSelector or '' 

273 feature.isNew = props.isNew or False 

274 feature.views = props.views or {} 

275 

276 for fld in self.fields: 

277 fld.from_props(feature, mc) 

278 

279 return feature 

280 

281 def feature_to_props(self, feature, mc): 

282 feature.props = gws.FeatureProps( 

283 attributes={}, 

284 cssSelector=feature.cssSelector, 

285 category=feature.category or '', 

286 errors=feature.errors or [], 

287 isNew=feature.isNew, 

288 modelUid=self.uid, 

289 uid=feature.uid(), 

290 views=feature.views, 

291 ) 

292 

293 for fld in self.fields: 

294 fld.to_props(feature, mc) 

295 

296 return feature.props 

297 

298 def feature_to_view_props(self, feature, mc): 

299 props = self.feature_to_props(feature, mc) 

300 

301 a = {} 

302 

303 if self.uidName: 

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

305 if self.geometryName: 

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

307 

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

309 props.attributes = a 

310 

311 return props 

312 

313 

314## 

315 

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

317 

318_DEFAULT_FIELD_TYPES = { 

319 gws.AttributeType.str: 'text', 

320 gws.AttributeType.int: 'integer', 

321 gws.AttributeType.date: 'date', 

322 gws.AttributeType.bool: 'bool', 

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

324 gws.AttributeType.datetime: 'datetime', 

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

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

327 gws.AttributeType.float: 'float', 

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

329 gws.AttributeType.geometry: 'geometry', 

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

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

332 gws.AttributeType.time: 'time', 

333}