djangopython-3.xdjango-modelsmodelchoicefield

in forms.py when i set "to_field_name" to more than one column name, it's giving me error


I have created a label_form_instance for modelchoicefield but the values in html are showing primary key values. To get rid of that, i use to_field_names but i can't provide more than one column name in it.

class firearmChoiceField(forms.ModelChoiceField):

    def label_from_instance(self, obj):
        return '%s%s, %s'%(obj.make,obj.firearm_model,obj.serial_no)


self.fields['firearm'] = firearmChoiceField(queryset = firearm_db.objects.all(),to_field_name="make,firearm_model,serial_no",required=False,empty_label='Select Firearm', widget = forms.Select(attrs={'label': ' ','class': 'form-control',}))

Solution

  • You can patch the prepare_value [GitHub] and to_python [GitHub] functions for that, for example:

    from django.core.exceptions import ValidationError
    
    class firearmChoiceField(forms.ModelChoiceField):
    
        def prepare_value(self, value):
            if hasattr(value, '_meta'):
                return '{}:{}:{}'.format(value.make,value.firearm_model,value.serial_no)
            return super().prepare_value(value)
    
        def to_python(self, value):
            if value in self.empty_values:
                return None
            try:
                make, firmod, serial = value.split(':')
                return firearm_db.objects.get(
                    make=make,
                    firearm_model=firmod,
                    serial_no=serial
                )
            except (ValueError, TypeError, firearm_db.DoesNotExist):
                raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')

    You thus should not specify the field_name here. In fact if we look at the original implemention, we see how this field_name is used:

    class ModelChoiceField(ChoiceField):
    
        # ...
    
        def prepare_value(self, value):
            if hasattr(value, '_meta'):
                if self.to_field_name:
                    return value.serializable_value(self.to_field_name)
                else:
                    return value.pk
            return super().prepare_value(value)
    
        def to_python(self, value):
            if value in self.empty_values:
                return None
            try:
                key = self.to_field_name or 'pk'
                value = self.queryset.get(**{key: value})
            except (ValueError, TypeError, self.queryset.model.DoesNotExist):
                raise ValidationError(self.error_messages['invalid_choice'], code='invalid_choice')
            return value

    In the prepare_value, we thus convert an object (here a firearm_db object) into a string that holds the value used in the <option value="...">s. The to_python function on the other hand performs the transformation back to a firearm object (or None in case the selection is empty).

    You will have to ensure that the two functions are each other inverse: each mapping with prepare_value should result in the same object when we perform a to_python on it. If for example here the make contains a colon (:), then this will fail, so it might require some extra finetuning.

    That being said, I am not sure why you want to use a more complicated value, and not use a primary key, a slug, or some hashed value for this.