Coverage for /Users/davegaeddert/Development/dropseed/plain/plain-models/plain/models/fields/reverse_related.py: 61%

1"""

2"Rel objects" for related fields.

3

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

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

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

7

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

9 they're the closest concept currently available.

10"""

11

12 from plain import exceptions

13 from plain.utils.functional import cached_property

14 from plain.utils.hashable import make_hashable

15

16 from . import BLANK_CHOICE_DASH

17 from .mixins import FieldCacheMixin

18

19

20 class 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 "<{}: {}.{}>".format(

126 type(self).__name__,

127 self.related_model._meta.package_label,

128 self.related_model._meta.model_name,

129 )

130

131 @property

132 def identity(self):

133 return (

134 self.field,

135 self.model,

136 self.related_name,

137 self.related_query_name,

138 make_hashable(self.limit_choices_to),

139 self.parent_link,

140 self.on_delete,

141 self.symmetrical,

142 self.multiple,

143 )

144

145 def __eq__(self, other):

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

147 return NotImplemented

148 return self.identity == other.identity

149

150 def __hash__(self):

151 return hash(self.identity)

152

153 def __getstate__(self):

154 state = self.__dict__.copy()

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

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

157 # removed because subclasses like ManyToOneRel may have a PathInfo

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

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

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

161 # delete.

162 state.pop("path_infos", None)

163 return state

164

165 def get_choices(

166 self,

167 include_blank=True,

168 blank_choice=BLANK_CHOICE_DASH,

169 limit_choices_to=None,

170 ordering=(),

171 ):

172 """

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

174 as <select> choices for this field.

175

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

177 initially for utilization by RelatedFieldListFilter.

178 """

179 limit_choices_to = limit_choices_to or self.limit_choices_to

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

181 if ordering:

182 qs = qs.order_by(*ordering)

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

184

185 def is_hidden(self):

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

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

188

189 def get_joining_columns(self):

190 return self.field.get_reverse_joining_columns()

191

192 def get_extra_restriction(self, alias, related_alias):

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

194

195 def set_field_name(self):

196 """

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

198 of app loading, so set_field_name is called from

199 set_attributes_from_rel()

200 """

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

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

203 self.field_name = None

204

205 def get_accessor_name(self, model=None):

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

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

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

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

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

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

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

213 model = model or self.related_model

214 if self.multiple:

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

216 # reverse accessor.

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

218 return None

219 if self.related_name:

220 return self.related_name

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

222

223 def get_path_info(self, filtered_relation=None):

224 if filtered_relation:

225 return self.field.get_reverse_path_info(filtered_relation)

226 else:

227 return self.field.reverse_path_infos

228

229 @cached_property

230 def path_infos(self):

231 return self.get_path_info()

232

233 def get_cache_name(self):

234 """

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

236 forward model on the reverse model.

237 """

238 return self.get_accessor_name()

239

240

241 class ManyToOneRel(ForeignObjectRel):

242 """

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

244

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

246 flags for the reverse relation.

247

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

249 fields we get the funny situation where

250 ``ManyToOneRel.many_to_one == False`` and

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

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

253 reverse relations into actual fields.

254 """

255

256 def __init__(

257 self,

258 field,

259 to,

260 field_name,

261 related_name=None,

262 related_query_name=None,

263 limit_choices_to=None,

264 parent_link=False,

265 on_delete=None,

266 ):

267 super().__init__(

268 field,

269 to,

270 related_name=related_name,

271 related_query_name=related_query_name,

272 limit_choices_to=limit_choices_to,

273 parent_link=parent_link,

274 on_delete=on_delete,

275 )

276

277 self.field_name = field_name

278

279 def __getstate__(self):

280 state = super().__getstate__()

281 state.pop("related_model", None)

282 return state

283

284 @property

285 def identity(self):

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

287

288 def get_related_field(self):

289 """

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

291 """

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

293 if not field.concrete:

294 raise exceptions.FieldDoesNotExist(

295 "No related field named '%s'" % self.field_name

296 )

297 return field

298

299 def set_field_name(self):

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

301

302

303 class OneToOneRel(ManyToOneRel):

304 """

305 Used by OneToOneField to store information about the relation.

306

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

308 flags for the reverse relation.

309 """

310

311 def __init__(

312 self,

313 field,

314 to,

315 field_name,

316 related_name=None,

317 related_query_name=None,

318 limit_choices_to=None,

319 parent_link=False,

320 on_delete=None,

321 ):

322 super().__init__(

323 field,

324 to,

325 field_name,

326 related_name=related_name,

327 related_query_name=related_query_name,

328 limit_choices_to=limit_choices_to,

329 parent_link=parent_link,

330 on_delete=on_delete,

331 )

332

333 self.multiple = False

334

335

336 class ManyToManyRel(ForeignObjectRel):

337 """

338 Used by ManyToManyField to store information about the relation.

339

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

341 flags for the reverse relation.

342 """

343

344 def __init__(

345 self,

346 field,

347 to,

348 related_name=None,

349 related_query_name=None,

350 limit_choices_to=None,

351 symmetrical=True,

352 through=None,

353 through_fields=None,

354 db_constraint=True,

355 ):

356 super().__init__(

357 field,

358 to,

359 related_name=related_name,

360 related_query_name=related_query_name,

361 limit_choices_to=limit_choices_to,

362 )

363

364 if through and not db_constraint:

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

366 self.through = through

367

368 if through_fields and not through:

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

370 self.through_fields = through_fields

371

372 self.symmetrical = symmetrical

373 self.db_constraint = db_constraint

374

375 @property

376 def identity(self):

377 return super().identity + (

378 self.through,

379 make_hashable(self.through_fields),

380 self.db_constraint,

381 )

382

383 def get_related_field(self):

384 """

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

386 Provided for symmetry with ManyToOneRel.

387 """

388 opts = self.through._meta

389 if self.through_fields:

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

391 else:

392 for field in opts.fields:

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

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

395 break

396 return field.foreign_related_fields[0]