Skip to content

Commit

Permalink
Fixed #342 -- added readonly_fields to ModelAdmin. Thanks Alex Gaynor…
Browse files Browse the repository at this point in the history
… 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
  • Loading branch information
brosner committed Dec 22, 2009
1 parent 9233d04 commit bcd9482
Show file tree
Hide file tree
Showing 13 changed files with 504 additions and 162 deletions.
136 changes: 113 additions & 23 deletions django/contrib/admin/helpers.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,18 @@

from django import forms
from django.conf import settings
from django.utils.html import escape
from django.utils.safestring import mark_safe
from django.utils.encoding import force_unicode
from django.contrib.admin.util import flatten_fieldsets
from django.contrib.admin.util import flatten_fieldsets, lookup_field
from django.contrib.admin.util import display_for_field, label_for_field
from django.contrib.contenttypes.models import ContentType
from django.core.exceptions import ObjectDoesNotExist
from django.db.models.fields import FieldDoesNotExist
from django.db.models.fields.related import ManyToManyRel
from django.forms.util import flatatt
from django.utils.encoding import force_unicode, smart_unicode
from django.utils.html import escape, conditional_escape
from django.utils.safestring import mark_safe
from django.utils.translation import ugettext_lazy as _


ACTION_CHECKBOX_NAME = '_selected_action'

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

class AdminForm(object):
def __init__(self, form, fieldsets, prepopulated_fields):
def __init__(self, form, fieldsets, prepopulated_fields, readonly_fields=None, model_admin=None):
self.form, self.fieldsets = form, normalize_fieldsets(fieldsets)
self.prepopulated_fields = [{
'field': form[field_name],
'dependencies': [form[f] for f in dependencies]
} for field_name, dependencies in prepopulated_fields.items()]
self.model_admin = model_admin
if readonly_fields is None:
readonly_fields = ()
self.readonly_fields = readonly_fields

def __iter__(self):
for name, options in self.fieldsets:
yield Fieldset(self.form, name, **options)
yield Fieldset(self.form, name,
readonly_fields=self.readonly_fields,
model_admin=self.model_admin,
**options
)

def first_field(self):
try:
Expand All @@ -49,11 +62,14 @@ def _media(self):
media = property(_media)

class Fieldset(object):
def __init__(self, form, name=None, fields=(), classes=(), description=None):
def __init__(self, form, name=None, readonly_fields=(), fields=(), classes=(),
description=None, model_admin=None):
self.form = form
self.name, self.fields = name, fields
self.classes = u' '.join(classes)
self.description = description
self.model_admin = model_admin
self.readonly_fields = readonly_fields

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

def __iter__(self):
for field in self.fields:
yield Fieldline(self.form, field)
yield Fieldline(self.form, field, self.readonly_fields, model_admin=self.model_admin)

class Fieldline(object):
def __init__(self, form, field):
def __init__(self, form, field, readonly_fields=None, model_admin=None):
self.form = form # A django.forms.Form instance
if isinstance(field, basestring):
if not hasattr(field, "__iter__"):
self.fields = [field]
else:
self.fields = field
self.model_admin = model_admin
if readonly_fields is None:
readonly_fields = ()
self.readonly_fields = readonly_fields

def __iter__(self):
for i, field in enumerate(self.fields):
yield AdminField(self.form, field, is_first=(i == 0))
if field in self.readonly_fields:
yield AdminReadonlyField(self.form, field, is_first=(i == 0),
model_admin=self.model_admin)
else:
yield AdminField(self.form, field, is_first=(i == 0))

def errors(self):
return mark_safe(u'\n'.join([self.form[f].errors.as_ul() for f in self.fields]).strip('\n'))
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'))

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

class AdminReadonlyField(object):
def __init__(self, form, field, is_first, model_admin=None):
self.field = field
self.form = form
self.model_admin = model_admin
self.is_first = is_first
self.is_checkbox = False
self.is_readonly = True

def label_tag(self):
attrs = {}
if not self.is_first:
attrs["class"] = "inline"
name = forms.forms.pretty_name(
label_for_field(self.field, self.model_admin.model, self.model_admin)
)
contents = force_unicode(escape(name)) + u":"
return mark_safe('<label%(attrs)s>%(contents)s</label>' % {
"attrs": flatatt(attrs),
"contents": contents,
})

def contents(self):
from django.contrib.admin.templatetags.admin_list import _boolean_icon
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
field, obj, model_admin = self.field, self.form.instance, self.model_admin
try:
f, attr, value = lookup_field(field, obj, model_admin)
except (AttributeError, ObjectDoesNotExist):
result_repr = EMPTY_CHANGELIST_VALUE
else:
if f is None:
boolean = getattr(attr, "boolean", False)
if boolean:
result_repr = _boolean_icon(value)
else:
result_repr = smart_unicode(value)
if getattr(attr, "allow_tags", False):
result_repr = mark_safe(result_repr)
else:
if value is None:
result_repr = EMPTY_CHANGELIST_VALUE
elif isinstance(f.rel, ManyToManyRel):
result_repr = ", ".join(map(unicode, value.all()))
else:
result_repr = display_for_field(value, f)
return conditional_escape(result_repr)

class InlineAdminFormSet(object):
"""
A wrapper around an inline formset for use in the admin system.
"""
def __init__(self, inline, formset, fieldsets):
def __init__(self, inline, formset, fieldsets, readonly_fields=None, model_admin=None):
self.opts = inline
self.formset = formset
self.fieldsets = fieldsets
self.model_admin = model_admin
if readonly_fields is None:
readonly_fields = ()
self.readonly_fields = readonly_fields

def __iter__(self):
for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, original)
yield InlineAdminForm(self.formset, form, self.fieldsets,
self.opts.prepopulated_fields, original, self.readonly_fields,
model_admin=self.model_admin)
for form in self.formset.extra_forms:
yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, None)
yield InlineAdminForm(self.formset, form, self.fieldsets,
self.opts.prepopulated_fields, None, self.readonly_fields,
model_admin=self.model_admin)

def fields(self):
fk = getattr(self.formset, "fk", None)
for field_name in flatten_fieldsets(self.fieldsets):
if fk and fk.name == field_name:
for i, field in enumerate(flatten_fieldsets(self.fieldsets)):
if fk and fk.name == field:
continue
yield self.formset.form.base_fields[field_name]
if field in self.readonly_fields:
label = label_for_field(field, self.opts.model, self.model_admin)
yield (False, forms.forms.pretty_name(label))
else:
field = self.formset.form.base_fields[field]
yield (field.widget.is_hidden, field.label)

def _media(self):
media = self.opts.media + self.formset.media
Expand All @@ -133,17 +218,21 @@ class InlineAdminForm(AdminForm):
"""
A wrapper around an inline form for use in the admin system.
"""
def __init__(self, formset, form, fieldsets, prepopulated_fields, original):
def __init__(self, formset, form, fieldsets, prepopulated_fields, original,
readonly_fields=None, model_admin=None):
self.formset = formset
self.model_admin = model_admin
self.original = original
if original is not None:
self.original_content_type_id = ContentType.objects.get_for_model(original).pk
self.show_url = original and hasattr(original, 'get_absolute_url')
super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields)
super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields,
readonly_fields)

def __iter__(self):
for name, options in self.fieldsets:
yield InlineFieldset(self.formset, self.form, name, **options)
yield InlineFieldset(self.formset, self.form, name,
self.readonly_fields, model_admin=self.model_admin, **options)

def has_auto_field(self):
if self.form._meta.model._meta.has_auto_field:
Expand Down Expand Up @@ -194,7 +283,8 @@ def __iter__(self):
for field in self.fields:
if fk and fk.name == field:
continue
yield Fieldline(self.form, field)
yield Fieldline(self.form, field, self.readonly_fields,
model_admin=self.model_admin)

class AdminErrorList(forms.util.ErrorList):
"""
Expand Down
2 changes: 1 addition & 1 deletion django/contrib/admin/media/css/base.css
Original file line number Diff line number Diff line change
Expand Up @@ -344,7 +344,7 @@ table.orderable-initalized .order-cell, body>tr>td.order-cell {

/* FORM DEFAULTS */

input, textarea, select {
input, textarea, select, .form-row p {
margin: 2px 0;
padding: 2px 3px;
vertical-align: middle;
Expand Down
36 changes: 28 additions & 8 deletions django/contrib/admin/options.py
Original file line number Diff line number Diff line change
Expand Up @@ -67,6 +67,7 @@ class BaseModelAdmin(object):
radio_fields = {}
prepopulated_fields = {}
formfield_overrides = {}
readonly_fields = ()

def __init__(self):
self.formfield_overrides = dict(FORMFIELD_FOR_DBFIELD_DEFAULTS, **self.formfield_overrides)
Expand Down Expand Up @@ -178,6 +179,9 @@ def _declared_fieldsets(self):
return None
declared_fieldsets = property(_declared_fieldsets)

def get_readonly_fields(self, request, obj=None):
return self.readonly_fields

class ModelAdmin(BaseModelAdmin):
"Encapsulates all admin options and functionality for a given model."
__metaclass__ = forms.MediaDefiningClass
Expand Down Expand Up @@ -327,7 +331,8 @@ def get_fieldsets(self, request, obj=None):
if self.declared_fieldsets:
return self.declared_fieldsets
form = self.get_form(request, obj)
return [(None, {'fields': form.base_fields.keys()})]
fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
return [(None, {'fields': fields})]

def get_form(self, request, obj=None, **kwargs):
"""
Expand All @@ -342,12 +347,15 @@ def get_form(self, request, obj=None, **kwargs):
exclude = []
else:
exclude = list(self.exclude)
exclude.extend(kwargs.get("exclude", []))
exclude.extend(self.get_readonly_fields(request, obj))
# if exclude is an empty list we pass None to be consistant with the
# default on modelform_factory
exclude = exclude or None
defaults = {
"form": self.form,
"fields": fields,
"exclude": (exclude + kwargs.get("exclude", [])) or None,
"exclude": exclude,
"formfield_callback": curry(self.formfield_for_dbfield, request=request),
}
defaults.update(kwargs)
Expand Down Expand Up @@ -782,13 +790,17 @@ def add_view(self, request, form_url='', extra_context=None):
queryset=inline.queryset(request))
formsets.append(formset)

adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields)
adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
self.prepopulated_fields, self.get_readonly_fields(request),
model_admin=self)
media = self.media + adminForm.media

inline_admin_formsets = []
for inline, formset in zip(self.inline_instances, formsets):
fieldsets = list(inline.get_fieldsets(request))
inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
readonly = list(inline.get_readonly_fields(request))
inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
fieldsets, readonly, model_admin=self)
inline_admin_formsets.append(inline_admin_formset)
media = media + inline_admin_formset.media

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

adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields)
adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
self.prepopulated_fields, self.get_readonly_fields(request, obj),
model_admin=self)
media = self.media + adminForm.media

inline_admin_formsets = []
for inline, formset in zip(self.inline_instances, formsets):
fieldsets = list(inline.get_fieldsets(request, obj))
inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
readonly = list(inline.get_readonly_fields(request, obj))
inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
fieldsets, readonly, model_admin=self)
inline_admin_formsets.append(inline_admin_formset)
media = media + inline_admin_formset.media

Expand Down Expand Up @@ -1174,14 +1190,17 @@ def get_formset(self, request, obj=None, **kwargs):
exclude = []
else:
exclude = list(self.exclude)
exclude.extend(kwargs.get("exclude", []))
exclude.extend(self.get_readonly_fields(request, obj))
# if exclude is an empty list we use None, since that's the actual
# default
exclude = exclude or None
defaults = {
"form": self.form,
"formset": self.formset,
"fk_name": self.fk_name,
"fields": fields,
"exclude": (exclude + kwargs.get("exclude", [])) or None,
"exclude": exclude,
"formfield_callback": curry(self.formfield_for_dbfield, request=request),
"extra": self.extra,
"max_num": self.max_num,
Expand All @@ -1193,7 +1212,8 @@ def get_fieldsets(self, request, obj=None):
if self.declared_fieldsets:
return self.declared_fieldsets
form = self.get_formset(request).form
return [(None, {'fields': form.base_fields.keys()})]
fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
return [(None, {'fields': fields})]

def queryset(self, request):
return self.model._default_manager.all()
Expand Down
16 changes: 10 additions & 6 deletions django/contrib/admin/templates/admin/edit_inline/tabular.html
Original file line number Diff line number Diff line change
Expand Up @@ -7,10 +7,10 @@ <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{{ inline_admin_formset.formset.non_form_errors }}
<table>
<thead><tr>
{% for field in inline_admin_formset.fields %}
{% if not field.is_hidden %}
<th {% if forloop.first %}colspan="2"{% endif %}>{{ field.label|capfirst }}</th>
{% endif %}
{% for is_hidden, label in inline_admin_formset.fields %}
{% if not is_hidden %}
<th {% if forloop.first %}colspan="2"{% endif %}>{{ label|capfirst }}</th>
{% endif %}
{% endfor %}
{% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}
</tr></thead>
Expand Down Expand Up @@ -44,8 +44,12 @@ <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{% for line in fieldset %}
{% for field in line %}
<td class="{{ field.field.name }}">
{{ field.field.errors.as_ul }}
{{ field.field }}
{% if field.is_readonly %}
<p>{{ field.contents }}</p>
{% else %}
{{ field.field.errors.as_ul }}
{{ field.field }}
{% endif %}
</td>
{% endfor %}
{% endfor %}
Expand Down
Loading

0 comments on commit bcd9482

Please sign in to comment.