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