diff --git a/recruitment/forms.py b/recruitment/forms.py index 642246ddb..4881db315 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -50,6 +50,8 @@ from recruitment.models import ( RecruitmentSurvey, RejectedCandidate, RejectReason, + Resume, + Skill, SkillZone, SkillZoneCandidate, Stage, @@ -257,6 +259,7 @@ class RecruitmentCreationForm(ModelForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + reload_queryset(self.fields) if not self.instance.pk: self.fields["recruitment_managers"] = HorillaMultiSelectField( @@ -271,11 +274,31 @@ class RecruitmentCreationForm(ModelForm): label="Employee", ) + skill_choices = [("", _("---Choose Skills---"))] + list( + self.fields["skills"].queryset.values_list("id", "title") + ) + self.fields["skills"].choices = skill_choices + self.fields["skills"].choices += [("create", _("Create new skill "))] + + # def create_option(self, *args,**kwargs): + # option = super().create_option(*args,**kwargs) + + # if option.get('value') == "create": + # option['attrs']['class'] = 'text-danger' + + # return option + def clean(self): if isinstance(self.fields["recruitment_managers"], HorillaMultiSelectField): ids = self.data.getlist("recruitment_managers") if ids: self.errors.pop("recruitment_managers", None) + open_positions = self.cleaned_data.get("open_positions") + is_published = self.cleaned_data.get("is_published") + if is_published and not open_positions: + raise forms.ValidationError( + _("Job position is required if the recruitment is publishing.") + ) super().clean() @@ -423,6 +446,9 @@ class CandidateCreationForm(ModelForm): return super().clean() +from horilla.horilla_middlewares import _thread_locals + + class ApplicationForm(RegistrationForm): """ Form for create Candidate @@ -468,8 +494,23 @@ class ApplicationForm(RegistrationForm): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) + request = getattr(_thread_locals, "request", None) + self.fields["recruitment_id"].widget.attrs = {"data-widget": "ajax-widget"} self.fields["job_position_id"].widget.attrs = {"data-widget": "ajax-widget"} + if request and request.user.has_perm("recruitment.add_candidate"): + self.fields["profile"].required = False + + def clean(self, *args, **kwargs): + name = self.cleaned_data["name"] + request = getattr(_thread_locals, "request", None) + + if request and request.user.has_perm("recruitment.add_candidate"): + profile_pic_url = f"https://ui-avatars.com/api/?name={name}" + self.cleaned_data["profile"] = profile_pic_url + + super().clean() + return self.cleaned_data class RecruitmentDropDownForm(DropDownForm): @@ -665,7 +706,9 @@ class QuestionForm(ModelForm): required=False, label=_("Recruitment"), ) - options = forms.CharField(widget=forms.TextInput, label=_("Options"), required=True) + options = forms.CharField( + widget=forms.TextInput, label=_("Options"), required=False + ) class Meta: """ @@ -697,7 +740,7 @@ class QuestionForm(ModelForm): cleaned_data = super().clean() recruitment = self.cleaned_data["recruitment"] question_type = self.cleaned_data["type"] - options = self.cleaned_data["options"] + options = self.cleaned_data.get("options") if not recruitment.exists(): # or jobs.exists()): raise ValidationError( {"recruitment": _("Choose any recruitment to apply this question")} @@ -740,7 +783,7 @@ class QuestionForm(ModelForm): } ), label=_("Options"), - required=True, + required=False, initial=initial, ) @@ -834,7 +877,20 @@ class AddQuestionForm(Form): return table_html -exclude_fields = ["id", "profile", "portfolio", "resume", "sequence", "schedule_date"] +exclude_fields = [ + "id", + "profile", + "portfolio", + "resume", + "sequence", + "schedule_date", + "created_at", + "created_by", + "modified_by", + "is_active", + "last_updated", + "horilla_history", +] class CandidateExportForm(forms.Form): @@ -1160,3 +1216,24 @@ class ScheduleInterviewForm(ModelForm): context = {"form": self} table_html = render_to_string("common_form.html", context) return table_html + + +class SkillsForm(ModelForm): + class Meta: + model = Skill + fields = ["title"] + + +class ResumeForm(ModelForm): + class Meta: + model = Resume + fields = ["file", "recruitment_id"] + widgets = {"recruitment_id": forms.HiddenInput()} + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields["file"].widget.attrs.update( + { + "onchange": "submitForm($(this))", + } + ) diff --git a/recruitment/models.py b/recruitment/models.py index 62500bdb9..d3a8240f4 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -80,6 +80,18 @@ class SurveyTemplate(HorillaModel): return self.title +class Skill(HorillaModel): + title = models.CharField(max_length=100) + + def __str__(self): + return self.title + + def save(self, *args, **kwargs): + title = self.title + self.title = title.capitalize() + super().save(*args, **kwargs) + + class Recruitment(HorillaModel): """ Recruitment model @@ -136,6 +148,7 @@ class Recruitment(HorillaModel): ) start_date = models.DateField(default=django.utils.timezone.now) end_date = models.DateField(blank=True, null=True) + skills = models.ManyToManyField(Skill, blank=True) objects = HorillaCompanyManager() default = models.manager.Manager() @@ -175,14 +188,20 @@ class Recruitment(HorillaModel): def clean(self): if self.title is None: raise ValidationError({"title": _("This field is required")}) + if self.is_published: + if self.vacancy <= 0: + raise ValidationError( + _( + "Vacancy must be greater than zero if the recruitment is publishing." + ) + ) + if self.end_date is not None and ( self.start_date is not None and self.start_date > self.end_date ): raise ValidationError( {"end_date": _("End date cannot be less than start date.")} ) - # if not self.is_event_based and self.job_position_id is None: - # raise ValidationError({"job_position_id": _("This field is required")}) return super().clean() def save(self, *args, **kwargs): @@ -888,3 +907,19 @@ class InterviewSchedule(HorillaModel): def __str__(self) -> str: return f"{self.candidate_id} -Interview." + + +class Resume(models.Model): + file = models.FileField( + upload_to="recruitment/resume", + validators=[ + validate_pdf, + ], + ) + recruitment_id = models.ForeignKey( + Recruitment, on_delete=models.CASCADE, related_name="resume" + ) + is_candidate = models.BooleanField(default=False) + + def __str__(self): + return f"{self.recruitment_id} - Resume {self.pk}" diff --git a/recruitment/templates/candidate/candidate_nav.html b/recruitment/templates/candidate/candidate_nav.html index 73997817c..92124b5e6 100644 --- a/recruitment/templates/candidate/candidate_nav.html +++ b/recruitment/templates/candidate/candidate_nav.html @@ -6,39 +6,12 @@ aria-labelledby="candidateExport" aria-hidden="true" > -
-
-

- {% trans "Export Candidates" %} -

- -
-
- {% csrf_token %} {% include 'candidate/export_filter.html'%} - -
-
-
-
+
+ >

{% trans "Candidates" %} @@ -48,7 +21,7 @@ x-data="{searchShow: false}" role="button" aria-label="Toggle Search" @click="searchShow = !searchShow" - > + > {% trans "Export" %} {% endif %} diff --git a/recruitment/templates/candidate/export_filter.html b/recruitment/templates/candidate/export_filter.html index efb038280..eb396bb3c 100644 --- a/recruitment/templates/candidate/export_filter.html +++ b/recruitment/templates/candidate/export_filter.html @@ -1,170 +1,197 @@ {% load static %} {% load i18n %} -
-
-
{% trans "Excel columns" %}
-
-
-
-
- -
-
-
-
- {% for field in export_fields.selected_fields %} -
-
- -
-
- {% endfor %} -
-
-
-
-
-
{% trans "Candidates" %}
-
-
-
-
- - {{export_obj.form.mobile}} -
-
- - {{export_obj.form.interview_date}} -
-
- - {{export_obj.form.country}} -
-
- - {{export_obj.form.hired}} -
-
-
- - {{export_obj.form.rejected_candidate__reject_reason_id}} -
-
-
-
-
- - {{export_obj.form.email}} -
- -
- - {{export_obj.form.gender}} -
- -
- - {{export_obj.form.state}} -
- -
- - {{export_obj.form.canceled}} -
-
-
- - {{export_obj.form.offer_letter_status}} -
-
-
-
-
-
-
-
{% trans "Recruitment" %}
-
-
-
-
- - {{export_obj.form.recruitment_id}} -
- -
- - {{export_obj.form.job_position_id}} -
- -
- - {{export_obj.form.start_date}} -
-
- - {{export_obj.form.recruitment_id__closed}} -
- -
- - {{export_obj.form.stage_id__stage_type}} -
-
- - {{export_obj.form.stage_id__stage_managers}} -
-
-
-
- - {{export_obj.form.stage_id}} -
-
- - {{export_obj.form.job_position_id__department_id}} -
- -
- - {{export_obj.form.recruitment_id__company_id}} -
- -
- - {{export_obj.form.recruitment_id__recruitment_managers}} -
-
- - {{export_obj.form.end_date}} -
-
-
-
-
- -
-
{% trans "Advanced" %}
-
-
-
-
- - {{export_obj.form.scheduled_from}} -
-
- - {{export_obj.form.is_active}} -
-
-
-
- - {{export_obj.form.scheduled_till}} -
-
-
-
-
-
+
+

+ {% trans "Export Candidates" %} +

+ +
+
+
+ {% csrf_token %} +
+
+
{% trans "Excel columns" %}
+
+
+
+
+ +
+
+
+
+ {% for field in export_column.selected_fields %} +
+
+ +
+
+ {% endfor %} +
+
+
+
+
+
{% trans "Candidates" %}
+
+
+
+
+ + {{export_filter.form.mobile}} +
+
+ + {{export_filter.form.interview_date}} +
+
+ + {{export_filter.form.country}} +
+
+ + {{export_filter.form.hired}} +
+
+
+ + {{export_filter.form.rejected_candidate__reject_reason_id}} +
+
+
+
+
+ + {{export_filter.form.email}} +
+ +
+ + {{export_filter.form.gender}} +
+ +
+ + {{export_filter.form.state}} +
+ +
+ + {{export_filter.form.canceled}} +
+
+
+ + {{export_filter.form.offer_letter_status}} +
+
+
+
+
+
+
+
{% trans "Recruitment" %}
+
+
+
+
+ + {{export_filter.form.recruitment_id}} +
+ +
+ + {{export_filter.form.job_position_id}} +
+ +
+ + {{export_filter.form.start_date}} +
+
+ + {{export_filter.form.recruitment_id__closed}} +
+ +
+ + {{export_filter.form.stage_id__stage_type}} +
+
+ + {{export_filter.form.stage_id__stage_managers}} +
+
+
+
+ + {{export_filter.form.stage_id}} +
+
+ + {{export_filter.form.job_position_id__department_id}} +
+ +
+ + {{export_filter.form.recruitment_id__company_id}} +
+ +
+ + {{export_filter.form.recruitment_id__recruitment_managers}} +
+
+ + {{export_filter.form.end_date}} +
+
+
+
+
+ +
+
{% trans "Advanced" %}
+
+
+
+
+ + {{export_filter.form.scheduled_from}} +
+
+ + {{export_filter.form.is_active}} +
+
+
+
+ + {{export_filter.form.scheduled_till}} +
+
+
+
+
+
+
+ + +
diff --git a/recruitment/templates/recruitment/recruitment_update_form.html b/recruitment/templates/recruitment/recruitment_update_form.html index cc5f9ae65..824289449 100644 --- a/recruitment/templates/recruitment/recruitment_update_form.html +++ b/recruitment/templates/recruitment/recruitment_update_form.html @@ -1,83 +1,161 @@ -{% load i18n %} {% if messages %} +{% load i18n %} + + + +{% if messages %}
- {% for message in messages %} -
{{ message }}
- {% endfor %} + {% for message in messages %} +
+ {{ message }} +
+ {% endfor %}
{% endif %} -
- {% csrf_token %} -
- {% csrf_token %} -
-
- - {{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}}
-
-
-
-
-
- -
-
+
+
+
+ {% csrf_token %} +
+ {% csrf_token %} +
+ {% for error in form.non_field_errors %} +
    +
  • {{error}}
  • +
+ {% endfor %} +
+ + {{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.skills}} +
+
+ +
+
{{form.is_published}}
+
+
+
+
+
+ +
+
+
diff --git a/recruitment/templates/survey/question_template_organized_form.html b/recruitment/templates/survey/question_template_organized_form.html index b05a308e6..db60cc8b6 100644 --- a/recruitment/templates/survey/question_template_organized_form.html +++ b/recruitment/templates/survey/question_template_organized_form.html @@ -1,6 +1,5 @@ {% load i18n %}{% load widget_tweaks %} {% load attendancefilters %} {% load basefilters %} -{{form.option_count}}
{% if form.verbose_name %}
diff --git a/recruitment/templates/survey/survey_card.html b/recruitment/templates/survey/survey_card.html index b5a4b354f..e2e3a9ee6 100644 --- a/recruitment/templates/survey/survey_card.html +++ b/recruitment/templates/survey/survey_card.html @@ -9,146 +9,147 @@
{% include "survey/template_accordion.html" %}
-{% comment %}
- {% include "survey/templates.html" %} -
{% endcomment %}
{% if questions %} -
-
- {% for question in questions %} -
-
- {{question}} - - {% for rec in question.recruitment_ids.all %} {{rec}},  {% endfor %} - -
- {% if perms.recruitment.delete_recruitmentsurvey or perms.recruitment.change_recruitmentsurvey %} -
-
- -
- -
+
+
+ {% for question in questions %} +
+
+ {{question}} + + {% for rec in question.recruitment_ids.all %} {{rec}},  + {% endfor %} + +
+ {% if perms.recruitment.delete_recruitmentsurvey or perms.recruitment.change_recruitmentsurvey %} +
+
+ +
+
- {% endif %}
- {% endfor %} + {% endif %}
+ {% endfor %} +
-
- - {% trans "Page" %} {{ questions.number }} {% trans "of" %} {{ questions.paginator.num_pages }}. - - -
+ hx-get="{% url 'rec-filter-survey' %}?{{pd}}&page=1" + class="oh-pagination__link" + >{% trans "First" %} + + +
  • + {% trans "Previous" %} + +
  • + {% endif %} {% if questions.has_next %} +
  • + {% trans "Next" %} + +
  • +
  • + {% trans "Last" %} + +
  • + {% endif %} + +
    +
    {% else %} -
    - Page not found. 404. -
    {% trans "No questions have been established yet." %}
    -
    +
    + Page not found. 404. +
    + {% trans "No questions have been established yet." %} +
    +
    {% endif %}
    diff --git a/recruitment/templates/survey/template_form.html b/recruitment/templates/survey/template_form.html index 2be18813c..897a9ced7 100644 --- a/recruitment/templates/survey/template_form.html +++ b/recruitment/templates/survey/template_form.html @@ -1,7 +1,6 @@
    {% csrf_token %} {{form.as_p}}
    diff --git a/recruitment/templates/survey/view_question_templates.html b/recruitment/templates/survey/view_question_templates.html index 7a3ab8b4f..bb6f7ea02 100644 --- a/recruitment/templates/survey/view_question_templates.html +++ b/recruitment/templates/survey/view_question_templates.html @@ -136,17 +136,6 @@
    - {% comment %} {% if perms.recruitment.add_recruitmentsurvey %} - - {% endif %} {% endcomment %}

    @@ -196,30 +185,6 @@ {% endif %} - {% comment %}
  • - {% trans "Templates" %} - {% if perms.recruitment.add_recruitmentsurvey %} - - {% endif %} -
  • {% endcomment %}
    {% include "survey/survey_card.html" %} @@ -259,5 +224,16 @@
    - + {% endblock content %} diff --git a/recruitment/views/views.py b/recruitment/views/views.py index dbd7ff19c..c9fa8a6d9 100644 --- a/recruitment/views/views.py +++ b/recruitment/views/views.py @@ -32,7 +32,7 @@ from django.core.mail import EmailMessage from django.core.paginator import Paginator from django.db.models import ProtectedError, Q from django.http import HttpResponse, HttpResponseRedirect, JsonResponse -from django.shortcuts import redirect, render +from django.shortcuts import get_object_or_404, redirect, render from django.urls import reverse from django.utils import timezone from django.utils.translation import gettext_lazy as _ @@ -70,7 +70,9 @@ from recruitment.forms import ( OfferLetterForm, RecruitmentCreationForm, RejectReasonForm, + ResumeForm, ScheduleInterviewForm, + SkillsForm, SkillZoneCandidateForm, SkillZoneCreateForm, StageCreationForm, @@ -88,6 +90,8 @@ from recruitment.models import ( RecruitmentMailTemplate, RecruitmentSurvey, RejectReason, + Resume, + Skill, SkillZone, SkillZoneCandidate, Stage, @@ -209,6 +213,11 @@ def recruitment(request): to the recruitment managers """ form = RecruitmentCreationForm() + if request.GET: + form = RecruitmentCreationForm(request.GET) + dynamic = ( + request.GET.get("dynamic") if request.GET.get("dynamic") != "None" else None + ) if request.method == "POST": form = RecruitmentCreationForm(request.POST) if form.is_valid(): @@ -242,7 +251,9 @@ def recruitment(request): redirect=reverse("pipeline"), ) return HttpResponse("") - return render(request, "recruitment/recruitment_form.html", {"form": form}) + return render( + request, "recruitment/recruitment_form.html", {"form": form, "dynamic": dynamic} + ) @login_required @@ -255,7 +266,7 @@ def recruitment_view(request): request.GET.copy().update({"is_active": "on"}) form = RecruitmentCreationForm() queryset = Recruitment.objects.filter(is_active=True) - if queryset.exists(): + if Recruitment.objects.all(): template = "recruitment/recruitment_view.html" else: template = "recruitment/recruitment_empty.html" @@ -303,6 +314,11 @@ def recruitment_update(request, rec_id): for survey in survey_templates: survey_template_list.append(survey.template_id.all()) form = RecruitmentCreationForm(instance=recruitment_obj) + if request.GET: + form = RecruitmentCreationForm(request.GET) + dynamic = ( + request.GET.get("dynamic") if request.GET.get("dynamic") != "None" else None + ) if request.method == "POST": form = RecruitmentCreationForm(request.POST, instance=recruitment_obj) if form.is_valid(): @@ -338,7 +354,11 @@ def recruitment_update(request, rec_id): return HttpResponse( response.content.decode("utf-8") + "" ) - return render(request, "recruitment/recruitment_update_form.html", {"form": form}) + return render( + request, + "recruitment/recruitment_update_form.html", + {"form": form, "dynamic": dynamic}, + ) def paginator_qry_recruitment_limited(qryset, page_number): @@ -1313,8 +1333,6 @@ def candidate_view(request): ) filter_obj = CandidateFilter(request.GET, queryset=candidates) - export_fields = CandidateExportForm() - export_obj = CandidateFilter(request.GET, queryset=candidates) if Candidate.objects.exists(): template = "candidate/candidate_view.html" else: @@ -1332,8 +1350,6 @@ def candidate_view(request): "data": paginator_qry(filter_obj.qs, request.GET.get("page")), "pd": previous_data, "f": filter_obj, - "export_fields": export_fields, - "export_obj": export_obj, "view_type": view_type, "filter_dict": data_dict, "gp_fields": CandidateReGroup.fields, @@ -1431,6 +1447,14 @@ def candidate_export(request): """ This method is used to Export candidate data """ + if request.META.get("HTTP_HX_REQUEST"): + export_column = CandidateExportForm() + export_filter = CandidateFilter() + content = { + "export_filter": export_filter, + "export_column": export_column, + } + return render(request, "candidate/export_filter.html", context=content) return export_data( request=request, model=Candidate, @@ -1489,7 +1513,10 @@ def candidate_view_individual(request, cand_id, **kwargs): """ This method is used to view profile of candidate. """ - candidate_obj = Candidate.objects.get(id=cand_id) + candidate_obj = Candidate.find(cand_id) + if not candidate_obj: + messages.error(request, _("Candidate not found")) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) mails = list(Candidate.objects.values_list("email", flat=True)) # Query the User model to check if any email is present @@ -1600,7 +1627,10 @@ def candidate_conversion(request, cand_id, **kwargs): Args: cand_id : candidate instance id """ - candidate_obj = Candidate.objects.filter(id=cand_id).first() + candidate_obj = Candidate.find(cand_id) + if not candidate_obj: + messages.error(request, _("Candidate not found")) + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) can_name = candidate_obj.name can_mob = candidate_obj.mobile can_job = candidate_obj.job_position_id @@ -2149,10 +2179,12 @@ def skill_zone_delete(request, sz_id): GET : return Skill zone view template """ try: - SkillZone.objects.get(id=sz_id).delete() - messages.success(request, _("Skill zone deleted successfully..")) - except SkillZone.DoesNotExist: - messages.error(request, _("Skill zone not found.")) + skill_zone = SkillZone.find(sz_id) + if skill_zone: + skill_zone.delete() + messages.success(request, _("Skill zone deleted successfully..")) + else: + messages.error(request, _("Skill zone not found.")) except ProtectedError: messages.error(request, _("Related entries exists")) return redirect(skill_zone_view) @@ -2171,8 +2203,8 @@ def skill_zone_archive(request, sz_id): Returns: GET : return Skill zone view template """ - try: - skill_zone = SkillZone.objects.get(id=sz_id) + skill_zone = SkillZone.find(sz_id) + if skill_zone: is_active = skill_zone.is_active if is_active == True: skill_zone.is_active = False @@ -2183,7 +2215,6 @@ def skill_zone_archive(request, sz_id): 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( @@ -2193,11 +2224,9 @@ def skill_zone_archive(request, sz_id): i.is_active = True i.save() messages.success(request, _("Skill zone unarchived successfully..")) - skill_zone.save() - except SkillZone.DoesNotExist: + else: messages.error(request, _("Skill zone not found.")) - return redirect(skill_zone_view) @@ -2767,3 +2796,175 @@ def check_vaccancy(request): if stage and stage.recruitment_id.is_vacancy_filled(): message = _("Vaccancy is filled") return JsonResponse({"message": message}) + + +@login_required +def create_skills(request): + instance_id = eval(str(request.GET.get("instance_id"))) + dynamic = request.GET.get("dynamic") + hx_vals = request.GET.get("data") + instance = None + if instance_id: + instance = Skill.objects.get(id=instance_id) + form = SkillsForm(instance=instance) + if request.method == "POST": + form = SkillsForm(request.POST, instance=instance) + if form.is_valid(): + form.save() + messages.success(request, "Skill created successfully") + + if request.GET.get("dynamic") == "True": + from django.urls import reverse + + url = reverse("recruitment-create") + instance = Skill.objects.all().last() + mutable_get = request.GET.copy() + skills = mutable_get.getlist("skills") + skills.remove("create") + skills.append(str(instance.id)) + mutable_get["skills"] = skills[-1] + skills.pop() + data = mutable_get.urlencode() + try: + for item in skills: + data += f"&skills={item}" + except: + pass + return redirect(f"{url}?{data}") + + return HttpResponse("") + + context = { + "form": form, + "dynamic": dynamic, + "hx_vals": hx_vals, + } + + return render(request, "settings/skills/skills_form.html", context=context) + + +@login_required +@permission_required("recruitment.delete_rejectreason") +def delete_skills(request): + """ + This method is used to delete the reject reasons + """ + ids = request.GET.getlist("ids") + skills = Skill.objects.filter(id__in=ids) + for skill in skills: + skill.delete() + messages.success(request, f"{skill.title} is deleted.") + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + + +def view_bulk_resumes(request): + rec_id = eval(str(request.GET.get("rec_id"))) + resumes = Resume.objects.filter(recruitment_id=rec_id) + + return render( + request, "pipeline/bulk_resume.html", {"resumes": resumes, "rec_id": rec_id} + ) + + +def add_bulk_resumes(request): + rec_id = eval(str(request.GET.get("rec_id"))) + recruitment = Recruitment.objects.get(id=rec_id) + if request.method == "POST": + files = request.FILES.getlist("files") + for file in files: + Resume.objects.create( + file=file, + recruitment_id=recruitment, + ) + + url = reverse("view-bulk-resume") + query_params = f"?rec_id={rec_id}" + + return redirect(f"{url}{query_params}") + + +@login_required +def delete_resume_file(request): + """ + Used to delete attachment + """ + ids = request.GET.getlist("ids") + rec_id = request.GET.get("rec_id") + Resume.objects.filter(id__in=ids).delete() + + url = reverse("view-bulk-resume") + query_params = f"?rec_id={rec_id}" + + return redirect(f"{url}{query_params}") + + +def extract_words_from_pdf(pdf_file): + # Open the PDF file + pdf_document = fitz.open(pdf_file.path) + + words = [] + + for page_num in range(len(pdf_document)): + page = pdf_document.load_page(page_num) + page_text = page.get_text() + + # Use regular expression to extract words + page_words = re.findall(r"\b\w+\b", page_text.lower()) + + words.extend(page_words) + + pdf_document.close() + + return words + + +@login_required +def matching_resumes(request, rec_id): + recruitment = Recruitment.objects.filter(id=rec_id).first() + skills = recruitment.skills.values_list("title", flat=True) + resumes = recruitment.resume.all() + is_candidate = resumes.filter(is_candidate=True) + is_candidate_ids = set(is_candidate.values_list("id", flat=True)) + + resume_ranks = [] + for resume in resumes: + words = extract_words_from_pdf(resume.file) + matching_skills_count = sum(skill.lower() in words for skill in skills) + + resume_ranks.append( + {"resume": resume, "matching_skills_count": matching_skills_count} + ) + + candidate_resumes = [ + rank for rank in resume_ranks if rank["resume"].id in is_candidate_ids + ] + non_candidate_resumes = [ + rank for rank in resume_ranks if rank["resume"].id not in is_candidate_ids + ] + + non_candidate_resumes = sorted( + non_candidate_resumes, key=lambda x: x["matching_skills_count"], reverse=True + ) + candidate_resumes = sorted( + candidate_resumes, key=lambda x: x["matching_skills_count"], reverse=True + ) + + ranked_resumes = non_candidate_resumes + candidate_resumes + + return render( + request, + "pipeline/matching_resumes.html", + { + "matched_resumes": ranked_resumes, + "rec_id": rec_id, + }, + ) + + +def matching_resume_completion(request): + resume_id = request.GET.get("resume_id") + resume_obj = get_object_or_404(Resume, id=resume_id) + resume_file = resume_obj.file + contact_info = extract_info(resume_file) + + return JsonResponse(contact_info)