Source code for claims.views

import logging
from typing import Any, Dict

from django.contrib.auth.mixins import LoginRequiredMixin
from django.db.models import Q, QuerySet
from django.http import HttpRequest, HttpResponse
from django.shortcuts import get_object_or_404, render
from django.utils import timezone
from django.views.generic import DetailView, ListView, View, FormView
from django.urls import reverse_lazy
from django.contrib import messages
from .forms import RegistrationForm

from .models import Claim, Note

logger = logging.getLogger(__name__)


[docs] class ClaimListView(LoginRequiredMixin, ListView): """ Displays a list of all claims in the Claims Dashboard. This view supports: - Searching by insurer name and patient name. - Filtering by claim status and flagged status. - Sorting on multiple fields. - HTMX-powered partial updates for filtering, sorting, and infinite scroll. """ model = Claim template_name = "claims/claim_list.html" context_object_name = "claims" paginate_by = 50 def _apply_search_filter(self, queryset: QuerySet, search_query: str) -> QuerySet: """Applies a search filter to the queryset based on patient or insurer name.""" if search_query: return queryset.filter( Q(patient_name__icontains=search_query) | Q(insurer_name__icontains=search_query) ) return queryset def _apply_status_filter(self, queryset: QuerySet, status_filter: str) -> QuerySet: """Applies a filter for a specific claim status.""" if status_filter: return queryset.filter(status=status_filter.upper()) return queryset def _apply_flagged_filter( self, queryset: QuerySet, flagged_filter: str ) -> QuerySet: """Applies a filter to show only flagged claims.""" if flagged_filter == "true": return queryset.filter(is_flagged=True) return queryset def _apply_sorting(self, queryset: QuerySet, sort_by: str) -> QuerySet: """Applies sorting to the queryset based on the provided field.""" allowed_sort_fields = [ "id", "patient_name", "billed_amount", "paid_amount", "status", "insurer_name", "discharge_date", ] # Fallback to a default sort order if the requested field is not allowed. sort_field = sort_by.lstrip("-") if sort_field not in allowed_sort_fields: sort_by = "-discharge_date" return queryset.order_by(sort_by)
[docs] def get_queryset(self) -> QuerySet[Claim]: """ Overrides the default queryset to implement search, filter, and sorting logic. """ queryset = super().get_queryset().select_related("details") # Get search/filter parameters from the request URL. search_query = self.request.GET.get("search", "") status_filter = self.request.GET.get("status", "") flagged_filter = self.request.GET.get("flagged", "") sort_by = self.request.GET.get("sort", "id") # Default sort. # Chain the filtering and sorting methods. queryset = self._apply_search_filter(queryset, search_query) queryset = self._apply_status_filter(queryset, status_filter) queryset = self._apply_flagged_filter(queryset, flagged_filter) queryset = self._apply_sorting(queryset, sort_by) return queryset
[docs] def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: """ Adds filter and sorting values to the context for use in the template. """ context = super().get_context_data(**kwargs) # Pass the request object to the template for the custom template tags. context["request"] = self.request # Provide current filter values to the template to repopulate controls. context["claim_statuses"] = Claim.ClaimStatus.choices context["current_search"] = self.request.GET.get("search", "") context["current_status"] = self.request.GET.get("status", "") context["current_flagged"] = self.request.GET.get("flagged", "") # Provide current sort values to the template. sort_param = self.request.GET.get("sort", "-discharge_date") context["current_sort_param"] = sort_param context["current_sort_field"] = sort_param.lstrip("-") context["current_sort_dir"] = "desc" if sort_param.startswith("-") else "asc" return context
[docs] def get_template_names(self) -> list[str]: """ If the request is from HTMX, return the appropriate partial template. - For infinite scroll, return just the table rows. - For sorting/filtering, return the entire table body. """ if self.request.htmx: if self.request.GET.get("_partial") == "rows": return ["claims/partials/_claim_rows.html"] return ["claims/partials/_claims_table.html"] return [self.template_name]
[docs] class ClaimDetailView(LoginRequiredMixin, DetailView): """ Handles fetching and displaying the details for a single claim. This view is designed to be rendered within the main list view via an HTMX request, to avoid a full page reload. """ model = Claim template_name = "claims/partials/_claim_detail.html" context_object_name = "claim" pk_url_kwarg = "claim_id"
[docs] def get_context_data(self, **kwargs: Any) -> Dict[str, Any]: """Adds the claim's notes to the context, ordered by most recent first.""" context = super().get_context_data(**kwargs) context["notes"] = self.object.notes.all().order_by("-created_at") return context
[docs] class ToggleFlagView(LoginRequiredMixin, View): """ Handles POST requests to toggle the 'is_flagged' status of a claim. This view returns an HTML partial of the updated flag button and triggers an HTMX event to refresh the claim detail view if it's open. """ def post(self, request: HttpRequest, claim_id: int) -> HttpResponse: claim = get_object_or_404(Claim, id=claim_id) claim.is_flagged = not claim.is_flagged if claim.is_flagged: claim.flagged_by = request.user claim.flagged_at = timezone.now() else: claim.flagged_by = None claim.flagged_at = None claim.save(update_fields=["is_flagged", "flagged_by", "flagged_at"]) logger.info( f"User '{request.user.username}' (ID: {request.user.id}) " f"set is_flagged to {claim.is_flagged} for Claim ID {claim_id}." ) # Render the button's HTML partial in response to the POST request. response = render( request, "claims/partials/_flag_button.html", {"claim": claim} ) # Add a special HTMX header to broadcast an event with the claim's ID. # This allows other components on the page to react to the change. response["HX-Trigger"] = f"refresh-claim-detail-{claim_id}" return response
[docs] class AddNoteView(LoginRequiredMixin, View): """ Handles POST requests to add a new note to a claim. Returns an HTML partial of the updated notes section. """ def post(self, request: HttpRequest, claim_id: int) -> HttpResponse: claim = get_object_or_404(Claim, id=claim_id) note_text = request.POST.get("note", "").strip() if note_text: note = Note.objects.create(claim=claim, note=note_text, user=request.user) logger.info( f"User '{request.user.username}' (ID: {request.user.id}) " f"added Note ID {note.id} to Claim ID {claim_id}." ) # Return the updated notes list, ordered by most recent first. notes = claim.notes.all().order_by("-created_at") return render(request, "claims/partials/_notes_section.html", {"notes": notes})
[docs] class FlagButtonView(LoginRequiredMixin, View): """ Returns the current flag button partial for a claim without mutating state. Used to refresh the flag icon in the list row when the flag is toggled elsewhere. """ def get(self, request: HttpRequest, claim_id: int) -> HttpResponse: claim = get_object_or_404(Claim, id=claim_id) return render(request, "claims/partials/_flag_button.html", {"claim": claim})
[docs] class RegisterView(FormView): """Allow users to create an account and then log in.""" template_name = "registration/register.html" form_class = RegistrationForm success_url = reverse_lazy("login")
[docs] def form_valid(self, form) -> HttpResponse: form.save() messages.success(self.request, "Account created! You can now log in.") return super().form_valid(form)