""" 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 django.utils.dateparse import parse_duration 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)" )) # Calculate cutoff time cutoff_time = timezone.now() - timedelta(hours=hours) # Find surveys that should be marked as abandoned # Criteria: # 1. Status is 'viewed' or 'in_progress' # 2. Token hasn't expired # 3. Last opened at least X hours ago # 4. Not already abandoned, completed, expired, or cancelled surveys_to_abandon = SurveyInstance.objects.filter( status__in=['viewed', 'in_progress'], token_expires_at__gt=timezone.now(), # Not expired 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 # Get question count for this survey 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 # Mark surveys as abandoned updated = 0 for survey in surveys_to_abandon: # Update status survey.status = 'abandoned' survey.save(update_fields=['status']) # Track abandonment event 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': survey.status, } ) updated += 1 self.stdout.write(self.style.SUCCESS( f"\nSuccessfully marked {updated} surveys as abandoned" ))