Ticket #342: readonly-admin.4.diff

File readonly-admin.4.diff, 21.9 KB (added by Alex, 6 years ago)

Added tests.

  • django/contrib/admin/helpers.py

    diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
    index 40437c0..d8726ea 100644
    a b  
    1 
    21from django import forms
    32from 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
    73from django.contrib.admin.util import flatten_fieldsets
    84from django.contrib.contenttypes.models import ContentType
     5from django.forms.util import flatatt
     6from django.utils.encoding import force_unicode
     7from django.utils.html import escape
     8from django.utils.safestring import mark_safe
     9from django.utils.text import capfirst
    910from django.utils.translation import ugettext_lazy as _
    1011
     12
    1113ACTION_CHECKBOX_NAME = '_selected_action'
    1214
    1315class ActionForm(forms.Form):
    class ActionForm(forms.Form): 
    1618checkbox = forms.CheckboxInput({'class': 'action-select'}, lambda value: False)
    1719
    1820class AdminForm(object):
    19     def __init__(self, form, fieldsets, prepopulated_fields):
     21    def __init__(self, form, fieldsets, prepopulated_fields, readonly_fields):
    2022        self.form, self.fieldsets = form, normalize_fieldsets(fieldsets)
    2123        self.prepopulated_fields = [{
    2224            'field': form[field_name],
    2325            'dependencies': [form[f] for f in dependencies]
    2426        } for field_name, dependencies in prepopulated_fields.items()]
     27        self.readonly_fields = readonly_fields
    2528
    2629    def __iter__(self):
    2730        for name, options in self.fieldsets:
    28             yield Fieldset(self.form, name, **options)
     31            yield Fieldset(self.form, name,
     32                readonly_fields=self.readonly_fields, **options)
    2933
    3034    def first_field(self):
    3135        try:
    class AdminForm(object): 
    4953    media = property(_media)
    5054
    5155class Fieldset(object):
    52     def __init__(self, form, name=None, fields=(), classes=(), description=None):
     56    def __init__(self, form, name=None, readonly_fields=(), fields=(), classes=(),
     57        description=None):
    5358        self.form = form
    5459        self.name, self.fields = name, fields
    5560        self.classes = u' '.join(classes)
    5661        self.description = description
     62        self.readonly_fields = readonly_fields
    5763
    5864    def _media(self):
    5965        if 'collapse' in self.classes:
    class Fieldset(object): 
    6369
    6470    def __iter__(self):
    6571        for field in self.fields:
    66             yield Fieldline(self.form, field)
     72            yield Fieldline(self.form, field, self.readonly_fields)
    6773
    6874class Fieldline(object):
    69     def __init__(self, form, field):
     75    def __init__(self, form, field, readonly_fields):
    7076        self.form = form # A django.forms.Form instance
    7177        if isinstance(field, basestring):
    7278            self.fields = [field]
    7379        else:
    7480            self.fields = field
     81        self.readonly_fields = readonly_fields
    7582
    7683    def __iter__(self):
    7784        for i, field in enumerate(self.fields):
    78             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))
    7989
    8090    def errors(self):
    81         return mark_safe(u'\n'.join([self.form[f].errors.as_ul() for f in self.fields]).strip('\n'))
     91        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'))
    8292
    8393class AdminField(object):
    8494    def __init__(self, form, field, is_first):
    class AdminField(object): 
    100110        attrs = classes and {'class': u' '.join(classes)} or {}
    101111        return self.field.label_tag(contents=contents, attrs=attrs)
    102112
     113class AdminReadonlyField(object):
     114    def __init__(self, form, field, is_first):
     115        self.field = field
     116        self.form = form
     117        self.is_first = is_first
     118        self.is_checkbox = False
     119        self.is_readonly = True
     120   
     121    def label_tag(self):
     122        attrs = {}
     123        if not self.is_first:
     124            attrs["class"] = "inline"
     125        contents = force_unicode(escape(capfirst(self.field))) + u":"
     126        return mark_safe('<label %(attrs)s>%(contents)s</label>' % {
     127            "attrs": flatatt(attrs),
     128            "contents": contents,
     129        })
     130   
     131    def contents(self):
     132        return getattr(self.form.instance, self.field)
     133
    103134class InlineAdminFormSet(object):
    104135    """
    105136    A wrapper around an inline formset for use in the admin system.
    106137    """
    107     def __init__(self, inline, formset, fieldsets):
     138    def __init__(self, inline, formset, fieldsets, readonly_fields):
    108139        self.opts = inline
    109140        self.formset = formset
    110141        self.fieldsets = fieldsets
     142        self.readonly_fields = readonly_fields
    111143
    112144    def __iter__(self):
    113145        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)
     146            yield InlineAdminForm(self.formset, form, self.fieldsets,
     147                self.opts.prepopulated_fields, original, self.readonly_fields)
    115148        for form in self.formset.extra_forms:
    116             yield InlineAdminForm(self.formset, form, self.fieldsets, self.opts.prepopulated_fields, None)
     149            yield InlineAdminForm(self.formset, form, self.fieldsets,
     150                self.opts.prepopulated_fields, None, self.readonly_fields)
    117151
    118152    def fields(self):
    119153        fk = getattr(self.formset, "fk", None)
    class InlineAdminForm(AdminForm): 
    133167    """
    134168    A wrapper around an inline form for use in the admin system.
    135169    """
    136     def __init__(self, formset, form, fieldsets, prepopulated_fields, original):
     170    def __init__(self, formset, form, fieldsets, prepopulated_fields, original,
     171        readonly_fields):
    137172        self.formset = formset
    138173        self.original = original
    139174        if original is not None:
    140175            self.original_content_type_id = ContentType.objects.get_for_model(original).pk
    141176        self.show_url = original and hasattr(original, 'get_absolute_url')
    142         super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields)
     177        super(InlineAdminForm, self).__init__(form, fieldsets, prepopulated_fields,
     178            readonly_fields)
    143179
    144180    def __iter__(self):
    145181        for name, options in self.fieldsets:
    146             yield InlineFieldset(self.formset, self.form, name, **options)
     182            yield InlineFieldset(self.formset, self.form, name, self.readonly_fields, **options)
    147183
    148184    def has_auto_field(self):
    149185        if self.form._meta.model._meta.has_auto_field:
    class InlineFieldset(Fieldset): 
    194230        for field in self.fields:
    195231            if fk and fk.name == field:
    196232                continue
    197             yield Fieldline(self.form, field)
     233            yield Fieldline(self.form, field, self.readonly_fields)
    198234
    199235class AdminErrorList(forms.util.ErrorList):
    200236    """
  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index c702e87..43387e5 100644
    a b class BaseModelAdmin(object): 
    6565    radio_fields = {}
    6666    prepopulated_fields = {}
    6767    formfield_overrides = {}
     68    readonly_fields = ()
    6869
    6970    def __init__(self):
    7071        self.formfield_overrides = dict(FORMFIELD_FOR_DBFIELD_DEFAULTS, **self.formfield_overrides)
    class BaseModelAdmin(object): 
    172173            return [(None, {'fields': self.fields})]
    173174        return None
    174175    declared_fieldsets = property(_declared_fieldsets)
     176   
     177    def get_readonly_fields(self, request, obj=None):
     178        return self.readonly_fields
    175179
    176180class ModelAdmin(BaseModelAdmin):
    177181    "Encapsulates all admin options and functionality for a given model."
    class ModelAdmin(BaseModelAdmin): 
    322326        if self.declared_fieldsets:
    323327            return self.declared_fieldsets
    324328        form = self.get_form(request, obj)
    325         return [(None, {'fields': form.base_fields.keys()})]
     329        fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
     330        return [(None, {'fields': fields})]
    326331
    327332    def get_form(self, request, obj=None, **kwargs):
    328333        """
    class ModelAdmin(BaseModelAdmin): 
    337342            exclude = []
    338343        else:
    339344            exclude = list(self.exclude)
     345        exclude.extend(kwargs.get("exclude", []))
     346        exclude.extend(self.get_readonly_fields(request, obj))
    340347        # if exclude is an empty list we pass None to be consistant with the
    341348        # default on modelform_factory
     349        exclude = exclude or None
    342350        defaults = {
    343351            "form": self.form,
    344352            "fields": fields,
    345             "exclude": (exclude + kwargs.get("exclude", [])) or None,
     353            "exclude": exclude,
    346354            "formfield_callback": curry(self.formfield_for_dbfield, request=request),
    347355        }
    348356        defaults.update(kwargs)
    class ModelAdmin(BaseModelAdmin): 
    762770                formset = FormSet(instance=self.model(), prefix=prefix)
    763771                formsets.append(formset)
    764772
    765         adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields)
     773        adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
     774            self.prepopulated_fields, self.get_readonly_fields(request)
     775        )
    766776        media = self.media + adminForm.media
    767777
    768778        inline_admin_formsets = []
    769779        for inline, formset in zip(self.inline_instances, formsets):
    770780            fieldsets = list(inline.get_fieldsets(request))
    771             inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
     781            readonly = list(inline.get_readonly_fields(request))
     782            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
     783                fieldsets, readonly)
    772784            inline_admin_formsets.append(inline_admin_formset)
    773785            media = media + inline_admin_formset.media
    774786
    class ModelAdmin(BaseModelAdmin): 
    851863                formset = FormSet(instance=obj, prefix=prefix)
    852864                formsets.append(formset)
    853865
    854         adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields)
     866        adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
     867            self.prepopulated_fields, self.get_readonly_fields(request, obj))
    855868        media = self.media + adminForm.media
    856869
    857870        inline_admin_formsets = []
    858871        for inline, formset in zip(self.inline_instances, formsets):
    859872            fieldsets = list(inline.get_fieldsets(request, obj))
    860             inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
     873            readonly = list(inline.get_readonly_fields(request, obj))
     874            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
     875                fieldsets, readonly)
    861876            inline_admin_formsets.append(inline_admin_formset)
    862877            media = media + inline_admin_formset.media
    863878
    class InlineModelAdmin(BaseModelAdmin): 
    11491164            exclude = []
    11501165        else:
    11511166            exclude = list(self.exclude)
     1167        exclude.extend(kwargs.get("exclude", []))
     1168        exclude.extend(self.get_readonly_fields(request, obj))
    11521169        # if exclude is an empty list we use None, since that's the actual
    11531170        # default
     1171        exclude = exclude or None
    11541172        defaults = {
    11551173            "form": self.form,
    11561174            "formset": self.formset,
    11571175            "fk_name": self.fk_name,
    11581176            "fields": fields,
    1159             "exclude": (exclude + kwargs.get("exclude", [])) or None,
     1177            "exclude": exclude,
    11601178            "formfield_callback": curry(self.formfield_for_dbfield, request=request),
    11611179            "extra": self.extra,
    11621180            "max_num": self.max_num,
    class InlineModelAdmin(BaseModelAdmin): 
    11681186        if self.declared_fieldsets:
    11691187            return self.declared_fieldsets
    11701188        form = self.get_formset(request).form
    1171         return [(None, {'fields': form.base_fields.keys()})]
     1189        fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
     1190        return [(None, {'fields': fields})]
    11721191
    11731192class StackedInline(InlineModelAdmin):
    11741193    template = 'admin/edit_inline/stacked.html'
  • django/contrib/admin/templates/admin/includes/fieldset.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 b  
    11<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                            {{ field.contents }}
     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 %}
    1928</fieldset>
  • django/contrib/admin/validation.py

    diff --git a/django/contrib/admin/validation.py b/django/contrib/admin/validation.py
    index 05e5c6d..9cbfe76 100644
    a b def validate(cls, model): 
    122122            if '__' in field:
    123123                continue
    124124            get_field(cls, model, opts, 'ordering[%d]' % idx, field)
     125   
     126    if hasattr(cls, "readonly_fields"):
     127        check_isseq(cls, "readonly_fields", cls.readonly_fields)
     128        for idx, field in enumerate(cls.readonly_fields):
     129            try:
     130                field = opts.get_field_by_name(field)[0]
     131            except models.FieldDoesNotExist:
     132                raise ImproperlyConfigured("'%s.readonly_fields[%d]' refers to a "
     133                    "field, '%s', not defined on %s."
     134                    % (cls.__name__, idx, field, model.__name__))
     135           
    125136
    126137    # list_select_related = False
    127138    # save_as = False
  • docs/ref/contrib/admin/index.txt

    diff --git a/docs/ref/contrib/admin/index.txt b/docs/ref/contrib/admin/index.txt
    index c1e05ed..34d283b 100644
    a b into a ``Input`` widget for either a ``ForeignKey`` or ``ManyToManyField``:: 
    540540    class ArticleAdmin(admin.ModelAdmin):
    541541        raw_id_fields = ("newspaper",)
    542542
     543.. attribute:: ModelAdmin.readonly_fields
     544
     545By default any field Django's admin displays is shown as editable.  Any fields
     546in this option (which should be a ``list`` or ``tuple``) will be displayed as
     547just showing the data they contain, without the ability to be edited.  Note
     548that fields in this option shouldn't be in the ``fields`` or ``exclude``
     549options, they can however be in the ``fieldsets`` option to control where they
     550appear.
     551
    543552.. attribute:: ModelAdmin.save_as
    544553
    545554Set ``save_as`` to enable a "save as" feature on admin change forms.
    model instance:: 
    744753                instance.save()
    745754            formset.save_m2m()
    746755
     756.. method:: ModelAdmin.get_readonly_fields(self, request, obj=None)
     757
     758The ``get_readonly_fields`` method is given the ``HttpRequest`` and the
     759``obj`` being edited (or ``None`` on a add form) and is expected to return a
     760``list`` or ``tuple`` of field names that will be displayed as read-only, as
     761described above in the ``ModelAdmin.readonly_fields`` section.  Note that this
     762method will be called several times during a given request, therefore if it
     763performs and very expensive calculations it may be wise to cache them.
     764
    747765.. method:: ModelAdmin.get_urls(self)
    748766
    749767.. versionadded:: 1.1
  • tests/regressiontests/admin_validation/models.py

    diff --git a/tests/regressiontests/admin_validation/models.py b/tests/regressiontests/admin_validation/models.py
    index 5506114..797bf2e 100644
    a b Exception: <class 'regressiontests.admin_validation.models.TwoAlbumFKAndAnE'> ha 
    9595
    9696>>> validate_inline(TwoAlbumFKAndAnEInline, None, Album)
    9797
     98>>> class SongAdmin(admin.ModelAdmin):
     99...     readonly_fields = ("title",)
     100
     101>>> validate(SongAdmin, Song)
     102
     103>>> class SongAdmin(admin.ModelAdmin):
     104...     readonly_fields = ("title", "nonexistant")
     105
     106>>> validate(SongAdmin, Song)
     107Traceback (most recent call last):
     108    ...
     109ImproperlyConfigured: 'SongAdmin.readonly_fields[1]' refers to a field, 'nonexistant', not defined on Song.
     110
     111
    98112"""}
  • tests/regressiontests/admin_views/models.py

    diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py
    index 50bc05e..7b81bb0 100644
    a b  
    11# -*- coding: utf-8 -*-
     2import datetime
    23import tempfile
    34import os
    4 from django.core.files.storage import FileSystemStorage
    5 from django.db import models
     5
    66from django.contrib import admin
     7from django.core.files.storage import FileSystemStorage
    78from django.core.mail import EmailMessage
     9from django.db import models
     10
    811
    912class Section(models.Model):
    1013    """
    class CategoryInline(admin.StackedInline): 
    420423class CollectorAdmin(admin.ModelAdmin):
    421424    inlines = [WidgetInline, DooHickeyInline, GrommetInline, WhatsitInline, FancyDoodadInline, CategoryInline]
    422425
     426class Post(models.Model):
     427    title = models.CharField(max_length=100)
     428    content = models.TextField()
     429    posted = models.DateField(default=datetime.date.today)
     430
     431class PostAdmin(admin.ModelAdmin):
     432    readonly_fields = ('posted',)
     433
    423434admin.site.register(Article, ArticleAdmin)
    424435admin.site.register(CustomArticle, CustomArticleAdmin)
    425436admin.site.register(Section, save_as=True, inlines=[ArticleInline])
    admin.site.register(Recommendation, RecommendationAdmin) 
    443454admin.site.register(Recommender)
    444455admin.site.register(Collector, CollectorAdmin)
    445456admin.site.register(Category, CategoryAdmin)
     457admin.site.register(Post, PostAdmin)
    446458
    447459# We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2.
    448460# That way we cover all four cases:
  • tests/regressiontests/admin_views/tests.py

    diff --git a/tests/regressiontests/admin_views/tests.py b/tests/regressiontests/admin_views/tests.py
    index c7893ec..0ee9dac 100644
    a b from models import Article, BarAccount, CustomArticle, EmptyModel, \ 
    1818    ExternalSubscriber, FooAccount, Gallery, ModelWithStringPrimaryKey, \
    1919    Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, \
    2020    Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, \
    21     Category
     21    Category, Post
    2222
    23 try:
    24     set
    25 except NameError:
    26     from sets import Set as set
    2723
    2824class AdminViewBasicTest(TestCase):
    2925    fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', 'admin-views-fabrics.xml']
    class NeverCacheTests(TestCase): 
    16171613        "Check the never-cache status of the Javascript i18n view"
    16181614        response = self.client.get('/test_admin/jsi18n/')
    16191615        self.failUnlessEqual(get_max_age(response), None)
     1616
     1617
     1618class ReadonlyTest(TestCase):
     1619    fixtures = ['admin-views-users.xml']
     1620
     1621    def setUp(self):
     1622        self.client.login(username='super', password='secret')
     1623
     1624    def tearDown(self):
     1625        self.client.logout()
     1626
     1627    def test_readonly_get(self):
     1628        response = self.client.get('/test_admin/admin/admin_views/post/add/')
     1629        self.assertEqual(response.status_code, 200)
     1630        self.assertNotContains(response, 'name="posted"')
     1631        # 3 fields + 2 submit buttons
     1632        self.assertEqual(response.content.count("input"), 5)
     1633        self.assertContains(response, str(datetime.date.today()))
     1634   
     1635    def test_readonly_post(self):
     1636        data = {
     1637            "title": "Django Got Readonly Fields",
     1638            "content": "This is an incredible development."
     1639        }
     1640        response = self.client.post('/test_admin/admin/admin_views/post/add/', data)
     1641        self.assertEqual(response.status_code, 302)
     1642        self.assertEqual(Post.objects.count(), 1)
     1643        p = Post.objects.get()
     1644        self.assertEqual(p.posted, datetime.date.today())
     1645       
     1646        data["posted"] = "10-8-1990" # some date that's not today
     1647        response = self.client.post('/test_admin/admin/admin_views/post/add/', data)
     1648        self.assertEqual(response.status_code, 302)
     1649        self.assertEqual(Post.objects.count(), 2)
     1650        p = Post.objects.order_by('-id')[0]
     1651        self.assertEqual(p.posted, datetime.date.today())
Back to Top