Marwan Alwali a4665842c9 update
2025-11-23 10:58:07 +03:00

439 lines
13 KiB
Python

"""
Forms for the notifications app.
"""
from django import forms
from django.utils.translation import gettext_lazy as _
from django.core.exceptions import ValidationError
from .models import MessageTemplate, Message, NotificationPreference
class MessageTemplateForm(forms.ModelForm):
"""Form for creating/editing message templates."""
class Meta:
model = MessageTemplate
fields = [
'code', 'name', 'channel', 'subject',
'body_en', 'body_ar', 'variables', 'is_active'
]
widgets = {
'code': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': _('e.g., appointment_reminder')
}),
'name': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': _('Template Name')
}),
'channel': forms.Select(attrs={
'class': 'form-select'
}),
'subject': forms.TextInput(attrs={
'class': 'form-control',
'placeholder': _('Email subject (optional)')
}),
'body_en': forms.Textarea(attrs={
'class': 'form-control',
'rows': 6,
'placeholder': _('Message body in English. Use {variable_name} for variables.')
}),
'body_ar': forms.Textarea(attrs={
'class': 'form-control',
'rows': 6,
'placeholder': _('Message body in Arabic (optional)')
}),
'variables': forms.Textarea(attrs={
'class': 'form-control',
'rows': 3,
'placeholder': _('["patient_name", "appointment_date", "clinic_name"]')
}),
'is_active': forms.CheckboxInput(attrs={
'class': 'form-check-input'
}),
}
def clean_variables(self):
"""Validate and parse variables JSON."""
import json
variables = self.cleaned_data.get('variables')
if isinstance(variables, str):
try:
variables = json.loads(variables)
except json.JSONDecodeError:
raise ValidationError(_('Invalid JSON format for variables'))
if not isinstance(variables, list):
raise ValidationError(_('Variables must be a list'))
return variables
def clean_code(self):
"""Ensure code is unique."""
code = self.cleaned_data.get('code')
# Check if code already exists (excluding current instance)
qs = MessageTemplate.objects.filter(code=code)
if self.instance.pk:
qs = qs.exclude(pk=self.instance.pk)
if qs.exists():
raise ValidationError(_('A template with this code already exists'))
return code
class MessageFilterForm(forms.Form):
"""Form for filtering messages."""
search = forms.CharField(
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': _('Search by recipient or content...')
})
)
channel = forms.ChoiceField(
required=False,
choices=[('', _('All Channels'))] + list(Message.Channel.choices),
widget=forms.Select(attrs={
'class': 'form-select'
})
)
status = forms.ChoiceField(
required=False,
choices=[('', _('All Statuses'))] + list(Message.Status.choices),
widget=forms.Select(attrs={
'class': 'form-select'
})
)
date_from = forms.DateField(
required=False,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
})
)
date_to = forms.DateField(
required=False,
widget=forms.DateInput(attrs={
'class': 'form-control',
'type': 'date'
})
)
template = forms.ModelChoiceField(
queryset=MessageTemplate.objects.none(),
required=False,
widget=forms.Select(attrs={
'class': 'form-select'
}),
empty_label=_('All Templates')
)
def __init__(self, *args, tenant=None, **kwargs):
super().__init__(*args, **kwargs)
if tenant:
self.fields['template'].queryset = MessageTemplate.objects.filter(
tenant=tenant
).order_by('name')
class BulkMessageForm(forms.Form):
"""Form for sending bulk messages."""
channel = forms.ChoiceField(
label=_('Channel'),
choices=Message.Channel.choices,
widget=forms.Select(attrs={
'class': 'form-select'
})
)
use_template = forms.BooleanField(
label=_('Use Template'),
required=False,
initial=True,
widget=forms.CheckboxInput(attrs={
'class': 'form-check-input',
'id': 'useTemplateCheckbox'
})
)
template = forms.ModelChoiceField(
label=_('Template'),
queryset=MessageTemplate.objects.none(),
required=False,
widget=forms.Select(attrs={
'class': 'form-select select2',
'id': 'templateSelect'
})
)
subject = forms.CharField(
label=_('Subject'),
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': _('Email subject (for email channel only)')
})
)
message = forms.CharField(
label=_('Message'),
required=False,
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 6,
'placeholder': _('Enter your message here...')
})
)
recipient_filter = forms.ChoiceField(
label=_('Recipients'),
choices=[
('all', _('All Patients')),
('tags', _('By Tags')),
('custom', _('Custom List')),
],
widget=forms.RadioSelect(attrs={
'class': 'form-check-input'
})
)
tags = forms.CharField(
label=_('Patient Tags'),
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': _('Enter tags separated by commas')
})
)
recipients = forms.CharField(
label=_('Recipients'),
required=False,
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': _('Enter phone numbers or emails, one per line')
})
)
def __init__(self, *args, tenant=None, **kwargs):
super().__init__(*args, **kwargs)
if tenant:
self.fields['template'].queryset = MessageTemplate.objects.filter(
tenant=tenant,
is_active=True
).order_by('name')
def clean(self):
cleaned_data = super().clean()
use_template = cleaned_data.get('use_template')
template = cleaned_data.get('template')
message = cleaned_data.get('message')
recipient_filter = cleaned_data.get('recipient_filter')
tags = cleaned_data.get('tags')
recipients = cleaned_data.get('recipients')
# Validate message content
if use_template and not template:
raise ValidationError(_('Please select a template'))
if not use_template and not message:
raise ValidationError(_('Please enter a message'))
# Validate recipients
if recipient_filter == 'tags' and not tags:
raise ValidationError(_('Please enter at least one tag'))
if recipient_filter == 'custom' and not recipients:
raise ValidationError(_('Please enter at least one recipient'))
return cleaned_data
class TestTemplateForm(forms.Form):
"""Form for testing message templates."""
template = forms.ModelChoiceField(
label=_('Template'),
queryset=MessageTemplate.objects.none(),
widget=forms.Select(attrs={
'class': 'form-select select2'
})
)
test_recipient = forms.CharField(
label=_('Test Recipient'),
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': _('Phone number or email')
})
)
language = forms.ChoiceField(
label=_('Language'),
choices=[('en', _('English')), ('ar', _('Arabic'))],
widget=forms.Select(attrs={
'class': 'form-select'
})
)
variables = forms.CharField(
label=_('Variables (JSON)'),
required=False,
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 4,
'placeholder': _('{"patient_name": "Ahmed", "appointment_date": "2025-10-15"}')
})
)
def __init__(self, *args, tenant=None, **kwargs):
super().__init__(*args, **kwargs)
if tenant:
self.fields['template'].queryset = MessageTemplate.objects.filter(
tenant=tenant,
is_active=True
).order_by('name')
def clean_variables(self):
"""Validate and parse variables JSON."""
import json
variables = self.cleaned_data.get('variables')
if not variables:
return {}
try:
variables = json.loads(variables)
except json.JSONDecodeError:
raise ValidationError(_('Invalid JSON format'))
if not isinstance(variables, dict):
raise ValidationError(_('Variables must be a JSON object'))
return variables
class MessageRetryForm(forms.Form):
"""Form for retrying failed messages."""
message_ids = forms.CharField(
widget=forms.HiddenInput()
)
def clean_message_ids(self):
"""Parse and validate message IDs."""
import json
message_ids = self.cleaned_data.get('message_ids')
try:
message_ids = json.loads(message_ids)
except json.JSONDecodeError:
raise ValidationError(_('Invalid message IDs'))
if not isinstance(message_ids, list):
raise ValidationError(_('Message IDs must be a list'))
return message_ids
class BroadcastNotificationForm(forms.Form):
"""Form for creating broadcast notifications (general or role-based)."""
BROADCAST_TYPE_CHOICES = [
('general', _('General (All Users)')),
('role_based', _('Role-Based (Specific Roles)')),
]
broadcast_type = forms.ChoiceField(
label=_('Broadcast Type'),
choices=BROADCAST_TYPE_CHOICES,
widget=forms.RadioSelect(attrs={
'class': 'form-check-input'
}),
initial='general'
)
target_roles = forms.MultipleChoiceField(
label=_('Target Roles'),
choices=[], # Will be populated in __init__
required=False,
widget=forms.CheckboxSelectMultiple(attrs={
'class': 'form-check-input'
}),
help_text=_('Select which user roles should see this notification')
)
title = forms.CharField(
label=_('Title'),
max_length=200,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': _('Notification title')
})
)
message = forms.CharField(
label=_('Message'),
widget=forms.Textarea(attrs={
'class': 'form-control',
'rows': 5,
'placeholder': _('Notification message')
})
)
notification_type = forms.ChoiceField(
label=_('Type'),
choices=[], # Will be populated in __init__
widget=forms.Select(attrs={
'class': 'form-select'
}),
initial='INFO'
)
action_url = forms.CharField(
label=_('Action URL'),
required=False,
widget=forms.TextInput(attrs={
'class': 'form-control',
'placeholder': _('Optional URL to navigate to when clicked')
}),
help_text=_('Leave empty if no action is needed')
)
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
# Import here to avoid circular imports
from .models import Notification
from core.models import User
# Set notification type choices
self.fields['notification_type'].choices = Notification.NotificationType.choices
# Set role choices
self.fields['target_roles'].choices = User.Role.choices
def clean(self):
cleaned_data = super().clean()
broadcast_type = cleaned_data.get('broadcast_type')
target_roles = cleaned_data.get('target_roles')
# Validate that roles are selected for role-based notifications
if broadcast_type == 'role_based' and not target_roles:
raise ValidationError({
'target_roles': _('Please select at least one role for role-based notifications')
})
return cleaned_data