HH/apps/surveys/management/commands/mark_abandoned_surveys.py
2026-01-24 15:27:30 +03:00

122 lines
4.3 KiB
Python

"""
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"
))