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

1"""Related Linked Feature List field 

2 

3Represents an M:N relationship between two models via a link table ("associative entity"):: 

4 

5 +---------+ +---------------+ +---------+ 

6 | table A | | link table | | table B | 

7 +---------+ +---------------+ +---------+ 

8 | key_a |-------<<| a b |>>-------| key_b | 

9 +---------+ +---------------+ +---------+ 

10 

11""" 

12 

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 

18 

19gws.ext.new.modelField('relatedLinkedFeatureList') 

20 

21 

22class Config(related_field.Config): 

23 """Configuration for related linked feature list field.""" 

24 

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.""" 

37 

38 

39class Props(related_field.Props): 

40 pass 

41 

42 

43class Object(related_field.Object): 

44 attributeType = gws.AttributeType.featurelist 

45 

46 def configure_relationship(self): 

47 to_mod = self.get_model(self.cfg('toModel')) 

48 link_tab = self.model.db.table(self.cfg('linkTableName')) 

49 

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] 

72 

73 ## 

74 

75 def after_select(self, features, mc): 

76 if not mc.user.can_read(self) or mc.relDepth >= mc.maxDepth: 

77 return 

78 

79 for f in features: 

80 f.set(self.name, []) 

81 

82 uid_to_f = {f.uid(): f for f in features} 

83 

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 ) 

102 

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)) 

107 

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) 

115 

116 def after_create_related(self, to_feature, mc): 

117 if to_feature.model != self.rel.to.model: 

118 return 

119 

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() 

127 

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)) 

137 

138 self.create_links(new_links, mc) 

139 

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) 

148 

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) 

152 

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 

156 

157 cur_links = self.get_links([key], mc) 

158 to_uids = set(to_feature.uid() for to_feature in feature.get(self.name, [])) 

159 

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)} 

168 

169 new_links = set() 

170 

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)) 

174 

175 self.create_links(new_links - cur_links, mc) 

176 self.delete_links(cur_links - new_links, mc) 

177 

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)) 

187 

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) 

199 

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)