Coverage for src/lexigram/admin/forms/state.py: 87%

62 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-21 14:56 +0800

1"""Unified Form State Management.""" 

2 

3from __future__ import annotations 

4 

5from dataclasses import dataclass, field 

6from datetime import datetime 

7from typing import Any 

8 

9 

10@dataclass 

11class FormState: 

12 """Manages the state of a form, including values and errors.""" 

13 

14 data: dict[str, Any] = field(default_factory=dict) 

15 errors: dict[str, list[str]] = field(default_factory=dict) 

16 is_dirty: bool = False 

17 is_submitting: bool = False 

18 last_saved_at: datetime | None = None 

19 

20 def update_field(self, name: str, value: Any) -> Any: 

21 """Update a field's value and mark as dirty.""" 

22 self.data[name] = value 

23 self.is_dirty = True 

24 

25 def set_error(self, name: str, error: str) -> Any: 

26 """Set an error for a field.""" 

27 if name not in self.errors: 

28 self.errors[name] = [] 

29 self.errors[name].append(error) 

30 

31 def clear_errors(self, name: str | None = None) -> Any: 

32 """Clear errors for a specific field or all fields.""" 

33 if name: 

34 self.errors.pop(name, None) 

35 else: 

36 self.errors.clear() 

37 

38 @property 

39 def has_errors(self) -> bool: 

40 """Check if any field has errors.""" 

41 return len(self.errors) > 0 

42 

43 def to_dict(self) -> dict[str, Any]: 

44 """Convert state to a dictionary for persistence.""" 

45 return { 

46 "data": self.data, 

47 "errors": self.errors, 

48 "is_dirty": self.is_dirty, 

49 "last_saved_at": self.last_saved_at.isoformat() 

50 if self.last_saved_at 

51 else None, 

52 } 

53 

54 @classmethod 

55 def from_dict(cls, data: dict[str, Any]) -> FormState: 

56 """Create state from a dictionary.""" 

57 last_saved = data.get("last_saved_at") 

58 return cls( 

59 data=data.get("data", {}), 

60 errors=data.get("errors", {}), 

61 is_dirty=data.get("is_dirty", False), 

62 last_saved_at=datetime.fromisoformat(last_saved) if last_saved else None, 

63 ) 

64 

65 

66class FormStore: 

67 """Orchestrates form state and validation logic.""" 

68 

69 def __init__( 

70 self, 

71 initial_values: dict[str, Any] | None = None, 

72 validation_engine: Any | None = None, 

73 ): 

74 self.state = FormState(data=initial_values or {}) 

75 self.validation_engine = validation_engine 

76 

77 @property 

78 def data(self) -> dict[str, Any]: 

79 return self.state.data 

80 

81 @property 

82 def errors(self) -> dict[str, list[str]]: 

83 return self.state.errors 

84 

85 @property 

86 def is_dirty(self) -> bool: 

87 return self.state.is_dirty 

88 

89 def get_value(self, name: str, default: Any = None) -> Any: 

90 return self.data.get(name, default) 

91 

92 def set_value(self, name: str, value: Any) -> Any: 

93 self.state.update_field(name, value) 

94 

95 async def validate(self) -> bool: 

96 """Validate the current form data using the validation engine.""" 

97 if not self.validation_engine: 

98 return True 

99 

100 self.state.clear_errors() 

101 errors = await self.validation_engine.validate_form(self.data) 

102 

103 if errors: 

104 for field_name, field_errors in errors.items(): 

105 for error in field_errors: 

106 msg = error.message if hasattr(error, "message") else str(error) 

107 self.state.set_error(field_name, msg) 

108 return False 

109 

110 return True 

111 

112 def to_dict(self) -> dict[str, Any]: 

113 return self.state.to_dict()