diff --git a/recruitment/candidate_sync_service.py b/recruitment/candidate_sync_service.py index 65a84a3..7e76f33 100644 --- a/recruitment/candidate_sync_service.py +++ b/recruitment/candidate_sync_service.py @@ -35,7 +35,7 @@ class CandidateSyncService: } # Get all hired candidates for this job - hired_candidates = list(job.hired_candidates.select_related('job')) + hired_candidates = list(job.hired_applications.select_related('job')) results['total_candidates'] = len(hired_candidates) diff --git a/recruitment/models.py b/recruitment/models.py index 0ea413a..1441182 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -132,7 +132,7 @@ class JobPosting(Base): # Application Information ---job detail apply link for the candidates application_url = models.URLField( validators=[URLValidator()], - help_text="URL where candidates apply", + help_text="URL where applicants apply", null=True, blank=True, ) @@ -222,7 +222,7 @@ class JobPosting(Base): related_name="jobs", verbose_name=_("Hiring Agency"), help_text=_( - "External agency responsible for sourcing candidates for this role" + "External agency responsible for sourcing applicants for this role" ), ) cancel_reason = models.TextField( @@ -365,7 +365,7 @@ class JobPosting(Base): @property def current_applications_count(self): - """Returns the current number of candidates associated with this job.""" + """Returns the current number of applications associated with this job.""" return self.applications.count() @property @@ -377,7 +377,7 @@ class JobPosting(Base): return self.current_applications_count >= self.max_applications @property - def all_candidates(self): + def all_applications(self): # 1. Define the safe JSON extraction and conversion expression safe_score_expression = Cast( Coalesce( @@ -397,68 +397,70 @@ class JobPosting(Base): ).order_by("-sortable_score") @property - def screening_candidates(self): - return self.all_candidates.filter(stage="Applied") + def screening_applications(self): + return self.all_applications.filter(stage="Applied") @property - def exam_candidates(self): - return self.all_candidates.filter(stage="Exam") + def exam_applications(self): + return self.all_applications.filter(stage="Exam") @property - def interview_candidates(self): - return self.all_candidates.filter(stage="Interview") + def interview_applications(self): + return self.all_applications.filter(stage="Interview") @property - def document_review_candidates(self): - return self.all_candidates.filter(stage="Document Review") + def document_review_applications(self): + return self.all_applications.filter(stage="Document Review") @property - def offer_candidates(self): - return self.all_candidates.filter(stage="Offer") + def offer_applications(self): + return self.all_applications.filter(stage="Offer") @property - def accepted_candidates(self): - return self.all_candidates.filter(offer_status="Accepted") + def accepted_applications(self): + return self.all_applications.filter(offer_status="Accepted") @property - def hired_candidates(self): - return self.all_candidates.filter(stage="Hired") + def hired_applications(self): + return self.all_applications.filter(stage="Hired") # counts @property - def all_candidates_count(self): - return self.all_candidates.count() + def all_applications_count(self): + return self.all_applications.count() @property - def screening_candidates_count(self): - return self.all_candidates.filter(stage="Applied").count() or 0 + def screening_applications_count(self): + return self.all_applications.filter(stage="Applied").count() or 0 @property - def exam_candidates_count(self): - return self.all_candidates.filter(stage="Exam").count() or 0 + def exam_applications_count(self): + return self.all_applications.filter(stage="Exam").count() or 0 @property - def interview_candidates_count(self): - return self.all_candidates.filter(stage="Interview").count() or 0 + def interview_applications_count(self): + return self.all_applications.filter(stage="Interview").count() or 0 @property - def document_review_candidates_count(self): - return self.all_candidates.filter(stage="Document Review").count() or 0 + def document_review_applications_count(self): + return self.all_applications.filter(stage="Document Review").count() or 0 @property - def offer_candidates_count(self): - return self.all_candidates.filter(stage="Offer").count() or 0 + def offer_applications_count(self): + return self.all_applications.filter(stage="Offer").count() or 0 @property - def hired_candidates_count(self): - return self.all_candidates.filter(stage="Hired").count() or 0 + def hired_applications_count(self): + return self.all_applications.filter(stage="Hired").count() or 0 @property def vacancy_fill_rate(self): total_positions = self.open_positions + print(total_positions) - no_of_positions_filled = self.applications.filter(stage__in=["HIRED"]).count() + no_of_positions_filled = self.applications.filter(stage__in=["Hired"]).count() + print(no_of_positions_filled) if total_positions > 0: vacancy_fill_rate = no_of_positions_filled / total_positions diff --git a/recruitment/tasks.py b/recruitment/tasks.py index c3aec80..2ec216b 100644 --- a/recruitment/tasks.py +++ b/recruitment/tasks.py @@ -866,7 +866,7 @@ def sync_hired_candidates_task(job_slug): # action=IntegrationLog.ActionChoices.SYNC, # endpoint="multi_source_sync", # method="BACKGROUND_TASK", - # request_data={"job_slug": job_slug, "candidate_count": job.accepted_candidates.count()}, + # request_data={"job_slug": job_slug, "candidate_count": job.accepted_applications.count()}, # response_data=results, # status_code="SUCCESS" if results.get('summary', {}).get('failed', 0) == 0 else "PARTIAL", # ip_address="127.0.0.1", # Background task diff --git a/recruitment/views.py b/recruitment/views.py index 1724361..768c148 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -452,13 +452,13 @@ def edit_job(request, slug): if form.is_valid(): try: form.save() - messages.success(request, f'Job "{job.title}" updated successfully!') + messages.success(request, _('Job "%(title)s" updated successfully!') % {'title': job.title}) return redirect("job_list") except Exception as e: logger.error(f"Error updating job: {e}") - messages.error(request, f"Error updating job: {e}") + messages.error(request, _('Error updating job: %(error)s') % {'error': e}) else: - messages.error(request, "Please correct the errors below.") + messages.error(request, _("Please correct the errors below.")) else: job = get_object_or_404(JobPosting, slug=slug) form = JobPostingForm(instance=job) @@ -478,18 +478,18 @@ def job_detail(request, slug): """View details of a specific job""" job = get_object_or_404(JobPosting, slug=slug) # Get all applications for this job, ordered by most recent - applicants = job.applications.all().order_by("-created_at") + applications = job.applications.all().order_by("-created_at") # Count applications by stage for summary statistics - total_applicant = applicants.count() + total_applications = applications.count() - applied_count = applicants.filter(stage="Applied").count() + applied_count = applications.filter(stage="Applied").count() - exam_count = applicants.filter(stage="Exam").count() + exam_count = applications.filter(stage="Exam").count() - interview_count = applicants.filter(stage="Interview").count() + interview_count = applications.filter(stage="Interview").count() - offer_count = applicants.filter(stage="Offer").count() + offer_count = applications.filter(stage="Offer").count() status_form = JobPostingStatusForm(instance=job) linkedin_content_form = LinkedPostContentForm(instance=job) @@ -528,8 +528,8 @@ def job_detail(request, slug): # --- 2. Quality Metrics (JSON Aggregation) --- - candidates_with_score = applicants.filter(is_resume_parsed=True) - total_candidates = candidates_with_score.count() # For context + applications_with_score = applications.filter(is_resume_parsed=True) + total_applications_ = applications_with_score.count() # For context # Define the queryset for applications that have been parsed score_expression = Cast( @@ -544,34 +544,34 @@ def job_detail(request, slug): ) # 2. ANNOTATE the queryset with the new field - candidates_with_score = candidates_with_score.annotate( + applications_with_score = applications_with_score.annotate( annotated_match_score=score_expression ) - avg_match_score_result = candidates_with_score.aggregate( + avg_match_score_result = applications_with_score.aggregate( avg_score=Avg('annotated_match_score') ) avg_match_score = avg_match_score_result.get("avg_score") or 0.0 - high_potential_count = candidates_with_score.filter( + high_potential_count = applications_with_score.filter( annotated_match_score__gte=HIGH_POTENTIAL_THRESHOLD ).count() high_potential_ratio = ( - round((high_potential_count / total_candidates) * 100, 1) - if total_candidates > 0 + round((high_potential_count / total_applications_) * 100, 1) + if total_applications_ > 0 else 0 ) # --- 3. Time Metrics (Duration Aggregation) --- # Metric: Average Time from Applied to Interview (T2I) - t2i_candidates = applicants.filter(interview_date__isnull=False).annotate( + t2i_applications = applications.filter(interview_date__isnull=False).annotate( time_to_interview=ExpressionWrapper( F("interview_date") - F("created_at"), output_field=DurationField() ) ) - avg_t2i_duration = t2i_candidates.aggregate(avg_t2i=Avg("time_to_interview"))[ + avg_t2i_duration = t2i_applications.aggregate(avg_t2i=Avg("time_to_interview"))[ "avg_t2i" ] @@ -583,14 +583,14 @@ def job_detail(request, slug): ) # Metric: Average Time in Exam Stage - t_in_exam_candidates = applicants.filter( + t_in_exam_applications = applications.filter( exam_date__isnull=False, interview_date__isnull=False ).annotate( time_in_exam=ExpressionWrapper( F("interview_date") - F("exam_date"), output_field=DurationField() ) ) - avg_t_in_exam_duration = t_in_exam_candidates.aggregate( + avg_t_in_exam_duration = t_in_exam_applications.aggregate( avg_t_in_exam=Avg("time_in_exam") )["avg_t_in_exam"] @@ -602,26 +602,27 @@ def job_detail(request, slug): ) category_data = ( - applicants.filter(ai_analysis_data__analysis_data__category__isnull=False) - .values("ai_analysis_data__analysis_data__category") + applications.filter(ai_analysis_data__analysis_data_en__category__isnull=False) + .values("ai_analysis_data__analysis_data_en__category") .annotate( - candidate_count=Count("id"), + application_count=Count("id"), category=Cast( - "ai_analysis_data__analysis_data__category", output_field=CharField() + "ai_analysis_data__analysis_data_en__category", output_field=CharField() ), ) - .order_by("ai_analysis_data__analysis_data__category") + .order_by("ai_analysis_data__analysis_data_en__category") ) # Prepare data for Chart.js - print(category_data) + categories = [item["category"] for item in category_data] - candidate_counts = [item["candidate_count"] for item in category_data] + + applications_count = [item["application_count"] for item in category_data] # avg_scores = [round(item['avg_match_score'], 2) if item['avg_match_score'] is not None else 0 for item in category_data] context = { "job": job, - "applicants": applicants, - "total_applicants": total_applicant, # This was total_candidates in the prompt, using total_applicant for consistency + "applications": applications, + "total_applications": total_applications, # This was total_candidates in the prompt, using total_applicant for consistency "applied_count": applied_count, "exam_count": exam_count, "interview_count": interview_count, @@ -629,7 +630,7 @@ def job_detail(request, slug): "status_form": status_form, "image_upload_form": image_upload_form, "categories": categories, - "candidate_counts": candidate_counts, + "applications_count": applications_count, # 'avg_scores': avg_scores, # New statistics "avg_match_score": avg_match_score, @@ -1765,7 +1766,7 @@ def applications_screening_view(request, slug): Manage candidate tiers and stage transitions """ job = get_object_or_404(JobPosting, slug=slug) - candidates = job.screening_candidates + applications = job.screening_applications # Get filter parameters min_ai_score_str = request.GET.get("min_ai_score") @@ -1805,31 +1806,31 @@ def applications_screening_view(request, slug): # Apply filters if min_ai_score > 0: - candidates = candidates.filter( - ai_analysis_data__analysis_data__match_score__gte=min_ai_score + applications = applications.filter( + ai_analysis_data__analysis_data_en__match_score__gte=min_ai_score ) if min_experience > 0: - candidates = candidates.filter( - ai_analysis_data__analysis_data__years_of_experience__gte=min_experience + applications = applications.filter( + ai_analysis_data__analysis_data_en__years_of_experience__gte=min_experience ) if screening_rating: - candidates = candidates.filter( - ai_analysis_data__analysis_data__screening_stage_rating=screening_rating + applications = applications.filter( + ai_analysis_data__analysis_data_en__screening_stage_rating=screening_rating ) if gpa: - candidates = candidates.filter( + applications = applications.filter( person__gpa__gt= gpa ) - print(candidates) + print(applications) if tier1_count > 0: - candidates = candidates[:tier1_count] + applications = applications[:tier1_count] context = { "job": job, - "candidates": candidates, + "applications": applications, "min_ai_score": min_ai_score, "min_experience": min_experience, "screening_rating": screening_rating, @@ -1838,7 +1839,7 @@ def applications_screening_view(request, slug): "current_stage": "Applied", } - return render(request, "recruitment/candidate_screening_view.html", context) + return render(request, "recruitment/applications_screening_view.html", context) @staff_user_required @@ -1847,8 +1848,8 @@ def applications_exam_view(request, slug): Manage candidate tiers and stage transitions """ job = get_object_or_404(JobPosting, slug=slug) - context = {"job": job, "candidates": job.exam_candidates, "current_stage": "Exam"} - return render(request, "recruitment/candidate_exam_view.html", context) + context = {"job": job, "applications": job.exam_applications, "current_stage": "Exam"} + return render(request, "recruitment/applications_exam_view.html", context) @staff_user_required @@ -1888,9 +1889,9 @@ def bulk_update_application_exam_status(request, slug): def application_criteria_view_htmx(request, pk): - candidate = get_object_or_404(Application, pk=pk) + application = get_object_or_404(Application, pk=pk) return render( - request, "includes/candidate_modal_body.html", {"candidate": candidate} + request, "includes/application_modal_body.html", {"application": application} ) @@ -1989,11 +1990,11 @@ def applications_interview_view(request, slug): job = get_object_or_404(JobPosting, slug=slug) context = { "job": job, - "candidates": job.interview_candidates, + "applications": job.interview_applications, "current_stage": "Interview", } - return render(request, "recruitment/candidate_interview_view.html", context) + return render(request, "recruitment/applications_interview_view.html", context) @staff_user_required @@ -2004,12 +2005,12 @@ def applications_document_review_view(request, slug): job = get_object_or_404(JobPosting, slug=slug) # Get candidates from Interview stage who need document review - candidates = job.document_review_candidates.select_related('person') - print(candidates) + applications = job.document_review_applications.select_related('person') + # Get search query for filtering search_query = request.GET.get('q', '') if search_query: - candidates = candidates.filter( + applications = applications.filter( Q(person__first_name__icontains=search_query) | Q(person__last_name__icontains=search_query) | Q(person__email__icontains=search_query) @@ -2017,11 +2018,11 @@ def applications_document_review_view(request, slug): context = { "job": job, - "candidates": candidates, + "applications": applications, "current_stage": "Document Review", "search_query": search_query, } - return render(request, "recruitment/candidate_document_review_view.html", context) + return render(request, "recruitment/applications_document_review_view.html", context) @staff_user_required @@ -2066,7 +2067,7 @@ def reschedule_meeting_for_application(request, slug, candidate_id, meeting_id): @staff_user_required def schedule_meeting_for_application(request, slug, candidate_pk, meeting_id): job = get_object_or_404(JobPosting, slug=slug) - candidate = get_object_or_404(Application, pk=candidate_pk) + application = get_object_or_404(Application, pk=candidate_pk) meeting = get_object_or_404(ZoomMeetingDetails, pk=meeting_id) if request.method == "POST": result = delete_zoom_meeting(meeting.meeting_id) @@ -2082,7 +2083,7 @@ def schedule_meeting_for_application(request, slug, candidate_pk, meeting_id): context = { "job": job, - "candidate": candidate, + "application": application, "meeting": meeting, "delete_url": reverse( "schedule_meeting_for_application", @@ -2767,7 +2768,7 @@ def schedule_meeting_for_application(request, slug, candidate_pk): Handles POST to process the form, create a meeting, and redirect back. """ job = get_object_or_404(JobPosting, slug=slug) - candidate = get_object_or_404(Application, pk=candidate_pk, job=job) + application = get_object_or_404(Application, pk=candidate_pk, job=job) if request.method == "POST": form = ZoomMeetingForm(request.POST) @@ -2778,7 +2779,7 @@ def schedule_meeting_for_application(request, slug, candidate_pk): # Use a default topic if not provided if not topic_val: - topic_val = f"Interview: {job.title} with {candidate.name}" + topic_val = f"Interview: {job.title} with {application.name}" # Ensure start_time is in the future if start_time_val <= timezone.now(): @@ -2788,7 +2789,7 @@ def schedule_meeting_for_application(request, slug, candidate_pk): # return render(request, "recruitment/schedule_meeting_form.html", { # 'form': form, # 'job': job, - # 'candidate': candidate, + # 'application': application, # 'initial_topic': topic_val, # 'initial_start_time': start_time_val.strftime('%Y-%m-%dT%H:%M') if start_time_val else '', # 'initial_duration': duration_val @@ -2821,14 +2822,14 @@ def schedule_meeting_for_application(request, slug, candidate_pk): ) # Create a ScheduledInterview record ScheduledInterview.objects.create( - application=candidate, + application=application, job=job, interview_location=zoom_meeting_instance, interview_date=start_time_val.date(), interview_time=start_time_val.time(), status="scheduled", ) - messages.success(request, f"Meeting scheduled with {candidate.name}.") + messages.success(request, f"Meeting scheduled with {application.name}.") return redirect("applications_interview_view", slug=job.slug) else: messages.error( @@ -2842,7 +2843,7 @@ def schedule_meeting_for_application(request, slug, candidate_pk): { "form": form, "job": job, - "candidate": candidate, + "application": application, "initial_topic": topic_val, "initial_start_time": start_time_val.strftime("%Y-%m-%dT%H:%M") if start_time_val @@ -2858,9 +2859,9 @@ def schedule_meeting_for_application(request, slug, candidate_pk): { "form": form, "job": job, - "candidate": candidate, + "application": application, "initial_topic": request.POST.get( - "topic", f"Interview: {job.title} with {candidate.name}" + "topic", f"Interview: {job.title} with {application.name}" ), "initial_start_time": request.POST.get("start_time", ""), "initial_duration": request.POST.get("duration", 60), @@ -2868,7 +2869,7 @@ def schedule_meeting_for_application(request, slug, candidate_pk): ) else: # GET request initial_data = { - "topic": f"Interview: {job.title} with {candidate.name}", + "topic": f"Interview: {job.title} with {application.name}", "start_time": (timezone.now() + timedelta(hours=1)).strftime( "%Y-%m-%dT%H:%M" ), # Default to 1 hour from now @@ -2878,7 +2879,7 @@ def schedule_meeting_for_application(request, slug, candidate_pk): return render( request, "meetings/schedule_meeting_form.html", - {"form": form, "job": job, "candidate": candidate}, + {"form": form, "job": job, "application": application}, ) diff --git a/recruitment/views_frontend.py b/recruitment/views_frontend.py index ecc1533..83fae93 100644 --- a/recruitment/views_frontend.py +++ b/recruitment/views_frontend.py @@ -30,6 +30,7 @@ from django.shortcuts import render from django.utils import timezone from datetime import timedelta import json +from django.utils.translation import gettext_lazy as _ # Add imports for user type restrictions from recruitment.decorators import StaffRequiredMixin, staff_user_required @@ -91,7 +92,7 @@ class JobUpdateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, form_class = forms.JobPostingForm template_name = 'jobs/edit_job.html' success_url = reverse_lazy('job_list') - success_message = 'Job updated successfully.' + success_message = _('Job updated successfully.') slug_url_kwarg = 'slug' @@ -99,12 +100,12 @@ class JobDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, model = models.JobPosting template_name = 'jobs/partials/delete_modal.html' success_url = reverse_lazy('job_list') - success_message = 'Job deleted successfully.' + success_message = _('Job deleted successfully.') slug_url_kwarg = 'slug' class JobApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView): model = models.Application - template_name = 'jobs/job_candidates_list.html' + template_name = 'jobs/job_applications_list.html' context_object_name = 'applications' paginate_by = 10 @@ -146,13 +147,13 @@ class JobApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView): class ApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView): model = models.Application - template_name = 'recruitment/candidate_list.html' + template_name = 'recruitment/applications_list.html' context_object_name = 'applications' paginate_by = 100 def get_queryset(self): queryset = super().get_queryset() - + # Handle search search_query = self.request.GET.get('search', '') job = self.request.GET.get('job', '') @@ -186,10 +187,10 @@ class ApplicationListView(LoginRequiredMixin, StaffRequiredMixin, ListView): class ApplicationCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, CreateView): model = models.Application form_class = forms.ApplicationForm - template_name = 'recruitment/candidate_create.html' + template_name = 'recruitment/application_create.html' success_url = reverse_lazy('application_list') - success_message = 'Candidate created successfully.' - + success_message = _('Application created successfully.') + def get_initial(self): initial = super().get_initial() if 'slug' in self.kwargs: @@ -215,17 +216,17 @@ class ApplicationCreateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessa class ApplicationUpdateView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, UpdateView): model = models.Application form_class = forms.ApplicationForm - template_name = 'recruitment/candidate_update.html' + template_name = 'recruitment/application_update.html' success_url = reverse_lazy('application_list') - success_message = 'Candidate updated successfully.' + success_message = _('Application updated successfully.') slug_url_kwarg = 'slug' class ApplicationDeleteView(LoginRequiredMixin, StaffRequiredMixin, SuccessMessageMixin, DeleteView): model = models.Application - template_name = 'recruitment/candidate_delete.html' + template_name = 'recruitment/application_delete.html' success_url = reverse_lazy('application_list') - success_message = 'Candidate deleted successfully.' + success_message = _('Application deleted successfully.') slug_url_kwarg = 'slug' @@ -255,9 +256,9 @@ def training_list(request): @staff_user_required def application_detail(request, slug): from rich.json import JSON - candidate = get_object_or_404(models.Application, slug=slug) + application = get_object_or_404(models.Application, slug=slug) try: - parsed = ast.literal_eval(candidate.parsed_summary) + parsed = ast.literal_eval(application.parsed_summary) except: parsed = {} @@ -271,7 +272,7 @@ def application_detail(request, slug): # parsed = JSON(json.dumps(parsed), indent=2, highlight=True, skip_keys=False, ensure_ascii=False, check_circular=True, allow_nan=True, default=None, sort_keys=False) # parsed = json_to_markdown_table([parsed]) return render(request, 'recruitment/application_detail.html', { - 'candidate': candidate, + 'application': application, 'parsed': parsed, 'stage_form': stage_form, }) @@ -287,7 +288,7 @@ def application_resume_template_view(request, slug): messages.error(request, _("You don't have permission to view this page.")) return redirect('application_list') - return render(request, 'recruitment/candidate_resume_template.html', { + return render(request, 'recruitment/application_resume_template.html', { 'application': application }) @@ -301,7 +302,7 @@ def application_update_stage(request, slug): stage_value = form.cleaned_data['stage'] application.stage = stage_value application.save(update_fields=['stage']) - messages.success(request,"application Stage Updated") + messages.success(request,_("application Stage Updated")) return redirect("application_detail",slug=application.slug) class TrainingListView(LoginRequiredMixin, StaffRequiredMixin, ListView): @@ -386,7 +387,7 @@ def dashboard_view(request): # --- 1. BASE QUERYSETS & GLOBAL METRICS (UNFILTERED) --- all_jobs_queryset = models.JobPosting.objects.all().order_by('-created_at') - all_candidates_queryset = models.Application.objects.all() + all_applications_queryset = models.Application.objects.all() # Global KPI Card Metrics total_jobs_global = all_jobs_queryset.count() @@ -400,7 +401,7 @@ def dashboard_view(request): # --- 2. TIME SERIES: GLOBAL DAILY APPLICANTS --- # Group ALL candidates by creation date - global_daily_applications_qs = all_candidates_queryset.annotate( + global_daily_applications_qs = all_applications_queryset.annotate( date=TruncDate('created_at') ).values('date').annotate( count=Count('pk') @@ -412,14 +413,14 @@ def dashboard_view(request): # --- 3. FILTERING LOGIC: Determine the scope for scoped metrics --- - candidate_queryset = all_candidates_queryset + application_queryset = all_applications_queryset job_scope_queryset = all_jobs_queryset interview_queryset = models.ScheduledInterview.objects.all() current_job = None if selected_job_pk: # Filter all base querysets - candidate_queryset = candidate_queryset.filter(job__pk=selected_job_pk) + application_queryset = application_queryset.filter(job__pk=selected_job_pk) interview_queryset = interview_queryset.filter(job__pk=selected_job_pk) try: @@ -434,7 +435,7 @@ def dashboard_view(request): scoped_dates = [] scoped_counts = [] if selected_job_pk: - scoped_daily_applications_qs = candidate_queryset.annotate( + scoped_daily_applications_qs = application_queryset.annotate( date=TruncDate('created_at') ).values('date').annotate( count=Count('pk') @@ -446,7 +447,7 @@ def dashboard_view(request): # --- 5. SCOPED CORE AGGREGATIONS (FILTERED OR ALL) --- - total_candidates = candidate_queryset.count() + total_applications = application_queryset.count() score_expression = Cast( @@ -461,7 +462,7 @@ def dashboard_view(request): ) # 2. ANNOTATE the queryset with the new field - candidates_with_score_query = candidate_queryset.annotate( + applications_with_score_query = application_queryset.annotate( annotated_match_score=score_expression ) @@ -493,24 +494,24 @@ def dashboard_view(request): # A. Pipeline & Volume Metrics (Scoped) total_active_jobs = job_scope_queryset.filter(status="ACTIVE").count() last_week = timezone.now() - timedelta(days=7) - new_candidates_7days = candidate_queryset.filter(created_at__gte=last_week).count() + new_applications_7days = application_queryset.filter(created_at__gte=last_week).count() open_positions_agg = job_scope_queryset.filter(status="ACTIVE").aggregate(total_open=Sum('open_positions')) total_open_positions = open_positions_agg['total_open'] or 0 average_applications_result = job_scope_queryset.annotate( - candidate_count=Count('applications', distinct=True) - ).aggregate(avg_apps=Avg('candidate_count'))['avg_apps'] + applications_count=Count('applications', distinct=True) + ).aggregate(avg_apps=Avg('applications_count'))['avg_apps'] average_applications = round(average_applications_result or 0, 2) # B. Efficiency & Conversion Metrics (Scoped) - hired_candidates = candidate_queryset.filter( + hired_applications = application_queryset.filter( stage='Hired' ) - lst=[c.time_to_hire_days for c in hired_candidates] + lst=[c.time_to_hire_days for c in hired_applications] - time_to_hire_query = hired_candidates.annotate( + time_to_hire_query = hired_applications.annotate( time_diff=ExpressionWrapper( F('join_date') - F('created_at__date'), output_field=fields.DurationField() @@ -527,11 +528,11 @@ def dashboard_view(request): ) print(avg_time_to_hire_days) - applied_count = candidate_queryset.filter(stage='Applied').count() - advanced_count = candidate_queryset.filter(stage__in=['Exam', 'Interview', 'Offer']).count() + applied_count = application_queryset.filter(stage='Applied').count() + advanced_count = application_queryset.filter(stage__in=['Exam', 'Interview', 'Offer']).count() screening_pass_rate = round( (advanced_count / applied_count) * 100, 1 ) if applied_count > 0 else 0 - offers_extended_count = candidate_queryset.filter(stage='Offer').count() - offers_accepted_count = candidate_queryset.filter(offer_status='Accepted').count() + offers_extended_count = application_queryset.filter(stage='Offer').count() + offers_accepted_count = application_queryset.filter(offer_status='Accepted').count() offers_accepted_rate = round( (offers_accepted_count / offers_extended_count) * 100, 1 ) if offers_extended_count > 0 else 0 filled_positions = offers_accepted_count vacancy_fill_rate = round( (filled_positions / total_open_positions) * 100, 1 ) if total_open_positions > 0 else 0 @@ -542,21 +543,21 @@ def dashboard_view(request): meetings_scheduled_this_week = interview_queryset.filter( interview_date__week=current_week, interview_date__year=current_year ).count() - avg_match_score_result = candidates_with_score_query.aggregate(avg_score=Avg('annotated_match_score'))['avg_score'] + avg_match_score_result = applications_with_score_query.aggregate(avg_score=Avg('annotated_match_score'))['avg_score'] avg_match_score = round(avg_match_score_result or 0, 1) - high_potential_count = candidates_with_score_query.filter(annotated_match_score__gte=HIGH_POTENTIAL_THRESHOLD).count() - high_potential_ratio = round( (high_potential_count / total_candidates) * 100, 1 ) if total_candidates > 0 else 0 - total_scored_candidates = candidates_with_score_query.count() - scored_ratio = round( (total_scored_candidates / total_candidates) * 100, 1 ) if total_candidates > 0 else 0 + high_potential_count = applications_with_score_query.filter(annotated_match_score__gte=HIGH_POTENTIAL_THRESHOLD).count() + high_potential_ratio = round( (high_potential_count / total_applications) * 100, 1 ) if total_applications > 0 else 0 + total_scored_candidates = applications_with_score_query.count() + scored_ratio = round( (total_scored_candidates / total_applications) * 100, 1 ) if total_applications > 0 else 0 # --- 6. CHART DATA PREPARATION --- # A. Pipeline Funnel (Scoped) - stage_counts = candidate_queryset.values('stage').annotate(count=Count('stage')) + stage_counts = application_queryset.values('stage').annotate(count=Count('stage')) stage_map = {item['stage']: item['count'] for item in stage_counts} - candidate_stage = ['Applied', 'Exam', 'Interview', 'Offer', 'Hired'] - candidates_count = [ + application_stage = ['Applied', 'Exam', 'Interview', 'Offer', 'Hired'] + application_count = [ stage_map.get('Applied', 0), stage_map.get('Exam', 0), stage_map.get('Interview', 0), stage_map.get('Offer', 0), stage_map.get('Hired',0) ] @@ -570,9 +571,9 @@ def dashboard_view(request): rotation_degrees_final = round(min(rotation_degrees, 180), 1) # Ensure max 180 degrees # - hiring_source_counts = candidate_queryset.values('hiring_source').annotate(count=Count('stage')) + hiring_source_counts = application_queryset.values('hiring_source').annotate(count=Count('stage')) source_map= {item['hiring_source']: item['count'] for item in hiring_source_counts} - candidates_count_in_each_source = [ + applications_count_in_each_source = [ source_map.get('Public', 0), source_map.get('Internal', 0), source_map.get('Agency', 0), ] @@ -589,8 +590,8 @@ def dashboard_view(request): # Scoped KPIs 'total_active_jobs': total_active_jobs, - 'total_candidates': total_candidates, - 'new_candidates_7days': new_candidates_7days, + 'total_applications': total_applications, + 'new_applications_7days': new_applications_7days, 'total_open_positions': total_open_positions, 'average_applications': average_applications, 'avg_time_to_hire_days': avg_time_to_hire_days, @@ -604,8 +605,8 @@ def dashboard_view(request): 'scored_ratio': scored_ratio, # Chart Data - 'candidate_stage': json.dumps(candidate_stage), - 'candidates_count': json.dumps(candidates_count), + 'application_stage': json.dumps(application_stage), + 'application_count': json.dumps(application_count), 'job_titles': json.dumps(job_titles), 'job_app_counts': json.dumps(job_app_counts), # 'source_volume_chart_data' is intentionally REMOVED @@ -628,7 +629,7 @@ def dashboard_view(request): 'current_job': current_job, - 'candidates_count_in_each_source': json.dumps(candidates_count_in_each_source), + 'applications_count_in_each_source': json.dumps(applications_count_in_each_source), 'all_hiring_sources': json.dumps(all_hiring_sources), } @@ -642,95 +643,95 @@ def applications_offer_view(request, slug): job = get_object_or_404(models.JobPosting, slug=slug) # Filter candidates for this specific job and stage - candidates = job.offer_candidates + applications = job.offer_applications # Handle search search_query = request.GET.get('search', '') if search_query: - candidates = candidates.filter( + applications = applications.filter( Q(first_name__icontains=search_query) | Q(last_name__icontains=search_query) | Q(email__icontains=search_query) | Q(phone__icontains=search_query) ) - candidates = candidates.order_by('-created_at') + applications = applications.order_by('-created_at') context = { 'job': job, - 'candidates': candidates, + 'applications': applications, 'search_query': search_query, 'current_stage': 'Offer', } - return render(request, 'recruitment/candidate_offer_view.html', context) + return render(request, 'recruitment/applications_offer_view.html', context) @login_required @staff_user_required def applications_hired_view(request, slug): - """View for hired candidates""" + """View for hired applications""" job = get_object_or_404(models.JobPosting, slug=slug) - # Filter candidates with offer_status = 'Accepted' - candidates = job.hired_candidates + # Filter applications with offer_status = 'Accepted' + applications = job.hired_applications # Handle search search_query = request.GET.get('search', '') if search_query: - candidates = candidates.filter( + applications = applications.filter( Q(first_name__icontains=search_query) | Q(last_name__icontains=search_query) | Q(email__icontains=search_query) | Q(phone__icontains=search_query) ) - candidates = candidates.order_by('-created_at') + applications = applications.order_by('-created_at') context = { 'job': job, - 'candidates': candidates, + 'applications': applications, 'search_query': search_query, 'current_stage': 'Hired', } - return render(request, 'recruitment/candidate_hired_view.html', context) + return render(request, 'recruitment/applications_hired_view.html', context) @login_required @staff_user_required -def update_application_status(request, job_slug, candidate_slug, stage_type, status): +def update_application_status(request, job_slug, application_slug, stage_type, status): """Handle exam/interview/offer status updates""" from django.utils import timezone job = get_object_or_404(models.JobPosting, slug=job_slug) - candidate = get_object_or_404(models.Application, slug=candidate_slug, job=job) + application = get_object_or_404(models.Application, slug=application_slug, job=job) if request.method == "POST": if stage_type == 'exam': status = request.POST.get("exam_status") score = request.POST.get("exam_score") - candidate.exam_status = status - candidate.exam_score = score - candidate.exam_date = timezone.now() - candidate.save(update_fields=['exam_status','exam_score', 'exam_date']) - return render(request,'recruitment/partials/exam-results.html',{'candidate':candidate,'job':job}) + application.exam_status = status + application.exam_score = score + application.exam_date = timezone.now() + application.save(update_fields=['exam_status','exam_score', 'exam_date']) + return render(request,'recruitment/partials/exam-results.html',{'application':application,'job':job}) elif stage_type == 'interview': - candidate.interview_status = status - candidate.interview_date = timezone.now() - candidate.save(update_fields=['interview_status', 'interview_date']) - return render(request,'recruitment/partials/interview-results.html',{'candidate':candidate,'job':job}) + application.interview_status = status + application.interview_date = timezone.now() + application.save(update_fields=['interview_status', 'interview_date']) + return render(request,'recruitment/partials/interview-results.html',{'application':application,'job':job}) elif stage_type == 'offer': - candidate.offer_status = status - candidate.offer_date = timezone.now() - candidate.save(update_fields=['offer_status', 'offer_date']) - return render(request,'recruitment/partials/offer-results.html',{'candidate':candidate,'job':job}) - return redirect('application_detail', candidate.slug) + application.offer_status = status + application.offer_date = timezone.now() + application.save(update_fields=['offer_status', 'offer_date']) + return render(request,'recruitment/partials/offer-results.html',{'application':application,'job':job}) + return redirect('application_detail', application.slug) else: if stage_type == 'exam': - return render(request,"includes/candidate_update_exam_form.html",{'candidate':candidate,'job':job}) + return render(request,"includes/applications_update_exam_form.html",{'application':application,'job':job}) elif stage_type == 'interview': - return render(request,"includes/candidate_update_interview_form.html",{'candidate':candidate,'job':job}) + return render(request,"includes/applications_update_interview_form.html",{'application':application,'job':job}) elif stage_type == 'offer': - return render(request,"includes/candidate_update_offer_form.html",{'candidate':candidate,'job':job}) + return render(request,"includes/applications_update_offer_form.html",{'application':application,'job':job}) # Stage configuration for CSV export @@ -766,7 +767,7 @@ STAGE_CONFIG = { @login_required @staff_user_required def export_applications_csv(request, job_slug, stage): - """Export candidates for a specific stage as CSV""" + """Export applications for a specific stage as CSV""" job = get_object_or_404(models.JobPosting, slug=job_slug) # Validate stage @@ -776,23 +777,23 @@ def export_applications_csv(request, job_slug, stage): config = STAGE_CONFIG[stage] - # Filter candidates based on stage + # Filter applications based on stage if stage == 'hired': - candidates = job.applications.filter(**config['filter']) + applications = job.applications.filter(**config['filter']) else: - candidates = job.applications.filter(**config['filter']) + applications = job.applications.filter(**config['filter']) # Handle search if provided search_query = request.GET.get('search', '') if search_query: - candidates = candidates.filter( + applications = applications.filter( Q(first_name__icontains=search_query) | Q(last_name__icontains=search_query) | Q(email__icontains=search_query) | Q(phone__icontains=search_query) ) - candidates = candidates.order_by('-created_at') + applications = applications.order_by('-created_at') # Create CSV response response = HttpResponse(content_type='text/csv') @@ -809,87 +810,87 @@ def export_applications_csv(request, job_slug, stage): headers.extend(['Job Title', 'Department']) writer.writerow(headers) - # Write candidate data - for candidate in candidates: + # Write application data + for application in applications: row = [] # Extract data based on stage configuration for field in config['fields']: if field == 'name': - row.append(candidate.name) + row.append(application.name) elif field == 'email': - row.append(candidate.email) + row.append(application.email) elif field == 'phone': - row.append(candidate.phone) + row.append(application.phone) elif field == 'created_at': - row.append(candidate.created_at.strftime('%Y-%m-%d %H:%M') if candidate.created_at else '') + row.append(application.created_at.strftime('%Y-%m-%d %H:%M') if application.created_at else '') elif field == 'stage': - row.append(candidate.stage or '') + row.append(application.stage or '') elif field == 'exam_status': - row.append(candidate.exam_status or '') + row.append(application.exam_status or '') elif field == 'exam_date': - row.append(candidate.exam_date.strftime('%Y-%m-%d %H:%M') if candidate.exam_date else '') + row.append(application.exam_date.strftime('%Y-%m-%d %H:%M') if application.exam_date else '') elif field == 'interview_status': - row.append(candidate.interview_status or '') + row.append(application.interview_status or '') elif field == 'interview_date': - row.append(candidate.interview_date.strftime('%Y-%m-%d %H:%M') if candidate.interview_date else '') + row.append(application.interview_date.strftime('%Y-%m-%d %H:%M') if application.interview_date else '') elif field == 'offer_status': - row.append(candidate.offer_status or '') + row.append(application.offer_status or '') elif field == 'offer_date': - row.append(candidate.offer_date.strftime('%Y-%m-%d %H:%M') if candidate.offer_date else '') + row.append(application.offer_date.strftime('%Y-%m-%d %H:%M') if application.offer_date else '') elif field == 'ai_score': # Extract AI score using model property try: - score = candidate.match_score + score = application.match_score row.append(f"{score}%" if score else '') except: row.append('') elif field == 'years_experience': # Extract years of experience using model property try: - years = candidate.years_of_experience + years = application.years_of_experience row.append(f"{years}" if years else '') except: row.append('') elif field == 'screening_rating': # Extract screening rating using model property try: - rating = candidate.screening_stage_rating + rating = application.screening_stage_rating row.append(rating if rating else '') except: row.append('') elif field == 'professional_category': # Extract professional category using model property try: - category = candidate.professional_category + category = application.professional_category row.append(category if category else '') except: row.append('') elif field == 'top_skills': # Extract top 3 skills using model property try: - skills = candidate.top_3_keywords + skills = application.top_3_keywords row.append(', '.join(skills) if skills else '') except: row.append('') elif field == 'strengths': # Extract strengths using model property try: - strengths = candidate.strengths + strengths = application.strengths row.append(strengths if strengths else '') except: row.append('') elif field == 'weaknesses': # Extract weaknesses using model property try: - weaknesses = candidate.weaknesses + weaknesses = application.weaknesses row.append(weaknesses if weaknesses else '') except: row.append('') elif field == 'join_date': - row.append(candidate.join_date.strftime('%Y-%m-%d') if candidate.join_date else '') + row.append(application.join_date.strftime('%Y-%m-%d') if application.join_date else '') else: - row.append(getattr(candidate, field, '')) + row.append(getattr(application, field, '')) # Add job information row.extend([job.title, job.department or '']) diff --git a/templates/includes/candidate_modal_body.html b/templates/includes/application_modal_body.html similarity index 75% rename from templates/includes/candidate_modal_body.html rename to templates/includes/application_modal_body.html index d87c54d..3c0b7b7 100644 --- a/templates/includes/candidate_modal_body.html +++ b/templates/includes/application_modal_body.html @@ -2,7 +2,7 @@ {% get_current_language as LANGUAGE_CODE %} {% if LANGUAGE_CODE == 'en' %} -
{{ candidate.job_fit_narrative }}
+{{ application.job_fit_narrative }}
{{ candidate.years_of_experience }} {% trans "years" %}
-{% trans "Recent Role:" %} {{ candidate.most_recent_job_title }}
+{{ application.years_of_experience }} {% trans "years" %}
+{% trans "Recent Role:" %} {{ application.most_recent_job_title }}
{% trans "Soft Skills:" %} {{ candidate.soft_skills_score }}%
+{% trans "Soft Skills:" %} {{ application.soft_skills_score }}%
{% trans "Industry Match:" %} - - {{ candidate.industry_match_score }}% + + {{ application.industry_match_score }}%
{{ candidate.job_fit_narrative_ar }}
+{{ application.job_fit_narrative_ar }}
{{ candidate.years_of_experience }} {% trans "years" %}
-{% trans "Recent Role:" %} {{ candidate.most_recent_job_title_ar }}
+{{ application.years_of_experience }} {% trans "years" %}
+{% trans "Recent Role:" %} {{ application.most_recent_job_title_ar }}
{% trans "Soft Skills:" %} {{ candidate.soft_skills_score }}%
+{% trans "Soft Skills:" %} {{ application.soft_skills_score }}%
{% trans "Industry Match:" %} - - {{ candidate.industry_match_score }}% + + {{ application.industry_match_score }}%
{% trans "Are you sure you want to delete this application for" %} "{{ object.candidate.full_name }}" {% trans "for the job" %} "{{ object.job.title }}"? {% trans "This action cannot be undone." %}
+ {% if object.job %} +{% trans "Job:" %} {{ object.job.title }}
+ {% endif %} + {% if object.candidate %} +{% trans "Candidate:" %} {{ object.candidate.full_name }}
+ {% endif %} +{% trans "Application Date:" %} {{ object.created_at|date:"M d, Y" }}
+