diff --git a/NorahUniversity/settings.py b/NorahUniversity/settings.py index ca20ac3..702acec 100644 --- a/NorahUniversity/settings.py +++ b/NorahUniversity/settings.py @@ -298,7 +298,7 @@ Q_CLUSTER = { "name": "KAAUH_CLUSTER", "workers": 2, "recycle": 500, - "timeout": 60, + "timeout": 120, "max_attempts": 1, "compress": True, "save_limit": 250, diff --git a/recruitment/models.py b/recruitment/models.py index f6b852c..898b0c7 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -18,7 +18,9 @@ from .validators import validate_hash_tags, validate_image_size from django.contrib.auth.models import AbstractUser from django.contrib.contenttypes.fields import GenericForeignKey from django.contrib.contenttypes.models import ContentType - +from django.db.models import F, Value, IntegerField, CharField +from django.db.models.functions import Coalesce, Cast +from django.db.models.fields.json import KeyTransform, KeyTextTransform class CustomUser(AbstractUser): """Custom user model extending AbstractUser""" @@ -370,14 +372,22 @@ class JobPosting(Base): @property def all_candidates(self): - return self.applications.annotate( - sortable_score=Coalesce( - Cast( - "ai_analysis_data__analysis_data__match_score", - output_field=IntegerField(), + # 1. Define the safe JSON extraction and conversion expression + safe_score_expression = Cast( + Coalesce( + # Extract the score explicitly as a text string (KeyTextTransform) + KeyTextTransform( + 'match_score', + KeyTransform('analysis_data_en', 'ai_analysis_data') ), - 0, - ) + Value('0'), # Replace SQL NULL (from missing score) with the string '0' + ), + output_field=IntegerField() # Cast the resulting string ('90' or '0') to an integer + ) + + # 2. Annotate the score using the safe expression + return self.applications.annotate( + sortable_score=safe_score_expression ).order_by("-sortable_score") @property @@ -411,16 +421,8 @@ class JobPosting(Base): # counts @property def all_candidates_count(self): - return ( - self.candidates.annotate( - sortable_score=Cast( - "ai_analysis_data__match_score", output_field=CharField() - ) - ) - .order_by("-sortable_score") - .count() - or 0 - ) + return self.all_candidates.count() + @property def screening_candidates_count(self): @@ -746,88 +748,162 @@ class Application(Base): # ==================================================================== # ✨ PROPERTIES (GETTERS) - Migrated from Candidate # ==================================================================== - @property - def resume_data(self): - return self.ai_analysis_data.get("resume_data", {}) + # @property + # def resume_data(self): + # return self.ai_analysis_data.get("resume_data", {}) + + # @property + # def analysis_data(self): + # return self.ai_analysis_data.get("analysis_data", {}) @property - def analysis_data(self): - return self.ai_analysis_data.get("analysis_data", {}) + def resume_data_en(self): + return self.ai_analysis_data.get("resume_data_en", {}) + + @property + def resume_data_ar(self): + return self.ai_analysis_data.get("resume_data_ar", {}) + + @property + def analysis_data_en(self): + return self.ai_analysis_data.get("analysis_data_en", {}) + @property + def analysis_data_ar(self): + return self.ai_analysis_data.get("analysis_data_ar", {}) + @property def match_score(self) -> int: """1. A score from 0 to 100 representing how well the candidate fits the role.""" - return self.analysis_data.get("match_score", 0) + return self.analysis_data_en.get("match_score", 0) @property def years_of_experience(self) -> float: """4. The total number of years of professional experience as a numerical value.""" - return self.analysis_data.get("years_of_experience", 0.0) + return self.analysis_data_en.get("years_of_experience", 0.0) @property def soft_skills_score(self) -> int: """15. A score (0-100) for inferred non-technical skills.""" - return self.analysis_data.get("soft_skills_score", 0) + return self.analysis_data_en.get("soft_skills_score", 0) @property def industry_match_score(self) -> int: """16. A score (0-100) for the relevance of the candidate's industry experience.""" - return self.analysis_data.get("experience_industry_match", 0) + return self.analysis_data_en.get("experience_industry_match", 0) @property def min_requirements_met(self) -> bool: """14. Boolean (true/false) indicating if all non-negotiable minimum requirements are met.""" - return self.analysis_data.get("min_req_met_bool", False) + return self.analysis_data_en.get("min_req_met_bool", False) @property def screening_stage_rating(self) -> str: """13. A standardized rating (e.g., "A - Highly Qualified", "B - Qualified").""" - return self.analysis_data.get("screening_stage_rating", "N/A") + return self.analysis_data_en.get("screening_stage_rating", "N/A") @property def top_3_keywords(self) -> List[str]: """10. A list of the three most dominant and relevant technical skills or technologies.""" - return self.analysis_data.get("top_3_keywords", []) + return self.analysis_data_en.get("top_3_keywords", []) @property def most_recent_job_title(self) -> str: """8. The candidate's most recent or current professional job title.""" - return self.analysis_data.get("most_recent_job_title", "N/A") + return self.analysis_data_en.get("most_recent_job_title", "N/A") @property def criteria_checklist(self) -> Dict[str, str]: """5 & 6. An object rating the candidate's match for each specific criterion.""" - return self.analysis_data.get("criteria_checklist", {}) + return self.analysis_data_en.get("criteria_checklist", {}) @property def professional_category(self) -> str: """7. The most fitting professional field or category for the individual.""" - return self.analysis_data.get("category", "N/A") + return self.analysis_data_en.get("category", "N/A") @property def language_fluency(self) -> List[Dict[str, str]]: """12. A list of languages and their fluency levels mentioned.""" - return self.analysis_data.get("language_fluency", []) + return self.analysis_data_en.get("language_fluency", []) @property def strengths(self) -> str: """2. A brief summary of why the candidate is a strong fit.""" - return self.analysis_data.get("strengths", "") + return self.analysis_data_en.get("strengths", "") @property def weaknesses(self) -> str: """3. A brief summary of where the candidate falls short or what criteria are missing.""" - return self.analysis_data.get("weaknesses", "") + return self.analysis_data_en.get("weaknesses", "") @property def job_fit_narrative(self) -> str: """11. A single, concise sentence summarizing the core fit.""" - return self.analysis_data.get("job_fit_narrative", "") + return self.analysis_data_en.get("job_fit_narrative", "") @property def recommendation(self) -> str: """9. Provide a detailed final recommendation for the candidate.""" - return self.analysis_data.get("recommendation", "") + return self.analysis_data_en.get("recommendation", "") + + #for arabic + + @property + def min_requirements_met_ar(self) -> bool: + """14. Boolean (true/false) indicating if all non-negotiable minimum requirements are met.""" + return self.analysis_data_ar.get("min_req_met_bool", False) + + @property + def screening_stage_rating_ar(self) -> str: + """13. A standardized rating (e.g., "A - Highly Qualified", "B - Qualified").""" + return self.analysis_data_ar.get("screening_stage_rating", "N/A") + + @property + def top_3_keywords_ar(self) -> List[str]: + """10. A list of the three most dominant and relevant technical skills or technologies.""" + return self.analysis_data_ar.get("top_3_keywords", []) + + @property + def most_recent_job_title_ar(self) -> str: + """8. The candidate's most recent or current professional job title.""" + return self.analysis_data_ar.get("most_recent_job_title", "N/A") + + @property + def criteria_checklist_ar(self) -> Dict[str, str]: + """5 & 6. An object rating the candidate's match for each specific criterion.""" + return self.analysis_data_ar.get("criteria_checklist", {}) + + @property + def professional_category_ar(self) -> str: + """7. The most fitting professional field or category for the individual.""" + return self.analysis_data_ar.get("category", "N/A") + + @property + def language_fluency_ar(self) -> List[Dict[str, str]]: + """12. A list of languages and their fluency levels mentioned.""" + return self.analysis_data_ar.get("language_fluency", []) + + @property + def strengths_ar(self) -> str: + """2. A brief summary of why the candidate is a strong fit.""" + return self.analysis_data_ar.get("strengths", "") + + @property + def weaknesses_ar(self) -> str: + """3. A brief summary of where the candidate falls short or what criteria are missing.""" + return self.analysis_data_ar.get("weaknesses", "") + + @property + def job_fit_narrative_ar(self) -> str: + """11. A single, concise sentence summarizing the core fit.""" + return self.analysis_data_ar.get("job_fit_narrative", "") + + @property + def recommendation_ar(self) -> str: + """9. Provide a detailed final recommendation for the candidate.""" + return self.analysis_data_ar.get("recommendation", "") + # ==================================================================== # 🔄 HELPER METHODS diff --git a/recruitment/score_utils.py b/recruitment/score_utils.py deleted file mode 100644 index a9b457a..0000000 --- a/recruitment/score_utils.py +++ /dev/null @@ -1,36 +0,0 @@ -from django.db.models import Value, IntegerField, CharField, F -from django.db.models.functions import Coalesce, Cast, Replace, NullIf, KeyTextTransform - -# Define the path to the match score -# Based on your tracebacks, the path is: ai_analysis_data -> analysis_data -> match_score -SCORE_PATH_RAW = F('ai_analysis_data__analysis_data__match_score') - -# Define a robust annotation expression for safely extracting and casting the match score. -# This sequence handles three common failure points: -# 1. Missing keys (handled by Coalesce). -# 2. Textual scores (e.g., "N/A" or "") (handled by NullIf). -# 3. Quoted numeric scores (e.g., "50") from JSONB extraction (handled by Replace). -def get_safe_score_annotation(): - """ - Returns a Django Expression object that safely extracts a score from the - JSONField, cleans it, and casts it to an IntegerField. - """ - - # 1. Extract the JSON value as text and force a CharField for cleaning functions - # Using the double-underscore path is equivalent to the KeyTextTransform - # for the final nested key in a PostgreSQL JSONField. - extracted_text = Cast(SCORE_PATH_RAW, output_field=CharField()) - - # 2. Clean up any residual double-quotes that sometimes remain if the data - # was stored as a quoted string (e.g., "50") - cleaned_text = Replace(extracted_text, Value('"'), Value('')) - - # 3. Use NullIf to convert the cleaned text to NULL if it is an empty string - # (or if it was a non-numeric string like "N/A" after quote removal) - null_if_empty = NullIf(cleaned_text, Value('')) - - # 4. Cast the result (which is now either a clean numeric string or NULL) to an IntegerField. - final_cast = Cast(null_if_empty, output_field=IntegerField()) - - # 5. Use Coalesce to ensure NULL scores (from errors or missing data) default to 0. - return Coalesce(final_cast, Value(0)) \ No newline at end of file diff --git a/recruitment/signals.py b/recruitment/signals.py index e567816..8a6559a 100644 --- a/recruitment/signals.py +++ b/recruitment/signals.py @@ -86,7 +86,7 @@ def score_candidate_resume(sender, instance, created, **kwargs): if instance.resume and not instance.is_resume_parsed: logger.info(f"Scoring resume for candidate {instance.pk}") async_task( - "recruitment.tasks.handle_reume_parsing_and_scoring", + "recruitment.tasks.handle_resume_parsing_and_scoring", instance.pk, hook="recruitment.hooks.callback_ai_parsing", ) diff --git a/recruitment/tasks.py b/recruitment/tasks.py index ceab43b..0ec864d 100644 --- a/recruitment/tasks.py +++ b/recruitment/tasks.py @@ -238,14 +238,219 @@ def safe_cast_to_float(value, default=0.0): return default return default -def handle_reume_parsing_and_scoring(pk): +# def handle_resume_parsing_and_scoring(pk): +# """ +# Optimized Django-Q task to parse a resume, score the candidate against a job, +# and atomically save the results. +# """ + +# # --- 1. Robust Object Retrieval (Prevents looping on DoesNotExist) --- +# try: +# instance = Application.objects.get(pk=pk) +# except Application.DoesNotExist: +# # Exit gracefully if the candidate was deleted after the task was queued +# logger.warning(f"Candidate matching query does not exist for pk={pk}. Exiting task.") +# print(f"Candidate matching query does not exist for pk={pk}. Exiting task.") +# return + +# logger.info(f"Scoring resume for candidate {pk}") +# print(f"Scoring resume for candidate {pk}") + +# # --- 2. I/O and Initial Data Check --- +# try: +# file_path = instance.resume.path +# if not os.path.exists(file_path): +# logger.warning(f"Resume file not found: {file_path}") +# print(f"Resume file not found: {file_path}") +# # Consider marking the task as unsuccessful but don't re-queue +# return + +# # Use the new unified document parser +# resume_text = extract_text_from_document(file_path) +# job_detail = f"{instance.job.description} {instance.job.qualifications}" + +# except Exception as e: +# logger.error(f"Error during initial data retrieval/parsing for candidate {instance.pk}: {e}") +# print(f"Error during initial data retrieval/parsing for candidate {instance.pk}: {e}") +# return +# print(resume_text) +# # --- 3. Single, Combined LLM Prompt (Major Cost & Latency Optimization) --- +# prompt = f""" +# You are an expert AI system functioning as both a Resume Parser and a Technical Recruiter. + +# Your task is to: +# 1. **PARSE**: Extract all key-value information from the provided RESUME TEXT into a clean JSON structure under the key 'resume_data', preserving the original text and it's formatting and dont add any extra text. +# 2. **SCORE**: Analyze the parsed data against the JOB CRITERIA and generate a comprehensive score and analysis under the key 'analysis_data'. + +# **JOB CRITERIA:** +# {job_detail} + +# **RESUME TEXT:** +# {resume_text} + +# **STRICT JSON OUTPUT INSTRUCTIONS:** +# Output a single, valid JSON object with ONLY the following two top-level keys: + + +# 1. "resume_data": {{ +# "full_name": "Full name of the candidate", +# "current_title": "Most recent or current job title", +# "location": "City and state", +# "contact": "Phone number and email", +# "linkedin": "LinkedIn profile URL", +# "github": "GitHub or portfolio URL", +# "summary": "Brief professional profile or summary (1–2 sentences)", +# "education": [{{ +# "institution": "Institution name", +# "degree": "Degree name", +# "year": "Year of graduation" (if provided) or '', +# "gpa": "GPA (if provided)", +# "relevant_courses": ["list", "of", "courses"](if provided) or [] +# }}], +# "skills": {{ +# "category_1": ["skill_a", "skill_b"], +# "uncategorized": ["tool_x"] +# }}, +# "experience": [{{ +# "company": "Company name", +# "job_title": "Job Title", +# "location": "Location", +# "start_date": "YYYY-MM", +# "end_date": "YYYY-MM or Present", +# "key_achievements": ["concise bullet points"] (if provided) or [] +# }}], +# "projects": [{{ +# "name": "Project name", +# "year": "Year", +# "technologies_used": ["list", "of", "tech"] (if provided) or [], +# "brief_description": "description" +# }}] +# }} + +# 2. "analysis_data": {{ +# "match_score": "Integer Score 0-100", +# "strengths": "Brief summary of strengths", +# "weaknesses": "Brief summary of weaknesses", +# "years_of_experience": "Total years of experience (float, e.g., 6.5)", +# "criteria_checklist": List of job requirements if any {{ "Python": "Met", "AWS": "Not Met"}} only output the criteria_checklist in one of ('Met','Not Met') don't output any extra text, +# "category": "Most fitting professional field (e.g., Data Science), only output the category name and no other text example ('Software Development', 'correct') , ('Software Development and devops','wrong') ('Software Development / Backend Development','wrong')", +# "most_recent_job_title": "Candidate's most recent job title", +# "recommendation": "Detailed hiring recommendation narrative", +# "top_3_keywords": ["keyword1", "keyword2", "keyword3"], +# "job_fit_narrative": "Single, concise summary sentence", +# "language_fluency": ["language: fluency_level"], +# "screening_stage_rating": "Standardized rating (Highly Qualified, Qualified , Partially Qualified, Not Qualified)", +# "min_req_met_bool": "Boolean (true/false)", +# "soft_skills_score": "Integer Score 0-100 for inferred non-technical skills", +# "experience_industry_match": "Integer Score 0-100 for industry relevance", +# "seniority_level_match": "Integer Score 0-100 for alignment with JD's seniority level", +# "red_flags": ["List of any potential concerns (if any): e.g., 'Employment gap 1 year', 'Frequent job hopping', 'Missing required certification'"], +# "employment_stability_score": "Integer Score 0-100 (Higher is more stable/longer tenure) (if possible)", +# "transferable_skills_narrative": "A brief sentence describing the relevance of non-core experience (if applicable).", +# "cultural_fit_keywords": ["A list of 3-5 keywords extracted from the resume (if possible) (e.g., 'team-player', 'mentored', 'cross-functional')"] +# }} + +# If a top-level key or its required fields are missing, set the field to null, an empty list, or an empty object as appropriate. + +# Output only valid JSON—no markdown, no extra text. +# """ + +# try: +# result = ai_handler(prompt) +# if result['status'] == 'error': +# logger.error(f"AI handler returned error for candidate {instance.pk}") +# print(f"AI handler returned error for candidate {instance.pk}") +# return +# # Ensure the result is parsed as a Python dict (if ai_handler returns a JSON string) +# data = result['data'] + +# if isinstance(data, str): +# data = json.loads(data) +# print(data) + +# # parsed_summary = data.get('parsed_data', {}) +# # scoring_result = data.get('scoring_data', {}) + +# except Exception as e: +# logger.error(f"AI handler failed for candidate {instance.pk}: {e}") +# print(f"AI handler failed for candidate {instance.pk}: {e}") +# return + +# # --- 4. Atomic Database Update (Ensures data integrity) --- +# with transaction.atomic(): + +# # Map JSON keys to model fields with appropriate defaults +# # update_map = { +# # 'match_score': ('match_score', 0), +# # 'years_of_experience': ('years_of_experience', 0.0), +# # 'soft_skills_score': ('soft_skills_score', 0), +# # 'experience_industry_match': ('experience_industry_match', 0), + +# # 'min_req_met_bool': ('min_req_met_bool', False), +# # 'screening_stage_rating': ('screening_stage_rating', 'N/A'), +# # 'most_recent_job_title': ('most_recent_job_title', 'N/A'), +# # 'top_3_keywords': ('top_3_keywords', []), + +# # 'strengths': ('strengths', ''), +# # 'weaknesses': ('weaknesses', ''), +# # 'job_fit_narrative': ('job_fit_narrative', ''), +# # 'recommendation': ('recommendation', ''), + +# # 'criteria_checklist': ('criteria_checklist', {}), +# # 'language_fluency': ('language_fluency', []), +# # 'category': ('category', 'N/A'), +# # } + +# # Apply scoring results to the instance +# # for model_field, (json_key, default_value) in update_map.items(): +# # instance.ai_analysis_data[model_field] = scoring_result.get(json_key, default_value) +# # instance.set_field(model_field, scoring_result.get(json_key, default_value)) +# # instance.set_field("match_score" , int(safe_cast_to_float(scoring_result.get('match_score', 0), default=0))) +# # instance.set_field("years_of_experience" , safe_cast_to_float(scoring_result.get('years_of_experience', 0.0))) +# # instance.set_field("soft_skills_score" , int(safe_cast_to_float(scoring_result.get('soft_skills_score', 0), default=0))) +# # instance.set_field("experience_industry_match" , int(safe_cast_to_float(scoring_result.get('experience_industry_match', 0), default=0))) + +# # # Other Model Fields +# # instance.set_field("min_req_met_bool" , scoring_result.get('min_req_met_bool', False)) +# # instance.set_field("screening_stage_rating" , scoring_result.get('screening_stage_rating', 'N/A')) +# # instance.set_field("category" , scoring_result.get('category', 'N/A')) +# # instance.set_field("most_recent_job_title" , scoring_result.get('most_recent_job_title', 'N/A')) +# # instance.set_field("top_3_keywords" , scoring_result.get('top_3_keywords', [])) +# # instance.set_field("strengths" , scoring_result.get('strengths', '')) +# # instance.set_field("weaknesses" , scoring_result.get('weaknesses', '')) +# # instance.set_field("job_fit_narrative" , scoring_result.get('job_fit_narrative', '')) +# # instance.set_field("recommendation" , scoring_result.get('recommendation', '')) +# # instance.set_field("criteria_checklist" , scoring_result.get('criteria_checklist', {})) +# # instance.set_field("language_fluency" , scoring_result.get('language_fluency', [])) + + +# # 2. Update the Full JSON Field (ai_analysis_data) +# if instance.ai_analysis_data is None: +# instance.ai_analysis_data = {} + +# # Save both structured outputs into the single JSONField for completeness +# instance.ai_analysis_data = data +# # instance.ai_analysis_data['parsed_data'] = parsed_summary +# # instance.ai_analysis_data['scoring_data'] = scoring_result + +# # Apply parsing results +# # instance.parsed_summary = json.dumps(parsed_summary) +# instance.is_resume_parsed = True + +# instance.save(update_fields=['ai_analysis_data', 'is_resume_parsed']) + +# logger.info(f"Successfully scored and saved analysis for candidate {instance.id}") +# print(f"Successfully scored and saved analysis for candidate {instance.id}") + +def handle_resume_parsing_and_scoring(pk: int): """ - Optimized Django-Q task to parse a resume, score the candidate against a job, + Optimized Django-Q task to parse a resume in English and Arabic, score the candidate, and atomically save the results. """ # --- 1. Robust Object Retrieval (Prevents looping on DoesNotExist) --- try: + # NOTE: Replace 'Application.objects.get' with your actual model manager call instance = Application.objects.get(pk=pk) except Application.DoesNotExist: # Exit gracefully if the candidate was deleted after the task was queued @@ -258,11 +463,11 @@ def handle_reume_parsing_and_scoring(pk): # --- 2. I/O and Initial Data Check --- try: + # Assuming instance.resume is a Django FileField file_path = instance.resume.path if not os.path.exists(file_path): logger.warning(f"Resume file not found: {file_path}") print(f"Resume file not found: {file_path}") - # Consider marking the task as unsuccessful but don't re-queue return # Use the new unified document parser @@ -276,11 +481,13 @@ def handle_reume_parsing_and_scoring(pk): print(resume_text) # --- 3. Single, Combined LLM Prompt (Major Cost & Latency Optimization) --- prompt = f""" - You are an expert AI system functioning as both a Resume Parser and a Technical Recruiter. + You are an expert AI system functioning as both a Resume Parser and a Technical Recruiter, capable of multi-language output. Your task is to: - 1. **PARSE**: Extract all key-value information from the provided RESUME TEXT into a clean JSON structure under the key 'resume_data', preserving the original text and it's formatting and dont add any extra text. - 2. **SCORE**: Analyze the parsed data against the JOB CRITERIA and generate a comprehensive score and analysis under the key 'analysis_data'. + 1. **PARSE (English)**: Extract all key-value information from the RESUME TEXT into a clean JSON structure under the key **'resume_data_en'**. + 2. **PARSE (Arabic)**: Translate and output the exact same parsed data structure into Arabic under the key **'resume_data_ar'**. The keys must remain in English, but the values (names, titles, summaries, descriptions) must be in Arabic. + 3. **SCORE (English)**: Analyze the data against the JOB CRITERIA and generate a comprehensive score and analysis under **'analysis_data_en'**, including an English narrative/recommendation. + 4. **SCORE (Arabic)**: Output an identical analysis structure under **'analysis_data_ar'**, but ensure the narrative fields (**recommendation**, **job_fit_narrative**, **strengths**, **weaknesses**, **transferable_skills_narrative**) are translated into Arabic. All numerical and list fields (scores, checklist, keywords) must be identical to the English analysis. **JOB CRITERIA:** {job_detail} @@ -289,10 +496,16 @@ def handle_reume_parsing_and_scoring(pk): {resume_text} **STRICT JSON OUTPUT INSTRUCTIONS:** - Output a single, valid JSON object with ONLY the following two top-level keys: + You MUST output a single, valid JSON object. + This object MUST contain ONLY the following four top-level keys: + 1. "resume_data_en" + 2. "resume_data_ar" + 3. "analysis_data_en" + 4. "analysis_data_ar" + **ABSOLUTELY DO NOT use generic keys like "resume_data" or "analysis_data" at the top level.** - 1. "resume_data": {{ + 1. "resume_data_en": {{ /* English Parsed Data */ "full_name": "Full name of the candidate", "current_title": "Most recent or current job title", "location": "City and state", @@ -327,7 +540,43 @@ def handle_reume_parsing_and_scoring(pk): }}] }} - 2. "analysis_data": {{ + 2. "resume_data_ar": {{ /* Arabic Translated Parsed Data (Keys in English, Values in Arabic) */ + "full_name": "الاسم الكامل للمرشح", + "current_title": "أحدث أو الحالي مسمى وظيفي", + "location": "المدينة والدولة", + "contact": "رقم الهاتف والبريد الإلكتروني", + "linkedin": "رابط ملف LinkedIn الشخصي", + "github": "رابط GitHub أو ملف الأعمال", + "summary": "ملف تعريفي مهني موجز أو ملخص (جملة واحدة أو جملتين)", + "education": [{{ + "institution": "اسم المؤسسة", + "degree": "اسم الدرجة العلمية", + "year": "سنة التخرج (إذا توفرت) أو ''", + "gpa": "المعدل التراكمي (إذا توفر)", + "relevant_courses": ["قائمة", "بالدورات", "ذات", "الصلة"](إذا توفرت) أو [] + }}], + "skills": {{ + "category_1": ["مهارة_أ", "مهارة_ب"], + "uncategorized": ["أداة_س"] + }}, + "experience": [{{ + "company": "اسم الشركة", + "job_title": "المسمى الوظيفي", + "location": "الموقع", + "start_date": "السنة-الشهر (YYYY-MM)", + "end_date": "السنة-الشهر (YYYY-MM) أو Present", + "key_achievements": ["نقاط", "رئيسية", "موجزة", "للإنجازات"] (إذا توفرت) أو [] + }}], + "projects": [{{ + "name": "اسم المشروع", + "year": "السنة", + "technologies_used": ["قائمة", "بالتقنيات", "المستخدمة"] (إذا توفرت) أو [], + "brief_description": "وصف موجز" + }}] + }} + + + 3. "analysis_data_en": {{ /* English Analysis and Narratives */ "match_score": "Integer Score 0-100", "strengths": "Brief summary of strengths", "weaknesses": "Brief summary of weaknesses", @@ -350,26 +599,48 @@ def handle_reume_parsing_and_scoring(pk): "cultural_fit_keywords": ["A list of 3-5 keywords extracted from the resume (if possible) (e.g., 'team-player', 'mentored', 'cross-functional')"] }} + 4. "analysis_data_ar": {{ /* Identical Analysis structure, but with Arabic Translated Narratives */ + "match_score": "Integer Score 0-100", + "strengths": "ملخص موجز لنقاط القوة", + "weaknesses": "ملخص موجز لنقاط الضعف", + "years_of_experience": "Total years of experience (float, e.g., 6.5)", + "criteria_checklist": List of job requirements if any {{ "Python": "Met", "AWS": "Not Met"}} only output the criteria_checklist in one of ('Met','Not Met') don't output any extra text, + "category": "Most fitting professional field (e.g., Data Science), only output the category name and no other text example ('Software Development', 'correct') , ('Software Development and devops','wrong') ('Software Development / Backend Development','wrong')", + "most_recent_job_title": "Candidate's most recent job title", + "recommendation": "سرد تفصيلي بتوصية التوظيف", + "top_3_keywords": ["keyword1", "keyword2", "keyword3"], + "job_fit_narrative": "جملة واحدة موجزة تلخص مدى ملاءمة الوظيفة", + "language_fluency": ["language: fluency_level"], + "screening_stage_rating": "Standardized rating (Highly Qualified, Qualified , Partially Qualified, Not Qualified)", + "min_req_met_bool": "Boolean (true/false)", + "soft_skills_score": "Integer Score 0-100 for inferred non-technical skills", + "experience_industry_match": "Integer Score 0-100 for industry relevance", + "seniority_level_match": "Integer Score 0-100 for alignment with JD's seniority level", + "red_flags": ["List of any potential concerns (if any): e.g., 'Employment gap 1 year', 'Frequent job hopping', 'Missing required certification'"], + "employment_stability_score": "Integer Score 0-100 (Higher is more stable/longer tenure) (if possible)", + "transferable_skills_narrative": "جملة موجزة تصف أهمية الخبرة غير الأساسية (إذا انطبقت).", + "cultural_fit_keywords": ["A list of 3-5 keywords extracted from the resume (if possible) (e.g., 'team-player', 'mentored', 'cross-functional')"] + }} + If a top-level key or its required fields are missing, set the field to null, an empty list, or an empty object as appropriate. Output only valid JSON—no markdown, no extra text. """ try: + # Call the AI handler result = ai_handler(prompt) if result['status'] == 'error': logger.error(f"AI handler returned error for candidate {instance.pk}") print(f"AI handler returned error for candidate {instance.pk}") return - # Ensure the result is parsed as a Python dict (if ai_handler returns a JSON string) + + # Ensure the result is parsed as a Python dict data = result['data'] if isinstance(data, str): data = json.loads(data) print(data) - # parsed_summary = data.get('parsed_data', {}) - # scoring_result = data.get('scoring_data', {}) - except Exception as e: logger.error(f"AI handler failed for candidate {instance.pk}: {e}") print(f"AI handler failed for candidate {instance.pk}: {e}") @@ -377,69 +648,22 @@ def handle_reume_parsing_and_scoring(pk): # --- 4. Atomic Database Update (Ensures data integrity) --- with transaction.atomic(): - - # Map JSON keys to model fields with appropriate defaults - # update_map = { - # 'match_score': ('match_score', 0), - # 'years_of_experience': ('years_of_experience', 0.0), - # 'soft_skills_score': ('soft_skills_score', 0), - # 'experience_industry_match': ('experience_industry_match', 0), - - # 'min_req_met_bool': ('min_req_met_bool', False), - # 'screening_stage_rating': ('screening_stage_rating', 'N/A'), - # 'most_recent_job_title': ('most_recent_job_title', 'N/A'), - # 'top_3_keywords': ('top_3_keywords', []), - - # 'strengths': ('strengths', ''), - # 'weaknesses': ('weaknesses', ''), - # 'job_fit_narrative': ('job_fit_narrative', ''), - # 'recommendation': ('recommendation', ''), - - # 'criteria_checklist': ('criteria_checklist', {}), - # 'language_fluency': ('language_fluency', []), - # 'category': ('category', 'N/A'), - # } - - # Apply scoring results to the instance - # for model_field, (json_key, default_value) in update_map.items(): - # instance.ai_analysis_data[model_field] = scoring_result.get(json_key, default_value) - # instance.set_field(model_field, scoring_result.get(json_key, default_value)) - # instance.set_field("match_score" , int(safe_cast_to_float(scoring_result.get('match_score', 0), default=0))) - # instance.set_field("years_of_experience" , safe_cast_to_float(scoring_result.get('years_of_experience', 0.0))) - # instance.set_field("soft_skills_score" , int(safe_cast_to_float(scoring_result.get('soft_skills_score', 0), default=0))) - # instance.set_field("experience_industry_match" , int(safe_cast_to_float(scoring_result.get('experience_industry_match', 0), default=0))) - - # # Other Model Fields - # instance.set_field("min_req_met_bool" , scoring_result.get('min_req_met_bool', False)) - # instance.set_field("screening_stage_rating" , scoring_result.get('screening_stage_rating', 'N/A')) - # instance.set_field("category" , scoring_result.get('category', 'N/A')) - # instance.set_field("most_recent_job_title" , scoring_result.get('most_recent_job_title', 'N/A')) - # instance.set_field("top_3_keywords" , scoring_result.get('top_3_keywords', [])) - # instance.set_field("strengths" , scoring_result.get('strengths', '')) - # instance.set_field("weaknesses" , scoring_result.get('weaknesses', '')) - # instance.set_field("job_fit_narrative" , scoring_result.get('job_fit_narrative', '')) - # instance.set_field("recommendation" , scoring_result.get('recommendation', '')) - # instance.set_field("criteria_checklist" , scoring_result.get('criteria_checklist', {})) - # instance.set_field("language_fluency" , scoring_result.get('language_fluency', [])) - - # 2. Update the Full JSON Field (ai_analysis_data) if instance.ai_analysis_data is None: instance.ai_analysis_data = {} - # Save both structured outputs into the single JSONField for completeness + # Save all four structured outputs into the single JSONField instance.ai_analysis_data = data - # instance.ai_analysis_data['parsed_data'] = parsed_summary - # instance.ai_analysis_data['scoring_data'] = scoring_result - - # Apply parsing results - # instance.parsed_summary = json.dumps(parsed_summary) instance.is_resume_parsed = True + # Save changes to the database + # NOTE: If you extract individual fields (like match_score) to separate columns, + # ensure those are handled here, using data.get('analysis_data_en', {}).get('match_score'). instance.save(update_fields=['ai_analysis_data', 'is_resume_parsed']) - logger.info(f"Successfully scored and saved analysis for candidate {instance.id}") - print(f"Successfully scored and saved analysis for candidate {instance.id}") + logger.info(f"Successfully scored and saved analysis (EN/AR) for candidate {instance.id}") + print(f"Successfully scored and saved analysis (EN/AR) for candidate {instance.id}") + from django.utils import timezone diff --git a/recruitment/views.py b/recruitment/views.py index b23d527..c9c87e3 100644 --- a/recruitment/views.py +++ b/recruitment/views.py @@ -469,7 +469,10 @@ SCORE_PATH = "ai_analysis_data__analysis_data__match_score" HIGH_POTENTIAL_THRESHOLD = 75 from django.contrib.sites.shortcuts import get_current_site - +from django.db.models.functions import Coalesce, Cast # Coalesce handles NULLs +from django.db.models import Avg, IntegerField, Value # Value is used for the default '0' +# These are essential for safely querying PostgreSQL JSONB fields +from django.db.models.fields.json import KeyTransform, KeyTextTransform @staff_user_required def job_detail(request, slug): """View details of a specific job""" @@ -525,20 +528,35 @@ 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 - - candidates_with_score = applicants.filter(is_resume_parsed=True).annotate( - annotated_match_score=Coalesce(Cast(SCORE_PATH, output_field=IntegerField()), 0) + # Define the queryset for applications that have been parsed + score_expression = Cast( + Coalesce( + KeyTextTransform( + 'match_score', + KeyTransform('analysis_data_en', 'ai_analysis_data') + ), + Value('0'), + ), + output_field=IntegerField() ) - total_candidates = applicants.count() + # 2. ANNOTATE the queryset with the new field + candidates_with_score = candidates_with_score.annotate( + annotated_match_score=score_expression + ) + avg_match_score_result = candidates_with_score.aggregate( - avg_score=Avg("annotated_match_score") - )["avg_score"] - avg_match_score = round(avg_match_score_result or 0, 1) + 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( annotated_match_score__gte=HIGH_POTENTIAL_THRESHOLD ).count() + high_potential_ratio = ( round((high_potential_count / total_candidates) * 100, 1) if total_candidates > 0 @@ -1779,7 +1797,7 @@ def candidate_screening_view(request, slug): gpa = 0 except ValueError: - # This catches if the user enters non-numeric text (e.g., "abc") + # This catches if the user enters non-numeric text (e.g., "abc") min_ai_score = 0 min_experience = 0 tier1_count = 0 @@ -4127,7 +4145,7 @@ def candidate_application_detail(request, slug): ai_analysis = None if application.ai_analysis_data: try: - ai_analysis = application.ai_analysis_data.get('analysis_data', {}) + ai_analysis = application.ai_analysis_data.get('analysis_data_en', {}) except (AttributeError, KeyError): ai_analysis = {} diff --git a/recruitment/views_frontend.py b/recruitment/views_frontend.py index e9c4fb7..3dc8d31 100644 --- a/recruitment/views_frontend.py +++ b/recruitment/views_frontend.py @@ -4,7 +4,7 @@ from datetime import datetime from django.shortcuts import render, get_object_or_404,redirect from django.contrib import messages from django.http import JsonResponse, HttpResponse -from django.db.models.fields.json import KeyTextTransform +from django.db.models.fields.json import KeyTextTransform,KeyTransform from recruitment.utils import json_to_markdown_table from django.db.models import Count, Avg, F, FloatField from django.db.models.functions import Cast @@ -232,7 +232,7 @@ def retry_scoring_view(request,slug): application = get_object_or_404(models.Application, slug=slug) async_task( - 'recruitment.tasks.handle_reume_parsing_and_scoring', + 'recruitment.tasks.handle_resume_parsing_and_scoring', application.pk, hook='recruitment.hooks.callback_ai_parsing', sync=True, @@ -263,7 +263,7 @@ def candidate_detail(request, slug): if request.user.is_staff: stage_form = forms.ApplicationStageForm() - + # 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]) @@ -445,15 +445,25 @@ def dashboard_view(request): total_candidates = candidate_queryset.count() - candidates_with_score_query = candidate_queryset.filter( - is_resume_parsed=True - ).annotate( - annotated_match_score=Coalesce( - Cast(SCORE_PATH, output_field=IntegerField()), - 0 - ) + + score_expression = Cast( + Coalesce( + KeyTextTransform( + 'match_score', + KeyTransform('analysis_data_en', 'ai_analysis_data') + ), + Value('0'), + ), + output_field=IntegerField() ) + # 2. ANNOTATE the queryset with the new field + candidates_with_score_query = candidate_queryset.annotate( + annotated_match_score=score_expression + ) + + + # safe_match_score_cast = Cast( # # 3. If the result after stripping quotes is an empty string (''), convert it to NULL. # NullIf( diff --git a/templates/includes/candidate_modal_body.html b/templates/includes/candidate_modal_body.html index 11d426f..d87c54d 100644 --- a/templates/includes/candidate_modal_body.html +++ b/templates/includes/candidate_modal_body.html @@ -1,123 +1,252 @@ {% load i18n %} -
{% trans "AI Score" %}: {{ candidate.match_score }}% {{ candidate.professional_category }}
+{% get_current_language as LANGUAGE_CODE %} -
-
-
- - {% trans "Job Fit" %} +{% if LANGUAGE_CODE == 'en' %} +
{% trans "AI Score" %}: {{ candidate.match_score }}% {{ candidate.professional_category }}
+ +
+
+
+ + {% trans "Job Fit" %} +
+

{{ candidate.job_fit_narrative }}

+
+
+
+ + {% trans "Top Keywords" %} +
+
+ {% for keyword in candidate.top_3_keywords %} + {{ keyword }} + {% endfor %} +
-

{{ candidate.job_fit_narrative }}

-
-
- - {% trans "Top Keywords" %} + +
+
+
+ + {% trans "Experience" %} +
+

{{ candidate.years_of_experience }} {% trans "years" %}

+

{% trans "Recent Role:" %} {{ candidate.most_recent_job_title }}

-
- {% for keyword in candidate.top_3_keywords %} - {{ keyword }} +
+
+ + {% trans "Skills" %} +
+

{% trans "Soft Skills:" %} {{ candidate.soft_skills_score }}%

+

{% trans "Industry Match:" %} + + {{ candidate.industry_match_score }}% + +

+
+
+ +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + + + + + + + + {% for criterion, status in candidate.criteria_checklist.items %} + + + + + {% endfor %} + +
{% trans "Criteria" %}{% trans "Status" %}
{{ criterion }} + {% if status == "Met" %} + {% trans "Met" %} + {% elif status == "Not Met" %} + {% trans "Not Met" %} + {% else %} + {{ status }} + {% endif %} +
+
+
+ +
+
+
+ + {% trans "Minimum Requirements" %} +
+ {% if candidate.min_requirements_met %} + {% trans "Met" %} + {% else %} + {% trans "Not Met" %} + {% endif %} +
+
+
+ + {% trans "Screening Rating" %} +
+ {{ candidate.screening_stage_rating }} +
+
+ + {% if candidate.language_fluency %} +
+ +
+ {% for language in candidate.language_fluency %} + {{ language }} {% endfor %}
-
+ {% endif %} -
-
-
- - {% trans "Experience" %} +{% else %} +
{% trans "AI Score" %}: {{ candidate.match_score }}% {{ candidate.professional_category_ar }}
+ +
+
+
+ + {% trans "Job Fit" %} +
+

{{ candidate.job_fit_narrative_ar }}

-

{{ candidate.years_of_experience }} {% trans "years" %}

-

{% trans "Recent Role:" %} {{ candidate.most_recent_job_title }}

-
-
-
- - {% trans "Skills" %} -
-

{% trans "Soft Skills:" %} {{ candidate.soft_skills_score }}%

-

{% trans "Industry Match:" %} - - {{ candidate.industry_match_score }}% - -

-
-
- -
- - -
- -
- - -
- -
- - -
- -
- -
- - - - - - - - - {% for criterion, status in candidate.criteria_checklist.items %} - - - - +
+
+ + {% trans "Top Keywords" %} +
+
+ {% for keyword in candidate.top_3_keywords_ar %} + {{ keyword }} {% endfor %} -
-
{% trans "Criteria" %}{% trans "Status" %}
{{ criterion }} - {% if status == "Met" %} - {% trans "Met" %} - {% elif status == "Not Met" %} - {% trans "Not Met" %} - {% else %} - {{ status }} - {% endif %} -
-
-
- -
-
-
- - {% trans "Minimum Requirements" %} +
- {% if candidate.min_requirements_met %} - {% trans "Met" %} - {% else %} - {% trans "Not Met" %} - {% endif %}
-
-
- - {% trans "Screening Rating" %} -
- {{ candidate.screening_stage_rating }} -
-
-{% if candidate.language_fluency %} -
- -
- {% for language in candidate.language_fluency %} - {{ language }} - {% endfor %} +
+
+
+ + {% trans "Experience" %} +
+

{{ candidate.years_of_experience }} {% trans "years" %}

+

{% trans "Recent Role:" %} {{ candidate.most_recent_job_title_ar }}

+
+
+
+ + {% trans "Skills" %} +
+

{% trans "Soft Skills:" %} {{ candidate.soft_skills_score }}%

+

{% trans "Industry Match:" %} + + {{ candidate.industry_match_score }}% + +

+
-
-{% endif %} + +
+ + +
+ +
+ + +
+ +
+ + +
+ +
+ +
+ + + + + + + + + {% for criterion, status in candidate.criteria_checklist_ar.items %} + + + + + {% endfor %} + +
{% trans "Criteria" %}{% trans "Status" %}
{{ criterion }} + {% if status == "Met" %} + {% trans "Met" %} + {% elif status == "Not Met" %} + {% trans "Not Met" %} + {% else %} + {{ status }} + {% endif %} +
+
+
+ +
+
+
+ + {% trans "Minimum Requirements" %} +
+ {% if candidate.min_requirements_met_ar %} + {% trans "Met" %} + {% else %} + {% trans "Not Met" %} + {% endif %} +
+
+
+ + {% trans "Screening Rating" %} +
+ {{ candidate.screening_stage_rating_ar }} +
+
+ + {% if candidate.language_fluency_ar %} +
+ +
+ {% for language in candidate.language_fluency_ar %} + {{ language }} + {% endfor %} +
+
+ {% endif %} + +{% endif %} \ No newline at end of file diff --git a/templates/recruitment/candidate_detail.html b/templates/recruitment/candidate_detail.html index fcd6d5f..ff6be2f 100644 --- a/templates/recruitment/candidate_detail.html +++ b/templates/recruitment/candidate_detail.html @@ -480,6 +480,7 @@ {# TAB 5 CONTENT: PARSED SUMMARY #} {% if candidate.parsed_summary %} +
{% trans "AI Generated Summary" %}
diff --git a/templates/recruitment/candidate_document_review_view.html b/templates/recruitment/candidate_document_review_view.html index 3295c7f..2e3edc1 100644 --- a/templates/recruitment/candidate_document_review_view.html +++ b/templates/recruitment/candidate_document_review_view.html @@ -211,11 +211,11 @@
- {% trans "Export CSV" %} - + {% endcomment %} {% trans "Back to Job" %} diff --git a/templates/recruitment/candidate_interview_view.html b/templates/recruitment/candidate_interview_view.html index ccebff7..b84ce8b 100644 --- a/templates/recruitment/candidate_interview_view.html +++ b/templates/recruitment/candidate_interview_view.html @@ -293,9 +293,7 @@ title="View Profile"> {{ candidate.name }} - {% comment %}
- {{ candidate.name }} -
{% endcomment %} +
diff --git a/templates/recruitment/candidate_resume_template.html b/templates/recruitment/candidate_resume_template.html index 2dd63f7..8ee3b9b 100644 --- a/templates/recruitment/candidate_resume_template.html +++ b/templates/recruitment/candidate_resume_template.html @@ -4,7 +4,13 @@ - {{ candidate.resume_data.full_name|default:"Candidate" }} - Candidate Profile + {% get_current_language as LANGUAGE_CODE %} + + {% if LANGUAGE_CODE == 'ar' %} + {{ candidate.resume_data_ar.full_name|default:"Candidate" }} - Candidate Profile + {% else %} + {{ candidate.resume_data_en.full_name|default:"Candidate" }} - Candidate Profile + {% endif %} @@ -519,7 +525,8 @@ -
+ + {% comment %}
{% include 'recruitment/partials/ai_overview_breadcromb.html' %} @@ -902,7 +909,712 @@ {% endif %}
+
{% endcomment %} + + + +{% if LANGUAGE_CODE == 'ar' %} + {% with data_source=candidate.resume_data_ar analysis_source=candidate.analysis_data_ar %} +
+ + {% include 'recruitment/partials/ai_overview_breadcromb.html' %} + +
+
+

{{ data_source.full_name|default:"اسم المرشح" }}

+

{{ data_source.current_title|default:"المسمى الوظيفي" }}

+
+
+ {{ data_source.location|default:"الموقع" }} + +
+
+ {{ data_source.contact|default:"معلومات الاتصال" }} + +
+ {% if data_source.linkedin %} +
+ +
+ {% endif %} + {% if data_source.github %} +
+ +
+ {% endif %} +
+
+
+
{{ analysis_source.match_score|default:0 }}%
+
{% trans "Match Score" %}
+
+ {{ analysis_source.screening_stage_rating|default:"التقييم" }} +
+
+
+ +
+
+
+

+ {% trans "Analysis" %} + +

+ + {% if analysis_source.category %} +
+ {% trans "Target Role Category:" %} + {{ analysis_source.category }} +
+ {% endif %} + + {% if analysis_source.red_flags %} +
+

{% trans "Red Flags" %}

+

{{ analysis_source.red_flags|join:". "|default:"لا يوجد." }}

+
+ {% endif %} + + {% if analysis_source.strengths %} +
+

{% trans "Strengths" %}

+

{{ analysis_source.strengths }}

+
+ {% endif %} + + {% if analysis_source.weaknesses %} +
+

{% trans "Weaknesses" %}

+

{{ analysis_source.weaknesses }}

+
+ {% endif %} + + {% if analysis_source.recommendation %} +
+

{% trans "Recommendation" %}

+

{{ analysis_source.recommendation }}

+
+ {% endif %} +
+ + {% if analysis_source.criteria_checklist %} +
+

+ {% trans "Required Criteria Check" %} + +

+
+ {% for criterion, status in analysis_source.criteria_checklist.items %} +
+ {{ criterion }} + + {% if status == 'Met' %} {% trans "Met" %} + {% elif status == 'Not Mentioned' %} {% trans "Not Mentioned" %} + {% else %} {{ status }} + {% endif %} + +
+ {% endfor %} +
+
+ {% endif %} + +
+

+ {% trans "Skills" %} + +

+ {% if data_source.skills %} + {% for category, skills in data_source.skills.items %} +
+

{{ category|cut:"_"|title }}

+
+ {% for skill in skills %} + {{ skill }} + {% endfor %} +
+
+ {% endfor %} + {% else %} +

{% trans "Skills information not available." %}

+ {% endif %} +
+ +
+

+ {% trans "Languages" %} + +

+ {% if analysis_source.language_fluency %} + {% for language in analysis_source.language_fluency %} +
+
+ {{ language }} +
+
+ {% with fluency_check=language|lower %} +
+
+ {% endwith %} +
+
+ {% endfor %} + {% else %} +

{% trans "Language information not available." %}

+ {% endif %} +
+ +
+

+ {% trans "Key Metrics" %} + +

+
+ {% if analysis_source.min_req_met_bool is not none %} +
+ {% trans "Min Requirements Met:" %} + + {% if analysis_source.min_req_met_bool %} {% trans "Yes" %}{% else %} {% trans "No" %}{% endif %} + +
+ {% endif %} + + {% if analysis_source.years_of_experience is not none %} +
+ {% trans "Total Experience:" %} + {{ analysis_source.years_of_experience|floatformat:1 }} {% trans "years" %} +
+ {% endif %} + + {% if analysis_source.most_recent_job_title %} +
+ {% trans "Most Recent Title (Scoring):" %} + {{ analysis_source.most_recent_job_title }} +
+ {% endif %} + + {% if analysis_source.seniority_level_match is not none %} +
+ {% trans "Seniority Match:" %} + {{ analysis_source.seniority_level_match|default:0 }}/100 +
+ {% endif %} + + {% if analysis_source.soft_skills_score is not none %} +
+ {% trans "Soft Skills Score:" %} + {{ analysis_source.soft_skills_score|default:0 }}/100 +
+ {% endif %} + + {% if analysis_source.employment_stability_score is not none %} +
+ {% trans "Stability Score:" %} + {{ analysis_source.employment_stability_score|default:0 }}/100 +
+ {% endif %} + + {% if analysis_source.experience_industry_match is not none %} +
+ {% trans "Industry Match:" %} + {{ analysis_source.experience_industry_match|default:0 }}/100 +
+ {% endif %} +
+ {% if analysis_source.transferable_skills_narrative %} +
+ {% trans "Transferable Skills:" %} + {{ analysis_source.transferable_skills_narrative }} +
+ {% endif %} +
+
+ +
+ +
+

+ {% trans "Summary" %} + +

+

+ {{ data_source.summary|default:"الملخص المهني غير متوفر." }} +

+
+ +
+

+ {% trans "Experience" %} + +

+ {% for experience in data_source.experience %} +
+
+
+

{{ experience.job_title }}

+

{{ experience.company }}

+
+ + {% if experience.end_date == "Present" %}{% trans "Present" %}{% else %}{{ experience.end_date|default:"حالي" }}{% endif %} + +
+

+ {% if experience.end_date and experience.end_date != "Present" %}{{ experience.end_date }}{% else %}{% trans "Present" %}{% endif %} + - {% if experience.start_date %}{{ experience.start_date }}{% endif %} + + {% if experience.location %}{{ experience.location }}{% endif %} +

+ {% if experience.key_achievements %} +
    + {% for achievement in experience.key_achievements %} +
  • {{ achievement }}
  • + {% endfor %} +
+ {% endif %} +
+ {% endfor %} +
+ +
+

+ {% trans "Education" %} + +

+ {% for education in data_source.education %} +
+
+ +
+
+

{{ education.degree }}

+

{{ education.institution }}

+ {% if education.year %} +

{{ education.year }}

+ {% endif %} + {% if education.gpa %} +

{% trans "GPA" %}: {{ education.gpa }}

+ {% endif %} + {% if education.relevant_courses %} +

{% trans "Courses" %}: {{ education.relevant_courses|join:", " }}

+ {% endif %} +
+
+ {% endfor %} +
+ +
+

+ {% trans "Projects" %} + +

+ {% for project in data_source.projects %} +
+

{{ project.name }}

+

{{ project.brief_description }}

+ {% if project.technologies_used %} +
+ {% for tech in project.technologies_used %} + {{ tech }} + {% endfor %} +
+ {% endif %} +
+ {% endfor %} + {% if not data_source.projects %} +

{% trans "No projects detailed in the resume." %}

+ {% endif %} +
+ + {% if analysis_source.top_3_keywords or analysis_source.cultural_fit_keywords %} +
+

+ {% trans "Keywords" %} + +

+ + {% if analysis_source.top_3_keywords %} +
+

{% trans "Top Keywords (Job Match)" %}

+
+ {% for keyword in analysis_source.top_3_keywords %} + {{ keyword }} + {% endfor %} +
+
+ {% endif %} + + {% if analysis_source.cultural_fit_keywords %} +
+

{% trans "Cultural Fit Keywords" %}

+
+ {% for keyword in analysis_source.cultural_fit_keywords %} + {{ keyword }} + {% endfor %} +
+
+ {% endif %} +
+ {% endif %} +
+
+ {% endwith %} + +{% else %} + {% with data_source=candidate.resume_data_en analysis_source=candidate.analysis_data_en %} +
+ + {% include 'recruitment/partials/ai_overview_breadcromb.html' %} +
+
+

{{ data_source.full_name|default:"Candidate Name" }}

+

{{ data_source.current_title|default:"Professional Title" }}

+
+
+ + {{ data_source.location|default:"Location" }} +
+
+ + {{ data_source.contact|default:"Contact Information" }} +
+ {% if data_source.linkedin %} +
+ + +
+ {% endif %} + {% if data_source.github %} +
+ + +
+ {% endif %} +
+
+
+
{{ analysis_source.match_score|default:0 }}%
+
{% trans "Match Score" %}
+
+ {{ analysis_source.screening_stage_rating|default:"Assessment" }} +
+
+
+ +
+
+ +
+

+ + {% trans "Summary" %} +

+

+ {{ data_source.summary|default:"Professional summary not available." }} +

+
+ +
+

+ + {% trans "Experience" %} +

+ {% for experience in data_source.experience %} +
+
+
+

{{ experience.job_title }}

+

{{ experience.company }}

+
+ + {% if experience.end_date == "Present" %}{% trans "Present" %}{% else %}{{ experience.end_date|default:"Current" }}{% endif %} + +
+

+ + {% if experience.start_date %}{{ experience.start_date }}{% endif %} - + {% if experience.end_date and experience.end_date != "Present" %}{{ experience.end_date }}{% else %}{% trans "Present" %}{% endif %} + {% if experience.location %}{{ experience.location }}{% endif %} +

+ {% if experience.key_achievements %} +
    + {% for achievement in experience.key_achievements %} +
  • {{ achievement }}
  • + {% endfor %} +
+ {% endif %} +
+ {% endfor %} +
+ +
+

+ + {% trans "Education" %} +

+ {% for education in data_source.education %} +
+
+ +
+
+

{{ education.degree }}

+

{{ education.institution }}

+ {% if education.year %} +

{{ education.year }}

+ {% endif %} + {% if education.gpa %} +

{% trans "GPA" %}: {{ education.gpa }}

+ {% endif %} + {% if education.relevant_courses %} +

{% trans "Courses" %}: {{ education.relevant_courses|join:", " }}

+ {% endif %} +
+
+ {% endfor %} +
+ +
+

+ + {% trans "Projects" %} +

+ {% for project in data_source.projects %} +
+

{{ project.name }}

+

{{ project.brief_description }}

+ {% if project.technologies_used %} +
+ {% for tech in project.technologies_used %} + {{ tech }} + {% endfor %} +
+ {% endif %} +
+ {% endfor %} + {% if not data_source.projects %} +

{% trans "No projects detailed in the resume." %}

+ {% endif %} +
+ + {% if analysis_source.top_3_keywords or analysis_source.cultural_fit_keywords %} +
+

+ + {% trans "Keywords" %} +

+ + {% if analysis_source.top_3_keywords %} +
+

{% trans "Top Keywords (Job Match)" %}

+
+ {% for keyword in analysis_source.top_3_keywords %} + {{ keyword }} + {% endfor %} +
+
+ {% endif %} + + {% if analysis_source.cultural_fit_keywords %} +
+

{% trans "Cultural Fit Keywords" %}

+
+ {% for keyword in analysis_source.cultural_fit_keywords %} + {{ keyword }} + {% endfor %} +
+
+ {% endif %} +
+ {% endif %} +
+ +
+
+

+ + {% trans "Analysis" %} +

+ + {% if analysis_source.category %} +
+ {% trans "Target Role Category:" %} + {{ analysis_source.category }} +
+ {% endif %} + + + {% if analysis_source.red_flags %} +
+

{% trans "Red Flags" %}

+

{{ analysis_source.red_flags|join:". "|default:"None." }}

+
+ {% endif %} + + {% if analysis_source.strengths %} +
+

{% trans "Strengths" %}

+

{{ analysis_source.strengths }}

+
+ {% endif %} + + {% if analysis_source.weaknesses %} +
+

{% trans "Weaknesses" %}

+

{{ analysis_source.weaknesses }}

+
+ {% endif %} + + {% if analysis_source.recommendation %} +
+

{% trans "Recommendation" %}

+

{{ analysis_source.recommendation }}

+
+ {% endif %} +
+ + {% if analysis_source.criteria_checklist %} +
+

+ + {% trans "Required Criteria Check" %} +

+
+ {% for criterion, status in analysis_source.criteria_checklist.items %} +
+ {{ criterion }} + + {% if status == 'Met' %} {% trans "Met" %} + {% elif status == 'Not Mentioned' %} {% trans "Not Mentioned" %} + {% else %} {{ status }} + {% endif %} + +
+ {% endfor %} +
+
+ {% endif %} + +
+

+ + {% trans "Skills" %} +

+ {% if data_source.skills %} + {% for category, skills in data_source.skills.items %} +
+

{{ category|cut:"_"|title }}

+
+ {% for skill in skills %} + {{ skill }} + {% endfor %} +
+
+ {% endfor %} + {% else %} +

{% trans "Skills information not available." %}

+ {% endif %} +
+ +
+

+ + {% trans "Languages" %} +

+ {% if analysis_source.language_fluency %} + {% for language in analysis_source.language_fluency %} +
+
+ {{ language }} +
+
+ {% with fluency_check=language|lower %} +
+
+ {% endwith %} +
+
+ {% endfor %} + {% else %} +

{% trans "Language information not available." %}

+ {% endif %} +
+ + +
+

+ + {% trans "Key Metrics" %} +

+
+ {% if analysis_source.min_req_met_bool is not none %} +
+ {% trans "Min Requirements Met:" %} + + {% if analysis_source.min_req_met_bool %} {% trans "Yes" %}{% else %} {% trans "No" %}{% endif %} + +
+ {% endif %} + + {% if analysis_source.years_of_experience is not none %} +
+ {% trans "Total Experience:" %} + {{ analysis_source.years_of_experience|floatformat:1 }} {% trans "years" %} +
+ {% endif %} + + {% if analysis_source.most_recent_job_title %} +
+ {% trans "Most Recent Title (Scoring):" %} + {{ analysis_source.most_recent_job_title }} +
+ {% endif %} + + {% if analysis_source.seniority_level_match is not none %} +
+ {% trans "Seniority Match:" %} + {{ analysis_source.seniority_level_match|default:0 }}/100 +
+ {% endif %} + + {% if analysis_source.soft_skills_score is not none %} +
+ {% trans "Soft Skills Score:" %} + {{ analysis_source.soft_skills_score|default:0 }}/100 +
+ {% endif %} + + {% if analysis_source.employment_stability_score is not none %} +
+ {% trans "Stability Score:" %} + {{ analysis_source.employment_stability_score|default:0 }}/100 +
+ {% endif %} + + {% if analysis_source.experience_industry_match is not none %} +
+ {% trans "Industry Match:" %} + {{ analysis_source.experience_industry_match|default:0 }}/100 +
+ {% endif %} +
+ {% if analysis_source.transferable_skills_narrative %} +
+ {% trans "Transferable Skills:" %} + {{ analysis_source.transferable_skills_narrative }} +
+ {% endif %} +
+
+
+
+ {% endwith %} + +{% endif %}