Skip to content
This repository

HTTPS clone URL

Subversion checkout URL

You can clone with HTTPS or Subversion.

Download ZIP
Browse code

Fixed #342 -- added readonly_fields to ModelAdmin. Thanks Alex Gaynor…

… for bootstrapping the patch.

ModelAdmin has been given a readonly_fields that allow field and calculated
values to be displayed alongside editable fields. This works on model
add/change pages and inlines.

git-svn-id: http://code.djangoproject.com/svn/django/trunk@11965 bcc190cf-cafb-0310-a4f2-bffc1f526a37
  • Loading branch information...
commit bcd9482a2019158f4580c24cd50ee8bfae9b2739 1 parent 9233d04
Brian Rosner authored December 22, 2009
136  django/contrib/admin/helpers.py
... ...
@@ -1,13 +1,18 @@
1  
-
2 1
 from django import forms
3 2
 from django.conf import settings
4  
-from django.utils.html import escape
5  
-from django.utils.safestring import mark_safe
6  
-from django.utils.encoding import force_unicode
7  
-from django.contrib.admin.util import flatten_fieldsets
  3
+from django.contrib.admin.util import flatten_fieldsets, lookup_field
  4
+from django.contrib.admin.util import display_for_field, label_for_field
8 5
 from django.contrib.contenttypes.models import ContentType
  6
+from django.core.exceptions import ObjectDoesNotExist
  7
+from django.db.models.fields import FieldDoesNotExist
  8
+from django.db.models.fields.related import ManyToManyRel
  9
+from django.forms.util import flatatt
  10
+from django.utils.encoding import force_unicode, smart_unicode
  11
+from django.utils.html import escape, conditional_escape
  12
+from django.utils.safestring import mark_safe
9 13
 from django.utils.translation import ugettext_lazy as _
10 14
 
  15
+
11 16
 ACTION_CHECKBOX_NAME = '_selected_action'
12 17
 
13 18
 class ActionForm(forms.Form):
@@ -16,16 +21,24 @@ class ActionForm(forms.Form):
16 21
 checkbox = forms.CheckboxInput({'class': 'action-select'}, lambda value: False)
17 22
 
18 23
 class AdminForm(object):
19  
-    def __init__(self, form, fieldsets, prepopulated_fields):
  24
+    def __init__(self, form, fieldsets, prepopulated_fields, readonly_fields=None, model_admin=None):
20 25
         self.form, self.fieldsets = form, normalize_fieldsets(fieldsets)
21 26
         self.prepopulated_fields = [{
22 27
             'field': form[field_name],
23 28
             'dependencies': [form[f] for f in dependencies]
24 29
         } for field_name, dependencies in prepopulated_fields.items()]
  30
+        self.model_admin = model_admin
  31
+        if readonly_fields is None:
  32
+            readonly_fields = ()
  33
+        self.readonly_fields = readonly_fields
25 34
 
26 35
     def __iter__(self):
27 36
         for name, options in self.fieldsets:
28  
-            yield Fieldset(self.form, name, **options)
  37
+            yield Fieldset(self.form, name,
  38
+                readonly_fields=self.readonly_fields,
  39
+                model_admin=self.model_admin,
  40
+                **options
  41
+            )
29 42
 
30 43
     def first_field(self):
31 44
         try:
@@ -49,11 +62,14 @@ def _media(self):
49 62
     media = property(_media)
50 63
 
51 64
 class Fieldset(object):
52  
-    def __init__(self, form, name=None, fields=(), classes=(), description=None):
  65
+    def __init__(self, form, name=None, readonly_fields=(), fields=(), classes=(),
  66
+      description=None, model_admin=None):
53 67
         self.form = form
54 68
         self.name, self.fields = name, fields
55 69
         self.classes = u' '.join(classes)
56 70
         self.description = description
  71
+        self.model_admin = model_admin
  72
+        self.readonly_fields = readonly_fields
57 73
 
58 74
     def _media(self):
59 75
         if 'collapse' in self.classes:
@@ -63,22 +79,30 @@ def _media(self):
63 79
 
64 80
     def __iter__(self):
65 81
         for field in self.fields:
66  
-            yield Fieldline(self.form, field)
  82
+            yield Fieldline(self.form, field, self.readonly_fields, model_admin=self.model_admin)
67 83
 
68 84
 class Fieldline(object):
69  
-    def __init__(self, form, field):
  85
+    def __init__(self, form, field, readonly_fields=None, model_admin=None):
70 86
         self.form = form # A django.forms.Form instance
71  
-        if isinstance(field, basestring):
  87
+        if not hasattr(field, "__iter__"):
72 88
             self.fields = [field]
73 89
         else:
74 90
             self.fields = field
  91
+        self.model_admin = model_admin
  92
+        if readonly_fields is None:
  93
+            readonly_fields = ()
  94
+        self.readonly_fields = readonly_fields
75 95
 
76 96
     def __iter__(self):
77 97
         for i, field in enumerate(self.fields):
78  
-            yield AdminField(self.form, field, is_first=(i == 0))
  98
+            if field in self.readonly_fields:
  99
+                yield AdminReadonlyField(self.form, field, is_first=(i == 0),
  100
+                    model_admin=self.model_admin)
  101
+            else:
  102
+                yield AdminField(self.form, field, is_first=(i == 0))
79 103
 
80 104
     def errors(self):
81  
-        return mark_safe(u'\n'.join([self.form[f].errors.as_ul() for f in self.fields]).strip('\n'))
  105
+        return mark_safe(u'\n'.join([self.form[f].errors.as_ul() for f in self.fields if f not in self.readonly_fields]).strip('\n'))
82 106
 
83 107
 class AdminField(object):
84 108
     def __init__(self, form, field, is_first):
@@ -100,27 +124,88 @@ def label_tag(self):
100 124
         attrs = classes and {'class': u' '.join(classes)} or {}
101 125
         return self.field.label_tag(contents=contents, attrs=attrs)
102 126
 
  127
+class AdminReadonlyField(object):
  128
+    def __init__(self, form, field, is_first, model_admin=None):
  129
+        self.field = field
  130
+        self.form = form
  131
+        self.model_admin = model_admin
  132
+        self.is_first = is_first
  133
+        self.is_checkbox = False
  134
+        self.is_readonly = True
  135
+
  136
+    def label_tag(self):
  137
+        attrs = {}
  138
+        if not self.is_first:
  139
+            attrs["class"] = "inline"
  140
+        name = forms.forms.pretty_name(
  141
+            label_for_field(self.field, self.model_admin.model, self.model_admin)
  142
+        )
  143
+        contents = force_unicode(escape(name)) + u":"
  144
+        return mark_safe('<label%(attrs)s>%(contents)s</label>' % {
  145
+            "attrs": flatatt(attrs),
  146
+            "contents": contents,
  147
+        })
  148
+
  149
+    def contents(self):
  150
+        from django.contrib.admin.templatetags.admin_list import _boolean_icon
  151
+        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
  152
+        field, obj, model_admin = self.field, self.form.instance, self.model_admin
  153
+        try:
  154
+            f, attr, value = lookup_field(field, obj, model_admin)
  155
+        except (AttributeError, ObjectDoesNotExist):
  156
+            result_repr = EMPTY_CHANGELIST_VALUE
  157
+        else:
  158
+            if f is None:
  159
+                boolean = getattr(attr, "boolean", False)
  160
+                if boolean:
  161
+                    result_repr = _boolean_icon(value)
  162
+                else:
  163
+                    result_repr = smart_unicode(value)
  164
+                    if getattr(attr, "allow_tags", False):
  165
+                        result_repr = mark_safe(result_repr)
  166
+            else:
  167
+                if value is None:
  168
+                    result_repr = EMPTY_CHANGELIST_VALUE
  169
+                elif isinstance(f.rel, ManyToManyRel):
  170
+                    result_repr = ", ".join(map(unicode, value.all()))
  171
+                else:
  172
+                    result_repr = display_for_field(value, f)
  173
+        return conditional_escape(result_repr)
  174
+
103 175
 class InlineAdminFormSet(object):
104 176
     """
105 177
     A wrapper around an inline formset for use in the admin system.
106 178
     """
107  
-    def __init__(self, inline, formset, fieldsets):
  179
+    def __init__(self, inline, formset, fieldsets, readonly_fields=None, model_admin=None):
108 180
         self.opts = inline
109 181
         self.formset = formset
110 182
         self.fieldsets = fieldsets
  183
+        self.model_admin = model_admin
  184
+        if readonly_fields is None:
  185
+            readonly_fields = ()
  186
+        self.readonly_fields = readonly_fields
111 187
 
112 188
     def __iter__(self):
113 189
         for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
114  
-            yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, original)
  190
+            yield InlineAdminForm(self.formset, form, self.fieldsets,
  191
+                self.opts.prepopulated_fields, original, self.readonly_fields,
  192
+                model_admin=self.model_admin)
115 193
         for form in self.formset.extra_forms:
116  
-            yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, None)
  194
+            yield InlineAdminForm(self.formset, form, self.fieldsets,
  195
+                self.opts.prepopulated_fields, None, self.readonly_fields,
  196
+                model_admin=self.model_admin)
117 197
 
118 198
     def fields(self):
119 199
         fk = getattr(self.formset, "fk", None)
120  
-        for field_name in flatten_fieldsets(self.fieldsets):
121  
-            if fk and fk.name == field_name:
  200
+        for i, field in enumerate(flatten_fieldsets(self.fieldsets)):
  201
+            if fk and fk.name == field:
122 202
                 continue
123  
-            yield self.formset.form.base_fields[field_name]
  203
+            if field in self.readonly_fields:
  204
+                label = label_for_field(field, self.opts.model, self.model_admin)
  205
+                yield (False, forms.forms.pretty_name(label))
  206
+            else:
  207
+                field = self.formset.form.base_fields[field]
  208
+                yield (field.widget.is_hidden, field.label)
124 209
 
125 210
     def _media(self):
126 211
         media = self.opts.media + self.formset.media
@@ -133,17 +218,21 @@ class InlineAdminForm(AdminForm):
133 218
     """
134 219
     A wrapper around an inline form for use in the admin system.
135 220
     """
136  
-    def __init__(self, formset, form, fieldsets, prepopulated_fields, original):
  221
+    def __init__(self, formset, form, fieldsets, prepopulated_fields, original,
  222
+      readonly_fields=None, model_admin=None):
137 223
         self.formset = formset
  224
+        self.model_admin = model_admin
138 225
         self.original = original
139 226
         if original is not None:
140 227
             self.original_content_type_id = ContentType.objects.get_for_model(original).pk
141 228
         self.show_url = original and hasattr(original, 'get_absolute_url')
142  
-        super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields)
  229
+        super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields,
  230
+            readonly_fields)
143 231
 
144 232
     def __iter__(self):
145 233
         for name, options in self.fieldsets:
146  
-            yield InlineFieldset(self.formset, self.form, name, **options)
  234
+            yield InlineFieldset(self.formset, self.form, name,
  235
+                self.readonly_fields, model_admin=self.model_admin, **options)
147 236
 
148 237
     def has_auto_field(self):
149 238
         if self.form._meta.model._meta.has_auto_field:
@@ -194,7 +283,8 @@ def __iter__(self):
194 283
         for field in self.fields:
195 284
             if fk and fk.name == field:
196 285
                 continue
197  
-            yield Fieldline(self.form, field)
  286
+            yield Fieldline(self.form, field, self.readonly_fields,
  287
+                model_admin=self.model_admin)
198 288
 
199 289
 class AdminErrorList(forms.util.ErrorList):
200 290
     """
2  django/contrib/admin/media/css/base.css
@@ -344,7 +344,7 @@ table.orderable-initalized .order-cell, body>tr>td.order-cell {
344 344
 
345 345
 /* FORM DEFAULTS */
346 346
 
347  
-input, textarea, select {
  347
+input, textarea, select, .form-row p {
348 348
     margin: 2px 0;
349 349
     padding: 2px 3px;
350 350
     vertical-align: middle;
36  django/contrib/admin/options.py
@@ -67,6 +67,7 @@ class BaseModelAdmin(object):
67 67
     radio_fields = {}
68 68
     prepopulated_fields = {}
69 69
     formfield_overrides = {}
  70
+    readonly_fields = ()
70 71
 
71 72
     def __init__(self):
72 73
         self.formfield_overrides = dict(FORMFIELD_FOR_DBFIELD_DEFAULTS, **self.formfield_overrides)
@@ -178,6 +179,9 @@ def _declared_fieldsets(self):
178 179
         return None
179 180
     declared_fieldsets = property(_declared_fieldsets)
180 181
 
  182
+    def get_readonly_fields(self, request, obj=None):
  183
+        return self.readonly_fields
  184
+
181 185
 class ModelAdmin(BaseModelAdmin):
182 186
     "Encapsulates all admin options and functionality for a given model."
183 187
     __metaclass__ = forms.MediaDefiningClass
@@ -327,7 +331,8 @@ def get_fieldsets(self, request, obj=None):
327 331
         if self.declared_fieldsets:
328 332
             return self.declared_fieldsets
329 333
         form = self.get_form(request, obj)
330  
-        return [(None, {'fields': form.base_fields.keys()})]
  334
+        fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
  335
+        return [(None, {'fields': fields})]
331 336
 
332 337
     def get_form(self, request, obj=None, **kwargs):
333 338
         """
@@ -342,12 +347,15 @@ def get_form(self, request, obj=None, **kwargs):
342 347
             exclude = []
343 348
         else:
344 349
             exclude = list(self.exclude)
  350
+        exclude.extend(kwargs.get("exclude", []))
  351
+        exclude.extend(self.get_readonly_fields(request, obj))
345 352
         # if exclude is an empty list we pass None to be consistant with the
346 353
         # default on modelform_factory
  354
+        exclude = exclude or None
347 355
         defaults = {
348 356
             "form": self.form,
349 357
             "fields": fields,
350  
-            "exclude": (exclude + kwargs.get("exclude", [])) or None,
  358
+            "exclude": exclude,
351 359
             "formfield_callback": curry(self.formfield_for_dbfield, request=request),
352 360
         }
353 361
         defaults.update(kwargs)
@@ -782,13 +790,17 @@ def add_view(self, request, form_url='', extra_context=None):
782 790
                                   queryset=inline.queryset(request))
783 791
                 formsets.append(formset)
784 792
 
785  
-        adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields)
  793
+        adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
  794
+            self.prepopulated_fields, self.get_readonly_fields(request),
  795
+            model_admin=self)
786 796
         media = self.media + adminForm.media
787 797
 
788 798
         inline_admin_formsets = []
789 799
         for inline, formset in zip(self.inline_instances, formsets):
790 800
             fieldsets = list(inline.get_fieldsets(request))
791  
-            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
  801
+            readonly = list(inline.get_readonly_fields(request))
  802
+            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
  803
+                fieldsets, readonly, model_admin=self)
792 804
             inline_admin_formsets.append(inline_admin_formset)
793 805
             media = media + inline_admin_formset.media
794 806
 
@@ -875,13 +887,17 @@ def change_view(self, request, object_id, extra_context=None):
875 887
                                   queryset=inline.queryset(request))
876 888
                 formsets.append(formset)
877 889
 
878  
-        adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields)
  890
+        adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
  891
+            self.prepopulated_fields, self.get_readonly_fields(request, obj),
  892
+            model_admin=self)
879 893
         media = self.media + adminForm.media
880 894
 
881 895
         inline_admin_formsets = []
882 896
         for inline, formset in zip(self.inline_instances, formsets):
883 897
             fieldsets = list(inline.get_fieldsets(request, obj))
884  
-            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
  898
+            readonly = list(inline.get_readonly_fields(request, obj))
  899
+            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
  900
+                fieldsets, readonly, model_admin=self)
885 901
             inline_admin_formsets.append(inline_admin_formset)
886 902
             media = media + inline_admin_formset.media
887 903
 
@@ -1174,14 +1190,17 @@ def get_formset(self, request, obj=None, **kwargs):
1174 1190
             exclude = []
1175 1191
         else:
1176 1192
             exclude = list(self.exclude)
  1193
+        exclude.extend(kwargs.get("exclude", []))
  1194
+        exclude.extend(self.get_readonly_fields(request, obj))
1177 1195
         # if exclude is an empty list we use None, since that's the actual
1178 1196
         # default
  1197
+        exclude = exclude or None
1179 1198
         defaults = {
1180 1199
             "form": self.form,
1181 1200
             "formset": self.formset,
1182 1201
             "fk_name": self.fk_name,
1183 1202
             "fields": fields,
1184  
-            "exclude": (exclude + kwargs.get("exclude", [])) or None,
  1203
+            "exclude": exclude,
1185 1204
             "formfield_callback": curry(self.formfield_for_dbfield, request=request),
1186 1205
             "extra": self.extra,
1187 1206
             "max_num": self.max_num,
@@ -1193,7 +1212,8 @@ def get_fieldsets(self, request, obj=None):
1193 1212
         if self.declared_fieldsets:
1194 1213
             return self.declared_fieldsets
1195 1214
         form = self.get_formset(request).form
1196  
-        return [(None, {'fields': form.base_fields.keys()})]
  1215
+        fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
  1216
+        return [(None, {'fields': fields})]
1197 1217
 
1198 1218
     def queryset(self, request):
1199 1219
         return self.model._default_manager.all()
16  django/contrib/admin/templates/admin/edit_inline/tabular.html
@@ -7,10 +7,10 @@
7 7
    {{ inline_admin_formset.formset.non_form_errors }}
8 8
    <table>
9 9
      <thead><tr>
10  
-     {% for field in inline_admin_formset.fields %}
11  
-       {% if not field.is_hidden %}
12  
-         <th {% if forloop.first %}colspan="2"{% endif %}>{{ field.label|capfirst }}</th>
13  
-        {% endif %}
  10
+     {% for is_hidden, label in inline_admin_formset.fields %}
  11
+       {% if not is_hidden %}
  12
+         <th {% if forloop.first %}colspan="2"{% endif %}>{{ label|capfirst }}</th>
  13
+       {% endif %}
14 14
      {% endfor %}
15 15
      {% if inline_admin_formset.formset.can_delete %}<th>{% trans "Delete?" %}</th>{% endif %}
16 16
      </tr></thead>
@@ -44,8 +44,12 @@
44 44
           {% for line in fieldset %}
45 45
             {% for field in line %}
46 46
               <td class="{{ field.field.name }}">
47  
-              {{ field.field.errors.as_ul }}
48  
-              {{ field.field }}
  47
+              {% if field.is_readonly %}
  48
+                  <p>{{ field.contents }}</p>
  49
+              {% else %}
  50
+                  {{ field.field.errors.as_ul }}
  51
+                  {{ field.field }}
  52
+              {% endif %}
49 53
               </td>
50 54
             {% endfor %}
51 55
           {% endfor %}
43  django/contrib/admin/templates/admin/includes/fieldset.html
... ...
@@ -1,19 +1,28 @@
1 1
 <fieldset class="module aligned {{ fieldset.classes }}">
2  
-  {% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %}
3  
-  {% if fieldset.description %}<div class="description">{{ fieldset.description|safe }}</div>{% endif %}
4  
-  {% for line in fieldset %}
5  
-      <div class="form-row{% if line.errors %} errors{% endif %} {% for field in line %}{{ field.field.name }} {% endfor %} ">
6  
-      {{ line.errors }}
7  
-      {% for field in line %}
8  
-      <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
9  
-          {% if field.is_checkbox %}
10  
-              {{ field.field }}{{ field.label_tag }}
11  
-          {% else %}
12  
-              {{ field.label_tag }}{{ field.field }}
13  
-          {% endif %}
14  
-          {% if field.field.field.help_text %}<p class="help">{{ field.field.field.help_text|safe }}</p>{% endif %}
15  
-      </div>
16  
-      {% endfor %}
17  
-      </div>
18  
-  {% endfor %}
  2
+    {% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %}
  3
+    {% if fieldset.description %}
  4
+        <div class="description">{{ fieldset.description|safe }}</div>
  5
+    {% endif %}
  6
+    {% for line in fieldset %}
  7
+        <div class="form-row{% if line.errors %} errors{% endif %} {% for field in line %}{{ field.field.name }} {% endfor %} ">
  8
+            {{ line.errors }}
  9
+            {% for field in line %}
  10
+                <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
  11
+                    {% if field.is_checkbox %}
  12
+                        {{ field.field }}{{ field.label_tag }}
  13
+                    {% else %}
  14
+                        {{ field.label_tag }}
  15
+                        {% if field.is_readonly %}
  16
+                            <p>{{ field.contents }}</p>
  17
+                        {% else %}
  18
+                            {{ field.field }}
  19
+                        {% endif %}
  20
+                    {% endif %}
  21
+                    {% if field.field.field.help_text %}
  22
+                        <p class="help">{{ field.field.field.help_text|safe }}</p>
  23
+                    {% endif %}
  24
+                </div>
  25
+            {% endfor %}
  26
+        </div>
  27
+    {% endfor %}
19 28
 </fieldset>
120  django/contrib/admin/templatetags/admin_list.py
... ...
@@ -1,16 +1,20 @@
  1
+import datetime
  2
+
1 3
 from django.conf import settings
  4
+from django.contrib.admin.util import lookup_field, display_for_field, label_for_field
2 5
 from django.contrib.admin.views.main import ALL_VAR, EMPTY_CHANGELIST_VALUE
3 6
 from django.contrib.admin.views.main import ORDER_VAR, ORDER_TYPE_VAR, PAGE_VAR, SEARCH_VAR
4 7
 from django.core.exceptions import ObjectDoesNotExist
5 8
 from django.db import models
  9
+from django.forms.forms import pretty_name
6 10
 from django.utils import formats
7 11
 from django.utils.html import escape, conditional_escape
8  
-from django.utils.text import capfirst
9 12
 from django.utils.safestring import mark_safe
  13
+from django.utils.text import capfirst
10 14
 from django.utils.translation import ugettext as _
11  
-from django.utils.encoding import smart_unicode, smart_str, force_unicode
  15
+from django.utils.encoding import smart_unicode, force_unicode
12 16
 from django.template import Library
13  
-import datetime
  17
+
14 18
 
15 19
 register = Library()
16 20
 
@@ -76,41 +80,15 @@ def result_headers(cl):
76 80
         try:
77 81
             f = lookup_opts.get_field(field_name)
78 82
             admin_order_field = None
  83
+            header = f.verbose_name
79 84
         except models.FieldDoesNotExist:
80  
-            # For non-field list_display values, check for the function
81  
-            # attribute "short_description". If that doesn't exist, fall back
82  
-            # to the method name. And __str__ and __unicode__ are special-cases.
83  
-            if field_name == '__unicode__':
84  
-                header = force_unicode(lookup_opts.verbose_name)
85  
-            elif field_name == '__str__':
86  
-                header = smart_str(lookup_opts.verbose_name)
87  
-            else:
88  
-                if callable(field_name):
89  
-                    attr = field_name # field_name can be a callable
90  
-                else:
91  
-                    try:
92  
-                        attr = getattr(cl.model_admin, field_name)
93  
-                    except AttributeError:
94  
-                        try:
95  
-                            attr = getattr(cl.model, field_name)
96  
-                        except AttributeError:
97  
-                            raise AttributeError, \
98  
-                                "'%s' model or '%s' objects have no attribute '%s'" % \
99  
-                                    (lookup_opts.object_name, cl.model_admin.__class__, field_name)
100  
-
101  
-                try:
102  
-                    header = attr.short_description
103  
-                except AttributeError:
104  
-                    if callable(field_name):
105  
-                        header = field_name.__name__
106  
-                    else:
107  
-                        header = field_name
108  
-                    header = header.replace('_', ' ')
  85
+            header = label_for_field(field_name, cl.model, cl.model_admin)
109 86
             # if the field is the action checkbox: no sorting and special class
110 87
             if field_name == 'action_checkbox':
111 88
                 yield {"text": header,
112 89
                        "class_attrib": mark_safe(' class="action-checkbox-column"')}
113 90
                 continue
  91
+            header = pretty_name(header)
114 92
 
115 93
             # It is a non-field, but perhaps one that is sortable
116 94
             admin_order_field = getattr(attr, "admin_order_field", None)
@@ -120,8 +98,6 @@ def result_headers(cl):
120 98
 
121 99
             # So this _is_ a sortable non-field.  Go to the yield
122 100
             # after the else clause.
123  
-        else:
124  
-            header = f.verbose_name
125 101
 
126 102
         th_classes = []
127 103
         new_order_type = 'asc'
@@ -129,10 +105,12 @@ def result_headers(cl):
129 105
             th_classes.append('sorted %sending' % cl.order_type.lower())
130 106
             new_order_type = {'asc': 'desc', 'desc': 'asc'}[cl.order_type.lower()]
131 107
 
132  
-        yield {"text": header,
133  
-               "sortable": True,
134  
-               "url": cl.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
135  
-               "class_attrib": mark_safe(th_classes and ' class="%s"' % ' '.join(th_classes) or '')}
  108
+        yield {
  109
+            "text": header,
  110
+            "sortable": True,
  111
+            "url": cl.get_query_string({ORDER_VAR: i, ORDER_TYPE_VAR: new_order_type}),
  112
+            "class_attrib": mark_safe(th_classes and ' class="%s"' % ' '.join(th_classes) or '')
  113
+        }
136 114
 
137 115
 def _boolean_icon(field_val):
138 116
     BOOLEAN_MAPPING = {True: 'yes', False: 'no', None: 'unknown'}
@@ -144,24 +122,11 @@ def items_for_result(cl, result, form):
144 122
     for field_name in cl.list_display:
145 123
         row_class = ''
146 124
         try:
147  
-            f = cl.lookup_opts.get_field(field_name)
148  
-        except models.FieldDoesNotExist:
149  
-            # For non-field list_display values, the value is either a method,
150  
-            # property or returned via a callable.
151  
-            try:
152  
-                if callable(field_name):
153  
-                    attr = field_name
154  
-                    value = attr(result)
155  
-                elif hasattr(cl.model_admin, field_name) and \
156  
-                   not field_name == '__str__' and not field_name == '__unicode__':
157  
-                    attr = getattr(cl.model_admin, field_name)
158  
-                    value = attr(result)
159  
-                else:
160  
-                    attr = getattr(result, field_name)
161  
-                    if callable(attr):
162  
-                        value = attr()
163  
-                    else:
164  
-                        value = attr
  125
+            f, attr, value = lookup_field(field_name, result, cl.model_admin)
  126
+        except (AttributeError, ObjectDoesNotExist):
  127
+            result_repr = EMPTY_CHANGELIST_VALUE
  128
+        else:
  129
+            if f is None:
165 130
                 allow_tags = getattr(attr, 'allow_tags', False)
166 131
                 boolean = getattr(attr, 'boolean', False)
167 132
                 if boolean:
@@ -169,50 +134,21 @@ def items_for_result(cl, result, form):
169 134
                     result_repr = _boolean_icon(value)
170 135
                 else:
171 136
                     result_repr = smart_unicode(value)
172  
-            except (AttributeError, ObjectDoesNotExist):
173  
-                result_repr = EMPTY_CHANGELIST_VALUE
174  
-            else:
175 137
                 # Strip HTML tags in the resulting text, except if the
176 138
                 # function has an "allow_tags" attribute set to True.
177 139
                 if not allow_tags:
178 140
                     result_repr = escape(result_repr)
179 141
                 else:
180 142
                     result_repr = mark_safe(result_repr)
181  
-        else:
182  
-            field_val = getattr(result, f.attname)
183  
-
184  
-            if isinstance(f.rel, models.ManyToOneRel):
185  
-                if field_val is not None:
186  
-                    result_repr = escape(getattr(result, f.name))
187  
-                else:
188  
-                    result_repr = EMPTY_CHANGELIST_VALUE
189  
-            # Dates and times are special: They're formatted in a certain way.
190  
-            elif isinstance(f, models.DateField) or isinstance(f, models.TimeField):
191  
-                if field_val:
192  
-                    result_repr = formats.localize(field_val)
193  
-                else:
194  
-                    result_repr = EMPTY_CHANGELIST_VALUE
195  
-            elif isinstance(f, models.DecimalField):
196  
-                if field_val:
197  
-                    result_repr = formats.number_format(field_val, f.decimal_places)
198  
-                else:
  143
+            else:
  144
+                if value is None:
199 145
                     result_repr = EMPTY_CHANGELIST_VALUE
200  
-                row_class = ' class="nowrap"'
201  
-            elif isinstance(f, models.FloatField):
202  
-                if field_val:
203  
-                    result_repr = formats.number_format(field_val)
  146
+                if isinstance(f.rel, models.ManyToOneRel):
  147
+                    result_repr = escape(getattr(result, f.name))
204 148
                 else:
205  
-                    result_repr = EMPTY_CHANGELIST_VALUE
206  
-                row_class = ' class="nowrap"'
207  
-            # Booleans are special: We use images.
208  
-            elif isinstance(f, models.BooleanField) or isinstance(f, models.NullBooleanField):
209  
-                result_repr = _boolean_icon(field_val)
210  
-            # Fields with choices are special: Use the representation
211  
-            # of the choice.
212  
-            elif f.flatchoices:
213  
-                result_repr = dict(f.flatchoices).get(field_val, EMPTY_CHANGELIST_VALUE)
214  
-            else:
215  
-                result_repr = escape(field_val)
  149
+                    result_repr = display_for_field(value, f)
  150
+                if isinstance(f, models.DateField) or isinstance(f, models.TimeField):
  151
+                    row_class = ' class="nowrap"'
216 152
         if force_unicode(result_repr) == '':
217 153
             result_repr = mark_safe('&nbsp;')
218 154
         # If list_display_links not defined, add the link tag to the first field
75  django/contrib/admin/util.py
... ...
@@ -1,12 +1,14 @@
1 1
 from django.core.exceptions import ObjectDoesNotExist
2 2
 from django.db import models
  3
+from django.utils import formats
3 4
 from django.utils.html import escape
4 5
 from django.utils.safestring import mark_safe
5 6
 from django.utils.text import capfirst
6  
-from django.utils.encoding import force_unicode
  7
+from django.utils.encoding import force_unicode, smart_unicode, smart_str
7 8
 from django.utils.translation import ungettext, ugettext as _
8 9
 from django.core.urlresolvers import reverse, NoReverseMatch
9 10
 
  11
+
10 12
 def quote(s):
11 13
     """
12 14
     Ensure that primary key values do not confuse the admin URLs by escaping
@@ -221,3 +223,74 @@ def model_ngettext(obj, n=None):
221 223
     d = model_format_dict(obj)
222 224
     singular, plural = d["verbose_name"], d["verbose_name_plural"]
223 225
     return ungettext(singular, plural, n or 0)
  226
+
  227
+def lookup_field(name, obj, model_admin=None):
  228
+    opts = obj._meta
  229
+    try:
  230
+        f = opts.get_field(name)
  231
+    except models.FieldDoesNotExist:
  232
+        # For non-field values, the value is either a method, property or
  233
+        # returned via a callable.
  234
+        if callable(name):
  235
+            attr = name
  236
+            value = attr(obj)
  237
+        elif (model_admin is not None and hasattr(model_admin, name) and
  238
+          not name == '__str__' and not name == '__unicode__'):
  239
+            attr = getattr(model_admin, name)
  240
+            value = attr(obj)
  241
+        else:
  242
+            attr = getattr(obj, name)
  243
+            if callable(attr):
  244
+                value = attr()
  245
+            else:
  246
+                value = attr
  247
+        f = None
  248
+    else:
  249
+        attr = None
  250
+        value = getattr(obj, f.attname)
  251
+    return f, attr, value
  252
+
  253
+def label_for_field(name, model, model_admin):
  254
+    try:
  255
+        model._meta.get_field_by_name(name)[0]
  256
+        return name
  257
+    except models.FieldDoesNotExist:
  258
+        if name == "__unicode__":
  259
+            return force_unicode(model._meta.verbose_name)
  260
+        if name == "__str__":
  261
+            return smart_str(model._meta.verbose_name)
  262
+        if callable(name):
  263
+            attr = name
  264
+        elif hasattr(model_admin, name):
  265
+            attr = getattr(model_admin, name)
  266
+        elif hasattr(model, name):
  267
+            attr = getattr(model, name)
  268
+        else:
  269
+            raise AttributeError
  270
+
  271
+        if hasattr(attr, "short_description"):
  272
+            return attr.short_description
  273
+        elif callable(attr):
  274
+            if attr.__name__ == "<lambda>":
  275
+                return "--"
  276
+            else:
  277
+                return attr.__name__
  278
+        else:
  279
+            return name
  280
+
  281
+
  282
+def display_for_field(value, field):
  283
+    from django.contrib.admin.templatetags.admin_list import _boolean_icon
  284
+    from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
  285
+    if isinstance(field, models.DateField) or isinstance(field, models.TimeField):
  286
+        return formats.localize(value)
  287
+    elif isinstance(field, models.BooleanField) or isinstance(field, models.NullBooleanField):
  288
+        return _boolean_icon(value)
  289
+    elif isinstance(field, models.DecimalField):
  290
+        return formats.number_format(value, field.decimal_places)
  291
+    elif isinstance(field, models.FloatField):
  292
+        return formats.number_format(value)
  293
+    elif field.flatchoices:
  294
+        return dict(field.flatchoices).get(value, EMPTY_CHANGELIST_VALUE)
  295
+    else:
  296
+        return smart_unicode(value)
27  django/contrib/admin/validation.py
... ...
@@ -1,13 +1,11 @@
1  
-try:
2  
-    set
3  
-except NameError:
4  
-    from sets import Set as set   # Python 2.3 fallback
5  
-
6 1
 from django.core.exceptions import ImproperlyConfigured
7 2
 from django.db import models
8  
-from django.forms.models import BaseModelForm, BaseModelFormSet, fields_for_model, _get_foreign_key
  3
+from django.forms.models import (BaseModelForm, BaseModelFormSet, fields_for_model,
  4
+    _get_foreign_key)
9 5
 from django.contrib.admin.options import flatten_fieldsets, BaseModelAdmin
10 6
 from django.contrib.admin.options import HORIZONTAL, VERTICAL
  7
+from django.contrib.admin.util import lookup_field
  8
+
11 9
 
12 10
 __all__ = ['validate']
13 11
 
@@ -123,6 +121,18 @@ def validate(cls, model):
123 121
                 continue
124 122
             get_field(cls, model, opts, 'ordering[%d]' % idx, field)
125 123
 
  124
+    if hasattr(cls, "readonly_fields"):
  125
+        check_isseq(cls, "readonly_fields", cls.readonly_fields)
  126
+        for idx, field in enumerate(cls.readonly_fields):
  127
+            if not callable(field):
  128
+                if not hasattr(cls, field):
  129
+                    if not hasattr(model, field):
  130
+                        try:
  131
+                            opts.get_field(field)
  132
+                        except models.FieldDoesNotExist:
  133
+                            raise ImproperlyConfigured("%s.readonly_fields[%d], %r is not a callable or an attribute of %r or found in the model %r."
  134
+                                % (cls.__name__, idx, field, cls.__name__, model._meta.object_name))
  135
+
126 136
     # list_select_related = False
127 137
     # save_as = False
128 138
     # save_on_top = False
@@ -195,6 +205,11 @@ def validate_base(cls, model):
195 205
     if cls.fields: # default value is None
196 206
         check_isseq(cls, 'fields', cls.fields)
197 207
         for field in cls.fields:
  208
+            if field in cls.readonly_fields:
  209
+                # Stuff can be put in fields that isn't actually a model field
  210
+                # if it's in readonly_fields, readonly_fields will handle the
  211
+                # validation of such things.
  212
+                continue
198 213
             check_formfield(cls, model, opts, 'fields', field)
199 214
             f = get_field(cls, model, opts, 'fields', field)
200 215
             if isinstance(f, models.ManyToManyField) and not f.rel.through._meta.auto_created:
34  docs/ref/contrib/admin/index.txt
@@ -172,6 +172,11 @@ The ``field_options`` dictionary can have the following keys:
172 172
             'fields': (('first_name', 'last_name'), 'address', 'city', 'state'),
173 173
             }
174 174
 
  175
+        .. versionadded:: 1.2
  176
+
  177
+        ``fields`` can contain values defined in
  178
+        :attr:`ModelAdmin.readonly_fields` to be displayed as read-only.
  179
+
175 180
     * ``classes``
176 181
         A list containing extra CSS classes to apply to the fieldset.
177 182
 
@@ -210,6 +215,11 @@ the ``django.contrib.flatpages.FlatPage`` model as follows::
210 215
 In the above example, only the fields 'url', 'title' and 'content' will be
211 216
 displayed, sequentially, in the form.
212 217
 
  218
+.. versionadded:: 1.2
  219
+
  220
+``fields`` can contain values defined in :attr:`ModelAdmin.readonly_fields`
  221
+to be displayed as read-only.
  222
+
213 223
 .. admonition:: Note
214 224
 
215 225
     This ``fields`` option should not be confused with the ``fields``
@@ -540,6 +550,21 @@ into a ``Input`` widget for either a ``ForeignKey`` or ``ManyToManyField``::
540 550
     class ArticleAdmin(admin.ModelAdmin):
541 551
         raw_id_fields = ("newspaper",)
542 552
 
  553
+.. attribute:: ModelAdmin.readonly_fields
  554
+
  555
+.. versionadded:: 1.2
  556
+
  557
+By default the admin shows all fields as editable. Any fields in this option
  558
+(which should be a ``list`` or ``tuple``) will display its data as-is and
  559
+non-editable. This option behaves nearly identical to :attr:`ModelAdmin.list_display`.
  560
+Usage is the same, however, when you specify :attr:`ModelAdmin.fields` or
  561
+:attr:`ModelAdmin.fieldsets` the read-only fields must be present to be shown
  562
+(they are ignored otherwise).
  563
+
  564
+If ``readonly_fields`` is used without defining explicit ordering through
  565
+:attr:`ModelAdmin.fields` or :attr:`ModelAdmin.fieldsets` they will be added
  566
+last after all editable fields.
  567
+
543 568
 .. attribute:: ModelAdmin.save_as
544 569
 
545 570
 Set ``save_as`` to enable a "save as" feature on admin change forms.
@@ -744,6 +769,15 @@ model instance::
744 769
                 instance.save()
745 770
             formset.save_m2m()
746 771
 
  772
+.. method:: ModelAdmin.get_readonly_fields(self, request, obj=None)
  773
+
  774
+.. versionadded:: 1.2
  775
+
  776
+The ``get_readonly_fields`` method is given the ``HttpRequest`` and the
  777
+``obj`` being edited (or ``None`` on an add form) and is expected to return a
  778
+``list`` or ``tuple`` of field names that will be displayed as read-only, as
  779
+described above in the :attr:`ModelAdmin.readonly_fields` section.
  780
+
747 781
 .. method:: ModelAdmin.get_urls(self)
748 782
 
749 783
 .. versionadded:: 1.1
7  docs/releases/1.2.txt
@@ -485,3 +485,10 @@ enabled, dates and numbers on templates will be displayed using the format
485 485
 specified for the current locale. Django will also use localized formats
486 486
 when parsing data in forms.
487 487
 See :ref:`Format localization <format-localization>` for more details.
  488
+
  489
+Added ``readonly_fields`` to ``ModelAdmin``
  490
+-------------------------------------------
  491
+
  492
+:attr:`django.contrib.admin.ModelAdmin.readonly_fields` has been added to
  493
+enable non-editable fields in add/change pages for models and inlines. Field
  494
+and calculated values can be displayed along side editable fields.
61  tests/regressiontests/admin_validation/models.py
@@ -19,6 +19,10 @@ class Meta:
19 19
     def __unicode__(self):
20 20
         return self.title
21 21
 
  22
+    def readonly_method_on_model(self):
  23
+        # does nothing
  24
+        pass
  25
+
22 26
 
23 27
 class TwoAlbumFKAndAnE(models.Model):
24 28
     album1 = models.ForeignKey(Album, related_name="album1_set")
@@ -110,6 +114,63 @@ class AuthorsBooks(models.Model):
110 114
 
111 115
 >>> validate_inline(TwoAlbumFKAndAnEInline, None, Album)
112 116
 
  117
+>>> class SongAdmin(admin.ModelAdmin):
  118
+...     readonly_fields = ("title",)
  119
+
  120
+>>> validate(SongAdmin, Song)
  121
+
  122
+>>> def my_function(obj):
  123
+...     # does nothing
  124
+...     pass
  125
+>>> class SongAdmin(admin.ModelAdmin):
  126
+...     readonly_fields = (my_function,)
  127
+
  128
+>>> validate(SongAdmin, Song)
  129
+
  130
+>>> class SongAdmin(admin.ModelAdmin):
  131
+...     readonly_fields = ("readonly_method_on_modeladmin",)
  132
+...
  133
+...     def readonly_method_on_modeladmin(self, obj):
  134
+...         # does nothing
  135
+...         pass
  136
+
  137
+>>> validate(SongAdmin, Song)
  138
+
  139
+>>> class SongAdmin(admin.ModelAdmin):
  140
+...     readonly_fields = ("readonly_method_on_model",)
  141
+
  142
+>>> validate(SongAdmin, Song)
  143
+
  144
+>>> class SongAdmin(admin.ModelAdmin):
  145
+...     readonly_fields = ("title", "nonexistant")
  146
+
  147
+>>> validate(SongAdmin, Song)
  148
+Traceback (most recent call last):
  149
+    ...
  150
+ImproperlyConfigured: SongAdmin.readonly_fields[1], 'nonexistant' is not a callable or an attribute of 'SongAdmin' or found in the model 'Song'.
  151
+
  152
+>>> class SongAdmin(admin.ModelAdmin):
  153
+...     readonly_fields = ("title", "awesome_song")
  154
+...     fields = ("album", "title", "awesome_song")
  155
+
  156
+>>> validate(SongAdmin, Song)
  157
+Traceback (most recent call last):
  158
+    ...
  159
+ImproperlyConfigured: SongAdmin.readonly_fields[1], 'awesome_song' is not a callable or an attribute of 'SongAdmin' or found in the model 'Song'.
  160
+
  161
+>>> class SongAdmin(SongAdmin):
  162
+...     def awesome_song(self, instance):
  163
+...         if instance.title == "Born to Run":
  164
+...             return "Best Ever!"
  165
+...         return "Status unknown."
  166
+
  167
+>>> validate(SongAdmin, Song)
  168
+
  169
+>>> class SongAdmin(admin.ModelAdmin):
  170
+...     readonly_fields = (lambda obj: "test",)
  171
+
  172
+>>> validate(SongAdmin, Song)
  173
+
113 174
 # Regression test for #12203/#12237 - Fail more gracefully when a M2M field that
114 175
 # specifies the 'through' option is included in the 'fields' or the 'fieldsets'
115 176
 # ModelAdmin options.
50  tests/regressiontests/admin_views/models.py
... ...
@@ -1,11 +1,14 @@
1 1
 # -*- coding: utf-8 -*-
  2
+import datetime
2 3
 import tempfile
3 4
 import os
4  
-from django.core.files.storage import FileSystemStorage
5  
-from django.db import models
  5
+
6 6
 from django.contrib import admin
  7
+from django.core.files.storage import FileSystemStorage
7 8
 from django.contrib.admin.views.main import ChangeList
8 9
 from django.core.mail import EmailMessage
  10
+from django.db import models
  11
+
9 12
 
10 13
 class Section(models.Model):
11 14
     """
@@ -419,7 +422,47 @@ class CategoryInline(admin.StackedInline):
419 422
     model = Category
420 423
 
421 424
 class CollectorAdmin(admin.ModelAdmin):
422  
-    inlines = [WidgetInline, DooHickeyInline, GrommetInline, WhatsitInline, FancyDoodadInline, CategoryInline]
  425
+    inlines = [
  426
+        WidgetInline, DooHickeyInline, GrommetInline, WhatsitInline,
  427
+        FancyDoodadInline, CategoryInline
  428
+    ]
  429
+
  430
+class Link(models.Model):
  431
+    posted = models.DateField(
  432
+        default=lambda: datetime.date.today() - datetime.timedelta(days=7)
  433
+    )
  434
+    url = models.URLField()
  435
+    post = models.ForeignKey("Post")
  436
+
  437
+
  438
+class LinkInline(admin.TabularInline):
  439
+    model = Link
  440
+    extra = 1
  441
+
  442
+    readonly_fields = ("posted",)
  443
+
  444
+
  445
+class Post(models.Model):
  446
+    title = models.CharField(max_length=100)
  447
+    content = models.TextField()
  448
+    posted = models.DateField(default=datetime.date.today)
  449
+
  450
+    def awesomeness_level(self):
  451
+        return "Very awesome."
  452
+
  453
+class PostAdmin(admin.ModelAdmin):
  454
+    readonly_fields = ('posted', 'awesomeness_level', 'coolness', lambda obj: "foo")
  455
+
  456
+    inlines = [
  457
+        LinkInline
  458
+    ]
  459
+
  460
+    def coolness(self, instance):
  461
+        if instance.pk:
  462
+            return "%d amount of cool." % instance.pk
  463
+        else:
  464
+            return "Unkown coolness."
  465
+
423 466
 
424 467
 class Gadget(models.Model):
425 468
     name = models.CharField(max_length=100)
@@ -458,6 +501,7 @@ def get_changelist(self, request, **kwargs):
458 501
 admin.site.register(Recommender)
459 502
 admin.site.register(Collector, CollectorAdmin)
460 503
 admin.site.register(Category, CategoryAdmin)
  504
+admin.site.register(Post, PostAdmin)
461 505
 admin.site.register(Gadget, GadgetAdmin)
462 506
 
463 507
 # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
59  tests/regressiontests/admin_views/tests.py
@@ -10,20 +10,18 @@
10 10
 from django.contrib.admin.sites import LOGIN_FORM_KEY
11 11
 from django.contrib.admin.util import quote
12 12
 from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
  13
+from django.utils import formats
13 14
 from django.utils.cache import get_max_age
14 15
 from django.utils.html import escape
  16
+from django.utils.translation import get_date_formats
15 17
 
16 18
 # local test models
17 19
 from models import Article, BarAccount, CustomArticle, EmptyModel, \
18 20
     ExternalSubscriber, FooAccount, Gallery, ModelWithStringPrimaryKey, \
19 21
     Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, \
20 22
     Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, \
21  
-    Category
  23
+    Category, Post
22 24
 
23  
-try:
24  
-    set
25  
-except NameError:
26  
-    from sets import Set as set
27 25
 
28 26
 class AdminViewBasicTest(TestCase):
29 27
     fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', 'admin-views-fabrics.xml']
@@ -1688,3 +1686,54 @@ def testJsi18n(self):
1688 1686
         "Check the never-cache status of the Javascript i18n view"
1689 1687
         response = self.client.get('/test_admin/jsi18n/')
1690 1688
         self.failUnlessEqual(get_max_age(response), None)
  1689
+
  1690
+
  1691
+class ReadonlyTest(TestCase):
  1692
+    fixtures = ['admin-views-users.xml']
  1693
+
  1694
+    def setUp(self):
  1695
+        self.client.login(username='super', password='secret')
  1696
+
  1697
+    def tearDown(self):
  1698
+        self.client.logout()
  1699
+
  1700
+    def test_readonly_get(self):
  1701
+        response = self.client.get('/test_admin/admin/admin_views/post/add/')
  1702
+        self.assertEqual(response.status_code, 200)
  1703
+        self.assertNotContains(response, 'name="posted"')
  1704
+        # 3 fields + 2 submit buttons + 2 inline management form fields, + 2
  1705
+        # hidden fields for inlines + 1 field for the inline
  1706
+        self.assertEqual(response.content.count("input"), 10)
  1707
+        self.assertContains(response, formats.localize(datetime.date.today()))
  1708
+        self.assertContains(response,
  1709
+            "<label>Awesomeness level:</label>")
  1710
+        self.assertContains(response, "Very awesome.")
  1711
+        self.assertContains(response, "Unkown coolness.")
  1712
+        self.assertContains(response, "foo")
  1713
+        self.assertContains(response,
  1714
+            formats.localize(datetime.date.today() - datetime.timedelta(days=7))
  1715
+        )
  1716
+
  1717
+        p = Post.objects.create(title="I worked on readonly_fields", content="Its good stuff")
  1718
+        response = self.client.get('/test_admin/admin/admin_views/post/%d/' % p.pk)
  1719
+        self.assertContains(response, "%d amount of cool" % p.pk)
  1720
+
  1721
+    def test_readonly_post(self):
  1722
+        data = {
  1723
+            "title": "Django Got Readonly Fields",
  1724
+            "content": "This is an incredible development.",
  1725
+            "link_set-TOTAL_FORMS": "1",
  1726
+            "link_set-INITIAL_FORMS": "0",
  1727
+        }
  1728
+        response = self.client.post('/test_admin/admin/admin_views/post/add/', data)
  1729
+        self.assertEqual(response.status_code, 302)
  1730
+        self.assertEqual(Post.objects.count(), 1)
  1731
+        p = Post.objects.get()
  1732
+        self.assertEqual(p.posted, datetime.date.today())
  1733
+
  1734
+        data["posted"] = "10-8-1990" # some date that's not today
  1735
+        response = self.client.post('/test_admin/admin/admin_views/post/add/', data)
  1736
+        self.assertEqual(response.status_code, 302)
  1737
+        self.assertEqual(Post.objects.count(), 2)
  1738
+        p = Post.objects.order_by('-id')[0]
  1739
+        self.assertEqual(p.posted, datetime.date.today())

0 notes on commit bcd9482

Please sign in to comment.
Something went wrong with that request. Please try again.