Как создавать пользовательские поля модели
Введение
В документации справочника по моделям объясняется, как использовать стандартные классы полей Django — CharField, DateField и т. д. Для многих задач этих классов будет достаточно. Однако иногда версия Django не отвечает вашим точным требованиям или вам может понадобиться поле, полностью отличающееся от тех, что входят в состав Django.
Встроенные типы полей Django охватывают не все возможные типы столбцов базы данных, а только распространённые, например VARCHAR и INTEGER. Для более редких типов столбцов, таких как географические многоугольники или даже пользовательские типы, например пользовательские типы PostgreSQL, можно определить собственные подклассы Django Field.
В качестве альтернативы у вас может быть сложный объект Python, который можно каким-либо образом сериализовать для хранения в стандартном типе столбца базы данных. В этом случае подкласс Field также поможет использовать ваш объект в моделях.
Объект для примера
Создание пользовательских полей требует внимания к деталям. Чтобы упростить изложение, на протяжении всего документа мы будем использовать один и тот же пример: класс-обёртку для объекта Python, представляющего раздачу карт в бридже. Не беспокойтесь: чтобы понять этот пример, не нужно уметь играть в бридж. Достаточно знать, что 52 карты поровну раздаются четырём игрокам, которых традиционно называют север, восток, юг и запад. Наш класс выглядит примерно так:
class Hand:
"""A hand of cards (bridge style)"""
def __init__(self, north, east, south, west):
# Input parameters are lists of cards ('Ah', '9s', etc.)
self.north = north
self.east = east
self.south = south
self.west = west
# ... (other possibly useful methods omitted) ...
Это обычный класс Python, не связанный с Django. Мы хотим иметь возможность делать в моделях примерно следующее (предполагается, что атрибут hand модели является экземпляром Hand):
example = MyModel.objects.get(pk=1) print(example.hand.north) new_hand = Hand(north, east, south, west) example.hand = new_hand example.save()
Мы присваиваем значения атрибуту hand модели и получаем их из него так же, как у любого другого класса Python. Нужно лишь указать Django, как сохранять и загружать такой объект.
Чтобы использовать класс Hand в моделях, нам не нужно изменять этот класс. Это удобно, поскольку позволяет легко добавлять поддержку моделей для существующих классов, исходный код которых нельзя изменить.
Примечание
Возможно, вам нужно использовать только пользовательские типы столбцов базы данных, а данные в моделях обрабатывать как стандартные типы Python, например строки или числа с плавающей точкой. Этот случай похож на наш пример с Hand; по ходу изложения мы будем отмечать все различия.
Теоретические основы
Хранение в базе данных
Начнём с полей модели. Если разобрать задачу на части, поле модели позволяет преобразовать обычный объект Python — строку, логическое значение, datetime или что-то более сложное, например Hand, — в формат, удобный для работы с базой данных, и обратно. (Такой формат также удобен для сериализации, но, как мы увидим далее, с ней проще работать, когда вы уже разобрались с взаимодействием с базой данных.)
Поля модели необходимо каким-либо образом преобразовать, чтобы они поместились в существующий тип столбца базы данных. В разных базах данных доступны разные наборы допустимых типов столбцов, но правило остаётся неизменным: работать можно только с этими типами. Всё, что вы хотите сохранить в базе данных, должно соответствовать одному из них.
Обычно вы либо создаёте поле Django, соответствующее определённому типу столбца базы данных, либо вам потребуется способ преобразовать данные, например, в строку.
В нашем примере с Hand данные карт можно преобразовать в строку из 104 символов, объединив все карты в заранее определённом порядке — например, сначала все карты севера, затем востока, юга и запада. Таким образом, объекты Hand можно сохранять в текстовые или символьные столбцы базы данных.
Что делает класс поля?
Все поля Django (и когда в этом документе мы говорим о полях, мы всегда имеем в виду поля моделей, а не поля форм) являются подклассами django.db.models.Field. Большая часть информации, которую Django хранит о поле, общая для всех полей: имя, справочный текст, уникальность и так далее. За хранение всей этой информации отвечает Field. Позже мы подробно рассмотрим возможности Field; пока достаточно сказать, что все поля наследуются от Field, а затем настраивают ключевые аспекты поведения класса.
Важно понимать, что класс поля Django — это не то, что хранится в атрибутах модели. Атрибуты модели содержат обычные объекты Python. Классы полей, определённые в модели, фактически хранятся в классе Meta при создании класса модели (точные подробности этого процесса здесь не важны). Это связано с тем, что классы полей не нужны для простого создания и изменения атрибутов. Вместо этого они предоставляют механизм преобразования значения атрибута в значение, хранящееся в базе данных или передаваемое сериализатору, и обратно.
Помните об этом, создавая собственные пользовательские поля. Создаваемый вами подкласс Django Field предоставляет механизм преобразования экземпляров Python в значения базы данных или сериализатора и обратно разными способами (например, сохранение значения и использование значения для поиска — это разные операции). Если это кажется сложным, не беспокойтесь: в примерах ниже всё станет понятнее. Просто помните, что для создания пользовательского поля вам часто понадобится два класса:
- Первый класс — это объект Python, с которым будут работать пользователи. Они будут присваивать его атрибуту модели, считывать его для отображения и выполнять подобные действия. В нашем примере это класс
Hand. - Второй класс — это подкласс
Field. Он умеет преобразовывать экземпляры первого класса из формы постоянного хранения в форму Python и обратно.
Создание подкласса поля
Планируя свой подкласс Field, сначала подумайте, на какой из существующих классов Field больше всего похоже ваше новое поле. Можно ли унаследовать существующее поле Django и сэкономить время? Если нет, следует унаследовать класс Field, от которого происходят все остальные.
Инициализация нового поля сводится к отделению аргументов, специфичных для вашего случая, от общих аргументов и передаче последних методу __init__() класса Field (или родительского класса).
В нашем примере мы назовём поле HandField. (Рекомендуется называть подкласс Field именем <Something>Field, чтобы его можно было легко распознать как подкласс Field.) Оно не ведёт себя как какое-либо существующее поле, поэтому мы унаследуем его непосредственно от Field:
from django.db import models
class HandField(models.Field):
description = "A hand of cards (bridge style)"
def __init__(self, *args, **kwargs):
kwargs["max_length"] = 104
super().__init__(*args, **kwargs)
Наше поле HandField принимает большинство стандартных параметров полей (см. список ниже), но мы задаём ему фиксированную длину, поскольку оно должно содержать только 52 значения карт и их масти — всего 104 символа.
Примечание
Многие модельные поля Django принимают параметры, которые никак не используют. Например, объекту django.db.models.DateField можно передать и editable, и auto_now, но он проигнорирует параметр editable (задание auto_now подразумевает editable=False). В этом случае ошибка не возникает.
Такое поведение упрощает классы полей, поскольку им не нужно проверять параметры, которые им не требуются. Они передают все параметры родительскому классу, а затем не используют их. Вы сами решаете, должны ли ваши поля строже проверять переданные параметры или придерживаться более гибкого поведения существующих полей.
Метод Field.__init__() принимает следующие параметры:
verbose_namenameprimary_keymax_lengthuniqueblanknulldb_index-
rel: используется для связанных полей (например,ForeignKey). Только для расширенного использования. defaulteditable-
serialize: если значение равноFalse, поле не будет сериализовано при передаче модели сериализаторам Django — сериализаторам. Значение по умолчанию —True. unique_for_dateunique_for_monthunique_for_yearchoiceshelp_textdb_column-
db_tablespace: используется только при создании индексов, если СУБД поддерживает табличные пространства. Обычно этот параметр можно игнорировать. -
auto_created:True, если поле было создано автоматически, как, например,OneToOneField, используемое при наследовании моделей. Только для расширенного использования.
Все параметры из списка выше, для которых не приведено пояснение, имеют то же значение, что и для обычных полей Django. Примеры и подробности см. в документации по полям.
Декомпозиция поля
Созданию метода __init__() соответствует создание метода deconstruct(). Он используется при миграциях моделей, чтобы сообщить Django, как преобразовать экземпляр нового поля в сериализованную форму — в частности, какие аргументы передать в __init__(), чтобы восстановить его.
Если вы не добавляли новых параметров к унаследованному полю, создавать новый метод deconstruct() не нужно. Однако если вы изменяете аргументы, передаваемые в __init__() (как мы делаем в HandField), потребуется дополнить возвращаемые значения.
deconstruct() возвращает кортеж из четырёх элементов: имя атрибута поля, полный путь импорта класса поля, позиционные аргументы (в виде списка) и именованные аргументы (в виде словаря). Обратите внимание: это отличается от метода deconstruct() для пользовательских классов, который возвращает кортеж из трёх элементов.
Создавая пользовательское поле, вы можете не беспокоиться о первых двух значениях: базовый класс Field содержит весь код для определения имени атрибута поля и пути импорта. Однако позиционные и именованные аргументы требуют внимания, поскольку именно их, вероятно, вы меняете.
Например, в нашем классе HandField мы всегда принудительно задаём max_length в __init__(). Метод deconstruct() базового класса Field заметит это и попытается вернуть значение в именованных аргументах; поэтому мы можем убрать его из именованных аргументов для большей наглядности:
from django.db import models
class HandField(models.Field):
def __init__(self, *args, **kwargs):
kwargs["max_length"] = 104
super().__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
del kwargs["max_length"]
return name, path, args, kwargs
Если вы добавите новый именованный аргумент, необходимо написать код в deconstruct(), который самостоятельно поместит его значение в kwargs. Также следует опускать значение из kwargs, если оно не требуется для восстановления состояния поля, например когда используется значение по умолчанию:
from django.db import models
class CommaSepField(models.Field):
"Implements comma-separated storage of lists"
def __init__(self, separator=",", *args, **kwargs):
self.separator = separator
super().__init__(*args, **kwargs)
def deconstruct(self):
name, path, args, kwargs = super().deconstruct()
# Only include kwarg if it's not the default
if self.separator != ",":
kwargs["separator"] = self.separator
return name, path, args, kwargs
Более сложные примеры выходят за рамки этого документа, но помните: для любой конфигурации экземпляра Field метод deconstruct() должен возвращать аргументы, которые можно передать в __init__, чтобы восстановить это состояние.
Будьте особенно внимательны, если задаёте новые значения по умолчанию для аргументов суперкласса Field: убедитесь, что они всегда включаются в результат, а не исчезают при совпадении со старым значением по умолчанию.
Кроме того, старайтесь не возвращать значения в виде позиционных аргументов; по возможности возвращайте их как именованные аргументы для лучшей совместимости с будущими версиями. Если имена меняются чаще, чем позиции в списке аргументов конструктора, можно предпочесть позиционные аргументы, но учитывайте, что сериализованную версию вашего поля будут восстанавливать довольно долго (возможно, годами) — в зависимости от срока жизни миграций.
Результаты декомпозиции можно увидеть в миграциях, использующих это поле. Проверить декомпозицию можно в модульных тестах, декомпозировав поле и восстановив его:
name, path, args, kwargs = my_field_instance.deconstruct() new_instance = MyField(*args, **kwargs) self.assertEqual(my_field_instance.some_attribute, new_instance.some_attribute)
Атрибуты поля, не влияющие на определение столбца базы данных
Можно переопределить Field.non_db_attrs, чтобы настроить атрибуты поля, не влияющие на определение столбца. Этот метод используется при миграциях моделей для обнаружения операций AlterField, не вносящих изменений.
Например:
class CommaSepField(models.Field):
@property
def non_db_attrs(self):
return super().non_db_attrs + ("separator",)
Изменение базового класса пользовательского поля
Изменить базовый класс пользовательского поля нельзя: Django не обнаружит такое изменение и не создаст для него миграцию. Например, если вы начали с такого класса:
class CustomCharField(models.CharField): ...
а затем решили использовать TextField, изменить подкласс следующим образом нельзя:
class CustomCharField(models.TextField): ...
Вместо этого необходимо создать новый класс пользовательского поля и обновить модели, чтобы они ссылались на него:
class CustomCharField(models.CharField): ... class CustomTextField(models.TextField): ...
Как сказано в разделе удаление полей, исходный класс CustomCharField необходимо сохранять, пока существуют миграции, в которых на него есть ссылки.
Документирование пользовательского поля
Как и всегда, следует документировать тип поля, чтобы пользователи понимали, что это такое. Помимо строки документации, полезной разработчикам, можно также позволить пользователям приложения администрирования видеть краткое описание типа поля с помощью приложения django.contrib.admindocs. Для этого укажите описательный текст в атрибуте класса description пользовательского поля. В приведённом выше примере приложение admindocs покажет для поля HandField описание «Рука карт (в стиле бриджа)».
В представлении django.contrib.admindocs описание поля форматируется с помощью field.__dict__, что позволяет включать в него аргументы поля. Например, описание для CharField выглядит так:
description = _("String (up to %(max_length)s)")
Полезные методы
Создав подкласс Field, вы можете переопределить несколько стандартных методов в зависимости от поведения поля. Методы ниже перечислены примерно в порядке убывания важности, поэтому начните с верхних.
Пользовательские типы баз данных
Предположим, вы создали пользовательский тип PostgreSQL с именем mytype. Можно унаследовать Field и реализовать метод db_type() следующим образом:
from django.db import models
class MytypeField(models.Field):
def db_type(self, connection):
return "mytype"
Создав MytypeField, вы сможете использовать его в любой модели, как и любой другой тип Field:
class Person(models.Model):
name = models.CharField(max_length=80)
something_else = MytypeField()
Если вы разрабатываете приложение, не зависящее от конкретной базы данных, необходимо учитывать различия в типах столбцов баз данных. Например, тип столбца даты и времени в PostgreSQL называется timestamp, а такой же столбец в MySQL — datetime. Это можно обработать в методе db_type(), проверив атрибут connection.vendor. Встроенные имена поставщиков в текущей версии: sqlite, postgresql, mysql и oracle.
Например:
class MyDateField(models.Field):
def db_type(self, connection):
if connection.vendor == "mysql":
return "datetime"
else:
return "timestamp"
Методы db_type() и rel_db_type() вызываются Django при создании SQL-операторов CREATE TABLE для вашего приложения, то есть при первоначальном создании таблиц. Эти методы также вызываются при создании предложения WHERE, включающего поле модели, — например, при получении данных с помощью методов QuerySet get(), filter() и exclude(), если поле модели передано в качестве аргумента.
Некоторые типы столбцов базы данных принимают параметры, например CHAR(25), где параметр 25 задаёт максимальную длину столбца. В таких случаях гибче указывать параметр в модели, а не задавать его жёстко в методе db_type(). Например, вряд ли имеет смысл использовать CharMaxlength25Field, как показано здесь:
# This is a silly example of hardcoded parameters.
class CharMaxlength25Field(models.Field):
def db_type(self, connection):
return "char(25)"
# In the model:
class MyModel(models.Model):
# ...
my_field = CharMaxlength25Field()
Лучше сделать так, чтобы параметр можно было указать во время выполнения, то есть при создании экземпляра класса. Для этого реализуйте Field.__init__(), например так:
# This is a much more flexible example.
class BetterCharField(models.Field):
def __init__(self, max_length, *args, **kwargs):
self.max_length = max_length
super().__init__(*args, **kwargs)
def db_type(self, connection):
return "char(%s)" % self.max_length
# In the model:
class MyModel(models.Model):
# ...
my_field = BetterCharField(25)
Наконец, если для вашего столбца требуется действительно сложная настройка SQL, верните None из метода db_type(). Это заставит код Django для создания SQL пропустить данное поле. Тогда вам придётся самостоятельно создать столбец в нужной таблице другим способом, но так можно указать Django не вмешиваться.
Метод rel_db_type() вызывается такими полями, как ForeignKey и OneToOneField, которые ссылаются на другое поле, чтобы определить тип данных столбца в базе данных. Например, если у вас есть UnsignedAutoField, внешние ключи, ссылающиеся на это поле, также должны использовать тот же тип данных:
# MySQL unsigned integer (range 0 to 4294967295).
class UnsignedAutoField(models.AutoField):
def db_type(self, connection):
return "integer UNSIGNED AUTO_INCREMENT"
def rel_db_type(self, connection):
return "integer UNSIGNED"
Преобразование значений в объекты Python
Если ваш пользовательский класс Field работает со структурами данных, более сложными, чем строки, даты, целые числа или числа с плавающей точкой, может потребоваться переопределить методы from_db_value() и to_python().
Если подкласс поля содержит метод from_db_value(), он будет вызываться во всех случаях загрузки данных из базы данных, включая агрегаты и вызовы values().
Метод to_python() вызывается при десериализации и во время метода clean(), используемого в формах.
Как правило, to_python() должен корректно обрабатывать любые из следующих аргументов:
- Экземпляр соответствующего типа (например,
Handв нашем примере). - Строка
-
None(если поле допускаетnull=True)
В нашем классе HandField данные хранятся в базе данных в поле типа VARCHAR, поэтому в from_db_value() необходимо уметь обрабатывать строки и None. В to_python() нужно также обрабатывать экземпляры Hand:
import re
from django.core.exceptions import ValidationError
from django.db import models
from django.utils.translation import gettext_lazy as _
def parse_hand(hand_string):
"""Takes a string of cards and splits into a full hand."""
p1 = re.compile(".{26}")
p2 = re.compile("..")
args = [p2.findall(x) for x in p1.findall(hand_string)]
if len(args) != 4:
raise ValidationError(_("Invalid input for a Hand instance"))
return Hand(*args)
class HandField(models.Field):
# ...
def from_db_value(self, value, expression, connection):
if value is None:
return value
return parse_hand(value)
def to_python(self, value):
if isinstance(value, Hand):
return value
if value is None:
return value
return parse_hand(value)
Обратите внимание, что эти методы всегда возвращают экземпляр Hand. Именно такой тип объекта Python мы хотим хранить в атрибуте модели.
В методе to_python() при любой ошибке преобразования значения следует возбуждать исключение ValidationError.
Преобразование объектов Python в значения запросов
Поскольку для работы с базой данных требуется преобразование в обоих направлениях, при переопределении from_db_value() необходимо также переопределить get_prep_value(), чтобы преобразовывать объекты Python обратно в значения запросов.
Например:
class HandField(models.Field):
# ...
def get_prep_value(self, value):
return "".join(
["".join(l) for l in (value.north, value.east, value.south, value.west)]
)
Предупреждение
Если пользовательское поле использует в MySQL типы CHAR, VARCHAR или TEXT, необходимо убедиться, что get_prep_value() всегда возвращает строку. При выполнении запроса MySQL использует гибкое и непредсказуемое сопоставление для этих типов, если переданное значение является целым числом, из-за чего результаты запросов могут содержать неожиданные объекты. Этой проблемы не возникнет, если get_prep_value() всегда возвращает строку.
Преобразование значений запросов в значения базы данных
Некоторые типы данных (например, даты) должны иметь определённый формат, прежде чем их можно будет использовать в базе данных. Такие преобразования следует выполнять в методе get_db_prep_value(). В параметре connection передаётся конкретное подключение, которое будет использоваться для запроса. Это позволяет применять специфичную для СУБД логику преобразования, если она необходима.
Например, Django использует следующий метод для поля BinaryField:
def get_db_prep_value(self, value, connection, prepared=False):
value = super().get_db_prep_value(value, connection, prepared)
if value is not None:
return connection.Database.Binary(value)
return value
Если при сохранении вашему пользовательскому полю требуется особое преобразование, отличающееся от преобразования обычных параметров запроса, можно переопределить get_db_prep_save().
Предварительная обработка значений перед сохранением
Чтобы предварительно обработать значение непосредственно перед сохранением, можно использовать pre_save(). Например, поле Django DateTimeField использует этот метод, чтобы правильно задать атрибут при использовании auto_now или auto_now_add.
Если вы переопределяете этот метод, в конце он должен возвращать значение атрибута. Если значение изменилось, следует также обновить атрибут модели, чтобы код, содержащий ссылки на модель, всегда видел актуальное значение.
Указание поля формы для поля модели
Чтобы настроить поле формы, используемое в ModelForm, можно переопределить formfield().
Класс поля формы можно указать с помощью аргументов form_class и choices_form_class; второй используется, если для поля заданы варианты выбора, а первый — в противном случае. Если эти аргументы не заданы, будут использоваться CharField или TypedChoiceField.
Весь словарь kwargs передаётся непосредственно методу __init__() поля формы. Обычно достаточно задать подходящее значение по умолчанию для аргумента form_class (и, возможно, choices_form_class), а дальнейшую обработку передать родительскому классу. Возможно, для этого потребуется написать собственное поле формы (и даже виджет формы). Подробнее см. в документации по формам.
Чтобы исключить поле из ModelForm, можно переопределить метод formfield(), возвращающий None.
Продолжая наш пример, метод formfield() можно написать так:
class HandField(models.Field):
# ...
def formfield(self, **kwargs):
# Exclude the field from the ModelForm when some condition is met.
some_condition = kwargs.get("some_condition", False)
if some_condition:
return None
# Set up some defaults while letting the caller override them.
defaults = {"form_class": MyFormField}
defaults.update(kwargs)
return super().formfield(**defaults)
Предполагается, что мы импортировали класс поля MyFormField (у которого есть собственный виджет по умолчанию). Подробности создания пользовательских полей форм в этом документе не рассматриваются.
Имитация встроенных типов полей
Если вы создали метод db_type(), беспокоиться о get_internal_type() не нужно — он будет использоваться нечасто. Однако иногда тип хранилища данных вашей базы похож на тип другого поля, и тогда можно воспользоваться логикой этого поля для создания нужного столбца.
Например:
class HandField(models.Field):
# ...
def get_internal_type(self):
return "CharField"
Независимо от используемой СУБД, это обеспечит создание правильного типа столбца для хранения строки при выполнении команды migrate и других SQL-команд.
Если get_internal_type() возвращает строку, неизвестную Django для используемой СУБД (то есть отсутствующую в django.db.backends.<db_name>.base.DatabaseWrapper.data_types), сериализатор всё равно использует эту строку, но метод db_type() по умолчанию вернёт None. Причины, по которым это может быть полезно, описаны в документации к db_type(). Если вы собираетесь использовать результат сериализации где-либо за пределами Django, указать описательную строку в качестве типа поля для сериализатора будет полезно.
Преобразование данных поля для сериализации
Чтобы настроить сериализацию значений сериализатором, можно переопределить value_to_string(). Лучше всего получить значение поля перед сериализацией с помощью value_from_object(). Например, поскольку HandField и так использует для хранения данных строки, можно повторно использовать существующий код преобразования:
class HandField(models.Field):
# ...
def value_to_string(self, obj):
value = self.value_from_object(obj)
return self.get_prep_value(value)
Общие рекомендации
Создание пользовательского поля может быть непростой задачей, особенно если требуется сложное преобразование между типами Python и форматами базы данных и сериализации. Вот несколько советов, которые помогут упростить процесс:
- Изучите существующие поля Django (в django/db/models/fields/__init__.py) для вдохновения. Попробуйте найти похожее на нужное вам поле и немного расширить его, вместо того чтобы создавать совершенно новое поле с нуля.
- Добавьте метод
__str__()в класс, который вы оборачиваете в поле. Во многих случаях код поля по умолчанию вызывает для значенияstr(). (В наших примерахvalueбыл бы экземпляромHand, а неHandField.) Поэтому, если ваш метод__str__()автоматически преобразует объект Python в строковое представление, это может избавить вас от множества лишней работы.
Создание подкласса FileField
Помимо описанных выше методов, у полей, работающих с файлами, есть несколько особых требований, которые необходимо учитывать. Большая часть механизмов, предоставляемых FileField, например управление сохранением и извлечением данных из базы данных, может остаться без изменений. Подклассам останется решить задачу поддержки определённого типа файла.
Django предоставляет класс File, который служит прокси для содержимого файла и операций с ним. От него можно создать подкласс, чтобы настроить доступ к файлу и список доступных методов. Он находится в django.db.models.fields.files, а его поведение по умолчанию описано в документации по файлам.
После создания подкласса File необходимо указать новому подклассу FileField, что он должен его использовать. Для этого присвойте новый подкласс File специальному атрибуту attr_class подкласса FileField.
Несколько советов
Помимо описанных выше деталей, есть несколько рекомендаций, которые могут значительно повысить эффективность и читаемость кода поля.
- Исходный код собственного класса
ImageFieldDjango (в файле django/db/models/fields/files.py) — отличный пример того, как создать подклассFileFieldдля поддержки определённого типа файла: в нём применены все описанные выше методы. - По возможности кэшируйте атрибуты файла. Поскольку файлы могут храниться в удалённых системах хранения, их извлечение может требовать дополнительного времени и даже денег, хотя это не всегда необходимо. Получив файл, чтобы узнать какие-либо данные о его содержимом, кэшируйте как можно больше этих данных, чтобы сократить число повторных извлечений файла при последующих запросах этой информации.
© Django Software Foundation and individual contributors
Licensed under the BSD License.
https://docs.djangoproject.com/en/6.0/howto/custom-model-fields/