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
« prev ^ index » next coverage.py v7.8.0, created at 2025-04-17 01:37 +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 name: str
16 width: Optional[int]
19class ClientOptions(gws.Data):
20 """Client options for a model"""
22 keepFormOpen: bool = False
23 """Keep the edit form open after save"""
26class Config(gws.ConfigWithAccess):
27 """Model configuration"""
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)"""
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]
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()
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}')
91 def configure_model(self):
92 """Model configuration protocol."""
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()
102 def configure_provider(self):
103 return False
105 def configure_sources(self):
106 return False
108 def configure_fields(self):
109 has_conf = False
110 has_auto = False
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()
119 return has_conf or has_auto
121 def configure_auto_fields(self):
122 desc = self.describe()
123 if not desc:
124 return False
126 exclude = set(self.cfg('excludeColumns', default=[]))
127 exclude.update(fld.name for fld in self.fields)
129 for col in desc.columns:
130 if col.name in exclude:
131 continue
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
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)
149 return True
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
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
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
181 def configure_templates(self):
182 return gws.config.util.configure_templates_for(self)
184 ##
186 def props(self, user):
187 layer = cast(gws.Layer, self.find_closest(gws.ext.object.layer))
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 )
210 ##
212 def table_view_columns(self, user):
213 if not self.withTableView:
214 return []
216 cols = []
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))
230 return cols
232 def field(self, name):
233 for fld in self.fields:
234 if fld.name == name:
235 return fld
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
243 def related_models(self):
244 d = {}
246 for fld in self.fields:
247 for model in fld.related_models():
248 d[model.uid] = model
250 return list(d.values())
252 ##
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)
260 def find_features(self, search, mc):
261 return []
263 ##
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 {}
272 for fld in self.fields:
273 fld.from_props(feature, mc)
275 return feature
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 )
288 for fld in self.fields:
289 fld.to_props(feature, mc)
291 return feature.props
293 def feature_to_view_props(self, feature, mc):
294 props = self.feature_to_props(feature, mc)
296 a = {}
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)
303 props.attributes = a
304 props.modelUid = ''
306 return props
309##
311# @TODO this should be populated dynamically from available gws.ext.object.modelField types
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}