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

101 statements  

« prev     ^ index     » next       coverage.py v7.13.5, created at 2026-08-13 22:14 +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_form( 

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

19 ) -> Element: 

20 kwargs: dict[str, Any] = { 

21 "name": self.name, 

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

23 } 

24 if self.label is not None: 

25 kwargs["label"] = self.label 

26 if errors: 

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

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

29 

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

31 if value is None: 

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

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

34 

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

36 if raw is None: 

37 return Ok(None) 

38 stripped = raw.strip() 

39 if not stripped: 

40 if self.nullable: 

41 return Ok(None) 

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

43 try: 

44 parsed = date.fromisoformat(stripped) 

45 return Ok(parsed) 

46 except (ValueError, TypeError): 

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

48 

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

50 if value is None: 

51 return "" 

52 return value.isoformat() 

53 

54 

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

56class DateTimeField(SchemaField[datetime]): 

57 """A datetime input field.""" 

58 

59 def render_form( 

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

61 ) -> Element: 

62 kwargs: dict[str, Any] = { 

63 "name": self.name, 

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

65 } 

66 if self.label is not None: 

67 kwargs["label"] = self.label 

68 if errors: 

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

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

71 

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

73 if value is None: 

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

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

76 

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

78 if raw is None: 

79 return Ok(None) 

80 stripped = raw.strip() 

81 if not stripped: 

82 if self.nullable: 

83 return Ok(None) 

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

85 try: 

86 parsed = datetime.fromisoformat(stripped) 

87 return Ok(parsed) 

88 except (ValueError, TypeError): 

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

90 

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

92 if value is None: 

93 return "" 

94 return value.isoformat() 

95 

96 

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

98class TimeField(SchemaField[time]): 

99 """A time input field.""" 

100 

101 def render_form( 

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

103 ) -> Element: 

104 kwargs: dict[str, Any] = { 

105 "name": self.name, 

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

107 } 

108 if self.label is not None: 

109 kwargs["label"] = self.label 

110 if errors: 

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

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

113 

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

115 if value is None: 

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

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

118 

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

120 if raw is None: 

121 return Ok(None) 

122 stripped = raw.strip() 

123 if not stripped: 

124 if self.nullable: 

125 return Ok(None) 

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

127 try: 

128 parts = stripped.split(":") 

129 hour = int(parts[0]) 

130 minute = int(parts[1]) 

131 parsed = time(hour, minute) 

132 return Ok(parsed) 

133 except (ValueError, TypeError, IndexError): 

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

135 

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

137 if value is None: 

138 return "" 

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