1from __future__ import annotations
2
3from collections.abc import Callable, Sequence
4from typing import TYPE_CHECKING, Any
5
6from plain import validators
7from plain.preflight import PreflightResult
8from plain.validators import MaxLengthValidator
9
10from .base import NOT_PROVIDED, ChoicesField, ColumnField
11
12if TYPE_CHECKING:
13 from plain.postgres.functions.random import RandomString
14
15
16class TextField(ChoicesField[str]):
17 db_type_sql = "text"
18
19 def __init__(
20 self,
21 *,
22 max_length: int | None = None,
23 choices: Any = None,
24 required: bool = True,
25 allow_null: bool = False,
26 default: Any = NOT_PROVIDED,
27 validators: Sequence[Callable[..., Any]] = (),
28 ):
29 self.max_length = max_length
30 super().__init__(
31 choices=choices,
32 required=required,
33 allow_null=allow_null,
34 default=default,
35 validators=validators,
36 )
37 if self.max_length is not None:
38 self.validators.append(MaxLengthValidator(self.max_length))
39
40 def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
41 name, path, args, kwargs = super().deconstruct()
42 if self.max_length is not None:
43 kwargs["max_length"] = self.max_length
44 return name, path, args, kwargs
45
46 @property
47 def description(self) -> str:
48 if self.max_length is not None:
49 return "String (up to %(max_length)s)"
50 else:
51 return "String (unlimited)"
52
53 def preflight(self, **kwargs: Any) -> list[PreflightResult]:
54 return [
55 *super().preflight(**kwargs),
56 *self._check_max_length_attribute(),
57 ]
58
59 def _check_max_length_attribute(self, **kwargs: Any) -> list[PreflightResult]:
60 if self.max_length is None:
61 return []
62 elif (
63 not isinstance(self.max_length, int)
64 or isinstance(self.max_length, bool)
65 or self.max_length <= 0
66 ):
67 return [
68 PreflightResult(
69 fix="'max_length' must be a positive integer.",
70 obj=self,
71 id="fields.textfield_invalid_max_length",
72 )
73 ]
74 else:
75 return []
76
77 def _max_length_for_choices_check(self) -> int | None:
78 return self.max_length
79
80 def to_python(self, value: Any) -> str | None:
81 if isinstance(value, str) or value is None:
82 return value
83 return str(value)
84
85 def get_prep_value(self, value: Any) -> Any:
86 value = super().get_prep_value(value)
87 return self.to_python(value)
88
89
90class EmailField(TextField):
91 default_validators = [validators.validate_email]
92
93
94class URLField(TextField):
95 default_validators = [validators.URLValidator()]
96
97
98class RandomStringField(ColumnField[str]):
99 """Text column whose value is a Postgres-generated random hex string.
100
101 The column carries a ``DEFAULT`` that evaluates per row, so raw SQL and
102 ORM inserts both get a fresh ``length``-character hex string. Pass an
103 explicit value at ``create()`` time to override.
104 """
105
106 db_type_sql = "text"
107
108 def __init__(
109 self,
110 *,
111 length: int,
112 required: bool = True,
113 allow_null: bool = False,
114 validators: Sequence[Callable[..., Any]] = (),
115 ):
116 from plain.postgres.functions.random import RandomString
117
118 self._expression = RandomString(length=length)
119 super().__init__(
120 required=required,
121 allow_null=allow_null,
122 validators=validators,
123 )
124
125 def get_db_default_expression(self) -> RandomString:
126 return self._expression
127
128 def deconstruct(self) -> tuple[str | None, str, list[Any], dict[str, Any]]:
129 name, path, args, kwargs = super().deconstruct()
130 kwargs["length"] = self._expression.length
131 return name, path, args, kwargs