update
This commit is contained in:
parent
6b85b05882
commit
0c980b4706
@ -63479,3 +63479,162 @@ Traceback (most recent call last):
|
|||||||
raise FieldError(
|
raise FieldError(
|
||||||
django.core.exceptions.FieldError: Invalid field name(s) given in select_related: 'order'. Choices are: tenant, patient, referring_physician, radiologist, encounter, imaging_order, created_by, report
|
django.core.exceptions.FieldError: Invalid field name(s) given in select_related: 'order'. Choices are: tenant, patient, referring_physician, radiologist, encounter, imaging_order, created_by, report
|
||||||
ERROR 2025-08-13 19:30:12,351 basehttp 56119 6167998464 "GET /en/radiology/studies/ HTTP/1.1" 500 210392
|
ERROR 2025-08-13 19:30:12,351 basehttp 56119 6167998464 "GET /en/radiology/studies/ HTTP/1.1" 500 210392
|
||||||
|
INFO 2025-08-13 19:39:49,795 autoreload 60497 8601149632 Watching for file changes with StatReloader
|
||||||
|
ERROR 2025-08-13 19:39:54,177 log 60497 6204993536 Internal Server Error: /en/radiology/studies/
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner
|
||||||
|
response = get_response(request)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/core/handlers/base.py", line 220, in _get_response
|
||||||
|
response = response.render()
|
||||||
|
^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/response.py", line 114, in render
|
||||||
|
self.content = self.rendered_content
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/response.py", line 92, in rendered_content
|
||||||
|
return template.render(context, self._request)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/backends/django.py", line 107, in render
|
||||||
|
return self.template.render(context)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 171, in render
|
||||||
|
return self._render(context)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 163, in _render
|
||||||
|
return self.nodelist.render(context)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 1016, in render
|
||||||
|
return SafeString("".join([node.render_annotated(context) for node in self]))
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 977, in render_annotated
|
||||||
|
return self.render(context)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/loader_tags.py", line 159, in render
|
||||||
|
return compiled_parent._render(context)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 163, in _render
|
||||||
|
return self.nodelist.render(context)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 1016, in render
|
||||||
|
return SafeString("".join([node.render_annotated(context) for node in self]))
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 977, in render_annotated
|
||||||
|
return self.render(context)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/loader_tags.py", line 65, in render
|
||||||
|
result = block.nodelist.render(context)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 1016, in render
|
||||||
|
return SafeString("".join([node.render_annotated(context) for node in self]))
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 977, in render_annotated
|
||||||
|
return self.render(context)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/defaulttags.py", line 326, in render
|
||||||
|
if match:
|
||||||
|
^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/query.py", line 398, in __bool__
|
||||||
|
self._fetch_all()
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/query.py", line 1949, in _fetch_all
|
||||||
|
self._result_cache = list(self._iterable_class(self))
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/query.py", line 91, in __iter__
|
||||||
|
results = compiler.execute_sql(
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 1610, in execute_sql
|
||||||
|
sql, params = self.as_sql()
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 766, in as_sql
|
||||||
|
extra_select, order_by, group_by = self.pre_sql_setup(
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 85, in pre_sql_setup
|
||||||
|
self.setup_query(with_col_aliases=with_col_aliases)
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 74, in setup_query
|
||||||
|
self.select, self.klass_info, self.annotation_col_map = self.get_select(
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 299, in get_select
|
||||||
|
related_klass_infos = self.get_related_selections(select, select_mask)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 1396, in get_related_selections
|
||||||
|
raise FieldError(
|
||||||
|
django.core.exceptions.FieldError: Invalid field name(s) given in select_related: 'order'. Choices are: tenant, patient, referring_physician, radiologist, encounter, imaging_order, created_by, report
|
||||||
|
ERROR 2025-08-13 19:39:54,180 basehttp 60497 6204993536 "GET /en/radiology/studies/ HTTP/1.1" 500 210392
|
||||||
|
ERROR 2025-08-13 19:39:55,602 log 60497 6204993536 Internal Server Error: /en/radiology/studies/
|
||||||
|
Traceback (most recent call last):
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/core/handlers/exception.py", line 55, in inner
|
||||||
|
response = get_response(request)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/core/handlers/base.py", line 220, in _get_response
|
||||||
|
response = response.render()
|
||||||
|
^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/response.py", line 114, in render
|
||||||
|
self.content = self.rendered_content
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/response.py", line 92, in rendered_content
|
||||||
|
return template.render(context, self._request)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/backends/django.py", line 107, in render
|
||||||
|
return self.template.render(context)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 171, in render
|
||||||
|
return self._render(context)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 163, in _render
|
||||||
|
return self.nodelist.render(context)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 1016, in render
|
||||||
|
return SafeString("".join([node.render_annotated(context) for node in self]))
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 977, in render_annotated
|
||||||
|
return self.render(context)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/loader_tags.py", line 159, in render
|
||||||
|
return compiled_parent._render(context)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 163, in _render
|
||||||
|
return self.nodelist.render(context)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 1016, in render
|
||||||
|
return SafeString("".join([node.render_annotated(context) for node in self]))
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 977, in render_annotated
|
||||||
|
return self.render(context)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/loader_tags.py", line 65, in render
|
||||||
|
result = block.nodelist.render(context)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 1016, in render
|
||||||
|
return SafeString("".join([node.render_annotated(context) for node in self]))
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/base.py", line 977, in render_annotated
|
||||||
|
return self.render(context)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/template/defaulttags.py", line 326, in render
|
||||||
|
if match:
|
||||||
|
^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/query.py", line 398, in __bool__
|
||||||
|
self._fetch_all()
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/query.py", line 1949, in _fetch_all
|
||||||
|
self._result_cache = list(self._iterable_class(self))
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/query.py", line 91, in __iter__
|
||||||
|
results = compiler.execute_sql(
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 1610, in execute_sql
|
||||||
|
sql, params = self.as_sql()
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 766, in as_sql
|
||||||
|
extra_select, order_by, group_by = self.pre_sql_setup(
|
||||||
|
^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 85, in pre_sql_setup
|
||||||
|
self.setup_query(with_col_aliases=with_col_aliases)
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 74, in setup_query
|
||||||
|
self.select, self.klass_info, self.annotation_col_map = self.get_select(
|
||||||
|
^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 299, in get_select
|
||||||
|
related_klass_infos = self.get_related_selections(select, select_mask)
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
File "/Users/marwanalwali/manus_project/hospital_management_system_v4/.venv/lib/python3.12/site-packages/django/db/models/sql/compiler.py", line 1396, in get_related_selections
|
||||||
|
raise FieldError(
|
||||||
|
django.core.exceptions.FieldError: Invalid field name(s) given in select_related: 'order'. Choices are: tenant, patient, referring_physician, radiologist, encounter, imaging_order, created_by, report
|
||||||
|
ERROR 2025-08-13 19:39:55,605 basehttp 60497 6204993536 "GET /en/radiology/studies/ HTTP/1.1" 500 210392
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
@ -43,9 +43,13 @@ urlpatterns = [
|
|||||||
# ============================================================================
|
# ============================================================================
|
||||||
# IMAGING SERIES URLS (RESTRICTED CRUD - Clinical Data)
|
# IMAGING SERIES URLS (RESTRICTED CRUD - Clinical Data)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
path('series/', views.ImagingSeriesListView.as_view(), name='imaging_series_list'),
|
# urls.py
|
||||||
|
path('studies/<uuid:study_pk>/series/', views.ImagingSeriesListView.as_view(), name='imaging_series_list'),
|
||||||
|
path('series/<uuid:pk>/', views.ImagingSeriesDetailView.as_view(), name='imaging_series_detail'),
|
||||||
path('series/create/', views.ImagingSeriesCreateView.as_view(), name='imaging_series_create'),
|
path('series/create/', views.ImagingSeriesCreateView.as_view(), name='imaging_series_create'),
|
||||||
path('series/<int:pk>/', views.ImagingSeriesDetailView.as_view(), name='imaging_series_detail'),
|
# path('series/<uuid:pk>/edit/', views.ImagingSeriesUpdateView.as_view(), name='imaging_series_edit'),
|
||||||
|
# path('series/<uuid:pk>/delete/', views.ImagingSeriesDeleteView.as_view(), name='imaging_series_delete'),
|
||||||
|
|
||||||
# Note: Limited update capabilities for series data
|
# Note: Limited update capabilities for series data
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
@ -630,7 +630,7 @@ class ImagingSeriesDetailView(LoginRequiredMixin, DetailView):
|
|||||||
Display detailed information about an imaging series.
|
Display detailed information about an imaging series.
|
||||||
"""
|
"""
|
||||||
model = ImagingSeries
|
model = ImagingSeries
|
||||||
template_name = 'radiology/imaging_series_detail.html'
|
template_name = 'radiology/series/imaging_series_detail.html'
|
||||||
context_object_name = 'imaging_series'
|
context_object_name = 'imaging_series'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -652,7 +652,7 @@ class ImagingSeriesCreateView(LoginRequiredMixin, PermissionRequiredMixin, Creat
|
|||||||
"""
|
"""
|
||||||
model = ImagingSeries
|
model = ImagingSeries
|
||||||
form_class = ImagingSeriesForm
|
form_class = ImagingSeriesForm
|
||||||
template_name = 'radiology/imaging_series_form.html'
|
template_name = 'radiology/series/imaging_series_form.html'
|
||||||
permission_required = 'radiology.add_imagingseries'
|
permission_required = 'radiology.add_imagingseries'
|
||||||
success_url = reverse_lazy('radiology:imaging_series_list')
|
success_url = reverse_lazy('radiology:imaging_series_list')
|
||||||
|
|
||||||
@ -676,6 +676,8 @@ class ImagingSeriesCreateView(LoginRequiredMixin, PermissionRequiredMixin, Creat
|
|||||||
return response
|
return response
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# DICOM IMAGE VIEWS (READ-ONLY - System Generated)
|
# DICOM IMAGE VIEWS (READ-ONLY - System Generated)
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
@ -735,7 +737,7 @@ class RadiologyReportListView(LoginRequiredMixin, ListView):
|
|||||||
List all radiology reports with filtering and search.
|
List all radiology reports with filtering and search.
|
||||||
"""
|
"""
|
||||||
model = RadiologyReport
|
model = RadiologyReport
|
||||||
template_name = 'radiology/radiology_report_list.html'
|
template_name = 'radiology/reports/radiology_report_list.html'
|
||||||
context_object_name = 'radiology_reports'
|
context_object_name = 'radiology_reports'
|
||||||
paginate_by = 25
|
paginate_by = 25
|
||||||
|
|
||||||
@ -785,7 +787,7 @@ class RadiologyReportDetailView(LoginRequiredMixin, DetailView):
|
|||||||
Display detailed information about a radiology report.
|
Display detailed information about a radiology report.
|
||||||
"""
|
"""
|
||||||
model = RadiologyReport
|
model = RadiologyReport
|
||||||
template_name = 'radiology/radiology_report_detail.html'
|
template_name = 'radiology/reports/radiology_report_detail.html'
|
||||||
context_object_name = 'radiology_report'
|
context_object_name = 'radiology_report'
|
||||||
|
|
||||||
def get_queryset(self):
|
def get_queryset(self):
|
||||||
@ -798,7 +800,7 @@ class RadiologyReportCreateView(LoginRequiredMixin, PermissionRequiredMixin, Cre
|
|||||||
"""
|
"""
|
||||||
model = RadiologyReport
|
model = RadiologyReport
|
||||||
form_class = RadiologyReportForm
|
form_class = RadiologyReportForm
|
||||||
template_name = 'radiology/radiology_report_form.html'
|
template_name = 'radiology/reports/radiology_report_form.html'
|
||||||
permission_required = 'radiology.add_radiologyreport'
|
permission_required = 'radiology.add_radiologyreport'
|
||||||
success_url = reverse_lazy('radiology:radiology_report_list')
|
success_url = reverse_lazy('radiology:radiology_report_list')
|
||||||
|
|
||||||
|
|||||||
441
templates/radiology/series/imaging_series_confirm_delete.html
Normal file
441
templates/radiology/series/imaging_series_confirm_delete.html
Normal file
@ -0,0 +1,441 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Delete Imaging Series - {{ series.series_description|default:"Series" }} - Hospital Management{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-title">
|
||||||
|
<h4>Delete Imaging Series</h4>
|
||||||
|
<h6>Confirm deletion of Series {{ series.series_number }}</h6>
|
||||||
|
</div>
|
||||||
|
<div class="page-btn">
|
||||||
|
<a href="{% url 'radiology:imaging_series_detail' series.pk %}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>Back to Series
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Warning Alert -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="alert alert-danger" role="alert">
|
||||||
|
<div class="d-flex align-items-center">
|
||||||
|
<i class="fas fa-exclamation-triangle fa-2x me-3"></i>
|
||||||
|
<div>
|
||||||
|
<h5 class="alert-heading mb-1">Warning: Permanent Deletion</h5>
|
||||||
|
<p class="mb-0">This action cannot be undone. All associated images and data will be permanently deleted.</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Series Information -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-danger">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-layer-group me-2"></i>Series to be Deleted
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Series Number:</label>
|
||||||
|
<p class="fw-bold">{{ series.series_number }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Series Description:</label>
|
||||||
|
<p>{{ series.series_description|default:"No description" }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Protocol Name:</label>
|
||||||
|
<p>{{ series.protocol_name|default:"N/A" }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Modality:</label>
|
||||||
|
<p><span class="badge bg-primary">{{ series.get_modality_display }}</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Series Instance UID:</label>
|
||||||
|
<p class="text-monospace small">{{ series.series_instance_uid }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Series Date:</label>
|
||||||
|
<p>{{ series.series_date|date:"F d, Y" }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Series Time:</label>
|
||||||
|
<p>{{ series.series_time|time:"H:i:s" }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Number of Images:</label>
|
||||||
|
<p class="fw-bold text-danger">{{ series.number_of_images|default:0 }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Study Context -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<i class="fas fa-folder-open me-2"></i>Study Context
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Patient:</label>
|
||||||
|
<p class="fw-bold">{{ series.study.patient.get_full_name }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Study Description:</label>
|
||||||
|
<p>{{ series.study.study_description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Accession Number:</label>
|
||||||
|
<p>{{ series.study.accession_number }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Study Date:</label>
|
||||||
|
<p>{{ series.study.study_date|date:"M d, Y" }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Impact Analysis -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-warning">
|
||||||
|
<div class="card-header bg-warning">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-exclamation-circle me-2"></i>Deletion Impact Analysis
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="text-danger">Data to be Deleted:</h6>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li><i class="fas fa-times text-danger me-2"></i>{{ series.number_of_images|default:0 }} DICOM images</li>
|
||||||
|
<li><i class="fas fa-times text-danger me-2"></i>Series metadata and technical parameters</li>
|
||||||
|
<li><i class="fas fa-times text-danger me-2"></i>Associated annotations and measurements</li>
|
||||||
|
<li><i class="fas fa-times text-danger me-2"></i>Processing history and logs</li>
|
||||||
|
{% if series.reports.exists %}
|
||||||
|
<li><i class="fas fa-times text-danger me-2"></i>{{ series.reports.count }} associated report(s)</li>
|
||||||
|
{% endif %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<h6 class="text-info">What will remain:</h6>
|
||||||
|
<ul class="list-unstyled">
|
||||||
|
<li><i class="fas fa-check text-success me-2"></i>Parent study will remain intact</li>
|
||||||
|
<li><i class="fas fa-check text-success me-2"></i>Other series in the study</li>
|
||||||
|
<li><i class="fas fa-check text-success me-2"></i>Patient information</li>
|
||||||
|
<li><i class="fas fa-check text-success me-2"></i>Study-level reports</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{% if blocking_conditions %}
|
||||||
|
<div class="alert alert-danger mt-3">
|
||||||
|
<h6 class="alert-heading">
|
||||||
|
<i class="fas fa-ban me-2"></i>Deletion Blocked
|
||||||
|
</h6>
|
||||||
|
<p class="mb-2">This series cannot be deleted due to the following conditions:</p>
|
||||||
|
<ul class="mb-0">
|
||||||
|
{% for condition in blocking_conditions %}
|
||||||
|
<li>{{ condition }}</li>
|
||||||
|
{% endfor %}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Alternative Actions -->
|
||||||
|
{% if not blocking_conditions %}
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<i class="fas fa-lightbulb me-2"></i>Alternative Actions
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<p class="text-muted mb-3">Consider these alternatives before permanently deleting the series:</p>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="alternative-action">
|
||||||
|
<h6><i class="fas fa-archive text-warning me-2"></i>Archive Series</h6>
|
||||||
|
<p class="text-muted">Move the series to archive storage instead of deleting</p>
|
||||||
|
<button class="btn btn-outline-warning btn-sm" onclick="archiveSeries()">
|
||||||
|
<i class="fas fa-archive me-1"></i>Archive Instead
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="alternative-action">
|
||||||
|
<h6><i class="fas fa-download text-info me-2"></i>Backup First</h6>
|
||||||
|
<p class="text-muted">Download a backup before deletion</p>
|
||||||
|
<a href="{% url 'radiology:imaging_series_download' series.pk %}"
|
||||||
|
class="btn btn-outline-info btn-sm">
|
||||||
|
<i class="fas fa-download me-1"></i>Download Backup
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Confirmation Form -->
|
||||||
|
{% if not blocking_conditions %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card border-danger">
|
||||||
|
<div class="card-header bg-danger text-white">
|
||||||
|
<h5 class="card-title mb-0">
|
||||||
|
<i class="fas fa-trash me-2"></i>Confirm Deletion
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<form method="post" id="delete-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<label class="form-label">
|
||||||
|
<strong>Type "DELETE" to confirm:</strong>
|
||||||
|
</label>
|
||||||
|
<input type="text" class="form-control" id="confirmation-input"
|
||||||
|
placeholder="Type DELETE to confirm deletion" required>
|
||||||
|
<small class="form-text text-muted">
|
||||||
|
This confirmation is required to prevent accidental deletion.
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group mb-3">
|
||||||
|
<label class="form-label">Reason for Deletion:</label>
|
||||||
|
<select class="form-select" name="deletion_reason" required>
|
||||||
|
<option value="">Select a reason...</option>
|
||||||
|
<option value="duplicate">Duplicate series</option>
|
||||||
|
<option value="poor_quality">Poor image quality</option>
|
||||||
|
<option value="wrong_patient">Wrong patient</option>
|
||||||
|
<option value="technical_error">Technical error</option>
|
||||||
|
<option value="patient_request">Patient request</option>
|
||||||
|
<option value="other">Other</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-group mb-4">
|
||||||
|
<label class="form-label">Additional Notes:</label>
|
||||||
|
<textarea class="form-control" name="deletion_notes" rows="3"
|
||||||
|
placeholder="Optional: Provide additional details about the deletion..."></textarea>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="form-check mb-4">
|
||||||
|
<input class="form-check-input" type="checkbox" id="confirm-understanding" required>
|
||||||
|
<label class="form-check-label" for="confirm-understanding">
|
||||||
|
<strong>I understand that this action is permanent and cannot be undone</strong>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<a href="{% url 'radiology:imaging_series_detail' series.pk %}"
|
||||||
|
class="btn btn-secondary me-2">
|
||||||
|
<i class="fas fa-times me-1"></i>Cancel
|
||||||
|
</a>
|
||||||
|
<button type="button" class="btn btn-outline-warning me-2" onclick="archiveSeries()">
|
||||||
|
<i class="fas fa-archive me-1"></i>Archive Instead
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="danger-actions">
|
||||||
|
<button type="submit" class="btn btn-danger" id="delete-btn" disabled>
|
||||||
|
<i class="fas fa-trash me-1"></i>Permanently Delete Series
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<!-- Blocked Deletion Actions -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body text-center">
|
||||||
|
<i class="fas fa-ban fa-3x text-danger mb-3"></i>
|
||||||
|
<h5 class="text-danger">Deletion Not Allowed</h5>
|
||||||
|
<p class="text-muted">This series cannot be deleted due to the conditions listed above.</p>
|
||||||
|
<a href="{% url 'radiology:imaging_series_detail' series.pk %}" class="btn btn-primary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>Return to Series
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Enable delete button only when conditions are met
|
||||||
|
$('#confirmation-input, #confirm-understanding').on('input change', function() {
|
||||||
|
validateForm();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form submission validation
|
||||||
|
$('#delete-form').on('submit', function(e) {
|
||||||
|
if (!validateForm()) {
|
||||||
|
e.preventDefault();
|
||||||
|
alert('Please complete all required fields and confirmations.');
|
||||||
|
} else {
|
||||||
|
if (!confirm('Are you absolutely sure you want to permanently delete this imaging series? This action cannot be undone.')) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function validateForm() {
|
||||||
|
const confirmationText = $('#confirmation-input').val().trim().toUpperCase();
|
||||||
|
const confirmationChecked = $('#confirm-understanding').is(':checked');
|
||||||
|
const reasonSelected = $('select[name="deletion_reason"]').val();
|
||||||
|
|
||||||
|
const isValid = confirmationText === 'DELETE' && confirmationChecked && reasonSelected;
|
||||||
|
|
||||||
|
$('#delete-btn').prop('disabled', !isValid);
|
||||||
|
|
||||||
|
if (isValid) {
|
||||||
|
$('#delete-btn').removeClass('btn-outline-danger').addClass('btn-danger');
|
||||||
|
} else {
|
||||||
|
$('#delete-btn').removeClass('btn-danger').addClass('btn-outline-danger');
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function archiveSeries() {
|
||||||
|
if (confirm('Archive this series instead of deleting it?')) {
|
||||||
|
// AJAX call to archive series
|
||||||
|
$.ajax({
|
||||||
|
url: '{% url "radiology:imaging_series_archive" series.pk %}',
|
||||||
|
method: 'POST',
|
||||||
|
data: {
|
||||||
|
'csrfmiddlewaretoken': $('[name=csrfmiddlewaretoken]').val()
|
||||||
|
},
|
||||||
|
success: function(response) {
|
||||||
|
alert('Series has been archived successfully.');
|
||||||
|
window.location.href = '{% url "radiology:imaging_series_list" series.study.pk %}';
|
||||||
|
},
|
||||||
|
error: function() {
|
||||||
|
alert('Error archiving series. Please try again.');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.info-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-group .form-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-group p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alternative-action {
|
||||||
|
padding: 15px;
|
||||||
|
border: 1px solid #e9ecef;
|
||||||
|
border-radius: 5px;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alternative-action h6 {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.alternative-action p {
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .btn {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
#delete-btn:disabled {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.d-flex.justify-content-between {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .btn {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
628
templates/radiology/series/imaging_series_detail.html
Normal file
628
templates/radiology/series/imaging_series_detail.html
Normal file
@ -0,0 +1,628 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}Series {{ series.series_number }} - {{ series.series_description|default:"Imaging Series" }} - Hospital Management{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-title">
|
||||||
|
<h4>Imaging Series Details</h4>
|
||||||
|
<h6>Series {{ series.series_number }} - {{ series.series_description|default:"No Description" }}</h6>
|
||||||
|
</div>
|
||||||
|
<div class="page-btn">
|
||||||
|
<a href="{% url 'radiology:imaging_series_list' series.study.pk %}" class="btn btn-secondary me-2">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>Back to Series List
|
||||||
|
</a>
|
||||||
|
<a href="{% url 'radiology:imaging_series_edit' series.pk %}" class="btn btn-primary me-2">
|
||||||
|
<i class="fas fa-edit me-1"></i>Edit Series
|
||||||
|
</a>
|
||||||
|
<div class="btn-group">
|
||||||
|
<button class="btn btn-success dropdown-toggle" data-bs-toggle="dropdown">
|
||||||
|
<i class="fas fa-download me-1"></i>Download
|
||||||
|
</button>
|
||||||
|
<ul class="dropdown-menu">
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="{% url 'radiology:imaging_series_download' series.pk %}">
|
||||||
|
<i class="fas fa-file-medical me-2"></i>DICOM Files
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#" onclick="exportSeries('{{ series.pk }}', 'pdf')">
|
||||||
|
<i class="fas fa-file-pdf me-2"></i>PDF Report
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<a class="dropdown-item" href="#" onclick="exportSeries('{{ series.pk }}', 'zip')">
|
||||||
|
<i class="fas fa-file-archive me-2"></i>ZIP Archive
|
||||||
|
</a>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Series Information -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-lg-8">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>Series Information
|
||||||
|
</h5>
|
||||||
|
<div class="card-tools">
|
||||||
|
<span class="badge bg-{{ series.get_status_color|default:'success' }} fs-6">
|
||||||
|
{{ series.get_status_display|default:'Completed' }}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Series Number:</label>
|
||||||
|
<p class="fw-bold">{{ series.series_number }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Series Description:</label>
|
||||||
|
<p>{{ series.series_description|default:"No description provided" }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Protocol Name:</label>
|
||||||
|
<p>{{ series.protocol_name|default:"N/A" }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Modality:</label>
|
||||||
|
<p><span class="badge bg-primary">{{ series.get_modality_display }}</span></p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Series Instance UID:</label>
|
||||||
|
<p class="text-monospace small">{{ series.series_instance_uid }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Series Date:</label>
|
||||||
|
<p>{{ series.series_date|date:"F d, Y" }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Series Time:</label>
|
||||||
|
<p>{{ series.series_time|time:"H:i:s" }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Number of Images:</label>
|
||||||
|
<p class="fw-bold">{{ series.number_of_images|default:0 }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-lg-4">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<i class="fas fa-chart-bar me-2"></i>Series Statistics
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon">
|
||||||
|
<i class="fas fa-images text-primary"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<h6>{{ series.number_of_images|default:0 }}</h6>
|
||||||
|
<p>Total Images</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon">
|
||||||
|
<i class="fas fa-hdd text-info"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<h6>{{ series.total_size|default:"N/A" }}</h6>
|
||||||
|
<p>Total Size</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="stat-item">
|
||||||
|
<div class="stat-icon">
|
||||||
|
<i class="fas fa-clock text-warning"></i>
|
||||||
|
</div>
|
||||||
|
<div class="stat-content">
|
||||||
|
<h6>{{ series.acquisition_duration|default:"N/A" }}</h6>
|
||||||
|
<p>Acquisition Time</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Study Context -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<i class="fas fa-folder-open me-2"></i>Study Context
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Patient:</label>
|
||||||
|
<p class="fw-bold">{{ series.study.patient.get_full_name }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Study Description:</label>
|
||||||
|
<p>{{ series.study.study_description }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Accession Number:</label>
|
||||||
|
<p>{{ series.study.accession_number }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Referring Physician:</label>
|
||||||
|
<p>{{ series.study.referring_physician.get_full_name }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Technical Parameters -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<i class="fas fa-cogs me-2"></i>Technical Parameters
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Slice Thickness:</label>
|
||||||
|
<p>{{ series.slice_thickness|default:"N/A" }}{% if series.slice_thickness %} mm{% endif %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Spacing Between Slices:</label>
|
||||||
|
<p>{{ series.spacing_between_slices|default:"N/A" }}{% if series.spacing_between_slices %} mm{% endif %}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Pixel Spacing:</label>
|
||||||
|
<p>{{ series.pixel_spacing|default:"N/A" }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Image Orientation:</label>
|
||||||
|
<p>{{ series.image_orientation|default:"N/A" }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if series.acquisition_parameters %}
|
||||||
|
<div class="row mt-3">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Additional Parameters:</label>
|
||||||
|
<div class="parameter-list">
|
||||||
|
{% for key, value in series.acquisition_parameters.items %}
|
||||||
|
<div class="parameter-item">
|
||||||
|
<span class="parameter-key">{{ key }}:</span>
|
||||||
|
<span class="parameter-value">{{ value }}</span>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Image Viewer -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<i class="fas fa-images me-2"></i>Image Viewer
|
||||||
|
</h5>
|
||||||
|
<div class="card-tools">
|
||||||
|
<button class="btn btn-outline-primary btn-sm" onclick="openFullViewer()">
|
||||||
|
<i class="fas fa-expand me-1"></i>Full Screen Viewer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="image-viewer-container" class="text-center">
|
||||||
|
{% if series.number_of_images > 0 %}
|
||||||
|
<div class="image-viewer">
|
||||||
|
<div class="viewer-controls mb-3">
|
||||||
|
<div class="btn-group me-3">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" onclick="previousImage()">
|
||||||
|
<i class="fas fa-chevron-left"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" onclick="playPause()">
|
||||||
|
<i class="fas fa-play" id="play-icon"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" onclick="nextImage()">
|
||||||
|
<i class="fas fa-chevron-right"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="btn-group me-3">
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" onclick="zoomIn()">
|
||||||
|
<i class="fas fa-search-plus"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" onclick="zoomOut()">
|
||||||
|
<i class="fas fa-search-minus"></i>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-outline-secondary btn-sm" onclick="resetZoom()">
|
||||||
|
<i class="fas fa-expand-arrows-alt"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="image-counter">
|
||||||
|
<span id="current-image">1</span> / <span id="total-images">{{ series.number_of_images }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-display">
|
||||||
|
<div id="dicom-viewer" style="width: 100%; height: 400px; background: #000; border: 1px solid #ddd;">
|
||||||
|
<!-- DICOM viewer will be initialized here -->
|
||||||
|
<div class="d-flex align-items-center justify-content-center h-100 text-white">
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="fas fa-image fa-3x mb-3"></i>
|
||||||
|
<p>DICOM Viewer Loading...</p>
|
||||||
|
<button class="btn btn-primary" onclick="initializeViewer()">
|
||||||
|
<i class="fas fa-play me-1"></i>Initialize Viewer
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="image-slider mt-3">
|
||||||
|
<input type="range" class="form-range" id="image-slider"
|
||||||
|
min="1" max="{{ series.number_of_images }}" value="1"
|
||||||
|
onchange="goToImage(this.value)">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% else %}
|
||||||
|
<div class="no-images py-5">
|
||||||
|
<i class="fas fa-image fa-3x text-muted mb-3"></i>
|
||||||
|
<h5 class="text-muted">No Images Available</h5>
|
||||||
|
<p class="text-muted">This series does not contain any images yet.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Related Series -->
|
||||||
|
{% if related_series %}
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<i class="fas fa-layer-group me-2"></i>Related Series in Study
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
{% for related in related_series %}
|
||||||
|
<div class="col-lg-4 col-md-6 mb-3">
|
||||||
|
<div class="card border {% if related.pk == series.pk %}border-primary{% endif %}">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-start mb-2">
|
||||||
|
<h6 class="card-title mb-0">Series {{ related.series_number }}</h6>
|
||||||
|
{% if related.pk == series.pk %}
|
||||||
|
<span class="badge bg-primary">Current</span>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
<p class="card-text small text-muted">
|
||||||
|
{{ related.series_description|default:"No description" }}
|
||||||
|
</p>
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<small class="text-muted">
|
||||||
|
<span class="badge bg-secondary">{{ related.get_modality_display }}</span>
|
||||||
|
</small>
|
||||||
|
{% if related.pk != series.pk %}
|
||||||
|
<a href="{% url 'radiology:imaging_series_detail' related.pk %}"
|
||||||
|
class="btn btn-outline-primary btn-sm">
|
||||||
|
<i class="fas fa-eye me-1"></i>View
|
||||||
|
</a>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Action Buttons -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div class="action-buttons">
|
||||||
|
<a href="{% url 'radiology:imaging_series_edit' series.pk %}" class="btn btn-primary me-2">
|
||||||
|
<i class="fas fa-edit me-1"></i>Edit Series
|
||||||
|
</a>
|
||||||
|
<button class="btn btn-info me-2" onclick="openFullViewer()">
|
||||||
|
<i class="fas fa-expand me-1"></i>Full Screen Viewer
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-success me-2" onclick="downloadSeries()">
|
||||||
|
<i class="fas fa-download me-1"></i>Download DICOM
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="danger-actions">
|
||||||
|
<a href="{% url 'radiology:imaging_series_delete' series.pk %}"
|
||||||
|
class="btn btn-outline-danger"
|
||||||
|
onclick="return confirm('Are you sure you want to delete this series? This action cannot be undone.')">
|
||||||
|
<i class="fas fa-trash me-1"></i>Delete Series
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
let currentImageIndex = 1;
|
||||||
|
let totalImages = {{ series.number_of_images|default:0 }};
|
||||||
|
let isPlaying = false;
|
||||||
|
let playInterval;
|
||||||
|
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Initialize viewer if images are available
|
||||||
|
if (totalImages > 0) {
|
||||||
|
initializeViewer();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function initializeViewer() {
|
||||||
|
// Initialize DICOM viewer
|
||||||
|
// This would integrate with a DICOM viewer library like Cornerstone.js
|
||||||
|
console.log('Initializing DICOM viewer for series: {{ series.series_instance_uid }}');
|
||||||
|
|
||||||
|
// Placeholder for DICOM viewer initialization
|
||||||
|
$('#dicom-viewer').html(`
|
||||||
|
<div class="d-flex align-items-center justify-content-center h-100 text-white">
|
||||||
|
<div class="text-center">
|
||||||
|
<i class="fas fa-check-circle fa-3x mb-3 text-success"></i>
|
||||||
|
<p>DICOM Viewer Initialized</p>
|
||||||
|
<p class="small">Series: {{ series.series_instance_uid }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`);
|
||||||
|
}
|
||||||
|
|
||||||
|
function previousImage() {
|
||||||
|
if (currentImageIndex > 1) {
|
||||||
|
currentImageIndex--;
|
||||||
|
updateImageDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function nextImage() {
|
||||||
|
if (currentImageIndex < totalImages) {
|
||||||
|
currentImageIndex++;
|
||||||
|
updateImageDisplay();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function goToImage(index) {
|
||||||
|
currentImageIndex = parseInt(index);
|
||||||
|
updateImageDisplay();
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateImageDisplay() {
|
||||||
|
$('#current-image').text(currentImageIndex);
|
||||||
|
$('#image-slider').val(currentImageIndex);
|
||||||
|
|
||||||
|
// Update DICOM viewer to show current image
|
||||||
|
console.log('Displaying image:', currentImageIndex);
|
||||||
|
}
|
||||||
|
|
||||||
|
function playPause() {
|
||||||
|
if (isPlaying) {
|
||||||
|
clearInterval(playInterval);
|
||||||
|
$('#play-icon').removeClass('fa-pause').addClass('fa-play');
|
||||||
|
isPlaying = false;
|
||||||
|
} else {
|
||||||
|
playInterval = setInterval(function() {
|
||||||
|
if (currentImageIndex < totalImages) {
|
||||||
|
nextImage();
|
||||||
|
} else {
|
||||||
|
currentImageIndex = 1;
|
||||||
|
updateImageDisplay();
|
||||||
|
}
|
||||||
|
}, 500);
|
||||||
|
$('#play-icon').removeClass('fa-play').addClass('fa-pause');
|
||||||
|
isPlaying = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomIn() {
|
||||||
|
// Implement zoom in functionality
|
||||||
|
console.log('Zoom in');
|
||||||
|
}
|
||||||
|
|
||||||
|
function zoomOut() {
|
||||||
|
// Implement zoom out functionality
|
||||||
|
console.log('Zoom out');
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetZoom() {
|
||||||
|
// Implement reset zoom functionality
|
||||||
|
console.log('Reset zoom');
|
||||||
|
}
|
||||||
|
|
||||||
|
function openFullViewer() {
|
||||||
|
// Open full screen DICOM viewer
|
||||||
|
window.open(`/radiology/series/{{ series.pk }}/viewer/`, '_blank');
|
||||||
|
}
|
||||||
|
|
||||||
|
function downloadSeries() {
|
||||||
|
// Download series as DICOM files
|
||||||
|
window.location.href = `{% url 'radiology:imaging_series_download' series.pk %}`;
|
||||||
|
}
|
||||||
|
|
||||||
|
function exportSeries(seriesId, format) {
|
||||||
|
// Export series in specified format
|
||||||
|
if (confirm(`Export series as ${format.toUpperCase()}?`)) {
|
||||||
|
window.location.href = `/radiology/series/${seriesId}/export/${format}/`;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.info-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-group .form-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-group p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
padding: 10px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-icon {
|
||||||
|
margin-right: 15px;
|
||||||
|
font-size: 24px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content h6 {
|
||||||
|
margin-bottom: 0;
|
||||||
|
font-weight: bold;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat-content p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: #6c757d;
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-list {
|
||||||
|
background: #f8f9fa;
|
||||||
|
padding: 15px;
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-item {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding: 4px 0;
|
||||||
|
border-bottom: 1px solid #e9ecef;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-item:last-child {
|
||||||
|
border-bottom: none;
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-key {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.parameter-value {
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.viewer-controls {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
align-items: center;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-counter {
|
||||||
|
font-weight: bold;
|
||||||
|
color: #495057;
|
||||||
|
}
|
||||||
|
|
||||||
|
.image-display {
|
||||||
|
position: relative;
|
||||||
|
margin: 20px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .btn {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.viewer-controls {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.action-buttons .btn {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.d-flex.justify-content-between {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
565
templates/radiology/series/imaging_series_form.html
Normal file
565
templates/radiology/series/imaging_series_form.html
Normal file
@ -0,0 +1,565 @@
|
|||||||
|
{% extends 'base.html' %}
|
||||||
|
{% load static %}
|
||||||
|
|
||||||
|
{% block title %}{% if form.instance.pk %}Edit{% else %}Add{% endif %} Imaging Series - Hospital Management{% endblock %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="content">
|
||||||
|
<div class="container-fluid">
|
||||||
|
<!-- Page Header -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="page-header">
|
||||||
|
<div class="page-title">
|
||||||
|
<h4>{% if form.instance.pk %}Edit{% else %}Add{% endif %} Imaging Series</h4>
|
||||||
|
<h6>{% if study %}Study: {{ study.study_description|default:"Imaging Study" }}{% endif %}</h6>
|
||||||
|
</div>
|
||||||
|
<div class="page-btn">
|
||||||
|
<a href="{% if study %}{% url 'radiology:imaging_series_list' study.pk %}{% else %}{% url 'radiology:imaging_series_list' form.instance.study.pk %}{% endif %}" class="btn btn-secondary">
|
||||||
|
<i class="fas fa-arrow-left me-1"></i>Back to Series List
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="post" enctype="multipart/form-data" id="series-form">
|
||||||
|
{% csrf_token %}
|
||||||
|
|
||||||
|
<!-- Basic Information -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<i class="fas fa-info-circle me-2"></i>Basic Information
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="{{ form.series_number.id_for_label }}">
|
||||||
|
Series Number <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
{{ form.series_number }}
|
||||||
|
{% if form.series_number.errors %}
|
||||||
|
<div class="text-danger">
|
||||||
|
{% for error in form.series_number.errors %}
|
||||||
|
<small>{{ error }}</small>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<small class="form-text text-muted">Unique series number within the study</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="{{ form.modality.id_for_label }}">
|
||||||
|
Modality <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
{{ form.modality }}
|
||||||
|
{% if form.modality.errors %}
|
||||||
|
<div class="text-danger">
|
||||||
|
{% for error in form.modality.errors %}
|
||||||
|
<small>{{ error }}</small>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-12">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="{{ form.series_description.id_for_label }}">
|
||||||
|
Series Description
|
||||||
|
</label>
|
||||||
|
{{ form.series_description }}
|
||||||
|
{% if form.series_description.errors %}
|
||||||
|
<div class="text-danger">
|
||||||
|
{% for error in form.series_description.errors %}
|
||||||
|
<small>{{ error }}</small>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<small class="form-text text-muted">Descriptive name for this imaging series</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="{{ form.protocol_name.id_for_label }}">
|
||||||
|
Protocol Name
|
||||||
|
</label>
|
||||||
|
{{ form.protocol_name }}
|
||||||
|
{% if form.protocol_name.errors %}
|
||||||
|
<div class="text-danger">
|
||||||
|
{% for error in form.protocol_name.errors %}
|
||||||
|
<small>{{ error }}</small>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="{{ form.series_instance_uid.id_for_label }}">
|
||||||
|
Series Instance UID <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
{{ form.series_instance_uid }}
|
||||||
|
{% if form.series_instance_uid.errors %}
|
||||||
|
<div class="text-danger">
|
||||||
|
{% for error in form.series_instance_uid.errors %}
|
||||||
|
<small>{{ error }}</small>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<small class="form-text text-muted">DICOM Series Instance UID</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Date and Time Information -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<i class="fas fa-calendar-alt me-2"></i>Date and Time Information
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="{{ form.series_date.id_for_label }}">
|
||||||
|
Series Date <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
{{ form.series_date }}
|
||||||
|
{% if form.series_date.errors %}
|
||||||
|
<div class="text-danger">
|
||||||
|
{% for error in form.series_date.errors %}
|
||||||
|
<small>{{ error }}</small>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="{{ form.series_time.id_for_label }}">
|
||||||
|
Series Time <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
{{ form.series_time }}
|
||||||
|
{% if form.series_time.errors %}
|
||||||
|
<div class="text-danger">
|
||||||
|
{% for error in form.series_time.errors %}
|
||||||
|
<small>{{ error }}</small>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="{{ form.series_datetime.id_for_label }}">
|
||||||
|
Series DateTime <span class="text-danger">*</span>
|
||||||
|
</label>
|
||||||
|
{{ form.series_datetime }}
|
||||||
|
{% if form.series_datetime.errors %}
|
||||||
|
<div class="text-danger">
|
||||||
|
{% for error in form.series_datetime.errors %}
|
||||||
|
<small>{{ error }}</small>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<small class="form-text text-muted">Combined date and time</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Technical Parameters -->
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<i class="fas fa-cogs me-2"></i>Technical Parameters
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="{{ form.slice_thickness.id_for_label }}">
|
||||||
|
Slice Thickness (mm)
|
||||||
|
</label>
|
||||||
|
{{ form.slice_thickness }}
|
||||||
|
{% if form.slice_thickness.errors %}
|
||||||
|
<div class="text-danger">
|
||||||
|
{% for error in form.slice_thickness.errors %}
|
||||||
|
<small>{{ error }}</small>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="{{ form.spacing_between_slices.id_for_label }}">
|
||||||
|
Spacing Between Slices (mm)
|
||||||
|
</label>
|
||||||
|
{{ form.spacing_between_slices }}
|
||||||
|
{% if form.spacing_between_slices.errors %}
|
||||||
|
<div class="text-danger">
|
||||||
|
{% for error in form.spacing_between_slices.errors %}
|
||||||
|
<small>{{ error }}</small>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="{{ form.pixel_spacing.id_for_label }}">
|
||||||
|
Pixel Spacing
|
||||||
|
</label>
|
||||||
|
{{ form.pixel_spacing }}
|
||||||
|
{% if form.pixel_spacing.errors %}
|
||||||
|
<div class="text-danger">
|
||||||
|
{% for error in form.pixel_spacing.errors %}
|
||||||
|
<small>{{ error }}</small>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<small class="form-text text-muted">Format: row spacing\column spacing</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="form-group">
|
||||||
|
<label class="form-label" for="{{ form.image_orientation.id_for_label }}">
|
||||||
|
Image Orientation
|
||||||
|
</label>
|
||||||
|
{{ form.image_orientation }}
|
||||||
|
{% if form.image_orientation.errors %}
|
||||||
|
<div class="text-danger">
|
||||||
|
{% for error in form.image_orientation.errors %}
|
||||||
|
<small>{{ error }}</small>
|
||||||
|
{% endfor %}
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
<small class="form-text text-muted">DICOM Image Orientation Patient</small>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Additional Information -->
|
||||||
|
{% if form.instance.pk %}
|
||||||
|
<div class="row mb-4">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-header">
|
||||||
|
<h5 class="card-title">
|
||||||
|
<i class="fas fa-info me-2"></i>Additional Information
|
||||||
|
</h5>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Series ID:</label>
|
||||||
|
<p class="text-monospace">{{ form.instance.series_id }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Number of Images:</label>
|
||||||
|
<p>{{ form.instance.number_of_images|default:0 }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% if form.instance.created_at %}
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Created:</label>
|
||||||
|
<p>{{ form.instance.created_at|date:"M d, Y H:i" }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="col-md-6">
|
||||||
|
<div class="info-group">
|
||||||
|
<label class="form-label">Last Modified:</label>
|
||||||
|
<p>{{ form.instance.updated_at|date:"M d, Y H:i" }}</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
|
<!-- Form Actions -->
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<div class="card">
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="d-flex justify-content-between align-items-center">
|
||||||
|
<div class="form-actions">
|
||||||
|
<button type="submit" class="btn btn-primary me-2">
|
||||||
|
<i class="fas fa-save me-1"></i>
|
||||||
|
{% if form.instance.pk %}Update{% else %}Create{% endif %} Series
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary me-2" onclick="resetForm()">
|
||||||
|
<i class="fas fa-undo me-1"></i>Reset
|
||||||
|
</button>
|
||||||
|
<a href="{% if study %}{% url 'radiology:imaging_series_list' study.pk %}{% else %}{% url 'radiology:imaging_series_list' form.instance.study.pk %}{% endif %}"
|
||||||
|
class="btn btn-outline-danger">
|
||||||
|
<i class="fas fa-times me-1"></i>Cancel
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% if form.instance.pk %}
|
||||||
|
<div class="danger-actions">
|
||||||
|
<a href="{% url 'radiology:imaging_series_delete' form.instance.pk %}"
|
||||||
|
class="btn btn-outline-danger"
|
||||||
|
onclick="return confirm('Are you sure you want to delete this series? This action cannot be undone.')">
|
||||||
|
<i class="fas fa-trash me-1"></i>Delete Series
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script>
|
||||||
|
$(document).ready(function() {
|
||||||
|
// Initialize form
|
||||||
|
initializeForm();
|
||||||
|
|
||||||
|
// Auto-generate Series Instance UID if empty
|
||||||
|
if (!$('#{{ form.series_instance_uid.id_for_label }}').val()) {
|
||||||
|
generateSeriesInstanceUID();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Sync date and time with datetime field
|
||||||
|
$('#{{ form.series_date.id_for_label }}, #{{ form.series_time.id_for_label }}').on('change', function() {
|
||||||
|
syncDateTime();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Form validation
|
||||||
|
$('#series-form').on('submit', function(e) {
|
||||||
|
if (!validateForm()) {
|
||||||
|
e.preventDefault();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
function initializeForm() {
|
||||||
|
// Add form classes
|
||||||
|
$('.form-control, .form-select').addClass('form-control');
|
||||||
|
|
||||||
|
// Set current date/time if creating new series
|
||||||
|
{% if not form.instance.pk %}
|
||||||
|
const now = new Date();
|
||||||
|
const today = now.toISOString().split('T')[0];
|
||||||
|
const currentTime = now.toTimeString().split(' ')[0].substring(0, 5);
|
||||||
|
const currentDateTime = now.toISOString().slice(0, 16);
|
||||||
|
|
||||||
|
if (!$('#{{ form.series_date.id_for_label }}').val()) {
|
||||||
|
$('#{{ form.series_date.id_for_label }}').val(today);
|
||||||
|
}
|
||||||
|
if (!$('#{{ form.series_time.id_for_label }}').val()) {
|
||||||
|
$('#{{ form.series_time.id_for_label }}').val(currentTime);
|
||||||
|
}
|
||||||
|
if (!$('#{{ form.series_datetime.id_for_label }}').val()) {
|
||||||
|
$('#{{ form.series_datetime.id_for_label }}').val(currentDateTime);
|
||||||
|
}
|
||||||
|
{% endif %}
|
||||||
|
}
|
||||||
|
|
||||||
|
function generateSeriesInstanceUID() {
|
||||||
|
// Generate a DICOM-compliant Series Instance UID
|
||||||
|
const timestamp = Date.now();
|
||||||
|
const random = Math.floor(Math.random() * 1000000);
|
||||||
|
const uid = `1.2.826.0.1.3680043.8.498.${timestamp}.${random}`;
|
||||||
|
$('#{{ form.series_instance_uid.id_for_label }}').val(uid);
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncDateTime() {
|
||||||
|
const date = $('#{{ form.series_date.id_for_label }}').val();
|
||||||
|
const time = $('#{{ form.series_time.id_for_label }}').val();
|
||||||
|
|
||||||
|
if (date && time) {
|
||||||
|
const datetime = `${date}T${time}`;
|
||||||
|
$('#{{ form.series_datetime.id_for_label }}').val(datetime);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateForm() {
|
||||||
|
let isValid = true;
|
||||||
|
const requiredFields = [
|
||||||
|
'{{ form.series_number.id_for_label }}',
|
||||||
|
'{{ form.modality.id_for_label }}',
|
||||||
|
'{{ form.series_instance_uid.id_for_label }}',
|
||||||
|
'{{ form.series_date.id_for_label }}',
|
||||||
|
'{{ form.series_time.id_for_label }}',
|
||||||
|
'{{ form.series_datetime.id_for_label }}'
|
||||||
|
];
|
||||||
|
|
||||||
|
// Clear previous error states
|
||||||
|
$('.is-invalid').removeClass('is-invalid');
|
||||||
|
|
||||||
|
// Validate required fields
|
||||||
|
requiredFields.forEach(function(fieldId) {
|
||||||
|
const field = $('#' + fieldId);
|
||||||
|
if (!field.val() || field.val().trim() === '') {
|
||||||
|
field.addClass('is-invalid');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Validate Series Instance UID format
|
||||||
|
const uid = $('#{{ form.series_instance_uid.id_for_label }}').val();
|
||||||
|
if (uid && !isValidUID(uid)) {
|
||||||
|
$('#{{ form.series_instance_uid.id_for_label }}').addClass('is-invalid');
|
||||||
|
alert('Please enter a valid DICOM Series Instance UID');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Validate numeric fields
|
||||||
|
const numericFields = [
|
||||||
|
'{{ form.slice_thickness.id_for_label }}',
|
||||||
|
'{{ form.spacing_between_slices.id_for_label }}'
|
||||||
|
];
|
||||||
|
|
||||||
|
numericFields.forEach(function(fieldId) {
|
||||||
|
const field = $('#' + fieldId);
|
||||||
|
const value = field.val();
|
||||||
|
if (value && (isNaN(value) || parseFloat(value) < 0)) {
|
||||||
|
field.addClass('is-invalid');
|
||||||
|
isValid = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!isValid) {
|
||||||
|
alert('Please correct the highlighted fields before submitting.');
|
||||||
|
}
|
||||||
|
|
||||||
|
return isValid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function isValidUID(uid) {
|
||||||
|
// Basic DICOM UID validation
|
||||||
|
const uidPattern = /^[0-9]+(\.[0-9]+)*$/;
|
||||||
|
return uidPattern.test(uid) && uid.length <= 64;
|
||||||
|
}
|
||||||
|
|
||||||
|
function resetForm() {
|
||||||
|
if (confirm('Are you sure you want to reset the form? All unsaved changes will be lost.')) {
|
||||||
|
document.getElementById('series-form').reset();
|
||||||
|
$('.is-invalid').removeClass('is-invalid');
|
||||||
|
initializeForm();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Auto-save functionality (optional)
|
||||||
|
let autoSaveTimer;
|
||||||
|
function enableAutoSave() {
|
||||||
|
$('#series-form input, #series-form select, #series-form textarea').on('input change', function() {
|
||||||
|
clearTimeout(autoSaveTimer);
|
||||||
|
autoSaveTimer = setTimeout(function() {
|
||||||
|
// Auto-save logic here
|
||||||
|
console.log('Auto-saving form...');
|
||||||
|
}, 30000); // Auto-save after 30 seconds of inactivity
|
||||||
|
});
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.info-group {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-group .form-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
margin-bottom: 5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.info-group p {
|
||||||
|
margin-bottom: 0;
|
||||||
|
color: #6c757d;
|
||||||
|
padding: 8px 12px;
|
||||||
|
background: #f8f9fa;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-group {
|
||||||
|
margin-bottom: 20px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-label {
|
||||||
|
font-weight: 600;
|
||||||
|
color: #495057;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.text-danger {
|
||||||
|
color: #dc3545 !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
.is-invalid {
|
||||||
|
border-color: #dc3545;
|
||||||
|
box-shadow: 0 0 0 0.2rem rgba(220, 53, 69, 0.25);
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions .btn {
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.danger-actions {
|
||||||
|
margin-left: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Mobile Responsive */
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
.d-flex.justify-content-between {
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.form-actions .btn {
|
||||||
|
margin-right: 0;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
Loading…
x
Reference in New Issue
Block a user