In Django forms, how do I set a field to read-only (or disabled) so that it cannot be edited?

Keywords: Django xml Attribute

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()

#1 building

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

#2 building

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)

#3 building

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.

#4 building

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

Form:

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.

#5 building

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)

Posted by Tonka1979 on Fri, 07 Feb 2020 00:42:47 -0800