From 5fc5d935120a5d9d428b969ad209695704c10c73 Mon Sep 17 00:00:00 2001 From: Hasan Ramezani Date: Wed, 21 Nov 2018 21:58:04 +0100 Subject: [PATCH] Fixed #29956 -- Allowed overriding an order field widget in formsets. --- django/forms/formsets.py | 21 ++++++++++-- docs/releases/3.0.txt | 5 ++- docs/topics/forms/formsets.txt | 43 ++++++++++++++++++++++++ tests/forms_tests/tests/test_formsets.py | 26 ++++++++++++++ 4 files changed, 91 insertions(+), 4 deletions(-) diff --git a/django/forms/formsets.py b/django/forms/formsets.py index ed93fa852e..44c2cb07f4 100644 --- a/django/forms/formsets.py +++ b/django/forms/formsets.py @@ -2,7 +2,7 @@ from django.core.exceptions import ValidationError from django.forms import Form from django.forms.fields import BooleanField, IntegerField from django.forms.utils import ErrorList -from django.forms.widgets import HiddenInput +from django.forms.widgets import HiddenInput, NumberInput from django.utils.functional import cached_property from django.utils.html import html_safe from django.utils.safestring import mark_safe @@ -47,6 +47,8 @@ class BaseFormSet: """ A collection of instances of the same Form class. """ + ordering_widget = NumberInput + def __init__(self, data=None, files=None, auto_id='id_%s', prefix=None, initial=None, error_class=ErrorList, form_kwargs=None): self.is_bound = data is not None or files is not None @@ -264,6 +266,10 @@ class BaseFormSet: def get_default_prefix(cls): return 'form' + @classmethod + def get_ordering_widget(cls): + return cls.ordering_widget + def non_form_errors(self): """ Return an ErrorList of errors that aren't associated with a particular @@ -368,9 +374,18 @@ class BaseFormSet: if self.can_order: # Only pre-fill the ordering field for initial forms. if index is not None and index < self.initial_form_count(): - form.fields[ORDERING_FIELD_NAME] = IntegerField(label=_('Order'), initial=index + 1, required=False) + form.fields[ORDERING_FIELD_NAME] = IntegerField( + label=_('Order'), + initial=index + 1, + required=False, + widget=self.get_ordering_widget(), + ) else: - form.fields[ORDERING_FIELD_NAME] = IntegerField(label=_('Order'), required=False) + form.fields[ORDERING_FIELD_NAME] = IntegerField( + label=_('Order'), + required=False, + widget=self.get_ordering_widget(), + ) if self.can_delete: form.fields[DELETION_FIELD_NAME] = BooleanField(label=_('Delete'), required=False) diff --git a/docs/releases/3.0.txt b/docs/releases/3.0.txt index 71a8aef971..cf50eea1b5 100644 --- a/docs/releases/3.0.txt +++ b/docs/releases/3.0.txt @@ -139,7 +139,10 @@ File Uploads Forms ~~~~~ -* ... +* Formsets may control the widget used when ordering forms via + :attr:`~django.forms.formsets.BaseFormSet.can_order` by setting the + :attr:`~django.forms.formsets.BaseFormSet.ordering_widget` attribute or + overriding :attr:`~django.forms.formsets.BaseFormSet.get_ordering_widget()`. Generic Views ~~~~~~~~~~~~~ diff --git a/docs/topics/forms/formsets.txt b/docs/topics/forms/formsets.txt index 0c4c7f0bee..b9c8721bf5 100644 --- a/docs/topics/forms/formsets.txt +++ b/docs/topics/forms/formsets.txt @@ -448,6 +448,49 @@ happen when the user changes these values:: {'pub_date': datetime.date(2008, 5, 11), 'ORDER': 1, 'title': 'Article #2'} {'pub_date': datetime.date(2008, 5, 10), 'ORDER': 2, 'title': 'Article #1'} +:class:`~django.forms.formsets.BaseFormSet` also provides an +:attr:`~django.forms.formsets.BaseFormSet.ordering_widget` attribute and +:meth:`~django.forms.formsets.BaseFormSet.get_ordering_widget` method that +control the widget used with +:attr:`~django.forms.formsets.BaseFormSet.can_order`. + +``ordering_widget`` +^^^^^^^^^^^^^^^^^^^ + +.. versionadded:: 3.0 + +.. attribute:: BaseFormSet.ordering_widget + +Default: :class:`~django.forms.NumberInput` + +Set ``ordering_widget`` to specify the widget class to be used with +``can_order``:: + + >>> from django.forms import BaseFormSet, formset_factory + >>> from myapp.forms import ArticleForm + >>> class BaseArticleFormSet(BaseFormSet): + ... ordering_widget = HiddenInput + + >>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet, can_order=True) + +``get_ordering_widget`` +^^^^^^^^^^^^^^^^^^^^^^^ + +.. versionadded:: 3.0 + +.. method:: BaseFormSet.get_ordering_widget() + +Override ``get_ordering_widget()`` if you need to provide a widget instance for +use with ``can_order``:: + + >>> from django.forms import BaseFormSet, formset_factory + >>> from myapp.forms import ArticleForm + >>> class BaseArticleFormSet(BaseFormSet): + ... def get_ordering_widget(self): + ... return HiddenInput(attrs={'class': 'ordering'}) + + >>> ArticleFormSet = formset_factory(ArticleForm, formset=BaseArticleFormSet, can_order=True) + ``can_delete`` -------------- diff --git a/tests/forms_tests/tests/test_formsets.py b/tests/forms_tests/tests/test_formsets.py index 231436f73e..7c6a244c90 100644 --- a/tests/forms_tests/tests/test_formsets.py +++ b/tests/forms_tests/tests/test_formsets.py @@ -8,6 +8,7 @@ from django.forms import ( ) from django.forms.formsets import BaseFormSet, all_valid, formset_factory from django.forms.utils import ErrorList +from django.forms.widgets import HiddenInput from django.test import SimpleTestCase @@ -591,6 +592,31 @@ class FormsFormsetTestCase(SimpleTestCase): ], ) + def test_formsets_with_order_custom_widget(self): + class OrderingAttributFormSet(BaseFormSet): + ordering_widget = HiddenInput + + class OrderingMethodFormSet(BaseFormSet): + def get_ordering_widget(self): + return HiddenInput(attrs={'class': 'ordering'}) + + tests = ( + (OrderingAttributFormSet, ''), + (OrderingMethodFormSet, ''), + ) + for formset_class, order_html in tests: + with self.subTest(formset_class=formset_class): + ArticleFormSet = formset_factory(ArticleForm, formset=formset_class, can_order=True) + formset = ArticleFormSet(auto_id=False) + self.assertHTMLEqual( + '\n'.join(form.as_ul() for form in formset.forms), + ( + '
  • Title:
  • ' + '
  • Pub date: ' + '%s
  • ' % order_html + ), + ) + def test_empty_ordered_fields(self): """ Ordering fields are allowed to be left blank. If they are left blank,