Skip to content
/ django Public

Fixed #29205 -- MultiValueField ignores a required value of a sub field#12642

Closed
smithdc1 wants to merge 6 commits intodjango:masterfrom
smithdc1:29205
Closed

Fixed #29205 -- MultiValueField ignores a required value of a sub field#12642
smithdc1 wants to merge 6 commits intodjango:masterfrom
smithdc1:29205

Conversation

@smithdc1
Copy link
Member

Copy link
Member

@carltongibson carltongibson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi @smithdc1.

Thanks for this. Good start.

We can work on the code changes. I've left a few initial comments.

More though I'm not sure we've 100% pinned down the exact behaviour change we're looking for in the changes to the test cases.

  • I thought if the parent field was not required then it could be emitted entirely.
  • Then it looks like we'll need to adjust some of the existing cases (linked in the inline comments). If so we have to pulled out the behaviour change clearly so that we can make sure it's acceptable and document it suitably.

Comment on lines +223 to +224
if isinstance(widget, MultiWidget) or isinstance(self.field.widget, MultiWidget):
widgets = widget.widgets or self.field.widget.widgets
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think leave the original widget = widget or self.field.widget here. Then do the isinstance() check, so that we're no repeating ourselves in the else block.

widgets might then be clearer as subwidgets. (Or such.)

field.required and
self.form.use_required_attribute
):
widget.attrs["required"] = True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will this permanently adjust the attr on the widget instance?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, this makes me sad 👎

Having looked at this again (seem to learn something new every day 👍 ) I think I've been 'abusing' the wrong widget in fields.py (next comment) and I'm therefore correcting my error here.

Comment on lines +991 to +996
if (self.required and (self.require_all_fields or f.required)) or (
not self.required and not self.require_all_fields and f.required
):
f.required = f.widget.is_required = True
else:
f.required = f.widget.is_required = False
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The conditional here needs a comment at least. (Did we change the rules here?)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to set is_required on the widget, so we can us this later on. My realisation tonight is here I'm changing is_required on the wrong widget. Here I'm setting it on field.widget. What I need to do is set it on widget.widets.widget. Or more simply, it needs to set on the widgets of the MultiWidget rather than the widgets of the fields of the MultiField.

I'm hoping this will simplify all of this a great deal as we can 'just' rely on get_context to do the right thing if we set it up right here. 🤞

Leave it with me and I'll revert after some more thought.

with self.assertRaisesMessage(ValidationError, "'Enter a complete value.', 'Enter an extension.'"):
self.assertIsNone(f.clean(None))
with self.assertRaisesMessage(ValidationError, "'Enter a complete value.', 'Enter an extension.'"):
self.assertIsNone(f.clean([]))
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Where PhoneField(required=False, ...) I'm expecting totally empty values to not raise, as per the old test.

The MVF itself is optional. If submitted, and require_all_fields=False, we defer to the individual fields to determine if they can be omitted. (That was the thought on the ticket right?)

IIUC: It looks like it's these cases here that we need to adjust. (Assuming we can do that from a BC POV.)

Comment on lines +236 to +241
if (
widget.use_required_attribute(self.initial) and
self.field.required and
self.form.use_required_attribute
):
attrs["required"] = True
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We need to factor this differently, as it's an exact duplicate of the added block above.

try:
field_value = value[i]
except IndexError:
except (IndexError, TypeError):
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's the addition here for?

@carltongibson carltongibson self-assigned this Apr 1, 2020
@smithdc1
Copy link
Member Author

smithdc1 commented Apr 3, 2020

Hi @carltongibson,

Let's try and agree the expected behaviour.

The ticket explained that it is odd to allow a null response when a widget is required. This is true; however, just looking at the clean validation for a moment, I think the existing behaviours meet this requirement. What is needed is row 6 in the table below, I had tried to fix this by changing the behaviour of test 2. I now believe this to be the incorrect approach. The table below I've reverted the clean logic back to the existing behaviour.

To bring this to life I think there are two valid use cases.

  1. You have a field with a number of subwidgets, say a telephone field, it could be acceptable to not enter a number at all; but if you are going to fill it in, then fill it in correctly [row 2 in table].

  2. Taking the telephone example above, maybe we want them to fill it in, but it's OK not to fill in an extension [row 6]

Whilst this satisfies the requirement from a clean perspective. The issue is that under scenario 2, this currently results in a HTML5 required attribute being added to all of the fields, irrespective of the widget's required status. My current view is therefore that the change we need to make is to the HTML5 required attributes under scenario 5 in the table below.

a b c d e f [ref]
F | F | F | F | n/a | Valid - Test 4
F | F | T | T | Valid | Incomplete Error- Test 4
F | T | F | F | Valid | Valid - Test 2
F | T | T | F | Valid | Valid - Test 2
T | F | F | F | n/a | Valid- Test 3
T | F | T | T | Required Error | Incomplete Error - Test 3
T | T | F | T | Required Error | Required Error - Test 1
T | T | T | T | Required Error | Required Error - Test 1

a = required
b = require_all_fields
c = subfield.required
d = HTML5 required
e = Entire field not completed test
f = Partially completed test

Appreciate your thoughts.

@smithdc1
Copy link
Member Author

Some further thoughts on this. Apologies, for another long post....

Whilst we still need to agree the correct behaviour (previous post) I need to find a way of managing required at a more granular level. I've found, what I think, is a neat way of fixing this but causes many of the tests to fail.... (more on this later)

The issues I've been struggling with is that boundfield only adds attrs at the parent widget level. We need a more granular level of control. Therefore here is a 'gist' which shows my current idea.

Step 1) Set the sub widget's is_required on __init__ of the field, in a similar way to other fields.
Step 2) Let's use that variable when building the context for each subwidget in MultiWidget.get_context().
Step 3) Add an exception to boundfield.build_widget_attrs(), to not add required to MultiWidgets

This works and I think is much cleaner than my previous options. (I don't think it permanently changes with widgets attrs for example.)

There is a big but... many of the tests currently fail. This is because the zip in the MultiValueField __init__ method assumes self.widget is a MultiWidget with subwidgets. If this isn't the case then it fails. Some of the tests in the test suite are only testing the field and so haven't setup a MultiWidget.

So my question: Is there a use case for a MultiValueField where it's widget doesn't have subwidgets where the number of subwidgets and fields are equal?

I think the answer to this should be no......

@jacobtylerwalls
Copy link
Member

Hi @dcsmith1. I had a look here and left some remarks on the original ticket. Essentially, I think we can vastly reduce the scope of the changes here by not making changes to validation and only dealing with the required HTML attributes in a scenario where required=True and require_all_fields=False on a MultiValueField.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants