Поля и виджеты

Все типы столбцов, которые понимает FastFort, элемент управления, который для них рисуется, и четыре случая, когда очевидный элемент управления испортил бы данные.

Доступно с версии v0.1.0

Вы не выбираете элементы управления — это делает тип столбца. Тридцать семь типов полей отображаются на двадцать один элемент управления, и это соответствие — одна таблица, а не ветвление в каждом шаблоне.

Соответствие

ТипЭлемент управления
STRINGtext
TEXTtextarea
INTEGER · BIGINT · FLOATnumberГраницы берутся из столбца
DECIMALdecimalТочность и масштаб берутся из столбца
MONEYmoney
BOOLEANcheckbox…кроме случая, когда допускает NULL — см. ниже
DATEdateКалендарь, три вида
DATETIMEdatetimeКалендарь плюс отрисованные часы
TIMEtime
DURATIONdurationЧетыре подписанных числовых поля
UUIDtext
JSONjsonНе отправит форму с невалидным JSON
HSTOREkeyvalueПо одной паре key: value на строку
ENUMselectЗначения считываются из столбца
EMAIL · URLemail · url
PASSWORDpasswordНовый пароль плюс подтверждение
FILE · IMAGEfile · imageКарточка загрузки, drag-and-drop
ARRAYtagsКаждый элемент проверяется по типу элемента
RANGE · MULTIRANGErangeДва поля и переключатель границ
INETinetПринимает хост или сеть
MACADDRmac
BITSbitsТолько 0 и 1
GEOMETRYgeometryТекстовое поле плюс карта
VECTORtextareaНа практике только для чтения
BINARY · SEARCH_VECTORreadonlyПоказывается, но никогда не записывается
FOREIGN_KEY · ONE_TO_ONErelationПоле выбора с поиском
MANY_TO_MANYrelationsУдаляемые метки
UNKNOWNreadonlyСтрока только для чтения, а не сломанная страница

Четыре важных случая

Это случаи, когда отрисовка очевидного элемента управления приводит к потере данных.

Логическое поле, допускающее NULL, — не чекбокс

У чекбокса два состояния, а у столбца — три. Если отрисовать его как чекбокс, NULL превратится в False в первый же раз, когда кто-нибудь откроет форму и нажмёт «Сохранить», — тихое изменение данных, которое никто не просил вносить.

two_factor_enabled: Mapped[bool | None] = mapped_column(default=None)

Получает трёхпозиционный элемент управления. «Не задано» остаётся не задано.

Интервал — это не текстовое поле

planned_duration: Mapped[dt.timedelta | None] = mapped_column(sa.Interval())

Четыре подписанных поля — дни, часы, минуты, секунды. Текстовое поле, ожидающее 2 08:30:00, — это формат, о котором кому-то придётся рассказывать, и который всё равно введут неправильно.

Подсказка формата под ним показывается только при работающем скрипте, потому что, как только скрипт заменил поле на четыре подписанных поля ввода, инструкция вводить HH:MM:SS относится к элементу управления, которого на странице уже нет.

Hstore — это не repr Python-словаря

attributes: Mapped[dict[str, str] | None] = mapped_column(postgresql.HSTORE())

По одной паре key: value на строку. В ячейке списка это отображается как пары значений, а не как {'colour': 'black'} — что для любого, кто не программирует на Python, выглядит как испорченные данные.

Диапазону не нужны скобки

dock_numbers: Mapped[Any | None] = mapped_column(postgresql.INT4RANGE())
contract_period: Mapped[Any | None] = mapped_column(postgresql.DATERANGE())

Два поля и переключатель границ. Для конечных точек используется та же таблица соответствия элементов управления, что и для отдельного столбца такого типа, — поэтому оба поля daterange получают тот же календарь, что и обычный столбец Date, и они не могут разойтись между собой.

MULTIRANGE сворачивается в один диапазон на строку внутри того же элемента управления.

Переопределение одного столбца

Есть вещи, о которых столбец сам вам не скажет. Строка из семи символов — это цвет только потому, что так решил ваш проект.

class TagAdmin(admin.ModelAdmin):
    formfield_overrides = {"colour": "color"}
class ProductAdmin(admin.ModelAdmin):
    formfield_overrides = {
        "image": "image",         # a String holding a path
        "datasheet": "file",
        "description": "richtext",
    }

Имена виджетов проверяются на этапе объявления. Опечатка — это ошибка со списком всех доступных имён, а не поле, которое молча становится доступным только для чтения.

Переопределение типа везде

Если в вашем проекте есть тип EmailAddress, используемый в двадцати моделях, вам нужен элемент управления для email везде, где он применяется, — а не только в тех моделях, о которых кто-то вспомнил. Один-единственный забытый formfield_overrides — это хеш пароля в обычном текстовом поле на странице, которую никто не проверил.

import sqlalchemy as sa

from fastfort.orm.sqlalchemy import Classification, register_type
from fastfort.spec import FieldType


class EmailAddress(sa.types.TypeDecorator[str]):
    impl = sa.String
    cache_ok = True


class PasswordHash(sa.types.TypeDecorator[str]):
    impl = sa.String
    cache_ok = True


_RULES = {EmailAddress: FieldType.EMAIL, PasswordHash: FieldType.PASSWORD}


def _classify(column):
    for declared, field_type in _RULES.items():
        if isinstance(column.type, declared):
            return Classification(field_type)
    # Decline, and the next rule gets a turn. Every rule follows this contract.
    return None


def register_project_types() -> None:
    register_type(_classify, first=True)

Тут важно не ошибиться в двух вещах:

first=True. Без этого правило вообще никогда не сработает — TypeDecorator над String совпадёт со встроенным правилом для String задолго до того, как будет достигнуто правило, добавленное в конец, и столбец классифицируется как обычный текст.

Вызывайте это до того, как что-либо будет проинтроспектировано. Спецификации кешируются на бэкенде, поэтому спецификация, построенная до регистрации правила, так и останется неверной.

def build_fort() -> FastFort:
    register_project_types()      # first statement, not after
    fort = FastFort(...)

Загрузка файлов

file и image — это обычные строковые столбцы, хранящие путь. Байты сохраняются под media.root; столбец хранит то, что было записано.

Карточка загрузки поддерживает drag-and-drop с превью изображения или видео через URL.createObjectURL — и внутри неё сохраняется нативный <input type="file">, который и выполняет отправку, поэтому всё работает даже при отключённом JavaScript.

Загруженные файлы отдаются через админку, за тем же контролем доступа, что и любое другое представление. Загруженный файл — это такая же запись, как и любая другая строка, а не публичный ресурс.

В столбце списка

Столбец с изображением в list_display отображает миниатюру, а не хранимый путь, — путь как раз то, на что картинка отвечает мгновенно. Определяющим является элемент управления, который отрисовала бы форма, поэтому обычный String, который formfield_overrides переопределил как изображение, тоже получает миниатюру:

class UserAdmin(admin.ModelAdmin):
    list_display = ("id", "avatar", "email", "full_name")
    formfield_overrides = {"avatar": "image"}

Строки, для которых картинки ещё нет, получают заглушку того же размера, чтобы таблица не становилась рваной. Миниатюры подгружаются лениво: страница из ста строк не открывает сто запросов ещё до того, как до них долистают.

Только для чтения

BINARY и SEARCH_VECTOR показываются, но никогда не записываются — не существует поля, в которое можно было бы вписать растровое изображение или полнотекстовый индекс. UNKNOWN ведёт себя так же: столбец, который ничего не может классифицировать, деградирует до строки только для чтения, а не обрушивает страницу.

Последнее — это документированное состояние, за которым стоит тест, потому что состояние, которое ничем не проверяется, — это состояние, которое сломается в первый же раз, когда до него доберётся реальный проект.