messages creation for email

This commit is contained in:
Faheed 2025-11-18 18:26:45 +03:00
parent ee88e8bad8
commit acee95995c
13 changed files with 167 additions and 129 deletions

2
.gitignore vendored
View File

@ -53,7 +53,7 @@ htmlcov/
# Media and Static files (if served locally and not meant for version control) # Media and Static files (if served locally and not meant for version control)
media/ media/
static/
# Deployment files # Deployment files
*.tar.gz *.tar.gz

View File

@ -1,15 +1,24 @@
""" """
Email service for sending notifications related to agency messaging. 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.core.mail import send_mail, EmailMultiAlternatives
from django.conf import settings from django.conf import settings
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.utils.html import strip_tags from django.utils.html import strip_tags
from django.contrib.auth import get_user_model
import logging import logging
from .models import Message
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
User=get_user_model()
class EmailService: class EmailService:
""" """
@ -225,17 +234,10 @@ def send_interview_invitation_email(candidate, job, meeting_details=None, recipi
return {'success': False, 'error': error_msg} return {'success': False, 'error': error_msg}
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__)
def send_bulk_email(subject, message, recipient_list, request=None, attachments=None, async_task_=False, from_interview=False):
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, Send bulk email to multiple recipients with HTML support and attachments,
supporting synchronous or asynchronous dispatch. supporting synchronous or asynchronous dispatch.
@ -301,7 +303,8 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
processed_attachments = attachments if attachments else [] processed_attachments = attachments if attachments else []
task_ids = [] 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: if not from_interview:
# Loop through ALL final customized sends # Loop through ALL final customized sends
for recipient_email, custom_message in customized_sends: for recipient_email, custom_message in customized_sends:
@ -311,7 +314,10 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
custom_message, # Pass the custom message custom_message, # Pass the custom message
[recipient_email], # Pass the specific recipient as a list of one [recipient_email], # Pass the specific recipient as a list of one
processed_attachments, processed_attachments,
hook='recruitment.tasks.email_success_hook' sender_user_id,
job_id,
hook='recruitment.tasks.email_success_hook',
) )
task_ids.append(task_id) task_ids.append(task_id)
@ -350,80 +356,101 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
except Exception as e: except Exception as e:
logger.error(f"Failed to queue async tasks: {str(e)}", exc_info=True) 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)}"} 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) --- # --- 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: try:
email_obj.send(fail_silently=False) # NOTE: The synchronous block below should also use the 'customized_sends'
successful_sends += 1 # 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: except Exception as e:
logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True) error_msg = f"Failed to process bulk email send request: {str(e)}"
logger.error(error_msg, exc_info=True)
if not from_interview: return {'success': False, 'error': error_msg}
# Send Emails - Pure Candidates
for email in pure_candidate_emails:
candidate_name = Application.objects.filter(email=email).first().first_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(email=candidate_email).first().first_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}

View File

@ -12,8 +12,9 @@ from . linkedin_service import LinkedInService
from django.shortcuts import get_object_or_404 from django.shortcuts import get_object_or_404
from . models import JobPosting from . models import JobPosting
from django.utils import timezone from django.utils import timezone
from . models import InterviewSchedule,ScheduledInterview,ZoomMeetingDetails from . models import InterviewSchedule,ScheduledInterview,ZoomMeetingDetails,Message
from django.contrib.auth import get_user_model
User = get_user_model()
# Add python-docx import for Word document processing # Add python-docx import for Word document processing
try: try:
from docx import Document from docx import Document
@ -28,7 +29,7 @@ logger = logging.getLogger(__name__)
OPENROUTER_API_KEY ='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' OPENROUTER_API_KEY ='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'
# OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free' # OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free'
OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct' OPENROUTER_MODEL = 'openai/gpt-oss-20b'
# OPENROUTER_MODEL = 'openai/gpt-oss-20b' # OPENROUTER_MODEL = 'openai/gpt-oss-20b'
# OPENROUTER_MODEL = 'mistralai/mistral-small-3.2-24b-instruct:free' # OPENROUTER_MODEL = 'mistralai/mistral-small-3.2-24b-instruct:free'
@ -506,7 +507,6 @@ def handle_zoom_webhook_event(payload):
Background task to process a Zoom webhook event and update the local ZoomMeeting status. Background task to process a Zoom webhook event and update the local ZoomMeeting status.
It handles: created, updated, started, ended, and deleted events. It handles: created, updated, started, ended, and deleted events.
""" """
print(payload)
event_type = payload.get('event') event_type = payload.get('event')
object_data = payload['payload']['object'] object_data = payload['payload']['object']
@ -535,9 +535,7 @@ def handle_zoom_webhook_event(payload):
# elif event_type == 'meeting.updated': # elif event_type == 'meeting.updated':
# Only update time fields if they are in the payload # Only update time fields if they are in the payload
print(object_data) print(object_data)
meeting_start_time = object_data.get('start_time', meeting_instance.start_time) meeting_instance.start_time = object_data.get('start_time', meeting_instance.start_time)
if meeting_start_time:
meeting_instance.start_time = datetime.fromisoformat(meeting_start_time)
meeting_instance.duration = object_data.get('duration', meeting_instance.duration) meeting_instance.duration = object_data.get('duration', meeting_instance.duration)
meeting_instance.timezone = object_data.get('timezone', meeting_instance.timezone) meeting_instance.timezone = object_data.get('timezone', meeting_instance.timezone)
@ -758,7 +756,7 @@ from django.conf import settings
from django.core.mail import EmailMultiAlternatives from django.core.mail import EmailMultiAlternatives
from django.utils.html import strip_tags from django.utils.html import strip_tags
def _task_send_individual_email(subject, body_message, recipient, attachments): def _task_send_individual_email(subject, body_message, recipient, attachments,sender,job):
"""Internal helper to create and send a single email.""" """Internal helper to create and send a single email."""
from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa') from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa')
@ -778,16 +776,36 @@ def _task_send_individual_email(subject, body_message, recipient, attachments):
email_obj.attach(filename, content, content_type) email_obj.attach(filename, content, content_type)
try: try:
email_obj.send(fail_silently=False) result=email_obj.send(fail_silently=False)
return True
if result==1:
try:
user=get_object_or_404(User,email=recipient)
new_message = Message.objects.create(
sender=sender,
recipient=user,
job=job,
subject=subject,
content=body_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")
except Exception as e: except Exception as e:
logger.error(f"Task failed to send email to {recipient}: {str(e)}", exc_info=True) logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True)
return False
def send_bulk_email_task(subject, message, recipient_list, attachments=None, hook='recruitment.tasks.email_success_hook'): def send_bulk_email_task(subject, message, recipient_list,attachments=None,sender_user_id=None,job_id=None, hook='recruitment.tasks.email_success_hook'):
""" """
Django-Q background task to send pre-formatted email to a list of recipients. Django-Q background task to send pre-formatted email to a list of recipients.,
Receives arguments directly from the async_task call. Receives arguments directly from the async_task call.
""" """
logger.info(f"Starting bulk email task for {len(recipient_list)} recipients") logger.info(f"Starting bulk email task for {len(recipient_list)} recipients")
@ -796,11 +814,13 @@ def send_bulk_email_task(subject, message, recipient_list, attachments=None, hoo
if not recipient_list: if not recipient_list:
return {'success': False, 'error': 'No recipients provided to task.'} return {'success': False, 'error': 'No recipients provided to task.'}
sender=get_object_or_404(User,pk=sender_user_id)
job=get_object_or_404(JobPosting,pk=job_id)
# Since the async caller sends one task per recipient, total_recipients should be 1. # Since the async caller sends one task per recipient, total_recipients should be 1.
for recipient in recipient_list: for recipient in recipient_list:
# The 'message' is the custom message specific to this recipient. # The 'message' is the custom message specific to this recipient.
if _task_send_individual_email(subject, message, recipient, attachments): if _task_send_individual_email(subject, message, recipient, attachments,sender,job):
successful_sends += 1 successful_sends += 1
if successful_sends > 0: if successful_sends > 0:

View File

@ -5206,7 +5206,7 @@ def compose_candidate_email(request, job_slug):
if request.method == 'POST': if request.method == 'POST':
print("........................................................inside candidate conpose.............")
candidate_ids = request.POST.getlist('candidate_ids') candidate_ids = request.POST.getlist('candidate_ids')
candidates=Application.objects.filter(id__in=candidate_ids) candidates=Application.objects.filter(id__in=candidate_ids)
form = CandidateEmailForm(job, candidates, request.POST) form = CandidateEmailForm(job, candidates, request.POST)
@ -5233,14 +5233,16 @@ def compose_candidate_email(request, job_slug):
# Send emails using email service (no attachments, synchronous to avoid pickle issues) # Send emails using email service (no attachments, synchronous to avoid pickle issues)
email_result = send_bulk_email( email_result = send_bulk_email( #
subject=subject, subject=subject,
message=message, message=message,
recipient_list=email_addresses, recipient_list=email_addresses,
request=request, request=request,
attachments=None, attachments=None,
async_task_=True, # Changed to False to avoid pickle issues async_task_=True, # Changed to False to avoid pickle issues
from_interview=False from_interview=False,
job=job
) )
if email_result["success"]: if email_result["success"]:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 8.4 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 282 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 74 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 82 KiB

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 10 KiB

View File

@ -155,7 +155,7 @@
<i class="fas fa-user-friends me-2"></i> {% trans "Applicants List" %} <i class="fas fa-user-friends me-2"></i> {% trans "Applicants List" %}
</h1> </h1>
<a href="{% url 'person_create' %}" class="btn btn-main-action"> <a href="{% url 'person_create' %}" class="btn btn-main-action">
<i class="fas fa-plus me-1"></i> {% trans "Add New" %} <i class="fas fa-plus me-1"></i> {% trans "Add New Applicant" %}
</a> </a>
</div> </div>
@ -163,18 +163,14 @@
<div class="card mb-4 shadow-sm no-hover"> <div class="card mb-4 shadow-sm no-hover">
<div class="card-body"> <div class="card-body">
<div class="row g-4"> <div class="row g-4">
<div class="col-md-6"> <div class="col-md-6">
<label for="search" class="form-label small text-muted">{% trans "Search by Name or Email" %}</label> <label for="search" class="form-label small text-muted">{% trans "Search by Name or Email" %}</label>
<form method="get" action="" class="w-100"> <div class="input-group input-group-lg">
<div class="input-group input-group-lg"> <form method="get" action="" class="w-100">
<input type="text" name="q" class="form-control" id="search" {% include 'includes/search_form.html' %}
placeholder="{% trans 'Search applicant...' %}" </form>
value="{{ request.GET.q }}"> </div>
<button class="btn btn-main-action" type="submit">
<i class="fas fa-search"></i>
</button>
</div>
</form>
</div> </div>
<div class="col-md-6"> <div class="col-md-6">
@ -200,10 +196,10 @@
</select> </select>
</div> </div>
<div class="col-md-4 d-flex justify-content-end align-self-end"> <div class="col-md-4 d-flex">
<div class="filter-buttons"> <div class="filter-buttons">
<button type="submit" class="btn btn-main-action btn-sm"> <button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-filter me-1"></i> {% trans "Apply" %} <i class="fas fa-filter me-1"></i> {% trans "Apply Filter" %}
</button> </button>
{% if request.GET.q or request.GET.nationality or request.GET.gender %} {% if request.GET.q or request.GET.nationality or request.GET.gender %}
<a href="{% url 'person_list' %}" class="btn btn-outline-secondary btn-sm"> <a href="{% url 'person_list' %}" class="btn btn-outline-secondary btn-sm">
@ -217,7 +213,8 @@
</div> </div>
</div> </div>
</div> </div>
{% if people_list %} {% if people_list %}
<div id="person-list"> <div id="person-list">
<!-- View Switcher --> <!-- View Switcher -->