Ticket #8306: admin-field-overrides.patch
File admin-field-overrides.patch, 17.9 KB (added by , 16 years ago) |
---|
-
django/contrib/admin/options.py
diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py index bb51875..d35d963 100644
a b get_ul_class = lambda x: 'radiolist%s' % ((x == HORIZONTAL) and ' inline' or '') 28 28 class IncorrectLookupParameters(Exception): 29 29 pass 30 30 31 # Defaults for dbfield_formfield_overrides. ModelAdmin subclasses can change this 32 # by adding to ModelAdmin.dbfield_formfield_overrides. 33 34 FORMFIELD_FOR_DBFIELD_DEFAULTS = { 35 models.DateTimeField: { 36 'form_class': forms.SplitDateTimeField, 37 'widget': widgets.AdminSplitDateTime 38 }, 39 models.DateField: {'widget': widgets.AdminDateWidget}, 40 models.TimeField: {'widget': widgets.AdminTimeWidget}, 41 models.TextField: {'widget': widgets.AdminTextareaWidget}, 42 models.URLField: {'widget': widgets.AdminURLFieldWidget}, 43 models.IntegerField: {'widget': widgets.AdminIntegerFieldWidget}, 44 models.CharField: {'widget': widgets.AdminTextInputWidget}, 45 models.ImageField: {'widget': widgets.AdminFileWidget}, 46 models.FileField: {'widget': widgets.AdminFileWidget}, 47 } 48 49 31 50 class BaseModelAdmin(object): 32 51 """Functionality common to both ModelAdmin and InlineAdmin.""" 52 33 53 raw_id_fields = () 34 54 fields = None 35 55 exclude = None … … class BaseModelAdmin(object): 39 59 filter_horizontal = () 40 60 radio_fields = {} 41 61 prepopulated_fields = {} 62 formfield_overrides = {} 63 64 def __init__(self): 65 self.formfield_overrides = dict(FORMFIELD_FOR_DBFIELD_DEFAULTS, **self.formfield_overrides) 42 66 43 67 def formfield_for_dbfield(self, db_field, **kwargs): 44 68 """ … … class BaseModelAdmin(object): 51 75 # If the field specifies choices, we don't need to look for special 52 76 # admin widgets - we just need to use a select widget of some kind. 53 77 if db_field.choices: 54 if db_field.name in self.radio_fields: 55 # If the field is named as a radio_field, use a RadioSelect 78 return self.formfield_for_choice_field(db_field, **kwargs) 79 80 # ForeignKey or ManyToManyFields 81 if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)): 82 # Combine the field kwargs with any options for formfield_overrides. 83 # Make sure the passed in **kwargs override anything in 84 # formfield_overrides because **kwargs is more specific, and should 85 # always win. 86 if db_field.__class__ in self.formfield_overrides: 87 kwargs = dict(self.formfield_overrides[db_field.__class__], **kwargs) 88 89 # Get the correct formfield. 90 if isinstance(db_field, models.ForeignKey): 91 formfield = self.formfield_for_foreignkey(db_field, **kwargs) 92 elif isinstance(db_field, models.ManyToManyField): 93 formfield = self.formfield_for_manytomany(db_field, **kwargs) 94 95 # For non-raw_id fields, wrap the widget with a wrapper that adds 96 # extra HTML -- the "add other" interface -- to the end of the 97 # rendered output. formfield can be None if it came from a 98 # OneToOneField with parent_link=True or a M2M intermediary. 99 if formfield and db_field.name not in self.raw_id_fields: 100 formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site) 101 102 return formfield 103 104 # If we've got overrides for the formfield defined, use 'em. **kwargs 105 # passed to formfield_for_dbfield override the defaults. 106 if db_field.__class__ in self.formfield_overrides: 107 kwargs = dict(self.formfield_overrides[db_field.__class__], **kwargs) 108 return db_field.formfield(**kwargs) 109 110 # For any other type of field, just call its formfield() method. 111 return db_field.formfield(**kwargs) 112 113 def formfield_for_choice_field(self, db_field, **kwargs): 114 """ 115 Get a form Field for a database Field that has declared choices. 116 """ 117 # If the field is named as a radio_field, use a RadioSelect 118 if db_field.name in self.radio_fields: 119 # Avoid stomping on custom widget/choices arguments. 120 if 'widget' not in kwargs: 56 121 kwargs['widget'] = widgets.AdminRadioSelect(attrs={ 57 122 'class': get_ul_class(self.radio_fields[db_field.name]), 58 123 }) 124 if 'choices' not in kwargs: 59 125 kwargs['choices'] = db_field.get_choices( 60 126 include_blank = db_field.blank, 61 127 blank_choice=[('', _('None'))] 62 128 ) 63 return db_field.formfield(**kwargs) 64 else: 65 # Otherwise, use the default select widget. 66 return db_field.formfield(**kwargs) 67 68 # For DateTimeFields, use a special field and widget. 69 if isinstance(db_field, models.DateTimeField): 70 kwargs['form_class'] = forms.SplitDateTimeField 71 kwargs['widget'] = widgets.AdminSplitDateTime() 72 return db_field.formfield(**kwargs) 73 74 # For DateFields, add a custom CSS class. 75 if isinstance(db_field, models.DateField): 76 kwargs['widget'] = widgets.AdminDateWidget 77 return db_field.formfield(**kwargs) 78 79 # For TimeFields, add a custom CSS class. 80 if isinstance(db_field, models.TimeField): 81 kwargs['widget'] = widgets.AdminTimeWidget 82 return db_field.formfield(**kwargs) 83 84 # For TextFields, add a custom CSS class. 85 if isinstance(db_field, models.TextField): 86 kwargs['widget'] = widgets.AdminTextareaWidget 87 return db_field.formfield(**kwargs) 88 89 # For URLFields, add a custom CSS class. 90 if isinstance(db_field, models.URLField): 91 kwargs['widget'] = widgets.AdminURLFieldWidget 92 return db_field.formfield(**kwargs) 93 94 # For IntegerFields, add a custom CSS class. 95 if isinstance(db_field, models.IntegerField): 96 kwargs['widget'] = widgets.AdminIntegerFieldWidget 97 return db_field.formfield(**kwargs) 98 99 # For CommaSeparatedIntegerFields, add a custom CSS class. 100 if isinstance(db_field, models.CommaSeparatedIntegerField): 101 kwargs['widget'] = widgets.AdminCommaSeparatedIntegerFieldWidget 102 return db_field.formfield(**kwargs) 103 104 # For TextInputs, add a custom CSS class. 105 if isinstance(db_field, models.CharField): 106 kwargs['widget'] = widgets.AdminTextInputWidget 107 return db_field.formfield(**kwargs) 129 return db_field.formfield(**kwargs) 130 131 def formfield_for_foreignkey(self, db_field, **kwargs): 132 """ 133 Get a form Field for a ForeignKey. 134 """ 135 if db_field.name in self.raw_id_fields: 136 kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel) 137 elif db_field.name in self.radio_fields: 138 kwargs['widget'] = widgets.AdminRadioSelect(attrs={ 139 'class': get_ul_class(self.radio_fields[db_field.name]), 140 }) 141 kwargs['empty_label'] = db_field.blank and _('None') or None 108 142 109 # For FileFields and ImageFields add a link to the current file. 110 if isinstance(db_field, models.ImageField) or isinstance(db_field, models.FileField): 111 kwargs['widget'] = widgets.AdminFileWidget 112 return db_field.formfield(**kwargs) 143 return db_field.formfield(**kwargs) 144 145 def formfield_for_manytomany(self, db_field, **kwargs): 146 """ 147 Get a form Field for a ManyToManyField. 148 """ 149 # If it uses an intermediary model, don't show field in admin. 150 if db_field.rel.through is not None: 151 return None 113 152 114 # For ForeignKey or ManyToManyFields, use a special widget. 115 if isinstance(db_field, (models.ForeignKey, models.ManyToManyField)): 116 if isinstance(db_field, models.ForeignKey) and db_field.name in self.raw_id_fields: 117 kwargs['widget'] = widgets.ForeignKeyRawIdWidget(db_field.rel) 118 elif isinstance(db_field, models.ForeignKey) and db_field.name in self.radio_fields: 119 kwargs['widget'] = widgets.AdminRadioSelect(attrs={ 120 'class': get_ul_class(self.radio_fields[db_field.name]), 121 }) 122 kwargs['empty_label'] = db_field.blank and _('None') or None 123 else: 124 if isinstance(db_field, models.ManyToManyField): 125 # If it uses an intermediary model, don't show field in admin. 126 if db_field.rel.through is not None: 127 return None 128 elif db_field.name in self.raw_id_fields: 129 kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel) 130 kwargs['help_text'] = '' 131 elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)): 132 kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical)) 133 # Wrap the widget's render() method with a method that adds 134 # extra HTML to the end of the rendered output. 135 formfield = db_field.formfield(**kwargs) 136 # Don't wrap raw_id fields. Their add function is in the popup window. 137 if not db_field.name in self.raw_id_fields: 138 # formfield can be None if it came from a OneToOneField with 139 # parent_link=True 140 if formfield is not None: 141 formfield.widget = widgets.RelatedFieldWidgetWrapper(formfield.widget, db_field.rel, self.admin_site) 142 return formfield 153 if db_field.name in self.raw_id_fields: 154 kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel) 155 kwargs['help_text'] = '' 156 elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)): 157 kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical)) 143 158 144 # For any other type of field, just call its formfield() method.145 159 return db_field.formfield(**kwargs) 146 160 147 161 def _declared_fieldsets(self): -
docs/ref/contrib/admin.txt
diff --git a/docs/ref/contrib/admin.txt b/docs/ref/contrib/admin.txt index a50aa13..178e9c0 100644
a b with an operator: 597 597 Performs a full-text match. This is like the default search method but uses 598 598 an index. Currently this is only available for MySQL. 599 599 600 ``formfield_overrides`` 601 ~~~~~~~~~~~~~~~~~~~~~~~ 602 603 This provides a quick-and-dirty way to override some of the 604 :class:`~django.forms.Field` options for use in the admin. 605 ``formfield_overrides`` is a dictionary mapping a field class to a dict of 606 arguments to pass to the field at construction time. 607 608 Since that's a bit abstract, let's look at a concrete example. The most common 609 use of ``formfield_overrides`` is to add a custom widget for a certain type of 610 field. So, imagine we've written a ``RichTextEditorWidget`` that we'd like to 611 use for large text fields instead of the default ``<textarea>``. Here's how we'd 612 do that:: 613 614 from django.db import models 615 from django.contrib import admin 616 617 # Import our custom widget and our model from where they're defined 618 from myapp.widgets import RichTextEditorWidget 619 from myapp.models import MyModel 620 621 class MyModelAdmin(admin.ModelAdmin): 622 formfield_overrides = { 623 models.TextField: {'widget': RichTextEditorWidget}, 624 } 625 626 Note that the key in the dictionary is the actual field class, *not* a string. 627 The value is another dictionary; these arguments will be passed to 628 :meth:`~django.forms.Field.__init__`. See :ref:`ref-forms-api` for details. 629 630 .. warning:: 631 632 If you want to use a custom widget with a relation field (i.e. 633 :class:`~django.db.models.ForeignKey` or 634 :class:`~django.db.models.ManyToManyField`), make sure you haven't included 635 that field's name in ``raw_id_fields`` or ``radio_fields``. 636 637 ``formfield_overrides`` won't let you change the widget on relation fields 638 that have ``raw_id_fields`` or ``radio_fields`` set. That's because 639 ``raw_id_fields`` and ``radio_fields`` imply custom widgets of their own. 640 600 641 ``ModelAdmin`` methods 601 642 ---------------------- 602 643 -
tests/regressiontests/admin_widgets/models.py
diff --git a/tests/regressiontests/admin_widgets/models.py b/tests/regressiontests/admin_widgets/models.py index ddfc6c2..35d4b5b 100644
a b from django.core.files.storage import default_storage 5 5 6 6 class Member(models.Model): 7 7 name = models.CharField(max_length=100) 8 birthdate = models.DateTimeField(blank=True, null=True) 9 gender = models.CharField(max_length=1, blank=True, choices=[('M','Male'), ('F', 'Female')]) 8 10 9 11 def __unicode__(self): 10 12 return self.name … … class Inventory(models.Model): 40 42 41 43 def __unicode__(self): 42 44 return self.name 45 46 class Event(models.Model): 47 band = models.ForeignKey(Band) 48 date = models.DateField(blank=True, null=True) 49 start_time = models.TimeField(blank=True, null=True) 50 description = models.TextField(blank=True) 51 link = models.URLField(blank=True) 52 min_age = models.IntegerField(blank=True, null=True) 43 53 44 54 __test__ = {'WIDGETS_TESTS': """ 45 55 >>> from datetime import datetime -
new file tests/regressiontests/admin_widgets/tests.py
diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py new file mode 100644 index 0000000..1043e39
- + 1 from django import forms 2 from django.contrib import admin 3 from django.contrib.admin import widgets 4 from unittest import TestCase 5 import models 6 7 class AdminFormfieldForDBFieldTests(TestCase): 8 """ 9 Tests for correct behavior of ModelAdmin.formfield_for_dbfield 10 """ 11 12 def assertFormfield(self, model, fieldname, widgetclass, **admin_overrides): 13 """ 14 Helper to call formfield_for_dbfield for a given model and field name 15 and verify that the returned formfield is appropriate. 16 """ 17 # Override any settings on the model admin 18 class MyModelAdmin(admin.ModelAdmin): pass 19 for k in admin_overrides: 20 setattr(MyModelAdmin, k, admin_overrides[k]) 21 22 # Construct the admin, and ask it for a formfield 23 ma = MyModelAdmin(model, admin.site) 24 ff = ma.formfield_for_dbfield(model._meta.get_field(fieldname)) 25 26 # "unwrap" the widget wrapper, if needed 27 if isinstance(ff.widget, widgets.RelatedFieldWidgetWrapper): 28 widget = ff.widget.widget 29 else: 30 widget = ff.widget 31 32 # Check that we got a field of the right type 33 self.assert_( 34 isinstance(widget, widgetclass), 35 "Wrong widget for %s.%s: expected %s, got %s" % \ 36 (model.__class__.__name__, fieldname, widgetclass, type(widget)) 37 ) 38 39 # Return the formfield so that other tests can continue 40 return ff 41 42 def testDateField(self): 43 self.assertFormfield(models.Event, 'date', widgets.AdminDateWidget) 44 45 def testDateTimeField(self): 46 self.assertFormfield(models.Member, 'birthdate', widgets.AdminSplitDateTime) 47 48 def testTimeField(self): 49 self.assertFormfield(models.Event, 'start_time', widgets.AdminTimeWidget) 50 51 def testTextField(self): 52 self.assertFormfield(models.Event, 'description', widgets.AdminTextareaWidget) 53 54 def testURLField(self): 55 self.assertFormfield(models.Event, 'link', widgets.AdminURLFieldWidget) 56 57 def testIntegerField(self): 58 self.assertFormfield(models.Event, 'min_age', widgets.AdminIntegerFieldWidget) 59 60 def testCharField(self): 61 self.assertFormfield(models.Member, 'name', widgets.AdminTextInputWidget) 62 63 def testFileField(self): 64 self.assertFormfield(models.Album, 'cover_art', widgets.AdminFileWidget) 65 66 def testForeignKey(self): 67 self.assertFormfield(models.Event, 'band', forms.Select) 68 69 def testRawIDForeignKey(self): 70 self.assertFormfield(models.Event, 'band', widgets.ForeignKeyRawIdWidget, 71 raw_id_fields=['band']) 72 73 def testRadioFieldsForeignKey(self): 74 ff = self.assertFormfield(models.Event, 'band', widgets.AdminRadioSelect, 75 radio_fields={'band':admin.VERTICAL}) 76 self.assertEqual(ff.empty_label, None) 77 78 def testManyToMany(self): 79 self.assertFormfield(models.Band, 'members', forms.SelectMultiple) 80 81 def testRawIDManyTOMany(self): 82 self.assertFormfield(models.Band, 'members', widgets.ManyToManyRawIdWidget, 83 raw_id_fields=['members']) 84 85 def testFilteredManyToMany(self): 86 self.assertFormfield(models.Band, 'members', widgets.FilteredSelectMultiple, 87 filter_vertical=['members']) 88 89 def testFormfieldOverrides(self): 90 self.assertFormfield(models.Event, 'date', forms.TextInput, 91 formfield_overrides={'widget': forms.TextInput}) 92 93 def testFieldWithChoices(self): 94 self.assertFormfield(models.Member, 'gender', forms.Select) 95 96 def testChoicesWithRadioFields(self): 97 self.assertFormfield(models.Member, 'gender', widgets.AdminRadioSelect, 98 radio_fields={'gender':admin.VERTICAL}) 99 100 No newline at end of file