Coverage for /var/srv/projects/api.amasfac.comuna18.com/tmp/venv/lib/python3.9/site-packages/polymorphic/formsets/models.py: 21%

148 statements  

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

1from collections import OrderedDict 

2 

3from django import forms 

4from django.contrib.contenttypes.models import ContentType 

5from django.core.exceptions import ImproperlyConfigured, ValidationError 

6from django.forms.models import ( 

7 BaseInlineFormSet, 

8 BaseModelFormSet, 

9 ModelForm, 

10 inlineformset_factory, 

11 modelform_factory, 

12 modelformset_factory, 

13) 

14from django.utils.functional import cached_property 

15 

16from polymorphic.models import PolymorphicModel 

17 

18from .utils import add_media 

19 

20 

21class UnsupportedChildType(LookupError): 

22 pass 

23 

24 

25class PolymorphicFormSetChild: 

26 """ 

27 Metadata to define the inline of a polymorphic child. 

28 Provide this information in the :func:'polymorphic_inlineformset_factory' construction. 

29 """ 

30 

31 def __init__( 

32 self, 

33 model, 

34 form=ModelForm, 

35 fields=None, 

36 exclude=None, 

37 formfield_callback=None, 

38 widgets=None, 

39 localized_fields=None, 

40 labels=None, 

41 help_texts=None, 

42 error_messages=None, 

43 ): 

44 

45 self.model = model 

46 

47 # Instead of initializing the form here right away, 

48 # the settings are saved so get_form() can receive additional exclude kwargs. 

49 # This is mostly needed for the generic inline formsets 

50 self._form_base = form 

51 self.fields = fields 

52 self.exclude = exclude or () 

53 self.formfield_callback = formfield_callback 

54 self.widgets = widgets 

55 self.localized_fields = localized_fields 

56 self.labels = labels 

57 self.help_texts = help_texts 

58 self.error_messages = error_messages 

59 

60 @cached_property 

61 def content_type(self): 

62 """ 

63 Expose the ContentType that the child relates to. 

64 This can be used for the ''polymorphic_ctype'' field. 

65 """ 

66 return ContentType.objects.get_for_model(self.model, for_concrete_model=False) 

67 

68 def get_form(self, **kwargs): 

69 """ 

70 Construct the form class for the formset child. 

71 """ 

72 # Do what modelformset_factory() / inlineformset_factory() does to the 'form' argument; 

73 # Construct the form with the given ModelFormOptions values 

74 

75 # Fields can be overwritten. To support the global 'polymorphic_child_forms_factory' kwargs, 

76 # that doesn't completely replace all 'exclude' settings defined per child type, 

77 # we allow to define things like 'extra_...' fields that are amended to the current child settings. 

78 

79 exclude = list(self.exclude) 

80 extra_exclude = kwargs.pop("extra_exclude", None) 

81 if extra_exclude: 

82 exclude += list(extra_exclude) 

83 

84 defaults = { 

85 "form": self._form_base, 

86 "formfield_callback": self.formfield_callback, 

87 "fields": self.fields, 

88 "exclude": exclude, 

89 # 'for_concrete_model': for_concrete_model, 

90 "localized_fields": self.localized_fields, 

91 "labels": self.labels, 

92 "help_texts": self.help_texts, 

93 "error_messages": self.error_messages, 

94 "widgets": self.widgets, 

95 # 'field_classes': field_classes, 

96 } 

97 defaults.update(kwargs) 

98 

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

100 

101 

102def polymorphic_child_forms_factory(formset_children, **kwargs): 

103 """ 

104 Construct the forms for the formset children. 

105 This is mostly used internally, and rarely needs to be used by external projects. 

106 When using the factory methods (:func:'polymorphic_inlineformset_factory'), 

107 this feature is called already for you. 

108 """ 

109 child_forms = OrderedDict() 

110 

111 for formset_child in formset_children: 

112 child_forms[formset_child.model] = formset_child.get_form(**kwargs) 

113 

114 return child_forms 

115 

116 

117class BasePolymorphicModelFormSet(BaseModelFormSet): 

118 """ 

119 A formset that can produce different forms depending on the object type. 

120 

121 Note that the 'add' feature is therefore more complex, 

122 as all variations need ot be exposed somewhere. 

123 

124 When switching existing formsets to the polymorphic formset, 

125 note that the ID field will no longer be named ''model_ptr'', 

126 but just appear as ''id''. 

127 """ 

128 

129 # Assigned by the factory 

130 child_forms = OrderedDict() 

131 

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

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

134 self.queryset_data = self.get_queryset() 

135 

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

137 """ 

138 Create the form, depending on the model that's behind it. 

139 """ 

140 # BaseModelFormSet logic 

141 if self.is_bound and i < self.initial_form_count(): 

142 pk_key = f"{self.add_prefix(i)}-{self.model._meta.pk.name}" 

143 pk = self.data[pk_key] 

144 pk_field = self.model._meta.pk 

145 to_python = self._get_to_python(pk_field) 

146 pk = to_python(pk) 

147 kwargs["instance"] = self._existing_object(pk) 

148 if i < self.initial_form_count() and "instance" not in kwargs: 

149 kwargs["instance"] = self.get_queryset()[i] 

150 if i >= self.initial_form_count() and self.initial_extra: 

151 # Set initial values for extra forms 

152 try: 

153 kwargs["initial"] = self.initial_extra[i - self.initial_form_count()] 

154 except IndexError: 

155 pass 

156 

157 # BaseFormSet logic, with custom formset_class 

158 defaults = { 

159 "auto_id": self.auto_id, 

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

161 "error_class": self.error_class, 

162 } 

163 if self.is_bound: 

164 defaults["data"] = self.data 

165 defaults["files"] = self.files 

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

167 try: 

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

169 except IndexError: 

170 pass 

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

172 # the minimum forms. 

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

174 defaults["empty_permitted"] = True 

175 defaults["use_required_attribute"] = False 

176 defaults.update(kwargs) 

177 

178 # Need to find the model that will be displayed in this form. 

179 # Hence, peeking in the self.queryset_data beforehand. 

180 if self.is_bound: 

181 if "instance" in defaults: 

182 # Object is already bound to a model, won't change the content type 

183 model = defaults["instance"].get_real_instance_class() # allow proxy models 

184 else: 

185 # Extra or empty form, use the provided type. 

186 # Note this completely tru 

187 prefix = defaults["prefix"] 

188 try: 

189 ct_id = int(self.data[f"{prefix}-polymorphic_ctype"]) 

190 except (KeyError, ValueError): 

191 raise ValidationError( 

192 "Formset row {} has no 'polymorphic_ctype' defined!".format(prefix) 

193 ) 

194 

195 model = ContentType.objects.get_for_id(ct_id).model_class() 

196 if model not in self.child_forms: 

197 # Perform basic validation, as we skip the ChoiceField here. 

198 raise UnsupportedChildType( 

199 f"Child model type {model} is not part of the formset" 

200 ) 

201 else: 

202 if "instance" in defaults: 

203 model = defaults["instance"].get_real_instance_class() # allow proxy models 

204 elif "polymorphic_ctype" in defaults.get("initial", {}): 

205 model = defaults["initial"]["polymorphic_ctype"].model_class() 

206 elif i < len(self.queryset_data): 

207 model = self.queryset_data[i].__class__ 

208 else: 

209 # Extra forms, cycle between all types 

210 # TODO: take the 'extra' value of each child formset into account. 

211 total_known = len(self.queryset_data) 

212 child_models = list(self.child_forms.keys()) 

213 model = child_models[(i - total_known) % len(child_models)] 

214 

215 form_class = self.get_form_class(model) 

216 form = form_class(**defaults) 

217 self.add_fields(form, i) 

218 return form 

219 

220 def add_fields(self, form, index): 

221 """Add a hidden field for the content type.""" 

222 ct = ContentType.objects.get_for_model(form._meta.model, for_concrete_model=False) 

223 choices = [(ct.pk, ct)] # Single choice, existing forms can't change the value. 

224 form.fields["polymorphic_ctype"] = forms.TypedChoiceField( 

225 choices=choices, 

226 initial=ct.pk, 

227 required=False, 

228 widget=forms.HiddenInput, 

229 coerce=int, 

230 ) 

231 super().add_fields(form, index) 

232 

233 def get_form_class(self, model): 

234 """ 

235 Return the proper form class for the given model. 

236 """ 

237 if not self.child_forms: 

238 raise ImproperlyConfigured(f"No 'child_forms' defined in {self.__class__.__name__}") 

239 if not issubclass(model, PolymorphicModel): 

240 raise TypeError(f"Expect polymorphic model type, not {model}") 

241 

242 try: 

243 return self.child_forms[model] 

244 except KeyError: 

245 # This may happen when the query returns objects of a type that was not handled by the formset. 

246 raise UnsupportedChildType( 

247 "The '{}' found a '{}' model in the queryset, " 

248 "but no form class is registered to display it.".format( 

249 self.__class__.__name__, model.__name__ 

250 ) 

251 ) 

252 

253 def is_multipart(self): 

254 """ 

255 Returns True if the formset needs to be multipart, i.e. it 

256 has FileInput. Otherwise, False. 

257 """ 

258 return any(f.is_multipart() for f in self.empty_forms) 

259 

260 @property 

261 def media(self): 

262 # Include the media of all form types. 

263 # The form media includes all form widget media 

264 media = forms.Media() 

265 for form in self.empty_forms: 

266 add_media(media, form.media) 

267 return media 

268 

269 @cached_property 

270 def empty_forms(self): 

271 """ 

272 Return all possible empty forms 

273 """ 

274 forms = [] 

275 for model, form_class in self.child_forms.items(): 

276 kwargs = self.get_form_kwargs(None) 

277 

278 form = form_class( 

279 auto_id=self.auto_id, 

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

281 empty_permitted=True, 

282 use_required_attribute=False, 

283 **kwargs, 

284 ) 

285 self.add_fields(form, None) 

286 forms.append(form) 

287 return forms 

288 

289 @property 

290 def empty_form(self): 

291 # TODO: make an exception when can_add_base is defined? 

292 raise RuntimeError( 

293 "'empty_form' is not used in polymorphic formsets, use 'empty_forms' instead." 

294 ) 

295 

296 

297def polymorphic_modelformset_factory( 

298 model, 

299 formset_children, 

300 formset=BasePolymorphicModelFormSet, 

301 # Base field 

302 # TODO: should these fields be removed in favor of creating 

303 # the base form as a formset child too? 

304 form=ModelForm, 

305 fields=None, 

306 exclude=None, 

307 extra=1, 

308 can_order=False, 

309 can_delete=True, 

310 max_num=None, 

311 formfield_callback=None, 

312 widgets=None, 

313 validate_max=False, 

314 localized_fields=None, 

315 labels=None, 

316 help_texts=None, 

317 error_messages=None, 

318 min_num=None, 

319 validate_min=False, 

320 field_classes=None, 

321 child_form_kwargs=None, 

322): 

323 """ 

324 Construct the class for an polymorphic model formset. 

325 

326 All arguments are identical to :func:'~django.forms.models.modelformset_factory', 

327 with the exception of the ''formset_children'' argument. 

328 

329 :param formset_children: A list of all child :class:'PolymorphicFormSetChild' objects 

330 that tell the inline how to render the child model types. 

331 :type formset_children: Iterable[PolymorphicFormSetChild] 

332 :rtype: type 

333 """ 

334 kwargs = { 

335 "model": model, 

336 "form": form, 

337 "formfield_callback": formfield_callback, 

338 "formset": formset, 

339 "extra": extra, 

340 "can_delete": can_delete, 

341 "can_order": can_order, 

342 "fields": fields, 

343 "exclude": exclude, 

344 "min_num": min_num, 

345 "max_num": max_num, 

346 "widgets": widgets, 

347 "validate_min": validate_min, 

348 "validate_max": validate_max, 

349 "localized_fields": localized_fields, 

350 "labels": labels, 

351 "help_texts": help_texts, 

352 "error_messages": error_messages, 

353 "field_classes": field_classes, 

354 } 

355 FormSet = modelformset_factory(**kwargs) 

356 

357 child_kwargs = { 

358 # 'exclude': exclude, 

359 } 

360 if child_form_kwargs: 

361 child_kwargs.update(child_form_kwargs) 

362 

363 FormSet.child_forms = polymorphic_child_forms_factory(formset_children, **child_kwargs) 

364 return FormSet 

365 

366 

367class BasePolymorphicInlineFormSet(BaseInlineFormSet, BasePolymorphicModelFormSet): 

368 """ 

369 Polymorphic formset variation for inline formsets 

370 """ 

371 

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

373 return super()._construct_form(i, **kwargs) 

374 

375 

376def polymorphic_inlineformset_factory( 

377 parent_model, 

378 model, 

379 formset_children, 

380 formset=BasePolymorphicInlineFormSet, 

381 fk_name=None, 

382 # Base field 

383 # TODO: should these fields be removed in favor of creating 

384 # the base form as a formset child too? 

385 form=ModelForm, 

386 fields=None, 

387 exclude=None, 

388 extra=1, 

389 can_order=False, 

390 can_delete=True, 

391 max_num=None, 

392 formfield_callback=None, 

393 widgets=None, 

394 validate_max=False, 

395 localized_fields=None, 

396 labels=None, 

397 help_texts=None, 

398 error_messages=None, 

399 min_num=None, 

400 validate_min=False, 

401 field_classes=None, 

402 child_form_kwargs=None, 

403): 

404 """ 

405 Construct the class for an inline polymorphic formset. 

406 

407 All arguments are identical to :func:'~django.forms.models.inlineformset_factory', 

408 with the exception of the ''formset_children'' argument. 

409 

410 :param formset_children: A list of all child :class:'PolymorphicFormSetChild' objects 

411 that tell the inline how to render the child model types. 

412 :type formset_children: Iterable[PolymorphicFormSetChild] 

413 :rtype: type 

414 """ 

415 kwargs = { 

416 "parent_model": parent_model, 

417 "model": model, 

418 "form": form, 

419 "formfield_callback": formfield_callback, 

420 "formset": formset, 

421 "fk_name": fk_name, 

422 "extra": extra, 

423 "can_delete": can_delete, 

424 "can_order": can_order, 

425 "fields": fields, 

426 "exclude": exclude, 

427 "min_num": min_num, 

428 "max_num": max_num, 

429 "widgets": widgets, 

430 "validate_min": validate_min, 

431 "validate_max": validate_max, 

432 "localized_fields": localized_fields, 

433 "labels": labels, 

434 "help_texts": help_texts, 

435 "error_messages": error_messages, 

436 "field_classes": field_classes, 

437 } 

438 FormSet = inlineformset_factory(**kwargs) 

439 

440 child_kwargs = { 

441 # 'exclude': exclude, 

442 } 

443 if child_form_kwargs: 

444 child_kwargs.update(child_form_kwargs) 

445 

446 FormSet.child_forms = polymorphic_child_forms_factory(formset_children, **child_kwargs) 

447 return FormSet