From c719c91a801aad39e0eec563c9f15cd02f4182b8 Mon Sep 17 00:00:00 2001 From: Horilla Date: Mon, 8 Jul 2024 14:55:34 +0530 Subject: [PATCH] [ADD] RECRUITMENT: Resume shortlisting to recruitment pipeline --- base/urls.py | 5 + base/views.py | 8 +- horilla/decorators.py | 34 + recruitment/sidebar.py | 5 +- .../templates/candidate/application_form.html | 81 +- .../candidate/candidate_create_form_as_p.html | 3 - .../templates/pipeline/bulk_resume.html | 67 ++ .../templates/pipeline/matching_resumes.html | 42 + recruitment/templates/pipeline/nav.html | 895 ++++++++++-------- .../templates/pipeline/pipeline_empty.html | 2 +- .../templates/pipeline/pipeline_tabs.html | 14 +- recruitment/templates/recruitment/nav.html | 13 +- .../recruitment/recruitment_component.html | 4 +- .../recruitment/recruitment_form.html | 256 +++-- .../settings/skills/skills_form.html | 27 + .../settings/skills/skills_list.html | 50 + .../settings/skills/skills_view.html | 33 + recruitment/urls.py | 35 + recruitment/views/search.py | 23 +- recruitment/views/surveys.py | 68 +- templates/settings.html | 16 +- 21 files changed, 1147 insertions(+), 534 deletions(-) create mode 100644 recruitment/templates/pipeline/bulk_resume.html create mode 100644 recruitment/templates/pipeline/matching_resumes.html create mode 100644 recruitment/templates/settings/skills/skills_form.html create mode 100644 recruitment/templates/settings/skills/skills_list.html create mode 100644 recruitment/templates/settings/skills/skills_view.html diff --git a/base/urls.py b/base/urls.py index 174ad6733..7a9ee3e9b 100644 --- a/base/urls.py +++ b/base/urls.py @@ -1054,4 +1054,9 @@ urlpatterns = [ views.edit_allowed_ips, name="edit-allowed-ip", ), + path( + "settings/skills-view/", + views.skills_view, + name="skills-view", + ), ] diff --git a/base/views.py b/base/views.py index b3d5fda0b..593f4c924 100644 --- a/base/views.py +++ b/base/views.py @@ -152,7 +152,7 @@ from payroll.forms.component_forms import PayrollSettingsForm from payroll.models.models import EncashmentGeneralSettings from payroll.models.tax_models import PayrollSettings from pms.models import KeyResult -from recruitment.models import RejectReason +from recruitment.models import RejectReason, Skill def custom404(request): @@ -6249,3 +6249,9 @@ def edit_allowed_ips(request): "attendance/ip_restriction/restrict_update_form.html", {"form": form, "id": id}, ) + + +@login_required +def skills_view(request): + skills = Skill.objects.all() + return render(request, "settings/skills/skills_view.html", {"skills": skills}) diff --git a/horilla/decorators.py b/horilla/decorators.py index 7bbc2bfb4..090377f9e 100755 --- a/horilla/decorators.py +++ b/horilla/decorators.py @@ -12,6 +12,7 @@ from base.models import BiometricAttendance, MultipleApprovalManagers from employee.models import Employee, EmployeeWorkInformation from horilla import settings from horilla.settings import BASE_DIR, TEMPLATES +from recruitment.models import Recruitment logger = logging.getLogger(__name__) @@ -171,6 +172,39 @@ def manager_can_enter(function, perm): return _function +@decorator_with_arguments +def is_recruitment_manager(function, perm): + """ + This method is used to check permission to employee for enter to the function if the employee + do not have permission also checks, has manager of any recruitment. + """ + + def _function(request, *args, **kwargs): + + user = request.user + perm = "recruitment.view_recruitmentsurvey" + employee = user.employee_get + is_manager = False + recs = Recruitment.objects.all() + for i in recs: + for manager in i.recruitment_managers.all(): + if request.user.employee_get == manager: + is_manager = True + + if user.has_perm(perm) or is_manager: + return function(request, *args, **kwargs) + else: + messages.info(request, "You dont have permission.") + previous_url = request.META.get("HTTP_REFERER", "/") + script = f'' + key = "HTTP_HX_REQUEST" + if key in request.META.keys(): + return render(request, "decorator_404.html") + return HttpResponse(script) + + return _function + + from urllib.parse import urlparse diff --git a/recruitment/sidebar.py b/recruitment/sidebar.py index 9c8c1f51d..cabc7f4db 100644 --- a/recruitment/sidebar.py +++ b/recruitment/sidebar.py @@ -87,7 +87,10 @@ def candidates_accessibility( def survey_accessibility( request, _submenu: dict = {}, user_perms: PermWrapper = [], *args, **kwargs ) -> bool: - return request.user.has_perm("recruitment.view_recruitmentsurvey") + _submenu["redirect"] = _submenu["redirect"] + "?closed=false" + return is_stagemanager(request.user) or request.user.has_perm( + "recruitment.view_recruitment" + ) def recruitment_accessibility( diff --git a/recruitment/templates/candidate/application_form.html b/recruitment/templates/candidate/application_form.html index 4c82fbb41..c4474bc07 100644 --- a/recruitment/templates/candidate/application_form.html +++ b/recruitment/templates/candidate/application_form.html @@ -55,7 +55,6 @@ type="file" id="photo" name="profile" - required class="oh-input oh-input--file oh-input--file-sm mt-4 oh-upload-input" data-target=".oh-upload-photo" /> @@ -90,9 +89,10 @@ /> - {% endcomment %}
+ {% if resume.file %} +

Currenty: {{ resume.file.name }}

+

Change:

+ + {% endif %} {{form.resume.errors}} @@ -322,6 +327,12 @@
+ {% if resume %} +
+ + +
+ {% else %}
{{recruitment.description|safe}} @@ -332,12 +343,17 @@ >
+ {% endif %} {% include "filter_tags.html" %}
- - - {% trans "Closed" %} - - - - {% trans "Open" %} - + + + {% trans "Closed" %} + + + + {% trans "Open" %} +
{% if not request.user.driverviewed_set.first or "pipeline" not in request.user.driverviewed_set.first.user_viewed %} {% endif %} - diff --git a/recruitment/templates/pipeline/pipeline_empty.html b/recruitment/templates/pipeline/pipeline_empty.html index 9849351ac..812e5a56e 100644 --- a/recruitment/templates/pipeline/pipeline_empty.html +++ b/recruitment/templates/pipeline/pipeline_empty.html @@ -42,7 +42,7 @@ x-data="{searchShow: false}" {% if perms.recruitment.add_recruitment %} - diff --git a/recruitment/templates/pipeline/pipeline_tabs.html b/recruitment/templates/pipeline/pipeline_tabs.html index 93c9947fb..83942578a 100644 --- a/recruitment/templates/pipeline/pipeline_tabs.html +++ b/recruitment/templates/pipeline/pipeline_tabs.html @@ -50,6 +50,17 @@ > {% endif %} + {% if perms.recruitment.change_recruitment or request.user|recruitment_manages:rec %} +
  • + {% trans "Resume Shortlisting" %} +
  • + {% endif %} {% elif perms.recruitment.change_recruitment or request.user|recruitment_manages:rec %}
  • - - -
  • diff --git a/recruitment/templates/recruitment/nav.html b/recruitment/templates/recruitment/nav.html index 300592ef7..7145e4e63 100644 --- a/recruitment/templates/recruitment/nav.html +++ b/recruitment/templates/recruitment/nav.html @@ -58,8 +58,8 @@ x-data="{searchShow: false}" @@ -76,6 +76,13 @@ x-data="{searchShow: false}"
    + + + +
    - + diff --git a/recruitment/templates/recruitment/recruitment_component.html b/recruitment/templates/recruitment/recruitment_component.html index e7766b6d3..306bb901d 100644 --- a/recruitment/templates/recruitment/recruitment_component.html +++ b/recruitment/templates/recruitment/recruitment_component.html @@ -174,9 +174,9 @@
    {% if perms.recruitment.change_recruitment %} - + {% endif %} - + {% if perms.recruitment.delete_recruitment %} {% if rec.is_active %} diff --git a/recruitment/templates/recruitment/recruitment_form.html b/recruitment/templates/recruitment/recruitment_form.html index dc4ea4f2f..cd46b3b48 100644 --- a/recruitment/templates/recruitment/recruitment_form.html +++ b/recruitment/templates/recruitment/recruitment_form.html @@ -1,95 +1,177 @@ {% load i18n %} {% load static %} -
    -
    - {% csrf_token %} -
    -
    - {% for error in form.non_field_errors %} -
      -
    • {{error}}
    • -
    - {% endfor %} - + function skillChange(selectElement) { + var selectedSkill = selectElement.val(); + var parentForm = selectElement.parents().closest("form"); + if (selectedSkill && selectedSkill.includes("create")) { + let dynamicskills = $("#dynamicSkills"); + var view = parentForm.serialize(); + dynamicskills.attr("hx-vals", `{"data":"${view}"}`); + dynamicskills.click(); + } + } + $(document).ready(function(){ + $("[name= 'skills']").on("change", function(){ + skillChange($(this)) + }) + }); + {% if dynamic %} + setTimeout(function () { + $('#dynamicCreateModal').removeClass('oh-modal--show'); + }, 500); + {% endif %} + + +{% if messages %} +
    + {% for message in messages %} +
    +
    + {{ message }} +
    +
    + {% endfor %} +
    +{% endif %} +
    +
    {% if duplicate %}{% trans "Duplicate Recruitment" %}{% else %}{% trans "Add Recruitment" %}{% endif %} + +
    +
    + +
    + {% csrf_token %} +
    +
    + {% for error in form.non_field_errors %} +
      +
    • {{error}}
    • +
    + {% endfor %} + + {{form.title}} {{form.title.errors}} +
    +
    + + {{form.description}} {{form.description.errors}} +
    +
    - {{form.title}} {{form.title.errors}} -
    -
    - - {{form.description}} {{form.description.errors}} -
    -
    - - {{form.open_positions}} {{form.open_positions.errors}} -
    -
    - - {{form.recruitment_managers}} - {{form.recruitment_managers.errors}} -
    -
    - - {{form.start_date}} {{form.start_date.errors}} -
    -
    - - {{form.end_date}} {{form.end_date.errors}} -
    -
    - - {{form.vacancy}} {{form.vacancy.errors}} -
    -
    - - {{form.company_id}} {{form.company_id.errors}} -
    -
    - - {{form.survey_templates}} -
    -
    - -
    -
    {{form.is_published}}
    + + {{form.open_positions}} {{form.open_positions.errors}} +
    +
    + + {{form.recruitment_managers}} + {{form.recruitment_managers.errors}} +
    +
    + + {{form.start_date}} {{form.start_date.errors}} +
    +
    + + {{form.end_date}} {{form.end_date.errors}} +
    +
    + + {{form.vacancy}} {{form.vacancy.errors}} +
    +
    + + {{form.company_id}} {{form.company_id.errors}} +
    +
    + + {{form.survey_templates}} +
    +
    + + {{form.skills}} +
    +
    + +
    +
    {{form.is_published}}
    +
    -
    -
    - -
    - - +
    + +
    + +
    + diff --git a/recruitment/templates/settings/skills/skills_form.html b/recruitment/templates/settings/skills/skills_form.html new file mode 100644 index 000000000..70b646426 --- /dev/null +++ b/recruitment/templates/settings/skills/skills_form.html @@ -0,0 +1,27 @@ +{% load i18n %} +
    +
    + {% trans "Skills" %} + +
    + +
    +
    +
    + {% csrf_token %} + {{form.as_p}} + +
    +
    +
    +
    diff --git a/recruitment/templates/settings/skills/skills_list.html b/recruitment/templates/settings/skills/skills_list.html new file mode 100644 index 000000000..0228b203c --- /dev/null +++ b/recruitment/templates/settings/skills/skills_list.html @@ -0,0 +1,50 @@ +{% load i18n %} +
    +
    +
    +
    +
    {% trans "Sl.No" %}
    +
    {% trans "Skill" %}
    + {% if perms.recruitment.change_recruitment or perms.recruitment.delete_recruitment or perms.recruitment.add_recruitment %} +
    {% trans "Actions" %}
    + {% endif %} +
    +
    +
    + {% for skill in skills %} +
    +
    {{forloop.counter}}
    +
    {{skill.title}}
    + {% if perms.base.change_department or perms.base.delete_department %} +
    +
    + {% if perms.base.change_department %} + + + {% endif %} + {% if perms.base.delete_deaprtment %} + + + + {% endif %} +
    +
    + {% endif %} +
    + {% endfor %} +
    +
    +
    diff --git a/recruitment/templates/settings/skills/skills_view.html b/recruitment/templates/settings/skills/skills_view.html new file mode 100644 index 000000000..747c25d2e --- /dev/null +++ b/recruitment/templates/settings/skills/skills_view.html @@ -0,0 +1,33 @@ +{% extends 'settings.html' %} +{% block settings %} + {% load i18n static %} +
    +
    +

    {% trans 'Skills' %}

    + {% if perms.recruitment.add_rejectreason %} + + {% endif %} +
    + {% if skills|length %} + {% include 'settings/skills/skills_list.html' %} + {% else %} +
    + Page not found. 404. +
    {% trans 'There are no skills added at this moment.' %}
    +
    + {% endif %} +
    + + +{% endblock %} diff --git a/recruitment/urls.py b/recruitment/urls.py index 2f18f570e..c7b3ab680 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -562,4 +562,39 @@ urlpatterns = [ views.check_vaccancy, name="check-vaccancy", ), + path( + "create-skills/", + views.create_skills, + name="create-skills", + ), + path( + "delete-skills/", + views.delete_skills, + name="delete-skills", + ), + path( + "add-bulk-resume/", + views.add_bulk_resumes, + name="add-bulk-resume", + ), + path( + "view-bulk-resume/", + views.view_bulk_resumes, + name="view-bulk-resume", + ), + path( + "delete-resume-file/", + views.delete_resume_file, + name="delete-resume-file", + ), + path( + "matching-resumes/", + views.matching_resumes, + name="matching-resumes", + ), + path( + "matching-resume-completion", + views.matching_resume_completion, + name="matching-resume-completion", + ), ] diff --git a/recruitment/views/search.py b/recruitment/views/search.py index 8a5542991..129807c6f 100644 --- a/recruitment/views/search.py +++ b/recruitment/views/search.py @@ -12,7 +12,12 @@ from django.core.paginator import Paginator from django.shortcuts import render from base.methods import get_key_instances, get_pagination, sortby -from horilla.decorators import hx_request_required, login_required, permission_required +from horilla.decorators import ( + hx_request_required, + is_recruitment_manager, + login_required, + permission_required, +) from horilla.group_by import group_by_queryset from horilla.group_by import group_by_queryset as general_group_by from recruitment.filters import ( @@ -172,13 +177,25 @@ def candidate_filter_view(request): @login_required @hx_request_required -@permission_required(perm="recruitment.view_recruitmentsurvey") +@is_recruitment_manager(perm="recruitment.view_recruitmentsurvey") def filter_survey(request): """ This method is used to filter/search the recruitment surveys """ + + recs = Recruitment.objects.all() + ids = [] + for i in recs: + for manager in i.recruitment_managers.all(): + if request.user.employee_get == manager: + ids.append(i.id) + if request.user.has_perm("view_recruitmentsurvey"): + questions = RecruitmentSurvey.objects.all() + else: + questions = RecruitmentSurvey.objects.filter(recruitment_ids__in=ids) + previous_data = request.GET.urlencode() - filter_obj = SurveyFilter(request.GET) + filter_obj = SurveyFilter(request.GET, questions) questions = filter_obj.qs templates = group_by_queryset( questions.filter(template_id__isnull=False).distinct(), diff --git a/recruitment/views/surveys.py b/recruitment/views/surveys.py index 775019b31..bd3e32a30 100644 --- a/recruitment/views/surveys.py +++ b/recruitment/views/surveys.py @@ -10,13 +10,20 @@ from datetime import datetime from django.contrib import messages from django.core import serializers from django.core.files.storage import default_storage +from django.core.files.uploadedfile import SimpleUploadedFile from django.db.models import ProtectedError from django.http import HttpResponse, HttpResponseRedirect from django.shortcuts import redirect, render from django.utils.translation import gettext_lazy as _ from base.methods import closest_numbers, get_pagination -from horilla.decorators import hx_request_required, login_required, permission_required +from horilla.decorators import ( + hx_request_required, + is_recruitment_manager, + login_required, + manager_can_enter, + permission_required, +) from recruitment.filters import SurveyFilter from recruitment.forms import ( AddQuestionForm, @@ -31,6 +38,7 @@ from recruitment.models import ( Recruitment, RecruitmentSurvey, RecruitmentSurveyAnswer, + Resume, Stage, SurveyTemplate, ) @@ -130,12 +138,21 @@ def candidate_survey(request): @login_required -@permission_required(perm="recruitment.view_recruitmentsurvey") +@is_recruitment_manager(perm="recruitment.view_recruitmentsurvey") def view_question_template(request): """ This method is used to view the question template """ - questions = RecruitmentSurvey.objects.all() + recs = Recruitment.objects.all() + ids = [] + for i in recs: + for manager in i.recruitment_managers.all(): + if request.user.employee_get == manager: + ids.append(i.id) + if request.user.has_perm("view_recruitmentsurvey"): + questions = RecruitmentSurvey.objects.all() + else: + questions = RecruitmentSurvey.objects.filter(recruitment_ids__in=ids) templates = group_by_queryset( questions.filter(template_id__isnull=False).distinct(), "template_id__title", @@ -269,11 +286,27 @@ def application_form(request): form = ApplicationForm() recruitment = None recruitment_id = request.GET.get("recruitmentId") + resume_id = request.GET.get("resumeId") + resume_obj = Resume.objects.filter(id=resume_id).first() + + if resume_obj: + initial_data = {"resume": resume_obj.file.url if resume_obj else None} + form = ApplicationForm(initial=initial_data) + if recruitment_id is not None: recruitment = Recruitment.objects.filter(id=recruitment_id) if recruitment.exists(): recruitment = recruitment.first() if request.POST: + + if "resume" not in request.FILES and resume_id: + if resume_obj and resume_obj.file: + file_content = resume_obj.file.read() + pdf_file = SimpleUploadedFile( + resume_obj.file.name, file_content, content_type="application/pdf" + ) + request.FILES["resume"] = pdf_file + form = ApplicationForm(request.POST, request.FILES) if form.is_valid(): candidate_obj = form.save(commit=False) @@ -286,30 +319,37 @@ def application_form(request): messages.success(request, _("Application saved.")) resume = request.FILES["resume"] - profile = request.FILES["profile"] resume_path = f"recruitment/resume/{resume.name}" - profile_path = f"recruitment/profile/{profile.name}" with default_storage.open(resume_path, "wb+") as destination: for chunk in resume.chunks(): destination.write(chunk) - with default_storage.open(profile_path, "wb+") as destination: - for chunk in profile.chunks(): - destination.write(chunk) - candidate_obj.resume = resume_path - candidate_obj.profile = profile_path - + try: + profile = request.FILES["profile"] if request.FILES["profile"] else None + profile_path = f"recruitment/profile/{profile.name}" + with default_storage.open(profile_path, "wb+") as destination: + for chunk in profile.chunks(): + destination.write(chunk) + candidate_obj.profile = profile_path + except: + pass request.session["candidate"] = serializers.serialize( "json", [candidate_obj] ) if RecruitmentSurvey.objects.filter( recruitment_ids=recruitment_id ).exists(): - return redirect(candidate_survey) + if not request.user.has_perm("perms.recruitment.add_candidate"): + return redirect(candidate_survey) candidate_obj.save() + + if resume_obj: + resume_obj.is_candidate = True + resume_obj.save() + return render(request, "candidate/success.html") form.fields["job_position_id"].queryset = ( form.instance.recruitment_id.open_positions.all() @@ -317,13 +357,13 @@ def application_form(request): return render( request, "candidate/application_form.html", - {"form": form, "recruitment": recruitment}, + {"form": form, "recruitment": recruitment, "resume": resume_obj}, ) @login_required @hx_request_required -@permission_required(perm="recruitment.view_recruitmentsurvey") +@is_recruitment_manager(perm="recruitment.view_recruitmentsurvey") def single_survey(request, survey_id): """ This view method is used to single view of question template diff --git a/templates/settings.html b/templates/settings.html index 39cc12e8b..32d606926 100644 --- a/templates/settings.html +++ b/templates/settings.html @@ -42,10 +42,7 @@ style="font-weight: bold" id="mobileMenu" > - -
      -
    • @@ -179,7 +176,6 @@ > {% endif %}
      -
    @@ -199,13 +195,23 @@
    {% if perms.recruitment.view_rejectreason %} {% trans "Candidate Reject Reason" %} {% endif %}
    +
    + {% if perms.recruitment.add_recruitment %} + {% trans "Skills" %} + {% endif %} +