diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
index 40437c0..d8726ea 100644
--- a/django/contrib/admin/helpers.py
+++ b/django/contrib/admin/helpers.py
@@ -1,13 +1,15 @@
-
 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.contenttypes.models import ContentType
+from django.forms.util import flatatt
+from django.utils.encoding import force_unicode
+from django.utils.html import escape
+from django.utils.safestring import mark_safe
+from django.utils.text import capfirst
 from django.utils.translation import ugettext_lazy as _
 
+
 ACTION_CHECKBOX_NAME = '_selected_action'
 
 class ActionForm(forms.Form):
@@ -16,16 +18,18 @@ 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):
         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.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, **options)
 
     def first_field(self):
         try:
@@ -49,11 +53,13 @@ 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):
         self.form = form
         self.name, self.fields = name, fields
         self.classes = u' '.join(classes)
         self.description = description
+        self.readonly_fields = readonly_fields
 
     def _media(self):
         if 'collapse' in self.classes:
@@ -63,22 +69,26 @@ class Fieldset(object):
 
     def __iter__(self):
         for field in self.fields:
-            yield Fieldline(self.form, field)
+            yield Fieldline(self.form, field, self.readonly_fields)
 
 class Fieldline(object):
-    def __init__(self, form, field):
+    def __init__(self, form, field, readonly_fields):
         self.form = form # A django.forms.Form instance
         if isinstance(field, basestring):
             self.fields = [field]
         else:
             self.fields = field
+        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))
+            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,20 +110,44 @@ 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):
+        self.field = field
+        self.form = form
+        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"
+        contents = force_unicode(escape(capfirst(self.field))) + u":"
+        return mark_safe('<label %(attrs)s>%(contents)s</label>' % {
+            "attrs": flatatt(attrs),
+            "contents": contents,
+        })
+    
+    def contents(self):
+        return getattr(self.form.instance, self.field)
+
 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):
         self.opts = inline
         self.formset = formset
         self.fieldsets = fieldsets
+        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)
         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)
 
     def fields(self):
         fk = getattr(self.formset, "fk", None)
@@ -133,17 +167,19 @@ 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):
         self.formset = formset
         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, **options)
 
     def has_auto_field(self):
         if self.form._meta.model._meta.has_auto_field:
@@ -194,7 +230,7 @@ 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)
 
 class AdminErrorList(forms.util.ErrorList):
     """
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
index 3144a22..57000b8 100644
--- a/django/contrib/admin/options.py
+++ b/django/contrib/admin/options.py
@@ -64,6 +64,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)
@@ -171,6 +172,9 @@ class BaseModelAdmin(object):
             return [(None, {'fields': self.fields})]
         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."
@@ -321,7 +325,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):
         """
@@ -336,12 +341,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)
@@ -759,13 +767,17 @@ class ModelAdmin(BaseModelAdmin):
                 formset = FormSet(instance=self.model(), prefix=prefix)
                 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)
+        )
         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)
             inline_admin_formsets.append(inline_admin_formset)
             media = media + inline_admin_formset.media
 
@@ -847,13 +859,16 @@ class ModelAdmin(BaseModelAdmin):
                 formset = FormSet(instance=obj, prefix=prefix)
                 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))
         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)
             inline_admin_formsets.append(inline_admin_formset)
             media = media + inline_admin_formset.media
 
@@ -1144,14 +1159,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,
@@ -1163,7 +1181,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})]
 
 class StackedInline(InlineModelAdmin):
     template = 'admin/edit_inline/stacked.html'
diff --git a/django/contrib/admin/templates/admin/includes/fieldset.html b/django/contrib/admin/templates/admin/includes/fieldset.html
index 8ee24b1..bcde368 100644
--- a/django/contrib/admin/templates/admin/includes/fieldset.html
+++ b/django/contrib/admin/templates/admin/includes/fieldset.html
@@ -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 %}
+                            {{ field.contents }}
+                        {% 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>
diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
index c1e05ed..34d283b 100644
--- a/docs/ref/contrib/admin/index.txt
+++ b/docs/ref/contrib/admin/index.txt
@@ -540,6 +540,15 @@ into a ``Input`` widget for either a ``ForeignKey`` or ``ManyToManyField``::
     class ArticleAdmin(admin.ModelAdmin):
         raw_id_fields = ("newspaper",)
 
+.. attribute:: ModelAdmin.readonly_fields
+
+By default any field Django's admin displays is shown as editable.  Any fields
+in this option (which should be a ``list`` or ``tuple``) will be displayed as
+just showing the data they contain, without the ability to be edited.  Note
+that fields in this option shouldn't be in the ``fields`` or ``exclude``
+options, they can however be in the ``fieldsets`` option to control where they
+appear.
+
 .. attribute:: ModelAdmin.save_as
 
 Set ``save_as`` to enable a "save as" feature on admin change forms.
@@ -744,6 +753,15 @@ model instance::
                 instance.save()
             formset.save_m2m()
 
+.. method:: ModelAdmin.get_readonly_fields(self, request, obj=None)
+
+The ``get_readonly_fields`` method is given the ``HttpRequest`` and the 
+``obj`` being edited (or ``None`` on a 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 ``ModelAdmin.readonly_fields`` section.  Note that this
+method will be called several times during a given request, therefore if it
+performs and very expensive calculations it may be wise to cache them.
+
 .. method:: ModelAdmin.get_urls(self)
 
 .. versionadded:: 1.1
