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

1"""Related feature field. 

2 

3Represents a child->parent M:1 relationship to another model:: 

4 

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

6 | model | | toModel | 

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

8 | fromColumn |-------->| toColumn | 

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

10 

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. 

18 

19Example:: 

20 

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

30 

31import gws 

32import gws.base.model 

33import gws.base.model.related_field as related_field 

34import gws.lib.sa as sa 

35 

36 

37@gws.ext.config.modelField('relatedFeature') 

38class Config(related_field.Config): 

39 """Field referring to a feature in another model.""" 

40 

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

47 

48 

49@gws.ext.props.modelField('relatedFeature') 

50class Props(related_field.Props): 

51 pass 

52 

53 

54@gws.ext.object.modelField('relatedFeature') 

55class Object(related_field.Object): 

56 """Related feature field object.""" 

57 

58 attributeType = gws.AttributeType.feature 

59 

60 def configure_relationship(self): 

61 dst_mod = self.get_model(self.cfg('toModel')) 

62 

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] 

76 

77 ## 

78 

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] 

89 

90 def after_create_related(self, dst_feature, mc): 

91 if dst_feature.model != self.rel.dst.model: 

92 return 

93 

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) 

110 

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. 

113 

114 Args: 

115 rel: The relationship side to query. 

116 key: The key value. 

117 mc: The model context. 

118 

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

125 

126 def after_select(self, features, mc): 

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

128 return 

129 

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

131 

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 ) 

145 

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

150 

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) 

158 

159 def before_create(self, feature, mc): 

160 self.before_write(feature, mc) 

161 

162 def before_update(self, feature, mc): 

163 self.before_write(feature, mc) 

164 

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. 

167 

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. 

170 

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 

177 

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