Coverage for src/lexigram/admin/schema/datetime_.py: 33%
113 statements
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:31 +0800
« prev ^ index » next coverage.py v7.15.4, created at 2026-08-24 23:31 +0800
1from __future__ import annotations
3from dataclasses import dataclass
4from datetime import date, datetime, time
5from typing import Any
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
13@dataclass(frozen=True, kw_only=True)
14class DateField(SchemaField[date]):
15 """A date input field."""
17 def render_infolist_entry(self, value: Any) -> Any:
18 from lexigram.ui import InfolistEntry, InfolistEntryType
20 return InfolistEntry(
21 name=self.name,
22 label=self.label or self.name.replace("_", " ").title(),
23 value=value,
24 type=InfolistEntryType.DATE,
25 )
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()
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"))
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"))
61 def to_form(self, value: date | None) -> str:
62 if value is None:
63 return ""
64 return value.isoformat()
67@dataclass(frozen=True, kw_only=True)
68class DateTimeField(SchemaField[datetime]):
69 """A datetime input field."""
71 def render_infolist_entry(self, value: Any) -> Any:
72 from lexigram.ui import InfolistEntry, InfolistEntryType
74 return InfolistEntry(
75 name=self.name,
76 label=self.label or self.name.replace("_", " ").title(),
77 value=value,
78 type=InfolistEntryType.DATE,
79 )
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()
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"))
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"))
115 def to_form(self, value: datetime | None) -> str:
116 if value is None:
117 return ""
118 return value.isoformat()
121@dataclass(frozen=True, kw_only=True)
122class TimeField(SchemaField[time]):
123 """A time input field."""
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()
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"))
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"))
162 def to_form(self, value: time | None) -> str:
163 if value is None:
164 return ""
165 return value.strftime("%H:%M")