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

1059 statements  

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

1import copy 

2import json 

3import re 

4from functools import partial, update_wrapper 

5from urllib.parse import quote as urlquote 

6 

7from django import forms 

8from django.conf import settings 

9from django.contrib import messages 

10from django.contrib.admin import helpers, widgets 

11from django.contrib.admin.checks import ( 

12 BaseModelAdminChecks, 

13 InlineModelAdminChecks, 

14 ModelAdminChecks, 

15) 

16from django.contrib.admin.decorators import display 

17from django.contrib.admin.exceptions import DisallowedModelAdminToField 

18from django.contrib.admin.templatetags.admin_urls import add_preserved_filters 

19from django.contrib.admin.utils import ( 

20 NestedObjects, 

21 construct_change_message, 

22 flatten_fieldsets, 

23 get_deleted_objects, 

24 lookup_spawns_duplicates, 

25 model_format_dict, 

26 model_ngettext, 

27 quote, 

28 unquote, 

29) 

30from django.contrib.admin.widgets import AutocompleteSelect, AutocompleteSelectMultiple 

31from django.contrib.auth import get_permission_codename 

32from django.core.exceptions import ( 

33 FieldDoesNotExist, 

34 FieldError, 

35 PermissionDenied, 

36 ValidationError, 

37) 

38from django.core.paginator import Paginator 

39from django.db import models, router, transaction 

40from django.db.models.constants import LOOKUP_SEP 

41from django.forms.formsets import DELETION_FIELD_NAME, all_valid 

42from django.forms.models import ( 

43 BaseInlineFormSet, 

44 inlineformset_factory, 

45 modelform_defines_fields, 

46 modelform_factory, 

47 modelformset_factory, 

48) 

49from django.forms.widgets import CheckboxSelectMultiple, SelectMultiple 

50from django.http import HttpResponseRedirect 

51from django.http.response import HttpResponseBase 

52from django.template.response import SimpleTemplateResponse, TemplateResponse 

53from django.urls import reverse 

54from django.utils.decorators import method_decorator 

55from django.utils.html import format_html 

56from django.utils.http import urlencode 

57from django.utils.safestring import mark_safe 

58from django.utils.text import ( 

59 capfirst, 

60 format_lazy, 

61 get_text_list, 

62 smart_split, 

63 unescape_string_literal, 

64) 

65from django.utils.translation import gettext as _ 

66from django.utils.translation import ngettext 

67from django.views.decorators.csrf import csrf_protect 

68from django.views.generic import RedirectView 

69 

70IS_POPUP_VAR = "_popup" 

71TO_FIELD_VAR = "_to_field" 

72 

73 

74HORIZONTAL, VERTICAL = 1, 2 

75 

76 

77def get_content_type_for_model(obj): 

78 # Since this module gets imported in the application's root package, 

79 # it cannot import models from other applications at the module level. 

80 from django.contrib.contenttypes.models import ContentType 

81 

82 return ContentType.objects.get_for_model(obj, for_concrete_model=False) 

83 

84 

85def get_ul_class(radio_style): 

86 return "radiolist" if radio_style == VERTICAL else "radiolist inline" 

87 

88 

89class IncorrectLookupParameters(Exception): 

90 pass 

91 

92 

93# Defaults for formfield_overrides. ModelAdmin subclasses can change this 

94# by adding to ModelAdmin.formfield_overrides. 

95 

96FORMFIELD_FOR_DBFIELD_DEFAULTS = { 

97 models.DateTimeField: { 

98 "form_class": forms.SplitDateTimeField, 

99 "widget": widgets.AdminSplitDateTime, 

100 }, 

101 models.DateField: {"widget": widgets.AdminDateWidget}, 

102 models.TimeField: {"widget": widgets.AdminTimeWidget}, 

103 models.TextField: {"widget": widgets.AdminTextareaWidget}, 

104 models.URLField: {"widget": widgets.AdminURLFieldWidget}, 

105 models.IntegerField: {"widget": widgets.AdminIntegerFieldWidget}, 

106 models.BigIntegerField: {"widget": widgets.AdminBigIntegerFieldWidget}, 

107 models.CharField: {"widget": widgets.AdminTextInputWidget}, 

108 models.ImageField: {"widget": widgets.AdminFileWidget}, 

109 models.FileField: {"widget": widgets.AdminFileWidget}, 

110 models.EmailField: {"widget": widgets.AdminEmailInputWidget}, 

111 models.UUIDField: {"widget": widgets.AdminUUIDInputWidget}, 

112} 

113 

114csrf_protect_m = method_decorator(csrf_protect) 

115 

116 

117class BaseModelAdmin(metaclass=forms.MediaDefiningClass): 

118 """Functionality common to both ModelAdmin and InlineAdmin.""" 

119 

120 autocomplete_fields = () 

121 raw_id_fields = () 

122 fields = None 

123 exclude = None 

124 fieldsets = None 

125 form = forms.ModelForm 

126 filter_vertical = () 

127 filter_horizontal = () 

128 radio_fields = {} 

129 prepopulated_fields = {} 

130 formfield_overrides = {} 

131 readonly_fields = () 

132 ordering = None 

133 sortable_by = None 

134 view_on_site = True 

135 show_full_result_count = True 

136 checks_class = BaseModelAdminChecks 

137 

138 def check(self, **kwargs): 

139 return self.checks_class().check(self, **kwargs) 

140 

141 def __init__(self): 

142 # Merge FORMFIELD_FOR_DBFIELD_DEFAULTS with the formfield_overrides 

143 # rather than simply overwriting. 

144 overrides = copy.deepcopy(FORMFIELD_FOR_DBFIELD_DEFAULTS) 

145 for k, v in self.formfield_overrides.items(): 145 ↛ 146line 145 didn't jump to line 146, because the loop on line 145 never started

146 overrides.setdefault(k, {}).update(v) 

147 self.formfield_overrides = overrides 

148 

149 def formfield_for_dbfield(self, db_field, request, **kwargs): 

150 """ 

151 Hook for specifying the form Field instance for a given database Field 

152 instance. 

153 

154 If kwargs are given, they're passed to the form Field's constructor. 

155 """ 

156 # If the field specifies choices, we don't need to look for special 

157 # admin widgets - we just need to use a select widget of some kind. 

158 if db_field.choices: 

159 return self.formfield_for_choice_field(db_field, request, **kwargs) 

160 

161 # ForeignKey or ManyToManyFields 

162 if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)): 

163 # Combine the field kwargs with any options for formfield_overrides. 

164 # Make sure the passed in **kwargs override anything in 

165 # formfield_overrides because **kwargs is more specific, and should 

166 # always win. 

167 if db_field.__class__ in self.formfield_overrides: 

168 kwargs = {**self.formfield_overrides[db_field.__class__], **kwargs} 

169 

170 # Get the correct formfield. 

171 if isinstance(db_field, models.ForeignKey): 

172 formfield = self.formfield_for_foreignkey(db_field, request, **kwargs) 

173 elif isinstance(db_field, models.ManyToManyField): 

174 formfield = self.formfield_for_manytomany(db_field, request, **kwargs) 

175 

176 # For non-raw_id fields, wrap the widget with a wrapper that adds 

177 # extra HTML -- the "add other" interface -- to the end of the 

178 # rendered output. formfield can be None if it came from a 

179 # OneToOneField with parent_link=True or a M2M intermediary. 

180 if formfield and db_field.name not in self.raw_id_fields: 

181 related_modeladmin = self.admin_site._registry.get( 

182 db_field.remote_field.model 

183 ) 

184 wrapper_kwargs = {} 

185 if related_modeladmin: 

186 wrapper_kwargs.update( 

187 can_add_related=related_modeladmin.has_add_permission(request), 

188 can_change_related=related_modeladmin.has_change_permission( 

189 request 

190 ), 

191 can_delete_related=related_modeladmin.has_delete_permission( 

192 request 

193 ), 

194 can_view_related=related_modeladmin.has_view_permission( 

195 request 

196 ), 

197 ) 

198 formfield.widget = widgets.RelatedFieldWidgetWrapper( 

199 formfield.widget, 

200 db_field.remote_field, 

201 self.admin_site, 

202 **wrapper_kwargs, 

203 ) 

204 

205 return formfield 

206 

207 # If we've got overrides for the formfield defined, use 'em. **kwargs 

208 # passed to formfield_for_dbfield override the defaults. 

209 for klass in db_field.__class__.mro(): 

210 if klass in self.formfield_overrides: 

211 kwargs = {**copy.deepcopy(self.formfield_overrides[klass]), **kwargs} 

212 return db_field.formfield(**kwargs) 

213 

214 # For any other type of field, just call its formfield() method. 

215 return db_field.formfield(**kwargs) 

216 

217 def formfield_for_choice_field(self, db_field, request, **kwargs): 

218 """ 

219 Get a form Field for a database Field that has declared choices. 

220 """ 

221 # If the field is named as a radio_field, use a RadioSelect 

222 if db_field.name in self.radio_fields: 

223 # Avoid stomping on custom widget/choices arguments. 

224 if "widget" not in kwargs: 

225 kwargs["widget"] = widgets.AdminRadioSelect( 

226 attrs={ 

227 "class": get_ul_class(self.radio_fields[db_field.name]), 

228 } 

229 ) 

230 if "choices" not in kwargs: 

231 kwargs["choices"] = db_field.get_choices( 

232 include_blank=db_field.blank, blank_choice=[("", _("None"))] 

233 ) 

234 return db_field.formfield(**kwargs) 

235 

236 def get_field_queryset(self, db, db_field, request): 

237 """ 

238 If the ModelAdmin specifies ordering, the queryset should respect that 

239 ordering. Otherwise don't specify the queryset, let the field decide 

240 (return None in that case). 

241 """ 

242 related_admin = self.admin_site._registry.get(db_field.remote_field.model) 

243 if related_admin is not None: 

244 ordering = related_admin.get_ordering(request) 

245 if ordering is not None and ordering != (): 

246 return db_field.remote_field.model._default_manager.using(db).order_by( 

247 *ordering 

248 ) 

249 return None 

250 

251 def formfield_for_foreignkey(self, db_field, request, **kwargs): 

252 """ 

253 Get a form Field for a ForeignKey. 

254 """ 

255 db = kwargs.get("using") 

256 

257 if "widget" not in kwargs: 

258 if db_field.name in self.get_autocomplete_fields(request): 

259 kwargs["widget"] = AutocompleteSelect( 

260 db_field, self.admin_site, using=db 

261 ) 

262 elif db_field.name in self.raw_id_fields: 

263 kwargs["widget"] = widgets.ForeignKeyRawIdWidget( 

264 db_field.remote_field, self.admin_site, using=db 

265 ) 

266 elif db_field.name in self.radio_fields: 

267 kwargs["widget"] = widgets.AdminRadioSelect( 

268 attrs={ 

269 "class": get_ul_class(self.radio_fields[db_field.name]), 

270 } 

271 ) 

272 kwargs["empty_label"] = _("None") if db_field.blank else None 

273 

274 if "queryset" not in kwargs: 

275 queryset = self.get_field_queryset(db, db_field, request) 

276 if queryset is not None: 

277 kwargs["queryset"] = queryset 

278 

279 return db_field.formfield(**kwargs) 

280 

281 def formfield_for_manytomany(self, db_field, request, **kwargs): 

282 """ 

283 Get a form Field for a ManyToManyField. 

284 """ 

285 # If it uses an intermediary model that isn't auto created, don't show 

286 # a field in admin. 

287 if not db_field.remote_field.through._meta.auto_created: 

288 return None 

289 db = kwargs.get("using") 

290 

291 if "widget" not in kwargs: 

292 autocomplete_fields = self.get_autocomplete_fields(request) 

293 if db_field.name in autocomplete_fields: 

294 kwargs["widget"] = AutocompleteSelectMultiple( 

295 db_field, 

296 self.admin_site, 

297 using=db, 

298 ) 

299 elif db_field.name in self.raw_id_fields: 

300 kwargs["widget"] = widgets.ManyToManyRawIdWidget( 

301 db_field.remote_field, 

302 self.admin_site, 

303 using=db, 

304 ) 

305 elif db_field.name in [*self.filter_vertical, *self.filter_horizontal]: 

306 kwargs["widget"] = widgets.FilteredSelectMultiple( 

307 db_field.verbose_name, db_field.name in self.filter_vertical 

308 ) 

309 if "queryset" not in kwargs: 

310 queryset = self.get_field_queryset(db, db_field, request) 

311 if queryset is not None: 

312 kwargs["queryset"] = queryset 

313 

314 form_field = db_field.formfield(**kwargs) 

315 if isinstance(form_field.widget, SelectMultiple) and not isinstance( 

316 form_field.widget, (CheckboxSelectMultiple, AutocompleteSelectMultiple) 

317 ): 

318 msg = _( 

319 "Hold down “Control”, or “Command” on a Mac, to select more than one." 

320 ) 

321 help_text = form_field.help_text 

322 form_field.help_text = ( 

323 format_lazy("{} {}", help_text, msg) if help_text else msg 

324 ) 

325 return form_field 

326 

327 def get_autocomplete_fields(self, request): 

328 """ 

329 Return a list of ForeignKey and/or ManyToMany fields which should use 

330 an autocomplete widget. 

331 """ 

332 return self.autocomplete_fields 

333 

334 def get_view_on_site_url(self, obj=None): 

335 if obj is None or not self.view_on_site: 

336 return None 

337 

338 if callable(self.view_on_site): 

339 return self.view_on_site(obj) 

340 elif hasattr(obj, "get_absolute_url"): 

341 # use the ContentType lookup if view_on_site is True 

342 return reverse( 

343 "admin:view_on_site", 

344 kwargs={ 

345 "content_type_id": get_content_type_for_model(obj).pk, 

346 "object_id": obj.pk, 

347 }, 

348 ) 

349 

350 def get_empty_value_display(self): 

351 """ 

352 Return the empty_value_display set on ModelAdmin or AdminSite. 

353 """ 

354 try: 

355 return mark_safe(self.empty_value_display) 

356 except AttributeError: 

357 return mark_safe(self.admin_site.empty_value_display) 

358 

359 def get_exclude(self, request, obj=None): 

360 """ 

361 Hook for specifying exclude. 

362 """ 

363 return self.exclude 

364 

365 def get_fields(self, request, obj=None): 

366 """ 

367 Hook for specifying fields. 

368 """ 

369 if self.fields: 

370 return self.fields 

371 # _get_form_for_get_fields() is implemented in subclasses. 

372 form = self._get_form_for_get_fields(request, obj) 

373 return [*form.base_fields, *self.get_readonly_fields(request, obj)] 

374 

375 def get_fieldsets(self, request, obj=None): 

376 """ 

377 Hook for specifying fieldsets. 

378 """ 

379 if self.fieldsets: 

380 return self.fieldsets 

381 return [(None, {"fields": self.get_fields(request, obj)})] 

382 

383 def get_inlines(self, request, obj): 

384 """Hook for specifying custom inlines.""" 

385 return self.inlines 

386 

387 def get_ordering(self, request): 

388 """ 

389 Hook for specifying field ordering. 

390 """ 

391 return self.ordering or () # otherwise we might try to *None, which is bad ;) 

392 

393 def get_readonly_fields(self, request, obj=None): 

394 """ 

395 Hook for specifying custom readonly fields. 

396 """ 

397 return self.readonly_fields 

398 

399 def get_prepopulated_fields(self, request, obj=None): 

400 """ 

401 Hook for specifying custom prepopulated fields. 

402 """ 

403 return self.prepopulated_fields 

404 

405 def get_queryset(self, request): 

406 """ 

407 Return a QuerySet of all model instances that can be edited by the 

408 admin site. This is used by changelist_view. 

409 """ 

410 qs = self.model._default_manager.get_queryset() 

411 # TODO: this should be handled by some parameter to the ChangeList. 

412 ordering = self.get_ordering(request) 

413 if ordering: 

414 qs = qs.order_by(*ordering) 

415 return qs 

416 

417 def get_sortable_by(self, request): 

418 """Hook for specifying which fields can be sorted in the changelist.""" 

419 return ( 

420 self.sortable_by 

421 if self.sortable_by is not None 

422 else self.get_list_display(request) 

423 ) 

424 

425 def lookup_allowed(self, lookup, value): 

426 from django.contrib.admin.filters import SimpleListFilter 

427 

428 model = self.model 

429 # Check FKey lookups that are allowed, so that popups produced by 

430 # ForeignKeyRawIdWidget, on the basis of ForeignKey.limit_choices_to, 

431 # are allowed to work. 

432 for fk_lookup in model._meta.related_fkey_lookups: 

433 # As ``limit_choices_to`` can be a callable, invoke it here. 

434 if callable(fk_lookup): 

435 fk_lookup = fk_lookup() 

436 if (lookup, value) in widgets.url_params_from_lookup_dict( 

437 fk_lookup 

438 ).items(): 

439 return True 

440 

441 relation_parts = [] 

442 prev_field = None 

443 for part in lookup.split(LOOKUP_SEP): 

444 try: 

445 field = model._meta.get_field(part) 

446 except FieldDoesNotExist: 

447 # Lookups on nonexistent fields are ok, since they're ignored 

448 # later. 

449 break 

450 # It is allowed to filter on values that would be found from local 

451 # model anyways. For example, if you filter on employee__department__id, 

452 # then the id value would be found already from employee__department_id. 

453 if not prev_field or ( 

454 prev_field.is_relation 

455 and field not in prev_field.get_path_info()[-1].target_fields 

456 ): 

457 relation_parts.append(part) 

458 if not getattr(field, "get_path_info", None): 

459 # This is not a relational field, so further parts 

460 # must be transforms. 

461 break 

462 prev_field = field 

463 model = field.get_path_info()[-1].to_opts.model 

464 

465 if len(relation_parts) <= 1: 

466 # Either a local field filter, or no fields at all. 

467 return True 

468 valid_lookups = {self.date_hierarchy} 

469 for filter_item in self.list_filter: 

470 if isinstance(filter_item, type) and issubclass( 

471 filter_item, SimpleListFilter 

472 ): 

473 valid_lookups.add(filter_item.parameter_name) 

474 elif isinstance(filter_item, (list, tuple)): 

475 valid_lookups.add(filter_item[0]) 

476 else: 

477 valid_lookups.add(filter_item) 

478 

479 # Is it a valid relational lookup? 

480 return not { 

481 LOOKUP_SEP.join(relation_parts), 

482 LOOKUP_SEP.join(relation_parts + [part]), 

483 }.isdisjoint(valid_lookups) 

484 

485 def to_field_allowed(self, request, to_field): 

486 """ 

487 Return True if the model associated with this admin should be 

488 allowed to be referenced by the specified field. 

489 """ 

490 opts = self.model._meta 

491 

492 try: 

493 field = opts.get_field(to_field) 

494 except FieldDoesNotExist: 

495 return False 

496 

497 # Always allow referencing the primary key since it's already possible 

498 # to get this information from the change view URL. 

499 if field.primary_key: 

500 return True 

501 

502 # Allow reverse relationships to models defining m2m fields if they 

503 # target the specified field. 

504 for many_to_many in opts.many_to_many: 

505 if many_to_many.m2m_target_field_name() == to_field: 

506 return True 

507 

508 # Make sure at least one of the models registered for this site 

509 # references this field through a FK or a M2M relationship. 

510 registered_models = set() 

511 for model, admin in self.admin_site._registry.items(): 

512 registered_models.add(model) 

513 for inline in admin.inlines: 

514 registered_models.add(inline.model) 

515 

516 related_objects = ( 

517 f 

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

519 if (f.auto_created and not f.concrete) 

520 ) 

521 for related_object in related_objects: 

522 related_model = related_object.related_model 

523 remote_field = related_object.field.remote_field 

524 if ( 

525 any(issubclass(model, related_model) for model in registered_models) 

526 and hasattr(remote_field, "get_related_field") 

527 and remote_field.get_related_field() == field 

528 ): 

529 return True 

530 

531 return False 

532 

533 def has_add_permission(self, request): 

534 """ 

535 Return True if the given request has permission to add an object. 

536 Can be overridden by the user in subclasses. 

537 """ 

538 opts = self.opts 

539 codename = get_permission_codename("add", opts) 

540 return request.user.has_perm("%s.%s" % (opts.app_label, codename)) 

541 

542 def has_change_permission(self, request, obj=None): 

543 """ 

544 Return True if the given request has permission to change the given 

545 Django model instance, the default implementation doesn't examine the 

546 `obj` parameter. 

547 

548 Can be overridden by the user in subclasses. In such case it should 

549 return True if the given request has permission to change the `obj` 

550 model instance. If `obj` is None, this should return True if the given 

551 request has permission to change *any* object of the given type. 

552 """ 

553 opts = self.opts 

554 codename = get_permission_codename("change", opts) 

555 return request.user.has_perm("%s.%s" % (opts.app_label, codename)) 

556 

557 def has_delete_permission(self, request, obj=None): 

558 """ 

559 Return True if the given request has permission to change the given 

560 Django model instance, the default implementation doesn't examine the 

561 `obj` parameter. 

562 

563 Can be overridden by the user in subclasses. In such case it should 

564 return True if the given request has permission to delete the `obj` 

565 model instance. If `obj` is None, this should return True if the given 

566 request has permission to delete *any* object of the given type. 

567 """ 

568 opts = self.opts 

569 codename = get_permission_codename("delete", opts) 

570 return request.user.has_perm("%s.%s" % (opts.app_label, codename)) 

571 

572 def has_view_permission(self, request, obj=None): 

573 """ 

574 Return True if the given request has permission to view the given 

575 Django model instance. The default implementation doesn't examine the 

576 `obj` parameter. 

577 

578 If overridden by the user in subclasses, it should return True if the 

579 given request has permission to view the `obj` model instance. If `obj` 

580 is None, it should return True if the request has permission to view 

581 any object of the given type. 

582 """ 

583 opts = self.opts 

584 codename_view = get_permission_codename("view", opts) 

585 codename_change = get_permission_codename("change", opts) 

586 return request.user.has_perm( 

587 "%s.%s" % (opts.app_label, codename_view) 

588 ) or request.user.has_perm("%s.%s" % (opts.app_label, codename_change)) 

589 

590 def has_view_or_change_permission(self, request, obj=None): 

591 return self.has_view_permission(request, obj) or self.has_change_permission( 

592 request, obj 

593 ) 

594 

595 def has_module_permission(self, request): 

596 """ 

597 Return True if the given request has any permission in the given 

598 app label. 

599 

600 Can be overridden by the user in subclasses. In such case it should 

601 return True if the given request has permission to view the module on 

602 the admin index page and access the module's index page. Overriding it 

603 does not restrict access to the add, change or delete views. Use 

604 `ModelAdmin.has_(add|change|delete)_permission` for that. 

605 """ 

606 return request.user.has_module_perms(self.opts.app_label) 

607 

608 

609class ModelAdmin(BaseModelAdmin): 

610 """Encapsulate all admin options and functionality for a given model.""" 

611 

612 list_display = ("__str__",) 

613 list_display_links = () 

614 list_filter = () 

615 list_select_related = False 

616 list_per_page = 100 

617 list_max_show_all = 200 

618 list_editable = () 

619 search_fields = () 

620 search_help_text = None 

621 date_hierarchy = None 

622 save_as = False 

623 save_as_continue = True 

624 save_on_top = False 

625 paginator = Paginator 

626 preserve_filters = True 

627 inlines = [] 

628 

629 # Custom templates (designed to be over-ridden in subclasses) 

630 add_form_template = None 

631 change_form_template = None 

632 change_list_template = None 

633 delete_confirmation_template = None 

634 delete_selected_confirmation_template = None 

635 object_history_template = None 

636 popup_response_template = None 

637 

638 # Actions 

639 actions = [] 

640 action_form = helpers.ActionForm 

641 actions_on_top = True 

642 actions_on_bottom = False 

643 actions_selection_counter = True 

644 checks_class = ModelAdminChecks 

645 

646 def __init__(self, model, admin_site): 

647 self.model = model 

648 self.opts = model._meta 

649 self.admin_site = admin_site 

650 super().__init__() 

651 

652 def __str__(self): 

653 return "%s.%s" % (self.model._meta.app_label, self.__class__.__name__) 

654 

655 def __repr__(self): 

656 return ( 

657 f"<{self.__class__.__qualname__}: model={self.model.__qualname__} " 

658 f"site={self.admin_site!r}>" 

659 ) 

660 

661 def get_inline_instances(self, request, obj=None): 

662 inline_instances = [] 

663 for inline_class in self.get_inlines(request, obj): 

664 inline = inline_class(self.model, self.admin_site) 

665 if request: 

666 if not ( 

667 inline.has_view_or_change_permission(request, obj) 

668 or inline.has_add_permission(request, obj) 

669 or inline.has_delete_permission(request, obj) 

670 ): 

671 continue 

672 if not inline.has_add_permission(request, obj): 

673 inline.max_num = 0 

674 inline_instances.append(inline) 

675 

676 return inline_instances 

677 

678 def get_urls(self): 

679 from django.urls import path 

680 

681 def wrap(view): 

682 def wrapper(*args, **kwargs): 

683 return self.admin_site.admin_view(view)(*args, **kwargs) 

684 

685 wrapper.model_admin = self 

686 return update_wrapper(wrapper, view) 

687 

688 info = self.model._meta.app_label, self.model._meta.model_name 

689 

690 return [ 

691 path("", wrap(self.changelist_view), name="%s_%s_changelist" % info), 

692 path("add/", wrap(self.add_view), name="%s_%s_add" % info), 

693 path( 

694 "<path:object_id>/history/", 

695 wrap(self.history_view), 

696 name="%s_%s_history" % info, 

697 ), 

698 path( 

699 "<path:object_id>/delete/", 

700 wrap(self.delete_view), 

701 name="%s_%s_delete" % info, 

702 ), 

703 path( 

704 "<path:object_id>/change/", 

705 wrap(self.change_view), 

706 name="%s_%s_change" % info, 

707 ), 

708 # For backwards compatibility (was the change url before 1.9) 

709 path( 

710 "<path:object_id>/", 

711 wrap( 

712 RedirectView.as_view( 

713 pattern_name="%s:%s_%s_change" 

714 % ((self.admin_site.name,) + info) 

715 ) 

716 ), 

717 ), 

718 ] 

719 

720 @property 

721 def urls(self): 

722 return self.get_urls() 

723 

724 @property 

725 def media(self): 

726 extra = "" if settings.DEBUG else ".min" 

727 js = [ 

728 "vendor/jquery/jquery%s.js" % extra, 

729 "jquery.init.js", 

730 "core.js", 

731 "admin/RelatedObjectLookups.js", 

732 "actions.js", 

733 "urlify.js", 

734 "prepopulate.js", 

735 "vendor/xregexp/xregexp%s.js" % extra, 

736 ] 

737 return forms.Media(js=["admin/js/%s" % url for url in js]) 

738 

739 def get_model_perms(self, request): 

740 """ 

741 Return a dict of all perms for this model. This dict has the keys 

742 ``add``, ``change``, ``delete``, and ``view`` mapping to the True/False 

743 for each of those actions. 

744 """ 

745 return { 

746 "add": self.has_add_permission(request), 

747 "change": self.has_change_permission(request), 

748 "delete": self.has_delete_permission(request), 

749 "view": self.has_view_permission(request), 

750 } 

751 

752 def _get_form_for_get_fields(self, request, obj): 

753 return self.get_form(request, obj, fields=None) 

754 

755 def get_form(self, request, obj=None, change=False, **kwargs): 

756 """ 

757 Return a Form class for use in the admin add view. This is used by 

758 add_view and change_view. 

759 """ 

760 if "fields" in kwargs: 

761 fields = kwargs.pop("fields") 

762 else: 

763 fields = flatten_fieldsets(self.get_fieldsets(request, obj)) 

764 excluded = self.get_exclude(request, obj) 

765 exclude = [] if excluded is None else list(excluded) 

766 readonly_fields = self.get_readonly_fields(request, obj) 

767 exclude.extend(readonly_fields) 

768 # Exclude all fields if it's a change form and the user doesn't have 

769 # the change permission. 

770 if ( 

771 change 

772 and hasattr(request, "user") 

773 and not self.has_change_permission(request, obj) 

774 ): 

775 exclude.extend(fields) 

776 if excluded is None and hasattr(self.form, "_meta") and self.form._meta.exclude: 

777 # Take the custom ModelForm's Meta.exclude into account only if the 

778 # ModelAdmin doesn't define its own. 

779 exclude.extend(self.form._meta.exclude) 

780 # if exclude is an empty list we pass None to be consistent with the 

781 # default on modelform_factory 

782 exclude = exclude or None 

783 

784 # Remove declared form fields which are in readonly_fields. 

785 new_attrs = dict.fromkeys( 

786 f for f in readonly_fields if f in self.form.declared_fields 

787 ) 

788 form = type(self.form.__name__, (self.form,), new_attrs) 

789 

790 defaults = { 

791 "form": form, 

792 "fields": fields, 

793 "exclude": exclude, 

794 "formfield_callback": partial(self.formfield_for_dbfield, request=request), 

795 **kwargs, 

796 } 

797 

798 if defaults["fields"] is None and not modelform_defines_fields( 

799 defaults["form"] 

800 ): 

801 defaults["fields"] = forms.ALL_FIELDS 

802 

803 try: 

804 return modelform_factory(self.model, **defaults) 

805 except FieldError as e: 

806 raise FieldError( 

807 "%s. Check fields/fieldsets/exclude attributes of class %s." 

808 % (e, self.__class__.__name__) 

809 ) 

810 

811 def get_changelist(self, request, **kwargs): 

812 """ 

813 Return the ChangeList class for use on the changelist page. 

814 """ 

815 from django.contrib.admin.views.main import ChangeList 

816 

817 return ChangeList 

818 

819 def get_changelist_instance(self, request): 

820 """ 

821 Return a `ChangeList` instance based on `request`. May raise 

822 `IncorrectLookupParameters`. 

823 """ 

824 list_display = self.get_list_display(request) 

825 list_display_links = self.get_list_display_links(request, list_display) 

826 # Add the action checkboxes if any actions are available. 

827 if self.get_actions(request): 

828 list_display = ["action_checkbox", *list_display] 

829 sortable_by = self.get_sortable_by(request) 

830 ChangeList = self.get_changelist(request) 

831 return ChangeList( 

832 request, 

833 self.model, 

834 list_display, 

835 list_display_links, 

836 self.get_list_filter(request), 

837 self.date_hierarchy, 

838 self.get_search_fields(request), 

839 self.get_list_select_related(request), 

840 self.list_per_page, 

841 self.list_max_show_all, 

842 self.list_editable, 

843 self, 

844 sortable_by, 

845 self.search_help_text, 

846 ) 

847 

848 def get_object(self, request, object_id, from_field=None): 

849 """ 

850 Return an instance matching the field and value provided, the primary 

851 key is used if no field is provided. Return ``None`` if no match is 

852 found or the object_id fails validation. 

853 """ 

854 queryset = self.get_queryset(request) 

855 model = queryset.model 

856 field = ( 

857 model._meta.pk if from_field is None else model._meta.get_field(from_field) 

858 ) 

859 try: 

860 object_id = field.to_python(object_id) 

861 return queryset.get(**{field.name: object_id}) 

862 except (model.DoesNotExist, ValidationError, ValueError): 

863 return None 

864 

865 def get_changelist_form(self, request, **kwargs): 

866 """ 

867 Return a Form class for use in the Formset on the changelist page. 

868 """ 

869 defaults = { 

870 "formfield_callback": partial(self.formfield_for_dbfield, request=request), 

871 **kwargs, 

872 } 

873 if defaults.get("fields") is None and not modelform_defines_fields( 

874 defaults.get("form") 

875 ): 

876 defaults["fields"] = forms.ALL_FIELDS 

877 

878 return modelform_factory(self.model, **defaults) 

879 

880 def get_changelist_formset(self, request, **kwargs): 

881 """ 

882 Return a FormSet class for use on the changelist page if list_editable 

883 is used. 

884 """ 

885 defaults = { 

886 "formfield_callback": partial(self.formfield_for_dbfield, request=request), 

887 **kwargs, 

888 } 

889 return modelformset_factory( 

890 self.model, 

891 self.get_changelist_form(request), 

892 extra=0, 

893 fields=self.list_editable, 

894 **defaults, 

895 ) 

896 

897 def get_formsets_with_inlines(self, request, obj=None): 

898 """ 

899 Yield formsets and the corresponding inlines. 

900 """ 

901 for inline in self.get_inline_instances(request, obj): 

902 yield inline.get_formset(request, obj), inline 

903 

904 def get_paginator( 

905 self, request, queryset, per_page, orphans=0, allow_empty_first_page=True 

906 ): 

907 return self.paginator(queryset, per_page, orphans, allow_empty_first_page) 

908 

909 def log_addition(self, request, obj, message): 

910 """ 

911 Log that an object has been successfully added. 

912 

913 The default implementation creates an admin LogEntry object. 

914 """ 

915 from django.contrib.admin.models import ADDITION, LogEntry 

916 

917 return LogEntry.objects.log_action( 

918 user_id=request.user.pk, 

919 content_type_id=get_content_type_for_model(obj).pk, 

920 object_id=obj.pk, 

921 object_repr=str(obj), 

922 action_flag=ADDITION, 

923 change_message=message, 

924 ) 

925 

926 def log_change(self, request, obj, message): 

927 """ 

928 Log that an object has been successfully changed. 

929 

930 The default implementation creates an admin LogEntry object. 

931 """ 

932 from django.contrib.admin.models import CHANGE, LogEntry 

933 

934 return LogEntry.objects.log_action( 

935 user_id=request.user.pk, 

936 content_type_id=get_content_type_for_model(obj).pk, 

937 object_id=obj.pk, 

938 object_repr=str(obj), 

939 action_flag=CHANGE, 

940 change_message=message, 

941 ) 

942 

943 def log_deletion(self, request, obj, object_repr): 

944 """ 

945 Log that an object will be deleted. Note that this method must be 

946 called before the deletion. 

947 

948 The default implementation creates an admin LogEntry object. 

949 """ 

950 from django.contrib.admin.models import DELETION, LogEntry 

951 

952 return LogEntry.objects.log_action( 

953 user_id=request.user.pk, 

954 content_type_id=get_content_type_for_model(obj).pk, 

955 object_id=obj.pk, 

956 object_repr=object_repr, 

957 action_flag=DELETION, 

958 ) 

959 

960 @display(description=mark_safe('<input type="checkbox" id="action-toggle">')) 

961 def action_checkbox(self, obj): 

962 """ 

963 A list_display column containing a checkbox widget. 

964 """ 

965 return helpers.checkbox.render(helpers.ACTION_CHECKBOX_NAME, str(obj.pk)) 

966 

967 @staticmethod 

968 def _get_action_description(func, name): 

969 return getattr(func, "short_description", capfirst(name.replace("_", " "))) 

970 

971 def _get_base_actions(self): 

972 """Return the list of actions, prior to any request-based filtering.""" 

973 actions = [] 

974 base_actions = (self.get_action(action) for action in self.actions or []) 

975 # get_action might have returned None, so filter any of those out. 

976 base_actions = [action for action in base_actions if action] 

977 base_action_names = {name for _, name, _ in base_actions} 

978 

979 # Gather actions from the admin site first 

980 for (name, func) in self.admin_site.actions: 

981 if name in base_action_names: 981 ↛ 982line 981 didn't jump to line 982, because the condition on line 981 was never true

982 continue 

983 description = self._get_action_description(func, name) 

984 actions.append((func, name, description)) 

985 # Add actions from this ModelAdmin. 

986 actions.extend(base_actions) 

987 return actions 

988 

989 def _filter_actions_by_permissions(self, request, actions): 

990 """Filter out any actions that the user doesn't have access to.""" 

991 filtered_actions = [] 

992 for action in actions: 

993 callable = action[0] 

994 if not hasattr(callable, "allowed_permissions"): 

995 filtered_actions.append(action) 

996 continue 

997 permission_checks = ( 

998 getattr(self, "has_%s_permission" % permission) 

999 for permission in callable.allowed_permissions 

1000 ) 

1001 if any(has_permission(request) for has_permission in permission_checks): 

1002 filtered_actions.append(action) 

1003 return filtered_actions 

1004 

1005 def get_actions(self, request): 

1006 """ 

1007 Return a dictionary mapping the names of all actions for this 

1008 ModelAdmin to a tuple of (callable, name, description) for each action. 

1009 """ 

1010 # If self.actions is set to None that means actions are disabled on 

1011 # this page. 

1012 if self.actions is None or IS_POPUP_VAR in request.GET: 

1013 return {} 

1014 actions = self._filter_actions_by_permissions(request, self._get_base_actions()) 

1015 return {name: (func, name, desc) for func, name, desc in actions} 

1016 

1017 def get_action_choices(self, request, default_choices=models.BLANK_CHOICE_DASH): 

1018 """ 

1019 Return a list of choices for use in a form object. Each choice is a 

1020 tuple (name, description). 

1021 """ 

1022 choices = [] + default_choices 

1023 for func, name, description in self.get_actions(request).values(): 

1024 choice = (name, description % model_format_dict(self.opts)) 

1025 choices.append(choice) 

1026 return choices 

1027 

1028 def get_action(self, action): 

1029 """ 

1030 Return a given action from a parameter, which can either be a callable, 

1031 or the name of a method on the ModelAdmin. Return is a tuple of 

1032 (callable, name, description). 

1033 """ 

1034 # If the action is a callable, just use it. 

1035 if callable(action): 1035 ↛ 1042line 1035 didn't jump to line 1042, because the condition on line 1035 was never false

1036 func = action 

1037 action = action.__name__ 

1038 

1039 # Next, look for a method. Grab it off self.__class__ to get an unbound 

1040 # method instead of a bound one; this ensures that the calling 

1041 # conventions are the same for functions and methods. 

1042 elif hasattr(self.__class__, action): 

1043 func = getattr(self.__class__, action) 

1044 

1045 # Finally, look for a named method on the admin site 

1046 else: 

1047 try: 

1048 func = self.admin_site.get_action(action) 

1049 except KeyError: 

1050 return None 

1051 

1052 description = self._get_action_description(func, action) 

1053 return func, action, description 

1054 

1055 def get_list_display(self, request): 

1056 """ 

1057 Return a sequence containing the fields to be displayed on the 

1058 changelist. 

1059 """ 

1060 return self.list_display 

1061 

1062 def get_list_display_links(self, request, list_display): 

1063 """ 

1064 Return a sequence containing the fields to be displayed as links 

1065 on the changelist. The list_display parameter is the list of fields 

1066 returned by get_list_display(). 

1067 """ 

1068 if ( 

1069 self.list_display_links 

1070 or self.list_display_links is None 

1071 or not list_display 

1072 ): 

1073 return self.list_display_links 

1074 else: 

1075 # Use only the first item in list_display as link 

1076 return list(list_display)[:1] 

1077 

1078 def get_list_filter(self, request): 

1079 """ 

1080 Return a sequence containing the fields to be displayed as filters in 

1081 the right sidebar of the changelist page. 

1082 """ 

1083 return self.list_filter 

1084 

1085 def get_list_select_related(self, request): 

1086 """ 

1087 Return a list of fields to add to the select_related() part of the 

1088 changelist items query. 

1089 """ 

1090 return self.list_select_related 

1091 

1092 def get_search_fields(self, request): 

1093 """ 

1094 Return a sequence containing the fields to be searched whenever 

1095 somebody submits a search query. 

1096 """ 

1097 return self.search_fields 

1098 

1099 def get_search_results(self, request, queryset, search_term): 

1100 """ 

1101 Return a tuple containing a queryset to implement the search 

1102 and a boolean indicating if the results may contain duplicates. 

1103 """ 

1104 # Apply keyword searches. 

1105 def construct_search(field_name): 

1106 if field_name.startswith("^"): 

1107 return "%s__istartswith" % field_name[1:] 

1108 elif field_name.startswith("="): 

1109 return "%s__iexact" % field_name[1:] 

1110 elif field_name.startswith("@"): 

1111 return "%s__search" % field_name[1:] 

1112 # Use field_name if it includes a lookup. 

1113 opts = queryset.model._meta 

1114 lookup_fields = field_name.split(LOOKUP_SEP) 

1115 # Go through the fields, following all relations. 

1116 prev_field = None 

1117 for path_part in lookup_fields: 

1118 if path_part == "pk": 

1119 path_part = opts.pk.name 

1120 try: 

1121 field = opts.get_field(path_part) 

1122 except FieldDoesNotExist: 

1123 # Use valid query lookups. 

1124 if prev_field and prev_field.get_lookup(path_part): 

1125 return field_name 

1126 else: 

1127 prev_field = field 

1128 if hasattr(field, "get_path_info"): 

1129 # Update opts to follow the relation. 

1130 opts = field.get_path_info()[-1].to_opts 

1131 # Otherwise, use the field with icontains. 

1132 return "%s__icontains" % field_name 

1133 

1134 may_have_duplicates = False 

1135 search_fields = self.get_search_fields(request) 

1136 if search_fields and search_term: 

1137 orm_lookups = [ 

1138 construct_search(str(search_field)) for search_field in search_fields 

1139 ] 

1140 for bit in smart_split(search_term): 

1141 if bit.startswith(('"', "'")) and bit[0] == bit[-1]: 

1142 bit = unescape_string_literal(bit) 

1143 or_queries = models.Q( 

1144 *((orm_lookup, bit) for orm_lookup in orm_lookups), 

1145 _connector=models.Q.OR, 

1146 ) 

1147 queryset = queryset.filter(or_queries) 

1148 may_have_duplicates |= any( 

1149 lookup_spawns_duplicates(self.opts, search_spec) 

1150 for search_spec in orm_lookups 

1151 ) 

1152 return queryset, may_have_duplicates 

1153 

1154 def get_preserved_filters(self, request): 

1155 """ 

1156 Return the preserved filters querystring. 

1157 """ 

1158 match = request.resolver_match 

1159 if self.preserve_filters and match: 

1160 opts = self.model._meta 

1161 current_url = "%s:%s" % (match.app_name, match.url_name) 

1162 changelist_url = "admin:%s_%s_changelist" % ( 

1163 opts.app_label, 

1164 opts.model_name, 

1165 ) 

1166 if current_url == changelist_url: 

1167 preserved_filters = request.GET.urlencode() 

1168 else: 

1169 preserved_filters = request.GET.get("_changelist_filters") 

1170 

1171 if preserved_filters: 

1172 return urlencode({"_changelist_filters": preserved_filters}) 

1173 return "" 

1174 

1175 def construct_change_message(self, request, form, formsets, add=False): 

1176 """ 

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

1178 """ 

1179 return construct_change_message(form, formsets, add) 

1180 

1181 def message_user( 

1182 self, request, message, level=messages.INFO, extra_tags="", fail_silently=False 

1183 ): 

1184 """ 

1185 Send a message to the user. The default implementation 

1186 posts a message using the django.contrib.messages backend. 

1187 

1188 Exposes almost the same API as messages.add_message(), but accepts the 

1189 positional arguments in a different order to maintain backwards 

1190 compatibility. For convenience, it accepts the `level` argument as 

1191 a string rather than the usual level number. 

1192 """ 

1193 if not isinstance(level, int): 

1194 # attempt to get the level if passed a string 

1195 try: 

1196 level = getattr(messages.constants, level.upper()) 

1197 except AttributeError: 

1198 levels = messages.constants.DEFAULT_TAGS.values() 

1199 levels_repr = ", ".join("`%s`" % level for level in levels) 

1200 raise ValueError( 

1201 "Bad message level string: `%s`. Possible values are: %s" 

1202 % (level, levels_repr) 

1203 ) 

1204 

1205 messages.add_message( 

1206 request, level, message, extra_tags=extra_tags, fail_silently=fail_silently 

1207 ) 

1208 

1209 def save_form(self, request, form, change): 

1210 """ 

1211 Given a ModelForm return an unsaved instance. ``change`` is True if 

1212 the object is being changed, and False if it's being added. 

1213 """ 

1214 return form.save(commit=False) 

1215 

1216 def save_model(self, request, obj, form, change): 

1217 """ 

1218 Given a model instance save it to the database. 

1219 """ 

1220 obj.save() 

1221 

1222 def delete_model(self, request, obj): 

1223 """ 

1224 Given a model instance delete it from the database. 

1225 """ 

1226 obj.delete() 

1227 

1228 def delete_queryset(self, request, queryset): 

1229 """Given a queryset, delete it from the database.""" 

1230 queryset.delete() 

1231 

1232 def save_formset(self, request, form, formset, change): 

1233 """ 

1234 Given an inline formset save it to the database. 

1235 """ 

1236 formset.save() 

1237 

1238 def save_related(self, request, form, formsets, change): 

1239 """ 

1240 Given the ``HttpRequest``, the parent ``ModelForm`` instance, the 

1241 list of inline formsets and a boolean value based on whether the 

1242 parent is being added or changed, save the related objects to the 

1243 database. Note that at this point save_form() and save_model() have 

1244 already been called. 

1245 """ 

1246 form.save_m2m() 

1247 for formset in formsets: 

1248 self.save_formset(request, form, formset, change=change) 

1249 

1250 def render_change_form( 

1251 self, request, context, add=False, change=False, form_url="", obj=None 

1252 ): 

1253 opts = self.model._meta 

1254 app_label = opts.app_label 

1255 preserved_filters = self.get_preserved_filters(request) 

1256 form_url = add_preserved_filters( 

1257 {"preserved_filters": preserved_filters, "opts": opts}, form_url 

1258 ) 

1259 view_on_site_url = self.get_view_on_site_url(obj) 

1260 has_editable_inline_admin_formsets = False 

1261 for inline in context["inline_admin_formsets"]: 

1262 if ( 

1263 inline.has_add_permission 

1264 or inline.has_change_permission 

1265 or inline.has_delete_permission 

1266 ): 

1267 has_editable_inline_admin_formsets = True 

1268 break 

1269 context.update( 

1270 { 

1271 "add": add, 

1272 "change": change, 

1273 "has_view_permission": self.has_view_permission(request, obj), 

1274 "has_add_permission": self.has_add_permission(request), 

1275 "has_change_permission": self.has_change_permission(request, obj), 

1276 "has_delete_permission": self.has_delete_permission(request, obj), 

1277 "has_editable_inline_admin_formsets": ( 

1278 has_editable_inline_admin_formsets 

1279 ), 

1280 "has_file_field": context["adminform"].form.is_multipart() 

1281 or any( 

1282 admin_formset.formset.is_multipart() 

1283 for admin_formset in context["inline_admin_formsets"] 

1284 ), 

1285 "has_absolute_url": view_on_site_url is not None, 

1286 "absolute_url": view_on_site_url, 

1287 "form_url": form_url, 

1288 "opts": opts, 

1289 "content_type_id": get_content_type_for_model(self.model).pk, 

1290 "save_as": self.save_as, 

1291 "save_on_top": self.save_on_top, 

1292 "to_field_var": TO_FIELD_VAR, 

1293 "is_popup_var": IS_POPUP_VAR, 

1294 "app_label": app_label, 

1295 } 

1296 ) 

1297 if add and self.add_form_template is not None: 

1298 form_template = self.add_form_template 

1299 else: 

1300 form_template = self.change_form_template 

1301 

1302 request.current_app = self.admin_site.name 

1303 

1304 return TemplateResponse( 

1305 request, 

1306 form_template 

1307 or [ 

1308 "admin/%s/%s/change_form.html" % (app_label, opts.model_name), 

1309 "admin/%s/change_form.html" % app_label, 

1310 "admin/change_form.html", 

1311 ], 

1312 context, 

1313 ) 

1314 

1315 def response_add(self, request, obj, post_url_continue=None): 

1316 """ 

1317 Determine the HttpResponse for the add_view stage. 

1318 """ 

1319 opts = obj._meta 

1320 preserved_filters = self.get_preserved_filters(request) 

1321 obj_url = reverse( 

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

1323 args=(quote(obj.pk),), 

1324 current_app=self.admin_site.name, 

1325 ) 

1326 # Add a link to the object's change form if the user can edit the obj. 

1327 if self.has_change_permission(request, obj): 

1328 obj_repr = format_html('<a href="{}">{}</a>', urlquote(obj_url), obj) 

1329 else: 

1330 obj_repr = str(obj) 

1331 msg_dict = { 

1332 "name": opts.verbose_name, 

1333 "obj": obj_repr, 

1334 } 

1335 # Here, we distinguish between different save types by checking for 

1336 # the presence of keys in request.POST. 

1337 

1338 if IS_POPUP_VAR in request.POST: 

1339 to_field = request.POST.get(TO_FIELD_VAR) 

1340 if to_field: 

1341 attr = str(to_field) 

1342 else: 

1343 attr = obj._meta.pk.attname 

1344 value = obj.serializable_value(attr) 

1345 popup_response_data = json.dumps( 

1346 { 

1347 "value": str(value), 

1348 "obj": str(obj), 

1349 } 

1350 ) 

1351 return TemplateResponse( 

1352 request, 

1353 self.popup_response_template 

1354 or [ 

1355 "admin/%s/%s/popup_response.html" 

1356 % (opts.app_label, opts.model_name), 

1357 "admin/%s/popup_response.html" % opts.app_label, 

1358 "admin/popup_response.html", 

1359 ], 

1360 { 

1361 "popup_response_data": popup_response_data, 

1362 }, 

1363 ) 

1364 

1365 elif "_continue" in request.POST or ( 

1366 # Redirecting after "Save as new". 

1367 "_saveasnew" in request.POST 

1368 and self.save_as_continue 

1369 and self.has_change_permission(request, obj) 

1370 ): 

1371 msg = _("The {name} “{obj}” was added successfully.") 

1372 if self.has_change_permission(request, obj): 

1373 msg += " " + _("You may edit it again below.") 

1374 self.message_user(request, format_html(msg, **msg_dict), messages.SUCCESS) 

1375 if post_url_continue is None: 

1376 post_url_continue = obj_url 

1377 post_url_continue = add_preserved_filters( 

1378 {"preserved_filters": preserved_filters, "opts": opts}, 

1379 post_url_continue, 

1380 ) 

1381 return HttpResponseRedirect(post_url_continue) 

1382 

1383 elif "_addanother" in request.POST: 

1384 msg = format_html( 

1385 _( 

1386 "The {name} “{obj}” was added successfully. You may add another " 

1387 "{name} below." 

1388 ), 

1389 **msg_dict, 

1390 ) 

1391 self.message_user(request, msg, messages.SUCCESS) 

1392 redirect_url = request.path 

1393 redirect_url = add_preserved_filters( 

1394 {"preserved_filters": preserved_filters, "opts": opts}, redirect_url 

1395 ) 

1396 return HttpResponseRedirect(redirect_url) 

1397 

1398 else: 

1399 msg = format_html( 

1400 _("The {name} “{obj}” was added successfully."), **msg_dict 

1401 ) 

1402 self.message_user(request, msg, messages.SUCCESS) 

1403 return self.response_post_save_add(request, obj) 

1404 

1405 def response_change(self, request, obj): 

1406 """ 

1407 Determine the HttpResponse for the change_view stage. 

1408 """ 

1409 

1410 if IS_POPUP_VAR in request.POST: 

1411 opts = obj._meta 

1412 to_field = request.POST.get(TO_FIELD_VAR) 

1413 attr = str(to_field) if to_field else opts.pk.attname 

1414 value = request.resolver_match.kwargs["object_id"] 

1415 new_value = obj.serializable_value(attr) 

1416 popup_response_data = json.dumps( 

1417 { 

1418 "action": "change", 

1419 "value": str(value), 

1420 "obj": str(obj), 

1421 "new_value": str(new_value), 

1422 } 

1423 ) 

1424 return TemplateResponse( 

1425 request, 

1426 self.popup_response_template 

1427 or [ 

1428 "admin/%s/%s/popup_response.html" 

1429 % (opts.app_label, opts.model_name), 

1430 "admin/%s/popup_response.html" % opts.app_label, 

1431 "admin/popup_response.html", 

1432 ], 

1433 { 

1434 "popup_response_data": popup_response_data, 

1435 }, 

1436 ) 

1437 

1438 opts = self.model._meta 

1439 preserved_filters = self.get_preserved_filters(request) 

1440 

1441 msg_dict = { 

1442 "name": opts.verbose_name, 

1443 "obj": format_html('<a href="{}">{}</a>', urlquote(request.path), obj), 

1444 } 

1445 if "_continue" in request.POST: 

1446 msg = format_html( 

1447 _( 

1448 "The {name} “{obj}” was changed successfully. You may edit it " 

1449 "again below." 

1450 ), 

1451 **msg_dict, 

1452 ) 

1453 self.message_user(request, msg, messages.SUCCESS) 

1454 redirect_url = request.path 

1455 redirect_url = add_preserved_filters( 

1456 {"preserved_filters": preserved_filters, "opts": opts}, redirect_url 

1457 ) 

1458 return HttpResponseRedirect(redirect_url) 

1459 

1460 elif "_saveasnew" in request.POST: 

1461 msg = format_html( 

1462 _( 

1463 "The {name} “{obj}” was added successfully. You may edit it again " 

1464 "below." 

1465 ), 

1466 **msg_dict, 

1467 ) 

1468 self.message_user(request, msg, messages.SUCCESS) 

1469 redirect_url = reverse( 

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

1471 args=(obj.pk,), 

1472 current_app=self.admin_site.name, 

1473 ) 

1474 redirect_url = add_preserved_filters( 

1475 {"preserved_filters": preserved_filters, "opts": opts}, redirect_url 

1476 ) 

1477 return HttpResponseRedirect(redirect_url) 

1478 

1479 elif "_addanother" in request.POST: 

1480 msg = format_html( 

1481 _( 

1482 "The {name} “{obj}” was changed successfully. You may add another " 

1483 "{name} below." 

1484 ), 

1485 **msg_dict, 

1486 ) 

1487 self.message_user(request, msg, messages.SUCCESS) 

1488 redirect_url = reverse( 

1489 "admin:%s_%s_add" % (opts.app_label, opts.model_name), 

1490 current_app=self.admin_site.name, 

1491 ) 

1492 redirect_url = add_preserved_filters( 

1493 {"preserved_filters": preserved_filters, "opts": opts}, redirect_url 

1494 ) 

1495 return HttpResponseRedirect(redirect_url) 

1496 

1497 else: 

1498 msg = format_html( 

1499 _("The {name} “{obj}” was changed successfully."), **msg_dict 

1500 ) 

1501 self.message_user(request, msg, messages.SUCCESS) 

1502 return self.response_post_save_change(request, obj) 

1503 

1504 def _response_post_save(self, request, obj): 

1505 opts = self.model._meta 

1506 if self.has_view_or_change_permission(request): 

1507 post_url = reverse( 

1508 "admin:%s_%s_changelist" % (opts.app_label, opts.model_name), 

1509 current_app=self.admin_site.name, 

1510 ) 

1511 preserved_filters = self.get_preserved_filters(request) 

1512 post_url = add_preserved_filters( 

1513 {"preserved_filters": preserved_filters, "opts": opts}, post_url 

1514 ) 

1515 else: 

1516 post_url = reverse("admin:index", current_app=self.admin_site.name) 

1517 return HttpResponseRedirect(post_url) 

1518 

1519 def response_post_save_add(self, request, obj): 

1520 """ 

1521 Figure out where to redirect after the 'Save' button has been pressed 

1522 when adding a new object. 

1523 """ 

1524 return self._response_post_save(request, obj) 

1525 

1526 def response_post_save_change(self, request, obj): 

1527 """ 

1528 Figure out where to redirect after the 'Save' button has been pressed 

1529 when editing an existing object. 

1530 """ 

1531 return self._response_post_save(request, obj) 

1532 

1533 def response_action(self, request, queryset): 

1534 """ 

1535 Handle an admin action. This is called if a request is POSTed to the 

1536 changelist; it returns an HttpResponse if the action was handled, and 

1537 None otherwise. 

1538 """ 

1539 

1540 # There can be multiple action forms on the page (at the top 

1541 # and bottom of the change list, for example). Get the action 

1542 # whose button was pushed. 

1543 try: 

1544 action_index = int(request.POST.get("index", 0)) 

1545 except ValueError: 

1546 action_index = 0 

1547 

1548 # Construct the action form. 

1549 data = request.POST.copy() 

1550 data.pop(helpers.ACTION_CHECKBOX_NAME, None) 

1551 data.pop("index", None) 

1552 

1553 # Use the action whose button was pushed 

1554 try: 

1555 data.update({"action": data.getlist("action")[action_index]}) 

1556 except IndexError: 

1557 # If we didn't get an action from the chosen form that's invalid 

1558 # POST data, so by deleting action it'll fail the validation check 

1559 # below. So no need to do anything here 

1560 pass 

1561 

1562 action_form = self.action_form(data, auto_id=None) 

1563 action_form.fields["action"].choices = self.get_action_choices(request) 

1564 

1565 # If the form's valid we can handle the action. 

1566 if action_form.is_valid(): 

1567 action = action_form.cleaned_data["action"] 

1568 select_across = action_form.cleaned_data["select_across"] 

1569 func = self.get_actions(request)[action][0] 

1570 

1571 # Get the list of selected PKs. If nothing's selected, we can't 

1572 # perform an action on it, so bail. Except we want to perform 

1573 # the action explicitly on all objects. 

1574 selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME) 

1575 if not selected and not select_across: 

1576 # Reminder that something needs to be selected or nothing will happen 

1577 msg = _( 

1578 "Items must be selected in order to perform " 

1579 "actions on them. No items have been changed." 

1580 ) 

1581 self.message_user(request, msg, messages.WARNING) 

1582 return None 

1583 

1584 if not select_across: 

1585 # Perform the action only on the selected objects 

1586 queryset = queryset.filter(pk__in=selected) 

1587 

1588 response = func(self, request, queryset) 

1589 

1590 # Actions may return an HttpResponse-like object, which will be 

1591 # used as the response from the POST. If not, we'll be a good 

1592 # little HTTP citizen and redirect back to the changelist page. 

1593 if isinstance(response, HttpResponseBase): 

1594 return response 

1595 else: 

1596 return HttpResponseRedirect(request.get_full_path()) 

1597 else: 

1598 msg = _("No action selected.") 

1599 self.message_user(request, msg, messages.WARNING) 

1600 return None 

1601 

1602 def response_delete(self, request, obj_display, obj_id): 

1603 """ 

1604 Determine the HttpResponse for the delete_view stage. 

1605 """ 

1606 opts = self.model._meta 

1607 

1608 if IS_POPUP_VAR in request.POST: 

1609 popup_response_data = json.dumps( 

1610 { 

1611 "action": "delete", 

1612 "value": str(obj_id), 

1613 } 

1614 ) 

1615 return TemplateResponse( 

1616 request, 

1617 self.popup_response_template 

1618 or [ 

1619 "admin/%s/%s/popup_response.html" 

1620 % (opts.app_label, opts.model_name), 

1621 "admin/%s/popup_response.html" % opts.app_label, 

1622 "admin/popup_response.html", 

1623 ], 

1624 { 

1625 "popup_response_data": popup_response_data, 

1626 }, 

1627 ) 

1628 

1629 self.message_user( 

1630 request, 

1631 _("The %(name)s “%(obj)s” was deleted successfully.") 

1632 % { 

1633 "name": opts.verbose_name, 

1634 "obj": obj_display, 

1635 }, 

1636 messages.SUCCESS, 

1637 ) 

1638 

1639 if self.has_change_permission(request, None): 

1640 post_url = reverse( 

1641 "admin:%s_%s_changelist" % (opts.app_label, opts.model_name), 

1642 current_app=self.admin_site.name, 

1643 ) 

1644 preserved_filters = self.get_preserved_filters(request) 

1645 post_url = add_preserved_filters( 

1646 {"preserved_filters": preserved_filters, "opts": opts}, post_url 

1647 ) 

1648 else: 

1649 post_url = reverse("admin:index", current_app=self.admin_site.name) 

1650 return HttpResponseRedirect(post_url) 

1651 

1652 def render_delete_form(self, request, context): 

1653 opts = self.model._meta 

1654 app_label = opts.app_label 

1655 

1656 request.current_app = self.admin_site.name 

1657 context.update( 

1658 to_field_var=TO_FIELD_VAR, 

1659 is_popup_var=IS_POPUP_VAR, 

1660 media=self.media, 

1661 ) 

1662 

1663 return TemplateResponse( 

1664 request, 

1665 self.delete_confirmation_template 

1666 or [ 

1667 "admin/{}/{}/delete_confirmation.html".format( 

1668 app_label, opts.model_name 

1669 ), 

1670 "admin/{}/delete_confirmation.html".format(app_label), 

1671 "admin/delete_confirmation.html", 

1672 ], 

1673 context, 

1674 ) 

1675 

1676 def get_inline_formsets(self, request, formsets, inline_instances, obj=None): 

1677 # Edit permissions on parent model are required for editable inlines. 

1678 can_edit_parent = ( 

1679 self.has_change_permission(request, obj) 

1680 if obj 

1681 else self.has_add_permission(request) 

1682 ) 

1683 inline_admin_formsets = [] 

1684 for inline, formset in zip(inline_instances, formsets): 

1685 fieldsets = list(inline.get_fieldsets(request, obj)) 

1686 readonly = list(inline.get_readonly_fields(request, obj)) 

1687 if can_edit_parent: 

1688 has_add_permission = inline.has_add_permission(request, obj) 

1689 has_change_permission = inline.has_change_permission(request, obj) 

1690 has_delete_permission = inline.has_delete_permission(request, obj) 

1691 else: 

1692 # Disable all edit-permissions, and overide formset settings. 

1693 has_add_permission = ( 

1694 has_change_permission 

1695 ) = has_delete_permission = False 

1696 formset.extra = formset.max_num = 0 

1697 has_view_permission = inline.has_view_permission(request, obj) 

1698 prepopulated = dict(inline.get_prepopulated_fields(request, obj)) 

1699 inline_admin_formset = helpers.InlineAdminFormSet( 

1700 inline, 

1701 formset, 

1702 fieldsets, 

1703 prepopulated, 

1704 readonly, 

1705 model_admin=self, 

1706 has_add_permission=has_add_permission, 

1707 has_change_permission=has_change_permission, 

1708 has_delete_permission=has_delete_permission, 

1709 has_view_permission=has_view_permission, 

1710 ) 

1711 inline_admin_formsets.append(inline_admin_formset) 

1712 return inline_admin_formsets 

1713 

1714 def get_changeform_initial_data(self, request): 

1715 """ 

1716 Get the initial form data from the request's GET params. 

1717 """ 

1718 initial = dict(request.GET.items()) 

1719 for k in initial: 

1720 try: 

1721 f = self.model._meta.get_field(k) 

1722 except FieldDoesNotExist: 

1723 continue 

1724 # We have to special-case M2Ms as a list of comma-separated PKs. 

1725 if isinstance(f, models.ManyToManyField): 

1726 initial[k] = initial[k].split(",") 

1727 return initial 

1728 

1729 def _get_obj_does_not_exist_redirect(self, request, opts, object_id): 

1730 """ 

1731 Create a message informing the user that the object doesn't exist 

1732 and return a redirect to the admin index page. 

1733 """ 

1734 msg = _("%(name)s with ID “%(key)s” doesn’t exist. Perhaps it was deleted?") % { 

1735 "name": opts.verbose_name, 

1736 "key": unquote(object_id), 

1737 } 

1738 self.message_user(request, msg, messages.WARNING) 

1739 url = reverse("admin:index", current_app=self.admin_site.name) 

1740 return HttpResponseRedirect(url) 

1741 

1742 @csrf_protect_m 

1743 def changeform_view(self, request, object_id=None, form_url="", extra_context=None): 

1744 with transaction.atomic(using=router.db_for_write(self.model)): 

1745 return self._changeform_view(request, object_id, form_url, extra_context) 

1746 

1747 def _changeform_view(self, request, object_id, form_url, extra_context): 

1748 to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR)) 

1749 if to_field and not self.to_field_allowed(request, to_field): 

1750 raise DisallowedModelAdminToField( 

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

1752 ) 

1753 

1754 model = self.model 

1755 opts = model._meta 

1756 

1757 if request.method == "POST" and "_saveasnew" in request.POST: 

1758 object_id = None 

1759 

1760 add = object_id is None 

1761 

1762 if add: 

1763 if not self.has_add_permission(request): 

1764 raise PermissionDenied 

1765 obj = None 

1766 

1767 else: 

1768 obj = self.get_object(request, unquote(object_id), to_field) 

1769 

1770 if request.method == "POST": 

1771 if not self.has_change_permission(request, obj): 

1772 raise PermissionDenied 

1773 else: 

1774 if not self.has_view_or_change_permission(request, obj): 

1775 raise PermissionDenied 

1776 

1777 if obj is None: 

1778 return self._get_obj_does_not_exist_redirect(request, opts, object_id) 

1779 

1780 fieldsets = self.get_fieldsets(request, obj) 

1781 ModelForm = self.get_form( 

1782 request, obj, change=not add, fields=flatten_fieldsets(fieldsets) 

1783 ) 

1784 if request.method == "POST": 

1785 form = ModelForm(request.POST, request.FILES, instance=obj) 

1786 formsets, inline_instances = self._create_formsets( 

1787 request, 

1788 form.instance if add else obj, 

1789 change=not add, 

1790 ) 

1791 form_validated = form.is_valid() 

1792 if form_validated: 

1793 new_object = self.save_form(request, form, change=not add) 

1794 else: 

1795 new_object = form.instance 

1796 if all_valid(formsets) and form_validated: 

1797 self.save_model(request, new_object, form, not add) 

1798 self.save_related(request, form, formsets, not add) 

1799 change_message = self.construct_change_message( 

1800 request, form, formsets, add 

1801 ) 

1802 if add: 

1803 self.log_addition(request, new_object, change_message) 

1804 return self.response_add(request, new_object) 

1805 else: 

1806 self.log_change(request, new_object, change_message) 

1807 return self.response_change(request, new_object) 

1808 else: 

1809 form_validated = False 

1810 else: 

1811 if add: 

1812 initial = self.get_changeform_initial_data(request) 

1813 form = ModelForm(initial=initial) 

1814 formsets, inline_instances = self._create_formsets( 

1815 request, form.instance, change=False 

1816 ) 

1817 else: 

1818 form = ModelForm(instance=obj) 

1819 formsets, inline_instances = self._create_formsets( 

1820 request, obj, change=True 

1821 ) 

1822 

1823 if not add and not self.has_change_permission(request, obj): 

1824 readonly_fields = flatten_fieldsets(fieldsets) 

1825 else: 

1826 readonly_fields = self.get_readonly_fields(request, obj) 

1827 adminForm = helpers.AdminForm( 

1828 form, 

1829 list(fieldsets), 

1830 # Clear prepopulated fields on a view-only form to avoid a crash. 

1831 self.get_prepopulated_fields(request, obj) 

1832 if add or self.has_change_permission(request, obj) 

1833 else {}, 

1834 readonly_fields, 

1835 model_admin=self, 

1836 ) 

1837 media = self.media + adminForm.media 

1838 

1839 inline_formsets = self.get_inline_formsets( 

1840 request, formsets, inline_instances, obj 

1841 ) 

1842 for inline_formset in inline_formsets: 

1843 media = media + inline_formset.media 

1844 

1845 if add: 

1846 title = _("Add %s") 

1847 elif self.has_change_permission(request, obj): 

1848 title = _("Change %s") 

1849 else: 

1850 title = _("View %s") 

1851 context = { 

1852 **self.admin_site.each_context(request), 

1853 "title": title % opts.verbose_name, 

1854 "subtitle": str(obj) if obj else None, 

1855 "adminform": adminForm, 

1856 "object_id": object_id, 

1857 "original": obj, 

1858 "is_popup": IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET, 

1859 "to_field": to_field, 

1860 "media": media, 

1861 "inline_admin_formsets": inline_formsets, 

1862 "errors": helpers.AdminErrorList(form, formsets), 

1863 "preserved_filters": self.get_preserved_filters(request), 

1864 } 

1865 

1866 # Hide the "Save" and "Save and continue" buttons if "Save as New" was 

1867 # previously chosen to prevent the interface from getting confusing. 

1868 if ( 

1869 request.method == "POST" 

1870 and not form_validated 

1871 and "_saveasnew" in request.POST 

1872 ): 

1873 context["show_save"] = False 

1874 context["show_save_and_continue"] = False 

1875 # Use the change template instead of the add template. 

1876 add = False 

1877 

1878 context.update(extra_context or {}) 

1879 

1880 return self.render_change_form( 

1881 request, context, add=add, change=not add, obj=obj, form_url=form_url 

1882 ) 

1883 

1884 def add_view(self, request, form_url="", extra_context=None): 

1885 return self.changeform_view(request, None, form_url, extra_context) 

1886 

1887 def change_view(self, request, object_id, form_url="", extra_context=None): 

1888 return self.changeform_view(request, object_id, form_url, extra_context) 

1889 

1890 def _get_edited_object_pks(self, request, prefix): 

1891 """Return POST data values of list_editable primary keys.""" 

1892 pk_pattern = re.compile( 

1893 r"{}-\d+-{}$".format(re.escape(prefix), self.model._meta.pk.name) 

1894 ) 

1895 return [value for key, value in request.POST.items() if pk_pattern.match(key)] 

1896 

1897 def _get_list_editable_queryset(self, request, prefix): 

1898 """ 

1899 Based on POST data, return a queryset of the objects that were edited 

1900 via list_editable. 

1901 """ 

1902 object_pks = self._get_edited_object_pks(request, prefix) 

1903 queryset = self.get_queryset(request) 

1904 validate = queryset.model._meta.pk.to_python 

1905 try: 

1906 for pk in object_pks: 

1907 validate(pk) 

1908 except ValidationError: 

1909 # Disable the optimization if the POST data was tampered with. 

1910 return queryset 

1911 return queryset.filter(pk__in=object_pks) 

1912 

1913 @csrf_protect_m 

1914 def changelist_view(self, request, extra_context=None): 

1915 """ 

1916 The 'change list' admin view for this model. 

1917 """ 

1918 from django.contrib.admin.views.main import ERROR_FLAG 

1919 

1920 opts = self.model._meta 

1921 app_label = opts.app_label 

1922 if not self.has_view_or_change_permission(request): 

1923 raise PermissionDenied 

1924 

1925 try: 

1926 cl = self.get_changelist_instance(request) 

1927 except IncorrectLookupParameters: 

1928 # Wacky lookup parameters were given, so redirect to the main 

1929 # changelist page, without parameters, and pass an 'invalid=1' 

1930 # parameter via the query string. If wacky parameters were given 

1931 # and the 'invalid=1' parameter was already in the query string, 

1932 # something is screwed up with the database, so display an error 

1933 # page. 

1934 if ERROR_FLAG in request.GET: 

1935 return SimpleTemplateResponse( 

1936 "admin/invalid_setup.html", 

1937 { 

1938 "title": _("Database error"), 

1939 }, 

1940 ) 

1941 return HttpResponseRedirect(request.path + "?" + ERROR_FLAG + "=1") 

1942 

1943 # If the request was POSTed, this might be a bulk action or a bulk 

1944 # edit. Try to look up an action or confirmation first, but if this 

1945 # isn't an action the POST will fall through to the bulk edit check, 

1946 # below. 

1947 action_failed = False 

1948 selected = request.POST.getlist(helpers.ACTION_CHECKBOX_NAME) 

1949 

1950 actions = self.get_actions(request) 

1951 # Actions with no confirmation 

1952 if ( 

1953 actions 

1954 and request.method == "POST" 

1955 and "index" in request.POST 

1956 and "_save" not in request.POST 

1957 ): 

1958 if selected: 

1959 response = self.response_action( 

1960 request, queryset=cl.get_queryset(request) 

1961 ) 

1962 if response: 

1963 return response 

1964 else: 

1965 action_failed = True 

1966 else: 

1967 msg = _( 

1968 "Items must be selected in order to perform " 

1969 "actions on them. No items have been changed." 

1970 ) 

1971 self.message_user(request, msg, messages.WARNING) 

1972 action_failed = True 

1973 

1974 # Actions with confirmation 

1975 if ( 

1976 actions 

1977 and request.method == "POST" 

1978 and helpers.ACTION_CHECKBOX_NAME in request.POST 

1979 and "index" not in request.POST 

1980 and "_save" not in request.POST 

1981 ): 

1982 if selected: 

1983 response = self.response_action( 

1984 request, queryset=cl.get_queryset(request) 

1985 ) 

1986 if response: 

1987 return response 

1988 else: 

1989 action_failed = True 

1990 

1991 if action_failed: 

1992 # Redirect back to the changelist page to avoid resubmitting the 

1993 # form if the user refreshes the browser or uses the "No, take 

1994 # me back" button on the action confirmation page. 

1995 return HttpResponseRedirect(request.get_full_path()) 

1996 

1997 # If we're allowing changelist editing, we need to construct a formset 

1998 # for the changelist given all the fields to be edited. Then we'll 

1999 # use the formset to validate/process POSTed data. 

2000 formset = cl.formset = None 

2001 

2002 # Handle POSTed bulk-edit data. 

2003 if request.method == "POST" and cl.list_editable and "_save" in request.POST: 

2004 if not self.has_change_permission(request): 

2005 raise PermissionDenied 

2006 FormSet = self.get_changelist_formset(request) 

2007 modified_objects = self._get_list_editable_queryset( 

2008 request, FormSet.get_default_prefix() 

2009 ) 

2010 formset = cl.formset = FormSet( 

2011 request.POST, request.FILES, queryset=modified_objects 

2012 ) 

2013 if formset.is_valid(): 

2014 changecount = 0 

2015 for form in formset.forms: 

2016 if form.has_changed(): 

2017 obj = self.save_form(request, form, change=True) 

2018 self.save_model(request, obj, form, change=True) 

2019 self.save_related(request, form, formsets=[], change=True) 

2020 change_msg = self.construct_change_message(request, form, None) 

2021 self.log_change(request, obj, change_msg) 

2022 changecount += 1 

2023 

2024 if changecount: 

2025 msg = ngettext( 

2026 "%(count)s %(name)s was changed successfully.", 

2027 "%(count)s %(name)s were changed successfully.", 

2028 changecount, 

2029 ) % { 

2030 "count": changecount, 

2031 "name": model_ngettext(opts, changecount), 

2032 } 

2033 self.message_user(request, msg, messages.SUCCESS) 

2034 

2035 return HttpResponseRedirect(request.get_full_path()) 

2036 

2037 # Handle GET -- construct a formset for display. 

2038 elif cl.list_editable and self.has_change_permission(request): 

2039 FormSet = self.get_changelist_formset(request) 

2040 formset = cl.formset = FormSet(queryset=cl.result_list) 

2041 

2042 # Build the list of media to be used by the formset. 

2043 if formset: 

2044 media = self.media + formset.media 

2045 else: 

2046 media = self.media 

2047 

2048 # Build the action form and populate it with available actions. 

2049 if actions: 

2050 action_form = self.action_form(auto_id=None) 

2051 action_form.fields["action"].choices = self.get_action_choices(request) 

2052 media += action_form.media 

2053 else: 

2054 action_form = None 

2055 

2056 selection_note_all = ngettext( 

2057 "%(total_count)s selected", "All %(total_count)s selected", cl.result_count 

2058 ) 

2059 

2060 context = { 

2061 **self.admin_site.each_context(request), 

2062 "module_name": str(opts.verbose_name_plural), 

2063 "selection_note": _("0 of %(cnt)s selected") % {"cnt": len(cl.result_list)}, 

2064 "selection_note_all": selection_note_all % {"total_count": cl.result_count}, 

2065 "title": cl.title, 

2066 "subtitle": None, 

2067 "is_popup": cl.is_popup, 

2068 "to_field": cl.to_field, 

2069 "cl": cl, 

2070 "media": media, 

2071 "has_add_permission": self.has_add_permission(request), 

2072 "opts": cl.opts, 

2073 "action_form": action_form, 

2074 "actions_on_top": self.actions_on_top, 

2075 "actions_on_bottom": self.actions_on_bottom, 

2076 "actions_selection_counter": self.actions_selection_counter, 

2077 "preserved_filters": self.get_preserved_filters(request), 

2078 **(extra_context or {}), 

2079 } 

2080 

2081 request.current_app = self.admin_site.name 

2082 

2083 return TemplateResponse( 

2084 request, 

2085 self.change_list_template 

2086 or [ 

2087 "admin/%s/%s/change_list.html" % (app_label, opts.model_name), 

2088 "admin/%s/change_list.html" % app_label, 

2089 "admin/change_list.html", 

2090 ], 

2091 context, 

2092 ) 

2093 

2094 def get_deleted_objects(self, objs, request): 

2095 """ 

2096 Hook for customizing the delete process for the delete view and the 

2097 "delete selected" action. 

2098 """ 

2099 return get_deleted_objects(objs, request, self.admin_site) 

2100 

2101 @csrf_protect_m 

2102 def delete_view(self, request, object_id, extra_context=None): 

2103 with transaction.atomic(using=router.db_for_write(self.model)): 

2104 return self._delete_view(request, object_id, extra_context) 

2105 

2106 def _delete_view(self, request, object_id, extra_context): 

2107 "The 'delete' admin view for this model." 

2108 opts = self.model._meta 

2109 app_label = opts.app_label 

2110 

2111 to_field = request.POST.get(TO_FIELD_VAR, request.GET.get(TO_FIELD_VAR)) 

2112 if to_field and not self.to_field_allowed(request, to_field): 

2113 raise DisallowedModelAdminToField( 

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

2115 ) 

2116 

2117 obj = self.get_object(request, unquote(object_id), to_field) 

2118 

2119 if not self.has_delete_permission(request, obj): 

2120 raise PermissionDenied 

2121 

2122 if obj is None: 

2123 return self._get_obj_does_not_exist_redirect(request, opts, object_id) 

2124 

2125 # Populate deleted_objects, a data structure of all related objects that 

2126 # will also be deleted. 

2127 ( 

2128 deleted_objects, 

2129 model_count, 

2130 perms_needed, 

2131 protected, 

2132 ) = self.get_deleted_objects([obj], request) 

2133 

2134 if request.POST and not protected: # The user has confirmed the deletion. 

2135 if perms_needed: 

2136 raise PermissionDenied 

2137 obj_display = str(obj) 

2138 attr = str(to_field) if to_field else opts.pk.attname 

2139 obj_id = obj.serializable_value(attr) 

2140 self.log_deletion(request, obj, obj_display) 

2141 self.delete_model(request, obj) 

2142 

2143 return self.response_delete(request, obj_display, obj_id) 

2144 

2145 object_name = str(opts.verbose_name) 

2146 

2147 if perms_needed or protected: 

2148 title = _("Cannot delete %(name)s") % {"name": object_name} 

2149 else: 

2150 title = _("Are you sure?") 

2151 

2152 context = { 

2153 **self.admin_site.each_context(request), 

2154 "title": title, 

2155 "subtitle": None, 

2156 "object_name": object_name, 

2157 "object": obj, 

2158 "deleted_objects": deleted_objects, 

2159 "model_count": dict(model_count).items(), 

2160 "perms_lacking": perms_needed, 

2161 "protected": protected, 

2162 "opts": opts, 

2163 "app_label": app_label, 

2164 "preserved_filters": self.get_preserved_filters(request), 

2165 "is_popup": IS_POPUP_VAR in request.POST or IS_POPUP_VAR in request.GET, 

2166 "to_field": to_field, 

2167 **(extra_context or {}), 

2168 } 

2169 

2170 return self.render_delete_form(request, context) 

2171 

2172 def history_view(self, request, object_id, extra_context=None): 

2173 "The 'history' admin view for this model." 

2174 from django.contrib.admin.models import LogEntry 

2175 

2176 # First check if the user can see this history. 

2177 model = self.model 

2178 obj = self.get_object(request, unquote(object_id)) 

2179 if obj is None: 

2180 return self._get_obj_does_not_exist_redirect( 

2181 request, model._meta, object_id 

2182 ) 

2183 

2184 if not self.has_view_or_change_permission(request, obj): 

2185 raise PermissionDenied 

2186 

2187 # Then get the history for this object. 

2188 opts = model._meta 

2189 app_label = opts.app_label 

2190 action_list = ( 

2191 LogEntry.objects.filter( 

2192 object_id=unquote(object_id), 

2193 content_type=get_content_type_for_model(model), 

2194 ) 

2195 .select_related() 

2196 .order_by("action_time") 

2197 ) 

2198 

2199 context = { 

2200 **self.admin_site.each_context(request), 

2201 "title": _("Change history: %s") % obj, 

2202 "subtitle": None, 

2203 "action_list": action_list, 

2204 "module_name": str(capfirst(opts.verbose_name_plural)), 

2205 "object": obj, 

2206 "opts": opts, 

2207 "preserved_filters": self.get_preserved_filters(request), 

2208 **(extra_context or {}), 

2209 } 

2210 

2211 request.current_app = self.admin_site.name 

2212 

2213 return TemplateResponse( 

2214 request, 

2215 self.object_history_template 

2216 or [ 

2217 "admin/%s/%s/object_history.html" % (app_label, opts.model_name), 

2218 "admin/%s/object_history.html" % app_label, 

2219 "admin/object_history.html", 

2220 ], 

2221 context, 

2222 ) 

2223 

2224 def get_formset_kwargs(self, request, obj, inline, prefix): 

2225 formset_params = { 

2226 "instance": obj, 

2227 "prefix": prefix, 

2228 "queryset": inline.get_queryset(request), 

2229 } 

2230 if request.method == "POST": 

2231 formset_params.update( 

2232 { 

2233 "data": request.POST.copy(), 

2234 "files": request.FILES, 

2235 "save_as_new": "_saveasnew" in request.POST, 

2236 } 

2237 ) 

2238 return formset_params 

2239 

2240 def _create_formsets(self, request, obj, change): 

2241 "Helper function to generate formsets for add/change_view." 

2242 formsets = [] 

2243 inline_instances = [] 

2244 prefixes = {} 

2245 get_formsets_args = [request] 

2246 if change: 

2247 get_formsets_args.append(obj) 

2248 for FormSet, inline in self.get_formsets_with_inlines(*get_formsets_args): 

2249 prefix = FormSet.get_default_prefix() 

2250 prefixes[prefix] = prefixes.get(prefix, 0) + 1 

2251 if prefixes[prefix] != 1 or not prefix: 

2252 prefix = "%s-%s" % (prefix, prefixes[prefix]) 

2253 formset_params = self.get_formset_kwargs(request, obj, inline, prefix) 

2254 formset = FormSet(**formset_params) 

2255 

2256 def user_deleted_form(request, obj, formset, index): 

2257 """Return whether or not the user deleted the form.""" 

2258 return ( 

2259 inline.has_delete_permission(request, obj) 

2260 and "{}-{}-DELETE".format(formset.prefix, index) in request.POST 

2261 ) 

2262 

2263 # Bypass validation of each view-only inline form (since the form's 

2264 # data won't be in request.POST), unless the form was deleted. 

2265 if not inline.has_change_permission(request, obj if change else None): 

2266 for index, form in enumerate(formset.initial_forms): 

2267 if user_deleted_form(request, obj, formset, index): 

2268 continue 

2269 form._errors = {} 

2270 form.cleaned_data = form.initial 

2271 formsets.append(formset) 

2272 inline_instances.append(inline) 

2273 return formsets, inline_instances 

2274 

2275 

2276class InlineModelAdmin(BaseModelAdmin): 

2277 """ 

2278 Options for inline editing of ``model`` instances. 

2279 

2280 Provide ``fk_name`` to specify the attribute name of the ``ForeignKey`` 

2281 from ``model`` to its parent. This is required if ``model`` has more than 

2282 one ``ForeignKey`` to its parent. 

2283 """ 

2284 

2285 model = None 

2286 fk_name = None 

2287 formset = BaseInlineFormSet 

2288 extra = 3 

2289 min_num = None 

2290 max_num = None 

2291 template = None 

2292 verbose_name = None 

2293 verbose_name_plural = None 

2294 can_delete = True 

2295 show_change_link = False 

2296 checks_class = InlineModelAdminChecks 

2297 classes = None 

2298 

2299 def __init__(self, parent_model, admin_site): 

2300 self.admin_site = admin_site 

2301 self.parent_model = parent_model 

2302 self.opts = self.model._meta 

2303 self.has_registered_model = admin_site.is_registered(self.model) 

2304 super().__init__() 

2305 if self.verbose_name_plural is None: 2305 ↛ 2310line 2305 didn't jump to line 2310, because the condition on line 2305 was never false

2306 if self.verbose_name is None: 2306 ↛ 2309line 2306 didn't jump to line 2309, because the condition on line 2306 was never false

2307 self.verbose_name_plural = self.model._meta.verbose_name_plural 

2308 else: 

2309 self.verbose_name_plural = format_lazy("{}s", self.verbose_name) 

2310 if self.verbose_name is None: 2310 ↛ exitline 2310 didn't return from function '__init__', because the condition on line 2310 was never false

2311 self.verbose_name = self.model._meta.verbose_name 

2312 

2313 @property 

2314 def media(self): 

2315 extra = "" if settings.DEBUG else ".min" 

2316 js = ["vendor/jquery/jquery%s.js" % extra, "jquery.init.js", "inlines.js"] 

2317 if self.filter_vertical or self.filter_horizontal: 

2318 js.extend(["SelectBox.js", "SelectFilter2.js"]) 

2319 if self.classes and "collapse" in self.classes: 

2320 js.append("collapse.js") 

2321 return forms.Media(js=["admin/js/%s" % url for url in js]) 

2322 

2323 def get_extra(self, request, obj=None, **kwargs): 

2324 """Hook for customizing the number of extra inline forms.""" 

2325 return self.extra 

2326 

2327 def get_min_num(self, request, obj=None, **kwargs): 

2328 """Hook for customizing the min number of inline forms.""" 

2329 return self.min_num 

2330 

2331 def get_max_num(self, request, obj=None, **kwargs): 

2332 """Hook for customizing the max number of extra inline forms.""" 

2333 return self.max_num 

2334 

2335 def get_formset(self, request, obj=None, **kwargs): 

2336 """Return a BaseInlineFormSet class for use in admin add/change views.""" 

2337 if "fields" in kwargs: 

2338 fields = kwargs.pop("fields") 

2339 else: 

2340 fields = flatten_fieldsets(self.get_fieldsets(request, obj)) 

2341 excluded = self.get_exclude(request, obj) 

2342 exclude = [] if excluded is None else list(excluded) 

2343 exclude.extend(self.get_readonly_fields(request, obj)) 

2344 if excluded is None and hasattr(self.form, "_meta") and self.form._meta.exclude: 

2345 # Take the custom ModelForm's Meta.exclude into account only if the 

2346 # InlineModelAdmin doesn't define its own. 

2347 exclude.extend(self.form._meta.exclude) 

2348 # If exclude is an empty list we use None, since that's the actual 

2349 # default. 

2350 exclude = exclude or None 

2351 can_delete = self.can_delete and self.has_delete_permission(request, obj) 

2352 defaults = { 

2353 "form": self.form, 

2354 "formset": self.formset, 

2355 "fk_name": self.fk_name, 

2356 "fields": fields, 

2357 "exclude": exclude, 

2358 "formfield_callback": partial(self.formfield_for_dbfield, request=request), 

2359 "extra": self.get_extra(request, obj, **kwargs), 

2360 "min_num": self.get_min_num(request, obj, **kwargs), 

2361 "max_num": self.get_max_num(request, obj, **kwargs), 

2362 "can_delete": can_delete, 

2363 **kwargs, 

2364 } 

2365 

2366 base_model_form = defaults["form"] 

2367 can_change = self.has_change_permission(request, obj) if request else True 

2368 can_add = self.has_add_permission(request, obj) if request else True 

2369 

2370 class DeleteProtectedModelForm(base_model_form): 

2371 def hand_clean_DELETE(self): 

2372 """ 

2373 We don't validate the 'DELETE' field itself because on 

2374 templates it's not rendered using the field information, but 

2375 just using a generic "deletion_field" of the InlineModelAdmin. 

2376 """ 

2377 if self.cleaned_data.get(DELETION_FIELD_NAME, False): 

2378 using = router.db_for_write(self._meta.model) 

2379 collector = NestedObjects(using=using) 

2380 if self.instance._state.adding: 

2381 return 

2382 collector.collect([self.instance]) 

2383 if collector.protected: 

2384 objs = [] 

2385 for p in collector.protected: 

2386 objs.append( 

2387 # Translators: Model verbose name and instance 

2388 # representation, suitable to be an item in a 

2389 # list. 

2390 _("%(class_name)s %(instance)s") 

2391 % {"class_name": p._meta.verbose_name, "instance": p} 

2392 ) 

2393 params = { 

2394 "class_name": self._meta.model._meta.verbose_name, 

2395 "instance": self.instance, 

2396 "related_objects": get_text_list(objs, _("and")), 

2397 } 

2398 msg = _( 

2399 "Deleting %(class_name)s %(instance)s would require " 

2400 "deleting the following protected related objects: " 

2401 "%(related_objects)s" 

2402 ) 

2403 raise ValidationError( 

2404 msg, code="deleting_protected", params=params 

2405 ) 

2406 

2407 def is_valid(self): 

2408 result = super().is_valid() 

2409 self.hand_clean_DELETE() 

2410 return result 

2411 

2412 def has_changed(self): 

2413 # Protect against unauthorized edits. 

2414 if not can_change and not self.instance._state.adding: 

2415 return False 

2416 if not can_add and self.instance._state.adding: 

2417 return False 

2418 return super().has_changed() 

2419 

2420 defaults["form"] = DeleteProtectedModelForm 

2421 

2422 if defaults["fields"] is None and not modelform_defines_fields( 

2423 defaults["form"] 

2424 ): 

2425 defaults["fields"] = forms.ALL_FIELDS 

2426 

2427 return inlineformset_factory(self.parent_model, self.model, **defaults) 

2428 

2429 def _get_form_for_get_fields(self, request, obj=None): 

2430 return self.get_formset(request, obj, fields=None).form 

2431 

2432 def get_queryset(self, request): 

2433 queryset = super().get_queryset(request) 

2434 if not self.has_view_or_change_permission(request): 

2435 queryset = queryset.none() 

2436 return queryset 

2437 

2438 def _has_any_perms_for_target_model(self, request, perms): 

2439 """ 

2440 This method is called only when the ModelAdmin's model is for an 

2441 ManyToManyField's implicit through model (if self.opts.auto_created). 

2442 Return True if the user has any of the given permissions ('add', 

2443 'change', etc.) for the model that points to the through model. 

2444 """ 

2445 opts = self.opts 

2446 # Find the target model of an auto-created many-to-many relationship. 

2447 for field in opts.fields: 

2448 if field.remote_field and field.remote_field.model != self.parent_model: 

2449 opts = field.remote_field.model._meta 

2450 break 

2451 return any( 

2452 request.user.has_perm( 

2453 "%s.%s" % (opts.app_label, get_permission_codename(perm, opts)) 

2454 ) 

2455 for perm in perms 

2456 ) 

2457 

2458 def has_add_permission(self, request, obj): 

2459 if self.opts.auto_created: 

2460 # Auto-created intermediate models don't have their own 

2461 # permissions. The user needs to have the change permission for the 

2462 # related model in order to be able to do anything with the 

2463 # intermediate model. 

2464 return self._has_any_perms_for_target_model(request, ["change"]) 

2465 return super().has_add_permission(request) 

2466 

2467 def has_change_permission(self, request, obj=None): 

2468 if self.opts.auto_created: 

2469 # Same comment as has_add_permission(). 

2470 return self._has_any_perms_for_target_model(request, ["change"]) 

2471 return super().has_change_permission(request) 

2472 

2473 def has_delete_permission(self, request, obj=None): 

2474 if self.opts.auto_created: 

2475 # Same comment as has_add_permission(). 

2476 return self._has_any_perms_for_target_model(request, ["change"]) 

2477 return super().has_delete_permission(request, obj) 

2478 

2479 def has_view_permission(self, request, obj=None): 

2480 if self.opts.auto_created: 

2481 # Same comment as has_add_permission(). The 'change' permission 

2482 # also implies the 'view' permission. 

2483 return self._has_any_perms_for_target_model(request, ["view", "change"]) 

2484 return super().has_view_permission(request) 

2485 

2486 

2487class StackedInline(InlineModelAdmin): 

2488 template = "admin/edit_inline/stacked.html" 

2489 

2490 

2491class TabularInline(InlineModelAdmin): 

2492 template = "admin/edit_inline/tabular.html"