agdar/core/forms.py
Marwan Alwali 6e50a3f12e update
2025-11-06 14:27:26 +03:00

664 lines
24 KiB
Python

"""
Forms for core app.
"""
from django import forms
from django.contrib.auth.forms import UserCreationForm as BaseUserCreationForm, PasswordChangeForm
from django.utils.translation import gettext_lazy as _
from .models import TenantSetting, SettingTemplate, Patient, Consent, ConsentTemplate, User
class PatientForm(forms.ModelForm):
"""Form for creating and updating patients."""
class Meta:
model = Patient
fields = [
'first_name_en', 'father_name_en', 'grandfather_name_en', 'last_name_en',
'first_name_ar', 'father_name_ar', 'grandfather_name_ar', 'last_name_ar',
'national_id', 'date_of_birth', 'sex',
'phone', 'email', 'address', 'city', 'postal_code',
'caregiver_name', 'caregiver_phone', 'caregiver_relationship',
'emergency_contact'
]
labels = {
'first_name_en': _('First Name (English)'),
'father_name_en': _("Father's Name (English)"),
'grandfather_name_en': _("Grandfather's Name (English)"),
'last_name_en': _('Last Name (English)'),
'first_name_ar': _('First Name (Arabic)'),
'father_name_ar': _("Father's Name (Arabic)"),
'grandfather_name_ar': _("Grandfather's Name (Arabic)"),
'last_name_ar': _('Last Name (Arabic)'),
'national_id': _('National ID'),
'date_of_birth': _('Date of Birth'),
'sex': _('Gender'),
'phone': _('Phone Number'),
'email': _('Email Address'),
'address': _('Address'),
'city': _('City'),
'postal_code': _('Postal Code'),
'caregiver_name': _('Caregiver Name'),
'caregiver_phone': _('Caregiver Phone'),
'caregiver_relationship': _('Relationship to Patient'),
'emergency_contact': _('Emergency Contact Information'),
}
widgets = {
'date_of_birth': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}),
'first_name_en': forms.TextInput(attrs={'class': 'form-control'}),
'father_name_en': forms.TextInput(attrs={'class': 'form-control'}),
'grandfather_name_en': forms.TextInput(attrs={'class': 'form-control'}),
'last_name_en': forms.TextInput(attrs={'class': 'form-control'}),
'first_name_ar': forms.TextInput(attrs={'class': 'form-control'}),
'father_name_ar': forms.TextInput(attrs={'class': 'form-control'}),
'grandfather_name_ar': forms.TextInput(attrs={'class': 'form-control'}),
'last_name_ar': forms.TextInput(attrs={'class': 'form-control'}),
'national_id': forms.TextInput(attrs={'class': 'form-control'}),
'sex': forms.Select(attrs={'class': 'form-select'}),
'phone': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'address': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
'city': forms.TextInput(attrs={'class': 'form-control'}),
'postal_code': forms.TextInput(attrs={'class': 'form-control'}),
'caregiver_name': forms.TextInput(attrs={'class': 'form-control'}),
'caregiver_phone': forms.TextInput(attrs={'class': 'form-control'}),
'caregiver_relationship': forms.TextInput(attrs={'class': 'form-control'}),
'emergency_contact': forms.Textarea(attrs={'class': 'form-control', 'rows': 3}),
}
class ConsentForm(forms.ModelForm):
"""Form for creating consents with digital signature support."""
# Additional field for signature data (base64)
signature_data = forms.CharField(
required=False,
widget=forms.HiddenInput()
)
class Meta:
model = Consent
fields = ['patient', 'consent_type', 'content_text', 'signed_by_name',
'signed_by_relationship', 'signature_method']
widgets = {
'patient': forms.Select(attrs={'class': 'form-select select2'}),
'consent_type': forms.Select(attrs={'class': 'form-select'}),
'content_text': forms.HiddenInput(), # Hidden, populated via JS
'signed_by_name': forms.TextInput(attrs={'class': 'form-control'}),
'signed_by_relationship': forms.TextInput(attrs={'class': 'form-control'}),
'signature_method': forms.HiddenInput(), # Hidden, set to DRAWN
}
def clean_signature_data(self):
"""Validate signature data."""
signature_data = self.cleaned_data.get('signature_data')
if not signature_data:
raise forms.ValidationError('Signature is required.')
# Validate base64 format
if not signature_data.startswith('data:image/png;base64,'):
raise forms.ValidationError('Invalid signature format.')
return signature_data
def save(self, commit=True):
"""Save consent and process signature image."""
import base64
from django.core.files.base import ContentFile
from django.utils import timezone
import hashlib
instance = super().save(commit=False)
# Process signature data
signature_data = self.cleaned_data.get('signature_data')
if signature_data:
# Extract base64 data
format, imgstr = signature_data.split(';base64,')
ext = format.split('/')[-1]
# Decode base64 to binary
signature_binary = base64.b64decode(imgstr)
# Generate filename
timestamp = timezone.now().strftime('%Y%m%d_%H%M%S')
filename = f'signature_{instance.patient.mrn}_{timestamp}.{ext}'
# Save to signature_image field
instance.signature_image.save(
filename,
ContentFile(signature_binary),
save=False
)
# Calculate signature hash for verification
instance.signature_hash = hashlib.sha256(signature_binary).hexdigest()
# Set signed_at timestamp
if not instance.signed_at:
instance.signed_at = timezone.now()
if commit:
instance.save()
return instance
class PatientSearchForm(forms.Form):
"""Form for searching patients."""
search = forms.CharField(
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': _('Search by name, MRN, phone, or national ID...')
})
)
status = forms.ChoiceField(
required=False,
choices=[('', _('All')), ('active', _('Active')), ('inactive', _('Inactive'))],
widget=forms.Select(attrs={'class': 'form-select'})
)
gender = forms.ChoiceField(
required=False,
choices=[('', _('All'))] + list(Patient.Sex.choices),
widget=forms.Select(attrs={'class': 'form-select'})
)
class TenantSettingsForm(forms.Form):
"""
Dynamic form for tenant settings based on templates.
"""
def __init__(self, *args, **kwargs):
self.tenant = kwargs.pop('tenant', None)
self.category = kwargs.pop('category', None)
self.user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
# Get templates for this category
templates = SettingTemplate.objects.filter(
category=self.category,
is_active=True
).order_by('order')
# Create form fields dynamically
for template in templates:
field = self._create_field_for_template(template)
self.fields[template.key] = field
# Set initial value if exists
try:
tenant_setting = TenantSetting.objects.get(
tenant=self.tenant,
template=template
)
if template.data_type == SettingTemplate.DataType.FILE:
self.initial[template.key] = tenant_setting.file_value
elif template.data_type == SettingTemplate.DataType.ENCRYPTED:
# Show placeholder for encrypted values
if tenant_setting.encrypted_value:
self.initial[template.key] = '***ENCRYPTED***'
elif template.data_type == SettingTemplate.DataType.BOOLEAN:
self.initial[template.key] = tenant_setting.value.lower() in ('true', '1', 'yes')
else:
self.initial[template.key] = tenant_setting.value
except TenantSetting.DoesNotExist:
if template.default_value:
self.initial[template.key] = template.default_value
def _create_field_for_template(self, template):
"""Create appropriate form field based on template data type."""
label = template.label_en
required = template.is_required
help_text = template.help_text_en
if template.data_type == SettingTemplate.DataType.STRING:
return forms.CharField(
label=label,
required=required,
help_text=help_text,
max_length=500,
widget=forms.TextInput(attrs={'class': 'form-control'})
)
elif template.data_type == SettingTemplate.DataType.TEXT:
return forms.CharField(
label=label,
required=required,
help_text=help_text,
widget=forms.Textarea(attrs={'class': 'form-control', 'rows': 3})
)
elif template.data_type == SettingTemplate.DataType.INTEGER:
return forms.IntegerField(
label=label,
required=required,
help_text=help_text,
widget=forms.NumberInput(attrs={'class': 'form-control'})
)
elif template.data_type == SettingTemplate.DataType.BOOLEAN:
return forms.BooleanField(
label=label,
required=False,
help_text=help_text,
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
)
elif template.data_type == SettingTemplate.DataType.CHOICE:
choices = [(choice['value'], choice['label']) for choice in template.choices]
return forms.ChoiceField(
label=label,
required=required,
help_text=help_text,
choices=choices,
widget=forms.Select(attrs={'class': 'form-select'})
)
elif template.data_type == SettingTemplate.DataType.FILE:
return forms.FileField(
label=label,
required=False,
help_text=help_text,
widget=forms.FileInput(attrs={'class': 'form-control'})
)
elif template.data_type == SettingTemplate.DataType.ENCRYPTED:
return forms.CharField(
label=label,
required=False,
help_text=help_text + ' (Leave blank to keep current value)',
widget=forms.PasswordInput(attrs={'class': 'form-control'})
)
elif template.data_type == SettingTemplate.DataType.EMAIL:
return forms.EmailField(
label=label,
required=required,
help_text=help_text,
widget=forms.EmailInput(attrs={'class': 'form-control'})
)
elif template.data_type == SettingTemplate.DataType.URL:
return forms.URLField(
label=label,
required=required,
help_text=help_text,
widget=forms.URLInput(attrs={'class': 'form-control'})
)
elif template.data_type == SettingTemplate.DataType.PHONE:
return forms.CharField(
label=label,
required=required,
help_text=help_text,
widget=forms.TextInput(attrs={'class': 'form-control', 'type': 'tel'})
)
elif template.data_type == SettingTemplate.DataType.COLOR:
return forms.CharField(
label=label,
required=required,
help_text=help_text,
widget=forms.TextInput(attrs={'class': 'form-control', 'type': 'color'})
)
else:
return forms.CharField(
label=label,
required=required,
help_text=help_text,
widget=forms.TextInput(attrs={'class': 'form-control'})
)
def save(self):
"""Save form data to tenant settings."""
from .settings_service import get_tenant_settings_service
service = get_tenant_settings_service(self.tenant)
saved_count = 0
for key, value in self.cleaned_data.items():
# Skip encrypted fields with placeholder value
if value == '***ENCRYPTED***':
continue
# Skip empty encrypted fields (keep existing value)
template = SettingTemplate.objects.get(key=key)
if template.data_type == SettingTemplate.DataType.ENCRYPTED and not value:
continue
try:
service.set_setting(key, value, self.user)
saved_count += 1
except Exception as e:
# Log error but continue
print(f"Error saving setting {key}: {e}")
return saved_count
# ============================================================================
# USER PROFILE FORMS
# ============================================================================
class UserProfileForm(forms.ModelForm):
"""
Form for users to edit their own profile.
"""
class Meta:
model = User
fields = [
'first_name', 'last_name', 'email', 'phone_number',
'profile_picture', 'bio', 'timezone'
]
widgets = {
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'phone_number': forms.TextInput(attrs={'class': 'form-control'}),
'profile_picture': forms.FileInput(attrs={'class': 'form-control'}),
'bio': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
'timezone': forms.Select(attrs={'class': 'form-select'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add timezone choices
self.fields['timezone'].widget.choices = [
('Asia/Riyadh', _('Riyadh (GMT+3)')),
('Asia/Dubai', _('Dubai (GMT+4)')),
('Asia/Kuwait', _('Kuwait (GMT+3)')),
('Asia/Bahrain', _('Bahrain (GMT+3)')),
('Asia/Qatar', _('Qatar (GMT+3)')),
('UTC', _('UTC (GMT+0)')),
]
class UserPreferencesForm(forms.Form):
"""
Form for user preferences (stored in JSON field).
"""
language = forms.ChoiceField(
label=_("Preferred Language"),
choices=[('en', _('English')), ('ar', _('Arabic'))],
widget=forms.Select(attrs={'class': 'form-select'}),
required=False
)
email_notifications = forms.BooleanField(
label=_("Email Notifications"),
required=False,
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
)
sms_notifications = forms.BooleanField(
label=_("SMS Notifications"),
required=False,
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
)
appointment_reminders = forms.BooleanField(
label=_("Appointment Reminders"),
required=False,
widget=forms.CheckboxInput(attrs={'class': 'form-check-input'})
)
dashboard_layout = forms.ChoiceField(
label=_("Dashboard Layout"),
choices=[
('default', _('Default')),
('compact', _('Compact')),
('detailed', _('Detailed'))
],
widget=forms.Select(attrs={'class': 'form-select'}),
required=False
)
def __init__(self, *args, **kwargs):
self.user = kwargs.pop('user', None)
super().__init__(*args, **kwargs)
# Load current preferences
if self.user and self.user.preferences:
for field_name in self.fields:
if field_name in self.user.preferences:
self.initial[field_name] = self.user.preferences[field_name]
def save(self):
"""Save preferences to user's preferences JSON field."""
if not self.user:
return
# Update preferences
if not self.user.preferences:
self.user.preferences = {}
for field_name, value in self.cleaned_data.items():
self.user.preferences[field_name] = value
self.user.save(update_fields=['preferences'])
class UserPasswordChangeForm(PasswordChangeForm):
"""
Custom password change form with Bootstrap styling.
"""
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add Bootstrap classes
for field_name in self.fields:
self.fields[field_name].widget.attrs['class'] = 'form-control'
class UserAdminForm(forms.ModelForm):
"""
Form for admins to create/edit staff members.
"""
class Meta:
model = User
fields = [
'username', 'first_name', 'last_name', 'email',
'phone_number', 'employee_id', 'role',
'is_active', 'is_staff', 'profile_picture', 'bio'
]
widgets = {
'username': forms.TextInput(attrs={'class': 'form-control'}),
'first_name': forms.TextInput(attrs={'class': 'form-control'}),
'last_name': forms.TextInput(attrs={'class': 'form-control'}),
'email': forms.EmailInput(attrs={'class': 'form-control'}),
'phone_number': forms.TextInput(attrs={'class': 'form-control'}),
'employee_id': forms.TextInput(attrs={'class': 'form-control'}),
'role': forms.Select(attrs={'class': 'form-select'}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'is_staff': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'profile_picture': forms.FileInput(attrs={'class': 'form-control'}),
'bio': forms.Textarea(attrs={'class': 'form-control', 'rows': 4}),
}
password1 = forms.CharField(
label=_("Password"),
required=False,
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
help_text=_("Leave blank to keep current password (for updates)")
)
password2 = forms.CharField(
label=_("Password Confirmation"),
required=False,
widget=forms.PasswordInput(attrs={'class': 'form-control'}),
help_text=_("Enter the same password as before, for verification")
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make password required for new users
if not self.instance.pk:
self.fields['password1'].required = True
self.fields['password2'].required = True
def clean_password2(self):
"""Validate that passwords match."""
password1 = self.cleaned_data.get('password1')
password2 = self.cleaned_data.get('password2')
if password1 and password2 and password1 != password2:
raise forms.ValidationError(_("Passwords don't match"))
return password2
def save(self, commit=True):
"""Save user with password if provided."""
user = super().save(commit=False)
# Set password if provided
password = self.cleaned_data.get('password1')
if password:
user.set_password(password)
if commit:
user.save()
return user
class UserSearchForm(forms.Form):
"""
Form for searching staff members.
"""
search = forms.CharField(
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': _('Search by name, email, or employee ID...')
})
)
role = forms.ChoiceField(
required=False,
choices=[('', _('All Roles'))] + list(User.Role.choices),
widget=forms.Select(attrs={'class': 'form-select'})
)
status = forms.ChoiceField(
required=False,
choices=[
('', _('All')),
('active', _('Active')),
('inactive', _('Inactive'))
],
widget=forms.Select(attrs={'class': 'form-select'})
)
# ============================================================================
# USER SIGNUP FORM
# ============================================================================
class ConsentTemplateForm(forms.ModelForm):
"""Form for creating and updating consent templates."""
class Meta:
model = ConsentTemplate
fields = [
'consent_type', 'title_en', 'title_ar',
'content_en', 'content_ar', 'is_active', 'version'
]
widgets = {
'consent_type': forms.Select(attrs={'class': 'form-select'}),
'title_en': forms.TextInput(attrs={'class': 'form-control'}),
'title_ar': forms.TextInput(attrs={'class': 'form-control'}),
'content_en': forms.Textarea(attrs={
'class': 'form-control',
'rows': 10,
'placeholder': _('Use placeholders: {patient_name}, {patient_mrn}, {date}, {patient_dob}, {patient_age}')
}),
'content_ar': forms.Textarea(attrs={
'class': 'form-control',
'rows': 10,
'placeholder': _('Use placeholders: {patient_name}, {patient_mrn}, {date}, {patient_dob}, {patient_age}')
}),
'is_active': forms.CheckboxInput(attrs={'class': 'form-check-input'}),
'version': forms.NumberInput(attrs={'class': 'form-control'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Make version read-only for updates
if self.instance.pk:
self.fields['version'].disabled = True
self.fields['version'].help_text = _('Version is auto-incremented on save')
class UserSignupForm(BaseUserCreationForm):
"""
Custom user signup form with additional fields.
"""
email = forms.EmailField(
label=_("Email Address"),
required=True,
widget=forms.EmailInput(attrs={'class': 'form-control fs-13px h-45px'})
)
first_name = forms.CharField(
label=_("First Name"),
required=False,
max_length=150,
widget=forms.TextInput(attrs={'class': 'form-control fs-13px h-45px'})
)
last_name = forms.CharField(
label=_("Last Name"),
required=False,
max_length=150,
widget=forms.TextInput(attrs={'class': 'form-control fs-13px h-45px'})
)
class Meta:
model = User
fields = ('username', 'email', 'first_name', 'last_name', 'password1', 'password2')
widgets = {
'username': forms.TextInput(attrs={'class': 'form-control fs-13px h-45px'}),
}
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Add Bootstrap classes to password fields
self.fields['password1'].widget.attrs['class'] = 'form-control fs-13px h-45px'
self.fields['password2'].widget.attrs['class'] = 'form-control fs-13px h-45px'
def clean_email(self):
"""Validate that email is unique."""
email = self.cleaned_data.get('email')
if User.objects.filter(email=email).exists():
raise forms.ValidationError(_("A user with this email already exists."))
return email
def save(self, commit=True):
"""Save user with email."""
user = super().save(commit=False)
user.email = self.cleaned_data['email']
user.first_name = self.cleaned_data.get('first_name', '')
user.last_name = self.cleaned_data.get('last_name', '')
if commit:
user.save()
return user