Ticket #342: readonly-admin.5.diff
File readonly-admin.5.diff, 21.4 KB (added by , 15 years ago) |
---|
-
django/contrib/admin/helpers.py
1 2 1 from django import forms 3 2 from django.conf import settings 3 from django.contrib.admin.util import flatten_fieldsets 4 from django.contrib.contenttypes.models import ContentType 5 from django.forms.util import flatatt 6 from django.utils.encoding import force_unicode 4 7 from django.utils.html import escape 5 8 from 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 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): … … 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: … … 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: … … 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): … … 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) … … 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: … … 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
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) … … 173 174 return [(None, {'fields': self.fields})] 174 175 return None 175 176 declared_fieldsets = property(_declared_fieldsets) 177 178 def get_readonly_fields(self, request, obj=None): 179 return self.readonly_fields 176 180 177 181 class ModelAdmin(BaseModelAdmin): 178 182 "Encapsulates all admin options and functionality for a given model." … … 323 327 if self.declared_fieldsets: 324 328 return self.declared_fieldsets 325 329 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})] 327 332 328 333 def get_form(self, request, obj=None, **kwargs): 329 334 """ … … 338 343 exclude = [] 339 344 else: 340 345 exclude = list(self.exclude) 346 exclude.extend(kwargs.get("exclude", [])) 347 exclude.extend(self.get_readonly_fields(request, obj)) 341 348 # if exclude is an empty list we pass None to be consistant with the 342 349 # default on modelform_factory 350 exclude = exclude or None 343 351 defaults = { 344 352 "form": self.form, 345 353 "fields": fields, 346 "exclude": (exclude + kwargs.get("exclude", [])) or None,354 "exclude": exclude, 347 355 "formfield_callback": curry(self.formfield_for_dbfield, request=request), 348 356 } 349 357 defaults.update(kwargs) … … 763 771 formset = FormSet(instance=self.model(), prefix=prefix) 764 772 formsets.append(formset) 765 773 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 ) 767 777 media = self.media + adminForm.media 768 778 769 779 inline_admin_formsets = [] 770 780 for inline, formset in zip(self.inline_instances, formsets): 771 781 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) 773 785 inline_admin_formsets.append(inline_admin_formset) 774 786 media = media + inline_admin_formset.media 775 787 … … 852 864 formset = FormSet(instance=obj, prefix=prefix) 853 865 formsets.append(formset) 854 866 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)) 856 869 media = self.media + adminForm.media 857 870 858 871 inline_admin_formsets = [] 859 872 for inline, formset in zip(self.inline_instances, formsets): 860 873 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) 862 877 inline_admin_formsets.append(inline_admin_formset) 863 878 media = media + inline_admin_formset.media 864 879 … … 1150 1165 exclude = [] 1151 1166 else: 1152 1167 exclude = list(self.exclude) 1168 exclude.extend(kwargs.get("exclude", [])) 1169 exclude.extend(self.get_readonly_fields(request, obj)) 1153 1170 # if exclude is an empty list we use None, since that's the actual 1154 1171 # default 1172 exclude = exclude or None 1155 1173 defaults = { 1156 1174 "form": self.form, 1157 1175 "formset": self.formset, 1158 1176 "fk_name": self.fk_name, 1159 1177 "fields": fields, 1160 "exclude": (exclude + kwargs.get("exclude", [])) or None,1178 "exclude": exclude, 1161 1179 "formfield_callback": curry(self.formfield_for_dbfield, request=request), 1162 1180 "extra": self.extra, 1163 1181 "max_num": self.max_num, … … 1169 1187 if self.declared_fieldsets: 1170 1188 return self.declared_fieldsets 1171 1189 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})] 1173 1192 1174 1193 class StackedInline(InlineModelAdmin): 1175 1194 template = 'admin/edit_inline/stacked.html' -
django/contrib/admin/validation.py
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 -
django/contrib/admin/templates/admin/includes/fieldset.html
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> -
docs/ref/contrib/admin/index.txt
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. … … 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
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
1 1 # -*- coding: utf-8 -*- 2 import datetime 2 3 import tempfile 3 4 import os 5 6 from django.contrib import admin 4 7 from django.core.files.storage import FileSystemStorage 8 from django.core.mail import EmailMessage 5 9 from django.db import models 6 from django.contrib import admin7 from django.core.mail import EmailMessage8 10 11 9 12 class Section(models.Model): 10 13 """ 11 14 A simple section that links to articles, to test linking to related items … … 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]) … … 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
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'] … … 1627 1623 "Check the never-cache status of the Javascript i18n view" 1628 1624 response = self.client.get('/test_admin/jsi18n/') 1629 1625 self.failUnlessEqual(get_max_age(response), None) 1626 1627 1628 class 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())