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

329 statements  

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

1from datetime import datetime, timedelta 

2 

3from django import forms 

4from django.conf import settings 

5from django.contrib import messages 

6from django.contrib.admin import FieldListFilter 

7from django.contrib.admin.exceptions import ( 

8 DisallowedModelAdminLookup, 

9 DisallowedModelAdminToField, 

10) 

11from django.contrib.admin.options import ( 

12 IS_POPUP_VAR, 

13 TO_FIELD_VAR, 

14 IncorrectLookupParameters, 

15) 

16from django.contrib.admin.utils import ( 

17 get_fields_from_path, 

18 lookup_spawns_duplicates, 

19 prepare_lookup_value, 

20 quote, 

21) 

22from django.core.exceptions import ( 

23 FieldDoesNotExist, 

24 ImproperlyConfigured, 

25 SuspiciousOperation, 

26) 

27from django.core.paginator import InvalidPage 

28from django.db.models import Exists, F, Field, ManyToOneRel, OrderBy, OuterRef 

29from django.db.models.expressions import Combinable 

30from django.urls import reverse 

31from django.utils.http import urlencode 

32from django.utils.timezone import make_aware 

33from django.utils.translation import gettext 

34 

35# Changelist settings 

36ALL_VAR = "all" 

37ORDER_VAR = "o" 

38PAGE_VAR = "p" 

39SEARCH_VAR = "q" 

40ERROR_FLAG = "e" 

41 

42IGNORED_PARAMS = (ALL_VAR, ORDER_VAR, SEARCH_VAR, IS_POPUP_VAR, TO_FIELD_VAR) 

43 

44 

45class ChangeListSearchForm(forms.Form): 

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

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

48 # Populate "fields" dynamically because SEARCH_VAR is a variable: 

49 self.fields = { 

50 SEARCH_VAR: forms.CharField(required=False, strip=False), 

51 } 

52 

53 

54class ChangeList: 

55 search_form_class = ChangeListSearchForm 

56 

57 def __init__( 

58 self, 

59 request, 

60 model, 

61 list_display, 

62 list_display_links, 

63 list_filter, 

64 date_hierarchy, 

65 search_fields, 

66 list_select_related, 

67 list_per_page, 

68 list_max_show_all, 

69 list_editable, 

70 model_admin, 

71 sortable_by, 

72 search_help_text, 

73 ): 

74 self.model = model 

75 self.opts = model._meta 

76 self.lookup_opts = self.opts 

77 self.root_queryset = model_admin.get_queryset(request) 

78 self.list_display = list_display 

79 self.list_display_links = list_display_links 

80 self.list_filter = list_filter 

81 self.has_filters = None 

82 self.has_active_filters = None 

83 self.clear_all_filters_qs = None 

84 self.date_hierarchy = date_hierarchy 

85 self.search_fields = search_fields 

86 self.list_select_related = list_select_related 

87 self.list_per_page = list_per_page 

88 self.list_max_show_all = list_max_show_all 

89 self.model_admin = model_admin 

90 self.preserved_filters = model_admin.get_preserved_filters(request) 

91 self.sortable_by = sortable_by 

92 self.search_help_text = search_help_text 

93 

94 # Get search parameters from the query string. 

95 _search_form = self.search_form_class(request.GET) 

96 if not _search_form.is_valid(): 

97 for error in _search_form.errors.values(): 

98 messages.error(request, ", ".join(error)) 

99 self.query = _search_form.cleaned_data.get(SEARCH_VAR) or "" 

100 try: 

101 self.page_num = int(request.GET.get(PAGE_VAR, 1)) 

102 except ValueError: 

103 self.page_num = 1 

104 self.show_all = ALL_VAR in request.GET 

105 self.is_popup = IS_POPUP_VAR in request.GET 

106 to_field = request.GET.get(TO_FIELD_VAR) 

107 if to_field and not model_admin.to_field_allowed(request, to_field): 

108 raise DisallowedModelAdminToField( 

109 "The field %s cannot be referenced." % to_field 

110 ) 

111 self.to_field = to_field 

112 self.params = dict(request.GET.items()) 

113 if PAGE_VAR in self.params: 

114 del self.params[PAGE_VAR] 

115 if ERROR_FLAG in self.params: 

116 del self.params[ERROR_FLAG] 

117 

118 if self.is_popup: 

119 self.list_editable = () 

120 else: 

121 self.list_editable = list_editable 

122 self.queryset = self.get_queryset(request) 

123 self.get_results(request) 

124 if self.is_popup: 

125 title = gettext("Select %s") 

126 elif self.model_admin.has_change_permission(request): 

127 title = gettext("Select %s to change") 

128 else: 

129 title = gettext("Select %s to view") 

130 self.title = title % self.opts.verbose_name 

131 self.pk_attname = self.lookup_opts.pk.attname 

132 

133 def __repr__(self): 

134 return "<%s: model=%s model_admin=%s>" % ( 

135 self.__class__.__qualname__, 

136 self.model.__qualname__, 

137 self.model_admin.__class__.__qualname__, 

138 ) 

139 

140 def get_filters_params(self, params=None): 

141 """ 

142 Return all params except IGNORED_PARAMS. 

143 """ 

144 params = params or self.params 

145 lookup_params = params.copy() # a dictionary of the query string 

146 # Remove all the parameters that are globally and systematically 

147 # ignored. 

148 for ignored in IGNORED_PARAMS: 

149 if ignored in lookup_params: 

150 del lookup_params[ignored] 

151 return lookup_params 

152 

153 def get_filters(self, request): 

154 lookup_params = self.get_filters_params() 

155 may_have_duplicates = False 

156 has_active_filters = False 

157 

158 for key, value in lookup_params.items(): 

159 if not self.model_admin.lookup_allowed(key, value): 

160 raise DisallowedModelAdminLookup("Filtering by %s not allowed" % key) 

161 

162 filter_specs = [] 

163 for list_filter in self.list_filter: 

164 lookup_params_count = len(lookup_params) 

165 if callable(list_filter): 

166 # This is simply a custom list filter class. 

167 spec = list_filter(request, lookup_params, self.model, self.model_admin) 

168 else: 

169 field_path = None 

170 if isinstance(list_filter, (tuple, list)): 

171 # This is a custom FieldListFilter class for a given field. 

172 field, field_list_filter_class = list_filter 

173 else: 

174 # This is simply a field name, so use the default 

175 # FieldListFilter class that has been registered for the 

176 # type of the given field. 

177 field, field_list_filter_class = list_filter, FieldListFilter.create 

178 if not isinstance(field, Field): 

179 field_path = field 

180 field = get_fields_from_path(self.model, field_path)[-1] 

181 

182 spec = field_list_filter_class( 

183 field, 

184 request, 

185 lookup_params, 

186 self.model, 

187 self.model_admin, 

188 field_path=field_path, 

189 ) 

190 # field_list_filter_class removes any lookup_params it 

191 # processes. If that happened, check if duplicates should be 

192 # removed. 

193 if lookup_params_count > len(lookup_params): 

194 may_have_duplicates |= lookup_spawns_duplicates( 

195 self.lookup_opts, 

196 field_path, 

197 ) 

198 if spec and spec.has_output(): 

199 filter_specs.append(spec) 

200 if lookup_params_count > len(lookup_params): 

201 has_active_filters = True 

202 

203 if self.date_hierarchy: 

204 # Create bounded lookup parameters so that the query is more 

205 # efficient. 

206 year = lookup_params.pop("%s__year" % self.date_hierarchy, None) 

207 if year is not None: 

208 month = lookup_params.pop("%s__month" % self.date_hierarchy, None) 

209 day = lookup_params.pop("%s__day" % self.date_hierarchy, None) 

210 try: 

211 from_date = datetime( 

212 int(year), 

213 int(month if month is not None else 1), 

214 int(day if day is not None else 1), 

215 ) 

216 except ValueError as e: 

217 raise IncorrectLookupParameters(e) from e 

218 if day: 

219 to_date = from_date + timedelta(days=1) 

220 elif month: 

221 # In this branch, from_date will always be the first of a 

222 # month, so advancing 32 days gives the next month. 

223 to_date = (from_date + timedelta(days=32)).replace(day=1) 

224 else: 

225 to_date = from_date.replace(year=from_date.year + 1) 

226 if settings.USE_TZ: 

227 from_date = make_aware(from_date) 

228 to_date = make_aware(to_date) 

229 lookup_params.update( 

230 { 

231 "%s__gte" % self.date_hierarchy: from_date, 

232 "%s__lt" % self.date_hierarchy: to_date, 

233 } 

234 ) 

235 

236 # At this point, all the parameters used by the various ListFilters 

237 # have been removed from lookup_params, which now only contains other 

238 # parameters passed via the query string. We now loop through the 

239 # remaining parameters both to ensure that all the parameters are valid 

240 # fields and to determine if at least one of them spawns duplicates. If 

241 # the lookup parameters aren't real fields, then bail out. 

242 try: 

243 for key, value in lookup_params.items(): 

244 lookup_params[key] = prepare_lookup_value(key, value) 

245 may_have_duplicates |= lookup_spawns_duplicates(self.lookup_opts, key) 

246 return ( 

247 filter_specs, 

248 bool(filter_specs), 

249 lookup_params, 

250 may_have_duplicates, 

251 has_active_filters, 

252 ) 

253 except FieldDoesNotExist as e: 

254 raise IncorrectLookupParameters(e) from e 

255 

256 def get_query_string(self, new_params=None, remove=None): 

257 if new_params is None: 

258 new_params = {} 

259 if remove is None: 

260 remove = [] 

261 p = self.params.copy() 

262 for r in remove: 

263 for k in list(p): 

264 if k.startswith(r): 

265 del p[k] 

266 for k, v in new_params.items(): 

267 if v is None: 

268 if k in p: 

269 del p[k] 

270 else: 

271 p[k] = v 

272 return "?%s" % urlencode(sorted(p.items())) 

273 

274 def get_results(self, request): 

275 paginator = self.model_admin.get_paginator( 

276 request, self.queryset, self.list_per_page 

277 ) 

278 # Get the number of objects, with admin filters applied. 

279 result_count = paginator.count 

280 

281 # Get the total number of objects, with no admin filters applied. 

282 if self.model_admin.show_full_result_count: 

283 full_result_count = self.root_queryset.count() 

284 else: 

285 full_result_count = None 

286 can_show_all = result_count <= self.list_max_show_all 

287 multi_page = result_count > self.list_per_page 

288 

289 # Get the list of objects to display on this page. 

290 if (self.show_all and can_show_all) or not multi_page: 

291 result_list = self.queryset._clone() 

292 else: 

293 try: 

294 result_list = paginator.page(self.page_num).object_list 

295 except InvalidPage: 

296 raise IncorrectLookupParameters 

297 

298 self.result_count = result_count 

299 self.show_full_result_count = self.model_admin.show_full_result_count 

300 # Admin actions are shown if there is at least one entry 

301 # or if entries are not counted because show_full_result_count is disabled 

302 self.show_admin_actions = not self.show_full_result_count or bool( 

303 full_result_count 

304 ) 

305 self.full_result_count = full_result_count 

306 self.result_list = result_list 

307 self.can_show_all = can_show_all 

308 self.multi_page = multi_page 

309 self.paginator = paginator 

310 

311 def _get_default_ordering(self): 

312 ordering = [] 

313 if self.model_admin.ordering: 

314 ordering = self.model_admin.ordering 

315 elif self.lookup_opts.ordering: 

316 ordering = self.lookup_opts.ordering 

317 return ordering 

318 

319 def get_ordering_field(self, field_name): 

320 """ 

321 Return the proper model field name corresponding to the given 

322 field_name to use for ordering. field_name may either be the name of a 

323 proper model field or the name of a method (on the admin or model) or a 

324 callable with the 'admin_order_field' attribute. Return None if no 

325 proper model field name can be matched. 

326 """ 

327 try: 

328 field = self.lookup_opts.get_field(field_name) 

329 return field.name 

330 except FieldDoesNotExist: 

331 # See whether field_name is a name of a non-field 

332 # that allows sorting. 

333 if callable(field_name): 

334 attr = field_name 

335 elif hasattr(self.model_admin, field_name): 

336 attr = getattr(self.model_admin, field_name) 

337 else: 

338 attr = getattr(self.model, field_name) 

339 if isinstance(attr, property) and hasattr(attr, "fget"): 

340 attr = attr.fget 

341 return getattr(attr, "admin_order_field", None) 

342 

343 def get_ordering(self, request, queryset): 

344 """ 

345 Return the list of ordering fields for the change list. 

346 First check the get_ordering() method in model admin, then check 

347 the object's default ordering. Then, any manually-specified ordering 

348 from the query string overrides anything. Finally, a deterministic 

349 order is guaranteed by calling _get_deterministic_ordering() with the 

350 constructed ordering. 

351 """ 

352 params = self.params 

353 ordering = list( 

354 self.model_admin.get_ordering(request) or self._get_default_ordering() 

355 ) 

356 if ORDER_VAR in params: 

357 # Clear ordering and used params 

358 ordering = [] 

359 order_params = params[ORDER_VAR].split(".") 

360 for p in order_params: 

361 try: 

362 none, pfx, idx = p.rpartition("-") 

363 field_name = self.list_display[int(idx)] 

364 order_field = self.get_ordering_field(field_name) 

365 if not order_field: 

366 continue # No 'admin_order_field', skip it 

367 if isinstance(order_field, OrderBy): 

368 if pfx == "-": 

369 order_field = order_field.copy() 

370 order_field.reverse_ordering() 

371 ordering.append(order_field) 

372 elif hasattr(order_field, "resolve_expression"): 

373 # order_field is an expression. 

374 ordering.append( 

375 order_field.desc() if pfx == "-" else order_field.asc() 

376 ) 

377 # reverse order if order_field has already "-" as prefix 

378 elif order_field.startswith("-") and pfx == "-": 

379 ordering.append(order_field[1:]) 

380 else: 

381 ordering.append(pfx + order_field) 

382 except (IndexError, ValueError): 

383 continue # Invalid ordering specified, skip it. 

384 

385 # Add the given query's ordering fields, if any. 

386 ordering.extend(queryset.query.order_by) 

387 

388 return self._get_deterministic_ordering(ordering) 

389 

390 def _get_deterministic_ordering(self, ordering): 

391 """ 

392 Ensure a deterministic order across all database backends. Search for a 

393 single field or unique together set of fields providing a total 

394 ordering. If these are missing, augment the ordering with a descendant 

395 primary key. 

396 """ 

397 ordering = list(ordering) 

398 ordering_fields = set() 

399 total_ordering_fields = {"pk"} | { 

400 field.attname 

401 for field in self.lookup_opts.fields 

402 if field.unique and not field.null 

403 } 

404 for part in ordering: 

405 # Search for single field providing a total ordering. 

406 field_name = None 

407 if isinstance(part, str): 

408 field_name = part.lstrip("-") 

409 elif isinstance(part, F): 

410 field_name = part.name 

411 elif isinstance(part, OrderBy) and isinstance(part.expression, F): 

412 field_name = part.expression.name 

413 if field_name: 

414 # Normalize attname references by using get_field(). 

415 try: 

416 field = self.lookup_opts.get_field(field_name) 

417 except FieldDoesNotExist: 

418 # Could be "?" for random ordering or a related field 

419 # lookup. Skip this part of introspection for now. 

420 continue 

421 # Ordering by a related field name orders by the referenced 

422 # model's ordering. Skip this part of introspection for now. 

423 if field.remote_field and field_name == field.name: 

424 continue 

425 if field.attname in total_ordering_fields: 

426 break 

427 ordering_fields.add(field.attname) 

428 else: 

429 # No single total ordering field, try unique_together and total 

430 # unique constraints. 

431 constraint_field_names = ( 

432 *self.lookup_opts.unique_together, 

433 *( 

434 constraint.fields 

435 for constraint in self.lookup_opts.total_unique_constraints 

436 ), 

437 ) 

438 for field_names in constraint_field_names: 

439 # Normalize attname references by using get_field(). 

440 fields = [ 

441 self.lookup_opts.get_field(field_name) for field_name in field_names 

442 ] 

443 # Composite unique constraints containing a nullable column 

444 # cannot ensure total ordering. 

445 if any(field.null for field in fields): 

446 continue 

447 if ordering_fields.issuperset(field.attname for field in fields): 

448 break 

449 else: 

450 # If no set of unique fields is present in the ordering, rely 

451 # on the primary key to provide total ordering. 

452 ordering.append("-pk") 

453 return ordering 

454 

455 def get_ordering_field_columns(self): 

456 """ 

457 Return a dictionary of ordering field column numbers and asc/desc. 

458 """ 

459 # We must cope with more than one column having the same underlying sort 

460 # field, so we base things on column numbers. 

461 ordering = self._get_default_ordering() 

462 ordering_fields = {} 

463 if ORDER_VAR not in self.params: 

464 # for ordering specified on ModelAdmin or model Meta, we don't know 

465 # the right column numbers absolutely, because there might be more 

466 # than one column associated with that ordering, so we guess. 

467 for field in ordering: 

468 if isinstance(field, (Combinable, OrderBy)): 

469 if not isinstance(field, OrderBy): 

470 field = field.asc() 

471 if isinstance(field.expression, F): 

472 order_type = "desc" if field.descending else "asc" 

473 field = field.expression.name 

474 else: 

475 continue 

476 elif field.startswith("-"): 

477 field = field[1:] 

478 order_type = "desc" 

479 else: 

480 order_type = "asc" 

481 for index, attr in enumerate(self.list_display): 

482 if self.get_ordering_field(attr) == field: 

483 ordering_fields[index] = order_type 

484 break 

485 else: 

486 for p in self.params[ORDER_VAR].split("."): 

487 none, pfx, idx = p.rpartition("-") 

488 try: 

489 idx = int(idx) 

490 except ValueError: 

491 continue # skip it 

492 ordering_fields[idx] = "desc" if pfx == "-" else "asc" 

493 return ordering_fields 

494 

495 def get_queryset(self, request): 

496 # First, we collect all the declared list filters. 

497 ( 

498 self.filter_specs, 

499 self.has_filters, 

500 remaining_lookup_params, 

501 filters_may_have_duplicates, 

502 self.has_active_filters, 

503 ) = self.get_filters(request) 

504 # Then, we let every list filter modify the queryset to its liking. 

505 qs = self.root_queryset 

506 for filter_spec in self.filter_specs: 

507 new_qs = filter_spec.queryset(request, qs) 

508 if new_qs is not None: 

509 qs = new_qs 

510 

511 try: 

512 # Finally, we apply the remaining lookup parameters from the query 

513 # string (i.e. those that haven't already been processed by the 

514 # filters). 

515 qs = qs.filter(**remaining_lookup_params) 

516 except (SuspiciousOperation, ImproperlyConfigured): 

517 # Allow certain types of errors to be re-raised as-is so that the 

518 # caller can treat them in a special way. 

519 raise 

520 except Exception as e: 

521 # Every other error is caught with a naked except, because we don't 

522 # have any other way of validating lookup parameters. They might be 

523 # invalid if the keyword arguments are incorrect, or if the values 

524 # are not in the correct type, so we might get FieldError, 

525 # ValueError, ValidationError, or ?. 

526 raise IncorrectLookupParameters(e) 

527 

528 # Apply search results 

529 qs, search_may_have_duplicates = self.model_admin.get_search_results( 

530 request, 

531 qs, 

532 self.query, 

533 ) 

534 

535 # Set query string for clearing all filters. 

536 self.clear_all_filters_qs = self.get_query_string( 

537 new_params=remaining_lookup_params, 

538 remove=self.get_filters_params(), 

539 ) 

540 # Remove duplicates from results, if necessary 

541 if filters_may_have_duplicates | search_may_have_duplicates: 

542 qs = qs.filter(pk=OuterRef("pk")) 

543 qs = self.root_queryset.filter(Exists(qs)) 

544 

545 # Set ordering. 

546 ordering = self.get_ordering(request, qs) 

547 qs = qs.order_by(*ordering) 

548 

549 if not qs.query.select_related: 

550 qs = self.apply_select_related(qs) 

551 

552 return qs 

553 

554 def apply_select_related(self, qs): 

555 if self.list_select_related is True: 

556 return qs.select_related() 

557 

558 if self.list_select_related is False: 

559 if self.has_related_field_in_list_display(): 

560 return qs.select_related() 

561 

562 if self.list_select_related: 

563 return qs.select_related(*self.list_select_related) 

564 return qs 

565 

566 def has_related_field_in_list_display(self): 

567 for field_name in self.list_display: 

568 try: 

569 field = self.lookup_opts.get_field(field_name) 

570 except FieldDoesNotExist: 

571 pass 

572 else: 

573 if isinstance(field.remote_field, ManyToOneRel): 

574 # <FK>_id field names don't require a join. 

575 if field_name != field.get_attname(): 

576 return True 

577 return False 

578 

579 def url_for_result(self, result): 

580 pk = getattr(result, self.pk_attname) 

581 return reverse( 

582 "admin:%s_%s_change" % (self.opts.app_label, self.opts.model_name), 

583 args=(quote(pk),), 

584 current_app=self.model_admin.admin_site.name, 

585 )