first commit regarding the ai_analysis to support en and ar in branch ai_overview

This commit is contained in:
Faheed 2025-11-24 19:03:33 +03:00
parent fe5fd7424d
commit 8bc3747afe
13 changed files with 1416 additions and 279 deletions

View File

@ -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,

View File

@ -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

View File

@ -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))

View File

@ -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",
)

View File

@ -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 (12 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 JSONno 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

View File

@ -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 = {}

View File

@ -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(

View File

@ -1,123 +1,252 @@
{% load i18n %}
<h5> {% trans "AI Score" %}: <span class="badge bg-success"><i class="fas fa-robot me-1"></i> {{ candidate.match_score }}%</span> <span class="badge bg-success"><i class="fas fa-graduation-cap me-1"></i> {{ candidate.professional_category }} </span></h5>
{% get_current_language as LANGUAGE_CODE %}
<div class="row mb-3">
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-briefcase me-2 text-primary"></i>
<small class="text-muted">{% trans "Job Fit" %}</small>
{% if LANGUAGE_CODE == 'en' %}
<h5> {% trans "AI Score" %}: <span class="badge bg-success"><i class="fas fa-robot me-1"></i> {{ candidate.match_score }}%</span> <span class="badge bg-success"><i class="fas fa-graduation-cap me-1"></i> {{ candidate.professional_category }} </span></h5>
<div class="row mb-3">
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-briefcase me-2 text-primary"></i>
<small class="text-muted">{% trans "Job Fit" %}</small>
</div>
<p class="mb-1">{{ candidate.job_fit_narrative }}</p>
</div>
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-star me-2 text-warning"></i>
<small class="text-muted">{% trans "Top Keywords" %}</small>
</div>
<div class="d-flex flex-wrap gap-1">
{% for keyword in candidate.top_3_keywords %}
<span class="badge bg-info text-dark me-1">{{ keyword }}</span>
{% endfor %}
</div>
</div>
<p class="mb-1">{{ candidate.job_fit_narrative }}</p>
</div>
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-star me-2 text-warning"></i>
<small class="text-muted">{% trans "Top Keywords" %}</small>
<div class="row mb-3">
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-clock me-2 text-info"></i>
<small class="text-muted">{% trans "Experience" %}</small>
</div>
<p class="mb-1"><strong>{{ candidate.years_of_experience }}</strong> {% trans "years" %}</p>
<p class="mb-0"><strong>{% trans "Recent Role:" %}</strong> {{ candidate.most_recent_job_title }}</p>
</div>
<div class="d-flex flex-wrap gap-1">
{% for keyword in candidate.top_3_keywords %}
<span class="badge bg-info text-dark me-1">{{ keyword }}</span>
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-chart-line me-2 text-success"></i>
<small class="text-muted">{% trans "Skills" %}</small>
</div>
<p class="mb-1"><strong>{% trans "Soft Skills:" %}</strong> {{ candidate.soft_skills_score }}%</p>
<p class="mb-0"><strong>{% trans "Industry Match:" %}</strong>
<span class="badge {% if candidate.industry_match_score >= 70 %}bg-success{% elif candidate.industry_match_score >= 40 %}bg-warning{% else %}bg-danger{% endif %}">
{{ candidate.industry_match_score }}%
</span>
</p>
</div>
</div>
<div class="mb-3">
<label class="form-label"><i class="fas fa-comment me-1 text-info"></i> {% trans "Recommendation" %}</label>
<textarea class="form-control" rows="6" readonly>{{ candidate.recommendation }}</textarea>
</div>
<div class="mb-3">
<label class="form-label"><i class="fas fa-thumbs-up me-1 text-success"></i> {% trans "Strengths" %}</label>
<textarea class="form-control" rows="4" readonly>{{ candidate.strengths }}</textarea>
</div>
<div class="mb-3">
<label class="form-label"><i class="fas fa-thumbs-down me-1 text-danger"></i> {% trans "Weaknesses" %}</label>
<textarea class="form-control" rows="4" readonly>{{ candidate.weaknesses }}</textarea>
</div>
<div class="mb-3">
<label class="form-label"><i class="fas fa-list-check me-1"></i> {% trans "Criteria Assessment" %}</label>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>{% trans "Criteria" %}</th>
<th>{% trans "Status" %}</th>
</tr>
</thead>
<tbody>
{% for criterion, status in candidate.criteria_checklist.items %}
<tr>
<td>{{ criterion }}</td>
<td>
{% if status == "Met" %}
<span class="badge bg-success"><i class="fas fa-check me-1"></i> {% trans "Met" %}</span>
{% elif status == "Not Met" %}
<span class="badge bg-danger"><i class="fas fa-times me-1"></i> {% trans "Not Met" %}</span>
{% else %}
<span class="badge bg-secondary">{{ status }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-check-circle me-2 text-success"></i>
<small class="text-muted">{% trans "Minimum Requirements" %}</small>
</div>
{% if candidate.min_requirements_met %}
<span class="badge bg-success">{% trans "Met" %}</span>
{% else %}
<span class="badge bg-danger">{% trans "Not Met" %}</span>
{% endif %}
</div>
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-star me-2 text-warning"></i>
<small class="text-muted">{% trans "Screening Rating" %}</small>
</div>
<span class="badge bg-secondary">{{ candidate.screening_stage_rating }}</span>
</div>
</div>
{% if candidate.language_fluency %}
<div class="mb-3">
<label class="form-label"><i class="fas fa-language me-1 text-info"></i> {% trans "Language Fluency" %}</label>
<div class="d-flex flex-wrap gap-2">
{% for language in candidate.language_fluency %}
<span class="badge bg-light text-dark">{{ language }}</span>
{% endfor %}
</div>
</div>
</div>
{% endif %}
<div class="row mb-3">
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-clock me-2 text-info"></i>
<small class="text-muted">{% trans "Experience" %}</small>
{% else %}
<h5> {% trans "AI Score" %}: <span class="badge bg-success"><i class="fas fa-robot me-1"></i> {{ candidate.match_score }}%</span> <span class="badge bg-success"><i class="fas fa-graduation-cap me-1"></i> {{ candidate.professional_category_ar }} </span></h5>
<div class="row mb-3">
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-briefcase me-2 text-primary"></i>
<small class="text-muted">{% trans "Job Fit" %}</small>
</div>
<p class="mb-1">{{ candidate.job_fit_narrative_ar }}</p>
</div>
<p class="mb-1"><strong>{{ candidate.years_of_experience }}</strong> {% trans "years" %}</p>
<p class="mb-0"><strong>{% trans "Recent Role:" %}</strong> {{ candidate.most_recent_job_title }}</p>
</div>
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-chart-line me-2 text-success"></i>
<small class="text-muted">{% trans "Skills" %}</small>
</div>
<p class="mb-1"><strong>{% trans "Soft Skills:" %}</strong> {{ candidate.soft_skills_score }}%</p>
<p class="mb-0"><strong>{% trans "Industry Match:" %}</strong>
<span class="badge {% if candidate.industry_match_score >= 70 %}bg-success{% elif candidate.industry_match_score >= 40 %}bg-warning{% else %}bg-danger{% endif %}">
{{ candidate.industry_match_score }}%
</span>
</p>
</div>
</div>
<div class="mb-3">
<label class="form-label"><i class="fas fa-comment me-1 text-info"></i> {% trans "Recommendation" %}</label>
<textarea class="form-control" rows="6" readonly>{{ candidate.recommendation }}</textarea>
</div>
<div class="mb-3">
<label class="form-label"><i class="fas fa-thumbs-up me-1 text-success"></i> {% trans "Strengths" %}</label>
<textarea class="form-control" rows="4" readonly>{{ candidate.strengths }}</textarea>
</div>
<div class="mb-3">
<label class="form-label"><i class="fas fa-thumbs-down me-1 text-danger"></i> {% trans "Weaknesses" %}</label>
<textarea class="form-control" rows="4" readonly>{{ candidate.weaknesses }}</textarea>
</div>
<div class="mb-3">
<label class="form-label"><i class="fas fa-list-check me-1"></i> {% trans "Criteria Assessment" %}</label>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>{% trans "Criteria" %}</th>
<th>{% trans "Status" %}</th>
</tr>
</thead>
<tbody>
{% for criterion, status in candidate.criteria_checklist.items %}
<tr>
<td>{{ criterion }}</td>
<td>
{% if status == "Met" %}
<span class="badge bg-success"><i class="fas fa-check me-1"></i> {% trans "Met" %}</span>
{% elif status == "Not Met" %}
<span class="badge bg-danger"><i class="fas fa-times me-1"></i> {% trans "Not Met" %}</span>
{% else %}
<span class="badge bg-secondary">{{ status }}</span>
{% endif %}
</td>
</tr>
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-star me-2 text-warning"></i>
<small class="text-muted">{% trans "Top Keywords" %}</small>
</div>
<div class="d-flex flex-wrap gap-1">
{% for keyword in candidate.top_3_keywords_ar %}
<span class="badge bg-info text-dark me-1">{{ keyword }}</span>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-check-circle me-2 text-success"></i>
<small class="text-muted">{% trans "Minimum Requirements" %}</small>
</div>
</div>
{% if candidate.min_requirements_met %}
<span class="badge bg-success">{% trans "Met" %}</span>
{% else %}
<span class="badge bg-danger">{% trans "Not Met" %}</span>
{% endif %}
</div>
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-star me-2 text-warning"></i>
<small class="text-muted">{% trans "Screening Rating" %}</small>
</div>
<span class="badge bg-secondary">{{ candidate.screening_stage_rating }}</span>
</div>
</div>
{% if candidate.language_fluency %}
<div class="mb-3">
<label class="form-label"><i class="fas fa-language me-1 text-info"></i> {% trans "Language Fluency" %}</label>
<div class="d-flex flex-wrap gap-2">
{% for language in candidate.language_fluency %}
<span class="badge bg-light text-dark">{{ language }}</span>
{% endfor %}
<div class="row mb-3">
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-clock me-2 text-info"></i>
<small class="text-muted">{% trans "Experience" %}</small>
</div>
<p class="mb-1"><strong>{{ candidate.years_of_experience }}</strong> {% trans "years" %}</p>
<p class="mb-0"><strong>{% trans "Recent Role:" %}</strong> {{ candidate.most_recent_job_title_ar }}</p>
</div>
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-chart-line me-2 text-success"></i>
<small class="text-muted">{% trans "Skills" %}</small>
</div>
<p class="mb-1"><strong>{% trans "Soft Skills:" %}</strong> {{ candidate.soft_skills_score }}%</p>
<p class="mb-0"><strong>{% trans "Industry Match:" %}</strong>
<span class="badge {% if candidate.industry_match_score >= 70 %}bg-success{% elif candidate.industry_match_score >= 40 %}bg-warning{% else %}bg-danger{% endif %}">
{{ candidate.industry_match_score }}%
</span>
</p>
</div>
</div>
</div>
{% endif %}
<div class="mb-3">
<label class="form-label"><i class="fas fa-comment me-1 text-info"></i> {% trans "Recommendation" %}</label>
<textarea class="form-control" rows="6" readonly>{{ candidate.recommendation_ar }}</textarea>
</div>
<div class="mb-3">
<label class="form-label"><i class="fas fa-thumbs-up me-1 text-success"></i> {% trans "Strengths" %}</label>
<textarea class="form-control" rows="4" readonly>{{ candidate.strengths_ar }}</textarea>
</div>
<div class="mb-3">
<label class="form-label"><i class="fas fa-thumbs-down me-1 text-danger"></i> {% trans "Weaknesses" %}</label>
<textarea class="form-control" rows="4" readonly>{{ candidate.weaknesses_ar }}</textarea>
</div>
<div class="mb-3">
<label class="form-label"><i class="fas fa-list-check me-1"></i> {% trans "Criteria Assessment" %}</label>
<div class="table-responsive">
<table class="table table-sm">
<thead>
<tr>
<th>{% trans "Criteria" %}</th>
<th>{% trans "Status" %}</th>
</tr>
</thead>
<tbody>
{% for criterion, status in candidate.criteria_checklist_ar.items %}
<tr>
<td>{{ criterion }}</td>
<td>
{% if status == "Met" %}
<span class="badge bg-success"><i class="fas fa-check me-1"></i> {% trans "Met" %}</span>
{% elif status == "Not Met" %}
<span class="badge bg-danger"><i class="fas fa-times me-1"></i> {% trans "Not Met" %}</span>
{% else %}
<span class="badge bg-secondary">{{ status }}</span>
{% endif %}
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
</div>
<div class="row mb-3">
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-check-circle me-2 text-success"></i>
<small class="text-muted">{% trans "Minimum Requirements" %}</small>
</div>
{% if candidate.min_requirements_met_ar %}
<span class="badge bg-success">{% trans "Met" %}</span>
{% else %}
<span class="badge bg-danger">{% trans "Not Met" %}</span>
{% endif %}
</div>
<div class="col-md-6">
<div class="d-flex align-items-center mb-2">
<i class="fas fa-star me-2 text-warning"></i>
<small class="text-muted">{% trans "Screening Rating" %}</small>
</div>
<span class="badge bg-secondary">{{ candidate.screening_stage_rating_ar }}</span>
</div>
</div>
{% if candidate.language_fluency_ar %}
<div class="mb-3">
<label class="form-label"><i class="fas fa-language me-1 text-info"></i> {% trans "Language Fluency" %}</label>
<div class="d-flex flex-wrap gap-2">
{% for language in candidate.language_fluency_ar %}
<span class="badge bg-light text-dark">{{ language }}</span>
{% endfor %}
</div>
</div>
{% endif %}
{% endif %}

View File

@ -480,6 +480,7 @@
{# TAB 5 CONTENT: PARSED SUMMARY #}
{% if candidate.parsed_summary %}
<div class="tab-pane fade" id="summary-pane" role="tabpanel" aria-labelledby="summary-tab">
<h5 class="text-primary mb-4">{% trans "AI Generated Summary" %}</h5>
<div class="border-start border-primary ps-3 pt-1 pb-1">

View File

@ -211,11 +211,11 @@
</h2>
</div>
<div class="d-flex gap-2">
<a href="{% url 'export_candidates_csv' job.slug 'document_review' %}"
{% comment %} <a href="{% url 'export_candidates_csv' job.slug 'document_review' %}"
class="btn btn-outline-secondary"
title="{% trans 'Export document review candidates to CSV' %}">
<i class="fas fa-download me-1"></i> {% trans "Export CSV" %}
</a>
</a> {% endcomment %}
<a href="{% url 'job_detail' job.slug %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Job" %}
</a>

View File

@ -293,9 +293,7 @@
title="View Profile">
{{ candidate.name }}
</button>
{% comment %} <div class="candidate-name">
{{ candidate.name }}
</div> {% endcomment %}
</td>
<td>
<div class="candidate-details">

View File

@ -4,7 +4,13 @@
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>{{ candidate.resume_data.full_name|default:"Candidate" }} - Candidate Profile</title>
{% get_current_language as LANGUAGE_CODE %}
{% if LANGUAGE_CODE == 'ar' %}
<title>{{ candidate.resume_data_ar.full_name|default:"Candidate" }} - Candidate Profile</title>
{% else %}
<title>{{ candidate.resume_data_en.full_name|default:"Candidate" }} - Candidate Profile</title>
{% endif %}
<!-- Use a modern icon set -->
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
@ -519,7 +525,8 @@
</head>
<body class="bg-kaauh-light-bg font-sans">
<div class="container container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto;">
{% comment %} <div class="container container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto;">
{% include 'recruitment/partials/ai_overview_breadcromb.html' %}
<!-- Header Section -->
@ -902,7 +909,712 @@
{% endif %}
</section>
</div>
</div> {% endcomment %}
{% if LANGUAGE_CODE == 'ar' %}
{% with data_source=candidate.resume_data_ar analysis_source=candidate.analysis_data_ar %}
<div class="container container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto; direction: rtl; text-align: right;">
{% include 'recruitment/partials/ai_overview_breadcromb.html' %}
<header class="header-box" style="direction: rtl;">
<div class="header-info" style="text-align: right;">
<h1>{{ data_source.full_name|default:"اسم المرشح" }}</h1>
<p>{{ data_source.current_title|default:"المسمى الوظيفي" }}</p>
<div class="contact-details" style="justify-content: flex-end;">
<div class="contact-item" style="flex-direction: row-reverse;">
<span>{{ data_source.location|default:"الموقع" }}</span>
<i class="fas fa-map-marker-alt"></i>
</div>
<div class="contact-item" style="flex-direction: row-reverse;">
<span title="معلومات الاتصال: الهاتف والبريد الإلكتروني">{{ data_source.contact|default:"معلومات الاتصال" }}</span>
<i class="fas fa-id-card"></i>
</div>
{% if data_source.linkedin %}
<div class="contact-item" style="flex-direction: row-reverse;">
<a href="{{ data_source.linkedin }}" target="_blank"><i class="fab fa-linkedin text-white"></i></a>
</div>
{% endif %}
{% if data_source.github %}
<div class="contact-item" style="flex-direction: row-reverse;">
<a href="{{ data_source.github }}" target="_blank"><i class="fab fa-github text-white"></i></a>
</div>
{% endif %}
</div>
</div>
<div class="score-box" style="text-align: center;">
<div class="score-value">{{ analysis_source.match_score|default:0 }}%</div>
<div class="score-text">{% trans "Match Score" %}</div>
<div class="assessment-rating
{% if analysis_source.match_score|default:0 < 50 %}score-red{% elif analysis_source.match_score|default:0 < 75 %}score-yellow{% else %}score-green{% endif %}">
{{ analysis_source.screening_stage_rating|default:"التقييم" }}
</div>
</div>
</header>
<div class="content-grid" style="grid-template-areas: 'right left';">
<div class="space-y-6" style="grid-area: right;">
<section class="card-section">
<h2 class="section-title" style="flex-direction: row-reverse;">
{% trans "Analysis" %}
<i class="fas fa-chart-line"></i>
</h2>
{% if analysis_source.category %}
<div class="analysis-metric" style="margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--color-gray-100); flex-direction: row-reverse;">
<span class="metric-title">{% trans "Target Role Category:" %}</span>
<span class="metric-value" style="color: var(--kaauh-teal);">{{ analysis_source.category }}</span>
</div>
{% endif %}
{% if analysis_source.red_flags %}
<div class="narrative-box">
<h3 class="flag-title red" style="flex-direction: row-reverse;"><i class="fas fa-flag"></i>{% trans "Red Flags" %}</h3>
<p class="narrative-text">{{ analysis_source.red_flags|join:". "|default:"لا يوجد." }}</p>
</div>
{% endif %}
{% if analysis_source.strengths %}
<div class="narrative-box strength-box">
<h3 class="flag-title green" style="flex-direction: row-reverse;"><i class="fas fa-circle-check"></i>{% trans "Strengths" %}</h3>
<p class="narrative-text">{{ analysis_source.strengths }}</p>
</div>
{% endif %}
{% if analysis_source.weaknesses %}
<div class="narrative-box" style="margin-bottom: 1rem;">
<h3 class="flag-title red" style="flex-direction: row-reverse;"><i class="fas fa-triangle-exclamation"></i>{% trans "Weaknesses" %}</h3>
<p class="narrative-text">{{ analysis_source.weaknesses }}</p>
</div>
{% endif %}
{% if analysis_source.recommendation %}
<div class="analysis-summary">
<h3 style="font-size: 0.875rem;">{% trans "Recommendation" %}</h3>
<p style="font-size: 0.875rem;">{{ analysis_source.recommendation }}</p>
</div>
{% endif %}
</section>
{% if analysis_source.criteria_checklist %}
<section class="card-section">
<h2 class="section-title" style="flex-direction: row-reverse;">
{% trans "Required Criteria Check" %}
<i class="fas fa-list-check"></i>
</h2>
<div style="margin-top: 0.75rem;">
{% for criterion, status in analysis_source.criteria_checklist.items %}
<div class="criteria-item" style="justify-content: space-between; flex-direction: row-reverse;">
<span class="text-gray-700">{{ criterion }}</span>
<span class="metric-value" style="font-size: 0.875rem;">
{% if status == 'Met' %}<span class="text-green-check" style="flex-direction: row-reverse;"><i class="fas fa-check-circle"></i> {% trans "Met" %}</span>
{% elif status == 'Not Mentioned' %}<span class="text-yellow-exclaim" style="flex-direction: row-reverse;"><i class="fas fa-exclamation-circle"></i> {% trans "Not Mentioned" %}</span>
{% else %}<span class="text-red-x" style="flex-direction: row-reverse;"><i class="fas fa-times-circle"></i> {{ status }}</span>
{% endif %}
</span>
</div>
{% endfor %}
</div>
</section>
{% endif %}
<section class="card-section">
<h2 class="section-title" style="flex-direction: row-reverse;">
{% trans "Skills" %}
<i class="fas fa-tools"></i>
</h2>
{% if data_source.skills %}
{% for category, skills in data_source.skills.items %}
<div style="margin-bottom: 1rem;">
<h3 class="keyword-subheader" style="color: var(--color-gray-700); flex-direction: row-reverse;"><i class="fas fa-list-alt" style="color: transparent;"></i>{{ category|cut:"_"|title }}</h3>
<div class="tag-list" style="justify-content: flex-end;">
{% for skill in skills %}
<span class="tag-item" style="color: var(--kaauh-teal-dark);">{{ skill }}</span>
{% endfor %}
</div>
</div>
{% endfor %}
{% else %}
<p style="color: var(--color-gray-500); font-size: 0.875rem;">{% trans "Skills information not available." %}</p>
{% endif %}
</section>
<section class="card-section">
<h2 class="section-title" style="flex-direction: row-reverse;">
{% trans "Languages" %}
<i class="fas fa-language"></i>
</h2>
{% if analysis_source.language_fluency %}
{% for language in analysis_source.language_fluency %}
<div style="margin-bottom: 0.75rem;">
<div class="analysis-metric" style="margin-bottom: 0.25rem; border-bottom: none; flex-direction: row-reverse;">
<span class="metric-title">{{ language }}</span>
</div>
<div class="progress-container" style="height: 0.5rem;">
{% with fluency_check=language|lower %}
<div class="language-bar"
style="width: {% if 'native' in fluency_check %}100{% elif 'fluent' in fluency_check %}85{% elif 'intermediate' in fluency_check %}50{% elif 'basic' in fluency_check %}25{% else %}10{% endif %}%">
</div>
{% endwith %}
</div>
</div>
{% endfor %}
{% else %}
<p style="color: var(--color-gray-500); font-size: 0.875rem;">{% trans "Language information not available." %}</p>
{% endif %}
</section>
<section class="card-section">
<h2 class="section-title" style="flex-direction: row-reverse;">
{% trans "Key Metrics" %}
<i class="fas fa-chart-pie"></i>
</h2>
<div style="margin-top: 0.75rem;">
{% if analysis_source.min_req_met_bool is not none %}
<div class="analysis-metric" style="flex-direction: row-reverse; justify-content: space-between;">
<span class="metric-label" style="flex-direction: row-reverse;"><i class="fas fa-shield-halved"></i>{% trans "Min Requirements Met:" %}</span>
<span class="metric-value {% if analysis_source.min_req_met_bool %}text-green-check{% else %}text-red-x{% endif %}" style="flex-direction: row-reverse;">
{% if analysis_source.min_req_met_bool %}<i class="fas fa-check-circle"></i> {% trans "Yes" %}{% else %}<i class="fas fa-times-circle"></i> {% trans "No" %}{% endif %}
</span>
</div>
{% endif %}
{% if analysis_source.years_of_experience is not none %}
<div class="analysis-metric" style="flex-direction: row-reverse; justify-content: space-between;">
<span class="metric-label" style="flex-direction: row-reverse;"><i class="fas fa-clock"></i>{% trans "Total Experience:" %}</span>
<span class="metric-value">{{ analysis_source.years_of_experience|floatformat:1 }} {% trans "years" %}</span>
</div>
{% endif %}
{% if analysis_source.most_recent_job_title %}
<div class="analysis-metric" style="flex-direction: row-reverse; justify-content: space-between;">
<span class="metric-label" style="flex-direction: row-reverse;"><i class="fas fa-id-badge"></i>{% trans "Most Recent Title (Scoring):" %}</span>
<span class="metric-value max-w-50-percent" style="text-align: left;">{{ analysis_source.most_recent_job_title }}</span>
</div>
{% endif %}
{% if analysis_source.seniority_level_match is not none %}
<div class="analysis-metric" style="flex-direction: row-reverse; justify-content: space-between;">
<span class="metric-label" style="flex-direction: row-reverse;"><i class="fas fa-user-tie"></i>{% trans "Seniority Match:" %}</span>
<span class="metric-value">{{ analysis_source.seniority_level_match|default:0 }}/100</span>
</div>
{% endif %}
{% if analysis_source.soft_skills_score is not none %}
<div class="analysis-metric" style="flex-direction: row-reverse; justify-content: space-between;">
<span class="metric-label" style="flex-direction: row-reverse;"><i class="fas fa-handshake"></i>{% trans "Soft Skills Score:" %}</span>
<span class="metric-value">{{ analysis_source.soft_skills_score|default:0 }}/100</span>
</div>
{% endif %}
{% if analysis_source.employment_stability_score is not none %}
<div class="analysis-metric" style="flex-direction: row-reverse; justify-content: space-between;">
<span class="metric-label" style="flex-direction: row-reverse;"><i class="fas fa-anchor"></i>{% trans "Stability Score:" %}</span>
<span class="metric-value">{{ analysis_source.employment_stability_score|default:0 }}/100</span>
</div>
{% endif %}
{% if analysis_source.experience_industry_match is not none %}
<div class="analysis-metric" style="border-bottom: none; padding-bottom: 0; flex-direction: row-reverse; justify-content: space-between;">
<span class="metric-label" style="flex-direction: row-reverse;"><i class="fas fa-industry"></i>{% trans "Industry Match:" %}</span>
<span class="metric-value">{{ analysis_source.experience_industry_match|default:0 }}/100</span>
</div>
{% endif %}
</div>
{% if analysis_source.transferable_skills_narrative %}
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--color-gray-100); font-size: 0.875rem; color: var(--color-gray-500); text-align: right;">
{% trans "Transferable Skills:" %} <i class="fas fa-puzzle-piece" style="margin-left: 0.25rem;"></i>
{{ analysis_source.transferable_skills_narrative }}
</div>
{% endif %}
</section>
</div>
<div class="space-y-6" style="grid-area: left;">
<section class="card-section">
<h2 class="section-title" style="flex-direction: row-reverse;">
{% trans "Summary" %}
<i class="fas fa-user-circle"></i>
</h2>
<p class="summary-text">
{{ data_source.summary|default:"الملخص المهني غير متوفر." }}
</p>
</section>
<section class="card-section">
<h2 class="section-title" style="margin-bottom: 1.5rem; flex-direction: row-reverse;">
{% trans "Experience" %}
<i class="fas fa-briefcase"></i>
</h2>
{% for experience in data_source.experience %}
<div class="experience-item">
<div class="experience-header" style="flex-direction: row-reverse;">
<div>
<h3>{{ experience.job_title }}</h3>
<p>{{ experience.company }}</p>
</div>
<span class="experience-tag">
{% if experience.end_date == "Present" %}{% trans "Present" %}{% else %}{{ experience.end_date|default:"حالي" }}{% endif %}
</span>
</div>
<p class="experience-meta" style="flex-direction: row-reverse; justify-content: flex-end;">
{% if experience.end_date and experience.end_date != "Present" %}{{ experience.end_date }}{% else %}{% trans "Present" %}{% endif %}
- {% if experience.start_date %}{{ experience.start_date }}{% endif %}
<i class="fas fa-calendar-alt"></i>
{% if experience.location %}<span style="margin-right: 1rem;"><i class="fas fa-map-pin"></i>{{ experience.location }}</span>{% endif %}
</p>
{% if experience.key_achievements %}
<ul class="achievement-list" style="padding-right: 1.5rem; padding-left: 0; list-style-type: none;">
{% for achievement in experience.key_achievements %}
<li style="text-align: right;"><i class="fas fa-caret-left" style="margin-left: 0.5rem;"></i>{{ achievement }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endfor %}
</section>
<section class="card-section">
<h2 class="section-title" style="margin-bottom: 1.5rem; flex-direction: row-reverse;">
{% trans "Education" %}
<i class="fas fa-graduation-cap"></i>
</h2>
{% for education in data_source.education %}
<div class="education-item" style="flex-direction: row-reverse;">
<div class="icon-badge" style="margin-left: 1rem; margin-right: 0;">
<i class="fas fa-certificate"></i>
</div>
<div class="education-details" style="text-align: right;">
<h3>{{ education.degree }}</h3>
<p>{{ education.institution }}</p>
{% if education.year %}
<p class="meta" style="flex-direction: row-reverse; justify-content: flex-end;"><i class="fas fa-calendar-alt"></i> {{ education.year }}</p>
{% endif %}
{% if education.gpa %}
<p class="meta" style="flex-direction: row-reverse; justify-content: flex-end;"><i class="fas fa-award"></i> {% trans "GPA" %}: {{ education.gpa }}</p>
{% endif %}
{% if education.relevant_courses %}
<p class="meta" style="margin-top: 0.25rem; text-align: right;">{% trans "Courses" %}: {{ education.relevant_courses|join:", " }}</p>
{% endif %}
</div>
</div>
{% endfor %}
</section>
<section class="card-section">
<h2 class="section-title" style="flex-direction: row-reverse;">
{% trans "Projects" %}
<i class="fas fa-project-diagram"></i>
</h2>
{% for project in data_source.projects %}
<div class="project-item">
<h3>{{ project.name }}</h3>
<p class="description">{{ project.brief_description }}</p>
{% if project.technologies_used %}
<div class="tag-list" style="justify-content: flex-end;">
{% for tech in project.technologies_used %}
<span class="tag-item">{{ tech }}</span>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
{% if not data_source.projects %}
<p style="color: var(--color-gray-500); font-size: 0.875rem;">{% trans "No projects detailed in the resume." %}</p>
{% endif %}
</section>
{% if analysis_source.top_3_keywords or analysis_source.cultural_fit_keywords %}
<section class="card-section" style="margin-top: 0;">
<h2 class="section-title" style="flex-direction: row-reverse;">
{% trans "Keywords" %}
<i class="fas fa-tags"></i>
</h2>
{% if analysis_source.top_3_keywords %}
<div style="margin-bottom: 1rem;">
<h3 class="keyword-subheader" style="flex-direction: row-reverse;"><i class="fas fa-key"></i>{% trans "Top Keywords (Job Match)" %}</h3>
<div class="tag-list" style="justify-content: flex-end;">
{% for keyword in analysis_source.top_3_keywords %}
<span class="keyword-tag">{{ keyword }}</span>
{% endfor %}
</div>
</div>
{% endif %}
{% if analysis_source.cultural_fit_keywords %}
<div>
<h3 class="keyword-subheader" style="flex-direction: row-reverse;"><i class="fas fa-users"></i>{% trans "Cultural Fit Keywords" %}</h3>
<div class="tag-list" style="justify-content: flex-end;">
{% for keyword in analysis_source.cultural_fit_keywords %}
<span class="cultural-tag">{{ keyword }}</span>
{% endfor %}
</div>
</div>
{% endif %}
</section>
{% endif %}
</div>
</div>
</div>
{% endwith %}
{% else %}
{% with data_source=candidate.resume_data_en analysis_source=candidate.analysis_data_en %}
<div class="container container-fluid flex-grow-1" style="max-width: 1600px; margin: 0 auto;">
{% include 'recruitment/partials/ai_overview_breadcromb.html' %}
<header class="header-box">
<div class="header-info">
<h1>{{ data_source.full_name|default:"Candidate Name" }}</h1>
<p>{{ data_source.current_title|default:"Professional Title" }}</p>
<div class="contact-details">
<div class="contact-item">
<i class="fas fa-map-marker-alt"></i>
<span>{{ data_source.location|default:"Location" }}</span>
</div>
<div class="contact-item">
<i class="fas fa-id-card"></i>
<span title="Contact Information: Phone and Email">{{ data_source.contact|default:"Contact Information" }}</span>
</div>
{% if data_source.linkedin %}
<div class="contact-item">
<a href="{{ data_source.linkedin }}" target="_blank"><i class="fab fa-linkedin text-white"></i></a>
</div>
{% endif %}
{% if data_source.github %}
<div class="contact-item">
<a href="{{ data_source.github }}" target="_blank"><i class="fab fa-github text-white"></i></a>
</div>
{% endif %}
</div>
</div>
<div class="score-box">
<div class="score-value">{{ analysis_source.match_score|default:0 }}%</div>
<div class="score-text">{% trans "Match Score" %}</div>
<div class="assessment-rating
{% if analysis_source.match_score|default:0 < 50 %}score-red{% elif analysis_source.match_score|default:0 < 75 %}score-yellow{% else %}score-green{% endif %}">
{{ analysis_source.screening_stage_rating|default:"Assessment" }}
</div>
</div>
</header>
<div class="content-grid">
<div class="space-y-6">
<section class="card-section">
<h2 class="section-title">
<i class="fas fa-user-circle"></i>
{% trans "Summary" %}
</h2>
<p class="summary-text">
{{ data_source.summary|default:"Professional summary not available." }}
</p>
</section>
<section class="card-section">
<h2 class="section-title" style="margin-bottom: 1.5rem;">
<i class="fas fa-briefcase"></i>
{% trans "Experience" %}
</h2>
{% for experience in data_source.experience %}
<div class="experience-item">
<div class="experience-header">
<div>
<h3>{{ experience.job_title }}</h3>
<p>{{ experience.company }}</p>
</div>
<span class="experience-tag">
{% if experience.end_date == "Present" %}{% trans "Present" %}{% else %}{{ experience.end_date|default:"Current" }}{% endif %}
</span>
</div>
<p class="experience-meta">
<i class="fas fa-calendar-alt"></i>
{% 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 %}<span style="margin-left: 1rem;"><i class="fas fa-map-pin"></i>{{ experience.location }}</span>{% endif %}
</p>
{% if experience.key_achievements %}
<ul class="achievement-list">
{% for achievement in experience.key_achievements %}
<li><i class="fas fa-caret-right"></i>{{ achievement }}</li>
{% endfor %}
</ul>
{% endif %}
</div>
{% endfor %}
</section>
<section class="card-section">
<h2 class="section-title" style="margin-bottom: 1.5rem;">
<i class="fas fa-graduation-cap"></i>
{% trans "Education" %}
</h2>
{% for education in data_source.education %}
<div class="education-item">
<div class="icon-badge">
<i class="fas fa-certificate"></i>
</div>
<div class="education-details">
<h3>{{ education.degree }}</h3>
<p>{{ education.institution }}</p>
{% if education.year %}
<p class="meta"><i class="fas fa-calendar-alt"></i> {{ education.year }}</p>
{% endif %}
{% if education.gpa %}
<p class="meta"><i class="fas fa-award"></i> {% trans "GPA" %}: {{ education.gpa }}</p>
{% endif %}
{% if education.relevant_courses %}
<p class="meta" style="margin-top: 0.25rem;">{% trans "Courses" %}: {{ education.relevant_courses|join:", " }}</p>
{% endif %}
</div>
</div>
{% endfor %}
</section>
<section class="card-section">
<h2 class="section-title">
<i class="fas fa-project-diagram"></i>
{% trans "Projects" %}
</h2>
{% for project in data_source.projects %}
<div class="project-item">
<h3>{{ project.name }}</h3>
<p class="description">{{ project.brief_description }}</p>
{% if project.technologies_used %}
<div class="tag-list">
{% for tech in project.technologies_used %}
<span class="tag-item">{{ tech }}</span>
{% endfor %}
</div>
{% endif %}
</div>
{% endfor %}
{% if not data_source.projects %}
<p style="color: var(--color-gray-500); font-size: 0.875rem;">{% trans "No projects detailed in the resume." %}</p>
{% endif %}
</section>
{% if analysis_source.top_3_keywords or analysis_source.cultural_fit_keywords %}
<section class="card-section" style="margin-top: 0;">
<h2 class="section-title">
<i class="fas fa-tags"></i>
{% trans "Keywords" %}
</h2>
{% if analysis_source.top_3_keywords %}
<div style="margin-bottom: 1rem;">
<h3 class="keyword-subheader"><i class="fas fa-key"></i>{% trans "Top Keywords (Job Match)" %}</h3>
<div class="tag-list">
{% for keyword in analysis_source.top_3_keywords %}
<span class="keyword-tag">{{ keyword }}</span>
{% endfor %}
</div>
</div>
{% endif %}
{% if analysis_source.cultural_fit_keywords %}
<div>
<h3 class="keyword-subheader"><i class="fas fa-users"></i>{% trans "Cultural Fit Keywords" %}</h3>
<div class="tag-list">
{% for keyword in analysis_source.cultural_fit_keywords %}
<span class="cultural-tag">{{ keyword }}</span>
{% endfor %}
</div>
</div>
{% endif %}
</section>
{% endif %}
</div>
<div class="space-y-6">
<section class="card-section">
<h2 class="section-title">
<i class="fas fa-chart-line"></i>
{% trans "Analysis" %}
</h2>
{% if analysis_source.category %}
<div class="analysis-metric" style="margin-bottom: 0.75rem; padding-bottom: 0.5rem; border-bottom: 1px solid var(--color-gray-100);">
<span class="metric-title">{% trans "Target Role Category:" %}</span>
<span class="metric-value" style="color: var(--kaauh-teal);">{{ analysis_source.category }}</span>
</div>
{% endif %}
{% if analysis_source.red_flags %}
<div class="narrative-box">
<h3 class="flag-title red"><i class="fas fa-flag"></i>{% trans "Red Flags" %}</h3>
<p class="narrative-text">{{ analysis_source.red_flags|join:". "|default:"None." }}</p>
</div>
{% endif %}
{% if analysis_source.strengths %}
<div class="narrative-box strength-box">
<h3 class="flag-title green"><i class="fas fa-circle-check"></i>{% trans "Strengths" %}</h3>
<p class="narrative-text">{{ analysis_source.strengths }}</p>
</div>
{% endif %}
{% if analysis_source.weaknesses %}
<div class="narrative-box" style="margin-bottom: 1rem;">
<h3 class="flag-title red"><i class="fas fa-triangle-exclamation"></i>{% trans "Weaknesses" %}</h3>
<p class="narrative-text">{{ analysis_source.weaknesses }}</p>
</div>
{% endif %}
{% if analysis_source.recommendation %}
<div class="analysis-summary">
<h3 style="font-size: 0.875rem;">{% trans "Recommendation" %}</h3>
<p style="font-size: 0.875rem;">{{ analysis_source.recommendation }}</p>
</div>
{% endif %}
</section>
{% if analysis_source.criteria_checklist %}
<section class="card-section">
<h2 class="section-title">
<i class="fas fa-list-check"></i>
{% trans "Required Criteria Check" %}
</h2>
<div style="margin-top: 0.75rem;">
{% for criterion, status in analysis_source.criteria_checklist.items %}
<div class="criteria-item">
<span class="text-gray-700">{{ criterion }}</span>
<span class="metric-value" style="font-size: 0.875rem;">
{% if status == 'Met' %}<span class="text-green-check"><i class="fas fa-check-circle"></i> {% trans "Met" %}</span>
{% elif status == 'Not Mentioned' %}<span class="text-yellow-exclaim"><i class="fas fa-exclamation-circle"></i> {% trans "Not Mentioned" %}</span>
{% else %}<span class="text-red-x"><i class="fas fa-times-circle"></i> {{ status }}</span>
{% endif %}
</span>
</div>
{% endfor %}
</div>
</section>
{% endif %}
<section class="card-section">
<h2 class="section-title">
<i class="fas fa-tools"></i>
{% trans "Skills" %}
</h2>
{% if data_source.skills %}
{% for category, skills in data_source.skills.items %}
<div style="margin-bottom: 1rem;">
<h3 class="keyword-subheader" style="color: var(--color-gray-700);"><i class="fas fa-list-alt" style="color: transparent;"></i>{{ category|cut:"_"|title }}</h3>
<div class="tag-list">
{% for skill in skills %}
<span class="tag-item" style="color: var(--kaauh-teal-dark);">{{ skill }}</span>
{% endfor %}
</div>
</div>
{% endfor %}
{% else %}
<p style="color: var(--color-gray-500); font-size: 0.875rem;">{% trans "Skills information not available." %}</p>
{% endif %}
</section>
<section class="card-section">
<h2 class="section-title">
<i class="fas fa-language"></i>
{% trans "Languages" %}
</h2>
{% if analysis_source.language_fluency %}
{% for language in analysis_source.language_fluency %}
<div style="margin-bottom: 0.75rem;">
<div class="analysis-metric" style="margin-bottom: 0.25rem; border-bottom: none;">
<span class="metric-title">{{ language }}</span>
</div>
<div class="progress-container" style="height: 0.5rem;">
{% with fluency_check=language|lower %}
<div class="language-bar"
style="width: {% if 'native' in fluency_check %}100{% elif 'fluent' in fluency_check %}85{% elif 'intermediate' in fluency_check %}50{% elif 'basic' in fluency_check %}25{% else %}10{% endif %}%">
</div>
{% endwith %}
</div>
</div>
{% endfor %}
{% else %}
<p style="color: var(--color-gray-500); font-size: 0.875rem;">{% trans "Language information not available." %}</p>
{% endif %}
</section>
<section class="card-section">
<h2 class="section-title">
<i class="fas fa-chart-pie"></i>
{% trans "Key Metrics" %}
</h2>
<div style="margin-top: 0.75rem;">
{% if analysis_source.min_req_met_bool is not none %}
<div class="analysis-metric">
<span class="metric-label"><i class="fas fa-shield-halved"></i>{% trans "Min Requirements Met:" %}</span>
<span class="metric-value {% if analysis_source.min_req_met_bool %}text-green-check{% else %}text-red-x{% endif %}">
{% if analysis_source.min_req_met_bool %}<i class="fas fa-check-circle"></i> {% trans "Yes" %}{% else %}<i class="fas fa-times-circle"></i> {% trans "No" %}{% endif %}
</span>
</div>
{% endif %}
{% if analysis_source.years_of_experience is not none %}
<div class="analysis-metric">
<span class="metric-label"><i class="fas fa-clock"></i>{% trans "Total Experience:" %}</span>
<span class="metric-value">{{ analysis_source.years_of_experience|floatformat:1 }} {% trans "years" %}</span>
</div>
{% endif %}
{% if analysis_source.most_recent_job_title %}
<div class="analysis-metric">
<span class="metric-label"><i class="fas fa-id-badge"></i>{% trans "Most Recent Title (Scoring):" %}</span>
<span class="metric-value max-w-50-percent" style="text-align: right;">{{ analysis_source.most_recent_job_title }}</span>
</div>
{% endif %}
{% if analysis_source.seniority_level_match is not none %}
<div class="analysis-metric">
<span class="metric-label"><i class="fas fa-user-tie"></i>{% trans "Seniority Match:" %}</span>
<span class="metric-value">{{ analysis_source.seniority_level_match|default:0 }}/100</span>
</div>
{% endif %}
{% if analysis_source.soft_skills_score is not none %}
<div class="analysis-metric">
<span class="metric-label"><i class="fas fa-handshake"></i>{% trans "Soft Skills Score:" %}</span>
<span class="metric-value">{{ analysis_source.soft_skills_score|default:0 }}/100</span>
</div>
{% endif %}
{% if analysis_source.employment_stability_score is not none %}
<div class="analysis-metric">
<span class="metric-label"><i class="fas fa-anchor"></i>{% trans "Stability Score:" %}</span>
<span class="metric-value">{{ analysis_source.employment_stability_score|default:0 }}/100</span>
</div>
{% endif %}
{% if analysis_source.experience_industry_match is not none %}
<div class="analysis-metric" style="border-bottom: none; padding-bottom: 0;">
<span class="metric-label"><i class="fas fa-industry"></i>{% trans "Industry Match:" %}</span>
<span class="metric-value">{{ analysis_source.experience_industry_match|default:0 }}/100</span>
</div>
{% endif %}
</div>
{% if analysis_source.transferable_skills_narrative %}
<div style="margin-top: 1rem; padding-top: 1rem; border-top: 1px solid var(--color-gray-100); font-size: 0.875rem; color: var(--color-gray-500);">
<i class="fas fa-puzzle-piece" style="margin-right: 0.25rem;"></i> {% trans "Transferable Skills:" %}
{{ analysis_source.transferable_skills_narrative }}
</div>
{% endif %}
</section>
</div>
</div>
</div>
{% endwith %}
{% endif %}
<script>
// Simple progress bar animation

View File

@ -1,4 +1,5 @@
{% load i18n %}
{% get_current_language as LANGUAGE_CODE %}
{% with request.resolver_match as resolved %}
{% if 'resume-template' in resolved.route and resolved.kwargs.slug == candidate.slug %}
<nav aria-label="breadcrumb" style="font-family: 'Inter', sans-serif; margin-bottom:1.5rem;">
@ -90,7 +91,11 @@
onmouseover="this.style.color='#000000';"
onmouseout="this.style.color='#6c757d';"
>
{{candidate.resume_data.full_name}}
{% if LANGUAGE_CODE == 'en' %}
{{candidate.resume_data_en.full_name}}
{% else%}
{{candidate.resume_data_ar.full_name}}
{% endif %}
</a>
</li>