Coverage for gws-app/gws/base/model/field.py: 79%
111 statements
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
« prev ^ index » next coverage.py v7.16.2, created at 2026-10-05 13:35 +0200
1"""Base model field."""
3from typing import Optional, cast
5import gws
8class Props(gws.Props):
9 attributeType: gws.AttributeType
10 geometryType: gws.GeometryType
11 name: str
12 title: str
13 type: str
14 widget: gws.ext.props.modelWidget
15 uid: str
16 relatedModelUids: list[str]
19class Config(gws.ConfigWithAccess):
20 """Configuration for the model field."""
22 name: str
23 """Field name, matching the column or attribute name in the source."""
24 title: Optional[str]
25 """Field title shown in the client."""
27 isPrimaryKey: Optional[bool]
28 """The field is a primary key."""
29 isRequired: Optional[bool]
30 """The field must not be empty."""
31 isUnique: Optional[bool]
32 """The field is unique."""
33 isAuto: Optional[bool]
34 """The value is set by the database and never written."""
35 isHidden: Optional[bool]
36 """The field is not automatically displayed in the UI."""
38 values: Optional[list[gws.ext.config.modelValue]]
39 """Value sources that compute the field value on read, create or update."""
40 validators: Optional[list[gws.ext.config.modelValidator]]
41 """Additional validators for the field value."""
43 widget: Optional[gws.ext.config.modelWidget]
44 """Widget that displays and edits the field in the client."""
47##
50class Object(gws.ModelField):
51 """Base model field.
53 Provides the field flags, value objects, validators, widget, props and validation.
54 Subclasses implement reading and writing the field value and the conversion between
55 record, feature and props values.
56 """
58 notEmptyValidator: gws.ModelValidator
59 """Validator that checks that the value is not empty."""
60 formatValidator: gws.ModelValidator
61 """Validator that checks the format of the value."""
63 def configure(self):
64 self.model = self.cfg('_defaultModel')
65 self.name = self.cfg('name')
66 self.title = self.cfg('title', default=self.name)
68 self.values = []
69 self.validators = []
70 self.widget = None
72 self.configure_flags()
73 self.configure_values()
74 self.configure_validators()
75 self.configure_widget()
77 def configure_flags(self):
78 """Set the field flags from the configuration or the source column.
80 ``isPrimaryKey``, ``isRequired`` and ``isAuto`` default to the properties of the
81 source column with the field name. ``isUnique`` and ``isHidden`` default to False.
82 """
83 col = self.describe()
85 p = self.cfg('isPrimaryKey')
86 if p is not None:
87 self.isPrimaryKey = p
88 else:
89 self.isPrimaryKey = col and col.isPrimaryKey
91 p = self.cfg('isRequired')
92 if p is not None:
93 self.isRequired = p
94 else:
95 self.isRequired = col and not col.isNullable
97 p = self.cfg('isAuto')
98 if p is not None:
99 self.isAuto = p
100 else:
101 self.isAuto = col and col.isAutoincrement
103 p = self.cfg('isUnique')
104 if p is not None:
105 self.isUnique = p
106 else:
107 self.isUnique = False
109 self.isHidden = self.cfg('isHidden', default=False)
111 def configure_values(self):
112 """Create the configured value objects.
114 Returns:
115 True if values are configured, None otherwise.
116 """
117 p = self.cfg('values')
118 if p:
119 self.values = self.create_children(gws.ext.object.modelValue, p)
120 return True
122 def configure_validators(self):
123 """Create the configured validators.
125 Configured ``notEmpty`` and ``format`` validators replace the shared default ones,
126 which apply to ``create`` and ``update``.
128 Returns:
129 Always True.
130 """
131 vd_not_empty = None
132 vd_format = None
134 for p in self.cfg('validators', default=[]):
135 vd = self.create_validator(p)
136 if vd.extType == 'notEmpty':
137 vd_not_empty = vd
138 elif vd.extType == 'format':
139 vd_format = vd
140 else:
141 self.validators.append(vd)
143 self.notEmptyValidator = vd_not_empty or self.root.create_shared(
144 gws.ext.object.modelValidator,
145 type='notEmpty',
146 uid='gws.base.model.field.default_validator_notEmpty',
147 forCreate=True,
148 forUpdate=True,
149 )
151 self.formatValidator = vd_format or self.root.create_shared(
152 gws.ext.object.modelValidator,
153 type='format',
154 uid='gws.base.model.field.default_validator_format',
155 forCreate=True,
156 forUpdate=True,
157 )
159 return True
161 def create_validator(self, cfg):
162 """Create a validator as a child of this field.
164 Args:
165 cfg: The validator configuration.
167 Returns:
168 The validator object.
169 """
170 return self.create_child(gws.ext.object.modelValidator, cfg)
172 def configure_widget(self):
173 """Create the configured widget.
175 Returns:
176 True if a widget is configured, None otherwise.
177 """
178 p = self.cfg('widget')
179 if p:
180 self.widget = self.create_child(gws.ext.object.modelWidget, p)
181 return True
183 ##
185 def props(self, user):
186 wp = None
187 if self.widget:
188 wp = gws.props_of(self.widget, user, self)
189 if not user.can_write(self):
190 wp.readOnly = True
192 return Props(
193 attributeType=self.attributeType,
194 name=self.name,
195 title=self.title,
196 type=self.extType,
197 widget=wp,
198 uid=self.uid,
199 relatedModelUids=[m.uid for m in self.related_models() if user.can_read(m)],
200 )
202 ##
204 def do_validate(self, feature, mc):
205 # apply the 'notEmpty' validator and exit immediately if it fails
206 # (no error message if field is not required)
208 ok = self.notEmptyValidator.validate(self, feature, mc)
209 if not ok:
210 if self.isRequired:
211 feature.errors.append(
212 gws.ModelValidationError(
213 fieldName=self.name,
214 message=self.notEmptyValidator.message,
215 )
216 )
217 return
219 # apply the 'format' validator
221 ok = self.formatValidator.validate(self, feature, mc)
222 if not ok:
223 feature.errors.append(
224 gws.ModelValidationError(
225 fieldName=self.name,
226 message=self.formatValidator.message,
227 )
228 )
229 return
231 # apply others
233 for vd in self.validators:
234 if mc.op in vd.ops:
235 ok = vd.validate(self, feature, mc)
236 if not ok:
237 feature.errors.append(
238 gws.ModelValidationError(
239 fieldName=self.name,
240 message=vd.message,
241 )
242 )
243 return
245 def related_models(self):
246 return []
248 def find_relatable_features(self, search, mc):
249 return []
251 def raw_to_python(self, feature, value, mc):
252 return value
254 def prop_to_python(self, feature, value, mc):
255 return value
257 def python_to_raw(self, feature, value, mc):
258 return value
260 def python_to_prop(self, feature, value, mc):
261 return value
263 ##
265 def describe(self):
266 desc = self.model.describe()
267 if desc:
268 return desc.columnMap.get(self.name)