import datetime import logging from collections import OrderedDict from django import forms from django.forms import ModelForm, Form from django.utils.dates import MONTHS from web.models import Subject, StudySubject, Worker, Appointment, Visit, AppointmentType, ContactAttempt, \ AppointmentTypeLink, \ Availability, Holiday from web.models.constants import SUBJECT_TYPE_CHOICES, SCREENING_NUMBER_PREFIXES_FOR_TYPE, COUNTRY_OTHER_ID from web.views.notifications import get_filter_locations """ Possible redundancy, but if need arises, contents of forms can be easily customized """ DATE_FORMAT_TIME = "%H:%M" CURRENT_YEAR = datetime.datetime.now().year YEAR_CHOICES = tuple(range(CURRENT_YEAR, CURRENT_YEAR - 120, -1)) FUTURE_YEAR_CHOICES = tuple(range(CURRENT_YEAR, CURRENT_YEAR + 5, 1)) DATEPICKER_DATE_ATTRS = { 'class': 'datepicker', 'data-date-format': 'yyyy-mm-dd', 'data-date-orientation': 'bottom' } DATETIMEPICKER_DATE_ATTRS = { 'class': 'datetimepicker', 'data-date-format': 'Y-MM-DD HH:mm', } TIMEPICKER_DATE_ATTRS = { 'class': 'datetimepicker', 'data-date-format': 'HH:mm', 'data-date-stepping': 15, } START_YEAR_STATISTICS = 2015 APPOINTMENT_TYPES_FIELD_POSITION = 1 logger = logging.getLogger(__name__) def validate_subject_nd_number(self, cleaned_data): if cleaned_data['nd_number'] != "": subjects_from_db = StudySubject.objects.filter(nd_number=cleaned_data['nd_number']) if subjects_from_db: if subjects_from_db[0].screening_number != cleaned_data.get('screening_number', ''): self.add_error('nd_number', "ND number already in use") def validate_subject_country(self, cleaned_data): if cleaned_data['country'].id == COUNTRY_OTHER_ID: self.add_error('country', "Select valid country") def validate_subject_resign_reason(self, cleaned_data): if cleaned_data['resigned'] and cleaned_data['resign_reason'] == '': self.add_error('resign_reason', "Resign reason cannot be empty") def validate_subject_mpower_number(self, cleaned_data): if cleaned_data['mpower_id'] != "": subjects_from_db = StudySubject.objects.filter(mpower_id=cleaned_data['mpower_id']) if subjects_from_db: if subjects_from_db[0].screening_number != cleaned_data.get('screening_number', ''): self.add_error('mpower_id', "mPower number already in use") class StudySubjectAddForm(ModelForm): date_born = forms.DateField(label="Date of birth", widget=forms.DateInput(DATEPICKER_DATE_ATTRS, "%Y-%m-%d"), required=False ) datetime_contact_reminder = forms.DateTimeField(label="Contact on", widget=forms.DateTimeInput(DATETIMEPICKER_DATE_ATTRS), required=False ) class Meta: model = StudySubject fields = '__all__' exclude = ['resigned', 'resign_reason'] def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) if user is None: raise TypeError("User not defined") self.user = Worker.get_by_user(user) if self.user is None: raise TypeError("Worker not defined for: " + user.username) super(ModelForm, self).__init__(*args, **kwargs) self.fields['screening_number'].required = False def build_screening_number(self, cleaned_data): screening_number = cleaned_data.get('screening_number', None) if not screening_number: prefix_screening_number = self.get_prefix_screening_number() if prefix_screening_number is not None: screening_number = get_new_screening_number(prefix_screening_number) return screening_number def clean(self): cleaned_data = super(StudySubjectAddForm, self).clean() screening_number = self.build_screening_number(cleaned_data) if screening_number is not None: cleaned_data['screening_number'] = screening_number subjects_from_db = StudySubject.objects.filter(screening_number=screening_number) if len(subjects_from_db) > 0: self.add_error('screening_number', "Screening number already in use") validate_subject_nd_number(self, cleaned_data) validate_subject_mpower_number(self, cleaned_data) return cleaned_data def get_prefix_screening_number(self): default_location = self.cleaned_data.get('default_location', None) screening_number_prefix = None if default_location is not None and default_location.prefix: screening_number_prefix = default_location.prefix else: subject_type = self.cleaned_data.get('type', None) if subject_type is not None: screening_number_prefix = SCREENING_NUMBER_PREFIXES_FOR_TYPE[subject_type] if screening_number_prefix is None: return None prefix_screening_number = screening_number_prefix + "-" return prefix_screening_number def get_new_screening_number(screening_number_prefix): result_number = 0 subjects = StudySubject.objects.filter(screening_number__contains=screening_number_prefix) for subject in subjects: screening_numbers = subject.screening_number.split(";") for screening_number in screening_numbers: screening_number = screening_number.strip() if screening_number.startswith(screening_number_prefix): number = screening_number[len(screening_number_prefix):] try: result_number = max(result_number, int(number)) except ValueError: pass return screening_number_prefix + str(result_number + 1).zfill(3) class SubjectDetailForm(ModelForm): class Meta: model = StudySubject fields = '__all__' class StudySubjectEditForm(ModelForm): datetime_contact_reminder = forms.DateTimeField(label="Contact on", widget=forms.DateTimeInput(DATETIMEPICKER_DATE_ATTRS), required=False ) date_born = forms.DateField(label="Date of birth", widget=forms.DateInput(DATEPICKER_DATE_ATTRS, "%Y-%m-%d"), required=False ) def __init__(self, *args, **kwargs): was_resigned = kwargs.get('was_resigned', False) if 'was_resigned' in kwargs: kwargs.pop('was_resigned') super(StudySubjectEditForm, self).__init__(*args, **kwargs) instance = getattr(self, 'instance', None) if instance and instance.id: self.fields['screening_number'].widget.attrs['readonly'] = True if was_resigned: self.fields['resigned'].disabled = True def clean(self): validate_subject_nd_number(self, self.cleaned_data) validate_subject_mpower_number(self, self.cleaned_data) validate_subject_resign_reason(self, self.cleaned_data) class Meta: model = StudySubject fields = '__all__' class WorkerAddForm(ModelForm): class Meta: model = Worker exclude = ['appointments'] class WorkerEditForm(ModelForm): class Meta: model = Worker fields = '__all__' class AppointmentDetailForm(ModelForm): class Meta: model = Appointment fields = '__all__' datetime_when = forms.DateTimeField(label='Appointment on (YYYY-MM-DD HH:MM)', widget=forms.DateTimeInput(DATETIMEPICKER_DATE_ATTRS) ) class AppointmentEditForm(ModelForm): class Meta: model = Appointment fields = '__all__' exclude = ['appointment_types'] datetime_when = forms.DateTimeField(label='Appointment on (YYYY-MM-DD HH:MM)', widget=forms.DateTimeInput(DATETIMEPICKER_DATE_ATTRS) ) def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) if user is None: raise TypeError("User not defined") self.user = Worker.get_by_user(user) if self.user is None: raise TypeError("Worker not defined for: " + user.username) super(ModelForm, self).__init__(*args, **kwargs) if 'instance' in kwargs: initial_appointment_types = AppointmentTypeLink.objects.filter(appointment=kwargs['instance']).values_list( 'appointment_type', flat=True) else: initial_appointment_types = [] fields = OrderedDict() for i, tuple in enumerate(self.fields.items()): key, value = tuple fields[key] = value if i == APPOINTMENT_TYPES_FIELD_POSITION: fields['appointment_types'] = forms.ModelMultipleChoiceField(required=False, widget=forms.CheckboxSelectMultiple, queryset=AppointmentType.objects.all(), initial=initial_appointment_types) self.fields = fields self.fields['worker_assigned'].queryset = Worker.objects.filter( locations__in=get_filter_locations(self.user)).distinct().order_by('first_name', 'last_name') self.fields['location'].queryset = get_filter_locations(self.user) def clean_location(self): location = self.cleaned_data['location'] if self.user.locations.filter(id=location.id).count() == 0: self.add_error('location', "You cannot create appointment for this location") else: return location def save(self, commit=True): appointment = super(AppointmentEditForm, self).save(commit) # if appointment date change, remove appointment_type links if 'datetime_when' in self.changed_data: AppointmentTypeLink.objects.filter(appointment=appointment).delete() appointment_type_links = [] else: appointment_type_links = AppointmentTypeLink.objects.filter(appointment=appointment).all() appointment_types_from_links = [] appointment_types = self.cleaned_data['appointment_types'] for appointment_type_link in appointment_type_links: if appointment_type_link.appointment_type not in appointment_types: # we delete appointment links for appointments types that have been removed appointment_type_link.delete() else: appointment_types_from_links.append(appointment_type_link.appointment_type) for appointment_type in appointment_types: if appointment_type not in appointment_types_from_links: # we create new appointment links for appointments types that have been added appointment_type_link = AppointmentTypeLink(appointment=appointment, appointment_type=appointment_type) appointment_type_link.save() return appointment class AppointmentAddForm(ModelForm): class Meta: model = Appointment exclude = ['status', 'appointment_types'] datetime_when = forms.DateTimeField(label='Appointment on (YYYY-MM-DD HH:MM)', widget=forms.DateTimeInput(DATETIMEPICKER_DATE_ATTRS) ) def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) if user is None: raise TypeError("User not defined") self.user = Worker.get_by_user(user) if self.user is None: raise TypeError("Worker not defined for: " + user.username) super(ModelForm, self).__init__(*args, **kwargs) fields = OrderedDict() for i, tuple in enumerate(self.fields.items()): key, value = tuple fields[key] = value if i == APPOINTMENT_TYPES_FIELD_POSITION: fields['appointment_types'] = forms.ModelMultipleChoiceField(required=False, widget=forms.CheckboxSelectMultiple, queryset=AppointmentType.objects.all(), ) self.fields = fields self.fields['worker_assigned'].queryset = Worker.objects.filter( locations__in=get_filter_locations(self.user)).distinct().order_by('first_name', 'last_name') self.fields['location'].queryset = get_filter_locations(self.user) def clean_location(self): location = self.cleaned_data['location'] if self.user.locations.filter(id=location.id).count() == 0: self.add_error('location', "You cannot create appointment for this location") else: return location def save(self, commit=True): appointment = super(AppointmentAddForm, self).save(commit) appointment_types = self.cleaned_data['appointment_types'] for appointment_type in appointment_types: appointment_type_link = AppointmentTypeLink(appointment=appointment, appointment_type=appointment_type) appointment_type_link.save() return appointment class VisitDetailForm(ModelForm): datetime_begin = forms.DateField(label="Visit begins on", widget=forms.DateInput(DATEPICKER_DATE_ATTRS, "%Y-%m-%d") ) datetime_end = forms.DateField(label="Visit ends on", widget=forms.DateInput(DATEPICKER_DATE_ATTRS, "%Y-%m-%d") ) post_mail_sent = forms.RadioSelect() appointment_types = forms.ModelMultipleChoiceField(required=False, widget=forms.CheckboxSelectMultiple, queryset=AppointmentType.objects.all()) class Meta: model = Visit exclude = ['is_finished', 'visit_number'] class VisitAddForm(ModelForm): subject = forms.ModelChoiceField(queryset=StudySubject.objects.order_by('subject__last_name', 'subject__first_name')) datetime_begin = forms.DateField(label="Visit begins on", widget=forms.TextInput(attrs=DATEPICKER_DATE_ATTRS) ) datetime_end = forms.DateField(label="Visit ends on", widget=forms.TextInput(attrs=DATEPICKER_DATE_ATTRS) ) appointment_types = forms.ModelMultipleChoiceField(required=False, widget=forms.CheckboxSelectMultiple, queryset=AppointmentType.objects.all()) class Meta: model = Visit exclude = ['is_finished', 'visit_number'] def clean(self): super(VisitAddForm, self).clean() if 'datetime_begin' not in self.cleaned_data or 'datetime_end' not in self.cleaned_data: return if self.cleaned_data['datetime_begin'] >= self.cleaned_data['datetime_end']: self.add_error('datetime_begin', "Start date must be before end date") self.add_error('datetime_end', "End date must be after start date") class ContactAttemptForm(ModelForm): datetime_when = forms.DateTimeField(label='When? (YYYY-MM-DD HH:MM)', widget=forms.DateTimeInput(DATETIMEPICKER_DATE_ATTRS) ) class Meta: model = ContactAttempt fields = '__all__' def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) if user is None: raise TypeError("User not defined") self.user = Worker.get_by_user(user) if self.user is None: raise TypeError("Worker not defined for: " + user.username) subject = kwargs.pop('subject', None) super(ContactAttemptForm, self).__init__(*args, **kwargs) self.fields['subject'].initial = subject.id self.fields['subject'].disabled = True self.fields['worker'].initial = self.user class ContactAttemptEditForm(ModelForm): datetime_when = forms.DateTimeField(label='When? (YYYY-MM-DD HH:MM)', widget=forms.DateTimeInput(DATETIMEPICKER_DATE_ATTRS) ) class Meta: model = ContactAttempt fields = '__all__' def __init__(self, *args, **kwargs): user = kwargs.pop('user', None) if user is None: raise TypeError("User not defined") self.user = Worker.get_by_user(user) if self.user is None: raise TypeError("Worker not defined for: " + user.username) super(ContactAttemptEditForm, self).__init__(*args, **kwargs) self.fields['subject'].disabled = True class KitRequestForm(Form): start_date = forms.DateField(label="From date", widget=forms.DateInput(DATEPICKER_DATE_ATTRS, "%Y-%m-%d"), required=False ) end_date = forms.DateField(label="End date", widget=forms.DateInput(DATEPICKER_DATE_ATTRS, "%Y-%m-%d"), required=False ) class StatisticsForm(Form): def __init__(self, *args, **kwargs): super(StatisticsForm, self).__init__(*args) visit_choices = kwargs['visit_choices'] month = kwargs['month'] year = kwargs['year'] now = datetime.datetime.now() year_now = now.year number_of_years_for_statistics = year_now - START_YEAR_STATISTICS + 2 year_choices = [(START_YEAR_STATISTICS + i, START_YEAR_STATISTICS + i) for i in range(0, number_of_years_for_statistics + 1)] self.fields['month'] = forms.ChoiceField(choices=MONTHS.items(), initial=month) self.fields['year'] = forms.ChoiceField(choices=year_choices, initial=year) choices = [(-1, "all")] choices.extend(SUBJECT_TYPE_CHOICES.items()) self.fields['subject_type'] = forms.ChoiceField(choices=choices, initial="-1") self.fields['visit'] = forms.ChoiceField(choices=visit_choices, initial="-1") class AvailabilityAddForm(ModelForm): available_from = forms.TimeField(label="Available from", widget=forms.TimeInput(TIMEPICKER_DATE_ATTRS), initial="8:00", ) available_till = forms.TimeField(label="Available until", widget=forms.TimeInput(TIMEPICKER_DATE_ATTRS), initial="17:00", ) class Meta: model = Availability fields = '__all__' def clean(self): worker = Worker.objects.get(id=self.cleaned_data["person"].id) availabilities = worker.availability_set.all() for availability in availabilities: validate_availability_conflict(self, self.cleaned_data, availability) class AvailabilityEditForm(ModelForm): available_from = forms.TimeField(label="Available from", widget=forms.TimeInput(TIMEPICKER_DATE_ATTRS), ) available_till = forms.TimeField(label="Available until", widget=forms.TimeInput(TIMEPICKER_DATE_ATTRS), ) class Meta: model = Availability fields = '__all__' def __init__(self, *args, **kwargs): super(ModelForm, self).__init__(*args, **kwargs) instance = getattr(self, 'instance', None) if instance is not None: self.availability_id = instance.id self.fields['person'].disabled = True def clean(self): worker = Worker.objects.get(id=self.cleaned_data["person"].id) availabilities = worker.availability_set.all() for availability in availabilities: if availability.id <> self.availability_id: validate_availability_conflict(self, self.cleaned_data, availability) def validate_availability_conflict(self, cleaned_data, availability): start_hour = self.cleaned_data.get("available_from", None) end_hour = self.cleaned_data.get("available_till", None) if availability.day_number == self.cleaned_data.get("day_number", None) and \ ((start_hour <= availability.available_from < end_hour) or (start_hour < availability.available_till <= end_hour) or (availability.available_from <= start_hour < availability.available_till) or (availability.available_from < end_hour <= availability.available_till)): error = "User has defined availability for this day that overlaps: " + availability.available_from.strftime( DATE_FORMAT_TIME) + ", " + availability.available_till.strftime(DATE_FORMAT_TIME) self.add_error('day_number', error) self.add_error('available_from', error) self.add_error('available_till', error) class HolidayAddForm(ModelForm): datetime_start = forms.DateTimeField(widget=forms.DateTimeInput(DATETIMEPICKER_DATE_ATTRS), initial=datetime.datetime.now().replace(hour=8, minute=0), ) datetime_end = forms.DateTimeField(widget=forms.DateTimeInput(DATETIMEPICKER_DATE_ATTRS), initial=datetime.datetime.now().replace(hour=17, minute=0), ) class Meta: model = Holiday fields = '__all__' def clean(self): worker = Worker.objects.get(id=self.cleaned_data["person"].id) availabilities = worker.availability_set.all() for availability in availabilities: validate_availability_conflict(self, self.cleaned_data, availability) class SubjectAddForm(ModelForm): date_born = forms.DateField(label="Date of birth", widget=forms.DateInput(DATEPICKER_DATE_ATTRS, "%Y-%m-%d"), required=False ) class Meta: model = Subject fields = '__all__' exclude = ['dead'] def build_screening_number(self, cleaned_data): screening_number = cleaned_data.get('screening_number', None) if not screening_number: prefix_screening_number = self.get_prefix_screening_number() if prefix_screening_number is not None: screening_number = get_new_screening_number(prefix_screening_number) return screening_number def clean(self): cleaned_data = super(SubjectAddForm, self).clean() validate_subject_country(self, cleaned_data) return cleaned_data class SubjectEditForm(ModelForm): date_born = forms.DateField(label="Date of birth", widget=forms.DateInput(DATEPICKER_DATE_ATTRS, "%Y-%m-%d"), required=False ) def __init__(self, *args, **kwargs): was_dead = kwargs.get('was_dead', False) if 'was_dead' in kwargs: kwargs.pop('was_dead') super(SubjectEditForm, self).__init__(*args, **kwargs) if was_dead: self.fields['dead'].disabled = True def clean(self): validate_subject_country(self, self.cleaned_data) class Meta: model = Subject fields = '__all__'