From 2211c5f3b276800cd6071d6182de68a37b510b5a Mon Sep 17 00:00:00 2001 From: Faheed Date: Sun, 30 Nov 2025 12:20:33 +0300 Subject: [PATCH] ui and error fixes --- recruitment/forms.py | 3 + recruitment/models.py | 4 + recruitment/tasks.py | 6 +- recruitment/views.py | 705 ++++++++++-------- recruitment/views_frontend.py | 10 +- templates/includes/email_compose_form.html | 8 +- .../interviews/interview_create_onsite.html | 31 +- .../interviews/interview_create_remote.html | 18 +- .../interview_create_type_selection.html | 25 +- ...l.html => application_message_detail.html} | 0 ...orm.html => application_message_form.html} | 4 +- ...ist.html => application_message_list.html} | 0 templates/messages/message_detail.html | 15 +- templates/messages/message_list.html | 2 +- templates/people/person_list.html | 4 +- .../recruitment/agency_assignment_detail.html | 10 +- templates/recruitment/agency_detail.html | 36 +- .../agency_portal_assignment_detail.html | 72 +- ... => agency_portal_submit_application.html} | 0 templates/recruitment/applicant_profile.html | 14 +- 20 files changed, 553 insertions(+), 414 deletions(-) rename templates/messages/{candidate_message_detail.html => application_message_detail.html} (100%) rename templates/messages/{candidate_message_form.html => application_message_form.html} (99%) rename templates/messages/{candidate_message_list.html => application_message_list.html} (100%) rename templates/recruitment/{agency_portal_submit_candidate.html => agency_portal_submit_application.html} (100%) diff --git a/recruitment/forms.py b/recruitment/forms.py index 49fa7d8..75561f4 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -2157,6 +2157,8 @@ class MessageForm(forms.ModelForm): hiring_agency__user=self.user, status="ACTIVE" ).order_by("-created_at") + print(self.user) + print("Agency user job queryset:", self.fields["job"].queryset) elif self.user.user_type == "candidate": # Candidates can only see jobs they applied for self.fields["job"].queryset = JobPosting.objects.filter( @@ -2179,6 +2181,7 @@ class MessageForm(forms.ModelForm): self.fields["recipient"].queryset = User.objects.filter( user_type="staff" ).distinct().order_by("username") + elif self.user.user_type == "candidate": # Candidates can only message staff self.fields["recipient"].queryset = User.objects.filter( diff --git a/recruitment/models.py b/recruitment/models.py index ea8ed31..d9e2346 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -468,6 +468,10 @@ class JobPosting(Base): vacancy_fill_rate = 0.0 return vacancy_fill_rate + + def has_already_applied_to_this_job(self, person): + """Check if a given person has already applied to this job.""" + return self.applications.filter(person=person).exists() class JobPostingImage(models.Model): diff --git a/recruitment/tasks.py b/recruitment/tasks.py index 4716133..6df188a 100644 --- a/recruitment/tasks.py +++ b/recruitment/tasks.py @@ -1069,9 +1069,11 @@ def send_bulk_email_task(subject, message, recipient_list,attachments=None,sende # 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,sender,job): + r=_task_send_individual_email(subject, message, recipient, attachments,sender,job) + print(f"Email send result for {recipient}: {r}") + if r: successful_sends += 1 - + print(f"successful_sends: {successful_sends} out of {total_recipients}") if successful_sends > 0: logger.info(f"Bulk email task completed successfully. Sent to {successful_sends}/{total_recipients} recipients.") return { diff --git a/recruitment/views.py b/recruitment/views.py index 8b381c4..f366432 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -1196,12 +1196,26 @@ def application_submit_form(request, template_slug): """Display the form as a step-by-step wizard""" if not request.user.is_authenticated: return redirect("application_signup",slug=template_slug) + job = get_object_or_404(JobPosting, form_template__slug=template_slug) + if request.user.user_type == "candidate": + person=request.user.person_profile + if job.has_already_applied_to_this_job(person): + messages.error( + request, + _( + "You have already applied to this job: Multiple applications are not allowed." + ), + ) + return redirect("job_application_detail", slug=job.slug) + + template = get_object_or_404(FormTemplate, slug=template_slug, is_active=True) stage = template.stages.filter(name="Contact Information") - job_id = template.job.internal_job_id - job = template.job + # job_id = template.job.internal_job_id + # job = template. + job_id=job.internal_job_id is_limit_exceeded = job.is_application_limit_reached if is_limit_exceeded: messages.error( @@ -1235,7 +1249,7 @@ def application_submit_form(request, template_slug): @require_POST def application_submit(request, template_slug): """Handle form submission""" - if not request.user.is_authenticated :# or request.user.user_type != "candidate": + if not request.user.is_authenticated :# or request.user.user_type != "application": return JsonResponse({"success": False, "message": "Unauthorized access."}) template = get_object_or_404(FormTemplate, slug=template_slug) job = template.job @@ -1437,10 +1451,10 @@ def form_submission_details(request, template_id, slug): # def _handle_get_request(request, slug, job): # """ -# Handles GET requests, setting up forms and restoring candidate selections +# Handles GET requests, setting up forms and restoring application selections # from the session for persistence. # """ -# SESSION_KEY = f"schedule_candidate_ids_{slug}" +# SESSION_KEY = f"schedule_application_ids_{slug}" # form = BulkInterviewTemplateForm(slug=slug) # # break_formset = BreakTimeFormSet(prefix='breaktime') @@ -1449,11 +1463,11 @@ def form_submission_details(request, template_id, slug): # # 1. Capture IDs from HTMX request and store in session (when first clicked) # if "HX-Request" in request.headers: -# candidate_ids = request.GET.getlist("candidate_ids") +# application_ids = request.GET.getlist("application_ids") -# if candidate_ids: -# request.session[SESSION_KEY] = candidate_ids -# selected_ids = candidate_ids +# if application_ids: +# request.session[SESSION_KEY] = application_ids +# selected_ids = application_ids # # 2. Restore IDs from session (on refresh or navigation) # if not selected_ids: @@ -1461,9 +1475,9 @@ def form_submission_details(request, template_id, slug): # # 3. Use the list of IDs to initialize the form # if selected_ids: -# candidates_to_load = Application.objects.filter(pk__in=selected_ids) -# print(candidates_to_load) -# form.initial["applications"] = candidates_to_load +# applications_to_load = Application.objects.filter(pk__in=selected_ids) +# print(applications_to_load) +# form.initial["applications"] = applications_to_load # return render( # request, @@ -1554,7 +1568,7 @@ def form_submission_details(request, template_id, slug): # "buffer_time": buffer_time, # "break_start_time": break_start_time.isoformat() if break_start_time else None, # "break_end_time": break_end_time.isoformat() if break_end_time else None, -# "candidate_ids": [c.id for c in applications], +# "application_ids": [c.id for c in applications], # "schedule_interview_type":schedule_interview_type # } @@ -1596,7 +1610,7 @@ def form_submission_details(request, template_id, slug): # """ # SESSION_DATA_KEY = "interview_schedule_data" -# SESSION_ID_KEY = f"schedule_candidate_ids_{slug}" +# SESSION_ID_KEY = f"schedule_application_ids_{slug}" # # 1. Get schedule data from session # schedule_data = request.session.get(SESSION_DATA_KEY) @@ -1633,22 +1647,22 @@ def form_submission_details(request, template_id, slug): # if SESSION_DATA_KEY in request.session: del request.session[SESSION_DATA_KEY] # return redirect("schedule_interviews", slug=slug) -# # 3. Setup candidates and get slots -# candidates = Application.objects.filter(id__in=schedule_data["candidate_ids"]) -# schedule.applications.set(candidates) +# # 3. Setup applications and get slots +# applications = Application.objects.filter(id__in=schedule_data["application_ids"]) +# schedule.applications.set(applications) # available_slots = get_available_time_slots(schedule) # # 4. Handle Remote/Onsite logic # if schedule_data.get("schedule_interview_type") == 'Remote': # # ... (Remote logic remains unchanged) # queued_count = 0 -# for i, candidate in enumerate(candidates): +# for i, application in enumerate(applications): # if i < len(available_slots): # slot = available_slots[i] # async_task( # "recruitment.tasks.create_interview_and_meeting", -# candidate.pk, job.pk, schedule.pk, slot["date"], slot["time"], schedule.interview_duration, +# application.pk, job.pk, schedule.pk, slot["date"], slot["time"], schedule.interview_duration, # ) # queued_count += 1 @@ -1681,8 +1695,8 @@ def form_submission_details(request, template_id, slug): # try: -# # 1. Iterate over candidates and create a NEW Location object for EACH -# for i, candidate in enumerate(candidates): +# # 1. Iterate over applications and create a NEW Location object for EACH +# for i, application in enumerate(applications): # if i < len(available_slots): # slot = available_slots[i] @@ -1702,7 +1716,7 @@ def form_submission_details(request, template_id, slug): # # 2. Create the ScheduledInterview, linking the unique location # ScheduledInterview.objects.create( -# application=candidate, +# application=application, # job=job, # schedule=schedule, # interview_date=slot['date'], @@ -1712,7 +1726,7 @@ def form_submission_details(request, template_id, slug): # messages.success( # request, -# f"Onsite schedule interviews created successfully for {len(candidates)} candidates." +# f"Onsite schedule interviews created successfully for {len(applications)} applications." # ) # # Clear session data keys upon successful completion @@ -1757,7 +1771,7 @@ def form_submission_details(request, template_id, slug): @staff_user_required def applications_screening_view(request, slug): """ - Manage candidate tiers and stage transitions + Manage application tiers and stage transitions """ job = get_object_or_404(JobPosting, slug=slug) applications = job.screening_applications @@ -1839,7 +1853,7 @@ def applications_screening_view(request, slug): @staff_user_required def applications_exam_view(request, slug): """ - Manage candidate tiers and stage transitions + Manage application tiers and stage transitions """ job = get_object_or_404(JobPosting, slug=slug) context = {"job": job, "applications": job.exam_applications, "current_stage": "Exam"} @@ -1905,7 +1919,7 @@ def application_update_status(request, slug): job = get_object_or_404(JobPosting, slug=slug) mark_as = request.POST.get("mark_as") if mark_as != "----------": - application_ids = request.POST.getlist("candidate_ids") + application_ids = request.POST.getlist("application_ids") if c := Application.objects.filter(pk__in=application_ids): if mark_as == "Exam": @@ -1916,7 +1930,7 @@ def application_update_status(request, slug): offer_date=None, hired_date=None, stage=mark_as, - applicant_status="Candidate" + applicant_status="application" if mark_as in ["Exam", "Interview","Document Review", "Offer"] else "Applicant", ) @@ -1927,7 +1941,7 @@ def application_update_status(request, slug): stage=mark_as, offer_date=None, hired_date=None, - applicant_status="Candidate" + applicant_status="application" if mark_as in ["Exam", "Interview", "Document Review","Offer"] else "Applicant", ) @@ -1937,7 +1951,7 @@ def application_update_status(request, slug): stage=mark_as, offer_date=None, hired_date=None, - applicant_status="Candidate" + applicant_status="application" if mark_as in ["Exam", "Interview", "Document Review","Offer"] else "Applicant", ) @@ -1947,7 +1961,7 @@ def application_update_status(request, slug): stage=mark_as, offer_date=timezone.now(), hired_date=None, - applicant_status="Candidate" + applicant_status="application" if mark_as in ["Exam", "Interview", "Document Review","Offer"] else "Applicant", ) @@ -1956,7 +1970,7 @@ def application_update_status(request, slug): c.update( stage=mark_as, hired_date=timezone.now(), - applicant_status="Candidate" + applicant_status="application" if mark_as in ["Exam", "Interview", "Offer"] else "Applicant", ) @@ -1968,7 +1982,7 @@ def application_update_status(request, slug): interview_date=None, offer_date=None, hired_date=None, - applicant_status="Candidate" + applicant_status="application" if mark_as in ["Exam", "Interview", "Offer"] else "Applicant", ) @@ -1994,11 +2008,11 @@ def applications_interview_view(request, slug): @staff_user_required def applications_document_review_view(request, slug): """ - Document review view for candidates after interview stage and before offer stage + Document review view for applications after interview stage and before offer stage """ job = get_object_or_404(JobPosting, slug=slug) - # Get candidates from Interview stage who need document review + # Get applications from Interview stage who need document review applications = job.document_review_applications.select_related('person') # Get search query for filtering search_query = request.GET.get('q', '') @@ -2019,9 +2033,9 @@ def applications_document_review_view(request, slug): # @staff_user_required -# def reschedule_meeting_for_application(request, slug, candidate_id, meeting_id): +# def reschedule_meeting_for_application(request, slug, application_id, meeting_id): # job = get_object_or_404(JobPosting, slug=slug) -# candidate = get_object_or_404(Application, pk=candidate_id) +# application = get_object_or_404(Application, pk=application_id) # meeting = get_object_or_404(ZoomMeetingDetails, pk=meeting_id) # form = ZoomMeetingForm(instance=meeting) @@ -2039,7 +2053,7 @@ def applications_document_review_view(request, slug): # return redirect( # "reschedule_meeting_for_application", # slug=job.slug, -# candidate_id=candidate_id, +# application_id=candidate_id, # meeting_id=meeting_id, # ) @@ -2140,62 +2154,62 @@ def applications_document_review_view(request, slug): # @staff_user_required # def interview_calendar_view(request, slug): - job = get_object_or_404(JobPosting, slug=slug) + # job = get_object_or_404(JobPosting, slug=slug) - # Get all scheduled interviews for this job - scheduled_interviews = ScheduledInterview.objects.filter(job=job).select_related( - "applicaton", "zoom_meeting" - ) + # # Get all scheduled interviews for this job + # scheduled_interviews = ScheduledInterview.objects.filter(job=job).select_related( + # "applicaton", "zoom_meeting" + # ) - # Convert interviews to calendar events - events = [] - for interview in scheduled_interviews: - # Create start datetime - start_datetime = datetime.combine( - interview.interview_date, interview.interview_time - ) + # # Convert interviews to calendar events + # events = [] + # for interview in scheduled_interviews: + # # Create start datetime + # start_datetime = datetime.combine( + # interview.interview_date, interview.interview_time + # ) - # Calculate end datetime based on interview duration - duration = interview.zoom_meeting.duration if interview.zoom_meeting else 60 - end_datetime = start_datetime + timedelta(minutes=duration) + # # Calculate end datetime based on interview duration + # duration = interview.zoom_meeting.duration if interview.zoom_meeting else 60 + # end_datetime = start_datetime + timedelta(minutes=duration) - # Determine event color based on status - color = "#00636e" # Default color - if interview.status == "confirmed": - color = "#00a86b" # Green for confirmed - elif interview.status == "cancelled": - color = "#e74c3c" # Red for cancelled - elif interview.status == "completed": - color = "#95a5a6" # Gray for completed + # # Determine event color based on status + # color = "#00636e" # Default color + # if interview.status == "confirmed": + # color = "#00a86b" # Green for confirmed + # elif interview.status == "cancelled": + # color = "#e74c3c" # Red for cancelled + # elif interview.status == "completed": + # color = "#95a5a6" # Gray for completed - events.append( - { - "title": f"Interview: {interview.candidate.name}", - "start": start_datetime.isoformat(), - "end": end_datetime.isoformat(), - "url": f"{request.path}interview/{interview.id}/", - "color": color, - "extendedProps": { - "candidate": interview.candidate.name, - "email": interview.candidate.email, - "status": interview.status, - "meeting_id": interview.zoom_meeting.meeting_id - if interview.zoom_meeting - else None, - "join_url": interview.zoom_meeting.join_url - if interview.zoom_meeting - else None, - }, - } - ) + # events.append( + # { + # "title": f"Interview: {interview.candidate.name}", + # "start": start_datetime.isoformat(), + # "end": end_datetime.isoformat(), + # "url": f"{request.path}interview/{interview.id}/", + # "color": color, + # "extendedProps": { + # "candidate": interview.candidate.name, + # "email": interview.candidate.email, + # "status": interview.status, + # "meeting_id": interview.zoom_meeting.meeting_id + # if interview.zoom_meeting + # else None, + # "join_url": interview.zoom_meeting.join_url + # if interview.zoom_meeting + # else None, + # }, + # } + # ) - context = { - "job": job, - "events": events, - "calendar_color": "#00636e", - } + # context = { + # "job": job, + # "events": events, + # "calendar_color": "#00636e", + # } - return render(request, "recruitment/interview_calendar.html", context) + # return render(request, "recruitment/interview_calendar.html", context) # @staff_user_required @@ -3320,27 +3334,27 @@ def agency_detail(request, slug): """View details of a specific hiring agency""" agency = get_object_or_404(HiringAgency, slug=slug) - # Get candidates associated with this agency - candidates = Application.objects.filter(hiring_agency=agency).order_by( + # Get applications associated with this agency + applications = Application.objects.filter(hiring_agency=agency).order_by( "-created_at" ) # Statistics - total_candidates = candidates.count() - active_candidates = candidates.filter( + total_applications = applications.count() + active_applications = applications.filter( stage__in=["Applied", "Screening", "Exam", "Interview", "Offer"] ).count() - hired_candidates = candidates.filter(stage="Hired").count() - rejected_candidates = candidates.filter(stage="Rejected").count() + hired_applications = applications.filter(stage="Hired").count() + rejected_applications = applications.filter(stage="Rejected").count() job_assignments=AgencyJobAssignment.objects.filter(agency=agency) print(job_assignments) context = { "agency": agency, - "candidates": candidates[:10], # Show recent 10 candidates - "total_candidates": total_candidates, - "active_candidates": active_candidates, - "hired_candidates": hired_candidates, - "rejected_candidates": rejected_candidates, + "applications": applications[:10], # Show recent 10 applications + "total_applications": total_applications, + "active_applications": active_applications, + "hired_applications": hired_applications, + "rejected_applications": rejected_applications, "generated_password": agency.generated_password if agency.generated_password else None, @@ -4078,19 +4092,19 @@ def applicant_portal_dashboard(request): if not request.user.is_authenticated: return redirect("account_login") - # Get candidate profile (Person record) + # Get application profile (Person record) try: applicant = request.user.person_profile except: - messages.error(request, "No candidate profile found.") + messages.error(request, "No application profile found.") return redirect("account_login") - # Get candidate's applications with related job data + # Get application's applications with related job data applications = Application.objects.filter( person=applicant ).select_related('job').order_by('-created_at') - # Get candidate's documents using the Person documents property + # Get application's documents using the Person documents property documents = applicant.documents.order_by('-created_at') # Add password change form for modal @@ -4116,19 +4130,19 @@ def applicant_application_detail(request, slug): if not request.user.is_authenticated: return redirect("account_login") - # Get candidate profile (Person record) + # Get application profile (Person record) agency = getattr(request.user,"agency_profile",None) if agency: - candidate = get_object_or_404(Application,slug=slug) + application = get_object_or_404(Application,slug=slug) # if Application.objects.filter(person=candidate,hirin).exists() else: try: - candidate = request.user.person_profile + applicant = request.user.person_profile except: - messages.error(request, "No candidate profile found.") + messages.error(request, "No applicant profile found.") return redirect("account_login") - # Get the specific application and verify it belongs to this candidate + # Get the specific application and verify it belongs to this applicant application = get_object_or_404( Application.objects.select_related( 'job', 'person' @@ -4136,7 +4150,7 @@ def applicant_application_detail(request, slug): 'scheduled_interviews' # Only prefetch interviews, not documents (Generic FK) ), slug=slug, - person=candidate.person if agency else candidate + person=application.person if agency else applicant ) # Get AI analysis data if available @@ -4155,7 +4169,7 @@ def applicant_application_detail(request, slug): context = { "application": application, - "candidate": candidate, + "applicant": applicant, "ai_analysis": ai_analysis, "interviews": interviews, "documents": documents, @@ -4319,11 +4333,11 @@ def agency_portal_submit_application_page(request, slug): if request.method == "POST": form = ApplicationForm(request.POST, request.FILES,current_agency=current_agency,current_job=current_job) if form.is_valid(): - candidate = form.save(commit=False) + application = form.save(commit=False) - candidate.hiring_source = "AGENCY" - candidate.hiring_agency = assignment.agency - candidate.save() + application.hiring_source = "AGENCY" + application.hiring_agency = assignment.agency + application.save() assignment.increment_submission_count() return redirect("agency_portal_dashboard") @@ -4339,12 +4353,12 @@ def agency_portal_submit_application_page(request, slug): "total_submitted": total_submitted, "job": assignment.job, } - return render(request, "recruitment/agency_portal_submit_candidate.html", context) + return render(request, "recruitment/agency_portal_submit_application.html", context) @agency_user_required def agency_portal_submit_application(request): - """Handle candidate submission via AJAX (for embedded form)""" + """Handle application submission via AJAX (for embedded form)""" assignment_id = request.session.get("agency_assignment_id") if not assignment_id: return redirect("agency_portal_login") @@ -4353,24 +4367,24 @@ def agency_portal_submit_application(request): AgencyJobAssignment.objects.select_related("agency", "job"), id=assignment_id ) if assignment.is_full: - messages.error(request, "Maximum candidate limit reached for this assignment.") + messages.error(request, "Maximum application limit reached for this assignment.") return redirect("agency_portal_assignment_detail", slug=assignment.slug) # Check if assignment allows submission if not assignment.can_submit: messages.error( request, - "Cannot submit candidates: Assignment is not active, expired, or full.", + "Cannot submit applications: Assignment is not active, expired, or full.", ) return redirect("agency_portal_dashboard") if request.method == "POST": form = AgencyApplicationSubmissionForm(assignment, request.POST, request.FILES) if form.is_valid(): - candidate = form.save(commit=False) - candidate.hiring_source = "AGENCY" - candidate.hiring_agency = assignment.agency - candidate.save() + application = form.save(commit=False) + application.hiring_source = "AGENCY" + application.hiring_agency = assignment.agency + application.save() # Increment the assignment's submitted count assignment.increment_submission_count() @@ -4379,12 +4393,12 @@ def agency_portal_submit_application(request): return JsonResponse( { "success": True, - "message": f"Candidate {candidate.name} submitted successfully!", + "message": f"application {application.name} submitted successfully!", } ) else: messages.success( - request, f"Candidate {candidate.name} submitted successfully!" + request, f"application {application.name} submitted successfully!" ) return redirect("agency_portal_dashboard") else: @@ -4400,10 +4414,10 @@ def agency_portal_submit_application(request): context = { "form": form, "assignment": assignment, - "title": f"Submit Candidate for {assignment.job.title}", - "button_text": "Submit Candidate", + "title": f"Submit application for {assignment.job.title}", + "button_text": "Submit application", } - return render(request, "recruitment/agency_portal_submit_candidate.html", context) + return render(request, "recruitment/agency_portal_submit_application.html", context) def agency_portal_assignment_detail(request, slug): @@ -4445,8 +4459,8 @@ def agency_assignment_detail_agency(request, slug, assignment_id): ) return redirect("agency_portal_dashboard") - # Get candidates submitted by this agency for this job - candidates = Application.objects.filter( + # Get applications submitted by this agency for this job + applications = Application.objects.filter( hiring_agency=assignment.agency, job=assignment.job ).order_by("-created_at") @@ -4456,8 +4470,8 @@ def agency_assignment_detail_agency(request, slug, assignment_id): # Mark messages as read # No messages to mark as read - # Pagination for candidates - paginator = Paginator(candidates, 20) # Show 20 candidates per page + # Pagination for applications + paginator = Paginator(applications, 20) # Show 20 applications per page page_number = request.GET.get("page") page_obj = paginator.get_page(page_number) @@ -4467,12 +4481,12 @@ def agency_assignment_detail_agency(request, slug, assignment_id): message_page_obj = message_paginator.get_page(message_page_number) # Calculate progress ring offset for circular progress indicator - total_candidates = candidates.count() - max_candidates = assignment.max_candidates + total_applications = applications.count() + max_applications = assignment.max_candidates circumference = 326.73 # 2 * π * r where r=52 - if max_candidates > 0: - progress_percentage = total_candidates / max_candidates + if max_applications > 0: + progress_percentage = total_applications / max_applications stroke_dashoffset = circumference - (circumference * progress_percentage) else: stroke_dashoffset = circumference @@ -4481,7 +4495,7 @@ def agency_assignment_detail_agency(request, slug, assignment_id): "assignment": assignment, "page_obj": page_obj, "message_page_obj": message_page_obj, - "total_candidates": total_candidates, + "total_applications": total_applications, "stroke_dashoffset": stroke_dashoffset, } return render(request, "recruitment/agency_portal_assignment_detail.html", context) @@ -4494,8 +4508,8 @@ def agency_assignment_detail_admin(request, slug): AgencyJobAssignment.objects.select_related("agency", "job"), slug=slug ) - # Get candidates submitted by this agency for this job - candidates = Application.objects.filter( + # Get applications submitted by this agency for this job + applications = Application.objects.filter( hiring_agency=assignment.agency, job=assignment.job ).order_by("-created_at") @@ -4507,16 +4521,18 @@ def agency_assignment_detail_admin(request, slug): context = { "assignment": assignment, - "candidates": candidates, + "applications": applications, "access_link": access_link, - "total_candidates": candidates.count(), + "total_applications": applications.count(), } return render(request, "recruitment/agency_assignment_detail.html", context) + +#will check the changes application to appliaction in this function @agency_user_required -def agency_portal_edit_application(request, candidate_id): - """Edit a candidate for agency portal""" +def agency_portal_edit_application(request, application_id): + """Edit a application for agency portal""" assignment_id = request.session.get("agency_assignment_id") if not assignment_id: return redirect("agency_portal_login") @@ -4528,45 +4544,45 @@ def agency_portal_edit_application(request, candidate_id): agency = current_assignment.agency - # Get candidate and verify it belongs to this agency - candidate = get_object_or_404(Application, id=candidate_id, hiring_agency=agency) + # Get application and verify it belongs to this agency + application = get_object_or_404(Application, id=application_id, hiring_agency=agency) if request.method == "POST": # Handle form submission - candidate.first_name = request.POST.get("first_name", candidate.first_name) - candidate.last_name = request.POST.get("last_name", candidate.last_name) - candidate.email = request.POST.get("email", candidate.email) - candidate.phone = request.POST.get("phone", candidate.phone) - candidate.address = request.POST.get("address", candidate.address) + application.first_name = request.POST.get("first_name", application.first_name) + application.last_name = request.POST.get("last_name", application.last_name) + application.email = request.POST.get("email", application.email) + application.phone = request.POST.get("phone", application.phone) + application.address = request.POST.get("address", application.address) # Handle resume upload if provided if "resume" in request.FILES: - candidate.resume = request.FILES["resume"] + application.resume = request.FILES["resume"] try: - candidate.save() + application.save() messages.success( - request, f"Candidate {candidate.name} updated successfully!" + request, f"Application {application.name} updated successfully!" ) return redirect( "agency_assignment_detail", - slug=candidate.job.agencyjobassignment_set.first().slug, + slug=application.job.agencyjobassignment_set.first().slug, ) except Exception as e: - messages.error(request, f"Error updating candidate: {e}") + messages.error(request, f"Error updating application: {e}") # For GET requests or POST errors, return JSON response for AJAX if request.headers.get("X-Requested-With") == "XMLHttpRequest": return JsonResponse( { "success": True, - "candidate": { - "id": candidate.id, - "first_name": candidate.first_name, - "last_name": candidate.last_name, - "email": candidate.email, - "phone": candidate.phone, - "address": candidate.address, + "application": { + "id": application.id, + "first_name": application.first_name, + "last_name": application.last_name, + "email": application.email, + "phone": application.phone, + "address": application.address, }, } ) @@ -4576,8 +4592,8 @@ def agency_portal_edit_application(request, candidate_id): @agency_user_required -def agency_portal_delete_application(request, candidate_id): - """Delete a candidate for agency portal""" +def agency_portal_delete_application(request, application_id): + """Delete a application for agency portal""" assignment_id = request.session.get("agency_assignment_id") if not assignment_id: return redirect("agency_portal_login") @@ -4589,20 +4605,20 @@ def agency_portal_delete_application(request, candidate_id): agency = current_assignment.agency - # Get candidate and verify it belongs to this agency - candidate = get_object_or_404(Application, id=candidate_id, hiring_agency=agency) + # Get application and verify it belongs to this agency + application = get_object_or_404(Application, id=application_id, hiring_agency=agency) if request.method == "POST": try: - candidate_name = candidate.name - candidate.delete() + application_name = application.name + application.delete() - current_assignment.candidates_submitted -= 1 + current_assignment.candidates_submitted -= 1 #in the modal current_assignment.status = current_assignment.AssignmentStatus.ACTIVE current_assignment.save(update_fields=["candidates_submitted", "status"]) messages.success( - request, f"Candidate {candidate_name} removed successfully!" + request, f"Application {application_name} removed successfully!" ) return JsonResponse({"success": True}) except Exception as e: @@ -4661,7 +4677,7 @@ def message_list(request): "search_query": search_query, } if request.user.user_type != "staff": - return render(request, "messages/candidate_message_list.html", context) + return render(request, "messages/application_message_list.html", context) return render(request, "messages/message_list.html", context) @@ -4687,7 +4703,7 @@ def message_detail(request, message_id): "message": message, } if request.user.user_type != "staff": - return render(request, "messages/candidate_message_detail.html", context) + return render(request, "messages/application_message_detail.html", context) return render(request, "messages/message_detail.html", context) @@ -4743,7 +4759,7 @@ def message_create(request): "form": form, } if request.user.user_type != "staff": - return render(request, "messages/candidate_message_form.html", context) + return render(request, "messages/application_message_form.html", context) return render(request, "messages/message_form.html", context) @@ -4811,7 +4827,7 @@ def message_reply(request, message_id): "parent_message": parent_message, } if request.user.user_type != "staff": - return render(request, "messages/candidate_message_form.html", context) + return render(request, "messages/application_message_form.html", context) return render(request, "messages/message_form.html", context) @@ -4869,35 +4885,47 @@ def message_mark_unread(request, message_id): @login_required def message_delete(request, message_id): - """Delete a message""" + """ + Deletes a message using a POST request, primarily designed for HTMX. + Redirects to the message list on success (either via standard redirect + or HTMX's hx-redirect header). + """ + + # 1. Retrieve the message + # Use select_related to fetch linked objects efficiently for checks/logging message = get_object_or_404( Message.objects.select_related("sender", "recipient"), id=message_id ) - # Check if user has permission to delete this message + # 2. Permission Check + # Only the sender or recipient can delete the message if message.sender != request.user and message.recipient != request.user: messages.error(request, "You don't have permission to delete this message.") + + # HTMX requests should handle redirection via client-side logic (hx-redirect) + if "HX-Request" in request.headers: + # Returning 403 or 400 is ideal, but 200 with an empty body is often accepted + # by HTMX and the message is shown on the next page/refresh. + return HttpResponse(status=403) + + # Standard navigation redirect return redirect("message_list") + # 3. Handle POST Request (Deletion) if request.method == "POST": message.delete() messages.success(request, "Message deleted successfully.") # Handle HTMX requests if "HX-Request" in request.headers: - return HttpResponse(status=200) # HTMX success response - - return redirect("message_list") - - # For GET requests, show confirmation page - context = { - "message": message, - "title": "Delete Message", - "message": f"Are you sure you want to delete this message from {message.sender.get_full_name() or message.sender.username}?", - "cancel_url": reverse("message_detail", kwargs={"message_id": message_id}), - } - return render(request, "messages/message_confirm_delete.html", context) + # 1. Set the HTMX response header for redirection + response = HttpResponse(status=200) + response["HX-Redirect"] = reverse("message_list") # <--- EXPLICIT HEADER + return response + # Standard navigation fallback + return redirect("message_list") + @login_required def api_unread_count(request): @@ -4927,10 +4955,10 @@ def document_upload(request, slug): # Handle Person document upload try: person = get_object_or_404(Person, id=actual_application_id) - # Check if user owns this person (for candidate portal) + # Check if user owns this person (for applicant portal) if request.user.user_type == "candidate": - candidate = request.user.person_profile - if person != candidate: + applicant = request.user.person_profile + if person != applicant: messages.error(request, "You can only upload documents to your own profile.") return JsonResponse({"success": False, "error": "Permission denied"}) except (ValueError, Person.DoesNotExist): @@ -4942,15 +4970,15 @@ def document_upload(request, slug): except (ValueError, Application.DoesNotExist): return JsonResponse({"success": False, "error": "Invalid application ID"}) - # Check if user owns this application (for candidate portal) + # Check if user owns this application (for applicant portal) if request.user.user_type == "candidate": try: - candidate = request.user.person_profile - if application.person != candidate: + applicant = request.user.person_profile + if application.person != applicant: messages.error(request, "You can only upload documents to your own applications.") return JsonResponse({"success": False, "error": "Permission denied"}) except: - messages.error(request, "No candidate profile found.") + messages.error(request, "No applicant profile found.") return JsonResponse({"success": False, "error": "Permission denied"}) if request.method == "POST": @@ -5024,54 +5052,138 @@ def document_upload(request, slug): return redirect("application_detail", slug=application.job.slug) +# @login_required +# def document_delete(request, document_id): +# """Delete a document""" +# document = get_object_or_404(Document, id=document_id) + +# # Check permission - document is now linked to Application or Person via Generic Foreign Key +# if hasattr(document.content_object, "job"): +# # Application document +# if ( +# document.content_object.job.assigned_to != request.user +# and not request.user.is_superuser +# ): +# messages.error( +# request, "You don't have permission to delete this document." +# ) +# return JsonResponse({"success": False, "error": "Permission denied"}) +# job_slug = document.content_object.job.slug +# redirect_url = "applicant_portal_dashboard" if request.user.user_type == "candidate" else "job_detail" +# elif hasattr(document.content_object, "person"): +# # Person document +# if request.user.user_type == "candidate": +# applicant = request.user.person_profile +# if document.content_object != applicant: +# messages.error( +# request, "You can only delete your own documents." +# ) +# return JsonResponse({"success": False, "error": "Permission denied"}) +# redirect_url = "applicant_portal_dashboard" +# else: +# # Handle other content object types +# messages.error(request, "You don't have permission to delete this document.") +# return JsonResponse({"success": False, "error": "Permission denied"}) + +# if request.method == "POST": +# file_name = document.file.name if document.file else "Unknown" +# document.delete() +# messages.success(request, f'Document "{file_name}" deleted successfully!') + +# # Handle AJAX requests +# if request.headers.get("X-Requested-With") == "XMLHttpRequest": +# return JsonResponse( +# {"success": True, "message": "Document deleted successfully!"} +# ) +# else: +# return redirect("application_detail", slug=job_slug) + +# return JsonResponse({"success": False, "error": "Method not allowed"}) + + @login_required def document_delete(request, document_id): - """Delete a document""" + """Delete a document using a POST request (ideal for HTMX).""" document = get_object_or_404(Document, id=document_id) + + # Initialize variables for redirection outside of the complex logic + is_htmx = "HX-Request" in request.headers + + # 1. Permission and Context Initialization + has_permission = False + + content_object = document.content_object + + # Case A: Document linked to an Application (via content_object) + if hasattr(content_object, "job"): + # Staff/Superuser checking against Application's Job assignment + if (content_object.job.assigned_to == request.user) or request.user.is_superuser: + has_permission = True + + # Candidate checking if the Application belongs to them + elif request.user.user_type == "candidate" and content_object.person.user == request.user: + has_permission = True - # Check permission - document is now linked to Application or Person via Generic Foreign Key - if hasattr(document.content_object, "job"): - # Application document - if ( - document.content_object.job.assigned_to != request.user - and not request.user.is_superuser - ): - messages.error( - request, "You don't have permission to delete this document." - ) - return JsonResponse({"success": False, "error": "Permission denied"}) - job_slug = document.content_object.job.slug - redirect_url = "applicant_portal_dashboard" if request.user.user_type == "candidate" else "job_detail" - elif hasattr(document.content_object, "person"): - # Person document + # Determine redirect URL for non-HTMX requests (fallback) if request.user.user_type == "candidate": - candidate = request.user.person_profile - if document.content_object != candidate: - messages.error( - request, "You can only delete your own documents." - ) - return JsonResponse({"success": False, "error": "Permission denied"}) - redirect_url = "applicant_portal_dashboard" + # Assuming you redirect to the candidate's main dashboard after deleting their app document + redirect_view_name = "applicant_portal_dashboard" + else: + # Assuming you redirect to the job detail page for staff + redirect_view_name = "job_detail" + redirect_args = [content_object.job.slug] # Pass the job slug + + # Case B: Document linked directly to a Person (e.g., profile document) + elif hasattr(content_object, "user"): + # Check if the document belongs to the requesting candidate + if request.user.user_type == "candidate" and content_object.user == request.user: + has_permission = True + redirect_view_name = "applicant_portal_dashboard" + # Check if the requesting user is staff/superuser (Staff can delete profile docs) + elif request.user.is_staff or request.user.is_superuser: + has_permission = True + # Staff should probably go to the person's profile detail, but defaulting to a safe spot. + redirect_view_name = "dashboard" + + # Case C: No clear content object linkage or unhandled type else: - # Handle other content object types - messages.error(request, "You don't have permission to delete this document.") - return JsonResponse({"success": False, "error": "Permission denied"}) + has_permission = request.user.is_superuser # Only superuser can delete unlinked docs + + # 2. Enforce Permissions + if not has_permission: + messages.error(request, "Permission denied: You cannot delete this document.") + # Return a 403 response for HTMX/AJAX + return HttpResponse(status=403) + + + # 3. Handle POST Request (Deletion) if request.method == "POST": file_name = document.file.name if document.file else "Unknown" document.delete() messages.success(request, f'Document "{file_name}" deleted successfully!') - # Handle AJAX requests - if request.headers.get("X-Requested-With") == "XMLHttpRequest": - return JsonResponse( - {"success": True, "message": "Document deleted successfully!"} - ) + # --- HTMX / AJAX Response --- + if is_htmx or request.headers.get("X-Requested-With") == "XMLHttpRequest": + # For HTMX, return a 200 OK. The front-end is expected to use hx-swap='outerHTML' + # to remove the element, or hx-redirect to navigate. + return HttpResponse(status=200) + + # --- Standard Navigation Fallback --- else: - return redirect("application_detail", slug=job_slug) - - return JsonResponse({"success": False, "error": "Method not allowed"}) + try: + # Use the calculated redirect view name and arguments + if 'redirect_args' in locals(): + return redirect(redirect_view_name, *redirect_args) + else: + return redirect(redirect_view_name) + except NameError: + # If no specific redirect_view_name was set (e.g., Case C failure) + return redirect("dashboard") + # 4. Handle non-POST (e.g., GET) + # The delete view should not be accessed via GET. + return HttpResponse(status=405) # Method Not Allowed @login_required def document_download(request, document_id): @@ -5093,8 +5205,8 @@ def document_download(request, document_id): elif hasattr(document.content_object, "person"): # Person document if request.user.user_type == "candidate": - candidate = request.user.person_profile - if document.content_object != candidate: + applicant = request.user.person_profile + if document.content_object != applicant: messages.error( request, "You can only download your own documents." ) @@ -5114,15 +5226,7 @@ def document_download(request, document_id): return JsonResponse({"success": False, "error": "File not found"}) - if document.file: - response = HttpResponse( - document.file.read(), content_type="application/octet-stream" - ) - response["Content-Disposition"] = f'attachment; filename="{document.file.name}"' - return response - - return JsonResponse({"success": False, "error": "File not found"}) - + @login_required def portal_logout(request): @@ -5135,31 +5239,31 @@ def portal_logout(request): # Interview Creation Views @staff_user_required -def interview_create_type_selection(request, candidate_slug): - """Show interview type selection page for a candidate""" - candidate = get_object_or_404(Application, slug=candidate_slug) +def interview_create_type_selection(request, application_slug): + """Show interview type selection page for a application""" + application = get_object_or_404(Application, slug=application_slug) - # Validate candidate is in Interview stage - if candidate.stage != 'Interview': - messages.error(request, f"Candidate {candidate.name} is not in Interview stage.") - return redirect('candidate_interview_view', slug=candidate.job.slug) + # Validate application is in Interview stage + if application.stage != 'Interview': + messages.error(request, f"application {application.name} is not in Interview stage.") + return redirect('applications_interview_view', slug=application.job.slug) context = { - 'candidate': candidate, - 'job': candidate.job, + 'application': application, + 'job': application.job, } return render(request, 'interviews/interview_create_type_selection.html', context) @staff_user_required -def interview_create_remote(request, candidate_slug): - """Create remote interview for a candidate""" - application = get_object_or_404(Application, slug=candidate_slug) +def interview_create_remote(request, application_slug): + """Create remote interview for a application""" + application = get_object_or_404(Application, slug=application_slug) - # Validate candidate is in Interview stage - # if candidate.stage != 'Interview': - # messages.error(request, f"Candidate {candidate.name} is not in Interview stage.") - # return redirect('candidate_interview_view', slug=candidate.job.slug) + # Validate application is in Interview stage + # if application.stage != 'Interview': + # messages.error(request, f"application {application.name} is not in Interview stage.") + # return redirect('application_interview_view', slug=application.job.slug) if request.method == 'POST': form = RemoteInterviewForm(request.POST) @@ -5206,7 +5310,7 @@ def interview_create_remote(request, candidate_slug): form = RemoteInterviewForm() form.initial['topic'] = f"Interview for {application.job.title} - {application.name}" context = { - 'candidate': application, + 'application': application, 'job': application.job, 'form': form, } @@ -5214,14 +5318,14 @@ def interview_create_remote(request, candidate_slug): @staff_user_required -def interview_create_onsite(request, candidate_slug): - """Create onsite interview for a candidate""" - candidate = get_object_or_404(Application, slug=candidate_slug) +def interview_create_onsite(request, application_slug): + """Create onsite interview for a application""" + application = get_object_or_404(Application, slug=application_slug) - # Validate candidate is in Interview stage - # if candidate.stage != 'Interview': - # messages.error(request, f"Candidate {candidate.name} is not in Interview stage.") - # return redirect('candidate_interview_view', slug=candidate.job.slug) + # Validate application is in Interview stage + # if application.stage != 'Interview': + # messages.error(request, f"application {application.name} is not in Interview stage.") + # return redirect('application_interview_view', slug=application.job.slug) if request.method == 'POST': from .models import Interview @@ -5235,7 +5339,7 @@ def interview_create_onsite(request, candidate_slug): physical_address=form.cleaned_data["physical_address"], duration=form.cleaned_data["duration"],location_type="Onsite",status="SCHEDULED") - schedule = ScheduledInterview.objects.create(application=candidate,job=candidate.job,interview=interview,interview_date=form.cleaned_data["interview_date"],interview_time=form.cleaned_data["interview_time"]) + schedule = ScheduledInterview.objects.create(application=application,job=application.job,interview=interview,interview_date=form.cleaned_data["interview_date"],interview_time=form.cleaned_data["interview_time"]) # Create ScheduledInterview record # interview = form.save(commit=False) # interview.interview_type = 'ONSITE' @@ -5264,19 +5368,19 @@ def interview_create_onsite(request, candidate_slug): # interview.interview_location = onsite_location # interview.save() - messages.success(request, f"Onsite interview scheduled for {candidate.name}") + messages.success(request, f"Onsite interview scheduled for {application.name}") return redirect('interview_detail', slug=schedule.slug) except Exception as e: messages.error(request, f"Error creating onsite interview: {str(e)}") else: # Pre-populate topic - form.initial['topic'] = f"Interview for {candidate.job.title} - {candidate.name}" + form.initial['topic'] = f"Interview for {application.job.title} - {application.name}" form = OnsiteInterviewForm() context = { - 'candidate': candidate, - 'job': candidate.job, + 'application': application, + 'job': application.job, 'form': form, } return render(request, 'interviews/interview_create_onsite.html', context) @@ -5363,10 +5467,10 @@ def agency_access_link_reactivate(request, slug): @agency_user_required -def api_application_detail(request, candidate_id): - """API endpoint to get candidate details for agency portal""" +def api_application_detail(request, application_id): + """API endpoint to get application details for agency portal""" try: - # Get candidate from session-based agency access + # Get application from session-based agency access assignment_id = request.session.get("agency_assignment_id") if not assignment_id: return JsonResponse({"success": False, "error": "Access denied"}) @@ -5378,20 +5482,20 @@ def api_application_detail(request, candidate_id): agency = current_assignment.agency - # Get candidate and verify it belongs to this agency - candidate = get_object_or_404( - Application, id=candidate_id, hiring_agency=agency + # Get application and verify it belongs to this agency + application = get_object_or_404( + Application, id=application_id, hiring_agency=agency ) - # Return candidate data + # Return application data response_data = { "success": True, - "id": candidate.id, - "first_name": candidate.first_name, - "last_name": candidate.last_name, - "email": candidate.email, - "phone": candidate.phone, - "address": candidate.address, + "id": application.id, + "first_name": application.first_name, + "last_name": application.last_name, + "email": application.email, + "phone": application.phone, + "address": application.address, } return JsonResponse(response_data) @@ -5402,7 +5506,7 @@ def api_application_detail(request, candidate_id): @staff_user_required def compose_application_email(request, job_slug): - """Compose email to participants about a candidate""" + """Compose email to participants about a application""" from .email_service import send_bulk_email job = get_object_or_404(JobPosting, slug=job_slug) @@ -5411,14 +5515,20 @@ def compose_application_email(request, job_slug): # if request.method == "POST": # form = CandidateEmailForm(job, candidate, request.POST) candidate_ids=request.GET.getlist('candidate_ids') - candidates=Application.objects.filter(id__in=candidate_ids) + + print("candidate_ids:",candidate_ids) + + + applications=Application.objects.filter(id__in=candidate_ids) if request.method == 'POST': candidate_ids = request.POST.getlist('candidate_ids') - candidates=Application.objects.filter(id__in=candidate_ids) - form = CandidateEmailForm(job, candidates, request.POST) + print("candidate_ids from post:", candidate_ids) + + applications=Application.objects.filter(id__in=candidate_ids) + form = CandidateEmailForm(job, applications, request.POST) if form.is_valid(): print("form is valid ...") # Get email addresses @@ -5454,23 +5564,28 @@ def compose_application_email(request, job_slug): ) if email_result["success"]: - for candidate in candidates: - if hasattr(candidate, 'person') and candidate.person: + for application in applications: + if hasattr(application, 'person') and application.person: try: + print(request.user) + print(application.person.user) + print(subject) + print(message) + print(job) + Message.objects.create( sender=request.user, - recipient=candidate.person.user, + recipient=application.person.user, subject=subject, content=message, job=job, - message_type='email', - is_email_sent=True, - email_address=candidate.person.email if candidate.person.email else candidate.email + message_type='job_related', + is_read=False, ) except Exception as e: # Log error but don't fail the entire process - print(f"Error creating message") + print(f"CRITICAL ERROR creating message for {application.person.user.email}: {e}") messages.success( request, @@ -5500,7 +5615,7 @@ def compose_application_email(request, job_slug): return render( request, "includes/email_compose_form.html", - {"form": form, "job": job, "candidate": candidates}, + {"form": form, "job": job, "applications": applications}, ) @@ -5521,18 +5636,18 @@ def compose_application_email(request, job_slug): return render( request, "includes/email_compose_form.html", - {"form": form, "job": job, "candidates": candidates}, + {"form": form, "job": job, "applications": applications}, ) else: # GET request - show the form - form = CandidateEmailForm(job, candidates) + form = CandidateEmailForm(job, applications) return render( request, "includes/email_compose_form.html", - # {"form": form, "job": job, "candidates": candidates}, - {"form": form,"job":job}, + # {"form": form, "job": job, "applications": applications}, + {"form": form,"job":job,"applications":applications}, ) diff --git a/recruitment/views_frontend.py b/recruitment/views_frontend.py index fc15046..7148b34 100644 --- a/recruitment/views_frontend.py +++ b/recruitment/views_frontend.py @@ -279,7 +279,7 @@ def application_detail(request, slug): @login_required @staff_user_required def application_resume_template_view(request, slug): - """Display formatted resume template for a candidate""" + """Display formatted resume template for a application""" application = get_object_or_404(models.Application, slug=slug) if not request.user.is_staff: @@ -398,7 +398,7 @@ def dashboard_view(request): # --- 2. TIME SERIES: GLOBAL DAILY APPLICANTS --- - # Group ALL candidates by creation date + # Group ALL applications by creation date global_daily_applications_qs = all_applications_queryset.annotate( date=TruncDate('created_at') ).values('date').annotate( @@ -482,7 +482,7 @@ def dashboard_view(request): # ) - # candidates_with_score_query= candidate_queryset.filter(is_resume_parsed=True).annotate( + # applications_with_score_query= application_queryset.filter(is_resume_parsed=True).annotate( # # The Coalesce handles NULL values (from missing data, non-numeric data, or NullIf) and sets them to 0. # annotated_match_score=Coalesce(safe_match_score_cast, Value(0)) # ) @@ -637,10 +637,10 @@ def dashboard_view(request): @login_required @staff_user_required def applications_offer_view(request, slug): - """View for candidates in the Offer stage""" + """View for applications in the Offer stage""" job = get_object_or_404(models.JobPosting, slug=slug) - # Filter candidates for this specific job and stage + # Filter applications for this specific job and stage applications = job.offer_applications # Handle search diff --git a/templates/includes/email_compose_form.html b/templates/includes/email_compose_form.html index 269ad2b..770d854 100644 --- a/templates/includes/email_compose_form.html +++ b/templates/includes/email_compose_form.html @@ -18,7 +18,7 @@
cb.value), - include_candidate_info: form.querySelector('#{{ form.include_candidate_info.id_for_label }}').checked, + include_application_info: form.querySelector('#{{ form.include_application_info.id_for_label }}').checked, include_meeting_details: form.querySelector('#{{ form.include_meeting_details.id_for_label }}').checked }; @@ -428,8 +428,8 @@ document.addEventListener('DOMContentLoaded', function() { } // Restore checkboxes - if (draft.include_candidate_info) { - form.querySelector('#{{ form.include_candidate_info.id_for_label }}').checked = draft.include_candidate_info; + if (draft.include_application_info) { + form.querySelector('#{{ form.include_application_info.id_for_label }}').checked = draft.include_application_info; } if (draft.include_meeting_details) { form.querySelector('#{{ form.include_meeting_details.id_for_label }}').checked = draft.include_meeting_details; diff --git a/templates/interviews/interview_create_onsite.html b/templates/interviews/interview_create_onsite.html index 441ac15..7e20298 100644 --- a/templates/interviews/interview_create_onsite.html +++ b/templates/interviews/interview_create_onsite.html @@ -1,6 +1,7 @@ {% extends "base.html" %} +{% load i18n %} -{% block title %}Create Onsite Interview{% endblock %} +{% block title %}{% trans "Create Onsite Interview" %}{% endblock %} {% block content %}
@@ -10,18 +11,18 @@

- Create Onsite Interview for {{ candidate.name }} + {% blocktrans %}Create Onsite Interview for {{ application.name }}{% endblocktrans %}

- - Back to Candidate List + {% trans "Back to application List" %}

- Schedule an onsite interview for {{ candidate.name }} - for the position of {{ job.title }}. + {% blocktrans %}Schedule an onsite interview for {{ application.name }} + for the position of {{ job.title }}.{% endblocktrans %}

{% if messages %} @@ -33,7 +34,7 @@ {% endfor %} {% endif %} - + {% csrf_token %}
@@ -41,7 +42,7 @@
{{ form.topic }} {% if form.topic.errors %} @@ -57,7 +58,7 @@
{{ form.interview_date }} {% if form.interview_date.errors %} @@ -72,7 +73,7 @@
{{ form.interview_time }} {% if form.interview_time.errors %} @@ -89,7 +90,7 @@
{{ form.duration }} {% if form.duration.errors %} @@ -132,7 +133,7 @@
{{ form.physical_address }} {% if form.physical_address.errors %} @@ -147,7 +148,7 @@
{{ form.room_number }} {% if form.room_number.errors %} @@ -203,7 +204,7 @@
@@ -226,7 +227,7 @@ document.addEventListener('DOMContentLoaded', function() { dateInput.addEventListener('change', function() { if (this.value < today) { - this.setCustomValidity('Interview date must be in the future'); + this.setCustomValidity('{% trans "Interview date must be in the future" %}'); } else { this.setCustomValidity(''); } diff --git a/templates/interviews/interview_create_remote.html b/templates/interviews/interview_create_remote.html index 0a20013..482ea38 100644 --- a/templates/interviews/interview_create_remote.html +++ b/templates/interviews/interview_create_remote.html @@ -1,7 +1,7 @@ {% extends "base.html" %} {% load i18n crispy_forms_tags %} -{% block title %}Create Remote Interview{% endblock %} +{% block title %}{% trans "Create Remote Interview" %}{% endblock %} {% block content %}
@@ -11,18 +11,18 @@

- Create Remote Interview for {{ candidate.name }} + {% blocktrans %}Create Remote Interview for {{ application.name }}{% endblocktrans %}

- - Back to Candidate List + {% trans "Back to application List" %}

- Schedule a remote interview for {{ candidate.name }} - for the position of {{ job.title }}. + {% blocktrans %}Schedule a remote interview for {{ application.name }} + for the position of {{ job.title }}.{% endblocktrans %}

{% if messages %} @@ -34,13 +34,13 @@ {% endfor %} {% endif %} -
+ {% csrf_token %} {{form|crispy}}
@@ -63,7 +63,7 @@ document.addEventListener('DOMContentLoaded', function() { dateInput.addEventListener('change', function() { if (this.value < today) { - this.setCustomValidity('Interview date must be in the future'); + this.setCustomValidity('{% trans "Interview date must be in the future" %}'); } else { this.setCustomValidity(''); } diff --git a/templates/interviews/interview_create_type_selection.html b/templates/interviews/interview_create_type_selection.html index 3b5ad56..99d3c70 100644 --- a/templates/interviews/interview_create_type_selection.html +++ b/templates/interviews/interview_create_type_selection.html @@ -1,6 +1,7 @@ {% extends "base.html" %} +{% load i18n %} -{% block title %}Create Interview - Select Type{% endblock %} +{% block title %}{% trans "Create Interview - Select Type" %}{% endblock %} {% block content %}
@@ -10,41 +11,41 @@

- Create Interview for {{ candidate.name }} + {% blocktrans %}Create Interview for {{ application.name }}{% endblocktrans %}

- Select the type of interview you want to schedule for {{ candidate.name }} - for the position of {{ job.title }}. + {% blocktrans %}Select the type of interview you want to schedule for {{ application.name }} + for the position of {{ job.title }}.{% endblocktrans %}

diff --git a/templates/messages/candidate_message_detail.html b/templates/messages/application_message_detail.html similarity index 100% rename from templates/messages/candidate_message_detail.html rename to templates/messages/application_message_detail.html diff --git a/templates/messages/candidate_message_form.html b/templates/messages/application_message_form.html similarity index 99% rename from templates/messages/candidate_message_form.html rename to templates/messages/application_message_form.html index e95a0ba..cf9f760 100644 --- a/templates/messages/candidate_message_form.html +++ b/templates/messages/application_message_form.html @@ -1,7 +1,7 @@ {% extends "portal_base.html" %} {% load static %} - -{% block title %}{% if form.instance.pk %}{% trans "Reply to Message"%}{% else %}{% trans"Compose Message"%}{% endif %}{% endblock %} +{% load i18n %} +{% block title %}{% if form.instance.pk %}{% trans "Reply to Message" %}{% else %}{% trans "Compose Message" %}{% endif %}{% endblock %} {% block content %}
diff --git a/templates/messages/candidate_message_list.html b/templates/messages/application_message_list.html similarity index 100% rename from templates/messages/candidate_message_list.html rename to templates/messages/application_message_list.html diff --git a/templates/messages/message_detail.html b/templates/messages/message_detail.html index dedba03..6ee0fda 100644 --- a/templates/messages/message_detail.html +++ b/templates/messages/message_detail.html @@ -27,12 +27,15 @@ {% trans "Mark Unread" %} {% endif %} - - {% trans "Delete" %} - + {% trans "Back to Messages" %} diff --git a/templates/messages/message_list.html b/templates/messages/message_list.html index 45a4139..337280c 100644 --- a/templates/messages/message_list.html +++ b/templates/messages/message_list.html @@ -138,7 +138,7 @@ diff --git a/templates/people/person_list.html b/templates/people/person_list.html index 375169e..0afa091 100644 --- a/templates/people/person_list.html +++ b/templates/people/person_list.html @@ -370,13 +370,13 @@ class="btn btn-sm btn-outline-secondary"> {% trans "Edit" %} - + {% endcomment %} {% endif %}
diff --git a/templates/recruitment/agency_assignment_detail.html b/templates/recruitment/agency_assignment_detail.html index 9f869d2..0782f49 100644 --- a/templates/recruitment/agency_assignment_detail.html +++ b/templates/recruitment/agency_assignment_detail.html @@ -224,7 +224,7 @@
- {% trans "Submitted Applications" %} ({{ total_candidates }}) + {% trans "Submitted Applications" %} ({{ total_applications }})
{% if access_link %} @@ -327,19 +327,19 @@ style="stroke-dasharray: 326.73; stroke-dashoffset: {{ stroke_dashoffset }};"/>
-
{{ total_candidates }}
+
{{ total_applications }}
{% trans "of" %} {{ assignment.max_candidates}}
-
{{ total_candidates }}
+
{{ total_applications }}
/ {{ assignment.max_candidates }} {% trans "applications" %}
- {% widthratio total_candidates assignment.max_candidates 100 as progress %} + {% widthratio total_applications assignment.max_candidates 100 as progress %}
@@ -353,7 +353,7 @@
- {% trans "Send Message" %} diff --git a/templates/recruitment/agency_detail.html b/templates/recruitment/agency_detail.html index afc79a3..333b6ca 100644 --- a/templates/recruitment/agency_detail.html +++ b/templates/recruitment/agency_detail.html @@ -313,7 +313,7 @@ {{ agency.name }}

- {% trans "Hiring Agency Details and Candidate Management" %} + {% trans "Hiring Agency Details and Application Management" %}

@@ -531,7 +531,7 @@ aria-selected="true" > - {% trans "Recent Candidates" %} + {% trans "Recent Applications" %}
@@ -195,18 +195,18 @@
{% if assignment.can_submit %} - {% trans "Submit New Candidate" %} + {% trans "Submit New application" %} {% else %}
{% if assignment.is_expired %} {% trans "This assignment has expired. Submissions are no longer accepted." %} {% elif assignment.is_full %} - {% trans "Maximum candidate limit reached for this assignment." %} + {% trans "Maximum application limit reached for this assignment." %} {% else %} {% trans "This assignment is not currently active." %} {% endif %} @@ -221,14 +221,14 @@
- {% endcomment %} + {% endcomment %}
- {% trans "Submitted Candidates" %} ({{ total_candidates }}) + {% trans "Submitted applications" %} ({{ total_applications }})
- {{ total_candidates }}/{{ assignment.max_candidates }} + {{ total_applications }}/{{ assignment.max_applications }}
{% if page_obj %} @@ -244,27 +244,27 @@ - {% for candidate in page_obj %} + {% for application in page_obj %} -
{{ candidate.name }}
+
{{ application.name }}
-
{{ candidate.email }}
-
{{ candidate.phone }}
+
{{ application.email }}
+
{{ application.phone }}
- {{ candidate.get_stage_display }} + {{ application.get_stage_display }}
- {{ candidate.created_at|date:"Y-m-d H:i" }} + {{ application.created_at|date:"Y-m-d H:i" }}
- + @@ -311,9 +311,9 @@ {% else %}
-
{% trans "No candidates submitted yet" %}
+
{% trans "No applications submitted yet" %}

- {% trans "Submit candidates using the form above to get started." %} + {% trans "Submit applications using the form above to get started." %}

{% endif %} @@ -348,19 +348,19 @@ style="stroke-dasharray: 326.73; stroke-dashoffset: {{ stroke_dashoffset }};"/>
- {% widthratio total_candidates assignment.max_candidates 100 as progress %} + {% widthratio total_applications assignment.max_candidates 100 as progress %} {{ progress|floatformat:0 }}%
-
{{ total_candidates }}
-
/ {{ assignment.max_candidates }} {% trans "candidates" %}
+
{{ total_applications }}
+
/ {{ assignment.max_candidates }} {% trans "applications" %}
- {% widthratio total_candidates assignment.max_candidates 100 as progress %} + {% widthratio total_applications assignment.max_candidates 100 as progress %}
@@ -415,7 +415,7 @@
- {% widthratio total_candidates assignment.max_candidates 100 as progress %} + {% widthratio total_applications assignment.max_candidates 100 as progress %} {{ progress|floatformat:1 }}%
@@ -512,14 +512,14 @@
- +