Coverage for /Users/davegaeddert/Developer/dropseed/plain/plain-models/plain/models/fields/reverse_related.py: 60%

153 statements  

« prev     ^ index     » next       coverage.py v7.6.9, created at 2024-12-23 11:16 -0600

1""" 

2"Rel objects" for related fields. 

3 

4"Rel objects" (for lack of a better name) carry information about the relation 

5modeled by a related field and provide some utility functions. They're stored 

6in the ``remote_field`` attribute of the field. 

7 

8They also act as reverse fields for the purposes of the Meta API because 

9they're the closest concept currently available. 

10""" 

11 

12from plain import exceptions 

13from plain.utils.functional import cached_property 

14from plain.utils.hashable import make_hashable 

15 

16from . import BLANK_CHOICE_DASH 

17from .mixins import FieldCacheMixin 

18 

19 

20class ForeignObjectRel(FieldCacheMixin): 

21 """ 

22 Used by ForeignObject to store information about the relation. 

23 

24 ``_meta.get_fields()`` returns this class to provide access to the field 

25 flags for the reverse relation. 

26 """ 

27 

28 # Field flags 

29 auto_created = True 

30 concrete = False 

31 editable = False 

32 is_relation = True 

33 

34 # Reverse relations are always nullable (Plain can't enforce that a 

35 # foreign key on the related model points to this model). 

36 null = True 

37 empty_strings_allowed = False 

38 

39 def __init__( 

40 self, 

41 field, 

42 to, 

43 related_name=None, 

44 related_query_name=None, 

45 limit_choices_to=None, 

46 parent_link=False, 

47 on_delete=None, 

48 ): 

49 self.field = field 

50 self.model = to 

51 self.related_name = related_name 

52 self.related_query_name = related_query_name 

53 self.limit_choices_to = {} if limit_choices_to is None else limit_choices_to 

54 self.parent_link = parent_link 

55 self.on_delete = on_delete 

56 

57 self.symmetrical = False 

58 self.multiple = True 

59 

60 # Some of the following cached_properties can't be initialized in 

61 # __init__ as the field doesn't have its model yet. Calling these methods 

62 # before field.contribute_to_class() has been called will result in 

63 # AttributeError 

64 @cached_property 

65 def hidden(self): 

66 return self.is_hidden() 

67 

68 @cached_property 

69 def name(self): 

70 return self.field.related_query_name() 

71 

72 @property 

73 def remote_field(self): 

74 return self.field 

75 

76 @property 

77 def target_field(self): 

78 """ 

79 When filtering against this relation, return the field on the remote 

80 model against which the filtering should happen. 

81 """ 

82 target_fields = self.path_infos[-1].target_fields 

83 if len(target_fields) > 1: 

84 raise exceptions.FieldError( 

85 "Can't use target_field for multicolumn relations." 

86 ) 

87 return target_fields[0] 

88 

89 @cached_property 

90 def related_model(self): 

91 if not self.field.model: 

92 raise AttributeError( 

93 "This property can't be accessed before self.field.contribute_to_class " 

94 "has been called." 

95 ) 

96 return self.field.model 

97 

98 @cached_property 

99 def many_to_many(self): 

100 return self.field.many_to_many 

101 

102 @cached_property 

103 def many_to_one(self): 

104 return self.field.one_to_many 

105 

106 @cached_property 

107 def one_to_many(self): 

108 return self.field.many_to_one 

109 

110 @cached_property 

111 def one_to_one(self): 

112 return self.field.one_to_one 

113 

114 def get_lookup(self, lookup_name): 

115 return self.field.get_lookup(lookup_name) 

116 

117 def get_internal_type(self): 

118 return self.field.get_internal_type() 

119 

120 @property 

121 def db_type(self): 

122 return self.field.db_type 

123 

124 def __repr__(self): 

125 return f"<{type(self).__name__}: {self.related_model._meta.package_label}.{self.related_model._meta.model_name}>" 

126 

127 @property 

128 def identity(self): 

129 return ( 

130 self.field, 

131 self.model, 

132 self.related_name, 

133 self.related_query_name, 

134 make_hashable(self.limit_choices_to), 

135 self.parent_link, 

136 self.on_delete, 

137 self.symmetrical, 

138 self.multiple, 

139 ) 

140 

141 def __eq__(self, other): 

142 if not isinstance(other, self.__class__): 

143 return NotImplemented 

144 return self.identity == other.identity 

145 

146 def __hash__(self): 

147 return hash(self.identity) 

148 

149 def __getstate__(self): 

150 state = self.__dict__.copy() 

151 # Delete the path_infos cached property because it can be recalculated 

152 # at first invocation after deserialization. The attribute must be 

153 # removed because subclasses like ManyToOneRel may have a PathInfo 

154 # which contains an intermediate M2M table that's been dynamically 

155 # created and doesn't exist in the .models module. 

156 # This is a reverse relation, so there is no reverse_path_infos to 

157 # delete. 

158 state.pop("path_infos", None) 

159 return state 

160 

161 def get_choices( 

162 self, 

163 include_blank=True, 

164 blank_choice=BLANK_CHOICE_DASH, 

165 limit_choices_to=None, 

166 ordering=(), 

167 ): 

168 """ 

169 Return choices with a default blank choices included, for use 

170 as <select> choices for this field. 

171 

172 Analog of plain.models.fields.Field.get_choices(), provided 

173 initially for utilization by RelatedFieldListFilter. 

174 """ 

175 limit_choices_to = limit_choices_to or self.limit_choices_to 

176 qs = self.related_model._default_manager.complex_filter(limit_choices_to) 

177 if ordering: 

178 qs = qs.order_by(*ordering) 

179 return (blank_choice if include_blank else []) + [(x.pk, str(x)) for x in qs] 

180 

181 def is_hidden(self): 

182 """Should the related object be hidden?""" 

183 return bool(self.related_name) and self.related_name[-1] == "+" 

184 

185 def get_joining_columns(self): 

186 return self.field.get_reverse_joining_columns() 

187 

188 def get_extra_restriction(self, alias, related_alias): 

189 return self.field.get_extra_restriction(related_alias, alias) 

190 

191 def set_field_name(self): 

192 """ 

193 Set the related field's name, this is not available until later stages 

194 of app loading, so set_field_name is called from 

195 set_attributes_from_rel() 

196 """ 

197 # By default foreign object doesn't relate to any remote field (for 

198 # example custom multicolumn joins currently have no remote field). 

199 self.field_name = None 

200 

201 def get_accessor_name(self, model=None): 

202 # This method encapsulates the logic that decides what name to give an 

203 # accessor descriptor that retrieves related many-to-one or 

204 # many-to-many objects. It uses the lowercased object_name + "_set", 

205 # but this can be overridden with the "related_name" option. Due to 

206 # backwards compatibility ModelForms need to be able to provide an 

207 # alternate model. See BaseInlineFormSet.get_default_prefix(). 

208 opts = model._meta if model else self.related_model._meta 

209 model = model or self.related_model 

210 if self.multiple: 

211 # If this is a symmetrical m2m relation on self, there is no 

212 # reverse accessor. 

213 if self.symmetrical and model == self.model: 

214 return None 

215 if self.related_name: 

216 return self.related_name 

217 return opts.model_name + ("_set" if self.multiple else "") 

218 

219 def get_path_info(self, filtered_relation=None): 

220 if filtered_relation: 

221 return self.field.get_reverse_path_info(filtered_relation) 

222 else: 

223 return self.field.reverse_path_infos 

224 

225 @cached_property 

226 def path_infos(self): 

227 return self.get_path_info() 

228 

229 def get_cache_name(self): 

230 """ 

231 Return the name of the cache key to use for storing an instance of the 

232 forward model on the reverse model. 

233 """ 

234 return self.get_accessor_name() 

235 

236 

237class ManyToOneRel(ForeignObjectRel): 

238 """ 

239 Used by the ForeignKey field to store information about the relation. 

240 

241 ``_meta.get_fields()`` returns this class to provide access to the field 

242 flags for the reverse relation. 

243 

244 Note: Because we somewhat abuse the Rel objects by using them as reverse 

245 fields we get the funny situation where 

246 ``ManyToOneRel.many_to_one == False`` and 

247 ``ManyToOneRel.one_to_many == True``. This is unfortunate but the actual 

248 ManyToOneRel class is a private API and there is work underway to turn 

249 reverse relations into actual fields. 

250 """ 

251 

252 def __init__( 

253 self, 

254 field, 

255 to, 

256 field_name, 

257 related_name=None, 

258 related_query_name=None, 

259 limit_choices_to=None, 

260 parent_link=False, 

261 on_delete=None, 

262 ): 

263 super().__init__( 

264 field, 

265 to, 

266 related_name=related_name, 

267 related_query_name=related_query_name, 

268 limit_choices_to=limit_choices_to, 

269 parent_link=parent_link, 

270 on_delete=on_delete, 

271 ) 

272 

273 self.field_name = field_name 

274 

275 def __getstate__(self): 

276 state = super().__getstate__() 

277 state.pop("related_model", None) 

278 return state 

279 

280 @property 

281 def identity(self): 

282 return super().identity + (self.field_name,) 

283 

284 def get_related_field(self): 

285 """ 

286 Return the Field in the 'to' object to which this relationship is tied. 

287 """ 

288 field = self.model._meta.get_field(self.field_name) 

289 if not field.concrete: 

290 raise exceptions.FieldDoesNotExist( 

291 f"No related field named '{self.field_name}'" 

292 ) 

293 return field 

294 

295 def set_field_name(self): 

296 self.field_name = self.field_name or self.model._meta.pk.name 

297 

298 

299class OneToOneRel(ManyToOneRel): 

300 """ 

301 Used by OneToOneField to store information about the relation. 

302 

303 ``_meta.get_fields()`` returns this class to provide access to the field 

304 flags for the reverse relation. 

305 """ 

306 

307 def __init__( 

308 self, 

309 field, 

310 to, 

311 field_name, 

312 related_name=None, 

313 related_query_name=None, 

314 limit_choices_to=None, 

315 parent_link=False, 

316 on_delete=None, 

317 ): 

318 super().__init__( 

319 field, 

320 to, 

321 field_name, 

322 related_name=related_name, 

323 related_query_name=related_query_name, 

324 limit_choices_to=limit_choices_to, 

325 parent_link=parent_link, 

326 on_delete=on_delete, 

327 ) 

328 

329 self.multiple = False 

330 

331 

332class ManyToManyRel(ForeignObjectRel): 

333 """ 

334 Used by ManyToManyField to store information about the relation. 

335 

336 ``_meta.get_fields()`` returns this class to provide access to the field 

337 flags for the reverse relation. 

338 """ 

339 

340 def __init__( 

341 self, 

342 field, 

343 to, 

344 related_name=None, 

345 related_query_name=None, 

346 limit_choices_to=None, 

347 symmetrical=True, 

348 through=None, 

349 through_fields=None, 

350 db_constraint=True, 

351 ): 

352 super().__init__( 

353 field, 

354 to, 

355 related_name=related_name, 

356 related_query_name=related_query_name, 

357 limit_choices_to=limit_choices_to, 

358 ) 

359 

360 if through and not db_constraint: 

361 raise ValueError("Can't supply a through model and db_constraint=False") 

362 self.through = through 

363 

364 if through_fields and not through: 

365 raise ValueError("Cannot specify through_fields without a through model") 

366 self.through_fields = through_fields 

367 

368 self.symmetrical = symmetrical 

369 self.db_constraint = db_constraint 

370 

371 @property 

372 def identity(self): 

373 return super().identity + ( 

374 self.through, 

375 make_hashable(self.through_fields), 

376 self.db_constraint, 

377 ) 

378 

379 def get_related_field(self): 

380 """ 

381 Return the field in the 'to' object to which this relationship is tied. 

382 Provided for symmetry with ManyToOneRel. 

383 """ 

384 opts = self.through._meta 

385 if self.through_fields: 

386 field = opts.get_field(self.through_fields[0]) 

387 else: 

388 for field in opts.fields: 

389 rel = getattr(field, "remote_field", None) 

390 if rel and rel.model == self.model: 

391 break 

392 return field.foreign_related_fields[0]