diff --git a/.gitignore b/.gitignore index 9d5f7ef..f765b7b 100644 --- a/.gitignore +++ b/.gitignore @@ -53,7 +53,7 @@ htmlcov/ # Media and Static files (if served locally and not meant for version control) media/ - +static/ # Deployment files *.tar.gz diff --git a/recruitment/__pycache__/urls.cpython-312.pyc b/recruitment/__pycache__/urls.cpython-312.pyc index 543d155..4afb170 100644 Binary files a/recruitment/__pycache__/urls.cpython-312.pyc and b/recruitment/__pycache__/urls.cpython-312.pyc differ diff --git a/recruitment/__pycache__/views.cpython-312.pyc b/recruitment/__pycache__/views.cpython-312.pyc index 23d8225..a4efc7d 100644 Binary files a/recruitment/__pycache__/views.cpython-312.pyc and b/recruitment/__pycache__/views.cpython-312.pyc differ diff --git a/recruitment/email_service.py b/recruitment/email_service.py index e4adc32..8ddedda 100644 --- a/recruitment/email_service.py +++ b/recruitment/email_service.py @@ -1,15 +1,24 @@ """ 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: """ @@ -225,17 +234,10 @@ def send_interview_invitation_email(candidate, job, meeting_details=None, recipi 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, 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 [] 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: @@ -311,7 +314,10 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments= custom_message, # Pass the custom message [recipient_email], # Pass the specific recipient as a list of one processed_attachments, - hook='recruitment.tasks.email_success_hook' + sender_user_id, + job_id, + hook='recruitment.tasks.email_success_hook', + ) task_ids.append(task_id) @@ -350,80 +356,101 @@ def send_bulk_email(subject, message, recipient_list, request=None, attachments= 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: - email_obj.send(fail_silently=False) - successful_sends += 1 + # 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: - 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 - 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} \ No newline at end of file + 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} \ No newline at end of file diff --git a/recruitment/tasks.py b/recruitment/tasks.py index 7eaf1d4..8ecf803 100644 --- a/recruitment/tasks.py +++ b/recruitment/tasks.py @@ -12,8 +12,9 @@ from . linkedin_service import LinkedInService from django.shortcuts import get_object_or_404 from . models import JobPosting 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 try: from docx import Document @@ -28,7 +29,7 @@ logger = logging.getLogger(__name__) OPENROUTER_API_KEY ='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' # 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 = '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. It handles: created, updated, started, ended, and deleted events. """ - print(payload) event_type = payload.get('event') object_data = payload['payload']['object'] @@ -535,9 +535,7 @@ def handle_zoom_webhook_event(payload): # elif event_type == 'meeting.updated': # Only update time fields if they are in the payload print(object_data) - meeting_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.start_time = object_data.get('start_time', meeting_instance.start_time) meeting_instance.duration = object_data.get('duration', meeting_instance.duration) 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.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.""" 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) try: - email_obj.send(fail_silently=False) - return True + 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=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: - logger.error(f"Task failed to send email to {recipient}: {str(e)}", exc_info=True) - return False + 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, 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. """ 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: 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. for recipient in recipient_list: # 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 if successful_sends > 0: diff --git a/recruitment/views.py b/recruitment/views.py index 675ee16..72b5e92 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -5206,7 +5206,7 @@ def compose_candidate_email(request, job_slug): if request.method == 'POST': - print("........................................................inside candidate conpose.............") + candidate_ids = request.POST.getlist('candidate_ids') candidates=Application.objects.filter(id__in=candidate_ids) 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) - email_result = send_bulk_email( + email_result = send_bulk_email( # subject=subject, message=message, recipient_list=email_addresses, request=request, attachments=None, async_task_=True, # Changed to False to avoid pickle issues - from_interview=False + from_interview=False, + job=job + ) if email_result["success"]: diff --git a/static/image/kaauh.jpeg b/static/image/kaauh.jpeg deleted file mode 100644 index f569ae6..0000000 Binary files a/static/image/kaauh.jpeg and /dev/null differ diff --git a/static/image/kaauh.png b/static/image/kaauh.png deleted file mode 100644 index bd28a2f..0000000 Binary files a/static/image/kaauh.png and /dev/null differ diff --git a/static/image/kaauh_banner.png b/static/image/kaauh_banner.png deleted file mode 100644 index 4065c35..0000000 Binary files a/static/image/kaauh_banner.png and /dev/null differ diff --git a/static/image/kaauh_green.png b/static/image/kaauh_green.png deleted file mode 100644 index 561e750..0000000 Binary files a/static/image/kaauh_green.png and /dev/null differ diff --git a/static/image/kaauh_green1.png b/static/image/kaauh_green1.png deleted file mode 100644 index 10dbccb..0000000 Binary files a/static/image/kaauh_green1.png and /dev/null differ diff --git a/static/image/vision2.svg b/static/image/vision2.svg deleted file mode 100644 index 43156d8..0000000 --- a/static/image/vision2.svg +++ /dev/null @@ -1,8 +0,0 @@ - diff --git a/templates/people/person_list.html b/templates/people/person_list.html index 2035003..a76aded 100644 --- a/templates/people/person_list.html +++ b/templates/people/person_list.html @@ -155,7 +155,7 @@ {% trans "Applicants List" %} - {% trans "Add New" %} + {% trans "Add New Applicant" %} @@ -163,18 +163,14 @@