Code

Ticket #342: readonly-admin.7.diff

File readonly-admin.7.diff, 24.9 KB (added by Alex, 5 years ago)

Pretty printing

Line 
1diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
2index 40437c0..8d93b46 100644
3--- a/django/contrib/admin/helpers.py
4+++ b/django/contrib/admin/helpers.py
5@@ -1,12 +1,18 @@
6-
7 from django import forms
8 from django.conf import settings
9-from django.utils.html import escape
10-from django.utils.safestring import mark_safe
11-from django.utils.encoding import force_unicode
12 from django.contrib.admin.util import flatten_fieldsets
13 from django.contrib.contenttypes.models import ContentType
14-from django.utils.translation import ugettext_lazy as _
15+from django.db import models
16+from django.db.models.fields import FieldDoesNotExist
17+from django.db.models.fields.related import ManyToManyRel
18+from django.forms.util import flatatt
19+from django.utils import dateformat
20+from django.utils.encoding import force_unicode, smart_unicode
21+from django.utils.html import escape
22+from django.utils.safestring import mark_safe
23+from django.utils.text import capfirst
24+from django.utils.translation import get_date_formats, ugettext_lazy as _
25+
26 
27 ACTION_CHECKBOX_NAME = '_selected_action'
28 
29@@ -16,16 +22,18 @@ class ActionForm(forms.Form):
30 checkbox = forms.CheckboxInput({'class': 'action-select'}, lambda value: False)
31 
32 class AdminForm(object):
33-    def __init__(self, form, fieldsets, prepopulated_fields):
34+    def __init__(self, form, fieldsets, prepopulated_fields, readonly_fields):
35         self.form, self.fieldsets = form, normalize_fieldsets(fieldsets)
36         self.prepopulated_fields = [{
37             'field': form[field_name],
38             'dependencies': [form[f] for f in dependencies]
39         } for field_name, dependencies in prepopulated_fields.items()]
40+        self.readonly_fields = readonly_fields
41 
42     def __iter__(self):
43         for name, options in self.fieldsets:
44-            yield Fieldset(self.form, name, **options)
45+            yield Fieldset(self.form, name,
46+                readonly_fields=self.readonly_fields, **options)
47 
48     def first_field(self):
49         try:
50@@ -49,11 +57,13 @@ class AdminForm(object):
51     media = property(_media)
52 
53 class Fieldset(object):
54-    def __init__(self, form, name=None, fields=(), classes=(), description=None):
55+    def __init__(self, form, name=None, readonly_fields=(), fields=(), classes=(),
56+        description=None):
57         self.form = form
58         self.name, self.fields = name, fields
59         self.classes = u' '.join(classes)
60         self.description = description
61+        self.readonly_fields = readonly_fields
62 
63     def _media(self):
64         if 'collapse' in self.classes:
65@@ -63,22 +73,26 @@ class Fieldset(object):
66 
67     def __iter__(self):
68         for field in self.fields:
69-            yield Fieldline(self.form, field)
70+            yield Fieldline(self.form, field, self.readonly_fields)
71 
72 class Fieldline(object):
73-    def __init__(self, form, field):
74+    def __init__(self, form, field, readonly_fields):
75         self.form = form # A django.forms.Form instance
76         if isinstance(field, basestring):
77             self.fields = [field]
78         else:
79             self.fields = field
80+        self.readonly_fields = readonly_fields
81 
82     def __iter__(self):
83         for i, field in enumerate(self.fields):
84-            yield AdminField(self.form, field, is_first=(i == 0))
85+            if field in self.readonly_fields:
86+                yield AdminReadonlyField(self.form, field, is_first=(i == 0))
87+            else:
88+                yield AdminField(self.form, field, is_first=(i == 0))
89 
90     def errors(self):
91-        return mark_safe(u'\n'.join([self.form[f].errors.as_ul() for f in self.fields]).strip('\n'))
92+        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'))
93 
94 class AdminField(object):
95     def __init__(self, form, field, is_first):
96@@ -100,20 +114,87 @@ class AdminField(object):
97         attrs = classes and {'class': u' '.join(classes)} or {}
98         return self.field.label_tag(contents=contents, attrs=attrs)
99 
100+class AdminReadonlyField(object):
101+    def __init__(self, form, field, is_first):
102+        self.field = field
103+        self.form = form
104+        self.is_first = is_first
105+        self.is_checkbox = False
106+        self.is_readonly = True
107+   
108+    def label_tag(self):
109+        attrs = {}
110+        if not self.is_first:
111+            attrs["class"] = "inline"
112+        contents = force_unicode(escape(forms.forms.pretty_name(self.field))) + u":"
113+        return mark_safe('<label %(attrs)s>%(contents)s</label>' % {
114+            "attrs": flatatt(attrs),
115+            "contents": contents,
116+        })
117+   
118+    def contents(self):
119+        from django.contrib.admin.templatetags.admin_list import _boolean_icon
120+        from django.contrib.admin.views.main import EMPTY_CHANGELIST_VALUE
121+        attr = getattr(self.form.instance, self.field)
122+        allow_tags = False
123+        boolean = False
124+        try:
125+            f = self.form.instance._meta.get_field_by_name(self.field)[0]
126+        except FieldDoesNotExist:
127+            value = attr
128+            if callable(attr):
129+                value = attr()
130+            allow_tags = getattr(attr, "allow_tags", False)
131+            boolean = getattr(attr, "boolean", False)
132+            if boolean:
133+                allow_tags = True
134+                result_repr = _boolean_icon(value)
135+            else:
136+                result_repr = smart_unicode(value)
137+        else:
138+            value = attr
139+            if value is None:
140+                result_repr = EMPTY_CHANGELIST_VALUE
141+            elif isinstance(f.rel, ManyToManyRel):
142+                result_repr = ", ".join(map(result_repr, value.all()))
143+            elif isinstance(f, models.DateField) or isinstance(f, models.TimeField):
144+                date_format, datetime_format, time_format = get_date_formats()
145+                if isinstance(f, models.DateTimeField):
146+                    result_repr = capfirst(dateformat.format(value, datetime_format))
147+                elif isinstance(f, models.TimeField):
148+                    result_repr = capfirst(dateformat.time_format(value, time_format))
149+                else:
150+                    result_repr = capfirst(dateformat.format(value, date_format))
151+            elif isinstance(f, models.BooleanField) or isinstance(f, models.NullBooleanField):
152+                result_repr = _boolean_icon(value)
153+            elif isinstance(f, models.DecimalField):
154+                result_repr = ("%%.%sf" % f.decimal_places) % value
155+            elif f.flatchoices:
156+                result_repr = dict(f.flatchoices).get(value, EMPTY_CHANGELIST_VALUE)
157+            else:
158+                result_repr = smart_unicode(value)
159+       
160+        if allow_tags:
161+            return mark_safe(result_repr)
162+        return escape(result_repr)
163+
164 class InlineAdminFormSet(object):
165     """
166     A wrapper around an inline formset for use in the admin system.
167     """
168-    def __init__(self, inline, formset, fieldsets):
169+    def __init__(self, inline, formset, fieldsets, readonly_fields):
170         self.opts = inline
171         self.formset = formset
172         self.fieldsets = fieldsets
173+        self.readonly_fields = readonly_fields
174 
175     def __iter__(self):
176         for form, original in zip(self.formset.initial_forms, self.formset.get_queryset()):
177-            yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, original)
178+            yield InlineAdminForm(self.formset, form, self.fieldsets,
179+                self.opts.prepopulated_fields, original, self.readonly_fields)
180         for form in self.formset.extra_forms:
181-            yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, None)
182+            yield InlineAdminForm(self.formset, form, self.fieldsets,
183+                self.opts.prepopulated_fields, None, self.readonly_fields)
184 
185     def fields(self):
186         fk = getattr(self.formset, "fk", None)
187@@ -133,17 +214,19 @@ class InlineAdminForm(AdminForm):
188     """
189     A wrapper around an inline form for use in the admin system.
190     """
191-    def __init__(self, formset, form, fieldsets, prepopulated_fields, original):
192+    def __init__(self, formset, form, fieldsets, prepopulated_fields, original,
193+        readonly_fields):
194         self.formset = formset
195         self.original = original
196         if original is not None:
197             self.original_content_type_id = ContentType.objects.get_for_model(original).pk
198         self.show_url = original and hasattr(original, 'get_absolute_url')
199-        super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields)
200+        super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields,
201+            readonly_fields)
202 
203     def __iter__(self):
204         for name, options in self.fieldsets:
205-            yield InlineFieldset(self.formset, self.form, name, **options)
206+            yield InlineFieldset(self.formset, self.form, name, self.readonly_fields, **options)
207 
208     def has_auto_field(self):
209         if self.form._meta.model._meta.has_auto_field:
210@@ -194,7 +277,7 @@ class InlineFieldset(Fieldset):
211         for field in self.fields:
212             if fk and fk.name == field:
213                 continue
214-            yield Fieldline(self.form, field)
215+            yield Fieldline(self.form, field, self.readonly_fields)
216 
217 class AdminErrorList(forms.util.ErrorList):
218     """
219diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
220index 7193bee..8d605ea 100644
221--- a/django/contrib/admin/options.py
222+++ b/django/contrib/admin/options.py
223@@ -66,6 +66,7 @@ class BaseModelAdmin(object):
224     radio_fields = {}
225     prepopulated_fields = {}
226     formfield_overrides = {}
227+    readonly_fields = ()
228 
229     def __init__(self):
230         self.formfield_overrides = dict(FORMFIELD_FOR_DBFIELD_DEFAULTS, **self.formfield_overrides)
231@@ -174,6 +175,9 @@ class BaseModelAdmin(object):
232             return [(None, {'fields': self.fields})]
233         return None
234     declared_fieldsets = property(_declared_fieldsets)
235+   
236+    def get_readonly_fields(self, request, obj=None):
237+        return self.readonly_fields
238 
239 class ModelAdmin(BaseModelAdmin):
240     "Encapsulates all admin options and functionality for a given model."
241@@ -324,7 +328,8 @@ class ModelAdmin(BaseModelAdmin):
242         if self.declared_fieldsets:
243             return self.declared_fieldsets
244         form = self.get_form(request, obj)
245-        return [(None, {'fields': form.base_fields.keys()})]
246+        fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
247+        return [(None, {'fields': fields})]
248 
249     def get_form(self, request, obj=None, **kwargs):
250         """
251@@ -339,12 +344,15 @@ class ModelAdmin(BaseModelAdmin):
252             exclude = []
253         else:
254             exclude = list(self.exclude)
255+        exclude.extend(kwargs.get("exclude", []))
256+        exclude.extend(self.get_readonly_fields(request, obj))
257         # if exclude is an empty list we pass None to be consistant with the
258         # default on modelform_factory
259+        exclude = exclude or None
260         defaults = {
261             "form": self.form,
262             "fields": fields,
263-            "exclude": (exclude + kwargs.get("exclude", [])) or None,
264+            "exclude": exclude,
265             "formfield_callback": curry(self.formfield_for_dbfield, request=request),
266         }
267         defaults.update(kwargs)
268@@ -764,13 +772,17 @@ class ModelAdmin(BaseModelAdmin):
269                 formset = FormSet(instance=self.model(), prefix=prefix)
270                 formsets.append(formset)
271 
272-        adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields)
273+        adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
274+            self.prepopulated_fields, self.get_readonly_fields(request)
275+        )
276         media = self.media + adminForm.media
277 
278         inline_admin_formsets = []
279         for inline, formset in zip(self.inline_instances, formsets):
280             fieldsets = list(inline.get_fieldsets(request))
281-            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
282+            readonly = list(inline.get_readonly_fields(request))
283+            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
284+                fieldsets, readonly)
285             inline_admin_formsets.append(inline_admin_formset)
286             media = media + inline_admin_formset.media
287 
288@@ -853,13 +865,16 @@ class ModelAdmin(BaseModelAdmin):
289                 formset = FormSet(instance=obj, prefix=prefix)
290                 formsets.append(formset)
291 
292-        adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields)
293+        adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
294+            self.prepopulated_fields, self.get_readonly_fields(request, obj))
295         media = self.media + adminForm.media
296 
297         inline_admin_formsets = []
298         for inline, formset in zip(self.inline_instances, formsets):
299             fieldsets = list(inline.get_fieldsets(request, obj))
300-            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
301+            readonly = list(inline.get_readonly_fields(request, obj))
302+            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
303+                fieldsets, readonly)
304             inline_admin_formsets.append(inline_admin_formset)
305             media = media + inline_admin_formset.media
306 
307@@ -1151,14 +1166,17 @@ class InlineModelAdmin(BaseModelAdmin):
308             exclude = []
309         else:
310             exclude = list(self.exclude)
311+        exclude.extend(kwargs.get("exclude", []))
312+        exclude.extend(self.get_readonly_fields(request, obj))
313         # if exclude is an empty list we use None, since that's the actual
314         # default
315+        exclude = exclude or None
316         defaults = {
317             "form": self.form,
318             "formset": self.formset,
319             "fk_name": self.fk_name,
320             "fields": fields,
321-            "exclude": (exclude + kwargs.get("exclude", [])) or None,
322+            "exclude": exclude,
323             "formfield_callback": curry(self.formfield_for_dbfield, request=request),
324             "extra": self.extra,
325             "max_num": self.max_num,
326@@ -1170,7 +1188,8 @@ class InlineModelAdmin(BaseModelAdmin):
327         if self.declared_fieldsets:
328             return self.declared_fieldsets
329         form = self.get_formset(request).form
330-        return [(None, {'fields': form.base_fields.keys()})]
331+        fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
332+        return [(None, {'fields': fields})]
333 
334 class StackedInline(InlineModelAdmin):
335     template = 'admin/edit_inline/stacked.html'
336diff --git a/django/contrib/admin/templates/admin/includes/fieldset.html b/django/contrib/admin/templates/admin/includes/fieldset.html
337index 8ee24b1..bcde368 100644
338--- a/django/contrib/admin/templates/admin/includes/fieldset.html
339+++ b/django/contrib/admin/templates/admin/includes/fieldset.html
340@@ -1,19 +1,28 @@
341 <fieldset class="module aligned {{ fieldset.classes }}">
342-  {% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %}
343-  {% if fieldset.description %}<div class="description">{{ fieldset.description|safe }}</div>{% endif %}
344-  {% for line in fieldset %}
345-      <div class="form-row{% if line.errors %} errors{% endif %} {% for field in line %}{{ field.field.name }} {% endfor %} ">
346-      {{ line.errors }}
347-      {% for field in line %}
348-      <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
349-          {% if field.is_checkbox %}
350-              {{ field.field }}{{ field.label_tag }}
351-          {% else %}
352-              {{ field.label_tag }}{{ field.field }}
353-          {% endif %}
354-          {% if field.field.field.help_text %}<p class="help">{{ field.field.field.help_text|safe }}</p>{% endif %}
355-      </div>
356-      {% endfor %}
357-      </div>
358-  {% endfor %}
359+    {% if fieldset.name %}<h2>{{ fieldset.name }}</h2>{% endif %}
360+    {% if fieldset.description %}
361+        <div class="description">{{ fieldset.description|safe }}</div>
362+    {% endif %}
363+    {% for line in fieldset %}
364+        <div class="form-row{% if line.errors %} errors{% endif %} {% for field in line %}{{ field.field.name }} {% endfor %} ">
365+            {{ line.errors }}
366+            {% for field in line %}
367+                <div{% if not line.fields|length_is:"1" %} class="field-box"{% endif %}>
368+                    {% if field.is_checkbox %}
369+                        {{ field.field }}{{ field.label_tag }}
370+                    {% else %}
371+                        {{ field.label_tag }}
372+                        {% if field.is_readonly %}
373+                            {{ field.contents }}
374+                        {% else %}
375+                            {{ field.field }}
376+                        {% endif %}
377+                    {% endif %}
378+                    {% if field.field.field.help_text %}
379+                        <p class="help">{{ field.field.field.help_text|safe }}</p>
380+                    {% endif %}
381+                </div>
382+            {% endfor %}
383+        </div>
384+    {% endfor %}
385 </fieldset>
386diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
387index 726da65..ace2297 100644
388--- a/django/contrib/admin/validation.py
389+++ b/django/contrib/admin/validation.py
390@@ -122,6 +122,17 @@ def validate(cls, model):
391             if '__' in field:
392                 continue
393             get_field(cls, model, opts, 'ordering[%d]' % idx, field)
394+   
395+    if hasattr(cls, "readonly_fields"):
396+        check_isseq(cls, "readonly_fields", cls.readonly_fields)
397+        for idx, field in enumerate(cls.readonly_fields):
398+            try:
399+                field = opts.get_field_by_name(field)[0]
400+            except models.FieldDoesNotExist:
401+                raise ImproperlyConfigured("'%s.readonly_fields[%d]' refers to a "
402+                    "field, '%s', not defined on %s."
403+                    % (cls.__name__, idx, field, model.__name__))
404+           
405 
406     # list_select_related = False
407     # save_as = False
408diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
409index 0f746bf..36a7d9e 100644
410--- a/docs/ref/contrib/admin/index.txt
411+++ b/docs/ref/contrib/admin/index.txt
412@@ -540,6 +540,15 @@ into a ``Input`` widget for either a ``ForeignKey`` or ``ManyToManyField``::
413     class ArticleAdmin(admin.ModelAdmin):
414         raw_id_fields = ("newspaper",)
415 
416+.. attribute:: ModelAdmin.readonly_fields
417+
418+By default any field Django's admin displays is shown as editable.  Any fields
419+in this option (which should be a ``list`` or ``tuple``) will be displayed as
420+just showing the data they contain, without the ability to be edited.  Note
421+that fields in this option shouldn't be in the ``fields`` or ``exclude``
422+options, they can however be in the ``fieldsets`` option to control where they
423+appear.
424+
425 .. attribute:: ModelAdmin.save_as
426 
427 Set ``save_as`` to enable a "save as" feature on admin change forms.
428@@ -744,6 +753,15 @@ model instance::
429                 instance.save()
430             formset.save_m2m()
431 
432+.. method:: ModelAdmin.get_readonly_fields(self, request, obj=None)
433+
434+The ``get_readonly_fields`` method is given the ``HttpRequest`` and the
435+``obj`` being edited (or ``None`` on a add form) and is expected to return a
436+``list`` or ``tuple`` of field names that will be displayed as read-only, as
437+described above in the ``ModelAdmin.readonly_fields`` section.  Note that this
438+method will be called several times during a given request, therefore if it
439+performs and very expensive calculations it may be wise to cache them.
440+
441 .. method:: ModelAdmin.get_urls(self)
442 
443 .. versionadded:: 1.1
444diff --git a/tests/regressiontests/admin_validation/models.py b/tests/regressiontests/admin_validation/models.py
445index eb53a9d..760469b 100644
446--- a/tests/regressiontests/admin_validation/models.py
447+++ b/tests/regressiontests/admin_validation/models.py
448@@ -110,6 +110,19 @@ Exception: <class 'regressiontests.admin_validation.models.TwoAlbumFKAndAnE'> ha
449 
450 >>> validate_inline(TwoAlbumFKAndAnEInline, None, Album)
451 
452+>>> class SongAdmin(admin.ModelAdmin):
453+...     readonly_fields = ("title",)
454+
455+>>> validate(SongAdmin, Song)
456+
457+>>> class SongAdmin(admin.ModelAdmin):
458+...     readonly_fields = ("title", "nonexistant")
459+
460+>>> validate(SongAdmin, Song)
461+Traceback (most recent call last):
462+    ...
463+ImproperlyConfigured: 'SongAdmin.readonly_fields[1]' refers to a field, 'nonexistant', not defined on Song.
464+
465 # Regression test for #12203/#12237 - Fail more gracefully when a M2M field that
466 # specifies the 'through' option is included in the 'fields' or the 'fieldsets'
467 # ModelAdmin options.
468diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
469index 50bc05e..7b81bb0 100644
470--- a/tests/regressiontests/admin_views/models.py
471+++ b/tests/regressiontests/admin_views/models.py
472@@ -1,10 +1,13 @@
473 # -*- coding: utf-8 -*-
474+import datetime
475 import tempfile
476 import os
477-from django.core.files.storage import FileSystemStorage
478-from django.db import models
479+
480 from django.contrib import admin
481+from django.core.files.storage import FileSystemStorage
482 from django.core.mail import EmailMessage
483+from django.db import models
484+
485 
486 class Section(models.Model):
487     """
488@@ -420,6 +423,14 @@ class CategoryInline(admin.StackedInline):
489 class CollectorAdmin(admin.ModelAdmin):
490     inlines = [WidgetInline, DooHickeyInline, GrommetInline, WhatsitInline, FancyDoodadInline, CategoryInline]
491 
492+class Post(models.Model):
493+    title = models.CharField(max_length=100)
494+    content = models.TextField()
495+    posted = models.DateField(default=datetime.date.today)
496+
497+class PostAdmin(admin.ModelAdmin):
498+    readonly_fields = ('posted',)
499+
500 admin.site.register(Article, ArticleAdmin)
501 admin.site.register(CustomArticle, CustomArticleAdmin)
502 admin.site.register(Section, save_as=True, inlines=[ArticleInline])
503@@ -443,6 +454,7 @@ admin.site.register(Recommendation, RecommendationAdmin)
504 admin.site.register(Recommender)
505 admin.site.register(Collector, CollectorAdmin)
506 admin.site.register(Category, CategoryAdmin)
507+admin.site.register(Post, PostAdmin)
508 
509 # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
510 # That way we cover all four cases:
511diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
512index 167498a..3e19bf3 100644
513--- a/tests/regressiontests/admin_views/tests.py
514+++ b/tests/regressiontests/admin_views/tests.py
515@@ -10,20 +10,18 @@ from django.contrib.admin.models import LogEntry, DELETION
516 from django.contrib.admin.sites import LOGIN_FORM_KEY
517 from django.contrib.admin.util import quote
518 from django.contrib.admin.helpers import ACTION_CHECKBOX_NAME
519+from django.utils import dateformat
520 from django.utils.cache import get_max_age
521 from django.utils.html import escape
522+from django.utils.translation import get_date_formats
523 
524 # local test models
525 from models import Article, BarAccount, CustomArticle, EmptyModel, \
526     ExternalSubscriber, FooAccount, Gallery, ModelWithStringPrimaryKey, \
527     Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, \
528     Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, \
529-    Category
530+    Category, Post
531 
532-try:
533-    set
534-except NameError:
535-    from sets import Set as set
536 
537 class AdminViewBasicTest(TestCase):
538     fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', 'admin-views-fabrics.xml']
539@@ -1633,3 +1631,40 @@ class NeverCacheTests(TestCase):
540         "Check the never-cache status of the Javascript i18n view"
541         response = self.client.get('/test_admin/jsi18n/')
542         self.failUnlessEqual(get_max_age(response), None)
543+
544+
545+class ReadonlyTest(TestCase):
546+    fixtures = ['admin-views-users.xml']
547+
548+    def setUp(self):
549+        self.client.login(username='super', password='secret')
550+
551+    def tearDown(self):
552+        self.client.logout()
553+
554+    def test_readonly_get(self):
555+        response = self.client.get('/test_admin/admin/admin_views/post/add/')
556+        self.assertEqual(response.status_code, 200)
557+        self.assertNotContains(response, 'name="posted"')
558+        # 3 fields + 2 submit buttons
559+        self.assertEqual(response.content.count("input"), 5)
560+        self.assertContains(response,
561+            dateformat.format(datetime.date.today(), get_date_formats()[0]))
562+   
563+    def test_readonly_post(self):
564+        data = {
565+            "title": "Django Got Readonly Fields",
566+            "content": "This is an incredible development."
567+        }
568+        response = self.client.post('/test_admin/admin/admin_views/post/add/', data)
569+        self.assertEqual(response.status_code, 302)
570+        self.assertEqual(Post.objects.count(), 1)
571+        p = Post.objects.get()
572+        self.assertEqual(p.posted, datetime.date.today())
573+       
574+        data["posted"] = "10-8-1990" # some date that's not today
575+        response = self.client.post('/test_admin/admin/admin_views/post/add/', data)
576+        self.assertEqual(response.status_code, 302)
577+        self.assertEqual(Post.objects.count(), 2)
578+        p = Post.objects.order_by('-id')[0]
579+        self.assertEqual(p.posted, datetime.date.today())