""" Management command to mark surveys as abandoned. Marks surveys that have been opened or started but not completed within a configurable time period as abandoned. Usage: python manage.py mark_abandoned_surveys python manage.py mark_abandoned_surveys --hours 24 python manage.py mark_abandoned_surveys --dry-run """ from django.core.management.base import BaseCommand from django.utils import timezone from django.conf import settings from datetime import timedelta from apps.surveys.models import SurveyInstance, SurveyTracking class Command(BaseCommand): help = "Mark surveys as abandoned if not completed within specified time" def add_arguments(self, parser): parser.add_argument( "--hours", type=int, default=getattr(settings, "SURVEY_ABANDONMENT_HOURS", 24), help="Hours after which to mark survey as abandoned (default: 24)", ) parser.add_argument("--dry-run", action="store_true", help="Show what would be done without making changes") def handle(self, *args, **options): hours = options["hours"] dry_run = options["dry_run"] self.stdout.write( self.style.SUCCESS(f"{'[DRY RUN] ' if dry_run else ''}Marking surveys as abandoned (after {hours} hours)") ) cutoff_time = timezone.now() - timedelta(hours=hours) surveys_to_abandon = SurveyInstance.objects.filter( status__in=["viewed", "in_progress"], token_expires_at__gt=timezone.now(), last_opened_at__lt=cutoff_time ).select_related("survey_template", "patient") count = surveys_to_abandon.count() if count == 0: self.stdout.write(self.style.WARNING("No surveys to mark as abandoned")) return self.stdout.write(f"Found {count} surveys to mark as abandoned:") for survey in surveys_to_abandon: time_since_open = timezone.now() - survey.last_opened_at hours_since_open = time_since_open.total_seconds() / 3600 tracking_events = survey.tracking_events.filter(event_type="question_answered").count() self.stdout.write( f" - {survey.survey_template.name} | " f"Patient: {survey.patient.get_full_name()} | " f"Status: {survey.status} | " f"Opened: {survey.last_opened_at.strftime('%Y-%m-%d %H:%M')} | " f"{hours_since_open:.1f} hours ago | " f"Questions answered: {tracking_events}" ) if dry_run: self.stdout.write(self.style.WARNING(f"\n[DRY RUN] Would mark {count} surveys as abandoned")) return updated = 0 for survey in surveys_to_abandon: original_status = survey.status time_since_open = timezone.now() - survey.last_opened_at survey.status = "abandoned" survey.save(update_fields=["status"]) tracking_events = survey.tracking_events.filter(event_type="question_answered") SurveyTracking.objects.create( survey_instance=survey, event_type="survey_abandoned", current_question=tracking_events.count(), total_time_spent=survey.time_spent_seconds or 0, metadata={ "time_since_open_hours": round(time_since_open.total_seconds() / 3600, 2), "questions_answered": tracking_events.count(), "original_status": original_status, }, ) updated += 1 self.stdout.write(self.style.SUCCESS(f"\nSuccessfully marked {updated} surveys as abandoned"))