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

204 statements  

« prev     ^ index     » next       coverage.py v7.8.0, created at 2025-04-17 01:37 +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 name: str 

16 width: Optional[int] 

17 

18 

19class ClientOptions(gws.Data): 

20 """Client options for a model""" 

21 

22 keepFormOpen: bool = False 

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

24 

25 

26class Config(gws.ConfigWithAccess): 

27 """Model configuration""" 

28 

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

30 """model fields""" 

31 loadingStrategy: Optional[gws.FeatureLoadingStrategy] 

32 """loading strategy for features""" 

33 title: str = '' 

34 """model title""" 

35 isEditable: bool = False 

36 """this model is editable""" 

37 withAutoFields: bool = False 

38 """autoload non-configured model fields from the source""" 

39 excludeColumns: Optional[list[str]] 

40 """exclude columns names from autoload""" 

41 withTableView: bool = True 

42 """enable table view for this model""" 

43 tableViewColumns: Optional[list[TableViewColumn]] 

44 """fields to include in the table view""" 

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

46 """feature templates""" 

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

48 """default sorting""" 

49 clientOptions: Optional[ClientOptions] 

50 """Client options for a model. (added in 8.1)""" 

51 

52 

53class Props(gws.Props): 

54 clientOptions: gws.ModelClientOptions 

55 canCreate: bool 

56 canDelete: bool 

57 canRead: bool 

58 canWrite: bool 

59 isEditable: bool 

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

61 geometryCrs: Optional[str] 

62 geometryName: Optional[str] 

63 geometryType: Optional[gws.GeometryType] 

64 layerUid: Optional[str] 

65 loadingStrategy: gws.FeatureLoadingStrategy 

66 supportsGeometrySearch: bool 

67 supportsKeywordSearch: bool 

68 tableViewColumns: list[TableViewColumn] 

69 title: str 

70 uid: str 

71 uidName: Optional[str] 

72 

73 

74class Object(gws.Model): 

75 def configure(self): 

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

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

78 self.fields = [] 

79 self.geometryCrs = None 

80 self.geometryName = '' 

81 self.geometryType = None 

82 self.uidName = '' 

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

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

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

86 

87 def post_configure(self): 

88 if self.isEditable and not self.uidName: 

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

90 

91 def configure_model(self): 

92 """Model configuration protocol.""" 

93 

94 self.configure_provider() 

95 self.configure_sources() 

96 self.configure_fields() 

97 self.configure_uid() 

98 self.configure_geometry() 

99 self.configure_sort() 

100 self.configure_templates() 

101 

102 def configure_provider(self): 

103 return False 

104 

105 def configure_sources(self): 

106 return False 

107 

108 def configure_fields(self): 

109 has_conf = False 

110 has_auto = False 

111 

112 p = self.cfg('fields') 

113 if p: 

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

115 has_conf = True 

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

117 has_auto = self.configure_auto_fields() 

118 

119 return has_conf or has_auto 

120 

121 def configure_auto_fields(self): 

122 desc = self.describe() 

123 if not desc: 

124 return False 

125 

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

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

128 

129 for col in desc.columns: 

130 if col.name in exclude: 

131 continue 

132 

133 typ = _DEFAULT_FIELD_TYPES.get(col.type) 

134 if not typ: 

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

136 continue 

137 

138 cfg = gws.Config( 

139 type=typ, 

140 name=col.name, 

141 isPrimaryKey=col.isPrimaryKey, 

142 isRequired=not col.isNullable, 

143 ) 

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

145 if fld: 

146 self.fields.append(fld) 

147 exclude.add(fld.name) 

148 

149 return True 

150 

151 def configure_uid(self): 

152 if self.uidName: 

153 return True 

154 uids = [] 

155 for fld in self.fields: 

156 if fld.isPrimaryKey: 

157 uids.append(fld.name) 

158 if len(uids) == 1: 

159 self.uidName = uids[0] 

160 return True 

161 

162 def configure_geometry(self): 

163 for fld in self.fields: 

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

165 self.geometryName = fld.name 

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

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

168 return True 

169 

170 def configure_sort(self): 

171 p = self.cfg('sort') 

172 if p: 

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

174 return True 

175 if self.uidName: 

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

177 return False 

178 self.defaultSort = [] 

179 return False 

180 

181 def configure_templates(self): 

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

183 

184 ## 

185 

186 def props(self, user): 

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

188 

189 return gws.Props( 

190 clientOptions=self.clientOptions, 

191 canCreate=user.can_create(self), 

192 canDelete=user.can_delete(self), 

193 canRead=user.can_read(self), 

194 canWrite=user.can_write(self), 

195 fields=self.fields, 

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

197 geometryName=self.geometryName, 

198 geometryType=self.geometryType, 

199 isEditable=self.isEditable, 

200 layerUid=layer.uid if layer else None, 

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

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

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

204 tableViewColumns=self.table_view_columns(user), 

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

206 uid=self.uid, 

207 uidName=self.uidName, 

208 ) 

209 

210 ## 

211 

212 def table_view_columns(self, user): 

213 if not self.withTableView: 

214 return [] 

215 

216 cols = [] 

217 

218 p = self.cfg('tableViewColumns') 

219 if p: 

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

221 for c in p: 

222 fld = fmap.get(c.name) 

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

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

225 else: 

226 for fld in self.fields: 

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

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

229 

230 return cols 

231 

232 def field(self, name): 

233 for fld in self.fields: 

234 if fld.name == name: 

235 return fld 

236 

237 def validate_feature(self, feature, mc): 

238 feature.errors = [] 

239 for fld in self.fields: 

240 fld.do_validate(feature, mc) 

241 return len(feature.errors) == 0 

242 

243 def related_models(self): 

244 d = {} 

245 

246 for fld in self.fields: 

247 for model in fld.related_models(): 

248 d[model.uid] = model 

249 

250 return list(d.values()) 

251 

252 ## 

253 

254 def get_features(self, uids, mc): 

255 if not uids: 

256 return [] 

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

258 return self.find_features(search, mc) 

259 

260 def find_features(self, search, mc): 

261 return [] 

262 

263 ## 

264 

265 def feature_from_props(self, props, mc): 

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

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

268 feature.cssSelector = props.cssSelector or '' 

269 feature.isNew = props.isNew or False 

270 feature.views = props.views or {} 

271 

272 for fld in self.fields: 

273 fld.from_props(feature, mc) 

274 

275 return feature 

276 

277 def feature_to_props(self, feature, mc): 

278 feature.props = gws.FeatureProps( 

279 attributes={}, 

280 cssSelector=feature.cssSelector, 

281 errors=feature.errors or [], 

282 isNew=feature.isNew, 

283 modelUid=self.uid, 

284 uid=feature.uid(), 

285 views=feature.views, 

286 ) 

287 

288 for fld in self.fields: 

289 fld.to_props(feature, mc) 

290 

291 return feature.props 

292 

293 def feature_to_view_props(self, feature, mc): 

294 props = self.feature_to_props(feature, mc) 

295 

296 a = {} 

297 

298 if self.uidName: 

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

300 if self.geometryName: 

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

302 

303 props.attributes = a 

304 props.modelUid = '' 

305 

306 return props 

307 

308 

309## 

310 

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

312 

313_DEFAULT_FIELD_TYPES = { 

314 gws.AttributeType.str: 'text', 

315 gws.AttributeType.int: 'integer', 

316 gws.AttributeType.date: 'date', 

317 gws.AttributeType.bool: 'bool', 

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

319 gws.AttributeType.datetime: 'datetime', 

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

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

322 gws.AttributeType.float: 'float', 

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

324 gws.AttributeType.geometry: 'geometry', 

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

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

327 gws.AttributeType.time: 'time', 

328}