Coverage for src/lexigram/admin/schema/text.py: 16%

93 statements  

« prev     ^ index     » next       coverage.py v7.15.4, created at 2026-08-24 23:31 +0800

1from __future__ import annotations 

2 

3from dataclasses import dataclass 

4from typing import Any 

5 

6from lexigram.admin.schema.base import SchemaField 

7from lexigram.admin.schema.exceptions import FieldError 

8from lexigram.result import Ok, Result 

9from lexigram.ui import Element, TextInput 

10 

11 

12@dataclass(frozen=True, kw_only=True) 

13class TextField(SchemaField[str]): 

14 """A single-line text input field.""" 

15 

16 def render_form( 

17 self, value: str | None, *, errors: list[str] | None = None 

18 ) -> Element: 

19 kwargs: dict[str, Any] = { 

20 "name": self.name, 

21 "value": value if value is not None else "", 

22 } 

23 if self.placeholder is not None: 

24 kwargs["placeholder"] = self.placeholder 

25 if self.label is not None: 

26 kwargs["label"] = self.label 

27 if errors: 

28 kwargs["error"] = errors[0] 

29 if self.readonly: 

30 kwargs["disabled"] = True 

31 return TextInput(**kwargs).render() 

32 

33 def render_column(self, record: Any, value: str | None) -> Element: 

34 if value is None: 

35 return Element("span", "\u2014", class_="text-muted") 

36 return Element("span", value) 

37 

38 def from_form(self, raw: str | None) -> Result[str | None, FieldError]: 

39 if raw is None: 

40 return Ok(None) 

41 stripped = raw.strip() 

42 if not stripped: 

43 if self.nullable: 

44 return Ok(None) 

45 return Ok("") 

46 return Ok(stripped) 

47 

48 

49@dataclass(frozen=True, kw_only=True) 

50class EmailField(TextField): 

51 """An email input field. 

52 

53 Renders an ``<input type="email">`` form control and displays 

54 the value as a ``mailto:`` link in columns. 

55 """ 

56 

57 def render_infolist_entry(self, value: Any) -> Any: 

58 from lexigram.ui import InfolistEntry, InfolistEntryType 

59 

60 return InfolistEntry( 

61 name=self.name, 

62 label=self.label or self.name.replace("_", " ").title(), 

63 value=value, 

64 type=InfolistEntryType.EMAIL, 

65 ) 

66 

67 def render_form( 

68 self, value: str | None, *, errors: list[str] | None = None 

69 ) -> Element: 

70 kwargs: dict[str, Any] = { 

71 "name": self.name, 

72 "value": value if value is not None else "", 

73 "input_type": "email", 

74 } 

75 if self.placeholder is not None: 

76 kwargs["placeholder"] = self.placeholder 

77 if self.label is not None: 

78 kwargs["label"] = self.label 

79 if errors: 

80 kwargs["error"] = errors[0] 

81 if self.readonly: 

82 kwargs["disabled"] = True 

83 return TextInput(**kwargs).render() 

84 

85 def render_column(self, record: Any, value: str | None) -> Element: 

86 if value is None: 

87 return Element("span", "\u2014", class_="text-muted") 

88 return Element("a", value, href=f"mailto:{value}") 

89 

90 

91@dataclass(frozen=True, kw_only=True) 

92class PasswordField(TextField): 

93 """A password input field. 

94 

95 Renders an ``<input type="password">`` form control and masks 

96 the value in columns. 

97 """ 

98 

99 def render_infolist_entry(self, value: Any) -> Any: 

100 from lexigram.ui import InfolistEntry, InfolistEntryType 

101 

102 return InfolistEntry( 

103 name=self.name, 

104 label=self.label or self.name.replace("_", " ").title(), 

105 value="\u2022\u2022\u2022\u2022\u2022\u2022" if value is not None else None, 

106 type=InfolistEntryType.TEXT, 

107 ) 

108 

109 def render_form( 

110 self, value: str | None, *, errors: list[str] | None = None 

111 ) -> Element: 

112 kwargs: dict[str, Any] = { 

113 "name": self.name, 

114 "value": value if value is not None else "", 

115 "input_type": "password", 

116 } 

117 if self.placeholder is not None: 

118 kwargs["placeholder"] = self.placeholder 

119 if self.label is not None: 

120 kwargs["label"] = self.label 

121 if errors: 

122 kwargs["error"] = errors[0] 

123 if self.readonly: 

124 kwargs["disabled"] = True 

125 return TextInput(**kwargs).render() 

126 

127 def render_column(self, record: Any, value: str | None) -> Element: 

128 if value is None: 

129 return Element("span", "\u2014", class_="text-muted") 

130 return Element("span", "\u2022\u2022\u2022\u2022\u2022\u2022") 

131 

132 

133@dataclass(frozen=True, kw_only=True) 

134class URLField(TextField): 

135 """A URL input field. 

136 

137 Renders an ``<input type="url">`` form control and displays 

138 the value as a clickable link in columns. 

139 """ 

140 

141 def render_infolist_entry(self, value: Any) -> Any: 

142 from lexigram.ui import InfolistEntry, InfolistEntryType 

143 

144 return InfolistEntry( 

145 name=self.name, 

146 label=self.label or self.name.replace("_", " ").title(), 

147 value=value, 

148 type=InfolistEntryType.URL, 

149 ) 

150 

151 def render_form( 

152 self, value: str | None, *, errors: list[str] | None = None 

153 ) -> Element: 

154 kwargs: dict[str, Any] = { 

155 "name": self.name, 

156 "value": value if value is not None else "", 

157 "input_type": "url", 

158 } 

159 if self.placeholder is not None: 

160 kwargs["placeholder"] = self.placeholder 

161 if self.label is not None: 

162 kwargs["label"] = self.label 

163 if errors: 

164 kwargs["error"] = errors[0] 

165 if self.readonly: 

166 kwargs["disabled"] = True 

167 return TextInput(**kwargs).render() 

168 

169 def render_column(self, record: Any, value: str | None) -> Element: 

170 if value is None: 

171 return Element("span", "\u2014", class_="text-muted") 

172 return Element("a", value, href=value, target="_blank")