Coverage for gws-app/gws/plugin/model_field/related_linked_feature_list/__init__.py: 97%
86 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 linked feature list field.
3Represents an M:N relationship between two models via a link table ("associative entity")::
5 +------------+ +-------------------------------+ +-----------+
6 | this model | | linkTableName | | toModel |
7 +------------+ +-------------------------------+ +-----------+
8 | fromColumn |-------<<| linkFromColumn linkToColumn |>>-------| toColumn |
9 +------------+ +-------------------------------+ +-----------+
11The value of the field is the list of linked features of the related model.
12``fromColumn`` and ``toColumn`` default to the primary keys. The link table
13must be in the same database as this model.
15When a feature is written, the rows of the link table are synchronized with
16the field value: missing links are inserted, links to features no longer in
17the list are deleted. The linked features themselves are not changed. When a
18related feature is created together with features of this model, links to
19them are inserted. Without a configured widget, the field uses a
20``featureList`` widget.
22Example::
24 fields+ {
25 name "tags"
26 type "relatedLinkedFeatureList"
27 toModel "model_tag"
28 linkTableName "edit.poi_tag"
29 linkFromColumn "poi_id"
30 linkToColumn "tag_id"
31 }
32"""
34import gws
35import gws.base.database.model
36import gws.base.model
37import gws.base.model.related_field as related_field
38import gws.lib.sa as sa
41@gws.ext.config.modelField('relatedLinkedFeatureList')
42class Config(related_field.Config):
43 """Field listing features of another model, related via a link table."""
45 fromColumn: str = ''
46 """Key column in this table, primary key by default."""
47 toModel: str
48 """UID of the related model."""
49 toColumn: str = ''
50 """Key column in the related table, primary key by default."""
51 linkTableName: str
52 """Link table name."""
53 linkFromColumn: str
54 """Column in the link table that refers to this model."""
55 linkToColumn: str
56 """Column in the link table that refers to the related model."""
59@gws.ext.props.modelField('relatedLinkedFeatureList')
60class Props(related_field.Props):
61 pass
64@gws.ext.object.modelField('relatedLinkedFeatureList')
65class Object(related_field.Object):
66 """Related linked feature list field object."""
68 attributeType = gws.AttributeType.featurelist
70 def configure_relationship(self):
71 dst_mod = self.get_model(self.cfg('toModel'))
72 link_tab = self.model.db.table(self.cfg('linkTableName'))
74 self.rel = related_field.Relationship(
75 src=related_field.RelRef(
76 model=self.model,
77 keyName=self.model.column(self.cfg('fromColumn') or self.model.uidName).name,
78 ),
79 dstList=[
80 related_field.RelRef(
81 model=dst_mod,
82 keyName=dst_mod.column(self.cfg('toColumn') or dst_mod.uidName).name,
83 )
84 ],
85 link=related_field.Link(
86 tableName=self.cfg('linkTableName'),
87 srcKeyName=self.model.db.column(link_tab, self.cfg('linkFromColumn')).name,
88 dstKeyName=self.model.db.column(link_tab, self.cfg('linkToColumn')).name,
89 ),
90 )
91 self.rel.dst = self.rel.dstList[0]
93 ##
95 def after_select(self, features, mc):
96 if not mc.user.can_read(self) or mc.relDepth >= mc.maxDepth:
97 return
99 for f in features:
100 f.set(self.name, [])
102 uid_to_f = {f.uid(): f for f in features}
104 sql = (
105 sa.select(
106 self.rel.dst.model.uid_column(),
107 self.rel.src.model.uid_column(),
108 )
109 .select_from(
110 self.rel.dst.model.table().join(
111 self.link_table(),
112 self.link_column(self.rel.link.dstKeyName) == self.key_column(self.rel.dst),
113 ).join(
114 self.rel.src.model.table(),
115 self.link_column(self.rel.link.srcKeyName) == self.key_column(self.rel.src),
116 )
117 )
118 .where(
119 self.rel.src.model.uid_equals(uid_to_f),
120 )
121 )
123 r_to_uids = {}
124 with self.model.db.connect() as conn:
125 for r, u in conn.execute(sql):
126 r_to_uids.setdefault(str(r), []).append(str(u))
128 for dst_feature in self.rel.dst.model.get_features(
129 r_to_uids,
130 gws.base.model.secondary_context(mc),
131 ):
132 for uid in r_to_uids.get(dst_feature.uid(), []):
133 feature = uid_to_f.get(uid)
134 feature.get(self.name).append(dst_feature)
136 def after_create_related(self, dst_feature, mc):
137 if dst_feature.model != self.rel.dst.model:
138 return
140 right_key = self.key_for_uid(
141 self.rel.dst.model,
142 self.key_column(self.rel.dst),
143 dst_feature.insertedPrimaryKey,
144 mc,
145 )
146 new_links = set()
148 for feature in dst_feature.createWithFeatures:
149 if feature.model == self.model:
150 key = self.key_for_uid(
151 self.rel.src.model,
152 self.key_column(self.rel.src),
153 feature.uid(),
154 mc,
155 )
156 new_links.add((key, right_key))
158 self.create_links(new_links, mc)
160 def after_create(self, feature, mc):
161 key = self.key_for_uid(
162 self.model,
163 self.key_column(self.rel.src),
164 feature.insertedPrimaryKey,
165 mc,
166 )
167 self.after_write(feature, key, mc)
169 def after_update(self, feature, mc):
170 key = self.key_for_uid(self.model, self.key_column(self.rel.src), feature.uid(), mc)
171 self.after_write(feature, key, mc)
173 def after_write(self, feature, key, mc: gws.ModelContext):
174 """Synchronize the link table with the field value of a written feature.
176 Does nothing if the user may not write the field or the maximum relation
177 depth is reached.
179 Args:
180 feature: The created or updated feature.
181 key: Value of the key column of the feature.
182 mc: The model context.
183 """
184 if not mc.user.can_write(self) or mc.relDepth >= mc.maxDepth:
185 return
187 cur_links = self.get_links([key], mc)
188 dst_uids = set(dst_feature.uid() for dst_feature in feature.get(self.name, []))
190 sql = sa.select(
191 self.rel.dst.model.uid_column(),
192 self.key_column(self.rel.dst),
193 ).where(
194 self.rel.dst.model.uid_equals(dst_uids),
195 )
196 with self.rel.dst.model.db.connect() as conn:
197 r_uid_to_key = {str(u): k for u, k in conn.execute(sql)}
199 new_links = set()
201 for dst_feature in feature.get(self.name, []):
202 right_key = r_uid_to_key.get(dst_feature.uid())
203 new_links.add((key, right_key))
205 self.create_links(new_links - cur_links, mc)
206 self.delete_links(cur_links - new_links, mc)
208 def get_links(self, left_keys, mc):
209 """Read the links of the given keys from the link table.
211 Args:
212 left_keys: Key values of features of this model.
213 mc: The model context.
215 Returns:
216 A set of ``(src_key, dst_key)`` tuples.
217 """
218 sql = sa.select(
219 self.link_column(self.rel.link.srcKeyName),
220 self.link_column(self.rel.link.dstKeyName),
221 ).where(
222 self.link_column(self.rel.link.srcKeyName).in_(left_keys),
223 )
224 with self.model.db.connect() as conn:
225 return set((lk, rk) for lk, rk in conn.execute(sql))
227 def create_links(self, links, mc):
228 """Insert links into the link table.
230 Args:
231 links: ``(src_key, dst_key)`` tuples.
232 mc: The model context.
233 """
234 sql = sa.insert(self.link_table())
235 # fmt: off
236 values = [
237 {self.rel.link.srcKeyName: lk, self.rel.link.dstKeyName: rk}
238 for lk, rk in links
239 ]
240 # fmt: on
241 if values:
242 with self.model.db.connect() as conn:
243 conn.execute(sql, values)
245 def delete_links(self, links, mc):
246 """Delete links from the link table.
248 Args:
249 links: ``(src_key, dst_key)`` tuples.
250 mc: The model context.
251 """
252 with self.model.db.connect() as conn:
253 for lk, rk in links:
254 sql = sa.delete(
255 self.link_table(),
256 ).where(
257 self.link_column(self.rel.link.srcKeyName) == lk,
258 self.link_column(self.rel.link.dstKeyName) == rk,
259 )
260 conn.execute(sql)