439 lines
13 KiB
Python
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
|