Coverage for src / lexigram / admin / ui / organisms / form_registry.py: 51%

59 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-13 22:14 +0800

1"""Registry for form field renderers.""" 

2 

3from __future__ import annotations 

4 

5from typing import TYPE_CHECKING, Any, Protocol 

6 

7from lexigram.ui import DateInput, NumberInput, RichSelect, Switch, TextArea, TextInput 

8 

9if TYPE_CHECKING: 

10 from lexigram.admin.forms import AbstractField, FieldType 

11 

12 

13class FormFieldRendererProtocol(Protocol): 

14 """Protocol for form field renderers.""" 

15 

16 def can_render(self, field_type: FieldType) -> bool: ... 

17 def render(self, field: AbstractField, current_value: Any) -> Any: ... 

18 

19 

20class TextAreaRenderer: 

21 def can_render(self, field_type: FieldType) -> bool: 

22 from lexigram.admin.forms import FieldType 

23 

24 return field_type == FieldType.TEXTAREA 

25 

26 def render(self, field: AbstractField, current_value: Any) -> Any: 

27 return TextArea( 

28 name=field.name, 

29 label=field.label, 

30 value=str(current_value) if current_value else "", 

31 placeholder=field.placeholder, 

32 error=getattr(field, "errors", [None])[0] 

33 if getattr(field, "errors", []) 

34 else None, 

35 rows=4, 

36 disabled=not getattr(field, "editable", True), 

37 ) 

38 

39 

40class SelectRenderer: 

41 def can_render(self, field_type: FieldType) -> bool: 

42 from lexigram.admin.forms import FieldType 

43 

44 return field_type == FieldType.SELECT 

45 

46 def render(self, field: AbstractField, current_value: Any) -> Any: 

47 options_with_selection = [ 

48 {**opt, "selected": str(current_value) == str(opt["value"])} 

49 for opt in field.options # type: ignore[attr-defined] 

50 ] 

51 return RichSelect( 

52 label=field.label, # type: ignore[arg-type] 

53 name=field.name, 

54 options=options_with_selection, 

55 value=current_value, 

56 error=getattr(field, "errors", [None])[0] 

57 if getattr(field, "errors", []) 

58 else None, 

59 disabled=not getattr(field, "editable", True), 

60 ) 

61 

62 

63class CheckboxRenderer: 

64 def can_render(self, field_type: FieldType) -> bool: 

65 from lexigram.admin.forms import FieldType 

66 

67 return field_type == FieldType.CHECKBOX 

68 

69 def render(self, field: AbstractField, current_value: Any) -> Any: 

70 return Switch( 

71 label=field.label, # type: ignore[arg-type] 

72 name=field.name, 

73 value=bool(current_value) if current_value else False, 

74 description=field.help_text, 

75 error=getattr(field, "errors", [None])[0] 

76 if getattr(field, "errors", []) 

77 else None, 

78 disabled=not getattr(field, "editable", True), 

79 ) 

80 

81 

82class BelongsToFormRenderer: 

83 def can_render(self, field_type: FieldType) -> bool: 

84 from lexigram.admin.forms import FieldType 

85 

86 return field_type == FieldType.BELONGS_TO 

87 

88 def render(self, field: AbstractField, current_value: Any) -> Any: 

89 from lexigram.ui.atoms.inputs.selection.relational import BelongsTo 

90 

91 return BelongsTo( 

92 name=field.name, 

93 resource=getattr(field, "metadata", {}).get("related_resource", ""), 

94 value=current_value, 

95 label=getattr(field, "label", field.name.title()), 

96 ).render() 

97 

98 

99class HasManyFormRenderer: 

100 def can_render(self, field_type: FieldType) -> bool: 

101 from lexigram.admin.forms import FieldType 

102 

103 return field_type == FieldType.HAS_MANY 

104 

105 def render(self, field: AbstractField, current_value: Any) -> Any: 

106 from lexigram.ui.atoms.inputs.selection.choice import MultiSelect 

107 

108 choices = [ 

109 (o.get("value"), o.get("label")) for o in getattr(field, "choices", []) 

110 ] 

111 return MultiSelect( 

112 name=field.name, 

113 choices=choices, 

114 value=current_value if isinstance(current_value, list) else [], 

115 label=getattr(field, "label", field.name.title()), 

116 ).render() 

117 

118 

119class InputRenderer: 

120 """Renderer for standard and specific input types.""" 

121 

122 def __init__(self) -> None: 

123 from lexigram.admin.forms import FieldType 

124 

125 self.type_map = { 

126 FieldType.TEXT: (TextInput, "text"), 

127 FieldType.EMAIL: (TextInput, "email"), 

128 FieldType.PASSWORD: (TextInput, "password"), 

129 FieldType.NUMBER: (NumberInput, None), 

130 FieldType.DATE: (DateInput, None), 

131 } 

132 

133 def can_render(self, field_type: FieldType) -> bool: 

134 return field_type in self.type_map or True # Fallback 

135 

136 def render(self, field: AbstractField, current_value: Any) -> Any: 

137 atom_class, input_type = self.type_map.get(field.type, (TextInput, "text")) # type: ignore[attr-defined] 

138 

139 props = { 

140 "name": field.name, 

141 "label": field.label, 

142 "value": str(current_value) if current_value is not None else "", 

143 "placeholder": field.placeholder, 

144 "error": field.error, # type: ignore[attr-defined] 

145 "disabled": not getattr(field, "editable", True), 

146 } 

147 if input_type: 

148 props["type"] = input_type 

149 

150 return atom_class(**props) 

151 

152 

153class FormFieldRegistry: 

154 """Registry for form field renderers.""" 

155 

156 def __init__(self) -> None: 

157 self._renderers: list[FormFieldRendererProtocol] = [ 

158 TextAreaRenderer(), 

159 SelectRenderer(), 

160 CheckboxRenderer(), 

161 BelongsToFormRenderer(), 

162 HasManyFormRenderer(), 

163 InputRenderer(), # Catch-all 

164 ] 

165 

166 def get_renderer(self, field_type: FieldType) -> FormFieldRendererProtocol: 

167 for renderer in self._renderers: 

168 if renderer.can_render(field_type): 

169 return renderer 

170 return InputRenderer() 

171 

172 

173_form_field_registry = FormFieldRegistry()