Sunday, December 28, 2008

Building a Read Only Field in Django

One commonly requested feature in Django is to have a field on a form(or in the admin), that is read only. Such a thing is going may be a Django 1.1 feature for the admin, exclusively, since this is the level that it makes sense at, a form is by definition for inputing data, not displaying data. However, it is still possible to do this with Django now, and it doesn't even take very much code. As I've said, doing it in this manner(as a form field) isn't particularly intuitive or sensible, however it is possible.

The first thing we need to examine is how we would want to use this, for our purposes we'll use this just like we would a normal field on a form:

from django import forms
from django.contrib.auth.models import User

class UserForm(forms.ModelForm):
email = ReadOnlyField()

class Meta:
model = User
fields = ['email', 'username']


So we need to write a field, our field will actually need to be a subclass of FileField, at first glance this makes absolutely no sense, our field isn't taking files, it isn't taking any data at all. However FileFields receive the initial data for their clean() method, which other fields don't, and we need this behavior for our field to work:

class ReadOnlyField(forms.FileField):
widget = ReadOnlyWidget
def __init__(self, widget=None, label=None, initial=None, help_text=None):
forms.Field.__init__(self, label=label, initial=initial,
help_text=help_text, widget=widget)

def clean(self, value, initial):
self.widget.initial = initial
return initial


As you can see in the clean method we are exploiting this feature in order to give our widget the initial value, which it normally won't have access to at render time.

Now we write our ReadOnlyWidget:

from django.forms.util import flatatt

class ReadOnlyWidget(forms.Widget):
def render(self, name, value, attrs):
final_attrs = self.build_attrs(attrs, name=name)
if hasattr(self, 'initial'):
value = self.initial
return "%s

" % (flatatt(final_attrs), value or '')

def _has_changed(self, initial, data):
return False


Our widget simply renders the initial value to a p tag, instead of as an input tag. We also override the _has_changed method to always return False, this is used in formsets to avoid resaving data that hasn't changed, since our input can't change data, obviously it won't change.

And that's all there is to it, less than 25 lines of code in all. As I said earlier this is a fairly poor architecture, and I wouldn't recommend it, however it does work and serves as proof that Django will allow you to do just about anything in you give in a try.

6 comments:

  1. To where will we write ReadOnlyWidget? Admin.py?

    ReplyDelete
  2. Thanks for posting. I needed a readonly widget and this helped me guide in the right direction. A few things, do you have to manually account for the 'initial' value, can't you just use the 'value' that is passed in?

    Also, in ReadOnlyWidget.render you are expanding "%s", but passing in multiple values that should be expanding (flatatt(final_attrs), value or '') which I think is a syntax error.

    ReplyDelete
  3. I'd say I strongly disagree that 'a form is by definition for inputing data, not displaying data'. That's a pretty narrow view. Different fields of a form may be editable at different contexts. It doesn't make much sense to pass along a completely separate data structure when it is helpful to DISPLAY but not make editable certain properties of a model.

    ReplyDelete
  4. If the purpose of a form is to allow the user to enter information, then it follows that a form is more user-friendly when a context (existing, pertinent information that cannot be edited) is provided to inform the user's decisions as they enter data requested by the form. No?

    ReplyDelete
  5. Yes, technically html forms exists for POSTing data only, but from general UI perspective, on custom layout forms and grid-like formsets, readonly field feature IS very common too, so I definitelly vote for such feature in django forms, to be django more friendly for data processing (edit-intensive) applications. Question is only how to WELL integrate right solution to existing forms api.

    ReplyDelete
  6. Thanks a bunch! This saved me from quite a bit of frustration.

    But might I suggest that instead of inheriting FileField, just inherit regular field and set widgets initial value in the fields constructor. Also, ReadOnlyWidget had a bit of a bug in rendering since it lacked the wrapping in p tag.

    Here's my version:
    from django.utils.html import escape
    from django.utils.safestring import mark_safe
    from django.forms.util import flatatt

    class ReadOnlyWidget(forms.Widget):
        def render(self, name, value, attrs):
            final_attrs = self.build_attrs(attrs, name=name)
            if hasattr(self, 'initial'):
                value = self.initial
                return mark_safe("<p %s>%s</p>" % (flatatt(final_attrs), escape(value) or ''))

        def _has_changed(self, initial, data):
            return False

    class ReadOnlyField(forms.Field):
        widget = ReadOnlyWidget
        def __init__(self, widget=None, label=None, initial=None, help_text=None):
            super(type(self), self).__init__(self, label=label, initial=initial,
                help_text=help_text, widget=widget)
            self.widget.initial = initial

        def clean(self, value):
            return self.widget.initial

    ReplyDelete

Note: Only a member of this blog may post a comment.