From da05441f94c4654d37be59e52fd0cd5139ca0698 Mon Sep 17 00:00:00 2001 From: Faheed Date: Thu, 6 Nov 2025 17:40:26 +0300 Subject: [PATCH] transfered the relationships of participants to candidate instead of job --- .../__pycache__/settings.cpython-312.pyc | Bin 8450 -> 8593 bytes NorahUniversity/settings.py | 8 + recruitment/__pycache__/admin.cpython-312.pyc | Bin 12213 -> 12213 bytes recruitment/__pycache__/forms.cpython-312.pyc | Bin 61800 -> 63914 bytes .../__pycache__/models.cpython-312.pyc | Bin 84841 -> 84841 bytes .../__pycache__/signals.cpython-312.pyc | Bin 6701 -> 6701 bytes recruitment/__pycache__/urls.cpython-312.pyc | Bin 17904 -> 17904 bytes recruitment/__pycache__/views.cpython-312.pyc | Bin 140696 -> 140579 bytes recruitment/email_service.py | 255 +++++---- recruitment/forms.py | 134 +++-- recruitment/migrations/0001_initial.py | 2 +- recruitment/models.py | 2 + recruitment/views.py | 83 ++- templates/applicant/applicant_profile.html | 538 +++++++++++++----- templates/applicant/application_detail.html | 90 ++- templates/applicant/career.html | 156 +++++ .../partials/candidate_facing_base.html | 290 ++++------ templates/base.html | 15 +- templates/includes/email_compose_form.html | 15 +- .../recruitment/candidate_exam_view.html | 33 ++ .../recruitment/candidate_hired_view.html | 131 ++++- .../recruitment/candidate_interview_view.html | 4 +- .../recruitment/candidate_offer_view.html | 36 ++ .../recruitment/candidate_screening_view.html | 43 +- 24 files changed, 1284 insertions(+), 551 deletions(-) create mode 100644 templates/applicant/career.html diff --git a/NorahUniversity/__pycache__/settings.cpython-312.pyc b/NorahUniversity/__pycache__/settings.cpython-312.pyc index d539c8664e3858a8949fadf53a5c040affe3e97a..b1e307ca3c51ab2119b2006d6be017fa63c65c00 100644 GIT binary patch delta 198 zcmZp2n&`}XnwOW00SJzkb7#(&$ScWsZKL{nW}OtqlnFV$xqeapj0~y%Sqnf)P(jMX zOvb3dN+!)on{P2|2#Yb^;!MuZD=taQD=EIkmXes4o?4_Cw3%7lgO%~t`d5Zo66#)P>v^TN< delta 73 zcmbQ}+~mZ2nwOW00SLmXIWjdT@=7wU+o-;tnWd6RbHZjO77bxW-^~u<9;}m(%J^~p Y2C8EO;^Oelva;bU92XfBi+%$60Hcu-00000 diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py index d741e46..a402e87 100644 --- a/NorahUniversity/settings.py +++ b/NorahUniversity/settings.py @@ -401,3 +401,11 @@ CKEDITOR_5_CONFIGS = { # Define a constant in settings.py to specify file upload permissions CKEDITOR_5_FILE_UPLOAD_PERMISSION = "staff" # Possible values: "staff", "authenticated", "any" + + + + +from django.contrib.messages import constants as messages +MESSAGE_TAGS = { + messages.ERROR: 'danger', +} diff --git a/recruitment/__pycache__/admin.cpython-312.pyc b/recruitment/__pycache__/admin.cpython-312.pyc index 86a871d863cc43ee5e660b7aa6ef660f064e8499..1ada161f6f8048855a269fddc703a190ee1be617 100644 GIT binary patch delta 20 acmdlQzcrrwG%qg~0}vQ`ac|^ar4Il;Yz005 delta 20 acmdlQzcrrwG%qg~0}zOKFm2>sr4Il-XayDk diff --git a/recruitment/__pycache__/forms.cpython-312.pyc b/recruitment/__pycache__/forms.cpython-312.pyc index 20e290369c535d0d212ac8acc2c5b3fc947a866f..67ec042f71e5e635f93295832c1d1dc71f937491 100644 GIT binary patch delta 3814 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 delta 1841 zcma)5YfMvT82-M~`~7y>(iZHY1!-xxYO8XQZ9)xRnCN~?7A2Ka7)Ak4>p(0+H?y$B zWsq;15Ov@$L0N=!8r_y`#!Z(jSAAp`&dgk`jP zaYsvsHK^xKsas3yXeq5fBVG|{SdK)`?o^i3bUP@MI?7$k57(mA-$N(+1;Ww$(%|8sl7vWDoutSgVnX_*kyL!4BI z@hLu;OQjZ4^^2+a4HPi~+s(mg%9ui^UQEkl%4NGs!4mr9u1Ta=)K{#~SNvanlAcdp z(vN}tZo~~a6snktF~?N%3O-snr?3=u1RTLtz`06VUs-uoRj6ZfugS$~ zCPXhL>!AkK-m{^Od%MLNe2VVg+W>DMwlD?d+}DPtq#(`7%58W~b@C1$=kW24JkF7K z$5GI=HK|VH)ft>RBWZFDsLpH8X<26;YsepYhxb=-{t9+&@Q1peef*XXwP= zD*+&4$Gc^uOsk7nljW%v~z$>Wx@zJ>Mk1t?+ z=*b4CX=nv)qTQesxUe{x-n}(R zx_L62BeMsKd0z?VD;Xn8aT6LHydje_WDYcp8FG_$cYj`A-k>{S&mBC(*$cX50%>R3 zq{G=?(pNIyuY;|u!n>mBTzjc#sA!A~Oxb`vV}`=;s^M*8x`Ii)nY9$$))yw7 zp8n3h&cR5+xn|^b&Kc-d&pW5QJXOF^1w7^FDF4+|mhva4V0UG*bIJOKOJzf4V`TA^ z1}n^3fS&wpQ?D*TdJ;M>EBC&b^?{TO-kQ%@^QQqcq)m{iQzAV7J*x#Nsl3(0Sv@N* zNRX1wTXQ&T&fLZLLdMHT0C&gr)x|+Ak$*!`9P|)BmKJX&h?`ndFjI0f0}ALd1l<_l}PNJ{q=AA~~t-Ope-T;LT2;2Yw diff --git a/recruitment/__pycache__/signals.cpython-312.pyc b/recruitment/__pycache__/signals.cpython-312.pyc index 05801f66ece865ead60e49c7ebb7a9410b4c079d..cd9491638ec98b6e8248d6baef84f1903d7b5a2c 100644 GIT binary patch delta 20 acmZ2$vetzAG%qg~0}!Ztac|^Sk^%rV#RM?` delta 20 acmZ2$vetzAG%qg~0}yO$<=DurBn1FF0R;E} diff --git a/recruitment/__pycache__/urls.cpython-312.pyc b/recruitment/__pycache__/urls.cpython-312.pyc index 7f41aa63dd7b383180c31e97baa4b8f31617c0f0..e4baf7c9886d8cd9704d50ab1c135c558bbdff58 100644 GIT binary patch delta 22 ccmey+&G@04k^3|+FBbz4_;_(|8(dH?_b delta 22 ccmey+&G@04k^3|+FBbz4%>U21k^8AD09m*PfB*mh diff --git a/recruitment/__pycache__/views.cpython-312.pyc b/recruitment/__pycache__/views.cpython-312.pyc index 80c6e30bed2cac2c2d7fe2d1bbf8571ec489ad28..336d5a183920160f4b8ceb15ccdec773aef8463e 100644 GIT binary patch delta 1670 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 diff --git a/recruitment/email_service.py b/recruitment/email_service.py index a048cf9..8395d13 100644 --- a/recruitment/email_service.py +++ b/recruitment/email_service.py @@ -224,138 +224,165 @@ def send_interview_invitation_email(candidate, job, meeting_details=None, recipi logger.error(error_msg, exc_info=True) return {'success': False, 'error': error_msg} - +from .models import Candidate def send_bulk_email(subject, message, recipient_list, request=None, attachments=None, async_task_=False): """ - Send bulk email to multiple recipients with HTML support and attachments. - - Args: - subject: Email subject - message: Email message (can be HTML) - recipient_list: List of email addresses - request: Django request object (optional) - attachments: List of file attachments (optional) - async_task: Whether to run as background task (default: False) - - Returns: - dict: Result with success status and error message if failed + Send bulk email to multiple recipients with HTML support and attachments, + supporting synchronous or asynchronous dispatch. """ - # Handle async task execution + + + # Define messages (Placeholders) + + + all_candidate_emails = [] + candidate_through_agency_emails=[] + participant_emails = [] + agency_emails = [] + left_candidate_emails = [] + + if not recipient_list: + return {'success': False, 'error': 'No recipients provided'} + + + for email in recipient_list: + email = email.strip().lower() # Clean input email + if email: + + candidate = Candidate.objects.filter(email=email).first() + if candidate: + all_candidate_emails.append(email) + else: + participant_emails.append(email) + + + for email in all_candidate_emails: + + candidate = Candidate.objects.filter(email=email).first() + + if candidate: + + if candidate.hiring_source == 'Agency' and hasattr(candidate, 'hiring_agency') and candidate.hiring_agency: + agency = candidate.hiring_agency + candidate_through_agency_emails.append(email) + if agency and agency.email: + agency_emails.append(agency.email) + else: + left_candidate_emails.append(email) + else: + left_candidate_emails.append(email) + + # Determine unique recipients + unique_left_candidates = list(set(left_candidate_emails)) # Convert to list for async task + unique_agencies = list(agency_emails) + unique_participants = list(set(participant_emails)) + + total_recipients = len(unique_left_candidates) + len(unique_agencies) + len(unique_participants) + if total_recipients == 0: + return {'success': False, 'error': 'No valid email addresses found after categorization'} + + + # --- 3. Handle ASYNC Dispatch --- if async_task_: - print("hereeeeeee") - from django_q.tasks import async_task + try: + from django_q.tasks import async_task + + # Simple, serializable attachment format assumed: list of (filename, content, content_type) tuples + processed_attachments = attachments if attachments else [] + + task_ids = [] - # Process attachments for background task serialization - # processed_attachments = [] - # if attachments: - # for attachment in attachments: - # if hasattr(attachment, 'read'): - # # File-like object - save to temporary file - # filename = getattr(attachment, 'name', 'attachment') - # content_type = getattr(attachment, 'content_type', 'application/octet-stream') + # Queue Left Candidates + if unique_left_candidates: + task_id = async_task( + 'recruitment.tasks.send_bulk_email_task', + subject, + message, + recipient_list, + processed_attachments, # Pass serializable data + hook='recruitment.tasks.email_success_hook' # Example hook + ) + task_ids.append(task_id) + + logger.info(f" email queued. ID: {task_id}") - # # Create temporary file - # with tempfile.NamedTemporaryFile(delete=False, suffix=f'_{filename}') as temp_file: - # content = attachment.read() - # temp_file.write(content) - # temp_file_path = temp_file.name + return { + 'success': True, + 'async': True, + 'task_ids': task_ids, + 'message': f'Emails queued for background sending to {total_recipients} recipient(s)' + } + except ImportError: + logger.error("Async execution requested, but django_q or required modules not found. Defaulting to sync.") + async_task_ = False # Fallback to sync + 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)}"} - # # Store file info for background task - # processed_attachments.append({ - # 'file_path': temp_file_path, - # 'filename': filename, - # 'content_type': content_type - # }) - # elif isinstance(attachment, tuple) and len(attachment) == 3: - # # (filename, content, content_type) tuple - can be serialized directly - # processed_attachments.append(attachment) - - # Queue the email sending as a background task - task_id = async_task( - 'recruitment.tasks.send_bulk_email_task', - subject, - message, - recipient_list, - request, - ) - logger.info(f"Bulk email queued as background task with ID: {task_id}") - return { - 'success': True, - 'async': True, - 'task_id': task_id, - 'message': f'Email queued for background sending to {len(recipient_list)} recipient(s)' - } - - # Synchronous execution (default behavior) + # --- 4. Handle SYNCHRONOUS Send (If async_task_=False or fallback) --- try: - if not recipient_list: - return {'success': False, 'error': 'No recipients provided'} - - # Clean recipient list and remove duplicates - clean_recipients = [] - seen_emails = set() - - for recipient in recipient_list: - email = recipient.strip().lower() - if email and email not in seen_emails: - clean_recipients.append(email) - seen_emails.add(email) - - if not clean_recipients: - return {'success': False, 'error': 'No valid email addresses found'} - - # Prepare email content from_email = getattr(settings, 'DEFAULT_FROM_EMAIL', 'noreply@kaauh.edu.sa') - - # Check if message contains HTML tags is_html = '<' in message and '>' in message + successful_sends = 0 - if is_html: - # Create HTML email with plain text fallback - plain_message = strip_tags(message) + # Helper Function for Sync Send + def send_individual_email(recipient, body_message): + 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]) + + # Attachment Logic + 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: + # FIX: Added the critical .send() call + email_obj.send(fail_silently=False) + successful_sends += 1 + except Exception as e: + logger.error(f"Failed to send email to {recipient}: {str(e)}", exc_info=True) - # Create email with both HTML and plain text versions - email = EmailMultiAlternatives( - subject=subject, - body=plain_message, - from_email=from_email, - to=clean_recipients, - ) - email.attach_alternative(message, "text/html") - else: - # Plain text email - email = EmailMultiAlternatives( - subject=subject, - body=message, - from_email=from_email, - to=clean_recipients, - ) - # Add attachments if provided - # if attachments: - # for attachment in attachments: - # if hasattr(attachment, 'read'): - # # File-like object - # filename = getattr(attachment, 'name', 'attachment') - # content = attachment.read() - # content_type = getattr(attachment, 'content_type', 'application/octet-stream') - # email.attach(filename, content, content_type) - # elif isinstance(attachment, tuple) and len(attachment) == 3: - # # (filename, content, content_type) tuple - # filename, content, content_type = attachment - # email.attach(filename, content, content_type) + # Send Emails + for email in unique_left_candidates: + candidate_name=Candidate.objects.filter(email=email).first().first_name + candidate_message = f"Hi, {candidate_name}"+"\n"+message + + send_individual_email(email, candidate_message) + + i=0 + for email in unique_agencies: + candidate_name=Candidate.objects.filter(email=candidate_through_agency_emails[i]).first().first_name + agency_message = f"Hi, {candidate_name}"+"\n"+message + send_individual_email(email, agency_message) - # Send email - email.send(fail_silently=False) + for email in unique_participants: + + participant_message = "Hello Participant! This is a general notification for you." + send_individual_email(email, participant_message) - logger.info(f"Bulk email sent successfully to {len(clean_recipients)} recipients") + + logger.info(f"Bulk email processing complete. Sent successfully to {successful_sends} out of {total_recipients} unique recipients.") return { 'success': True, - 'recipients_count': len(clean_recipients), - 'message': f'Email sent successfully to {len(clean_recipients)} recipient(s)' + '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 send bulk email: {str(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} + return {'success': False, 'error': error_msg} \ No newline at end of file diff --git a/recruitment/forms.py b/recruitment/forms.py index 4142ebe..b553d1e 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -1270,7 +1270,7 @@ class CandidateEmailForm(forms.Form): 'class': 'form-check' }), label=_('Select Candidates'), # Use a descriptive label - required=True + required=False ) # to = forms.MultipleChoiceField( @@ -1308,7 +1308,7 @@ class CandidateEmailForm(forms.Form): 'class': 'form-check' }), label=_('Recipients'), - required=True + required=False ) # include_candidate_info = forms.BooleanField( @@ -1334,15 +1334,18 @@ class CandidateEmailForm(forms.Form): super().__init__(*args, **kwargs) self.job = job self.candidates=candidates + stage=self.candidates.first().stage # Get all participants and users for this job recipient_choices = [] # Add job participants - for participant in job.participants.all(): - recipient_choices.append( - (f'participant_{participant.id}', f'{participant.name} - {participant.designation} (Participant)') - ) + #show particpants only in the interview stage + if stage=='Interview': + for participant in job.participants.all(): + recipient_choices.append( + (f'participant_{participant.id}', f'{participant.name} - {participant.designation} (Participant)') + ) # Add job users for user in job.users.all(): @@ -1373,7 +1376,60 @@ class CandidateEmailForm(forms.Form): def _get_initial_message(self): """Generate initial message with candidate and meeting information""" - message_parts = ['hiiiiiiii'] + candidate=self.candidates.first() + message_parts=[] + if candidate.stage == 'Applied': + message_parts = [ + f"Than you, for your interest in the {self.job.title} role.", + f"We regret to inform you that you were not selected to move forward to the exam round at this time.", + f"We encourage you to check our career page for further updates and future opportunities:", + f"https://kaauh/careers", + f"Wishing you the best in your job search,", + f"The KAAUH Hiring team" + ] + elif candidate.stage == 'Exam': + message_parts = [ + f"Than you,for your interest in the {self.job.title} role.", + f"We're pleased to inform you that your initial screening was successful!", + f"The next step is the mandatory online assessment exam.", + f"Please complete the assessment by using the following link:", + f"https://kaauh/hire/exam", + f"We look forward to reviewing your results.", + f"Best regards, The KAAUH Hiring team" + ] + + elif candidate.stage == 'Exam': + message_parts = [ + f"Than you, for your interest in the {self.job.title} role.", + f"We're pleased to inform you that your initial screening was successful!", + f"The next step is the mandatory online assessment exam.", + f"Please complete the assessment by using the following link:", + f"https://kaauh/hire/exam", + f"We look forward to reviewing your results.", + f"Best regards, The KAAUH Hiring team" + ] + + elif candidate.stage == 'Offer': + message_parts = [ + f"Congratulations, ! We are delighted to inform you that we are extending a formal offer of employment for the {self.job.title} role.", + f"This is an exciting moment, and we look forward to having you join the KAAUH team.", + f"A detailed offer letter and compensation package will be sent to you via email within 24 hours.", + f"In the meantime, please contact our HR department at [HR Contact] if you have immediate questions.", + f"Welcome to the team!", + f"Best regards, The KAAUH Hiring team" + ] + elif candidate.stage == 'Hired': + message_parts = [ + f"Welcome aboard,!", + f"We are thrilled to officially confirm your employment as our new {self.job.title}.", + f"You will receive a separate email shortly with details regarding your start date, first-day instructions, and onboarding documents.", + f"We look forward to seeing you at KAAUH.", + f"If you have any questions before your start date, please contact [Onboarding Contact].", + f"Best regards, The KAAUH Hiring team" + ] + + + # # Add candidate information # if self.candidate: @@ -1394,12 +1450,12 @@ class CandidateEmailForm(forms.Form): return '\n'.join(message_parts) - def clean_recipients(self): - """Ensure at least one recipient is selected""" - recipients = self.cleaned_data.get('recipients') - if not recipients: - raise forms.ValidationError(_('Please select at least one recipient.')) - return recipients + # def clean_recipients(self): + # """Ensure at least one recipient is selected""" + # recipients = self.cleaned_data.get('recipients') + # if not recipients: + # raise forms.ValidationError(_('Please select at least one recipient.')) + # return recipients # def clean_to(self): # """Ensure at least one recipient is selected""" @@ -1414,32 +1470,32 @@ class CandidateEmailForm(forms.Form): email_addresses = [] recipients = self.cleaned_data.get('recipients', []) candidates=self.cleaned_data.get('to',[]) - - for recipient in recipients: - if recipient.startswith('participant_'): - participant_id = recipient.split('_')[1] - try: - participant = Participants.objects.get(id=participant_id) - email_addresses.append(participant.email) - except Participants.DoesNotExist: - continue - elif recipient.startswith('user_'): - user_id = recipient.split('_')[1] - try: - user = User.objects.get(id=user_id) - email_addresses.append(user.email) - except User.DoesNotExist: - continue - - for candidate in candidates: - if candidate.startswith('candidate_'): - print("candidadte: {candidate}") - candidate_id = candidate.split('_')[1] - try: - candidate = Candidate.objects.get(id=candidate_id) - email_addresses.append(candidate.email) - except Candidate.DoesNotExist: - continue + if recipients: + for recipient in recipients: + if recipient.startswith('participant_'): + participant_id = recipient.split('_')[1] + try: + participant = Participants.objects.get(id=participant_id) + email_addresses.append(participant.email) + except Participants.DoesNotExist: + continue + elif recipient.startswith('user_'): + user_id = recipient.split('_')[1] + try: + user = User.objects.get(id=user_id) + email_addresses.append(user.email) + except User.DoesNotExist: + continue + if candidates: + for candidate in candidates: + if candidate.startswith('candidate_'): + print("candidadte: {candidate}") + candidate_id = candidate.split('_')[1] + try: + candidate = Candidate.objects.get(id=candidate_id) + email_addresses.append(candidate.email) + except Candidate.DoesNotExist: + continue return list(set(email_addresses)) # Remove duplicates diff --git a/recruitment/migrations/0001_initial.py b/recruitment/migrations/0001_initial.py index 306ceb7..6e83eef 100644 --- a/recruitment/migrations/0001_initial.py +++ b/recruitment/migrations/0001_initial.py @@ -1,4 +1,4 @@ -# Generated by Django 5.2.6 on 2025-10-30 10:22 +# Generated by Django 5.2.7 on 2025-11-05 13:05 import django.core.validators import django.db.models.deletion diff --git a/recruitment/models.py b/recruitment/models.py index ad673b9..cc73897 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -1763,3 +1763,5 @@ class Participants(Base): def __str__(self): return f"{self.name} - {self.email}" + + diff --git a/recruitment/views.py b/recruitment/views.py index 44c510c..7473177 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -560,7 +560,7 @@ def kaauh_career(request): form_template__is_active=True ) - return render(request,'jobs/career.html',{'active_jobs':active_jobs}) + return render(request,'applicant/career.html',{'active_jobs':active_jobs}) # job detail facing the candidate: def application_detail(request, slug): @@ -3708,30 +3708,29 @@ def compose_candidate_email(request, job_slug): candidate_ids=request.GET.getlist('candidate_ids') candidates=Candidate.objects.filter(id__in=candidate_ids) - print(candidates) - # candidate = get_object_or_404(Candidate, slug=candidate_slug, job=job) if request.method == 'POST': candidate_ids = request.POST.getlist('candidate_ids') - print(f"inside the POST {candidate_ids}" ) candidates=Candidate.objects.filter(id__in=candidate_ids) form = CandidateEmailForm(job, candidates, request.POST) - - - print(form) - if form.is_valid(): print("form is valid ...") # Get email addresses email_addresses = form.get_email_addresses() - print(f"hiii {email_addresses} ") + + if not email_addresses: - messages.error(request, 'No valid email addresses found for selected recipients.') - return render(request, 'includes/email_compose_form.html', { - 'form': form, - 'job': job, - 'candidate': candidates - }) + messages.error(request, 'No email selected') + referer = request.META.get('HTTP_REFERER') + + if referer: + # Redirect back to the referring page + return redirect(referer) + else: + + return redirect('dashboard') + + # Check if this is an interview invitation subject = form.cleaned_data.get('subject', '').lower() @@ -3739,7 +3738,7 @@ def compose_candidate_email(request, job_slug): if is_interview_invitation: # Use HTML template for interview invitations - meeting_details = None + # meeting_details = None # if form.cleaned_data.get('include_meeting_details'): # # Try to get meeting details from candidate # meeting_details = { @@ -3751,9 +3750,9 @@ def compose_candidate_email(request, job_slug): from .email_service import send_interview_invitation_email email_result = send_interview_invitation_email( - candidate=candidates, + candidates=candidates, job=job, - meeting_details=meeting_details, + # meeting_details=meeting_details, recipient_list=email_addresses ) else: @@ -3762,41 +3761,41 @@ def compose_candidate_email(request, job_slug): subject = form.cleaned_data.get('subject') # Send emails using email service (no attachments, synchronous to avoid pickle issues) - + email_result = send_bulk_email( subject=subject, message=message, recipient_list=email_addresses, request=request, - async_task_=False # Changed to False to avoid pickle issues + async_task_=True # Changed to False to avoid pickle issues ) - if email_result['success']: - messages.success(request, f'Email sent successfully to {len(email_addresses)} recipient(s).') + if email_result['success']: + messages.success(request, f'Email sent successfully to {len(email_addresses)} recipient(s).') - # # For HTMX requests, return success response - # if 'HX-Request' in request.headers: - # return JsonResponse({ - # 'success': True, - # 'message': f'Email sent successfully to {len(email_addresses)} recipient(s).' - # }) + # # For HTMX requests, return success response + # if 'HX-Request' in request.headers: + # return JsonResponse({ + # 'success': True, + # 'message': f'Email sent successfully to {len(email_addresses)} recipient(s).' + # }) - return redirect('candidate_interview_view', slug=job.slug) - else: - messages.error(request, f'Failed to send email: {email_result.get("message", "Unknown error")}') + return redirect('candidate_interview_view', slug=job.slug) + else: + messages.error(request, f'Failed to send email: {email_result.get("message", "Unknown error")}') - # For HTMX requests, return error response - # if 'HX-Request' in request.headers: - # return JsonResponse({ - # 'success': False, - # 'error': email_result.get("message", "Failed to send email") - # }) + # For HTMX requests, return error response + # if 'HX-Request' in request.headers: + # return JsonResponse({ + # 'success': False, + # 'error': email_result.get("message", "Failed to send email") + # }) - return render(request, 'includes/email_compose_form.html', { - 'form': form, - 'job': job, - 'candidate': candidates - }) + return render(request, 'includes/email_compose_form.html', { + 'form': form, + 'job': job, + 'candidate': candidates + }) # except Exception as e: # logger.error(f"Error sending candidate email: {e}") diff --git a/templates/applicant/applicant_profile.html b/templates/applicant/applicant_profile.html index 87f05c2..9acbcc4 100644 --- a/templates/applicant/applicant_profile.html +++ b/templates/applicant/applicant_profile.html @@ -2,161 +2,421 @@ {% load static i18n %} -{% block title %}{% trans "My Profile" %} - {{ block.super }}{% endblock %} +{% block title %}{% trans "My Dashboard" %} - {{ block.super }}{% endblock %} {% block customCSS %} + {% endblock %} {% block content %} -
+
- {# Profile Header #} -
-

- {% trans "My Candidate Profile" %} + {# Header: Larger, more dynamic on large screens. Stacks cleanly on mobile. #} +
+

+ {% trans "Your Candidate Dashboard" %}

- - {% trans "Edit Profile" %} + + {% trans "Update Profile" %}
- {# Profile and Account Management Row #} -
- - {# Candidate Details Card #} -
-
-
-
- {% trans 'Profile Picture' %} -
-

{{ candidate.name|default:"N/A" }}

-

{{ candidate.email }}

-
-
- -
    -
  • - - {% trans "Phone" %} {{ candidate.phone|default:"N/A" }} -
  • -
  • - - {% trans "Nationality" %} {{ candidate.get_nationality_display|default:"N/A" }} -
  • -
  • - - {% trans "Date of Birth" %} {{ candidate.date_of_birth|date:"M d, Y"|default:"N/A" }} -
  • -
  • - - {% trans "Resume" %} - {% if candidate.resume %} - - {% trans "View/Download" %} - - {% else %} - {% trans "Not uploaded" %} - {% endif %} -
  • -
-
-
-
- - {# Account Management / Quick Actions Card #} -
-
-
-

{% trans "Account Settings" %}

-
-
-
- - -
-
-

{% trans "Your profile is essential for the application process. Keep your resume and contact information up-to-date for timely communication." %}

-
-
-
-
+ {# Candidate Quick Overview Card: Use a softer background color #} +
+
+ {% trans 'Profile Picture' %} +
+

{{ candidate.name|default:"Candidate Name" }}

+

{{ candidate.email }}

- {# Application Tracking Section #} -

- {% trans "My Applications History" %} -

- - {% if applications %} -
- - - - - - - - - - - - {% for application in applications %} - - - - - - - - {% endfor %} - -
{% trans "Job Title" %}{% trans "Applied On" %}{% trans "Current Stage" %}{% trans "Status" %}{% trans "Action" %}
- - {{ application.job.title }} - - {{ application.applied_date|date:"d M Y" }} - - {{ application.stage }} - - - {% if application.is_active %} - {% trans "Active" %} - {% else %} - {% trans "Closed" %} - {% endif %} - - - {% trans "Details" %} - -
-
+ {# ================================================= #} + {# MAIN TABBED INTERFACE #} + {# ================================================= #} +
- {# Placeholder for Pagination #} - {% comment %} {% include "includes/paginator.html" with page_obj=applications_page %} {% endcomment %} - - {% else %} -
- -
{% trans "You have no active applications." %}
- - {% trans "View Available Jobs" %} - + {# Tab Navigation: Used nav-scroll for responsiveness #} + - {% endif %} + + {# Tab Content #} +
+ +
+

{% trans "Personal Information" %}

+
    +
  • +
    {% trans "Phone" %}
    + {{ candidate.phone|default:"N/A" }} +
  • +
  • +
    {% trans "Nationality" %}
    + {{ candidate.get_nationality_display|default:"N/A" }} +
  • +
  • +
    {% trans "Date of Birth" %}
    + {{ candidate.date_of_birth|date:"M d, Y"|default:"N/A" }} +
  • +
  • {% trans "Use the 'Update Profile' button above to edit these details." %}
  • +
+ +
+ +

{% trans "Quick Actions" %}

+ +
+ +
+

{% trans "Application Tracking" %}

+ + {% if applications %} +
+ + + + + + + + + + + + {% for application in applications %} + + + + + + + + {% endfor %} + +
{% trans "Job Title" %}{% trans "Applied On" %}{% trans "Current Stage" %}{% trans "Status" %}{% trans "Action" %}
+ + {{ application.job.title }} + + {{ application.applied_date|date:"d M Y" }} + + {{ application.stage }} + + + {% if application.is_active %} + {% trans "Active" %} + {% else %} + {% trans "Closed" %} + {% endif %} + + + {% trans "Details" %} + +
+
+ + {% else %} +
+ +
{% trans "You haven't submitted any applications yet." %}
+ + {% trans "View Available Jobs" %} + +
+ {% endif %} +
+ +
+

{% trans "My Uploaded Documents" %}

+ +

{% trans "You can upload and manage your resume, certificates, and professional documents here. These documents will be attached to your applications." %}

+ + + {% trans "Upload New Document" %} + + +
+ + {# Example Document List (Refined structure) #} +
    +
  • +
    + **{% trans "Resume" %}** (CV\_John\_Doe\_2024.pdf) +
    +
    + {% trans "Uploaded: 10 Jan 2024" %} + + +
    +
  • +
  • +
    + **{% trans "Medical Certificate" %}** (Cert\_KSA\_MED.jpg) +
    +
    + {% trans "Uploaded: 22 Feb 2023" %} + + +
    +
  • +
+ +
+ +
+

{% trans "Security & Preferences" %}

+ +
+
+
+
{% trans "Password Security" %}
+

{% trans "Update your password regularly to keep your account secure." %}

+ + {% trans "Change Password" %} + +
+
+
+
+
{% trans "Email Preferences" %}
+

{% trans "Manage subscriptions and job alert settings." %}

+ + {% trans "Manage Alerts" %} + +
+
+
+ +
+ {% trans "To delete your profile, please contact HR support." %} +
+
+ +
+
+ {# ================================================= #} +
{% endblock %} \ No newline at end of file diff --git a/templates/applicant/application_detail.html b/templates/applicant/application_detail.html index a574903..64465e6 100644 --- a/templates/applicant/application_detail.html +++ b/templates/applicant/application_detail.html @@ -1,5 +1,6 @@ {% extends 'applicant/partials/candidate_facing_base.html'%} {% load static i18n %} + {% block content %}