#33106 closed New feature (wontfix)
ModelMultipleChoiceField clean() method calls prepare_value instead of to_python
| Reported by: | Adam McKay | Owned by: | nobody |
|---|---|---|---|
| Component: | Forms | Version: | 3.2 |
| Severity: | Normal | Keywords: | |
| Cc: | Triage Stage: | Unreviewed | |
| Has patch: | no | Needs documentation: | no |
| Needs tests: | no | Patch needs improvement: | no |
| Easy pickings: | no | UI/UX: | no |
Description
The Form and field validation documentation says:
The clean() method on a Field subclass is responsible for running to_python(), validate(), and run_validators() in the correct order and propagating their errors.
However the clean() method of ModelMultipleChoiceField calls value = self.prepare_value(value) which is causing issues for my use case of hiding primary keys on forms using a hashids module as I am subclassing the to_python and prepare_values methods to ensure the pk exposed to users are encoded/decoded as appropriate, however the to_python method is not called as an error “encodedValue” is not a valid value. is returned to the user because the value is not correctly decoded when it is checked as a valid choice.
I have written a test case for tests/model_forms/test_modelchoicefield.py in ModelChoiceFieldTests which for this test merely adds 42 to the pk before exposing it to the user to demonstrate the behaviour:
def test_clean_serializes_input(self):
class EncryptedModelMultipleChoiceField(forms.ModelMultipleChoiceField):
"""Hide pk by modifying by 42"""
def to_python(self, value):
if not value:
return []
if hasattr(value, '__iter__'):
return [int(getattr(v, 'pk', v)) - 42 for v in value]
return int(getattr(value, 'pk', value)) - 42
def prepare_value(self, value):
if not value:
return []
if hasattr(value, '__iter__'):
return [int(getattr(v, 'pk', v)) + 42 for v in value]
return int(getattr(value, 'pk', value)) + 42
f = EncryptedModelMultipleChoiceField(Category.objects.all())
print(f.widget.render('name', []),)
self.assertHTMLEqual(
f.widget.render('name', []),
"""<select name="name" multiple>
<option value="%s">Entertainment</option>
<option value="%s">A test</option>
<option value="%s">Third</option>
</select>""" % (self.c1.pk + 42, self.c2.pk + 42, self.c3.pk + 42),
)
with self.assertRaises(ValidationError):
f.clean('')
with self.assertRaises(ValidationError):
f.clean(None)
with self.assertRaises(ValidationError):
f.clean(0)
self.assertEqual(['Entertainment'], [c.name for c in f.clean([self.c1.pk + 42])])
self.assertEqual(['Entertainment', 'Third'], [c.name for c in f.clean([self.c1.pk + 42, self.c3.pk + 42])])
Change History (1)
comment:1 by , 4 years ago
| Resolution: | → wontfix |
|---|---|
| Status: | new → closed |
| Type: | Bug → New feature |
Hi Adam — thanks for the report. Interesting.
This kind of mapping has never really been supported, so I'd say this is a new feature rather than a bug. That's probably a small distinction but it does point to a way forward, which is that it would be good to see the required implementation out of Django before deciding whether to make a change here. (It's quite a niche use-case.)
Your test doesn't seem right. If I comment out the offending line, the test still fails (
43 is not one of the available choices.rather than85 is not one of the available choices.— so the42is not being applied the extra time, but neither is it being removed into_python) but now we get the additional failure from #26970 (4e861682904744b0ea3ead8552513c6f1a826c5a).I use hashids myself at times. I'm inclined to think addressing the mapping at the form
Fieldlevel is something of a no-man's land. I'd either push it towards the model field, which is what django-hashid-field does — thechoicethen just working — or (which is where I'd look since I don't tend to use the custom model field) push the mapping to a custom ModelChoiceIterator/ModelChoiceIteratorValue pair for generating the HTML and a custom widget to map the value back invalue_from_datadict(), thus keeping the mapping at the periphery. Otherwise a bit more work onModelMultipleChoiceFieldsubclass looks needed. (Likely Nathan on django-hashid-field would be able to guide you more.)