From d0975094f636e18b49d8f8e74fee965a51f4e329 Mon Sep 17 00:00:00 2001 From: Faheed Date: Thu, 11 Dec 2025 14:29:00 +0300 Subject: [PATCH] email send from interview detail page --- recruitment/email_service.py | 183 +++++++++------------ recruitment/forms.py | 34 ++-- recruitment/views.py | 66 +++++--- templates/interviews/interview_detail.html | 33 +--- 4 files changed, 143 insertions(+), 173 deletions(-) diff --git a/recruitment/email_service.py b/recruitment/email_service.py index 19d68a5..294c818 100644 --- a/recruitment/email_service.py +++ b/recruitment/email_service.py @@ -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, 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 = [] + agency_emails = [] + pure_candidate_emails = [] + candidate_through_agency_emails = [] - if not recipient_list: - return {'success': False, 'error': 'No recipients provided'} + if not recipient_list: + return {'success': False, 'error': 'No recipients provided'} - # This must contain (final_recipient_email, customized_message) for ALL sends - customized_sends = [] + # 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() + # 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 + 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_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 + # --- 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 + # 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 + # --- 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 + # 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) + # 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.'} - 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) --- @@ -306,49 +303,30 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments= 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 - - task_id = async_task( - 'recruitment.tasks.send_bulk_email_task', - subject, - customized_sends, - processed_attachments, - sender_user_id, - job_id, - hook='recruitment.tasks.email_success_hook', + + task_id = async_task( + 'recruitment.tasks.send_bulk_email_task', + subject, + customized_sends, + processed_attachments, + sender_user_id, + job_id, + 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 { - '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)' - } + return { + 'success': True, + 'async': True, + 'task_ids': task_ids, + 'message': f'Emails queued for background sending to {len(task_ids)} recipient(s).' + } except ImportError: @@ -398,38 +376,29 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments= 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(email=email).first().first_name - candidate_message = f"Hi, {candidate_name}" + "\n" + message - send_individual_email(email, candidate_message) + + # 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 + # 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).' - } + 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.' + } + except Exception as e: error_msg = f"Failed to process bulk email send request: {str(e)}" diff --git a/recruitment/forms.py b/recruitment/forms.py index 5464df7..e8696de 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -2036,12 +2036,11 @@ class SettingsForm(forms.ModelForm): class InterviewEmailForm(forms.Form): """Form for composing emails to participants about a candidate""" - to = forms.MultipleChoiceField( - widget=forms.CheckboxSelectMultiple(attrs={ - 'class': 'form-check' - }), - label=_('Select Candidates'), # Use a descriptive label - required=False + to = forms.CharField( + + label=_('To'), # Use a descriptive label + required=True, + ) subject = forms.CharField( @@ -2069,14 +2068,16 @@ class InterviewEmailForm(forms.Form): def __init__(self, job, application,schedule, *args, **kwargs): applicant=application.person.user interview=schedule.interview - + super().__init__(*args, **kwargs) if application.hiring_agency: self.fields['to'].initial=application.hiring_agency.email + self.fields['to'].disabled= True + + else: self.fields['to'].initial=application.person.email - - - super().__init__(*args, **kwargs) + self.fields['to'].disabled= True + # 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: -Date: {interview.interview_date} -Time: {interview.interview_time} +Date: {interview.start_time.strftime("%d-%m-%Y")} +Time: {interview.start_time.strftime("%I:%M %p")} +Interview Duration: {interview.duration} minutes Job: {job.title} """ - if schedule.location_type == 'Remote': - initial_message += "This is a remote schedule. You will receive the meeting link separately.\n\n" + if interview.location_type == 'Remote': + initial_message += f"Pease join using meeting link {interview.details_url} .\n\n" 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 += """ Best regards, -HR Team +KAAUH Hiring Team """ self.fields['message'].initial = initial_message diff --git a/recruitment/views.py b/recruitment/views.py index 5d0a101..bdf3c1b 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -90,7 +90,8 @@ from .forms import ( BulkInterviewTemplateForm, SettingsForm, InterviewCancelForm, - InterviewEmailForm + InterviewEmailForm, + ApplicationStageForm ) from .utils import generate_random_password from django.views.decorators.csrf import csrf_exempt @@ -1018,13 +1019,15 @@ def delete_form_template(request, template_id): ) -@login_required -@staff_or_candidate_required -def application_submit_form(request, template_slug): +# @login_required +# @staff_or_candidate_required +def application_submit_form(request, slug): """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: 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": person=request.user.person_profile if job.has_already_applied_to_this_job(person): @@ -3949,11 +3952,11 @@ def api_application_detail(request, candidate_id): @login_required @staff_user_required -def compose_application_email(request, job_slug): +def compose_application_email(request, slug): """Compose email to participants about a candidate""" 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') candidates=Application.objects.filter(id__in=candidate_ids) @@ -3997,7 +4000,6 @@ def compose_application_email(request, job_slug): request=request, attachments=None, async_task_=True, # Changed to False to avoid pickle issues - from_interview=False, job=job ) @@ -4386,16 +4388,19 @@ def interview_detail(request, slug): schedule = get_object_or_404(ScheduledInterview, slug=slug) interview = schedule.interview - + application=schedule.application + job=schedule.job reschedule_form = ScheduledInterviewForm() reschedule_form.initial['topic'] = interview.topic meeting=interview + interview_email_form=InterviewEmailForm(job,application,schedule) context = { 'schedule': schedule, 'interview': interview, 'reschedule_form':reschedule_form, 'interview_status_form':ScheduledInterviewUpdateStatusForm(), 'cancel_form':InterviewCancelForm(instance=meeting), + 'interview_email_form':interview_email_form } 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): schedule=get_object_or_404(ScheduledInterview,slug=slug) - application=schedule.application.first() + application=schedule.application job=application.job form=InterviewEmailForm(job,application,schedule) if request.method=='POST': - recipient=form.cleaned_data.get('to').strip() - body_message=form.cleaned_data.get('message') - sender=request.user - job=job - pass + form=InterviewEmailForm(job, application, schedule, request.POST) + if form.is_valid(): + recipient=form.cleaned_data.get('to').strip() + body_message=form.cleaned_data.get('message') + 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) - - - # async_task('recruitment.tasks._task_send_individual_email', 'value1', 'value2') - # def _task_send_individual_email(subject, body_message, recipient, attachments,sender,job): \ No newline at end of file + # This is the final return, which handles GET requests and invalid POST requests. + return redirect('interview_detail',slug=schedule.slug) + \ No newline at end of file diff --git a/templates/interviews/interview_detail.html b/templates/interviews/interview_detail.html index cc5d3a1..9580c80 100644 --- a/templates/interviews/interview_detail.html +++ b/templates/interviews/interview_detail.html @@ -595,38 +595,9 @@