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.
To where will we write ReadOnlyWidget? Admin.py?
ReplyDeleteThanks 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?
ReplyDeleteAlso, 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.
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.
ReplyDeleteIf 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?
ReplyDeleteYes, 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.
ReplyDeleteThanks a bunch! This saved me from quite a bit of frustration.
ReplyDeleteBut 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