I just wrote Field and Widget subclasses, that solve this particular problem and could be used with JS autocompletion, for example - and is reusable. Still, it required more work than your solution and I'm not sure whether you'll want to use mine or not. Either way - I hope I'll get few upvotes - I spent quite some time and effort writing this...
Instead of defining your ModelForm like you did and messing with clean_
I suggest something like that:
class SongForm(forms.ModelForm):
artist = CustomModelChoiceField( queryset = Artist.objects.all(), query_field = "name" )
class Meta:
model = Song
Now, CustomModelChoiceField (I can't think of better name for the class) is ModelChoiceField subclass, which is good, because we can use queryset
argument to narrow acceptable choices. If widget
argument is not present, like above, the default one for this field is used (more about it later). query_field
is optional and defaults to "pk"
. So, here is the field code:
class CustomModelChoiceField( forms.ModelChoiceField ):
def __init__( self, queryset, query_field = "pk", **kwargs ):
if "widget" not in kwargs:
kwargs["widget"] = ModelTextInput( model_class = queryset.model, query_field = query_field )
super( CustomModelChoiceField, self ).__init__( queryset, **kwargs )
def to_python( self, value ):
try:
int(value)
except:
from django.core.exceptions import ValidationError
raise ValidationError(self.error_messages['invalid_choice'])
return super( CustomModelChoiceField, self ).to_python( value )
What body of __init__
means is that setting widget = None
during creation of CustomModelChoiceField
gives us plain ModelChoiceField
(which was very helpful while debugging...). Now, actual work is done in ModelTextInput
widget:
class ModelTextInput( forms.TextInput ):
def __init__( self, model_class, query_field, attrs = None ):
self.model_class = model_class
self.query_field = query_field
super( ModelTextInput, self ).__init__( attrs )
def render(self, name, value, attrs = None ):
try:
obj = self.model_class.objects.get( pk = value )
value = getattr( obj, self.query_field )
except:
pass
return super(ModelTextInput, self).render( name, value, attrs )
def value_from_datadict( self, data, files, name ):
try:
return self.model_class.objects.get( **{ self.query_field : data[name] } ).id
except:
return data[name]
It's essentially TextInput, that is aware of two additional things - which attribute of which model it represents. ( model_class
should be replaced with queryset
for narrowing of possible choices to actually work, I'll fix it later). Looking at implementation of value_from_datadict
it's easy to spot why to_python
in the field had to be overridden - it expects int
value, but does not check if it's true - and just passes the value to associated model, which fails with ugly exception.
I tested this for a while and it works - you can specify different model fields by which form field will try to find your artist
, form error handling is done automatically by base classes and you don't need to write custom clean_
method every time you want to use similar functionality.
I'm too tired right now, but I'll try to edit this post (and code) tomorrow.