127 lines
4.7 KiB
Python
127 lines
4.7 KiB
Python
"""
|
|
Management command to backfill monthly physician ratings from existing
|
|
individual ratings that have not yet been aggregated.
|
|
|
|
Usage:
|
|
python manage.py backfill_physician_monthly_ratings
|
|
python manage.py backfill_physician_monthly_ratings --year 2026 --month 4
|
|
python manage.py backfill_physician_monthly_ratings --dry-run
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from django.core.management.base import BaseCommand
|
|
|
|
from apps.physicians.adapter import DoctorRatingAdapter
|
|
from apps.physicians.models import PhysicianIndividualRating
|
|
from apps.organizations.models import Hospital
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
|
|
class Command(BaseCommand):
|
|
help = "Backfill PhysicianMonthlyRating aggregates from unaggregated PhysicianIndividualRating records"
|
|
|
|
def add_arguments(self, parser):
|
|
parser.add_argument("--year", type=int, help="Specific year to backfill (e.g. 2026)")
|
|
parser.add_argument("--month", type=int, help="Specific month to backfill (1-12)")
|
|
parser.add_argument("--hospital-id", type=str, help="Optional hospital UUID to limit backfill")
|
|
parser.add_argument(
|
|
"--dry-run",
|
|
action="store_true",
|
|
help="Show what would be aggregated without saving changes",
|
|
)
|
|
|
|
def handle(self, *args, **options):
|
|
year: Optional[int] = options.get("year")
|
|
month: Optional[int] = options.get("month")
|
|
hospital_id: Optional[str] = options.get("hospital_id")
|
|
dry_run: bool = options["dry_run"]
|
|
|
|
hospital = None
|
|
if hospital_id:
|
|
try:
|
|
hospital = Hospital.objects.get(id=hospital_id)
|
|
except Hospital.DoesNotExist:
|
|
self.stdout.write(self.style.ERROR(f"Hospital {hospital_id} not found"))
|
|
return
|
|
|
|
# Build queryset of unaggregated records
|
|
qs = PhysicianIndividualRating.objects.filter(is_aggregated=False)
|
|
if year:
|
|
qs = qs.filter(rating_date__year=year)
|
|
if month:
|
|
qs = qs.filter(rating_date__month=month)
|
|
if hospital:
|
|
qs = qs.filter(hospital=hospital)
|
|
|
|
total_unaggregated = qs.count()
|
|
if total_unaggregated == 0:
|
|
self.stdout.write(self.style.SUCCESS("No unaggregated individual ratings found."))
|
|
return
|
|
|
|
self.stdout.write(f"Found {total_unaggregated} unaggregated individual rating(s).")
|
|
|
|
# Determine distinct periods/hospitals to process
|
|
periods = (
|
|
qs.values("rating_date__year", "rating_date__month", "hospital")
|
|
.distinct()
|
|
.order_by("rating_date__year", "rating_date__month", "hospital")
|
|
)
|
|
|
|
total_aggregated = 0
|
|
total_skipped = 0
|
|
total_errors = 0
|
|
|
|
for period in periods:
|
|
p_year = period["rating_date__year"]
|
|
p_month = period["rating_date__month"]
|
|
p_hospital_id = period["hospital"]
|
|
|
|
p_hospital = None
|
|
if p_hospital_id:
|
|
try:
|
|
p_hospital = Hospital.objects.get(id=p_hospital_id)
|
|
except Hospital.DoesNotExist:
|
|
self.stdout.write(
|
|
self.style.WARNING(f"Skipping {p_year}-{p_month:02d} hospital {p_hospital_id} (not found)")
|
|
)
|
|
continue
|
|
|
|
label = f"{p_year}-{p_month:02d}"
|
|
if p_hospital:
|
|
label += f" ({p_hospital.name})"
|
|
|
|
if dry_run:
|
|
count = qs.filter(
|
|
rating_date__year=p_year,
|
|
rating_date__month=p_month,
|
|
hospital=p_hospital_id,
|
|
).count()
|
|
self.stdout.write(f"[DRY RUN] Would aggregate {count} rating(s) for {label}")
|
|
continue
|
|
|
|
self.stdout.write(f"Aggregating {label} ...")
|
|
results = DoctorRatingAdapter.aggregate_monthly_ratings(year=p_year, month=p_month, hospital=p_hospital)
|
|
total_aggregated += results["aggregated"]
|
|
total_skipped += results.get("skipped_unlinked", 0)
|
|
total_errors += len(results["errors"])
|
|
|
|
self.stdout.write(
|
|
f" -> Aggregated: {results['aggregated']}, "
|
|
f"Skipped (unlinked): {results.get('skipped_unlinked', 0)}, "
|
|
f"Errors: {len(results['errors'])}"
|
|
)
|
|
|
|
if not dry_run:
|
|
self.stdout.write(
|
|
self.style.SUCCESS(
|
|
f"\nBackfill complete: {total_aggregated} monthly record(s) updated, "
|
|
f"{total_skipped} unlinked rating(s) skipped, "
|
|
f"{total_errors} error(s)."
|
|
)
|
|
)
|
|
else:
|
|
self.stdout.write(self.style.NOTICE("\nDry run complete. No changes were saved."))
|