In Django forms, how do I make fields read-only (or disabled)?
When creating a new entry using a form, all fields should be enabled - however, some fields must be read-only when the record is in update mode.
For example, when creating a new Item model, all fields must be editable, but when updating records, is there a way to disable the sku field so that it is visible but not editable?
class Item(models.Model): sku = models.CharField(max_length=50) description = models.CharField(max_length=200) added_by = models.ForeignKey(User) class ItemForm(ModelForm): class Meta: model = Item exclude = ('added_by') def new_item_view(request): if request.method == 'POST': form = ItemForm(request.POST) # Validate and save else: form = ItemForm() # Render the view
Can I reuse the ItemForm class? What changes do you need to make in the ItemForm or Item model class? Do I need to write another class "ItemUpdateForm" to update the project?
def update_item_view(request): if request.method == 'POST': form = ItemUpdateForm(request.POST) # Validate and save else: form = ItemUpdateForm()
For the administrator version, if you have multiple fields, I think this is a more compact approach:
def get_readonly_fields(self, request, obj=None): skips = ('sku', 'other_field') fields = super(ItemAdmin, self).get_readonly_fields(request, obj) if not obj: return [field for field in fields if not field in skips] return fields
according to Christophe 31's answer , which is a slightly more involved version. It does not depend on the read-only property. This creates problems, such as the selection box can still be changed and the data selector still pops up.
Instead, the form field widget is wrapped in a read-only widget so that the form remains valid. The contents of the original widget are displayed in the < span class = "hidden" > < / span > tag. If the widget has the render_readonly() method, it will use that method as visible text, otherwise it will parse the HTML of the original widget and try to guess the best representation.
import django.forms.widgets as f import xml.etree.ElementTree as etree from django.utils.safestring import mark_safe def make_readonly(form): """ Makes all fields on the form readonly and prevents it from POST hacks. """ def _get_cleaner(_form, field): def clean_field(): return getattr(_form.instance, field, None) return clean_field for field_name in form.fields.keys(): form.fields[field_name].widget = ReadOnlyWidget( initial_widget=form.fields[field_name].widget) setattr(form, "clean_" + field_name, _get_cleaner(form, field_name)) form.is_readonly = True class ReadOnlyWidget(f.Select): """ Renders the content of the initial widget in a hidden <span>. If the initial widget has a ``render_readonly()`` method it uses that as display text, otherwise it tries to guess by parsing the html of the initial widget. """ def __init__(self, initial_widget, *args, **kwargs): self.initial_widget = initial_widget super(ReadOnlyWidget, self).__init__(*args, **kwargs) def render(self, *args, **kwargs): def guess_readonly_text(original_content): root = etree.fromstring("<span>%s</span>" % original_content) for element in root: if element.tag == 'input': return element.get('value') if element.tag == 'select': for option in element: if option.get('selected'): return option.text if element.tag == 'textarea': return element.text return "N/A" original_content = self.initial_widget.render(*args, **kwargs) try: readonly_text = self.initial_widget.render_readonly(*args, **kwargs) except AttributeError: readonly_text = guess_readonly_text(original_content) return mark_safe("""<span class="hidden">%s</span>%s""" % ( original_content, readonly_text)) # Usage example 1. self.fields['my_field'].widget = ReadOnlyWidget(self.fields['my_field'].widget) # Usage example 2. form = MyForm() make_readonly(form)
In order for this feature to apply to the ForeignKey field, some changes need to be made. First, the SELECT HTML tag does not have a readonly attribute. We need to use disabled="disabled" instead. However, the browser then does not send back any form data for that field. Therefore, we need to set the field to not be needed in order for the field to validate correctly. Then we need to reset the value to the previous value so that it is not set to null.
Therefore, for foreign keys, you will need to do the following:
class ItemForm(ModelForm): def __init__(self, *args, **kwargs): super(ItemForm, self).__init__(*args, **kwargs) instance = getattr(self, 'instance', None) if instance and instance.id: self.fields['sku'].required = False self.fields['sku'].widget.attrs['disabled'] = 'disabled' def clean_sku(self): # As shown in the above answer. instance = getattr(self, 'instance', None) if instance: return instance.sku else: return self.cleaned_data.get('sku', None)
In this way, the browser will not let the user change the field and will always POST because it is blank. Then, we override the clean method to set the value of the field to the original value in the instance.
I just created the simplest widget for a read-only field - I really don't understand why the form doesn't have this:
class ReadOnlyWidget(widgets.Widget): """Some of these values are read only - just a bit of text...""" def render(self, _, value, attrs=None): return value
my_read_only = CharField(widget=ReadOnlyWidget())
Very simple - and let me output. It's convenient in a form set with a bunch of read-only values. Of course - you can also be smarter and give it an attrs div so you can add classes to it.
There are two other (similar) methods, one of which is generic:
1) The first method - delete the fields in the save () method, for example (untested;):
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) The second method - resets the field to its initial value in the purge method:
def clean_<fieldname>(self): return self.initial[<fieldname>] # or getattr(self.instance, fieldname)
Based on the second method, I summarize it as follows:
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, fieldname)