HH/apps/physicians/tasks.py
2026-02-22 08:35:53 +03:00

262 lines
8.6 KiB
Python

"""
Physicians Celery Tasks
Background tasks for:
- Processing doctor rating import jobs
- Monthly aggregation of ratings
- Ranking updates
"""
import logging
from celery import shared_task
from django.utils import timezone
from apps.organizations.models import Hospital
from .adapter import DoctorRatingAdapter
from .models import DoctorRatingImportJob, PhysicianIndividualRating, PhysicianMonthlyRating
logger = logging.getLogger(__name__)
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def process_doctor_rating_job(self, job_id: str):
"""
Process a doctor rating import job in the background.
This task is called when a bulk import is queued (from API or CSV upload).
"""
try:
job = DoctorRatingImportJob.objects.get(id=job_id)
except DoctorRatingImportJob.DoesNotExist:
logger.error(f"Doctor rating import job {job_id} not found")
return {'error': 'Job not found'}
try:
# Update job status
job.status = DoctorRatingImportJob.JobStatus.PROCESSING
job.started_at = timezone.now()
job.save()
logger.info(f"Starting doctor rating import job {job_id}: {job.total_records} records")
# Get raw data
records = job.raw_data
hospital = job.hospital
# Process through adapter
results = DoctorRatingAdapter.process_bulk_ratings(
records=records,
hospital=hospital,
job=job
)
logger.info(f"Completed doctor rating import job {job_id}: "
f"{results['success']} success, {results['failed']} failed")
return {
'job_id': job_id,
'total': results['total'],
'success': results['success'],
'failed': results['failed'],
'skipped': results['skipped'],
'staff_matched': results['staff_matched']
}
except Exception as exc:
logger.error(f"Error processing doctor rating job {job_id}: {str(exc)}", exc_info=True)
# Update job status
job.status = DoctorRatingImportJob.JobStatus.FAILED
job.error_message = str(exc)
job.completed_at = timezone.now()
job.save()
# Retry
raise self.retry(exc=exc)
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def aggregate_monthly_ratings_task(self, year: int, month: int, hospital_id: str = None):
"""
Aggregate individual ratings into monthly summaries.
Args:
year: Year to aggregate
month: Month to aggregate (1-12)
hospital_id: Optional hospital ID to filter by
"""
try:
logger.info(f"Starting monthly aggregation for {year}-{month:02d}")
hospital = None
if hospital_id:
try:
hospital = Hospital.objects.get(id=hospital_id)
except Hospital.DoesNotExist:
logger.error(f"Hospital {hospital_id} not found")
return {'error': 'Hospital not found'}
# Run aggregation
results = DoctorRatingAdapter.aggregate_monthly_ratings(
year=year,
month=month,
hospital=hospital
)
logger.info(f"Completed monthly aggregation for {year}-{month:02d}: "
f"{results['aggregated']} physicians aggregated")
# Calculate rankings after aggregation
if hospital:
update_hospital_rankings.delay(year, month, hospital_id)
else:
# Update rankings for all hospitals
for h in Hospital.objects.filter(status='active'):
update_hospital_rankings.delay(year, month, str(h.id))
return {
'year': year,
'month': month,
'hospital_id': hospital_id,
'aggregated': results['aggregated'],
'errors': len(results['errors'])
}
except Exception as exc:
logger.error(f"Error aggregating monthly ratings: {str(exc)}", exc_info=True)
raise self.retry(exc=exc)
@shared_task(bind=True, max_retries=3, default_retry_delay=60)
def update_hospital_rankings(self, year: int, month: int, hospital_id: str):
"""
Update hospital and department rankings for physicians.
This should be called after monthly aggregation is complete.
"""
try:
from django.db.models import Window, F
from django.db.models.functions import RowNumber
hospital = Hospital.objects.get(id=hospital_id)
logger.info(f"Updating rankings for {hospital.name} - {year}-{month:02d}")
# Get all ratings for this hospital and period
ratings = PhysicianMonthlyRating.objects.filter(
staff__hospital=hospital,
year=year,
month=month
).select_related('staff', 'staff__department')
# Update hospital rankings (order by average_rating desc)
hospital_rankings = list(ratings.order_by('-average_rating'))
for rank, rating in enumerate(hospital_rankings, start=1):
rating.hospital_rank = rank
rating.save(update_fields=['hospital_rank'])
# Update department rankings
from apps.organizations.models import Department
departments = Department.objects.filter(hospital=hospital)
for dept in departments:
dept_ratings = ratings.filter(staff__department=dept).order_by('-average_rating')
for rank, rating in enumerate(dept_ratings, start=1):
rating.department_rank = rank
rating.save(update_fields=['department_rank'])
logger.info(f"Updated rankings for {hospital.name}: "
f"{len(hospital_rankings)} physicians ranked")
return {
'hospital_id': hospital_id,
'hospital_name': hospital.name,
'year': year,
'month': month,
'total_ranked': len(hospital_rankings)
}
except Exception as exc:
logger.error(f"Error updating rankings: {str(exc)}", exc_info=True)
raise self.retry(exc=exc)
@shared_task
def auto_aggregate_daily():
"""
Daily task to automatically aggregate unaggregated ratings.
This task should be scheduled to run daily to keep monthly ratings up-to-date.
"""
try:
logger.info("Starting daily auto-aggregation of doctor ratings")
# Find months with unaggregated ratings
unaggregated = PhysicianIndividualRating.objects.filter(
is_aggregated=False
).values('rating_date__year', 'rating_date__month').distinct()
aggregated_count = 0
for item in unaggregated:
year = item['rating_date__year']
month = item['rating_date__month']
# Aggregate for each hospital separately
hospitals_with_ratings = PhysicianIndividualRating.objects.filter(
is_aggregated=False,
rating_date__year=year,
rating_date__month=month
).values_list('hospital', flat=True).distinct()
for hospital_id in hospitals_with_ratings:
results = DoctorRatingAdapter.aggregate_monthly_ratings(
year=year,
month=month,
hospital_id=hospital_id
)
aggregated_count += results['aggregated']
logger.info(f"Daily auto-aggregation complete: {aggregated_count} physicians updated")
return {
'aggregated_count': aggregated_count
}
except Exception as e:
logger.error(f"Error in daily auto-aggregation: {str(e)}", exc_info=True)
return {'error': str(e)}
@shared_task
def cleanup_old_import_jobs(days: int = 30):
"""
Clean up old completed import jobs and their raw data.
Args:
days: Delete jobs older than this many days
"""
from datetime import timedelta
cutoff_date = timezone.now() - timedelta(days=days)
old_jobs = DoctorRatingImportJob.objects.filter(
created_at__lt=cutoff_date,
status__in=[
DoctorRatingImportJob.JobStatus.COMPLETED,
DoctorRatingImportJob.JobStatus.FAILED
]
)
count = old_jobs.count()
# Clear raw data first to save space
for job in old_jobs:
if job.raw_data:
job.raw_data = []
job.save(update_fields=['raw_data'])
logger.info(f"Cleaned up {count} old doctor rating import jobs")
return {'cleaned_count': count}