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.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):
|
||||
|
@ -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:
|
||||
|
@ -49,11 +62,14 @@ class AdminForm(object):
|
|||
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:
|
||||
|
@ -63,22 +79,30 @@ class Fieldset(object):
|
|||
|
||||
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):
|
||||
|
@ -100,27 +124,88 @@ class AdminField(object):
|
|||
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
|
||||
|
@ -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:
|
||||
|
@ -194,7 +283,8 @@ class InlineFieldset(Fieldset):
|
|||
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):
|
||||
"""
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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)
|
||||
|
@ -178,6 +179,9 @@ class BaseModelAdmin(object):
|
|||
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
|
||||
|
@ -327,7 +331,8 @@ class ModelAdmin(BaseModelAdmin):
|
|||
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):
|
||||
"""
|
||||
|
@ -342,12 +347,15 @@ class ModelAdmin(BaseModelAdmin):
|
|||
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)
|
||||
|
@ -782,13 +790,17 @@ class ModelAdmin(BaseModelAdmin):
|
|||
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
|
||||
|
||||
|
@ -875,13 +887,17 @@ class ModelAdmin(BaseModelAdmin):
|
|||
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
|
||||
|
||||
|
@ -1174,14 +1190,17 @@ class InlineModelAdmin(BaseModelAdmin):
|
|||
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,
|
||||
|
@ -1193,7 +1212,8 @@ class InlineModelAdmin(BaseModelAdmin):
|
|||
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()
|
||||
|
|
|
@ -7,10 +7,10 @@
|
|||
{{ 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>
|
||||
|
@ -44,8 +44,12 @@
|
|||
{% 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 %}
|
||||
|
|
|
@ -1,19 +1,28 @@
|
|||
<fieldset class="module aligned {{ fieldset.classes }}">
|
||||
{% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %}
|
||||
{% if fieldset.description %}<div class="description">{{ fieldset.description|safe }}</div>{% endif %}
|
||||
{% for line in fieldset %}
|
||||
<div class="form-row{% if line.errors %} errors{% endif %} {% for field in line %}{{ field.field.name }} {% endfor %} ">
|
||||
{{ line.errors }}
|
||||
{% for field in line %}
|
||||
<div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
|
||||
{% if field.is_checkbox %}
|
||||
{{ field.field }}{{ field.label_tag }}
|
||||
{% else %}
|
||||
{{ field.label_tag }}{{ field.field }}
|
||||
{% endif %}
|
||||
{% if field.field.field.help_text %}<p class="help">{{ field.field.field.help_text|safe }}</p>{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
{% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %}
|
||||
{% if fieldset.description %}
|
||||
<div class="description">{{ fieldset.description|safe }}</div>
|
||||
{% endif %}
|
||||
{% for line in fieldset %}
|
||||
<div class="form-row{% if line.errors %} errors{% endif %} {% for field in line %}{{ field.field.name }} {% endfor %} ">
|
||||
{{ line.errors }}
|
||||
{% for field in line %}
|
||||
<div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
|
||||
{% if field.is_checkbox %}
|
||||
{{ field.field }}{{ field.label_tag }}
|
||||
{% else %}
|
||||
{{ field.label_tag }}
|
||||
{% if field.is_readonly %}
|
||||
<p>{{ field.contents }}</p>
|
||||
{% else %}
|
||||
{{ 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>
|
||||
|
|
|
@ -1,16 +1,20 @@
|
|||
import datetime
|
||||
|
||||
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 ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR, SEARCH_VAR
|
||||
from django.core.exceptions import ObjectDoesNotExist
|
||||
from django.db import models
|
||||
from django.forms.forms import pretty_name
|
||||
from django.utils import formats
|
||||
from django.utils.html import escape, conditional_escape
|
||||
from django.utils.text import capfirst
|
||||
from django.utils.safestring import mark_safe
|
||||
from django.utils.text import capfirst
|
||||
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
|
||||
import datetime
|
||||
|
||||
|
||||
register = Library()
|
||||
|
||||
|
@ -76,41 +80,15 @@ def result_headers(cl):
|
|||
try:
|
||||
f = lookup_opts.get_field(field_name)
|
||||
admin_order_field = None
|
||||
header = f.verbose_name
|
||||
except models.FieldDoesNotExist:
|
||||
# For non-field list_display values, check for the function
|
||||
# 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('_', ' ')
|
||||
header = label_for_field(field_name, cl.model, cl.model_admin)
|
||||
# if the field is the action checkbox: no sorting and special class
|
||||
if field_name == 'action_checkbox':
|
||||
yield {"text": header,
|
||||
"class_attrib": mark_safe(' class="action-checkbox-column"')}
|
||||
continue
|
||||
header = pretty_name(header)
|
||||
|
||||
# It is a non-field, but perhaps one that is sortable
|
||||
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
|
||||
# after the else clause.
|
||||
else:
|
||||
header = f.verbose_name
|
||||
|
||||
th_classes = []
|
||||
new_order_type = 'asc'
|
||||
|
@ -129,10 +105,12 @@ def result_headers(cl):
|
|||
th_classes.append('sorted %sending' % cl.order_type.lower())
|
||||
new_order_type = {'asc': 'desc', 'desc': 'asc'}[cl.order_type.lower()]
|
||||
|
||||
yield {"text": header,
|
||||
"sortable": True,
|
||||
"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 '')}
|
||||
yield {
|
||||
"text": header,
|
||||
"sortable": True,
|
||||
"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):
|
||||
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:
|
||||
row_class = ''
|
||||
try:
|
||||
f = cl.lookup_opts.get_field(field_name)
|
||||
except models.FieldDoesNotExist:
|
||||
# For non-field list_display values, the value is either a method,
|
||||
# property or returned via a callable.
|
||||
try:
|
||||
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
|
||||
f, attr, value = lookup_field(field_name, result, cl.model_admin)
|
||||
except (AttributeError, ObjectDoesNotExist):
|
||||
result_repr = EMPTY_CHANGELIST_VALUE
|
||||
else:
|
||||
if f is None:
|
||||
allow_tags = getattr(attr, 'allow_tags', False)
|
||||
boolean = getattr(attr, 'boolean', False)
|
||||
if boolean:
|
||||
|
@ -169,50 +134,21 @@ def items_for_result(cl, result, form):
|
|||
result_repr = _boolean_icon(value)
|
||||
else:
|
||||
result_repr = smart_unicode(value)
|
||||
except (AttributeError, ObjectDoesNotExist):
|
||||
result_repr = EMPTY_CHANGELIST_VALUE
|
||||
else:
|
||||
# Strip HTML tags in the resulting text, except if the
|
||||
# function has an "allow_tags" attribute set to True.
|
||||
if not allow_tags:
|
||||
result_repr = escape(result_repr)
|
||||
else:
|
||||
result_repr = mark_safe(result_repr)
|
||||
else:
|
||||
field_val = getattr(result, f.attname)
|
||||
|
||||
if isinstance(f.rel, models.ManyToOneRel):
|
||||
if field_val is not None:
|
||||
else:
|
||||
if value is None:
|
||||
result_repr = EMPTY_CHANGELIST_VALUE
|
||||
if isinstance(f.rel, models.ManyToOneRel):
|
||||
result_repr = escape(getattr(result, f.name))
|
||||
else:
|
||||
result_repr = EMPTY_CHANGELIST_VALUE
|
||||
# Dates and times are special: They're formatted in a certain way.
|
||||
elif isinstance(f, models.DateField) or isinstance(f, models.TimeField):
|
||||
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)
|
||||
result_repr = display_for_field(value, f)
|
||||
if isinstance(f, models.DateField) or isinstance(f, models.TimeField):
|
||||
row_class = ' class="nowrap"'
|
||||
if force_unicode(result_repr) == '':
|
||||
result_repr = mark_safe(' ')
|
||||
# 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.db import models
|
||||
from django.utils import formats
|
||||
from django.utils.html import escape
|
||||
from django.utils.safestring import mark_safe
|
||||
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.core.urlresolvers import reverse, NoReverseMatch
|
||||
|
||||
|
||||
def quote(s):
|
||||
"""
|
||||
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)
|
||||
singular, plural = d["verbose_name"], d["verbose_name_plural"]
|
||||
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.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 HORIZONTAL, VERTICAL
|
||||
from django.contrib.admin.util import lookup_field
|
||||
|
||||
|
||||
__all__ = ['validate']
|
||||
|
||||
|
@ -123,6 +121,18 @@ def validate(cls, model):
|
|||
continue
|
||||
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
|
||||
# save_as = False
|
||||
# save_on_top = False
|
||||
|
@ -195,6 +205,11 @@ def validate_base(cls, model):
|
|||
if cls.fields: # default value is None
|
||||
check_isseq(cls, 'fields', 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)
|
||||
f = get_field(cls, model, opts, 'fields', field)
|
||||
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'),
|
||||
}
|
||||
|
||||
.. versionadded:: 1.2
|
||||
|
||||
``fields`` can contain values defined in
|
||||
:attr:`ModelAdmin.readonly_fields` to be displayed as read-only.
|
||||
|
||||
* ``classes``
|
||||
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
|
||||
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
|
||||
|
||||
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):
|
||||
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
|
||||
|
||||
Set ``save_as`` to enable a "save as" feature on admin change forms.
|
||||
|
@ -744,6 +769,15 @@ model instance::
|
|||
instance.save()
|
||||
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)
|
||||
|
||||
.. 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
|
||||
when parsing data in forms.
|
||||
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):
|
||||
return self.title
|
||||
|
||||
def readonly_method_on_model(self):
|
||||
# does nothing
|
||||
pass
|
||||
|
||||
|
||||
class TwoAlbumFKAndAnE(models.Model):
|
||||
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)
|
||||
|
||||
>>> 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
|
||||
# specifies the 'through' option is included in the 'fields' or the 'fieldsets'
|
||||
# ModelAdmin options.
|
||||
|
|
|
@ -1,11 +1,14 @@
|
|||
# -*- coding: utf-8 -*-
|
||||
import datetime
|
||||
import tempfile
|
||||
import os
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
from django.db import models
|
||||
|
||||
from django.contrib import admin
|
||||
from django.core.files.storage import FileSystemStorage
|
||||
from django.contrib.admin.views.main import ChangeList
|
||||
from django.core.mail import EmailMessage
|
||||
from django.db import models
|
||||
|
||||
|
||||
class Section(models.Model):
|
||||
"""
|
||||
|
@ -419,7 +422,47 @@ class CategoryInline(admin.StackedInline):
|
|||
model = Category
|
||||
|
||||
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):
|
||||
name = models.CharField(max_length=100)
|
||||
|
@ -458,6 +501,7 @@ admin.site.register(Recommendation, RecommendationAdmin)
|
|||
admin.site.register(Recommender)
|
||||
admin.site.register(Collector, CollectorAdmin)
|
||||
admin.site.register(Category, CategoryAdmin)
|
||||
admin.site.register(Post, PostAdmin)
|
||||
admin.site.register(Gadget, GadgetAdmin)
|
||||
|
||||
# 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.util import quote
|
||||
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.html import escape
|
||||
from django.utils.translation import get_date_formats
|
||||
|
||||
# local test models
|
||||
from models import Article, BarAccount, CustomArticle, EmptyModel, \
|
||||
ExternalSubscriber, FooAccount, Gallery, ModelWithStringPrimaryKey, \
|
||||
Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, \
|
||||
Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, \
|
||||
Category
|
||||
Category, Post
|
||||
|
||||
try:
|
||||
set
|
||||
except NameError:
|
||||
from sets import Set as set
|
||||
|
||||
class AdminViewBasicTest(TestCase):
|
||||
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"
|
||||
response = self.client.get('/test_admin/jsi18n/')
|
||||
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