spinner for parsing
This commit is contained in:
parent
08774489bc
commit
2cfab7c3ef
Binary file not shown.
Binary file not shown.
@ -680,36 +680,8 @@ class Candidate(Base):
|
|||||||
return future_meetings or today_future_meetings
|
return future_meetings or today_future_meetings
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def check_and_retry_ai_scoring(self):
|
def scoring_timeout(self):
|
||||||
"""
|
return timezone.now() <= (self.created_at + timezone.timedelta(minutes=5))
|
||||||
Triggers an immediate save ONLY if:
|
|
||||||
1. The resume hasn't been parsed yet.
|
|
||||||
2. At least 5 minutes have passed since the last attempt.
|
|
||||||
Returns True if a save was performed, False otherwise.
|
|
||||||
"""
|
|
||||||
from datetime import timedelta
|
|
||||||
|
|
||||||
min_delay = timedelta(minutes=5)
|
|
||||||
|
|
||||||
time_since_last_attempt = timezone.now() - self.created_at
|
|
||||||
|
|
||||||
if not self.is_resume_parsed and time_since_last_attempt >= min_delay:
|
|
||||||
|
|
||||||
# 1. Update the retry timestamp
|
|
||||||
self.last_retry_attempt = timezone.now()
|
|
||||||
|
|
||||||
|
|
||||||
self.save()
|
|
||||||
|
|
||||||
return True
|
|
||||||
|
|
||||||
return False
|
|
||||||
|
|
||||||
# @property
|
|
||||||
# def time_to_hire(self):
|
|
||||||
# time_to_hire=self.hired_date-self.created_at
|
|
||||||
# return time_to_hire
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
class TrainingMaterial(Base):
|
class TrainingMaterial(Base):
|
||||||
|
|||||||
@ -223,10 +223,17 @@ class CandidateDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
|
|||||||
|
|
||||||
|
|
||||||
def retry_scoring_view(request,slug):
|
def retry_scoring_view(request,slug):
|
||||||
if request.method == 'POST':
|
from django_q.tasks import async_task
|
||||||
candidate = get_object_or_404(models.Candidate, slug=slug)
|
|
||||||
candidate.save()
|
candidate = get_object_or_404(models.Candidate, slug=slug)
|
||||||
return redirect('candidate_detail', slug=candidate.slug)
|
|
||||||
|
async_task(
|
||||||
|
'recruitment.tasks.handle_reume_parsing_and_scoring',
|
||||||
|
candidate.pk,
|
||||||
|
hook='recruitment.hooks.callback_ai_parsing',
|
||||||
|
sync=True,
|
||||||
|
)
|
||||||
|
return redirect('candidate_detail', slug=candidate.slug)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
@ -507,6 +514,15 @@ def dashboard_view(request):
|
|||||||
rotation_degrees = rotation_percent * 180
|
rotation_degrees = rotation_percent * 180
|
||||||
rotation_degrees_final = round(min(rotation_degrees, 180), 1) # Ensure max 180 degrees
|
rotation_degrees_final = round(min(rotation_degrees, 180), 1) # Ensure max 180 degrees
|
||||||
|
|
||||||
|
#
|
||||||
|
hiring_source_counts = candidate_queryset.values('hiring_source').annotate(count=Count('stage'))
|
||||||
|
source_map= {item['hiring_source']: item['count'] for item in hiring_source_counts}
|
||||||
|
candidates_count_in_each_source = [
|
||||||
|
source_map.get('Public', 0), source_map.get('Internal', 0), source_map.get('Agency', 0),
|
||||||
|
|
||||||
|
]
|
||||||
|
all_hiring_sources=["Public", "Internal", "Agency"]
|
||||||
|
|
||||||
|
|
||||||
# --- 8. CONTEXT RETURN ---
|
# --- 8. CONTEXT RETURN ---
|
||||||
|
|
||||||
@ -555,6 +571,10 @@ def dashboard_view(request):
|
|||||||
'jobs': all_jobs_queryset,
|
'jobs': all_jobs_queryset,
|
||||||
'current_job_id': selected_job_pk,
|
'current_job_id': selected_job_pk,
|
||||||
'current_job': current_job,
|
'current_job': current_job,
|
||||||
|
|
||||||
|
|
||||||
|
'candidates_count_in_each_source': json.dumps(candidates_count_in_each_source),
|
||||||
|
'all_hiring_sources': json.dumps(all_hiring_sources),
|
||||||
}
|
}
|
||||||
|
|
||||||
return render(request, 'recruitment/dashboard.html', context)
|
return render(request, 'recruitment/dashboard.html', context)
|
||||||
|
|||||||
@ -705,6 +705,5 @@
|
|||||||
{% comment %} {% endif %} {% endcomment %}
|
{% comment %} {% endif %} {% endcomment %}
|
||||||
|
|
||||||
{% block customJS %}{% endblock %}
|
{% block customJS %}{% endblock %}
|
||||||
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -1,6 +1,6 @@
|
|||||||
{% load crispy_forms_tags %}
|
{% load crispy_forms_tags %}
|
||||||
<div class="modal fade mt-4" id="linkedinData" tabindex="-1" aria-labelledby="myModalLabel" aria-hidden="true">
|
<div class="modal fade mt-4" id="linkedinData" tabindex="-1" aria-labelledby="myModalLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog">
|
<div class="modal-dialog modal-xl" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="myModalLabel">Edit linkedin Post content</h5>
|
<h5 class="modal-title" id="myModalLabel">Edit linkedin Post content</h5>
|
||||||
|
|||||||
@ -659,46 +659,30 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="resume-parsed-section">
|
||||||
{% if candidate.is_resume_parsed %}
|
{% if candidate.is_resume_parsed %}
|
||||||
{% include 'recruitment/candidate_resume_template.html' %}
|
{% include 'recruitment/candidate_resume_template.html' %}
|
||||||
{% else %}
|
{% else %}
|
||||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="text-decoration-none">
|
{% if candidate.scoring_timeout %}
|
||||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%;" class="mb-2">
|
<div style="display: flex; justify-content: center; align-items: center; height: 100%;" class="mb-2">
|
||||||
|
<div class="ai-loading-container">
|
||||||
<div class="ai-loading-container">
|
<i class="fas fa-robot ai-robot-icon"></i>
|
||||||
{# Robot Icon (Requires Font Awesome or similar library) #}
|
<span>Resume is been Scoring...</span>
|
||||||
<i class="fas fa-robot ai-robot-icon"></i>
|
|
||||||
|
|
||||||
{# The Spinner #}
|
|
||||||
<svg class="kaats-spinner" viewBox="0 0 50 50">
|
|
||||||
<circle cx="25" cy="25" r="20"></circle>
|
|
||||||
<circle class="path" cx="25" cy="25" r="20" fill="none" stroke-width="4"
|
|
||||||
style="animation: kaats-spinner-dash 1.5s ease-in-out infinite;"></circle>
|
|
||||||
</svg>
|
|
||||||
|
|
||||||
<span>AI Scoring...</span>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
</a>
|
</div>
|
||||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%;">
|
{% else %}
|
||||||
{% if candidate.check_and_retry_ai_scoring %}
|
<div style="display: flex; justify-content: center; align-items: center; height: 100%;">
|
||||||
<form method="post" action="{% url 'candidate_retry_scoring' slug=candidate.slug %}" class="d-inline">
|
<button type="submit" class="btn btn-sm btn-main-action" hx-get="{% url 'candidate_retry_scoring' candidate.slug %}" hx-select=".resume-parsed-section" hx-target=".resume-parsed-section" hx-swap="outerHTML" hx-on:click="this.disabled=true;this.innerHTML=`Scoring Resume , Please Wait.. <i class='fa-solid fa-spinner fa-spin'></i>`">
|
||||||
{% csrf_token %}
|
|
||||||
|
|
||||||
{# Use your established teal button style #}
|
|
||||||
<button type="submit" class="btn btn-sm btn-main-action">
|
|
||||||
<i class="fas fa-redo me-1"></i>
|
|
||||||
{% trans "Retry AI Scoring" %}
|
{% trans "Retry AI Scoring" %}
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{# STAGE UPDATE MODAL INCLUDED FOR STAFF USERS #}
|
||||||
|
|
||||||
|
|
||||||
{% if user.is_staff %}
|
{% if user.is_staff %}
|
||||||
|
|||||||
@ -438,7 +438,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal fade" id="jobAssignmentModal" tabindex="-1" aria-labelledby="jobAssignmentLabel" aria-hidden="true">
|
<div class="modal fade" id="jobAssignmentModal" tabindex="-1" aria-labelledby="jobAssignmentLabel" aria-hidden="true">
|
||||||
<div class="modal-dialog" role="document">
|
<div class="modal-dialog modal-lg" role="document">
|
||||||
<div class="modal-content">
|
<div class="modal-content">
|
||||||
<div class="modal-header">
|
<div class="modal-header">
|
||||||
<h5 class="modal-title" id="jobAssignmentLabel">{% trans "Manage all participants" %}</h5>
|
<h5 class="modal-title" id="jobAssignmentLabel">{% trans "Manage all participants" %}</h5>
|
||||||
@ -449,20 +449,32 @@
|
|||||||
<form method="post" action="{% url 'candidate_interview_view' job.slug %}">
|
<form method="post" action="{% url 'candidate_interview_view' job.slug %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div class="modal-body">
|
<div class="modal-body table-responsive">
|
||||||
{{ job.internal_job_id }} {{ job.title}}
|
{{ job.internal_job_id }} {{ job.title}}
|
||||||
|
|
||||||
<hr>
|
<hr>
|
||||||
|
|
||||||
<h3>👥 {% trans "Participants" %}</h3>
|
|
||||||
{{ form.participants.errors }}
|
|
||||||
{{ form.participants }}
|
|
||||||
|
|
||||||
<hr>
|
<table class="table tab table-bordered mt-3">
|
||||||
|
<thead>
|
||||||
|
<th class="col">👥 {% trans "Participants" %}</th>
|
||||||
|
<th class="col">🧑💼 {% trans "Users" %}</th>
|
||||||
|
</thead>
|
||||||
|
|
||||||
<h3>🧑💼 {% trans "Users" %}</h3>
|
<tbody>
|
||||||
{{ form.users.errors }}
|
|
||||||
{{ form.users }}
|
<tr>
|
||||||
|
<td>
|
||||||
|
{{ form.participants.errors }}
|
||||||
|
{{ form.participants }}
|
||||||
|
</td>
|
||||||
|
<td> {{ form.users.errors }}
|
||||||
|
{{ form.users }}
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
|
||||||
|
|
||||||
|
</table>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="modal-footer">
|
<div class="modal-footer">
|
||||||
|
|||||||
@ -361,7 +361,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" style="width: 8%;">
|
<th scope="col" style="width: 8%;">
|
||||||
<i class="fas fa-user me-1"></i> {% trans "Candidate Name" %}
|
<i class="fas fa-user me-1"></i> {% trans "Name" %}
|
||||||
</th>
|
</th>
|
||||||
<th scope="col" style="width: 10%;">
|
<th scope="col" style="width: 10%;">
|
||||||
<i class="fas fa-phone me-1"></i> {% trans "Contact Info" %}
|
<i class="fas fa-phone me-1"></i> {% trans "Contact Info" %}
|
||||||
|
|||||||
@ -224,6 +224,20 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<div class="card shadow-sm no-hover mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h6 class="mb-0">
|
||||||
|
<i class="fas fa-chart-pie me-2 text-primary"></i>
|
||||||
|
{% trans "Candidates From Each Sources" %}
|
||||||
|
</h6>
|
||||||
|
</div>
|
||||||
|
<div class="card-body p-4">
|
||||||
|
<div style="height: 300px;">
|
||||||
|
<canvas id="candidatesourceschart"></canvas>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
@ -442,6 +456,86 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
// Chart for Candidate Categories and Match Scores
|
||||||
|
document.addEventListener('DOMContentLoaded', function() {
|
||||||
|
const ctx = document.getElementById('candidatesourceschart');
|
||||||
|
if (!ctx) {
|
||||||
|
console.warn('Candidates sources chart element not found.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const chartCtx = ctx.getContext('2d');
|
||||||
|
|
||||||
|
// Safely get job_category_data from Django context
|
||||||
|
// Using window.jobChartData to avoid template parsing issues
|
||||||
|
|
||||||
|
|
||||||
|
if (categories.length > 0) { // Only render if there's data
|
||||||
|
const chart = new Chart(chartCtx, {
|
||||||
|
type: 'doughnut',
|
||||||
|
data: {
|
||||||
|
labels: categories,
|
||||||
|
datasets: [
|
||||||
|
{
|
||||||
|
label: 'Number of Candidates',
|
||||||
|
data: candidates_count_in_each_source,
|
||||||
|
backgroundColor: [
|
||||||
|
'rgba(0, 99, 110, 0.7)', // --kaauh-teal
|
||||||
|
'rgba(23, 162, 184, 0.7)', // Teal shade
|
||||||
|
'rgba(0, 150, 136, 0.7)', // Teal green
|
||||||
|
'rgba(0, 188, 212, 0.7)', // Cyan
|
||||||
|
'rgba(38, 166, 154, 0.7)', // Turquoise
|
||||||
|
'rgba(77, 182, 172, 0.7)', // Medium teal
|
||||||
|
// Add more colors if you expect more categories
|
||||||
|
],
|
||||||
|
borderColor: [
|
||||||
|
'rgba(0, 99, 110, 1)',
|
||||||
|
'rgba(23, 162, 184, 1)',
|
||||||
|
'rgba(0, 150, 136, 1)',
|
||||||
|
'rgba(0, 188, 212, 1)',
|
||||||
|
'rgba(38, 166, 154, 1)',
|
||||||
|
'rgba(77, 182, 172, 1)',
|
||||||
|
// Add more colors if you expect more categories
|
||||||
|
],
|
||||||
|
borderWidth: 1,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true,
|
||||||
|
maintainAspectRatio: false, // Important for fixed height container
|
||||||
|
plugins: {
|
||||||
|
legend: {
|
||||||
|
position: 'right', // Position legend for doughnut chart
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
display: false, // Chart title is handled by the card header
|
||||||
|
},
|
||||||
|
tooltip: {
|
||||||
|
callbacks: {
|
||||||
|
label: function(context) {
|
||||||
|
let label = context.label || '';
|
||||||
|
if (label) {
|
||||||
|
label += ': ';
|
||||||
|
}
|
||||||
|
label += context.parsed + ' candidate(s)';
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
// Display a message if no data is available
|
||||||
|
chartCtx.canvas.parentNode.innerHTML = '<p class="text-center text-muted mt-4">No candidate category data available for this job.</p>';
|
||||||
|
}
|
||||||
|
});
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
Loading…
x
Reference in New Issue
Block a user