views:

26

answers:

2

I have changed a ForeignKey in a model form, to use a TextBox instead. Then I override clean method to return the object based on the name field (instead id field)

class SongForm(forms.ModelForm):
artist = forms.CharField(widget=forms.TextInput())

def clean_artist(self):
    data = self.cleaned_data['artist']
    artist = Artist.objects.get(name=data)
    self.cleaned_data['artist_id'] = artist.id
    return artist

class Meta:
    model = Song

It saves the form correctly, how ever when it renders again appears the id value instead the name value. How may I change the display values of a django form? I think overriding init will do it, but can't find where is the value property

A: 

I have just got it, the initial hash was what I was missing:

if self.instance.id:
    val = self.initial['artist']
    self.initial['artist'] = Artist.objects.get(id=val).name
juanefren
+2  A: 

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.

cji
haven't tried yet, but looks very interesting
juanefren