576 lines
17 KiB
Python
576 lines
17 KiB
Python
"""
|
|
Radiology app admin configuration.
|
|
"""
|
|
|
|
from django.contrib import admin
|
|
from django.utils.html import format_html
|
|
from django.urls import reverse
|
|
from django.utils.safestring import mark_safe
|
|
from .models import (
|
|
ImagingStudy, ImagingSeries, DICOMImage,
|
|
RadiologyReport, ReportTemplate, ImagingOrder
|
|
)
|
|
|
|
|
|
class ImagingSeriesInline(admin.TabularInline):
|
|
"""
|
|
Inline admin for imaging series.
|
|
"""
|
|
model = ImagingSeries
|
|
extra = 0
|
|
fields = [
|
|
'series_number', 'modality', 'series_description',
|
|
'number_of_instances', 'series_size', 'image_quality'
|
|
]
|
|
readonly_fields = ['number_of_instances', 'series_size']
|
|
|
|
|
|
class DICOMImageInline(admin.TabularInline):
|
|
"""
|
|
Inline admin for DICOM images.
|
|
"""
|
|
model = DICOMImage
|
|
extra = 0
|
|
fields = [
|
|
'instance_number', 'rows', 'columns',
|
|
'file_size', 'image_quality', 'processed'
|
|
]
|
|
readonly_fields = ['file_size']
|
|
|
|
|
|
@admin.register(ImagingStudy)
|
|
class ImagingStudyAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for imaging studies.
|
|
"""
|
|
list_display = [
|
|
'accession_number', 'patient_name', 'modality',
|
|
'study_description', 'study_datetime', 'status',
|
|
'priority', 'radiologist', 'number_of_series'
|
|
]
|
|
list_filter = [
|
|
'tenant', 'modality', 'status', 'priority',
|
|
'study_datetime', 'image_quality', 'completion_status'
|
|
]
|
|
search_fields = [
|
|
'accession_number', 'study_instance_uid',
|
|
'patient__first_name', 'patient__last_name',
|
|
'patient__mrn', 'study_description'
|
|
]
|
|
readonly_fields = [
|
|
'study_id', 'accession_number', 'study_datetime',
|
|
'number_of_series', 'number_of_instances', 'study_size',
|
|
'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Study Information', {
|
|
'fields': [
|
|
'study_id', 'study_instance_uid', 'accession_number',
|
|
'tenant', 'patient'
|
|
]
|
|
}),
|
|
('Study Details', {
|
|
'fields': [
|
|
'modality', 'study_description', 'body_part', 'study_datetime'
|
|
]
|
|
}),
|
|
('Clinical Information', {
|
|
'fields': [
|
|
'clinical_indication', 'clinical_history', 'diagnosis_code',
|
|
'referring_physician', 'radiologist'
|
|
]
|
|
}),
|
|
('Status and Priority', {
|
|
'fields': [
|
|
'status', 'priority', 'image_quality', 'completion_status'
|
|
]
|
|
}),
|
|
('Technical Parameters', {
|
|
'fields': [
|
|
'kvp', 'mas', 'exposure_time', 'slice_thickness'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Equipment Information', {
|
|
'fields': [
|
|
'station_name', 'manufacturer', 'model_name'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Study Metrics', {
|
|
'fields': [
|
|
'number_of_series', 'number_of_instances', 'study_size'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('PACS Information', {
|
|
'fields': [
|
|
'pacs_location', 'archived', 'archive_location'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Related Information', {
|
|
'fields': [
|
|
'encounter', 'imaging_order'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
inlines = [ImagingSeriesInline]
|
|
date_hierarchy = 'study_datetime'
|
|
|
|
def patient_name(self, obj):
|
|
"""Display patient name."""
|
|
return obj.patient.get_full_name()
|
|
patient_name.short_description = 'Patient'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(tenant=request.user.tenant)
|
|
return qs.select_related('patient', 'radiologist', 'referring_physician')
|
|
|
|
|
|
@admin.register(ImagingSeries)
|
|
class ImagingSeriesAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for imaging series.
|
|
"""
|
|
list_display = [
|
|
'series_number', 'study_accession', 'modality',
|
|
'series_description', 'series_datetime', 'number_of_instances',
|
|
'image_quality'
|
|
]
|
|
list_filter = [
|
|
'modality', 'series_datetime', 'image_quality',
|
|
'patient_position', 'contrast_route'
|
|
]
|
|
search_fields = [
|
|
'series_instance_uid', 'series_description',
|
|
'study__accession_number', 'study__patient__first_name',
|
|
'study__patient__last_name'
|
|
]
|
|
readonly_fields = [
|
|
'series_id', 'series_datetime', 'number_of_instances',
|
|
'series_size', 'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Series Information', {
|
|
'fields': [
|
|
'series_id', 'series_instance_uid', 'series_number',
|
|
'study'
|
|
]
|
|
}),
|
|
('Series Details', {
|
|
'fields': [
|
|
'modality', 'series_description', 'protocol_name',
|
|
'series_date', 'series_time', 'series_datetime'
|
|
]
|
|
}),
|
|
('Technical Parameters', {
|
|
'fields': [
|
|
'slice_thickness', 'spacing_between_slices',
|
|
'pixel_spacing', 'image_orientation'
|
|
]
|
|
}),
|
|
('Body Part and Position', {
|
|
'fields': [
|
|
'body_part', 'patient_position'
|
|
]
|
|
}),
|
|
('Contrast Information', {
|
|
'fields': [
|
|
'contrast_agent', 'contrast_route'
|
|
]
|
|
}),
|
|
('Series Metrics', {
|
|
'fields': [
|
|
'number_of_instances', 'series_size', 'image_quality'
|
|
]
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
inlines = [DICOMImageInline]
|
|
|
|
def study_accession(self, obj):
|
|
"""Display study accession number."""
|
|
return obj.study.accession_number
|
|
study_accession.short_description = 'Study'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(study__tenant=request.user.tenant)
|
|
return qs.select_related('study__patient')
|
|
|
|
|
|
@admin.register(DICOMImage)
|
|
class DICOMImageAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for DICOM images.
|
|
"""
|
|
list_display = [
|
|
'instance_number', 'series_info', 'sop_class_uid',
|
|
'rows', 'columns', 'file_size_mb', 'image_quality',
|
|
'processed', 'archived'
|
|
]
|
|
list_filter = [
|
|
'sop_class_uid', 'image_quality', 'processed',
|
|
'archived', 'content_date'
|
|
]
|
|
search_fields = [
|
|
'sop_instance_uid', 'series__series_instance_uid',
|
|
'series__study__accession_number'
|
|
]
|
|
readonly_fields = [
|
|
'image_id', 'file_size_mb', 'acquisition_datetime',
|
|
'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Image Information', {
|
|
'fields': [
|
|
'image_id', 'sop_instance_uid', 'instance_number',
|
|
'series'
|
|
]
|
|
}),
|
|
('Image Details', {
|
|
'fields': [
|
|
'image_type', 'sop_class_uid'
|
|
]
|
|
}),
|
|
('Image Dimensions', {
|
|
'fields': [
|
|
'rows', 'columns', 'bits_allocated', 'bits_stored'
|
|
]
|
|
}),
|
|
('Image Position and Orientation', {
|
|
'fields': [
|
|
'image_position', 'image_orientation', 'slice_location'
|
|
]
|
|
}),
|
|
('Window/Level Settings', {
|
|
'fields': [
|
|
'window_center', 'window_width'
|
|
]
|
|
}),
|
|
('File Information', {
|
|
'fields': [
|
|
'file_path', 'file_size', 'file_size_mb',
|
|
'transfer_syntax_uid'
|
|
]
|
|
}),
|
|
('Content Information', {
|
|
'fields': [
|
|
'content_date', 'content_time', 'acquisition_datetime'
|
|
]
|
|
}),
|
|
('Quality and Status', {
|
|
'fields': [
|
|
'image_quality', 'processed', 'archived'
|
|
]
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
|
|
def series_info(self, obj):
|
|
"""Display series information."""
|
|
return f"Series {obj.series.series_number} - {obj.series.study.accession_number}"
|
|
series_info.short_description = 'Series'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(series__study__tenant=request.user.tenant)
|
|
return qs.select_related('series__study')
|
|
|
|
|
|
@admin.register(RadiologyReport)
|
|
class RadiologyReportAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for radiology reports.
|
|
"""
|
|
list_display = [
|
|
'study_accession', 'patient_name', 'radiologist',
|
|
'status', 'critical_finding', 'finalized_datetime',
|
|
'turnaround_time'
|
|
]
|
|
list_filter = [
|
|
'status', 'critical_finding', 'critical_communicated',
|
|
'finalized_datetime', 'radiologist'
|
|
]
|
|
search_fields = [
|
|
'study__accession_number', 'study__patient__first_name',
|
|
'study__patient__last_name', 'radiologist__first_name',
|
|
'radiologist__last_name', 'findings', 'impression'
|
|
]
|
|
readonly_fields = [
|
|
'report_id', 'report_length', 'turnaround_time',
|
|
'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Report Information', {
|
|
'fields': [
|
|
'report_id', 'study', 'template_used'
|
|
]
|
|
}),
|
|
('Radiologist Information', {
|
|
'fields': [
|
|
'radiologist', 'dictated_by', 'transcribed_by'
|
|
]
|
|
}),
|
|
('Report Content', {
|
|
'fields': [
|
|
'clinical_history', 'technique', 'findings',
|
|
'impression', 'recommendations'
|
|
]
|
|
}),
|
|
('Report Status', {
|
|
'fields': [
|
|
'status'
|
|
]
|
|
}),
|
|
('Critical Findings', {
|
|
'fields': [
|
|
'critical_finding', 'critical_communicated',
|
|
'critical_communicated_to', 'critical_communicated_datetime'
|
|
]
|
|
}),
|
|
('Report Dates', {
|
|
'fields': [
|
|
'dictated_datetime', 'transcribed_datetime',
|
|
'verified_datetime', 'finalized_datetime'
|
|
]
|
|
}),
|
|
('Structured Reporting', {
|
|
'fields': [
|
|
'structured_data'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Quality and Metrics', {
|
|
'fields': [
|
|
'report_length', 'turnaround_time'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Addenda', {
|
|
'fields': [
|
|
'addendum', 'addendum_datetime'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
|
|
def study_accession(self, obj):
|
|
"""Display study accession number."""
|
|
return obj.study.accession_number
|
|
study_accession.short_description = 'Study'
|
|
|
|
def patient_name(self, obj):
|
|
"""Display patient name."""
|
|
return obj.study.patient.get_full_name()
|
|
patient_name.short_description = 'Patient'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(study__tenant=request.user.tenant)
|
|
return qs.select_related('study__patient', 'radiologist')
|
|
|
|
|
|
@admin.register(ReportTemplate)
|
|
class ReportTemplateAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for report templates.
|
|
"""
|
|
list_display = [
|
|
'name', 'modality', 'body_part', 'is_active',
|
|
'is_default', 'usage_count', 'created_by'
|
|
]
|
|
list_filter = [
|
|
'tenant', 'modality', 'body_part', 'is_active', 'is_default'
|
|
]
|
|
search_fields = [
|
|
'name', 'description', 'findings_template', 'impression_template'
|
|
]
|
|
readonly_fields = [
|
|
'template_id', 'usage_count', 'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Template Information', {
|
|
'fields': [
|
|
'template_id', 'name', 'description', 'tenant'
|
|
]
|
|
}),
|
|
('Template Scope', {
|
|
'fields': [
|
|
'modality', 'body_part'
|
|
]
|
|
}),
|
|
('Template Content', {
|
|
'fields': [
|
|
'clinical_history_template', 'technique_template',
|
|
'findings_template', 'impression_template',
|
|
'recommendations_template'
|
|
]
|
|
}),
|
|
('Structured Reporting', {
|
|
'fields': [
|
|
'structured_fields'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Template Status', {
|
|
'fields': [
|
|
'is_active', 'is_default'
|
|
]
|
|
}),
|
|
('Usage Statistics', {
|
|
'fields': [
|
|
'usage_count'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at', 'created_by'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(tenant=request.user.tenant)
|
|
return qs.select_related('created_by')
|
|
|
|
|
|
@admin.register(ImagingOrder)
|
|
class ImagingOrderAdmin(admin.ModelAdmin):
|
|
"""
|
|
Admin interface for imaging orders.
|
|
"""
|
|
list_display = [
|
|
'order_number', 'patient_name', 'modality',
|
|
'study_description', 'priority', 'status',
|
|
'ordering_provider', 'order_datetime'
|
|
]
|
|
list_filter = [
|
|
'tenant', 'modality', 'priority', 'status',
|
|
'contrast_required', 'order_datetime'
|
|
]
|
|
search_fields = [
|
|
'order_number', 'patient__first_name', 'patient__last_name',
|
|
'patient__mrn', 'study_description', 'clinical_indication'
|
|
]
|
|
readonly_fields = [
|
|
'order_id', 'order_number', 'created_at', 'updated_at'
|
|
]
|
|
fieldsets = [
|
|
('Order Information', {
|
|
'fields': [
|
|
'order_id', 'order_number', 'tenant'
|
|
]
|
|
}),
|
|
('Patient and Provider', {
|
|
'fields': [
|
|
'patient', 'ordering_provider'
|
|
]
|
|
}),
|
|
('Order Details', {
|
|
'fields': [
|
|
'order_datetime', 'priority'
|
|
]
|
|
}),
|
|
('Imaging Details', {
|
|
'fields': [
|
|
'modality', 'study_description', 'body_part'
|
|
]
|
|
}),
|
|
('Clinical Information', {
|
|
'fields': [
|
|
'clinical_indication', 'clinical_history', 'diagnosis_code'
|
|
]
|
|
}),
|
|
('Contrast Information', {
|
|
'fields': [
|
|
'contrast_required', 'contrast_type', 'contrast_route'
|
|
]
|
|
}),
|
|
('Scheduling Information', {
|
|
'fields': [
|
|
'requested_datetime', 'scheduled_datetime'
|
|
]
|
|
}),
|
|
('Status', {
|
|
'fields': [
|
|
'status'
|
|
]
|
|
}),
|
|
('Related Information', {
|
|
'fields': [
|
|
'encounter'
|
|
]
|
|
}),
|
|
('Special Instructions', {
|
|
'fields': [
|
|
'special_instructions', 'patient_preparation'
|
|
],
|
|
'classes': ['collapse']
|
|
}),
|
|
('Metadata', {
|
|
'fields': [
|
|
'created_at', 'updated_at'
|
|
],
|
|
'classes': ['collapse']
|
|
})
|
|
]
|
|
date_hierarchy = 'order_datetime'
|
|
|
|
def patient_name(self, obj):
|
|
"""Display patient name."""
|
|
return obj.patient.get_full_name()
|
|
patient_name.short_description = 'Patient'
|
|
|
|
def get_queryset(self, request):
|
|
"""Filter by user's tenant."""
|
|
qs = super().get_queryset(request)
|
|
if hasattr(request.user, 'tenant'):
|
|
qs = qs.filter(tenant=request.user.tenant)
|
|
return qs.select_related('patient', 'ordering_provider')
|
|
|
|
|
|
# Customize admin site
|
|
admin.site.site_header = "Hospital Management System - Radiology"
|
|
admin.site.site_title = "Radiology Admin"
|
|
admin.site.index_title = "Radiology Administration"
|
|
|