third refactoring for candidate in ai_ovverview branch

This commit is contained in:
Faheed 2025-11-27 15:44:39 +03:00
parent 8b51416e10
commit 2486eb791e
18 changed files with 174 additions and 172 deletions

View File

@ -25,7 +25,7 @@ urlpatterns = [
path('application/<slug:template_slug>/', views.application_submit_form, name='application_submit_form'),
path('application/<slug:template_slug>/submit/', views.application_submit, name='application_submit'),
path('application/<slug:slug>/apply/', views.application_detail, name='application_detail'),
path('application/<slug:slug>/apply/', views.job_application_detail, name='job_application_detail'),
path('application/<slug:slug>/signup/', views.application_signup, name='application_signup'),
path('application/<slug:slug>/success/', views.application_success, name='application_success'),
# path('application/applicant/profile', views.applicant_profile, name='applicant_profile'),

View File

@ -293,6 +293,7 @@ class ApplicationForm(forms.ModelForm):
"resume",
]
labels = {
"person":_("Applicant"),
"resume": _("Resume"),
"hiring_source": _("Hiring Type"),
"hiring_agency": _("Hiring Agency"),
@ -903,7 +904,7 @@ class FormTemplateIsActiveForm(forms.ModelForm):
fields = ["is_active"]
class CandidateExamDateForm(forms.ModelForm):
class ApplicationExamDateForm(forms.ModelForm):
class Meta:
model = Application
fields = ["exam_date"]

View File

@ -23,7 +23,7 @@ from .views import (
applications_exam_view, applications_interview_view, api_schedule_application_meeting
)
from .views_frontend import CandidateListView, JobListView
from .utils import create_zoom_meeting, get_candidates_from_request
from .utils import create_zoom_meeting, get_applications_from_request
class BaseTestCase(TestCase):
@ -586,8 +586,8 @@ class UtilityFunctionTests(BaseTestCase):
self.assertEqual(result['status'], 'success')
self.assertIn('meeting_id', result['meeting_details'])
def test_get_candidates_from_request(self):
"""Test the get_candidates_from_request utility function"""
def get_applications_from_request(self):
"""Test the get_applications_from_request utility function"""
# This would be tested with a request that has candidate_ids
pass

View File

@ -41,7 +41,7 @@ from .views import (
# from .views_frontend import CandidateListView, JobListView, JobCreateView
from .utils import (
create_zoom_meeting, delete_zoom_meeting, update_zoom_meeting,
get_zoom_meeting_details, get_candidates_from_request,
get_zoom_meeting_details, get_applications_from_request,
get_available_time_slots
)
# from .zoom_api import ZoomAPIError

View File

@ -194,7 +194,7 @@ urlpatterns = [
views_frontend.update_application_status,
name="update_application_status",
),
# Sync URLs
# Sync URLs (check)
path(
"jobs/<slug:job_slug>/sync-hired-applications/",
views_frontend.sync_hired_applications,
@ -205,6 +205,8 @@ urlpatterns = [
views_frontend.test_source_connection,
name="test_source_connection",
),
path(
"jobs/<slug:slug>/<int:application_id>/reschedule_meeting_for_application/<int:meeting_id>/",
views.reschedule_meeting_for_application,
@ -357,7 +359,7 @@ urlpatterns = [
path("agencies/<slug:slug>/", views.agency_detail, name="agency_detail"),
path("agencies/<slug:slug>/update/", views.agency_update, name="agency_update"),
path("agencies/<slug:slug>/delete/", views.agency_delete, name="agency_delete"),
path(
path( #check the html of this url it is not used anywhere
"agencies/<slug:slug>/applications/",
views.agency_applications,
name="agency_applications",
@ -369,12 +371,12 @@ urlpatterns = [
views.agency_assignment_list,
name="agency_assignment_list",
),
path(
path( #check
"agency-assignments/create/",
views.agency_assignment_create,
name="agency_assignment_create",
),
path(
path(#check
"agency-assignments/<slug:slug>/create/",
views.agency_assignment_create,
name="agency_assignment_create",
@ -423,7 +425,7 @@ urlpatterns = [
# path('admin/messages/<int:message_id>/mark-read/', views.admin_mark_message_read, name='admin_mark_message_read'),
# path('admin/messages/<int:message_id>/delete/', views.admin_delete_message, name='admin_delete_message'),
# Agency Portal URLs (for external agencies)
path("portal/login/", views.agency_portal_login, name="agency_portal_login"),
# path("portal/login/", views.agency_portal_login, name="agency_portal_login"),
path("portal/<int:pk>/reset/", views.portal_password_reset, name="portal_password_reset"),
path(
"portal/dashboard/",

View File

@ -571,10 +571,10 @@ def json_to_markdown_table(data_list):
return markdown
def get_candidates_from_request(request):
def get_applications_from_request(request):
for c in request.POST.items():
try:
yield models.Candidate.objects.get(pk=c[0])
yield models.Application.objects.get(pk=c[0])
except Exception as e:
logger.error(e)
yield None

View File

@ -61,7 +61,7 @@ from django.urls import reverse_lazy
from django.db.models import Count, Avg, F, Q
from .forms import (
ZoomMeetingForm,
CandidateExamDateForm,
ApplicationExamDateForm,
JobPostingForm,
JobPostingImageForm,
InterviewNoteForm,
@ -98,7 +98,7 @@ from django.views.generic import (
from .utils import (
create_zoom_meeting,
delete_zoom_meeting,
get_candidates_from_request,
get_applications_from_request,
update_meeting,
update_zoom_meeting,
get_zoom_meeting_details,
@ -864,9 +864,9 @@ def kaauh_career(request):
# job detail facing the candidate:
def application_detail(request, slug):
def job_application_detail(request, slug):
job = get_object_or_404(JobPosting, slug=slug)
return render(request, "applicant/application_detail.html", {"job": job})
return render(request, "applicant/job_application_detail.html", {"job": job})
@login_required
@ -1854,18 +1854,18 @@ def applications_exam_view(request, slug):
@staff_user_required
def update_application_exam_status(request, slug):
candidate = get_object_or_404(Application, slug=slug)
application = get_object_or_404(Application, slug=slug)
if request.method == "POST":
form = CandidateExamDateForm(request.POST, instance=candidate)
form = ApplicationExamDateForm(request.POST, instance=application)
if form.is_valid():
form.save()
return redirect("applications_exam_view", slug=candidate.job.slug)
return redirect("applications_exam_view", slug=application.job.slug)
else:
form = CandidateExamDateForm(request.POST, instance=candidate)
form = ApplicationExamDateForm(request.POST, instance=application)
return render(
request,
"includes/candidate_exam_status_form.html",
{"candidate": candidate, "form": form},
"includes/application_exam_status_form.html",
{"application": application, "form": form},
)
@ -1874,17 +1874,17 @@ def bulk_update_application_exam_status(request, slug):
job = get_object_or_404(JobPosting, slug=slug)
status = request.headers.get("status")
if status:
for candidate in get_candidates_from_request(request):
for application in get_applications_from_request(request):
try:
if status == "pass":
candidate.exam_status = "Passed"
candidate.stage = "Interview"
application.exam_status = "Passed"
application.stage = "Interview"
else:
candidate.exam_status = "Failed"
candidate.save()
application.exam_status = "Failed"
application.save()
except Exception as e:
print(e)
messages.success(request, f"Updated exam status selected candidates")
messages.success(request, f"Updated exam status selected applications")
return redirect("applications_exam_view", slug=job.slug)
@ -1897,13 +1897,13 @@ def application_criteria_view_htmx(request, pk):
@staff_user_required
def application_set_exam_date(request, slug):
candidate = get_object_or_404(Application, slug=slug)
candidate.exam_date = timezone.now()
candidate.save()
application = get_object_or_404(Application, slug=slug)
application.exam_date = timezone.now()
application.save()
messages.success(
request, f"Set exam date for {candidate.name} to {candidate.exam_date}"
request, f"Set exam date for {application.name} to {application.exam_date}"
)
return redirect("applications_screening_view", slug=candidate.job.slug)
return redirect("applications_screening_view", slug=application.job.slug)
@staff_user_required
@ -1911,9 +1911,9 @@ def application_update_status(request, slug):
job = get_object_or_404(JobPosting, slug=slug)
mark_as = request.POST.get("mark_as")
if mark_as != "----------":
candidate_ids = request.POST.getlist("candidate_ids")
print(candidate_ids)
if c := Application.objects.filter(pk__in=candidate_ids):
application_ids = request.POST.getlist("candidate_ids")
if c := Application.objects.filter(pk__in=application_ids):
if mark_as == "Exam":
print("exam")
c.update(
@ -1979,7 +1979,7 @@ def application_update_status(request, slug):
else "Applicant",
)
messages.success(request, f"Candidates Updated")
messages.success(request, f"Applications Updated")
response = HttpResponse(redirect("applications_screening_view", slug=job.slug))
response.headers["HX-Refresh"] = "true"
return response
@ -3710,22 +3710,22 @@ def agency_delete(request, slug):
@staff_user_required
def agency_applications(request, slug):
"""View all candidates from a specific agency"""
"""View all applications from a specific agency"""
agency = get_object_or_404(HiringAgency, slug=slug)
candidates = Application.objects.filter(hiring_agency=agency).order_by(
applications = Application.objects.filter(hiring_agency=agency).order_by(
"-created_at"
)
# Filter by stage if provided
stage_filter = request.GET.get("stage")
if stage_filter:
candidates = candidates.filter(stage=stage_filter)
applications = applications.filter(stage=stage_filter)
# Get total candidates before pagination for accurate count
total_candidates = candidates.count()
# Get total applications before pagination for accurate count
total_applications = applications.count()
# Pagination
paginator = Paginator(candidates, 20) # Show 20 candidates per page
paginator = Paginator(applications, 20) # Show 20 applications per page
page_number = request.GET.get("page")
page_obj = paginator.get_page(page_number)
@ -3733,9 +3733,9 @@ def agency_applications(request, slug):
"agency": agency,
"page_obj": page_obj,
"stage_filter": stage_filter,
"total_candidates": total_candidates,
"total_applications": total_applications,
}
return render(request, "recruitment/agency_candidates.html", context)
return render(request, "recruitment/agency_applications.html", context)
# Agency Portal Management Views
@ -3817,8 +3817,8 @@ def agency_assignment_detail(request, slug):
AgencyJobAssignment.objects.select_related("agency", "job"), slug=slug
)
# Get candidates submitted by this agency for this job
candidates = Application.objects.filter(
# Get applications submitted by this agency for this job
applications = Application.objects.filter(
hiring_agency=assignment.agency, job=assignment.job
).order_by("-created_at")
@ -3827,21 +3827,21 @@ def agency_assignment_detail(request, slug):
# Get messages for this assignment
total_candidates = candidates.count()
max_candidates = assignment.max_candidates
total_applications = applications.count()
max_applications = assignment.max_candidates
circumference = 326.73 # 2 * π * r where r=52
if max_candidates > 0:
progress_percentage = total_candidates / max_candidates
if max_applications > 0:
progress_percentage = total_applications / max_applications
stroke_dashoffset = circumference - (circumference * progress_percentage)
else:
stroke_dashoffset = circumference
context = {
"assignment": assignment,
"candidates": candidates,
"applications": applications,
"access_link": access_link,
"total_candidates": candidates.count(),
"total_applications": applications.count(),
"stroke_dashoffset": stroke_dashoffset,
}
return render(request, "recruitment/agency_assignment_detail.html", context)
@ -3973,40 +3973,40 @@ def portal_password_reset(request,pk):
for error in errors:
messages.error(request, f"{field}: {error}")
# Agency Portal Views (for external agencies)
def agency_portal_login(request):
"""Agency login page"""
# if request.session.get("agency_assignment_id"):
# return redirect("agency_portal_dashboard")
if request.method == "POST":
form = AgencyLoginForm(request.POST)
# # Agency Portal Views (for external agencies)
# def agency_portal_login(request):
# """Agency login page"""
# # if request.session.get("agency_assignment_id"):
# # return redirect("agency_portal_dashboard")
# if request.method == "POST":
# form = AgencyLoginForm(request.POST)
if form.is_valid():
# Check if validated_access_link attribute exists
# if form.is_valid():
# # Check if validated_access_link attribute exists
# if hasattr(form, "validated_access_link"):
# access_link = form.validated_access_link
# access_link.record_access()
# # if hasattr(form, "validated_access_link"):
# # access_link = form.validated_access_link
# # access_link.record_access()
# Store assignment in session
# request.session["agency_assignment_id"] = access_link.assignment.id
# request.session["agency_name"] = access_link.assignment.agency.name
# # Store assignment in session
# # request.session["agency_assignment_id"] = access_link.assignment.id
# # request.session["agency_name"] = access_link.assignment.agency.name
messages.success(request, f"Welcome, {access_link.assignment.agency.name}!")
return redirect("agency_portal_dashboard")
else:
messages.error(request, "Invalid token or password.")
else:
form = AgencyLoginForm()
# messages.success(request, f"Welcome, {access_link.assignment.agency.name}!")
# return redirect("agency_portal_dashboard")
# else:
# messages.error(request, "Invalid token or password.")
# else:
# form = AgencyLoginForm()
context = {
"form": form,
}
return render(request, "recruitment/agency_portal_login.html", context)
# context = {
# "form": form,
# }
# return render(request, "recruitment/agency_portal_login.html", context)
def portal_login(request):
"""Unified portal login for agency and candidate"""
"""Unified portal login for agency and applicant"""
if request.user.is_authenticated:
if request.user.user_type == "agency":
return redirect("agency_portal_dashboard")
@ -4077,24 +4077,24 @@ def portal_login(request):
@login_required
@candidate_user_required
def applicant_portal_dashboard(request):
"""Candidate portal dashboard"""
"""applicant portal dashboard"""
if not request.user.is_authenticated:
return redirect("account_login")
# Get candidate profile (Person record)
try:
candidate = request.user.person_profile
applicant = request.user.person_profile
except:
messages.error(request, "No candidate profile found.")
return redirect("account_login")
# Get candidate's applications with related job data
applications = Application.objects.filter(
person=candidate
person=applicant
).select_related('job').order_by('-created_at')
# Get candidate's documents using the Person documents property
documents = candidate.documents.order_by('-created_at')
documents = applicant.documents.order_by('-created_at')
# Add password change form for modal
password_form = PasswordResetForm()
@ -4104,13 +4104,13 @@ def applicant_portal_dashboard(request):
document_form = DocumentUploadForm()
context = {
"candidate": candidate,
"applicant": applicant,
"applications": applications,
"documents": documents,
"password_form": password_form,
"document_form": document_form,
}
return render(request, "recruitment/candidate_profile.html", context)
return render(request, "recruitment/applicant_profile.html", context)
@login_required
@ -4163,7 +4163,7 @@ def applicant_application_detail(request, slug):
"interviews": interviews,
"documents": documents,
}
return render(request, "recruitment/candidate_application_detail.html", context)
return render(request, "recruitment/applicant_application_detail.html", context)
@agency_user_required
@ -4243,17 +4243,17 @@ def agency_portal_dashboard(request):
# Calculate statistics for each assignment
assignment_stats = []
for assignment in assignments:
candidates = Application.objects.filter(
applications = Application.objects.filter(
hiring_agency=agency, job=assignment.job
).order_by("-created_at")
unread_messages = 0
unread_messages = Message.objects.filter(job=assignment.job,recipient=agency.user,is_read=False).count()
assignment_stats.append(
{
"assignment": assignment,
"candidates": candidates,
"candidate_count": candidates.count(),
"applications": applications,
"application_count": applications.count(),
"unread_messages": unread_messages,
"days_remaining": assignment.days_remaining,
"is_active": assignment.is_currently_active,
@ -4262,7 +4262,7 @@ def agency_portal_dashboard(request):
)
# Get overall statistics
total_candidates = sum(stats["candidate_count"] for stats in assignment_stats)
total_applications = sum(stats["application_count"] for stats in assignment_stats)
total_unread_messages = sum(stats["unread_messages"] for stats in assignment_stats)
active_assignments = sum(1 for stats in assignment_stats if stats["is_active"])
@ -4272,7 +4272,7 @@ def agency_portal_dashboard(request):
"assignment_stats": assignment_stats,
"total_assignments": assignments.count(),
"active_assignments": active_assignments,
"total_candidates": total_candidates,
"total_applications": total_applications,
"total_unread_messages": total_unread_messages,
}
return render(request, "recruitment/agency_portal_dashboard.html", context)
@ -4280,7 +4280,7 @@ def agency_portal_dashboard(request):
@agency_user_required
def agency_portal_submit_application_page(request, slug):
"""Dedicated page for submitting a candidate"""
"""Dedicated page for submitting a application """
# assignment_id = request.session.get("agency_assignment_id")
# if not assignment_id:
# return redirect("agency_portal_login")
@ -4296,7 +4296,7 @@ def agency_portal_submit_application_page(request, slug):
current_job=assignment.job
if assignment.is_full:
messages.error(request, "Maximum candidate limit reached for this assignment.")
messages.error(request, "Maximum Application limit reached for this assignment.")
return redirect("agency_portal_assignment_detail", slug=assignment.slug)
# Verify this assignment belongs to the same agency as the logged-in session
if assignment.agency.id != assignment.agency.id:
@ -4309,11 +4309,11 @@ def agency_portal_submit_application_page(request, slug):
if not assignment.can_submit:
messages.error(
request,
"Cannot submit candidates: Assignment is not active, expired, or full.",
"Cannot submit applications: Assignment is not active, expired, or full.",
)
return redirect("agency_portal_assignment_detail", slug=assignment.slug)
# Get total submitted candidates for this assignment
# Get total submitted applications for this assignment
total_submitted = Application.objects.filter(
hiring_agency=assignment.agency, job=assignment.job
).count()

View File

@ -25,7 +25,6 @@ from django.urls import reverse_lazy
from django.db.models import FloatField
from django.db.models import F, IntegerField, Count, Avg, Sum, Q, ExpressionWrapper, fields, Value,CharField
from django.db.models.functions import Cast, Coalesce, TruncDate
from django.contrib.auth.decorators import login_required
from django.shortcuts import render
from django.utils import timezone
from datetime import timedelta
@ -33,7 +32,7 @@ import json
from django.utils.translation import gettext_lazy as _
# Add imports for user type restrictions
from recruitment.decorators import StaffRequiredMixin, staff_user_required
from recruitment.decorators import StaffRequiredMixin, staff_user_required,candidate_user_required,staff_or_candidate_required
from datastar_py.django import (
@ -907,7 +906,7 @@ def export_applications_csv(request, job_slug, stage):
@login_required
@staff_user_required
def sync_hired_applications(request, job_slug):
"""Sync hired candidates to external sources using Django-Q"""
"""Sync hired applications to external sources using Django-Q"""
from django_q.tasks import async_task
from .tasks import sync_hired_candidates_task

View File

@ -191,7 +191,7 @@
{% for job in active_jobs %}
{# Optimized Job Listing Card #}
<a href="{% url 'application_detail' job.slug %}"
<a href="{% url 'job_application_detail' job.slug %}"
class="card d-block text-decoration-none text-dark job-listing-card p-4 border-2 shadow-hover transition-all">
<div class="d-flex justify-content-between align-items-start mb-2">

View File

@ -1,5 +1,5 @@
{% load i18n %}
{% url 'update_application_exam_status' slug=candidate.slug as url %}
{% url 'update_application_exam_status' slug=application.slug as url %}
<form data-on-submit="@post('{{url}}', {contentType: 'form', headers: {'X-CSRFToken': '{{ csrf_token }}'}})">
{% csrf_token %}
{{ form.as_p }}

View File

@ -75,7 +75,7 @@
<div class="collapse navbar-collapse" id="agencyNavbar">
<div class="navbar-nav ms-auto">
<li class="nav-item me-2">
<li class="nav-item me-2">
{% if LANGUAGE_CODE == 'en' %}
<form action="{% url 'set_language' %}" method="post" class="d-inline">{% csrf_token %}
<input name="next" type="hidden" value="{{ request.get_full_path }}">
@ -120,7 +120,7 @@
{% comment %}
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle text-white" href="#" role="button" data-bs-toggle="dropdown"
data-bs-offset="0, 8" aria-expanded="false" aria-label="{% trans 'Toggle language menu' %}">
@ -145,8 +145,8 @@
</form>
</li>
</ul>
</li>
{% endcomment %}
</li> {% endcomment %}
{% if request.user.is_authenticated %}
<li class="nav-item">
<a class="nav-link text-white" href="{% url 'user_detail' request.user.pk %}">

View File

@ -207,7 +207,7 @@
<div class="alert alert-info mx-2">
<i class="fas fa-info-circle me-2"></i>
{% trans "Share these credentials securely with the agency. They can use this information to log in and submit candidates." %}
{% trans "Share these credentials securely with the agency. They can use this information to log in and submit applications." %}
</div>
{% if access_link %}
@ -219,12 +219,12 @@
</div>
</div> {% endcomment %}
<!-- Candidates Card -->
<!-- Applications Card -->
<div class="kaauh-card p-4">
<div class="d-flex justify-content-between align-items-center mb-4">
<h5 class="mb-0" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-users me-2"></i>
{% trans "Submitted Candidates" %} ({{ total_candidates }})
{% trans "Submitted Applications" %} ({{ total_candidates }})
</h5>
{% if access_link %}
<a href="{% url 'agency_portal_login' %}" target="_blank" class="btn btn-outline-info btn-sm">
@ -233,12 +233,12 @@
{% endif %}
</div>
{% if candidates %}
{% if applications %}
<div class="table-responsive">
<table class="table table-hover">
<thead>
<tr>
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Candidate"%}</th>
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Application"%}</th>
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Contact" %}
</th>
<th class="px-4 py-3 text-uppercase small fw-bold text-muted">{% trans "Stage" %}
@ -248,36 +248,36 @@
</tr>
</thead>
<tbody>
{% for candidate in candidates %}
{% for application in applications %}
<tr>
<td>
<div class="fw-bold">{{ candidate.name }}</div>
<div class="fw-bold">{{ application.name }}</div>
</td>
<td>
<div class="small">
<div><i class="fas fa-envelope me-1"></i> {{ candidate.email }}</div>
<div><i class="fas fa-phone me-1"></i> {{ candidate.phone }}</div>
<div><i class="fas fa-envelope me-1"></i> {{ application.email }}</div>
<div><i class="fas fa-phone me-1"></i> {{ application.phone }}</div>
</div>
</td>
<td>
<span class="badge bg-info">{{ candidate.get_stage_display }}</span>
<span class="badge bg-info">{{ application.get_stage_display }}</span>
</td>
<td>
<div class="small text-muted">
<div class="mb-1"><i class="fas fa-envelope me-2 w-20"></i>
{{candidate.email }}</div>
<div><i class="fas fa-phone me-2 w-20"></i>{{ candidate.phone }}</div>
{{application.email }}</div>
<div><i class="fas fa-phone me-2 w-20"></i>{{ application.phone }}</div>
</div>
</td>
<td class="px-4">
<span class="badge bg-soft-info text-info rounded-pill px-3">
{{candidate.get_stage_display }}</span>
{{application.get_stage_display }}</span>
</td>
<td class="px-4">
<span class="small text-muted">{{ candidate.created_at|date:"M d, Y" }}</span>
<span class="small text-muted">{{ application.created_at|date:"M d, Y" }}</span>
</td>
<td class="px-4 text-end">
<a href="{% url 'application_detail' candidate.slug %}"
<a href="{% url 'application_detail' application.slug %}"
class="btn btn-sm btn-outline-primary" title="{% trans 'View Details' %}">
<i class="fas fa-eye"></i>
</a>
@ -290,9 +290,9 @@
{% else %}
<div class="text-center py-4">
<i class="fas fa-users fa-2x text-muted mb-3"></i>
<h6 class="text-muted">{% trans "No candidates submitted yet" %}</h6>
<h6 class="text-muted">{% trans "No applications submitted yet" %}</h6>
<p class="text-muted small">
{% trans "Candidates will appear here once the agency submits them through their portal." %}
{% trans "Applications will appear here once the agency submits them through their portal." %}
</p>
</div>
{% endif %}
@ -335,7 +335,7 @@
<div class="text-center">
<div class="h4 mb-1">{{ total_candidates }}</div>
<div class="text-muted">/ {{ assignment.max_candidates }} {% trans "candidates" %}</div>
<div class="text-muted">/ {{ assignment.max_candidates }} {% trans "applications" %}</div>
</div>
<div class="progress mt-3" style="height: 8px;">

View File

@ -50,7 +50,7 @@
</div>
<div>
{% comment %} <a href="{% url 'agency_portal_submit_application' %}" class="btn btn-main-action me-2">
<i class="fas fa-user-plus me-1"></i> {% trans "Submit Candidate" %}
<i class="fas fa-user-plus me-1"></i> {% trans "Submit Application" %}
</a>
<a href="#" class="btn btn-outline-secondary position-relative">
<i class="fas fa-envelope me-1"></i> {% trans "Messages" %}
@ -93,8 +93,8 @@
<div class="text-info mb-2">
<i class="fas fa-users fa-2x"></i>
</div>
<h4 class="card-title">{{ total_candidates }}</h4>
<p class="card-text text-muted">{% trans "Total Candidates" %}</p>
<h4 class="card-title">{{ total_applications }}</h4>
<p class="card-text text-muted">{% trans "Total Applications" %}</p>
</div>
</div>
</div>
@ -169,8 +169,8 @@
</strong>
</div>
<div class="col-6">
<small class="text-muted d-block">{% trans "Candidates" %}</small>
<strong>{{ stats.candidate_count }} / {{ stats.assignment.max_candidates }}</strong>
<small class="text-muted d-block">{% trans "Applications" %}</small>
<strong>{{ stats.application_count }} / {{ stats.assignment.max_applications }}</strong>
</div>
</div>
@ -178,10 +178,10 @@
<div class="mb-3">
<div class="d-flex justify-content-between mb-1">
<small class="text-muted">{% trans "Submission Progress" %}</small>
<small class="text-muted">{{ stats.candidate_count }}/{{ stats.assignment.max_candidates }}</small>
<small class="text-muted">{{ stats.application_count }}/{{ stats.assignment.max_applications }}</small>
</div>
<div class="progress" style="height: 6px;">
{% with progress=stats.candidate_count %}
{% with progress=stats.application_count %}
<div class="progress-bar {% if progress >= 90 %}bg-danger{% elif progress >= 70 %}bg-warning{% else %}bg-success{% endif %}"
style="width: {{ progress|floatformat:0 }}%"></div>
{% endwith %}
@ -194,7 +194,7 @@
{% if stats.can_submit %}
<a href="{% url 'agency_portal_submit_application_page' stats.assignment.slug %}"
class="btn btn-sm btn-main-action">
<i class="fas fa-user-plus me-1"></i> {% trans "Submit Candidate" %}
<i class="fas fa-user-plus me-1"></i> {% trans "Submit Application" %}
</a>
{% else %}
<button class="btn btn-sm btn-secondary" disabled>

View File

@ -1,7 +1,7 @@
{% extends 'portal_base.html' %}
{% load static i18n crispy_forms_tags %}
{% block title %}{% trans "Submit Candidate" %} - {{ assignment.job.title }} - Agency Portal{% endblock %}
{% block title %}{% trans "Submit Application" %} - {{ assignment.job.title }} - Agency Portal{% endblock %}
{% block customCSS %}
<style>
@ -97,13 +97,13 @@
</li>
{% comment %} <li class="breadcrumb-item active" aria-current="page">
{% trans "Submit Candidate" %}
{% trans "Submit Application" %}
</li> {% endcomment %}
<li class="breadcrumb-item active" aria-current="page" style="
color: #F43B5E; /* Rosy Accent Color */
font-weight: 600; ">
{% trans "Submit Candidate" %}</li>
{% trans "Submit Application" %}</li>
</ol>
</nav>
@ -112,11 +112,11 @@
<div>
<h1 class="h3 mb-1" style="color: var(--kaauh-teal-dark); font-weight: 700;">
<i class="fas fa-user-plus me-2"></i>
{% trans "Submit New Candidate" %}
{% trans "Submit New Application" %}
</h1>
<p class="text-muted mb-0">
<!-- Button trigger modal -->
{% trans "Submit a candidate for" %}
{% trans "Submit a Application for" %}
{{ assignment.job.title }}
</p>
@ -171,16 +171,16 @@
<div class="kaauh-card p-4">
<h5 class="mb-4" style="color: var(--kaauh-teal-dark);">
<i class="fas fa-user-edit me-2"></i>
{% trans "Candidate Information" %}
{% trans "Application Information" %}
</h5>
<form method="post" enctype="multipart/form-data" id="candidateForm"
<form method="post" enctype="multipart/form-data" id="ApplicationForm"
action="{% url 'agency_portal_submit_application_page' assignment.slug %}">
{% csrf_token %}
{{form|crispy}}
<button type="submit" class="btn btn-main-action">
<i class="fas fa-user-plus me-2"></i>
{% trans "Submit Candidate" %}
{% trans "Submit Application" %}
</button>
</form>
</div>
@ -188,14 +188,14 @@
<div class="kaauh-card p-4">
<div class="text-center py-5">
<i class="fas fa-exclamation-triangle fa-4x text-warning mb-4"></i>
<h4 class="text-warning mb-3">{% trans "Cannot Submit Candidates" %}</h4>
<h4 class="text-warning mb-3">{% trans "Cannot Submit Applications" %}</h4>
<div class="alert alert-warning d-inline-block">
{% if assignment.is_expired %}
<i class="fas fa-clock me-2"></i>
{% trans "This assignment has expired. Submissions are no longer accepted." %}
{% elif assignment.is_full %}
<i class="fas fa-users me-2"></i>
{% trans "Maximum candidate limit reached for this assignment." %}
{% trans "Maximum Application limit reached for this assignment." %}
{% else %}
<i class="fas fa-pause me-2"></i>
{% trans "This assignment is not currently active." %}
@ -222,7 +222,7 @@
<div class="spinner-border text-primary mb-3" role="status">
<span class="visually-hidden">{% trans "Loading..." %}</span>
</div>
<h6>{% trans "Submitting candidate..." %}</h6>
<h6>{% trans "Submitting Application..." %}</h6>
<p class="text-muted small">{% trans "Please wait while we process your submission." %}</p>
</div>
</div>
@ -240,7 +240,7 @@ document.addEventListener('DOMContentLoaded', function() {
const filePreview = document.getElementById('filePreview');
const fileName = document.getElementById('fileName');
const removeFileBtn = document.getElementById('removeFile');
const form = document.getElementById('candidateForm');
const form = document.getElementById('ApplicationForm');
const submitBtn = document.getElementById('submitBtn');
// File upload area click handler
@ -340,7 +340,7 @@ document.addEventListener('DOMContentLoaded', function() {
successAlert.style.cssText = 'position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
successAlert.innerHTML = `
<i class="fas fa-check-circle me-2"></i>
{% trans "Candidate submitted successfully!" %}
{% trans "Application submitted successfully!" %}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(successAlert);
@ -371,7 +371,7 @@ document.addEventListener('DOMContentLoaded', function() {
errorAlert.style.cssText = 'position: fixed; top: 20px; right: 20px; z-index: 9999; min-width: 300px;';
errorAlert.innerHTML = `
<i class="fas fa-exclamation-triangle me-2"></i>
${data.message || '{% trans "Error submitting candidate. Please try again." %}'}
${data.message || '{% trans "Error submitting Application. Please try again." %}'}
<button type="button" class="btn-close" data-bs-dismiss="alert"></button>
`;
document.body.appendChild(errorAlert);
@ -401,7 +401,7 @@ document.addEventListener('DOMContentLoaded', function() {
.finally(() => {
// Re-enable submit button
submitBtn.disabled = false;
submitBtn.innerHTML = '<i class="fas fa-paper-plane me-1"></i> {% trans "Submit Candidate" %}';
submitBtn.innerHTML = '<i class="fas fa-paper-plane me-1"></i> {% trans "Submit Application" %}';
});
});

View File

@ -292,23 +292,23 @@
{# Header: Larger, more dynamic on large screens. Stacks cleanly on mobile. #}
<div class="d-flex flex-column flex-md-row justify-content-between align-items-md-center mb-5">
<h1 class="display-6 display-md-5 fw-extrabold mb-3 mb-md-0" style="color: var(--kaauh-teal-dark);">
{% trans "Your Candidate Dashboard" %}
{% trans "Your Applicant Dashboard" %}
</h1>
{% comment %} <a href="#profile-details" data-bs-toggle="tab" class="btn btn-main-action btn-sm btn-md-lg px-4 py-2 rounded-pill shadow-sm shadow-md-lg">
<i class="fas fa-edit me-2"></i> {% trans "Update Profile" %}
</a> {% endcomment %}
</div>
{# Candidate Quick Overview Card: Use a softer background color #}
{# Applicant Quick Overview Card: Use a softer background color #}
<div class="card kaauh-card mb-5 p-4 bg-white">
<div class="d-flex align-items-center flex-column flex-sm-row text-center text-sm-start">
<img src="{% if candidate.user.profile_image %}{{ candidate.user.profile_image.url }}{% else %}{% static 'image/default_avatar.png' %}{% endif %}"
<img src="{% if applicant.user.profile_image %}{{ applicant.user.profile_image.url }}{% else %}{% static 'image/default_avatar.png' %}{% endif %}"
alt="{% trans 'Profile Picture' %}"
class="rounded-circle me-sm-4 mb-3 mb-sm-0 shadow-lg"
style="width: 80px; height: 80px; object-fit: cover; border: 4px solid var(--kaauh-teal-accent);">
<div>
<h3 class="card-title mb-1 fw-bold text-dark">{{ candidate.full_name|default:"Candidate Name" }}</h3>
<p class="text-gray-subtle mb-0">{{ candidate.email }}</p>
<h3 class="card-title mb-1 fw-bold text-dark">{{ applicant.full_name|default:"Applicant Name" }}</h3>
<p class="text-gray-subtle mb-0">{{ applicant.email }}</p>
</div>
</div>
</div>
@ -356,21 +356,21 @@
<ul class="list-unstyled profile-data-list p-0">
<li class="d-flex justify-content-between align-items-center">
<div><i class="fas fa-id-card me-2 text-primary-theme"></i> <strong>{% trans "First Name" %}</strong></div>
<span class="text-end">{{ candidate.first_name|default:"" }}</span>
<span class="text-end">{{ applicant.first_name|default:"" }}</span>
</li>
<li class="d-flex justify-content-between align-items-center">
<div><i class="fas fa-id-card me-2 text-primary-theme"></i> <strong>{% trans "Last Name" %}</strong></div>
<span class="text-end">{{ candidate.last_name|default:"" }}</span>
<span class="text-end">{{ applicant.last_name|default:"" }}</span>
</li>
{% if candidate.middle_name %}
{% if applicant.middle_name %}
<li class="d-flex justify-content-between align-items-center">
<div><i class="fas fa-id-card me-2 text-primary-theme"></i> <strong>{% trans "Middle Name" %}</strong></div>
<span class="text-end">{{ candidate.middle_name }}</span>
<span class="text-end">{{ applicant.middle_name }}</span>
</li>
{% endif %}
<li class="d-flex justify-content-between align-items-center">
<div><i class="fas fa-envelope me-2 text-primary-theme"></i> <strong>{% trans "Email" %}</strong></div>
<span class="text-end">{{ candidate.email|default:"" }}</span>
<span class="text-end">{{ applicant.email|default:"" }}</span>
</li>
</ul>
</div>
@ -383,19 +383,19 @@
<ul class="list-unstyled profile-data-list p-0">
<li class="d-flex justify-content-between align-items-center">
<div><i class="fas fa-phone-alt me-2 text-primary-theme"></i> <strong>{% trans "Phone" %}</strong></div>
<span class="text-end">{{ candidate.phone|default:"" }}</span>
<span class="text-end">{{ applicant.phone|default:"" }}</span>
</li>
{% if candidate.address %}
{% if applicant.address %}
<li class="d-flex align-items-start">
<div class="mb-1"><i class="fas fa-map-marker-alt me-2 text-primary-theme"></i> <strong>{% trans "Address" %}</strong></div>
<span class="text-end text-break">{{ candidate.address|linebreaksbr }}</span>
<span class="text-end text-break">{{ applicant.address|linebreaksbr }}</span>
</li>
{% endif %}
{% if candidate.linkedin_profile %}
{% if applicant.linkedin_profile %}
<li class="d-flex justify-content-between align-items-center">
<div><i class="fab fa-linkedin me-2 text-primary-theme"></i> <strong>{% trans "LinkedIn Profile" %}</strong></div>
<span class="text-end">
<a href="{{ candidate.linkedin_profile }}" target="_blank" class="text-primary-theme text-decoration-none">
<a href="{{ applicant.linkedin_profile }}" target="_blank" class="text-primary-theme text-decoration-none">
{% trans "View Profile" %} <i class="fas fa-external-link-alt ms-1"></i>
</a>
</span>
@ -412,15 +412,15 @@
<ul class="list-unstyled profile-data-list p-0">
<li class="d-flex justify-content-between align-items-center">
<div><i class="fas fa-calendar-alt me-2 text-primary-theme"></i> <strong>{% trans "Date of Birth" %}</strong></div>
<span class="text-end">{{ candidate.date_of_birth|date:"M d, Y"|default:"" }}</span>
<span class="text-end">{{ applicant.date_of_birth|date:"M d, Y"|default:"" }}</span>
</li>
<li class="d-flex justify-content-between align-items-center">
<div><i class="fas fa-venus-mars me-2 text-primary-theme"></i> <strong>{% trans "Gender" %}</strong></div>
<span class="text-end">{{ candidate.get_gender_display|default:"" }}</span>
<span class="text-end">{{ applicant.get_gender_display|default:"" }}</span>
</li>
<li class="d-flex justify-content-between align-items-center">
<div><i class="fas fa-globe me-2 text-primary-theme"></i> <strong>{% trans "Nationality" %}</strong></div>
<span class="text-end">{{ candidate.get_nationality_display|default:"" }}</span>
<span class="text-end">{{ applicant.get_nationality_display|default:"" }}</span>
</li>
</ul>
</div>
@ -431,16 +431,16 @@
<i class="fas fa-briefcase me-2 text-primary-theme"></i>{% trans "Professional Information" %}
</h4>
<ul class="list-unstyled profile-data-list p-0">
{% if candidate.user.designation %}
{% if applicant.user.designation %}
<li class="d-flex justify-content-between align-items-center">
<div><i class="fas fa-user-tie me-2 text-primary-theme"></i> <strong>{% trans "Designation" %}</strong></div>
<span class="text-end">{{ candidate.user.designation }}</span>
<span class="text-end">{{ applicant.user.designation }}</span>
</li>
{% endif %}
{% if candidate.gpa %}
{% if applicant.gpa %}
<li class="d-flex justify-content-between align-items-center">
<div><i class="fas fa-graduation-cap me-2 text-primary-theme"></i> <strong>{% trans "GPA" %}</strong></div>
<span class="text-end">{{ candidate.gpa }}</span>
<span class="text-end">{{ applicant.gpa }}</span>
</li>
{% endif %}
</ul>
@ -626,7 +626,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body" id="passwordModalBody">
<form action="{% url 'portal_password_reset' candidate.pk %}" method="post">
<form action="{% url 'portal_password_reset' applicant.pk %}" method="post">
{% csrf_token %}
{{ password_reset_form|crispy }}
<button type="submit" class="btn btn-main-action">{% trans "Change Password" %}</button>
@ -645,7 +645,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="{% url 'user_profile_image_update' candidate.pk %}" enctype="multipart/form-data" >
<form method="post" action="{% url 'user_profile_image_update' applicant.pk %}" enctype="multipart/form-data" >
{% csrf_token %}
<div class="mb-3">
@ -709,7 +709,7 @@
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<form method="post" action="{% url 'document_upload' candidate.id %}" enctype="multipart/form-data" id="documentUploadForm">
<form method="post" action="{% url 'document_upload' applicant.id %}" enctype="multipart/form-data" id="documentUploadForm">
<input type="hidden" name="upload_target" value="person">
{% csrf_token %}
<div class="mb-3">