77
88class AtLeastOneRequiredCondition (BaseCondition ):
99 """
10- Ensures that at least one field from a list is present.
11-
12- This condition validates that at least one field from a specified list of
13- fields is present and has a non-empty value.
10+ Ensures that at least N fields from a list are present and non-empty.
11+
12+ **Parameters:**
13+
14+ - **fields** (*list[str]*): List of field names to check for presence.
15+ - **min_required** (*int*, default: ``1``): Minimum number of fields that
16+ must be present and non-empty.
17+ - **check_empty** (*bool*, default: ``True``): Whether to consider empty
18+ strings, empty lists, and other "falsy" values as absent.
19+ - **custom_message** (*str*, optional): Custom error message to display
20+ when the condition fails.
21+
22+ **Expected Behavior:**
23+
24+ - Validates that at least ``min_required`` fields are present
25+ - Fields are considered "present" if they exist and are not None
26+ - With ``check_empty=True``, empty strings and empty containers count as
27+ absent
28+ - Provides clear error messages indicating how many fields are required vs.
29+ provided
30+
31+ **Use Cases:**
32+
33+ - **Contact information**: At least one of phone, email, or address
34+ - **Flexible forms**: Multiple optional ways to provide the same
35+ information
36+ - **Alternative fields**: Primary field or one of several backup fields
37+ - **Partial requirements**: At least N out of M total fields
38+
39+ **Example Usage:**
40+
41+ .. code-block:: python
42+
43+ class ProfileCompletionFilter(InputFilter):
44+ def __init__(self):
45+ super().__init__()
46+
47+ # All profile fields are optional individually
48+ self.add('bio')
49+ self.add('avatar_url')
50+ self.add('website')
51+ self.add('social_links')
52+ self.add('skills')
53+ self.add('certifications')
54+
55+ # At least 3 profile fields must be completed for profile visibility
56+ self.add_condition(
57+ AtLeastOneRequiredCondition(
58+ [
59+ 'bio', 'avatar_url', 'website', 'social_links',
60+ 'skills', 'certifications'
61+ ],
62+ min_required=3,
63+ check_empty=True,
64+ custom_message="Complete at least 3 profile sections"
65+ )
66+ )
67+
68+ **Validation Examples:**
69+
70+ .. code-block:: python
71+
72+ # Valid: phone provided
73+ {"phone": "555-1234", "email": "", "address": ""}
74+
75+ # Valid: email and address provided (exceeds minimum)
76+ {"phone": "", "email": "[email protected] ", "address": "123 Main St"} 77+
78+ # Invalid: all fields empty
79+ {"phone": "", "email": "", "address": ""}
80+
81+ # Invalid: all fields None or missing
82+ {"phone": None}
83+
84+ # With min_required=2:
85+ # Valid: 2 fields provided
86+ {"bio": "Hello", "avatar_url": "http://...", "website": ""}
87+
88+ # Invalid: only 1 field provided
89+ {"bio": "Hello", "avatar_url": "", "website": ""}
90+
91+ **Empty Value Handling:**
92+
93+ When ``check_empty=True`` (default), these values are considered absent:
94+ - ``None``
95+ - Empty string ``""``
96+ - Empty list ``[]``
97+ - Empty dict ``{}``
98+ - Zero ``0`` (depending on context)
99+
100+ When ``check_empty=False``, only ``None`` and missing keys are considered
101+ absent.
14102 """
15103
16104 def __init__ (
@@ -38,6 +126,39 @@ def __init__(
38126 self .check_empty = check_empty
39127 self .custom_message = custom_message
40128
129+ def _is_empty (self , value : Any ) -> bool :
130+ """
131+ Check if a value is considered empty.
132+
133+ Args:
134+ value: The value to check
135+
136+ Returns:
137+ True if value is empty, False otherwise
138+ """
139+ if value is None :
140+ return True
141+ if not self .check_empty :
142+ return False
143+ if isinstance (value , str ) and value .strip () == "" :
144+ return True
145+ return isinstance (value , (list , dict )) and len (value ) == 0
146+
147+ def _count_present_fields (self , data : Dict [str , Any ]) -> int :
148+ """
149+ Count the number of present (non-empty) fields.
150+
151+ Args:
152+ data: The data dictionary
153+
154+ Returns:
155+ Number of present fields
156+ """
157+ return sum (
158+ 1 for field in self .fields
159+ if field in data and not self ._is_empty (data [field ])
160+ )
161+
41162 def check (self , data : Dict [str , Any ]) -> bool :
42163 """
43164 Check if at least the minimum required fields are present.
@@ -51,26 +172,7 @@ def check(self, data: Dict[str, Any]) -> bool:
51172 if not isinstance (data , dict ):
52173 return False
53174
54- present_count = 0
55-
56- for field in self .fields :
57- if field in data and data [field ] is not None :
58- if (
59- self .check_empty
60- and isinstance (data [field ], str )
61- and data [field ].strip () == ""
62- ):
63- continue
64-
65- if (
66- self .check_empty
67- and isinstance (data [field ], (list , dict ))
68- and len (data [field ]) == 0
69- ):
70- continue
71-
72- present_count += 1
73-
175+ present_count = self ._count_present_fields (data )
74176 return present_count >= self .min_required
75177
76178 def get_error_message (self , data : Dict [str , Any ]) -> str :
@@ -86,35 +188,17 @@ def get_error_message(self, data: Dict[str, Any]) -> str:
86188 if self .custom_message :
87189 return self .custom_message
88190
89- present_fields = []
90- missing_fields = []
91-
92- for field in self .fields :
93- if field in data and data [field ] is not None :
94- if self .check_empty :
95- if (
96- isinstance (data [field ], str )
97- and data [field ].strip () == ""
98- ) or (
99- isinstance (data [field ], (list , dict ))
100- and len (data [field ]) == 0
101- ):
102- missing_fields .append (field )
103- else :
104- present_fields .append (field )
105- else :
106- present_fields .append (field )
107- else :
108- missing_fields .append (field )
191+ present_count = self ._count_present_fields (data )
192+ fields_list = ", " .join (self .fields )
109193
110194 if self .min_required == 1 :
111- fields_list = ", " .join (self .fields )
112195 return (
113- f"At least one of the following fields is required: { fields_list } . "
114- f"None were provided or all were empty."
196+ f"At least one of the following fields is required: "
197+ f"{ fields_list } . None were provided or all were empty."
115198 )
116- fields_list = ", " . join ( self . fields )
199+
117200 return (
118201 f"At least { self .min_required } of the following fields "
119- f"are required: { fields_list } . Only { len (present_fields )} provided."
202+ f"are required: { fields_list } . "
203+ f"Only { present_count } provided."
120204 )
0 commit comments