Skip to content

Commit 4c9d1e1

Browse files
Merge pull request #971 from SuffolkLITLab/copilot/fix-853
Add optional `required` parameter to _fields() methods for granular field requirement control
2 parents ebaa720 + 477d443 commit 4c9d1e1

File tree

2 files changed

+167
-3
lines changed

2 files changed

+167
-3
lines changed

docassemble/AssemblyLine/al_general.py

Lines changed: 54 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1114,6 +1114,7 @@ def name_fields(
11141114
maxlengths: Optional[Dict[str, int]] = None,
11151115
suffix_choices: Optional[Union[List[str], Callable]] = None,
11161116
title_options: Optional[Union[List[str], Callable]] = None,
1117+
required: Optional[Dict[str, bool]] = None,
11171118
) -> List[Dict[str, str]]:
11181119
"""
11191120
Generates suitable field prompts for a name based on the type of entity (person or business)
@@ -1133,6 +1134,7 @@ def name_fields(
11331134
maxlengths (Dict[str, int], optional): A dictionary of field names and their maximum lengths. Default is None.
11341135
suffix_choices (Union[List[str], Callable], optional): A list of suffix options or a callable to generate suffix options, or overridden by value of global `al_name_suffixes`.
11351136
title_options: (Union[List[str], Callable], optional): Deprecated parameter, use `title_choices` instead. If provided, it will be used to set the title choices.
1137+
required (Dict[str, bool], optional): A dictionary of field names and if they should be required. Default is None.
11361138
11371139
Returns:
11381140
List[Dict[str, str]]: A list of dictionaries where each dictionary contains field prompt details.
@@ -1196,6 +1198,17 @@ def name_fields(
11961198
if show_if:
11971199
for field in fields:
11981200
field["show if"] = show_if
1201+
1202+
if maxlengths:
1203+
for field in fields:
1204+
if field["field"] in maxlengths:
1205+
field["maxlength"] = maxlengths[field["field"]]
1206+
1207+
if required:
1208+
for field in fields:
1209+
if field["field"] in required:
1210+
field["required"] = required[field["field"]]
1211+
11991212
return fields
12001213
elif person_or_business == "business":
12011214
fields = [
@@ -1206,6 +1219,17 @@ def name_fields(
12061219
]
12071220
if show_if:
12081221
fields[0]["show if"] = show_if
1222+
1223+
if maxlengths:
1224+
for field in fields:
1225+
if field["field"] in maxlengths:
1226+
field["maxlength"] = maxlengths[field["field"]]
1227+
1228+
if required:
1229+
for field in fields:
1230+
if field["field"] in required:
1231+
field["required"] = required[field["field"]]
1232+
12091233
return fields
12101234
else:
12111235
# Note: the labels are template block objects: if they are keys,
@@ -1273,6 +1297,12 @@ def name_fields(
12731297
for field in fields:
12741298
if field["field"] in maxlengths:
12751299
field["maxlength"] = maxlengths[field["field"]]
1300+
1301+
if required:
1302+
for field in fields:
1303+
if field["field"] in required:
1304+
field["required"] = required[field["field"]]
1305+
12761306
return fields
12771307

12781308
def address_fields(
@@ -1324,6 +1354,7 @@ def gender_fields(
13241354
show_if: Union[str, Dict[str, str], None] = None,
13251355
maxlengths: Optional[Dict[str, int]] = None,
13261356
choices: Optional[Union[List[Dict[str, str]], Callable]] = None,
1357+
required: Optional[Dict[str, bool]] = None,
13271358
) -> List[Dict[str, str]]:
13281359
"""
13291360
Generate fields for capturing gender information, including a
@@ -1334,6 +1365,7 @@ def gender_fields(
13341365
show_if (Union[str, Dict[str, str], None]): Condition to determine if the field should be shown. Defaults to None.
13351366
maxlengths (Dict[str, int], optional): A dictionary of field names and their maximum lengths. Default is None.
13361367
choices (Optional[Union[List[Dict[str, str]], Callable]]): A list of choices of genders to use in the prompts, or a callable that returns such a list. Default set of choices includes male, female, nonbinary, prefer-not-to-say, self-described, and unknown.
1368+
required (Dict[str, bool], optional): A dictionary of field names and if they should be required. Default is None.
13371369
13381370
Returns:
13391371
List[Dict[str, str]]: A list of dictionaries with field prompts for gender.
@@ -1377,13 +1409,18 @@ def gender_fields(
13771409
if field["field"] in maxlengths:
13781410
field["maxlength"] = maxlengths[field["field"]]
13791411

1412+
if required:
1413+
for field in fields:
1414+
if field["field"] in required:
1415+
field["required"] = required[field["field"]]
1416+
13801417
return fields
13811418

13821419
def pronoun_fields(
13831420
self,
13841421
show_help=False,
13851422
show_if: Union[str, Dict[str, str], None] = None,
1386-
required: bool = False,
1423+
required: Union[bool, Dict[str, bool]] = False,
13871424
shuffle: bool = False,
13881425
show_unknown: Optional[Union[Literal["guess"], bool]] = "guess",
13891426
maxlengths: Optional[Dict[str, int]] = None,
@@ -1395,7 +1432,7 @@ def pronoun_fields(
13951432
Args:
13961433
show_help (bool): Whether to show additional help text. Defaults to False.
13971434
show_if (Union[str, Dict[str, str], None]): Condition to determine if the field should be shown. Defaults to None.
1398-
required (bool): Whether the field is required. Defaults to False.
1435+
required (Union[bool, Dict[str, bool]]): Whether the field is required. Can be a boolean (applies to all fields) or a dictionary of field names and if they should be required. Defaults to False.
13991436
shuffle (bool): Whether to shuffle the order of pronouns. Defaults to False.
14001437
show_unknown (Union[Literal["guess"], bool]): Whether to show an "unknown" option. Can be "guess", True, or False. Defaults to "guess".
14011438
maxlengths (Dict[str, int], optional): A dictionary of field names and their maximum lengths. Default is None.
@@ -1429,7 +1466,7 @@ def pronoun_fields(
14291466
"datatype": "checkboxes",
14301467
"choices": shuffled_choices + final_choices,
14311468
"none of the above": str(self.pronoun_prefer_not_to_say_label),
1432-
"required": required,
1469+
"required": required if isinstance(required, bool) else False,
14331470
},
14341471
self_described_input,
14351472
]
@@ -1444,6 +1481,12 @@ def pronoun_fields(
14441481
if field["field"] in maxlengths:
14451482
field["maxlength"] = maxlengths[field["field"]]
14461483

1484+
# Handle dictionary-based required parameter
1485+
if isinstance(required, dict):
1486+
for field in fields:
1487+
if field["field"] in required:
1488+
field["required"] = required[field["field"]]
1489+
14471490
return fields
14481491

14491492
def get_pronouns(self) -> set:
@@ -1482,6 +1525,7 @@ def language_fields(
14821525
style: str = "radio",
14831526
show_if: Union[str, Dict[str, str], None] = None,
14841527
maxlengths: Optional[Dict[str, int]] = None,
1528+
required: Optional[Dict[str, bool]] = None,
14851529
) -> List[Dict[str, str]]:
14861530
"""
14871531
Generate fields for capturing language preferences.
@@ -1491,6 +1535,7 @@ def language_fields(
14911535
style (str): The display style of choices. Defaults to "radio".
14921536
show_if (Union[str, Dict[str, str], None]): Condition to determine if the field should be shown. Defaults to None.
14931537
maxlengths (Dict[str, int], optional): A dictionary of field names and their maximum lengths. Default is None.
1538+
required (Dict[str, bool], optional): A dictionary of field names and if they should be required. Default is None.
14941539
14951540
Returns:
14961541
List[Dict[str, str]]: A list of dictionaries with field prompts for language preferences.
@@ -1525,6 +1570,12 @@ def language_fields(
15251570
for field in fields:
15261571
if field["field"] in maxlengths:
15271572
field["maxlength"] = maxlengths[field["field"]]
1573+
1574+
if required:
1575+
for field in fields:
1576+
if field["field"] in required:
1577+
field["required"] = required[field["field"]]
1578+
15281579
return fields
15291580

15301581
def language_name(self) -> str:

docassemble/AssemblyLine/test_al_general.py

Lines changed: 113 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,119 @@ def test_name_methods(self):
462462
self.assertEqual(self.individual.name_short(), "Johnny's Sandwiches")
463463
self.assertEqual(str(self.individual), "Johnny's Sandwiches")
464464

465+
def test_name_fields_required(self):
466+
"""Test the required parameter for name_fields method"""
467+
self.individual.instanceName = "test_individual"
468+
469+
# Test without required parameter - middle name should default to False
470+
fields = self.individual.name_fields(person_or_business="person")
471+
middle_field = None
472+
for field in fields:
473+
if "middle" in field["field"]:
474+
middle_field = field
475+
break
476+
self.assertIsNotNone(middle_field)
477+
self.assertEqual(middle_field["required"], False)
478+
479+
# Test with required parameter making middle name required
480+
fields = self.individual.name_fields(
481+
person_or_business="person", required={"test_individual.name.middle": True}
482+
)
483+
middle_field = None
484+
for field in fields:
485+
if "middle" in field["field"]:
486+
middle_field = field
487+
break
488+
self.assertIsNotNone(middle_field)
489+
self.assertEqual(middle_field["required"], True)
490+
491+
# Test business case with required parameter
492+
fields = self.individual.name_fields(
493+
person_or_business="business", required={"test_individual.name.first": True}
494+
)
495+
business_field = fields[0]
496+
self.assertEqual(business_field["required"], True)
497+
498+
def test_gender_fields_required(self):
499+
"""Test the required parameter for gender_fields method"""
500+
self.individual.instanceName = "test_individual"
501+
502+
# Test without required parameter
503+
fields = self.individual.gender_fields()
504+
gender_field = fields[0]
505+
self.assertNotIn(
506+
"required", gender_field
507+
) # Should not have required by default
508+
509+
# Test with required parameter
510+
fields = self.individual.gender_fields(
511+
required={"test_individual.gender": True}
512+
)
513+
gender_field = fields[0]
514+
self.assertEqual(gender_field["required"], True)
515+
516+
# Test key not in required dict - should not change default
517+
fields = self.individual.gender_fields(required={"other_field": True})
518+
gender_field = fields[0]
519+
self.assertNotIn("required", gender_field)
520+
521+
def test_pronoun_fields_required(self):
522+
"""Test the required parameter for pronoun_fields method (both bool and dict)"""
523+
self.individual.instanceName = "test_individual"
524+
525+
# Test with boolean required parameter (existing behavior)
526+
fields = self.individual.pronoun_fields(required=True)
527+
pronoun_field = fields[0]
528+
self.assertEqual(pronoun_field["required"], True)
529+
530+
# Test with boolean required parameter = False
531+
fields = self.individual.pronoun_fields(required=False)
532+
pronoun_field = fields[0]
533+
self.assertEqual(pronoun_field["required"], False)
534+
535+
# Test with dictionary required parameter (new behavior)
536+
fields = self.individual.pronoun_fields(
537+
required={"test_individual.pronouns": True}
538+
)
539+
pronoun_field = fields[0]
540+
self.assertEqual(pronoun_field["required"], True)
541+
542+
# Test with dictionary required parameter for different field
543+
fields = self.individual.pronoun_fields(
544+
required={"test_individual.pronouns_self_described": True}
545+
)
546+
pronoun_field = fields[0]
547+
self_described_field = fields[1]
548+
self.assertEqual(
549+
pronoun_field["required"], False
550+
) # First field should still be False
551+
self.assertEqual(self_described_field["required"], True)
552+
553+
def test_language_fields_required(self):
554+
"""Test the required parameter for language_fields method"""
555+
self.individual.instanceName = "test_individual"
556+
557+
# Test without required parameter
558+
fields = self.individual.language_fields()
559+
language_field = fields[0]
560+
self.assertNotIn(
561+
"required", language_field
562+
) # Should not have required by default
563+
564+
# Test with required parameter
565+
fields = self.individual.language_fields(
566+
required={"test_individual.language": True}
567+
)
568+
language_field = fields[0]
569+
self.assertEqual(language_field["required"], True)
570+
571+
# Test other language field
572+
fields = self.individual.language_fields(
573+
required={"test_individual.language_other": False}
574+
)
575+
other_field = fields[1]
576+
self.assertEqual(other_field["required"], False)
577+
465578

466579
class test_get_visible_al_nav_items(unittest.TestCase):
467580
def test_case_1(self):

0 commit comments

Comments
 (0)