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)
@ -351,6 +357,7 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
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: try:
# NOTE: The synchronous block below should also use the 'customized_sends' # NOTE: The synchronous block below should also use the 'customized_sends'
@ -385,7 +392,27 @@ def send_bulk_email(subject, message, recipient_list, request=None, 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)
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 successful_sends += 1
except Exception as e: except Exception as e:
logger.error(f"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)
@ -393,7 +420,7 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
if not from_interview: if not from_interview:
# Send Emails - Pure Candidates # Send Emails - Pure Candidates
for email in pure_candidate_emails: for email in pure_candidate_emails:
candidate_name = Application.objects.filter(email=email).first().first_name candidate_name = Application.objects.filter(person__email=email).first().person.full_name
candidate_message = f"Hi, {candidate_name}" + "\n" + message candidate_message = f"Hi, {candidate_name}" + "\n" + message
send_individual_email(email, candidate_message) send_individual_email(email, candidate_message)
@ -401,7 +428,7 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
i = 0 i = 0
for email in agency_emails: for email in agency_emails:
candidate_email = candidate_through_agency_emails[i] candidate_email = candidate_through_agency_emails[i]
candidate_name = Application.objects.filter(email=candidate_email).first().first_name candidate_name = Application.objects.filter(person__email=candidate_email).first().person.full_name
agency_message = f"Hi, {candidate_name}" + "\n" + message agency_message = f"Hi, {candidate_name}" + "\n" + message
send_individual_email(email, agency_message) send_individual_email(email, agency_message)
i += 1 i += 1

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: except Exception as e:
logger.error(f"Task failed to send email to {recipient}: {str(e)}", exc_info=True) logger.error(f"Email sent to {recipient}, but failed to store in DB: {str(e)}")
return False
def send_bulk_email_task(subject, message, recipient_list, attachments=None, hook='recruitment.tasks.email_success_hook'): else:
logger.error("fialed to send email")
except Exception as e:
logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True)
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")
@ -797,10 +815,12 @@ 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,19 +163,15 @@
<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">
<input type="text" name="q" class="form-control" id="search" <form method="get" action="" class="w-100">
placeholder="{% trans 'Search applicant...' %}" {% include 'includes/search_form.html' %}
value="{{ request.GET.q }}">
<button class="btn btn-main-action" type="submit">
<i class="fas fa-search"></i>
</button>
</div>
</form> </form>
</div> </div>
</div>
<div class="col-md-6"> <div class="col-md-6">
<form method="GET" class="row g-3 align-items-end h-100"> <form method="GET" class="row g-3 align-items-end h-100">
@ -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">
@ -218,6 +214,7 @@
</div> </div>
</div> </div>
{% if people_list %} {% if people_list %}
<div id="person-list"> <div id="person-list">
<!-- View Switcher --> <!-- View Switcher -->