• 3.2
  • 5.0
  • 6.1
  • Версия документации: 3.1

Поля конкретной модели PostgreSQL

Все эти поля доступны из модуля django.contrib.postgres.fields.

Индексирование этих полей

Index и Field.db_index оба создают индекс B-дерева, что не особенно полезно при запросе сложных типов данных. Такие индексы, как GinIndex и GistIndex, подходят лучше, хотя выбор индекса зависит от используемых вами запросов. Как правило, GiST может быть хорошим выбором для полей range и HStoreField, а GIN может быть полезен для ArrayField.

МассивФилд

class ArrayField(base_field, size=None, **options)

Поле для хранения списков данных. Можно использовать большинство типов полей, и вы передаете другой экземпляр поля как base_field. Вы также можете указать size. ArrayField может быть вложенным для хранения многомерных массивов.

Если вы задаете поле default, убедитесь, что это вызываемый объект, такой как list (для пустого значения по умолчанию) или вызываемый объект, который возвращает список (например, функция). Неправильное использование default=[] создает изменяемое значение по умолчанию, которое используется всеми экземплярами ArrayField.

base_field

Это обязательный аргумент.

Specifies the underlying data type and behavior for the array. It should be an instance of a subclass of Field. For example, it could be an IntegerField or a CharField. Most field types are permitted, with the exception of those handling relational data (ForeignKey, OneToOneField and ManyToManyField).

Поля массива можно вкладывать друг в друга — вы можете указать экземпляр ArrayField в качестве base_field. Например:

from django.contrib.postgres.fields import ArrayField
from django.db import models

class ChessBoard(models.Model):
    board = ArrayField(
        ArrayField(
            models.CharField(max_length=10, blank=True),
            size=8,
        ),
        size=8,
    )

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

size

Это необязательный аргумент.

Если оно передано, массив будет иметь указанный максимальный размер. Это будет передано в базу данных, хотя PostgreSQL в настоящее время не применяет это ограничение.

Примечание

При вложении ArrayField, независимо от того, используете ли вы параметр size или нет, PostgreSQL требует, чтобы массивы были прямоугольными:

from django.contrib.postgres.fields import ArrayField
from django.db import models

class Board(models.Model):
    pieces = ArrayField(ArrayField(models.IntegerField()))

# Valid
Board(pieces=[
    [2, 3],
    [2, 1],
])

# Not valid
Board(pieces=[
    [2, 3],
    [2],
])

Если требуются нестандартные формы, то базовое поле должно быть обнуляемым, а значения дополняться «Нет».

Запрос ArrayField

Существует ряд пользовательских поисков и преобразований для ArrayField. Мы будем использовать следующий пример модели:

from django.contrib.postgres.fields import ArrayField
from django.db import models

class Post(models.Model):
    name = models.CharField(max_length=200)
    tags = ArrayField(models.CharField(max_length=200), blank=True)

    def __str__(self):
        return self.name

contains

The contains lookup is overridden on ArrayField. The returned objects will be those where the values passed are a subset of the data. It uses the SQL operator @>. For example:

>>> Post.objects.create(name='First post', tags=['thoughts', 'django'])
>>> Post.objects.create(name='Second post', tags=['thoughts'])
>>> Post.objects.create(name='Third post', tags=['tutorial', 'django'])

>>> Post.objects.filter(tags__contains=['thoughts'])
<QuerySet [<Post: First post>, <Post: Second post>]>

>>> Post.objects.filter(tags__contains=['django'])
<QuerySet [<Post: First post>, <Post: Third post>]>

>>> Post.objects.filter(tags__contains=['django', 'thoughts'])
<QuerySet [<Post: First post>]>

contained_by

This is the inverse of the contains lookup - the objects returned will be those where the data is a subset of the values passed. It uses the SQL operator <@. For example:

>>> Post.objects.create(name='First post', tags=['thoughts', 'django'])
>>> Post.objects.create(name='Second post', tags=['thoughts'])
>>> Post.objects.create(name='Third post', tags=['tutorial', 'django'])

>>> Post.objects.filter(tags__contained_by=['thoughts', 'django'])
<QuerySet [<Post: First post>, <Post: Second post>]>

>>> Post.objects.filter(tags__contained_by=['thoughts', 'django', 'tutorial'])
<QuerySet [<Post: First post>, <Post: Second post>, <Post: Third post>]>

перекрытие

Returns objects where the data shares any results with the values passed. Uses the SQL operator &&. For example:

>>> Post.objects.create(name='First post', tags=['thoughts', 'django'])
>>> Post.objects.create(name='Second post', tags=['thoughts'])
>>> Post.objects.create(name='Third post', tags=['tutorial', 'django'])

>>> Post.objects.filter(tags__overlap=['thoughts'])
<QuerySet [<Post: First post>, <Post: Second post>]>

>>> Post.objects.filter(tags__overlap=['thoughts', 'tutorial'])
<QuerySet [<Post: First post>, <Post: Second post>, <Post: Third post>]>

лен

Returns the length of the array. The lookups available afterwards are those available for IntegerField. For example:

>>> Post.objects.create(name='First post', tags=['thoughts', 'django'])
>>> Post.objects.create(name='Second post', tags=['thoughts'])

>>> Post.objects.filter(tags__len=1)
<QuerySet [<Post: Second post>]>

Индексные преобразования

Index transforms index into the array. Any non-negative integer can be used. There are no errors if it exceeds the size of the array. The lookups available after the transform are those from the base_field. For example:

>>> Post.objects.create(name='First post', tags=['thoughts', 'django'])
>>> Post.objects.create(name='Second post', tags=['thoughts'])

>>> Post.objects.filter(tags__0='thoughts')
<QuerySet [<Post: First post>, <Post: Second post>]>

>>> Post.objects.filter(tags__1__iexact='Django')
<QuerySet [<Post: First post>]>

>>> Post.objects.filter(tags__276='javascript')
<QuerySet []>

Примечание

PostgreSQL использует индексацию с отсчетом от 1 для полей массива при написании необработанного SQL. Однако эти индексы и индексы, используемые в slices, используют индексацию, отсчитываемую от 0, чтобы быть совместимыми с Python.

Срезные преобразования

Slice transforms take a slice of the array. Any two non-negative integers can be used, separated by a single underscore. The lookups available after the transform do not change. For example:

>>> Post.objects.create(name='First post', tags=['thoughts', 'django'])
>>> Post.objects.create(name='Second post', tags=['thoughts'])
>>> Post.objects.create(name='Third post', tags=['django', 'python', 'thoughts'])

>>> Post.objects.filter(tags__0_1=['thoughts'])
<QuerySet [<Post: First post>, <Post: Second post>]>

>>> Post.objects.filter(tags__0_2__contains=['thoughts'])
<QuerySet [<Post: First post>, <Post: Second post>]>

Примечание

PostgreSQL использует индексацию с отсчетом от 1 для полей массива при написании необработанного SQL. Однако эти фрагменты и те, которые используются в indexes, используют индексацию, отсчитываемую от 0, чтобы быть совместимыми с Python.

Многомерные массивы с индексами и срезами

PostgreSQL имеет довольно эзотерическое поведение при использовании индексов и срезов в многомерных массивах. Всегда можно использовать индексы для доступа к окончательным базовым данным, но большинство других фрагментов ведут себя странно на уровне базы данных и не могут поддерживаться Django логически и последовательно.

CIText fields

class CIText(**options)

A mixin to create case-insensitive text fields backed by the citext type. Read about the performance considerations prior to using it.

To use citext, use the CITextExtension operation to setup the citext extension in PostgreSQL before the first CreateModel migration operation.

If you’re using an ArrayField of CIText fields, you must add 'django.contrib.postgres' in your INSTALLED_APPS, otherwise field values will appear as strings like '{thoughts,django}'.

Several fields that use the mixin are provided:

class CICharField(**options)
class CIEmailField(**options)
class CITextField(**options)

These fields subclass CharField, EmailField, and TextField, respectively.

max_length won’t be enforced in the database since citext behaves similar to PostgreSQL’s text type.

HStoreField

class HStoreField(**options)

Поле для хранения пар ключ-значение. Используемый тип данных Python — «dict». Ключи должны быть строками, а значения могут быть либо строками, либо значениями NULL («None» в Python).

Чтобы использовать это поле, вам необходимо:

  1. Добавьте 'django.contrib.postgres' в настройки:INSTALLED_APPS.

  2. Setup the hstore extension in PostgreSQL.

Вы увидите ошибку типа «невозможно адаптировать тип «dict», если пропустите первый шаг, или «тип «hstore» не существует», если пропустите второй.

Примечание

В некоторых случаях может быть полезно потребовать или ограничить ключи, действительные для данного поля. Это можно сделать с помощью KeysValidator.

Запрос HStoreField

Помимо возможности запроса по ключу, для HStoreField доступен ряд пользовательских поисков.

Мы будем использовать следующий пример модели:

from django.contrib.postgres.fields import HStoreField
from django.db import models

class Dog(models.Model):
    name = models.CharField(max_length=200)
    data = HStoreField()

    def __str__(self):
        return self.name

Ключевые запросы

To query based on a given key, you can use that key as the lookup name:

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})
>>> Dog.objects.create(name='Meg', data={'breed': 'collie'})

>>> Dog.objects.filter(data__breed='collie')
<QuerySet [<Dog: Meg>]>

You can chain other lookups after key lookups:

>>> Dog.objects.filter(data__breed__contains='l')
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>

Если ключ, который вы хотите запросить, конфликтует с именем другого поиска, вам нужно вместо этого использовать поиск hstorefield.contains.

Предупреждение

Поскольку любая строка может быть ключом в значении hstore, любой поиск, кроме перечисленных ниже, будет интерпретироваться как поиск ключа. Никаких ошибок не возникает. Будьте особенно осторожны с ошибками при вводе и всегда проверяйте, работают ли ваши запросы так, как вы задумали.

contains

The contains lookup is overridden on HStoreField. The returned objects are those where the given dict of key-value pairs are all contained in the field. It uses the SQL operator @>. For example:

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador', 'owner': 'Bob'})
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})
>>> Dog.objects.create(name='Fred', data={})

>>> Dog.objects.filter(data__contains={'owner': 'Bob'})
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>

>>> Dog.objects.filter(data__contains={'breed': 'collie'})
<QuerySet [<Dog: Meg>]>

contained_by

This is the inverse of the contains lookup - the objects returned will be those where the key-value pairs on the object are a subset of those in the value passed. It uses the SQL operator <@. For example:

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador', 'owner': 'Bob'})
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})
>>> Dog.objects.create(name='Fred', data={})

>>> Dog.objects.filter(data__contained_by={'breed': 'collie', 'owner': 'Bob'})
<QuerySet [<Dog: Meg>, <Dog: Fred>]>

>>> Dog.objects.filter(data__contained_by={'breed': 'collie'})
<QuerySet [<Dog: Fred>]>

has_key

Returns objects where the given key is in the data. Uses the SQL operator ?. For example:

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})

>>> Dog.objects.filter(data__has_key='owner')
<QuerySet [<Dog: Meg>]>

has_any_keys

Returns objects where any of the given keys are in the data. Uses the SQL operator ?|. For example:

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})
>>> Dog.objects.create(name='Meg', data={'owner': 'Bob'})
>>> Dog.objects.create(name='Fred', data={})

>>> Dog.objects.filter(data__has_any_keys=['owner', 'breed'])
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>

has_keys

Returns objects where all of the given keys are in the data. Uses the SQL operator ?&. For example:

>>> Dog.objects.create(name='Rufus', data={})
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})

>>> Dog.objects.filter(data__has_keys=['breed', 'owner'])
<QuerySet [<Dog: Meg>]>

ключи

Returns objects where the array of keys is the given value. Note that the order is not guaranteed to be reliable, so this transform is mainly useful for using in conjunction with lookups on ArrayField. Uses the SQL function akeys(). For example:

>>> Dog.objects.create(name='Rufus', data={'toy': 'bone'})
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})

>>> Dog.objects.filter(data__keys__overlap=['breed', 'toy'])
<QuerySet [<Dog: Rufus>, <Dog: Meg>]>

ценности

Returns objects where the array of values is the given value. Note that the order is not guaranteed to be reliable, so this transform is mainly useful for using in conjunction with lookups on ArrayField. Uses the SQL function avals(). For example:

>>> Dog.objects.create(name='Rufus', data={'breed': 'labrador'})
>>> Dog.objects.create(name='Meg', data={'breed': 'collie', 'owner': 'Bob'})

>>> Dog.objects.filter(data__values__contains=['collie'])
<QuerySet [<Dog: Meg>]>

JSONField

class JSONField(encoder=None, **options)

Поле для хранения данных в формате JSON. В Python данные представлены в собственном формате Python: словари, списки, строки, числа, логические значения и «Нет».

encoder

An optional JSON-encoding class to serialize data types not supported by the standard JSON serializer (datetime, uuid, etc.). For example, you can use the DjangoJSONEncoder class or any other json.JSONEncoder subclass.

When the value is retrieved from the database, it will be in the format chosen by the custom encoder (most often a string), so you’ll need to take extra steps to convert the value back to the initial data type (Model.from_db() and Field.from_db_value() are two possible hooks for that purpose). Your deserialization may need to account for the fact that you can’t be certain of the input type. For example, you run the risk of returning a datetime that was actually a string that just happened to be in the same format chosen for datetimes.

If you give the field a default, ensure it’s a callable such as dict (for an empty default) or a callable that returns a dict (such as a function). Incorrectly using default={} creates a mutable default that is shared between all instances of JSONField.

Примечание

PostgreSQL имеет два собственных типа данных на основе JSON: json и jsonb. Основное различие между ними заключается в том, как они хранятся и как их можно запрашивать. Поле json в PostgreSQL хранится как исходное строковое представление JSON и должно декодироваться на лету при запросе на основе ключей. Поле jsonb хранится на основе фактической структуры JSON, что позволяет индексировать. Компромиссом являются небольшие дополнительные затраты на запись в поле jsonb. JSONField использует jsonb.

Не рекомендуется, начиная с версии 3.1: Use django.db.models.JSONField instead.

Querying JSONField

See Запрос JSONField for details.

Поля диапазона

Существует пять типов полей диапазона, соответствующих встроенным типам диапазонов в PostgreSQL. Эти поля используются для хранения диапазона значений; например, временные метки начала и окончания события или диапазон возрастов, для которого подходит занятие.

All of the range fields translate to psycopg2 Range objects in Python, but also accept tuples as input if no bounds information is necessary. The default is lower bound included, upper bound excluded, that is [) (see the PostgreSQL documentation for details about different bounds).

IntegerRangeField

class IntegerRangeField(**options)

Stores a range of integers. Based on an IntegerField. Represented by an int4range in the database and a NumericRange in Python.

Независимо от границ, указанных при сохранении данных, PostgreSQL всегда возвращает диапазон в канонической форме, который включает нижнюю границу и исключает верхнюю границу, то есть [).

BigIntegerRangeField

class BigIntegerRangeField(**options)

Stores a range of large integers. Based on a BigIntegerField. Represented by an int8range in the database and a NumericRange in Python.

Независимо от границ, указанных при сохранении данных, PostgreSQL всегда возвращает диапазон в канонической форме, который включает нижнюю границу и исключает верхнюю границу, то есть [).

Десятичноеполедиапазона

class DecimalRangeField(**options)

Stores a range of floating point values. Based on a DecimalField. Represented by a numrange in the database and a NumericRange in Python.

DateTimeRangeField

class DateTimeRangeField(**options)

Stores a range of timestamps. Based on a DateTimeField. Represented by a tstzrange in the database and a DateTimeTZRange in Python.

ДатаРангеФилд

class DateRangeField(**options)

Stores a range of dates. Based on a DateField. Represented by a daterange in the database and a DateRange in Python.

Независимо от границ, указанных при сохранении данных, PostgreSQL всегда возвращает диапазон в канонической форме, который включает нижнюю границу и исключает верхнюю границу, то есть [).

Запрос полей диапазона

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

from django.contrib.postgres.fields import IntegerRangeField
from django.db import models

class Event(models.Model):
    name = models.CharField(max_length=200)
    ages = IntegerRangeField()
    start = models.DateTimeField()

    def __str__(self):
        return self.name

We will also use the following example objects:

>>> import datetime
>>> from django.utils import timezone
>>> now = timezone.now()
>>> Event.objects.create(name='Soft play', ages=(0, 10), start=now)
>>> Event.objects.create(name='Pub trip', ages=(21, None), start=now - datetime.timedelta(days=1))

и NumericRange:

>>> from psycopg2.extras import NumericRange

Функции сдерживания

Как и в случае с другими полями PostgreSQL, здесь есть три стандартных оператора включения: contains, contained_by и overlap, использующие операторы SQL @>, <@ и && соответственно.

contains
>>> Event.objects.filter(ages__contains=NumericRange(4, 5))
<QuerySet [<Event: Soft play>]>
contained_by
>>> Event.objects.filter(ages__contained_by=NumericRange(0, 15))
<QuerySet [<Event: Soft play>]>

The contained_by lookup is also available on the non-range field types: SmallAutoField, AutoField, BigAutoField, SmallIntegerField, IntegerField, BigIntegerField, DecimalField, FloatField, DateField, and DateTimeField. For example:

>>> from psycopg2.extras import DateTimeTZRange
>>> Event.objects.filter(
...     start__contained_by=DateTimeTZRange(
...         timezone.now() - datetime.timedelta(hours=1),
...         timezone.now() + datetime.timedelta(hours=1),
...     ),
... )
<QuerySet [<Event: Soft play>]>
Changed in Django 3.1:

Support for SmallAutoField, AutoField, BigAutoField, SmallIntegerField, and DecimalField was added.

перекрытие
>>> Event.objects.filter(ages__overlap=NumericRange(8, 12))
<QuerySet [<Event: Soft play>]>

Функции сравнения

Поля диапазона поддерживают стандартные поиски: lt, gt, lte и gte. Это не особенно полезно — сначала сравниваются нижние границы, а затем только при необходимости верхние границы. Эта стратегия также используется для заказа по полю диапазона. Лучше использовать конкретные операторы сравнения диапазонов.

полностью_lt

Возвращаемые диапазоны строго меньше переданного диапазона. Другими словами, все точки в возвращаемом диапазоне меньше всех точек в переданном диапазоне.

>>> Event.objects.filter(ages__fully_lt=NumericRange(11, 15))
<QuerySet [<Event: Soft play>]>
полностью_gt

Возвращаемые диапазоны строго превышают переданный диапазон. Другими словами, все точки в возвращаемом диапазоне больше, чем все точки в переданном диапазоне.

>>> Event.objects.filter(ages__fully_gt=NumericRange(11, 15))
<QuerySet [<Event: Pub trip>]>
not_lt

Возвращаемые диапазоны не содержат точек меньше переданного диапазона, то есть нижняя граница возвращаемого диапазона является как минимум нижней границей переданного диапазона.

>>> Event.objects.filter(ages__not_lt=NumericRange(0, 15))
<QuerySet [<Event: Soft play>, <Event: Pub trip>]>
not_gt

Возвращаемые диапазоны не содержат точек, превышающих переданный диапазон, то есть верхняя граница возвращаемого диапазона не превышает верхней границы переданного диапазона.

>>> Event.objects.filter(ages__not_gt=NumericRange(3, 10))
<QuerySet [<Event: Soft play>]>
adjacent_to

Возвращенные диапазоны имеют общую границу с переданным диапазоном.

>>> Event.objects.filter(ages__adjacent_to=NumericRange(10, 21))
<QuerySet [<Event: Soft play>, <Event: Pub trip>]>

Запрос с использованием границ

There are three transforms available for use in queries. You can extract the lower or upper bound, or query based on emptiness.

startswith

Возвращенные объекты имеют заданную нижнюю границу. Может быть привязан к допустимым поискам для базового поля.

>>> Event.objects.filter(ages__startswith=21)
<QuerySet [<Event: Pub trip>]>
endswith

Возвращенные объекты имеют заданную верхнюю границу. Может быть привязан к допустимым поискам для базового поля.

>>> Event.objects.filter(ages__endswith=10)
<QuerySet [<Event: Soft play>]>
пустой

Возвращаемые объекты представляют собой пустые диапазоны. Может быть привязан к допустимому поиску для BooleanField.

>>> Event.objects.filter(ages__isempty=True)
<QuerySet []>
lower_inc
New in Django 3.1.

Возвращает объекты, имеющие включающие или исключающие нижние границы, в зависимости от переданного логического значения. Может быть привязан к допустимому поиску для BooleanField.

>>> Event.objects.filter(ages__lower_inc=True)
<QuerySet [<Event: Soft play>, <Event: Pub trip>]>
lower_inf
New in Django 3.1.

Возвращает объекты, имеющие неограниченную (бесконечную) или ограниченную нижнюю границу, в зависимости от переданного логического значения. Может быть привязан к допустимому поиску для BooleanField.

>>> Event.objects.filter(ages__lower_inf=True)
<QuerySet []>
upper_inc
New in Django 3.1.

Возвращает объекты, которые имеют инклюзивные или исключительные верхние границы, в зависимости от переданного логического значения. Может быть привязан к допустимому поиску для BooleanField.

>>> Event.objects.filter(ages__upper_inc=True)
<QuerySet []>
upper_inf
New in Django 3.1.

Возвращает объекты, имеющие неограниченную (бесконечную) или ограниченную верхнюю границу, в зависимости от переданного логического значения. Может быть привязан к допустимому поиску для BooleanField.

>>> Event.objects.filter(ages__upper_inf=True)
<QuerySet [<Event: Pub trip>]>

Определение собственных типов диапазонов

PostgreSQL allows the definition of custom range types. Django’s model and form field implementations use base classes below, and psycopg2 provides a register_range() to allow use of custom range types.

class RangeField(**options)

Базовый класс для полей модельного ряда.

base_field

Используемый класс поля модели.

range_type

The psycopg2 range type to use.

form_field

Используемый класс поля формы. Должен быть подклассом django.contrib.postgres.forms.BaseRangeField.

class django.contrib.postgres.forms.BaseRangeField

Базовый класс для полей диапазона формы.

base_field

Поле формы, которое нужно использовать.

range_type

The psycopg2 range type to use.

Операторы диапазона

New in Django 3.0.
class RangeOperators

PostgreSQL предоставляет набор операторов SQL, которые можно использовать вместе с типами данных диапазона (полную информацию об операторах диапазона см. в документации PostgreSQL <https://www.postgresql.org/docs/current/functions-range.html#RANGE-OPERATORS-TABLE>`_). Этот класс задуман как удобный способ избежать опечаток. Имена операторов перекрываются с именами соответствующих поисков.

class RangeOperators:
    EQUAL = '='
    NOT_EQUAL = '<>'
    CONTAINS = '@>'
    CONTAINED_BY = '<@'
    OVERLAPS = '&&'
    FULLY_LT = '<<'
    FULLY_GT = '>>'
    NOT_LT = '&>'
    NOT_GT = '&<'
    ADJACENT_TO = '-|-'

Выражения RangeBoundary()

New in Django 3.0.
class RangeBoundary(inclusive_lower=True, inclusive_upper=False)
inclusive_lower

Если True (по умолчанию), нижняя граница является включающей '[', в противном случае она является эксклюзивной '('.

inclusive_upper

Если False (по умолчанию), верхняя граница является исключающей ')', в противном случае она включает ']'.

Выражение RangeBoundary() представляет границы диапазона. Его можно использовать с пользовательскими функциями диапазона, которые ожидают границы, например, для определения ExclusionConstraint. Подробную информацию см. в документации PostgreSQL <https://www.postgresql.org/docs/current/rangetypes.html#RANGETYPES-INCLUSIVITY>`_.

Back to Top