Coverage for gws-app/gws/plugin/model_field/related_feature/__init__.py: 84%
67 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"""Related feature field.
3Represents a child->parent M:1 relationship to another model::
5 +-------------+ +--------------+
6 | model | | toModel |
7 +-------------+ +--------------+
8 | fromColumn |-------->| toColumn |
9 +-------------+ +--------------+
11The value of the field is the parent feature. ``fromColumn`` is the foreign
12key column in this model's table, ``toColumn`` the key column in the related
13model, by default its primary key. When a feature is written, the key of the
14selected parent feature is stored in ``fromColumn``. When a parent feature is
15created together with a child feature, the child's ``fromColumn`` is set to
16the new parent's key. Without a configured widget, the field uses a
17``featureSelect`` widget.
19Example::
21 fields+ {
22 name "category"
23 type "relatedFeature"
24 fromColumn "category_id"
25 toModel "model_category"
26 toColumn "id"
27 widget.type "featureSelect"
28 }
29"""
31import gws
32import gws.base.model
33import gws.base.model.related_field as related_field
34import gws.lib.sa as sa
37@gws.ext.config.modelField('relatedFeature')
38class Config(related_field.Config):
39 """Field referring to a feature in another model."""
41 fromColumn: str
42 """Foreign key column in this table."""
43 toModel: str
44 """UID of the related model."""
45 toColumn: str = ''
46 """Key column in the related model, primary key by default."""
49@gws.ext.props.modelField('relatedFeature')
50class Props(related_field.Props):
51 pass
54@gws.ext.object.modelField('relatedFeature')
55class Object(related_field.Object):
56 """Related feature field object."""
58 attributeType = gws.AttributeType.feature
60 def configure_relationship(self):
61 dst_mod = self.get_model(self.cfg('toModel'))
63 self.rel = related_field.Relationship(
64 src=related_field.RelRef(
65 model=self.model,
66 keyName=self.model.column(self.cfg('fromColumn')).name,
67 ),
68 dstList=[
69 related_field.RelRef(
70 model=dst_mod,
71 keyName=dst_mod.column(self.cfg('toColumn') or dst_mod.uidName).name,
72 )
73 ],
74 )
75 self.rel.dst = self.rel.dstList[0]
77 ##
79 def do_init(self, feature, mc):
80 key = feature.record.attributes.get(self.rel.src.keyName)
81 if key:
82 dst_uids = self.uids_for_key(self.rel.dst, key, mc)
83 dst_features = self.rel.dst.model.get_features(
84 dst_uids,
85 gws.base.model.secondary_context(mc),
86 )
87 if dst_features:
88 feature.attributes[self.name] = dst_features[0]
90 def after_create_related(self, dst_feature, mc):
91 if dst_feature.model != self.rel.dst.model:
92 return
94 for feature in dst_feature.createWithFeatures:
95 if feature.model == self.model:
96 key = self.key_for_uid(
97 self.rel.dst.model,
98 self.key_column(self.rel.dst),
99 dst_feature.insertedPrimaryKey,
100 mc,
101 )
102 if key:
103 sql = (
104 sa.update(self.model.table())
105 .values({self.rel.src.keyName: key})
106 .where(self.model.uid_equals(feature.uid()))
107 )
108 with self.model.db.connect() as conn:
109 conn.execute(sql)
111 def uids_for_key(self, rel: related_field.RelRef, key, mc):
112 """Find the uids of the features whose key column has the given value.
114 Args:
115 rel: The relationship side to query.
116 key: The key value.
117 mc: The model context.
119 Returns:
120 A set of feature uids as strings.
121 """
122 sql = sa.select(rel.model.uid_column()).where(self.key_column(rel) == key)
123 with rel.model.db.connect() as conn:
124 return set(str(u) for u in conn.execute(sql))
126 def after_select(self, features, mc):
127 if not mc.user.can_read(self) or mc.relDepth >= mc.maxDepth:
128 return
130 uid_to_f = {f.uid(): f for f in features}
132 sql = (
133 sa.select(
134 self.rel.dst.model.uid_column(),
135 self.rel.src.model.uid_column(),
136 )
137 .select_from(
138 self.rel.dst.model.table().join(
139 self.rel.src.model.table(),
140 self.key_column(self.rel.src) == self.key_column(self.rel.dst),
141 ),
142 )
143 .where(self.rel.src.model.uid_equals(uid_to_f))
144 )
146 r_to_uids = {}
147 with self.model.db.connect() as conn:
148 for r, u in conn.execute(sql):
149 r_to_uids.setdefault(str(r), []).append(str(u))
151 for dst_feature in self.rel.dst.model.get_features(
152 r_to_uids,
153 gws.base.model.secondary_context(mc),
154 ):
155 for uid in r_to_uids.get(dst_feature.uid(), []):
156 feature = uid_to_f.get(uid)
157 feature.set(self.name, dst_feature)
159 def before_create(self, feature, mc):
160 self.before_write(feature, mc)
162 def before_update(self, feature, mc):
163 self.before_write(feature, mc)
165 def before_write(self, feature: gws.Feature, mc: gws.ModelContext):
166 """Write the key of the related feature to the foreign key column of the record.
168 Does nothing if the user may not write the field or the feature has no value
169 for it. An empty value clears the foreign key.
171 Args:
172 feature: The feature being created or updated.
173 mc: The model context.
174 """
175 if not mc.user.can_write(self):
176 return
178 if feature.has(self.name):
179 key = None
180 dst_feature = feature.get(self.name)
181 if dst_feature:
182 key = self.key_for_uid(
183 self.rel.dst.model,
184 self.key_column(self.rel.dst),
185 dst_feature.uid(),
186 mc,
187 )
188 feature.record.attributes[self.rel.src.keyName] = key