Ticket #342: readonly-admin.3.diff
File readonly-admin.3.diff, 18.5 KB (added by , 15 years ago) |
---|
-
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 2 1 from django import forms 3 2 from django.conf import settings 4 from django.utils.html import escape5 from django.utils.safestring import mark_safe6 from django.utils.encoding import force_unicode7 3 from django.contrib.admin.util import flatten_fieldsets 8 4 from django.contrib.contenttypes.models import ContentType 5 from django.forms.util import flatatt 6 from django.utils.encoding import force_unicode 7 from django.utils.html import escape 8 from django.utils.safestring import mark_safe 9 from django.utils.text import capfirst 9 10 from django.utils.translation import ugettext_lazy as _ 10 11 12 11 13 ACTION_CHECKBOX_NAME = '_selected_action' 12 14 13 15 class ActionForm(forms.Form): … … class ActionForm(forms.Form): 16 18 checkbox = forms.CheckboxInput({'class': 'action-select'}, lambda value: False) 17 19 18 20 class AdminForm(object): 19 def __init__(self, form, fieldsets, prepopulated_fields ):21 def __init__(self, form, fieldsets, prepopulated_fields, readonly_fields): 20 22 self.form, self.fieldsets = form, normalize_fieldsets(fieldsets) 21 23 self.prepopulated_fields = [{ 22 24 'field': form[field_name], 23 25 'dependencies': [form[f] for f in dependencies] 24 26 } for field_name, dependencies in prepopulated_fields.items()] 27 self.readonly_fields = readonly_fields 25 28 26 29 def __iter__(self): 27 30 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) 29 33 30 34 def first_field(self): 31 35 try: … … class AdminForm(object): 49 53 media = property(_media) 50 54 51 55 class 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): 53 58 self.form = form 54 59 self.name, self.fields = name, fields 55 60 self.classes = u' '.join(classes) 56 61 self.description = description 62 self.readonly_fields = readonly_fields 57 63 58 64 def _media(self): 59 65 if 'collapse' in self.classes: … … class Fieldset(object): 63 69 64 70 def __iter__(self): 65 71 for field in self.fields: 66 yield Fieldline(self.form, field )72 yield Fieldline(self.form, field, self.readonly_fields) 67 73 68 74 class Fieldline(object): 69 def __init__(self, form, field ):75 def __init__(self, form, field, readonly_fields): 70 76 self.form = form # A django.forms.Form instance 71 77 if isinstance(field, basestring): 72 78 self.fields = [field] 73 79 else: 74 80 self.fields = field 81 self.readonly_fields = readonly_fields 75 82 76 83 def __iter__(self): 77 84 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)) 79 89 80 90 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')) 82 92 83 93 class AdminField(object): 84 94 def __init__(self, form, field, is_first): … … class AdminField(object): 100 110 attrs = classes and {'class': u' '.join(classes)} or {} 101 111 return self.field.label_tag(contents=contents, attrs=attrs) 102 112 113 class 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 103 134 class InlineAdminFormSet(object): 104 135 """ 105 136 A wrapper around an inline formset for use in the admin system. 106 137 """ 107 def __init__(self, inline, formset, fieldsets ):138 def __init__(self, inline, formset, fieldsets, readonly_fields): 108 139 self.opts = inline 109 140 self.formset = formset 110 141 self.fieldsets = fieldsets 142 self.readonly_fields = readonly_fields 111 143 112 144 def __iter__(self): 113 145 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) 115 148 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) 117 151 118 152 def fields(self): 119 153 fk = getattr(self.formset, "fk", None) … … class InlineAdminForm(AdminForm): 133 167 """ 134 168 A wrapper around an inline form for use in the admin system. 135 169 """ 136 def __init__(self, formset, form, fieldsets, prepopulated_fields, original): 170 def __init__(self, formset, form, fieldsets, prepopulated_fields, original, 171 readonly_fields): 137 172 self.formset = formset 138 173 self.original = original 139 174 if original is not None: 140 175 self.original_content_type_id = ContentType.objects.get_for_model(original).pk 141 176 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) 143 179 144 180 def __iter__(self): 145 181 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) 147 183 148 184 def has_auto_field(self): 149 185 if self.form._meta.model._meta.has_auto_field: … … class InlineFieldset(Fieldset): 194 230 for field in self.fields: 195 231 if fk and fk.name == field: 196 232 continue 197 yield Fieldline(self.form, field )233 yield Fieldline(self.form, field, self.readonly_fields) 198 234 199 235 class AdminErrorList(forms.util.ErrorList): 200 236 """ -
django/contrib/admin/options.py
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index 3144a22..57000b8 100644
a b class BaseModelAdmin(object): 64 64 radio_fields = {} 65 65 prepopulated_fields = {} 66 66 formfield_overrides = {} 67 readonly_fields = () 67 68 68 69 def __init__(self): 69 70 self.formfield_overrides = dict(FORMFIELD_FOR_DBFIELD_DEFAULTS, **self.formfield_overrides) … … class BaseModelAdmin(object): 171 172 return [(None, {'fields': self.fields})] 172 173 return None 173 174 declared_fieldsets = property(_declared_fieldsets) 175 176 def get_readonly_fields(self, request, obj=None): 177 return self.readonly_fields 174 178 175 179 class ModelAdmin(BaseModelAdmin): 176 180 "Encapsulates all admin options and functionality for a given model." … … class ModelAdmin(BaseModelAdmin): 321 325 if self.declared_fieldsets: 322 326 return self.declared_fieldsets 323 327 form = self.get_form(request, obj) 324 return [(None, {'fields': form.base_fields.keys()})] 328 fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj)) 329 return [(None, {'fields': fields})] 325 330 326 331 def get_form(self, request, obj=None, **kwargs): 327 332 """ … … class ModelAdmin(BaseModelAdmin): 336 341 exclude = [] 337 342 else: 338 343 exclude = list(self.exclude) 344 exclude.extend(kwargs.get("exclude", [])) 345 exclude.extend(self.get_readonly_fields(request, obj)) 339 346 # if exclude is an empty list we pass None to be consistant with the 340 347 # default on modelform_factory 348 exclude = exclude or None 341 349 defaults = { 342 350 "form": self.form, 343 351 "fields": fields, 344 "exclude": (exclude + kwargs.get("exclude", [])) or None,352 "exclude": exclude, 345 353 "formfield_callback": curry(self.formfield_for_dbfield, request=request), 346 354 } 347 355 defaults.update(kwargs) … … class ModelAdmin(BaseModelAdmin): 759 767 formset = FormSet(instance=self.model(), prefix=prefix) 760 768 formsets.append(formset) 761 769 762 adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), self.prepopulated_fields) 770 adminForm = helpers.AdminForm(form, list(self.get_fieldsets(request)), 771 self.prepopulated_fields, self.get_readonly_fields(request) 772 ) 763 773 media = self.media + adminForm.media 764 774 765 775 inline_admin_formsets = [] 766 776 for inline, formset in zip(self.inline_instances, formsets): 767 777 fieldsets = list(inline.get_fieldsets(request)) 768 inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets) 778 readonly = list(inline.get_readonly_fields(request)) 779 inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, 780 fieldsets, readonly) 769 781 inline_admin_formsets.append(inline_admin_formset) 770 782 media = media + inline_admin_formset.media 771 783 … … class ModelAdmin(BaseModelAdmin): 847 859 formset = FormSet(instance=obj, prefix=prefix) 848 860 formsets.append(formset) 849 861 850 adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), self.prepopulated_fields) 862 adminForm = helpers.AdminForm(form, self.get_fieldsets(request, obj), 863 self.prepopulated_fields, self.get_readonly_fields(request, obj)) 851 864 media = self.media + adminForm.media 852 865 853 866 inline_admin_formsets = [] 854 867 for inline, formset in zip(self.inline_instances, formsets): 855 868 fieldsets = list(inline.get_fieldsets(request, obj)) 856 inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, fieldsets) 869 readonly = list(inline.get_readonly_fields(request, obj)) 870 inline_admin_formset = helpers.InlineAdminFormSet(inline, formset, 871 fieldsets, readonly) 857 872 inline_admin_formsets.append(inline_admin_formset) 858 873 media = media + inline_admin_formset.media 859 874 … … class InlineModelAdmin(BaseModelAdmin): 1144 1159 exclude = [] 1145 1160 else: 1146 1161 exclude = list(self.exclude) 1162 exclude.extend(kwargs.get("exclude", [])) 1163 exclude.extend(self.get_readonly_fields(request, obj)) 1147 1164 # if exclude is an empty list we use None, since that's the actual 1148 1165 # default 1166 exclude = exclude or None 1149 1167 defaults = { 1150 1168 "form": self.form, 1151 1169 "formset": self.formset, 1152 1170 "fk_name": self.fk_name, 1153 1171 "fields": fields, 1154 "exclude": (exclude + kwargs.get("exclude", [])) or None,1172 "exclude": exclude, 1155 1173 "formfield_callback": curry(self.formfield_for_dbfield, request=request), 1156 1174 "extra": self.extra, 1157 1175 "max_num": self.max_num, … … class InlineModelAdmin(BaseModelAdmin): 1163 1181 if self.declared_fieldsets: 1164 1182 return self.declared_fieldsets 1165 1183 form = self.get_formset(request).form 1166 return [(None, {'fields': form.base_fields.keys()})] 1184 fields = form.base_fields.keys() + list(self.get_readonly_fields(request, obj)) 1185 return [(None, {'fields': fields})] 1167 1186 1168 1187 class StackedInline(InlineModelAdmin): 1169 1188 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 1 1 <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 %} 19 28 </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): 122 122 if '__' in field: 123 123 continue 124 124 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 125 136 126 137 # list_select_related = False 127 138 # 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``:: 540 540 class ArticleAdmin(admin.ModelAdmin): 541 541 raw_id_fields = ("newspaper",) 542 542 543 .. attribute:: ModelAdmin.readonly_fields 544 545 By default any field Django's admin displays is shown as editable. Any fields 546 in this option (which should be a ``list`` or ``tuple``) will be displayed as 547 just showing the data they contain, without the ability to be edited. Note 548 that fields in this option shouldn't be in the ``fields`` or ``exclude`` 549 options, they can however be in the ``fieldsets`` option to control where they 550 appear. 551 543 552 .. attribute:: ModelAdmin.save_as 544 553 545 554 Set ``save_as`` to enable a "save as" feature on admin change forms. … … model instance:: 744 753 instance.save() 745 754 formset.save_m2m() 746 755 756 .. method:: ModelAdmin.get_readonly_fields(self, request, obj=None) 757 758 The ``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 761 described above in the ``ModelAdmin.readonly_fields`` section. Note that this 762 method will be called several times during a given request, therefore if it 763 performs and very expensive calculations it may be wise to cache them. 764 747 765 .. method:: ModelAdmin.get_urls(self) 748 766 749 767 .. 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 95 95 96 96 >>> validate_inline(TwoAlbumFKAndAnEInline, None, Album) 97 97 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) 107 Traceback (most recent call last): 108 ... 109 ImproperlyConfigured: 'SongAdmin.readonly_fields[1]' refers to a field, 'nonexistant', not defined on Song. 110 111 98 112 """} -
tests/regressiontests/admin_views/models.py
diff --git a/tests/regressiontests/admin_views/models.py b/tests/regressiontests/admin_views/models.py index 50bc05e..792d117 100644
a b 1 1 # -*- coding: utf-8 -*- 2 2 import tempfile 3 3 import os 4 from django.core.files.storage import FileSystemStorage 5 from django.db import models 4 6 5 from django.contrib import admin 6 from django.core.files.storage import FileSystemStorage 7 7 from django.core.mail import EmailMessage 8 from django.db import models 9 8 10 9 11 class Section(models.Model): 10 12 """