Поля и виджеты
Все типы столбцов, которые понимает FastFort, элемент управления, который для них рисуется, и четыре случая, когда очевидный элемент управления испортил бы данные.
Доступно с версии v0.1.0
Вы не выбираете элементы управления — это делает тип столбца. Тридцать семь типов полей отображаются на двадцать один элемент управления, и это соответствие — одна таблица, а не ветвление в каждом шаблоне.
Соответствие
| Тип | Элемент управления | |
|---|---|---|
STRING | text | |
TEXT | textarea | |
INTEGER · BIGINT · FLOAT | number | Границы берутся из столбца |
DECIMAL | decimal | Точность и масштаб берутся из столбца |
MONEY | money | |
BOOLEAN | checkbox | …кроме случая, когда допускает NULL — см. ниже |
DATE | date | Календарь, три вида |
DATETIME | datetime | Календарь плюс отрисованные часы |
TIME | time | |
DURATION | duration | Четыре подписанных числовых поля |
UUID | text | |
JSON | json | Не отправит форму с невалидным JSON |
HSTORE | keyvalue | По одной паре key: value на строку |
ENUM | select | Значения считываются из столбца |
EMAIL · URL | email · url | |
PASSWORD | password | Новый пароль плюс подтверждение |
FILE · IMAGE | file · image | Карточка загрузки, drag-and-drop |
ARRAY | tags | Каждый элемент проверяется по типу элемента |
RANGE · MULTIRANGE | range | Два поля и переключатель границ |
INET | inet | Принимает хост или сеть |
MACADDR | mac | |
BITS | bits | Только 0 и 1 |
GEOMETRY | geometry | Текстовое поле плюс карта |
VECTOR | textarea | На практике только для чтения |
BINARY · SEARCH_VECTOR | readonly | Показывается, но никогда не записывается |
FOREIGN_KEY · ONE_TO_ONE | relation | Поле выбора с поиском |
MANY_TO_MANY | relations | Удаляемые метки |
UNKNOWN | readonly | Строка только для чтения, а не сломанная страница |
Четыре важных случая
Это случаи, когда отрисовка очевидного элемента управления приводит к потере данных.
Логическое поле, допускающее 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 ведёт себя так же: столбец, который ничего
не может классифицировать, деградирует до строки только для чтения, а не
обрушивает страницу.
Последнее — это документированное состояние, за которым стоит тест, потому что состояние, которое ничем не проверяется, — это состояние, которое сломается в первый же раз, когда до него доберётся реальный проект.