Source code for promapp.schedule_forms

from django import forms
from django.core.exceptions import ValidationError
from django.utils.translation import gettext_lazy as _
from django.utils import timezone
from datetime import datetime, timedelta
from .models import QuestionnairePatientSchedule, PatientQuestionnaire, Questionnaire
import json


[docs] class QuestionnaireScheduleForm(forms.Form): """ Form for scheduling questionnaires for a patient. Allows selecting multiple questionnaires and multiple dates. """ questionnaires = forms.MultipleChoiceField( required=True, widget=forms.CheckboxSelectMultiple(attrs={ 'class': 'schedule-questionnaire-checkbox' }), label=_('Select Questionnaires to Schedule'), help_text=_('Select one or more questionnaires to schedule') ) scheduled_dates = forms.CharField( required=True, widget=forms.HiddenInput(attrs={ 'id': 'scheduled-dates-input', 'class': 'scheduled-dates-field' }), label=_('Scheduled Dates'), help_text=_('Dates will be stored here from the calendar widget') )
[docs] def __init__(self, patient, *args, **kwargs): super().__init__(*args, **kwargs) self.patient = patient # Get assigned questionnaires for this patient assigned_pqs = PatientQuestionnaire.objects.filter( patient=patient ).select_related('questionnaire') # Build choices with questionnaire info choices = [] self.questionnaire_intervals = {} for pq in assigned_pqs: questionnaire = pq.questionnaire # Get translated name questionnaire_name = questionnaire.safe_translation_getter('name', any_language=True) or str(questionnaire.id) # Store interval for validation self.questionnaire_intervals[str(pq.id)] = questionnaire.questionnaire_answer_interval # Format interval for display interval_seconds = questionnaire.questionnaire_answer_interval if interval_seconds == 0: interval_display = _('No interval restriction') elif interval_seconds < 3600: minutes = interval_seconds // 60 interval_display = _(f'{minutes} minute(s)') elif interval_seconds < 86400: hours = interval_seconds // 3600 interval_display = _(f'{hours} hour(s)') else: days = interval_seconds // 86400 interval_display = _(f'{days} day(s)') label = f"{questionnaire_name} - Interval: {interval_display}" choices.append((str(pq.id), label)) self.fields['questionnaires'].choices = choices # Store intervals as JSON in a data attribute for JavaScript self.questionnaire_intervals_json = json.dumps(self.questionnaire_intervals)
[docs] def clean_scheduled_dates(self): """ Validate that scheduled dates are provided and in correct format. Expected format: JSON array of ISO datetime strings """ dates_json = self.cleaned_data.get('scheduled_dates', '') if not dates_json: raise ValidationError(_('Please select at least one date for scheduling.')) try: dates_list = json.loads(dates_json) except json.JSONDecodeError: raise ValidationError(_('Invalid date format. Please use the calendar widget.')) if not dates_list or not isinstance(dates_list, list): raise ValidationError(_('Please select at least one date for scheduling.')) # Parse and validate dates parsed_dates = [] for date_str in dates_list: try: # Parse ISO format datetime parsed_date = datetime.fromisoformat(date_str.replace('Z', '+00:00')) # Make timezone aware if needed if timezone.is_naive(parsed_date): parsed_date = timezone.make_aware(parsed_date) parsed_dates.append(parsed_date) except (ValueError, AttributeError) as e: raise ValidationError(_(f'Invalid date format: {date_str}')) # Sort dates chronologically parsed_dates.sort() # Store parsed dates for later use self.cleaned_dates = parsed_dates return dates_json
[docs] def clean(self): """ Validate that selected dates respect questionnaire answer intervals. """ cleaned_data = super().clean() questionnaires = cleaned_data.get('questionnaires', []) if not hasattr(self, 'cleaned_dates'): return cleaned_data # Validate intervals for each questionnaire for pq_id in questionnaires: interval_seconds = self.questionnaire_intervals.get(pq_id, 0) if interval_seconds > 0 and len(self.cleaned_dates) > 1: # Check that successive dates respect the interval for i in range(len(self.cleaned_dates) - 1): date1 = self.cleaned_dates[i] date2 = self.cleaned_dates[i + 1] time_diff = (date2 - date1).total_seconds() if time_diff < interval_seconds: # Get questionnaire name for error message pq = PatientQuestionnaire.objects.get(id=pq_id) questionnaire_name = pq.questionnaire.safe_translation_getter('name', any_language=True) or 'Questionnaire' # Format interval for error message if interval_seconds < 3600: minutes = interval_seconds // 60 interval_display = _(f'{minutes} minute(s)') elif interval_seconds < 86400: hours = interval_seconds // 3600 interval_display = _(f'{hours} hour(s)') else: days = interval_seconds // 86400 interval_display = _(f'{days} day(s)') raise ValidationError( _(f'The questionnaire "{questionnaire_name}" requires a minimum interval of {interval_display} between successive dates. ' f'The dates {date1.strftime("%Y-%m-%d %H:%M")} and {date2.strftime("%Y-%m-%d %H:%M")} are too close together.') ) return cleaned_data
[docs] def save(self): """ Create schedule entries for selected questionnaires and dates. Returns the number of schedules created. """ questionnaires = self.cleaned_data['questionnaires'] schedules_created = 0 for pq_id in questionnaires: pq = PatientQuestionnaire.objects.get(id=pq_id) for scheduled_datetime in self.cleaned_dates: # Check if schedule already exists for this datetime existing = QuestionnairePatientSchedule.objects.filter( patient_questionnaire=pq, date_assessment=scheduled_datetime ).exists() if not existing: QuestionnairePatientSchedule.objects.create( patient_questionnaire=pq, date_assessment=scheduled_datetime ) schedules_created += 1 return schedules_created