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
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 22:59 +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 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.."""
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]
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()
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}')
95 def configure_model(self):
96 """Model configuration protocol."""
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()
106 def configure_provider(self):
107 return False
109 def configure_sources(self):
110 return False
112 def configure_fields(self):
113 has_conf = False
114 has_auto = False
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()
123 return has_conf or has_auto
125 def configure_auto_fields(self):
126 desc = self.describe()
127 if not desc:
128 return False
130 exclude = set(self.cfg('excludeColumns', default=[]))
131 exclude.update(fld.name for fld in self.fields)
133 for col in desc.columns:
134 if col.name in exclude:
135 continue
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
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)
153 return True
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
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
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
185 def configure_templates(self):
186 return gws.config.util.configure_templates_for(self)
188 ##
190 def props(self, user):
191 layer = cast(gws.Layer, self.find_closest(gws.ext.object.layer))
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 )
214 ##
216 def table_view_columns(self, user):
217 if not self.withTableView:
218 return []
220 cols = []
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))
234 return cols
236 def field(self, name):
237 for fld in self.fields:
238 if fld.name == name:
239 return fld
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
247 def related_models(self):
248 d = {}
250 for fld in self.fields:
251 for model in fld.related_models():
252 d[model.uid] = model
254 return list(d.values())
256 ##
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)
264 def find_features(self, search, mc):
265 return []
267 ##
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 {}
276 for fld in self.fields:
277 fld.from_props(feature, mc)
279 return feature
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 )
293 for fld in self.fields:
294 fld.to_props(feature, mc)
296 return feature.props
298 def feature_to_view_props(self, feature, mc):
299 props = self.feature_to_props(feature, mc)
301 a = {}
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)
308 # only provide "uid" and "geometry" attributes for the view props
309 props.attributes = a
311 return props
314##
316# @TODO this should be populated dynamically from available gws.ext.object.modelField types
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}