first commit regarding the ai_analysis to support en and ar in branch ai_overview
This commit is contained in:
parent
fe5fd7424d
commit
8bc3747afe
@ -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,
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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))
|
||||
@ -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",
|
||||
)
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 = {}
|
||||
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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 %}
|
||||
@ -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">
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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">
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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>
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user