1import datetime
2import warnings
3from collections.abc import Callable
4
5from wtforms import widgets
6from wtforms.fields.core import Field
7from wtforms.utils import clean_datetime_format_for_strptime
8
9__all__ = (
10 "DateTimeField",
11 "DateField",
12 "TimeField",
13 "MonthField",
14 "DateTimeLocalField",
15 "WeekField",
16)
17
18
19class DateTimeField(Field):
20 """
21 A text field which stores a :class:`datetime.datetime` matching one or
22 several formats. If ``format`` is a list, any input value matching any
23 format will be accepted, and the first format in the list will be used
24 to produce HTML values.
25
26 .. deprecated:: 3.3.0
27 ``DateTimeField`` renders ``<input type="datetime">``, which is
28 obsolete. Use :class:`DateTimeLocalField` instead. ``DateTimeField``
29 will be removed in WTForms 3.4.
30 """
31
32 widget = widgets.DateTimeInput()
33
34 def __init__(
35 self,
36 label=None,
37 validators=None,
38 format="%Y-%m-%d %H:%M:%S",
39 invalid_value_message=None,
40 **kwargs,
41 ):
42 super().__init__(label, validators, **kwargs)
43 if type(self) is DateTimeField:
44 warnings.warn(
45 "'DateTimeField' renders <input type=\"datetime\">, which is"
46 " obsolete. Use 'DateTimeLocalField' instead. 'DateTimeField'"
47 " will be removed in WTForms 3.4.",
48 DeprecationWarning,
49 stacklevel=2,
50 )
51 self.format = format if isinstance(format, list) else [format]
52 self.strptime_format = clean_datetime_format_for_strptime(self.format)
53 self.invalid_value_message = invalid_value_message
54
55 def _value(self):
56 if self.raw_data:
57 return " ".join(self.raw_data)
58 format = self.format[0]
59 return self.data and self.data.strftime(format) or ""
60
61 def process_formdata(self, valuelist):
62 if not valuelist:
63 return
64
65 for format in self.strptime_format:
66 try:
67 date_str = " ".join(valuelist)
68 self.data = datetime.datetime.strptime(date_str, format)
69 return
70 except (ValueError, TypeError):
71 self.data = None
72
73 raise ValueError(
74 self.invalid_value_message or self.gettext("Not a valid datetime value.")
75 )
76
77
78class DateField(DateTimeField):
79 """
80 Same as :class:`~wtforms.fields.DateTimeField`, except stores a
81 :class:`datetime.date` and renders as an :mdn-input:`date`.
82 """
83
84 widget = widgets.DateInput()
85
86 def __init__(
87 self,
88 label=None,
89 validators=None,
90 format="%Y-%m-%d",
91 invalid_value_message=None,
92 **kwargs,
93 ):
94 super().__init__(
95 label,
96 validators,
97 format,
98 invalid_value_message=invalid_value_message,
99 **kwargs,
100 )
101
102 def process_formdata(self, valuelist):
103 if not valuelist:
104 return
105
106 for format in self.strptime_format:
107 try:
108 date_str = " ".join(valuelist)
109 self.data = datetime.datetime.strptime(date_str, format).date()
110 return
111 except (ValueError, TypeError):
112 self.data = None
113
114 raise ValueError(
115 self.invalid_value_message or self.gettext("Not a valid date value.")
116 )
117
118
119class TimeField(DateTimeField):
120 """
121 Same as :class:`~wtforms.fields.DateTimeField`, except stores a
122 :class:`datetime.time` and renders as an :mdn-input:`time`.
123 """
124
125 widget = widgets.TimeInput()
126
127 def __init__(
128 self,
129 label=None,
130 validators=None,
131 format="%H:%M",
132 invalid_value_message=None,
133 **kwargs,
134 ):
135 super().__init__(
136 label,
137 validators,
138 format,
139 invalid_value_message=invalid_value_message,
140 **kwargs,
141 )
142
143 def process_formdata(self, valuelist):
144 if not valuelist:
145 return
146
147 for format in self.strptime_format:
148 try:
149 time_str = " ".join(valuelist)
150 self.data = datetime.datetime.strptime(time_str, format).time()
151 return
152 except (ValueError, TypeError):
153 self.data = None
154
155 raise ValueError(
156 self.invalid_value_message or self.gettext("Not a valid time value.")
157 )
158
159
160class MonthField(DateField):
161 """
162 Same as :class:`~wtforms.fields.DateField`, except represents a month,
163 stores a :class:`datetime.date` with `day = 1`, and renders as an
164 :mdn-input:`month`.
165 """
166
167 widget = widgets.MonthInput()
168
169 def __init__(self, label=None, validators=None, format="%Y-%m", **kwargs):
170 super().__init__(label, validators, format, **kwargs)
171
172
173class WeekField(DateField):
174 """
175 Same as :class:`~wtforms.fields.DateField`, except represents a week,
176 stores a :class:`datetime.date` of the monday of the given week, and
177 renders as an :mdn-input:`week`.
178 """
179
180 widget = widgets.WeekInput()
181
182 def __init__(
183 self,
184 label=None,
185 validators=None,
186 format="%Y-W%W",
187 invalid_value_message=None,
188 **kwargs,
189 ):
190 super().__init__(
191 label,
192 validators,
193 format,
194 invalid_value_message=invalid_value_message,
195 **kwargs,
196 )
197
198 def process_formdata(self, valuelist):
199 if not valuelist:
200 return
201
202 for format in self.strptime_format:
203 try:
204 time_str = " ".join(valuelist)
205 if "%w" not in format:
206 # The '%w' week starting day is needed. This defaults it to monday
207 # like ISO 8601 indicates.
208 self.data = datetime.datetime.strptime(
209 f"{time_str}-1", f"{format}-%w"
210 ).date()
211 else:
212 self.data = datetime.datetime.strptime(time_str, format).date()
213 return
214 except (ValueError, TypeError):
215 self.data = None
216
217 raise ValueError(
218 self.invalid_value_message or self.gettext("Not a valid week value.")
219 )
220
221
222class DateTimeLocalField(DateTimeField):
223 """
224 Same as :class:`~wtforms.fields.DateTimeField`, but represents an
225 :mdn-input:`datetime-local`.
226
227 :param tz:
228 Optional timezone associated with the input. The HTML
229 ``datetime-local`` widget always renders and submits a naive
230 local datetime; ``tz`` declares the zone in which that local
231 datetime should be interpreted. Accepts:
232
233 - ``None`` (default): legacy behavior, :attr:`data` is naive.
234 - a :class:`datetime.tzinfo` instance: parsed values get this
235 zone attached, and aware values rendered through the field
236 are converted to it before being formatted.
237 - a callable returning a :class:`datetime.tzinfo` (or ``None``):
238 resolved on each access, useful when the zone depends on the
239 request context (e.g. user preferences). Returning ``None``
240 falls back to the naive behavior.
241
242 No correction is applied for DST gaps or overlaps — submitted
243 values are annotated as-is via ``replace(tzinfo=...)``.
244 """
245
246 widget = widgets.DateTimeLocalInput()
247
248 def __init__(
249 self,
250 *args,
251 tz: datetime.tzinfo | Callable[[], datetime.tzinfo | None] | None = None,
252 **kwargs,
253 ):
254 kwargs.setdefault(
255 "format",
256 [
257 "%Y-%m-%d %H:%M:%S",
258 "%Y-%m-%dT%H:%M:%S",
259 "%Y-%m-%d %H:%M",
260 "%Y-%m-%dT%H:%M",
261 ],
262 )
263 super().__init__(*args, **kwargs)
264 self.tz = tz
265
266 def _resolve_tz(self):
267 return self.tz() if callable(self.tz) else self.tz
268
269 def _value(self):
270 """Render :attr:`data`, converting aware values to ``tz`` and stripping
271 the zone before formatting."""
272 if self.raw_data:
273 return " ".join(self.raw_data)
274
275 if not self.data:
276 return ""
277
278 value = self.data
279 tz = self._resolve_tz()
280 if tz is not None and value.tzinfo is not None:
281 value = value.astimezone(tz).replace(tzinfo=None)
282
283 return value.strftime(self.format[0])
284
285 def process_formdata(self, valuelist):
286 """Parse the submitted value and annotate it with ``tz`` if set."""
287 super().process_formdata(valuelist)
288 tz = self._resolve_tz()
289 if tz is not None and self.data is not None:
290 self.data = self.data.replace(tzinfo=tz)