Ticket #9321: 9321+14402.ManyToManyField-help-text.diff

File 9321+14402.ManyToManyField-help-text.diff, 18.7 KB (added by Julien Phalip, 13 years ago)
  • django/contrib/admin/options.py

    diff --git a/django/contrib/admin/options.py b/django/contrib/admin/options.py
    index c603210..bd7b62b 100644
    a b class BaseModelAdmin(object):  
    175175
    176176        if db_field.name in self.raw_id_fields:
    177177            kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel, using=db)
    178             kwargs['help_text'] = ''
    179178        elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)):
    180179            kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical))
    181180
  • django/db/models/fields/related.py

    diff --git a/django/db/models/fields/related.py b/django/db/models/fields/related.py
    index ffa5692..3f2b219 100644
    a b from django.db.models.query import QuerySet  
    99from django.db.models.query_utils import QueryWrapper
    1010from django.db.models.deletion import CASCADE
    1111from django.utils.encoding import smart_unicode
    12 from django.utils.translation import (ugettext_lazy as _, string_concat,
    13     ungettext, ugettext)
     12from django.utils.translation import ugettext_lazy as _, ungettext, ugettext
    1413from django.utils.functional import curry
    1514from django.core import exceptions
    1615from django import forms
    class ManyToManyField(RelatedField, Field):  
    10211020
    10221021        Field.__init__(self, **kwargs)
    10231022
    1024         msg = _('Hold down "Control", or "Command" on a Mac, to select more than one.')
    1025         self.help_text = string_concat(self.help_text, ' ', msg)
    1026 
    10271023    def get_choices_default(self):
    10281024        return Field.get_choices(self, include_blank=False)
    10291025
  • django/forms/fields.py

    diff --git a/django/forms/fields.py b/django/forms/fields.py
    index a5ea81d..7bec9b9 100644
    a b class Field(object):  
    8080            label = smart_unicode(label)
    8181        self.required, self.label, self.initial = required, label, initial
    8282        self.show_hidden_initial = show_hidden_initial
    83         if help_text is None:
    84             self.help_text = u''
    85         else:
    86             self.help_text = smart_unicode(help_text)
    8783        widget = widget or self.widget
    8884        if isinstance(widget, type):
    8985            widget = widget()
    class Field(object):  
    10399
    104100        self.widget = widget
    105101
     102        if help_text is None:
     103            self.help_text = u''
     104        else:
     105            self.help_text = smart_unicode(help_text)
     106
     107        # Allow the widget to alter the help_text
     108        self.help_text = self.widget.alter_help_text(self.help_text)
     109
    106110        # Increase the creation counter, and save our local copy.
    107111        self.creation_counter = Field.creation_counter
    108112        Field.creation_counter += 1
  • django/forms/widgets.py

    diff --git a/django/forms/widgets.py b/django/forms/widgets.py
    index 03152ea..9ac2204 100644
    a b from util import flatatt  
    1212from django.conf import settings
    1313from django.utils.datastructures import MultiValueDict, MergeDict
    1414from django.utils.html import escape, conditional_escape
    15 from django.utils.translation import ugettext, ugettext_lazy
     15from django.utils.translation import ugettext, ugettext_lazy, string_concat
    1616from django.utils.encoding import StrAndUnicode, force_unicode
    17 from django.utils.safestring import mark_safe
     17from django.utils.safestring import mark_safe 
    1818from django.utils import datetime_safe, formats
    1919
    2020__all__ = (
    class Widget(object):  
    155155        memo[id(self)] = obj
    156156        return obj
    157157
     158    def alter_help_text(self, help_text):
     159        """
     160        Usually called by the form field to potentially alter the help text.
     161        """
     162        return help_text
     163   
    158164    def render(self, name, value, attrs=None):
    159165        """
    160166        Returns this Widget rendered as HTML, as a Unicode string.
    class NullBooleanSelect(Select):  
    575581        return initial != data
    576582
    577583class SelectMultiple(Select):
     584   
     585    def alter_help_text(self, help_text):
     586        extra_text = ugettext_lazy('Hold down "Control", or "Command" '
     587                                   'on a Mac, to select more than one.')
     588        return string_concat(help_text, ' ', extra_text)
     589   
    578590    def render(self, name, value, attrs=None, choices=()):
    579591        if value is None: value = []
    580592        final_attrs = self.build_attrs(attrs, name=name)
    class RadioSelect(Select):  
    690702    id_for_label = classmethod(id_for_label)
    691703
    692704class CheckboxSelectMultiple(SelectMultiple):
     705   
     706    def alter_help_text(self, help_text):
     707        # Do not show the 'Hold down "Control"' message that appears
     708        # for SelectMultiple.
     709        return help_text
     710   
    693711    def render(self, name, value, attrs=None, choices=()):
    694712        if value is None: value = []
    695713        has_id = attrs and 'id' in attrs
  • docs/ref/forms/widgets.txt

    diff --git a/docs/ref/forms/widgets.txt b/docs/ref/forms/widgets.txt
    index dbdf109..94f4c07 100644
    a b Django will then include the extra attributes in the rendered output::  
    258258    <tr><th>Name:</th><td><input type="text" name="name" class="special"/></td></tr>
    259259    <tr><th>Url:</th><td><input type="text" name="url"/></td></tr>
    260260    <tr><th>Comment:</th><td><input type="text" name="comment" size="40"/></td></tr>
     261
     262Altering the help text
     263----------------------
     264
     265.. versionadded:: 1.4
     266
     267In some cases you might want to customize a form field's help text depending
     268on the widget used. To do so, simply override the widget's
     269:meth:`~alter_help_text` method::
     270
     271    class MyCustomSelectMultiple(SelectMultiple):
     272       
     273        def alter_help_text(self, help_text):
     274            extra_text = 'You can hold down "Control" to select multiple ones.'
     275            return '%s %s' % (help_text, extra_text)
  • docs/releases/1.4.txt

    diff --git a/docs/releases/1.4.txt b/docs/releases/1.4.txt
    index 1b8510f..61fe723 100644
    a b A new helper function,  
    6060``template.Library`` to ease the creation of template tags that store some
    6161data in a specified context variable.
    6262
     63Widget.alter_help_text
     64~~~~~~~~~~~~~~~~~~~~~~
     65
     66The :meth:`~alter_help_text` method was added to `:class:~forms.widgets.Widget`
     67to allow a widget to customize the help text provided by a form field or a
     68model field. This introduced a backwards incompatible change if you are
     69defining a custom ``SelectMultiple`` widget. See `the notes on backwards
     70incompatible changes`_ below to address this change.
     71
     72.. _the notes on backwards incompatible changes: backwards-incompatible-changes-1.4_
     73
    6374.. _backwards-incompatible-changes-1.4:
    6475
    6576Backwards incompatible changes in 1.4
    you should add the following lines in your settings file::  
    214225
    215226Don't forget to escape characters that have a special meaning in a regular
    216227expression.
     228
     229Custom ``SelectMultiple`` widgets and ``alter_help_text``
     230~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
     231
     232The implementation of the new `meth:~Widget.alter_help_text` method was
     233originally prompted by the fixing of a bug where the 'Hold down "Control", or
     234"Command" on a Mac, to select more than one.' message would systematically be
     235appended to a ``ManyToManyField``'s help text, even if that field were
     236configured to use a different widget than ``SelectMultiple``. Thus, the
     237action of appending that message was transfered from ``ManyToManyField`` to
     238``SelectMultiple`` via the new ``alter_help_text`` method::
     239
     240    class SelectMultiple(Select):
     241   
     242        def alter_help_text(self, help_text):
     243            extra_text = ugettext_lazy('Hold down "Control", or "Command" '
     244                                       'on a Mac, to select more than one.')
     245            return string_concat(help_text, ' ', extra_text)
     246
     247This means that if you are defining a widget inheriting from
     248``SelectMultiple``, then that message will now systematically be appended to
     249the help text, which may not make sense in the context of use for that widget.
     250To cancel this new behaviour, simply override the ``alter_help_text`` method of
     251your widget as is done, for example, by the
     252`:class:~forms.widgets.CheckboxSelectMultiple` class::
     253
     254    class CheckboxSelectMultiple(SelectMultiple):
     255       
     256        def alter_help_text(self, help_text):
     257            return help_text
  • tests/regressiontests/admin_widgets/models.py

    diff --git a/tests/regressiontests/admin_widgets/models.py b/tests/regressiontests/admin_widgets/models.py
    index c187584..2e7a871 100644
    a b class CarTire(models.Model):  
    6767    A single car tire. This to test that a user can only select their own cars.
    6868    """
    6969    car = models.ForeignKey(Car)
     70
     71class Director(models.Model):
     72    name = models.CharField(max_length=40)
     73
     74    def __unicode__(self):
     75        return self.name
     76
     77class Genre(models.Model):
     78    name = models.CharField(max_length=40)
     79
     80    def __unicode__(self):
     81        return self.name
     82
     83class Orchestra(models.Model):
     84    name = models.CharField(max_length=100)
     85    members = models.ManyToManyField(Member, help_text='This is the orchestra members help_text.')
     86    director = models.ForeignKey(Director)
     87    genres = models.ManyToManyField(Genre, help_text='This is the orchestra genres help_text.')
     88
     89    def __unicode__(self):
     90        return self.name
  • tests/regressiontests/admin_widgets/tests.py

    diff --git a/tests/regressiontests/admin_widgets/tests.py b/tests/regressiontests/admin_widgets/tests.py
    index 8f7a36c..0524f86 100644
    a b class ManyToManyRawIdWidgetTest(DjangoTestCase):  
    321321        self.assertEqual(w._has_changed([1, 2], [u'1']), True)
    322322        self.assertEqual(w._has_changed([1, 2], [u'1', u'3']), True)
    323323
     324       
     325class ManyToManyRawIdWidgetHelpTextTest(DjangoTestCase):
     326    """
     327    Ensure that the help_text is displayed for M2M raw_id_fields.
     328    Refs #14402.
     329    """
     330    fixtures = ["admin-widgets-users.xml"]
     331    admin_root = '/widget_admin'
     332
     333    def setUp(self):
     334        self.client.login(username="super", password="secret")
     335
     336    def tearDown(self):
     337        self.client.logout()
     338
     339    def test_m2m_has_helptext(self):
     340        """Non raw_id m2m model field help_text shouldn't be affected by the fix for this ticket."""
     341        response = self.client.get('%s/admin_widgets/orchestra/add/' % self.admin_root)
     342        self.assert_("This is the orchestra genres help_text." in response.content)
     343
     344    def test_inline_m2m_has_helptext(self):
     345        """Non raw_id m2m model field help_text shouldn't be affected by the fix for this ticket (inline case)."""
     346        response = self.client.get('%s/admin_widgets/director/add/' % self.admin_root)
     347        self.assert_("This is the orchestra genres help_text." in response.content)
     348
     349    def test_raw_id_m2m_has_helptext(self):
     350        """raw_id m2m model field help_text shouldn't be ignored when displaying its admin widget."""
     351        response = self.client.get('%s/admin_widgets/orchestra/add/' % self.admin_root)
     352        self.assert_("This is the orchestra members help_text." in response.content)
     353
     354    def test_raw_id_inline_m2m_has_helptext(self):
     355        """raw_id m2m model field help_text shouldn't be ignored when displaying the admin widget (inline case)."""
     356        response = self.client.get('%s/admin_widgets/director/add/' % self.admin_root)
     357        self.assert_("This is the orchestra members help_text." in response.content)
     358       
    324359class RelatedFieldWidgetWrapperTests(DjangoTestCase):
    325360    def test_no_can_add_related(self):
    326361        rel = models.Inventory._meta.get_field('parent').rel
  • tests/regressiontests/admin_widgets/widgetadmin.py

    diff --git a/tests/regressiontests/admin_widgets/widgetadmin.py b/tests/regressiontests/admin_widgets/widgetadmin.py
    index 6f15d92..ae67fce 100644
    a b class CarTireAdmin(admin.ModelAdmin):  
    2222class EventAdmin(admin.ModelAdmin):
    2323    raw_id_fields = ['band']
    2424
     25class OrchestraAdmin(admin.ModelAdmin):
     26    raw_id_fields = ['members']
     27
     28class OrchestraInline(admin.StackedInline):
     29    model = models.Orchestra
     30    raw_id_fields = ['members']
     31
     32class DirectorAdmin(admin.ModelAdmin):
     33    inlines = [OrchestraInline]
     34   
    2535site = WidgetAdmin(name='widget-admin')
    2636
    2737site.register(models.User)
    2838site.register(models.Car, CarAdmin)
    2939site.register(models.CarTire, CarTireAdmin)
    3040site.register(models.Event, EventAdmin)
     41site.register(models.Orchestra, OrchestraAdmin)
     42site.register(models.Director, DirectorAdmin)
  • tests/regressiontests/forms/tests/forms.py

    diff --git a/tests/regressiontests/forms/tests/forms.py b/tests/regressiontests/forms/tests/forms.py
    index 91a7472..5b2b7b4 100644
    a b class FormsTestCase(TestCase):  
    10921092<option value="f" selected="selected">foo</option>
    10931093<option value="b" selected="selected">bar</option>
    10941094<option value="w">whiz</option>
    1095 </select></li>""")
     1095</select> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></li>""")
    10961096
    10971097        # The 'initial' parameter is meaningless if you pass data.
    10981098        p = UserRegistration({}, initial={'username': initial_django, 'options': initial_options}, auto_id=False)
    class FormsTestCase(TestCase):  
    11021102<option value="f">foo</option>
    11031103<option value="b">bar</option>
    11041104<option value="w">whiz</option>
    1105 </select></li>""")
     1105</select> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></li>""")
    11061106        p = UserRegistration({'username': u''}, initial={'username': initial_django}, auto_id=False)
    11071107        self.assertEqual(p.as_ul(), """<li><ul class="errorlist"><li>This field is required.</li></ul>Username: <input type="text" name="username" maxlength="10" /></li>
    11081108<li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li>
    class FormsTestCase(TestCase):  
    11101110<option value="f">foo</option>
    11111111<option value="b">bar</option>
    11121112<option value="w">whiz</option>
    1113 </select></li>""")
     1113</select> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></li>""")
    11141114        p = UserRegistration({'username': u'foo', 'options':['f','b']}, initial={'username': initial_django}, auto_id=False)
    11151115        self.assertEqual(p.as_ul(), """<li>Username: <input type="text" name="username" value="foo" maxlength="10" /></li>
    11161116<li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li>
    class FormsTestCase(TestCase):  
    11181118<option value="f" selected="selected">foo</option>
    11191119<option value="b" selected="selected">bar</option>
    11201120<option value="w">whiz</option>
    1121 </select></li>""")
     1121</select> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></li>""")
    11221122
    11231123        # A callable 'initial' value is *not* used as a fallback if data is not provided.
    11241124        # In this example, we don't provide a value for 'username', and the form raises a
    class FormsTestCase(TestCase):  
    11411141<option value="f">foo</option>
    11421142<option value="b" selected="selected">bar</option>
    11431143<option value="w" selected="selected">whiz</option>
    1144 </select></li>""")
     1144</select> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></li>""")
    11451145        p = UserRegistration(initial={'username': initial_stephane, 'options': initial_options}, auto_id=False)
    11461146        self.assertEqual(p.as_ul(), """<li>Username: <input type="text" name="username" value="stephane" maxlength="10" /></li>
    11471147<li>Password: <input type="password" name="password" /></li>
    class FormsTestCase(TestCase):  
    11491149<option value="f" selected="selected">foo</option>
    11501150<option value="b" selected="selected">bar</option>
    11511151<option value="w">whiz</option>
    1152 </select></li>""")
     1152</select> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></li>""")
    11531153
    11541154    def test_boundfield_values(self):
    11551155        # It's possible to get to the value which would be used for rendering
  • tests/regressiontests/model_forms_regress/tests.py

    diff --git a/tests/regressiontests/model_forms_regress/tests.py b/tests/regressiontests/model_forms_regress/tests.py
    index f536001..97ba60e 100644
    a b from datetime import date  
    33from django import forms
    44from django.core.exceptions import FieldError, ValidationError
    55from django.core.files.uploadedfile import SimpleUploadedFile
     6from django.forms.widgets import CheckboxSelectMultiple
    67from django.forms.models import (modelform_factory, ModelChoiceField,
    78    fields_for_model, construct_instance)
    89from django.utils import unittest
    class ManyToManyCallableInitialTests(TestCase):  
    140141</select> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></li>"""
    141142            % (book1.pk, book2.pk, book3.pk))
    142143
     144class ManyToManyHelpTextTests(TestCase):
     145   
     146    def test_dont_display_hold_down_command_help_text(self):
     147        """
     148        Ensures that the 'Hold down "Control"' message is not systematically displayed for
     149        a M2M field if it does not use the default SelectMultiple widget.
     150        Refs #9321.
     151        """
     152        # Override the widget and help_text
     153        def formfield_for_dbfield(db_field, **kwargs):
     154            if db_field.name == 'publications':
     155                kwargs['widget'] = CheckboxSelectMultiple
     156            return db_field.formfield(**kwargs)
     157
     158        # Set up some Publications to use as data
     159        book1 = Publication.objects.create(title="First Book", date_published=date(2007,1,1))
     160        book2 = Publication.objects.create(title="Second Book", date_published=date(2008,1,1))
     161
     162        # Create a ModelForm, instantiate it, and check that the output is as expected
     163        ModelForm = modelform_factory(Article, formfield_callback=formfield_for_dbfield)
     164        form = ModelForm()
     165        self.assertEqual(unicode(form['publications'].help_text), u'')
     166       
    143167class CFFForm(forms.ModelForm):
    144168    class Meta:
    145169        model = CustomFF
  • tests/regressiontests/model_formsets_regress/tests.py

    diff --git a/tests/regressiontests/model_formsets_regress/tests.py b/tests/regressiontests/model_formsets_regress/tests.py
    index e6c2633..bf86a85 100644
    a b class FormsetTests(TestCase):  
    228228            self.assertTrue(isinstance(form.errors, ErrorDict))
    229229            self.assertTrue(isinstance(form.non_field_errors(), ErrorList))
    230230
    231 class CustomWidget(forms.CharField):
     231class CustomWidget(forms.TextInput):
    232232    pass
    233233
    234234
Back to Top