Coverage for gws-app/gws/base/model/related_field.py: 48%
120 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 class for related fields."""
3from typing import Optional, cast
5import gws
6import gws.base.model
7import gws.lib.sa as sa
9from . import field
12class Config(field.Config):
13 pass
16class Props(field.Props):
17 pass
20class Link(gws.Data):
21 """Link table of a many-to-many relationship."""
23 tableName: str
24 """The link table name."""
25 srcKeyName: str
26 """Column in the link table that refers to the source model."""
27 dstKeyName: str
28 """Column in the link table that refers to the related model."""
31class RelRef(gws.Data):
32 """One side of a relationship."""
34 model: gws.DatabaseModel
35 """The model."""
36 keyName: str
37 """The column that takes part in the relationship."""
40class Relationship(gws.Data):
41 """Relationship between the model of a field and related models."""
43 src: RelRef
44 """The side of the field's own model."""
45 dst: RelRef
46 """The related side, for relationships with a single related model."""
47 dstList: list[RelRef]
48 """The related sides."""
49 link: Link
50 """The link table, for many-to-many relationships."""
51 deleteCascade: bool = False
52 """When unlinking related features, delete them instead of clearing their key."""
55class Object(field.Object):
56 """Base related field.
58 Links features of the field's model to features of other database models.
59 Provides the relationship description, conversion of related features to and from
60 props, and database helpers to read and update the keys of related features.
61 Subclasses describe the link in `configure_relationship`.
62 """
64 model: gws.DatabaseModel
65 """The model of this field."""
66 rel: Relationship
67 """The relationship of this field."""
69 def post_configure(self):
70 self.configure_relationship()
72 def configure_relationship(self):
73 """Set up the relationship ``rel``.
75 Called after configuration. The base implementation does nothing.
76 """
77 pass
79 def key_column(self, ref: RelRef) -> sa.Column:
80 """Return the key column of a relationship side.
82 Args:
83 ref: The relationship side.
85 Returns:
86 The column ``ref.keyName`` of ``ref.model``.
87 """
88 return ref.model.column(ref.keyName)
90 def link_table(self) -> sa.Table:
91 """Return the link table of the relationship."""
92 return self.model.db.table(self.rel.link.tableName)
94 def link_column(self, name: str) -> sa.Column:
95 """Return a column of the link table.
97 Args:
98 name: The column name.
100 Returns:
101 The column.
102 """
103 return self.model.db.column(self.link_table(), name)
105 def configure_widget(self):
106 if not super().configure_widget():
107 if self.attributeType == gws.AttributeType.feature:
108 self.widget = self.root.create_shared(gws.ext.object.modelWidget, type='featureSelect')
109 return True
110 if self.attributeType == gws.AttributeType.featurelist:
111 self.widget = self.root.create_shared(gws.ext.object.modelWidget, type='featureList')
112 return True
114 def get_model(self, uid: str) -> gws.DatabaseModel:
115 """Return a model by uid.
117 Args:
118 uid: The model uid.
120 Returns:
121 The model.
123 Raises:
124 gws.ConfigurationError: If the model is not found.
125 """
126 mod = self.root.get(uid)
127 if not mod:
128 raise gws.ConfigurationError(f'model {uid!r} not found')
129 return cast(gws.DatabaseModel, mod)
131 def find_relatable_features(self, search, mc):
132 return [f for dst in self.rel.dstList for f in dst.model.find_features(search, mc)]
134 def related_field(self, dst: RelRef) -> Optional[gws.ModelField]:
135 """Find the field of a related model that describes the reverse relationship.
137 Args:
138 dst: The related side of the relationship.
140 Returns:
141 The field of ``dst.model`` whose relationship starts at ``dst.model`` and ``dst.keyName``,
142 or None if there is no such field.
143 """
144 for fld in dst.model.fields:
145 rel2 = cast(Relationship, getattr(fld, 'rel', None))
146 if not rel2:
147 continue
148 if rel2.src.model == dst.model and rel2.src.keyName == dst.keyName:
149 return fld
151 def do_init_related(self, dst_feature, mc):
152 our_features = [f for f in dst_feature.createWithFeatures if f.model == self.model]
153 if not our_features:
154 return
156 for dst in self.rel.dstList:
157 if dst.model == dst_feature.model:
158 fld = self.related_field(dst)
159 if fld:
160 if fld.attributeType == gws.AttributeType.feature:
161 dst_feature.attributes[fld.name] = our_features[0]
162 if fld.attributeType == gws.AttributeType.featurelist:
163 dst_feature.attributes.setdefault(fld.name, []).extend(our_features)
165 def related_models(self):
166 return [dst.model for dst in self.rel.dstList]
168 ##
170 def to_props(self, feature, mc):
171 if not mc.user.can_read(self) or mc.relDepth >= mc.maxDepth:
172 return
174 value = feature.get(self.name)
175 if not value:
176 return
177 if not isinstance(value, list):
178 value = [value]
180 mc2 = gws.base.model.secondary_context(mc)
181 res = []
183 for v in value:
184 related = cast(gws.Feature, v)
185 if related:
186 p = related.model.feature_to_props(related, mc2)
187 if p:
188 res.append(p)
190 if self.attributeType == gws.AttributeType.featurelist:
191 feature.props.attributes[self.name] = res
192 elif res:
193 feature.props.attributes[self.name] = res[0]
195 def from_props(self, feature, mc):
196 if mc.relDepth >= mc.maxDepth:
197 return
199 value = feature.props.attributes.get(self.name)
200 if not value:
201 return
202 if not isinstance(value, list):
203 value = [value]
205 mc2 = gws.base.model.secondary_context(mc)
206 res = []
207 dst_model_map: dict[str, gws.Model] = {dst.model.uid: dst.model for dst in self.rel.dstList}
209 for v in value:
210 rel_props = cast(gws.FeatureProps, gws.u.to_data_object(v))
211 if rel_props:
212 dst_model = dst_model_map.get(rel_props.modelUid)
213 if dst_model:
214 related = dst_model.feature_from_props(rel_props, mc2)
215 if related:
216 res.append(related)
218 if self.attributeType == gws.AttributeType.featurelist:
219 feature.set(self.name, res)
220 elif res:
221 feature.set(self.name, res[0])
223 ##
225 def key_for_uid(
226 self,
227 model: gws.DatabaseModel,
228 key_column: sa.Column,
229 uid: gws.FeatureUid,
230 mc: gws.ModelContext,
231 ):
232 """Return the key value of the feature with the given uid.
234 Args:
235 model: The model to query.
236 key_column: The key column.
237 uid: The feature uid.
238 mc: The model context.
240 Returns:
241 The key value, or None if the feature is not found.
242 """
243 sql = sa.select(key_column).where(model.uid_equals(uid))
244 with model.db.connect() as conn:
245 rs = list(conn.execute(sql))
246 return rs[0][0] if rs else None