Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/contrib/admin/utils.py: 17%

317 statements  

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

1import datetime 

2import decimal 

3import json 

4from collections import defaultdict 

5 

6from django.core.exceptions import FieldDoesNotExist 

7from django.db import models, router 

8from django.db.models.constants import LOOKUP_SEP 

9from django.db.models.deletion import Collector 

10from django.forms.utils import pretty_name 

11from django.urls import NoReverseMatch, reverse 

12from django.utils import formats, timezone 

13from django.utils.html import format_html 

14from django.utils.regex_helper import _lazy_re_compile 

15from django.utils.text import capfirst 

16from django.utils.translation import ngettext 

17from django.utils.translation import override as translation_override 

18 

19QUOTE_MAP = {i: "_%02X" % i for i in b'":/_#?;@&=+$,"[]<>%\n\\'} 

20UNQUOTE_MAP = {v: chr(k) for k, v in QUOTE_MAP.items()} 

21UNQUOTE_RE = _lazy_re_compile("_(?:%s)" % "|".join([x[1:] for x in UNQUOTE_MAP])) 

22 

23 

24class FieldIsAForeignKeyColumnName(Exception): 

25 """A field is a foreign key attname, i.e. <FK>_id.""" 

26 

27 pass 

28 

29 

30def lookup_spawns_duplicates(opts, lookup_path): 

31 """ 

32 Return True if the given lookup path spawns duplicates. 

33 """ 

34 lookup_fields = lookup_path.split(LOOKUP_SEP) 

35 # Go through the fields (following all relations) and look for an m2m. 

36 for field_name in lookup_fields: 

37 if field_name == "pk": 

38 field_name = opts.pk.name 

39 try: 

40 field = opts.get_field(field_name) 

41 except FieldDoesNotExist: 

42 # Ignore query lookups. 

43 continue 

44 else: 

45 if hasattr(field, "get_path_info"): 

46 # This field is a relation; update opts to follow the relation. 

47 path_info = field.get_path_info() 

48 opts = path_info[-1].to_opts 

49 if any(path.m2m for path in path_info): 

50 # This field is a m2m relation so duplicates must be 

51 # handled. 

52 return True 

53 return False 

54 

55 

56def prepare_lookup_value(key, value): 

57 """ 

58 Return a lookup value prepared to be used in queryset filtering. 

59 """ 

60 # if key ends with __in, split parameter into separate values 

61 if key.endswith("__in"): 

62 value = value.split(",") 

63 # if key ends with __isnull, special case '' and the string literals 'false' and '0' 

64 elif key.endswith("__isnull"): 

65 value = value.lower() not in ("", "false", "0") 

66 return value 

67 

68 

69def quote(s): 

70 """ 

71 Ensure that primary key values do not confuse the admin URLs by escaping 

72 any '/', '_' and ':' and similarly problematic characters. 

73 Similar to urllib.parse.quote(), except that the quoting is slightly 

74 different so that it doesn't get automatically unquoted by the web browser. 

75 """ 

76 return s.translate(QUOTE_MAP) if isinstance(s, str) else s 

77 

78 

79def unquote(s): 

80 """Undo the effects of quote().""" 

81 return UNQUOTE_RE.sub(lambda m: UNQUOTE_MAP[m[0]], s) 

82 

83 

84def flatten(fields): 

85 """ 

86 Return a list which is a single level of flattening of the original list. 

87 """ 

88 flat = [] 

89 for field in fields: 

90 if isinstance(field, (list, tuple)): 90 ↛ 91line 90 didn't jump to line 91, because the condition on line 90 was never true

91 flat.extend(field) 

92 else: 

93 flat.append(field) 

94 return flat 

95 

96 

97def flatten_fieldsets(fieldsets): 

98 """Return a list of field names from an admin fieldsets structure.""" 

99 field_names = [] 

100 for name, opts in fieldsets: 

101 field_names.extend(flatten(opts["fields"])) 

102 return field_names 

103 

104 

105def get_deleted_objects(objs, request, admin_site): 

106 """ 

107 Find all objects related to ``objs`` that should also be deleted. ``objs`` 

108 must be a homogeneous iterable of objects (e.g. a QuerySet). 

109 

110 Return a nested list of strings suitable for display in the 

111 template with the ``unordered_list`` filter. 

112 """ 

113 try: 

114 obj = objs[0] 

115 except IndexError: 

116 return [], {}, set(), [] 

117 else: 

118 using = router.db_for_write(obj._meta.model) 

119 collector = NestedObjects(using=using) 

120 collector.collect(objs) 

121 perms_needed = set() 

122 

123 def format_callback(obj): 

124 model = obj.__class__ 

125 has_admin = model in admin_site._registry 

126 opts = obj._meta 

127 

128 no_edit_link = "%s: %s" % (capfirst(opts.verbose_name), obj) 

129 

130 if has_admin: 

131 if not admin_site._registry[model].has_delete_permission(request, obj): 

132 perms_needed.add(opts.verbose_name) 

133 try: 

134 admin_url = reverse( 

135 "%s:%s_%s_change" 

136 % (admin_site.name, opts.app_label, opts.model_name), 

137 None, 

138 (quote(obj.pk),), 

139 ) 

140 except NoReverseMatch: 

141 # Change url doesn't exist -- don't display link to edit 

142 return no_edit_link 

143 

144 # Display a link to the admin page. 

145 return format_html( 

146 '{}: <a href="{}">{}</a>', capfirst(opts.verbose_name), admin_url, obj 

147 ) 

148 else: 

149 # Don't display link to edit, because it either has no 

150 # admin or is edited inline. 

151 return no_edit_link 

152 

153 to_delete = collector.nested(format_callback) 

154 

155 protected = [format_callback(obj) for obj in collector.protected] 

156 model_count = { 

157 model._meta.verbose_name_plural: len(objs) 

158 for model, objs in collector.model_objs.items() 

159 } 

160 

161 return to_delete, model_count, perms_needed, protected 

162 

163 

164class NestedObjects(Collector): 

165 def __init__(self, *args, **kwargs): 

166 super().__init__(*args, **kwargs) 

167 self.edges = {} # {from_instance: [to_instances]} 

168 self.protected = set() 

169 self.model_objs = defaultdict(set) 

170 

171 def add_edge(self, source, target): 

172 self.edges.setdefault(source, []).append(target) 

173 

174 def collect(self, objs, source=None, source_attr=None, **kwargs): 

175 for obj in objs: 

176 if source_attr and not source_attr.endswith("+"): 

177 related_name = source_attr % { 

178 "class": source._meta.model_name, 

179 "app_label": source._meta.app_label, 

180 } 

181 self.add_edge(getattr(obj, related_name), obj) 

182 else: 

183 self.add_edge(None, obj) 

184 self.model_objs[obj._meta.model].add(obj) 

185 try: 

186 return super().collect(objs, source_attr=source_attr, **kwargs) 

187 except models.ProtectedError as e: 

188 self.protected.update(e.protected_objects) 

189 except models.RestrictedError as e: 

190 self.protected.update(e.restricted_objects) 

191 

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

193 qs = super().related_objects(related_model, related_fields, objs) 

194 return qs.select_related( 

195 *[related_field.name for related_field in related_fields] 

196 ) 

197 

198 def _nested(self, obj, seen, format_callback): 

199 if obj in seen: 

200 return [] 

201 seen.add(obj) 

202 children = [] 

203 for child in self.edges.get(obj, ()): 

204 children.extend(self._nested(child, seen, format_callback)) 

205 if format_callback: 

206 ret = [format_callback(obj)] 

207 else: 

208 ret = [obj] 

209 if children: 

210 ret.append(children) 

211 return ret 

212 

213 def nested(self, format_callback=None): 

214 """ 

215 Return the graph as a nested list. 

216 """ 

217 seen = set() 

218 roots = [] 

219 for root in self.edges.get(None, ()): 

220 roots.extend(self._nested(root, seen, format_callback)) 

221 return roots 

222 

223 def can_fast_delete(self, *args, **kwargs): 

224 """ 

225 We always want to load the objects into memory so that we can display 

226 them to the user in confirm page. 

227 """ 

228 return False 

229 

230 

231def model_format_dict(obj): 

232 """ 

233 Return a `dict` with keys 'verbose_name' and 'verbose_name_plural', 

234 typically for use with string formatting. 

235 

236 `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance. 

237 """ 

238 if isinstance(obj, (models.Model, models.base.ModelBase)): 

239 opts = obj._meta 

240 elif isinstance(obj, models.query.QuerySet): 

241 opts = obj.model._meta 

242 else: 

243 opts = obj 

244 return { 

245 "verbose_name": opts.verbose_name, 

246 "verbose_name_plural": opts.verbose_name_plural, 

247 } 

248 

249 

250def model_ngettext(obj, n=None): 

251 """ 

252 Return the appropriate `verbose_name` or `verbose_name_plural` value for 

253 `obj` depending on the count `n`. 

254 

255 `obj` may be a `Model` instance, `Model` subclass, or `QuerySet` instance. 

256 If `obj` is a `QuerySet` instance, `n` is optional and the length of the 

257 `QuerySet` is used. 

258 """ 

259 if isinstance(obj, models.query.QuerySet): 

260 if n is None: 

261 n = obj.count() 

262 obj = obj.model 

263 d = model_format_dict(obj) 

264 singular, plural = d["verbose_name"], d["verbose_name_plural"] 

265 return ngettext(singular, plural, n or 0) 

266 

267 

268def lookup_field(name, obj, model_admin=None): 

269 opts = obj._meta 

270 try: 

271 f = _get_non_gfk_field(opts, name) 

272 except (FieldDoesNotExist, FieldIsAForeignKeyColumnName): 

273 # For non-field values, the value is either a method, property or 

274 # returned via a callable. 

275 if callable(name): 

276 attr = name 

277 value = attr(obj) 

278 elif hasattr(model_admin, name) and name != "__str__": 

279 attr = getattr(model_admin, name) 

280 value = attr(obj) 

281 else: 

282 attr = getattr(obj, name) 

283 if callable(attr): 

284 value = attr() 

285 else: 

286 value = attr 

287 f = None 

288 else: 

289 attr = None 

290 value = getattr(obj, name) 

291 return f, attr, value 

292 

293 

294def _get_non_gfk_field(opts, name): 

295 """ 

296 For historical reasons, the admin app relies on GenericForeignKeys as being 

297 "not found" by get_field(). This could likely be cleaned up. 

298 

299 Reverse relations should also be excluded as these aren't attributes of the 

300 model (rather something like `foo_set`). 

301 """ 

302 field = opts.get_field(name) 

303 if ( 

304 field.is_relation 

305 and 

306 # Generic foreign keys OR reverse relations 

307 ((field.many_to_one and not field.related_model) or field.one_to_many) 

308 ): 

309 raise FieldDoesNotExist() 

310 

311 # Avoid coercing <FK>_id fields to FK 

312 if ( 

313 field.is_relation 

314 and not field.many_to_many 

315 and hasattr(field, "attname") 

316 and field.attname == name 

317 ): 

318 raise FieldIsAForeignKeyColumnName() 

319 

320 return field 

321 

322 

323def label_for_field(name, model, model_admin=None, return_attr=False, form=None): 

324 """ 

325 Return a sensible label for a field name. The name can be a callable, 

326 property (but not created with @property decorator), or the name of an 

327 object's attribute, as well as a model field. If return_attr is True, also 

328 return the resolved attribute (which could be a callable). This will be 

329 None if (and only if) the name refers to a field. 

330 """ 

331 attr = None 

332 try: 

333 field = _get_non_gfk_field(model._meta, name) 

334 try: 

335 label = field.verbose_name 

336 except AttributeError: 

337 # field is likely a ForeignObjectRel 

338 label = field.related_model._meta.verbose_name 

339 except FieldDoesNotExist: 

340 if name == "__str__": 

341 label = str(model._meta.verbose_name) 

342 attr = str 

343 else: 

344 if callable(name): 

345 attr = name 

346 elif hasattr(model_admin, name): 

347 attr = getattr(model_admin, name) 

348 elif hasattr(model, name): 

349 attr = getattr(model, name) 

350 elif form and name in form.fields: 

351 attr = form.fields[name] 

352 else: 

353 message = "Unable to lookup '%s' on %s" % ( 

354 name, 

355 model._meta.object_name, 

356 ) 

357 if model_admin: 

358 message += " or %s" % model_admin.__class__.__name__ 

359 if form: 

360 message += " or %s" % form.__class__.__name__ 

361 raise AttributeError(message) 

362 

363 if hasattr(attr, "short_description"): 

364 label = attr.short_description 

365 elif ( 

366 isinstance(attr, property) 

367 and hasattr(attr, "fget") 

368 and hasattr(attr.fget, "short_description") 

369 ): 

370 label = attr.fget.short_description 

371 elif callable(attr): 

372 if attr.__name__ == "<lambda>": 

373 label = "--" 

374 else: 

375 label = pretty_name(attr.__name__) 

376 else: 

377 label = pretty_name(name) 

378 except FieldIsAForeignKeyColumnName: 

379 label = pretty_name(name) 

380 attr = name 

381 

382 if return_attr: 

383 return (label, attr) 

384 else: 

385 return label 

386 

387 

388def help_text_for_field(name, model): 

389 help_text = "" 

390 try: 

391 field = _get_non_gfk_field(model._meta, name) 

392 except (FieldDoesNotExist, FieldIsAForeignKeyColumnName): 

393 pass 

394 else: 

395 if hasattr(field, "help_text"): 

396 help_text = field.help_text 

397 return help_text 

398 

399 

400def display_for_field(value, field, empty_value_display): 

401 from django.contrib.admin.templatetags.admin_list import _boolean_icon 

402 

403 if getattr(field, "flatchoices", None): 

404 return dict(field.flatchoices).get(value, empty_value_display) 

405 # BooleanField needs special-case null-handling, so it comes before the 

406 # general null test. 

407 elif isinstance(field, models.BooleanField): 

408 return _boolean_icon(value) 

409 elif value is None: 

410 return empty_value_display 

411 elif isinstance(field, models.DateTimeField): 

412 return formats.localize(timezone.template_localtime(value)) 

413 elif isinstance(field, (models.DateField, models.TimeField)): 

414 return formats.localize(value) 

415 elif isinstance(field, models.DecimalField): 

416 return formats.number_format(value, field.decimal_places) 

417 elif isinstance(field, (models.IntegerField, models.FloatField)): 

418 return formats.number_format(value) 

419 elif isinstance(field, models.FileField) and value: 

420 return format_html('<a href="{}">{}</a>', value.url, value) 

421 elif isinstance(field, models.JSONField) and value: 

422 try: 

423 return json.dumps(value, ensure_ascii=False, cls=field.encoder) 

424 except TypeError: 

425 return display_for_value(value, empty_value_display) 

426 else: 

427 return display_for_value(value, empty_value_display) 

428 

429 

430def display_for_value(value, empty_value_display, boolean=False): 

431 from django.contrib.admin.templatetags.admin_list import _boolean_icon 

432 

433 if boolean: 

434 return _boolean_icon(value) 

435 elif value is None: 

436 return empty_value_display 

437 elif isinstance(value, bool): 

438 return str(value) 

439 elif isinstance(value, datetime.datetime): 

440 return formats.localize(timezone.template_localtime(value)) 

441 elif isinstance(value, (datetime.date, datetime.time)): 

442 return formats.localize(value) 

443 elif isinstance(value, (int, decimal.Decimal, float)): 

444 return formats.number_format(value) 

445 elif isinstance(value, (list, tuple)): 

446 return ", ".join(str(v) for v in value) 

447 else: 

448 return str(value) 

449 

450 

451class NotRelationField(Exception): 

452 pass 

453 

454 

455def get_model_from_relation(field): 

456 if hasattr(field, "get_path_info"): 

457 return field.get_path_info()[-1].to_opts.model 

458 else: 

459 raise NotRelationField 

460 

461 

462def reverse_field_path(model, path): 

463 """Create a reversed field path. 

464 

465 E.g. Given (Order, "user__groups"), 

466 return (Group, "user__order"). 

467 

468 Final field must be a related model, not a data field. 

469 """ 

470 reversed_path = [] 

471 parent = model 

472 pieces = path.split(LOOKUP_SEP) 

473 for piece in pieces: 

474 field = parent._meta.get_field(piece) 

475 # skip trailing data field if extant: 

476 if len(reversed_path) == len(pieces) - 1: # final iteration 

477 try: 

478 get_model_from_relation(field) 

479 except NotRelationField: 

480 break 

481 

482 # Field should point to another model 

483 if field.is_relation and not (field.auto_created and not field.concrete): 

484 related_name = field.related_query_name() 

485 parent = field.remote_field.model 

486 else: 

487 related_name = field.field.name 

488 parent = field.related_model 

489 reversed_path.insert(0, related_name) 

490 return (parent, LOOKUP_SEP.join(reversed_path)) 

491 

492 

493def get_fields_from_path(model, path): 

494 """Return list of Fields given path relative to model. 

495 

496 e.g. (ModelX, "user__groups__name") -> [ 

497 <django.db.models.fields.related.ForeignKey object at 0x...>, 

498 <django.db.models.fields.related.ManyToManyField object at 0x...>, 

499 <django.db.models.fields.CharField object at 0x...>, 

500 ] 

501 """ 

502 pieces = path.split(LOOKUP_SEP) 

503 fields = [] 

504 for piece in pieces: 

505 if fields: 505 ↛ 506line 505 didn't jump to line 506, because the condition on line 505 was never true

506 parent = get_model_from_relation(fields[-1]) 

507 else: 

508 parent = model 

509 fields.append(parent._meta.get_field(piece)) 

510 return fields 

511 

512 

513def construct_change_message(form, formsets, add): 

514 """ 

515 Construct a JSON structure describing changes from a changed object. 

516 Translations are deactivated so that strings are stored untranslated. 

517 Translation happens later on LogEntry access. 

518 """ 

519 # Evaluating `form.changed_data` prior to disabling translations is required 

520 # to avoid fields affected by localization from being included incorrectly, 

521 # e.g. where date formats differ such as MM/DD/YYYY vs DD/MM/YYYY. 

522 changed_data = form.changed_data 

523 with translation_override(None): 

524 # Deactivate translations while fetching verbose_name for form 

525 # field labels and using `field_name`, if verbose_name is not provided. 

526 # Translations will happen later on LogEntry access. 

527 changed_field_labels = _get_changed_field_labels_from_form(form, changed_data) 

528 

529 change_message = [] 

530 if add: 

531 change_message.append({"added": {}}) 

532 elif form.changed_data: 

533 change_message.append({"changed": {"fields": changed_field_labels}}) 

534 if formsets: 

535 with translation_override(None): 

536 for formset in formsets: 

537 for added_object in formset.new_objects: 

538 change_message.append( 

539 { 

540 "added": { 

541 "name": str(added_object._meta.verbose_name), 

542 "object": str(added_object), 

543 } 

544 } 

545 ) 

546 for changed_object, changed_fields in formset.changed_objects: 

547 change_message.append( 

548 { 

549 "changed": { 

550 "name": str(changed_object._meta.verbose_name), 

551 "object": str(changed_object), 

552 "fields": _get_changed_field_labels_from_form( 

553 formset.forms[0], changed_fields 

554 ), 

555 } 

556 } 

557 ) 

558 for deleted_object in formset.deleted_objects: 

559 change_message.append( 

560 { 

561 "deleted": { 

562 "name": str(deleted_object._meta.verbose_name), 

563 "object": str(deleted_object), 

564 } 

565 } 

566 ) 

567 return change_message 

568 

569 

570def _get_changed_field_labels_from_form(form, changed_data): 

571 changed_field_labels = [] 

572 for field_name in changed_data: 

573 try: 

574 verbose_field_name = form.fields[field_name].label or field_name 

575 except KeyError: 

576 verbose_field_name = field_name 

577 changed_field_labels.append(str(verbose_field_name)) 

578 return changed_field_labels