Code

Ticket #8306: admin-field-overrides.patch

File admin-field-overrides.patch, 17.9 KB (added by jacob, 5 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