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