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

260 statements  

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

1""" 

2Form Widget classes specific to the Django admin site. 

3""" 

4import copy 

5import json 

6 

7from django import forms 

8from django.conf import settings 

9from django.core.exceptions import ValidationError 

10from django.core.validators import URLValidator 

11from django.db.models import CASCADE 

12from django.urls import reverse 

13from django.urls.exceptions import NoReverseMatch 

14from django.utils.html import smart_urlquote 

15from django.utils.http import urlencode 

16from django.utils.text import Truncator 

17from django.utils.translation import get_language 

18from django.utils.translation import gettext as _ 

19 

20 

21class FilteredSelectMultiple(forms.SelectMultiple): 

22 """ 

23 A SelectMultiple with a JavaScript filter interface. 

24 

25 Note that the resulting JavaScript assumes that the jsi18n 

26 catalog has been loaded in the page 

27 """ 

28 

29 class Media: 

30 js = [ 

31 "admin/js/core.js", 

32 "admin/js/SelectBox.js", 

33 "admin/js/SelectFilter2.js", 

34 ] 

35 

36 def __init__(self, verbose_name, is_stacked, attrs=None, choices=()): 

37 self.verbose_name = verbose_name 

38 self.is_stacked = is_stacked 

39 super().__init__(attrs, choices) 

40 

41 def get_context(self, name, value, attrs): 

42 context = super().get_context(name, value, attrs) 

43 context["widget"]["attrs"]["class"] = "selectfilter" 

44 if self.is_stacked: 

45 context["widget"]["attrs"]["class"] += "stacked" 

46 context["widget"]["attrs"]["data-field-name"] = self.verbose_name 

47 context["widget"]["attrs"]["data-is-stacked"] = int(self.is_stacked) 

48 return context 

49 

50 

51class AdminDateWidget(forms.DateInput): 

52 class Media: 

53 js = [ 

54 "admin/js/calendar.js", 

55 "admin/js/admin/DateTimeShortcuts.js", 

56 ] 

57 

58 def __init__(self, attrs=None, format=None): 

59 attrs = {"class": "vDateField", "size": "10", **(attrs or {})} 

60 super().__init__(attrs=attrs, format=format) 

61 

62 

63class AdminTimeWidget(forms.TimeInput): 

64 class Media: 

65 js = [ 

66 "admin/js/calendar.js", 

67 "admin/js/admin/DateTimeShortcuts.js", 

68 ] 

69 

70 def __init__(self, attrs=None, format=None): 

71 attrs = {"class": "vTimeField", "size": "8", **(attrs or {})} 

72 super().__init__(attrs=attrs, format=format) 

73 

74 

75class AdminSplitDateTime(forms.SplitDateTimeWidget): 

76 """ 

77 A SplitDateTime Widget that has some admin-specific styling. 

78 """ 

79 

80 template_name = "admin/widgets/split_datetime.html" 

81 

82 def __init__(self, attrs=None): 

83 widgets = [AdminDateWidget, AdminTimeWidget] 

84 # Note that we're calling MultiWidget, not SplitDateTimeWidget, because 

85 # we want to define widgets. 

86 forms.MultiWidget.__init__(self, widgets, attrs) 

87 

88 def get_context(self, name, value, attrs): 

89 context = super().get_context(name, value, attrs) 

90 context["date_label"] = _("Date:") 

91 context["time_label"] = _("Time:") 

92 return context 

93 

94 

95class AdminRadioSelect(forms.RadioSelect): 

96 template_name = "admin/widgets/radio.html" 

97 

98 

99class AdminFileWidget(forms.ClearableFileInput): 

100 template_name = "admin/widgets/clearable_file_input.html" 

101 

102 

103def url_params_from_lookup_dict(lookups): 

104 """ 

105 Convert the type of lookups specified in a ForeignKey limit_choices_to 

106 attribute to a dictionary of query parameters 

107 """ 

108 params = {} 

109 if lookups and hasattr(lookups, "items"): 

110 for k, v in lookups.items(): 

111 if callable(v): 

112 v = v() 

113 if isinstance(v, (tuple, list)): 

114 v = ",".join(str(x) for x in v) 

115 elif isinstance(v, bool): 

116 v = ("0", "1")[v] 

117 else: 

118 v = str(v) 

119 params[k] = v 

120 return params 

121 

122 

123class ForeignKeyRawIdWidget(forms.TextInput): 

124 """ 

125 A Widget for displaying ForeignKeys in the "raw_id" interface rather than 

126 in a <select> box. 

127 """ 

128 

129 template_name = "admin/widgets/foreign_key_raw_id.html" 

130 

131 def __init__(self, rel, admin_site, attrs=None, using=None): 

132 self.rel = rel 

133 self.admin_site = admin_site 

134 self.db = using 

135 super().__init__(attrs) 

136 

137 def get_context(self, name, value, attrs): 

138 context = super().get_context(name, value, attrs) 

139 rel_to = self.rel.model 

140 if rel_to in self.admin_site._registry: 

141 # The related object is registered with the same AdminSite 

142 related_url = reverse( 

143 "admin:%s_%s_changelist" 

144 % ( 

145 rel_to._meta.app_label, 

146 rel_to._meta.model_name, 

147 ), 

148 current_app=self.admin_site.name, 

149 ) 

150 

151 params = self.url_parameters() 

152 if params: 

153 related_url += "?" + urlencode(params) 

154 context["related_url"] = related_url 

155 context["link_title"] = _("Lookup") 

156 # The JavaScript code looks for this class. 

157 context["widget"]["attrs"].setdefault("class", "vForeignKeyRawIdAdminField") 

158 else: 

159 context["related_url"] = None 

160 if context["widget"]["value"]: 

161 context["link_label"], context["link_url"] = self.label_and_url_for_value( 

162 value 

163 ) 

164 else: 

165 context["link_label"] = None 

166 return context 

167 

168 def base_url_parameters(self): 

169 limit_choices_to = self.rel.limit_choices_to 

170 if callable(limit_choices_to): 

171 limit_choices_to = limit_choices_to() 

172 return url_params_from_lookup_dict(limit_choices_to) 

173 

174 def url_parameters(self): 

175 from django.contrib.admin.views.main import TO_FIELD_VAR 

176 

177 params = self.base_url_parameters() 

178 params.update({TO_FIELD_VAR: self.rel.get_related_field().name}) 

179 return params 

180 

181 def label_and_url_for_value(self, value): 

182 key = self.rel.get_related_field().name 

183 try: 

184 obj = self.rel.model._default_manager.using(self.db).get(**{key: value}) 

185 except (ValueError, self.rel.model.DoesNotExist, ValidationError): 

186 return "", "" 

187 

188 try: 

189 url = reverse( 

190 "%s:%s_%s_change" 

191 % ( 

192 self.admin_site.name, 

193 obj._meta.app_label, 

194 obj._meta.object_name.lower(), 

195 ), 

196 args=(obj.pk,), 

197 ) 

198 except NoReverseMatch: 

199 url = "" # Admin not registered for target model. 

200 

201 return Truncator(obj).words(14), url 

202 

203 

204class ManyToManyRawIdWidget(ForeignKeyRawIdWidget): 

205 """ 

206 A Widget for displaying ManyToMany ids in the "raw_id" interface rather than 

207 in a <select multiple> box. 

208 """ 

209 

210 template_name = "admin/widgets/many_to_many_raw_id.html" 

211 

212 def get_context(self, name, value, attrs): 

213 context = super().get_context(name, value, attrs) 

214 if self.rel.model in self.admin_site._registry: 

215 # The related object is registered with the same AdminSite 

216 context["widget"]["attrs"]["class"] = "vManyToManyRawIdAdminField" 

217 return context 

218 

219 def url_parameters(self): 

220 return self.base_url_parameters() 

221 

222 def label_and_url_for_value(self, value): 

223 return "", "" 

224 

225 def value_from_datadict(self, data, files, name): 

226 value = data.get(name) 

227 if value: 

228 return value.split(",") 

229 

230 def format_value(self, value): 

231 return ",".join(str(v) for v in value) if value else "" 

232 

233 

234class RelatedFieldWidgetWrapper(forms.Widget): 

235 """ 

236 This class is a wrapper to a given widget to add the add icon for the 

237 admin interface. 

238 """ 

239 

240 template_name = "admin/widgets/related_widget_wrapper.html" 

241 

242 def __init__( 

243 self, 

244 widget, 

245 rel, 

246 admin_site, 

247 can_add_related=None, 

248 can_change_related=False, 

249 can_delete_related=False, 

250 can_view_related=False, 

251 ): 

252 self.needs_multipart_form = widget.needs_multipart_form 

253 self.attrs = widget.attrs 

254 self.choices = widget.choices 

255 self.widget = widget 

256 self.rel = rel 

257 # Backwards compatible check for whether a user can add related 

258 # objects. 

259 if can_add_related is None: 

260 can_add_related = rel.model in admin_site._registry 

261 self.can_add_related = can_add_related 

262 # XXX: The UX does not support multiple selected values. 

263 multiple = getattr(widget, "allow_multiple_selected", False) 

264 self.can_change_related = not multiple and can_change_related 

265 # XXX: The deletion UX can be confusing when dealing with cascading deletion. 

266 cascade = getattr(rel, "on_delete", None) is CASCADE 

267 self.can_delete_related = not multiple and not cascade and can_delete_related 

268 self.can_view_related = not multiple and can_view_related 

269 # so we can check if the related object is registered with this AdminSite 

270 self.admin_site = admin_site 

271 

272 def __deepcopy__(self, memo): 

273 obj = copy.copy(self) 

274 obj.widget = copy.deepcopy(self.widget, memo) 

275 obj.attrs = self.widget.attrs 

276 memo[id(self)] = obj 

277 return obj 

278 

279 @property 

280 def is_hidden(self): 

281 return self.widget.is_hidden 

282 

283 @property 

284 def media(self): 

285 return self.widget.media 

286 

287 def get_related_url(self, info, action, *args): 

288 return reverse( 

289 "admin:%s_%s_%s" % (info + (action,)), 

290 current_app=self.admin_site.name, 

291 args=args, 

292 ) 

293 

294 def get_context(self, name, value, attrs): 

295 from django.contrib.admin.views.main import IS_POPUP_VAR, TO_FIELD_VAR 

296 

297 rel_opts = self.rel.model._meta 

298 info = (rel_opts.app_label, rel_opts.model_name) 

299 self.widget.choices = self.choices 

300 url_params = "&".join( 

301 "%s=%s" % param 

302 for param in [ 

303 (TO_FIELD_VAR, self.rel.get_related_field().name), 

304 (IS_POPUP_VAR, 1), 

305 ] 

306 ) 

307 context = { 

308 "rendered_widget": self.widget.render(name, value, attrs), 

309 "is_hidden": self.is_hidden, 

310 "name": name, 

311 "url_params": url_params, 

312 "model": rel_opts.verbose_name, 

313 "can_add_related": self.can_add_related, 

314 "can_change_related": self.can_change_related, 

315 "can_delete_related": self.can_delete_related, 

316 "can_view_related": self.can_view_related, 

317 } 

318 if self.can_add_related: 

319 context["add_related_url"] = self.get_related_url(info, "add") 

320 if self.can_delete_related: 

321 context["delete_related_template_url"] = self.get_related_url( 

322 info, "delete", "__fk__" 

323 ) 

324 if self.can_view_related or self.can_change_related: 

325 context["change_related_template_url"] = self.get_related_url( 

326 info, "change", "__fk__" 

327 ) 

328 return context 

329 

330 def value_from_datadict(self, data, files, name): 

331 return self.widget.value_from_datadict(data, files, name) 

332 

333 def value_omitted_from_data(self, data, files, name): 

334 return self.widget.value_omitted_from_data(data, files, name) 

335 

336 def id_for_label(self, id_): 

337 return self.widget.id_for_label(id_) 

338 

339 

340class AdminTextareaWidget(forms.Textarea): 

341 def __init__(self, attrs=None): 

342 super().__init__(attrs={"class": "vLargeTextField", **(attrs or {})}) 

343 

344 

345class AdminTextInputWidget(forms.TextInput): 

346 def __init__(self, attrs=None): 

347 super().__init__(attrs={"class": "vTextField", **(attrs or {})}) 

348 

349 

350class AdminEmailInputWidget(forms.EmailInput): 

351 def __init__(self, attrs=None): 

352 super().__init__(attrs={"class": "vTextField", **(attrs or {})}) 

353 

354 

355class AdminURLFieldWidget(forms.URLInput): 

356 template_name = "admin/widgets/url.html" 

357 

358 def __init__(self, attrs=None, validator_class=URLValidator): 

359 super().__init__(attrs={"class": "vURLField", **(attrs or {})}) 

360 self.validator = validator_class() 

361 

362 def get_context(self, name, value, attrs): 

363 try: 

364 self.validator(value if value else "") 

365 url_valid = True 

366 except ValidationError: 

367 url_valid = False 

368 context = super().get_context(name, value, attrs) 

369 context["current_label"] = _("Currently:") 

370 context["change_label"] = _("Change:") 

371 context["widget"]["href"] = ( 

372 smart_urlquote(context["widget"]["value"]) if value else "" 

373 ) 

374 context["url_valid"] = url_valid 

375 return context 

376 

377 

378class AdminIntegerFieldWidget(forms.NumberInput): 

379 class_name = "vIntegerField" 

380 

381 def __init__(self, attrs=None): 

382 super().__init__(attrs={"class": self.class_name, **(attrs or {})}) 

383 

384 

385class AdminBigIntegerFieldWidget(AdminIntegerFieldWidget): 

386 class_name = "vBigIntegerField" 

387 

388 

389class AdminUUIDInputWidget(forms.TextInput): 

390 def __init__(self, attrs=None): 

391 super().__init__(attrs={"class": "vUUIDField", **(attrs or {})}) 

392 

393 

394# Mapping of lowercase language codes [returned by Django's get_language()] to 

395# language codes supported by select2. 

396# See django/contrib/admin/static/admin/js/vendor/select2/i18n/* 

397SELECT2_TRANSLATIONS = { 

398 x.lower(): x 

399 for x in [ 

400 "ar", 

401 "az", 

402 "bg", 

403 "ca", 

404 "cs", 

405 "da", 

406 "de", 

407 "el", 

408 "en", 

409 "es", 

410 "et", 

411 "eu", 

412 "fa", 

413 "fi", 

414 "fr", 

415 "gl", 

416 "he", 

417 "hi", 

418 "hr", 

419 "hu", 

420 "id", 

421 "is", 

422 "it", 

423 "ja", 

424 "km", 

425 "ko", 

426 "lt", 

427 "lv", 

428 "mk", 

429 "ms", 

430 "nb", 

431 "nl", 

432 "pl", 

433 "pt-BR", 

434 "pt", 

435 "ro", 

436 "ru", 

437 "sk", 

438 "sr-Cyrl", 

439 "sr", 

440 "sv", 

441 "th", 

442 "tr", 

443 "uk", 

444 "vi", 

445 ] 

446} 

447SELECT2_TRANSLATIONS.update({"zh-hans": "zh-CN", "zh-hant": "zh-TW"}) 

448 

449 

450class AutocompleteMixin: 

451 """ 

452 Select widget mixin that loads options from AutocompleteJsonView via AJAX. 

453 

454 Renders the necessary data attributes for select2 and adds the static form 

455 media. 

456 """ 

457 

458 url_name = "%s:autocomplete" 

459 

460 def __init__(self, field, admin_site, attrs=None, choices=(), using=None): 

461 self.field = field 

462 self.admin_site = admin_site 

463 self.db = using 

464 self.choices = choices 

465 self.attrs = {} if attrs is None else attrs.copy() 

466 self.i18n_name = SELECT2_TRANSLATIONS.get(get_language()) 

467 

468 def get_url(self): 

469 return reverse(self.url_name % self.admin_site.name) 

470 

471 def build_attrs(self, base_attrs, extra_attrs=None): 

472 """ 

473 Set select2's AJAX attributes. 

474 

475 Attributes can be set using the html5 data attribute. 

476 Nested attributes require a double dash as per 

477 https://select2.org/configuration/data-attributes#nested-subkey-options 

478 """ 

479 attrs = super().build_attrs(base_attrs, extra_attrs=extra_attrs) 

480 attrs.setdefault("class", "") 

481 attrs.update( 

482 { 

483 "data-ajax--cache": "true", 

484 "data-ajax--delay": 250, 

485 "data-ajax--type": "GET", 

486 "data-ajax--url": self.get_url(), 

487 "data-app-label": self.field.model._meta.app_label, 

488 "data-model-name": self.field.model._meta.model_name, 

489 "data-field-name": self.field.name, 

490 "data-theme": "admin-autocomplete", 

491 "data-allow-clear": json.dumps(not self.is_required), 

492 "data-placeholder": "", # Allows clearing of the input. 

493 "lang": self.i18n_name, 

494 "class": attrs["class"] 

495 + (" " if attrs["class"] else "") 

496 + "admin-autocomplete", 

497 } 

498 ) 

499 return attrs 

500 

501 def optgroups(self, name, value, attr=None): 

502 """Return selected options based on the ModelChoiceIterator.""" 

503 default = (None, [], 0) 

504 groups = [default] 

505 has_selected = False 

506 selected_choices = { 

507 str(v) for v in value if str(v) not in self.choices.field.empty_values 

508 } 

509 if not self.is_required and not self.allow_multiple_selected: 

510 default[1].append(self.create_option(name, "", "", False, 0)) 

511 remote_model_opts = self.field.remote_field.model._meta 

512 to_field_name = getattr( 

513 self.field.remote_field, "field_name", remote_model_opts.pk.attname 

514 ) 

515 to_field_name = remote_model_opts.get_field(to_field_name).attname 

516 choices = ( 

517 (getattr(obj, to_field_name), self.choices.field.label_from_instance(obj)) 

518 for obj in self.choices.queryset.using(self.db).filter( 

519 **{"%s__in" % to_field_name: selected_choices} 

520 ) 

521 ) 

522 for option_value, option_label in choices: 

523 selected = str(option_value) in value and ( 

524 has_selected is False or self.allow_multiple_selected 

525 ) 

526 has_selected |= selected 

527 index = len(default[1]) 

528 subgroup = default[1] 

529 subgroup.append( 

530 self.create_option( 

531 name, option_value, option_label, selected_choices, index 

532 ) 

533 ) 

534 return groups 

535 

536 @property 

537 def media(self): 

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

539 i18n_file = ( 

540 ("admin/js/vendor/select2/i18n/%s.js" % self.i18n_name,) 

541 if self.i18n_name 

542 else () 

543 ) 

544 return forms.Media( 

545 js=( 

546 "admin/js/vendor/jquery/jquery%s.js" % extra, 

547 "admin/js/vendor/select2/select2.full%s.js" % extra, 

548 ) 

549 + i18n_file 

550 + ( 

551 "admin/js/jquery.init.js", 

552 "admin/js/autocomplete.js", 

553 ), 

554 css={ 

555 "screen": ( 

556 "admin/css/vendor/select2/select2%s.css" % extra, 

557 "admin/css/autocomplete.css", 

558 ), 

559 }, 

560 ) 

561 

562 

563class AutocompleteSelect(AutocompleteMixin, forms.Select): 

564 pass 

565 

566 

567class AutocompleteSelectMultiple(AutocompleteMixin, forms.SelectMultiple): 

568 pass