more update and add qcluster

This commit is contained in:
ismail 2025-10-12 13:30:16 +03:00
parent 322c98222d
commit 916cbc4fcf
26 changed files with 1719 additions and 444 deletions

View File

@ -55,7 +55,9 @@ INSTALLED_APPS = [
'django_extensions', 'django_extensions',
'template_partials', 'template_partials',
'django_countries', 'django_countries',
'django_celery_results' 'django_celery_results',
'django_q',
] ]
SITE_ID = 1 SITE_ID = 1
@ -223,207 +225,31 @@ LINKEDIN_CLIENT_ID = '867jwsiyem1504'
LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw==' LINKEDIN_CLIENT_SECRET = 'WPL_AP1.QNH5lYnfRSQpp0Qp.GO8Srw=='
LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/' LINKEDIN_REDIRECT_URI = 'http://127.0.0.1:8000/jobs/linkedin/callback/'
# customColorPalette = [
# {
# 'color': 'hsl(4, 90%, 58%)',
# 'label': 'Red'
# },
# {
# 'color': 'hsl(340, 82%, 52%)',
# 'label': 'Pink'
# },
# {
# 'color': 'hsl(291, 64%, 42%)',
# 'label': 'Purple'
# },
# {
# 'color': 'hsl(262, 52%, 47%)',
# 'label': 'Deep Purple'
# },
# {
# 'color': 'hsl(231, 48%, 48%)',
# 'label': 'Indigo'
# },
# {
# 'color': 'hsl(207, 90%, 54%)',
# 'label': 'Blue'
# },
# ]
# #ckeditor_5 config setthings:
# CKEDITOR_5_CONFIGS = {
# 'default': {
# 'toolbar': {
# 'items': ['heading', '|', 'bold', 'italic', 'link',
# 'bulletedList', 'numberedList', 'blockQuote', 'imageUpload', ],
# }
# }, Q_CLUSTER = {
# 'extends': { 'name': 'KAAUH_CLUSTER',
# 'blockToolbar': [ 'workers': 4,
# 'paragraph', 'heading1', 'heading2', 'heading3', 'recycle': 500,
# '|', 'timeout': 60,
# 'bulletedList', 'numberedList', 'compress': True,
# '|', 'save_limit': 250,
# 'blockQuote', 'queue_limit': 500,
# ], 'cpu_affinity': 1,
# 'toolbar': { 'label': 'Django Q2',
# 'items': ['heading', '|', 'outdent', 'indent', '|', 'bold', 'italic', 'link', 'underline', 'strikethrough', 'redis': {
# 'code','subscript', 'superscript', 'highlight', '|', 'codeBlock', 'sourceEditing', 'insertImage', 'host': '127.0.0.1',
# 'bulletedList', 'numberedList', 'todoList', '|', 'blockQuote', 'imageUpload', '|', 'port': 6379,
# 'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', 'mediaEmbed', 'removeFormat', 'db': 0, },
# 'insertTable', 'ALT_CLUSTERS': {
# ], 'long': {
# 'shouldNotGroupWhenFull': 'true' 'timeout': 3000,
# }, 'retry': 3600,
# 'image': { 'max_attempts': 2,
# 'toolbar': ['imageTextAlternative', '|', 'imageStyle:alignLeft',
# 'imageStyle:alignRight', 'imageStyle:alignCenter', 'imageStyle:side', '|'],
# 'styles': [
# 'full',
# 'side',
# 'alignLeft',
# 'alignRight',
# 'alignCenter',
# ]
# },
# 'table': {
# 'contentToolbar': [ 'tableColumn', 'tableRow', 'mergeTableCells',
# 'tableProperties', 'tableCellProperties' ],
# 'tableProperties': {
# 'borderColors': customColorPalette,
# 'backgroundColors': customColorPalette
# },
# 'tableCellProperties': {
# 'borderColors': customColorPalette,
# 'backgroundColors': customColorPalette
# }
# },
# 'heading' : {
# 'options': [
# { 'model': 'paragraph', 'title': 'Paragraph', 'class': 'ck-heading_paragraph' },
# { 'model': 'heading1', 'view': 'h1', 'title': 'Heading 1', 'class': 'ck-heading_heading1' },
# { 'model': 'heading2', 'view': 'h2', 'title': 'Heading 2', 'class': 'ck-heading_heading2' },
# { 'model': 'heading3', 'view': 'h3', 'title': 'Heading 3', 'class': 'ck-heading_heading3' }
# ]
# }
# },
# 'list': {
# 'properties': {
# 'styles': 'true',
# 'startIndex': 'true',
# 'reversed': 'true',
# }
# }
# }
# The customColorPalette constant must be defined before CKEDITOR_5_CONFIGS
customColorPalette = [
{
'color': 'hsl(4, 90%, 58%)',
'label': 'Red'
},
{
'color': 'hsl(340, 82%, 52%)',
'label': 'Pink'
},
{
'color': 'hsl(291, 64%, 42%)',
'label': 'Purple'
},
{
'color': 'hsl(262, 52%, 47%)',
'label': 'Deep Purple'
},
{
'color': 'hsl(231, 48%, 48%)',
'label': 'Indigo'
},
{
'color': 'hsl(207, 90%, 54%)',
'label': 'Blue'
},
]
CKEDITOR_5_CONFIGS = {
'default': {
'toolbar': {
'items': ['heading', '|', 'bold', 'italic', 'link',
'bulletedList', 'numberedList', 'blockQuote', 'imageUpload', ],
}
},
# Your existing 'extends' configuration remains unchanged
'extends': {
'blockToolbar': [
'paragraph', 'heading1', 'heading2', 'heading3',
'|',
'bulletedList', 'numberedList',
'|',
'blockQuote',
],
'toolbar': {
'items': ['heading', '|', 'outdent', 'indent', '|', 'bold', 'italic', 'link', 'underline', 'strikethrough',
'code','subscript', 'superscript', 'highlight', '|', 'codeBlock', 'sourceEditing', 'insertImage',
'bulletedList', 'numberedList', 'todoList', '|', 'blockQuote', 'imageUpload', '|',
'fontSize', 'fontFamily', 'fontColor', 'fontBackgroundColor', 'mediaEmbed', 'removeFormat',
'insertTable',
],
'shouldNotGroupWhenFull': 'true'
}, },
'image': { 'short': {
'toolbar': ['imageTextAlternative', '|', 'imageStyle:alignLeft', 'timeout': 10,
'imageStyle:alignRight', 'imageStyle:alignCenter', 'imageStyle:side', '|'], 'max_attempts': 1,
'styles': [
'full',
'side',
'alignLeft',
'alignRight',
'alignCenter',
]
}, },
'table': { }
'contentToolbar': [ 'tableColumn', 'tableRow', 'mergeTableCells',
'tableProperties', 'tableCellProperties' ],
'tableProperties': {
'borderColors': customColorPalette,
'backgroundColors': customColorPalette
},
'tableCellProperties': {
'borderColors': customColorPalette,
'backgroundColors': customColorPalette
}
},
'heading' : {
'options': [
{ 'model': 'paragraph', 'title': 'Paragraph', 'class': 'ck-heading_paragraph' },
{ 'model': 'heading1', 'view': 'h1', 'title': 'Heading 1', 'class': 'ck-heading_heading1' },
{ 'model': 'heading2', 'view': 'h2', 'title': 'Heading 2', 'class': 'ck-heading_heading2' },
{ 'model': 'heading3', 'view': 'h3', 'title': 'Heading 3', 'class': 'ck-heading_heading3' }
]
}
},
# Your existing 'list' configuration remains unchanged
'list': {
'properties': {
'styles': 'true',
'startIndex': 'true',
'reversed': 'true',
}
},
# *** NEW 'comment' CONFIGURATION ***
'comment': {
'toolbar': {
'items': [
'bold', 'italic', 'underline', 'link',
'bulletedList', 'numberedList', 'blockQuote',
'|', 'undo', 'redo'
],
},
# You can add other specific configurations for a comment field here,
# such as disabling image upload or advanced features to keep it lightweight.
},
} }

View File

@ -143,7 +143,7 @@ class JobPostingAdmin(admin.ModelAdmin):
@admin.register(Candidate) @admin.register(Candidate)
class CandidateAdmin(admin.ModelAdmin): class CandidateAdmin(admin.ModelAdmin):
list_display = ['full_name', 'job', 'email', 'phone', 'stage', 'applied', 'created_at'] list_display = ['full_name', 'job', 'email', 'phone', 'stage', 'applied','is_resume_parsed', 'created_at']
list_filter = ['stage', 'applied', 'created_at', 'job__department'] list_filter = ['stage', 'applied', 'created_at', 'job__department']
search_fields = ['first_name', 'last_name', 'email', 'phone'] search_fields = ['first_name', 'last_name', 'email', 'phone']
readonly_fields = ['slug', 'created_at', 'updated_at'] readonly_fields = ['slug', 'created_at', 'updated_at']

View File

@ -0,0 +1,18 @@
# Generated by Django 5.2.6 on 2025-10-11 14:47
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0003_candidate_is_resume_parsed_and_more'),
]
operations = [
migrations.AddField(
model_name='candidate',
name='applicant_status',
field=models.CharField(blank=True, choices=[('Applicant', 'Applicant'), ('Candidate', 'Candidate')], default='Applicant', max_length=100, null=True, verbose_name='Applicant Status'),
),
]

View File

@ -267,6 +267,10 @@ class Candidate(Base):
ACCEPTED = "Accepted", _("Accepted") ACCEPTED = "Accepted", _("Accepted")
REJECTED = "Rejected", _("Rejected") REJECTED = "Rejected", _("Rejected")
class ApplicantType(models.TextChoices):
APPLICANT = "Applicant", _("Applicant")
CANDIDATE = "Candidate", _("Candidate")
# Stage transition validation constants # Stage transition validation constants
STAGE_SEQUENCE = { STAGE_SEQUENCE = {
"Applied": ["Exam", "Interview", "Offer"], "Applied": ["Exam", "Interview", "Offer"],
@ -298,7 +302,14 @@ class Candidate(Base):
choices=Stage.choices, choices=Stage.choices,
verbose_name=_("Stage"), verbose_name=_("Stage"),
) )
applicant_status = models.CharField(
choices=ApplicantType.choices,
default="Applicant",
max_length=100,
null=True,
blank=True,
verbose_name=_("Applicant Status"),
)
exam_date = models.DateField(null=True, blank=True, verbose_name=_("Exam Date")) exam_date = models.DateField(null=True, blank=True, verbose_name=_("Exam Date"))
exam_status = models.CharField( exam_status = models.CharField(
choices=ExamStatus.choices, choices=ExamStatus.choices,

View File

@ -1,136 +1,21 @@
from . import models import logging
from django.urls import reverse
from django.db import transaction from django.db import transaction
from django.dispatch import receiver from django.dispatch import receiver
from django_q.tasks import async_task
from django.db.models.signals import post_save from django.db.models.signals import post_save
from .models import FormField,FormStage,FormTemplate from .models import FormField,FormStage,FormTemplate,Candidate
# @receiver(post_save, sender=models.Candidate)
# def parse_resume(sender, instance, created, **kwargs):
# if instance.resume and not instance.summary:
# from .utils import extract_summary_from_pdf,match_resume_with_job_description
# summary = extract_summary_from_pdf(instance.resume.path)
# if 'error' not in summary:
# instance.summary = summary
# instance.save()
# match_resume_with_job_description
import logging
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
import os
from .utils import extract_text_from_pdf,score_resume_with_openrouter
import asyncio
@receiver(post_save, sender=models.Candidate) @receiver(post_save, sender=Candidate)
def score_candidate_resume(sender, instance, created, **kwargs): def score_candidate_resume(sender, instance, created, **kwargs):
if instance.is_resume_parsed: if not instance.is_resume_parsed:
return logger.info(f"Scoring resume for candidate {instance.pk}")
try: async_task(
# Get absolute file path 'recruitment.tasks.handle_reume_parsing_and_scoring',
file_path = instance.resume.path instance.pk,
if not os.path.exists(file_path): # hook='myapp.tasks.email_sent_callback' # Optional callback
logger.warning(f"Resume file not found: {file_path}") )
return
resume_text = extract_text_from_pdf(file_path)
# if not resume_text:
# instance.scoring_error = "Could not extract text from resume."
# instance.save(update_fields=['scoring_error'])
# return
job_detail=str(instance.job.description)+str(instance.job.qualifications)
prompt1 = f"""
You are an expert resume parser and summarizer. Given a resume in plain text format, extract and organize the following key-value information into a clean, valid JSON object:
full_name: Full name of the candidate
current_title: Most recent or current job title
location: City and state (or country if outside the U.S.)
contact: Phone number and email (as a single string or separate fields)
linkedin: LinkedIn profile URL (if present)
github: GitHub or portfolio URL (if present)
summary: Brief professional profile or summary (12 sentences)
education: List of degrees, each with:
institution
degree
year
gpa (if provided)
relevant_courses (as a list, if mentioned)
skills: Grouped by category if possible (e.g., programming, big data, visualization), otherwise as a flat list of technologies/tools
experience: List of roles, each with:
company
job_title
location
start_date and end_date (or "Present" if applicable)
key_achievements (as a list of concise bullet points)
projects: List of notable projects (if clearly labeled), each with:
name
year
technologies_used
brief_description
Instructions:
Be concise but preserve key details.
Normalize formatting (e.g., Jun. 2014 2014-06).
Omit redundant or promotional language.
If a section is missing, omit the key or set it to null/empty list as appropriate.
Output only valid JSONno markdown, no extra text.
Now, process the following resume text:
{resume_text}
"""
result = score_resume_with_openrouter(prompt1)
prompt = f"""
You are an expert technical recruiter. Your task is to score the following candidate for the role of a Senior Data Analyst based on the provided job criteria.
**Job Criteria:**
{job_detail}
**Candidate's Extracted Resume Json:**
\"\"\"
{result}
\"\"\"
**Your Task:**
Provide a response in strict JSON format with the following keys:
1. 'match_score': A score from 0 to 100 representing how well the candidate fits the role.
2. 'strengths': A brief summary of why the candidate is a strong fit, referencing specific criteria.
3. 'weaknesses': A brief summary of where the candidate falls short or what criteria are missing.
4. 'criteria_checklist': An object where you rate the candidate's match for each specific criterion (e.g., {{'Python': 'Met', 'AWS': 'Not Mentioned'}}).
Only output valid JSON. Do not include any other text.
"""
result1 = score_resume_with_openrouter(prompt)
instance.parsed_summary = str(result)
# Update candidate with scoring results
instance.match_score = result1.get('match_score')
instance.strengths = result1.get('strengths', '')
instance.weaknesses = result1.get('weaknesses', '')
instance.criteria_checklist = result1.get('criteria_checklist', {})
instance.is_resume_parsed = True
# Save only scoring-related fields to avoid recursion
instance.save(update_fields=[
'match_score', 'strengths', 'weaknesses',
'criteria_checklist','parsed_summary', 'is_resume_parsed'
])
logger.info(f"Successfully scored resume for candidate {instance.id}")
except Exception as e:
# error_msg = str(e)[:500] # Truncate to fit TextField
# instance.scoring_error = error_msg
# instance.save(update_fields=['scoring_error'])
logger.error(f"Failed to score resume for candidate {instance.id}: {e}")
# @receiver(post_save,sender=models.Candidate)
# def trigger_scoring(sender,intance,created,**kwargs):
@receiver(post_save, sender=FormTemplate) @receiver(post_save, sender=FormTemplate)
def create_default_stages(sender, instance, created, **kwargs): def create_default_stages(sender, instance, created, **kwargs):

155
recruitment/tasks.py Normal file
View File

@ -0,0 +1,155 @@
import os
import json
import logging
import requests
from PyPDF2 import PdfReader
from recruitment.models import Candidate
logger = logging.getLogger(__name__)
OPENROUTER_API_KEY ='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'
OPENROUTER_MODEL = 'qwen/qwen-2.5-72b-instruct:free'
if not OPENROUTER_API_KEY:
logger.warning("OPENROUTER_API_KEY not set. Resume scoring will be skipped.")
def extract_text_from_pdf(file_path):
print("text extraction")
text = ""
try:
with open(file_path, "rb") as f:
reader = PdfReader(f)
for page in reader.pages:
text += (page.extract_text() or "")
except Exception as e:
logger.error(f"PDF extraction failed: {e}")
raise
return text.strip()
def ai_handler(prompt):
print("model call")
response = requests.post(
url="https://openrouter.ai/api/v1/chat/completions",
headers={
"Authorization": f"Bearer {OPENROUTER_API_KEY}",
"Content-Type": "application/json",
},
data=json.dumps({
"model": OPENROUTER_MODEL,
"messages": [{"role": "user", "content": prompt}],
},
)
)
res = {}
print(response.status_code)
if response.status_code == 200:
res = response.json()
content = res["choices"][0]['message']['content']
try:
content = content.replace("```json","").replace("```","")
res = json.loads(content)
except Exception as e:
print(e)
# res = raw_output["choices"][0]["message"]["content"]
else:
print("error response")
return res
def handle_reume_parsing_and_scoring(pk):
logger.info(f"Scoring resume for candidate {pk}")
try:
instance = Candidate.objects.get(pk=pk)
file_path = instance.resume.path
if not os.path.exists(file_path):
logger.warning(f"Resume file not found: {file_path}")
return
resume_text = extract_text_from_pdf(file_path)
job_detail= f"{instance.job.description} {instance.job.qualifications}"
resume_parser_prompt = f"""
You are an expert resume parser and summarizer. Given a resume in plain text format, extract and organize the following key-value information into a clean, valid JSON object:
full_name: Full name of the candidate
current_title: Most recent or current job title
location: City and state (or country if outside the U.S.)
contact: Phone number and email (as a single string or separate fields)
linkedin: LinkedIn profile URL (if present)
github: GitHub or portfolio URL (if present)
summary: Brief professional profile or summary (12 sentences)
education: List of degrees, each with:
institution
degree
year
gpa (if provided)
relevant_courses (as a list, if mentioned)
skills: Grouped by category if possible (e.g., programming, big data, visualization), otherwise as a flat list of technologies/tools
experience: List of roles, each with:
company
job_title
location
start_date and end_date (or "Present" if applicable)
key_achievements (as a list of concise bullet points)
projects: List of notable projects (if clearly labeled), each with:
name
year
technologies_used
brief_description
Instructions:
Be concise but preserve key details.
Normalize formatting (e.g., Jun. 2014 2014-06).
Omit redundant or promotional language.
If a section is missing, omit the key or set it to null/empty list as appropriate.
Output only valid JSONno markdown, no extra text.
Now, process the following resume text:
{resume_text}
"""
resume_parser_result = ai_handler(resume_parser_prompt)
resume_scoring_prompt = f"""
You are an expert technical recruiter. Your task is to score the following candidate for the role of a Senior Data Analyst based on the provided job criteria.
**Job Criteria:**
{job_detail}
**Candidate's Extracted Resume Json:**
\"\"\"
{resume_parser_result}
\"\"\"
**Your Task:**
Provide a response in strict JSON format with the following keys:
1. 'match_score': A score from 0 to 100 representing how well the candidate fits the role.
2. 'strengths': A brief summary of why the candidate is a strong fit, referencing specific criteria.
3. 'weaknesses': A brief summary of where the candidate falls short or what criteria are missing.
4. 'criteria_checklist': An object where you rate the candidate's match for each specific criterion (e.g., {{'Python': 'Met', 'AWS': 'Not Mentioned'}}).
Only output valid JSON. Do not include any other text.
"""
resume_scoring_result = ai_handler(resume_scoring_prompt)
instance.parsed_summary = str(resume_parser_result)
# Update candidate with scoring results
instance.match_score = resume_scoring_result.get('match_score')
instance.strengths = resume_scoring_result.get('strengths', '')
instance.weaknesses = resume_scoring_result.get('weaknesses', '')
instance.criteria_checklist = resume_scoring_result.get('criteria_checklist', {})
instance.is_resume_parsed = True
# Save only scoring-related fields to avoid recursion
instance.save(update_fields=[
'match_score', 'strengths', 'weaknesses',
'criteria_checklist','parsed_summary', 'is_resume_parsed'
])
logger.info(f"Successfully scored resume for candidate {instance.id}")
except Exception as e:
logger.error(f"Failed to score resume for candidate {instance.id}: {e}")

View File

@ -15,38 +15,60 @@ def get_stage_responses(stage_responses, stage_id):
return [] return []
@register.simple_tag @register.simple_tag
def get_all_responses_flat(stage_responses): def get_all_responses_flat(submission):
""" """
Template tag to get all responses flattened for table display. Template tag to get all responses from a FormSubmission flattened for table display.
Usage: {% get_all_responses_flat stage_responses as all_responses %} Usage: {% get_all_responses_flat submission as flat_responses %}
""" """
all_responses = [] all_responses = []
if stage_responses: if submission:
for stage_id, responses in stage_responses.items(): # Fetch all responses related to this submission, selecting related field and stage objects for efficiency
if responses: # Check if responses list exists and is not empty field_responses = submission.responses.all().select_related('field', 'field__stage').order_by('field__stage__order', 'field__order')
for response in responses:
# Check if response is an object or string
if hasattr(response, 'stage') and hasattr(response, 'field'):
stage_name = response.stage.name if hasattr(response.stage, 'name') else f"Stage {stage_id}"
field_label = response.field.label if hasattr(response.field, 'label') else "Unknown Field"
field_type = response.field.get_field_type_display() if hasattr(response.field, 'get_field_type_display') else "Unknown Type"
required = response.field.required if hasattr(response.field, 'required') else False
value = response.value if hasattr(response, 'value') else response
uploaded_file = response.uploaded_file if hasattr(response, 'uploaded_file') else None
else:
stage_name = f"Stage {stage_id}"
field_label = "Unknown Field"
field_type = "Text"
required = False
value = response
uploaded_file = None
all_responses.append({ for response in field_responses:
'stage_name': stage_name, stage_name = "N/A"
'field_label': field_label, field_label = "Unknown Field"
'field_type': field_type, field_type = "Text"
'required': required, required = False
'value': value, value = None
'uploaded_file': uploaded_file uploaded_file = None
})
if response.field:
field_label = response.field.label
field_type = response.field.get_field_type_display()
required = response.field.required
if response.field.stage:
stage_name = response.field.stage.name
value = response.value
uploaded_file = response.uploaded_file
all_responses.append({
'stage_name': stage_name,
'field_label': field_label,
'field_type': field_type,
'required': required,
'value': value,
'uploaded_file': uploaded_file
})
return all_responses return all_responses
@register.simple_tag
def get_field_response_for_submission(submission, field):
"""
Template tag to get the FieldResponse for a specific submission and field.
Usage: {% get_field_response_for_submission submission field as response %}
"""
try:
return submission.responses.filter(field=field).first()
except:
return None
@register.filter
def to_list(data):
"""
Template tag to convert a string to a list.
Usage: {% to_list "item1,item2,item3" as list %}
"""
return data.split(",") if data else []

View File

@ -9,7 +9,7 @@ urlpatterns = [
# Job URLs (using JobPosting model) # Job URLs (using JobPosting model)
path('jobs/', views_frontend.JobListView.as_view(), name='job_list'), path('jobs/', views_frontend.JobListView.as_view(), name='job_list'),
path('jobs/create/', views.create_job, name='job_create'), path('jobs/create/', views.create_job, name='job_create'),
path('job/<slug:slug>/upload_image_simple/', views.job_image_upload, name='job_image_upload'), path('job/<slug:slug>/upload_image_simple/', views.job_image_upload, name='job_image_upload'),
path('jobs/<slug:slug>/update/', views.edit_job, name='job_update'), path('jobs/<slug:slug>/update/', views.edit_job, name='job_update'),
# path('jobs/<slug:slug>/delete/', views., name='job_delete'), # path('jobs/<slug:slug>/delete/', views., name='job_delete'),
path('jobs/<slug:slug>/', views.job_detail, name='job_detail'), path('jobs/<slug:slug>/', views.job_detail, name='job_detail'),
@ -32,7 +32,7 @@ urlpatterns = [
path('candidates/<slug:slug>/delete/', views_frontend.CandidateDeleteView.as_view(), name='candidate_delete'), path('candidates/<slug:slug>/delete/', views_frontend.CandidateDeleteView.as_view(), name='candidate_delete'),
path('candidate/<slug:slug>/view/', views_frontend.candidate_detail, name='candidate_detail'), path('candidate/<slug:slug>/view/', views_frontend.candidate_detail, name='candidate_detail'),
path('candidate/<slug:slug>/update-stage/', views_frontend.candidate_update_stage, name='candidate_update_stage'), path('candidate/<slug:slug>/update-stage/', views_frontend.candidate_update_stage, name='candidate_update_stage'),
# Training URLs # Training URLs
path('training/', views_frontend.TrainingListView.as_view(), name='training_list'), path('training/', views_frontend.TrainingListView.as_view(), name='training_list'),
@ -64,11 +64,14 @@ urlpatterns = [
path('forms/builder/<int:template_id>/', views.form_builder, name='form_builder'), path('forms/builder/<int:template_id>/', views.form_builder, name='form_builder'),
path('forms/', views.form_templates_list, name='form_templates_list'), path('forms/', views.form_templates_list, name='form_templates_list'),
path('forms/create-template/', views.create_form_template, name='create_form_template'), path('forms/create-template/', views.create_form_template, name='create_form_template'),
path('jobs/<slug:slug>/candidate-tiers/', views.candidate_tier_management_view, name='candidate_tier_management'),
path('htmx/<int:pk>/candidate_criteria_view/', views.candidate_criteria_view_htmx, name='candidate_criteria_view_htmx'),
# path('forms/form/<int:template_id>/submit/', views.submit_form, name='submit_form'), # path('forms/form/<int:template_id>/submit/', views.submit_form, name='submit_form'),
# path('forms/form/<int:template_id>/', views.form_wizard_view, name='form_wizard'), # path('forms/form/<int:template_id>/', views.form_wizard_view, name='form_wizard'),
path('forms/<int:form_id>/submissions/<int:submission_id>/', views.form_submission_details, name='form_submission_details'), path('forms/<int:template_id>/submissions/<slug:slug>/', views.form_submission_details, name='form_submission_details'),
path('forms/template/<slug:slug>/submissions/', views.form_template_submissions_list, name='form_template_submissions_list'), path('forms/template/<slug:slug>/submissions/', views.form_template_submissions_list, name='form_template_submissions_list'),
path('forms/template/<int:template_id>/all-submissions/', views.form_template_all_submissions, name='form_template_all_submissions'),
# path('forms/<int:form_id>/', views.form_preview, name='form_preview'), # path('forms/<int:form_id>/', views.form_preview, name='form_preview'),
# path('forms/<int:form_id>/submit/', views.form_submit, name='form_submit'), # path('forms/<int:form_id>/submit/', views.form_submit, name='form_submit'),

View File

@ -991,13 +991,38 @@ def form_template_submissions_list(request, slug):
) )
def form_submission_details(request, template_id, submission_id): def form_template_all_submissions(request, template_id):
"""Display all submissions for a form template in table format"""
template = get_object_or_404(FormTemplate, id=template_id)
print(template)
# Get all submissions for this template
submissions = FormSubmission.objects.filter(template=template).order_by("-submitted_at")
# Get all fields for this template, ordered by stage and field order
fields = FormField.objects.filter(stage__template=template).select_related('stage').order_by('stage__order', 'order')
# Pagination
paginator = Paginator(submissions, 10) # Show 10 submissions per page
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
return render(
request,
"forms/form_template_all_submissions.html",
{
"template": template,
"page_obj": page_obj,
"fields": fields,
},
)
def form_submission_details(request, template_id, slug):
"""Display detailed view of a specific form submission""" """Display detailed view of a specific form submission"""
# Get the form template and verify ownership # Get the form template and verify ownership
template = get_object_or_404(FormTemplate, id=template_id, created_by=request.user) template = get_object_or_404(FormTemplate, id=template_id)
# Get the specific submission # Get the specific submission
submission = get_object_or_404(FormSubmission, id=submission_id, template=template) submission = get_object_or_404(FormSubmission, slug=slug, template=template)
# Get all stages with their fields # Get all stages with their fields
stages = template.stages.prefetch_related("fields").order_by("order") stages = template.stages.prefetch_related("fields").order_by("order")
@ -1192,3 +1217,133 @@ def schedule_interviews_view(request, job_id):
"interviews/schedule_interviews.html", "interviews/schedule_interviews.html",
{"form": form, "break_formset": break_formset, "job": job}, {"form": form, "break_formset": break_formset, "job": job},
) )
def candidate_tier_management_view(request, slug):
"""
Manage candidate tiers and stage transitions
"""
job = get_object_or_404(JobPosting, slug=slug)
# Get all candidates for this job, ordered by match score (descending)
candidates = job.candidates.all().order_by("-match_score")
# Get tier categorization parameters
tier1_count = int(request.GET.get("tier1_count", 100))
# Categorize candidates into tiers
tier1_candidates = candidates[:tier1_count] if tier1_count > 0 else []
remaining_candidates = candidates[tier1_count:] if tier1_count > 0 else []
if len(remaining_candidates) > 0:
# Tier 2: Next 50% of remaining candidates
tier2_count = max(1, len(remaining_candidates) // 2)
tier2_candidates = remaining_candidates[:tier2_count]
tier3_candidates = remaining_candidates[tier2_count:]
else:
tier2_candidates = []
tier3_candidates = []
# Handle form submissions
if request.method == "POST":
# Update tier categorization
if "update_tiers" in request.POST:
tier1_count = int(request.POST.get("tier1_count", 100))
messages.success(request, f"Tier categorization updated. Tier 1: {tier1_count} candidates")
return redirect("candidate_tier_management", slug=slug)
# Update individual candidate stages
elif "update_stage" in request.POST:
candidate_id = request.POST.get("candidate_id")
new_stage = request.POST.get("new_stage")
candidate = get_object_or_404(Candidate, id=candidate_id, job=job)
if candidate.can_transition_to(new_stage):
old_stage = candidate.stage
candidate.stage = new_stage
candidate.save()
messages.success(request, f"Updated {candidate.name} from {old_stage} to {new_stage}")
else:
messages.error(request, f"Cannot transition {candidate.name} from {candidate.stage} to {new_stage}")
# Update exam status
elif "update_exam_status" in request.POST:
candidate_id = request.POST.get("candidate_id")
exam_status = request.POST.get("exam_status")
exam_date = request.POST.get("exam_date")
candidate = get_object_or_404(Candidate, id=candidate_id, job=job)
if candidate.stage == "Exam":
candidate.exam_status = exam_status
if exam_date:
candidate.exam_date = exam_date
candidate.save()
messages.success(request, f"Updated exam status for {candidate.name}")
else:
messages.error(request, f"Can only update exam status for candidates in Exam stage")
# Bulk stage update
elif "bulk_update_stage" in request.POST:
selected_candidates = request.POST.getlist("selected_candidates")
new_stage = request.POST.get("bulk_new_stage")
updated_count = 0
for candidate_id in selected_candidates:
candidate = get_object_or_404(Candidate, id=candidate_id, job=job)
if candidate.can_transition_to(new_stage):
candidate.stage = new_stage
candidate.save()
updated_count += 1
messages.success(request, f"Updated {updated_count} candidates to {new_stage} stage")
# Mark individual candidate as Candidate
elif "mark_as_candidate" in request.POST:
candidate_id = request.POST.get("candidate_id")
candidate = get_object_or_404(Candidate, id=candidate_id, job=job)
if candidate.applicant_status == "Applicant":
candidate.applicant_status = "Candidate"
candidate.save()
messages.success(request, f"Marked {candidate.name} as Candidate")
else:
messages.info(request, f"{candidate.name} is already marked as Candidate")
# Mark all Tier 1 candidates as Candidates
elif "mark_as_candidates" in request.POST:
updated_count = 0
for candidate in tier1_candidates:
if candidate.applicant_status == "Applicant":
candidate.applicant_status = "Candidate"
candidate.save()
updated_count += 1
if updated_count > 0:
messages.success(request, f"Marked {updated_count} Tier 1 candidates as Candidates")
else:
messages.info(request, "All Tier 1 candidates are already marked as Candidates")
# Group candidates by current stage for display
stage_groups = {
"Applied": candidates.filter(stage="Applied"),
"Exam": candidates.filter(stage="Exam"),
"Interview": candidates.filter(stage="Interview"),
"Offer": candidates.filter(stage="Offer"),
}
context = {
"job": job,
"tier1_candidates": tier1_candidates,
"tier2_candidates": tier2_candidates,
"tier3_candidates": tier3_candidates,
"stage_groups": stage_groups,
"tier1_count": tier1_count,
"total_candidates": candidates.count(),
}
return render(request, "recruitment/candidate_tier_management.html", context)
def candidate_criteria_view_htmx(request, pk):
candidate = get_object_or_404(Candidate, pk=pk)
print(candidate)
return render(request, "includes/candidate_modal_body.html", {"candidate": candidate})

View File

@ -223,7 +223,7 @@ def candidate_detail(request, slug):
stage_form = forms.CandidateStageForm(candidate=candidate) stage_form = forms.CandidateStageForm(candidate=candidate)
# 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(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]) # parsed = json_to_markdown_table([parsed])
return render(request, 'recruitment/candidate_detail.html', { return render(request, 'recruitment/candidate_detail.html', {
'candidate': candidate, 'candidate': candidate,
'parsed': parsed, 'parsed': parsed,

View File

@ -9,8 +9,8 @@
<div class="col-12"> <div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-3"> <div class="d-flex justify-content-between align-items-center mb-3">
<h2>Submission Details</h2> <h2>Submission Details</h2>
<a href="" class="btn btn-outline-secondary"> <a href="{% url 'form_template_submissions_list' template.slug %}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> Back <i class="fas fa-arrow-left"></i> Back to Submissions
</a> </a>
</div> </div>
</div> </div>
@ -51,62 +51,79 @@
<div class="card"> <div class="card">
<div class="card-body"> <div class="card-body">
<h5 class="mb-3">Responses</h5> <h5 class="mb-3">Responses</h5>
{% get_all_responses_flat stage_responses as all_responses %} {% with submission=submission %}
{% if all_responses %} {% get_all_responses_flat submission as flat_responses %}
<div class="table-responsive">
<table class="table table-striped"> {% if flat_responses %}
<thead> <div class="table-responsive">
<tr> <table class="table table-striped table-bordered">
<th>Field Label</th> <thead class="table-light">
<th>Response Value</th> <tr>
<th>File</th> <th scope="col" style="width: 150px;">Field Label</th>
</tr> {% for response in flat_responses %}
</thead> <th scope="col">{{ response.field_label }}</th>
<tbody> {% endfor %}
{% for response in all_responses %} </tr>
<tr> </thead>
<td> <tbody>
<strong>{{ response.field_label }}</strong> <tr>
{% if response.required %} <td><strong>Response Value</strong></td>
<span class="text-danger">*</span> {% for response in flat_responses %}
{% endif %} <td>
</td> {% if response.uploaded_file %}
<td> <div>
{% if response.uploaded_file %} <span class="text-primary"><i class="fas fa-file"></i> {{ response.uploaded_file.name }}</span>
<span class="text-primary">File: {{ response.uploaded_file.name }}</span> <a href="{{ response.uploaded_file.url }}" class="btn btn-sm btn-outline-primary ms-2" target="_blank" title="Download File">
{% elif response.value %} <i class="fas fa-download"></i>
{% if response.field_type == 'checkbox' and response.value|length > 0 %} </a>
<div> </div>
{% for val in response.value %} {% elif response.value %}
<span class="badge bg-secondary me-1">{{ val }}</span> {% if response.field_type == 'checkbox' and response.value|length > 0 %}
{% endfor %} <div>
</div> {% for val in response.value %}
{% elif response.field_type == 'radio' or response.field_type == 'select' %} <span class="badge bg-secondary me-1">{{ val }}</span>
<span class="badge bg-info">{{ response.value }}</span> {% endfor %}
{% else %} </div>
<p class="mb-0">{{ response.value|linebreaksbr }}</p> {% elif response.field_type == 'radio' or response.field_type == 'select' %}
{% endif %} <span class="badge bg-info">{{ response.value }}</span>
{% else %} {% else %}
<span class="text-muted">Not provided</span> <p class="mb-0">{{ response.value|linebreaksbr }}</p>
{% endif %} {% endif %}
</td> {% else %}
<td> <span class="text-muted">Not provided</span>
{% if response.uploaded_file %} {% endif %}
<a href="{{ response.uploaded_file.url }}" class="btn btn-sm btn-outline-primary" target="_blank"> </td>
<i class="fas fa-download"></i> Download {% endfor %}
</a> </tr>
{% endif %} <tr>
</td> <td><strong>Stage</strong></td>
</tr> {% for response in flat_responses %}
{% endfor %} <td>
</tbody> {{ response.stage_name|default:"N/A" }}
</table> </td>
</div> {% endfor %}
{% else %} </tr>
<div class="text-center text-muted py-4"> <tr>
<p>No responses found for this submission.</p> <td><strong>Required</strong></td>
</div> {% for response in flat_responses %}
{% endif %} <td>
{% if response.required %}
<span class="text-danger"><i class="fas fa-asterisk"></i> Yes</span>
{% else %}
<span>No</span>
{% endif %}
</td>
{% endfor %}
</tr>
</tbody>
</table>
</div>
{% else %}
<div class="text-center text-muted py-4">
<p>No responses found for this submission.</p>
</div>
{% endif %}
{% endwith %}
</div> </div>
</div> </div>
</div> </div>
@ -119,6 +136,8 @@
border-top: none; border-top: none;
font-weight: 600; font-weight: 600;
color: #495057; color: #495057;
vertical-align: top;
white-space: nowrap;
} }
.table td { .table td {
vertical-align: top; vertical-align: top;
@ -126,5 +145,17 @@
.response-value { .response-value {
max-width: 300px; max-width: 300px;
} }
.table th:first-child,
.table td:first-child {
background-color: #f8f9fa;
font-weight: 600;
}
.table-striped > tbody > tr:nth-of-type(odd) > td {
background-color: rgba(0, 0, 0, 0.02);
}
.table-bordered th,
.table-bordered td {
border: 1px solid #dee2e6;
}
</style> </style>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,371 @@
{% extends 'base.html' %}
{% load static i18n form_filters %}
{% load partials %}
{% block title %}All Submissions for {{ template.name }} - ATS{% endblock %}
{% block customCSS %}
<style>
/* ================================================= */
/* UI Variables (Matching Form Templates List) */
/* ================================================= */
:root {
--kaauh-teal: #00636e;
--kaauh-teal-dark: #004a53;
--kaauh-border: #eaeff3;
--kaauh-primary-text: #343a40;
--kaauh-gray-light: #f8f9fa;
}
/* --- Typography and Color Overrides --- */
.text-primary { color: var(--kaauh-teal) !important; }
/* --- Button Base Styles (Matching Form Templates List) --- */
.btn-main-action {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
font-weight: 600;
padding: 0.375rem 0.75rem;
border-radius: 0.5rem;
transition: all 0.2s ease;
display: inline-flex;
align-items: center;
gap: 0.5rem;
}
.btn-main-action:hover {
background-color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal-dark);
transform: translateY(-1px);
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
color: white;
}
/* Secondary Button Style (for Edit/Preview) */
.btn-outline-secondary {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-teal);
}
.btn-outline-secondary:hover {
background-color: var(--kaauh-teal-dark);
color: white;
border-color: var(--kaauh-teal-dark);
}
/* Size Utilities (matching Bootstrap convention) */
.btn-lg {
padding: 0.75rem 1.5rem;
font-size: 1.1rem;
}
.btn-sm {
font-size: 0.8rem;
padding: 0.3rem 0.6rem;
}
/* --- Card and Layout Styles (Matching Form Templates List) --- */
.card {
border: 1px solid var(--kaauh-border);
border-radius: 0.75rem;
box-shadow: 0 4px 12px rgba(0,0,0,0.06);
background-color: white;
transition: transform 0.2s, box-shadow 0.2s;
}
.card-header {
background-color: var(--kaauh-teal-dark) !important;
border-bottom: 1px solid var(--kaauh-border);
color: white !important;
font-weight: 600;
padding: 1rem 1.25rem;
border-radius: 0.75rem 0.75rem 0 0;
}
.card-header h1 {
color: white !important;
font-weight: 700;
font-size: 1.5rem;
}
.card-header .fas {
color: white !important;
}
.card-header .small {
color: rgba(255, 255, 255, 0.7) !important;
}
.card-body {
padding: 1.25rem;
}
/* --- Compact Table Styles --- */
.table-responsive {
border-radius: 0.5rem;
overflow: auto;
max-height: 70vh;
display: flex;
flex-direction: column;
}
.table {
margin-bottom: 0;
min-width: max-content;
}
.table thead {
position: sticky;
top: 0;
z-index: 10;
}
.table thead th {
background-color: var(--kaauh-teal-dark);
color: white;
border-color: var(--kaauh-border);
font-weight: 600;
text-transform: uppercase;
font-size: 0.7rem;
letter-spacing: 0.3px;
padding: 0.5rem 0.75rem;
white-space: nowrap;
}
.table tbody td {
padding: 0.5rem 0.75rem;
vertical-align: middle;
border-color: var(--kaauh-border);
font-size: 0.9rem;
}
.table tbody tr {
transition: background-color 0.2s;
}
.table tbody tr:hover {
background-color: var(--kaauh-gray-light);
}
/* Compact form elements */
.file-response {
display: flex;
align-items: center;
gap: 0.25rem;
}
.badge-response {
margin: 0.05rem;
font-size: 0.75rem;
padding: 0.2rem 0.4rem;
}
.response-value p {
margin: 0;
font-size: 0.85rem;
}
.btn-sm {
font-size: 0.75rem;
padding: 0.2rem 0.4rem;
}
/* --- Pagination Styling (Matching Form Templates List) --- */
.pagination .page-item .page-link {
color: var(--kaauh-teal-dark);
border-color: var(--kaauh-border);
}
.pagination .page-item.active .page-link {
background-color: var(--kaauh-teal);
border-color: var(--kaauh-teal);
color: white;
}
.pagination .page-item:hover .page-link:not(.active) {
background-color: #e9ecef;
}
.pagination-info {
color: var(--kaauh-primary-text);
font-size: 0.9rem;
}
/* --- Empty State Theming --- */
.empty-state {
text-align: center;
padding: 3rem 1rem;
color: var(--kaauh-primary-text);
border: 2px dashed var(--kaauh-border);
border-radius: 0.75rem;
background-color: var(--kaauh-gray-light);
}
.empty-state i {
font-size: 3.5rem;
margin-bottom: 1rem;
color: var(--kaauh-teal-dark);
}
.empty-state .btn-main-action .fas {
color: white !important;
}
/* --- Breadcrumb --- */
.breadcrumb {
background-color: transparent;
padding: 0;
margin-bottom: 1rem;
}
.breadcrumb-item a {
color: var(--kaauh-teal-dark);
text-decoration: none;
}
.breadcrumb-item a:hover {
text-decoration: underline;
}
.breadcrumb-item.active {
color: var(--kaauh-primary-text);
}
/* --- Response Value Styling --- */
.response-value {
word-break: break-word;
max-width: 200px;
}
.file-response {
display: flex;
align-items: center;
gap: 0.5rem;
}
.badge-response {
margin: 0.1rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container py-4">
<nav aria-label="breadcrumb">
<ol class="breadcrumb">
<li class="breadcrumb-item"><a href="{% url 'dashboard' %}">{% trans "Dashboard" %}</a></li>
<li class="breadcrumb-item"><a href="{% url 'form_templates_list' %}">{% trans "Form Templates" %}</a></li>
<li class="breadcrumb-item"><a href="{% url 'form_template_submissions_list' template.slug %}">{% trans "Submissions" %}</a></li>
<li class="breadcrumb-item active">{% trans "All Submissions Table" %}</li>
</ol>
</nav>
<div class="card shadow-sm">
<div class="card-header d-flex justify-content-between align-items-center">
<div>
<h1 class="h3 mb-1 d-flex align-items-center">
<i class="fas fa-table me-2"></i>
{% trans "All Submissions for" %}: <span class="text-white ms-2">{{ template.name }}</span>
</h1>
<small class="text-white-50">Template ID: #{{ template.id }}</small>
</div>
<a href="{% url 'form_template_submissions_list' template.slug %}" class="btn btn-outline-light btn-sm">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Submissions" %}
</a>
</div>
<div class="card-body">
{% if page_obj.object_list %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th scope="col">{% trans "Submission ID" %}</th>
<th scope="col">{% trans "Applicant Name" %}</th>
<th scope="col">{% trans "Applicant Email" %}</th>
<th scope="col">{% trans "Submitted At" %}</th>
{% for field in fields %}
<th scope="col">{{ field.label }}</th>
{% endfor %}
</tr>
</thead>
<tbody>
{% for submission in page_obj %}
<tr>
<td class="fw-medium">{{ submission.id }}</td>
<td>{{ submission.applicant_name|default:"N/A" }}</td>
<td>{{ submission.applicant_email|default:"N/A" }}</td>
<td>{{ submission.submitted_at|date:"M d, Y H:i" }}</td>
{% for field in fields %}
{% get_field_response_for_submission submission field as response %}
<td class="response-value">
{% if response %}
{% if response.uploaded_file %}
<div class="file-response">
<a href="{{ response.uploaded_file.url }}" class="btn btn-sm btn-outline-primary" target="_blank" title="Download File">
<i class="fas fa-download"></i>
</a>
</div>
{% elif response.value %}
{% if response.field.field_type == 'checkbox' and response.value|length > 0 %}
<div>
{% for val in response.value|to_list %}
<span class="badge bg-secondary badge-response">{{ val }}</span>
{% endfor %}
</div>
{% elif response.field.field_type == 'radio' or response.field.field_type == 'select' %}
<span class="badge bg-info">{{ response.value }}</span>
{% else %}
<p class="mb-0">{{ response.value|linebreaksbr|truncatewords:10 }}</p>
{% endif %}
{% else %}
<span class="text-muted">Not provided</span>
{% endif %}
{% else %}
<span class="text-muted">Not provided</span>
{% endif %}
</td>
{% endfor %}
</tr>
{% endfor %}
</tbody>
</table>
</div>
<!-- Pagination -->
{% if page_obj.has_other_pages %}
<div class="d-flex flex-column flex-md-row justify-content-between align-items-center mt-4">
<div class="pagination-info mb-3 mb-md-0">
{% blocktrans with start=page_obj.start_index end=page_obj.end_index total=page_obj.paginator.count %}
Showing {{ start }} to {{ end }} of {{ total }} results.
{% endblocktrans %}
</div>
<nav aria-label="Page navigation">
<ul class="pagination pagination-sm mb-0">
{% if page_obj.has_previous %}
<li class="page-item">
<a class="page-link" href="?page=1" aria-label="First">
<span aria-hidden="true">&laquo;</span>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.previous_page_number }}" aria-label="Previous">
<span aria-hidden="true">&lsaquo;</span>
</a>
</li>
{% endif %}
<li class="page-item active">
<span class="page-link">
{% trans "Page" %} {{ page_obj.number }} {% trans "of" %} {{ page_obj.paginator.num_pages }}
</span>
</li>
{% if page_obj.has_next %}
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.next_page_number }}" aria-label="Next">
<span aria-hidden="true">&rsaquo;</span>
</a>
</li>
<li class="page-item">
<a class="page-link" href="?page={{ page_obj.paginator.num_pages }}" aria-label="Last">
<span aria-hidden="true">&raquo;</span>
</a>
</li>
{% endif %}
</ul>
</nav>
</div>
{% endif %}
{% else %}
<div class="empty-state">
<i class="fas fa-inbox"></i>
<h3 class="h5 mb-3">{% trans "No Submissions Found" %}</h3>
<p class="text-muted mb-4">
{% trans "There are no submissions for this form template yet." %}
</p>
<a href="{% url 'form_template_submissions_list' template.slug %}" class="btn btn-main-action">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Submissions" %}
</a>
</div>
{% endif %}
</div>
</div>
</div>
{% endblock %}

View File

@ -200,9 +200,14 @@
</h1> </h1>
<small class="text-white-50">Template ID: #{{ template.id }}</small> <small class="text-white-50">Template ID: #{{ template.id }}</small>
</div> </div>
<a href="{% url 'form_templates_list' %}" class="btn btn-outline-light btn-sm"> <div class="d-flex gap-2">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Templates" %} <a href="{% url 'form_template_all_submissions' template.id %}" class="btn btn-outline-light btn-sm">
</a> <i class="fas fa-table me-1"></i> {% trans "View All in Table" %}
</a>
<a href="{% url 'form_templates_list' %}" class="btn btn-outline-light btn-sm">
<i class="fas fa-arrow-left me-1"></i> {% trans "Back to Templates" %}
</a>
</div>
</div> </div>
<div class="card-body"> <div class="card-body">
{% if page_obj.object_list %} {% if page_obj.object_list %}
@ -231,10 +236,10 @@
<td>{{ submission.applicant_email|default:"N/A" }}</td> <td>{{ submission.applicant_email|default:"N/A" }}</td>
<td>{{ submission.submitted_at|date:"M d, Y H:i" }}</td> <td>{{ submission.submitted_at|date:"M d, Y H:i" }}</td>
<td class="text-end"> <td class="text-end">
<a href="{% url 'form_submission_details' template_id=template.id submission_id=submission.id %}" class="btn btn-sm btn-outline-primary"> <a href="{% url 'form_submission_details' template_id=submission.template.id slug=submission.slug %}" class="btn btn-sm btn-outline-primary">
<i class="fas fa-eye me-1"></i> {% trans "View Details" %} <i class="fas fa-eye me-1"></i> {% trans "View Details" %}
</a> </a>
</td> </td>
</tr> </tr>
{% endfor %} {% endfor %}
</tbody> </tbody>
@ -260,7 +265,7 @@
</p> </p>
</div> </div>
<div class="card-footer"> <div class="card-footer">
<a href="{% url 'form_submission_details' template_id=template.id submission_id=submission.id %}" class="btn btn-sm btn-outline-primary w-100"> <a href="{% url 'form_submission_details' template_id=template.id slug=submission.slug %}" class="btn btn-sm btn-outline-primary w-100">
<i class="fas fa-eye me-1"></i> {% trans "View Details" %} <i class="fas fa-eye me-1"></i> {% trans "View Details" %}
</a> </a>
</div> </div>

View File

@ -881,17 +881,28 @@
formData.append('csrfmiddlewaretoken', csrfToken); formData.append('csrfmiddlewaretoken', csrfToken);
// Add field responses // Add field responses
state.stages.forEach(stage => { state.stages.forEach(stage => {
stage.fields.forEach(field => { stage.fields.forEach(field => {
const value = state.formData[field.id]; const value = state.formData[field.id];
if (value !== undefined && value !== null) {
if (field.type === 'file' && value instanceof File) { // Always include the field, even if it's empty
if (field.type === 'file') {
if (value instanceof File) {
formData.append(`field_${field.id}`, value); formData.append(`field_${field.id}`, value);
} else if (field.type === 'checkbox') { } else {
// Include empty file field
formData.append(`field_${field.id}`, '');
}
} else if (field.type === 'checkbox') {
// For checkboxes, send empty array if no selection
if (Array.isArray(value) && value.length > 0) {
formData.append(`field_${field.id}`, JSON.stringify(value)); formData.append(`field_${field.id}`, JSON.stringify(value));
} else { } else {
formData.append(`field_${field.id}`, value); formData.append(`field_${field.id}`, JSON.stringify([]));
} }
} else {
// For other field types, send the value or empty string
formData.append(`field_${field.id}`, value || '');
} }
}); });
}); });

View File

@ -0,0 +1,21 @@
{% load i18n %}
<h5 class="modal-title" id="candidateviewModalLabel">{{ candidate.name }} - {% trans "Score" %}: <span class="badge bg-success"> {{ candidate.match_score }} </span></h5>
<div class="mb-3">
<label class="form-label">{% trans "Strengths" %}</label>
<textarea class="form-control" rows="3" readonly>{{ candidate.strengths }}</textarea>
</div>
<div class="mb-3">
<label class="form-label">{% trans "Weaknesses" %}</label>
<textarea class="form-control" rows="3" readonly>{{ candidate.weaknesses }}</textarea>
</div>
<div class="mb-3">
<label class="form-label">{% trans "Criteria Checklist" %}</label>
<ul class="list-group">
{% for key, value in candidate.criteria_checklist.items %}
<li class="list-group-item d-flex justify-content-between">
<span>{{ key }}</span>
<span class="badge bg-{{ value|yesno:"success,danger" }}">{{ value|yesno:"Yes,No" }}</span>
</li>
{% endfor %}
</ul>
</div>

View File

@ -136,7 +136,7 @@
} }
.right-column-tabs .nav-link { .right-column-tabs .nav-link {
padding: 0.9rem 1rem; padding: 0.9rem 1rem;
font-size: 0.95rem; font-size: 0.95rem;
font-weight: 600; font-weight: 600;
color: var(--kaauh-primary-text); color: var(--kaauh-primary-text);
border-radius: 0; border-radius: 0;
@ -409,11 +409,11 @@
<i class="fas fa-edit"></i> {% trans "Edit Job" %} <i class="fas fa-edit"></i> {% trans "Edit Job" %}
</a> </a>
<button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#myModalForm"> <button type="button" class="btn btn-outline-secondary" data-bs-toggle="modal" data-bs-target="#myModalForm">
<i class="fas fa-image me-1"></i> {% trans "Upload Image for Post" %} <i class="fas fa-image me-1"></i> {% trans "Upload Image for Post" %}
</button> </button>
</div> </div>
</div> </div>
</div> </div>
@ -472,9 +472,9 @@
{% trans "View All Applicants" %} ({{ total_candidates }}) {% trans "View All Applicants" %} ({{ total_candidates }})
</a> </a>
</div> </div>
{% endif %} {% endif %}
<div class="d-grid gap-2"> <div class="d-grid gap-2">
<a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action"> <a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action">
<i class="fas fa-user-plus"></i> {% trans "Create Candidate" %} <i class="fas fa-user-plus"></i> {% trans "Create Candidate" %}
@ -486,7 +486,7 @@
<div class="tab-pane fade" id="manage-pane" role="tabpanel" aria-labelledby="manage-tab"> <div class="tab-pane fade" id="manage-pane" role="tabpanel" aria-labelledby="manage-tab">
{# LinkedIn Integration (Content from old card) #} {# LinkedIn Integration (Content from old card) #}
{# Applicant Form Management (Content from old card) #} {# Applicant Form Management (Content from old card) #}
<h5 class="mb-3"><i class="fas fa-clipboard-list me-2 text-primary"></i>{% trans "Form Management" %}</h5> <h5 class="mb-3"><i class="fas fa-clipboard-list me-2 text-primary"></i>{% trans "Form Management" %}</h5>
@ -494,7 +494,7 @@
<p class="text-muted small mb-3"> <p class="text-muted small mb-3">
{% trans "Manage the custom application forms associated with this job posting." %} {% trans "Manage the custom application forms associated with this job posting." %}
</p> </p>
{% if not job.form_template %} {% if not job.form_template %}
<a href="{% url 'create_form_template' %}" class="btn btn-main-action"> <a href="{% url 'create_form_template' %}" class="btn btn-main-action">
<i class="fas fa-plus-circle me-2"></i> {% trans "Create New Form Template" %} <i class="fas fa-plus-circle me-2"></i> {% trans "Create New Form Template" %}
@ -505,7 +505,7 @@
</a> </a>
{% endif %} {% endif %}
</div> </div>
</div> </div>
@ -551,6 +551,43 @@
</div> </div>
{% endif %} {% endif %}
</div> </div>
{# Applicant Form Management (Content from old card) #}
<h5 class="mb-3"><i class="fas fa-clipboard-list me-2 text-primary"></i>{% trans "Form Management" %}</h5>
<div class="d-grid gap-2">
<p class="text-muted small mb-3">
{% trans "Manage the custom application forms associated with this job posting." %}
</p>
<a href="{% url 'create_form_template' %}" class="btn btn-main-action">
<i class="fas fa-plus-circle me-2"></i> {% trans "Create New Form" %}
</a>
<a href="" class="btn btn-outline-secondary">
<i class="fas fa-list-alt me-1"></i> {% trans "View All Existing Forms" %}
</a>
<a href="{% url 'candidate_create_for_job' job.slug %}" class="btn btn-main-action">
<i class="fas fa-user-plus"></i> {% trans "Create Candidate" %}
</a>
<a href="{% url 'candidate_tier_management' job.slug %}" class="btn btn-main-action">
<i class="fas fa-layer-group"></i> {% trans "Manage Tiers" %}
</a>
</div>
</div>
{# TAB 3: INTERNAL INFO CONTENT #}
<div class="tab-pane fade" id="internal-pane" role="tabpanel" aria-labelledby="internal-tab">
<h5 class="mb-3"><i class="fas fa-info-circle me-2 text-secondary"></i>{% trans "Internal Information" %}</h5>
<div class="small">
<p class="mb-1"><strong>{% trans "Internal Job ID:" %}</strong> {{ job.internal_job_id }}</p>
<p class="mb-1"><strong>{% trans "Created:" %}</strong> {{ job.created_at|date:"M d, Y" }}</p>
<p class="mb-1"><strong>{% trans "Last Updated:" %}</strong> {{ job.updated_at|date:"M d, Y" }}</p>
{% if job.reporting_to %}
<p class="mb-0"><strong>{% trans "Reports To:" %}</strong> {{ job.reporting_to }}</p>
{% endif %}
</div>
<div class="mt-4"> <div class="mt-4">
<a href="{% url 'job_list' %}" class="btn btn-outline-secondary w-100"> <a href="{% url 'job_list' %}" class="btn btn-outline-secondary w-100">
<i class="fas fa-arrow-left"></i> {% trans "Back to Jobs" %} <i class="fas fa-arrow-left"></i> {% trans "Back to Jobs" %}
@ -598,4 +635,4 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}

View File

@ -0,0 +1,724 @@
{% extends 'base.html' %}
{% load static i18n %}
{% block title %}Candidate Tier Management - {{ job.title }} - ATS{% endblock %}
{% block customCSS %}
<style>
/* Minimal Tier Management Styles */
.tier-controls {
background-color: #f8f9fa;
padding: 1rem;
border-radius: 0.375rem;
margin-bottom: 1.5rem;
}
.tier-controls .form-row {
display: flex;
align-items: end;
gap: 0.75rem;
}
.tier-controls .form-group {
flex: 1;
margin-bottom: 0;
}
.bulk-update-controls {
background-color: #f8f9fa;
padding: 1rem;
border-radius: 0.375rem;
margin-bottom: 1.5rem;
}
.stage-groups {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(300px, 1fr));
gap: 1rem;
margin-bottom: 2rem;
}
.stage-group {
border: 1px solid #dee2e6;
border-radius: 0.375rem;
overflow: hidden;
}
.stage-group .stage-header {
background-color: #495057;
color: white;
padding: 0.5rem 0.75rem;
font-weight: 500;
font-size: 0.95rem;
}
.stage-group .stage-body {
padding: 0.75rem;
min-height: 80px;
}
.stage-candidate {
padding: 0.375rem;
border-bottom: 1px solid #f1f3f4;
}
.stage-candidate:last-child {
border-bottom: none;
}
.match-score {
font-weight: 600;
color: #0056b3;
}
.btn-sm {
font-size: 0.75rem;
padding: 0.2rem 0.4rem;
}
/* Tab Styles for Tiers */
.nav-tabs {
border-bottom: 1px solid #dee2e6;
margin-bottom: 1rem;
}
.nav-tabs .nav-link {
border: none;
color: #495057;
font-weight: 500;
padding: 0.5rem 1rem;
transition: all 0.2s;
}
.nav-tabs .nav-link:hover {
border: none;
background-color: #f8f9fa;
}
.nav-tabs .nav-link.active {
color: #495057;
background-color: #fff;
border: none;
border-bottom: 2px solid #007bff;
font-weight: 600;
}
.tier-1 .nav-link {
color: #155724;
}
.tier-1 .nav-link.active {
border-bottom-color: #28a745;
}
.tier-2 .nav-link {
color: #856404;
}
.tier-2 .nav-link.active {
border-bottom-color: #ffc107;
}
.tier-3 .nav-link {
color: #721c24;
}
.tier-3 .nav-link.active {
border-bottom-color: #dc3545;
}
/* Candidate Table Styles */
.candidate-table {
width: 100%;
border-collapse: separate;
border-spacing: 0;
background-color: white;
border-radius: 0.375rem;
overflow: hidden;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.candidate-table thead {
background-color: #f8f9fa;
}
.candidate-table th {
padding: 0.75rem;
text-align: left;
font-weight: 600;
font-size: 0.875rem;
color: #495057;
border-bottom: 1px solid #dee2e6;
}
.candidate-table td {
padding: 0.75rem;
border-bottom: 1px solid #f1f3f4;
vertical-align: middle;
}
.candidate-table tbody tr:hover {
background-color: #f8f9fa;
}
.candidate-table tbody tr:last-child td {
border-bottom: none;
}
.candidate-name {
font-weight: 600;
font-size: 0.95rem;
}
.candidate-details {
font-size: 0.8rem;
color: #6c757d;
}
.candidate-table-responsive {
overflow-x: auto;
margin-bottom: 1rem;
}
.stage-badge {
padding: 0.125rem 0.5rem;
border-radius: 0.5rem;
font-size: 0.7rem;
font-weight: 600;
margin-left: 0.375rem;
}
.stage-Applied {
background-color: #e9ecef;
color: #495057;
}
.stage-Exam {
background-color: #cce5ff;
color: #004085;
}
.stage-Interview {
background-color: #d1ecf1;
color: #0c5460;
}
.stage-Offer {
background-color: #d4edda;
color: #155724;
}
.exam-controls {
display: flex;
align-items: center;
gap: 0.375rem;
margin-top: 0.375rem;
}
.exam-controls select,
.exam-controls input {
font-size: 0.75rem;
padding: 0.125rem 0.25rem;
}
.tier-badge {
font-size: 0.7rem;
padding: 0.125rem 0.375rem;
border-radius: 0.25rem;
background-color: rgba(0,0,0,0.1);
color: #495057;
margin-left: 0.375rem;
}
</style>
{% endblock %}
{% block content %}
<div class="container py-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<div>
<h1 class="h3 mb-1">
<i class="fas fa-layer-group me-2"></i>
{% trans "Candidate Tier Management" %} - {{ job.title }}
</h1>
<p class="text-muted mb-0">
Total Candidates: {{ total_candidates }}
</p>
</div>
<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>
</div>
<!-- Tier Controls -->
<div class="tier-controls">
<form method="post" class="mb-0">
{% csrf_token %}
<div class="form-row">
<div class="form-group">
<label for="tier1_count">{% trans "Number of candidates in Tier 1 (Top N)" %}</label>
<input type="number" name="tier1_count" id="tier1_count" class="form-control"
value="{{ tier1_count }}" min="1" max="{{ total_candidates }}">
</div>
<div class="form-group">
<button type="submit" name="update_tiers" class="btn btn-primary">
<i class="fas fa-sync-alt me-1"></i> {% trans "Update Tiers" %}
</button>
</div>
</div>
</form>
</div>
<!-- Bulk Stage Update Controls -->
<div class="bulk-update-controls">
<h4 class="h5 mb-3">{% trans "Bulk Stage Update" %}</h4>
<form method="post">
{% csrf_token %}
<div class="form-row align-items-end">
<div class="form-group">
<label for="bulk_new_stage">{% trans "Update Selected Candidates to" %}</label>
<select name="bulk_new_stage" id="bulk_new_stage" class="form-control" required>
<option value="">{% trans "Select Stage" %}</option>
<option value="Exam">{% trans "Exam" %}</option>
<option value="Interview">{% trans "Interview" %}</option>
<option value="Offer">{% trans "Offer" %}</option>
</select>
</div>
<div class="form-group">
<button type="submit" name="bulk_update_stage" class="btn btn-success">
<i class="fas fa-tasks me-1"></i> {% trans "Update Selected" %}
</button>
</div>
</div>
</form>
</div>
<!-- Stage Groups -->
{% comment %} <div class="stage-groups">
{% for stage_name, stage_candidates in stage_groups.items %}
<div class="stage-group">
<div class="stage-header">
{{ stage_name }}
<span class="badge badge-light">{{ stage_candidates.count }}</span>
</div>
<div class="stage-body">
{% for candidate in stage_candidates %}
<div class="stage-candidate">
<div class="form-check">
<input class="form-check-input" type="checkbox" name="selected_candidates"
value="{{ candidate.id }}" id="candidate_{{ candidate.id }}">
<label class="form-check-label d-flex justify-content-between align-items-start"
for="candidate_{{ candidate.id }}">
<div>
<div class="fw-medium">{{ candidate.name }}</div>
<div class="text-muted small">
Score: <span class="match-score">{{ candidate.match_score|default:"0" }}</span>
</div>
</div>
<div class="d-flex align-items-center">
{% if candidate.stage == "Exam" and candidate.exam_status %}
<span class="badge bg-info">{{ candidate.get_exam_status_display }}</span>
{% endif %}
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle"
type="button" data-bs-toggle="dropdown">
{% trans "Actions" %}
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% for next_stage in candidate.get_available_stages %}
<li>
<button type="submit" name="update_stage"
class="dropdown-item btn-sm"
formaction="?candidate_id={{ candidate.id }}&new_stage={{ next_stage }}">
Move to {{ next_stage }}
</button>
</li>
{% endfor %}
{% if candidate.stage == "Exam" %}
<li><hr class="dropdown-divider"></li>
<li>
<button type="button" class="dropdown-item btn-sm"
data-bs-toggle="modal"
data-bs-target="#examModal{{ candidate.id }}">
{% trans "Update Exam Status" %}
</button>
</li>
{% endif %}
</ul>
</div>
</div>
</label>
</div>
</div>
{% empty %}
<p class="text-muted text-center py-3">{% trans "No candidates in this stage" %}</p>
{% endfor %}
</div>
</div>
{% endfor %}
</div> {% endcomment %}
<!-- Tier Display -->
<h2 class="h4 mb-3 mt-5">{% trans "Candidate Tiers" %}</h2>
<!-- Tabs for Candidate Tiers -->
<ul class="nav nav-tabs tier-1" id="candidateTiersTabs" role="tablist">
<li class="nav-item" role="presentation">
<button class="nav-link active" id="tier1-tab" data-bs-toggle="tab" data-bs-target="#tier1" type="button"
role="tab" aria-controls="tier1" aria-selected="true">
{% trans "Tier 1 - Top Candidates" %}
<span class="tier-badge ms-1">{{ tier1_candidates|length }}</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tier2-tab" data-bs-toggle="tab" data-bs-target="#tier2" type="button"
role="tab" aria-controls="tier2" aria-selected="false">
{% trans "Tier 2 - Good Candidates" %}
<span class="tier-badge ms-1">{{ tier2_candidates|length }}</span>
</button>
</li>
<li class="nav-item" role="presentation">
<button class="nav-link" id="tier3-tab" data-bs-toggle="tab" data-bs-target="#tier3" type="button"
role="tab" aria-controls="tier3" aria-selected="false">
{% trans "Tier 3 - Other Candidates" %}
<span class="tier-badge ms-1">{{ tier3_candidates|length }}</span>
</button>
</li>
</ul>
<!-- Tab Content -->
<div class="tab-content" id="candidateTiersTabContent">
<!-- Tier 1 Tab -->
<div class="tab-pane fade show active" id="tier1" role="tabpanel" aria-labelledby="tier1-tab">
{% if tier1_candidates %}
<div class="mb-3 d-flex justify-content-end">
<button type="submit" name="mark_as_candidates"
class="btn btn-sm btn-success"
onclick="return confirm('Are you sure you want to mark all Tier 1 candidates as official candidates?')">
<i class="fas fa-user-check me-1"></i> {% trans "Mark as Candidates" %}
</button>
</div>
<div class="candidate-table-responsive">
<table class="candidate-table">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Contact" %}</th>
<th>{% trans "AI Score" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Stage" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for candidate in tier1_candidates %}
<tr>
<td>
<div class="candidate-name">{{ candidate.name }}</div>
</td>
<td>
<div class="candidate-details">
Email: {{ candidate.email }}<br>
Phone: {{ candidate.phone }}<br>
</div>
</td>
<td>
<span class="badge bg-success">{{ candidate.match_score|default:"0" }}</span>
</td>
<td>
<span class="badge {% if candidate.applicant_status == 'Candidate' %}bg-success{% else %}bg-secondary{% endif %}">
{{ candidate.get_applicant_status_display }}
</span>
</td>
<td>
<span class="stage-badge stage-{{ candidate.stage }}">
{{ candidate.get_stage_display }}
</span>
{% if candidate.stage == "Exam" and candidate.exam_status %}
<br>
<span class="badge bg-info">{{ candidate.get_exam_status_display }}</span>
{% endif %}
</td>
<td>
<button class="btn btn-primary"
data-bs-toggle="modal"
data-bs-target="#candidateviewModal"
hx-get="{% url 'candidate_criteria_view_htmx' candidate.pk %}"
hx-target="#candidateviewModalBody"
>
{% include "icons/view.html" %}
{% trans "View" %}</button>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-center text-muted py-3">{% trans "No candidates in Tier 1" %}</p>
{% endif %}
</div>
<!-- Tier 2 Tab -->
<div class="tab-pane fade" id="tier2" role="tabpanel" aria-labelledby="tier2-tab">
{% if tier2_candidates %}
<div class="candidate-table-responsive">
<table class="candidate-table">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Contact" %}</th>
<th>{% trans "AI Score" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Stage" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for candidate in tier2_candidates %}
<tr>
<td>
<div class="candidate-name">{{ candidate.name }}</div>
</td>
<td>
<div class="candidate-details">
Email: {{ candidate.email }}<br>
Phone: {{ candidate.phone }}<br>
</div>
</td>
<td>
<span class="badge bg-success">{{ candidate.match_score|default:"0" }}</span>
</td>
<td>
<span class="badge {% if candidate.applicant_status == 'Candidate' %}bg-success{% else %}bg-secondary{% endif %}">
{{ candidate.get_applicant_status_display }}
</span>
</td>
<td>
<span class="stage-badge stage-{{ candidate.stage }}">
{{ candidate.get_stage_display }}
</span>
{% if candidate.stage == "Exam" and candidate.exam_status %}
<br>
<span class="badge bg-info">{{ candidate.get_exam_status_display }}</span>
{% endif %}
</td>
<td>
<!-- Individual actions -->
<div class="d-flex flex-wrap gap-1 mb-1">
{% if candidate.applicant_status == 'Applicant' %}
<button type="submit" name="mark_as_candidate"
class="btn btn-sm btn-success"
formaction="?candidate_id={{ candidate.id }}&action=mark_as_candidate"
title="{% trans 'Mark as Candidate' %}">
<i class="fas fa-user-check"></i>
</button>
{% endif %}
{% for next_stage in candidate.get_available_stages %}
<button type="submit" name="update_stage"
class="btn btn-sm btn-outline-secondary"
formaction="?candidate_id={{ candidate.id }}&new_stage={{ next_stage }}"
title="{% trans 'Move to' %} {{ next_stage }}">
{{ next_stage }}
</button>
{% endfor %}
{% if candidate.stage == "Exam" %}
<button type="button" class="btn btn-sm btn-outline-info"
data-bs-toggle="modal"
data-bs-target="#examModal{{ candidate.id }}"
title="{% trans 'Update Exam Status' %}">
<i class="fas fa-clipboard-check"></i>
</button>
{% endif %}
</div>
<!-- Dropdown for additional actions -->
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button"
data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% for next_stage in candidate.get_available_stages %}
<li>
<button type="submit" name="update_stage"
class="dropdown-item btn-sm"
formaction="?candidate_id={{ candidate.id }}&new_stage={{ next_stage }}">
Move to {{ next_stage }}
</button>
</li>
{% endfor %}
{% if candidate.stage == "Exam" %}
<li><hr class="dropdown-divider"></li>
<li>
<button type="button" class="dropdown-item btn-sm"
data-bs-toggle="modal"
data-bs-target="#examModal{{ candidate.id }}">
{% trans "Update Exam Status" %}
</button>
</li>
{% endif %}
</ul>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-center text-muted py-3">{% trans "No candidates in Tier 2" %}</p>
{% endif %}
</div>
<!-- Tier 3 Tab -->
<div class="tab-pane fade" id="tier3" role="tabpanel" aria-labelledby="tier3-tab">
{% if tier3_candidates %}
<div class="candidate-table-responsive">
<table class="candidate-table">
<thead>
<tr>
<th>{% trans "Name" %}</th>
<th>{% trans "Contact" %}</th>
<th>{% trans "AI Score" %}</th>
<th>{% trans "Status" %}</th>
<th>{% trans "Stage" %}</th>
<th>{% trans "Actions" %}</th>
</tr>
</thead>
<tbody>
{% for candidate in tier3_candidates %}
<tr>
<td>
<div class="candidate-name">{{ candidate.name }}</div>
</td>
<td>
<div class="candidate-details">
Email: {{ candidate.email }}<br>
Phone: {{ candidate.phone }}<br>
</div>
</td>
<td>
<span class="badge bg-success">{{ candidate.match_score|default:"0" }}</span>
</td>
<td>
<span class="badge {% if candidate.applicant_status == 'Candidate' %}bg-success{% else %}bg-secondary{% endif %}">
{{ candidate.get_applicant_status_display }}
</span>
</td>
<td>
<span class="stage-badge stage-{{ candidate.stage }}">
{{ candidate.get_stage_display }}
</span>
{% if candidate.stage == "Exam" and candidate.exam_status %}
<br>
<span class="badge bg-info">{{ candidate.get_exam_status_display }}</span>
{% endif %}
</td>
<td>
<!-- Individual actions -->
<div class="d-flex flex-wrap gap-1 mb-1">
{% if candidate.applicant_status == 'Applicant' %}
<button type="submit" name="mark_as_candidate"
class="btn btn-sm btn-success"
formaction="?candidate_id={{ candidate.id }}&action=mark_as_candidate"
title="{% trans 'Mark as Candidate' %}">
<i class="fas fa-user-check"></i>
</button>
{% endif %}
{% for next_stage in candidate.get_available_stages %}
<button type="submit" name="update_stage"
class="btn btn-sm btn-outline-secondary"
formaction="?candidate_id={{ candidate.id }}&new_stage={{ next_stage }}"
title="{% trans 'Move to' %} {{ next_stage }}">
{{ next_stage }}
</button>
{% endfor %}
{% if candidate.stage == "Exam" %}
<button type="button" class="btn btn-sm btn-outline-info"
data-bs-toggle="modal"
data-bs-target="#examModal{{ candidate.id }}"
title="{% trans 'Update Exam Status' %}">
<i class="fas fa-clipboard-check"></i>
</button>
{% endif %}
</div>
<!-- Dropdown for additional actions -->
<div class="dropdown">
<button class="btn btn-sm btn-outline-secondary dropdown-toggle" type="button"
data-bs-toggle="dropdown">
<i class="fas fa-ellipsis-v"></i>
</button>
<ul class="dropdown-menu dropdown-menu-end">
{% for next_stage in candidate.get_available_stages %}
<li>
<button type="submit" name="update_stage"
class="dropdown-item btn-sm"
formaction="?candidate_id={{ candidate.id }}&new_stage={{ next_stage }}">
Move to {{ next_stage }}
</button>
</li>
{% endfor %}
{% if candidate.stage == "Exam" %}
<li><hr class="dropdown-divider"></li>
<li>
<button type="button" class="dropdown-item btn-sm"
data-bs-toggle="modal"
data-bs-target="#examModal{{ candidate.id }}">
{% trans "Update Exam Status" %}
</button>
</li>
{% endif %}
</ul>
</div>
</td>
</tr>
{% endfor %}
</tbody>
</table>
</div>
{% else %}
<p class="text-center text-muted py-3">{% trans "No candidates in Tier 3" %}</p>
{% endif %}
</div>
</div>
</div>
<!-- Exam Status Update Modals -->
{% for candidate in tier1_candidates|add:tier2_candidates|add:tier3_candidates %}
{% if candidate.stage == "Exam" %}
<div class="modal fade" id="examModal{{ candidate.id }}" tabindex="-1">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title">{% trans "Update Exam Status" %} - {{ candidate.name }}</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal"></button>
</div>
<div class="modal-body">
<form method="post">
{% csrf_token %}
<input type="hidden" name="candidate_id" value="{{ candidate.id }}">
<div class="mb-3">
<label class="form-label">{% trans "Exam Status" %}</label>
<select name="exam_status" class="form-select" required>
<option value="">{% trans "Select Status" %}</option>
<option value="Passed" {% if candidate.exam_status == "Passed" %}selected{% endif %}>
{% trans "Passed" %}
</option>
<option value="Failed" {% if candidate.exam_status == "Failed" %}selected{% endif %}>
{% trans "Failed" %}
</option>
</select>
</div>
<div class="mb-3">
<label class="form-label">{% trans "Exam Date" %}</label>
<input type="date" name="exam_date" class="form-control"
value="{{ candidate.exam_date|date:"Y-m-d" }}">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">
{% trans "Cancel" %}
</button>
<button type="submit" name="update_exam_status" class="btn btn-primary">
{% trans "Update Status" %}
</button>
</div>
</form>
</div>
</div>
</div>
</div>
{% endif %}
{% endfor %}
<div class="modal fade modal-lg" id="candidateviewModal" tabindex="-1" aria-labelledby="candidateviewModalLabel" aria-hidden="true">
<div class="modal-dialog">
<div class="modal-content">
<div class="modal-header">
<h5 class="modal-title" id="candidateviewModalLabel">Form Settings</h5>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div id="candidateviewModalBody" class="modal-body">
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
</div>
</div>
</div>
</div>
{% endblock %}
{% block customJS %}
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js"></script>
<script>
document.getElementById('tier1_count').addEventListener('change', function() {
const max = {{ total_candidates }};
if (this.value > max) {
this.value = max;
}
});
</script>
{% endblock customJS %}