email send from interview detail page

This commit is contained in:
Faheed 2025-12-11 14:29:00 +03:00
parent d90a8d5048
commit d0975094f6
4 changed files with 143 additions and 173 deletions

View File

@ -237,64 +237,61 @@ def send_interview_invitation_email(candidate, job, meeting_details=None, recipi
def send_bulk_email(subject, message, recipient_list, request=None, attachments=None, async_task_=False, from_interview=False,job=None): def send_bulk_email(subject, message, recipient_list, request=None, attachments=None, async_task_=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.
""" """
# --- 1. Categorization and Custom Message Preparation (CORRECTED) --- # --- 1. Categorization and Custom Message Preparation (CORRECTED) ---
if not from_interview:
agency_emails = [] agency_emails = []
pure_candidate_emails = [] pure_candidate_emails = []
candidate_through_agency_emails = [] candidate_through_agency_emails = []
if not recipient_list: if not recipient_list:
return {'success': False, 'error': 'No recipients provided'} return {'success': False, 'error': 'No recipients provided'}
# This must contain (final_recipient_email, customized_message) for ALL sends # This must contain (final_recipient_email, customized_message) for ALL sends
customized_sends = [] customized_sends = []
# 1a. Classify Recipients and Prepare Custom Messages # 1a. Classify Recipients and Prepare Custom Messages
for email in recipient_list: for email in recipient_list:
email = email.strip().lower() email = email.strip().lower()
try: try:
candidate = get_object_or_404(Application, person__email=email) candidate = get_object_or_404(Application, person__email=email)
except Exception: except Exception:
logger.warning(f"Candidate not found for email: {email}") logger.warning(f"Candidate not found for email: {email}")
continue continue
candidate_name = candidate.person.full_name candidate_name = candidate.person.full_name
# --- Candidate belongs to an agency (Final Recipient: Agency) --- # --- Candidate belongs to an agency (Final Recipient: Agency) ---
if candidate.hiring_agency and candidate.hiring_agency.email: if candidate.hiring_agency and candidate.hiring_agency.email:
agency_email = candidate.hiring_agency.email agency_email = candidate.hiring_agency.email
agency_message = f"Hi, {candidate_name}" + "\n" + message agency_message = f"Hi, {candidate_name}" + "\n" + message
# Add Agency email as the recipient with the custom message # Add Agency email as the recipient with the custom message
customized_sends.append((agency_email, agency_message)) customized_sends.append((agency_email, agency_message))
agency_emails.append(agency_email) agency_emails.append(agency_email)
candidate_through_agency_emails.append(candidate.email) # For sync block only candidate_through_agency_emails.append(candidate.email) # For sync block only
# --- Pure Candidate (Final Recipient: Candidate) --- # --- Pure Candidate (Final Recipient: Candidate) ---
else: else:
candidate_message = f"Hi, {candidate_name}" + "\n" + message candidate_message = f"Hi, {candidate_name}" + "\n" + message
# Add Candidate email as the recipient with the custom message # Add Candidate email as the recipient with the custom message
customized_sends.append((email, candidate_message)) customized_sends.append((email, candidate_message))
pure_candidate_emails.append(email) # For sync block only pure_candidate_emails.append(email) # For sync block only
# Calculate total recipients based on the size of the final send list # Calculate total recipients based on the size of the final send list
total_recipients = len(customized_sends) total_recipients = len(customized_sends)
if total_recipients == 0:
return {'success': False, 'error': 'No valid recipients found for sending.'}
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) --- # --- 2. Handle ASYNC Dispatch (FIXED: Single loop used) ---
@ -306,49 +303,30 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
job_id=job.id job_id=job.id
sender_user_id=request.user.id if request and hasattr(request, 'user') and request.user.is_authenticated else None 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 # Loop through ALL final customized sends
task_id = async_task( task_id = async_task(
'recruitment.tasks.send_bulk_email_task', 'recruitment.tasks.send_bulk_email_task',
subject, subject,
customized_sends, customized_sends,
processed_attachments, processed_attachments,
sender_user_id, sender_user_id,
job_id, job_id,
hook='recruitment.tasks.email_success_hook', hook='recruitment.tasks.email_success_hook',
) )
task_ids.append(task_id) task_ids.append(task_id)
logger.info(f"{len(task_ids)} tasks ({total_recipients} emails) queued.") logger.info(f"{len(task_ids)} tasks ({total_recipients} emails) queued.")
return { return {
'success': True, 'success': True,
'async': True, 'async': True,
'task_ids': task_ids, 'task_ids': task_ids,
'message': f'Emails queued for background sending to {len(task_ids)} recipient(s).' '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: except ImportError:
@ -398,38 +376,29 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments=
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)
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(email=email).first().first_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)
# Send Emails - Agencies # Send Emails - Agencies
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(email=candidate_email).first().first_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
logger.info(f"Bulk email processing complete. Sent successfully to {successful_sends} out of {total_recipients} unique recipients.") logger.info(f"Bulk email processing complete. Sent successfully to {successful_sends} out of {total_recipients} unique recipients.")
return { return {
'success': True, 'success': True,
'recipients_count': successful_sends, 'recipients_count': successful_sends,
'message': f'Email processing complete. {successful_sends} email(s) were sent successfully to {total_recipients} unique intended recipients.' '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:
error_msg = f"Failed to process bulk email send request: {str(e)}" error_msg = f"Failed to process bulk email send request: {str(e)}"

View File

@ -2036,12 +2036,11 @@ class SettingsForm(forms.ModelForm):
class InterviewEmailForm(forms.Form): class InterviewEmailForm(forms.Form):
"""Form for composing emails to participants about a candidate""" """Form for composing emails to participants about a candidate"""
to = forms.MultipleChoiceField( to = forms.CharField(
widget=forms.CheckboxSelectMultiple(attrs={
'class': 'form-check' label=_('To'), # Use a descriptive label
}), required=True,
label=_('Select Candidates'), # Use a descriptive label
required=False
) )
subject = forms.CharField( subject = forms.CharField(
@ -2069,14 +2068,16 @@ class InterviewEmailForm(forms.Form):
def __init__(self, job, application,schedule, *args, **kwargs): def __init__(self, job, application,schedule, *args, **kwargs):
applicant=application.person.user applicant=application.person.user
interview=schedule.interview interview=schedule.interview
super().__init__(*args, **kwargs)
if application.hiring_agency: if application.hiring_agency:
self.fields['to'].initial=application.hiring_agency.email self.fields['to'].initial=application.hiring_agency.email
self.fields['to'].disabled= True
else: else:
self.fields['to'].initial=application.person.email self.fields['to'].initial=application.person.email
self.fields['to'].disabled= True
super().__init__(*args, **kwargs)
# Set initial message with candidate and meeting info # Set initial message with candidate and meeting info
@ -2085,20 +2086,21 @@ Dear {applicant.first_name} {applicant.last_name},
Your interview details are as follows: Your interview details are as follows:
Date: {interview.interview_date} Date: {interview.start_time.strftime("%d-%m-%Y")}
Time: {interview.interview_time} Time: {interview.start_time.strftime("%I:%M %p")}
Interview Duration: {interview.duration} minutes
Job: {job.title} Job: {job.title}
""" """
if schedule.location_type == 'Remote': if interview.location_type == 'Remote':
initial_message += "This is a remote schedule. You will receive the meeting link separately.\n\n" initial_message += f"Pease join using meeting link {interview.details_url} .\n\n"
else: else:
email_body += "This is an onsite schedule. Please arrive 10 minutes early.\n\n" initial_message += "This is an onsite schedule. Please arrive 10 minutes early.\n\n"
initial_message += """ initial_message += """
Best regards, Best regards,
HR Team KAAUH Hiring Team
""" """
self.fields['message'].initial = initial_message self.fields['message'].initial = initial_message

View File

@ -90,7 +90,8 @@ from .forms import (
BulkInterviewTemplateForm, BulkInterviewTemplateForm,
SettingsForm, SettingsForm,
InterviewCancelForm, InterviewCancelForm,
InterviewEmailForm InterviewEmailForm,
ApplicationStageForm
) )
from .utils import generate_random_password from .utils import generate_random_password
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
@ -1018,13 +1019,15 @@ def delete_form_template(request, template_id):
) )
@login_required # @login_required
@staff_or_candidate_required # @staff_or_candidate_required
def application_submit_form(request, template_slug): def application_submit_form(request, slug):
"""Display the form as a step-by-step wizard""" """Display the form as a step-by-step wizard"""
form_template=get_object_or_404(FormTemplate,slug=slug,is_active=True)
if not request.user.is_authenticated: if not request.user.is_authenticated:
return redirect("application_signup",slug=slug) return redirect("application_signup",slug=slug)
job = get_object_or_404(JobPosting, form_template__slug=slug) print(form_template.job.slug)
job = get_object_or_404(JobPosting, slug=form_template.job.slug)
if request.user.user_type == "candidate": if request.user.user_type == "candidate":
person=request.user.person_profile person=request.user.person_profile
if job.has_already_applied_to_this_job(person): if job.has_already_applied_to_this_job(person):
@ -3949,11 +3952,11 @@ def api_application_detail(request, candidate_id):
@login_required @login_required
@staff_user_required @staff_user_required
def compose_application_email(request, job_slug): def compose_application_email(request, slug):
"""Compose email to participants about a candidate""" """Compose email to participants about a candidate"""
from .email_service import send_bulk_email from .email_service import send_bulk_email
job = get_object_or_404(JobPosting, slug=job_slug) job = get_object_or_404(JobPosting, slug=slug)
candidate_ids=request.GET.getlist('candidate_ids') candidate_ids=request.GET.getlist('candidate_ids')
candidates=Application.objects.filter(id__in=candidate_ids) candidates=Application.objects.filter(id__in=candidate_ids)
@ -3997,7 +4000,6 @@ def compose_application_email(request, job_slug):
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,
job=job job=job
) )
@ -4386,16 +4388,19 @@ def interview_detail(request, slug):
schedule = get_object_or_404(ScheduledInterview, slug=slug) schedule = get_object_or_404(ScheduledInterview, slug=slug)
interview = schedule.interview interview = schedule.interview
application=schedule.application
job=schedule.job
reschedule_form = ScheduledInterviewForm() reschedule_form = ScheduledInterviewForm()
reschedule_form.initial['topic'] = interview.topic reschedule_form.initial['topic'] = interview.topic
meeting=interview meeting=interview
interview_email_form=InterviewEmailForm(job,application,schedule)
context = { context = {
'schedule': schedule, 'schedule': schedule,
'interview': interview, 'interview': interview,
'reschedule_form':reschedule_form, 'reschedule_form':reschedule_form,
'interview_status_form':ScheduledInterviewUpdateStatusForm(), 'interview_status_form':ScheduledInterviewUpdateStatusForm(),
'cancel_form':InterviewCancelForm(instance=meeting), 'cancel_form':InterviewCancelForm(instance=meeting),
'interview_email_form':interview_email_form
} }
return render(request, 'interviews/interview_detail.html', context) return render(request, 'interviews/interview_detail.html', context)
@ -5741,17 +5746,40 @@ def sync_history(request, job_slug=None):
def send_interview_email(request,slug): def send_interview_email(request,slug):
schedule=get_object_or_404(ScheduledInterview,slug=slug) schedule=get_object_or_404(ScheduledInterview,slug=slug)
application=schedule.application.first() application=schedule.application
job=application.job job=application.job
form=InterviewEmailForm(job,application,schedule) form=InterviewEmailForm(job,application,schedule)
if request.method=='POST': if request.method=='POST':
recipient=form.cleaned_data.get('to').strip() form=InterviewEmailForm(job, application, schedule, request.POST)
body_message=form.cleaned_data.get('message') if form.is_valid():
sender=request.user recipient=form.cleaned_data.get('to').strip()
job=job body_message=form.cleaned_data.get('message')
pass subject=form.cleaned_data.get('subject')
sender=request.user
job=job
try:
email_result = async_task('recruitment.tasks._task_send_individual_email',
subject=subject,
body_message=body_message,
recipient=recipient,
attachments=None,
sender=sender,
job=job
)
if email_result:
messages.success(request, "Message sent successfully via email!")
else:
messages.warning(request, f"email failed: {email_result.get('message', 'Unknown error')}")
except Exception as e:
messages.warning(request, f"Message saved but email sending failed: {str(e)}")
else:
form=InterviewEmailForm(job,application,schedule)
else: # GET request
form = InterviewEmailForm(job, application, schedule)
# This is the final return, which handles GET requests and invalid POST requests.
return redirect('interview_detail',slug=schedule.slug)
# async_task('recruitment.tasks._task_send_individual_email', 'value1', 'value2')
# def _task_send_individual_email(subject, body_message, recipient, attachments,sender,job):

View File

@ -595,38 +595,9 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button> <button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
<form method="post" action="#"> <form method="post" action="{% url 'send_interview_email' schedule.slug %}">
{% csrf_token %} {% csrf_token %}
<div class="mb-3"> {{interview_email_form|crispy}}
<label for="email_to" class="form-label">{% trans "To" %}</label>
<input type="email" class="form-control" id="email_to" value="{{ schedule.application.email }}" readonly>
</div>
<div class="mb-3">
<label for="email_subject" class="form-label">{% trans "Subject" %}</label>
<input type="text" class="form-control" id="email_subject" name="subject"
value="{% trans 'Interview Details' %} - {{ schedule.job.title }}">
</div>
<div class="mb-3">
<label for="email_message" class="form-label">{% trans "Message" %}</label>
<textarea class="form-control" id="email_message" name="message" rows="6">
{% trans "Dear" %} {{ schedule.application.name }},
{% trans "Your interview details are as follows:" %}
{% trans "Date:" %} {{ schedule.interview_date|date:"d-m-Y" }}
{% trans "Time:" %} {{ schedule.interview_time|date:"h:i A" }}
{% trans "Job:" %} {{ schedule.job.title }}
{% if interview.location_type == 'Remote' %}
{% trans "This is a remote schedule. You will receive the meeting link separately." %}
{% else %}
{% trans "This is an onsite schedule. Please arrive 10 minutes early." %}
{% endif %}
{% trans "Best regards," %}
{% trans "HR Team" %}
</textarea>
</div>
<button type="submit" class="btn btn-main-action btn-sm"> <button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-paper-plane me-1"></i> {% trans "Send Email" %} <i class="fas fa-paper-plane me-1"></i> {% trans "Send Email" %}
</button> </button>