1- from PySide6 .QtGui import QValidator
1+ from PySide6 .QtCore import Signal
22from PySide6 .QtWidgets import (
33 QCheckBox ,
44 QComboBox ,
1111 QWidget ,
1212)
1313
14- from alinka . widget . validators import RequiredValidator
14+ from alinka import rspo_client
1515
1616
1717class ValidationMixin :
18- """Mixin providing reusable validation and highlighting functionality.
19-
20- It can be used by any component that needs field validation.
18+ """
19+ Mixin class to add validation functionality to components/containers
20+ Including this mixing ensures that all components/containers have a consistent
21+ interface for validation-related methods and properties.
22+ Method should be overridden in subclasses as needed.
2123 """
2224
23- def highlight_field (self , field_component : any , highlight : bool ) -> None :
24- """Trigger highlighting of a field component with consistent styling.
25-
26- Args:
27- field_component: The component to highlight (LabeledInputComponent or LabeledComboBoxComponent)
28- highlight: Whether to highlight (True) or clear highlighting (False)
29- """
30- if not hasattr (field_component , "line_edit" ) and not hasattr (field_component , "combobox" ):
31- return
32-
33- widget = getattr (field_component , "line_edit" , None ) or getattr (field_component , "combobox" , None )
34-
35- if highlight :
36- widget .setStyleSheet ("border: 2px solid #ff4444; background-color: #fff5f5; color: black;" )
37- else :
38- widget .setStyleSheet ("" )
39-
40- def validate_required_fields (self , required_fields : list ) -> bool :
41- """Validate a list of required fields and highlight missing ones.
42-
43- Args:
44- required_fields: List of field components to validate
45-
46- Returns:
47- bool: True if all fields are valid, False otherwise
48- """
49- is_valid = True
50-
51- for field in required_fields :
52- if not field .is_valid :
53- self .highlight_field (field , True )
54- is_valid = False
55- else :
56- self .highlight_field (field , False )
57-
58- return is_valid
59-
60- def clear_highlights (self , fields : list ) -> None :
61- """Clear highlighting from a list of fields.
62-
63- Args:
64- fields: List of field components to clear highlighting from
65- """
66- for field in fields :
67- self .highlight_field (field , False )
68-
69- def connect_field_clear_handlers (self , fields : list ) -> None :
70- """Connect text change handlers to clear highlights when users start typing.
71-
72- Args:
73- fields: List of field components to connect handlers to
74- """
75- for field in fields :
76- if hasattr (field , "line_edit" ):
77- # For input fields, clear highlight when text changes
78- field .line_edit .textChanged .connect (lambda checked , f = field : self .highlight_field (f , False ))
79- elif hasattr (field , "combobox" ):
80- # For combobox fields, clear highlight when selection changes
81- field .combobox .currentTextChanged .connect (lambda text , f = field : self .highlight_field (f , False ))
82- # Also connect to currentIndexChanged for more reliable triggering
83- field .combobox .currentIndexChanged .connect (lambda index , f = field : self .highlight_field (f , False ))
84-
85-
86- class BaseComponent (QFrame ):
8725 @property
8826 def is_valid (self ) -> bool :
89- """
90- Method to check if the component is valid.
91- If this method is not overridden in the subclass, component is always valid.
92- """
27+ """Method to check if the component/container is valid."""
9328 return True
9429
9530 @property
9631 def error_message (self ) -> str | None :
9732 """
9833 Method to get the error message if the component is not valid.
99- If this method is not overridden in the subclass, it returns None.
10034 Return type should be string - if there's error message or None if there's no error.
10135 """
10236 return None
10337
38+ def validate (self ) -> bool :
39+ """
40+ Trigger validation of this component/containers and subclasses.
41+ This method should also update the visual state of the component/container
42+ to reflect whether it's valid or not (e.g., highlighting fields in red)
43+ by calling self.display_validation_result(validation_result)
44+ """
45+ is_valid = self .is_valid
46+ self .display_validation_result (is_valid )
47+ return is_valid
10448
105- class LabeledInputComponent (BaseComponent ):
49+ def clear_validation_state (self ) -> None :
50+ """Reset component and it's parent validation state"""
51+ pass
52+
53+ def display_validation_result (self , validation_result : bool ) -> None :
54+ """Display validation result by updating component's appearance"""
55+ pass
56+
57+
58+ class LabeledInputComponent (ValidationMixin , QFrame ):
10659 def __init__ (
10760 self ,
10861 text : str ,
10962 parent : QWidget ,
11063 min_length : int | None = None ,
11164 required : bool = False ,
112- validator : QValidator | None = None ,
11365 ):
114- self .used_validator = None
11566 self .label = text
11667 self .is_required = required
11768 super ().__init__ (parent )
@@ -123,13 +74,7 @@ def __init__(
12374 if min_length :
12475 self .line_edit .setMinimumWidth (min_length )
12576
126- if validator :
127- self .used_validator = validator
128- elif required :
129- self .used_validator = RequiredValidator ()
130- if self .used_validator :
131- self .line_edit .setValidator (self .used_validator )
132- self .line_edit .editingFinished .connect (self .validate )
77+ self .line_edit .textChanged .connect (self .clear_validation_state )
13378
13479 layout .addWidget (label )
13580 layout .addWidget (self .line_edit )
@@ -145,32 +90,22 @@ def text(self, text):
14590 def clear (self ) -> None :
14691 self .line_edit .clear ()
14792
93+ def display_validation_result (self , validation_result : bool ) -> None :
94+ if validation_result :
95+ self .toggle_highlight ("lightgreen" )
96+ else :
97+ self .toggle_highlight ("mistyrose" )
98+
14899 def toggle_highlight (self , color : str | None ) -> None :
149100 if color :
150101 self .line_edit .setStyleSheet (f"background-color: { color } ;" )
151102 else :
152103 self .line_edit .setStyleSheet ("" )
153104
154- def validate (self ) -> None :
155- if self .used_validator :
156- validation_state , _ , _ = self .used_validator .validate (self .text , 0 )
157- if validation_state != QValidator .Acceptable :
158- self .toggle_highlight ("mistyrose" )
159- else :
160- self .toggle_highlight (None )
161-
162105 @property
163106 def is_valid (self ) -> bool :
164- # For required fields without a specific validator, check if they have content
165- if self .is_required and not self .used_validator :
107+ if self .is_required :
166108 return bool (self .text .strip ())
167-
168- # For fields with validators, check the validation state
169- if self .used_validator :
170- validation_state , _ , _ = self .used_validator .validate (self .text , 0 )
171- return validation_state == QValidator .Acceptable
172-
173- # For non-required fields without validators, always valid
174109 return True
175110
176111 @property
@@ -180,17 +115,26 @@ def error_message(self) -> str | None:
180115 if self .used_validator and not self .is_valid :
181116 return self .used_validator .default_error_message
182117
118+ def clear_validation_state (self ) -> None :
119+ """Reset component and it's parent validation state"""
120+ self .parent ().clear_validation_state ()
121+ self .toggle_highlight (None )
183122
184- class LabeledComboBoxComponent (BaseComponent ):
123+
124+ class LabeledComboBoxComponent (ValidationMixin , QFrame ):
185125 def __init__ (
186126 self ,
187127 text : str ,
188128 parent : QWidget ,
189129 min_length : int | None = None ,
190130 required : bool = False ,
131+ static : bool = False ,
191132 ):
192133 self .label = text
193134 self .is_required = required
135+ # If static, options won't change dynamically, e.g. school types
136+ # clear means in this case remove selection only
137+ self .is_static = static
194138 super ().__init__ (parent )
195139 layout = QVBoxLayout (self )
196140 layout .setContentsMargins (0 , 0 , 0 , 0 )
@@ -200,6 +144,7 @@ def __init__(
200144 if min_length :
201145 self .combobox .setMinimumWidth (min_length )
202146
147+ self .combobox .currentIndexChanged .connect (self .clear_validation_state )
203148 layout .addWidget (label )
204149 layout .addWidget (self .combobox )
205150
@@ -213,12 +158,37 @@ def text(self, text):
213158 if index >= 0 :
214159 self .combobox .setCurrentIndex (index )
215160
216- def clear (self ) -> None :
161+ def remove_selection (self ) -> None :
162+ self .combobox .setCurrentIndex (- 1 )
163+
164+ def clear_options (self ) -> None :
217165 self .combobox .clear ()
218166
167+ def clear (self ) -> None :
168+ self .remove_selection ()
169+ if not self .is_static :
170+ self .clear_options ()
171+
219172 def addItems (self , items : list [str ]) -> None :
220173 self .combobox .addItems (items )
221174
175+ def display_validation_result (self , validation_result : bool ) -> None :
176+ if validation_result :
177+ self .toggle_highlight ("lightgreen" )
178+ else :
179+ self .toggle_highlight ("mistyrose" )
180+
181+ def toggle_highlight (self , color : str | None ) -> None :
182+ if color :
183+ self .combobox .setStyleSheet (f"background-color: { color } ;" )
184+ else :
185+ self .combobox .setStyleSheet (None )
186+
187+ def clear_validation_state (self ) -> None :
188+ """Reset component and it's parent validation state"""
189+ self .parent ().clear_validation_state ()
190+ self .toggle_highlight (None )
191+
222192 @property
223193 def is_valid (self ) -> bool :
224194 if not self .is_required :
@@ -232,7 +202,7 @@ def error_message(self) -> str | None:
232202 return None
233203
234204
235- class LabeledCheckboxComponent (BaseComponent ):
205+ class LabeledCheckboxComponent (ValidationMixin , QFrame ):
236206 def __init__ (self , text , parent , label_position : str = "above" ):
237207 super ().__init__ (parent )
238208 label = QLabel (text = text , parent = self )
@@ -246,8 +216,11 @@ def __init__(self, text, parent, label_position: str = "above"):
246216 else :
247217 layout .addWidget (self .checkbox )
248218
219+ def clear (self ) -> None :
220+ self .checkbox .setChecked (False )
221+
249222
250- class LabeledDateComponent (BaseComponent ):
223+ class LabeledDateComponent (ValidationMixin , QFrame ):
251224 def __init__ (self , text , parent ):
252225 super ().__init__ (parent )
253226 layout = QVBoxLayout (self )
@@ -258,3 +231,61 @@ def __init__(self, text, parent):
258231 self .date_input .dateTime ()
259232 layout .addWidget (label )
260233 layout .addWidget (self .date_input )
234+
235+ def clear (self ) -> None :
236+ self .date_input .clear ()
237+
238+
239+ class SelectProvinceDistrictGroup (ValidationMixin , QFrame ):
240+ """Generic component to select provinece and district from RPSO"""
241+
242+ selection_changed = Signal ()
243+
244+ def __init__ (self , parent : QWidget ):
245+ super ().__init__ (parent )
246+ self .select_school_group = parent
247+
248+ location_frame_layout = QHBoxLayout (self )
249+ location_frame_layout .setContentsMargins (0 , 0 , 0 , 0 )
250+ self .province_combobox = LabeledComboBoxComponent ("Województwo" , self , required = True )
251+ self .province_combobox .combobox .setPlaceholderText ("Wybierz z listy..." )
252+ self .province_combobox .combobox .currentTextChanged .connect (self .on_province_changed )
253+
254+ provinces = rspo_client .list_provinces ()
255+ for province in provinces :
256+ self .province_combobox .combobox .addItem (province .name , province .id )
257+
258+ self .district_combobox = LabeledComboBoxComponent ("Powiat" , self , required = True )
259+ self .district_combobox .combobox .setPlaceholderText ("Wybierz z listy..." )
260+ self .district_combobox .combobox .currentTextChanged .connect (self .on_district_changed )
261+ location_frame_layout .addWidget (self .province_combobox )
262+ location_frame_layout .addWidget (self .district_combobox )
263+
264+ @property
265+ def province_id (self ) -> int | None :
266+ return self .province_combobox .combobox .currentData ()
267+
268+ @property
269+ def district_id (self ) -> int | None :
270+ return self .district_combobox .combobox .currentData ()
271+
272+ def populate_districts_combobox (self ) -> None :
273+ self .district_combobox .combobox .clear ()
274+ if not self .province_id :
275+ return
276+ districts = rspo_client .list_districts (province_id = self .province_id )
277+ for district in districts :
278+ self .district_combobox .combobox .addItem (district .name , district .id )
279+
280+ def on_province_changed (self ):
281+ """
282+ In case of province change we should clear all other selections and
283+ repopulate districts combobox
284+ """
285+ self .selection_changed .emit ()
286+ self .district_combobox .combobox .clear ()
287+ self .populate_districts_combobox ()
288+
289+ def on_district_changed (self ):
290+ """In case of district change we should only trigger selection changed signal"""
291+ self .selection_changed .emit ()
0 commit comments