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
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 12:46 +0200
1"""Base model."""
3from typing import Optional, cast
5import gws
6import gws.base.feature
7import gws.base.shape
8import gws.config.util
10DEFAULT_UID_NAME = 'uid'
11DEFAULT_GEOMETRY_NAME = 'geometry'
14class TableViewColumn(gws.Data):
15 """Table view column configuration."""
17 name: str
18 """Column name."""
19 width: Optional[int]
20 """Column width in pixels. If not set, the column will be auto-sized."""
23class ClientOptions(gws.Data):
24 """Client options for a model"""
26 keepFormOpen: bool = False
27 """Keep the edit form open after save"""
30class Config(gws.ConfigWithAccess):
31 """Model configuration"""
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.."""
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]
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()
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}')
98 def configure_model(self):
99 """Model configuration protocol."""
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()
109 def configure_provider(self):
110 return False
112 def configure_sources(self):
113 return False
115 def configure_fields(self):
116 has_conf = False
117 has_auto = False
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()
126 return has_conf or has_auto
128 def configure_auto_fields(self):
129 desc = self.describe()
130 if not desc:
131 return False
133 exclude = set(self.cfg('excludeColumns', default=[]))
134 exclude.update(fld.name for fld in self.fields)
136 for col in desc.columns:
137 if col.name in exclude:
138 continue
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
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)
156 return True
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
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
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
188 def configure_templates(self):
189 return gws.config.util.configure_templates_for(self)
191 ##
193 def props(self, user):
194 layer = cast(gws.Layer, self.find_closest(gws.ext.object.layer))
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 )
217 ##
219 def table_view_columns(self, user):
220 if not self.withTableView:
221 return []
223 cols = []
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))
237 return cols
239 def field(self, name):
240 for fld in self.fields:
241 if fld.name == name:
242 return fld
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
250 def related_models(self):
251 d = {}
253 for fld in self.fields:
254 for model in fld.related_models():
255 d[model.uid] = model
257 return list(d.values())
259 ##
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)
267 def get_feature(self, uid, mc):
268 features = self.get_features([uid], mc)
269 if features:
270 return features[0]
272 def find_features(self, search, mc):
273 return []
275 ##
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 {}
284 for fld in self.fields:
285 fld.from_props(feature, mc)
287 return feature
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 )
301 for fld in self.fields:
302 fld.to_props(feature, mc)
304 return feature.props
306 def feature_to_view_props(self, feature, mc):
307 props = self.feature_to_props(feature, mc)
309 a = {}
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)
316 # only provide "uid" and "geometry" attributes for the view props
317 props.attributes = a
319 return props
322##
324# @TODO this should be populated dynamically from available gws.ext.object.modelField types
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}