Compare commits

...

No commits in common. "master" and "main" have entirely different histories.
master ... main

35 changed files with 228 additions and 541 deletions

226
.gitignore vendored Normal file
View File

@ -0,0 +1,226 @@
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
# ---> VisualStudioCode
.vscode/*
!.vscode/settings.json
!.vscode/tasks.json
!.vscode/launch.json
!.vscode/extensions.json
!.vscode/*.code-snippets
# Local History for Visual Studio Code
.history/
# Built Visual Studio Code Extensions
*.vsix
# ---> Composer
composer.phar
/vendor/
# Commit your application's lock file https://getcomposer.org/doc/01-basic-usage.md#commit-your-composer-lock-file-to-version-control
# You may choose to ignore a library lock file http://getcomposer.org/doc/02-libraries.md#lock-file
# composer.lock
# ---> C++
# Prerequisites
*.d
# Compiled Object files
*.slo
*.lo
*.o
*.obj
# Precompiled Headers
*.gch
*.pch
# Compiled Dynamic libraries
*.so
*.dylib
*.dll
# Fortran module files
*.mod
*.smod
# Compiled Static libraries
*.lai
*.la
*.a
*.lib
# Executables
*.exe
*.out
*.app

2
README.md Normal file
View File

@ -0,0 +1,2 @@
# xray

View File

View File

@ -1,16 +0,0 @@
"""
ASGI config for backend project.
It exposes the ASGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/asgi/
"""
import os
from django.core.asgi import get_asgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
application = get_asgi_application()

View File

@ -1,145 +0,0 @@
"""
Django settings for backend project.
Generated by 'django-admin startproject' using Django 5.1.5.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/5.1/ref/settings/
"""
from pathlib import Path
# Build paths inside the project like this: BASE_DIR / 'subdir'.
BASE_DIR = Path(__file__).resolve().parent.parent
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/5.1/howto/deployment/checklist/
# SECURITY WARNING: keep the secret key used in production secret!
SECRET_KEY = 'django-insecure-z8m_e(v=^%)1f-=hgcl1d7a2%rg_#w$r$ig%x%l8ua_cj(ts)w'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = True
ALLOWED_HOSTS = []
# Application definition
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework', # Django Rest Framework
'orthanc', # Наше приложение
]
MIDDLEWARE = [
'django.middleware.security.SecurityMiddleware',
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
]
ROOT_URLCONF = 'backend.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
],
},
},
]
WSGI_APPLICATION = 'backend.wsgi.application'
# Database
# https://docs.djangoproject.com/en/5.1/ref/settings/#databases
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.postgresql',
'NAME': 'xray',
'USER': 'postgres',
'PASSWORD': '1240069630Bk!',
'HOST': 'localhost',
'PORT': '5432',
}
}
# Password validation
# https://docs.djangoproject.com/en/5.1/ref/settings/#auth-password-validators
AUTH_PASSWORD_VALIDATORS = [
{
'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.MinimumLengthValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.CommonPasswordValidator',
},
{
'NAME': 'django.contrib.auth.password_validation.NumericPasswordValidator',
},
]
# Internationalization
# https://docs.djangoproject.com/en/5.1/topics/i18n/
LANGUAGE_CODE = 'en-us'
TIME_ZONE = 'UTC'
USE_I18N = True
USE_TZ = True
# Static files (CSS, JavaScript, Images)
# https://docs.djangoproject.com/en/5.1/howto/static-files/
STATIC_URL = 'static/'
# Default primary key field type
# https://docs.djangoproject.com/en/5.1/ref/settings/#default-auto-field
DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField'
REST_FRAMEWORK = {
'DEFAULT_AUTHENTICATION_CLASSES': (
'rest_framework_simplejwt.authentication.JWTAuthentication',
),
'DEFAULT_PERMISSION_CLASSES': (
'rest_framework.permissions.IsAuthenticated',
),
}
# Настройки токенов
from datetime import timedelta
SIMPLE_JWT = {
'ACCESS_TOKEN_LIFETIME': timedelta(minutes=30), # Срок действия токена
'REFRESH_TOKEN_LIFETIME': timedelta(days=1),
'AUTH_HEADER_TYPES': ('Bearer',),
}

View File

@ -1,25 +0,0 @@
"""
URL configuration for backend project.
The `urlpatterns` list routes URLs to views. For more information please see:
https://docs.djangoproject.com/en/5.1/topics/http/urls/
Examples:
Function views
1. Add an import: from my_app import views
2. Add a URL to urlpatterns: path('', views.home, name='home')
Class-based views
1. Add an import: from other_app.views import Home
2. Add a URL to urlpatterns: path('', Home.as_view(), name='home')
Including another URLconf
1. Import the include() function: from django.urls import include, path
2. Add a URL to urlpatterns: path('blog/', include('blog.urls'))
"""
from django.contrib import admin
from django.urls import path, include
urlpatterns = [
path('admin/', admin.site.urls),
# path('patients/', include('orthanc.urls')),
path('auth/', include('orthanc.urls')),
]

View File

@ -1,16 +0,0 @@
"""
WSGI config for backend project.
It exposes the WSGI callable as a module-level variable named ``application``.
For more information on this file, see
https://docs.djangoproject.com/en/5.1/howto/deployment/wsgi/
"""
import os
from django.core.wsgi import get_wsgi_application
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
application = get_wsgi_application()

View File

@ -1,22 +0,0 @@
#!/usr/bin/env python
"""Django's command-line utility for administrative tasks."""
import os
import sys
def main():
"""Run administrative tasks."""
os.environ.setdefault('DJANGO_SETTINGS_MODULE', 'backend.settings')
try:
from django.core.management import execute_from_command_line
except ImportError as exc:
raise ImportError(
"Couldn't import Django. Are you sure it's installed and "
"available on your PYTHONPATH environment variable? Did you "
"forget to activate a virtual environment?"
) from exc
execute_from_command_line(sys.argv)
if __name__ == '__main__':
main()

View File

View File

@ -1,3 +0,0 @@
from django.contrib import admin
# Register your models here.

View File

@ -1,6 +0,0 @@
from django.apps import AppConfig
class OrthancConfig(AppConfig):
default_auto_field = 'django.db.models.BigAutoField'
name = 'orthanc'

View File

@ -1,27 +0,0 @@
# Generated by Django 5.1.5 on 2025-01-26 03:23
from django.db import migrations, models
class Migration(migrations.Migration):
initial = True
dependencies = [
]
operations = [
migrations.CreateModel(
name='Patient',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('orthanc_id', models.CharField(max_length=255, unique=True)),
('patient_id', models.CharField(blank=True, max_length=255, null=True)),
('patient_name', models.CharField(blank=True, max_length=255, null=True)),
('patient_sex', models.CharField(blank=True, max_length=10, null=True)),
('patient_birth_date', models.CharField(blank=True, max_length=20, null=True)),
('studies', models.JSONField(blank=True, null=True)),
('patient_metadata', models.JSONField(blank=True, null=True)),
],
),
]

View File

@ -1,39 +0,0 @@
# Generated by Django 5.1.5 on 2025-01-26 08:47
import django.db.models.deletion
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('orthanc', '0001_initial'),
]
operations = [
migrations.CreateModel(
name='Series',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('orthanc_id', models.CharField(max_length=255, unique=True)),
('parent_study', models.CharField(max_length=255)),
('main_dicom_tags', models.JSONField(blank=True, null=True)),
('status', models.CharField(blank=True, max_length=50, null=True)),
('is_stable', models.BooleanField(default=False)),
('last_update', models.CharField(blank=True, max_length=50, null=True)),
('patient', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='series', to='orthanc.patient')),
],
),
migrations.CreateModel(
name='Instance',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('orthanc_id', models.CharField(max_length=255, unique=True)),
('file_size', models.IntegerField(blank=True, null=True)),
('file_uuid', models.CharField(blank=True, max_length=255, null=True)),
('main_dicom_tags', models.JSONField(blank=True, null=True)),
('index_in_series', models.IntegerField(blank=True, null=True)),
('parent_series', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='instances', to='orthanc.series')),
],
),
]

View File

@ -1,36 +0,0 @@
from django.db import models
class Patient(models.Model):
orthanc_id = models.CharField(max_length=255, unique=True)
patient_id = models.CharField(max_length=255, null=True, blank=True)
patient_name = models.CharField(max_length=255, null=True, blank=True)
patient_sex = models.CharField(max_length=10, null=True, blank=True)
patient_birth_date = models.CharField(max_length=20, null=True, blank=True)
studies = models.JSONField(null=True, blank=True) # Массив ID исследований
patient_metadata = models.JSONField(null=True, blank=True)
def __str__(self):
return self.patient_name or "Unknown Patient"
class Series(models.Model):
orthanc_id = models.CharField(max_length=255, unique=True)
parent_study = models.CharField(max_length=255)
patient = models.ForeignKey(Patient, related_name="series", on_delete=models.CASCADE)
main_dicom_tags = models.JSONField(null=True, blank=True)
status = models.CharField(max_length=50, null=True, blank=True)
is_stable = models.BooleanField(default=False)
last_update = models.CharField(max_length=50, null=True, blank=True)
def __str__(self):
return self.main_dicom_tags.get("SeriesDescription", "Unknown Series")
class Instance(models.Model):
orthanc_id = models.CharField(max_length=255, unique=True)
parent_series = models.ForeignKey(Series, related_name="instances", on_delete=models.CASCADE)
file_size = models.IntegerField(null=True, blank=True)
file_uuid = models.CharField(max_length=255, null=True, blank=True)
main_dicom_tags = models.JSONField(null=True, blank=True)
index_in_series = models.IntegerField(null=True, blank=True)
def __str__(self):
return self.main_dicom_tags.get("SOPInstanceUID", "Unknown Instance")

View File

@ -1,7 +0,0 @@
from rest_framework import serializers
from .models import Patient
class PatientSerializer(serializers.ModelSerializer):
class Meta:
model = Patient
fields = "__all__"

View File

@ -1,108 +0,0 @@
import requests
from urllib3.exceptions import InsecureRequestWarning
from .models import Patient, Series, Instance
# Отключение предупреждений о небезопасном соединении
requests.packages.urllib3.disable_warnings(InsecureRequestWarning)
ORTHANC_BASE_URL = "http://192.168.2.60:8042"
ORTHANC_USERNAME = "writehost"
ORTHANC_PASSWORD = "writehost"
# 1. Получение всех пациентов
def fetch_patients():
url = f"{ORTHANC_BASE_URL}/patients"
response = requests.get(url, auth=(ORTHANC_USERNAME, ORTHANC_PASSWORD))
response.raise_for_status()
return response.json()
# 2. Получение информации о конкретном пациенте
def fetch_patient_details(orthanc_id):
url = f"{ORTHANC_BASE_URL}/patients/{orthanc_id}"
response = requests.get(url, auth=(ORTHANC_USERNAME, ORTHANC_PASSWORD))
response.raise_for_status()
return response.json()
# 3. Получение данных об исследовании
def fetch_study_details(study_id):
url = f"{ORTHANC_BASE_URL}/studies/{study_id}"
response = requests.get(url, auth=(ORTHANC_USERNAME, ORTHANC_PASSWORD))
response.raise_for_status()
return response.json()
# 4. Получение данных о серии
def fetch_series_details(series_id):
url = f"{ORTHANC_BASE_URL}/series/{series_id}"
response = requests.get(url, auth=(ORTHANC_USERNAME, ORTHANC_PASSWORD))
response.raise_for_status()
return response.json()
# 5. Получение данных об изображении (инстансе)
def fetch_instance_details(instance_id):
url = f"{ORTHANC_BASE_URL}/instances/{instance_id}"
response = requests.get(url, auth=(ORTHANC_USERNAME, ORTHANC_PASSWORD))
response.raise_for_status()
return response.json()
# Основная синхронизация
def sync_patients():
patient_ids = fetch_patients()
for patient_id in patient_ids:
patient_data = fetch_patient_details(patient_id)
# Синхронизация пациента
patient, created = Patient.objects.update_or_create(
orthanc_id=patient_id,
defaults={
"patient_id": patient_data["MainDicomTags"].get("PatientID"),
"patient_name": patient_data["MainDicomTags"].get("PatientName"),
"patient_sex": patient_data["MainDicomTags"].get("PatientSex"),
"patient_birth_date": patient_data["MainDicomTags"].get("PatientBirthDate"),
"studies": patient_data.get("Studies", []),
"patient_metadata": patient_data.get("Labels", []),
},
)
# Синхронизация исследований (Studies)
for study_id in patient_data.get("Studies", []):
sync_study(study_id, patient)
# Синхронизация исследования
def sync_study(study_id, patient):
study_data = fetch_study_details(study_id)
for series_id in study_data.get("Series", []):
sync_series(series_id, patient)
# Синхронизация серии
def sync_series(series_id, patient):
series_data = fetch_series_details(series_id)
series, created = Series.objects.update_or_create(
orthanc_id=series_id,
defaults={
"parent_study": series_data["ParentStudy"],
"patient": patient,
"main_dicom_tags": series_data.get("MainDicomTags", {}),
"status": series_data.get("Status"),
"is_stable": series_data.get("IsStable", False),
"last_update": series_data.get("LastUpdate"),
},
)
# Синхронизация снимков (Instances)
for instance_id in series_data.get("Instances", []):
sync_instance(instance_id, series)
# Синхронизация снимка
def sync_instance(instance_id, series):
instance_data = fetch_instance_details(instance_id)
Instance.objects.update_or_create(
orthanc_id=instance_id,
defaults={
"parent_series": series,
"file_size": instance_data.get("FileSize"),
"file_uuid": instance_data.get("FileUuid"),
"main_dicom_tags": instance_data.get("MainDicomTags", {}),
"index_in_series": instance_data.get("IndexInSeries"),
},
)

View File

@ -1,3 +0,0 @@
from django.test import TestCase
# Create your tests here.

View File

@ -1,12 +0,0 @@
from django.urls import path
from .views import PatientListView, SyncPatientsView, ProtectedView
from rest_framework_simplejwt.views import TokenObtainPairView, TokenRefreshView
urlpatterns = [
path('token/', TokenObtainPairView.as_view(), name='token_obtain_pair'), # Получение токена
path('token/refresh/', TokenRefreshView.as_view(), name='token_refresh'), # Обновление токена
path('', PatientListView.as_view(), name='patient-list'),
path('sync/', SyncPatientsView.as_view(), name='sync-patients'),
path('protected/', ProtectedView.as_view(), name='protected'),
]

View File

@ -1,76 +0,0 @@
# from rest_framework.views import APIView
# from rest_framework.response import Response
# from rest_framework import status
# from .models import Patient
# from .serializers import PatientSerializer
# from .tasks import fetch_patients, fetch_patient_details, sync_patients
#
#
# class PatientListView(APIView):
# def get(self, request):
# patients = Patient.objects.all()
# serializer = PatientSerializer(patients, many=True)
# return Response(serializer.data)
#
# class SyncPatientsView(APIView):
# def post(self, request):
# try:
# patient_ids = fetch_patients()
# for orthanc_id in patient_ids:
# patient_data = fetch_patient_details(orthanc_id)
# patient, created = Patient.objects.update_or_create(
# orthanc_id=orthanc_id,
# defaults={
# "patient_id": patient_data["MainDicomTags"].get("PatientID"),
# "patient_name": patient_data["MainDicomTags"].get("PatientName"),
# "patient_sex": patient_data["MainDicomTags"].get("PatientSex"),
# "patient_birth_date": patient_data["MainDicomTags"].get("PatientBirthDate"),
# "studies": patient_data.get("Studies", []),
# "patient_metadata": patient_data,
# },
# )
# return Response({"message": "Synchronization completed successfully"})
# except Exception as e:
# return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
#
# class SyncPatientsView(APIView):
# def post(self, request):
# try:
# sync_patients()
# return Response({"detail": "Synchronization completed successfully."}, status=status.HTTP_200_OK)
# except Exception as e:
# return Response({"detail": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework import status
from .models import Patient
from .serializers import PatientSerializer
from .tasks import fetch_patients, fetch_patient_details, sync_patients
from rest_framework.permissions import IsAuthenticated
class ProtectedView(APIView):
permission_classes = [IsAuthenticated]
def get(self, request):
return Response({"message": f"Привет, {request.user.username}! Вы авторизованы."})
# Эндпоинт для получения списка пациентов
class PatientListView(APIView):
def get(self, request):
patients = Patient.objects.all()
serializer = PatientSerializer(patients, many=True)
return Response(serializer.data)
# Эндпоинт для синхронизации данных с Orthanc
class SyncPatientsView(APIView):
def post(self, request):
try:
sync_patients()
return Response({"detail": "Synchronization completed successfully."}, status=status.HTTP_200_OK)
except Exception as e:
return Response({"error": str(e)}, status=status.HTTP_500_INTERNAL_SERVER_ERROR)