update the bulk button to select

This commit is contained in:
ismail 2025-10-14 15:55:53 +03:00
parent d0db3d1323
commit 302aa8d0bf
16 changed files with 328 additions and 193 deletions

View File

@ -0,0 +1,14 @@
# Generated by Django 5.2.6 on 2025-10-14 11:03
from django.db import migrations
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0010_alter_scheduledinterview_schedule'),
('recruitment', '0011_alter_jobpostingimage_job_and_more'),
]
operations = [
]

View File

@ -0,0 +1,21 @@
# Generated by Django 5.2.6 on 2025-10-14 11:24
import django.db.models.deletion
from django.conf import settings
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0012_merge_20251014_1403'),
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
]
operations = [
migrations.AlterField(
model_name='formtemplate',
name='created_by',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='form_templates', to=settings.AUTH_USER_MODEL),
),
]

View File

@ -273,12 +273,12 @@ class Candidate(Base):
CANDIDATE = "Candidate", _("Candidate") CANDIDATE = "Candidate", _("Candidate")
# Stage transition validation constants # Stage transition validation constants
STAGE_SEQUENCE = { # STAGE_SEQUENCE = {
"Applied": ["Exam", "Interview", "Offer"], # "Applied": ["Exam", "Interview", "Offer"],
"Exam": ["Interview", "Offer"], # "Exam": ["Interview", "Offer"],
"Interview": ["Offer"], # "Interview": ["Offer"],
"Offer": [], # Final stage - no further transitions # "Offer": [], # Final stage - no further transitions
} # }
job = models.ForeignKey( job = models.ForeignKey(
JobPosting, JobPosting,
@ -375,50 +375,50 @@ class Candidate(Base):
return self.resume.size return self.resume.size
return 0 return 0
def clean(self): # def clean(self):
"""Validate stage transitions""" # """Validate stage transitions"""
# Only validate if this is an existing record (not being created) # # Only validate if this is an existing record (not being created)
if self.pk and self.stage != self.__class__.objects.get(pk=self.pk).stage: # if self.pk and self.stage != self.__class__.objects.get(pk=self.pk).stage:
old_stage = self.__class__.objects.get(pk=self.pk).stage # old_stage = self.__class__.objects.get(pk=self.pk).stage
allowed_next_stages = self.STAGE_SEQUENCE.get(old_stage, []) # allowed_next_stages = self.STAGE_SEQUENCE.get(old_stage, [])
if self.stage not in allowed_next_stages: # if self.stage not in allowed_next_stages:
raise ValidationError( # raise ValidationError(
{ # {
"stage": f'Cannot transition from "{old_stage}" to "{self.stage}". ' # "stage": f'Cannot transition from "{old_stage}" to "{self.stage}". '
f"Allowed transitions: {', '.join(allowed_next_stages) or 'None (final stage)'}" # f"Allowed transitions: {', '.join(allowed_next_stages) or 'None (final stage)'}"
} # }
) # )
# Validate that the stage is a valid choice # # Validate that the stage is a valid choice
if self.stage not in [choice[0] for choice in self.Stage.choices]: # if self.stage not in [choice[0] for choice in self.Stage.choices]:
raise ValidationError( # raise ValidationError(
{ # {
"stage": f"Invalid stage. Must be one of: {', '.join(choice[0] for choice in self.Stage.choices)}" # "stage": f"Invalid stage. Must be one of: {', '.join(choice[0] for choice in self.Stage.choices)}"
} # }
) # )
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
"""Override save to ensure validation is called""" """Override save to ensure validation is called"""
self.clean() # Call validation before saving self.clean() # Call validation before saving
super().save(*args, **kwargs) super().save(*args, **kwargs)
def can_transition_to(self, new_stage): # def can_transition_to(self, new_stage):
"""Check if a stage transition is allowed""" # """Check if a stage transition is allowed"""
if not self.pk: # New record - can be in Applied stage # if not self.pk: # New record - can be in Applied stage
return new_stage == "Applied" # return new_stage == "Applied"
old_stage = self.__class__.objects.get(pk=self.pk).stage # old_stage = self.__class__.objects.get(pk=self.pk).stage
allowed_next_stages = self.STAGE_SEQUENCE.get(old_stage, []) # allowed_next_stages = self.STAGE_SEQUENCE.get(old_stage, [])
return new_stage in allowed_next_stages # return new_stage in allowed_next_stages
def get_available_stages(self): # def get_available_stages(self):
"""Get list of stages this candidate can transition to""" # """Get list of stages this candidate can transition to"""
if not self.pk: # New record # if not self.pk: # New record
return ["Applied"] # return ["Applied"]
old_stage = self.__class__.objects.get(pk=self.pk).stage # old_stage = self.__class__.objects.get(pk=self.pk).stage
return self.STAGE_SEQUENCE.get(old_stage, []) # return self.STAGE_SEQUENCE.get(old_stage, [])
@property @property
def submission(self): def submission(self):
@ -544,7 +544,7 @@ class FormTemplate(Base):
blank=True, help_text="Description of the form template" blank=True, help_text="Description of the form template"
) )
created_by = models.ForeignKey( created_by = models.ForeignKey(
User, on_delete=models.CASCADE, related_name="form_templates" User, on_delete=models.CASCADE, related_name="form_templates",null=True,blank=True
) )
is_active = models.BooleanField( is_active = models.BooleanField(
default=False, help_text="Whether this template is active" default=False, help_text="Whether this template is active"

View File

@ -7,10 +7,10 @@ from .models import FormField,FormStage,FormTemplate,Candidate,JobPosting
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
@receiver(post_save, sender=JobPosting) # @receiver(post_save, sender=JobPosting)
def create_form_for_job(sender, instance, created, **kwargs): # def create_form_for_job(sender, instance, created, **kwargs):
if created: # if created:
FormTemplate.objects.create(job=instance, is_active=True, name=instance.title) # FormTemplate.objects.create(job=instance, is_active=True, name=instance.title)
@receiver(post_save, sender=Candidate) @receiver(post_save, sender=Candidate)
def score_candidate_resume(sender, instance, created, **kwargs): def score_candidate_resume(sender, instance, created, **kwargs):
if not instance.is_resume_parsed: if not instance.is_resume_parsed:

View File

@ -17,7 +17,6 @@ urlpatterns = [
path('jobs/<slug:slug>/candidate/application/success', views.application_success, name='application_success'), path('jobs/<slug:slug>/candidate/application/success', views.application_success, name='application_success'),
path('careers/',views.kaauh_career,name='kaauh_career'), path('careers/',views.kaauh_career,name='kaauh_career'),
# LinkedIn Integration URLs # LinkedIn Integration URLs
path('jobs/<slug:slug>/post-to-linkedin/', views.post_to_linkedin, name='post_to_linkedin'), path('jobs/<slug:slug>/post-to-linkedin/', views.post_to_linkedin, name='post_to_linkedin'),
path('jobs/linkedin/login/', views.linkedin_login, name='linkedin_login'), path('jobs/linkedin/login/', views.linkedin_login, name='linkedin_login'),
@ -34,7 +33,6 @@ urlpatterns = [
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'),
path('training/create/', views_frontend.TrainingCreateView.as_view(), name='training_create'), path('training/create/', views_frontend.TrainingCreateView.as_view(), name='training_create'),
@ -75,7 +73,8 @@ urlpatterns = [
path('htmx/<int:pk>/candidate_criteria_view/', views.candidate_criteria_view_htmx, name='candidate_criteria_view_htmx'), path('htmx/<int:pk>/candidate_criteria_view/', views.candidate_criteria_view_htmx, name='candidate_criteria_view_htmx'),
path('htmx/<slug:slug>/candidate_set_exam_date/', views.candidate_set_exam_date, name='candidate_set_exam_date'), path('htmx/<slug:slug>/candidate_set_exam_date/', views.candidate_set_exam_date, name='candidate_set_exam_date'),
path('htmx/bulk_candidate_move_to_exam/', views.bulk_candidate_move_to_exam, name='bulk_candidate_move_to_exam'),
path('htmx/<slug:slug>/candidate_update_status/', views.candidate_update_status, name='candidate_update_status'),
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'),

View File

@ -5,7 +5,7 @@ from rich import print
from django.template.loader import render_to_string from django.template.loader import render_to_string
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from django.views.decorators.http import require_http_methods from django.views.decorators.http import require_http_methods
from django.http import JsonResponse from django.http import HttpResponse, JsonResponse
from datetime import datetime,time,timedelta from datetime import datetime,time,timedelta
from django.views import View from django.views import View
from django.db.models import Q from django.db.models import Q
@ -262,12 +262,13 @@ def create_job(request):
else: else:
job.created_by = request.POST.get("created_by", "").strip() job.created_by = request.POST.get("created_by", "").strip()
if not job.created_by: if not job.created_by:
job.created_by = "University Administrator" job.created_by = request.user.username
job.save() job.save()
job_apply_url_relative=reverse('job_detail_candidate',kwargs={'slug':job.slug}) job_apply_url_relative=reverse('job_detail_candidate',kwargs={'slug':job.slug})
job_apply_url_absolute=request.build_absolute_uri(job_apply_url_relative) job_apply_url_absolute=request.build_absolute_uri(job_apply_url_relative)
job.application_url=job_apply_url_absolute job.application_url=job_apply_url_absolute
FormTemplate.objects.create(job=job, is_active=True, name=job.title,created_by=request.user)
job.save() job.save()
messages.success(request, f'Job "{job.title}" created successfully!') messages.success(request, f'Job "{job.title}" created successfully!')
return redirect("job_list") return redirect("job_list")
@ -1646,12 +1647,10 @@ def candidate_screening_view(request, slug):
# This catches if the user enters non-numeric text (e.g., "abc") # This catches if the user enters non-numeric text (e.g., "abc")
min_ai_score = 0 min_ai_score = 0
tier1_count = 0 tier1_count = 0
print(min_ai_score)
print(tier1_count)
# You can now safely use min_ai_score and tier1_count as integers (0 or greater) # You can now safely use min_ai_score and tier1_count as integers (0 or greater)
if min_ai_score > 0: if min_ai_score > 0:
candidates = candidates.filter(match_score__gte=min_ai_score) candidates = candidates.filter(match_score__gte=min_ai_score)
print(candidates)
if tier1_count > 0: if tier1_count > 0:
candidates = candidates[:tier1_count] candidates = candidates[:tier1_count]
@ -1697,7 +1696,6 @@ def update_candidate_exam_status(request, slug):
def bulk_update_candidate_exam_status(request,slug): def bulk_update_candidate_exam_status(request,slug):
job = get_object_or_404(JobPosting, slug=slug) job = get_object_or_404(JobPosting, slug=slug)
status = request.headers.get('status') status = request.headers.get('status')
if status: if status:
for candidate in get_candidates_from_request(request): for candidate in get_candidates_from_request(request):
try: try:
@ -1724,19 +1722,18 @@ def candidate_set_exam_date(request, slug):
messages.success(request, f"Set exam date for {candidate.name} to {candidate.exam_date}") messages.success(request, f"Set exam date for {candidate.name} to {candidate.exam_date}")
return redirect("candidate_screening_view", slug=candidate.job.slug) return redirect("candidate_screening_view", slug=candidate.job.slug)
def bulk_candidate_move_to_exam(request): def candidate_update_status(request, slug):
for candidate in get_candidates_from_request(request): job = get_object_or_404(JobPosting, slug=slug)
candidate.stage = "Exam" mark_as = request.POST.get('mark_as')
candidate.applicant_status = "Candidate" candidate_ids = request.POST.getlist("candidate_ids")
candidate.exam_date = timezone.now()
candidate.save()
messages.success(request, f"Candidates Moved to Exam stage") if c := Candidate.objects.filter(pk__in = candidate_ids):
return redirect("candidate_screening_view", slug=candidate.job.slug) c.update(stage=mark_as,exam_date=timezone.now(),applicant_status="Candidate" if mark_as in ["Exam","Interview","Offer"] else "Applicant")
# def response():
# yield SSE.patch_elements("","") messages.success(request, f"Candidates Updated")
# yield SSE.execute_script("console.log('hello world');") response = HttpResponse(redirect("candidate_screening_view", slug=job.slug))
# return DatastarResponse(response()) response.headers["HX-Refresh"] = "true"
return response
def candidate_interview_view(request,slug): def candidate_interview_view(request,slug):
job = get_object_or_404(JobPosting,slug=slug) job = get_object_or_404(JobPosting,slug=slug)

View File

@ -588,7 +588,7 @@
}); });
</script> </script>
<script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js"></script> <script src="https://cdn.jsdelivr.net/npm/htmx.org@2.0.7/dist/htmx.min.js"></script>
<script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.5/bundles/datastar.js"></script> {% comment %} <script type="module" src="https://cdn.jsdelivr.net/gh/starfederation/datastar@1.0.0-RC.5/bundles/datastar.js"></script> {% endcomment %}
{% block customJS %}{% endblock %} {% block customJS %}{% endblock %}

View File

@ -253,32 +253,23 @@
<div class="kaauh-card shadow-sm p-3"> <div class="kaauh-card shadow-sm p-3">
<div class="candidate-table-responsive" data-signals__ifmissing="{_fetching: false, selections: Array({{ candidates|length }}).fill(false)}"> <div class="candidate-table-responsive" data-signals__ifmissing="{_fetching: false, selections: Array({{ candidates|length }}).fill(false)}">
{% url "bulk_update_candidate_exam_status" job.slug as bulk_update_candidate_exam_status_url %} <div class="col-md-3 col-sm-6 mb-3 d-flex gap-2">
<div class="mb-3 d-flex gap-2">
{% if candidates %} {% if candidates %}
<button class="btn btn-bulk-pass btn-sm" <form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post">
data-attr="{disabled: !$selections.filter(Boolean).length}" <div class="d-flex align-items-center">
data-on-click="@post('{{bulk_update_candidate_exam_status_url}}',{ <select name="mark_as" id="update_status" class="form-select form-select-sm" style="height: 3rem;">
contentType: 'form', <option value="Applied">
selector: '#candidate-form', <i class="fas fa-arrow-left me-1"></i> {% trans "Apply" %}
headers: {'X-CSRFToken': '{{ csrf_token }}','status': 'Passed'} </option>
})" <option value="Interview">
> <i class="fas fa-arrow-right me-1"></i> {% trans "Interview" %}
<i class="fas fa-check-circle me-1"></i> </option>
{% trans "Bulk Mark Passed (-> Interview)" %} </select>
</button> <button type="submit" class="btn btn-main-action btn-mds ms-2">
<button class="btn btn-bulk-fail btn-sm" <i class="fas fa-arrow-right me-1"></i> {% trans "Update" %}
data-attr="{disabled: !$selections.filter(Boolean).length}" </button>
data-on-click="@post('{{bulk_update_candidate_exam_status_url}}',{ </div>
contentType: 'form', </form>
selector: '#candidate-form',
headers: {'X-CSRFToken': '{{ csrf_token }}','status': 'Failed'}
})"
>
<i class="fas fa-times-circle me-1"></i>
{% trans "Bulk Mark Failed" %}
</button>
{% endif %} {% endif %}
</div> </div>
@ -291,11 +282,7 @@
{% if candidates %} {% if candidates %}
<div class="form-check"> <div class="form-check">
<input <input
data-bind-_all type="checkbox" class="form-check-input" id="selectAllCheckbox">
data-on-change="$selections = Array({{ candidates|length }}).fill($_all)"
data-effect="$selections; $_all = $selections.every(Boolean)"
data-attr-disabled="$_fetching"
type="checkbox" class="form-check-input" id="checkAll">
</div> </div>
{% endif %} {% endif %}
</th> </th>
@ -312,13 +299,11 @@
<tr> <tr>
<td> <td>
<div class="form-check"> <div class="form-check">
<input <input
data-bind-selections name="candidate_ids"
data-attr-disabled="$_fetching" value="{{ candidate.id }}"
name="candidate_ids" type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
value="{{ candidate.id }}" </div>
type="checkbox" class="form-check-input" id="candidate-{{ candidate.id }}">
</div>
</td> </td>
<td> <td>
<div class="candidate-name"> <div class="candidate-name">
@ -412,3 +397,69 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block customJS %}
<script>
document.addEventListener('DOMContentLoaded', function () {
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
if (selectAllCheckbox) {
// Function to safely update the header checkbox state
function updateSelectAllState() {
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
const totalCount = rowCheckboxes.length;
if (checkedCount === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
} else if (checkedCount === totalCount) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else {
// Set to indeterminate state (partially checked)
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
}
// IMPORTANT: We do NOT fire a change event here to prevent the infinite loop.
// Your existing data-bind-_all logic should handle the bulk action status.
}
// 1. Logic for the 'Select All' checkbox (Clicking it updates all rows)
selectAllCheckbox.addEventListener('change', function () {
const isChecked = selectAllCheckbox.checked;
// Temporarily disable the change listener on rows to prevent cascading events
rowCheckboxes.forEach(checkbox => checkbox.removeEventListener('change', updateSelectAllState));
// Update all row checkboxes
rowCheckboxes.forEach(function (checkbox) {
checkbox.checked = isChecked;
// You must still dispatch the event here so your framework's data-bind-selections
// picks up the change on individual elements. This should NOT trigger the updateSelectAllState.
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
});
// Re-attach the change listeners to the rows
rowCheckboxes.forEach(checkbox => checkbox.addEventListener('change', updateSelectAllState));
// Ensure the header state is correct after forcing all changes
updateSelectAllState();
});
// 2. Logic to update 'Select All' state based on row checkboxes
// Attach the function to be called whenever a row checkbox changes
rowCheckboxes.forEach(function (checkbox) {
checkbox.addEventListener('change', updateSelectAllState);
});
// Initial check to set the correct state on load (in case items are pre-checked)
updateSelectAllState();
}
});
</script>
{% endblock %}

View File

@ -194,48 +194,40 @@
</a> </a>
</div> </div>
<div class="filter-controls shadow-sm"> <div class="filter-controls shadow-sm">
<h4 class="h6 mb-3 fw-bold" style="color: var(--kaauh-primary-text);"> <h4 class="h6 mb-3 fw-bold" style="color: var(--kaauh-primary-text);">
<i class="fas fa-sort-numeric-up me-1"></i> {% trans "AI Scoring & Top Candidate Filter" %} <i class="fas fa-sort-numeric-up me-1"></i> {% trans "AI Scoring & Top Candidate Filter" %}
</h4> </h4>
<form method="GET" class="mb-0"> <form method="GET" class="mb-0 pb-3">
{% csrf_token %} {% csrf_token %}
<div class="row g-3 align-items-end"> <div class="d-flex flex-nowrap g-3 align-items-end" style="overflow-x: auto;">
<div class="col-md-3 col-sm-6"> <div class="p-2">
<label for="min_ai_score" class="form-label small text-muted"> <label for="min_ai_score" class="form-label small text-muted mb-1">
{% trans "Minimum AI Score" %} {% trans "Min AI Score" %}
</label>
<input type="number" name="min_ai_score" id="min_ai_score" class="form-control form-control-sm"
value="{{ min_ai_score}}" min="0" max="100" step="1"
placeholder="e.g., 75" style="min-width: 120px;">
</div>
<div class="col-md-2 col-sm-6"> <div class="p-2">
<label for="min_ai_score" class="form-label small text-muted mb-1"> <label for="tier1_count" class="form-label small text-muted mb-1">
{% trans "Min AI Score" %} {% trans "Top N" %}
</label> </label>
<input type="number" name="min_ai_score" id="min_ai_score" class="form-control form-control-sm" <input type="number" name="tier1_count" id="tier1_count" class="form-control form-control-sm"
value="{{ min_ai_score}}" min="0" max="100" step="1" value="{{ tier1_count }}" min="1" max="{{ total_candidates }}" style="min-width: 100px;">
placeholder="e.g., 75"> </div>
</div>
<div class="col-md-2 col-sm-6"> <div class="p-2">
<label for="tier1_count" class="form-label small text-muted mb-1"> <label class="form-label small text-muted mb-1 d-block">&nbsp;</label>
{% trans "Top N" %} <button type="submit" name="update_tiers" class="btn btn-main-action btn-sm w-100" style="min-width: 150px;">
</label> <i class="fas fa-sync-alt me-1"></i> {% trans "Update Filters" %}
<input type="number" name="tier1_count" id="tier1_count" class="form-control form-control-sm" </button>
value="{{ tier1_count }}" min="1" max="{{ total_candidates }}"> </div>
</div> </div>
</form>
<div class="col-md-3 col-sm-6">
<button type="submit" name="update_tiers" class="btn btn-main-action btn-sm w-100">
<i class="fas fa-sync-alt me-1"></i> {% trans "Update Filters" %}
</button>
</div>
{% comment %} Empty col for spacing (2 + 2 + 3 + 5 = 12) {% endcomment %}
<div class="col-md-5 d-none d-md-block"></div>
</div>
</form>
</div> </div>
<h2 class="h4 mb-3" style="color: var(--kaauh-primary-text);"> <h2 class="h4 mb-3" style="color: var(--kaauh-primary-text);">
@ -244,22 +236,23 @@
</h2> </h2>
<div class="kaauh-card shadow-sm p-3"> <div class="kaauh-card shadow-sm p-3">
{% url "bulk_candidate_move_to_exam" as move_to_exam_url %}
{% if candidates %} {% if candidates %}
<button class="btn btn-bulk-action btn-sm mb-3" <form hx-boost="true" hx-include="#candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post">
data-attr="{disabled: !$selections.filter(Boolean).length}" <div class="d-flex align-items-center">
data-on-click="@post('{{move_to_exam_url}}',{ <select name="mark_as" id="update_status" class="form-select form-select-sm" style="height: 3rem;">
contentType: 'form', <option value="Exam">
selector: '#candidate-form', <i class="fas fa-arrow-right me-1"></i> {% trans "Exam" %}
headers: {'X-CSRFToken': '{{ csrf_token }}'}})" </option>
> </select>
<i class="fas fa-arrow-right me-1"></i> {% trans "Bulk Move to Exam" %} <button type="submit" class="btn btn-main-action btn-mds ms-2">
</button> <i class="fas fa-arrow-right me-1"></i> {% trans "Update" %}
</button>
</div>
</form>
{% endif %} {% endif %}
<div class="table-responsive"> <div class="table-responsive">
<form id="candidate-form" action="{{move_to_exam_url}}" method="post"> <form id="candidate-form" action="{% url 'candidate_update_status' job.slug %}" method="post">
{% csrf_token %} {% csrf_token %}
<table class="table candidate-table align-middle"> <table class="table candidate-table align-middle">
<thead> <thead>
@ -268,11 +261,7 @@
{% if candidates %} {% if candidates %}
<div class="form-check"> <div class="form-check">
<input <input
data-bind-_all type="checkbox" class="form-check-input" id="selectAllCheckbox">
data-on-change="$selections = Array({{ candidates|length }}).fill($_all)"
data-effect="$selections; $_all = $selections.every(Boolean)"
data-attr-disabled="$_fetching"
type="checkbox" class="form-check-input" id="checkAll">
</div> </div>
{% endif %} {% endif %}
</th> </th>
@ -290,11 +279,9 @@
<td> <td>
<div class="form-check"> <div class="form-check">
<input <input
data-bind-selections
data-attr-disabled="$_fetching"
name="candidate_ids" name="candidate_ids"
value="{{ candidate.id }}" value="{{ candidate.id }}"
type="checkbox" class="form-check-input" id="candidate-{{ candidate.id }}"> type="checkbox" class="form-check-input rowCheckbox" id="candidate-{{ candidate.id }}">
</div> </div>
</td> </td>
<td> <td>
@ -379,3 +366,69 @@
</div> </div>
</div> </div>
{% endblock %} {% endblock %}
{% block customJS %}
<script>
document.addEventListener('DOMContentLoaded', function () {
const selectAllCheckbox = document.getElementById('selectAllCheckbox');
const rowCheckboxes = document.querySelectorAll('.rowCheckbox');
if (selectAllCheckbox) {
// Function to safely update the header checkbox state
function updateSelectAllState() {
const checkedCount = Array.from(rowCheckboxes).filter(cb => cb.checked).length;
const totalCount = rowCheckboxes.length;
if (checkedCount === 0) {
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = false;
} else if (checkedCount === totalCount) {
selectAllCheckbox.checked = true;
selectAllCheckbox.indeterminate = false;
} else {
// Set to indeterminate state (partially checked)
selectAllCheckbox.checked = false;
selectAllCheckbox.indeterminate = true;
}
// IMPORTANT: We do NOT fire a change event here to prevent the infinite loop.
// Your existing data-bind-_all logic should handle the bulk action status.
}
// 1. Logic for the 'Select All' checkbox (Clicking it updates all rows)
selectAllCheckbox.addEventListener('change', function () {
const isChecked = selectAllCheckbox.checked;
// Temporarily disable the change listener on rows to prevent cascading events
rowCheckboxes.forEach(checkbox => checkbox.removeEventListener('change', updateSelectAllState));
// Update all row checkboxes
rowCheckboxes.forEach(function (checkbox) {
checkbox.checked = isChecked;
// You must still dispatch the event here so your framework's data-bind-selections
// picks up the change on individual elements. This should NOT trigger the updateSelectAllState.
checkbox.dispatchEvent(new Event('change', { bubbles: true }));
});
// Re-attach the change listeners to the rows
rowCheckboxes.forEach(checkbox => checkbox.addEventListener('change', updateSelectAllState));
// Ensure the header state is correct after forcing all changes
updateSelectAllState();
});
// 2. Logic to update 'Select All' state based on row checkboxes
// Attach the function to be called whenever a row checkbox changes
rowCheckboxes.forEach(function (checkbox) {
checkbox.addEventListener('change', updateSelectAllState);
});
// Initial check to set the correct state on load (in case items are pre-checked)
updateSelectAllState();
}
});
</script>
{% endblock %}