""" Laboratory app views with healthcare-focused CRUD operations. Implements appropriate access patterns for clinical laboratory workflows. """ from django.shortcuts import render, get_object_or_404, redirect from django.contrib.auth.decorators import login_required from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin from django.views.generic import ( ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView ) from django.http import JsonResponse, HttpResponse from django.db.models import Q, Count, Avg, Sum, F from django.utils import timezone from django.contrib import messages from django.urls import reverse_lazy, reverse from django.core.paginator import Paginator from django.template.loader import render_to_string from datetime import datetime, timedelta, date import json from core.utils import AuditLogger from .models import ( LabTest, LabOrder, Specimen, LabResult, QualityControl, ReferenceRange ) from .forms import ( LabTestForm, LabOrderForm, SpecimenForm, LabResultForm, QualityControlForm, ReferenceRangeForm ) class LaboratoryDashboardView(LoginRequiredMixin, TemplateView): """ Main laboratory dashboard with key metrics and recent activity. """ template_name = 'laboratory/dashboard.html' def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) tenant = self.request.user.tenant today = timezone.now().date() # Dashboard statistics context.update({ 'pending_orders': LabOrder.objects.filter( tenant=tenant, status='PENDING' ).count(), 'in_progress_orders': LabOrder.objects.filter( tenant=tenant, status='IN_PROGRESS' ).count(), 'specimens_collected': Specimen.objects.filter( order__tenant=tenant, collected_datetime__date=today ).count(), 'results_pending': LabResult.objects.filter( order__tenant=tenant, status='PENDING' ).count(), 'results_completed_today': LabResult.objects.filter( order__tenant=tenant, analyzed_datetime__date=today, status='VERIFIED' ).count(), 'qc_tests_today': QualityControl.objects.filter( tenant=tenant, run_datetime=today ).count(), 'total_tests_available': LabTest.objects.filter( tenant=tenant, is_active=True ).count(), 'critical_results': LabResult.objects.filter( order__tenant=tenant, is_critical=True, status='VERIFIED', analyzed_datetime__date=today ).count(), }) # Recent orders context['recent_orders'] = LabOrder.objects.filter( tenant=tenant ).select_related('patient', 'ordering_provider').order_by('-order_datetime')[:10] # Recent results context['recent_results'] = LabResult.objects.filter( order__tenant=tenant, status='VERIFIED' ).select_related('order', 'test').order_by('-analyzed_datetime')[:10] return context class LabTestListView(LoginRequiredMixin, ListView): """ List all laboratory tests with filtering and search. """ model = LabTest template_name = 'laboratory/tests/lab_test_list.html' context_object_name = 'lab_tests' paginate_by = 25 def get_queryset(self): queryset = LabTest.objects.filter(tenant=self.request.user.tenant) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(test_name__icontains=search) | Q(test_code__icontains=search) | Q(test_description__icontains=search) ) # Filter by category category = self.request.GET.get('category') if category: queryset = queryset.filter(test_category=category) # Filter by active status active_only = self.request.GET.get('active_only') if active_only: queryset = queryset.filter(is_active=True) return queryset.order_by('test_name') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'categories': LabTest._meta.get_field('test_category').choices, 'search_query': self.request.GET.get('search', ''), }) return context class LabTestDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a laboratory test. """ model = LabTest template_name = 'laboratory/tests/lab_test_detail.html' context_object_name = 'lab_test' def get_queryset(self): return LabTest.objects.filter(tenant=self.request.user.tenant) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) lab_test = self.object # Get reference ranges for this test context['reference_ranges'] = lab_test.reference_ranges.all().order_by('age_min', 'gender') # Get recent orders for this test context['recent_orders'] = LabOrder.objects.filter( tests=lab_test, tenant=self.request.user.tenant ).select_related('patient').order_by('-order_datetime')[:10] # Get quality control records context['qc_records'] = QualityControl.objects.filter( test=lab_test, tenant=self.request.user.tenant ).order_by('-created_at')[:5] return context class LabTestCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ Create a new laboratory test. """ model = LabTest form_class = LabTestForm template_name = 'laboratory/tests/lab_test_form.html' permission_required = 'laboratory.add_labtest' success_url = reverse_lazy('laboratory:lab_test_list') def form_valid(self, form): form.instance.tenant = self.request.user.tenant response = super().form_valid(form) # Log the action AuditLogger.log_action( user=self.request.user, action='LAB_TEST_CREATED', model='LabTest', object_id=str(self.object.test_id), details={ 'test_name': self.object.test_name, 'test_code': self.object.test_code } ) messages.success(self.request, f'Lab test "{self.object.test_name}" created successfully.') return response class LabTestUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ Update an existing laboratory test. """ model = LabTest form_class = LabTestForm template_name = 'laboratory/tests/lab_test_form.html' permission_required = 'laboratory.change_labtest' def get_queryset(self): return LabTest.objects.filter(tenant=self.request.user.tenant) def get_success_url(self): return reverse('laboratory:lab_test_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): response = super().form_valid(form) # Log the action AuditLogger.log_action( user=self.request.user, action='LAB_TEST_UPDATED', model='LabTest', object_id=str(self.object.test_id), details={ 'test_name': self.object.test_name, 'changes': form.changed_data } ) messages.success(self.request, f'Lab test "{self.object.test_name}" updated successfully.') return response class LabTestDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): """ Delete a laboratory test (soft delete by deactivating). """ model = LabTest template_name = 'laboratory/tests/lab_test_confirm_delete.html' permission_required = 'laboratory.delete_labtest' success_url = reverse_lazy('laboratory:lab_test_list') def get_queryset(self): return LabTest.objects.filter(tenant=self.request.user.tenant) def delete(self, request, *args, **kwargs): self.object = self.get_object() # Soft delete by deactivating instead of actual deletion self.object.is_active = False self.object.save() # Log the action AuditLogger.log_action( user=request.user, action='LAB_TEST_DEACTIVATED', model='LabTest', object_id=str(self.object.test_id), details={'test_name': self.object.test_name} ) messages.success(request, f'Lab test "{self.object.test_name}" deactivated successfully.') return redirect(self.success_url) class ReferenceRangeListView(LoginRequiredMixin, ListView): """ List all reference ranges with filtering. """ model = ReferenceRange template_name = 'laboratory/reference_ranges/reference_range_list.html' context_object_name = 'reference_ranges' paginate_by = 25 def get_queryset(self): queryset = ReferenceRange.objects.filter(test__tenant=self.request.user.tenant) # Filter by test test_id = self.request.GET.get('test') if test_id: queryset = queryset.filter(test_id=test_id) # Filter by gender gender = self.request.GET.get('gender') if gender: queryset = queryset.filter(gender=gender) return queryset.select_related('test').order_by('test__test_name', 'age_min') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'lab_tests': LabTest.objects.filter( tenant=self.request.user.tenant, is_active=True ).order_by('test_name'), 'genders': ReferenceRange.Gender.choices, }) return context class ReferenceRangeDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a reference range. """ model = ReferenceRange template_name = 'laboratory/reference_ranges/reference_range_detail.html' context_object_name = 'reference_range' def get_queryset(self): return ReferenceRange.objects.filter(test__tenant=self.request.user.tenant) class ReferenceRangeCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ Create a new reference range. """ model = ReferenceRange form_class = ReferenceRangeForm template_name = 'laboratory/reference_ranges/reference_range_form.html' permission_required = 'laboratory.add_referencerange' success_url = reverse_lazy('laboratory:reference_range_list') def form_valid(self, form): form.instance.tenant = self.request.user.tenant response = super().form_valid(form) # Log the action AuditLogger.log_event( user=self.request.user, action='REFERENCE_RANGE_CREATED', model='ReferenceRange', object_id=str(self.object.id), details={ 'test_name': self.object.test.test_name, 'gender': self.object.gender, 'age_range': f"{self.object.age_min}-{self.object.age_max}" } ) messages.success(self.request, 'Reference range created successfully.') return response class ReferenceRangeUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ Update an existing reference range. """ model = ReferenceRange form_class = ReferenceRangeForm template_name = 'laboratory/reference_ranges/reference_range_form.html' permission_required = 'laboratory.change_referencerange' def get_queryset(self): return ReferenceRange.objects.filter(test__tenant=self.request.user.tenant) def get_success_url(self): return reverse('laboratory:reference_range_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): response = super().form_valid(form) # Log the action AuditLogger.log_event( user=self.request.user, action='REFERENCE_RANGE_UPDATED', model='ReferenceRange', object_id=str(self.object.id), details={ 'test_name': self.object.test.test_name, 'changes': form.changed_data } ) messages.success(self.request, 'Reference range updated successfully.') return response class ReferenceRangeDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): """ Delete a reference range. """ model = ReferenceRange template_name = 'laboratory/reference_ranges/reference_range_confirm_delete.html' permission_required = 'laboratory.delete_referencerange' success_url = reverse_lazy('laboratory:reference_range_list') def get_queryset(self): return ReferenceRange.objects.filter(test__tenant=self.request.user.tenant) def delete(self, request, *args, **kwargs): self.object = self.get_object() test_name = self.object.test.test_name response = super().delete(request, *args, **kwargs) # Log the action AuditLogger.log_event( user=request.user, action='REFERENCE_RANGE_DELETED', model='ReferenceRange', object_id=str(self.object.id), details={'test_name': test_name} ) messages.success(request, 'Reference range deleted successfully.') return response class LabOrderListView(LoginRequiredMixin, ListView): """ List all laboratory orders with filtering and search. """ model = LabOrder template_name = 'laboratory/orders/lab_order_list.html' context_object_name = 'orders' paginate_by = 25 def get_queryset(self): queryset = LabOrder.objects.filter(tenant=self.request.user.tenant) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(patient__first_name__icontains=search) | Q(patient__last_name__icontains=search) | Q(patient__mrn__icontains=search) | Q(tests__test_name__icontains=search) ) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) # Filter by priority priority = self.request.GET.get('priority') if priority: queryset = queryset.filter(priority=priority) # Filter by date range date_from = self.request.GET.get('date_from') date_to = self.request.GET.get('date_to') if date_from: queryset = queryset.filter(order_datetime__date__gte=date_from) if date_to: queryset = queryset.filter(order_datetime__date__lte=date_to) return queryset.select_related( 'patient', 'ordering_provider' ).order_by('-order_datetime') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'statuses': LabOrder._meta.get_field('status').choices, 'priorities': LabOrder._meta.get_field('priority').choices, }) return context class LabOrderDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a laboratory order. """ model = LabOrder template_name = 'laboratory/orders/lab_order_detail.html' context_object_name = 'lab_order' def get_queryset(self): return LabOrder.objects.filter(tenant=self.request.user.tenant) def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) lab_order = self.object # Get specimens for this order context['specimens'] = lab_order.specimens.all().order_by('-collected_datetime') # Get results for this order context['results'] = lab_order.results.all().order_by('-created_at') return context class LabOrderCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ Create a new laboratory order. """ model = LabOrder form_class = LabOrderForm template_name = 'laboratory/orders/lab_order_form.html' permission_required = 'laboratory.add_laborder' success_url = reverse_lazy('laboratory:lab_order_list') def form_valid(self, form): form.instance.tenant = self.request.user.tenant form.instance.ordering_provider = self.request.user form.instance.order_datetime = timezone.now() response = super().form_valid(form) # Log the action AuditLogger.log_action( user=self.request.user, action='LAB_ORDER_CREATED', model='LabOrder', object_id=str(self.object.order_id), details={ 'patient_name': f"{self.object.patient.first_name} {self.object.patient.last_name}", 'test_name': self.object.test.test_name, 'priority': self.object.priority } ) messages.success(self.request, 'Laboratory order created successfully.') return response class LabOrderUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ Update laboratory order (limited to status and notes only). """ model = LabOrder fields = ['status', 'notes'] # Restricted fields for clinical orders template_name = 'laboratory/orders/lab_order_form.html' permission_required = 'laboratory.change_laborder' def get_queryset(self): return LabOrder.objects.filter(tenant=self.request.user.tenant) def get_success_url(self): return reverse('laboratory:lab_order_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): response = super().form_valid(form) # Log the action AuditLogger.log_action( user=self.request.user, action='LAB_ORDER_UPDATED', model='LabOrder', object_id=str(self.object.order_id), details={ 'patient_name': f"{self.object.patient.first_name} {self.object.patient.last_name}", 'changes': form.changed_data } ) messages.success(self.request, 'Laboratory order updated successfully.') return response class SpecimenListView(LoginRequiredMixin, ListView): """ List all specimens with filtering and search. """ model = Specimen template_name = 'laboratory/specimens/specimen_list.html' context_object_name = 'specimens' paginate_by = 25 def get_queryset(self): queryset = Specimen.objects.filter(order__tenant=self.request.user.tenant) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(specimen_id__icontains=search) | Q(order__patient__first_name__icontains=search) | Q(order__patient__last_name__icontains=search) | Q(order__patient__mrn__icontains=search) ) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) # Filter by specimen type specimen_type = self.request.GET.get('specimen_type') if specimen_type: queryset = queryset.filter(specimen_type=specimen_type) return queryset.select_related('order__patient').order_by('-collected_datetime') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'statuses': Specimen._meta.get_field('status').choices, 'specimen_types': Specimen._meta.get_field('specimen_type').choices, }) return context class SpecimenDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a specimen. """ model = Specimen template_name = 'laboratory/specimens/specimen_detail.html' context_object_name = 'specimen' def get_queryset(self): return Specimen.objects.filter(order__tenant=self.request.user.tenant) class SpecimenCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ Create a new specimen record. """ model = Specimen form_class = SpecimenForm template_name = 'laboratory/specimens/specimen_form.html' permission_required = 'laboratory.add_specimen' success_url = reverse_lazy('laboratory:specimen_list') def form_valid(self, form): form.instance.tenant = self.request.user.tenant form.instance.collected_by = self.request.user response = super().form_valid(form) # Log the action AuditLogger.log_event( user=self.request.user, action='SPECIMEN_COLLECTED', model='Specimen', object_id=str(self.object.specimen_id), details={ 'patient_name': f"{self.object.order.patient.first_name} {self.object.order.patient.last_name}", 'specimen_type': self.object.specimen_type } ) messages.success(self.request, 'Specimen record created successfully.') return response class SpecimenUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ Update specimen (limited to status and processing notes). """ model = Specimen fields = ['status'] # Restricted fields template_name = 'laboratory/specimens/specimen_form.html' permission_required = 'laboratory.change_specimen' def get_queryset(self): return Specimen.objects.filter(order__tenant=self.request.user.tenant) def get_success_url(self): return reverse('laboratory:specimen_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): response = super().form_valid(form) # Log the action AuditLogger.log_event( user=self.request.user, action='SPECIMEN_UPDATED', model='Specimen', object_id=str(self.object.specimen_id), details={ 'patient_name': f"{self.object.order.patient.first_name} {self.object.order.patient.last_name}", 'changes': form.changed_data } ) messages.success(self.request, 'Specimen updated successfully.') return response class LabResultListView(LoginRequiredMixin, ListView): """ List all laboratory results with filtering and search. """ model = LabResult template_name = 'laboratory/results/lab_result_list.html' context_object_name = 'lab_results' paginate_by = 25 def get_queryset(self): tenant = self.request.user.tenant queryset = LabResult.objects.filter(order__tenant=tenant) # Search functionality search = self.request.GET.get('search') if search: queryset = queryset.filter( Q(order__patient__first_name__icontains=search) | Q(order__patient__last_name__icontains=search) | Q(order__patient__mrn__icontains=search) | Q(test__test_name__icontains=search) ) # Filter by status status = self.request.GET.get('status') if status: queryset = queryset.filter(status=status) # Filter by critical results critical_only = self.request.GET.get('critical_only') if critical_only: queryset = queryset.filter(is_critical=True) return queryset.select_related( 'order__patient', 'test', 'verified_by' ).order_by('-created_at') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'statuses': LabResult._meta.get_field('status').choices, }) return context class LabResultDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a laboratory result. """ model = LabResult template_name = 'laboratory/results/lab_result_detail.html' context_object_name = 'lab_result' def get_queryset(self): tenant = self.request.user.tenant return LabResult.objects.filter(order__tenant=tenant) def get_context_data(self, **kwargs): tenant = self.request.user.tenant context = super().get_context_data(**kwargs) lab_result = self.object # Get applicable reference ranges patient = lab_result.order.patient context['reference_ranges'] = ReferenceRange.objects.filter( test=lab_result.test, test__tenant=tenant, age_min__lte=patient.age if hasattr(patient, 'age') else 999, age_max__gte=patient.age if hasattr(patient, 'age') else 0, ).filter( Q(gender=patient.gender) | Q(gender='BOTH') ) return context class LabResultCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ Create a new laboratory result. """ model = LabResult form_class = LabResultForm template_name = 'laboratory/results/lab_result_form.html' permission_required = 'laboratory.add_labresult' success_url = reverse_lazy('laboratory:lab_result_list') def form_valid(self, form): form.instance.tenant = self.request.user.tenant form.instance.result_datetime = timezone.now() response = super().form_valid(form) # Log the action AuditLogger.log_action( user=self.request.user, action='LAB_RESULT_ENTERED', model='LabResult', object_id=str(self.object.result_id), details={ 'patient_name': f"{self.object.order.patient.first_name} {self.object.order.patient.last_name}", 'test_name': self.object.test.test_name, 'is_critical': self.object.is_critical } ) messages.success(self.request, 'Laboratory result entered successfully.') return response # Note: No UpdateView or DeleteView for LabResult - Append-only for clinical records # Results can only be verified or corrected through addendum process class QualityControlListView(LoginRequiredMixin, ListView): """ List all quality control records. """ model = QualityControl template_name = 'laboratory/quality_control/qc_sample_list.html' context_object_name = 'qc_samples' paginate_by = 25 def get_queryset(self): queryset = QualityControl.objects.filter(tenant=self.request.user.tenant) # Filter by test test_id = self.request.GET.get('test') if test_id: queryset = queryset.filter(test_id=test_id) # Filter by result result = self.request.GET.get('result') if result: queryset = queryset.filter(result=result) return queryset.select_related('test', 'performed_by') def get_context_data(self, **kwargs): context = super().get_context_data(**kwargs) context.update({ 'lab_tests': LabTest.objects.filter( tenant=self.request.user.tenant, is_active=True ).order_by('test_name'), 'results': QualityControl._meta.get_field('result').choices, }) return context class QualityControlDetailView(LoginRequiredMixin, DetailView): """ Display detailed information about a quality control record. """ model = QualityControl template_name = 'laboratory/quality_control/qc_sample_detail.html' context_object_name = 'qc_record' def get_queryset(self): return QualityControl.objects.filter(tenant=self.request.user.tenant) class QualityControlCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): """ Create a new quality control record. """ model = QualityControl form_class = QualityControlForm template_name = 'laboratory/quality_control/qc_sample_form.html' permission_required = 'laboratory.add_qualitycontrol' success_url = reverse_lazy('laboratory:quality_control_list') def form_valid(self, form): form.instance.tenant = self.request.user.tenant form.instance.performed_by = self.request.user response = super().form_valid(form) # Log the action AuditLogger.log_action( user=self.request.user, action='QC_TEST_PERFORMED', model='QualityControl', object_id=str(self.object.id), details={ 'test_name': self.object.test.test_name, 'result': self.object.result } ) messages.success(self.request, 'Quality control record created successfully.') return response class QualityControlUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): """ Update quality control record (limited fields). """ model = QualityControl fields = ['result', 'comments'] # Limited fields for operational data template_name = 'laboratory/quality_control_form.html' permission_required = 'laboratory.change_qualitycontrol' def get_queryset(self): return QualityControl.objects.filter(tenant=self.request.user.tenant) def get_success_url(self): return reverse('laboratory:quality_control_detail', kwargs={'pk': self.object.pk}) def form_valid(self, form): response = super().form_valid(form) # Log the action AuditLogger.log_action( user=self.request.user, action='QC_TEST_UPDATED', model='QualityControl', object_id=str(self.object.id), details={ 'test_name': self.object.test.test_name, 'changes': form.changed_data } ) messages.success(self.request, 'Quality control record updated successfully.') return response @login_required def laboratory_stats(request): """ HTMX endpoint for laboratory statistics. """ tenant = request.user.tenant today = timezone.now().date() stats = { 'pending_orders': LabOrder.objects.filter( tenant=tenant, status='PENDING' ).count(), 'in_progress_orders': LabOrder.objects.filter( tenant=tenant, status='IN_PROGRESS' ).count(), 'results_pending': LabResult.objects.filter( order__tenant=tenant, status='PENDING' ).count(), 'critical_results': LabResult.objects.filter( order__tenant=tenant, is_critical=True, status='VERIFIED', created_at__date=today ).count(), 'specimens_collected_today': LabResult.objects.filter( order__tenant=tenant, specimen__status='COLLECTED', specimen__collected_datetime__date=today ).count(), } return render(request, 'laboratory/partials/lab_stats.html', {'stats': stats}) @login_required def order_search(request): """ HTMX endpoint for order search. """ search = request.GET.get('search', '') status = request.GET.get('status', '') queryset = LabOrder.objects.filter(tenant=request.user.tenant) if search: queryset = queryset.filter( Q(patient__first_name__icontains=search) | Q(patient__last_name__icontains=search) | Q(patient__mrn__icontains=search) | Q(test__test_name__icontains=search) ) if status: queryset = queryset.filter(status=status) orders = queryset.select_related( 'patient', 'test', 'ordering_provider' ).order_by('-order_datetime')[:20] return render(request, 'laboratory/partials/order_list.html', {'orders': orders}) @login_required def receive_specimen(request, order_id): """ Mark specimen as received for processing. """ if request.method == 'POST': order = get_object_or_404( LabOrder, id=order_id, tenant=request.user.tenant ) # Update specimen status specimens = order.specimens.filter(status='COLLECTED') specimens.update( status='RECEIVED', received_datetime=timezone.now(), received_by=request.user ) # Update order status order.status = 'IN_PROGRESS' order.save() # Log the action AuditLogger.log_action( user=request.user, action='SPECIMEN_RECEIVED', model='LabOrder', object_id=str(order.order_id), details={ 'patient_name': f"{order.patient.first_name} {order.patient.last_name}", 'specimens_count': specimens.count() } ) messages.success(request, 'Specimen(s) marked as received.') if request.headers.get('HX-Request'): return render(request, 'laboratory/partials/order_status.html', {'order': order}) return redirect('laboratory:lab_order_detail', pk=order.pk) return JsonResponse({'success': False}) @login_required def start_processing(request, order_id): """ Start processing laboratory order. """ if request.method == 'POST': order = get_object_or_404( LabOrder, id=order_id, tenant=request.user.tenant ) order.status = 'IN_PROGRESS' order.save() # Log the action AuditLogger.log_action( user=request.user, action='LAB_PROCESSING_STARTED', model='LabOrder', object_id=str(order.order_id), details={ 'patient_name': f"{order.patient.first_name} {order.patient.last_name}", 'test_name': order.test.test_name } ) messages.success(request, 'Laboratory processing started.') if request.headers.get('HX-Request'): return render(request, 'laboratory/partials/order_status.html', {'order': order}) return redirect('laboratory:lab_order_detail', pk=order.pk) return JsonResponse({'success': False}) @login_required def reject_specimen(request, specimen_id): """ Reject a specimen with reason. """ if request.method == 'POST': specimen = get_object_or_404( Specimen, id=specimen_id, tenant=request.user.tenant ) rejection_reason = request.POST.get('rejection_reason', '') specimen.status = 'REJECTED' specimen.rejection_reason = rejection_reason specimen.rejected_by = request.user specimen.rejected_datetime = timezone.now() specimen.save() # Update order status specimen.order.status = 'CANCELLED' specimen.order.save() # Log the action AuditLogger.log_action( user=request.user, action='SPECIMEN_REJECTED', model='Specimen', object_id=str(specimen.specimen_id), details={ 'patient_name': f"{specimen.order.patient.first_name} {specimen.order.patient.last_name}", 'rejection_reason': rejection_reason } ) messages.warning(request, 'Specimen rejected.') if request.headers.get('HX-Request'): return render(request, 'laboratory/partials/specimen_status.html', {'specimen': specimen}) return redirect('laboratory:specimen_detail', pk=specimen.pk) return JsonResponse({'success': False}) @login_required def schedule_collection(request, order_id): """ Schedule specimen collection for an order. """ if request.method == 'POST': order = get_object_or_404( LabOrder, id=order_id, tenant=request.user.tenant ) collection_datetime = request.POST.get('collection_datetime') if collection_datetime: collection_datetime = timezone.datetime.fromisoformat(collection_datetime) order.scheduled_collection = collection_datetime order.status = 'SCHEDULED' order.save() # Log the action AuditLogger.log_action( user=request.user, action='COLLECTION_SCHEDULED', model='LabOrder', object_id=str(order.order_id), details={ 'patient_name': f"{order.patient.first_name} {order.patient.last_name}", 'scheduled_time': collection_datetime.isoformat() } ) messages.success(request, 'Collection scheduled successfully.') if request.headers.get('HX-Request'): return render(request, 'laboratory/partials/order_status.html', {'order': order}) return redirect('laboratory:lab_order_detail', pk=order.pk) return JsonResponse({'success': False}) @login_required def mark_collected(request, order_id): """ Mark specimen as collected. """ if request.method == 'POST': order = get_object_or_404( LabOrder, id=order_id, tenant=request.user.tenant ) # Create specimen record if it doesn't exist specimen, created = Specimen.objects.get_or_create( order=order, tenant=request.user.tenant, defaults={ 'specimen_type': 'BLOOD', # Default, should be specified 'collection_datetime': timezone.now(), 'collected_by': request.user, 'status': 'COLLECTED' } ) if not created: specimen.status = 'COLLECTED' specimen.collection_datetime = timezone.now() specimen.collected_by = request.user specimen.save() # Update order status order.status = 'COLLECTED' order.save() # Log the action AuditLogger.log_event( user=request.user, action='SPECIMEN_COLLECTED', model='LabOrder', object_id=str(order.order_id), details={ 'patient_name': f"{order.patient.first_name} {order.patient.last_name}", 'specimen_type': specimen.specimen_type } ) messages.success(request, 'Specimen marked as collected.') if request.headers.get('HX-Request'): return render(request, 'laboratory/partials/order_status.html', {'order': order}) return redirect('laboratory:lab_order_detail', pk=order.pk) return JsonResponse({'success': False}) # # """ # Laboratory app views with healthcare-focused CRUD operations. # Implements appropriate access patterns for clinical laboratory workflows. # """ # # from django.shortcuts import render, get_object_or_404, redirect # from django.contrib.auth.decorators import login_required # from django.contrib.auth.mixins import LoginRequiredMixin, PermissionRequiredMixin # from django.views.generic import ( # ListView, DetailView, CreateView, UpdateView, DeleteView, TemplateView # ) # from django.http import JsonResponse, HttpResponse # from django.db.models import Q, Count, Avg, Sum, F # from django.utils import timezone # from django.contrib import messages # from django.urls import reverse_lazy, reverse # from django.core.paginator import Paginator # from django.template.loader import render_to_string # from datetime import datetime, timedelta, date # import json # # from core.utils import AuditLogger # from .models import ( # LabTest, LabOrder, Specimen, LabResult, QualityControl, ReferenceRange # ) # from .forms import ( # LabTestForm, LabOrderForm, SpecimenForm, LabResultForm, # QualityControlForm, ReferenceRangeForm # ) # # # # ============================================================================ # # DASHBOARD AND OVERVIEW VIEWS # # ============================================================================ # # class LaboratoryDashboardView(LoginRequiredMixin, TemplateView): # """ # Main laboratory dashboard with key metrics and recent activity. # """ # template_name = 'laboratory/dashboard.html' # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # tenant = self.request.user.tenant # today = timezone.now().date() # # # Dashboard statistics # context.update({ # 'pending_orders': LabOrder.objects.filter( # tenant=tenant, # status='PENDING' # ).count(), # 'in_progress_orders': LabOrder.objects.filter( # tenant=tenant, # status='IN_PROGRESS' # ).count(), # 'specimens_collected': Specimen.objects.filter( # tenant=tenant, # collection_datetime__date=today # ).count(), # 'results_pending': LabResult.objects.filter( # tenant=tenant, # status='PENDING' # ).count(), # 'results_completed_today': LabResult.objects.filter( # tenant=tenant, # result_datetime__date=today, # status='VERIFIED' # ).count(), # 'qc_tests_today': QualityControl.objects.filter( # tenant=tenant, # test_date=today # ).count(), # 'total_tests_available': LabTest.objects.filter( # tenant=tenant, # is_active=True # ).count(), # 'critical_results': LabResult.objects.filter( # tenant=tenant, # is_critical=True, # status='VERIFIED', # result_datetime__date=today # ).count(), # }) # # # Recent orders # context['recent_orders'] = LabOrder.objects.filter( # tenant=tenant # ).select_related('patient', 'ordering_provider').order_by('-order_datetime')[:10] # # # Recent results # context['recent_results'] = LabResult.objects.filter( # tenant=tenant, # status='VERIFIED' # ).select_related('order', 'test').order_by('-result_datetime')[:10] # # return context # # # # ============================================================================ # # LAB TEST VIEWS (FULL CRUD - Master Data) # # ============================================================================ # # class LabTestListView(LoginRequiredMixin, ListView): # """ # List all laboratory tests with filtering and search. # """ # model = LabTest # template_name = 'laboratory/lab_test_list.html' # context_object_name = 'lab_tests' # paginate_by = 25 # # def get_queryset(self): # queryset = LabTest.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(test_name__icontains=search) | # Q(test_code__icontains=search) | # Q(test_description__icontains=search) # ) # # # Filter by category # category = self.request.GET.get('category') # if category: # queryset = queryset.filter(category=category) # # # Filter by active status # active_only = self.request.GET.get('active_only') # if active_only: # queryset = queryset.filter(is_active=True) # # return queryset.order_by('test_name') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'categories': LabTest._meta.get_field('category').choices, # 'search_query': self.request.GET.get('search', ''), # }) # return context # # # class LabTestDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a laboratory test. # """ # model = LabTest # template_name = 'laboratory/lab_test_detail.html' # context_object_name = 'lab_test' # # def get_queryset(self): # return LabTest.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # lab_test = self.object # # # Get reference ranges for this test # context['reference_ranges'] = lab_test.reference_ranges.all().order_by('age_min', 'gender') # # # Get recent orders for this test # context['recent_orders'] = LabOrder.objects.filter( # test=lab_test, # tenant=self.request.user.tenant # ).select_related('patient').order_by('-order_datetime')[:10] # # # Get quality control records # context['qc_records'] = QualityControl.objects.filter( # test=lab_test, # tenant=self.request.user.tenant # ).order_by('-test_date')[:5] # # return context # # # class LabTestCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new laboratory test. # """ # model = LabTest # form_class = LabTestForm # template_name = 'laboratory/lab_test_form.html' # permission_required = 'laboratory.add_labtest' # success_url = reverse_lazy('laboratory:lab_test_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='LAB_TEST_CREATED', # model='LabTest', # object_id=str(self.object.test_id), # details={ # 'test_name': self.object.test_name, # 'test_code': self.object.test_code # } # ) # # messages.success(self.request, f'Lab test "{self.object.test_name}" created successfully.') # return response # # # class LabTestUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update an existing laboratory test. # """ # model = LabTest # form_class = LabTestForm # template_name = 'laboratory/lab_test_form.html' # permission_required = 'laboratory.change_labtest' # # def get_queryset(self): # return LabTest.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('laboratory:lab_test_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='LAB_TEST_UPDATED', # model='LabTest', # object_id=str(self.object.test_id), # details={ # 'test_name': self.object.test_name, # 'changes': form.changed_data # } # ) # # messages.success(self.request, f'Lab test "{self.object.test_name}" updated successfully.') # return response # # # class LabTestDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): # """ # Delete a laboratory test (soft delete by deactivating). # """ # model = LabTest # template_name = 'laboratory/lab_test_confirm_delete.html' # permission_required = 'laboratory.delete_labtest' # success_url = reverse_lazy('laboratory:lab_test_list') # # def get_queryset(self): # return LabTest.objects.filter(tenant=self.request.user.tenant) # # def delete(self, request, *args, **kwargs): # self.object = self.get_object() # # # Soft delete by deactivating instead of actual deletion # self.object.is_active = False # self.object.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='LAB_TEST_DEACTIVATED', # model='LabTest', # object_id=str(self.object.test_id), # details={'test_name': self.object.test_name} # ) # # messages.success(request, f'Lab test "{self.object.test_name}" deactivated successfully.') # return redirect(self.success_url) # # # # ============================================================================ # # REFERENCE RANGE VIEWS (FULL CRUD - Reference Data) # # ============================================================================ # # class ReferenceRangeListView(LoginRequiredMixin, ListView): # """ # List all reference ranges with filtering. # """ # model = ReferenceRange # template_name = 'laboratory/reference_range_list.html' # context_object_name = 'reference_ranges' # paginate_by = 25 # # def get_queryset(self): # queryset = ReferenceRange.objects.filter(tenant=self.request.user.tenant) # # # Filter by test # test_id = self.request.GET.get('test') # if test_id: # queryset = queryset.filter(test_id=test_id) # # # Filter by gender # gender = self.request.GET.get('gender') # if gender: # queryset = queryset.filter(gender=gender) # # return queryset.select_related('test').order_by('test__test_name', 'age_min') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'lab_tests': LabTest.objects.filter( # tenant=self.request.user.tenant, # is_active=True # ).order_by('test_name'), # 'genders': ReferenceRange._meta.get_field('gender').choices, # }) # return context # # # class ReferenceRangeDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a reference range. # """ # model = ReferenceRange # template_name = 'laboratory/reference_range_detail.html' # context_object_name = 'reference_range' # # def get_queryset(self): # return ReferenceRange.objects.filter(tenant=self.request.user.tenant) # # # class ReferenceRangeCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new reference range. # """ # model = ReferenceRange # form_class = ReferenceRangeForm # template_name = 'laboratory/reference_range_form.html' # permission_required = 'laboratory.add_referencerange' # success_url = reverse_lazy('laboratory:reference_range_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='REFERENCE_RANGE_CREATED', # model='ReferenceRange', # object_id=str(self.object.id), # details={ # 'test_name': self.object.test.test_name, # 'gender': self.object.gender, # 'age_range': f"{self.object.age_min}-{self.object.age_max}" # } # ) # # messages.success(self.request, 'Reference range created successfully.') # return response # # # class ReferenceRangeUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update an existing reference range. # """ # model = ReferenceRange # form_class = ReferenceRangeForm # template_name = 'laboratory/reference_range_form.html' # permission_required = 'laboratory.change_referencerange' # # def get_queryset(self): # return ReferenceRange.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('laboratory:reference_range_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='REFERENCE_RANGE_UPDATED', # model='ReferenceRange', # object_id=str(self.object.id), # details={ # 'test_name': self.object.test.test_name, # 'changes': form.changed_data # } # ) # # messages.success(self.request, 'Reference range updated successfully.') # return response # # # class ReferenceRangeDeleteView(LoginRequiredMixin, PermissionRequiredMixin, DeleteView): # """ # Delete a reference range. # """ # model = ReferenceRange # template_name = 'laboratory/reference_range_confirm_delete.html' # permission_required = 'laboratory.delete_referencerange' # success_url = reverse_lazy('laboratory:reference_range_list') # # def get_queryset(self): # return ReferenceRange.objects.filter(tenant=self.request.user.tenant) # # def delete(self, request, *args, **kwargs): # self.object = self.get_object() # test_name = self.object.test.test_name # # response = super().delete(request, *args, **kwargs) # # # Log the action # AuditLogger.log_action( # user=request.user, # action='REFERENCE_RANGE_DELETED', # model='ReferenceRange', # object_id=str(self.object.id), # details={'test_name': test_name} # ) # # messages.success(request, 'Reference range deleted successfully.') # return response # # # # ============================================================================ # # LAB ORDER VIEWS (RESTRICTED CRUD - Clinical Orders) # # ============================================================================ # # class LabOrderListView(LoginRequiredMixin, ListView): # """ # List all laboratory orders with filtering and search. # """ # model = LabOrder # template_name = 'laboratory/lab_order_list.html' # context_object_name = 'lab_orders' # paginate_by = 25 # # def get_queryset(self): # queryset = LabOrder.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(patient__first_name__icontains=search) | # Q(patient__last_name__icontains=search) | # Q(patient__mrn__icontains=search) | # Q(test__test_name__icontains=search) # ) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by priority # priority = self.request.GET.get('priority') # if priority: # queryset = queryset.filter(priority=priority) # # # Filter by date range # date_from = self.request.GET.get('date_from') # date_to = self.request.GET.get('date_to') # if date_from: # queryset = queryset.filter(order_datetime__date__gte=date_from) # if date_to: # queryset = queryset.filter(order_datetime__date__lte=date_to) # # return queryset.select_related( # 'patient', 'test', 'ordering_provider' # ).order_by('-order_datetime') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'statuses': LabOrder._meta.get_field('status').choices, # 'priorities': LabOrder._meta.get_field('priority').choices, # }) # return context # # # class LabOrderDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a laboratory order. # """ # model = LabOrder # template_name = 'laboratory/lab_order_detail.html' # context_object_name = 'lab_order' # # def get_queryset(self): # return LabOrder.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # lab_order = self.object # # # Get specimens for this order # context['specimens'] = lab_order.specimens.all().order_by('-collection_datetime') # # # Get results for this order # context['results'] = lab_order.results.all().order_by('-result_datetime') # # return context # # # class LabOrderCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new laboratory order. # """ # model = LabOrder # form_class = LabOrderForm # template_name = 'laboratory/lab_order_form.html' # permission_required = 'laboratory.add_laborder' # success_url = reverse_lazy('laboratory:lab_order_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.ordering_provider = self.request.user # form.instance.order_datetime = timezone.now() # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='LAB_ORDER_CREATED', # model='LabOrder', # object_id=str(self.object.order_id), # details={ # 'patient_name': f"{self.object.patient.first_name} {self.object.patient.last_name}", # 'test_name': self.object.test.test_name, # 'priority': self.object.priority # } # ) # # messages.success(self.request, 'Laboratory order created successfully.') # return response # # # class LabOrderUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update laboratory order (limited to status and notes only). # """ # model = LabOrder # fields = ['status', 'notes'] # Restricted fields for clinical orders # template_name = 'laboratory/lab_order_update_form.html' # permission_required = 'laboratory.change_laborder' # # def get_queryset(self): # return LabOrder.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('laboratory:lab_order_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='LAB_ORDER_UPDATED', # model='LabOrder', # object_id=str(self.object.order_id), # details={ # 'patient_name': f"{self.object.patient.first_name} {self.object.patient.last_name}", # 'changes': form.changed_data # } # ) # # messages.success(self.request, 'Laboratory order updated successfully.') # return response # # # # ============================================================================ # # SPECIMEN VIEWS (RESTRICTED CRUD - Clinical Data) # # ============================================================================ # # class SpecimenListView(LoginRequiredMixin, ListView): # """ # List all specimens with filtering and search. # """ # model = Specimen # template_name = 'laboratory/specimen_list.html' # context_object_name = 'specimens' # paginate_by = 25 # # def get_queryset(self): # queryset = Specimen.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(specimen_id__icontains=search) | # Q(order__patient__first_name__icontains=search) | # Q(order__patient__last_name__icontains=search) | # Q(order__patient__mrn__icontains=search) # ) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by specimen type # specimen_type = self.request.GET.get('specimen_type') # if specimen_type: # queryset = queryset.filter(specimen_type=specimen_type) # # return queryset.select_related('order__patient', 'order__test').order_by('-collection_datetime') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'statuses': Specimen._meta.get_field('status').choices, # 'specimen_types': Specimen._meta.get_field('specimen_type').choices, # }) # return context # # # class SpecimenDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a specimen. # """ # model = Specimen # template_name = 'laboratory/specimen_detail.html' # context_object_name = 'specimen' # # def get_queryset(self): # return Specimen.objects.filter(tenant=self.request.user.tenant) # # # class SpecimenCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new specimen record. # """ # model = Specimen # form_class = SpecimenForm # template_name = 'laboratory/specimen_form.html' # permission_required = 'laboratory.add_specimen' # success_url = reverse_lazy('laboratory:specimen_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.collected_by = self.request.user # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='SPECIMEN_COLLECTED', # model='Specimen', # object_id=str(self.object.specimen_id), # details={ # 'patient_name': f"{self.object.order.patient.first_name} {self.object.order.patient.last_name}", # 'specimen_type': self.object.specimen_type # } # ) # # messages.success(self.request, 'Specimen record created successfully.') # return response # # # class SpecimenUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update specimen (limited to status and processing notes). # """ # model = Specimen # fields = ['status', 'processing_notes'] # Restricted fields # template_name = 'laboratory/specimen_update_form.html' # permission_required = 'laboratory.change_specimen' # # def get_queryset(self): # return Specimen.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('laboratory:specimen_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='SPECIMEN_UPDATED', # model='Specimen', # object_id=str(self.object.specimen_id), # details={ # 'patient_name': f"{self.object.order.patient.first_name} {self.object.order.patient.last_name}", # 'changes': form.changed_data # } # ) # # messages.success(self.request, 'Specimen updated successfully.') # return response # # # # ============================================================================ # # LAB RESULT VIEWS (APPEND-ONLY - Clinical Records) # # ============================================================================ # # class LabResultListView(LoginRequiredMixin, ListView): # """ # List all laboratory results with filtering and search. # """ # model = LabResult # template_name = 'laboratory/lab_result_list.html' # context_object_name = 'lab_results' # paginate_by = 25 # # def get_queryset(self): # queryset = LabResult.objects.filter(tenant=self.request.user.tenant) # # # Search functionality # search = self.request.GET.get('search') # if search: # queryset = queryset.filter( # Q(order__patient__first_name__icontains=search) | # Q(order__patient__last_name__icontains=search) | # Q(order__patient__mrn__icontains=search) | # Q(test__test_name__icontains=search) # ) # # # Filter by status # status = self.request.GET.get('status') # if status: # queryset = queryset.filter(status=status) # # # Filter by critical results # critical_only = self.request.GET.get('critical_only') # if critical_only: # queryset = queryset.filter(is_critical=True) # # return queryset.select_related( # 'order__patient', 'test', 'verified_by' # ).order_by('-result_datetime') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'statuses': LabResult._meta.get_field('status').choices, # }) # return context # # # class LabResultDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a laboratory result. # """ # model = LabResult # template_name = 'laboratory/lab_result_detail.html' # context_object_name = 'lab_result' # # def get_queryset(self): # return LabResult.objects.filter(tenant=self.request.user.tenant) # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # lab_result = self.object # # # Get applicable reference ranges # patient = lab_result.order.patient # context['reference_ranges'] = ReferenceRange.objects.filter( # test=lab_result.test, # tenant=self.request.user.tenant, # age_min__lte=patient.age if hasattr(patient, 'age') else 999, # age_max__gte=patient.age if hasattr(patient, 'age') else 0, # ).filter( # Q(gender=patient.gender) | Q(gender='BOTH') # ) # # return context # # # class LabResultCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new laboratory result. # """ # model = LabResult # form_class = LabResultForm # template_name = 'laboratory/lab_result_form.html' # permission_required = 'laboratory.add_labresult' # success_url = reverse_lazy('laboratory:lab_result_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.result_datetime = timezone.now() # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='LAB_RESULT_ENTERED', # model='LabResult', # object_id=str(self.object.result_id), # details={ # 'patient_name': f"{self.object.order.patient.first_name} {self.object.order.patient.last_name}", # 'test_name': self.object.test.test_name, # 'is_critical': self.object.is_critical # } # ) # # messages.success(self.request, 'Laboratory result entered successfully.') # return response # # # # Note: No UpdateView or DeleteView for LabResult - Append-only for clinical records # # Results can only be verified or corrected through addendum process # # # # ============================================================================ # # QUALITY CONTROL VIEWS (LIMITED CRUD - Operational Data) # # ============================================================================ # # class QualityControlListView(LoginRequiredMixin, ListView): # """ # List all quality control records. # """ # model = QualityControl # template_name = 'laboratory/quality_control_list.html' # context_object_name = 'qc_records' # paginate_by = 25 # # def get_queryset(self): # queryset = QualityControl.objects.filter(tenant=self.request.user.tenant) # # # Filter by test # test_id = self.request.GET.get('test') # if test_id: # queryset = queryset.filter(test_id=test_id) # # # Filter by result # result = self.request.GET.get('result') # if result: # queryset = queryset.filter(result=result) # # return queryset.select_related('test', 'performed_by').order_by('-test_date') # # def get_context_data(self, **kwargs): # context = super().get_context_data(**kwargs) # context.update({ # 'lab_tests': LabTest.objects.filter( # tenant=self.request.user.tenant, # is_active=True # ).order_by('test_name'), # 'results': QualityControl._meta.get_field('result').choices, # }) # return context # # # class QualityControlDetailView(LoginRequiredMixin, DetailView): # """ # Display detailed information about a quality control record. # """ # model = QualityControl # template_name = 'laboratory/quality_control_detail.html' # context_object_name = 'qc_record' # # def get_queryset(self): # return QualityControl.objects.filter(tenant=self.request.user.tenant) # # # class QualityControlCreateView(LoginRequiredMixin, PermissionRequiredMixin, CreateView): # """ # Create a new quality control record. # """ # model = QualityControl # form_class = QualityControlForm # template_name = 'laboratory/quality_control_form.html' # permission_required = 'laboratory.add_qualitycontrol' # success_url = reverse_lazy('laboratory:quality_control_list') # # def form_valid(self, form): # form.instance.tenant = self.request.user.tenant # form.instance.performed_by = self.request.user # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='QC_TEST_PERFORMED', # model='QualityControl', # object_id=str(self.object.id), # details={ # 'test_name': self.object.test.test_name, # 'result': self.object.result # } # ) # # messages.success(self.request, 'Quality control record created successfully.') # return response # # # class QualityControlUpdateView(LoginRequiredMixin, PermissionRequiredMixin, UpdateView): # """ # Update quality control record (limited fields). # """ # model = QualityControl # fields = ['result', 'comments'] # Limited fields for operational data # template_name = 'laboratory/quality_control_form.html' # permission_required = 'laboratory.change_qualitycontrol' # # def get_queryset(self): # return QualityControl.objects.filter(tenant=self.request.user.tenant) # # def get_success_url(self): # return reverse('laboratory:quality_control_detail', kwargs={'pk': self.object.pk}) # # def form_valid(self, form): # response = super().form_valid(form) # # # Log the action # AuditLogger.log_action( # user=self.request.user, # action='QC_TEST_UPDATED', # model='QualityControl', # object_id=str(self.object.id), # details={ # 'test_name': self.object.test.test_name, # 'changes': form.changed_data # } # ) # # messages.success(self.request, 'Quality control record updated successfully.') # return response # # # # ============================================================================ # # HTMX VIEWS FOR REAL-TIME UPDATES # # ============================================================================ # # @login_required # def laboratory_stats(request): # """ # HTMX endpoint for laboratory statistics. # """ # tenant = request.user.tenant # today = timezone.now().date() # # stats = { # 'pending_orders': LabOrder.objects.filter( # tenant=tenant, # status='PENDING' # ).count(), # 'in_progress_orders': LabOrder.objects.filter( # tenant=tenant, # status='IN_PROGRESS' # ).count(), # 'results_pending': LabResult.objects.filter( # tenant=tenant, # status='PENDING' # ).count(), # 'critical_results': LabResult.objects.filter( # tenant=tenant, # is_critical=True, # status='VERIFIED', # result_datetime__date=today # ).count(), # } # # return render(request, 'laboratory/partials/laboratory_stats.html', {'stats': stats}) # # # @login_required # def order_search(request): # """ # HTMX endpoint for order search. # """ # search = request.GET.get('search', '') # status = request.GET.get('status', '') # # queryset = LabOrder.objects.filter(tenant=request.user.tenant) # # if search: # queryset = queryset.filter( # Q(patient__first_name__icontains=search) | # Q(patient__last_name__icontains=search) | # Q(patient__mrn__icontains=search) | # Q(test__test_name__icontains=search) # ) # # if status: # queryset = queryset.filter(status=status) # # orders = queryset.select_related( # 'patient', 'test', 'ordering_provider' # ).order_by('-order_datetime')[:20] # # return render(request, 'laboratory/partials/order_list.html', {'orders': orders}) # # # # ============================================================================ # # ACTION VIEWS # # ============================================================================ # # @login_required # def receive_specimen(request, order_id): # """ # Mark specimen as received for processing. # """ # if request.method == 'POST': # order = get_object_or_404( # LabOrder, # id=order_id, # tenant=request.user.tenant # ) # # # Update specimen status # specimens = order.specimens.filter(status='COLLECTED') # specimens.update( # status='RECEIVED', # received_datetime=timezone.now(), # received_by=request.user # ) # # # Update order status # order.status = 'IN_PROGRESS' # order.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='SPECIMEN_RECEIVED', # model='LabOrder', # object_id=str(order.order_id), # details={ # 'patient_name': f"{order.patient.first_name} {order.patient.last_name}", # 'specimens_count': specimens.count() # } # ) # # messages.success(request, 'Specimen(s) marked as received.') # # if request.headers.get('HX-Request'): # return render(request, 'laboratory/partials/order_status.html', {'order': order}) # # return redirect('laboratory:lab_order_detail', pk=order.pk) # # return JsonResponse({'success': False}) # # # @login_required # def start_processing(request, order_id): # """ # Start processing laboratory order. # """ # if request.method == 'POST': # order = get_object_or_404( # LabOrder, # id=order_id, # tenant=request.user.tenant # ) # # order.status = 'IN_PROGRESS' # order.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='LAB_PROCESSING_STARTED', # model='LabOrder', # object_id=str(order.order_id), # details={ # 'patient_name': f"{order.patient.first_name} {order.patient.last_name}", # 'test_name': order.test.test_name # } # ) # # messages.success(request, 'Laboratory processing started.') # # if request.headers.get('HX-Request'): # return render(request, 'laboratory/partials/order_status.html', {'order': order}) # # return redirect('laboratory:lab_order_detail', pk=order.pk) # # return JsonResponse({'success': False}) # # # @login_required # def reject_specimen(request, specimen_id): # """ # Reject a specimen with reason. # """ # if request.method == 'POST': # specimen = get_object_or_404( # Specimen, # id=specimen_id, # tenant=request.user.tenant # ) # # rejection_reason = request.POST.get('rejection_reason', '') # # specimen.status = 'REJECTED' # specimen.rejection_reason = rejection_reason # specimen.rejected_by = request.user # specimen.rejected_datetime = timezone.now() # specimen.save() # # # Update order status # specimen.order.status = 'CANCELLED' # specimen.order.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='SPECIMEN_REJECTED', # model='Specimen', # object_id=str(specimen.specimen_id), # details={ # 'patient_name': f"{specimen.order.patient.first_name} {specimen.order.patient.last_name}", # 'rejection_reason': rejection_reason # } # ) # # messages.warning(request, 'Specimen rejected.') # # if request.headers.get('HX-Request'): # return render(request, 'laboratory/partials/specimen_status.html', {'specimen': specimen}) # # return redirect('laboratory:specimen_detail', pk=specimen.pk) # # return JsonResponse({'success': False}) # # # @login_required # def schedule_collection(request, order_id): # """ # Schedule specimen collection for an order. # """ # if request.method == 'POST': # order = get_object_or_404( # LabOrder, # id=order_id, # tenant=request.user.tenant # ) # # collection_datetime = request.POST.get('collection_datetime') # if collection_datetime: # collection_datetime = timezone.datetime.fromisoformat(collection_datetime) # # order.scheduled_collection = collection_datetime # order.status = 'SCHEDULED' # order.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='COLLECTION_SCHEDULED', # model='LabOrder', # object_id=str(order.order_id), # details={ # 'patient_name': f"{order.patient.first_name} {order.patient.last_name}", # 'scheduled_time': collection_datetime.isoformat() # } # ) # # messages.success(request, 'Collection scheduled successfully.') # # if request.headers.get('HX-Request'): # return render(request, 'laboratory/partials/order_status.html', {'order': order}) # # return redirect('laboratory:lab_order_detail', pk=order.pk) # # return JsonResponse({'success': False}) # # # @login_required # def mark_collected(request, order_id): # """ # Mark specimen as collected. # """ # if request.method == 'POST': # order = get_object_or_404( # LabOrder, # id=order_id, # tenant=request.user.tenant # ) # # # Create specimen record if it doesn't exist # specimen, created = Specimen.objects.get_or_create( # order=order, # tenant=request.user.tenant, # defaults={ # 'specimen_type': 'BLOOD', # Default, should be specified # 'collection_datetime': timezone.now(), # 'collected_by': request.user, # 'status': 'COLLECTED' # } # ) # # if not created: # specimen.status = 'COLLECTED' # specimen.collection_datetime = timezone.now() # specimen.collected_by = request.user # specimen.save() # # # Update order status # order.status = 'COLLECTED' # order.save() # # # Log the action # AuditLogger.log_action( # user=request.user, # action='SPECIMEN_COLLECTED', # model='LabOrder', # object_id=str(order.order_id), # details={ # 'patient_name': f"{order.patient.first_name} {order.patient.last_name}", # 'specimen_type': specimen.specimen_type # } # ) # # messages.success(request, 'Specimen marked as collected.') # # if request.headers.get('HX-Request'): # return render(request, 'laboratory/partials/order_status.html', {'order': order}) # # return redirect('laboratory:lab_order_detail', pk=order.pk) # # return JsonResponse({'success': False}) # # # @login_required def verify_result(request, result_id): """ Verify a laboratory result. """ if request.method == 'POST': result = get_object_or_404( LabResult, id=result_id, order__tenant=request.user.tenant ) if result.status != 'PENDING': return JsonResponse({'success': False, 'error': 'Result is not pending verification'}) result.status = 'VERIFIED' result.verified_by = request.user result.verified_datetime = timezone.now() result.save() # Log the action AuditLogger.log_action( user=request.user, action='LAB_RESULT_VERIFIED', model='LabResult', object_id=str(result.result_id), details={ 'patient_name': f"{result.order.patient.first_name} {result.order.patient.last_name}", 'test_name': result.test.test_name, 'result_value': result.value } ) messages.success(request, 'Result verified successfully.') if request.headers.get('HX-Request'): return render(request, 'laboratory/partials/result_row.html', {'result': result}) return redirect('laboratory:lab_result_list') return JsonResponse({'success': False}) @login_required def review_result(request, result_id): """ Mark a laboratory result as reviewed. """ if request.method == 'POST': result = get_object_or_404( LabResult, id=result_id, order__tenant=request.user.tenant ) # Add reviewed status or field if it exists in your model # For now, we'll use a custom field or status if hasattr(result, 'reviewed_by'): result.reviewed_by = request.user result.reviewed_datetime = timezone.now() result.save() # Log the action AuditLogger.log_action( user=request.user, action='LAB_RESULT_REVIEWED', model='LabResult', object_id=str(result.result_id), details={ 'patient_name': f"{result.order.patient.first_name} {result.order.patient.last_name}", 'test_name': result.test.test_name, 'result_value': result.value } ) return JsonResponse({'success': True, 'message': 'Result marked as reviewed'}) else: return JsonResponse({'success': False, 'error': 'Review functionality not available'}) return JsonResponse({'success': False, 'error': 'Invalid request method'}) @login_required def email_result(request, result_id): """ Email a laboratory result to specified address. """ if request.method == 'POST': result = get_object_or_404( LabResult, id=result_id, order__tenant=request.user.tenant ) email = request.POST.get('email') or request.GET.get('email') if not email: return JsonResponse({'success': False, 'error': 'Email address is required'}) # Here you would implement actual email sending # For now, we'll simulate success try: # TODO: Implement actual email sending logic # from django.core.mail import send_mail # send_mail( # f'Laboratory Result: {result.test.test_name}', # f'Patient: {result.order.patient.get_full_name()}\nResult: {result.value} {result.unit}\nStatus: {result.get_status_display()}', # 'noreply@hospital.com', # [email], # fail_silently=False, # ) # Log the action AuditLogger.log_action( user=request.user, action='LAB_RESULT_EMAILED', model='LabResult', object_id=str(result.result_id), details={ 'patient_name': f"{result.order.patient.first_name} {result.order.patient.last_name}", 'test_name': result.test.test_name, 'email_recipient': email } ) return JsonResponse({'success': True, 'message': f'Result emailed to {email}'}) except Exception as e: return JsonResponse({'success': False, 'error': f'Failed to send email: {str(e)}'}) return JsonResponse({'success': False, 'error': 'Invalid request method'}) @login_required def notify_critical(request, result_id): """ Send critical result notification to ordering provider. """ if request.method == 'POST': result = get_object_or_404( LabResult, id=result_id, order__tenant=request.user.tenant ) if result.critical_flag != 'CRITICAL': return JsonResponse({'success': False, 'error': 'Result is not critical'}) # Here you would implement actual notification sending # For now, we'll simulate success try: # TODO: Implement actual notification logic (email, SMS, etc.) # This could send notifications to: # - Ordering provider # - Primary care physician # - Critical result notification system # Log the action AuditLogger.log_action( user=request.user, action='CRITICAL_RESULT_NOTIFIED', model='LabResult', object_id=str(result.result_id), details={ 'patient_name': f"{result.order.patient.first_name} {result.order.patient.last_name}", 'test_name': result.test.test_name, 'result_value': result.value, 'ordering_provider': result.order.ordering_provider.get_full_name() if result.order.ordering_provider else 'Unknown' } ) return JsonResponse({'success': True, 'message': 'Critical result notification sent'}) except Exception as e: return JsonResponse({'success': False, 'error': f'Failed to send notification: {str(e)}'}) return JsonResponse({'success': False, 'error': 'Invalid request method'}) @login_required def bulk_verify_results(request): """ Bulk verify multiple laboratory results. """ if request.method == 'POST': result_ids = request.POST.getlist('result_ids[]') tenant = request.user.tenant results = LabResult.objects.filter( id__in=result_ids, order__tenant=tenant, status='PENDING' ) verified_count = results.update( status='VERIFIED', verified_by=request.user, verified_datetime=timezone.now() ) # Log the action AuditLogger.log_action( user=request.user, action='LAB_RESULTS_BULK_VERIFIED', model='LabResult', object_id=','.join(result_ids), details={ 'verified_count': verified_count, 'total_requested': len(result_ids) } ) messages.success(request, f'{verified_count} results verified successfully.') if request.headers.get('HX-Request'): return JsonResponse({'success': True, 'verified_count': verified_count}) return redirect('laboratory:lab_result_list') return JsonResponse({'success': False}) @login_required def bulk_release_results(request): """ Bulk release multiple laboratory results. """ if request.method == 'POST': result_ids = request.POST.getlist('result_ids[]') tenant = request.user.tenant results = LabResult.objects.filter( id__in=result_ids, order__tenant=tenant, status='VERIFIED' ) released_count = results.update(status='RELEASED') # Log the action AuditLogger.log_action( user=request.user, action='LAB_RESULTS_BULK_RELEASED', model='LabResult', object_id=','.join(result_ids), details={ 'released_count': released_count, 'total_requested': len(result_ids) } ) messages.success(request, f'{released_count} results released successfully.') if request.headers.get('HX-Request'): return JsonResponse({'success': True, 'released_count': released_count}) return redirect('laboratory:lab_result_list') return JsonResponse({'success': False}) @login_required def print_result_reports(request): """ Print reports for selected laboratory results. """ ids = request.GET.get('ids', '') if not ids: return HttpResponse('No results selected', status=400) result_ids = ids.split(',') tenant = request.user.tenant results = LabResult.objects.filter( id__in=result_ids, order__tenant=tenant ).select_related('order__patient', 'test', 'verified_by') # Generate PDF or HTML report context = { 'results': results, 'generated_at': timezone.now(), 'generated_by': request.user } return render(request, 'laboratory/reports/result_reports.html', context) @login_required def export_results(request): """ Export laboratory results to CSV/Excel. """ tenant = request.user.tenant # Get filter parameters status = request.GET.get('status') date_from = request.GET.get('date_from') date_to = request.GET.get('date_to') queryset = LabResult.objects.filter(order__tenant=tenant) if status: queryset = queryset.filter(status=status) if date_from: queryset = queryset.filter(result_date__gte=date_from) if date_to: queryset = queryset.filter(result_date__lte=date_to) results = queryset.select_related('order__patient', 'test', 'verified_by') # Generate CSV response import csv from django.http import HttpResponse response = HttpResponse(content_type='text/csv') response['Content-Disposition'] = 'attachment; filename="lab_results.csv"' writer = csv.writer(response) writer.writerow([ 'Result ID', 'Patient Name', 'Patient ID', 'Test Name', 'Result Value', 'Unit', 'Reference Range', 'Status', 'Result Date', 'Technologist', 'Verified By', 'Verified Date' ]) for result in results: writer.writerow([ result.result_id, result.order.patient.get_full_name(), result.order.patient.patient_id, result.test.test_name, result.value, result.unit or '', result.reference_range or '', result.get_status_display(), result.result_date.strftime('%Y-%m-%d %H:%M'), result.technologist.get_full_name() if result.technologist else '', result.verified_by.get_full_name() if result.verified_by else '', result.verified_datetime.strftime('%Y-%m-%d %H:%M') if result.verified_datetime else '' ]) return response @login_required def htmx_result_stats(request): """ HTMX endpoint for result statistics. """ tenant = request.user.tenant stats = { 'total_results': LabResult.objects.filter(order__tenant=tenant).count(), 'pending_review': LabResult.objects.filter(order__tenant=tenant, status='PENDING').count(), 'critical_results': LabResult.objects.filter(order__tenant=tenant, is_critical=True).count(), 'verified_results': LabResult.objects.filter(order__tenant=tenant, status='VERIFIED').count(), } return render(request, 'laboratory/partials/result_stats.html', {'stats': stats}) @login_required def htmx_filter_results(request): """ HTMX endpoint for filtering results. """ tenant = request.user.tenant status = request.GET.get('status') critical = request.GET.get('critical') date_period = request.GET.get('date') queryset = LabResult.objects.filter(order__tenant=tenant) # Apply filters if status and status != 'all': queryset = queryset.filter(status=status) if critical and critical != 'all': queryset = queryset.filter(critical_flag=critical) # Date filtering if date_period and date_period != 'all': today = timezone.now().date() if date_period == 'today': queryset = queryset.filter(result_date__date=today) elif date_period == 'week': week_ago = today - timedelta(days=7) queryset = queryset.filter(result_date__date__gte=week_ago) elif date_period == 'month': month_ago = today - timedelta(days=30) queryset = queryset.filter(result_date__date__gte=month_ago) results = queryset.select_related( 'order__patient', 'test', 'verified_by' ).order_by('-created_at')[:50] # Limit for performance return render(request, 'laboratory/partials/result_list.html', {'object_list': results}) @login_required def htmx_search_results(request): """ HTMX endpoint for searching results. """ tenant = request.user.tenant search_term = request.GET.get('search', '').strip() if not search_term: return render(request, 'laboratory/partials/result_list.html', {'object_list': []}) queryset = LabResult.objects.filter(order__tenant=tenant).filter( Q(order__patient__first_name__icontains=search_term) | Q(order__patient__last_name__icontains=search_term) | Q(order__patient__mrn__icontains=search_term) | Q(test__test_name__icontains=search_term) | Q(result_value__icontains=search_term) ).select_related( 'order__patient', 'test', 'verified_by' ).order_by('-analyzed_datetime')[:50] return render(request, 'laboratory/partials/result_list.html', {'object_list': queryset}) @login_required def check_critical_results(request): """ API endpoint to check for critical results that need attention. Referenced in dashboard template. """ tenant = request.user.tenant # Get critical results that haven't been called yet critical_results = LabResult.objects.filter( order__tenant=tenant, is_critical=True, critical_called=False, status='VERIFIED' ).count() return JsonResponse({ 'critical_count': critical_results, 'success': True })