Skip to content

Commit bcd9482

Browse files
committed
Fixed #342 -- added readonly_fields to ModelAdmin. Thanks Alex Gaynor for bootstrapping the patch.
ModelAdmin has been given a readonly_fields that allow field and calculated values to be displayed alongside editable fields. This works on model add/change pages and inlines. git-svn-id: http://code.djangoproject.com/svn/django/trunk@11965 bcc190cf-cafb-0310-a4f2-bffc1f526a37
1 parent 9233d04 commit bcd9482

File tree

13 files changed

+504
-162
lines changed

13 files changed

+504
-162
lines changed

django/contrib/admin/helpers.py

Lines changed: 113 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,18 @@
1-
21
from django import forms
32
from django.conf import settings
4-
from django.utils.html import escape
5-
from django.utils.safestring import mark_safe
6-
from django.utils.encoding import force_unicode
7-
from django.contrib.admin.util import flatten_fieldsets
3+
from django.contrib.admin.util import flatten_fieldsets, lookup_field
4+
from django.contrib.admin.util import display_for_field, label_for_field
85
from django.contrib.contenttypes.models import ContentType
6+
from django.core.exceptions import ObjectDoesNotExist
7+
from django.db.models.fields import FieldDoesNotExist
8+
from django.db.models.fields.related import ManyToManyRel
9+
from django.forms.util import flatatt
10+
from django.utils.encoding import force_unicode, smart_unicode
11+
from django.utils.html import escape, conditional_escape
12+
from django.utils.safestring import mark_safe
913
from django.utils.translation import ugettext_lazy as _
1014

15+
1116
ACTION_CHECKBOX_NAME = '_selected_action'
1217

1318
class ActionForm(forms.Form):
@@ -16,16 +21,24 @@ class ActionForm(forms.Form):
1621
checkbox = forms.CheckboxInput({'class': 'action-select'}, lambda value: False)
1722

1823
class AdminForm(object):
19-
def __init__(self, form, fieldsets, prepopulated_fields):
24+
def __init__(self, form, fieldsets, prepopulated_fields, readonly_fields=None, model_admin=None):
2025
self.form, self.fieldsets = form, normalize_fieldsets(fieldsets)
2126
self.prepopulated_fields = [{
2227
'field': form[field_name],
2328
'dependencies': [form[f] for f in dependencies]
2429
} for field_name, dependencies in prepopulated_fields.items()]
30+
self.model_admin = model_admin
31+
if readonly_fields is None:
32+
readonly_fields = ()
33+
self.readonly_fields = readonly_fields
2534

2635
def __iter__(self):
2736
for name, options in self.fieldsets:
28-
yield Fieldset(self.form, name, **options)
37+
yield Fieldset(self.form, name,
38+
readonly_fields=self.readonly_fields,
39+
model_admin=self.model_admin,
40+
**options
41+
)
2942

3043
def first_field(self):
3144
try:
@@ -49,11 +62,14 @@ def _media(self):
4962
media = property(_media)
5063

5164
class Fieldset(object):
52-
def __init__(self, form, name=None, fields=(), classes=(), description=None):
65+
def __init__(self, form, name=None, readonly_fields=(), fields=(), classes=(),
66+
description=None, model_admin=None):
5367
self.form = form
5468
self.name, self.fields = name, fields
5569
self.classes = u' '.join(classes)
5670
self.description = description
71+
self.model_admin = model_admin
72+
self.readonly_fields = readonly_fields
5773

5874
def _media(self):
5975
if 'collapse' in self.classes:
@@ -63,22 +79,30 @@ def _media(self):
6379

6480
def __iter__(self):
6581
for field in self.fields:
66-
yield Fieldline(self.form, field)
82+
yield Fieldline(self.form, field, self.readonly_fields, model_admin=self.model_admin)
6783

6884
class Fieldline(object):
69-
def __init__(self, form, field):
85+
def __init__(self, form, field, readonly_fields=None, model_admin=None):
7086
self.form = form # A django.forms.Form instance
71-
if isinstance(field, basestring):
87+
if not hasattr(field, "__iter__"):
7288
self.fields = [field]
7389
else:
7490
self.fields = field
91+
self.model_admin = model_admin
92+
if readonly_fields is None:
93+
readonly_fields = ()
94+
self.readonly_fields = readonly_fields
7595

7696
def __iter__(self):
7797
for i, field in enumerate(self.fields):
78-
yield AdminField(self.form, field, is_first=(i == 0))
98+
if field in self.readonly_fields:
99+
yield AdminReadonlyField(self.form, field, is_first=(i == 0),
100+
model_admin=self.model_admin)
101+
else:
102+
yield AdminField(self.form, field, is_first=(i == 0))
79103

80104
def errors(self):
81-
return mark_safe(u'\n'.join([self.form[f].errors.as_ul() for f in self.fields]).strip('\n'))
105+
return mark_safe(u'\n'.join([self.form[f].errors.as_ul() for f in self.fields if f not in self.readonly_fields]).strip('\n'))
82106

83107
class AdminField(object):
84108
def __init__(self, form, field, is_first):
@@ -100,27 +124,88 @@ def label_tag(self):
100124
attrs = classes and {'class': u' '.join(classes)} or {}
101125
return self.field.label_tag(contents=contents, attrs=attrs)
102126

127+
class AdminReadonlyField(object):
128+
def __init__(self, form, field, is_first, model_admin=None):
129+
self.field = field
130+
self.form = form
131+
self.model_admin = model_admin
132+
self.is_first = is_first
133+
self.is_checkbox = False
134+
self.is_readonly = True
135+
136+
def label_tag(self):
137+
attrs = {}
138+
if not self.is_first:
139+
attrs["class"] = "inline"
140+
name = forms.forms.pretty_name(
141+
label_for_field(self.field, self.model_admin.model, self.model_admin)
142+
)
143+
contents = force_unicode(escape(name)) + u":"
144+
return mark_safe('<label%(attrs)s>%(contents)s</label>' % {
145+
"attrs": flatatt(attrs),
146+
"contents": contents,
147+
})
148+
149+
def contents(self):
150+
from django.contrib.admin.templatetags.admin_list import _boolean_icon
151+
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
152+
field, obj, model_admin = self.field, self.form.instance, self.model_admin
153+
try:
154+
f, attr, value = lookup_field(field, obj, model_admin)
155+
except (AttributeError, ObjectDoesNotExist):
156+
result_repr = EMPTY_CHANGELIST_VALUE
157+
else:
158+
if f is None:
159+
boolean = getattr(attr, "boolean", False)
160+
if boolean:
161+
result_repr = _boolean_icon(value)
162+
else:
163+
result_repr = smart_unicode(value)
164+
if getattr(attr, "allow_tags", False):
165+
result_repr = mark_safe(result_repr)
166+
else:
167+
if value is None:
168+
result_repr = EMPTY_CHANGELIST_VALUE
169+
elif isinstance(f.rel, ManyToManyRel):
170+
result_repr = ", ".join(map(unicode, value.all()))
171+
else:
172+
result_repr = display_for_field(value, f)
173+
return conditional_escape(result_repr)
174+
103175
class InlineAdminFormSet(object):
104176
"""
105177
A wrapper around an inline formset for use in the admin system.
106178
"""
107-
def __init__(self, inline, formset, fieldsets):
179+
def __init__(self, inline, formset, fieldsets, readonly_fields=None, model_admin=None):
108180
self.opts = inline
109181
self.formset = formset
110182
self.fieldsets = fieldsets
183+
self.model_admin = model_admin
184+
if readonly_fields is None:
185+
readonly_fields = ()
186+
self.readonly_fields = readonly_fields
111187

112188
def __iter__(self):
113189
for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
114-
yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, original)
190+
yield InlineAdminForm(self.formset, form, self.fieldsets,
191+
self.opts.prepopulated_fields, original, self.readonly_fields,
192+
model_admin=self.model_admin)
115193
for form in self.formset.extra_forms:
116-
yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, None)
194+
yield InlineAdminForm(self.formset, form, self.fieldsets,
195+
self.opts.prepopulated_fields, None, self.readonly_fields,
196+
model_admin=self.model_admin)
117197

118198
def fields(self):
119199
fk = getattr(self.formset, "fk", None)
120-
for field_name in flatten_fieldsets(self.fieldsets):
121-
if fk and fk.name == field_name:
200+
for i, field in enumerate(flatten_fieldsets(self.fieldsets)):
201+
if fk and fk.name == field:
122202
continue
123-
yield self.formset.form.base_fields[field_name]
203+
if field in self.readonly_fields:
204+
label = label_for_field(field, self.opts.model, self.model_admin)
205+
yield (False, forms.forms.pretty_name(label))
206+
else:
207+
field = self.formset.form.base_fields[field]
208+
yield (field.widget.is_hidden, field.label)
124209

125210
def _media(self):
126211
media = self.opts.media + self.formset.media
@@ -133,17 +218,21 @@ class InlineAdminForm(AdminForm):
133218
"""
134219
A wrapper around an inline form for use in the admin system.
135220
"""
136-
def __init__(self, formset, form, fieldsets, prepopulated_fields, original):
221+
def __init__(self, formset, form, fieldsets, prepopulated_fields, original,
222+
readonly_fields=None, model_admin=None):
137223
self.formset = formset
224+
self.model_admin = model_admin
138225
self.original = original
139226
if original is not None:
140227
self.original_content_type_id = ContentType.objects.get_for_model(original).pk
141228
self.show_url = original and hasattr(original, 'get_absolute_url')
142-
super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields)
229+
super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields,
230+
readonly_fields)
143231

144232
def __iter__(self):
145233
for name, options in self.fieldsets:
146-
yield InlineFieldset(self.formset, self.form, name, **options)
234+
yield InlineFieldset(self.formset, self.form, name,
235+
self.readonly_fields, model_admin=self.model_admin, **options)
147236

148237
def has_auto_field(self):
149238
if self.form._meta.model._meta.has_auto_field:
@@ -194,7 +283,8 @@ def __iter__(self):
194283
for field in self.fields:
195284
if fk and fk.name == field:
196285
continue
197-
yield Fieldline(self.form, field)
286+
yield Fieldline(self.form, field, self.readonly_fields,
287+
model_admin=self.model_admin)
198288

199289
class AdminErrorList(forms.util.ErrorList):
200290
"""

django/contrib/admin/media/css/base.css

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -344,7 +344,7 @@ table.orderable-initalized .order-cell, body>tr>td.order-cell {
344344

345345
/* FORM DEFAULTS */
346346

347-
input, textarea, select {
347+
input, textarea, select, .form-row p {
348348
margin: 2px 0;
349349
padding: 2px 3px;
350350
vertical-align: middle;

django/contrib/admin/options.py

Lines changed: 28 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,7 @@ class BaseModelAdmin(object):
6767
radio_fields = {}
6868
prepopulated_fields = {}
6969
formfield_overrides = {}
70+
readonly_fields = ()
7071

7172
def __init__(self):
7273
self.formfield_overrides = dict(FORMFIELD_FOR_DBFIELD_DEFAULTS, **self.formfield_overrides)
@@ -178,6 +179,9 @@ def _declared_fieldsets(self):
178179
return None
179180
declared_fieldsets = property(_declared_fieldsets)
180181

182+
def get_readonly_fields(self, request, obj=None):
183+
return self.readonly_fields
184+
181185
class ModelAdmin(BaseModelAdmin):
182186
"Encapsulates all admin options and functionality for a given model."
183187
__metaclass__ = forms.MediaDefiningClass
@@ -327,7 +331,8 @@ def get_fieldsets(self, request, obj=None):
327331
if self.declared_fieldsets:
328332
return self.declared_fieldsets
329333
form = self.get_form(request, obj)
330-
return [(None, {'fields': form.base_fields.keys()})]
334+
fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
335+
return [(None, {'fields': fields})]
331336

332337
def get_form(self, request, obj=None, **kwargs):
333338
"""
@@ -342,12 +347,15 @@ def get_form(self, request, obj=None, **kwargs):
342347
exclude = []
343348
else:
344349
exclude = list(self.exclude)
350+
exclude.extend(kwargs.get("exclude", []))
351+
exclude.extend(self.get_readonly_fields(request, obj))
345352
# if exclude is an empty list we pass None to be consistant with the
346353
# default on modelform_factory
354+
exclude = exclude or None
347355
defaults = {
348356
"form": self.form,
349357
"fields": fields,
350-
"exclude": (exclude + kwargs.get("exclude", [])) or None,
358+
"exclude": exclude,
351359
"formfield_callback": curry(self.formfield_for_dbfield, request=request),
352360
}
353361
defaults.update(kwargs)
@@ -782,13 +790,17 @@ def add_view(self, request, form_url='', extra_context=None):
782790
queryset=inline.queryset(request))
783791
formsets.append(formset)
784792

785-
adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields)
793+
adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
794+
self.prepopulated_fields, self.get_readonly_fields(request),
795+
model_admin=self)
786796
media = self.media + adminForm.media
787797

788798
inline_admin_formsets = []
789799
for inline, formset in zip(self.inline_instances, formsets):
790800
fieldsets = list(inline.get_fieldsets(request))
791-
inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
801+
readonly = list(inline.get_readonly_fields(request))
802+
inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
803+
fieldsets, readonly, model_admin=self)
792804
inline_admin_formsets.append(inline_admin_formset)
793805
media = media + inline_admin_formset.media
794806

@@ -875,13 +887,17 @@ def change_view(self, request, object_id, extra_context=None):
875887
queryset=inline.queryset(request))
876888
formsets.append(formset)
877889

878-
adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields)
890+
adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
891+
self.prepopulated_fields, self.get_readonly_fields(request, obj),
892+
model_admin=self)
879893
media = self.media + adminForm.media
880894

881895
inline_admin_formsets = []
882896
for inline, formset in zip(self.inline_instances, formsets):
883897
fieldsets = list(inline.get_fieldsets(request, obj))
884-
inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
898+
readonly = list(inline.get_readonly_fields(request, obj))
899+
inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
900+
fieldsets, readonly, model_admin=self)
885901
inline_admin_formsets.append(inline_admin_formset)
886902
media = media + inline_admin_formset.media
887903

@@ -1174,14 +1190,17 @@ def get_formset(self, request, obj=None, **kwargs):
11741190
exclude = []
11751191
else:
11761192
exclude = list(self.exclude)
1193+
exclude.extend(kwargs.get("exclude", []))
1194+
exclude.extend(self.get_readonly_fields(request, obj))
11771195
# if exclude is an empty list we use None, since that's the actual
11781196
# default
1197+
exclude = exclude or None
11791198
defaults = {
11801199
"form": self.form,
11811200
"formset": self.formset,
11821201
"fk_name": self.fk_name,
11831202
"fields": fields,
1184-
"exclude": (exclude + kwargs.get("exclude", [])) or None,
1203+
"exclude": exclude,
11851204
"formfield_callback": curry(self.formfield_for_dbfield, request=request),
11861205
"extra": self.extra,
11871206
"max_num": self.max_num,
@@ -1193,7 +1212,8 @@ def get_fieldsets(self, request, obj=None):
11931212
if self.declared_fieldsets:
11941213
return self.declared_fieldsets
11951214
form = self.get_formset(request).form
1196-
return [(None, {'fields': form.base_fields.keys()})]
1215+
fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
1216+
return [(None, {'fields': fields})]
11971217

11981218
def queryset(self, request):
11991219
return self.model._default_manager.all()

django/contrib/admin/templates/admin/edit_inline/tabular.html

Lines changed: 10 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,10 @@ <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
77
{{ inline_admin_formset.formset.non_form_errors }}
88
<table>
99
<thead><tr>
10-
{% for field in inline_admin_formset.fields %}
11-
{% if not field.is_hidden %}
12-
<th {% if forloop.first %}colspan="2"{% endif %}>{{ field.label|capfirst }}</th>
13-
{% endif %}
10+
{% for is_hidden, label in inline_admin_formset.fields %}
11+
{% if not is_hidden %}
12+
<th {% if forloop.first %}colspan="2"{% endif %}>{{ label|capfirst }}</th>
13+
{% endif %}
1414
{% endfor %}
1515
{% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}
1616
</tr></thead>
@@ -44,8 +44,12 @@ <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
4444
{% for line in fieldset %}
4545
{% for field in line %}
4646
<td class="{{ field.field.name }}">
47-
{{ field.field.errors.as_ul }}
48-
{{ field.field }}
47+
{% if field.is_readonly %}
48+
<p>{{ field.contents }}</p>
49+
{% else %}
50+
{{ field.field.errors.as_ul }}
51+
{{ field.field }}
52+
{% endif %}
4953
</td>
5054
{% endfor %}
5155
{% endfor %}

0 commit comments

Comments
 (0)