Ticket #9321: 9321+14402.ManyToManyField-help-text.diff
File 9321+14402.ManyToManyField-help-text.diff, 18.7 KB (added by , 14 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): 175 175 176 176 if db_field.name in self.raw_id_fields: 177 177 kwargs['widget'] = widgets.ManyToManyRawIdWidget(db_field.rel, using=db) 178 kwargs['help_text'] = ''179 178 elif db_field.name in (list(self.filter_vertical) + list(self.filter_horizontal)): 180 179 kwargs['widget'] = widgets.FilteredSelectMultiple(db_field.verbose_name, (db_field.name in self.filter_vertical)) 181 180 -
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 9 9 from django.db.models.query_utils import QueryWrapper 10 10 from django.db.models.deletion import CASCADE 11 11 from django.utils.encoding import smart_unicode 12 from django.utils.translation import (ugettext_lazy as _, string_concat, 13 ungettext, ugettext) 12 from django.utils.translation import ugettext_lazy as _, ungettext, ugettext 14 13 from django.utils.functional import curry 15 14 from django.core import exceptions 16 15 from django import forms … … class ManyToManyField(RelatedField, Field): 1021 1020 1022 1021 Field.__init__(self, **kwargs) 1023 1022 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 1027 1023 def get_choices_default(self): 1028 1024 return Field.get_choices(self, include_blank=False) 1029 1025 -
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): 80 80 label = smart_unicode(label) 81 81 self.required, self.label, self.initial = required, label, initial 82 82 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)87 83 widget = widget or self.widget 88 84 if isinstance(widget, type): 89 85 widget = widget() … … class Field(object): 103 99 104 100 self.widget = widget 105 101 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 106 110 # Increase the creation counter, and save our local copy. 107 111 self.creation_counter = Field.creation_counter 108 112 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 12 12 from django.conf import settings 13 13 from django.utils.datastructures import MultiValueDict, MergeDict 14 14 from django.utils.html import escape, conditional_escape 15 from django.utils.translation import ugettext, ugettext_lazy 15 from django.utils.translation import ugettext, ugettext_lazy, string_concat 16 16 from django.utils.encoding import StrAndUnicode, force_unicode 17 from django.utils.safestring import mark_safe 17 from django.utils.safestring import mark_safe 18 18 from django.utils import datetime_safe, formats 19 19 20 20 __all__ = ( … … class Widget(object): 155 155 memo[id(self)] = obj 156 156 return obj 157 157 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 158 164 def render(self, name, value, attrs=None): 159 165 """ 160 166 Returns this Widget rendered as HTML, as a Unicode string. … … class NullBooleanSelect(Select): 575 581 return initial != data 576 582 577 583 class 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 578 590 def render(self, name, value, attrs=None, choices=()): 579 591 if value is None: value = [] 580 592 final_attrs = self.build_attrs(attrs, name=name) … … class RadioSelect(Select): 690 702 id_for_label = classmethod(id_for_label) 691 703 692 704 class 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 693 711 def render(self, name, value, attrs=None, choices=()): 694 712 if value is None: value = [] 695 713 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:: 258 258 <tr><th>Name:</th><td><input type="text" name="name" class="special"/></td></tr> 259 259 <tr><th>Url:</th><td><input type="text" name="url"/></td></tr> 260 260 <tr><th>Comment:</th><td><input type="text" name="comment" size="40"/></td></tr> 261 262 Altering the help text 263 ---------------------- 264 265 .. versionadded:: 1.4 266 267 In some cases you might want to customize a form field's help text depending 268 on 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, 60 60 ``template.Library`` to ease the creation of template tags that store some 61 61 data in a specified context variable. 62 62 63 Widget.alter_help_text 64 ~~~~~~~~~~~~~~~~~~~~~~ 65 66 The :meth:`~alter_help_text` method was added to `:class:~forms.widgets.Widget` 67 to allow a widget to customize the help text provided by a form field or a 68 model field. This introduced a backwards incompatible change if you are 69 defining a custom ``SelectMultiple`` widget. See `the notes on backwards 70 incompatible changes`_ below to address this change. 71 72 .. _the notes on backwards incompatible changes: backwards-incompatible-changes-1.4_ 73 63 74 .. _backwards-incompatible-changes-1.4: 64 75 65 76 Backwards incompatible changes in 1.4 … … you should add the following lines in your settings file:: 214 225 215 226 Don't forget to escape characters that have a special meaning in a regular 216 227 expression. 228 229 Custom ``SelectMultiple`` widgets and ``alter_help_text`` 230 ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ 231 232 The implementation of the new `meth:~Widget.alter_help_text` method was 233 originally 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 235 appended to a ``ManyToManyField``'s help text, even if that field were 236 configured to use a different widget than ``SelectMultiple``. Thus, the 237 action 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 247 This means that if you are defining a widget inheriting from 248 ``SelectMultiple``, then that message will now systematically be appended to 249 the help text, which may not make sense in the context of use for that widget. 250 To cancel this new behaviour, simply override the ``alter_help_text`` method of 251 your 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): 67 67 A single car tire. This to test that a user can only select their own cars. 68 68 """ 69 69 car = models.ForeignKey(Car) 70 71 class Director(models.Model): 72 name = models.CharField(max_length=40) 73 74 def __unicode__(self): 75 return self.name 76 77 class Genre(models.Model): 78 name = models.CharField(max_length=40) 79 80 def __unicode__(self): 81 return self.name 82 83 class 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): 321 321 self.assertEqual(w._has_changed([1, 2], [u'1']), True) 322 322 self.assertEqual(w._has_changed([1, 2], [u'1', u'3']), True) 323 323 324 325 class 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 324 359 class RelatedFieldWidgetWrapperTests(DjangoTestCase): 325 360 def test_no_can_add_related(self): 326 361 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): 22 22 class EventAdmin(admin.ModelAdmin): 23 23 raw_id_fields = ['band'] 24 24 25 class OrchestraAdmin(admin.ModelAdmin): 26 raw_id_fields = ['members'] 27 28 class OrchestraInline(admin.StackedInline): 29 model = models.Orchestra 30 raw_id_fields = ['members'] 31 32 class DirectorAdmin(admin.ModelAdmin): 33 inlines = [OrchestraInline] 34 25 35 site = WidgetAdmin(name='widget-admin') 26 36 27 37 site.register(models.User) 28 38 site.register(models.Car, CarAdmin) 29 39 site.register(models.CarTire, CarTireAdmin) 30 40 site.register(models.Event, EventAdmin) 41 site.register(models.Orchestra, OrchestraAdmin) 42 site.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): 1092 1092 <option value="f" selected="selected">foo</option> 1093 1093 <option value="b" selected="selected">bar</option> 1094 1094 <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>""") 1096 1096 1097 1097 # The 'initial' parameter is meaningless if you pass data. 1098 1098 p = UserRegistration({}, initial={'username': initial_django, 'options': initial_options}, auto_id=False) … … class FormsTestCase(TestCase): 1102 1102 <option value="f">foo</option> 1103 1103 <option value="b">bar</option> 1104 1104 <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>""") 1106 1106 p = UserRegistration({'username': u''}, initial={'username': initial_django}, auto_id=False) 1107 1107 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> 1108 1108 <li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li> … … class FormsTestCase(TestCase): 1110 1110 <option value="f">foo</option> 1111 1111 <option value="b">bar</option> 1112 1112 <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>""") 1114 1114 p = UserRegistration({'username': u'foo', 'options':['f','b']}, initial={'username': initial_django}, auto_id=False) 1115 1115 self.assertEqual(p.as_ul(), """<li>Username: <input type="text" name="username" value="foo" maxlength="10" /></li> 1116 1116 <li><ul class="errorlist"><li>This field is required.</li></ul>Password: <input type="password" name="password" /></li> … … class FormsTestCase(TestCase): 1118 1118 <option value="f" selected="selected">foo</option> 1119 1119 <option value="b" selected="selected">bar</option> 1120 1120 <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>""") 1122 1122 1123 1123 # A callable 'initial' value is *not* used as a fallback if data is not provided. 1124 1124 # In this example, we don't provide a value for 'username', and the form raises a … … class FormsTestCase(TestCase): 1141 1141 <option value="f">foo</option> 1142 1142 <option value="b" selected="selected">bar</option> 1143 1143 <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>""") 1145 1145 p = UserRegistration(initial={'username': initial_stephane, 'options': initial_options}, auto_id=False) 1146 1146 self.assertEqual(p.as_ul(), """<li>Username: <input type="text" name="username" value="stephane" maxlength="10" /></li> 1147 1147 <li>Password: <input type="password" name="password" /></li> … … class FormsTestCase(TestCase): 1149 1149 <option value="f" selected="selected">foo</option> 1150 1150 <option value="b" selected="selected">bar</option> 1151 1151 <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>""") 1153 1153 1154 1154 def test_boundfield_values(self): 1155 1155 # 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 3 3 from django import forms 4 4 from django.core.exceptions import FieldError, ValidationError 5 5 from django.core.files.uploadedfile import SimpleUploadedFile 6 from django.forms.widgets import CheckboxSelectMultiple 6 7 from django.forms.models import (modelform_factory, ModelChoiceField, 7 8 fields_for_model, construct_instance) 8 9 from django.utils import unittest … … class ManyToManyCallableInitialTests(TestCase): 140 141 </select> <span class="helptext"> Hold down "Control", or "Command" on a Mac, to select more than one.</span></li>""" 141 142 % (book1.pk, book2.pk, book3.pk)) 142 143 144 class 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 143 167 class CFFForm(forms.ModelForm): 144 168 class Meta: 145 169 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): 228 228 self.assertTrue(isinstance(form.errors, ErrorDict)) 229 229 self.assertTrue(isinstance(form.non_field_errors(), ErrorList)) 230 230 231 class CustomWidget(forms. CharField):231 class CustomWidget(forms.TextInput): 232 232 pass 233 233 234 234