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.2.3
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 or self.gettext(
54 "Not a valid datetime value."
55 )
56
57 def _value(self):
58 if self.raw_data:
59 return " ".join(self.raw_data)
60 format = self.format[0]
61 return self.data and self.data.strftime(format) or ""
62
63 def process_formdata(self, valuelist):
64 if not valuelist:
65 return
66
67 date_str = " ".join(valuelist)
68 for format in self.strptime_format:
69 try:
70 self.data = datetime.datetime.strptime(date_str, format)
71 return
72 except ValueError:
73 self.data = None
74
75 raise ValueError(self.invalid_value_message)
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 self.invalid_value_message = invalid_value_message or self.gettext(
102 "Not a valid date value."
103 )
104
105 def process_formdata(self, valuelist):
106 if not valuelist:
107 return
108
109 date_str = " ".join(valuelist)
110 for format in self.strptime_format:
111 try:
112 self.data = datetime.datetime.strptime(date_str, format).date()
113 return
114 except ValueError:
115 self.data = None
116
117 raise ValueError(self.invalid_value_message)
118
119
120class TimeField(DateTimeField):
121 """
122 Same as :class:`~wtforms.fields.DateTimeField`, except stores a
123 :class:`datetime.time` and renders as an :mdn-input:`time`.
124 """
125
126 widget = widgets.TimeInput()
127
128 def __init__(
129 self,
130 label=None,
131 validators=None,
132 format="%H:%M",
133 invalid_value_message=None,
134 **kwargs,
135 ):
136 super().__init__(
137 label,
138 validators,
139 format,
140 invalid_value_message=invalid_value_message,
141 **kwargs,
142 )
143 self.invalid_value_message = invalid_value_message or self.gettext(
144 "Not a valid time value."
145 )
146
147 def process_formdata(self, valuelist):
148 if not valuelist:
149 return
150
151 time_str = " ".join(valuelist)
152 for format in self.strptime_format:
153 try:
154 self.data = datetime.datetime.strptime(time_str, format).time()
155 return
156 except ValueError:
157 self.data = None
158
159 raise ValueError(self.invalid_value_message)
160
161
162class MonthField(DateField):
163 """
164 Same as :class:`~wtforms.fields.DateField`, except represents a month,
165 stores a :class:`datetime.date` with `day = 1`, and renders as an
166 :mdn-input:`month`.
167 """
168
169 widget = widgets.MonthInput()
170
171 def __init__(self, label=None, validators=None, format="%Y-%m", **kwargs):
172 super().__init__(label, validators, format, **kwargs)
173
174
175class WeekField(DateField):
176 """
177 Same as :class:`~wtforms.fields.DateField`, except represents a week,
178 stores a :class:`datetime.date` of the monday of the given week, and
179 renders as an :mdn-input:`week`.
180 """
181
182 widget = widgets.WeekInput()
183
184 def __init__(
185 self,
186 label=None,
187 validators=None,
188 format="%Y-W%W",
189 invalid_value_message=None,
190 **kwargs,
191 ):
192 super().__init__(
193 label,
194 validators,
195 format,
196 invalid_value_message=invalid_value_message,
197 **kwargs,
198 )
199 self.invalid_value_message = invalid_value_message or self.gettext(
200 "Not a valid week value."
201 )
202
203 def process_formdata(self, valuelist):
204 if not valuelist:
205 return
206
207 time_str = " ".join(valuelist)
208 for format in self.strptime_format:
209 try:
210 if "%w" not in format:
211 # The '%w' week starting day is needed. This defaults it to monday
212 # like ISO 8601 indicates.
213 self.data = datetime.datetime.strptime(
214 f"{time_str}-1", f"{format}-%w"
215 ).date()
216 else:
217 self.data = datetime.datetime.strptime(time_str, format).date()
218 return
219 except ValueError:
220 self.data = None
221
222 raise ValueError(self.invalid_value_message)
223
224
225class DateTimeLocalField(DateTimeField):
226 """
227 Same as :class:`~wtforms.fields.DateTimeField`, but represents an
228 :mdn-input:`datetime-local`.
229
230 :param tz:
231 Optional timezone associated with the input. The HTML
232 ``datetime-local`` widget always renders and submits a naive
233 local datetime; ``tz`` declares the zone in which that local
234 datetime should be interpreted. Accepts:
235
236 - ``None`` (default): legacy behavior, :attr:`data` is naive.
237 - a :class:`datetime.tzinfo` instance: parsed values get this
238 zone attached, and aware values rendered through the field
239 are converted to it before being formatted.
240 - a callable returning a :class:`datetime.tzinfo` (or ``None``):
241 resolved on each access, useful when the zone depends on the
242 request context (e.g. user preferences). Returning ``None``
243 falls back to the naive behavior.
244
245 No correction is applied for DST gaps or overlaps — submitted
246 values are annotated as-is via ``replace(tzinfo=...)``.
247 """
248
249 widget = widgets.DateTimeLocalInput()
250
251 def __init__(
252 self,
253 *args,
254 tz: datetime.tzinfo | Callable[[], datetime.tzinfo | None] | None = None,
255 **kwargs,
256 ):
257 kwargs.setdefault(
258 "format",
259 [
260 "%Y-%m-%d %H:%M:%S",
261 "%Y-%m-%dT%H:%M:%S",
262 "%Y-%m-%d %H:%M",
263 "%Y-%m-%dT%H:%M",
264 ],
265 )
266 super().__init__(*args, **kwargs)
267 self.tz = tz
268
269 def _resolve_tz(self):
270 return self.tz() if callable(self.tz) else self.tz
271
272 def _value(self):
273 """Render :attr:`data`, converting aware values to ``tz`` and stripping
274 the zone before formatting."""
275 if self.raw_data:
276 return " ".join(self.raw_data)
277
278 if not self.data:
279 return ""
280
281 value = self.data
282 tz = self._resolve_tz()
283 if tz is not None and value.tzinfo is not None:
284 value = value.astimezone(tz).replace(tzinfo=None)
285
286 return value.strftime(self.format[0])
287
288 def process_formdata(self, valuelist):
289 """Parse the submitted value and annotate it with ``tz`` if set."""
290 super().process_formdata(valuelist)
291 tz = self._resolve_tz()
292 if tz is not None and self.data is not None:
293 self.data = self.data.replace(tzinfo=tz)