17

My form field looks something like the following:

class FooForm(ModelForm):
    somefield = models.CharField(
        widget=forms.TextInput(attrs={'readonly':'readonly'})
    )

    class Meta:
        model = Foo

Geting an error like the following with the code above: init() got an unexpected keyword argument 'widget'

I thought this is a legitimate use of a form widget?

googletorp
  • 33,075
  • 15
  • 67
  • 82
randombits
  • 47,058
  • 76
  • 251
  • 433
  • This changed in django 1.9 https://stackoverflow.com/questions/324477/in-a-django-form-how-do-i-make-a-field-readonly-or-disabled-so-that-it-cannot – zudebluvstein Sep 29 '17 at 16:00

4 Answers4

44

You should use a form field and not a model field:

somefield = models.CharField(
    widget=forms.TextInput(attrs={'readonly': 'readonly'})
)

replaced with

somefield = forms.CharField(
    widget=forms.TextInput(attrs={'readonly': 'readonly'})
)

Should fix it.

Wolkenarchitekt
  • 20,170
  • 29
  • 111
  • 174
googletorp
  • 33,075
  • 15
  • 67
  • 82
20

Note that the readonly attribute does not keep Django from processing any value sent by the client. If it is important to you that the value doesn't change, no matter how creative your users are with FireBug, you need to use a more involved method, e.g. a ReadOnlyField/ReadOnlyWidget like demonstrated in a blog entry by Alex Gaynor.

Benjamin Wohlwend
  • 30,958
  • 11
  • 90
  • 100
5

I was going into the same problem so I created a Mixin that seems to work for my use cases.

class ReadOnlyFieldsMixin(object):
    readonly_fields =()

    def __init__(self, *args, **kwargs):
        super(ReadOnlyFieldsMixin, self).__init__(*args, **kwargs)
        for field in (field for name, field in self.fields.iteritems() if name in self.readonly_fields):
            field.widget.attrs['disabled'] = 'true'
            field.required = False

    def clean(self):
        cleaned_data = super(ReadOnlyFieldsMixin,self).clean()
        for field in self.readonly_fields:
           cleaned_data[field] = getattr(self.instance, field)

        return cleaned_data

Usage, just define which ones must be read only:

class MyFormWithReadOnlyFields(ReadOnlyFieldsMixin, MyForm):
    readonly_fields = ('field1', 'field2', 'fieldx')
Michael
  • 8,357
  • 20
  • 58
  • 86
1

As Benjamin (https://stackoverflow.com/a/2359167/565525) nicely explained, additionally to rendering correctly, you need to process field on backend properly.

There is an SO question and answers that has many good solutions. But anyway:

1) first approach - removing field in save() method, e.g. (not tested ;) ):

def save(self, *args, **kwargs):
    for fname in self.readonly_fields:
        if fname in self.cleaned_data:
            del self.cleaned_data[fname]
    return super(<form-name>, self).save(*args,**kwargs)

2) second approach - reset field to initial value in clean method:

def clean_<fieldname>(self):
    return self.initial[<fieldname>] # or getattr(self.instance, <fieldname>)

Based on second approach I generalized it like this:

from functools                 import partial

class <Form-name>(...):

    def __init__(self, ...):
        ...
        super(<Form-name>, self).__init__(*args, **kwargs)
        ...
        for i, (fname, field) in enumerate(self.fields.iteritems()):
            if fname in self.readonly_fields:
                field.widget.attrs['readonly'] = "readonly"
                field.required = False
                # set clean method to reset value back
                clean_method_name = "clean_%s" % fname
                assert clean_method_name not in dir(self)
                setattr(self, clean_method_name, partial(self._clean_for_readonly_field, fname=fname))


    def _clean_for_readonly_field(self, fname):
        """ will reset value to initial - nothing will be changed 
            needs to be added dynamically - partial, see init_fields
        """
        return self.initial[fname] # or getattr(self.instance, fname)
Community
  • 1
  • 1
Robert Lujo
  • 15,383
  • 5
  • 56
  • 73