Ticket #342: readonly-admin.6.diff

File readonly-admin.6.diff, 22.1 KB (added by Alex, 6 years ago)
  • django/contrib/admin/helpers.py

    diff --git a/django/contrib/admin/helpers.py b/django/contrib/admin/helpers.py
    index 40437c0..5a985ec 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(forms.forms.pretty_name(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 7193bee..8d605ea 100644
    a b class BaseModelAdmin(object): 
    6666    radio_fields = {}
    6767    prepopulated_fields = {}
    6868    formfield_overrides = {}
     69    readonly_fields = ()
    6970
    7071    def __init__(self):
    7172        self.formfield_overrides = dict(FORMFIELD_FOR_DBFIELD_DEFAULTS, **self.formfield_overrides)
    class BaseModelAdmin(object): 
    174175            return [(None, {'fields': self.fields})]
    175176        return None
    176177    declared_fieldsets = property(_declared_fieldsets)
     178   
     179    def get_readonly_fields(self, request, obj=None):
     180        return self.readonly_fields
    177181
    178182class ModelAdmin(BaseModelAdmin):
    179183    "Encapsulates all admin options and functionality for a given model."
    class ModelAdmin(BaseModelAdmin): 
    324328        if self.declared_fieldsets:
    325329            return self.declared_fieldsets
    326330        form = self.get_form(request, obj)
    327         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})]
    328333
    329334    def get_form(self, request, obj=None, **kwargs):
    330335        """
    class ModelAdmin(BaseModelAdmin): 
    339344            exclude = []
    340345        else:
    341346            exclude = list(self.exclude)
     347        exclude.extend(kwargs.get("exclude", []))
     348        exclude.extend(self.get_readonly_fields(request, obj))
    342349        # if exclude is an empty list we pass None to be consistant with the
    343350        # default on modelform_factory
     351        exclude = exclude or None
    344352        defaults = {
    345353            "form": self.form,
    346354            "fields": fields,
    347             "exclude": (exclude + kwargs.get("exclude", [])) or None,
     355            "exclude": exclude,
    348356            "formfield_callback": curry(self.formfield_for_dbfield, request=request),
    349357        }
    350358        defaults.update(kwargs)
    class ModelAdmin(BaseModelAdmin): 
    764772                formset = FormSet(instance=self.model(), prefix=prefix)
    765773                formsets.append(formset)
    766774
    767         adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields)
     775        adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
     776            self.prepopulated_fields, self.get_readonly_fields(request)
     777        )
    768778        media = self.media + adminForm.media
    769779
    770780        inline_admin_formsets = []
    771781        for inline, formset in zip(self.inline_instances, formsets):
    772782            fieldsets = list(inline.get_fieldsets(request))
    773             inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
     783            readonly = list(inline.get_readonly_fields(request))
     784            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
     785                fieldsets, readonly)
    774786            inline_admin_formsets.append(inline_admin_formset)
    775787            media = media + inline_admin_formset.media
    776788
    class ModelAdmin(BaseModelAdmin): 
    853865                formset = FormSet(instance=obj, prefix=prefix)
    854866                formsets.append(formset)
    855867
    856         adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields)
     868        adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
     869            self.prepopulated_fields, self.get_readonly_fields(request, obj))
    857870        media = self.media + adminForm.media
    858871
    859872        inline_admin_formsets = []
    860873        for inline, formset in zip(self.inline_instances, formsets):
    861874            fieldsets = list(inline.get_fieldsets(request, obj))
    862             inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
     875            readonly = list(inline.get_readonly_fields(request, obj))
     876            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
     877                fieldsets, readonly)
    863878            inline_admin_formsets.append(inline_admin_formset)
    864879            media = media + inline_admin_formset.media
    865880
    class InlineModelAdmin(BaseModelAdmin): 
    11511166            exclude = []
    11521167        else:
    11531168            exclude = list(self.exclude)
     1169        exclude.extend(kwargs.get("exclude", []))
     1170        exclude.extend(self.get_readonly_fields(request, obj))
    11541171        # if exclude is an empty list we use None, since that's the actual
    11551172        # default
     1173        exclude = exclude or None
    11561174        defaults = {
    11571175            "form": self.form,
    11581176            "formset": self.formset,
    11591177            "fk_name": self.fk_name,
    11601178            "fields": fields,
    1161             "exclude": (exclude + kwargs.get("exclude", [])) or None,
     1179            "exclude": exclude,
    11621180            "formfield_callback": curry(self.formfield_for_dbfield, request=request),
    11631181            "extra": self.extra,
    11641182            "max_num": self.max_num,
    class InlineModelAdmin(BaseModelAdmin): 
    11701188        if self.declared_fieldsets:
    11711189            return self.declared_fieldsets
    11721190        form = self.get_formset(request).form
    1173         return [(None, {'fields': form.base_fields.keys()})]
     1191        fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
     1192        return [(None, {'fields': fields})]
    11741193
    11751194class StackedInline(InlineModelAdmin):
    11761195    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 726da65..ace2297 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 0f746bf..36a7d9e 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 eb53a9d..760469b 100644
    a b Exception: <class 'regressiontests.admin_validation.models.TwoAlbumFKAndAnE'> ha 
    110110
    111111>>> validate_inline(TwoAlbumFKAndAnEInline, None, Album)
    112112
     113>>> class SongAdmin(admin.ModelAdmin):
     114...     readonly_fields = ("title",)
     115
     116>>> validate(SongAdmin, Song)
     117
     118>>> class SongAdmin(admin.ModelAdmin):
     119...     readonly_fields = ("title", "nonexistant")
     120
     121>>> validate(SongAdmin, Song)
     122Traceback (most recent call last):
     123    ...
     124ImproperlyConfigured: 'SongAdmin.readonly_fields[1]' refers to a field, 'nonexistant', not defined on Song.
     125
    113126# Regression test for #12203/#12237 - Fail more gracefully when a M2M field that
    114127# specifies the 'through' option is included in the 'fields' or the 'fieldsets'
    115128# ModelAdmin options.
  • 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 167498a..7b60f3d 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): 
    16331629        "Check the never-cache status of the Javascript i18n view"
    16341630        response = self.client.get('/test_admin/jsi18n/')
    16351631        self.failUnlessEqual(get_max_age(response), None)
     1632
     1633
     1634class ReadonlyTest(TestCase):
     1635    fixtures = ['admin-views-users.xml']
     1636
     1637    def setUp(self):
     1638        self.client.login(username='super', password='secret')
     1639
     1640    def tearDown(self):
     1641        self.client.logout()
     1642
     1643    def test_readonly_get(self):
     1644        response = self.client.get('/test_admin/admin/admin_views/post/add/')
     1645        self.assertEqual(response.status_code, 200)
     1646        self.assertNotContains(response, 'name="posted"')
     1647        # 3 fields + 2 submit buttons
     1648        self.assertEqual(response.content.count("input"), 5)
     1649        self.assertContains(response, str(datetime.date.today()))
     1650   
     1651    def test_readonly_post(self):
     1652        data = {
     1653            "title": "Django Got Readonly Fields",
     1654            "content": "This is an incredible development."
     1655        }
     1656        response = self.client.post('/test_admin/admin/admin_views/post/add/', data)
     1657        self.assertEqual(response.status_code, 302)
     1658        self.assertEqual(Post.objects.count(), 1)
     1659        p = Post.objects.get()
     1660        self.assertEqual(p.posted, datetime.date.today())
     1661       
     1662        data["posted"] = "10-8-1990" # some date that's not today
     1663        response = self.client.post('/test_admin/admin/admin_views/post/add/', data)
     1664        self.assertEqual(response.status_code, 302)
     1665        self.assertEqual(Post.objects.count(), 2)
     1666        p = Post.objects.order_by('-id')[0]
     1667        self.assertEqual(p.posted, datetime.date.today())
Back to Top