Ticket #342: readonly-admin.6.diff
File readonly-admin.6.diff, 22.1 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..5a985ec 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(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 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 7193bee..8d605ea 100644
a b class BaseModelAdmin(object): 66 66 radio_fields = {} 67 67 prepopulated_fields = {} 68 68 formfield_overrides = {} 69 readonly_fields = () 69 70 70 71 def __init__(self): 71 72 self.formfield_overrides = dict(FORMFIELD_FOR_DBFIELD_DEFAULTS, **self.formfield_overrides) … … class BaseModelAdmin(object): 174 175 return [(None, {'fields': self.fields})] 175 176 return None 176 177 declared_fieldsets = property(_declared_fieldsets) 178 179 def get_readonly_fields(self, request, obj=None): 180 return self.readonly_fields 177 181 178 182 class ModelAdmin(BaseModelAdmin): 179 183 "Encapsulates all admin options and functionality for a given model." … … class ModelAdmin(BaseModelAdmin): 324 328 if self.declared_fieldsets: 325 329 return self.declared_fieldsets 326 330 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})] 328 333 329 334 def get_form(self, request, obj=None, **kwargs): 330 335 """ … … class ModelAdmin(BaseModelAdmin): 339 344 exclude = [] 340 345 else: 341 346 exclude = list(self.exclude) 347 exclude.extend(kwargs.get("exclude", [])) 348 exclude.extend(self.get_readonly_fields(request, obj)) 342 349 # if exclude is an empty list we pass None to be consistant with the 343 350 # default on modelform_factory 351 exclude = exclude or None 344 352 defaults = { 345 353 "form": self.form, 346 354 "fields": fields, 347 "exclude": (exclude + kwargs.get("exclude", [])) or None,355 "exclude": exclude, 348 356 "formfield_callback": curry(self.formfield_for_dbfield, request=request), 349 357 } 350 358 defaults.update(kwargs) … … class ModelAdmin(BaseModelAdmin): 764 772 formset = FormSet(instance=self.model(), prefix=prefix) 765 773 formsets.append(formset) 766 774 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 ) 768 778 media = self.media + adminForm.media 769 779 770 780 inline_admin_formsets = [] 771 781 for inline, formset in zip(self.inline_instances, formsets): 772 782 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) 774 786 inline_admin_formsets.append(inline_admin_formset) 775 787 media = media + inline_admin_formset.media 776 788 … … class ModelAdmin(BaseModelAdmin): 853 865 formset = FormSet(instance=obj, prefix=prefix) 854 866 formsets.append(formset) 855 867 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)) 857 870 media = self.media + adminForm.media 858 871 859 872 inline_admin_formsets = [] 860 873 for inline, formset in zip(self.inline_instances, formsets): 861 874 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) 863 878 inline_admin_formsets.append(inline_admin_formset) 864 879 media = media + inline_admin_formset.media 865 880 … … class InlineModelAdmin(BaseModelAdmin): 1151 1166 exclude = [] 1152 1167 else: 1153 1168 exclude = list(self.exclude) 1169 exclude.extend(kwargs.get("exclude", [])) 1170 exclude.extend(self.get_readonly_fields(request, obj)) 1154 1171 # if exclude is an empty list we use None, since that's the actual 1155 1172 # default 1173 exclude = exclude or None 1156 1174 defaults = { 1157 1175 "form": self.form, 1158 1176 "formset": self.formset, 1159 1177 "fk_name": self.fk_name, 1160 1178 "fields": fields, 1161 "exclude": (exclude + kwargs.get("exclude", [])) or None,1179 "exclude": exclude, 1162 1180 "formfield_callback": curry(self.formfield_for_dbfield, request=request), 1163 1181 "extra": self.extra, 1164 1182 "max_num": self.max_num, … … class InlineModelAdmin(BaseModelAdmin): 1170 1188 if self.declared_fieldsets: 1171 1189 return self.declared_fieldsets 1172 1190 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})] 1174 1193 1175 1194 class StackedInline(InlineModelAdmin): 1176 1195 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 726da65..ace2297 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 0f746bf..36a7d9e 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 eb53a9d..760469b 100644
a b Exception: <class 'regressiontests.admin_validation.models.TwoAlbumFKAndAnE'> ha 110 110 111 111 >>> validate_inline(TwoAlbumFKAndAnEInline, None, Album) 112 112 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) 122 Traceback (most recent call last): 123 ... 124 ImproperlyConfigured: 'SongAdmin.readonly_fields[1]' refers to a field, 'nonexistant', not defined on Song. 125 113 126 # Regression test for #12203/#12237 - Fail more gracefully when a M2M field that 114 127 # specifies the 'through' option is included in the 'fields' or the 'fieldsets' 115 128 # 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 1 1 # -*- coding: utf-8 -*- 2 import datetime 2 3 import tempfile 3 4 import os 4 from django.core.files.storage import FileSystemStorage 5 from django.db import models 5 6 6 from django.contrib import admin 7 from django.core.files.storage import FileSystemStorage 7 8 from django.core.mail import EmailMessage 9 from django.db import models 10 8 11 9 12 class Section(models.Model): 10 13 """ … … class CategoryInline(admin.StackedInline): 420 423 class CollectorAdmin(admin.ModelAdmin): 421 424 inlines = [WidgetInline, DooHickeyInline, GrommetInline, WhatsitInline, FancyDoodadInline, CategoryInline] 422 425 426 class Post(models.Model): 427 title = models.CharField(max_length=100) 428 content = models.TextField() 429 posted = models.DateField(default=datetime.date.today) 430 431 class PostAdmin(admin.ModelAdmin): 432 readonly_fields = ('posted',) 433 423 434 admin.site.register(Article, ArticleAdmin) 424 435 admin.site.register(CustomArticle, CustomArticleAdmin) 425 436 admin.site.register(Section, save_as=True, inlines=[ArticleInline]) … … admin.site.register(Recommendation, RecommendationAdmin) 443 454 admin.site.register(Recommender) 444 455 admin.site.register(Collector, CollectorAdmin) 445 456 admin.site.register(Category, CategoryAdmin) 457 admin.site.register(Post, PostAdmin) 446 458 447 459 # We intentionally register Promo and ChapterXtra1 but not Chapter nor ChapterXtra2. 448 460 # 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, \ 18 18 ExternalSubscriber, FooAccount, Gallery, ModelWithStringPrimaryKey, \ 19 19 Person, Persona, Picture, Podcast, Section, Subscriber, Vodcast, \ 20 20 Language, Collector, Widget, Grommet, DooHickey, FancyDoodad, Whatsit, \ 21 Category 21 Category, Post 22 22 23 try:24 set25 except NameError:26 from sets import Set as set27 23 28 24 class AdminViewBasicTest(TestCase): 29 25 fixtures = ['admin-views-users.xml', 'admin-views-colors.xml', 'admin-views-fabrics.xml'] … … class NeverCacheTests(TestCase): 1633 1629 "Check the never-cache status of the Javascript i18n view" 1634 1630 response = self.client.get('/test_admin/jsi18n/') 1635 1631 self.failUnlessEqual(get_max_age(response), None) 1632 1633 1634 class 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())