1016 lines
33 KiB
Python
1016 lines
33 KiB
Python
"""
|
|
This module used for recruitment candidates
|
|
"""
|
|
|
|
import ast
|
|
import io
|
|
import json
|
|
import re
|
|
from typing import Any
|
|
|
|
from bs4 import BeautifulSoup
|
|
from django import forms
|
|
from django.contrib import messages
|
|
from django.http import HttpResponse
|
|
from django.shortcuts import render
|
|
from django.template.loader import render_to_string
|
|
from django.urls import reverse, reverse_lazy
|
|
from django.utils.decorators import method_decorator
|
|
from django.utils.translation import gettext_lazy as _
|
|
from django.views import View
|
|
from import_export import fields, resources
|
|
from openpyxl import Workbook
|
|
from openpyxl.styles import Alignment, Border, Font, PatternFill, Side
|
|
from openpyxl.utils import get_column_letter
|
|
from xhtml2pdf import pisa
|
|
|
|
from employee.forms import BulkUpdateFieldForm
|
|
from horilla.horilla_middlewares import _thread_locals
|
|
from horilla_views.cbv_methods import export_xlsx, login_required, permission_required
|
|
from horilla_views.forms import DynamicBulkUpdateForm
|
|
from horilla_views.generic.cbv.views import (
|
|
HorillaCardView,
|
|
HorillaDetailedView,
|
|
HorillaFormView,
|
|
HorillaListView,
|
|
HorillaNavView,
|
|
TemplateView,
|
|
)
|
|
from horilla_views.templatetags.generic_template_filters import getattribute
|
|
from recruitment.cbv.candidate_reject_reason import DynamicRejectReasonFormView
|
|
from recruitment.cbv_decorators import all_manager_can_enter, manager_can_enter
|
|
from recruitment.filters import CandidateFilter
|
|
from recruitment.forms import (
|
|
CandidateExportForm,
|
|
RejectedCandidateForm,
|
|
ToSkillZoneForm,
|
|
)
|
|
from recruitment.models import (
|
|
Candidate,
|
|
RecruitmentSurvey,
|
|
RecruitmentSurveyAnswer,
|
|
RejectedCandidate,
|
|
SkillZoneCandidate,
|
|
)
|
|
|
|
_getattribute = getattribute
|
|
|
|
|
|
def clean_column_name(question):
|
|
"""
|
|
Convert the question text into a safe attribute name by:
|
|
- Replacing spaces with underscores
|
|
- Removing special characters except underscores
|
|
"""
|
|
return re.sub(r"[^\w\s]", "", question).replace(" ", "_")
|
|
|
|
|
|
@method_decorator(
|
|
permission_required(perm="recruitment.view_candidate"), name="dispatch"
|
|
)
|
|
@method_decorator(login_required, name="dispatch")
|
|
class CandidatesView(TemplateView):
|
|
"""
|
|
For page view
|
|
|
|
"""
|
|
|
|
template_name = "cbv/candidates/candidates.html"
|
|
|
|
def get_context_data(self, **kwargs: Any) -> dict:
|
|
context = super().get_context_data(**kwargs)
|
|
update_fields = BulkUpdateFieldForm()
|
|
context["update_fields_form"] = update_fields
|
|
return context
|
|
|
|
|
|
@method_decorator(login_required, name="dispatch")
|
|
@method_decorator(manager_can_enter(perm="recruitment.view_candidate"), name="dispatch")
|
|
class ListCandidates(HorillaListView):
|
|
"""
|
|
List view of candidates
|
|
"""
|
|
|
|
model = Candidate
|
|
filter_class = CandidateFilter
|
|
bulk_template = "cbv/employees_view/bulk_update_page.html"
|
|
bulk_update_fields = [
|
|
"gender",
|
|
"job_position_id",
|
|
"hired_date",
|
|
"referral",
|
|
"country",
|
|
"state",
|
|
"city",
|
|
"zip",
|
|
"joining_date",
|
|
"probation_end",
|
|
]
|
|
|
|
def get_bulk_form(self):
|
|
"""
|
|
Bulk from generating method
|
|
"""
|
|
|
|
form = DynamicBulkUpdateForm(
|
|
root_model=Candidate, bulk_update_fields=self.bulk_update_fields
|
|
)
|
|
|
|
form.fields["country"] = forms.ChoiceField(
|
|
required=False,
|
|
widget=forms.Select(
|
|
attrs={
|
|
"class": "oh-select oh-select-2",
|
|
"required": False,
|
|
"style": "width: 100%; height:45px;",
|
|
}
|
|
),
|
|
)
|
|
|
|
form.fields["state"] = forms.ChoiceField(
|
|
required=False,
|
|
widget=forms.Select(
|
|
attrs={
|
|
"class": "oh-select oh-select-2",
|
|
"required": False,
|
|
"style": "width: 100%; height:45px;",
|
|
},
|
|
),
|
|
)
|
|
|
|
return form
|
|
|
|
def __init__(self, **kwargs: Any) -> None:
|
|
super().__init__(**kwargs)
|
|
self.export_fields = []
|
|
self.search_url = reverse("list-candidate")
|
|
if self.request.user.has_perm("recruitment.change_candidate"):
|
|
self.option_method = "options"
|
|
else:
|
|
self.option_method = None
|
|
|
|
unique_questions = RecruitmentSurvey.objects.values_list(
|
|
"question", flat=True
|
|
).distinct()
|
|
self.survey_question_mapping = {}
|
|
for question in unique_questions:
|
|
survey_question = (question, f"question_{clean_column_name(question)}")
|
|
self.survey_question_mapping[f"question_{clean_column_name(question)}"] = (
|
|
question
|
|
)
|
|
if not survey_question in self.export_fields:
|
|
self.export_fields.append(survey_question)
|
|
|
|
columns = [
|
|
("Candidates", "name", "get_avatar"),
|
|
("Email", "email"),
|
|
("Phone", "mobile"),
|
|
("Rating", "rating"),
|
|
("Recruitment", "recruitment_id"),
|
|
("Job Position", "job_position_id"),
|
|
("Hired Date", "hired_date"),
|
|
("Resume", "resume_pdf"),
|
|
]
|
|
default_columns = columns
|
|
|
|
header_attrs = {
|
|
"option": """
|
|
style ="width : 230px !important;"
|
|
""",
|
|
"action": """
|
|
style ="width : 200px !important;"
|
|
""",
|
|
"email": """
|
|
style ="width : 200px !important;"
|
|
""",
|
|
"rating": """
|
|
style ="width : 170px !important;"
|
|
""",
|
|
}
|
|
|
|
actions = [
|
|
{
|
|
"action": "Edit",
|
|
"icon": "create-outline",
|
|
"attrs": """class="oh-btn oh-btn--light-bkg w-100"
|
|
onclick="event.stopPropagation()
|
|
window.location.href='{get_update_url}' "
|
|
""",
|
|
},
|
|
{
|
|
"action": "Archive",
|
|
"accessibility": "recruitment.cbv.accessibility.archive_status",
|
|
"icon": "archive",
|
|
"attrs": """
|
|
class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100"
|
|
onclick="event.stopPropagation()
|
|
archiveCandidate({get_archive_url}); "
|
|
""",
|
|
},
|
|
{
|
|
"action": "Un-archive",
|
|
"accessibility": "recruitment.cbv.accessibility.unarchive_status",
|
|
"icon": "archive",
|
|
"attrs": """
|
|
class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100"
|
|
onclick="event.stopPropagation()
|
|
archiveCandidate({get_archive_url}); "
|
|
""",
|
|
},
|
|
{
|
|
"action": _("Delete"),
|
|
"icon": "trash-outline",
|
|
"attrs": """
|
|
class="oh-btn oh-btn--danger-outline oh-btn--light-bkg w-100"
|
|
hx-get="{get_delete_url}?model=recruitment.candidate&pk={pk}"
|
|
data-toggle="oh-modal-toggle"
|
|
data-target="#deleteConfirmation"
|
|
hx-target="#deleteConfirmationBody"
|
|
""",
|
|
},
|
|
]
|
|
|
|
sortby_mapping = [
|
|
("Candidates", "name", "get_avatar"),
|
|
]
|
|
row_status_indications = [
|
|
(
|
|
"canceled--dot",
|
|
"Canceled",
|
|
"""
|
|
onclick="
|
|
$('#applyFilter').closest('form').find('[name=canceled]').val('true');
|
|
$('#applyFilter').click();
|
|
"
|
|
""",
|
|
),
|
|
(
|
|
"nothired--dot",
|
|
"Not Hired",
|
|
"""
|
|
onclick="
|
|
$('#applyFilter').closest('form').find('[name=hired]').val('false');
|
|
$('#applyFilter').click();
|
|
"
|
|
""",
|
|
),
|
|
(
|
|
"hired--dot",
|
|
"Hired",
|
|
"""
|
|
onclick="$('#applyFilter').closest('form').find('[name=hired]').val('true');
|
|
$('#applyFilter').click();
|
|
"
|
|
""",
|
|
),
|
|
]
|
|
|
|
records_per_page = 10
|
|
|
|
row_status_class = "hired-{hired} canceled-{canceled}"
|
|
|
|
# row_attrs = """
|
|
# {is_employee_converted}
|
|
# hx-get='{get_details_candidate}'
|
|
# data-toggle="oh-modal-toggle"
|
|
# data-target="#genericModal"
|
|
# hx-target="#genericModalBody"
|
|
# """
|
|
row_attrs = """
|
|
{is_employee_converted}
|
|
onclick="window.location.href='{get_individual_url}?instance_ids={ordered_ids}'"
|
|
"""
|
|
|
|
def export_data(self, *args, **kwargs):
|
|
"""
|
|
Export with survey answer and question
|
|
"""
|
|
|
|
request = getattr(_thread_locals, "request", None)
|
|
ids = ast.literal_eval(request.POST["ids"])
|
|
_columns = ast.literal_eval(request.POST["columns"])
|
|
queryset = self.model.objects.filter(id__in=ids)
|
|
question_mapping = self.survey_question_mapping
|
|
export_format = request.POST.get("format", "xlsx")
|
|
|
|
_model = self.model
|
|
|
|
class HorillaListViewResorce(resources.ModelResource):
|
|
"""
|
|
Instant Resource class
|
|
"""
|
|
|
|
id = fields.Field(column_name="ID")
|
|
question = {}
|
|
|
|
class Meta:
|
|
"""
|
|
Meta class for additional option
|
|
"""
|
|
|
|
model = _model
|
|
fields = [field[1] for field in _columns] # 773
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
|
|
for field_tuple in _columns:
|
|
if field_tuple[1].startswith("question_"):
|
|
safe_field_name = field_tuple[1]
|
|
self.fields[safe_field_name] = fields.Field(
|
|
column_name=question_mapping[safe_field_name],
|
|
attribute=safe_field_name,
|
|
readonly=True,
|
|
)
|
|
|
|
def export_field(self, field, obj):
|
|
"""
|
|
Override this method to fetch the candidate's answers dynamically.
|
|
"""
|
|
|
|
if field.attribute:
|
|
# Get the stored JSON field containing answers
|
|
survey_answers = RecruitmentSurveyAnswer.objects.filter(
|
|
candidate_id=obj
|
|
).first()
|
|
if survey_answers and field.attribute.startswith("question_"):
|
|
survey_answers = survey_answers.answer_json
|
|
if isinstance(survey_answers, str):
|
|
try:
|
|
survey_answers = ast.literal_eval(
|
|
survey_answers
|
|
) # Convert string to dict
|
|
except Exception:
|
|
survey_answers = {}
|
|
|
|
# Extract the actual question text
|
|
|
|
original_question = question_mapping[field.attribute]
|
|
# Retrieve answer from JSON if available
|
|
answer = survey_answers.get(original_question, "")
|
|
if not answer:
|
|
answer = survey_answers.get(
|
|
"rating_" + original_question, ""
|
|
)
|
|
if not answer:
|
|
answer = survey_answers.get(
|
|
"percentage_" + original_question, ""
|
|
)
|
|
if not answer:
|
|
answer = survey_answers.get("file_" + original_question, "")
|
|
if not answer:
|
|
answer = survey_answers.get("date_" + original_question, "")
|
|
if not answer:
|
|
answer = survey_answers.get(
|
|
"multiple_choices_" + original_question, ""
|
|
)
|
|
return answer
|
|
|
|
return super().export_field(field, obj)
|
|
|
|
def dehydrate_id(self, instance):
|
|
"""
|
|
Dehydrate method for id field
|
|
"""
|
|
return instance.pk
|
|
|
|
for field_tuple in _columns:
|
|
if not field_tuple[1].startswith("question_"):
|
|
dynamic_fn_str = f"def dehydrate_{field_tuple[1]}(self, instance):return self.remove_extra_spaces(getattribute(instance, '{field_tuple[1]}'))"
|
|
exec(dynamic_fn_str)
|
|
dynamic_fn = locals()[f"dehydrate_{field_tuple[1]}"]
|
|
locals()[field_tuple[1]] = fields.Field(column_name=field_tuple[0])
|
|
|
|
def remove_extra_spaces(self, text):
|
|
"""
|
|
Remove blank space but keep line breaks and add new lines for <li> tags.
|
|
"""
|
|
soup = BeautifulSoup(str(text), "html.parser")
|
|
for li in soup.find_all("li"):
|
|
li.insert_before("\n")
|
|
li.unwrap()
|
|
text = soup.get_text()
|
|
lines = text.splitlines()
|
|
non_blank_lines = [line.strip() for line in lines if line.strip()]
|
|
cleaned_text = "\n".join(non_blank_lines)
|
|
return cleaned_text
|
|
|
|
book_resource = HorillaListViewResorce()
|
|
|
|
# Export the data using the resource
|
|
dataset = book_resource.export(queryset)
|
|
|
|
# Set the response headers
|
|
# file_name = self.export_file_name
|
|
if export_format == "json":
|
|
json_data = json.loads(dataset.export("json"))
|
|
response = HttpResponse(
|
|
json.dumps(json_data, indent=4), content_type="application/json"
|
|
)
|
|
response["Content-Disposition"] = (
|
|
f'attachment; filename="{self.export_file_name}.json"'
|
|
)
|
|
return response
|
|
|
|
# CSV
|
|
elif export_format == "csv":
|
|
csv_data = dataset.export("csv")
|
|
response = HttpResponse(csv_data, content_type="text/csv")
|
|
response["Content-Disposition"] = (
|
|
f'attachment; filename="{self.export_file_name}.csv"'
|
|
)
|
|
return response
|
|
elif export_format == "pdf":
|
|
|
|
headers = dataset.headers
|
|
rows = dataset.dict
|
|
|
|
# Render to HTML using a template
|
|
html_string = render_to_string(
|
|
"generic/export_pdf.html",
|
|
{
|
|
"headers": headers,
|
|
"rows": rows,
|
|
},
|
|
)
|
|
|
|
# Convert HTML to PDF using xhtml2pdf
|
|
result = io.BytesIO()
|
|
pisa_status = pisa.CreatePDF(html_string, dest=result)
|
|
|
|
if pisa_status.err:
|
|
return HttpResponse("PDF generation failed", status=500)
|
|
|
|
# Return response
|
|
response = HttpResponse(result.getvalue(), content_type="application/pdf")
|
|
response["Content-Disposition"] = (
|
|
f'attachment; filename="{self.export_file_name}.pdf"'
|
|
)
|
|
return response
|
|
|
|
# response = HttpResponse(
|
|
# dataset.export("xlsx"), content_type="application/vnd.ms-excel"
|
|
# )
|
|
# response["Content-Disposition"] = (
|
|
# f'attachment; filename="{self.export_file_name}.xls"'
|
|
# )
|
|
json_data = json.loads(dataset.export("json"))
|
|
headers = list(json_data[0].keys()) if json_data else []
|
|
|
|
wb = Workbook()
|
|
ws = wb.active
|
|
ws.title = "Exported Data"
|
|
|
|
# Styling
|
|
header_fill = PatternFill(
|
|
start_color="FFD700", end_color="FFD700", fill_type="solid"
|
|
)
|
|
bold_font = Font(bold=True)
|
|
thin_border = Border(
|
|
left=Side(style="thin"),
|
|
right=Side(style="thin"),
|
|
top=Side(style="thin"),
|
|
bottom=Side(style="thin"),
|
|
)
|
|
wrap_alignment = Alignment(vertical="top", wrap_text=True)
|
|
|
|
# Write headers
|
|
for col_idx, header in enumerate(headers, start=1):
|
|
cell = ws.cell(row=1, column=col_idx, value=header)
|
|
cell.fill = header_fill
|
|
cell.font = bold_font
|
|
cell.border = thin_border
|
|
cell.alignment = Alignment(
|
|
horizontal="center", vertical="center", wrap_text=True
|
|
)
|
|
|
|
# Write data rows
|
|
for row_idx, item in enumerate(json_data, start=2):
|
|
for col_idx, key in enumerate(headers, start=1):
|
|
value = item.get(key, "")
|
|
# Convert lists to newline-separated string
|
|
if isinstance(value, list):
|
|
value = "\n".join(str(v) for v in value)
|
|
elif isinstance(value, dict):
|
|
value = json.dumps(
|
|
value, ensure_ascii=False
|
|
) # or format it as needed
|
|
cell = ws.cell(row=row_idx, column=col_idx, value=value)
|
|
cell.border = thin_border
|
|
cell.alignment = wrap_alignment
|
|
|
|
# Auto-fit column widths
|
|
for col_cells in ws.columns:
|
|
max_len = max(len(str(cell.value or "")) for cell in col_cells)
|
|
col_letter = get_column_letter(col_cells[0].column)
|
|
ws.column_dimensions[col_letter].width = min(max_len + 5, 50)
|
|
|
|
# Output to Excel
|
|
output = io.BytesIO()
|
|
wb.save(output)
|
|
output.seek(0)
|
|
|
|
response = HttpResponse(
|
|
output.read(),
|
|
content_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
|
)
|
|
response["Content-Disposition"] = 'attachment; filename="exported_data.xlsx"'
|
|
return response
|
|
|
|
|
|
@method_decorator(login_required, name="dispatch")
|
|
@method_decorator(manager_can_enter(perm="recruitment.view_candidate"), name="dispatch")
|
|
class CardCandidates(HorillaCardView):
|
|
"""
|
|
For card view
|
|
"""
|
|
|
|
model = Candidate
|
|
filter_class = CandidateFilter
|
|
|
|
def __init__(self, **kwargs: Any) -> None:
|
|
super().__init__(**kwargs)
|
|
self.search_url = reverse("card-candidate")
|
|
|
|
details = {
|
|
"image_src": "get_avatar",
|
|
"title": "{get_full_name}",
|
|
"subtitle": "{email} <br> {get_job_position}",
|
|
}
|
|
|
|
actions = [
|
|
{
|
|
"action": "Convert to Employee",
|
|
"accessibility": "recruitment.cbv.accessibility.convert_emp",
|
|
"attrs": """
|
|
onclick="event.stopPropagation()
|
|
return confirm('Are you sure you want to convert this candidate into an employee?')"
|
|
href='{get_convert_to_emp}'
|
|
class="oh-dropdown__link"
|
|
|
|
""",
|
|
},
|
|
{
|
|
"action": "Add to Skill Zone",
|
|
"accessibility": "recruitment.cbv.accessibility.add_skill_zone",
|
|
"attrs": """
|
|
data-toggle="oh-modal-toggle"
|
|
data-target="#genericModal"
|
|
hx-get="{get_add_to_skill}"
|
|
hx-target="#genericModalBody"
|
|
class="oh-dropdown__link"
|
|
|
|
""",
|
|
},
|
|
{
|
|
"action": "View candidate self tracking",
|
|
"accessibility": "recruitment.cbv.accessibility.check_candidate_self_tracking",
|
|
"attrs": """
|
|
href="{get_self_tracking_url}"
|
|
class="oh-dropdown__link"
|
|
""",
|
|
},
|
|
{
|
|
"action": "Request Document",
|
|
"accessibility": "recruitment.cbv.accessibility.request_document",
|
|
"attrs": """
|
|
data-toggle="oh-modal-toggle"
|
|
data-target="#genericModal"
|
|
hx-get="{get_document_request_doc}"
|
|
hx-target="#genericModalBody"
|
|
class="oh-dropdown__link"
|
|
""",
|
|
},
|
|
{
|
|
"action": "Add to Rejected",
|
|
"accessibility": "recruitment.cbv.accessibility.add_reject",
|
|
"attrs": """
|
|
hx-target="#genericModalBody"
|
|
hx-swap="innerHTML"
|
|
data-toggle="oh-modal-toggle"
|
|
data-target="#genericModal"
|
|
hx-get="{get_add_to_reject}"
|
|
class="oh-dropdown__link"
|
|
|
|
""",
|
|
},
|
|
{
|
|
"action": "Edit Rejected Candidate",
|
|
"accessibility": "recruitment.cbv.accessibility.edit_reject",
|
|
"attrs": """
|
|
hx-target="#genericModalBody"
|
|
hx-swap="innerHTML"
|
|
data-toggle="oh-modal-toggle"
|
|
data-target="#genericModal"
|
|
hx-get="{get_add_to_reject}"
|
|
class="oh-dropdown__link"
|
|
|
|
""",
|
|
},
|
|
{
|
|
"action": "Edit Profile",
|
|
"attrs": """
|
|
onclick="event.stopPropagation()
|
|
window.location.href='{get_update_url}' "
|
|
class="oh-dropdown__link"
|
|
|
|
""",
|
|
},
|
|
{
|
|
"action": "archive_status",
|
|
"attrs": """
|
|
class="oh-dropdown__link"
|
|
onclick="archiveCandidate({get_archive_url});"
|
|
|
|
|
|
""",
|
|
},
|
|
{
|
|
"action": "Delete",
|
|
"attrs": """
|
|
class="oh-dropdown__link oh-dropdown__link--danger"
|
|
onclick="event.stopPropagation();
|
|
deleteCandidate('{get_delete_url}'); "
|
|
|
|
""",
|
|
},
|
|
]
|
|
card_status_indications = [
|
|
(
|
|
"canceled--dot",
|
|
"Canceled",
|
|
"""
|
|
onclick="
|
|
$('#applyFilter').closest('form').find('[name=canceled]').val('true');
|
|
$('#applyFilter').click();
|
|
"
|
|
""",
|
|
),
|
|
(
|
|
"nothired--dot",
|
|
"Not Hired",
|
|
"""
|
|
onclick="
|
|
$('#applyFilter').closest('form').find('[name=hired]').val('false');
|
|
$('#applyFilter').click();
|
|
"
|
|
""",
|
|
),
|
|
(
|
|
"hired--dot",
|
|
"Hired",
|
|
"""
|
|
onclick="$('#applyFilter').closest('form').find('[name=hired]').val('true');
|
|
$('#applyFilter').click();
|
|
"
|
|
""",
|
|
),
|
|
]
|
|
card_status_class = "hired-{hired} canceled-{canceled}"
|
|
card_attrs = """
|
|
onclick="window.location.href='{get_individual_url}?instance_ids={ordered_ids}'"
|
|
"""
|
|
|
|
records_per_page = 30
|
|
|
|
|
|
@method_decorator(login_required, name="dispatch")
|
|
@method_decorator(manager_can_enter(perm="recruitment.view_candidate"), name="dispatch")
|
|
class CandidateNav(HorillaNavView):
|
|
"""
|
|
For nav bar
|
|
"""
|
|
|
|
def __init__(self, **kwargs: Any) -> None:
|
|
super().__init__(**kwargs)
|
|
self.search_url = reverse("list-candidate")
|
|
self.create_attrs = f"""
|
|
href='{reverse_lazy('candidate-create')}'"
|
|
"""
|
|
self.actions = [
|
|
{
|
|
"action": "Export",
|
|
"attrs": f"""
|
|
data-toggle="oh-modal-toggle"
|
|
data-target="#genericModal"
|
|
hx-get="{reverse('export')}"
|
|
hx-target="#genericModalBody"
|
|
""",
|
|
},
|
|
{
|
|
"action": "Bulk mail",
|
|
"attrs": f"""
|
|
data-toggle="oh-modal-toggle"
|
|
data-target="#genericModal"
|
|
hx-get="{reverse('send-mail')}"
|
|
hx-target="#genericModalBody"
|
|
""",
|
|
},
|
|
{
|
|
"action": "Create document request",
|
|
"attrs": f"""
|
|
data-toggle="oh-modal-toggle"
|
|
data-target="#objectCreateModal"
|
|
hx-get="{reverse('candidate-document-request')}"
|
|
hx-target="#objectCreateModalTarget"
|
|
""",
|
|
},
|
|
{
|
|
"action": "Archive",
|
|
"attrs": """
|
|
id="archiveCandidates"
|
|
|
|
""",
|
|
},
|
|
{
|
|
"action": "Un archive",
|
|
"attrs": """
|
|
id="unArchiveCandidates"
|
|
|
|
""",
|
|
},
|
|
{
|
|
"action": "Delete",
|
|
"attrs": """
|
|
data-action = "delete"
|
|
id="deleteCandidates"
|
|
new_init
|
|
""",
|
|
},
|
|
]
|
|
|
|
self.view_types = [
|
|
{
|
|
"type": "list",
|
|
"icon": "list-outline",
|
|
"url": reverse("list-candidate"),
|
|
"attrs": """
|
|
title='List'
|
|
""",
|
|
},
|
|
{
|
|
"type": "card",
|
|
"icon": "grid-outline",
|
|
"url": reverse("card-candidate"),
|
|
"attrs": """
|
|
title='Card'
|
|
""",
|
|
},
|
|
]
|
|
self.filter_instance = CandidateFilter()
|
|
|
|
nav_title = "Candidates"
|
|
filter_body_template = "cbv/candidates/filter.html"
|
|
filter_form_context_name = "form"
|
|
search_swap_target = "#listContainer"
|
|
group_by_fields = [
|
|
("recruitment_id", "Recruitment"),
|
|
("job_position_id", "Job Position"),
|
|
("hired", "Hired"),
|
|
("country", "Country"),
|
|
("stage_id", "Stage"),
|
|
("joining_date", "Date joining"),
|
|
("probation_end", "Probation End"),
|
|
("offer_letter_status", "offer Letter Status"),
|
|
("rejected_candidate__reject_reason_id", "Reject reason"),
|
|
("skillzonecandidate_set", "Skill zone"),
|
|
]
|
|
|
|
|
|
@method_decorator(login_required, name="dispatch")
|
|
@method_decorator(manager_can_enter(perm="recruitment.view_candidate"), name="dispatch")
|
|
class ExportView(TemplateView):
|
|
"""
|
|
For candidate export
|
|
"""
|
|
|
|
template_name = "cbv/candidates/export.html"
|
|
|
|
def get_context_data(self, **kwargs: Any):
|
|
"""
|
|
Adds export fields and filter object to the context.
|
|
"""
|
|
context = super().get_context_data(**kwargs)
|
|
candidates = Candidate.objects.filter(is_active=True)
|
|
export_column = CandidateExportForm()
|
|
export_filter = CandidateFilter(queryset=candidates)
|
|
context["export_column"] = export_column
|
|
context["export_filter"] = export_filter
|
|
return context
|
|
|
|
|
|
@method_decorator(login_required, name="dispatch")
|
|
@method_decorator(manager_can_enter(perm="recruitment.view_candidate"), name="dispatch")
|
|
class AddToRejectedCandidatesView(View):
|
|
"""
|
|
Class for Add to reject candidate
|
|
"""
|
|
|
|
template_name = "onboarding/rejection/form.html"
|
|
|
|
def get(self, request):
|
|
"""
|
|
get method
|
|
"""
|
|
candidate_id = request.GET.get("candidate_id")
|
|
instance = None
|
|
if candidate_id:
|
|
instance = RejectedCandidate.objects.filter(
|
|
candidate_id=candidate_id
|
|
).first()
|
|
form = RejectedCandidateForm(
|
|
initial={"candidate_id": candidate_id}, instance=instance
|
|
)
|
|
return render(request, self.template_name, {"form": form})
|
|
|
|
def post(self, request):
|
|
"""
|
|
post method
|
|
"""
|
|
candidate_id = request.GET.get("candidate_id")
|
|
instance = None
|
|
if candidate_id:
|
|
instance = RejectedCandidate.objects.filter(
|
|
candidate_id=candidate_id
|
|
).first()
|
|
form = RejectedCandidateForm(request.POST, instance=instance)
|
|
if form.is_valid():
|
|
form.save()
|
|
messages.success(request, "Candidate reject reason saved")
|
|
return HttpResponse("<script>window.location.reload()</script>")
|
|
return render(request, self.template_name, {"form": form})
|
|
|
|
|
|
@method_decorator(login_required, name="dispatch")
|
|
@method_decorator(
|
|
all_manager_can_enter(perm="recruitment.view_candidate"), name="dispatch"
|
|
)
|
|
class CandidateDetail(HorillaDetailedView):
|
|
"""
|
|
Candidate detail
|
|
"""
|
|
|
|
title = "Candidate Details"
|
|
|
|
model = Candidate
|
|
|
|
header = {"title": "get_full_name", "subtitle": "get_email", "avatar": "get_avatar"}
|
|
|
|
body = [
|
|
("Gender", "gender"),
|
|
("Phone", "mobile"),
|
|
("Stage", "stage_drop_down"),
|
|
("Rating", "rating_bar"),
|
|
("Recruitment", "recruitment_id"),
|
|
("Job Position", "job_position_id"),
|
|
("Interview Table", "candidate_interview_view", True),
|
|
]
|
|
|
|
cols = {
|
|
"candidate_interview_view": 12,
|
|
}
|
|
|
|
def __init__(self, **kwargs):
|
|
super().__init__(**kwargs)
|
|
self.actions = [
|
|
{
|
|
"action": "Edit",
|
|
"icon": "create-outline",
|
|
"attrs": """
|
|
class="oh-btn oh-btn--info w-50"
|
|
onclick="window.location.href='{get_update_url}' "
|
|
""",
|
|
},
|
|
{
|
|
"action": "View",
|
|
"icon": "eye-outline",
|
|
"attrs": """
|
|
class="oh-btn oh-btn--success w-50"
|
|
onclick="window.location.href='{get_individual_url}'"
|
|
""",
|
|
},
|
|
]
|
|
|
|
if self.request.user.has_perm("recruitment.delete_candidate"):
|
|
self.actions.append(
|
|
{
|
|
"action": "Delete",
|
|
"icon": "trash-outline",
|
|
"accessibility": "recruitment.cbv.candidates.delete_cand",
|
|
"attrs": f"""
|
|
class="oh-btn oh-btn--danger w-50"
|
|
hx-get="{reverse_lazy("generic-delete")}?model=recruitment.Candidate&pk={{pk}}"
|
|
hx-target="#deleteConfirmationBody"
|
|
data-toggle="oh-modal-toggle"
|
|
data-target="#deleteConfirmation"
|
|
onclick="event.stopPropagation()
|
|
deleteCandidate('{{get_delete_url}}'); "
|
|
""",
|
|
}
|
|
)
|
|
|
|
|
|
class ToSkillZoneFormView(HorillaFormView):
|
|
"""
|
|
Form View
|
|
"""
|
|
|
|
model = SkillZoneCandidate
|
|
form_class = ToSkillZoneForm
|
|
new_display_title = "Add To Skill Zone"
|
|
|
|
def get_context_data(self, **kwargs):
|
|
"""
|
|
Returns context with form and candidate data.
|
|
"""
|
|
context = super().get_context_data(**kwargs)
|
|
candidate_id = self.kwargs.get("cand_id")
|
|
candidate = Candidate.objects.get(id=candidate_id)
|
|
form = self.form_class(
|
|
initial={
|
|
"candidate_id": candidate,
|
|
"skill_zone_ids": SkillZoneCandidate.objects.filter(
|
|
candidate_id=candidate
|
|
).values_list("skill_zone_id", flat=True),
|
|
}
|
|
)
|
|
context["form"] = form
|
|
return context
|
|
|
|
def form_invalid(self, form: Any) -> HttpResponse:
|
|
"""
|
|
Handles and renders form errors or defers to superclass.
|
|
"""
|
|
form = self.form_class(self.request.POST)
|
|
if not form.is_valid():
|
|
errors = form.errors.as_data()
|
|
return render(
|
|
self.request, self.template_name, {"form": form, "errors": errors}
|
|
)
|
|
return super().form_invalid(form)
|
|
|
|
def form_valid(self, form: ToSkillZoneForm) -> HttpResponse:
|
|
"""
|
|
Handles valid form submission and saves rejected candidate reason.
|
|
"""
|
|
if form.is_valid():
|
|
candidate_id = self.kwargs.get("cand_id")
|
|
candidate = Candidate.objects.get(id=candidate_id)
|
|
self.form_class(
|
|
initial={
|
|
"candidate_id": candidate,
|
|
"skill_zone_ids": SkillZoneCandidate.objects.filter(
|
|
candidate_id=candidate
|
|
).values_list("skill_zone_id", flat=True),
|
|
}
|
|
)
|
|
skill_zones = self.form.cleaned_data["skill_zone_ids"]
|
|
for zone in skill_zones:
|
|
if not SkillZoneCandidate.objects.filter(
|
|
candidate_id=candidate_id, skill_zone_id=zone
|
|
).exists():
|
|
zone_candidate = SkillZoneCandidate()
|
|
zone_candidate.candidate_id = candidate
|
|
zone_candidate.skill_zone_id = zone
|
|
zone_candidate.reason = self.form.cleaned_data["reason"]
|
|
zone_candidate.save()
|
|
message = "Candidate Added to skill zone successfully"
|
|
messages.success(self.request, _(message))
|
|
return self.HttpResponse()
|
|
return super().form_valid(form)
|
|
|
|
|
|
class RejectReasonFormView(HorillaFormView):
|
|
"""
|
|
Form View
|
|
"""
|
|
|
|
model = RejectedCandidate
|
|
form_class = RejectedCandidateForm
|
|
new_display_title = "Rejected Candidate"
|
|
dynamic_create_fields = [("reject_reason_id", DynamicRejectReasonFormView)]
|
|
template_name = "candidate/candidate_rejection_form.html"
|
|
|
|
def get_initial(self) -> dict:
|
|
initial = super().get_initial()
|
|
initial["candidate_id"] = self.request.GET.get("candidate_id")
|
|
return initial
|
|
|
|
def init_form(self, *args, data={}, files={}, instance=None, **kwargs):
|
|
candidate_id = self.request.GET.get("candidate_id")
|
|
instance = RejectedCandidate.objects.filter(candidate_id=candidate_id).first()
|
|
return super().init_form(
|
|
*args, data=data, files=files, instance=instance, **kwargs
|
|
)
|
|
|
|
def form_valid(self, form: RejectedCandidateForm) -> HttpResponse:
|
|
"""
|
|
Handles valid form submission and saves rejected candidate reason.
|
|
"""
|
|
if form.is_valid():
|
|
message = _("Candidate reject reason saved")
|
|
messages.success(self.request, _(message))
|
|
form.save()
|
|
return self.HttpResponse()
|
|
return super().form_valid(form)
|