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