Coverage for gws-app/gws/plugin/model_field/related_linked_feature_list/__init__.py: 97%
88 statements
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +0200
« prev ^ index » next coverage.py v7.11.0, created at 2025-10-16 23:09 +0200
1"""Related Linked Feature List field
3Represents an M:N relationship between two models via a link table ("associative entity")::
5 +---------+ +---------------+ +---------+
6 | table A | | link table | | table B |
7 +---------+ +---------------+ +---------+
8 | key_a |-------<<| a b |>>-------| key_b |
9 +---------+ +---------------+ +---------+
11"""
13import gws
14import gws.base.database.model
15import gws.base.model
16import gws.base.model.related_field as related_field
17import gws.lib.sa as sa
19gws.ext.new.modelField('relatedLinkedFeatureList')
22class Config(related_field.Config):
23 """Configuration for related linked feature list field."""
25 fromColumn: str = ''
26 """Key column in this table, primary key by default."""
27 toModel: str
28 """Related model."""
29 toColumn: str = ''
30 """Key column in the related table, primary key by default."""
31 linkTableName: str
32 """Link table name."""
33 linkFromColumn: str
34 """Link key column for this model."""
35 linkToColumn: str
36 """Link key column for the related model."""
39class Props(related_field.Props):
40 pass
43class Object(related_field.Object):
44 attributeType = gws.AttributeType.featurelist
46 def configure_relationship(self):
47 to_mod = self.get_model(self.cfg('toModel'))
48 link_tab = self.model.db.table(self.cfg('linkTableName'))
50 self.rel = related_field.Relationship(
51 src=related_field.RelRef(
52 model=self.model,
53 table=self.model.table(),
54 key=self.column_or_uid(self.model, self.cfg('fromColumn')),
55 uid=self.model.uid_column(),
56 ),
57 tos=[
58 related_field.RelRef(
59 model=to_mod,
60 table=to_mod.table(),
61 key=self.column_or_uid(to_mod, self.cfg('toColumn')),
62 uid=to_mod.uid_column(),
63 )
64 ],
65 link=related_field.Link(
66 table=link_tab,
67 fromKey=self.model.db.column(link_tab, self.cfg('linkFromColumn')),
68 toKey=self.model.db.column(link_tab, self.cfg('linkToColumn')),
69 ),
70 )
71 self.rel.to = self.rel.tos[0]
73 ##
75 def after_select(self, features, mc):
76 if not mc.user.can_read(self) or mc.relDepth >= mc.maxDepth:
77 return
79 for f in features:
80 f.set(self.name, [])
82 uid_to_f = {f.uid(): f for f in features}
84 sql = (
85 sa.select(
86 self.rel.to.uid,
87 self.rel.src.uid,
88 )
89 .select_from(
90 self.rel.to.table.join(
91 self.rel.link.table,
92 self.rel.link.toKey.__eq__(self.rel.to.key),
93 ).join(
94 self.rel.src.table,
95 self.rel.link.fromKey.__eq__(self.rel.src.key),
96 )
97 )
98 .where(
99 self.rel.src.uid.in_(uid_to_f),
100 )
101 )
103 r_to_uids = {}
104 with self.model.db.connect() as conn:
105 for r, u in conn.execute(sql):
106 r_to_uids.setdefault(str(r), []).append(str(u))
108 for to_feature in self.rel.to.model.get_features(
109 r_to_uids,
110 gws.base.model.secondary_context(mc),
111 ):
112 for uid in r_to_uids.get(to_feature.uid(), []):
113 feature = uid_to_f.get(uid)
114 feature.get(self.name).append(to_feature)
116 def after_create_related(self, to_feature, mc):
117 if to_feature.model != self.rel.to.model:
118 return
120 right_key = self.key_for_uid(
121 self.rel.to.model,
122 self.rel.to.key,
123 to_feature.insertedPrimaryKey,
124 mc,
125 )
126 new_links = set()
128 for feature in to_feature.createWithFeatures:
129 if feature.model == self.model:
130 key = self.key_for_uid(
131 self.rel.src.model,
132 self.rel.src.key,
133 feature.uid(),
134 mc,
135 )
136 new_links.add((key, right_key))
138 self.create_links(new_links, mc)
140 def after_create(self, feature, mc):
141 key = self.key_for_uid(
142 self.model,
143 self.rel.src.key,
144 feature.insertedPrimaryKey,
145 mc,
146 )
147 self.after_write(feature, key, mc)
149 def after_update(self, feature, mc):
150 key = self.key_for_uid(self.model, self.rel.src.key, feature.uid(), mc)
151 self.after_write(feature, key, mc)
153 def after_write(self, feature, key, mc: gws.ModelContext):
154 if not mc.user.can_write(self) or mc.relDepth >= mc.maxDepth:
155 return
157 cur_links = self.get_links([key], mc)
158 to_uids = set(to_feature.uid() for to_feature in feature.get(self.name, []))
160 sql = sa.select(
161 self.rel.to.uid,
162 self.rel.to.key,
163 ).where(
164 self.rel.to.uid.in_(to_uids),
165 )
166 with self.rel.to.model.db.connect() as conn:
167 r_uid_to_key = {str(u): k for u, k in conn.execute(sql)}
169 new_links = set()
171 for to_feature in feature.get(self.name, []):
172 right_key = r_uid_to_key.get(to_feature.uid())
173 new_links.add((key, right_key))
175 self.create_links(new_links - cur_links, mc)
176 self.delete_links(cur_links - new_links, mc)
178 def get_links(self, left_keys, mc):
179 sql = sa.select(
180 self.rel.link.fromKey,
181 self.rel.link.toKey,
182 ).where(
183 self.rel.link.fromKey.in_(left_keys),
184 )
185 with self.model.db.connect() as conn:
186 return set((lk, rk) for lk, rk in conn.execute(sql))
188 def create_links(self, links, mc):
189 sql = sa.insert(self.rel.link.table)
190 # fmt: off
191 values = [
192 {self.rel.link.fromKey.name: lk, self.rel.link.toKey.name: rk}
193 for lk, rk in links
194 ]
195 # fmt: on
196 if values:
197 with self.model.db.connect() as conn:
198 conn.execute(sql, values)
200 def delete_links(self, links, mc):
201 with self.model.db.connect() as conn:
202 for lk, rk in links:
203 sql = sa.delete(
204 self.rel.link.table,
205 ).where(
206 self.rel.link.fromKey.__eq__(lk),
207 self.rel.link.toKey.__eq__(rk),
208 )
209 conn.execute(sql)