Главная страница - nemopss/mpt-kpi GitHub Wiki
Глава 1.1 docker-compose.dev.yml
Глава 1.4.8 requirements.dev.in
Глава 1.4.9 requirements.dev.txt
Файл docker-compose.dev.yml используется для конфигурации приложения в среде разработки с помощью Docker Compose. Он определяет три основных сервиса: базу данных (db), фронтенд (frontend) и бэкенд (backend):
services:
db:
image: mysql:9.0.1
container_name: ${SERVICE}_dev_db
restart: always
environment:
MYSQL_USER: $DATABASE_USER
MYSQL_PASSWORD: $DATABASE_PASSWORD
MYSQL_DATABASE: $DATABASE_NAME
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
volumes:
- mpt-kpi-database:/var/lib/mysql
ports:
- ${DATABASE_PORT}:${DATABASE_PORT}
frontend:
container_name: ${SERVICE}_dev_frontend
env_file: .env
build:
context: ./frontend/
dockerfile: dev.Dockerfile
volumes:
- ./frontend:/app
ports:
- ${FRONTEND_PORT}:${FRONTEND_PORT}
backend:
container_name: ${SERVICE}_dev_backend
env_file: .env
build:
context: ./backend/
dockerfile: dev.Dockerfile
environment:
- DEBUG=1
volumes:
- ./backend:/app
ports:
- ${BACKEND_PORT}:${BACKEND_PORT}
depends_on:
- db
volumes:
mpt-kpi-database:
external: true
Пояснение каждого сервиса:
Сервис db
db:
image: mysql:9.0.1
container_name: ${SERVICE}_dev_db
restart: always
environment:
MYSQL_USER: $DATABASE_USER
MYSQL_PASSWORD: $DATABASE_PASSWORD
MYSQL_DATABASE: $DATABASE_NAME
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
volumes:
- mpt-kpi-database:/var/lib/mysql
ports:
- ${DATABASE_PORT}:${DATABASE_PORT}
• image: Используется образ MySQL версии 9.0.1, который будет загружен из Docker Hub;
• container_name: Задает имя контейнера на основе переменной окружения ${SERVICE}, чтобы обеспечить уникальность имени;
• restart: Политика перезапуска контейнера — он будет автоматически перезапускаться в случае сбоя;
• environment: Переменные окружения для настройки MySQL, такие как имя пользователя, пароль и имя базы данных. Значения берутся из файла .env;
• volumes: Используется для хранения данных MySQL вне контейнера, чтобы данные не терялись при его перезапуске. Здесь определен внешний том mpt-kpi-database;
• ports: Порт для связи с базой данных, который перенаправляются из контейнера на хост, что позволяет другим приложениям подключаться к базе данных.
Сервис frontend
frontend:
container_name: ${SERVICE}_dev_frontend
env_file: .env
build:
context: ./frontend/
dockerfile: dev.Dockerfile
volumes:
- ./frontend:/app
ports:
- ${FRONTEND_PORT}:${FRONTEND_PORT}
• container_name: Имя контейнера формируется на основе переменной ${SERVICE}, что помогает различать контейнеры разработки;
• env_file: Подключает файл .env, который содержит переменные окружения, используемые приложением;
• build: Определяет контекст сборки и Dockerfile, который будет использоваться для создания образа фронтенда из папки frontend;
• volumes: Монтирует локальную папку frontend в контейнер, что позволяет разработчикам сразу видеть изменения;
• ports: Настраивает перенаправление портов для фронтенд-приложения, обеспечивая доступ к нему через хост.
Сервис backend
backend:
container_name: ${SERVICE}_dev_backend
env_file: .env
build:
context: ./backend/
dockerfile: dev.Dockerfile
environment:
- DEBUG=1
volumes:
- ./backend:/app
ports:
- ${BACKEND_PORT}:${BACKEND_PORT}
depends_on:
- db
• container_name: Имя контейнера формируется так же, как и для других сервисов;
• env_file: Определяется подключение к файлу .env, содержащему переменные окружения для бэкенда;
• build: Определяет директорию сборки для бэкенд-приложения, указывая, что Dockerfile находится в папке backend.
• environment: Устанавливает переменную окружения DEBUG=1, которая может быть использована для включения режима отладки в приложении;
• volumes: Монтирует локальную папку backend в контейнер, что позволяет разработчикам сразу видеть изменения;
• ports: Настраивает перенаправление порта для бэкенд-приложения так же, как и для фронтенда.
• depends_on: Указывает, что сервис backend зависит от db. Это гарантирует, что база данных будет запущена прежде, чем будет запущен бэкенд.
Общий том:
volumes:
mpt-kpi-database:
external: true
• volumes: Определяет внешний том mpt-kpi-database, используемый для хранения данных MySQL. Это позволяет сохранить состояние базы данных за пределами контейнера, обеспечивая надежность данных при перезапусках контейнера.
Файл docker-compose.yml используется для настройки приложения с помощью Docker Compose в продуктивной или тестовой среде. Он определяет три основных сервиса: базу данных (db), фронтенд (frontend) и бэкенд (backend):
services:
db:
image: mysql:9.0.1
container_name: ${SERVICE}_db
restart: always
env_file: .env
environment:
MYSQL_USER: $DATABASE_USER
MYSQL_PASSWORD: $DATABASE_PASSWORD
MYSQL_DATABASE: $DATABASE_NAME
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
volumes:
- mpt-kpi-database:/var/lib/mysql
frontend:
container_name: ${SERVICE}_frontend
platform: linux/amd64
restart: always
env_file: .env
build:
context: ./frontend/
ports:
- 127.0.0.1:${FRONTEND_PORT}:${FRONTEND_PORT}
backend:
container_name: ${SERVICE}_backend
platform: linux/amd64
restart: always
env_file: .env
build:
context: ./backend/
environment:
- DATABASE_PORT
- FRONTEND_PORT
- BACKEND_PORT
- DATABASE_USER
- DATABASE_NAME
- DATABASE_PASSWORD
depends_on:
- db
volumes:
mpt-kpi-database:
external: true
Пояснение каждого сервиса:
Сервис db
db:
image: mysql:9.0.1
container_name: ${SERVICE}_db
restart: always
env_file: .env
environment:
MYSQL_USER: $DATABASE_USER
MYSQL_PASSWORD: $DATABASE_PASSWORD
MYSQL_DATABASE: $DATABASE_NAME
MYSQL_ALLOW_EMPTY_PASSWORD: "yes"
volumes:
- mpt-kpi-database:/var/lib/mysql
• image: Используется образ MySQL версии 9.0.1 из Docker Hub.
• container_name: Имя контейнера, формируемое на основе переменной окружения ${SERVICE}.
• restart: Политика автоматического перезапуска контейнера в случае его сбоя.
• env_file: Файл .env, в котором содержатся переменные окружения для конфигурации контейнера.
• environment: Переменные окружения, используемые для настройки учетных записей и базы данных MySQL. Значения берутся из файла .env.
• volumes: Монтирование внешнего тома mpt-kpi-database для хранения данных MySQL во избежание их потери при перезапуске контейнера.
Сервис frontend
frontend:
container_name: ${SERVICE}_frontend
platform: linux/amd64
restart: always
env_file: .env
build:
context: ./frontend/
ports:
- 127.0.0.1:${FRONTEND_PORT}:${FRONTEND_PORT}
• container_name: Имя контейнера на основе переменной ${SERVICE}.
• platform: Указывает, что контейнер будет работать на архитектуре linux/amd64.
• restart: Контейнер будет перезапускаться автоматически в случае сбоев.
• env_file: Связывает файл .env, содержащий переменные окружения для приложения.
• build: Определяет директорию сборки для фронтенд-приложения, указывая, что Dockerfile находится в папке frontend.
• ports: Настраивает перенаправление портов для доступа к приложению, связывая локальный IP 127.0.0.1 с портом фронтенда.
Сервис backend
backend:
container_name: ${SERVICE}_backend
platform: linux/amd64
restart: always
env_file: .env
build:
context: ./backend/
environment:
- DATABASE_PORT
- FRONTEND_PORT
- BACKEND_PORT
- DATABASE_USER
- DATABASE_NAME
- DATABASE_PASSWORD
depends_on:
- db
• container_name: Название контейнера формируется из переменной ${SERVICE}.
• platform: Указывает архитектуру, на которой будет работать контейнер (linux/amd64).
• restart: Автоматический перезапуск контейнера в случае его неудачи.
• env_file: Подключает файл .env с переменными окружения.
• build: Определяет директорию сборки для бэкенд-приложения, указывая, что Dockerfile находится в папке backend.
• environment: Перечисляет переменные окружения, которые будут доступны в контейнере. Эти переменные часто используются для настройки соединения с базой данных и другими сервисами.
• depends_on: Гарантирует, что база данных (db) будет запущена прежде, чем бэкенд начнет свою работу.
Общие тома
volumes:
mpt-kpi-database:
external: true
• volumes: Определение внешнего тома mpt-kpi-database, который используется для хранения данных MySQL. Это позволяет сохранить данные даже после перезапуска контейнеров.
Файл .env используется для хранения конфиденциальных данных и переменных окружения, которые используются в приложении. Он облегчает управление параметрами конфигурации, так как значения могут быть изменены без необходимости редактировать другие файлы.
SERVICE=mpt-kpi
DATABASE_PORT=3306
FRONTEND_PORT=3000
BACKEND_PORT=8000
DATABASE_USER=mpt-kpi
DATABASE_NAME=mpt-kpi
DATABASE_PASSWORD=
Объяснение переменных:
• SERVICE=mpt-kpi: Имя приложения. Это будет использоваться для установки имён контейнеров в файле docker-compose.yml.
• DATABASE_PORT=3306: Порт, на котором будет работать MySQL сервер.
• FRONTEND_PORT=3000: Порт, на котором будет доступно фронтенд-приложение.
• BACKEND_PORT=8000: Порт, на котором будет доступно бэкенд-приложение.
• DATABASE_USER=mpt-kpi: Имя пользователя базы данных для подключения к MySQL.
• DATABASE_NAME=mpt-kpi: Имя базы данных, которая будет создана в MySQL.
• DATABASE_PASSWORD=: Пароль для пользователя базы данных.
Данный файл конфигурации используется инструментом Alembic для управления миграциями в проекте. Он определяет параметры подключения к базе данных и настройки журналирования.
# A generic, single database configuration.
[alembic]
# template used to generate migration files
# file_template = %%(rev)s_%%(slug)s
# set to 'true' to run the environment during
# the 'revision' command, regardless of autogenerate
# revision_environment = false
# Logging configuration
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
[handlers]
keys = console
[formatters]
keys = generic
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
• file_template: Этот параметр задает шаблон для именования файлов миграции. По умолчанию он закомментирован;
• revision_environment: Если этот параметр установлен в true, среда будет запущена во время команды revision, независимо от автоматической генерации. По умолчанию он закомментирован.
Настройки логирования позволяют управлять уровнем и форматированием сообщений логов:
Логгеры
[loggers]
keys = root,sqlalchemy,alembic,flask_migrate
• Определяет набор логгеров, используемых в проекте. В данном случае: root, sqlalchemy, alembic и flask_migrate.
Обработчики
[handlers]
keys = console
• Определяет, какие обработчики будут использоваться для логирования. В этом случае используется только консольный обработчик.
Форматировщики
[formatters]
keys = generic
• Определяет форматировщик для сообщений логирования.
Конфигурация логгеров
[logger_root]
level = WARN
handlers = console
qualname =
[logger_sqlalchemy]
level = WARN
handlers =
qualname = sqlalchemy.engine
[logger_alembic]
level = INFO
handlers =
qualname = alembic
[logger_flask_migrate]
level = INFO
handlers =
qualname = flask_migrate
• logger_root: Корневой логгер. Установлен уровень логирования на WARN, регистрирует вывод во внешний обработчик.
• logger_sqlalchemy: Логгер для SQLAlchemy, уровень также установлен на WARN.
• logger_alembic: Логгер для Alembic, уровень INFO.
• logger_flask_migrate: Логгер для Flask-Migrate, уровень INFO.
Обработчик консоли
[handler_console]
class = StreamHandler
args = (sys.stderr,)
level = NOTSET
formatter = generic
• Это определение консольного обработчика, который выводит логи в стандартный поток ошибок (sys.stderr) с уровнем логирования NOTSET, что позволяет регистировать все уровни логов. Формат сообщений задается с помощью generic.
Формат сообщений логирования
[formatter_generic]
format = %(levelname)-5.5s [%(name)s] %(message)s
datefmt = %H:%M:%S
• Формат сообщений логов определяет стиль отображения сообщений и включает в себя уровень логирования, имя логгера и само сообщение, а также время сообщением в формате HH:MM:SS.
Файл env.py является ключевым компонентом для Alembic в проектах на базе Flask, обеспечивая настройку контекста миграций и управление подключениями к базе данных.
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
def get_engine():
try:
# this works with Flask-SQLAlchemy<3 and Alchemical
return current_app.extensions['migrate'].db.get_engine()
except (TypeError, AttributeError):
# this works with Flask-SQLAlchemy>=3
return current_app.extensions['migrate'].db.engine
def get_engine_url():
try:
return get_engine().url.render_as_string(hide_password=False).replace(
'%', '%%')
except AttributeError:
return str(get_engine().url).replace('%', '%%')
# add your model's MetaData object here
# for 'autogenerate' support
# from myapp import mymodel
# target_metadata = mymodel.Base.metadata
config.set_main_option('sqlalchemy.url', get_engine_url())
target_db = current_app.extensions['migrate'].db
# other values from the config, defined by the needs of env.py,
# can be acquired:
# my_important_option = config.get_main_option("my_important_option")
# ... etc.
def get_metadata():
if hasattr(target_db, 'metadatas'):
return target_db.metadatas[None]
return target_db.metadata
def run_migrations_offline():
"""Run migrations in 'offline' mode.
This configures the context with just a URL
and not an Engine, though an Engine is acceptable
here as well. By skipping the Engine creation
we don't even need a DBAPI to be available.
Calls to context.execute() here emit the given string to the
script output.
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=get_metadata(), literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
def run_migrations_online():
"""Run migrations in 'online' mode.
In this scenario we need to create an Engine
and associate a connection with the context.
"""
# this callback is used to prevent an auto-migration from being generated
# when there are no changes to the schema
# reference: http://alembic.zzzcomputing.com/en/latest/cookbook.html
def process_revision_directives(context, revision, directives):
if getattr(config.cmd_opts, 'autogenerate', False):
script = directives[0]
if script.upgrade_ops.is_empty():
directives[:] = []
logger.info('No changes in schema detected.')
conf_args = current_app.extensions['migrate'].configure_args
if conf_args.get("process_revision_directives") is None:
conf_args["process_revision_directives"] = process_revision_directives
connectable = get_engine()
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=get_metadata(),
**conf_args
)
with context.begin_transaction():
context.run_migrations()
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
Импортируемые модули
import logging
from logging.config import fileConfig
from flask import current_app
from alembic import context
• logging: Модуль для настройки логирования.
• flask.current_app: Позволяет получить доступ к текущему экземпляру приложения Flask.
• alembic.context: Предоставляет контекст для выполнения миграций.
Конфигурация Alembic
# this is the Alembic Config object, which provides
# access to the values within the .ini file in use.
config = context.config
• config: Представляет собой объект конфигурации Alembic, который предоставляет доступ к значениям из файла alembic.ini.
Настройка логирования
# Interpret the config file for Python logging.
# This line sets up loggers basically.
fileConfig(config.config_file_name)
logger = logging.getLogger('alembic.env')
• fileConfig: Настраивает логгеры в соответствии с конфигурацией из файла.
• logger: Создает логгер для Alembic с именем 'alembic.env'.
Получение движка базы данных
def get_engine():
try:
return current_app.extensions['migrate'].db.get_engine()
except (TypeError, AttributeError):
return current_app.extensions['migrate'].db.engine
• get_engine(): Функция возвращает движок базы данных, используя механизм расширений Flask-Migrate.
Получение URL подключения
def get_engine_url():
try:
return get_engine().url.render_as_string(hide_password=False).replace('%', '%%')
except AttributeError:
return str(get_engine().url).replace('%', '%%')
• get_engine_url(): Функция возвращает строку URL, представляющую соединение с базой данных. Она обрабатывает исключения и отображает пароль, если это необходимо.
Настройка метаданных
def get_metadata():
if hasattr(target_db, 'metadatas'):
return target_db.metadatas[None]
return target_db.metadata
• get_metadata(): Функция возвращает объект метаданных, содержащий информацию о таблицах базы данных.
Оффлайн-режим
def run_migrations_offline():
"""Run migrations in 'offline' mode.
...
"""
url = config.get_main_option("sqlalchemy.url")
context.configure(
url=url, target_metadata=get_metadata(), literal_binds=True
)
with context.begin_transaction():
context.run_migrations()
• run_migrations_offline(): Функция запускает миграции в оффлайн-режиме, настраивая контекст только с URL и метаданными.
Онлайн-режим
def run_migrations_online():
"""Run migrations in 'online' mode.
...
"""
def process_revision_directives(context, revision, directives):
...
conf_args = current_app.extensions['migrate'].configure_args
if conf_args.get("process_revision_directives") is None:
conf_args["process_revision_directives"] = process_revision_directives
connectable = get_engine()
with connectable.connect() as connection:
context.configure(
connection=connection,
target_metadata=get_metadata(),
**conf_args
)
with context.begin_transaction():
context.run_migrations()
• run_migrations_online(): Функция осуществляет миграции в онлайн-режиме, создавая соединение с базой данных и настраивая контекст с необходимыми метаданными и параметрами конфигурации.
Запуск миграций
if context.is_offline_mode():
run_migrations_offline()
else:
run_migrations_online()
• В зависимости от режима работы (оффлайн или онлайн), вызывается соответствующая функция для выполнения миграций.
Файл script.py.mako используется в Alembic для генерации миграционных скриптов с использованием шаблонов Mako. Этот файл содержит основные элементы, необходимые для выполнения миграций, такие как идентификаторы ревизий, функции для обновления и отката изменений базы данных.
"""${message}
Revision ID: ${up_revision}
Revises: ${down_revision | comma,n}
Create Date: ${create_date}
"""
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
def upgrade():
${upgrades if upgrades else "pass"}
def downgrade():
${downgrades if downgrades else "pass"}
Импортируемые модули
from alembic import op
import sqlalchemy as sa
${imports if imports else ""}
• from alembic import op: Импортирует объект op, используемый для выполнения операций миграции.
• import sqlalchemy as sa: Импортирует SQLAlchemy для работы с типами данных и операциями базы данных.
• imports: Здесь можно вставить дополнительные импорты, если они необходимы для миграции.
Идентификаторы ревизий
# revision identifiers, used by Alembic.
revision = ${repr(up_revision)}
down_revision = ${repr(down_revision)}
branch_labels = ${repr(branch_labels)}
depends_on = ${repr(depends_on)}
• revision: Текущий идентификатор ревизии.
• down_revision: Идентификатор предыдущей ревизии.
• branch_labels: Метки веток, относящихся к ревизии.
• depends_on: Зависимости от других ревизий, если таковые имеются.
Функция обновления
def upgrade():
${upgrades if upgrades else "pass"}
• upgrade(): Функция, которая выполняет изменения в базе данных для текущей ревизии. Если не заданы изменения, используется заглавный оператор pass, который ничего не делает.
Функция отката
def downgrade():
${downgrades if downgrades else "pass"}
• downgrade(): Функция, которая откатывает изменения миграции обратно к предыдущему состоянию. Аналогично функции upgrade, по умолчанию используется pass, если не заданы операции.
Этот файл представляет собой миграцию для создания начальной структуры базы данных. Он был сгенерирован с использованием Alembic и содержит описания необходимых таблиц, а также их атрибутов.
"""Initial migration
Revision ID: a32201abf005
Revises:
Create Date: 2024-12-02 06:47:03.151935
"""
from alembic import op
import sqlalchemy as sa
# revision identifiers, used by Alembic.
revision = 'a32201abf005'
down_revision = None
branch_labels = None
depends_on = None
def upgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.create_table('position',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('score_threshold', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('first_name', sa.String(length=50), nullable=False),
sa.Column('last_name', sa.String(length=50), nullable=False),
sa.Column('middle_name', sa.String(length=50), nullable=True),
sa.Column('position_id', sa.Integer(), nullable=True),
sa.Column('phone_number', sa.String(length=15), nullable=True),
sa.Column('email', sa.String(length=120), nullable=False),
sa.Column('password_hash', sa.String(length=128), nullable=True),
sa.Column('is_admin', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['position_id'], ['position.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('phone_number')
)
# ### end Alembic commands ###
def downgrade():
# ### commands auto generated by Alembic - please adjust! ###
op.drop_table('user')
op.drop_table('position')
# ### end Alembic commands ###
Импортируемые модули
from alembic import op
import sqlalchemy as sa
• alembic.op: Модуль, предоставляющий функции для работы с операциями миграции.
• sqlalchemy: Библиотека для работы с базами данных, предоставляющая инструменты для определения и манипуляций с таблицами и их столбцами.
Идентификаторы ревизий Alembic
revision = 'a32201abf005'
down_revision = None
branch_labels = None
depends_on = None
• revision: Уникальный идентификатор для данной миграции.
• down_revision: Указывает, что данная миграция не имеет предшествующей.
• branch_labels и depends_on: Не используются в данной миграции.
Функция upgrade()
Функция upgrade() используется для применения изменений в структуре базы данных. В данном случае она включает в себя создание двух таблиц: position и user.
Создание таблицы position
op.create_table('position',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('name', sa.String(length=100), nullable=False),
sa.Column('score_threshold', sa.Integer(), nullable=False),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('name')
)
• id: Целочисленный идентификатор, является первичным ключом.
• name: Строка длиной до 100 символов, уникальная для каждой позиции.
• score_threshold: Целочисленный порог баллов для позиции.
Создание таблицы user
op.create_table('user',
sa.Column('id', sa.Integer(), nullable=False),
sa.Column('first_name', sa.String(length=50), nullable=False),
sa.Column('last_name', sa.String(length=50), nullable=False),
sa.Column('middle_name', sa.String(length=50), nullable=True),
sa.Column('position_id', sa.Integer(), nullable=True),
sa.Column('phone_number', sa.String(length=15), nullable=True),
sa.Column('email', sa.String(length=120), nullable=False),
sa.Column('password_hash', sa.String(length=128), nullable=True),
sa.Column('is_admin', sa.Boolean(), nullable=True),
sa.ForeignKeyConstraint(['position_id'], ['position.id'], ),
sa.PrimaryKeyConstraint('id'),
sa.UniqueConstraint('email'),
sa.UniqueConstraint('phone_number')
)
• id: Целочисленный идентификатор, является первичным ключом.
• first_name: Имя пользователя, не может быть пустым.
• last_name: Фамилия пользователя, не может быть пустой.
• middle_name: Отчество пользователя (необязательный).
• position_id: Идентификатор позиции (внешний ключ, указывающий на таблицу position).
• phone_number: Номер телефона пользователя (уникальный).
• email: Адрес электронной почты (уникальный).
• password_hash: Хэш пароля пользователя (необязательный).
• is_admin: Указывает, является ли пользователь администратором (необязательный).
Функция downgrade() Функция downgrade() используется для отмены изменений, выполненных в функции upgrade(). В данном случае она удаляет созданные таблицы.
def downgrade():
op.drop_table('user')
op.drop_table('position')
• op.drop_table('user'): Удаляет таблицу user.
• op.drop_table('position'): Удаляет таблицу position.
В данной папке размещены файлы, отвечающие за создание RESTful API ресурсов с использованием Flask-RESTful. Каждый файл реализует определённый ресурс и обрабатывает запросы к ним, включая операции получения, обновления и создания данных.
Данный файл определяет класс BasicResource, который наследуется от Resource из библиотеки Flask-RESTful.
from flask_restful import Resource, reqparse
docs = {}
class BasicResource(Resource):
def get(self, source_id: int) -> dict:
return {source_id: docs.get(source_id)}
# FOR UPDATE
def put(self, source_id: int):
docs[source_id] = source_id
return {source_id: source_id}
# FOR CREATE
def post(self, source_id: int):
docs[source_id] = source_id
return {source_id: source_id}, 201
Данный файл предоставляет три основных метода для обработки HTTP-запросов:
• get(self, source_id: int) -> dict: Метод, который получает данные по source_id. Если данные не найдены, возвращает None.
• put(self, source_id: int): Метод веб-API для обновления данных. Принимает source_id, обновляет данные в словаре docs, и возвращает обновлённые данные.
• post(self, source_id: int): Метод для создания новой записи. Принимает source_id, добавляет его в словарь docs, и возвращает созданные данные с кодом ответа 201 (Created).
Файл содержит класс CertificatesResource, который также наследуется от Resource. Основная задача этого ресурса — предоставление информации о сертификатах.
from flask_restful import Resource, reqparse
from services.certificates_generator import Certificate, certificates_list
class CertificatesResource(Resource):
def get(self) -> dict:
return [certificate._asdict() for certificate in certificates_list]
• get(self) -> dict: Метод для получения списка сертификатов. Он возвращает список сертификатов в виде словарей, используя вспомогательную функцию _asdict() для преобразования объекта сертификата в словарь. Данные сертификатов поступают из certificates_list.
Файл описывает класс CriteriesResource, который отвечает за предоставление данных о критериях.
from flask_restful import Resource, reqparse
from services.criteries_generator import Criterion, criteries_list
class CriteriesResource(Resource):
def get(self) -> dict:
return [criterion._asdict() for criterion in criteries_list]
• get(self) -> dict: Метод для получения списка критериев. Он возвращает список объектов Criterion, преобразованных в словари с помощью метода _asdict(). Данные критериев поступают из criteries_list.
Файл содержит класс EmployeesResource, который реализует управление данными сотрудников.
from flask_restful import Resource, reqparse
from flask import request
from services.employees_generator import Employee, employees_list
# parser = reqparse.RequestParser()
# (
# parser.add_argument(argument) for argument in
# 'id first_name last_name surname job_id phone email'.split()
# )
class EmployeesResource(Resource):
def get(self) -> dict:
return [employee._asdict() for employee in employees_list]
def post(self) -> dict:
args = request.json
#args = parser.parse_args()
print(args)
employee = Employee(
id=args['id'],
first_name=args['first_name'],
last_name=args['last_name'],
surname=args['surname'],
job_id=args['job_id'],
phone=args['phone'],
email=args['email'],
)
employees_list.append(employee)
return employees_list[-1]._asdict()
• get(self) -> dict: Метод для получения списка всех сотрудников. Возвращает список сотрудников, преобразованных в словари с помощью метода /_asdict().
• post(self) -> dict: Метод для добавления нового сотрудника. Он извлекает данные из JSON тела запроса, создает новый объект Employee и добавляет его в список. Возвращает данные последнего добавленного сотрудника в виде словаря.
Файл реализует класс PDFResource, обеспечивающий функциональность работы с PDF файлами и другими поддерживаемыми форматами.
import os
from werkzeug.utils import secure_filename
from flask_restful import Resource, abort
from flask import request, send_from_directory
ALLOWED_EXTENSIONS = ['pdf', 'doc', 'docx', 'png']
def find_local_file(target_name) -> bool:
print(target_name)
for (root, folder, file_name) in os.walk('docs'):
if len(file_name) == 0: return False
if target_name in file_name: return True
return False
def validate_extension(file_ext) -> bool:
for ext in ALLOWED_EXTENSIONS:
if file_ext == ext: return True
return False
def build_name(file_name, file_ext) -> str: return file_name + '.' + file_ext
class PDFResource(Resource):
def post(self):
file = request.files.get('image')
print(file.filename)
save_filename, save_ext = secure_filename(file.filename).split('.')
if not validate_extension(save_ext): abort(415)
if find_local_file(file.filename):
save_filename += '_copy'
save_filename = build_name(save_filename, save_ext)
file.save(dst="docs/"+secure_filename(save_filename))
return {'file': 'saved:OK'}, 201
def get(self, filename):
if find_local_file(filename):
print(f'FOUND {filename}')
# Возвращает файл, который открывается в браузере
# На фронте можно просто дать ссылку на docs/<имя файла>
# <a href="docs/filo.pdf">File link</a>, должно работать
return send_from_directory('docs', secure_filename(filename))
else:
abort(404)
def delete(self, filename):
try:
os.remove(os.path.join('docs', secure_filename(filename)))
return {'file': 'deleted:OK'}, 200
except WindowsError as e:
print(e)
abort(404)
• post(self): Метод для загрузки файлов. Он извлекает файл из запроса, проверяет его расширение и наличие в локальной директории. Если файл с таким именем уже существует, добавляет суффикс _copy. Файл сохраняется в папку docs и возвращается статус 201 Created.
• get(self, filename): Метод для получения файла по имени. Он проверяет наличие файла в директории docs и возвращает его, если файл найден. В противном случае возвращает статус 404 Not Found.
• delete(self, filename): Метод для удаления файла. Он удаляет указанный файл из директории docs. Если файл не найден, возвращает статус 404 Not Found.
В данной папке находятся модули, генерирующие случайные данные для сертификатов, критериев и сотрудников, используемых в проекте.
В данном модуле определяется структура Certificate, которая представляет сертификаты, содержащие идентификатор, URL превью и идентификатор критерия.
import requests
import collections
import random
from flask import url_for
from .criteries_generator import criteries_list
Certificate = collections.namedtuple('Certificate', 'id preview_url criterion_id'.split())
urls = [
"https://xn----7sbbatcvjrscddclqofaivf1a1pxa.xn--p1ai/images/sertif.jpg",
"https://www.gostest.com/upload/medialibrary/ff2/ff2d3a6375ca7060f0ce1b5465cfae92.jpg",
"https://moseac.ru/images/article/5fb68313a9fa6.jpg",
"https://apit-kovrov.ru/files/uploads/knowledge_base/br1.1-do-2024.jpg",
"http://127.0.0.1:8000/api/media/test.jpg"
]
# Добавить подгрузку ссылок на документы + превью: <название_дока>, <название_дока>_preview.png
def _generate_random_certificate(id: int, url: str):
return Certificate(
id=id,
preview_url=url,
criterion_id=random.choice(range(len(criteries_list))),
)
certificates_list: list[Certificate] = [_generate_random_certificate(i, name) for i, name in enumerate(urls)] # Override this to use database requests
• urls(): Список, который содержит предопределенные ссылки на изображения сертификатов.
• _generate_random_certificate(): Функция, которая создает случайный сертификат с заданным идентификатором и URL.
• certificates_list: Список, который заполняется сертификатами, генерируемыми с помощью функции _generate_random_certificate для каждого URL.
В этом модуле определяется структура Criterion, представляющая критерии оценки. Каждый критерий содержит идентификатор, название и диапазон оценок.
import requests
import collections
import random
Criterion = collections.namedtuple('Criterion', 'id name mark_from mark_to'.split())
urls = [
"Привлечение дополнительных средств и спонсорской помощи для развития библиотеки",
"Публикации и освещение опыта методической работы библиотек в средствах массовой информации",
"Oрганизация и участие в мероприятиях муниципального и регионального уровня",
"Участие в разработке локальных нормативных документов",
]
def _generate_random_criterion(id: int, name: str):
return Criterion(
id=id,
name=name,
mark_from=random.choice(range(0, 10)),
mark_to=random.choice(range(10, 20)),
)
criteries_list: list[Criterion] = [_generate_random_criterion(i, name) for i, name in enumerate(urls)] # Override this to use database requests
• urls: Список, который включает задания и описания критериев, которые используются для создания объектов Criterion.
• _generate_random_criterion(): Функция, которая генерирует случайный критерий с заданным идентификатором и названием. Оценки формируются случайно из заранее определенных диапазонов.
• criteries_list: Список, который наполняется критериями, создаваемыми с помощью функции _generate_random_criterion.
В данном модуле определяется структура Employee, представляющая сотрудников с набором атрибутов.
import requests
import collections
import random
Employee = collections.namedtuple('Employee', 'id first_name last_name surname job_id phone email'.split())
first_names_url = 'https://raw.githubusercontent.com/Raven-SL/ru-pnames-list/refs/heads/master/lists/male_names_rus.txt'
last_names_url = 'https://raw.githubusercontent.com/Raven-SL/ru-pnames-list/refs/heads/master/lists/male_surnames_rus.txt'
first_names = requests.get(first_names_url).content.decode('utf-8').split('\n')
last_names = requests.get(last_names_url).content.decode('utf-8').split('\n')
surnames = [f'{name}ович' for name in first_names]
job_ids = ['Библиотекарь']
phones = ['+7(963)650-42-93']
emails = ['[email protected]']
def _generate_random_employee(id):
return Employee(
id=id,
first_name=random.choice(first_names),
last_name=random.choice(last_names),
surname=random.choice(surnames),
job_id=random.choice(job_ids),
phone=random.choice(phones),
email=random.choice(emails),
)
employees_list: list[Employee] = [_generate_random_employee(i) for i in range(100)] # Override this to use database requests
• Формирование имен и фамилий: Имена и фамилии загружаются из внешних источников. Служебные имена и контакты созданы из предопределенных списков.
• _generate_random_employee(): Функция генерирует случайного сотрудника с переданным идентификатором, выбирая случайные значения из списков имен, фамилий, должностей, телефонов и электронных почтовых адресов.
• employees_list: Список, который наполняется сотрудниками, генерируемыми с помощью функции _generate_random_employee в количестве 100 штук.
Данный HTML-файл представляет веб-страницу, включающую заголовок, ссылку на документ и форму для загрузки файлов. Структура файла соответствует стандартам HTML5.
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<div>
<a href="docs/filo.pdf">File link</a>
<form action="/docs" method="POST" enctype="multipart/form-data">
<input type="file" id="myFile" name="uploaded_file">
<button>UPLOAD</button>>
</form>
</div>
</body>
</html>
Тип документа:
<!DOCTYPE html>
• Определяет тип документа, указывая, что это HTML5.
Тег :
<html lang="en">
• Язык страницы установлен как английский (lang="en"), что способствует правильному определению языка для браузеров и поисковых систем.
Тег <body>:
<body>
<div>
<a href="docs/filo.pdf">File link</a>
<form action="/docs" method="POST" enctype="multipart/form-data">
<input type="file" id="myFile" name="uploaded_file">
<button>UPLOAD</button>
</form>
</div>
</body>
• <div>: Контейнер для группы элементов, используемый для структурирования содержимого.
• <a href="docs/filo.pdf">File link: Создает гиперссылку на файл filo.pdf, расположенный в папке docs.
• <form>: Определяет форму для загрузки файлов. Устанавливает следующие атрибуты:
-
action="/docs": Указывает URL для обработки отправленных данных.
-
method="POST": Устанавливает метод передачи данных.
-
enctype="multipart/form-data": Позволяет передавать файлы через форму.
• <input type="file" id="myFile" name="uploaded_file">: Элемент для выбора файла, которому назначены атрибуты id и name для идентификации в обработчике на сервере.
• <button>: Кнопка для отправки формы с загруженным файлом.
Данный конфигурационный файл используется для настройки инструмента проверки кода Flake8, который анализирует Python-код на соответствие стандартам стиля PEP 8 и выявляет ошибки программирования.
[flake8]
exclude =
migrations
media
max-line-length = 120
ignore =
# Avoid using null=True on string-based fields such CharField.
DJ01
# prefer-logging-interpolation: Use lazy % formatting in logging functions.
PIE803
T101
per-file-ignores =
common/num2t/__init__.py:S001
backend/common/num2t/__init__.py:S001
integration/models.py:E800
backend/integration/models.py:E800
business/tests/api/test_integration.py:Q001
backend/business/tests/api/test_integration.py:Q001
[pep8]
# https://pep8.readthedocs.io/en/latest/intro.html#error-codes
# E501
max-line-length = 120
• exclude: Определяет директории и файлы, исключаемые из проверки.
• max-line-length: Устанавливает максимальную длину строки. Параметр задан как 120 символов, что позволяет сохранять читаемость кода.
• ignore: Список ошибок, которые следует игнорировать при проверке. В данном случае игнорируются следующие коды:
-
DJ01: Предупреждение, связанное с использованием null=True для строковых полей, таких как CharField.
-
PIE803: Рекомендуется использовать ленивое форматирование с помощью % в функциях журналирования.
-
T101: Код типа ошибки, который будет проигнорирован.
• per-file-ignores: Позволяет игнорировать определенные ошибки для указанных файлов или модулей.
Этот файл конфигурации предназначен для настройки инструмента Ruff, который используется для статического анализа кода на Python. Он содержит правила форматирования и исключения для определённых директорий и файлов.
# https://github.com/charliermarsh/ruff#configuration
[tool.ruff]
line-length = 120
# Exclude a variety of commonly ignored directories.
exclude = [
".bzr",
".direnv",
".eggs",
".git",
".hg",
".mypy_cache",
".nox",
".pants.d",
".ruff_cache",
".svn",
".tox",
".venv",
"__pypackages__",
"_build",
"buck-out",
"build",
"dist",
"node_modules",
"venv",
# custom
"migrations",
]
[tool.ruff.per-file-ignores]
"main/views.py" = [
"E402",
"E721",
]
• line-length: Данная настройка указывает максимальную длину строки, установленную на 120 символов. Это значение может быть изменено в зависимости от стандартов кодирования.
Параметр exclude используется для указания директорий, которые должны быть исключены из анализа. В данном случае исключены следующие каталоги:
• .git, .hg: системы контроля версий,
• .venv, node_modules: каталоги, содержащие зависимости,
• .mypy_cache, pypackages: кеши инструментов.
Дополнительно включена пользовательская директория "migrations", чтобы исключить её из анализа.
Параметр per-file-ignores позволяет задавать игнорирование определённых правил для указанных файлов. В данном случае, для файла "main/views.py" игнорируются следующие коды ошибок:
• E402: Импорт не на верхнем уровне (imports not at top level);
• E721: Использование == для проверки на None (do not compare types, use is instead).
В этом файле определяются расширения Flask, которые будут использованы в приложении. Эти расширения предоставляют базовую функциональность для работы с базой данных, миграциями, аутентификацией пользователей и отправкой электронной почты.
from flask_sqlalchemy import SQLAlchemy
from flask_migrate import Migrate
from flask_login import LoginManager
from flask_mail import Mail
db = SQLAlchemy()
migrate = Migrate()
login_manager = LoginManager()
mail = Mail()
• db: Объект, который предоставляет основные методы и функционал для работы с базой данных через ORM (Object-Relational Mapping). Он позволяет управлять моделями, выполнять запросы к базе данных и обрабатывать транзакции.
• migrate: Объект, который отвечает за управление миграциями базы данных. С его помощью вы можете создавать, обновлять и откатывать изменения в структуре базы данных, максимально упрощая процесс внедрения новых моделей и изменений схемы.
• login_manager: Объект, который управляет аутентификацией пользователей. Он предоставляет инструменты для управления сессиями пользователей, а также для защиты маршрутов, доступ к которым имеют только авторизованные пользователи.
• mail: Объект, который используется для отправки электронной почты. Этот расширение предоставляет интерфейс для настройки и отправки писем прямо из приложения, включая поддержки HTML и вложений.
requiremnts.dev.in является входным файлом для управления зависимостями разработки. Он содержит список библиотек, необходимых для выполнения тестирования и линтинга кода:
# An extremely fast Python linter, written in Rust.
ruff
flake8
pytest
• ruff: быстрый линтер для Python, написанный на Rust;
• flake8: инструмент для проверки стиля кода;
• pytest: библиотека для написания тестов.
requiremnts.dev.in является автоматически сгенерированным результатом выполнения команды pip-compile с использованием requirements.dev.in. Он содержит фиксированные версии зависимостей, что позволяет избежать проблем с совместимостью:
#
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile requirements.dev.in
#
flake8==7.1.1
# via -r requirements.dev.in
iniconfig==2.0.0
# via pytest
mccabe==0.7.0
# via flake8
packaging==24.1
# via pytest
pluggy==1.5.0
# via pytest
pycodestyle==2.12.1
# via flake8
pyflakes==3.2.0
# via flake8
pytest==8.3.3
# via -r requirements.dev.in
ruff==0.6.5
# via -r requirements.dev.in
В данном файле перечислены основные зависимости приложения, которые необходимы для его функционирования. Он включает в себя различные библиотеки для веб-разработки на Flask:
requests~=2.32.3
mysql-connector-python~=9.0.0
Flask~=3.0.3
Flask-Cors~=5.0.0
Flask-RESTful~=0.3.10
Flask-SQLAlchemy~=3.1.1
Flask-Migrate~=4.0.7
Flask-Login~=0.6.3
Flask-Mail~=0.10.0
• requests: для работы с HTTP запросами;
• mysql-connector-python: библиотека для работы с базой данных MySQL;
• Flask: основной фреймворк для создания веб-приложений;
• Flask-Cors: для обработки CORS (Cross-Origin Resource Sharing);
• Flask-RESTful: для создания RESTful API;
• Flask-SQLAlchemy: для работы с базами данных;
• Flask-Migrate: для управления миграциями базы данных;
• Flask-Login: для управления аутентификацией;
• Flask-Mail: для отправки электронной почты.
requirements.txt - файл также создается автоматически с помощью pip-compile и содержит окончательные версии зависимостей, включая все зависимости, упомянутые в requirements.in.
#
# This file is autogenerated by pip-compile with Python 3.12
# by the following command:
#
# pip-compile
#
alembic==1.14.0
# via flask-migrate
aniso8601==9.0.1
# via flask-restful
blinker==1.8.2
# via
# flask
# flask-mail
certifi==2024.8.30
# via requests
charset-normalizer==3.4.0
# via requests
click==8.1.7
# via flask
flask==3.0.3
# via
# -r requirements.in
# flask-cors
# flask-login
# flask-mail
# flask-migrate
# flask-restful
# flask-sqlalchemy
flask-cors==5.0.0
# via -r requirements.in
flask-login==0.6.3
# via -r requirements.in
flask-mail==0.10.0
# via -r requirements.in
flask-migrate==4.0.7
# via -r requirements.in
flask-restful==0.3.10
# via -r requirements.in
flask-sqlalchemy==3.1.1
# via
# -r requirements.in
# flask-migrate
greenlet==3.1.1
# via sqlalchemy
idna==3.10
# via requests
itsdangerous==2.2.0
# via flask
jinja2==3.1.4
# via flask
mako==1.3.8
# via alembic
markupsafe==2.1.5
# via
# jinja2
# mako
# werkzeug
mysql-connector-python==9.0.0
# via -r requirements.in
pytz==2024.2
# via flask-restful
requests==2.32.3
# via -r requirements.in
six==1.16.0
# via flask-restful
sqlalchemy==2.0.36
# via
# alembic
# flask-sqlalchemy
typing-extensions==4.12.2
# via
# alembic
# sqlalchemy
urllib3==2.2.3
# via requests
werkzeug==3.0.4
# via
# flask
# flask-login
Данный файл предназначен для установки необходимых зависимостей и разработки приложения.
FROM python:3.12
ENV PYTHONUNBUFFERED 1
WORKDIR /app/
COPY requirements.txt .
COPY requirements.dev.txt .
RUN python -m pip install --upgrade pip && \
pip install pip-tools && \
pip install -r requirements.txt && \
pip install -r requirements.dev.txt
COPY . .
CMD ["python", "main.py"]
• FROM python:3.12: Использует образ Python версии 3.12;
• ENV PYTHONUNBUFFERED 1: Устанавливает переменную окружения, чтобы вывод Python не буферизовался (это полезно для логирования);
• WORKDIR /app/: Устанавливает рабочий каталог в контейнере;
• COPY requirements.txt . и COPY requirements.dev.txt .: Копирует файлы зависимостей внутрь контейнера;
• RUN python -m pip install --upgrade pip ...: Обновляет pip и устанавливает зависимости;
• COPY . .: Копирует все файлы приложения в контейнер;
• CMD ["python", "main.py"]: Команда для запуска приложения при старте контейнера.
Этот файл используется для создания оптимизированного образа для продакшн-окружения.
FROM python:3.12 AS builder
WORKDIR /app/
RUN apt update \
&& apt install -y --no-install-recommends gcc \
&& rm -rf /var/lib/apt/lists/*
RUN --mount=type=bind,source=requirements.txt,target=/app/requirements.txt \
pip wheel --no-cache-dir --no-deps -r /app/requirements.txt --wheel-dir /app/wheels
RUN --mount=type=bind,source=requirements.txt,target=/app/requirements.dev.txt \
pip wheel --no-cache-dir --no-deps -r /app/requirements.dev.txt --wheel-dir /app/wheels
#2#
FROM python:3.12-slim
COPY --from=builder /app/wheels /wheels
RUN pip install --upgrade pip && pip install --no-cache --no-cache-dir /wheels/*
WORKDIR /app
COPY . .
CMD ["python", "backend.py"]
• FROM python:3.12 AS builder: Первая стадия сборки, которая использует полный образ Python;
• RUN apt update ...: устанавливает компилятор C (gcc), необходимый для некоторых зависимостей, которые могут требовать компиляции;
• RUN --mount=type=bind,...: Использует механизм монтирования для установки зависимостей в виде wheel-файлов в папку /wheels (это делает сборку более быстрой и позволяет избежать повторной установки);
• FROM python:3.12-slim: Начинает вторую стадию с легкого образа Python, чтобы уменьшить размер конечного образа;
• COPY --from=builder /app/wheels /wheels: Копирование сгенерированных wheel-файлов из первой стадии в текущий образ;
• RUN pip install ...: Устанавливает зависимости из wheel-файлов;
• WORKDIR /app и COPY . .: Копирует файлы приложения в конечный образ;
• CMD ["python", "backend.py"]: Основная команда запуска приложения.
Данный SQL-скрипт предназначен для инициализации базы данных mpt_kpi. Скрипт включает создание необходимых таблиц и заполнение их начальными данными.
CREATE DATABASE IF NOT EXISTS mpt_kpi;
USE mpt_kpi;
CREATE TABLE IF NOT EXISTS Role (
role_id INT AUTO_INCREMENT PRIMARY KEY,
role_name VARCHAR(45) NOT NULL,
role_points INT NOT NULL,
role_description VARCHAR(125)
);
CREATE TABLE IF NOT EXISTS Users (
user_id INT AUTO_INCREMENT PRIMARY KEY,
user_name VARCHAR(45) NOT NULL,
user_surname VARCHAR(45) NOT NULL,
user_patronymic VARCHAR(45) NOT NULL,
user_email VARCHAR(45) NOT NULL,
user_phone VARCHAR(45) NOT NULL,
user_passhash VARCHAR(100) NOT NULL,
user_role VARCHAR(100) NOT NULL,
user_login VARCHAR(45) NOT NULL,
is_admin BOOLEAN NOT NULL DEFAULT FALSE,
role_role_id INT,
FOREIGN KEY (role_role_id) REFERENCES Role(role_id)
);
CREATE TABLE IF NOT EXISTS Criteria (
criteria_ID INT AUTO_INCREMENT PRIMARY KEY,
criteria_name VARCHAR(255) NOT NULL,
rating_from INT NOT NULL,
score_before INT NOT NULL
);
CREATE TABLE IF NOT EXISTS Certificates (
certificate_ID INT AUTO_INCREMENT PRIMARY KEY,
uploaded_by INT NOT NULL,
criteria_id INT NOT NULL,
is_approved BOOLEAN NOT NULL DEFAULT TRUE,
rating INT NOT NULL,
file_path VARCHAR(255) NOT NULL,
FOREIGN KEY (uploaded_by) REFERENCES Users(user_id),
FOREIGN KEY (criteria_id) REFERENCES Criteria(criteria_id)
);
INSERT INTO mpt_kpi.role VALUES ('1', 'Работник', '5', 'Работает');
INSERT INTO mpt_kpi.users VALUES ('1', 'Иван', 'Иванов', 'Иванович', '[email protected]', '79251232233', 'dsadxz', 'Работник', 'IvanII', '0', '1');
INSERT INTO mpt_kpi.criteria VALUES ('1', 'Доп. выходы', '1', '5');
INSERT INTO mpt_kpi.certificates VALUES ('1', '1', '1', '1', '5', 'file.path');
• CREATE DATABASE IF NOT EXISTS mpt_kpi: Создание базы данных, если она еще не была создана;
• USE mpt_kpi: Переключается на базу данных mpt_kpi.
Таблица Role предназначена для хранения информации о ролях пользователей. Структура таблицы включает следующие поля:
• role_id: уникальный идентификатор роли (PRIMARY KEY);
• role_name: наименование роли;
• role_points: количество баллов, связанных с ролью;
• role_description: описание роли.
Таблица Users содержит информацию о пользователях системы. Структура таблицы включает следующие поля.
• user_id: уникальный идентификатор пользователя (PRIMARY KEY).
• user_name: имя пользователя.
• user_surname: фамилия пользователя.
• user_patronymic: отчество пользователя.
• user_email: электронная почта пользователя.
• user_phone: номер телефона пользователя.
• user_passhash: хеш пароля.
• user_role: наименование роли пользователя.
• user_login: логин пользователя.
• is_admin: булево значение, указывающее, является ли пользователь администратором.
• role_role_id: внешний ключ, ссылающийся на идентификатор роли в таблице Role.
Таблица Criteria используется для хранения критериев оценивания. Структура таблицы включает следующие поля.
• criteria_ID: уникальный идентификатор критерия (PRIMARY KEY).
• criteria_name: название критерия.
• rating_from: минимальный рейтинг, с которого начинается оценка.
• score_before: предшествующий балл.
Таблица Certificates предназначена для хранения информации о сертификатах. Структура таблицы включает следующие поля:
• certificate_ID: уникальный идентификатор сертификата (PRIMARY KEY).
• uploaded_by: идентификатор пользователя, загрузившего сертификат (внешний ключ).
• criteria_id: идентификатор критерия, к которому относится сертификат (внешний ключ).
• is_approved: булево значение, указывающее, одобрен ли сертификат.
• rating: рейтинг сертификата.
• file_path: путь к файлу сертификата.
Команды, приведенные ниже, вставляют начальные данные в таблицы.
• INSERT INTO mpt_kpi.role VALUES ('1', 'Работник', '5', 'Работает'): Добавляется одна запись в таблицу Role с ролью "Работник".
• INSERT INTO mpt_kpi.users VALUES ('1', 'Иван', 'Иванов', 'Иванович', '[email protected]', '79251232233', 'dsadxz', 'Работник', 'IvanII', '0', '1'): Добавляется один пользователь в таблицу Users.
• INSERT INTO mpt_kpi.criteria VALUES ('1', 'Доп. выходы', '1', '5'): Добавляется один критерий в таблицу Criteria.
• INSERT INTO mpt_kpi.certificates VALUES ('1', '1', '1', '1', '5', 'file.path'): Добавляется один сертификат в таблицу Certificates.
В данном файле определены модели базы данных, используемые в приложении, которое работает с Flask и SQLAlchemy. В модели представлены Position и User, реализующие соответствующие сущности для хранения информации о должностях и пользователях системы.
from flask_login import UserMixin
from werkzeug.security import check_password_hash, generate_password_hash
from extensions import db
class Position(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True)
score_threshold = db.Column(db.Integer, nullable=False)
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.String(50), nullable=False)
last_name = db.Column(db.String(50), nullable=False)
middle_name = db.Column(db.String(50))
position_id = db.Column(db.Integer, db.ForeignKey("position.id"))
phone_number = db.Column(db.String(15), unique=True)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
is_admin = db.Column(db.Boolean, default=False)
position = db.relationship("Position", backref=db.backref("users", lazy=True))
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
Модель Position
class Position(db.Model):
id = db.Column(db.Integer, primary_key=True)
name = db.Column(db.String(100), nullable=False, unique=True)
score_threshold = db.Column(db.Integer, nullable=False)
• id: Уникальный идентификатор должности. Является первичным ключом.
• name: Название должности. Обязательное поле, должно быть уникальным.
• score_threshold: Пороговое значение для оценки, связанное с должностью. Обязательное поле.
Модель User
class User(UserMixin, db.Model):
id = db.Column(db.Integer, primary_key=True)
first_name = db.Column(db.String(50), nullable=False)
last_name = db.Column(db.String(50), nullable=False)
middle_name = db.Column(db.String(50))
position_id = db.Column(db.Integer, db.ForeignKey("position.id"))
phone_number = db.Column(db.String(15), unique=True)
email = db.Column(db.String(120), unique=True, nullable=False)
password_hash = db.Column(db.String(128))
is_admin = db.Column(db.Boolean, default=False)
position = db.relationship("Position", backref=db.backref("users", lazy=True))
def set_password(self, password):
self.password_hash = generate_password_hash(password)
def check_password(self, password):
return check_password_hash(self.password_hash, password)
• id: Уникальный идентификатор пользователя. Является первичным ключом.
• first_name: Имя пользователя. Обязательное поле.
• last_name: Фамилия пользователя. Обязательное поле.
• middle_name: Отчество пользователя. Необязательное поле.
• position_id: Идентификатор должности пользователя. Является внешним ключом, ссылающимся на модель Position.
• phone_number: Номер телефона пользователя. Должен быть уникальным.
• email: Электронная почта пользователя. Обязательное поле, должно быть уникальным.
• password_hash: Хэш пароля пользователя. Хранится в зашифрованном виде.
• is_admin: Флаг, указывающий, является ли пользователь администратором. По умолчанию устанавливается в значение False.
Методы
• set_password(self, password): Устанавливает хэшированный пароль для пользователя.
• check_password(self, password): Проверяет введённый пароль на соответствие хранившемуся хэшу.
В данном файле определяется класс конфигурации Config, который содержит настройки приложения. Эти настройки управляют различными параметрами, такими как секретный ключ, параметры подключения к базе данных и настройки для отправки электронной почты.
import os
class Config:
SECRET_KEY = os.getenv("SECRET_KEY", "mysecret")
SQLALCHEMY_DATABASE_URI = os.getenv("DATABASE_URL", "sqlite:///app.db")
SQLALCHEMY_TRACK_MODIFICATIONS = False
MAIL_SERVER = "smtp.example.com"
MAIL_PORT = 587
MAIL_USE_TLS = True
MAIL_USERNAME = os.getenv("MAIL_USERNAME")
MAIL_PASSWORD = os.getenv("MAIL_PASSWORD")
MAIL_DEFAULT_SENDER = os.getenv("MAIL_DEFAULT_SENDER")
• SECRET_KEY: Секретный ключ используется Flask для защиты данных, таких как сессии и куки. Он может быть извлечен из переменной окружения SECRET_KEY, что обеспечивает безопасность, однако в случае отсутствия переменной будет использовано значение по умолчанию "mysecret".
• SQLALCHEMY_DATABASE_URI: Этот атрибут указывает URI для подключения к базе данных. Значение может быть получено из переменной окружения DATABASE_URL, что позволяет гибко изменять базу данных без изменения кода. Если переменная не установлена, используется значение по умолчанию sqlite:///app.db (SQLite).
• SQLALCHEMY_TRACK_MODIFICATIONS: Данный параметр отключает отслеживание изменений объектов в SQLAlchemy, что снижает затрату памяти и улучшает производительность.
• MAIL_SERVER: Указывает сервер для отправки электронной почты.
• MAIL_PORT: Определяет порт, используемый для подключения к почтовому серверу.
• MAIL_USE_TLS: Включает использование TLS (Transport Layer Security) для безопасной передачи данных.
• MAIL_USERNAME: Имя пользователя для аутентификации на почтовом сервере, значение извлекается из переменной окружения MAIL_USERNAME.
• MAIL_PASSWORD: Пароль для аутентификации на почтовом сервере, значение извлекается из переменной окружения MAIL_PASSWORD.
• MAIL_DEFAULT_SENDER: Адрес электронной почты по умолчанию, с которого будут отправляться письма, значение извлекается из переменной окружения MAIL_DEFAULT_SENDER.
В данном файле routes.py определяются маршруты (routes) приложения Flask, которые используются для обработки HTTP-запросов. Он обеспечивает функциональность работы с пользователями и позициями, реализуя обработку запросов на получение и создание ресурсов, а также контроль доступа с помощью проверки административных прав текущего пользователя.
from flask import Blueprint, jsonify, request
from flask_login import current_user, login_required
from extensions import db
from models import Position, User
bp = Blueprint("main", __name__)
@bp.route("/users", methods=["GET", "POST"])
@login_required
def users():
if request.method == "GET":
users = User.query.all()
return jsonify([{"id": u.id, "email": u.email} for u in users])
elif request.method == "POST":
if not current_user.is_admin:
return jsonify({"error": "Access denied"}), 403
data = request.json
user = User(
first_name=data["first_name"],
last_name=data["last_name"],
email=data["email"],
)
db.session.add(user)
db.session.commit()
return jsonify({"message": "User created"}), 201
@bp.route("/positions", methods=["GET", "POST"])
@login_required
def positions():
if not current_user.is_admin:
return jsonify({"error": "Access denied"}), 403
if request.method == "GET":
positions = Position.query.all()
return jsonify([{"id": p.id, "name": p.name} for p in positions])
elif request.method == "POST":
data = request.json
position = Position(name=data["name"], score_threshold=data["score_threshold"])
db.session.add(position)
db.session.commit()
return jsonify({"message": "Position created"}), 201
Импортируемые модули
from flask import Blueprint, jsonify, request
from flask_login import current_user, login_required
from extensions import db
from models import Position, User
• Blueprint: Используется для создания модульной структуры приложения Flask.
• jsonify: Функция для преобразования Python-объектов в JSON-формат.
• request: Объект, который содержит данные текущего HTTP-запроса.
• current_user: Представляет текущего аутентифицированного пользователя.
• login_required: Декоратор, который гарантирует, что маршруты доступны только для аутентифицированных пользователей.
• db: Расширенный объект базы данных для работы с SQLAlchemy.
• User и Position: Модели, представляющие пользователей и позиции в базе данных соответственно.
Инициализация Blueprint
bp = Blueprint("main", __name__)
• Создается объект Blueprint с именем "main", который будет использоваться для группировки связанных маршрутов.
Маршрут /users
Метод GET
@bp.route("/users", methods=["GET", "POST"])
@login_required
def users():
if request.method == "GET":
users = User.query.all()
return jsonify([{"id": u.id, "email": u.email} for u in users])
• Этот маршрут обрабатывает запросы для получения списка пользователей. При выполнении GET-запроса возвращается список всех пользователей с их идентификаторами и электронными адресами в формате JSON.
Метод POST
elif request.method == "POST":
if not current_user.is_admin:
return jsonify({"error": "Access denied"}), 403
data = request.json
user = User(
first_name=data["first_name"],
last_name=data["last_name"],
email=data["email"],
)
db.session.add(user)
db.session.commit()
return jsonify({"message": "User created"}), 201
• Этот маршрут позволяет создавать новых пользователей. Доступ к этому маршруту имеют только администраторы. При выполнении POST-запроса требуется передать данные нового пользователя в формате JSON. Если текущий пользователь не является администратором, возвращается ошибка доступа.
Маршрут /positions
Метод GET
@bp.route("/positions", methods=["GET", "POST"])
@login_required
def positions():
if not current_user.is_admin:
return jsonify({"error": "Access denied"}), 403
if request.method == "GET":
positions = Position.query.all()
return jsonify([{"id": p.id, "name": p.name} for p in positions])
• Этот маршрут обрабатывает запросы для получения списка позиций. Доступен только для администраторов. При выполнении GET-запроса возвращает список всех позиций с их идентификаторами и названиями в формате JSON.
Метод POST
elif request.method == "POST":
data = request.json
position = Position(name=data["name"], score_threshold=data["score_threshold"])
db.session.add(position)
db.session.commit()
return jsonify({"message": "Position created"}), 201
• Этот маршрут позволяет создавать новые позиции. При выполнении POST-запроса требуется передать данные о новой позиции в формате JSON. Позволяет добавить название позиции и пороговое значение баллов.
В файле main.py осуществляется настройка и инициализация основного приложения Flask, который представляет собой ядро приложения и отвечает за его функционирование и доступность. Здесь реализована конфигурация для работы с RESTful API, инициализация необходимых расширений, регистрация маршрутов и запуск сервера.
from flask import Flask, render_template
from flask_cors import CORS
from flask_restful import Api
from os import environ
from config import Config
from extensions import db, migrate, login_manager, mail
from resources.basic_resource import BasicResource
from resources.pdf_resource import PDFResource
from resources.employees_resource import EmployeesResource
from resources.criteries_resource import CriteriesResource
from resources.certificates_resource import CertificatesResource
from auth import auth_bp
from routes import bp as main_bp
PORT = environ.get('BACKEND_PORT', 8000)
DEBUG = environ.get('DEBUG', True)
app = Flask(__name__)
app.config.from_object(Config)
cors = CORS(app)
app.config['CORS_HEADERS'] = 'Content-Type'
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 # 5 MB limit for uploads
api = Api(app)
api.add_resource(BasicResource, '/api', '/api/<int:source_id>')
api.add_resource(PDFResource, '/api/media/', '/api/media/<string:filename>')
api.add_resource(EmployeesResource, '/api/employees/', '/api/employees/')
api.add_resource(CriteriesResource, '/api/criteries/', '/api/criteries/')
api.add_resource(CertificatesResource, '/api/certificates/', '/api/certificates/')
# Initialize extensions
db.init_app(app)
migrate.init_app(app, db)
login_manager.init_app(app)
mail.init_app(app)
# Set up login behavior
@login_manager.user_loader
def load_user(user_id):
from backend.models import User
return User.query.get(int(user_id))
login_manager.login_view = "auth.login"
login_manager.login_message = "Please log in to access this page."
# Register blueprints
app.register_blueprint(main_bp)
app.register_blueprint(auth_bp)
@app.route('/', methods=['GET'])
def index():
return render_template('index.html')
PORT = environ.get('BACKEND_PORT', 8000)
DEBUG = environ.get('DEBUG', True)
app.run(extra_files=[], debug=DEBUG, host='0.0.0.0', port=PORT)
Импортируемые модули
from flask import Flask, render_template
from flask_cors import CORS
from flask_restful import Api
from os import environ
from config import Config
from extensions import db, migrate, login_manager, mail
from resources.basic_resource import BasicResource
from resources.pdf_resource import PDFResource
from resources.employees_resource import EmployeesResource
from resources.criteries_resource import CriteriesResource
from resources.certificates_resource import CertificatesResource
from auth import auth_bp
from routes import bp as main_bp
• Flask: Основной класс приложения Flask.
• render_template: Функция для рендеринга HTML-шаблонов.
• CORS: Расширение для настройки кросс-доменных запросов.
• Api: Класс для создания RESTful API с использованием Flask-RESTful.
• environ: Модуль для работы с переменными окружения.
Конфигурация приложения
PORT = environ.get('BACKEND_PORT', 8000)
DEBUG = environ.get('DEBUG', True)
app = Flask(__name__)
app.config.from_object(Config)
• Переменные окружения BACKEND_PORT и DEBUG используются для настройки порта, на котором будет запущен сервер, и режима отладки.
• Конфигурация приложения загружается из класса Config.
Настройка CORS
cors = CORS(app)
app.config['CORS_HEADERS'] = 'Content-Type'
app.config['MAX_CONTENT_LENGTH'] = 5 * 1024 * 1024 # 5 MB limit for uploads
• CORS: Настраивает поддержку кросс-доменных запросов для приложения.
• Устанавливается заголовок Content-Type и ограничение на размер загружаемых данных (5 MB).
Регистрация RESTful ресурсов
api = Api(app)
api.add_resource(BasicResource, '/api', '/api/<int:source_id>')
api.add_resource(PDFResource, '/api/media/', '/api/media/<string:filename>')
api.add_resource(EmployeesResource, '/api/employees/', '/api/employees/')
api.add_resource(CriteriesResource, '/api/criteries/', '/api/criteries/')
api.add_resource(CertificatesResource, '/api/certificates/', '/api/certificates/')
• Создается объект Api, который регистрирует ресурсы с соответствующими маршрутами для обработки запросов.
Инициализация расширений
# Initialize extensions
db.init_app(app)
migrate.init_app(app, db)
login_manager.init_app(app)
mail.init_app(app)
• Расширения (такие как db, migrate, login_manager, и mail) инициализируются с использованием экземпляра приложения app.
Настройка пользовательской аутентификации
@login_manager.user_loader
def load_user(user_id):
from backend.models import User
return User.query.get(int(user_id))
login_manager.login_view = "auth.login"
login_manager.login_message = "Please log in to access this page."
• Функция load_user загружает пользователя из базы данных по его идентификатору.
• Устанавливаются параметры для управления поведением аутентификации.
Регистрация Blueprints
# Register blueprints
app.register_blueprint(main_bp)
app.register_blueprint(auth_bp)
• Регистрируются ранее созданные Blueprint для основной логики приложения (main_bp) и аутентификации (auth_bp).
Главная Страница
@app.route('/', methods=['GET'])
def index():
return render_template('index.html')
• Определяется маршрут для главной страницы, которая рендерит шаблон index.html.
Запуск приложения
PORT = environ.get('BACKEND_PORT', 8000)
DEBUG = environ.get('DEBUG', True)
app.run(extra_files=[], debug=DEBUG, host='0.0.0.0', port=PORT)
• Приложение запускается на заданном порту и в заданном режиме отладки. Установлен хост 0.0.0.0, что позволяет приложению быть доступным извне.
Файл auth.py реализует функционал аутентификации пользователей в приложении Flask и содержит маршруты, необходимые для входа в систему, выхода из нее и сброса пароля через отправку электронных писем.
from flask import Blueprint, jsonify, request, url_for
from flask_login import login_user, logout_user
from flask_mail import Message
from extensions import db, mail
from models import User
auth_bp = Blueprint("auth", __name__)
@auth_bp.route("/login", methods=["POST"])
def login():
data = request.json
user = User.query.filter_by(email=data["email"]).first()
if user and user.check_password(data["password"]):
login_user(user)
return jsonify({"message": "Login successful"}), 200
return jsonify({"error": "Invalid credentials"}), 401
@auth_bp.route("/logout", methods=["POST"])
def logout():
logout_user()
return jsonify({"message": "Logout successful"}), 200
@auth_bp.route("/reset_password", methods=["POST"])
def reset_password():
data = request.json
user = User.query.filter_by(email=data["email"]).first()
if not user:
return jsonify({"error": "User not found"}), 404
token = user.id # Using ID as an example token
reset_url = url_for("auth.complete_reset", token=token, _external=True)
msg = Message("Password Reset Request", recipients=[user.email])
msg.body = f"To reset your password, visit the following link: {reset_url}"
mail.send(msg)
return jsonify({"message": "Reset link sent"}), 200
@auth_bp.route("/reset_password/<token>", methods=["POST"])
def complete_reset(token):
user = User.query.get(int(token))
if not user:
return jsonify({"error": "Invalid token"}), 400
data = request.json
user.set_password(data["password"])
db.session.commit()
return jsonify({"message": "Password reset successful"}), 200
Импортируемые модули
from flask import Blueprint, jsonify, request, url_for
from flask_login import login_user, logout_user
from flask_mail import Message
from extensions import db, mail
from models import User
• Blueprint: Используется для создания модуля аутентификации.
• jsonify: Функция для преобразования данных в JSON-формат.
• request: Объект, представляющий текущий HTTP-запрос.
• url_for: Функция для генерации URL-адресов по имени маршрута.
• login_user, logout_user: Функции для управления сессиями пользователей.
• Message: Класс для создания и отправки электронных писем.
Инициализация Blueprint
auth_bp = Blueprint("main", __name__)
• Создается объект Blueprint с именем "auth", который будет группировать маршруты, относящиеся к аутентификации.
Маршрут /login
@auth_bp.route("/login", methods=["POST"])
def login():
data = request.json
user = User.query.filter_by(email=data["email"]).first()
if user and user.check_password(data["password"]):
login_user(user)
return jsonify({"message": "Login successful"}), 200
return jsonify({"error": "Invalid credentials"}), 401
• Этот маршрут обрабатывает POST-запросы для входа пользователя в систему. Сначала происходит проверка существования пользователя по электронной почте, а затем проверяется правильность введенного пароля. В случае успешной аутентификации, пользователь будет залогинен и получит сообщение об успешном входе. Если данные некорректны, возвращается ошибка 401.
Маршрут /logout
@auth_bp.route("/logout", methods=["POST"])
def logout():
logout_user()
return jsonify({"message": "Logout successful"}), 200
• Этот маршрут обрабатывает POST-запросы для выхода пользователя из системы. Пользователь выйдет из аккаунта, и на ответ будет отправлено сообщение об успешном выходе.
Маршрут /reset_password
@auth_bp.route("/reset_password", methods=["POST"])
def reset_password():
data = request.json
user = User.query.filter_by(email=data["email"]).first()
if not user:
return jsonify({"error": "User not found"}), 404
token = user.id # Using ID as an example token
reset_url = url_for("auth.complete_reset", token=token, _external=True)
msg = Message("Password Reset Request", recipients=[user.email])
msg.body = f"To reset your password, visit the following link: {reset_url}"
mail.send(msg)
return jsonify({"message": "Reset link sent"}), 200
• Этот маршрут обрабатывает запросы на сброс пароля. На основе переданного адреса электронной почты ищется пользователь. Если пользователь не найден, возвращается ошибка 404. Если пользователь существует, создается временный токен (в данном случае используется идентификатор пользователя) и формируется URL для сброса пароля. На указанный адрес электронной почты отправляется сообщение с инструкцией по сбросу пароля.
Маршрут /reset_password/
@auth_bp.route("/reset_password/<token>", methods=["POST"])
def complete_reset(token):
user = User.query.get(int(token))
if not user:
return jsonify({"error": "Invalid token"}), 400
data = request.json
user.set_password(data["password"])
db.session.commit()
return jsonify({"message": "Password reset successful"}), 200
• Этот маршрут обрабатывает запросы для завершения процесса сброса пароля. Проверяется корректность токена (идентификатора пользователя). Если токен некорректный, возвращается ошибка 400. В случае корректного токена, новый пароль устанавливается для пользователя, и изменения сохраняются в базе данных.