Skip to content

Latest commit

 

History

History
459 lines (362 loc) · 25.5 KB

File metadata and controls

459 lines (362 loc) · 25.5 KB

RBTM-Web

Веб-интерфейс системы управления рентгеновским томографом. Django-приложение, выступающее в роли фронтенда и координатора для двух независимых микросервисов: storage (хранение данных экспериментов) и experiment (управление томографом).

Стек технологий

Компонент Версия
Python 3.12
Django 5.2
Django REST Framework 3.15.2
PostgreSQL 16
psycopg2-binary 2.9.9
pymemcache 4.0.0
numpy 1.26.4
Bootstrap 3 (django-bootstrap3 23.6)

Архитектура

┌─────────────────────────────────────────────────────────┐
│                     rbtm-web (этот репозиторий)         │
│                                                         │
│  Django 5.2 + Apache2/mod_wsgi + PostgreSQL             │
│                                                         │
│  ┌──────────┐  ┌──────────────┐  ┌──────────────────┐  │
│  │   main   │  │  experiment  │  │     storage      │  │
│  │  (auth)  │  │  (томограф)  │  │  (эксперименты)  │  │
│  └──────────┘  └──────┬───────┘  └────────┬─────────┘  │
└─────────────────────────────────────────────────────────┘
                         │                  │
                         ▼                  ▼
              ┌─────────────────┐  ┌────────────────────┐
              │ Experiment API  │  │   Storage API      │
              │ localhost:5001  │  │   localhost:5006   │
              └─────────────────┘  └────────────────────┘
  • main — аутентификация, профили пользователей, система ролей, запросы на смену роли
  • experiment — управление томографом: настройка, запуск/остановка экспериментов, интерфейс оборудования. Проксирует команды в Experiment API (HTTP)
  • storage — просмотр результатов экспериментов (HDF5-файлы), поиск по метаданным, визуализация кадров. Проксирует запросы в Storage API (HTTP)
  • reconstruction — студия реконструкции (/studio/<exp_id>/): страница и прокси /studio/api/* по белому списку к recon-service из rbtm-recon (токен RECON_TOKEN). Выкатка и проверка — docs/STUDIO-DEPLOY.md

Структура проекта

rbtm-web/
├── Dockerfile                  # python:3.12-slim + Apache2 + mod_wsgi
├── docker-compose.yml          # web + postgres:16
├── requirements.txt
├── 000-default.conf            # конфигурация Apache VirtualHost
├── apache2-foreground          # entrypoint для Docker
└── robotom/                    # Django-проект
    ├── manage.py
    ├── logs/                   # логи приложения
    ├── robotom/                # пакет настроек проекта
    │   ├── urls.py             # корневой URL-роутер
    │   ├── wsgi.py
    │   ├── utils.py            # общие утилиты (force_https и др.)
    │   ├── settings.py         # production (в git; секреты — из .env)
    │   ├── dev_settings.py     # настройки для разработки и общая часть production
    │   └── bamboo_settings.py  # настройки для CI (SQLite, наследует dev_settings)
    ├── main/                   # приложение: аутентификация и профили
    │   ├── models.py           # UserProfile, RoleRequest
    │   ├── views.py            # регистрация, вход, профиль, управление ролями
    │   ├── forms.py
    │   ├── admin.py
    │   ├── urls.py
    │   ├── migrations/
    │   ├── static/             # CSS, Bootstrap 3, изображения
    │   └── templates/
    │       ├── base.html       # базовый шаблон
    │       └── main/           # index, profile, manage_requests, role_request, done, empty
    ├── experiment/             # приложение: управление томографом
    │   ├── apps.py             # ExperimentConfig: создаёт Tomograph при первом запуске
    │   ├── models.py           # Tomograph (state: unavailable/ready/experiment)
    │   ├── views.py            # experiment_view, adjustment, interface, tomograph
    │   ├── admin.py
    │   ├── urls.py
    │   ├── migrations/
    │   ├── static/experiment/  # JS (jQuery, my_scripts), CSS
    │   └── templates/experiment/ # start, adjustment, interface
    ├── storage/                # приложение: просмотр результатов
    │   ├── views.py            # storage_view, storage_record_view, frames_downloading, delete_experiment
    │   ├── urls.py
    │   ├── static/storage/     # Three.js, bootstrap-image-gallery, storage_index.js
    │   └── templates/storage/  # storage_index, storage_record
    └── templates/              # глобальные шаблоны
        └── registration/       # login, logout, password_reset_*, registration_form

Модели данных

main.UserProfile

Расширяет стандартного User через OneToOneField. Поля:

  • full_name, gender, phone_number, address, work_place, degree, title
  • Флаги ролей: is_guest (по умолчанию), is_admin, is_experimentator, is_researcher
  • activation_key — для подтверждения email при регистрации

main.RoleRequest

Запрос пользователя на присвоение роли (ADM / EXP / RES). Связан с UserProfile через ForeignKey.

experiment.Tomograph

Состояние томографа: unavailable / ready / experiment. Один экземпляр на установку. Создаётся автоматически при первом запуске через сигнал post_migrate в ExperimentConfig.ready().

Форматы эксперимента

Простой режим (mode=simple)

В UI задаются: единое количество dark/empty, единая экспозиция, количество угловых позиций, угловой шаг, кадров на позицию.

Параметры, отправляемые в rbtm-drivers-next:

{
  "exp_id": "uuid",
  "specimen": "название",
  "tags": "тег1, тег2",
  "experiment parameters": {
    "advanced": false,
    "DARK":  { "count": 10,  "exposure": 3000.0 },
    "EMPTY": { "count": 10,  "exposure": 3000.0 },
    "DATA":  { "step count": 500, "exposure": 3000.0, "angle step": 0.36, "count per step": 1 }
  }
}

Последовательность кадров:

[dark × N] → [empty × N] → [data × step_count × cps]

Продвинутый режим (mode=advanced)

В UI задаются: единая экспозиция, длина серии dark/empty, периодичность вставки empty, количество data-позиций, угловой шаг, кадров на позицию.

Параметры, отправляемые в rbtm-drivers-next:

{
  "exp_id": "uuid",
  "specimen": "название",
  "tags": "тег1, тег2",
  "experiment parameters": {
    "advanced":            true,
    "exposure":            3000.0,
    "series_length":       10,
    "data_total":          500,
    "data_angle_step":     0.36,
    "data_count_per_step": 1,
    "empty_period":        50
  }
}

Последовательность кадров:

[dark × series_length]
[empty × series_length]
─ повтор для каждой угловой позиции ─────────────────────────────
  [data × data_count_per_step]
  каждые empty_period позиций (кроме последней):
    [empty × series_length]
    [data_check × data_count_per_step]  ← тот же угол, для контроля дрейфа
──────────────────────────────────────────────────────────────────

Визуализация (data_total=150, empty_period=50, series_length=5):

[dark×5][empty×5][data×50][empty×5][chk][data×50][empty×5][chk][data×50]

Мониторинг выполнения

На странице /experiment/interface/ отображается блок статуса в реальном времени (поллинг каждые 2 сек):

  • Bootstrap прогресс-бар
  • Цветной таймлайн сегментов (dark / empty / data / data_check)
  • Оценка оставшегося времени (ETA)
  • Превью последнего кадра из Storage (поллинг каждые 10 сек)
  • Кнопка "Закончить эксперимент" автоматически блокируется по завершении

URL-маршруты

Префикс Приложение Namespace
/ main (→ redirect на /storage/) main
/experiment/ experiment (→ redirect на /experiment/interface/) experiment
/experiment/control/ Управление томографом (источник + юстировка) experiment:index_control
/experiment/source/state/ Состояние источника (JSON, AJAX-поллинг) experiment:source_state
/experiment/interface/ Запуск эксперимента experiment:index_interface
/experiment/status/ Прокси к статусу (JSON) experiment:status
/experiment/storage-preview/ PNG превью из Storage для мониторинга experiment:storage_preview
/storage/ storage storage
/studio/<exp_id>/ Студия реконструкции (ADM/EXP/RES; запуск — ADM/EXP) reconstruction:studio
/studio/api/<path> Прокси к recon-service по белому списку (JSON и бинарные ответы, файлы потоком) reconstruction:api
/admin/ Django Admin —
/accounts/ django.contrib.auth —

Настройки

dev_settings.py — для локальной разработки

Параметр Описание
SECRET_KEY Секретный ключ Django
DEBUG True в dev, False в production
TOMO_NUM Номер томографа (суффикс в URL Experiment API, по умолчанию 1)
ALLOWED_HOSTS Реальный домен / IP в production
DATABASES.HOST localhost в dev; в production settings.py берёт хост из переменной окружения
STORAGE_HOST URL Storage API (default: http://localhost:5006/)
STORAGE_PUBLIC_HOST Публичный (доступный из браузера) адрес Storage; пусто → относительная ссылка через rbtm-proxy (default: '')
EXPERIMENT_HOST URL Experiment API (default: http://localhost:5001/)
TIMEOUT_DEFAULT Таймаут HTTP-запросов к внешним API, секунды (default: 120)
RECON_SERVICE_URL URL recon-service студии реконструкции (env, default: http://localhost:5560/; в production — http://web_reconstructor_1:5560/)
RECON_TOKEN Общий секрет с recon-service (env, из .env рядом с docker-compose.yml; пусто — прокси студии отвечает 503)
EMAIL_* Настройки SMTP для отправки писем активации
CACHES PyMemcacheCache — в dev отключён (DummyCache)

STORAGE_HDF5_FILE и STORAGE_FRAMES_PNG: это два разных случая, хотя оба указывают на nginx внутри контейнера rbtm-storage (порт 5006), а не на Flask-приложение — выделенных Flask-роутов для этих файлов нет.

  • STORAGE_FRAMES_PNG скачивает сам Django (requests.get на сервере) и лишь потом отдаёт PNG браузеру, поэтому внутренний адрес докер-сети STORAGE_HOST тут годится — ссылка всегда собирается через urljoin(STORAGE_HOST, ...), как и остальные STORAGE_*-адреса.
  • STORAGE_HDF5_FILE, наоборот, отдаётся в шаблоне как прямая ссылка <a href> — по ней переходит браузер пользователя. В production STORAGE_HOST указывает на внутренний адрес докер-сети (http://rbtmstorage_server_1:5006/), недоступный снаружи, поэтому собирать эту ссылку через urljoin(STORAGE_HOST, ...) нельзя. Вместо этого используется STORAGE_PUBLIC_HOST.rstrip('/') + '/storage/experiments/{exp_id}.h5': при пустом STORAGE_PUBLIC_HOST (по умолчанию) получается относительный путь /storage/experiments/{exp_id}.h5, который снаружи обслуживает rbtm-proxy (proxy_nginx.conf: location ~ ^/storage/experiments/(?<exp>[-\w]+).h5$, rewrite на :5006); при заданном STORAGE_PUBLIC_HOST получается абсолютный публичный адрес.

bamboo_settings.py — для CI

Наследует все настройки из dev_settings.py через from .dev_settings import *, переопределяет только:

  • DATABASES → SQLite

settings.py — production

robotom/robotom/settings.py хранится в git. Он импортирует dev_settings.py (общая часть: приложения, шаблоны, логи, маршруты Experiment/Storage API) и переопределяет только отличия production. Секреты и серверные значения берутся из окружения контейнера: файл .env рядом с docker-compose.yml подключается к сервису server через env_file (образец — .env.example; .env не в git и не попадает в образ).

Переменная .env Обязательна Умолчание в settings.py
SECRET_KEY да — (пусто: Django не обслуживает запросы; сборке образа не нужен)
EMAIL_HOST_PASSWORD да —
RECON_TOKEN да — (тот же, что в rbtm-recon/web/.env; пусто — студия отвечает 503)
ALLOWED_HOSTS, CSRF_TRUSTED_ORIGINS нет домены и IP нынешней установки (список через запятую)
EXPERIMENT_HOST нет http://10.0.6.86:5001/
STORAGE_HOST, STORAGE_PUBLIC_HOST нет http://rbtmstorage_server_1:5006/, пусто
RECONSTRUCTION_HOST, RECON_SERVICE_URL нет http://10.0.7.153:5550/, http://web_reconstructor_1:5560/
DB_HOST, DB_NAME, DB_USER, DB_PASSWORD, DB_PORT нет database, robotom_users, postgres, postgres, пусто
EMAIL_HOST, EMAIL_PORT, EMAIL_USE_TLS, EMAIL_HOST_USER, DEFAULT_FROM_EMAIL нет Gmail SMTP, 587, TLS, ящик проекта
DJANGO_DEBUG нет false

Адреса сервисов (*_HOST) dev_settings.py читает из окружения, потому что от них строятся все маршруты API; settings.py задаёт production-умолчания до импорта общей части. Хэшеры паролей включают MD5PasswordHasher — для старых учётных записей. Необязательный robotom/local_settings.py (вне git) подключается последним — для срочных правок на сервере.

Переход со старого (не из git) settings.py — один раз на сервере, до git pull:

mv robotom/robotom/settings.py robotom/robotom/settings.py.bak   # иначе pull откажется перезаписать файл
git pull --ff-only
python3 tools/settings_to_env.py robotom/robotom/settings.py.bak >> .env   # секреты — в файл, не на экран
chmod 600 .env

tools/settings_to_env.py переносит SECRET_KEY, EMAIL_HOST_PASSWORD и те значения старого файла, которые отличаются от умолчаний нового settings.py; в терминал секреты не печатает. RECON_TOKEN добавить отдельно.

Запуск

Docker (рекомендуется)

docker-compose up --build -d

Поднимает два контейнера:

  • rbtmweb_server_1 — Django 5.2 + Apache2 + mod_wsgi (Python 3.12)
  • rbtmweb_database_1 — PostgreSQL 16

Важно: server не стартует до готовности database (healthcheck).

После первого запуска применить миграции:

# Если база не создалась автоматически (POSTGRES_DB работает только при первой инициализации пустого тома)
docker exec rbtmweb_database_1 psql -U postgres -c "CREATE DATABASE robotom_users;"

docker-compose exec server python robotom/manage.py migrate --fake-initial

Локальная разработка

pip install -r requirements.txt

# Применить миграции
python robotom/manage.py migrate --settings=robotom.dev_settings

# Запустить сервер
python robotom/manage.py runserver --settings=robotom.dev_settings

Или через переменную окружения:

# Unix
export DJANGO_SETTINGS_MODULE=robotom.dev_settings
# Windows CMD
set DJANGO_SETTINGS_MODULE=robotom.dev_settings

python robotom/manage.py runserver

Тесты

python robotom/manage.py test main --settings=robotom.dev_settings
python robotom/manage.py test storage --settings=robotom.dev_settings
python robotom/manage.py test experiment --settings=robotom.dev_settings

Примечание: Тесты experiment и storage проверяют только страницы Django и не требуют запущенных внешних API-сервисов.

Статика и медиа

python robotom/manage.py collectstatic --settings=robotom.dev_settings
  • STATIC_ROOT → robotom/static/ (собирается при сборке Docker-образа)
  • MEDIA_ROOT → robotom/media/ (PNG-кадры, кешируемые из Storage API)
  • RECONSTRUCTION_ROOT → robotom/media/reconstructions/ (3D-реконструкции)

Медиафайлы генерируются динамически: при открытии страницы эксперимента Django скачивает PNG-кадры из rbtmstorage_server_1 и кеширует их в MEDIA_ROOT. При повторном обращении файл берётся из кеша (проверяется os.path.exists).

В Docker медиафайлы монтируются с хоста, чтобы переживать пересборки образа:

volumes:
  - /home/robotom/rbtm_data/rbtm_web/media:/var/www/web/robotom/media

Права на директорию (www-data = uid 33 в Debian):

sudo chown -R 33:33 /home/robotom/rbtm_data/rbtm_web/media
sudo chmod -R 775 /home/robotom/rbtm_data/rbtm_web/media

Права пользователей

Роль Код Доступ
Гость GST Просмотр хранилища (/storage/)
Исследователь RES Просмотр хранилища
Экспериментатор EXP Управление томографом (/experiment/)
Администратор ADM Всё + управление запросами на роли

Новый пользователь регистрируется с подтверждением по email, получает роль Гость. Для смены роли подаёт RoleRequest — администратор принимает или отклоняет запрос.

Миграция данных со старой базы (PostgreSQL 9.4 → 16)

Контекст

Старая база работала под Django 1.8 / PostgreSQL 9.4. Схема таблиц приложений (main, experiment) практически не изменилась. Поэтому дамп данных можно перелить напрямую.

Важно: PostgreSQL 16 не может читать файлы данных от PostgreSQL 9.4 — они физически несовместимы. Если том /home/robotom/rbtm_data/rbtm_web/db содержит старые данные, контейнер postgres:16 будет падать с ошибкой:

FATAL: database files are incompatible with server
DETAIL: The data directory was initialized by PostgreSQL version 9.4

Необходимо сначала сделать логический дамп (SQL), очистить том, и восстановить данные в postgres:16.


Полный сценарий миграции (рекомендуется)

Шаг 1. Остановить новый стек (если уже запущен)

docker-compose down

Шаг 2. Сделать дамп через временный postgres:9.4

docker run --rm \
  -v /home/robotom/rbtm_data/rbtm_web/db:/var/lib/postgresql/data \
  -e POSTGRES_PASSWORD=postgres \
  --name pg94_temp \
  -d postgres:9.4

sleep 5

docker exec pg94_temp pg_dump -U postgres robotom_users > robotom_backup.sql
docker stop pg94_temp

Шаг 3. Очистить том от старых файлов

# ВНИМАНИЕ: это удаляет все файлы PostgreSQL 9.4 из тома
sudo rm -rf /home/robotom/rbtm_data/rbtm_web/db/*

Шаг 4. Поднять новый стек

docker-compose up --build -d

Шаг 5. Восстановить дамп в postgres:16

docker exec -i rbtmweb_database_1 psql -U postgres -d robotom_users < robotom_backup.sql

Шаг 6. Применить миграции Django 5.2

docker-compose exec server python robotom/manage.py migrate --fake-initial

--fake-initial помечает 0001_initial-миграции как выполненные без создания таблиц — только если таблицы уже существуют в базе.

Шаг 7. Проверка

# Все миграции должны быть [X]
docker-compose exec server python robotom/manage.py showmigrations

# Системная проверка Django
docker-compose exec server python robotom/manage.py check

Возможные проблемы

Проблема Причина Решение
database files are incompatible with server Том содержит данные postgres:9.4, а контейнер postgres:16 Выполнить полный сценарий выше (дамп → очистить том → восстановить)
relation already exists Таблица есть в дампе и в новой базе Использовать --fake-initial при migrate
column ... does not exist В Django 5.2 добавились поля в системные таблицы Запустить migrate без --fake-initial
Пользователи не могут войти Старые хэши MD5/SHA1 Отправить ссылку сброса пароля через /accounts/password_reset/
UnicodeDecodeError при восстановлении Дамп не в UTF-8 Добавить --encoding=UTF8 при pg_dump
pg_dump: server version mismatch Версия pg_dump не совпадает с версией сервера Использовать временный контейнер postgres:9.4 (шаг 2)