Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/django/forms/formsets.py: 23%

242 statements  

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

1from django.core.exceptions import ValidationError 

2from django.forms import Form 

3from django.forms.fields import BooleanField, IntegerField 

4from django.forms.renderers import get_default_renderer 

5from django.forms.utils import ErrorList, RenderableFormMixin 

6from django.forms.widgets import CheckboxInput, HiddenInput, NumberInput 

7from django.utils.functional import cached_property 

8from django.utils.translation import gettext_lazy as _ 

9from django.utils.translation import ngettext 

10 

11__all__ = ("BaseFormSet", "formset_factory", "all_valid") 

12 

13# special field names 

14TOTAL_FORM_COUNT = "TOTAL_FORMS" 

15INITIAL_FORM_COUNT = "INITIAL_FORMS" 

16MIN_NUM_FORM_COUNT = "MIN_NUM_FORMS" 

17MAX_NUM_FORM_COUNT = "MAX_NUM_FORMS" 

18ORDERING_FIELD_NAME = "ORDER" 

19DELETION_FIELD_NAME = "DELETE" 

20 

21# default minimum number of forms in a formset 

22DEFAULT_MIN_NUM = 0 

23 

24# default maximum number of forms in a formset, to prevent memory exhaustion 

25DEFAULT_MAX_NUM = 1000 

26 

27 

28class ManagementForm(Form): 

29 """ 

30 Keep track of how many form instances are displayed on the page. If adding 

31 new forms via JavaScript, you should increment the count field of this form 

32 as well. 

33 """ 

34 

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

36 self.base_fields[TOTAL_FORM_COUNT] = IntegerField(widget=HiddenInput) 

37 self.base_fields[INITIAL_FORM_COUNT] = IntegerField(widget=HiddenInput) 

38 # MIN_NUM_FORM_COUNT and MAX_NUM_FORM_COUNT are output with the rest of 

39 # the management form, but only for the convenience of client-side 

40 # code. The POST value of them returned from the client is not checked. 

41 self.base_fields[MIN_NUM_FORM_COUNT] = IntegerField( 

42 required=False, widget=HiddenInput 

43 ) 

44 self.base_fields[MAX_NUM_FORM_COUNT] = IntegerField( 

45 required=False, widget=HiddenInput 

46 ) 

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

48 

49 def clean(self): 

50 cleaned_data = super().clean() 

51 # When the management form is invalid, we don't know how many forms 

52 # were submitted. 

53 cleaned_data.setdefault(TOTAL_FORM_COUNT, 0) 

54 cleaned_data.setdefault(INITIAL_FORM_COUNT, 0) 

55 return cleaned_data 

56 

57 

58class BaseFormSet(RenderableFormMixin): 

59 """ 

60 A collection of instances of the same Form class. 

61 """ 

62 

63 deletion_widget = CheckboxInput 

64 ordering_widget = NumberInput 

65 default_error_messages = { 

66 "missing_management_form": _( 

67 "ManagementForm data is missing or has been tampered with. Missing fields: " 

68 "%(field_names)s. You may need to file a bug report if the issue persists." 

69 ), 

70 } 

71 template_name = "django/forms/formsets/default.html" 

72 template_name_p = "django/forms/formsets/p.html" 

73 template_name_table = "django/forms/formsets/table.html" 

74 template_name_ul = "django/forms/formsets/ul.html" 

75 

76 def __init__( 

77 self, 

78 data=None, 

79 files=None, 

80 auto_id="id_%s", 

81 prefix=None, 

82 initial=None, 

83 error_class=ErrorList, 

84 form_kwargs=None, 

85 error_messages=None, 

86 ): 

87 self.is_bound = data is not None or files is not None 

88 self.prefix = prefix or self.get_default_prefix() 

89 self.auto_id = auto_id 

90 self.data = data or {} 

91 self.files = files or {} 

92 self.initial = initial 

93 self.form_kwargs = form_kwargs or {} 

94 self.error_class = error_class 

95 self._errors = None 

96 self._non_form_errors = None 

97 

98 messages = {} 

99 for cls in reversed(type(self).__mro__): 

100 messages.update(getattr(cls, "default_error_messages", {})) 

101 if error_messages is not None: 

102 messages.update(error_messages) 

103 self.error_messages = messages 

104 

105 def __iter__(self): 

106 """Yield the forms in the order they should be rendered.""" 

107 return iter(self.forms) 

108 

109 def __getitem__(self, index): 

110 """Return the form at the given index, based on the rendering order.""" 

111 return self.forms[index] 

112 

113 def __len__(self): 

114 return len(self.forms) 

115 

116 def __bool__(self): 

117 """ 

118 Return True since all formsets have a management form which is not 

119 included in the length. 

120 """ 

121 return True 

122 

123 @cached_property 

124 def management_form(self): 

125 """Return the ManagementForm instance for this FormSet.""" 

126 if self.is_bound: 

127 form = ManagementForm( 

128 self.data, 

129 auto_id=self.auto_id, 

130 prefix=self.prefix, 

131 renderer=self.renderer, 

132 ) 

133 form.full_clean() 

134 else: 

135 form = ManagementForm( 

136 auto_id=self.auto_id, 

137 prefix=self.prefix, 

138 initial={ 

139 TOTAL_FORM_COUNT: self.total_form_count(), 

140 INITIAL_FORM_COUNT: self.initial_form_count(), 

141 MIN_NUM_FORM_COUNT: self.min_num, 

142 MAX_NUM_FORM_COUNT: self.max_num, 

143 }, 

144 renderer=self.renderer, 

145 ) 

146 return form 

147 

148 def total_form_count(self): 

149 """Return the total number of forms in this FormSet.""" 

150 if self.is_bound: 

151 # return absolute_max if it is lower than the actual total form 

152 # count in the data; this is DoS protection to prevent clients 

153 # from forcing the server to instantiate arbitrary numbers of 

154 # forms 

155 return min( 

156 self.management_form.cleaned_data[TOTAL_FORM_COUNT], self.absolute_max 

157 ) 

158 else: 

159 initial_forms = self.initial_form_count() 

160 total_forms = max(initial_forms, self.min_num) + self.extra 

161 # Allow all existing related objects/inlines to be displayed, 

162 # but don't allow extra beyond max_num. 

163 if initial_forms > self.max_num >= 0: 

164 total_forms = initial_forms 

165 elif total_forms > self.max_num >= 0: 

166 total_forms = self.max_num 

167 return total_forms 

168 

169 def initial_form_count(self): 

170 """Return the number of forms that are required in this FormSet.""" 

171 if self.is_bound: 

172 return self.management_form.cleaned_data[INITIAL_FORM_COUNT] 

173 else: 

174 # Use the length of the initial data if it's there, 0 otherwise. 

175 initial_forms = len(self.initial) if self.initial else 0 

176 return initial_forms 

177 

178 @cached_property 

179 def forms(self): 

180 """Instantiate forms at first property access.""" 

181 # DoS protection is included in total_form_count() 

182 return [ 

183 self._construct_form(i, **self.get_form_kwargs(i)) 

184 for i in range(self.total_form_count()) 

185 ] 

186 

187 def get_form_kwargs(self, index): 

188 """ 

189 Return additional keyword arguments for each individual formset form. 

190 

191 index will be None if the form being constructed is a new empty 

192 form. 

193 """ 

194 return self.form_kwargs.copy() 

195 

196 def _construct_form(self, i, **kwargs): 

197 """Instantiate and return the i-th form instance in a formset.""" 

198 defaults = { 

199 "auto_id": self.auto_id, 

200 "prefix": self.add_prefix(i), 

201 "error_class": self.error_class, 

202 # Don't render the HTML 'required' attribute as it may cause 

203 # incorrect validation for extra, optional, and deleted 

204 # forms in the formset. 

205 "use_required_attribute": False, 

206 "renderer": self.renderer, 

207 } 

208 if self.is_bound: 

209 defaults["data"] = self.data 

210 defaults["files"] = self.files 

211 if self.initial and "initial" not in kwargs: 

212 try: 

213 defaults["initial"] = self.initial[i] 

214 except IndexError: 

215 pass 

216 # Allow extra forms to be empty, unless they're part of 

217 # the minimum forms. 

218 if i >= self.initial_form_count() and i >= self.min_num: 

219 defaults["empty_permitted"] = True 

220 defaults.update(kwargs) 

221 form = self.form(**defaults) 

222 self.add_fields(form, i) 

223 return form 

224 

225 @property 

226 def initial_forms(self): 

227 """Return a list of all the initial forms in this formset.""" 

228 return self.forms[: self.initial_form_count()] 

229 

230 @property 

231 def extra_forms(self): 

232 """Return a list of all the extra forms in this formset.""" 

233 return self.forms[self.initial_form_count() :] 

234 

235 @property 

236 def empty_form(self): 

237 form = self.form( 

238 auto_id=self.auto_id, 

239 prefix=self.add_prefix("__prefix__"), 

240 empty_permitted=True, 

241 use_required_attribute=False, 

242 **self.get_form_kwargs(None), 

243 renderer=self.renderer, 

244 ) 

245 self.add_fields(form, None) 

246 return form 

247 

248 @property 

249 def cleaned_data(self): 

250 """ 

251 Return a list of form.cleaned_data dicts for every form in self.forms. 

252 """ 

253 if not self.is_valid(): 

254 raise AttributeError( 

255 "'%s' object has no attribute 'cleaned_data'" % self.__class__.__name__ 

256 ) 

257 return [form.cleaned_data for form in self.forms] 

258 

259 @property 

260 def deleted_forms(self): 

261 """Return a list of forms that have been marked for deletion.""" 

262 if not self.is_valid() or not self.can_delete: 

263 return [] 

264 # construct _deleted_form_indexes which is just a list of form indexes 

265 # that have had their deletion widget set to True 

266 if not hasattr(self, "_deleted_form_indexes"): 

267 self._deleted_form_indexes = [] 

268 for i, form in enumerate(self.forms): 

269 # if this is an extra form and hasn't changed, don't consider it 

270 if i >= self.initial_form_count() and not form.has_changed(): 

271 continue 

272 if self._should_delete_form(form): 

273 self._deleted_form_indexes.append(i) 

274 return [self.forms[i] for i in self._deleted_form_indexes] 

275 

276 @property 

277 def ordered_forms(self): 

278 """ 

279 Return a list of form in the order specified by the incoming data. 

280 Raise an AttributeError if ordering is not allowed. 

281 """ 

282 if not self.is_valid() or not self.can_order: 

283 raise AttributeError( 

284 "'%s' object has no attribute 'ordered_forms'" % self.__class__.__name__ 

285 ) 

286 # Construct _ordering, which is a list of (form_index, order_field_value) 

287 # tuples. After constructing this list, we'll sort it by order_field_value 

288 # so we have a way to get to the form indexes in the order specified 

289 # by the form data. 

290 if not hasattr(self, "_ordering"): 

291 self._ordering = [] 

292 for i, form in enumerate(self.forms): 

293 # if this is an extra form and hasn't changed, don't consider it 

294 if i >= self.initial_form_count() and not form.has_changed(): 

295 continue 

296 # don't add data marked for deletion to self.ordered_data 

297 if self.can_delete and self._should_delete_form(form): 

298 continue 

299 self._ordering.append((i, form.cleaned_data[ORDERING_FIELD_NAME])) 

300 # After we're done populating self._ordering, sort it. 

301 # A sort function to order things numerically ascending, but 

302 # None should be sorted below anything else. Allowing None as 

303 # a comparison value makes it so we can leave ordering fields 

304 # blank. 

305 

306 def compare_ordering_key(k): 

307 if k[1] is None: 

308 return (1, 0) # +infinity, larger than any number 

309 return (0, k[1]) 

310 

311 self._ordering.sort(key=compare_ordering_key) 

312 # Return a list of form.cleaned_data dicts in the order specified by 

313 # the form data. 

314 return [self.forms[i[0]] for i in self._ordering] 

315 

316 @classmethod 

317 def get_default_prefix(cls): 

318 return "form" 

319 

320 @classmethod 

321 def get_deletion_widget(cls): 

322 return cls.deletion_widget 

323 

324 @classmethod 

325 def get_ordering_widget(cls): 

326 return cls.ordering_widget 

327 

328 def non_form_errors(self): 

329 """ 

330 Return an ErrorList of errors that aren't associated with a particular 

331 form -- i.e., from formset.clean(). Return an empty ErrorList if there 

332 are none. 

333 """ 

334 if self._non_form_errors is None: 

335 self.full_clean() 

336 return self._non_form_errors 

337 

338 @property 

339 def errors(self): 

340 """Return a list of form.errors for every form in self.forms.""" 

341 if self._errors is None: 

342 self.full_clean() 

343 return self._errors 

344 

345 def total_error_count(self): 

346 """Return the number of errors across all forms in the formset.""" 

347 return len(self.non_form_errors()) + sum( 

348 len(form_errors) for form_errors in self.errors 

349 ) 

350 

351 def _should_delete_form(self, form): 

352 """Return whether or not the form was marked for deletion.""" 

353 return form.cleaned_data.get(DELETION_FIELD_NAME, False) 

354 

355 def is_valid(self): 

356 """Return True if every form in self.forms is valid.""" 

357 if not self.is_bound: 

358 return False 

359 # Accessing errors triggers a full clean the first time only. 

360 self.errors 

361 # List comprehension ensures is_valid() is called for all forms. 

362 # Forms due to be deleted shouldn't cause the formset to be invalid. 

363 forms_valid = all( 

364 [ 

365 form.is_valid() 

366 for form in self.forms 

367 if not (self.can_delete and self._should_delete_form(form)) 

368 ] 

369 ) 

370 return forms_valid and not self.non_form_errors() 

371 

372 def full_clean(self): 

373 """ 

374 Clean all of self.data and populate self._errors and 

375 self._non_form_errors. 

376 """ 

377 self._errors = [] 

378 self._non_form_errors = self.error_class( 

379 error_class="nonform", renderer=self.renderer 

380 ) 

381 empty_forms_count = 0 

382 

383 if not self.is_bound: # Stop further processing. 

384 return 

385 

386 if not self.management_form.is_valid(): 

387 error = ValidationError( 

388 self.error_messages["missing_management_form"], 

389 params={ 

390 "field_names": ", ".join( 

391 self.management_form.add_prefix(field_name) 

392 for field_name in self.management_form.errors 

393 ), 

394 }, 

395 code="missing_management_form", 

396 ) 

397 self._non_form_errors.append(error) 

398 

399 for i, form in enumerate(self.forms): 

400 # Empty forms are unchanged forms beyond those with initial data. 

401 if not form.has_changed() and i >= self.initial_form_count(): 

402 empty_forms_count += 1 

403 # Accessing errors calls full_clean() if necessary. 

404 # _should_delete_form() requires cleaned_data. 

405 form_errors = form.errors 

406 if self.can_delete and self._should_delete_form(form): 

407 continue 

408 self._errors.append(form_errors) 

409 try: 

410 if ( 

411 self.validate_max 

412 and self.total_form_count() - len(self.deleted_forms) > self.max_num 

413 ) or self.management_form.cleaned_data[ 

414 TOTAL_FORM_COUNT 

415 ] > self.absolute_max: 

416 raise ValidationError( 

417 ngettext( 

418 "Please submit at most %d form.", 

419 "Please submit at most %d forms.", 

420 self.max_num, 

421 ) 

422 % self.max_num, 

423 code="too_many_forms", 

424 ) 

425 if ( 

426 self.validate_min 

427 and self.total_form_count() 

428 - len(self.deleted_forms) 

429 - empty_forms_count 

430 < self.min_num 

431 ): 

432 raise ValidationError( 

433 ngettext( 

434 "Please submit at least %d form.", 

435 "Please submit at least %d forms.", 

436 self.min_num, 

437 ) 

438 % self.min_num, 

439 code="too_few_forms", 

440 ) 

441 # Give self.clean() a chance to do cross-form validation. 

442 self.clean() 

443 except ValidationError as e: 

444 self._non_form_errors = self.error_class( 

445 e.error_list, 

446 error_class="nonform", 

447 renderer=self.renderer, 

448 ) 

449 

450 def clean(self): 

451 """ 

452 Hook for doing any extra formset-wide cleaning after Form.clean() has 

453 been called on every form. Any ValidationError raised by this method 

454 will not be associated with a particular form; it will be accessible 

455 via formset.non_form_errors() 

456 """ 

457 pass 

458 

459 def has_changed(self): 

460 """Return True if data in any form differs from initial.""" 

461 return any(form.has_changed() for form in self) 

462 

463 def add_fields(self, form, index): 

464 """A hook for adding extra fields on to each form instance.""" 

465 initial_form_count = self.initial_form_count() 

466 if self.can_order: 

467 # Only pre-fill the ordering field for initial forms. 

468 if index is not None and index < initial_form_count: 

469 form.fields[ORDERING_FIELD_NAME] = IntegerField( 

470 label=_("Order"), 

471 initial=index + 1, 

472 required=False, 

473 widget=self.get_ordering_widget(), 

474 ) 

475 else: 

476 form.fields[ORDERING_FIELD_NAME] = IntegerField( 

477 label=_("Order"), 

478 required=False, 

479 widget=self.get_ordering_widget(), 

480 ) 

481 if self.can_delete and (self.can_delete_extra or index < initial_form_count): 

482 form.fields[DELETION_FIELD_NAME] = BooleanField( 

483 label=_("Delete"), 

484 required=False, 

485 widget=self.get_deletion_widget(), 

486 ) 

487 

488 def add_prefix(self, index): 

489 return "%s-%s" % (self.prefix, index) 

490 

491 def is_multipart(self): 

492 """ 

493 Return True if the formset needs to be multipart, i.e. it 

494 has FileInput, or False otherwise. 

495 """ 

496 if self.forms: 

497 return self.forms[0].is_multipart() 

498 else: 

499 return self.empty_form.is_multipart() 

500 

501 @property 

502 def media(self): 

503 # All the forms on a FormSet are the same, so you only need to 

504 # interrogate the first form for media. 

505 if self.forms: 

506 return self.forms[0].media 

507 else: 

508 return self.empty_form.media 

509 

510 def get_context(self): 

511 return {"formset": self} 

512 

513 

514def formset_factory( 

515 form, 

516 formset=BaseFormSet, 

517 extra=1, 

518 can_order=False, 

519 can_delete=False, 

520 max_num=None, 

521 validate_max=False, 

522 min_num=None, 

523 validate_min=False, 

524 absolute_max=None, 

525 can_delete_extra=True, 

526 renderer=None, 

527): 

528 """Return a FormSet for the given form class.""" 

529 if min_num is None: 

530 min_num = DEFAULT_MIN_NUM 

531 if max_num is None: 

532 max_num = DEFAULT_MAX_NUM 

533 # absolute_max is a hard limit on forms instantiated, to prevent 

534 # memory-exhaustion attacks. Default to max_num + DEFAULT_MAX_NUM 

535 # (which is 2 * DEFAULT_MAX_NUM if max_num is None in the first place). 

536 if absolute_max is None: 

537 absolute_max = max_num + DEFAULT_MAX_NUM 

538 if max_num > absolute_max: 

539 raise ValueError("'absolute_max' must be greater or equal to 'max_num'.") 

540 attrs = { 

541 "form": form, 

542 "extra": extra, 

543 "can_order": can_order, 

544 "can_delete": can_delete, 

545 "can_delete_extra": can_delete_extra, 

546 "min_num": min_num, 

547 "max_num": max_num, 

548 "absolute_max": absolute_max, 

549 "validate_min": validate_min, 

550 "validate_max": validate_max, 

551 "renderer": renderer or get_default_renderer(), 

552 } 

553 return type(form.__name__ + "FormSet", (formset,), attrs) 

554 

555 

556def all_valid(formsets): 

557 """Validate every formset and return True if all are valid.""" 

558 # List comprehension ensures is_valid() is called for all formsets. 

559 return all([formset.is_valid() for formset in formsets])