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
This commit is contained in:
parent
9233d04265
commit
bcd9482a20
|
@ -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 @@ class AdminForm(object):
|
||||||
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 @@ class Fieldset(object):
|
||||||
|
|
||||||
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 @@ class AdminField(object):
|
||||||
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 @@ class InlineFieldset(Fieldset):
|
||||||
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 @@ class BaseModelAdmin(object):
|
||||||
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 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
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 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
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 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
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 @@ class ModelAdmin(BaseModelAdmin):
|
||||||
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 @@ class InlineModelAdmin(BaseModelAdmin):
|
||||||
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 @@ class InlineModelAdmin(BaseModelAdmin):
|
||||||
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 @@
|
||||||
{{ 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 @@
|
||||||
{% 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 %}
|
||||||
|
|
|
@ -1,19 +1,28 @@
|
||||||
<fieldset class="module aligned {{ fieldset.classes }}">
|
<fieldset class="module aligned {{ fieldset.classes }}">
|
||||||
{% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %}
|
{% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %}
|
||||||
{% if fieldset.description %}<div class="description">{{ fieldset.description|safe }}</div>{% endif %}
|
{% if fieldset.description %}
|
||||||
{% for line in fieldset %}
|
<div class="description">{{ fieldset.description|safe }}</div>
|
||||||
<div class="form-row{% if line.errors %} errors{% endif %} {% for field in line %}{{ field.field.name }} {% endfor %} ">
|
{% endif %}
|
||||||
{{ line.errors }}
|
{% for line in fieldset %}
|
||||||
{% for field in line %}
|
<div class="form-row{% if line.errors %} errors{% endif %} {% for field in line %}{{ field.field.name }} {% endfor %} ">
|
||||||
<div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
|
{{ line.errors }}
|
||||||
{% if field.is_checkbox %}
|
{% for field in line %}
|
||||||
{{ field.field }}{{ field.label_tag }}
|
<div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
|
||||||
{% else %}
|
{% if field.is_checkbox %}
|
||||||
{{ field.label_tag }}{{ field.field }}
|
{{ field.field }}{{ field.label_tag }}
|
||||||
{% endif %}
|
{% else %}
|
||||||
{% if field.field.field.help_text %}<p class="help">{{ field.field.field.help_text|safe }}</p>{% endif %}
|
{{ field.label_tag }}
|
||||||
</div>
|
{% if field.is_readonly %}
|
||||||
{% endfor %}
|
<p>{{ field.contents }}</p>
|
||||||
</div>
|
{% else %}
|
||||||
{% endfor %}
|
{{ field.field }}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
{% if field.field.field.help_text %}
|
||||||
|
<p class="help">{{ field.field.field.help_text|safe }}</p>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
</fieldset>
|
</fieldset>
|
||||||
|
|
|
@ -1,16 +1,20 @@
|
||||||
|
import datetime
|
||||||
|
|
||||||
from django.conf import settings
|
from django.conf import settings
|
||||||
|
from django.contrib.admin.util import lookup_field, display_for_field, label_for_field
|
||||||
from django.contrib.admin.views.main import ALL_VAR, EMPTY_CHANGELIST_VALUE
|
from django.contrib.admin.views.main import ALL_VAR, EMPTY_CHANGELIST_VALUE
|
||||||
from django.contrib.admin.views.main import ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR, SEARCH_VAR
|
from django.contrib.admin.views.main import ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR, SEARCH_VAR
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.forms.forms import pretty_name
|
||||||
from django.utils import formats
|
from django.utils import formats
|
||||||
from django.utils.html import escape, conditional_escape
|
from django.utils.html import escape, conditional_escape
|
||||||
from django.utils.text import capfirst
|
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
|
from django.utils.text import capfirst
|
||||||
from django.utils.translation import ugettext as _
|
from django.utils.translation import ugettext as _
|
||||||
from django.utils.encoding import smart_unicode, smart_str, force_unicode
|
from django.utils.encoding import smart_unicode, force_unicode
|
||||||
from django.template import Library
|
from django.template import Library
|
||||||
import datetime
|
|
||||||
|
|
||||||
register = Library()
|
register = Library()
|
||||||
|
|
||||||
|
@ -76,41 +80,15 @@ def result_headers(cl):
|
||||||
try:
|
try:
|
||||||
f = lookup_opts.get_field(field_name)
|
f = lookup_opts.get_field(field_name)
|
||||||
admin_order_field = None
|
admin_order_field = None
|
||||||
|
header = f.verbose_name
|
||||||
except models.FieldDoesNotExist:
|
except models.FieldDoesNotExist:
|
||||||
# For non-field list_display values, check for the function
|
header = label_for_field(field_name, cl.model, cl.model_admin)
|
||||||
# attribute "short_description". If that doesn't exist, fall back
|
|
||||||
# to the method name. And __str__ and __unicode__ are special-cases.
|
|
||||||
if field_name == '__unicode__':
|
|
||||||
header = force_unicode(lookup_opts.verbose_name)
|
|
||||||
elif field_name == '__str__':
|
|
||||||
header = smart_str(lookup_opts.verbose_name)
|
|
||||||
else:
|
|
||||||
if callable(field_name):
|
|
||||||
attr = field_name # field_name can be a callable
|
|
||||||
else:
|
|
||||||
try:
|
|
||||||
attr = getattr(cl.model_admin, field_name)
|
|
||||||
except AttributeError:
|
|
||||||
try:
|
|
||||||
attr = getattr(cl.model, field_name)
|
|
||||||
except AttributeError:
|
|
||||||
raise AttributeError, \
|
|
||||||
"'%s' model or '%s' objects have no attribute '%s'" % \
|
|
||||||
(lookup_opts.object_name, cl.model_admin.__class__, field_name)
|
|
||||||
|
|
||||||
try:
|
|
||||||
header = attr.short_description
|
|
||||||
except AttributeError:
|
|
||||||
if callable(field_name):
|
|
||||||
header = field_name.__name__
|
|
||||||
else:
|
|
||||||
header = field_name
|
|
||||||
header = header.replace('_', ' ')
|
|
||||||
# if the field is the action checkbox: no sorting and special class
|
# if the field is the action checkbox: no sorting and special class
|
||||||
if field_name == 'action_checkbox':
|
if field_name == 'action_checkbox':
|
||||||
yield {"text": header,
|
yield {"text": header,
|
||||||
"class_attrib": mark_safe(' class="action-checkbox-column"')}
|
"class_attrib": mark_safe(' class="action-checkbox-column"')}
|
||||||
continue
|
continue
|
||||||
|
header = pretty_name(header)
|
||||||
|
|
||||||
# It is a non-field, but perhaps one that is sortable
|
# It is a non-field, but perhaps one that is sortable
|
||||||
admin_order_field = getattr(attr, "admin_order_field", None)
|
admin_order_field = getattr(attr, "admin_order_field", None)
|
||||||
|
@ -120,8 +98,6 @@ def result_headers(cl):
|
||||||
|
|
||||||
# So this _is_ a sortable non-field. Go to the yield
|
# So this _is_ a sortable non-field. Go to the yield
|
||||||
# after the else clause.
|
# after the else clause.
|
||||||
else:
|
|
||||||
header = f.verbose_name
|
|
||||||
|
|
||||||
th_classes = []
|
th_classes = []
|
||||||
new_order_type = 'asc'
|
new_order_type = 'asc'
|
||||||
|
@ -129,10 +105,12 @@ def result_headers(cl):
|
||||||
th_classes.append('sorted %sending' % cl.order_type.lower())
|
th_classes.append('sorted %sending' % cl.order_type.lower())
|
||||||
new_order_type = {'asc': 'desc', 'desc': 'asc'}[cl.order_type.lower()]
|
new_order_type = {'asc': 'desc', 'desc': 'asc'}[cl.order_type.lower()]
|
||||||
|
|
||||||
yield {"text": header,
|
yield {
|
||||||
"sortable": True,
|
"text": header,
|
||||||
"url": cl.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
|
"sortable": True,
|
||||||
"class_attrib": mark_safe(th_classes and ' class="%s"' % ' '.join(th_classes) or '')}
|
"url": cl.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
|
||||||
|
"class_attrib": mark_safe(th_classes and ' class="%s"' % ' '.join(th_classes) or '')
|
||||||
|
}
|
||||||
|
|
||||||
def _boolean_icon(field_val):
|
def _boolean_icon(field_val):
|
||||||
BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'}
|
BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'}
|
||||||
|
@ -144,24 +122,11 @@ def items_for_result(cl, result, form):
|
||||||
for field_name in cl.list_display:
|
for field_name in cl.list_display:
|
||||||
row_class = ''
|
row_class = ''
|
||||||
try:
|
try:
|
||||||
f = cl.lookup_opts.get_field(field_name)
|
f, attr, value = lookup_field(field_name, result, cl.model_admin)
|
||||||
except models.FieldDoesNotExist:
|
except (AttributeError, ObjectDoesNotExist):
|
||||||
# For non-field list_display values, the value is either a method,
|
result_repr = EMPTY_CHANGELIST_VALUE
|
||||||
# property or returned via a callable.
|
else:
|
||||||
try:
|
if f is None:
|
||||||
if callable(field_name):
|
|
||||||
attr = field_name
|
|
||||||
value = attr(result)
|
|
||||||
elif hasattr(cl.model_admin, field_name) and \
|
|
||||||
not field_name == '__str__' and not field_name == '__unicode__':
|
|
||||||
attr = getattr(cl.model_admin, field_name)
|
|
||||||
value = attr(result)
|
|
||||||
else:
|
|
||||||
attr = getattr(result, field_name)
|
|
||||||
if callable(attr):
|
|
||||||
value = attr()
|
|
||||||
else:
|
|
||||||
value = attr
|
|
||||||
allow_tags = getattr(attr, 'allow_tags', False)
|
allow_tags = getattr(attr, 'allow_tags', False)
|
||||||
boolean = getattr(attr, 'boolean', False)
|
boolean = getattr(attr, 'boolean', False)
|
||||||
if boolean:
|
if boolean:
|
||||||
|
@ -169,50 +134,21 @@ def items_for_result(cl, result, form):
|
||||||
result_repr = _boolean_icon(value)
|
result_repr = _boolean_icon(value)
|
||||||
else:
|
else:
|
||||||
result_repr = smart_unicode(value)
|
result_repr = smart_unicode(value)
|
||||||
except (AttributeError, ObjectDoesNotExist):
|
|
||||||
result_repr = EMPTY_CHANGELIST_VALUE
|
|
||||||
else:
|
|
||||||
# Strip HTML tags in the resulting text, except if the
|
# Strip HTML tags in the resulting text, except if the
|
||||||
# function has an "allow_tags" attribute set to True.
|
# function has an "allow_tags" attribute set to True.
|
||||||
if not allow_tags:
|
if not allow_tags:
|
||||||
result_repr = escape(result_repr)
|
result_repr = escape(result_repr)
|
||||||
else:
|
else:
|
||||||
result_repr = mark_safe(result_repr)
|
result_repr = mark_safe(result_repr)
|
||||||
else:
|
else:
|
||||||
field_val = getattr(result, f.attname)
|
if value is None:
|
||||||
|
result_repr = EMPTY_CHANGELIST_VALUE
|
||||||
if isinstance(f.rel, models.ManyToOneRel):
|
if isinstance(f.rel, models.ManyToOneRel):
|
||||||
if field_val is not None:
|
|
||||||
result_repr = escape(getattr(result, f.name))
|
result_repr = escape(getattr(result, f.name))
|
||||||
else:
|
else:
|
||||||
result_repr = EMPTY_CHANGELIST_VALUE
|
result_repr = display_for_field(value, f)
|
||||||
# Dates and times are special: They're formatted in a certain way.
|
if isinstance(f, models.DateField) or isinstance(f, models.TimeField):
|
||||||
elif isinstance(f, models.DateField) or isinstance(f, models.TimeField):
|
row_class = ' class="nowrap"'
|
||||||
if field_val:
|
|
||||||
result_repr = formats.localize(field_val)
|
|
||||||
else:
|
|
||||||
result_repr = EMPTY_CHANGELIST_VALUE
|
|
||||||
elif isinstance(f, models.DecimalField):
|
|
||||||
if field_val:
|
|
||||||
result_repr = formats.number_format(field_val, f.decimal_places)
|
|
||||||
else:
|
|
||||||
result_repr = EMPTY_CHANGELIST_VALUE
|
|
||||||
row_class = ' class="nowrap"'
|
|
||||||
elif isinstance(f, models.FloatField):
|
|
||||||
if field_val:
|
|
||||||
result_repr = formats.number_format(field_val)
|
|
||||||
else:
|
|
||||||
result_repr = EMPTY_CHANGELIST_VALUE
|
|
||||||
row_class = ' class="nowrap"'
|
|
||||||
# Booleans are special: We use images.
|
|
||||||
elif isinstance(f, models.BooleanField) or isinstance(f, models.NullBooleanField):
|
|
||||||
result_repr = _boolean_icon(field_val)
|
|
||||||
# Fields with choices are special: Use the representation
|
|
||||||
# of the choice.
|
|
||||||
elif f.flatchoices:
|
|
||||||
result_repr = dict(f.flatchoices).get(field_val, EMPTY_CHANGELIST_VALUE)
|
|
||||||
else:
|
|
||||||
result_repr = escape(field_val)
|
|
||||||
if force_unicode(result_repr) == '':
|
if force_unicode(result_repr) == '':
|
||||||
result_repr = mark_safe(' ')
|
result_repr = mark_safe(' ')
|
||||||
# If list_display_links not defined, add the link tag to the first field
|
# If list_display_links not defined, add the link tag to the first field
|
||||||
|
|
|
@ -1,12 +1,14 @@
|
||||||
from django.core.exceptions import ObjectDoesNotExist
|
from django.core.exceptions import ObjectDoesNotExist
|
||||||
from django.db import models
|
from django.db import models
|
||||||
|
from django.utils import formats
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
from django.utils.safestring import mark_safe
|
from django.utils.safestring import mark_safe
|
||||||
from django.utils.text import capfirst
|
from django.utils.text import capfirst
|
||||||
from django.utils.encoding import force_unicode
|
from django.utils.encoding import force_unicode, smart_unicode, smart_str
|
||||||
from django.utils.translation import ungettext, ugettext as _
|
from django.utils.translation import ungettext, ugettext as _
|
||||||
from django.core.urlresolvers import reverse, NoReverseMatch
|
from django.core.urlresolvers import reverse, NoReverseMatch
|
||||||
|
|
||||||
|
|
||||||
def quote(s):
|
def quote(s):
|
||||||
"""
|
"""
|
||||||
Ensure that primary key values do not confuse the admin URLs by escaping
|
Ensure that primary key values do not confuse the admin URLs by escaping
|
||||||
|
@ -221,3 +223,74 @@ def model_ngettext(obj, n=None):
|
||||||
d = model_format_dict(obj)
|
d = model_format_dict(obj)
|
||||||
singular, plural = d["verbose_name"], d["verbose_name_plural"]
|
singular, plural = d["verbose_name"], d["verbose_name_plural"]
|
||||||
return ungettext(singular, plural, n or 0)
|
return ungettext(singular, plural, n or 0)
|
||||||
|
|
||||||
|
def lookup_field(name, obj, model_admin=None):
|
||||||
|
opts = obj._meta
|
||||||
|
try:
|
||||||
|
f = opts.get_field(name)
|
||||||
|
except models.FieldDoesNotExist:
|
||||||
|
# For non-field values, the value is either a method, property or
|
||||||
|
# returned via a callable.
|
||||||
|
if callable(name):
|
||||||
|
attr = name
|
||||||
|
value = attr(obj)
|
||||||
|
elif (model_admin is not None and hasattr(model_admin, name) and
|
||||||
|
not name == '__str__' and not name == '__unicode__'):
|
||||||
|
attr = getattr(model_admin, name)
|
||||||
|
value = attr(obj)
|
||||||
|
else:
|
||||||
|
attr = getattr(obj, name)
|
||||||
|
if callable(attr):
|
||||||
|
value = attr()
|
||||||
|
else:
|
||||||
|
value = attr
|
||||||
|
f = None
|
||||||
|
else:
|
||||||
|
attr = None
|
||||||
|
value = getattr(obj, f.attname)
|
||||||
|
return f, attr, value
|
||||||
|
|
||||||
|
def label_for_field(name, model, model_admin):
|
||||||
|
try:
|
||||||
|
model._meta.get_field_by_name(name)[0]
|
||||||
|
return name
|
||||||
|
except models.FieldDoesNotExist:
|
||||||
|
if name == "__unicode__":
|
||||||
|
return force_unicode(model._meta.verbose_name)
|
||||||
|
if name == "__str__":
|
||||||
|
return smart_str(model._meta.verbose_name)
|
||||||
|
if callable(name):
|
||||||
|
attr = name
|
||||||
|
elif hasattr(model_admin, name):
|
||||||
|
attr = getattr(model_admin, name)
|
||||||
|
elif hasattr(model, name):
|
||||||
|
attr = getattr(model, name)
|
||||||
|
else:
|
||||||
|
raise AttributeError
|
||||||
|
|
||||||
|
if hasattr(attr, "short_description"):
|
||||||
|
return attr.short_description
|
||||||
|
elif callable(attr):
|
||||||
|
if attr.__name__ == "<lambda>":
|
||||||
|
return "--"
|
||||||
|
else:
|
||||||
|
return attr.__name__
|
||||||
|
else:
|
||||||
|
return name
|
||||||
|
|
||||||
|
|
||||||
|
def display_for_field(value, field):
|
||||||
|
from django.contrib.admin.templatetags.admin_list import _boolean_icon
|
||||||
|
from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
|
||||||
|
if isinstance(field, models.DateField) or isinstance(field, models.TimeField):
|
||||||
|
return formats.localize(value)
|
||||||
|
elif isinstance(field, models.BooleanField) or isinstance(field, models.NullBooleanField):
|
||||||
|
return _boolean_icon(value)
|
||||||
|
elif isinstance(field, models.DecimalField):
|
||||||
|
return formats.number_format(value, field.decimal_places)
|
||||||
|
elif isinstance(field, models.FloatField):
|
||||||
|
return formats.number_format(value)
|
||||||
|
elif field.flatchoices:
|
||||||
|
return dict(field.flatchoices).get(value, EMPTY_CHANGELIST_VALUE)
|
||||||
|
else:
|
||||||
|
return smart_unicode(value)
|
||||||
|
|
|
@ -1,13 +1,11 @@
|
||||||
try:
|
|
||||||
set
|
|
||||||
except NameError:
|
|
||||||
from sets import Set as set # Python 2.3 fallback
|
|
||||||
|
|
||||||
from django.core.exceptions import ImproperlyConfigured
|
from django.core.exceptions import ImproperlyConfigured
|
||||||
from django.db import models
|
from django.db import models
|
||||||
from django.forms.models import BaseModelForm, BaseModelFormSet, fields_for_model, _get_foreign_key
|
from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model,
|
||||||
|
_get_foreign_key)
|
||||||
from django.contrib.admin.options import flatten_fieldsets, BaseModelAdmin
|
from django.contrib.admin.options import flatten_fieldsets, BaseModelAdmin
|
||||||
from django.contrib.admin.options import HORIZONTAL, VERTICAL
|
from django.contrib.admin.options import HORIZONTAL, VERTICAL
|
||||||
|
from django.contrib.admin.util import lookup_field
|
||||||
|
|
||||||
|
|
||||||
__all__ = ['validate']
|
__all__ = ['validate']
|
||||||
|
|
||||||
|
@ -123,6 +121,18 @@ def validate(cls, model):
|
||||||
continue
|
continue
|
||||||
get_field(cls, model, opts, 'ordering[%d]' % idx, field)
|
get_field(cls, model, opts, 'ordering[%d]' % idx, field)
|
||||||
|
|
||||||
|
if hasattr(cls, "readonly_fields"):
|
||||||
|
check_isseq(cls, "readonly_fields", cls.readonly_fields)
|
||||||
|
for idx, field in enumerate(cls.readonly_fields):
|
||||||
|
if not callable(field):
|
||||||
|
if not hasattr(cls, field):
|
||||||
|
if not hasattr(model, field):
|
||||||
|
try:
|
||||||
|
opts.get_field(field)
|
||||||
|
except models.FieldDoesNotExist:
|
||||||
|
raise ImproperlyConfigured("%s.readonly_fields[%d], %r is not a callable or an attribute of %r or found in the model %r."
|
||||||
|
% (cls.__name__, idx, field, cls.__name__, model._meta.object_name))
|
||||||
|
|
||||||
# list_select_related = False
|
# list_select_related = False
|
||||||
# save_as = False
|
# save_as = False
|
||||||
# save_on_top = False
|
# save_on_top = False
|
||||||
|
@ -195,6 +205,11 @@ def validate_base(cls, model):
|
||||||
if cls.fields: # default value is None
|
if cls.fields: # default value is None
|
||||||
check_isseq(cls, 'fields', cls.fields)
|
check_isseq(cls, 'fields', cls.fields)
|
||||||
for field in cls.fields:
|
for field in cls.fields:
|
||||||
|
if field in cls.readonly_fields:
|
||||||
|
# Stuff can be put in fields that isn't actually a model field
|
||||||
|
# if it's in readonly_fields, readonly_fields will handle the
|
||||||
|
# validation of such things.
|
||||||
|
continue
|
||||||
check_formfield(cls, model, opts, 'fields', field)
|
check_formfield(cls, model, opts, 'fields', field)
|
||||||
f = get_field(cls, model, opts, 'fields', field)
|
f = get_field(cls, model, opts, 'fields', field)
|
||||||
if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created:
|
if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created:
|
||||||
|
|
|
@ -172,6 +172,11 @@ The ``field_options`` dictionary can have the following keys:
|
||||||
'fields': (('first_name', 'last_name'), 'address', 'city', 'state'),
|
'fields': (('first_name', 'last_name'), 'address', 'city', 'state'),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.. versionadded:: 1.2
|
||||||
|
|
||||||
|
``fields`` can contain values defined in
|
||||||
|
:attr:`ModelAdmin.readonly_fields` to be displayed as read-only.
|
||||||
|
|
||||||
* ``classes``
|
* ``classes``
|
||||||
A list containing extra CSS classes to apply to the fieldset.
|
A list containing extra CSS classes to apply to the fieldset.
|
||||||
|
|
||||||
|
@ -210,6 +215,11 @@ the ``django.contrib.flatpages.FlatPage`` model as follows::
|
||||||
In the above example, only the fields 'url', 'title' and 'content' will be
|
In the above example, only the fields 'url', 'title' and 'content' will be
|
||||||
displayed, sequentially, in the form.
|
displayed, sequentially, in the form.
|
||||||
|
|
||||||
|
.. versionadded:: 1.2
|
||||||
|
|
||||||
|
``fields`` can contain values defined in :attr:`ModelAdmin.readonly_fields`
|
||||||
|
to be displayed as read-only.
|
||||||
|
|
||||||
.. admonition:: Note
|
.. admonition:: Note
|
||||||
|
|
||||||
This ``fields`` option should not be confused with the ``fields``
|
This ``fields`` option should not be confused with the ``fields``
|
||||||
|
@ -540,6 +550,21 @@ into a ``Input`` widget for either a ``ForeignKey`` or ``ManyToManyField``::
|
||||||
class ArticleAdmin(admin.ModelAdmin):
|
class ArticleAdmin(admin.ModelAdmin):
|
||||||
raw_id_fields = ("newspaper",)
|
raw_id_fields = ("newspaper",)
|
||||||
|
|
||||||
|
.. attribute:: ModelAdmin.readonly_fields
|
||||||
|
|
||||||
|
.. versionadded:: 1.2
|
||||||
|
|
||||||
|
By default the admin shows all fields as editable. Any fields in this option
|
||||||
|
(which should be a ``list`` or ``tuple``) will display its data as-is and
|
||||||
|
non-editable. This option behaves nearly identical to :attr:`ModelAdmin.list_display`.
|
||||||
|
Usage is the same, however, when you specify :attr:`ModelAdmin.fields` or
|
||||||
|
:attr:`ModelAdmin.fieldsets` the read-only fields must be present to be shown
|
||||||
|
(they are ignored otherwise).
|
||||||
|
|
||||||
|
If ``readonly_fields`` is used without defining explicit ordering through
|
||||||
|
:attr:`ModelAdmin.fields` or :attr:`ModelAdmin.fieldsets` they will be added
|
||||||
|
last after all editable fields.
|
||||||
|
|
||||||
.. attribute:: ModelAdmin.save_as
|
.. attribute:: ModelAdmin.save_as
|
||||||
|
|
||||||
Set ``save_as`` to enable a "save as" feature on admin change forms.
|
Set ``save_as`` to enable a "save as" feature on admin change forms.
|
||||||
|
@ -744,6 +769,15 @@ model instance::
|
||||||
instance.save()
|
instance.save()
|
||||||
formset.save_m2m()
|
formset.save_m2m()
|
||||||
|
|
||||||
|
.. method:: ModelAdmin.get_readonly_fields(self, request, obj=None)
|
||||||
|
|
||||||
|
.. versionadded:: 1.2
|
||||||
|
|
||||||
|
The ``get_readonly_fields`` method is given the ``HttpRequest`` and the
|
||||||
|
``obj`` being edited (or ``None`` on an add form) and is expected to return a
|
||||||
|
``list`` or ``tuple`` of field names that will be displayed as read-only, as
|
||||||
|
described above in the :attr:`ModelAdmin.readonly_fields` section.
|
||||||
|
|
||||||
.. method:: ModelAdmin.get_urls(self)
|
.. method:: ModelAdmin.get_urls(self)
|
||||||
|
|
||||||
.. versionadded:: 1.1
|
.. versionadded:: 1.1
|
||||||
|
|
|
@ -485,3 +485,10 @@ enabled, dates and numbers on templates will be displayed using the format
|
||||||
specified for the current locale. Django will also use localized formats
|
specified for the current locale. Django will also use localized formats
|
||||||
when parsing data in forms.
|
when parsing data in forms.
|
||||||
See :ref:`Format localization <format-localization>` for more details.
|
See :ref:`Format localization <format-localization>` for more details.
|
||||||
|
|
||||||
|
Added ``readonly_fields`` to ``ModelAdmin``
|
||||||
|
-------------------------------------------
|
||||||
|
|
||||||
|
:attr:`django.contrib.admin.ModelAdmin.readonly_fields` has been added to
|
||||||
|
enable non-editable fields in add/change pages for models and inlines. Field
|
||||||
|
and calculated values can be displayed along side editable fields.
|
||||||
|
|
|
@ -19,6 +19,10 @@ class Song(models.Model):
|
||||||
def __unicode__(self):
|
def __unicode__(self):
|
||||||
return self.title
|
return self.title
|
||||||
|
|
||||||
|
def readonly_method_on_model(self):
|
||||||
|
# does nothing
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
class TwoAlbumFKAndAnE(models.Model):
|
class TwoAlbumFKAndAnE(models.Model):
|
||||||
album1 = models.ForeignKey(Album, related_name="album1_set")
|
album1 = models.ForeignKey(Album, related_name="album1_set")
|
||||||
|
@ -110,6 +114,63 @@ Exception: <class 'regressiontests.admin_validation.models.TwoAlbumFKAndAnE'> ha
|
||||||
|
|
||||||
>>> validate_inline(TwoAlbumFKAndAnEInline, None, Album)
|
>>> validate_inline(TwoAlbumFKAndAnEInline, None, Album)
|
||||||
|
|
||||||
|
>>> class SongAdmin(admin.ModelAdmin):
|
||||||
|
... readonly_fields = ("title",)
|
||||||
|
|
||||||
|
>>> validate(SongAdmin, Song)
|
||||||
|
|
||||||
|
>>> def my_function(obj):
|
||||||
|
... # does nothing
|
||||||
|
... pass
|
||||||
|
>>> class SongAdmin(admin.ModelAdmin):
|
||||||
|
... readonly_fields = (my_function,)
|
||||||
|
|
||||||
|
>>> validate(SongAdmin, Song)
|
||||||
|
|
||||||
|
>>> class SongAdmin(admin.ModelAdmin):
|
||||||
|
... readonly_fields = ("readonly_method_on_modeladmin",)
|
||||||
|
...
|
||||||
|
... def readonly_method_on_modeladmin(self, obj):
|
||||||
|
... # does nothing
|
||||||
|
... pass
|
||||||
|
|
||||||
|
>>> validate(SongAdmin, Song)
|
||||||
|
|
||||||
|
>>> class SongAdmin(admin.ModelAdmin):
|
||||||
|
... readonly_fields = ("readonly_method_on_model",)
|
||||||
|
|
||||||
|
>>> validate(SongAdmin, Song)
|
||||||
|
|
||||||
|
>>> class SongAdmin(admin.ModelAdmin):
|
||||||
|
... readonly_fields = ("title", "nonexistant")
|
||||||
|
|
||||||
|
>>> validate(SongAdmin, Song)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ImproperlyConfigured: SongAdmin.readonly_fields[1], 'nonexistant' is not a callable or an attribute of 'SongAdmin' or found in the model 'Song'.
|
||||||
|
|
||||||
|
>>> class SongAdmin(admin.ModelAdmin):
|
||||||
|
... readonly_fields = ("title", "awesome_song")
|
||||||
|
... fields = ("album", "title", "awesome_song")
|
||||||
|
|
||||||
|
>>> validate(SongAdmin, Song)
|
||||||
|
Traceback (most recent call last):
|
||||||
|
...
|
||||||
|
ImproperlyConfigured: SongAdmin.readonly_fields[1], 'awesome_song' is not a callable or an attribute of 'SongAdmin' or found in the model 'Song'.
|
||||||
|
|
||||||
|
>>> class SongAdmin(SongAdmin):
|
||||||
|
... def awesome_song(self, instance):
|
||||||
|
... if instance.title == "Born to Run":
|
||||||
|
... return "Best Ever!"
|
||||||
|
... return "Status unknown."
|
||||||
|
|
||||||
|
>>> validate(SongAdmin, Song)
|
||||||
|
|
||||||
|
>>> class SongAdmin(admin.ModelAdmin):
|
||||||
|
... readonly_fields = (lambda obj: "test",)
|
||||||
|
|
||||||
|
>>> validate(SongAdmin, Song)
|
||||||
|
|
||||||
# Regression test for #12203/#12237 - Fail more gracefully when a M2M field that
|
# Regression test for #12203/#12237 - Fail more gracefully when a M2M field that
|
||||||
# specifies the 'through' option is included in the 'fields' or the 'fieldsets'
|
# specifies the 'through' option is included in the 'fields' or the 'fieldsets'
|
||||||
# ModelAdmin options.
|
# ModelAdmin options.
|
||||||
|
|
|
@ -1,11 +1,14 @@
|
||||||
# -*- coding: utf-8 -*-
|
# -*- coding: utf-8 -*-
|
||||||
|
import datetime
|
||||||
import tempfile
|
import tempfile
|
||||||
import os
|
import os
|
||||||
from django.core.files.storage import FileSystemStorage
|
|
||||||
from django.db import models
|
|
||||||
from django.contrib import admin
|
from django.contrib import admin
|
||||||
|
from django.core.files.storage import FileSystemStorage
|
||||||
from django.contrib.admin.views.main import ChangeList
|
from django.contrib.admin.views.main import ChangeList
|
||||||
from django.core.mail import EmailMessage
|
from django.core.mail import EmailMessage
|
||||||
|
from django.db import models
|
||||||
|
|
||||||
|
|
||||||
class Section(models.Model):
|
class Section(models.Model):
|
||||||
"""
|
"""
|
||||||
|
@ -419,7 +422,47 @@ class CategoryInline(admin.StackedInline):
|
||||||
model = Category
|
model = Category
|
||||||
|
|
||||||
class CollectorAdmin(admin.ModelAdmin):
|
class CollectorAdmin(admin.ModelAdmin):
|
||||||
inlines = [WidgetInline, DooHickeyInline, GrommetInline, WhatsitInline, FancyDoodadInline, CategoryInline]
|
inlines = [
|
||||||
|
WidgetInline, DooHickeyInline, GrommetInline, WhatsitInline,
|
||||||
|
FancyDoodadInline, CategoryInline
|
||||||
|
]
|
||||||
|
|
||||||
|
class Link(models.Model):
|
||||||
|
posted = models.DateField(
|
||||||
|
default=lambda: datetime.date.today() - datetime.timedelta(days=7)
|
||||||
|
)
|
||||||
|
url = models.URLField()
|
||||||
|
post = models.ForeignKey("Post")
|
||||||
|
|
||||||
|
|
||||||
|
class LinkInline(admin.TabularInline):
|
||||||
|
model = Link
|
||||||
|
extra = 1
|
||||||
|
|
||||||
|
readonly_fields = ("posted",)
|
||||||
|
|
||||||
|
|
||||||
|
class Post(models.Model):
|
||||||
|
title = models.CharField(max_length=100)
|
||||||
|
content = models.TextField()
|
||||||
|
posted = models.DateField(default=datetime.date.today)
|
||||||
|
|
||||||
|
def awesomeness_level(self):
|
||||||
|
return "Very awesome."
|
||||||
|
|
||||||
|
class PostAdmin(admin.ModelAdmin):
|
||||||
|
readonly_fields = ('posted', 'awesomeness_level', 'coolness', lambda obj: "foo")
|
||||||
|
|
||||||
|
inlines = [
|
||||||
|
LinkInline
|
||||||
|
]
|
||||||
|
|
||||||
|
def coolness(self, instance):
|
||||||
|
if instance.pk:
|
||||||
|
return "%d amount of cool." % instance.pk
|
||||||
|
else:
|
||||||
|
return "Unkown coolness."
|
||||||
|
|
||||||
|
|
||||||
class Gadget(models.Model):
|
class Gadget(models.Model):
|
||||||
name = models.CharField(max_length=100)
|
name = models.CharField(max_length=100)
|
||||||
|
@ -458,6 +501,7 @@ admin.site.register(Recommendation, RecommendationAdmin)
|
||||||
admin.site.register(Recommender)
|
admin.site.register(Recommender)
|
||||||
admin.site.register(Collector, CollectorAdmin)
|
admin.site.register(Collector, CollectorAdmin)
|
||||||
admin.site.register(Category, CategoryAdmin)
|
admin.site.register(Category, CategoryAdmin)
|
||||||
|
admin.site.register(Post, PostAdmin)
|
||||||
admin.site.register(Gadget, GadgetAdmin)
|
admin.site.register(Gadget, GadgetAdmin)
|
||||||
|
|
||||||
# We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
|
# We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
|
||||||
|
|
|
@ -10,20 +10,18 @@ from django.contrib.admin.models import LogEntry, DELETION
|
||||||
from django.contrib.admin.sites import LOGIN_FORM_KEY
|
from django.contrib.admin.sites import LOGIN_FORM_KEY
|
||||||
from django.contrib.admin.util import quote
|
from django.contrib.admin.util import quote
|
||||||
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
|
from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
|
||||||
|
from django.utils import formats
|
||||||
from django.utils.cache import get_max_age
|
from django.utils.cache import get_max_age
|
||||||
from django.utils.html import escape
|
from django.utils.html import escape
|
||||||
|
from django.utils.translation import get_date_formats
|
||||||
|
|
||||||
# local test models
|
# local test models
|
||||||
from models import Article, BarAccount, CustomArticle, EmptyModel, \
|
from models import Article, BarAccount, CustomArticle, EmptyModel, \
|
||||||
ExternalSubscriber, FooAccount, Gallery, ModelWithStringPrimaryKey, \
|
ExternalSubscriber, FooAccount, Gallery, ModelWithStringPrimaryKey, \
|
||||||
Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, \
|
Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, \
|
||||||
Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, \
|
Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, \
|
||||||
Category
|
Category, Post
|
||||||
|
|
||||||
try:
|
|
||||||
set
|
|
||||||
except NameError:
|
|
||||||
from sets import Set as set
|
|
||||||
|
|
||||||
class AdminViewBasicTest(TestCase):
|
class AdminViewBasicTest(TestCase):
|
||||||
fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', 'admin-views-fabrics.xml']
|
fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', 'admin-views-fabrics.xml']
|
||||||
|
@ -1688,3 +1686,54 @@ class NeverCacheTests(TestCase):
|
||||||
"Check the never-cache status of the Javascript i18n view"
|
"Check the never-cache status of the Javascript i18n view"
|
||||||
response = self.client.get('/test_admin/jsi18n/')
|
response = self.client.get('/test_admin/jsi18n/')
|
||||||
self.failUnlessEqual(get_max_age(response), None)
|
self.failUnlessEqual(get_max_age(response), None)
|
||||||
|
|
||||||
|
|
||||||
|
class ReadonlyTest(TestCase):
|
||||||
|
fixtures = ['admin-views-users.xml']
|
||||||
|
|
||||||
|
def setUp(self):
|
||||||
|
self.client.login(username='super', password='secret')
|
||||||
|
|
||||||
|
def tearDown(self):
|
||||||
|
self.client.logout()
|
||||||
|
|
||||||
|
def test_readonly_get(self):
|
||||||
|
response = self.client.get('/test_admin/admin/admin_views/post/add/')
|
||||||
|
self.assertEqual(response.status_code, 200)
|
||||||
|
self.assertNotContains(response, 'name="posted"')
|
||||||
|
# 3 fields + 2 submit buttons + 2 inline management form fields, + 2
|
||||||
|
# hidden fields for inlines + 1 field for the inline
|
||||||
|
self.assertEqual(response.content.count("input"), 10)
|
||||||
|
self.assertContains(response, formats.localize(datetime.date.today()))
|
||||||
|
self.assertContains(response,
|
||||||
|
"<label>Awesomeness level:</label>")
|
||||||
|
self.assertContains(response, "Very awesome.")
|
||||||
|
self.assertContains(response, "Unkown coolness.")
|
||||||
|
self.assertContains(response, "foo")
|
||||||
|
self.assertContains(response,
|
||||||
|
formats.localize(datetime.date.today() - datetime.timedelta(days=7))
|
||||||
|
)
|
||||||
|
|
||||||
|
p = Post.objects.create(title="I worked on readonly_fields", content="Its good stuff")
|
||||||
|
response = self.client.get('/test_admin/admin/admin_views/post/%d/' % p.pk)
|
||||||
|
self.assertContains(response, "%d amount of cool" % p.pk)
|
||||||
|
|
||||||
|
def test_readonly_post(self):
|
||||||
|
data = {
|
||||||
|
"title": "Django Got Readonly Fields",
|
||||||
|
"content": "This is an incredible development.",
|
||||||
|
"link_set-TOTAL_FORMS": "1",
|
||||||
|
"link_set-INITIAL_FORMS": "0",
|
||||||
|
}
|
||||||
|
response = self.client.post('/test_admin/admin/admin_views/post/add/', data)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(Post.objects.count(), 1)
|
||||||
|
p = Post.objects.get()
|
||||||
|
self.assertEqual(p.posted, datetime.date.today())
|
||||||
|
|
||||||
|
data["posted"] = "10-8-1990" # some date that's not today
|
||||||
|
response = self.client.post('/test_admin/admin/admin_views/post/add/', data)
|
||||||
|
self.assertEqual(response.status_code, 302)
|
||||||
|
self.assertEqual(Post.objects.count(), 2)
|
||||||
|
p = Post.objects.order_by('-id')[0]
|
||||||
|
self.assertEqual(p.posted, datetime.date.today())
|
||||||
|
|
Loading…
Reference in New Issue