[UPDT] RECRUITMENT: Skillzone archive update and dashboard design

This commit is contained in:
Horilla
2024-03-08 10:46:09 +05:30
parent 21a096c1a3
commit 30d3e2df41
10 changed files with 215 additions and 201 deletions

View File

@@ -6,6 +6,7 @@ This page is used to register the model with admins site.
from django.contrib import admin
from recruitment.models import (
CandidateRating,
SkillZone,
Stage,
Recruitment,
Candidate,
@@ -25,3 +26,4 @@ admin.site.register(RecruitmentSurveyAnswer)
admin.site.register(RecruitmentSurvey)
admin.site.register(RecruitmentMailTemplate)
admin.site.register(CandidateRating)
admin.site.register(SkillZone)

View File

@@ -479,6 +479,7 @@ class SkillZoneCandFilter(FilterSet):
model = SkillZoneCandidate
fields = [
"is_active",
"candidate_id",
"candidate_id__recruitment_id",
"candidate_id__stage_id",

View File

@@ -825,6 +825,7 @@ class SkillZoneCreateForm(ModelForm):
"created_on",
"objects",
"company_id",
"is_active",
]
def as_p(self, *args, **kwargs):
@@ -853,6 +854,7 @@ class SkillZoneCandidateForm(ModelForm):
fields = "__all__"
exclude = [
"added_on",
"is_active",
]
def as_p(self, *args, **kwargs):

View File

@@ -152,7 +152,7 @@
</div>
<div class="col-12 col-sm-12 col-md-6 col-lg-3 oh-card-dashboard--moveable me-4" style="background-color: white;">
{% comment %} <div class="col-12 col-sm-12 col-md-6 col-lg-3 oh-card-dashboard--moveable me-4" style="background-color: white;">
<div class="oh-card-dashboard oh-card-dashboard--no-scale oh-card-dashboard--transparent ">
<div class="oh-card-dashboard__header oh-card-dashboard__header--divider">
<span class="oh-card-dashboard__title">{% trans "Open Positions By Department" %}</span>
@@ -171,6 +171,33 @@
<h3 style="font-size:16px; margin-bottom:69px;" class="oh-404__subtitle">{% trans "No department-specific vacancies currently." %}</h3>
{% endif %}
</div>
</div>
</div> {% endcomment %}
<div class="col-12 col-sm-12 col-md-6 col-lg-3 oh-card-dashboard--moveable me-4" style="background-color: white;">
<div class="oh-card-dashboard oh-card-dashboard--no-scale oh-card-dashboard--transparent ">
<div class="oh-card-dashboard__header oh-card-dashboard__header--divider">
<span class="oh-card-dashboard__title">{% trans "Candidate Offer Letter Status" %}</span>
</div>
<div class="oh-card-dashboard__body" >
{% if total_candidates %}
<div >
<canvas id="candidateChart"></canvas>
</div>
{% else %}
<div style="display:flex;align-items: center;justify-content: center;">
<div style="height:282px; justify-content:center;align-items:center; margin-top: -59px;" class="d-flex">
<img style="height:125px;" src="{% static 'images/ui/no_vacancy.png' %}" class=""/>
</div>
</div>
<h3 style="font-size:16px; margin-bottom:69px;" class="oh-404__subtitle">{% trans "No Candidates available." %}</h3>
{% endif %}
</div>
</div>
</div>
@@ -216,7 +243,7 @@
<div class="oh-dashboard__movable-cards row mt-4">
{% if request.user|is_in_task_managers %}
<div
class="col-12 col-sm-12 col-md-6 col-lg-7 oh-card-dashboard--moveable me-4 ms-1"
class="col-12 col-sm-12 col-md-6 col-lg-7 oh-card-dashboard--moveable"
style="height: 438px;
background-color: white;"
id="taskStatus"
@@ -226,7 +253,7 @@
</div>
{% endif %}
<div
class="col-12 col-sm-12 col-md-6 col-lg-4 oh-card-dashboard--moveable"
class="col-12 col-sm-12 col-md-6 col-lg-5 oh-card-dashboard--moveable"
>
<div
class="oh-card-dashboard oh-card-dashboard--no-scale oh-card-dashboard--transparent"
@@ -239,7 +266,7 @@
</div>
<div class="oh-card-dashboard__body">
{% if stage_chart_count %}
<canvas id="recruitmentChart1" width="400" height="290"></canvas>
<canvas id="recruitmentChart1" width="385" height="228"></canvas>
{% else %}
<div style="height: 334px; display:flex;align-items: center;justify-content: center;" class="">
<div style="" class="">
@@ -282,104 +309,80 @@
</div>
{% endif %}
</div>
<div class="col-12 col-sm-12 col-md-6 col-lg-4 ms-3 oh-card-dashboard--moveable me-4" style="background-color: white;">
<div class="oh-card-dashboard oh-card-dashboard--no-scale oh-card-dashboard--transparent ">
<div class="oh-card-dashboard__header oh-card-dashboard__header--divider">
<span class="oh-card-dashboard__title">{% trans "Candidate Offer Letter Status" %}</span>
{% if ongoing_recruitments %}
<div class="col-12 col-sm-12 col-md-12 col-lg-5 oh-card-dashboard--moveable">
<div>
<div class="oh-card-dashboard__header">
<span class="oh-card-dashboard__title">{% trans "Current Hiring Pipeline" %}</span>
</div>
<div class="oh-card-dashboard__body" >
{% if total_candidates %}
<div >
<canvas id="candidateChart"></canvas>
</div>
{% else %}
<div style="display:flex;align-items: center;justify-content: center;">
<div style="height:282px; justify-content:center;align-items:center; margin-top: -59px;" class="d-flex">
<img style="height:125px;" src="{% static 'images/ui/no_vacancy.png' %}" class=""/>
<div class="oh-sticky-table float-start" style="height:500px">
<div class="oh-sticky-table__table">
<div class="oh-sticky-table__thead">
<div class="oh-sticky-table__tr">
<div class="oh-sticky-table__th">{% trans "Job Positions" %}</div>
<div class="oh-sticky-table__th">{% trans "Initial" %}</div>
<div class="oh-sticky-table__th">{% trans "Test" %}</div>
<div class="oh-sticky-table__th">{% trans "Interview" %}</div>
<div class="oh-sticky-table__th">{% trans "Hired" %}</div>
<div class="oh-sticky-table__th">{% trans "Cancelled" %}</div>
</div>
</div>
<div class="oh-sticky-table__tbody">
{% for job_item in job_data %}
<div class="oh-sticky-table__tr">
<div class="oh-sticky-table__sd">{{ job_item.0 }}</div>
<div class="oh-sticky-table__td">{{ job_item.1 }}</div>
<div class="oh-sticky-table__td">{{ job_item.2 }}</div>
<div class="oh-sticky-table__td">{{ job_item.3 }}</div>
<div class="oh-sticky-table__td">{{ job_item.4 }}</div>
<div class="oh-sticky-table__td">{{ job_item.5 }}</div>
</div>
{% endfor %}
</div>
</div>
<h3 style="font-size:16px; margin-bottom:69px;" class="oh-404__subtitle">{% trans "No Candidates available." %}</h3>
{% endif %}
</div>
</div>
</div>
{% endif %}
</div>
</div>
<div class="oh-dashboard__movable-cards row mt-4 mb-5">
<div class="col-12 col-sm-12 col-md-12 col-lg-7 oh-card-dashboard--moveable">
<div>
{% if ongoing_recruitments %}
<div class="col-12 col-sm-12 col-md-12 col-lg-6 oh-card-dashboard--moveable">
<div class="oh-card-dashboard__header">
<span class="oh-card-dashboard__title">{% trans "Current Hiring Pipeline" %}</span>
</div>
<div class="oh-sticky-table float-start" style="height:500px">
<div class="oh-sticky-table__table">
<div class="oh-sticky-table__thead">
<div class="oh-sticky-table__tr">
<div class="oh-sticky-table__th">{% trans "Job Positions" %}</div>
<div class="oh-sticky-table__th">{% trans "Initial" %}</div>
<div class="oh-sticky-table__th">{% trans "Test" %}</div>
<div class="oh-sticky-table__th">{% trans "Interview" %}</div>
<div class="oh-sticky-table__th">{% trans "Hired" %}</div>
<div class="oh-sticky-table__th">{% trans "Cancelled" %}</div>
<span class="oh-card-dashboard__title">{% trans "Ongoing Recruitments & Hiring Managers" %}</span>
</div>
<div class="oh-sticky-table float-start" style="height:200px">
<div class="oh-sticky-table__table">
<div class="oh-sticky-table__thead">
<div class="oh-sticky-table__tr">
<div class="oh-sticky-table__th">{% trans "Recruitment" %}</div>
<div class="oh-sticky-table__th">{% trans "Manager" %}</div>
</div>
</div>
<div class="oh-sticky-table__tbody">
{% for recruitment_title, managers in recruitment_manager_mapping.items %}
<div class="oh-sticky-table__tr">
<div class="oh-sticky-table__sd">{{ recruitment_title }}</div>
<div class="oh-sticky-table__td">{{ managers|join:", " }}</div>
</div>
{% endfor %}
</div>
</div>
<div class="oh-sticky-table__tbody">
{% for job_item in job_data %}
<div class="oh-sticky-table__tr">
<div class="oh-sticky-table__sd">{{ job_item.0 }}</div>
<div class="oh-sticky-table__td">{{ job_item.1 }}</div>
<div class="oh-sticky-table__td">{{ job_item.2 }}</div>
<div class="oh-sticky-table__td">{{ job_item.3 }}</div>
<div class="oh-sticky-table__td">{{ job_item.4 }}</div>
<div class="oh-sticky-table__td">{{ job_item.5 }}</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
{% endif %}
</div>
<div class="col-12 col-sm-12 col-md-12 col-lg-5 oh-card-dashboard--moveable">
<div class="oh-card-dashboard__header">
<span class="oh-card-dashboard__title">{% trans "Ongoing Recruitments & Hiring Managers" %}</span>
</div>
<div class="oh-sticky-table float-start">
<div class="oh-sticky-table__table">
<div class="oh-sticky-table__thead">
<div class="oh-sticky-table__tr">
<div class="oh-sticky-table__th">{% trans "Recruitment" %}</div>
<div class="oh-sticky-table__th">{% trans "Manager" %}</div>
</div>
</div>
<div class="oh-sticky-table__tbody">
{% for recruitment_title, managers in recruitment_manager_mapping.items %}
<div class="oh-sticky-table__tr">
<div class="oh-sticky-table__sd">{{ recruitment_title }}</div>
<div class="oh-sticky-table__td">{{ managers|join:", " }}</div>
</div>
{% endfor %}
</div>
</div>
</div>
</div>
</div>
</div>
</div>

View File

@@ -308,7 +308,7 @@
data-job-position ="{{cand.job_position_id}}"
>
<div class="oh-sticky-table__sd" style="z-index: 11 !important;">
<div class="oh-sticky-table__sd" style="z-index: 11 !important;" onclick="event.stopPropagation()">
<div class="centered-div">
<input type="checkbox" id="65" class="oh-input candidate-checkbox oh-input__checkbox stage-candidate-row" onchange="highlightRow($(this))">
</div>

View File

@@ -40,6 +40,14 @@ x-data="{searchShow: false}"
</div>
</div>
</div>
{% if perms.recruitment.add_recruitment %}
<button class="oh-btn oh-btn--secondary" data-toggle="oh-modal-toggle" data-target="#addRecruitmentModal" hx-target="#addRecruitmentModalBody" hx-get="{% url "recruitment-create" %}">
<ion-icon class="me-1" name="add-outline"></ion-icon>
{% trans 'Recruitment' %}
</button>
{% endif %}
<div class="oh-main__titlebar-button-container">
{% if perms.recruitment.add_recruitment %}
@@ -51,6 +59,29 @@ x-data="{searchShow: false}"
</div>
</section>
<div
class="oh-modal"
id="addRecruitmentModal"
role="dialog"
aria-labelledby="addRecruitmentModal"
aria-hidden="true"
>
<div class="oh-modal__dialog">
<div class="oh-modal__dialog-header">
<h5 class="oh-modal__dialog-title" id="addRecruitmentModalLabel"
>{% trans "Add Recruitment" %}</span
>
<button class="oh-modal__close" aria-label="Close" >
<ion-icon name="close-outline"></ion-icon>
</button>
</div>
<div class="oh-modal__dialog-body" id="addRecruitmentModalBody">
</div>
</div>
</div>
<div
class="oh-wrapper-main"
>

View File

@@ -99,126 +99,82 @@
</div>
</div>
{% for sz_candidate in skill_zone.list %}
{% if sz_candidate.is_active %}
<div class="oh-sticky-table__tbody"
{% comment %} onclick="window.location.href = `{% url 'candidate-view-individual' sz_candidate.candidate_id.id %}` " {% endcomment %}
>
<div class="oh-sticky-table__tr" draggable="true">
<div class="oh-sticky-table__sd" >
<div class="oh-profile oh-profile--md">
<div class="oh-profile__avatar mr-1">
{% if sz_candidate.candidate_id.profile %}
<img src="{{sz_candidate.candidate_id.get_avatar}}" class="oh-profile__image"
alt="" />
{% else %}
<img src="https://ui-avatars.com/api/?name={{sz_candidate.candidate_id}}&background=random"
class="oh-profile__image" alt="" />
{% endif %}
</div>
<span class="oh-profile__name oh-text--dark">{{sz_candidate}}</span>
</div>
</div>
<div class="oh-sticky-table__td ">{{sz_candidate.reason}}</div>
<div class="oh-sticky-table__td ">{{sz_candidate.added_on}}</div>
<div class="oh-sticky-table__td ">
<a
style="text-decoration: none"
class="oh-btn oh-btn--light"
href="/media/{{sz_candidate.candidate_id.resume}}"
target="_blank"
title="{% trans 'Resume' %}"
rel="noopener noreferrer"
style="flex: 1 0 auto; width:20px;height: 40.68px; padding: 0;"
><ion-icon name="document-outline"></ion-icon
></a>
</div>
<div class="oh-sticky-table__td " onclick="event.stopPropagation()">
<div class="oh-btn-group">
<button
class="oh-btn oh-btn--light-bkg w-50"
title="{% trans 'Edit' %}"
data-toggle="oh-modal-toggle"
data-target="#editCandModal"
hx-get="{% url 'skill-zone-cand-edit' sz_candidate.id %}"
hx-target="#editCandTarget">
<ion-icon name="create-outline"></ion-icon>
</button>
{% if sz_candidate.is_active %}
<form action="{% url 'skill-zone-cand-archive' sz_candidate.id %}" title="Archive" onsubmit="return confirm('{% trans "Do you want to archive this candidate from this skill zone" %}')" method='post'
class="w-50">
{% csrf_token %}
<button type='submit' class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100"
><ion-icon name="archive"></ion-icon></button>
</form>
<div class="oh-sticky-table__tbody"
onclick="window.location.href = `{% url 'candidate-view-individual' sz_candidate.candidate_id.id %}` "
>
<div class="oh-sticky-table__tr" draggable="true">
<div class="oh-sticky-table__sd" >
<div class="oh-profile oh-profile--md">
<div class="oh-profile__avatar mr-1">
{% if sz_candidate.candidate_id.profile %}
<img src="{{sz_candidate.candidate_id.get_avatar}}" class="oh-profile__image"
alt="" />
{% else %}
<form action="{% url 'skill-zone-cand-archive' sz_candidate.id %}" title="Un Archive" onsubmit="return confirm('{% trans "Do you want to un-archive this candidate from this skill zone" %}')" method='post'
class="w-50">
{% csrf_token %}
<button type='submit' class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100"
><ion-icon name="archive"></ion-icon></button>
</form>
<img src="https://ui-avatars.com/api/?name={{sz_candidate.candidate_id}}&background=random"
class="oh-profile__image" alt="" />
{% endif %}
<form action="{% url 'skill-zone-cand-delete' sz_candidate.id %}" onsubmit="return confirm('{% trans "Do you want to remove this candidate" %}')" method='post'
onsubmit="Are you sure want to delete this candidate?" class="w-50">
</div>
<span class="oh-profile__name oh-text--dark">{{sz_candidate}}</span>
</div>
</div>
<div class="oh-sticky-table__td ">{{sz_candidate.reason}}</div>
<div class="oh-sticky-table__td ">{{sz_candidate.added_on}}</div>
<div class="oh-sticky-table__td ">
<a
style="text-decoration: none"
class="oh-btn oh-btn--light"
href="/media/{{sz_candidate.candidate_id.resume}}"
target="_blank"
title="{% trans 'Resume' %}"
rel="noopener noreferrer"
style="flex: 1 0 auto; width:20px;height: 40.68px; padding: 0;"
><ion-icon name="document-outline"></ion-icon
></a>
</div>
<div class="oh-sticky-table__td " onclick="event.stopPropagation()">
<div class="oh-btn-group">
<button
class="oh-btn oh-btn--light-bkg w-50"
title="{% trans 'Edit' %}"
data-toggle="oh-modal-toggle"
data-target="#editCandModal"
hx-get="{% url 'skill-zone-cand-edit' sz_candidate.id %}"
hx-target="#editCandTarget">
<ion-icon name="create-outline"></ion-icon>
</button>
{% comment %} {% if sz_candidate.is_active %}
<form action="{% url 'skill-zone-cand-archive' sz_candidate.id %}" title="Archive" onsubmit="return confirm('{% trans "Do you want to archive this candidate from this skill zone" %}')" method='post'
class="w-50">
{% csrf_token %}
<button type='submit' class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100"
title="Remove"><ion-icon name="trash-outline"></ion-icon></button>
><ion-icon name="archive"></ion-icon></button>
</form>
</div>
{% else %}
<form action="{% url 'skill-zone-cand-archive' sz_candidate.id %}" title="Un Archive" onsubmit="return confirm('{% trans "Do you want to un-archive this candidate from this skill zone" %}')" method='post'
class="w-50">
{% csrf_token %}
<button type='submit' class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100"
><ion-icon name="archive"></ion-icon></button>
</form>
{% endif %} {% endcomment %}
<form action="{% url 'skill-zone-cand-delete' sz_candidate.id %}" onsubmit="return confirm('{% trans "Do you want to remove this candidate" %}')" method='post'
onsubmit="Are you sure want to delete this candidate?" class="w-50">
{% csrf_token %}
<button type='submit' class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100"
title="Remove"><ion-icon name="trash-outline"></ion-icon></button>
</form>
</div>
</div>
</div>
{% endif %}
</div>
{% endfor %}
</div>
</div>
<!-- end of sticky table -->
<!-- start of pagination -->
{% comment %} <div class="oh-pagination">
<span class="oh-pagination__page">
{% trans "Page" %} {{ my_leave_allocation_requests.number }} {% trans "of" %} {{ my_leave_allocation_requests.paginator.num_pages }}.
</span>
<nav class="oh-pagination__nav">
<div class="oh-pagination__input-container me-3">
<span class="oh-pagination__label me-1">{% trans "Page" %}</span>
<input type="number" name="m_page" class="oh-pagination__input" value="{{my_leave_allocation_requests.number}}"
hx-get="{% url 'leave-allocation-request-filter' %}?{{pd}}" hx-target="#skill_zone_container" min="1" />
<span class="oh-pagination__label">{% trans "of" %} {{my_leave_allocation_requests.paginator.num_pages}}</span>
</div>
<ul class="oh-pagination__items">
{% if my_leave_allocation_requests.has_previous %}
<li class="oh-pagination__item oh-pagination__item--wide">
<a hx-target='#skill_zone_container' hx-get="{% url 'leave-allocation-request-filter' %}?{{pd}}&m_page=1"
class="oh-pagination__link">{% trans "First" %}</a>
</li>
<li class="oh-pagination__item oh-pagination__item--wide">
<a hx-target='#skill_zone_container'
hx-get="{% url 'leave-allocation-request-filter' %}?{{pd}}&m_page={{ my_leave_allocation_requests.previous_page_number }}"
class="oh-pagination__link">{% trans "Previous" %}</a>
</li>
{% endif %}
{% if my_leave_allocation_requests.has_next %}
<li class="oh-pagination__item oh-pagination__item--wide">
<a hx-target='#skill_zone_container'
hx-get="{% url 'leave-allocation-request-filter' %}?{{pd}}&m_page={{ my_leave_allocation_requests.next_page_number }}"
class="oh-pagination__link">{% trans "Next" %}</a>
</li>
<li class="oh-pagination__item oh-pagination__item--wide">
<a hx-target='#skill_zone_container'
hx-get="{% url 'leave-allocation-request-filter' %}?{{pd}}&m_page={{ my_leave_allocation_requests.paginator.num_pages }}"
class="oh-pagination__link">{% trans "Last" %}</a>
</li>
{% endif %}
</ul>
</nav>
</div> {% endcomment %}
<!-- end of pagination -->
</div>
{% if skill_zone.list %}
<div class="oh-pagination">

View File

@@ -53,7 +53,7 @@
<div class="oh-accordion-header">{% trans "Candidate Filter" %}</div>
<div class="oh-accordion-body">
<div class="row">
<div class="col-sm-12 col-md-12 col-lg-12">
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Recruitment" %}</label>
{{f.form.candidate_id__recruitment_id}}
@@ -119,6 +119,12 @@
{{f.form.candidate_id__offer_letter_status}}
</div>
</div>
<div class="col-sm-12 col-md-12 col-lg-6">
<div class="oh-input-group">
<label class="oh-label">{% trans "Is Active" %}</label>
{{f.form.is_active}}
</div>
</div>
</div>
</div>
</div>

View File

@@ -97,8 +97,9 @@ def dashboard(request):
for rec in recruitment_obj:
data = [stage_type_candidate_count(rec, type[0]) for type in Stage.stage_types]
for i in data:
i += stage_chart_count
if i > 1:
stage_chart_count += i
if stage_chart_count >= 1:
stage_chart_count = 1
onboarding_count = Candidate.objects.filter(start_onboard=True)
@@ -138,7 +139,7 @@ def dashboard(request):
if total_hired_candidates != 0:
acceptance_ratio = f"{((onboarding_count / total_hired_candidates) * 100):.1f}"
skill_zone = SkillZone.objects.all()
skill_zone = SkillZone.objects.filter(is_active = True)
return render(
request,
"dashboard/dashboard.html",

View File

@@ -1287,7 +1287,7 @@ def send_acknowledgement(request):
(file.name, file.read(), file.content_type) for file in other_attachments
]
email_backend = ConfiguredEmailBackend()
host = email_backend.dynamic_username_with_display_name
host = email_backend.dynamic_username
candidate_obj = Candidate.objects.get(id=candidate_id)
template_attachment_ids = request.POST.getlist("template_attachments")
bodys = list(
@@ -1446,7 +1446,7 @@ def skill_zone_view(request):
for zone in skill_groups:
all_zones.append(zone["grouper"])
skill_zone_filtered = SkillZoneFilter(request.GET).qs
skill_zone_filtered = SkillZoneFilter(request.GET).qs.filter(is_active=True)
all_zone_objects = list(skill_zone_filtered)
unused_skill_zones = list(set(all_zone_objects) - set(all_zones))
@@ -1561,10 +1561,18 @@ def skill_zone_archive(request, sz_id):
is_active = skill_zone.is_active
if is_active == True:
skill_zone.is_active = False
skill_zone_candidates = SkillZoneCandidate.objects.filter(skill_zone_id = sz_id)
for i in skill_zone_candidates:
i.is_active = False
i.save()
messages.success(request, _("Skill zone archived successfully.."))
else:
skill_zone.is_active = True
skill_zone_candidates = SkillZoneCandidate.objects.filter(skill_zone_id = sz_id)
for i in skill_zone_candidates:
i.is_active = True
i.save()
messages.success(request, _("Skill zone unarchived successfully.."))
skill_zone.save()
@@ -1588,10 +1596,14 @@ def skill_zone_filter(request):
skill_zone_filtered = SkillZoneFilter(request.GET).qs
if request.GET.get("is_active") == "false":
skill_zone_filtered = SkillZoneFilter(request.GET).qs.filter(is_active=False)
candidates = candidates.filter(is_active=False)
candidates = SkillZoneCandFilter(request.GET).qs.filter(is_active=False)
print('============================')
print(candidates)
print('============================')
else:
skill_zone_filtered = SkillZoneFilter(request.GET).qs.filter(is_active=True)
candidates = candidates.filter(is_active=True)
candidates = SkillZoneCandFilter(request.GET).qs.filter(is_active=True)
skill_groups = group_by_queryset(
candidates,
"skill_zone_id",