Веб-интерфейс системы управления рентгеновским томографом. 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
Расширяет стандартного User через OneToOneField. Поля:
full_name,gender,phone_number,address,work_place,degree,title- Флаги ролей:
is_guest(по умолчанию),is_admin,is_experimentator,is_researcher activation_key— для подтверждения email при регистрации
Запрос пользователя на присвоение роли (ADM / EXP / RES). Связан с UserProfile через ForeignKey.
Состояние томографа: unavailable / ready / experiment. Один экземпляр на установку.
Создаётся автоматически при первом запуске через сигнал post_migrate в ExperimentConfig.ready().
В 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]
В 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 сек)
- Кнопка "Закончить эксперимент" автоматически блокируется по завершении
| Префикс | Приложение | 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 |
— |
| Параметр | Описание |
|---|---|
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>— по ней переходит браузер пользователя. В productionSTORAGE_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получается абсолютный публичный адрес.
Наследует все настройки из dev_settings.py через from .dev_settings import *,
переопределяет только:
DATABASES→ SQLite
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 .envtools/settings_to_env.py переносит SECRET_KEY, EMAIL_HOST_PASSWORD и те значения старого файла, которые
отличаются от умолчаний нового settings.py; в терминал секреты не печатает. RECON_TOKEN добавить отдельно.
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-initialpip 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 runserverpython 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_settingsSTATIC_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 — администратор принимает или отклоняет запрос.
Старая база работала под 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.
docker-compose downdocker 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# ВНИМАНИЕ: это удаляет все файлы PostgreSQL 9.4 из тома
sudo rm -rf /home/robotom/rbtm_data/rbtm_web/db/*docker-compose up --build -ddocker exec -i rbtmweb_database_1 psql -U postgres -d robotom_users < robotom_backup.sqldocker-compose exec server python robotom/manage.py migrate --fake-initial
--fake-initialпомечает0001_initial-миграции как выполненные без создания таблиц — только если таблицы уже существуют в базе.
# Все миграции должны быть [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) |