Все объекты определяются в директории models, где разбиваются на небольшие, связанные по смыслу файлы. В дальнейшем для включения файла в проект его нужно включить в файл _init_.py - в этом случае все вызовы и обращения к моделям в других модулях будут корректны.
Все маршруты разбиваются на небольшие, связанные по смыслу модули - blueprints в терминологии Flask'а, каждый из которых в дальнейшем вручную добавляется в _init_.py в качестве соответствующего модуля.
В директории helpers хранятся файлы, содержащие функции, в том или ином плане полезные для соответствующих модулей. Здесь в большинстве своём - хаос.
Каждый объект из models имеет атрибут - класс Meta, предназначение которого - хранить информацию, общую для всех объектов. Эта информация содержит обычно следующие поля:
-
verbose_name- наименование класса объекта в человеко-читаемом виде. Пример: Задача; -
verbose_name_plural- наименование множества объектов данного класса в человеко-читаемом виде. Пример: Задачи; -
description- текст, отображаемый в панели администрирования, и более подробно описывающий данный класс; -
title_new- поле, используемое для объектов перечисления в панели администрирования при добавлении нового объекта; -
icon- иконка данного объекта. Записывается в виде строки, которая будет подставлена в<i class="">; -
icon_index- иконка объекта, отображаемая на странице перечисления множества объектов (обычно совпадает сicon); -
column_index- это список имён столбцов, которые будут отображаться в панели администрирования на странице списка текущих объектов; -
additional_template_variables- это словарь дополнительных переменных, передаваемых в шаблон Jinja2 при отображении страницы добавления/редактирования объекта в панели администрирования; -
project_permission_actions- словарь разрешений для данного объекта, которые можно проверить и который представляет из себя словарьКлюч : Значение, где Ключ - это "Наименование" разрешения, сохраняемое в базе знаний (неизменяемый объект), а Значение - это "отображаемое" имя разрешения - именно оно будет отображаться в списке разрешений в панели администрирования. -
bleached_field- список атрибутов данного объекта, для которых необходимо выполнять очистку вводимых пользователем значений от соответствующих тэгов, поскольку они не экранируются на уровне Jinja2 или других точках прослушивания; -
fulltitle_lazy_string- атрибут, используемый в истории для отображения объектов типа "Многие ко многим", сохранения именно этой строки в json в базе данных, и создания истории именно через этот атрибут.
В панели администрирования присутствуют объекты перечисления и объекты состояния.
Объекты перечисления - это объекты, общие для всех объектов, создаваемых/изменяемых пользователями (проекты, задачи, вики-страницы и т.д.), выбираемые через поле select, и предназначенные для группировки признаков у данных объектов.
Объекты состояния - это объекты, общие для всех объектов, создаваемых/изменяемых пользователями (проекты, задачи, вики-страницы и т.д.), для которых определено условие изменения с одного значения на другое.
Для того, чтобы объект попал к объектам перечисления, его необходимо обернуть в декоратор project_enumerated_object из модуля app.helpers.admin_helpers.
Пример:
from app.helpers.admin_helpers import project_enumerated_object
@project_enumerated_object
class MyObject(db.Model):
...Для того, чтобы для объекта можно было определять условия изменения значений, его необходимо обернуть в декоратор project_status_object из модуля app.helpers.admin_helpers, а также определить атрибут can_switch_to_state в качестве Set от списка объектов того же класса, что и он сам.
Пример:
from app import db
import sqlalchemy.orm as so
from app.helpers.admin_helpers import project_status_object
@project_status_object
class MyObject(db.Model):
can_switch_to_state: so.Mapped[Set["MyObject"]]
...Для каждого объекта существуют роли - это функции, которые относительно данного объекта и данного пользователя возвращают True или False - обладает ли пользователь данной ролью или нет.
Считается, что администратор обладает всеми возможными ролями - ему доступны все действия. Администратор - это пользователь, для которого флаг is_administrator установлен в True.
Помимо глобальных ролей существуют также и роли на проекте - это просто список объектов класса ProjectRole. Для каждого проекта существует ассоциации между пользователем и ролью на текущем проекте. Для каждого объекта и для каждого действия существуют различные разрешения на текущий объект (create, show и т. д.) - для каждого объекта свой список.
Проверка на возможность роли выполнять определённые действия выполняется через функцию project_role_can_make_action из модуля app.models - эта функция возвращает True или False в зависимости от того, обладает ли текущий пользователь хоть одной ролью, которая может выполнять указанное действие. На вход функция принимает 3 обязательных параметра и один опциональный:
-
current_user- текущий пользователь (можно получить из current_user модуля flask_login) -
obj- экземпляр текущего объекта, для которого производится проверка; -
action- проверочное действие; -
project- текущий проект, для которого выполняется проверка на наличие прав. Можно не передавать, если объект уже связан с проектом.
Пример:
from app.roles import project_role_can_make_action
from flask_login import current_user
import app.models as models
from app import db
@bp.route('/')
@login_required
def test_route()
project = db.get_or_404(models.Project, 1)
if project_role_can_make_action(current_user, models.MyModel(), 'create', project=project):
...Также для эндпоинтов, доступ к которым может быть только у администратора, существует специальный декоратор administrator_only из модуля app.roles. Этот декоратор должен вызываться после login_required, иначе для неаутентифицированного пользователя вместо страницы аутентификации мы увидим ошибку 403.
Всего у объекта db из модуля app существует 2 сессии: session и technical_session.
Важно помнить, что у обоих сессий отключён параметр autoflush - это сделано для того, чтобы создавать элементы истории перед коммитом базы данных. Причём коммит сессии technical_session вызывается сразу после коммита session. Эта сессия нужна для добавления каких-либо атрибутов после их добавления и коммита в основную сессию. Например, для создания объектов типа "Пользовательское уведомление" сразу после создания объекта (во время создания - ссылка на объект отсутствует, поскольку ID ещё не присвоен).
Все объекты, сохраняемые в базе данных, соответствуют декларативному стилю SQLAlchemy, и должны наследоваться от декларативной модели db.Model.
Каждая модель должна иметь атрибуты:
-
id - тип - integer, кроме того, имеет primary_key=True опцию;
-
title - объект обязателен не всегда, но почти всегда именно его выбирают в случае, если другая модель на него ссылается, или же происходит их поиск в базе данных через плагин
bootstrap-table. Возможно созданиеtitleчерез декораторhybrid_propertyв SQLAlchemy.
Дополнительно, если модель ссылается на другую модель (через связь Один-к-Одному или Один-ко-Многим), то имя первичного атрибута (column или mapped_column) на стороне "Многие" должен заканчиваться на _id, и совпадать с именем отношения (relationship). Это требуется, в том числе, для корректного отображения изменений в истории, поиска по БД, сохранения формы в объект, редактировании в панели администратора и т.д.
Также каждый атрибут, который так или иначе будет изменяться, и для которого будет сохраняться история, должен иметь атрибут info, задающийся словарём, с ключом label и значением - именем соответствующего атрибута.
Пример:
from app import db
import sqlalchemy as sa
import sqlalchemy.orm as so
from flask_babel import lazy_gettext as _l
class MyModel(db.Model):
id: so.Mapped[int] = so.mapped_column(primary_key=True, info={'label': _l('ID')})
title: so.Mapped[str] = so.mapped_column(sa.String(50), info={'label': _l("Title")})
relationship_id: so.Mapped[int] = so.mapped_column(sa.ForeignKey('relation.id', ondelete='CASCADE'), info={'label': _l("My relation")})
relationship: so.Mapped["Relation"] = so.mapped_column(lazy='select', info={'label': _l("My relation")}, backref=so.backref("my backref", info={'label': _l("backref to relationship")}))Объекты, общие для всех проектов, и на которые ссылаются другие объекты (такие как, например, трекер задачи, приоритет задачи, и т.д.) называются объектами перечисления. Список этих объектов редактируется в панели администратора во вкладке
"Объекты перечисления". Эти объекты обязаны иметь атрибуты id и title, что требуется для работы большинства плагинов, включая, например, историю событий.
Также такой объект должен иметь класс Meta в качестве объекта, в котором определяются следующие атрибуты:
verbose_name- собственно имя объекта перечисления (например, "Приоритет задачи");verbose_name_plural- имя множества объектов перечисления (например, "Приоритеты задач");title_new- заголовок добавления нового объекта (например, "Добавить новый приоритет задач");column_index- список столбцов, по которым будет производиться поиск при запросе через плагин bootstrap-table. Можно добавлять все колонки.
Класс Meta может также иметь атрибут description, который будет отображаться в панели администратора напротив объекта перечисления.
Для добавления класса в качестве объекта перечисления необходимо обернуть его декоратором @project_enumerated_object:
@project_enumerated_object
class MyEnumeratedObject(db.Model):
id: so.Mapped[ID]
title: so.Mapped[str] = so.mapped_column(sa.String(50), info={'label': _l("Title")})
class Meta:
verbose_name = _l("My Enumerated Object")
verbose_name_plural = _l("My Enumerated Objects")
title_new = _l("Add new My Enumerated Objects")
description = _l("My Awesome Enumerated Object")
column_index = ['id', 'title']Объекты состояния - это объекты, общие для всех проектов, для которых определены условия изменения состояния на другое значение. Хорошим примером объекта состояния являются статусы задачи: Задача со статусом "Новая" может перейти в статус "В работе", но не может перейти в статус "Разрешена". Задача со статусом "В работе" может перейти в статус "Разрешена", "Отменена", "Приостановлена", но не может перейти в статус "Новая".
Для каждого объекта состояния необходимо вручную определять таблицу связи Многие-ко-Многим, а также атрибуты, по которым будет проходить эта связь...