Ticket #342: readonly-admin.5.diff

File readonly-admin.5.diff, 21.4 KB (added by fest, 14 years ago)

Fixed label tag generation to use pretty_name instead of capfirst, which now produces pretty field names when underscores are used.

  • django/contrib/admin/helpers.py

     
    1 
    21from django import forms
    32from django.conf import settings
     3from django.contrib.admin.util import flatten_fieldsets
     4from django.contrib.contenttypes.models import ContentType
     5from django.forms.util import flatatt
     6from django.utils.encoding import force_unicode
    47from django.utils.html import escape
    58from django.utils.safestring import mark_safe
    6 from django.utils.encoding import force_unicode
    7 from django.contrib.admin.util import flatten_fieldsets
    8 from django.contrib.contenttypes.models import ContentType
     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):
     
    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:
     
    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:
     
    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):
     
    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)
     
    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:
     
    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

     
    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)
     
    173174            return [(None, {'fields': self.fields})]
    174175        return None
    175176    declared_fieldsets = property(_declared_fieldsets)
     177   
     178    def get_readonly_fields(self, request, obj=None):
     179        return self.readonly_fields
    176180
    177181class ModelAdmin(BaseModelAdmin):
    178182    "Encapsulates all admin options and functionality for a given model."
     
    323327        if self.declared_fieldsets:
    324328            return self.declared_fieldsets
    325329        form = self.get_form(request, obj)
    326         return [(None, {'fields': form.base_fields.keys()})]
     330        fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
     331        return [(None, {'fields': fields})]
    327332
    328333    def get_form(self, request, obj=None, **kwargs):
    329334        """
     
    338343            exclude = []
    339344        else:
    340345            exclude = list(self.exclude)
     346        exclude.extend(kwargs.get("exclude", []))
     347        exclude.extend(self.get_readonly_fields(request, obj))
    341348        # if exclude is an empty list we pass None to be consistant with the
    342349        # default on modelform_factory
     350        exclude = exclude or None
    343351        defaults = {
    344352            "form": self.form,
    345353            "fields": fields,
    346             "exclude": (exclude + kwargs.get("exclude", [])) or None,
     354            "exclude": exclude,
    347355            "formfield_callback": curry(self.formfield_for_dbfield, request=request),
    348356        }
    349357        defaults.update(kwargs)
     
    763771                formset = FormSet(instance=self.model(), prefix=prefix)
    764772                formsets.append(formset)
    765773
    766         adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields)
     774        adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)),
     775            self.prepopulated_fields, self.get_readonly_fields(request)
     776        )
    767777        media = self.media + adminForm.media
    768778
    769779        inline_admin_formsets = []
    770780        for inline, formset in zip(self.inline_instances, formsets):
    771781            fieldsets = list(inline.get_fieldsets(request))
    772             inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
     782            readonly = list(inline.get_readonly_fields(request))
     783            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
     784                fieldsets, readonly)
    773785            inline_admin_formsets.append(inline_admin_formset)
    774786            media = media + inline_admin_formset.media
    775787
     
    852864                formset = FormSet(instance=obj, prefix=prefix)
    853865                formsets.append(formset)
    854866
    855         adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields)
     867        adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj),
     868            self.prepopulated_fields, self.get_readonly_fields(request, obj))
    856869        media = self.media + adminForm.media
    857870
    858871        inline_admin_formsets = []
    859872        for inline, formset in zip(self.inline_instances, formsets):
    860873            fieldsets = list(inline.get_fieldsets(request, obj))
    861             inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets)
     874            readonly = list(inline.get_readonly_fields(request, obj))
     875            inline_admin_formset = helpers.InlineAdminFormSet(inline, formset,
     876                fieldsets, readonly)
    862877            inline_admin_formsets.append(inline_admin_formset)
    863878            media = media + inline_admin_formset.media
    864879
     
    11501165            exclude = []
    11511166        else:
    11521167            exclude = list(self.exclude)
     1168        exclude.extend(kwargs.get("exclude", []))
     1169        exclude.extend(self.get_readonly_fields(request, obj))
    11531170        # if exclude is an empty list we use None, since that's the actual
    11541171        # default
     1172        exclude = exclude or None
    11551173        defaults = {
    11561174            "form": self.form,
    11571175            "formset": self.formset,
    11581176            "fk_name": self.fk_name,
    11591177            "fields": fields,
    1160             "exclude": (exclude + kwargs.get("exclude", [])) or None,
     1178            "exclude": exclude,
    11611179            "formfield_callback": curry(self.formfield_for_dbfield, request=request),
    11621180            "extra": self.extra,
    11631181            "max_num": self.max_num,
     
    11691187        if self.declared_fieldsets:
    11701188            return self.declared_fieldsets
    11711189        form = self.get_formset(request).form
    1172         return [(None, {'fields': form.base_fields.keys()})]
     1190        fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj))
     1191        return [(None, {'fields': fields})]
    11731192
    11741193class StackedInline(InlineModelAdmin):
    11751194    template = 'admin/edit_inline/stacked.html'
  • django/contrib/admin/validation.py

     
    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
  • django/contrib/admin/templates/admin/includes/fieldset.html

     
    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>
  • docs/ref/contrib/admin/index.txt

     
    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.
     
    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

     
    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

     
    11# -*- coding: utf-8 -*-
     2import datetime
    23import tempfile
    34import os
     5
     6from django.contrib import admin
    47from django.core.files.storage import FileSystemStorage
     8from django.core.mail import EmailMessage
    59from django.db import models
    6 from django.contrib import admin
    7 from django.core.mail import EmailMessage
    810
     11
    912class Section(models.Model):
    1013    """
    1114    A simple section that links to articles, to test linking to related items
     
    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])
     
    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

     
    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']
     
    16271623        "Check the never-cache status of the Javascript i18n view"
    16281624        response = self.client.get('/test_admin/jsi18n/')
    16291625        self.failUnlessEqual(get_max_age(response), None)
     1626
     1627
     1628class ReadonlyTest(TestCase):
     1629    fixtures = ['admin-views-users.xml']
     1630
     1631    def setUp(self):
     1632        self.client.login(username='super', password='secret')
     1633
     1634    def tearDown(self):
     1635        self.client.logout()
     1636
     1637    def test_readonly_get(self):
     1638        response = self.client.get('/test_admin/admin/admin_views/post/add/')
     1639        self.assertEqual(response.status_code, 200)
     1640        self.assertNotContains(response, 'name="posted"')
     1641        # 3 fields + 2 submit buttons
     1642        self.assertEqual(response.content.count("input"), 5)
     1643        self.assertContains(response, str(datetime.date.today()))
     1644   
     1645    def test_readonly_post(self):
     1646        data = {
     1647            "title": "Django Got Readonly Fields",
     1648            "content": "This is an incredible development."
     1649        }
     1650        response = self.client.post('/test_admin/admin/admin_views/post/add/', data)
     1651        self.assertEqual(response.status_code, 302)
     1652        self.assertEqual(Post.objects.count(), 1)
     1653        p = Post.objects.get()
     1654        self.assertEqual(p.posted, datetime.date.today())
     1655       
     1656        data["posted"] = "10-8-1990" # some date that's not today
     1657        response = self.client.post('/test_admin/admin/admin_views/post/add/', data)
     1658        self.assertEqual(response.status_code, 302)
     1659        self.assertEqual(Post.objects.count(), 2)
     1660        p = Post.objects.order_by('-id')[0]
     1661        self.assertEqual(p.posted, datetime.date.today())
Back to Top