456 lines
19 KiB
Python
456 lines
19 KiB
Python
"""
|
|
Email service for sending notifications related to agency messaging.
|
|
"""
|
|
from .models import Application
|
|
from django.shortcuts import get_object_or_404
|
|
import logging
|
|
from django.conf import settings
|
|
from django.core.mail import EmailMultiAlternatives
|
|
from django.utils.html import strip_tags
|
|
from django_q.tasks import async_task # Import needed at the top for clarity
|
|
|
|
logger = logging.getLogger(__name__)
|
|
from django.core.mail import send_mail, EmailMultiAlternatives
|
|
from django.conf import settings
|
|
from django.template.loader import render_to_string
|
|
from django.utils.html import strip_tags
|
|
from django.contrib.auth import get_user_model
|
|
import logging
|
|
from .models import Message
|
|
logger = logging.getLogger(__name__)
|
|
User=get_user_model()
|
|
|
|
class EmailService:
|
|
"""
|
|
Service class for handling email notifications
|
|
"""
|
|
|
|
def send_email(self, recipient_email, subject, body, html_body=None):
|
|
"""
|
|
Send email using Django's send_mail function
|
|
|
|
Args:
|
|
recipient_email: Email address to send to
|
|
subject: Email subject
|
|
body: Plain text email body
|
|
html_body: HTML email body (optional)
|
|
|
|
Returns:
|
|
dict: Result with success status and error message if failed
|
|
"""
|
|
try:
|
|
send_mail(
|
|
subject=subject,
|
|
message=body,
|
|
from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa'),
|
|
recipient_list=[recipient_email],
|
|
html_message=html_body,
|
|
fail_silently=False,
|
|
)
|
|
|
|
logger.info(f"Email sent successfully to {recipient_email}")
|
|
return {'success': True}
|
|
|
|
except Exception as e:
|
|
error_msg = f"Failed to send email to {recipient_email}: {str(e)}"
|
|
logger.error(error_msg)
|
|
return {'success': False, 'error': error_msg}
|
|
|
|
|
|
def send_agency_welcome_email(agency, access_link=None):
|
|
"""
|
|
Send welcome email to a new agency with portal access information.
|
|
|
|
Args:
|
|
agency: HiringAgency instance
|
|
access_link: AgencyAccessLink instance (optional)
|
|
|
|
Returns:
|
|
bool: True if email was sent successfully, False otherwise
|
|
"""
|
|
try:
|
|
if not agency.email:
|
|
logger.warning(f"No email found for agency {agency.id}")
|
|
return False
|
|
|
|
context = {
|
|
'agency': agency,
|
|
'access_link': access_link,
|
|
'portal_url': getattr(settings, 'AGENCY_PORTAL_URL', 'https://kaauh.edu.sa/portal/'),
|
|
}
|
|
|
|
# Render email templates
|
|
html_message = render_to_string('recruitment/emails/agency_welcome.html', context)
|
|
plain_message = strip_tags(html_message)
|
|
|
|
# Send email
|
|
send_mail(
|
|
subject='Welcome to KAAUH Recruitment Portal',
|
|
message=plain_message,
|
|
from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa'),
|
|
recipient_list=[agency.email],
|
|
html_message=html_message,
|
|
fail_silently=False,
|
|
)
|
|
|
|
logger.info(f"Welcome email sent to agency {agency.email}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send agency welcome email: {str(e)}")
|
|
return False
|
|
|
|
|
|
def send_assignment_notification_email(assignment, message_type='created'):
|
|
"""
|
|
Send email notification about assignment changes.
|
|
|
|
Args:
|
|
assignment: AgencyJobAssignment instance
|
|
message_type: Type of notification ('created', 'updated', 'deadline_extended')
|
|
|
|
Returns:
|
|
bool: True if email was sent successfully, False otherwise
|
|
"""
|
|
try:
|
|
if not assignment.agency.email:
|
|
logger.warning(f"No email found for agency {assignment.agency.id}")
|
|
return False
|
|
|
|
context = {
|
|
'assignment': assignment,
|
|
'agency': assignment.agency,
|
|
'job': assignment.job,
|
|
'message_type': message_type,
|
|
'portal_url': getattr(settings, 'AGENCY_PORTAL_URL', 'https://kaauh.edu.sa/portal/'),
|
|
}
|
|
|
|
# Render email templates
|
|
html_message = render_to_string('recruitment/emails/assignment_notification.html', context)
|
|
plain_message = strip_tags(html_message)
|
|
|
|
# Determine subject based on message type
|
|
subjects = {
|
|
'created': f'New Job Assignment: {assignment.job.title}',
|
|
'updated': f'Assignment Updated: {assignment.job.title}',
|
|
'deadline_extended': f'Deadline Extended: {assignment.job.title}',
|
|
}
|
|
subject = subjects.get(message_type, f'Assignment Notification: {assignment.job.title}')
|
|
|
|
# Send email
|
|
send_mail(
|
|
subject=subject,
|
|
message=plain_message,
|
|
from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa'),
|
|
recipient_list=[assignment.agency.email],
|
|
html_message=html_message,
|
|
fail_silently=False,
|
|
)
|
|
|
|
logger.info(f"Assignment notification email sent to {assignment.agency.email} for {message_type}")
|
|
return True
|
|
|
|
except Exception as e:
|
|
logger.error(f"Failed to send assignment notification email: {str(e)}")
|
|
return False
|
|
|
|
|
|
def send_interview_invitation_email(candidate, job, meeting_details=None, recipient_list=None):
|
|
"""
|
|
Send interview invitation email using HTML template.
|
|
|
|
Args:
|
|
candidate: Candidate instance
|
|
job: Job instance
|
|
meeting_details: Dictionary with meeting information (optional)
|
|
recipient_list: List of additional email addresses (optional)
|
|
|
|
Returns:
|
|
dict: Result with success status and error message if failed
|
|
"""
|
|
try:
|
|
# Prepare recipient list
|
|
recipients = []
|
|
if candidate.hiring_source == "Agency":
|
|
try:
|
|
recipients.append(candidate.hiring_agency.email)
|
|
except :
|
|
pass
|
|
else:
|
|
recipients.append(candidate.email)
|
|
|
|
if recipient_list:
|
|
recipients.extend(recipient_list)
|
|
|
|
|
|
if not recipients:
|
|
return {'success': False, 'error': 'No recipient email addresses provided'}
|
|
|
|
# Prepare context for template
|
|
context = {
|
|
'candidate_name': candidate.full_name or candidate.name,
|
|
'candidate_email': candidate.email,
|
|
'candidate_phone': candidate.phone or '',
|
|
'job_title': job.title,
|
|
'department': getattr(job, 'department', ''),
|
|
'company_name': getattr(settings, 'COMPANY_NAME', 'Norah University'),
|
|
}
|
|
|
|
# Add meeting details if provided
|
|
if meeting_details:
|
|
context.update({
|
|
'meeting_topic': meeting_details.get('topic', f'Interview for {job.title}'),
|
|
'meeting_date_time': meeting_details.get('date_time', ''),
|
|
'meeting_duration': meeting_details.get('duration', '60 minutes'),
|
|
'join_url': meeting_details.get('join_url', ''),
|
|
})
|
|
|
|
# Render HTML template
|
|
html_message = render_to_string('emails/interview_invitation.html', context)
|
|
plain_message = strip_tags(html_message)
|
|
|
|
# Create email with both HTML and plain text versions
|
|
email = EmailMultiAlternatives(
|
|
subject=f'Interview Invitation: {job.title}',
|
|
body=plain_message,
|
|
from_email=getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa'),
|
|
to=recipients,
|
|
)
|
|
email.attach_alternative(html_message, "text/html")
|
|
|
|
# Send email
|
|
email.send(fail_silently=False)
|
|
|
|
logger.info(f"Interview invitation email sent successfully to {', '.join(recipients)}")
|
|
return {
|
|
'success': True,
|
|
'recipients_count': len(recipients),
|
|
'message': f'Interview invitation sent successfully to {len(recipients)} recipient(s)'
|
|
}
|
|
|
|
except Exception as e:
|
|
error_msg = f"Failed to send interview invitation email: {str(e)}"
|
|
logger.error(error_msg, exc_info=True)
|
|
return {'success': False, 'error': error_msg}
|
|
|
|
|
|
|
|
|
|
|
|
def send_bulk_email(subject, message, recipient_list, request=None, attachments=None, async_task_=False, from_interview=False,job=None):
|
|
"""
|
|
Send bulk email to multiple recipients with HTML support and attachments,
|
|
supporting synchronous or asynchronous dispatch.
|
|
"""
|
|
|
|
# --- 1. Categorization and Custom Message Preparation (CORRECTED) ---
|
|
if not from_interview:
|
|
|
|
agency_emails = []
|
|
pure_candidate_emails = []
|
|
candidate_through_agency_emails = []
|
|
|
|
if not recipient_list:
|
|
return {'success': False, 'error': 'No recipients provided'}
|
|
|
|
# This must contain (final_recipient_email, customized_message) for ALL sends
|
|
customized_sends = []
|
|
|
|
# 1a. Classify Recipients and Prepare Custom Messages
|
|
for email in recipient_list:
|
|
email = email.strip().lower()
|
|
|
|
try:
|
|
candidate = get_object_or_404(Application, person__email=email)
|
|
except Exception:
|
|
logger.warning(f"Candidate not found for email: {email}")
|
|
continue
|
|
|
|
candidate_name = candidate.person.full_name
|
|
|
|
|
|
# --- Candidate belongs to an agency (Final Recipient: Agency) ---
|
|
if candidate.hiring_agency and candidate.hiring_agency.email:
|
|
agency_email = candidate.hiring_agency.email
|
|
agency_message = f"Hi, {candidate_name}" + "\n" + message
|
|
|
|
# Add Agency email as the recipient with the custom message
|
|
customized_sends.append((agency_email, agency_message))
|
|
agency_emails.append(agency_email)
|
|
candidate_through_agency_emails.append(candidate.email) # For sync block only
|
|
|
|
# --- Pure Candidate (Final Recipient: Candidate) ---
|
|
else:
|
|
candidate_message = f"Hi, {candidate_name}" + "\n" + message
|
|
|
|
# Add Candidate email as the recipient with the custom message
|
|
customized_sends.append((email, candidate_message))
|
|
pure_candidate_emails.append(email) # For sync block only
|
|
|
|
# Calculate total recipients based on the size of the final send list
|
|
total_recipients = len(customized_sends)
|
|
|
|
if total_recipients == 0:
|
|
return {'success': False, 'error': 'No valid recipients found for sending.'}
|
|
else:
|
|
# For interview flow
|
|
total_recipients = len(recipient_list)
|
|
|
|
|
|
# --- 2. Handle ASYNC Dispatch (FIXED: Single loop used) ---
|
|
if async_task_:
|
|
try:
|
|
|
|
processed_attachments = attachments if attachments else []
|
|
task_ids = []
|
|
job_id=job.id
|
|
sender_user_id=request.user.id if request and hasattr(request, 'user') and request.user.is_authenticated else None
|
|
if not from_interview:
|
|
# Loop through ALL final customized sends
|
|
for recipient_email, custom_message in customized_sends:
|
|
task_id = async_task(
|
|
'recruitment.tasks.send_bulk_email_task',
|
|
subject,
|
|
custom_message, # Pass the custom message
|
|
[recipient_email], # Pass the specific recipient as a list of one
|
|
processed_attachments,
|
|
sender_user_id,
|
|
job_id,
|
|
hook='recruitment.tasks.email_success_hook',
|
|
|
|
)
|
|
task_ids.append(task_id)
|
|
|
|
logger.info(f"{len(task_ids)} tasks ({total_recipients} emails) queued.")
|
|
|
|
return {
|
|
'success': True,
|
|
'async': True,
|
|
'task_ids': task_ids,
|
|
'message': f'Emails queued for background sending to {len(task_ids)} recipient(s).'
|
|
}
|
|
|
|
else: # from_interview is True (generic send to all participants)
|
|
task_id = async_task(
|
|
'recruitment.tasks.send_bulk_email_task',
|
|
subject,
|
|
message,
|
|
recipient_list, # Send the original message to the entire list
|
|
processed_attachments,
|
|
hook='recruitment.tasks.email_success_hook'
|
|
)
|
|
task_ids.append(task_id)
|
|
logger.info(f"Interview emails queued. ID: {task_id}")
|
|
|
|
return {
|
|
'success': True,
|
|
'async': True,
|
|
'task_ids': task_ids,
|
|
'message': f'Interview emails queued for background sending to {total_recipients} recipient(s)'
|
|
}
|
|
|
|
|
|
except ImportError:
|
|
logger.error("Async execution requested, but django_q or required modules not found. Defaulting to sync.")
|
|
async_task_ = False
|
|
except Exception as e:
|
|
logger.error(f"Failed to queue async tasks: {str(e)}", exc_info=True)
|
|
return {'success': False, 'error': f"Failed to queue async tasks: {str(e)}"}
|
|
|
|
else:
|
|
# --- 3. Handle SYNCHRONOUS Send (No changes needed here, as it was fixed previously) ---
|
|
try:
|
|
# NOTE: The synchronous block below should also use the 'customized_sends'
|
|
# list for consistency instead of rebuilding messages from 'pure_candidate_emails'
|
|
# and 'agency_emails', but keeping your current logic structure to minimize changes.
|
|
|
|
from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa')
|
|
is_html = '<' in message and '>' in message
|
|
successful_sends = 0
|
|
|
|
# Helper Function for Sync Send (as provided)
|
|
def send_individual_email(recipient, body_message):
|
|
# ... (Existing helper function logic) ...
|
|
nonlocal successful_sends
|
|
|
|
if is_html:
|
|
plain_message = strip_tags(body_message)
|
|
email_obj = EmailMultiAlternatives(subject=subject, body=plain_message, from_email=from_email, to=[recipient])
|
|
email_obj.attach_alternative(body_message, "text/html")
|
|
else:
|
|
email_obj = EmailMultiAlternatives(subject=subject, body=body_message, from_email=from_email, to=[recipient])
|
|
|
|
if attachments:
|
|
for attachment in attachments:
|
|
if hasattr(attachment, 'read'):
|
|
filename = getattr(attachment, 'name', 'attachment')
|
|
content = attachment.read()
|
|
content_type = getattr(attachment, 'content_type', 'application/octet-stream')
|
|
email_obj.attach(filename, content, content_type)
|
|
elif isinstance(attachment, tuple) and len(attachment) == 3:
|
|
filename, content, content_type = attachment
|
|
email_obj.attach(filename, content, content_type)
|
|
|
|
try:
|
|
result=email_obj.send(fail_silently=False)
|
|
if result==1:
|
|
try:
|
|
user=get_object_or_404(User,email=recipient)
|
|
new_message = Message.objects.create(
|
|
sender=request.user,
|
|
recipient=user,
|
|
job=job,
|
|
subject=subject,
|
|
content=message, # Store the full HTML or plain content
|
|
message_type='DIRECT',
|
|
is_read=False, # It's just sent, not read yet
|
|
)
|
|
logger.info(f"Stored sent message ID {new_message.id} in DB.")
|
|
except Exception as e:
|
|
logger.error(f"Email sent to {recipient}, but failed to store in DB: {str(e)}")
|
|
|
|
|
|
else:
|
|
logger.error("fialed to send email")
|
|
|
|
successful_sends += 1
|
|
except Exception as e:
|
|
logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True)
|
|
|
|
if not from_interview:
|
|
# Send Emails - Pure Candidates
|
|
for email in pure_candidate_emails:
|
|
candidate_name = Application.objects.filter(person__email=email).first().person.full_name
|
|
candidate_message = f"Hi, {candidate_name}" + "\n" + message
|
|
send_individual_email(email, candidate_message)
|
|
|
|
# Send Emails - Agencies
|
|
i = 0
|
|
for email in agency_emails:
|
|
candidate_email = candidate_through_agency_emails[i]
|
|
candidate_name = Application.objects.filter(person__email=candidate_email).first().person.full_name
|
|
agency_message = f"Hi, {candidate_name}" + "\n" + message
|
|
send_individual_email(email, agency_message)
|
|
i += 1
|
|
|
|
logger.info(f"Bulk email processing complete. Sent successfully to {successful_sends} out of {total_recipients} unique recipients.")
|
|
return {
|
|
'success': True,
|
|
'recipients_count': successful_sends,
|
|
'message': f'Email processing complete. {successful_sends} email(s) were sent successfully to {total_recipients} unique intended recipients.'
|
|
}
|
|
else:
|
|
for email in recipient_list:
|
|
send_individual_email(email, message)
|
|
|
|
logger.info(f"Interview email processing complete. Sent successfully to {successful_sends} out of {total_recipients} recipients.")
|
|
return {
|
|
'success': True,
|
|
'recipients_count': successful_sends,
|
|
'message': f'Interview emails sent successfully to {successful_sends} recipient(s).'
|
|
}
|
|
|
|
except Exception as e:
|
|
error_msg = f"Failed to process bulk email send request: {str(e)}"
|
|
logger.error(error_msg, exc_info=True)
|
|
return {'success': False, 'error': error_msg} |