Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/db/models/deletion.py: 62%

235 statements  

« prev     ^ index     » next       coverage.py v6.4.4, created at 2023-07-17 14:22 -0600

1from collections import Counter, defaultdict 

2from functools import partial 

3from itertools import chain 

4from operator import attrgetter 

5 

6from django.db import IntegrityError, connections, transaction 

7from django.db.models import query_utils, signals, sql 

8 

9 

10class ProtectedError(IntegrityError): 

11 def __init__(self, msg, protected_objects): 

12 self.protected_objects = protected_objects 

13 super().__init__(msg, protected_objects) 

14 

15 

16class RestrictedError(IntegrityError): 

17 def __init__(self, msg, restricted_objects): 

18 self.restricted_objects = restricted_objects 

19 super().__init__(msg, restricted_objects) 

20 

21 

22def CASCADE(collector, field, sub_objs, using): 

23 collector.collect( 

24 sub_objs, 

25 source=field.remote_field.model, 

26 source_attr=field.name, 

27 nullable=field.null, 

28 fail_on_restricted=False, 

29 ) 

30 if field.null and not connections[using].features.can_defer_constraint_checks: 

31 collector.add_field_update(field, None, sub_objs) 

32 

33 

34def PROTECT(collector, field, sub_objs, using): 

35 raise ProtectedError( 

36 "Cannot delete some instances of model '%s' because they are " 

37 "referenced through a protected foreign key: '%s.%s'" 

38 % ( 

39 field.remote_field.model.__name__, 

40 sub_objs[0].__class__.__name__, 

41 field.name, 

42 ), 

43 sub_objs, 

44 ) 

45 

46 

47def RESTRICT(collector, field, sub_objs, using): 

48 collector.add_restricted_objects(field, sub_objs) 

49 collector.add_dependency(field.remote_field.model, field.model) 

50 

51 

52def SET(value): 

53 if callable(value): 

54 

55 def set_on_delete(collector, field, sub_objs, using): 

56 collector.add_field_update(field, value(), sub_objs) 

57 

58 else: 

59 

60 def set_on_delete(collector, field, sub_objs, using): 

61 collector.add_field_update(field, value, sub_objs) 

62 

63 set_on_delete.deconstruct = lambda: ("django.db.models.SET", (value,), {}) 

64 return set_on_delete 

65 

66 

67def SET_NULL(collector, field, sub_objs, using): 

68 collector.add_field_update(field, None, sub_objs) 

69 

70 

71def SET_DEFAULT(collector, field, sub_objs, using): 

72 collector.add_field_update(field, field.get_default(), sub_objs) 

73 

74 

75def DO_NOTHING(collector, field, sub_objs, using): 

76 pass 

77 

78 

79def get_candidate_relations_to_delete(opts): 

80 # The candidate relations are the ones that come from N-1 and 1-1 relations. 

81 # N-N (i.e., many-to-many) relations aren't candidates for deletion. 

82 return ( 

83 f 

84 for f in opts.get_fields(include_hidden=True) 

85 if f.auto_created and not f.concrete and (f.one_to_one or f.one_to_many) 

86 ) 

87 

88 

89class Collector: 

90 def __init__(self, using): 

91 self.using = using 

92 # Initially, {model: {instances}}, later values become lists. 

93 self.data = defaultdict(set) 

94 # {model: {(field, value): {instances}}} 

95 self.field_updates = defaultdict(partial(defaultdict, set)) 

96 # {model: {field: {instances}}} 

97 self.restricted_objects = defaultdict(partial(defaultdict, set)) 

98 # fast_deletes is a list of queryset-likes that can be deleted without 

99 # fetching the objects into memory. 

100 self.fast_deletes = [] 

101 

102 # Tracks deletion-order dependency for databases without transactions 

103 # or ability to defer constraint checks. Only concrete model classes 

104 # should be included, as the dependencies exist only between actual 

105 # database tables; proxy models are represented here by their concrete 

106 # parent. 

107 self.dependencies = defaultdict(set) # {model: {models}} 

108 

109 def add(self, objs, source=None, nullable=False, reverse_dependency=False): 

110 """ 

111 Add 'objs' to the collection of objects to be deleted. If the call is 

112 the result of a cascade, 'source' should be the model that caused it, 

113 and 'nullable' should be set to True if the relation can be null. 

114 

115 Return a list of all objects that were not already collected. 

116 """ 

117 if not objs: 117 ↛ 118line 117 didn't jump to line 118, because the condition on line 117 was never true

118 return [] 

119 new_objs = [] 

120 model = objs[0].__class__ 

121 instances = self.data[model] 

122 for obj in objs: 

123 if obj not in instances: 123 ↛ 122line 123 didn't jump to line 122, because the condition on line 123 was never false

124 new_objs.append(obj) 

125 instances.update(new_objs) 

126 # Nullable relationships can be ignored -- they are nulled out before 

127 # deleting, and therefore do not affect the order in which objects have 

128 # to be deleted. 

129 if source is not None and not nullable: 129 ↛ 130line 129 didn't jump to line 130, because the condition on line 129 was never true

130 self.add_dependency(source, model, reverse_dependency=reverse_dependency) 

131 return new_objs 

132 

133 def add_dependency(self, model, dependency, reverse_dependency=False): 

134 if reverse_dependency: 

135 model, dependency = dependency, model 

136 self.dependencies[model._meta.concrete_model].add( 

137 dependency._meta.concrete_model 

138 ) 

139 self.data.setdefault(dependency, self.data.default_factory()) 

140 

141 def add_field_update(self, field, value, objs): 

142 """ 

143 Schedule a field update. 'objs' must be a homogeneous iterable 

144 collection of model instances (e.g. a QuerySet). 

145 """ 

146 if not objs: 

147 return 

148 model = objs[0].__class__ 

149 self.field_updates[model][field, value].update(objs) 

150 

151 def add_restricted_objects(self, field, objs): 

152 if objs: 

153 model = objs[0].__class__ 

154 self.restricted_objects[model][field].update(objs) 

155 

156 def clear_restricted_objects_from_set(self, model, objs): 

157 if model in self.restricted_objects: 157 ↛ 158line 157 didn't jump to line 158, because the condition on line 157 was never true

158 self.restricted_objects[model] = { 

159 field: items - objs 

160 for field, items in self.restricted_objects[model].items() 

161 } 

162 

163 def clear_restricted_objects_from_queryset(self, model, qs): 

164 if model in self.restricted_objects: 164 ↛ 165line 164 didn't jump to line 165, because the condition on line 164 was never true

165 objs = set( 

166 qs.filter( 

167 pk__in=[ 

168 obj.pk 

169 for objs in self.restricted_objects[model].values() 

170 for obj in objs 

171 ] 

172 ) 

173 ) 

174 self.clear_restricted_objects_from_set(model, objs) 

175 

176 def _has_signal_listeners(self, model): 

177 return signals.pre_delete.has_listeners( 

178 model 

179 ) or signals.post_delete.has_listeners(model) 

180 

181 def can_fast_delete(self, objs, from_field=None): 

182 """ 

183 Determine if the objects in the given queryset-like or single object 

184 can be fast-deleted. This can be done if there are no cascades, no 

185 parents and no signal listeners for the object class. 

186 

187 The 'from_field' tells where we are coming from - we need this to 

188 determine if the objects are in fact to be deleted. Allow also 

189 skipping parent -> child -> parent chain preventing fast delete of 

190 the child. 

191 """ 

192 if from_field and from_field.remote_field.on_delete is not CASCADE: 

193 return False 

194 if hasattr(objs, "_meta"): 

195 model = objs._meta.model 

196 elif hasattr(objs, "model") and hasattr(objs, "_raw_delete"): 196 ↛ 197line 196 didn't jump to line 197, because the condition on line 196 was never true

197 model = objs.model 

198 else: 

199 return False 

200 if self._has_signal_listeners(model): 200 ↛ 201line 200 didn't jump to line 201, because the condition on line 200 was never true

201 return False 

202 # The use of from_field comes from the need to avoid cascade back to 

203 # parent when parent delete is cascading to child. 

204 opts = model._meta 

205 return ( 

206 all( 

207 link == from_field 

208 for link in opts.concrete_model._meta.parents.values() 

209 ) 

210 and 

211 # Foreign keys pointing to this model. 

212 all( 

213 related.field.remote_field.on_delete is DO_NOTHING 

214 for related in get_candidate_relations_to_delete(opts) 

215 ) 

216 and ( 

217 # Something like generic foreign key. 

218 not any( 

219 hasattr(field, "bulk_related_objects") 

220 for field in opts.private_fields 

221 ) 

222 ) 

223 ) 

224 

225 def get_del_batches(self, objs, fields): 

226 """ 

227 Return the objs in suitably sized batches for the used connection. 

228 """ 

229 field_names = [field.name for field in fields] 

230 conn_batch_size = max( 

231 connections[self.using].ops.bulk_batch_size(field_names, objs), 1 

232 ) 

233 if len(objs) > conn_batch_size: 233 ↛ 234line 233 didn't jump to line 234, because the condition on line 233 was never true

234 return [ 

235 objs[i : i + conn_batch_size] 

236 for i in range(0, len(objs), conn_batch_size) 

237 ] 

238 else: 

239 return [objs] 

240 

241 def collect( 

242 self, 

243 objs, 

244 source=None, 

245 nullable=False, 

246 collect_related=True, 

247 source_attr=None, 

248 reverse_dependency=False, 

249 keep_parents=False, 

250 fail_on_restricted=True, 

251 ): 

252 """ 

253 Add 'objs' to the collection of objects to be deleted as well as all 

254 parent instances. 'objs' must be a homogeneous iterable collection of 

255 model instances (e.g. a QuerySet). If 'collect_related' is True, 

256 related objects will be handled by their respective on_delete handler. 

257 

258 If the call is the result of a cascade, 'source' should be the model 

259 that caused it and 'nullable' should be set to True, if the relation 

260 can be null. 

261 

262 If 'reverse_dependency' is True, 'source' will be deleted before the 

263 current model, rather than after. (Needed for cascading to parent 

264 models, the one case in which the cascade follows the forwards 

265 direction of an FK rather than the reverse direction.) 

266 

267 If 'keep_parents' is True, data of parent model's will be not deleted. 

268 

269 If 'fail_on_restricted' is False, error won't be raised even if it's 

270 prohibited to delete such objects due to RESTRICT, that defers 

271 restricted object checking in recursive calls where the top-level call 

272 may need to collect more objects to determine whether restricted ones 

273 can be deleted. 

274 """ 

275 if self.can_fast_delete(objs): 275 ↛ 276line 275 didn't jump to line 276, because the condition on line 275 was never true

276 self.fast_deletes.append(objs) 

277 return 

278 new_objs = self.add( 

279 objs, source, nullable, reverse_dependency=reverse_dependency 

280 ) 

281 if not new_objs: 281 ↛ 282line 281 didn't jump to line 282, because the condition on line 281 was never true

282 return 

283 

284 model = new_objs[0].__class__ 

285 

286 if not keep_parents: 286 ↛ 301line 286 didn't jump to line 301, because the condition on line 286 was never false

287 # Recursively collect concrete model's parent models, but not their 

288 # related objects. These will be found by meta.get_fields() 

289 concrete_model = model._meta.concrete_model 

290 for ptr in concrete_model._meta.parents.values(): 290 ↛ 291line 290 didn't jump to line 291, because the loop on line 290 never started

291 if ptr: 

292 parent_objs = [getattr(obj, ptr.name) for obj in new_objs] 

293 self.collect( 

294 parent_objs, 

295 source=model, 

296 source_attr=ptr.remote_field.related_name, 

297 collect_related=False, 

298 reverse_dependency=True, 

299 fail_on_restricted=False, 

300 ) 

301 if not collect_related: 301 ↛ 302line 301 didn't jump to line 302, because the condition on line 301 was never true

302 return 

303 

304 if keep_parents: 304 ↛ 305line 304 didn't jump to line 305, because the condition on line 304 was never true

305 parents = set(model._meta.get_parent_list()) 

306 model_fast_deletes = defaultdict(list) 

307 protected_objects = defaultdict(list) 

308 for related in get_candidate_relations_to_delete(model._meta): 

309 # Preserve parent reverse relationships if keep_parents=True. 

310 if keep_parents and related.model in parents: 310 ↛ 311line 310 didn't jump to line 311, because the condition on line 310 was never true

311 continue 

312 field = related.field 

313 if field.remote_field.on_delete == DO_NOTHING: 313 ↛ 314line 313 didn't jump to line 314, because the condition on line 313 was never true

314 continue 

315 related_model = related.related_model 

316 if self.can_fast_delete(related_model, from_field=field): 

317 model_fast_deletes[related_model].append(field) 

318 continue 

319 batches = self.get_del_batches(new_objs, [field]) 

320 for batch in batches: 

321 sub_objs = self.related_objects(related_model, [field], batch) 

322 # Non-referenced fields can be deferred if no signal receivers 

323 # are connected for the related model as they'll never be 

324 # exposed to the user. Skip field deferring when some 

325 # relationships are select_related as interactions between both 

326 # features are hard to get right. This should only happen in 

327 # the rare cases where .related_objects is overridden anyway. 

328 if not ( 328 ↛ 341line 328 didn't jump to line 341, because the condition on line 328 was never false

329 sub_objs.query.select_related 

330 or self._has_signal_listeners(related_model) 

331 ): 

332 referenced_fields = set( 

333 chain.from_iterable( 

334 (rf.attname for rf in rel.field.foreign_related_fields) 

335 for rel in get_candidate_relations_to_delete( 

336 related_model._meta 

337 ) 

338 ) 

339 ) 

340 sub_objs = sub_objs.only(*tuple(referenced_fields)) 

341 if sub_objs: 341 ↛ 342line 341 didn't jump to line 342, because the condition on line 341 was never true

342 try: 

343 field.remote_field.on_delete(self, field, sub_objs, self.using) 

344 except ProtectedError as error: 

345 key = "'%s.%s'" % (field.model.__name__, field.name) 

346 protected_objects[key] += error.protected_objects 

347 if protected_objects: 347 ↛ 348line 347 didn't jump to line 348, because the condition on line 347 was never true

348 raise ProtectedError( 

349 "Cannot delete some instances of model %r because they are " 

350 "referenced through protected foreign keys: %s." 

351 % ( 

352 model.__name__, 

353 ", ".join(protected_objects), 

354 ), 

355 set(chain.from_iterable(protected_objects.values())), 

356 ) 

357 for related_model, related_fields in model_fast_deletes.items(): 

358 batches = self.get_del_batches(new_objs, related_fields) 

359 for batch in batches: 

360 sub_objs = self.related_objects(related_model, related_fields, batch) 

361 self.fast_deletes.append(sub_objs) 

362 for field in model._meta.private_fields: 362 ↛ 363line 362 didn't jump to line 363, because the loop on line 362 never started

363 if hasattr(field, "bulk_related_objects"): 

364 # It's something like generic foreign key. 

365 sub_objs = field.bulk_related_objects(new_objs, self.using) 

366 self.collect( 

367 sub_objs, source=model, nullable=True, fail_on_restricted=False 

368 ) 

369 

370 if fail_on_restricted: 370 ↛ exitline 370 didn't return from function 'collect', because the condition on line 370 was never false

371 # Raise an error if collected restricted objects (RESTRICT) aren't 

372 # candidates for deletion also collected via CASCADE. 

373 for related_model, instances in self.data.items(): 

374 self.clear_restricted_objects_from_set(related_model, instances) 

375 for qs in self.fast_deletes: 

376 self.clear_restricted_objects_from_queryset(qs.model, qs) 

377 if self.restricted_objects.values(): 377 ↛ 378line 377 didn't jump to line 378, because the condition on line 377 was never true

378 restricted_objects = defaultdict(list) 

379 for related_model, fields in self.restricted_objects.items(): 

380 for field, objs in fields.items(): 

381 if objs: 

382 key = "'%s.%s'" % (related_model.__name__, field.name) 

383 restricted_objects[key] += objs 

384 if restricted_objects: 

385 raise RestrictedError( 

386 "Cannot delete some instances of model %r because " 

387 "they are referenced through restricted foreign keys: " 

388 "%s." 

389 % ( 

390 model.__name__, 

391 ", ".join(restricted_objects), 

392 ), 

393 set(chain.from_iterable(restricted_objects.values())), 

394 ) 

395 

396 def related_objects(self, related_model, related_fields, objs): 

397 """ 

398 Get a QuerySet of the related model to objs via related fields. 

399 """ 

400 predicate = query_utils.Q( 

401 *((f"{related_field.name}__in", objs) for related_field in related_fields), 

402 _connector=query_utils.Q.OR, 

403 ) 

404 return related_model._base_manager.using(self.using).filter(predicate) 

405 

406 def instances_with_model(self): 

407 for model, instances in self.data.items(): 

408 for obj in instances: 

409 yield model, obj 

410 

411 def sort(self): 

412 sorted_models = [] 

413 concrete_models = set() 

414 models = list(self.data) 

415 while len(sorted_models) < len(models): 

416 found = False 

417 for model in models: 

418 if model in sorted_models: 418 ↛ 419line 418 didn't jump to line 419, because the condition on line 418 was never true

419 continue 

420 dependencies = self.dependencies.get(model._meta.concrete_model) 

421 if not (dependencies and dependencies.difference(concrete_models)): 421 ↛ 417line 421 didn't jump to line 417, because the condition on line 421 was never false

422 sorted_models.append(model) 

423 concrete_models.add(model._meta.concrete_model) 

424 found = True 

425 if not found: 425 ↛ 426line 425 didn't jump to line 426, because the condition on line 425 was never true

426 return 

427 self.data = {model: self.data[model] for model in sorted_models} 

428 

429 def delete(self): 

430 # sort instance collections 

431 for model, instances in self.data.items(): 

432 self.data[model] = sorted(instances, key=attrgetter("pk")) 

433 

434 # if possible, bring the models in an order suitable for databases that 

435 # don't support transactions or cannot defer constraint checks until the 

436 # end of a transaction. 

437 self.sort() 

438 # number of objects deleted for each model label 

439 deleted_counter = Counter() 

440 

441 # Optimize for the case with a single obj and no dependencies 

442 if len(self.data) == 1 and len(instances) == 1: 442 ↛ 452line 442 didn't jump to line 452, because the condition on line 442 was never false

443 instance = list(instances)[0] 

444 if self.can_fast_delete(instance): 444 ↛ 445line 444 didn't jump to line 445, because the condition on line 444 was never true

445 with transaction.mark_for_rollback_on_error(self.using): 

446 count = sql.DeleteQuery(model).delete_batch( 

447 [instance.pk], self.using 

448 ) 

449 setattr(instance, model._meta.pk.attname, None) 

450 return count, {model._meta.label: count} 

451 

452 with transaction.atomic(using=self.using, savepoint=False): 

453 # send pre_delete signals 

454 for model, obj in self.instances_with_model(): 

455 if not model._meta.auto_created: 455 ↛ 454line 455 didn't jump to line 454, because the condition on line 455 was never false

456 signals.pre_delete.send( 

457 sender=model, instance=obj, using=self.using 

458 ) 

459 

460 # fast deletes 

461 for qs in self.fast_deletes: 

462 count = qs._raw_delete(using=self.using) 

463 if count: 463 ↛ 464line 463 didn't jump to line 464, because the condition on line 463 was never true

464 deleted_counter[qs.model._meta.label] += count 

465 

466 # update fields 

467 for model, instances_for_fieldvalues in self.field_updates.items(): 467 ↛ 468line 467 didn't jump to line 468, because the loop on line 467 never started

468 for (field, value), instances in instances_for_fieldvalues.items(): 

469 query = sql.UpdateQuery(model) 

470 query.update_batch( 

471 [obj.pk for obj in instances], {field.name: value}, self.using 

472 ) 

473 

474 # reverse instance collections 

475 for instances in self.data.values(): 

476 instances.reverse() 

477 

478 # delete instances 

479 for model, instances in self.data.items(): 

480 query = sql.DeleteQuery(model) 

481 pk_list = [obj.pk for obj in instances] 

482 count = query.delete_batch(pk_list, self.using) 

483 if count: 483 ↛ 486line 483 didn't jump to line 486, because the condition on line 483 was never false

484 deleted_counter[model._meta.label] += count 

485 

486 if not model._meta.auto_created: 486 ↛ 479line 486 didn't jump to line 479, because the condition on line 486 was never false

487 for obj in instances: 

488 signals.post_delete.send( 

489 sender=model, instance=obj, using=self.using 

490 ) 

491 

492 # update collected instances 

493 for instances_for_fieldvalues in self.field_updates.values(): 493 ↛ 494line 493 didn't jump to line 494, because the loop on line 493 never started

494 for (field, value), instances in instances_for_fieldvalues.items(): 

495 for obj in instances: 

496 setattr(obj, field.attname, value) 

497 for model, instances in self.data.items(): 

498 for instance in instances: 

499 setattr(instance, model._meta.pk.attname, None) 

500 return sum(deleted_counter.values()), dict(deleted_counter)