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
|
||||
|
||||
@property
|
||||
def check_and_retry_ai_scoring(self):
|
||||
"""
|
||||
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
|
||||
|
||||
def scoring_timeout(self):
|
||||
return timezone.now() <= (self.created_at + timezone.timedelta(minutes=5))
|
||||
|
||||
|
||||
class TrainingMaterial(Base):
|
||||
|
||||
@ -223,10 +223,17 @@ class CandidateDeleteView(LoginRequiredMixin, SuccessMessageMixin, DeleteView):
|
||||
|
||||
|
||||
def retry_scoring_view(request,slug):
|
||||
if request.method == 'POST':
|
||||
candidate = get_object_or_404(models.Candidate, slug=slug)
|
||||
candidate.save()
|
||||
return redirect('candidate_detail', slug=candidate.slug)
|
||||
from django_q.tasks import async_task
|
||||
|
||||
candidate = get_object_or_404(models.Candidate, slug=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_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 ---
|
||||
|
||||
@ -555,6 +571,10 @@ def dashboard_view(request):
|
||||
'jobs': all_jobs_queryset,
|
||||
'current_job_id': selected_job_pk,
|
||||
'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)
|
||||
|
||||
@ -705,6 +705,5 @@
|
||||
{% comment %} {% endif %} {% endcomment %}
|
||||
|
||||
{% block customJS %}{% endblock %}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
{% load crispy_forms_tags %}
|
||||
<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-header">
|
||||
<h5 class="modal-title" id="myModalLabel">Edit linkedin Post content</h5>
|
||||
|
||||
@ -659,46 +659,30 @@
|
||||
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
</div>
|
||||
|
||||
<div class="resume-parsed-section">
|
||||
{% if candidate.is_resume_parsed %}
|
||||
{% include 'recruitment/candidate_resume_template.html' %}
|
||||
{% else %}
|
||||
<a href="{% url 'candidate_detail' candidate.slug %}" class="text-decoration-none">
|
||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%;" class="mb-2">
|
||||
|
||||
<div class="ai-loading-container">
|
||||
{# Robot Icon (Requires Font Awesome or similar library) #}
|
||||
<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>
|
||||
</a>
|
||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%;">
|
||||
{% if candidate.check_and_retry_ai_scoring %}
|
||||
<form method="post" action="{% url 'candidate_retry_scoring' slug=candidate.slug %}" class="d-inline">
|
||||
{% 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>
|
||||
{% if candidate.scoring_timeout %}
|
||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%;" class="mb-2">
|
||||
<div class="ai-loading-container">
|
||||
<i class="fas fa-robot ai-robot-icon"></i>
|
||||
<span>Resume is been Scoring...</span>
|
||||
</div>
|
||||
</div>
|
||||
{% else %}
|
||||
<div style="display: flex; justify-content: center; align-items: center; height: 100%;">
|
||||
<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>`">
|
||||
{% trans "Retry AI Scoring" %}
|
||||
</button>
|
||||
</form>
|
||||
{% endif %}
|
||||
</div>
|
||||
</button>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
{# STAGE UPDATE MODAL INCLUDED FOR STAFF USERS #}
|
||||
|
||||
|
||||
{% if user.is_staff %}
|
||||
|
||||
@ -438,7 +438,7 @@
|
||||
</div>
|
||||
|
||||
<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-header">
|
||||
<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 %}">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="modal-body">
|
||||
<div class="modal-body table-responsive">
|
||||
{{ job.internal_job_id }} {{ job.title}}
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>👥 {% trans "Participants" %}</h3>
|
||||
{{ form.participants.errors }}
|
||||
{{ form.participants }}
|
||||
|
||||
<hr>
|
||||
|
||||
<h3>🧑💼 {% trans "Users" %}</h3>
|
||||
{{ form.users.errors }}
|
||||
{{ form.users }}
|
||||
|
||||
<table class="table tab table-bordered mt-3">
|
||||
<thead>
|
||||
<th class="col">👥 {% trans "Participants" %}</th>
|
||||
<th class="col">🧑💼 {% trans "Users" %}</th>
|
||||
</thead>
|
||||
|
||||
<tbody>
|
||||
|
||||
<tr>
|
||||
<td>
|
||||
{{ form.participants.errors }}
|
||||
{{ form.participants }}
|
||||
</td>
|
||||
<td> {{ form.users.errors }}
|
||||
{{ form.users }}
|
||||
</td>
|
||||
</tr>
|
||||
|
||||
|
||||
</table>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
|
||||
@ -361,7 +361,7 @@
|
||||
{% endif %}
|
||||
</th>
|
||||
<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 scope="col" style="width: 10%;">
|
||||
<i class="fas fa-phone me-1"></i> {% trans "Contact Info" %}
|
||||
|
||||
@ -223,6 +223,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>
|
||||
|
||||
@ -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>
|
||||
|
||||
{% endblock %}
|
||||
Loading…
x
Reference in New Issue
Block a user