Ticket #342: readonly-admin.4.diff
File readonly-admin.4.diff, 21.9 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 c702e87..43387e5 100644
a b class BaseModelAdmin(object): 65 65 radio_fields = {} 66 66 prepopulated_fields = {} 67 67 formfield_overrides = {} 68 readonly_fields = () 68 69 69 70 def __init__(self): 70 71 self.formfield_overrides = dict(FORMFIELD_FOR_DBFIELD_DEFAULTS, **self.formfield_overrides) … … class BaseModelAdmin(object): 172 173 return [(None, {'fields': self.fields})] 173 174 return None 174 175 declared_fieldsets = property(_declared_fieldsets) 176 177 def get_readonly_fields(self, request, obj=None): 178 return self.readonly_fields 175 179 176 180 class ModelAdmin(BaseModelAdmin): 177 181 "Encapsulates all admin options and functionality for a given model." … … class ModelAdmin(BaseModelAdmin): 322 326 if self.declared_fieldsets: 323 327 return self.declared_fieldsets 324 328 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})] 326 331 327 332 def get_form(self, request, obj=None, **kwargs): 328 333 """ … … class ModelAdmin(BaseModelAdmin): 337 342 exclude = [] 338 343 else: 339 344 exclude = list(self.exclude) 345 exclude.extend(kwargs.get("exclude", [])) 346 exclude.extend(self.get_readonly_fields(request, obj)) 340 347 # if exclude is an empty list we pass None to be consistant with the 341 348 # default on modelform_factory 349 exclude = exclude or None 342 350 defaults = { 343 351 "form": self.form, 344 352 "fields": fields, 345 "exclude": (exclude + kwargs.get("exclude", [])) or None,353 "exclude": exclude, 346 354 "formfield_callback": curry(self.formfield_for_dbfield, request=request), 347 355 } 348 356 defaults.update(kwargs) … … class ModelAdmin(BaseModelAdmin): 762 770 formset = FormSet(instance=self.model(), prefix=prefix) 763 771 formsets.append(formset) 764 772 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 ) 766 776 media = self.media + adminForm.media 767 777 768 778 inline_admin_formsets = [] 769 779 for inline, formset in zip(self.inline_instances, formsets): 770 780 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) 772 784 inline_admin_formsets.append(inline_admin_formset) 773 785 media = media + inline_admin_formset.media 774 786 … … class ModelAdmin(BaseModelAdmin): 851 863 formset = FormSet(instance=obj, prefix=prefix) 852 864 formsets.append(formset) 853 865 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)) 855 868 media = self.media + adminForm.media 856 869 857 870 inline_admin_formsets = [] 858 871 for inline, formset in zip(self.inline_instances, formsets): 859 872 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) 861 876 inline_admin_formsets.append(inline_admin_formset) 862 877 media = media + inline_admin_formset.media 863 878 … … class InlineModelAdmin(BaseModelAdmin): 1149 1164 exclude = [] 1150 1165 else: 1151 1166 exclude = list(self.exclude) 1167 exclude.extend(kwargs.get("exclude", [])) 1168 exclude.extend(self.get_readonly_fields(request, obj)) 1152 1169 # if exclude is an empty list we use None, since that's the actual 1153 1170 # default 1171 exclude = exclude or None 1154 1172 defaults = { 1155 1173 "form": self.form, 1156 1174 "formset": self.formset, 1157 1175 "fk_name": self.fk_name, 1158 1176 "fields": fields, 1159 "exclude": (exclude + kwargs.get("exclude", [])) or None,1177 "exclude": exclude, 1160 1178 "formfield_callback": curry(self.formfield_for_dbfield, request=request), 1161 1179 "extra": self.extra, 1162 1180 "max_num": self.max_num, … … class InlineModelAdmin(BaseModelAdmin): 1168 1186 if self.declared_fieldsets: 1169 1187 return self.declared_fieldsets 1170 1188 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})] 1172 1191 1173 1192 class StackedInline(InlineModelAdmin): 1174 1193 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..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 c7893ec..0ee9dac 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): 1617 1613 "Check the never-cache status of the Javascript i18n view" 1618 1614 response = self.client.get('/test_admin/jsi18n/') 1619 1615 self.failUnlessEqual(get_max_age(response), None) 1616 1617 1618 class 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())