Permalink
Browse files

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
  • Loading branch information...
brosner committed Dec 22, 2009
1 parent 9233d04 commit bcd9482a2019158f4580c24cd50ee8bfae9b2739
@@ -1,13 +1,18 @@
from django import forms from django import forms
from django.conf import settings from django.conf import settings
from django.utils.html import escape from django.contrib.admin.util import flatten_fieldsets, lookup_field
from django.utils.safestring import mark_safe from django.contrib.admin.util import display_for_field, label_for_field
from django.utils.encoding import force_unicode
from django.contrib.admin.util import flatten_fieldsets
from django.contrib.contenttypes.models import ContentType 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 _ from django.utils.translation import ugettext_lazy as _
ACTION_CHECKBOX_NAME = '_selected_action' ACTION_CHECKBOX_NAME = '_selected_action'
class ActionForm(forms.Form): class ActionForm(forms.Form):
@@ -16,16 +21,24 @@ class ActionForm(forms.Form):
checkbox = forms.CheckboxInput({'class': 'action-select'}, lambda value: False) checkbox = forms.CheckboxInput({'class': 'action-select'}, lambda value: False)
class AdminForm(object): 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.form, self.fieldsets = form, normalize_fieldsets(fieldsets)
self.prepopulated_fields = [{ self.prepopulated_fields = [{
'field': form[field_name], 'field': form[field_name],
'dependencies': [form[f] for f in dependencies] 'dependencies': [form[f] for f in dependencies]
} for field_name, dependencies in prepopulated_fields.items()] } 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): def __iter__(self):
for name, options in self.fieldsets: 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): def first_field(self):
try: try:
@@ -49,11 +62,14 @@ def _media(self):
media = property(_media) media = property(_media)
class Fieldset(object): 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.form = form
self.name, self.fields = name, fields self.name, self.fields = name, fields
self.classes = u' '.join(classes) self.classes = u' '.join(classes)
self.description = description self.description = description
self.model_admin = model_admin
self.readonly_fields = readonly_fields
def _media(self): def _media(self):
if 'collapse' in self.classes: if 'collapse' in self.classes:
@@ -63,22 +79,30 @@ def _media(self):
def __iter__(self): def __iter__(self):
for field in self.fields: 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): 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 self.form = form # A django.forms.Form instance
if isinstance(field, basestring): if not hasattr(field, "__iter__"):
self.fields = [field] self.fields = [field]
else: else:
self.fields = field self.fields = field
self.model_admin = model_admin
if readonly_fields is None:
readonly_fields = ()
self.readonly_fields = readonly_fields
def __iter__(self): def __iter__(self):
for i, field in enumerate(self.fields): 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): 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): class AdminField(object):
def __init__(self, form, field, is_first): def __init__(self, form, field, is_first):
@@ -100,27 +124,88 @@ def label_tag(self):
attrs = classes and {'class': u' '.join(classes)} or {} attrs = classes and {'class': u' '.join(classes)} or {}
return self.field.label_tag(contents=contents, attrs=attrs) 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): class InlineAdminFormSet(object):
""" """
A wrapper around an inline formset for use in the admin system. 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.opts = inline
self.formset = formset self.formset = formset
self.fieldsets = fieldsets self.fieldsets = fieldsets
self.model_admin = model_admin
if readonly_fields is None:
readonly_fields = ()
self.readonly_fields = readonly_fields
def __iter__(self): def __iter__(self):
for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()): 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: 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): def fields(self):
fk = getattr(self.formset, "fk", None) fk = getattr(self.formset, "fk", None)
for field_name in flatten_fieldsets(self.fieldsets): for i, field in enumerate(flatten_fieldsets(self.fieldsets)):
if fk and fk.name == field_name: if fk and fk.name == field:
continue 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): def _media(self):
media = self.opts.media + self.formset.media media = self.opts.media + self.formset.media
@@ -133,17 +218,21 @@ class InlineAdminForm(AdminForm):
""" """
A wrapper around an inline form for use in the admin system. 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.formset = formset
self.model_admin = model_admin
self.original = original self.original = original
if original is not None: if original is not None:
self.original_content_type_id = ContentType.objects.get_for_model(original).pk self.original_content_type_id = ContentType.objects.get_for_model(original).pk
self.show_url = original and hasattr(original, 'get_absolute_url') 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): def __iter__(self):
for name, options in self.fieldsets: 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): def has_auto_field(self):
if self.form._meta.model._meta.has_auto_field: if self.form._meta.model._meta.has_auto_field:
@@ -194,7 +283,8 @@ def __iter__(self):
for field in self.fields: for field in self.fields:
if fk and fk.name == field: if fk and fk.name == field:
continue continue
yield Fieldline(self.form, field) yield Fieldline(self.form, field, self.readonly_fields,
model_admin=self.model_admin)
class AdminErrorList(forms.util.ErrorList): class AdminErrorList(forms.util.ErrorList):
""" """
@@ -344,7 +344,7 @@ table.orderable-initalized .order-cell, body>tr>td.order-cell {
/* FORM DEFAULTS */ /* FORM DEFAULTS */
input, textarea, select { input, textarea, select, .form-row p {
margin: 2px 0; margin: 2px 0;
padding: 2px 3px; padding: 2px 3px;
vertical-align: middle; vertical-align: middle;
@@ -67,6 +67,7 @@ class BaseModelAdmin(object):
radio_fields = {} radio_fields = {}
prepopulated_fields = {} prepopulated_fields = {}
formfield_overrides = {} formfield_overrides = {}
readonly_fields = ()
def __init__(self): def __init__(self):
self.formfield_overrides = dict(FORMFIELD_FOR_DBFIELD_DEFAULTS, **self.formfield_overrides) self.formfield_overrides = dict(FORMFIELD_FOR_DBFIELD_DEFAULTS, **self.formfield_overrides)
@@ -178,6 +179,9 @@ def _declared_fieldsets(self):
return None return None
declared_fieldsets = property(_declared_fieldsets) declared_fieldsets = property(_declared_fieldsets)
def get_readonly_fields(self, request, obj=None):
return self.readonly_fields
class ModelAdmin(BaseModelAdmin): class ModelAdmin(BaseModelAdmin):
"Encapsulates all admin options and functionality for a given model." "Encapsulates all admin options and functionality for a given model."
__metaclass__ = forms.MediaDefiningClass __metaclass__ = forms.MediaDefiningClass
@@ -327,7 +331,8 @@ def get_fieldsets(self, request, obj=None):
if self.declared_fieldsets: if self.declared_fieldsets:
return self.declared_fieldsets return self.declared_fieldsets
form = self.get_form(request, obj) 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): def get_form(self, request, obj=None, **kwargs):
""" """
@@ -342,12 +347,15 @@ def get_form(self, request, obj=None, **kwargs):
exclude = [] exclude = []
else: else:
exclude = list(self.exclude) 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 # if exclude is an empty list we pass None to be consistant with the
# default on modelform_factory # default on modelform_factory
exclude = exclude or None
defaults = { defaults = {
"form": self.form, "form": self.form,
"fields": fields, "fields": fields,
"exclude": (exclude + kwargs.get("exclude", [])) or None, "exclude": exclude,
"formfield_callback": curry(self.formfield_for_dbfield, request=request), "formfield_callback": curry(self.formfield_for_dbfield, request=request),
} }
defaults.update(kwargs) defaults.update(kwargs)
@@ -782,13 +790,17 @@ def add_view(self, request, form_url='', extra_context=None):
queryset=inline.queryset(request)) queryset=inline.queryset(request))
formsets.append(formset) 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 media = self.media + adminForm.media
inline_admin_formsets = [] inline_admin_formsets = []
for inline, formset in zip(self.inline_instances, formsets): for inline, formset in zip(self.inline_instances, formsets):
fieldsets = list(inline.get_fieldsets(request)) 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) inline_admin_formsets.append(inline_admin_formset)
media = media + inline_admin_formset.media media = media + inline_admin_formset.media
@@ -875,13 +887,17 @@ def change_view(self, request, object_id, extra_context=None):
queryset=inline.queryset(request)) queryset=inline.queryset(request))
formsets.append(formset) 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 media = self.media + adminForm.media
inline_admin_formsets = [] inline_admin_formsets = []
for inline, formset in zip(self.inline_instances, formsets): for inline, formset in zip(self.inline_instances, formsets):
fieldsets = list(inline.get_fieldsets(request, obj)) 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) inline_admin_formsets.append(inline_admin_formset)
media = media + inline_admin_formset.media media = media + inline_admin_formset.media
@@ -1174,14 +1190,17 @@ def get_formset(self, request, obj=None, **kwargs):
exclude = [] exclude = []
else: else:
exclude = list(self.exclude) 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 # if exclude is an empty list we use None, since that's the actual
# default # default
exclude = exclude or None
defaults = { defaults = {
"form": self.form, "form": self.form,
"formset": self.formset, "formset": self.formset,
"fk_name": self.fk_name, "fk_name": self.fk_name,
"fields": fields, "fields": fields,
"exclude": (exclude + kwargs.get("exclude", [])) or None, "exclude": exclude,
"formfield_callback": curry(self.formfield_for_dbfield, request=request), "formfield_callback": curry(self.formfield_for_dbfield, request=request),
"extra": self.extra, "extra": self.extra,
"max_num": self.max_num, "max_num": self.max_num,
@@ -1193,7 +1212,8 @@ def get_fieldsets(self, request, obj=None):
if self.declared_fieldsets: if self.declared_fieldsets:
return self.declared_fieldsets return self.declared_fieldsets
form = self.get_formset(request).form 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): def queryset(self, request):
return self.model._default_manager.all() return self.model._default_manager.all()
@@ -7,10 +7,10 @@ <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{{ inline_admin_formset.formset.non_form_errors }} {{ inline_admin_formset.formset.non_form_errors }}
<table> <table>
<thead><tr> <thead><tr>
{% for field in inline_admin_formset.fields %} {% for is_hidden, label in inline_admin_formset.fields %}
{% if not field.is_hidden %} {% if not is_hidden %}
<th {% if forloop.first %}colspan="2"{% endif %}>{{ field.label|capfirst }}</th> <th {% if forloop.first %}colspan="2"{% endif %}>{{ label|capfirst }}</th>
{% endif %} {% endif %}
{% endfor %} {% endfor %}
{% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %} {% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}
</tr></thead> </tr></thead>
@@ -44,8 +44,12 @@ <h2>{{ inline_admin_formset.opts.verbose_name_plural|capfirst }}</h2>
{% for line in fieldset %} {% for line in fieldset %}
{% for field in line %} {% for field in line %}
<td class="{{ field.field.name }}"> <td class="{{ field.field.name }}">
{{ field.field.errors.as_ul }} {% if field.is_readonly %}
{{ field.field }} <p>{{ field.contents }}</p>
{% else %}
{{ field.field.errors.as_ul }}
{{ field.field }}
{% endif %}
</td> </td>
{% endfor %} {% endfor %}
{% endfor %} {% endfor %}
Oops, something went wrong.

0 comments on commit bcd9482

Please sign in to comment.