Compare commits

...

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

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)