﻿id	summary	reporter	owner	description	type	status	component	version	severity	resolution	keywords	cc	stage	has_patch	needs_docs	needs_tests	needs_better_patch	easy	ui_ux
33106	ModelMultipleChoiceField clean() method calls prepare_value instead of to_python	Adam McKay	nobody	"The [https://docs.djangoproject.com/en/3.2/ref/forms/validation/#form-and-field-validation 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 [https://hashids.org/ 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])])
}}}

"	New feature	closed	Forms	3.2	Normal	wontfix			Unreviewed	0	0	0	0	0	0
