Coverage for src/lexigram/admin/schema/datetime_.py: 88%

113 statements  

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

1from __future__ import annotations 

2 

3from dataclasses import dataclass 

4from datetime import date, datetime, time 

5from typing import Any 

6 

7from lexigram.admin.schema.base import SchemaField 

8from lexigram.admin.schema.exceptions import FieldError 

9from lexigram.result import Err, Ok, Result 

10from lexigram.ui import DateInput, Element, TimePicker 

11 

12 

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

14class DateField(SchemaField[date]): 

15 """A date input field.""" 

16 

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

18 from lexigram.ui import InfolistEntry, InfolistEntryType 

19 

20 return InfolistEntry( 

21 name=self.name, 

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

23 value=value, 

24 type=InfolistEntryType.DATE, 

25 ) 

26 

27 def render_form( 

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

29 ) -> Element: 

30 kwargs: dict[str, Any] = { 

31 "name": self.name, 

32 "value": value.isoformat() if value is not None else "", 

33 } 

34 if self.label is not None: 

35 kwargs["label"] = self.label 

36 if errors: 

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

38 if self.readonly: 

39 kwargs["disabled"] = True 

40 return DateInput(**kwargs).render() 

41 

42 def render_column(self, record: Any, value: date | None) -> Element: 

43 if value is None: 

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

45 return Element("span", value.strftime("%B %d, %Y")) 

46 

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

48 if raw is None: 

49 return Ok(None) 

50 stripped = raw.strip() 

51 if not stripped: 

52 if self.nullable: 

53 return Ok(None) 

54 return Err(FieldError("Must be a valid date")) 

55 try: 

56 parsed = date.fromisoformat(stripped) 

57 return Ok(parsed) 

58 except (ValueError, TypeError): 

59 return Err(FieldError("Must be a valid date")) 

60 

61 def to_form(self, value: date | None) -> str: 

62 if value is None: 

63 return "" 

64 return value.isoformat() 

65 

66 

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

68class DateTimeField(SchemaField[datetime]): 

69 """A datetime input field.""" 

70 

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

72 from lexigram.ui import InfolistEntry, InfolistEntryType 

73 

74 return InfolistEntry( 

75 name=self.name, 

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

77 value=value, 

78 type=InfolistEntryType.DATE, 

79 ) 

80 

81 def render_form( 

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

83 ) -> Element: 

84 kwargs: dict[str, Any] = { 

85 "name": self.name, 

86 "value": value.isoformat() if value is not None else "", 

87 } 

88 if self.label is not None: 

89 kwargs["label"] = self.label 

90 if errors: 

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

92 if self.readonly: 

93 kwargs["disabled"] = True 

94 return DateInput(input_type="datetime-local", **kwargs).render() 

95 

96 def render_column(self, record: Any, value: datetime | None) -> Element: 

97 if value is None: 

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

99 return Element("span", value.strftime("%B %d, %Y, %-I:%M %p")) 

100 

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

102 if raw is None: 

103 return Ok(None) 

104 stripped = raw.strip() 

105 if not stripped: 

106 if self.nullable: 

107 return Ok(None) 

108 return Err(FieldError("Must be a valid datetime")) 

109 try: 

110 parsed = datetime.fromisoformat(stripped) 

111 return Ok(parsed) 

112 except (ValueError, TypeError): 

113 return Err(FieldError("Must be a valid datetime")) 

114 

115 def to_form(self, value: datetime | None) -> str: 

116 if value is None: 

117 return "" 

118 return value.isoformat() 

119 

120 

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

122class TimeField(SchemaField[time]): 

123 """A time input field.""" 

124 

125 def render_form( 

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

127 ) -> Element: 

128 kwargs: dict[str, Any] = { 

129 "name": self.name, 

130 "value": value.strftime("%H:%M") if value is not None else "", 

131 } 

132 if self.label is not None: 

133 kwargs["label"] = self.label 

134 if errors: 

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

136 if self.readonly: 

137 kwargs["disabled"] = True 

138 return TimePicker(**kwargs).render() 

139 

140 def render_column(self, record: Any, value: time | None) -> Element: 

141 if value is None: 

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

143 return Element("span", value.strftime("%-I:%M %p")) 

144 

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

146 if raw is None: 

147 return Ok(None) 

148 stripped = raw.strip() 

149 if not stripped: 

150 if self.nullable: 

151 return Ok(None) 

152 return Err(FieldError("Must be a valid time")) 

153 try: 

154 parts = stripped.split(":") 

155 hour = int(parts[0]) 

156 minute = int(parts[1]) 

157 parsed = time(hour, minute) 

158 return Ok(parsed) 

159 except (ValueError, TypeError, IndexError): 

160 return Err(FieldError("Must be a valid time")) 

161 

162 def to_form(self, value: time | None) -> str: 

163 if value is None: 

164 return "" 

165 return value.strftime("%H:%M")