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