1"""
2Field classes.
3"""
4
5from __future__ import annotations
6
7import copy
8import datetime
9import enum
10import json
11import math
12import re
13import uuid
14from collections.abc import Callable, Iterable, Iterator, Sequence
15from decimal import Decimal, DecimalException
16from io import BytesIO
17from typing import TYPE_CHECKING, Any, Self
18from urllib.parse import urlsplit, urlunsplit
19
20from plain import validators as validators_
21from plain.exceptions import ValidationError
22from plain.utils import timezone
23from plain.utils.dateparse import parse_datetime, parse_duration
24from plain.utils.duration import duration_string
25from plain.utils.regex_helper import _lazy_re_compile
26from plain.utils.text import pluralize_lazy
27
28from .boundfield import BoundField
29from .exceptions import FormFieldMissingError
30
31if TYPE_CHECKING:
32 from .forms import BaseForm
33
34__all__ = [
35 "Field",
36 "TextField",
37 "IntegerField",
38 "DateField",
39 "TimeField",
40 "DateTimeField",
41 "DurationField",
42 "RegexField",
43 "EmailField",
44 "FileField",
45 "ImageField",
46 "URLField",
47 "BooleanField",
48 "NullBooleanField",
49 "ChoiceField",
50 "MultipleChoiceField",
51 "FloatField",
52 "DecimalField",
53 "JSONField",
54 "TypedChoiceField",
55 "UUIDField",
56]
57
58
59_FILE_INPUT_CONTRADICTION = object()
60
61
62class Field:
63 default_validators: list[Callable[[Any], None]] = [] # Default set of validators
64 # Add an 'invalid' entry to default_error_message if you want a specific
65 # field error message not raised by the field validators.
66 default_error_messages = {
67 "required": "This field is required.",
68 }
69 empty_values = list(validators_.EMPTY_VALUES)
70
71 def __init__(
72 self,
73 *,
74 required: bool = True,
75 initial: Any = None,
76 error_messages: dict[str, str] | None = None,
77 validators: Sequence[Callable[[Any], None]] = (),
78 ):
79 # required -- Boolean that specifies whether the field is required.
80 # True by default.
81 # initial -- A value to use in this Field's initial display. This value
82 # is *not* used as a fallback if data isn't given.
83 # error_messages -- An optional dictionary to override the default
84 # messages that the field will raise.
85 # validators -- List of additional validators to use
86 self.required = required
87 self.initial = initial
88
89 messages = {}
90 for c in reversed(self.__class__.__mro__):
91 messages.update(getattr(c, "default_error_messages", {}))
92 messages.update(error_messages or {})
93 self.error_messages = messages
94
95 self.validators = [*self.default_validators, *validators]
96
97 def prepare_value(self, value: Any) -> Any:
98 return value
99
100 def to_python(self, value: Any) -> Any:
101 return value
102
103 def validate(self, value: Any) -> None:
104 if value in self.empty_values and self.required:
105 raise ValidationError(self.error_messages["required"], code="required")
106
107 def run_validators(self, value: Any) -> None:
108 if value in self.empty_values:
109 return None
110 errors = []
111 for v in self.validators:
112 try:
113 v(value)
114 except ValidationError as e:
115 if hasattr(e, "code") and e.code in self.error_messages:
116 e.message = self.error_messages[e.code]
117 errors.extend(e.error_list)
118 if errors:
119 raise ValidationError(errors)
120
121 def clean(self, value: Any) -> Any:
122 """
123 Validate the given value and return its "cleaned" value as an
124 appropriate Python object. Raise ValidationError for any errors.
125 """
126 value = self.to_python(value)
127 self.validate(value)
128 self.run_validators(value)
129 return value
130
131 def bound_data(self, data: Any, initial: Any) -> Any:
132 """
133 Return the value that should be shown for this field on render of a
134 bound form, given the submitted POST data for the field and the initial
135 data, if any.
136
137 For most fields, this will simply be data; FileFields need to handle it
138 a bit differently.
139 """
140 return data
141
142 def has_changed(self, initial: Any, data: Any) -> bool:
143 """Return True if data differs from initial."""
144 try:
145 data = self.to_python(data)
146 if hasattr(self, "_coerce"):
147 return self._coerce(data) != self._coerce(initial) # ty: ignore[call-non-callable]
148 except ValidationError:
149 return True
150 # For purposes of seeing whether something has changed, None is
151 # the same as an empty string, if the data or initial value we get
152 # is None, replace it with ''.
153 initial_value = initial if initial is not None else ""
154 data_value = data if data is not None else ""
155 return initial_value != data_value
156
157 def get_bound_field(self, form: BaseForm, field_name: str) -> BoundField:
158 """
159 Return a BoundField instance that will be used when accessing the form
160 field in a template.
161 """
162 return BoundField(form, self, field_name)
163
164 def __deepcopy__(self: Self, memo: dict[int, Any]) -> Self:
165 result = copy.copy(self)
166 memo[id(self)] = result
167 result.error_messages = self.error_messages.copy()
168 result.validators = self.validators[:]
169 return result
170
171 def value_from_form_data(self, data: Any, files: Any, html_name: str) -> Any:
172 # By default, all fields are expected to be present in HTML form data.
173 try:
174 return data[html_name]
175 except KeyError as e:
176 raise FormFieldMissingError(html_name) from e
177
178 def value_from_json_data(self, data: Any, files: Any, html_name: str) -> Any:
179 if self.required and html_name not in data:
180 raise FormFieldMissingError(html_name)
181
182 return data.get(html_name, None)
183
184
185class TextField(Field):
186 def __init__(
187 self,
188 *,
189 max_length: int | None = None,
190 min_length: int | None = None,
191 strip: bool = True,
192 empty_value: str = "",
193 required: bool = True,
194 initial: Any = None,
195 error_messages: dict[str, str] | None = None,
196 validators: Sequence[Callable[[Any], None]] = (),
197 ):
198 self.max_length = max_length
199 self.min_length = min_length
200 self.strip = strip
201 self.empty_value = empty_value
202 super().__init__(
203 required=required,
204 initial=initial,
205 error_messages=error_messages,
206 validators=validators,
207 )
208 if min_length is not None:
209 self.validators.append(validators_.MinLengthValidator(int(min_length)))
210 if max_length is not None:
211 self.validators.append(validators_.MaxLengthValidator(int(max_length)))
212 self.validators.append(validators_.ProhibitNullCharactersValidator())
213
214 def to_python(self, value: Any) -> str:
215 """Return a string."""
216 if value not in self.empty_values:
217 value = str(value)
218 if self.strip:
219 value = value.strip()
220 if value in self.empty_values:
221 return self.empty_value
222 return value
223
224
225class NumericField(Field):
226 """Base class for numeric fields with min/max/step validation."""
227
228 def __init__(
229 self,
230 *,
231 max_value: int | float | Decimal | None = None,
232 min_value: int | float | Decimal | None = None,
233 step_size: int | float | Decimal | None = None,
234 required: bool = True,
235 initial: Any = None,
236 error_messages: dict[str, str] | None = None,
237 validators: Sequence[Callable[[Any], None]] = (),
238 ):
239 self.max_value, self.min_value, self.step_size = max_value, min_value, step_size
240 super().__init__(
241 required=required,
242 initial=initial,
243 error_messages=error_messages,
244 validators=validators,
245 )
246
247 if max_value is not None:
248 self.validators.append(validators_.MaxValueValidator(max_value))
249 if min_value is not None:
250 self.validators.append(validators_.MinValueValidator(min_value))
251 if step_size is not None:
252 self.validators.append(validators_.StepValueValidator(step_size))
253
254
255class IntegerField(NumericField):
256 default_error_messages = {
257 "invalid": "Enter a whole number.",
258 }
259 re_decimal = _lazy_re_compile(r"\.0*\s*$")
260
261 def to_python(self, value: Any) -> int | None:
262 """
263 Validate that int() can be called on the input. Return the result
264 of int() or None for empty values.
265 """
266 value = super().to_python(value)
267 if value in self.empty_values:
268 return None
269 # Strip trailing decimal and zeros.
270 try:
271 value = int(self.re_decimal.sub("", str(value)))
272 except (ValueError, TypeError):
273 raise ValidationError(self.error_messages["invalid"], code="invalid")
274 return value
275
276
277class FloatField(NumericField):
278 default_error_messages = {
279 "invalid": "Enter a number.",
280 }
281
282 def to_python(self, value: Any) -> float | None:
283 """
284 Validate that float() can be called on the input. Return the result
285 of float() or None for empty values.
286 """
287 value = super().to_python(value)
288 if value in self.empty_values:
289 return None
290 try:
291 value = float(value)
292 except (ValueError, TypeError):
293 raise ValidationError(self.error_messages["invalid"], code="invalid")
294 return value
295
296 def validate(self, value: Any) -> None:
297 super().validate(value)
298 if value in self.empty_values:
299 return None
300 if not math.isfinite(value):
301 raise ValidationError(self.error_messages["invalid"], code="invalid")
302
303
304class DecimalField(NumericField):
305 default_error_messages = {
306 "invalid": "Enter a number.",
307 }
308
309 def __init__(
310 self,
311 *,
312 max_value: Decimal | int | None = None,
313 min_value: Decimal | int | None = None,
314 max_digits: int | None = None,
315 decimal_places: int | None = None,
316 required: bool = True,
317 initial: Any = None,
318 error_messages: dict[str, str] | None = None,
319 validators: Sequence[Callable[[Any], None]] = (),
320 ):
321 self.max_digits, self.decimal_places = max_digits, decimal_places
322 super().__init__(
323 max_value=max_value,
324 min_value=min_value,
325 required=required,
326 initial=initial,
327 error_messages=error_messages,
328 validators=validators,
329 )
330 self.validators.append(validators_.DecimalValidator(max_digits, decimal_places))
331
332 def to_python(self, value: Any) -> Decimal | None:
333 """
334 Validate that the input is a decimal number. Return a Decimal
335 instance or None for empty values. Ensure that there are no more
336 than max_digits in the number and no more than decimal_places digits
337 after the decimal point.
338 """
339 if value in self.empty_values:
340 return None
341 try:
342 value = Decimal(str(value))
343 except DecimalException:
344 raise ValidationError(self.error_messages["invalid"], code="invalid")
345 return value
346
347 def validate(self, value: Any) -> None:
348 super().validate(value)
349 if value in self.empty_values:
350 return None
351 if not value.is_finite():
352 raise ValidationError(
353 self.error_messages["invalid"],
354 code="invalid",
355 params={"value": value},
356 )
357
358
359class BaseTemporalField(Field):
360 # Default formats to be used when parsing dates from input boxes, in order
361 # See all available format string here:
362 # https://docs.python.org/library/datetime.html#strftime-behavior
363 # * Note that these format strings are different from the ones to display dates
364 DATE_INPUT_FORMATS = [
365 "%Y-%m-%d", # '2006-10-25'
366 "%m/%d/%Y", # '10/25/2006'
367 "%m/%d/%y", # '10/25/06'
368 "%b %d %Y", # 'Oct 25 2006'
369 "%b %d, %Y", # 'Oct 25, 2006'
370 "%d %b %Y", # '25 Oct 2006'
371 "%d %b, %Y", # '25 Oct, 2006'
372 "%B %d %Y", # 'October 25 2006'
373 "%B %d, %Y", # 'October 25, 2006'
374 "%d %B %Y", # '25 October 2006'
375 "%d %B, %Y", # '25 October, 2006'
376 ]
377
378 # Default formats to be used when parsing times from input boxes, in order
379 # See all available format string here:
380 # https://docs.python.org/library/datetime.html#strftime-behavior
381 # * Note that these format strings are different from the ones to display dates
382 TIME_INPUT_FORMATS = [
383 "%H:%M:%S", # '14:30:59'
384 "%H:%M:%S.%f", # '14:30:59.000200'
385 "%H:%M", # '14:30'
386 ]
387
388 # Default formats to be used when parsing dates and times from input boxes,
389 # in order
390 # See all available format string here:
391 # https://docs.python.org/library/datetime.html#strftime-behavior
392 # * Note that these format strings are different from the ones to display dates
393 DATETIME_INPUT_FORMATS = [
394 "%Y-%m-%d %H:%M:%S", # '2006-10-25 14:30:59'
395 "%Y-%m-%d %H:%M:%S.%f", # '2006-10-25 14:30:59.000200'
396 "%Y-%m-%d %H:%M", # '2006-10-25 14:30'
397 "%m/%d/%Y %H:%M:%S", # '10/25/2006 14:30:59'
398 "%m/%d/%Y %H:%M:%S.%f", # '10/25/2006 14:30:59.000200'
399 "%m/%d/%Y %H:%M", # '10/25/2006 14:30'
400 "%m/%d/%y %H:%M:%S", # '10/25/06 14:30:59'
401 "%m/%d/%y %H:%M:%S.%f", # '10/25/06 14:30:59.000200'
402 "%m/%d/%y %H:%M", # '10/25/06 14:30'
403 ]
404
405 def __init__(
406 self,
407 *,
408 input_formats: list[str] | None = None,
409 required: bool = True,
410 initial: Any = None,
411 error_messages: dict[str, str] | None = None,
412 validators: Sequence[Callable[[Any], None]] = (),
413 ):
414 super().__init__(
415 required=required,
416 initial=initial,
417 error_messages=error_messages,
418 validators=validators,
419 )
420 if input_formats is not None:
421 self.input_formats = input_formats
422
423 def to_python(self, value: Any) -> Any:
424 value = value.strip()
425 # Try to strptime against each input format.
426 for format in self.input_formats:
427 try:
428 return self.strptime(value, format)
429 except (ValueError, TypeError):
430 continue
431 raise ValidationError(self.error_messages["invalid"], code="invalid")
432
433 def strptime(self, value: str, format: str) -> Any:
434 raise NotImplementedError("Subclasses must define this method.")
435
436
437class DateField(BaseTemporalField):
438 input_formats = BaseTemporalField.DATE_INPUT_FORMATS
439 default_error_messages = {
440 "invalid": "Enter a valid date.",
441 }
442
443 def to_python(self, value: Any) -> datetime.date | None:
444 """
445 Validate that the input can be converted to a date. Return a Python
446 datetime.date object.
447 """
448 if value in self.empty_values:
449 return None
450 if isinstance(value, datetime.datetime):
451 return value.date()
452 if isinstance(value, datetime.date):
453 return value
454 return super().to_python(value)
455
456 def strptime(self, value: str, format: str) -> datetime.date:
457 return datetime.datetime.strptime(value, format).date()
458
459
460class TimeField(BaseTemporalField):
461 input_formats = BaseTemporalField.TIME_INPUT_FORMATS
462 default_error_messages = {"invalid": "Enter a valid time."}
463
464 def to_python(self, value: Any) -> datetime.time | None:
465 """
466 Validate that the input can be converted to a time. Return a Python
467 datetime.time object.
468 """
469 if value in self.empty_values:
470 return None
471 if isinstance(value, datetime.time):
472 return value
473 return super().to_python(value)
474
475 def strptime(self, value: str, format: str) -> datetime.time:
476 return datetime.datetime.strptime(value, format).time()
477
478
479class DateTimeFormatsIterator:
480 def __iter__(self) -> Any:
481 yield from BaseTemporalField.DATETIME_INPUT_FORMATS
482 yield from BaseTemporalField.DATE_INPUT_FORMATS
483
484
485class DateTimeField(BaseTemporalField):
486 input_formats = DateTimeFormatsIterator()
487 default_error_messages = {
488 "invalid": "Enter a valid date/time.",
489 }
490
491 def prepare_value(self, value: Any) -> Any:
492 if isinstance(value, datetime.datetime):
493 value = to_current_timezone(value)
494 return value
495
496 def to_python(self, value: Any) -> datetime.datetime | None:
497 """
498 Validate that the input can be converted to a datetime. Return a
499 Python datetime.datetime object.
500 """
501 if value in self.empty_values:
502 return None
503 if isinstance(value, datetime.datetime):
504 return from_current_timezone(value)
505 if isinstance(value, datetime.date):
506 result = datetime.datetime(value.year, value.month, value.day)
507 return from_current_timezone(result)
508 try:
509 result = parse_datetime(value.strip())
510 except ValueError:
511 raise ValidationError(self.error_messages["invalid"], code="invalid")
512 if not result:
513 result = super().to_python(value)
514 return from_current_timezone(result)
515
516 def strptime(self, value: str, format: str) -> datetime.datetime:
517 return datetime.datetime.strptime(value, format)
518
519
520class DurationField(Field):
521 default_error_messages = {
522 "invalid": "Enter a valid duration.",
523 "overflow": "The number of days must be between {min_days} and {max_days}.",
524 }
525
526 def prepare_value(self, value: Any) -> Any:
527 if isinstance(value, datetime.timedelta):
528 return duration_string(value)
529 return value
530
531 def to_python(self, value: Any) -> datetime.timedelta | None:
532 if value in self.empty_values:
533 return None
534 if isinstance(value, datetime.timedelta):
535 return value
536 try:
537 value = parse_duration(str(value))
538 except OverflowError:
539 raise ValidationError(
540 self.error_messages["overflow"].format(
541 min_days=datetime.timedelta.min.days,
542 max_days=datetime.timedelta.max.days,
543 ),
544 code="overflow",
545 )
546 if value is None:
547 raise ValidationError(self.error_messages["invalid"], code="invalid")
548 return value
549
550
551class RegexField(TextField):
552 def __init__(
553 self,
554 regex: str | re.Pattern[str],
555 *,
556 max_length: int | None = None,
557 min_length: int | None = None,
558 strip: bool = False,
559 empty_value: str = "",
560 required: bool = True,
561 initial: Any = None,
562 error_messages: dict[str, str] | None = None,
563 validators: Sequence[Callable[[Any], None]] = (),
564 ) -> None:
565 """
566 regex can be either a string or a compiled regular expression object.
567 """
568 super().__init__(
569 max_length=max_length,
570 min_length=min_length,
571 strip=strip,
572 empty_value=empty_value,
573 required=required,
574 initial=initial,
575 error_messages=error_messages,
576 validators=validators,
577 )
578 self._set_regex(regex)
579
580 def _get_regex(self) -> re.Pattern[str]:
581 return self._regex
582
583 def _set_regex(self, regex: str | re.Pattern[str]) -> None:
584 if isinstance(regex, str):
585 regex = re.compile(regex)
586 self._regex = regex
587 if (
588 hasattr(self, "_regex_validator")
589 and self._regex_validator in self.validators
590 ):
591 self.validators.remove(self._regex_validator)
592 self._regex_validator = validators_.RegexValidator(regex=regex)
593 self.validators.append(self._regex_validator)
594
595 regex = property(_get_regex, _set_regex)
596
597
598class EmailField(TextField):
599 default_validators = [validators_.validate_email]
600
601 def __init__(
602 self,
603 *,
604 max_length: int | None = None,
605 min_length: int | None = None,
606 strip: bool = True,
607 empty_value: str = "",
608 required: bool = True,
609 initial: Any = None,
610 error_messages: dict[str, str] | None = None,
611 validators: Sequence[Callable[[Any], None]] = (),
612 ) -> None:
613 super().__init__(
614 max_length=max_length,
615 min_length=min_length,
616 strip=strip,
617 empty_value=empty_value,
618 required=required,
619 initial=initial,
620 error_messages=error_messages,
621 validators=validators,
622 )
623
624
625class FileField(Field):
626 default_error_messages = {
627 "invalid": "No file was submitted. Check the encoding type on the form.",
628 "missing": "No file was submitted.",
629 "empty": "The submitted file is empty.",
630 "text": pluralize_lazy(
631 "Ensure this filename has at most %(max)d character (it has %(length)d).",
632 "Ensure this filename has at most %(max)d characters (it has %(length)d).",
633 "max",
634 ),
635 "contradiction": "Please either submit a file or check the clear checkbox, not both.",
636 }
637
638 def __init__(
639 self,
640 *,
641 max_length: int | None = None,
642 allow_empty_file: bool = False,
643 required: bool = True,
644 initial: Any = None,
645 error_messages: dict[str, str] | None = None,
646 validators: Sequence[Callable[[Any], None]] = (),
647 ) -> None:
648 self.max_length = max_length
649 self.allow_empty_file = allow_empty_file
650 super().__init__(
651 required=required,
652 initial=initial,
653 error_messages=error_messages,
654 validators=validators,
655 )
656
657 def to_python(self, value: Any) -> Any:
658 if value in self.empty_values:
659 return None
660
661 # UploadedFile objects should have name and size attributes.
662 try:
663 file_name = value.name
664 file_size = value.size
665 except AttributeError:
666 raise ValidationError(self.error_messages["invalid"], code="invalid")
667
668 if self.max_length is not None and len(file_name) > self.max_length:
669 params = {"max": self.max_length, "length": len(file_name)}
670 raise ValidationError(
671 self.error_messages["max_length"], code="max_length", params=params
672 )
673 if not file_name:
674 raise ValidationError(self.error_messages["invalid"], code="invalid")
675 if not self.allow_empty_file and not file_size:
676 raise ValidationError(self.error_messages["empty"], code="empty")
677
678 return value
679
680 def clean(self, data: Any, initial: Any = None) -> Any: # ty: ignore[invalid-method-override]
681 # If the widget got contradictory inputs, we raise a validation error
682 if data is _FILE_INPUT_CONTRADICTION:
683 raise ValidationError(
684 self.error_messages["contradiction"], code="contradiction"
685 )
686 # False means the field value should be cleared; further validation is
687 # not needed.
688 if data is False:
689 if not self.required:
690 return False
691 # If the field is required, clearing is not possible (the widget
692 # shouldn't return False data in that case anyway). False is not
693 # in self.empty_value; if a False value makes it this far
694 # it should be validated from here on out as None (so it will be
695 # caught by the required check).
696 data = None
697 if not data and initial:
698 return initial
699 return super().clean(data)
700
701 def bound_data(self, data: Any, initial: Any) -> Any:
702 return initial
703
704 def has_changed(self, initial: Any, data: Any) -> bool:
705 return data is not None
706
707 def value_from_form_data(self, data: Any, files: Any, html_name: str) -> Any:
708 return files.get(html_name)
709
710 def value_from_json_data(self, data: Any, files: Any, html_name: str) -> Any:
711 return files.get(html_name)
712
713
714class ImageField(FileField):
715 default_validators = [validators_.validate_image_file_extension]
716 default_error_messages = {
717 "invalid_image": "Upload a valid image. The file you uploaded was either not an image or a corrupted image.",
718 }
719
720 def to_python(self, value: Any) -> Any:
721 """
722 Check that the file-upload field data contains a valid image (GIF, JPG,
723 PNG, etc. -- whatever Pillow supports).
724 """
725 f = super().to_python(value)
726 if f is None:
727 return None
728
729 from PIL import Image # ty: ignore[unresolved-import]
730
731 # We need to get a file object for Pillow. We might have a path or we might
732 # have to read the data into memory.
733 if hasattr(value, "temporary_file_path"):
734 file = value.temporary_file_path()
735 else:
736 if hasattr(value, "read"):
737 file = BytesIO(value.read())
738 else:
739 file = BytesIO(value["content"])
740
741 try:
742 # load() could spot a truncated JPEG, but it loads the entire
743 # image in memory, which is a DoS vector. See #3848 and #18520.
744 image = Image.open(file)
745 # verify() must be called immediately after the constructor.
746 image.verify()
747
748 # Annotating so subclasses can reuse it for their own validation
749 f.image = image
750 # Pillow doesn't detect the MIME type of all formats. In those
751 # cases, content_type will be None.
752 f.content_type = Image.MIME.get(image.format)
753 except Exception as exc:
754 # Pillow doesn't recognize it as an image.
755 raise ValidationError(
756 self.error_messages["invalid_image"],
757 code="invalid_image",
758 ) from exc
759 if hasattr(f, "seek") and callable(f.seek):
760 f.seek(0)
761 return f
762
763
764class URLField(TextField):
765 default_error_messages = {
766 "invalid": "Enter a valid URL.",
767 }
768 default_validators = [validators_.URLValidator()]
769
770 def __init__(
771 self,
772 *,
773 max_length: int | None = None,
774 min_length: int | None = None,
775 strip: bool = True,
776 empty_value: str = "",
777 required: bool = True,
778 initial: Any = None,
779 error_messages: dict[str, str] | None = None,
780 validators: Sequence[Callable[[Any], None]] = (),
781 ) -> None:
782 super().__init__(
783 max_length=max_length,
784 min_length=min_length,
785 strip=strip,
786 empty_value=empty_value,
787 required=required,
788 initial=initial,
789 error_messages=error_messages,
790 validators=validators,
791 )
792
793 def to_python(self, value: Any) -> str:
794 def split_url(url: str | bytes) -> list[str]:
795 """
796 Return a list of url parts via urlparse.urlsplit(), or raise
797 ValidationError for some malformed URLs.
798 """
799 try:
800 # Ensure url is a string for consistent typing
801 if isinstance(url, bytes):
802 url = url.decode("utf-8")
803 return list(urlsplit(url))
804 except ValueError:
805 # urlparse.urlsplit can raise a ValueError with some
806 # misformatted URLs.
807 raise ValidationError(self.error_messages["invalid"], code="invalid")
808
809 value = super().to_python(value)
810 if value:
811 url_fields = split_url(value)
812 if not url_fields[0]:
813 # If no URL scheme given, assume http://
814 url_fields[0] = "http"
815 if not url_fields[1]:
816 # Assume that if no domain is provided, that the path segment
817 # contains the domain.
818 url_fields[1] = url_fields[2]
819 url_fields[2] = ""
820 # Rebuild the url_fields list, since the domain segment may now
821 # contain the path too.
822 url_result = urlunsplit(url_fields)
823 url_fields = split_url(
824 str(url_result) if isinstance(url_result, bytes) else url_result
825 )
826 value = str(urlunsplit(url_fields))
827 return value
828
829
830class BooleanField(Field):
831 def to_python(self, value: Any) -> bool:
832 """Return a Python boolean object."""
833 # Explicitly check for the string 'False', which is what a hidden field
834 # will submit for False. Also check for '0', since this is what
835 # RadioSelect will provide. Because bool("True") == bool('1') == True,
836 # we don't need to handle that explicitly.
837 if isinstance(value, str) and value.lower() in ("false", "0"):
838 value = False
839 else:
840 value = bool(value)
841 return super().to_python(value)
842
843 def validate(self, value: Any) -> None:
844 if not value and self.required:
845 raise ValidationError(self.error_messages["required"], code="required")
846
847 def has_changed(self, initial: Any, data: Any) -> bool:
848 # Sometimes data or initial may be a string equivalent of a boolean
849 # so we should run it through to_python first to get a boolean value
850 return self.to_python(initial) != self.to_python(data)
851
852 def value_from_form_data(
853 self, data: Any, files: Any, html_name: str
854 ) -> bool | None:
855 if html_name not in data:
856 # Unselected checkboxes aren't in HTML form data, so return False
857 return False
858
859 value = data.get(html_name)
860 # Translate true and false strings to boolean values.
861 return {
862 True: True,
863 "True": True,
864 "False": False,
865 False: False,
866 "true": True,
867 "false": False,
868 "on": True,
869 }.get(value)
870
871 def value_from_json_data(self, data: Any, files: Any, html_name: str) -> Any:
872 # Boolean fields must be present in the JSON data
873 try:
874 return data[html_name]
875 except KeyError as e:
876 raise FormFieldMissingError(html_name) from e
877
878
879class NullBooleanField(BooleanField):
880 """
881 A field whose valid values are None, True, and False. Clean invalid values
882 to None.
883 """
884
885 def to_python(self, value: Any) -> bool | None: # ty: ignore[invalid-method-override]
886 """
887 Explicitly check for the string 'True' and 'False', which is what a
888 hidden field will submit for True and False, for 'true' and 'false',
889 which are likely to be returned by JavaScript serializations of forms,
890 and for '1' and '0', which is what a RadioField will submit. Unlike
891 the Booleanfield, this field must check for True because it doesn't
892 use the bool() function.
893 """
894 if value in (True, "True", "true", "1"):
895 return True
896 elif value in (False, "False", "false", "0"):
897 return False
898 else:
899 return None
900
901 def validate(self, value: Any) -> None:
902 pass
903
904
905class CallableChoiceIterator:
906 def __init__(self, choices_func: Callable[[], Any]) -> None:
907 self.choices_func = choices_func
908
909 def __iter__(self) -> Iterator[Any]:
910 yield from self.choices_func()
911
912
913class ChoiceField(Field):
914 default_error_messages = {
915 "invalid_choice": "Select a valid choice. %(value)s is not one of the available choices.",
916 }
917
918 _choices: CallableChoiceIterator | list[Any] # Set by choices property setter
919
920 def __init__(
921 self,
922 *,
923 choices: Any = (),
924 required: bool = True,
925 initial: Any = None,
926 error_messages: dict[str, str] | None = None,
927 validators: Sequence[Callable[[Any], None]] = (),
928 ) -> None:
929 super().__init__(
930 required=required,
931 initial=initial,
932 error_messages=error_messages,
933 validators=validators,
934 )
935 if hasattr(choices, "choices"):
936 choices = choices.choices
937 elif isinstance(choices, enum.EnumMeta):
938 choices = [(member.value, member.name) for member in choices]
939 self.choices = choices
940
941 def __deepcopy__(self, memo: dict[int, Any]) -> ChoiceField:
942 result = super().__deepcopy__(memo)
943 result._choices = copy.deepcopy(self._choices, memo)
944 return result
945
946 def _get_choices(self) -> Iterable[Any]:
947 return self._choices
948
949 def _set_choices(self, value: Any) -> None:
950 # Setting choices also sets the choices on the widget.
951 # choices can be any iterable, but we call list() on it because
952 # it will be consumed more than once.
953 if callable(value):
954 value = CallableChoiceIterator(value)
955 else:
956 value = list(value)
957
958 self._choices = value
959
960 choices = property(_get_choices, _set_choices)
961
962 def to_python(self, value: Any) -> str:
963 """Return a string."""
964 if value in self.empty_values:
965 return ""
966 return str(value)
967
968 def validate(self, value: Any) -> None:
969 """Validate that the input is in self.choices."""
970 super().validate(value)
971 if value and not self.valid_value(value):
972 raise ValidationError(
973 self.error_messages["invalid_choice"],
974 code="invalid_choice",
975 params={"value": value},
976 )
977
978 def valid_value(self, value: Any) -> bool:
979 """Check to see if the provided value is a valid choice."""
980 text_value = str(value)
981 for k, v in self.choices:
982 if isinstance(v, list | tuple):
983 # This is an optgroup, so look inside the group for options
984 for k2, _ in v:
985 if value == k2 or text_value == str(k2):
986 return True
987 else:
988 if value == k or text_value == str(k):
989 return True
990 return False
991
992
993class TypedChoiceField(ChoiceField):
994 def __init__(
995 self,
996 *,
997 coerce: Callable[[Any], Any] = lambda val: val,
998 empty_value: Any = "",
999 choices: Any = (),
1000 required: bool = True,
1001 initial: Any = None,
1002 error_messages: dict[str, str] | None = None,
1003 validators: Sequence[Callable[[Any], None]] = (),
1004 ) -> None:
1005 self.coerce = coerce
1006 self.empty_value = empty_value
1007 super().__init__(
1008 choices=choices,
1009 required=required,
1010 initial=initial,
1011 error_messages=error_messages,
1012 validators=validators,
1013 )
1014
1015 def _coerce(self, value: Any) -> Any:
1016 """
1017 Validate that the value can be coerced to the right type (if not empty).
1018 """
1019 if value == self.empty_value or value in self.empty_values:
1020 return self.empty_value
1021 try:
1022 value = self.coerce(value)
1023 except (ValueError, TypeError, ValidationError):
1024 raise ValidationError(
1025 self.error_messages["invalid_choice"],
1026 code="invalid_choice",
1027 params={"value": value},
1028 )
1029 return value
1030
1031 def clean(self, value: Any) -> Any:
1032 value = super().clean(value)
1033 return self._coerce(value)
1034
1035
1036class MultipleChoiceField(ChoiceField):
1037 default_error_messages = {
1038 "invalid_choice": "Select a valid choice. %(value)s is not one of the available choices.",
1039 "invalid_list": "Enter a list of values.",
1040 }
1041
1042 def to_python(self, value: Any) -> list[str]: # ty: ignore[invalid-method-override]
1043 if not value:
1044 return []
1045 elif not isinstance(value, list | tuple):
1046 raise ValidationError(
1047 self.error_messages["invalid_list"], code="invalid_list"
1048 )
1049 return [str(val) for val in value]
1050
1051 def validate(self, value: Any) -> None:
1052 """Validate that the input is a list or tuple."""
1053 if self.required and not value:
1054 raise ValidationError(self.error_messages["required"], code="required")
1055 # Validate that each value in the value list is in self.choices.
1056 for val in value:
1057 if not self.valid_value(val):
1058 raise ValidationError(
1059 self.error_messages["invalid_choice"],
1060 code="invalid_choice",
1061 params={"value": val},
1062 )
1063
1064 def has_changed(self, initial: Any, data: Any) -> bool:
1065 if initial is None:
1066 initial = []
1067 if data is None:
1068 data = []
1069 if len(initial) != len(data):
1070 return True
1071 initial_set = {str(value) for value in initial}
1072 data_set = {str(value) for value in data}
1073 return data_set != initial_set
1074
1075 def value_from_form_data(self, data: Any, files: Any, html_name: str) -> Any:
1076 return data.getlist(html_name)
1077
1078
1079class UUIDField(TextField):
1080 default_error_messages = {
1081 "invalid": "Enter a valid UUID.",
1082 }
1083
1084 def prepare_value(self, value: Any) -> Any:
1085 if isinstance(value, uuid.UUID):
1086 return str(value)
1087 return value
1088
1089 def to_python(self, value: Any) -> uuid.UUID | None: # ty: ignore[invalid-method-override]
1090 value = super().to_python(value)
1091 if value in self.empty_values:
1092 return None
1093 if not isinstance(value, uuid.UUID):
1094 try:
1095 value = uuid.UUID(value)
1096 except ValueError:
1097 raise ValidationError(self.error_messages["invalid"], code="invalid")
1098 return value
1099
1100
1101class InvalidJSONInput(str):
1102 pass
1103
1104
1105class JSONString(str):
1106 pass
1107
1108
1109class JSONField(TextField):
1110 default_error_messages = {
1111 "invalid": "Enter a valid JSON.",
1112 }
1113
1114 def __init__(
1115 self,
1116 encoder: Any = None,
1117 decoder: Any = None,
1118 indent: int | None = None,
1119 sort_keys: bool = False,
1120 *,
1121 max_length: int | None = None,
1122 min_length: int | None = None,
1123 strip: bool = True,
1124 empty_value: str = "",
1125 required: bool = True,
1126 initial: Any = None,
1127 error_messages: dict[str, str] | None = None,
1128 validators: Sequence[Callable[[Any], None]] = (),
1129 ) -> None:
1130 self.encoder = encoder
1131 self.decoder = decoder
1132 self.indent = indent
1133 self.sort_keys = sort_keys
1134 super().__init__(
1135 max_length=max_length,
1136 min_length=min_length,
1137 strip=strip,
1138 empty_value=empty_value,
1139 required=required,
1140 initial=initial,
1141 error_messages=error_messages,
1142 validators=validators,
1143 )
1144
1145 def to_python(self, value: Any) -> Any:
1146 if value in self.empty_values:
1147 return None
1148 elif isinstance(value, list | dict | int | float | JSONString):
1149 return value
1150 try:
1151 converted = json.loads(value, cls=self.decoder)
1152 except json.JSONDecodeError:
1153 raise ValidationError(
1154 self.error_messages["invalid"],
1155 code="invalid",
1156 params={"value": value},
1157 )
1158 if isinstance(converted, str):
1159 return JSONString(converted)
1160 else:
1161 return converted
1162
1163 def bound_data(self, data: Any, initial: Any) -> Any:
1164 if data is None:
1165 return None
1166 try:
1167 return json.loads(data, cls=self.decoder)
1168 except json.JSONDecodeError:
1169 return InvalidJSONInput(data)
1170
1171 def prepare_value(self, value: Any) -> Any:
1172 if isinstance(value, InvalidJSONInput):
1173 return value
1174 return json.dumps(
1175 value,
1176 indent=self.indent,
1177 sort_keys=self.sort_keys,
1178 ensure_ascii=False,
1179 cls=self.encoder,
1180 )
1181
1182 def has_changed(self, initial: Any, data: Any) -> bool:
1183 if super().has_changed(initial, data):
1184 return True
1185 # For purposes of seeing whether something has changed, True isn't the
1186 # same as 1 and the order of keys doesn't matter.
1187 return json.dumps(initial, sort_keys=True, cls=self.encoder) != json.dumps(
1188 self.to_python(data), sort_keys=True, cls=self.encoder
1189 )
1190
1191
1192def from_current_timezone(value: datetime.datetime | None) -> datetime.datetime | None:
1193 """
1194 When time zone support is enabled, convert naive datetimes
1195 entered in the current time zone to aware datetimes.
1196 """
1197 if value is not None and timezone.is_naive(value):
1198 current_timezone = timezone.get_current_timezone()
1199 try:
1200 if timezone._datetime_ambiguous_or_imaginary(value, current_timezone):
1201 raise ValueError("Ambiguous or non-existent time.")
1202 return timezone.make_aware(value, current_timezone)
1203 except Exception as exc:
1204 raise ValidationError(
1205 (
1206 "%(datetime)s couldn't be interpreted "
1207 "in time zone %(current_timezone)s; it "
1208 "may be ambiguous or it may not exist."
1209 ),
1210 code="ambiguous_timezone",
1211 params={"datetime": value, "current_timezone": current_timezone},
1212 ) from exc
1213 return value
1214
1215
1216def to_current_timezone(value: datetime.datetime | None) -> datetime.datetime | None:
1217 """
1218 When time zone support is enabled, convert aware datetimes
1219 to naive datetimes in the current time zone for display.
1220 """
1221 if value is not None and timezone.is_aware(value):
1222 return timezone.make_naive(value)
1223 return value