Ticket #8306: admin-field-overrides.patch

File admin-field-overrides.patch, 17.9 KB (added by jacob, 7 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 '') 
    2828class IncorrectLookupParameters(Exception):
    2929    pass
    3030
     31# Defaults for dbfield_formfield_overrides. ModelAdmin subclasses can change this
     32# by adding to ModelAdmin.dbfield_formfield_overrides.
     33   
     34FORMFIELD_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
    3150class BaseModelAdmin(object):
    3251    """Functionality common to both ModelAdmin and InlineAdmin."""
     52   
    3353    raw_id_fields = ()
    3454    fields = None
    3555    exclude = None
    class BaseModelAdmin(object): 
    3959    filter_horizontal = ()
    4060    radio_fields = {}
    4161    prepopulated_fields = {}
     62    formfield_overrides = {}
     63   
     64    def __init__(self):
     65        self.formfield_overrides = dict(FORMFIELD_FOR_DBFIELD_DEFAULTS, **self.formfield_overrides)
    4266   
    4367    def formfield_for_dbfield(self, db_field, **kwargs):
    4468        """
    class BaseModelAdmin(object): 
    5175        # If the field specifies choices, we don't need to look for special
    5276        # admin widgets - we just need to use a select widget of some kind.
    5377        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:
    56121                kwargs['widget'] = widgets.AdminRadioSelect(attrs={
    57122                    'class': get_ul_class(self.radio_fields[db_field.name]),
    58123                })
     124            if 'choices' not in kwargs:
    59125                kwargs['choices'] = db_field.get_choices(
    60126                    include_blank = db_field.blank,
    61127                    blank_choice=[('', _('None'))]
    62128                )
    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
    108142       
    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
    113152       
    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))
    143158       
    144         # For any other type of field, just call its formfield() method.
    145159        return db_field.formfield(**kwargs)
    146160   
    147161    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: 
    597597    Performs a full-text match. This is like the default search method but uses
    598598    an index. Currently this is only available for MySQL.
    599599
     600``formfield_overrides``
     601~~~~~~~~~~~~~~~~~~~~~~~
     602
     603This 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
     606arguments to pass to the field at construction time.
     607
     608Since that's a bit abstract, let's look at a concrete example. The most common
     609use of ``formfield_overrides`` is to add a custom widget for a certain type of
     610field. So, imagine we've written a ``RichTextEditorWidget`` that we'd like to
     611use for large text fields instead of the default ``<textarea>``. Here's how we'd
     612do 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       
     626Note that the key in the dictionary is the actual field class, *not* a string.
     627The 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
    600641``ModelAdmin`` methods
    601642----------------------
    602643
  • 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 
    55
    66class Member(models.Model):
    77    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')])
    810
    911    def __unicode__(self):
    1012        return self.name
    class Inventory(models.Model): 
    4042
    4143   def __unicode__(self):
    4244      return self.name
     45     
     46class 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)
    4353
    4454__test__ = {'WIDGETS_TESTS': """
    4555>>> 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
    - +  
     1from django import forms
     2from django.contrib import admin
     3from django.contrib.admin import widgets
     4from unittest import TestCase
     5import models
     6
     7class 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
Back to Top