diff --git a/recruitment/filters.py b/recruitment/filters.py index e5d6167e0..ea1d8e662 100644 --- a/recruitment/filters.py +++ b/recruitment/filters.py @@ -10,6 +10,7 @@ import django_filters from django import forms from recruitment.models import ( Candidate, + InterviewSchedule, Recruitment, SkillZone, SkillZoneCandidate, @@ -135,6 +136,7 @@ class CandidateFilter(FilterSet): "rejected_candidate__reject_reason_id", "offer_letter_status", "candidate_rating__rating", + "candidate_interview__employee_id", ] def __init__(self, *args, **kwargs): @@ -536,3 +538,73 @@ class SkillZoneCandFilter(FilterSet): queryset.filter(candidate_id__name__icontains=value) | queryset.filter(skill_zone_id__title__icontains=value) ).distinct() + + +class InterviewFilter(FilterSet): + """ + Filter set class for Candidate model + + Args: + FilterSet (class): custom filter set class to apply styling + """ + + search = django_filters.CharFilter(field_name="candidate_id__name", lookup_expr="icontains") + + scheduled_from = django_filters.DateFilter( + field_name="interview_date", + lookup_expr="gte", + widget=forms.DateInput(attrs={"type": "date"}), + ) + # schedule_date = django_filters.DateFilter( + # field_name="interview_date", + # widget=forms.DateInput(attrs={"type": "date"}), + # ) + scheduled_till = django_filters.DateFilter( + field_name="interview_date", + lookup_expr="lte", + widget=forms.DateInput(attrs={"type": "date"}), + ) + + class Meta: + """ + Meta class to add the additional info + """ + + model = InterviewSchedule + fields = [ + "candidate_id", + "employee_id", + "interview_date", + # "recruitment", + # "recruitment_id", + # "stage_id", + # "schedule_date", + # "email", + # "mobile", + # "country", + # "state", + # "city", + # "zip", + # "gender", + # "start_onboard", + # "hired", + # "canceled", + # "is_active", + # "recruitment_id__company_id", + # "job_position_id", + # "recruitment_id__closed", + # "recruitment_id__is_active", + # "job_position_id__department_id", + # "recruitment_id__recruitment_managers", + # "stage_id__stage_managers", + # "stage_id__stage_type", + # "joining_date", + # "skillzonecandidate_set__skill_zone_id", + # "skillzonecandidate_set__candidate_id", + # "portal_sent", + # "joining_set", + # "rejected_candidate__reject_reason_id", + # "offer_letter_status", + # "candidate_rating__rating", + # "candidate_interview__employee_id", + ] diff --git a/recruitment/forms.py b/recruitment/forms.py index 8e70f9cf2..d6088076a 100644 --- a/recruitment/forms.py +++ b/recruitment/forms.py @@ -1026,16 +1026,17 @@ class ScheduleInterviewForm(ModelForm): def clean(self): + instance = self.instance cleaned_data = super().clean() interview_date = cleaned_data.get('interview_date') interview_time = cleaned_data.get('interview_time') - if interview_date and interview_date < date.today(): + if not instance.pk and interview_date and interview_date < date.today(): self.add_error('interview_date', _("Interview date cannot be in the past.")) - if interview_time: + if not instance.pk and interview_time: now = datetime.now().time() - if interview_date == date.today() and interview_time < now: + if not instance.pk and interview_date == date.today() and interview_time < now: self.add_error('interview_time', _("Interview time cannot be in the past.")) return cleaned_data diff --git a/recruitment/models.py b/recruitment/models.py index 4587eacd4..72fdd09fd 100644 --- a/recruitment/models.py +++ b/recruitment/models.py @@ -801,6 +801,7 @@ class InterviewSchedule(HorillaModel): employee_id = models.ManyToManyField(Employee, verbose_name=_("interviewer")) interview_date = models.DateField(verbose_name=_("Interview Date")) interview_time = models.TimeField(verbose_name=_("Interview Time")) + completed = models.BooleanField(default=False, verbose_name=_("Is Interview Completed")) def __str__(self) -> str: return f"{self.candidate_id} -Interview." diff --git a/recruitment/templates/candidate/candidate_card.html b/recruitment/templates/candidate/candidate_card.html index 1c106669e..ba60b23ca 100644 --- a/recruitment/templates/candidate/candidate_card.html +++ b/recruitment/templates/candidate/candidate_card.html @@ -48,40 +48,56 @@ \ No newline at end of file diff --git a/recruitment/templates/candidate/interview_list.html b/recruitment/templates/candidate/interview_list.html new file mode 100644 index 000000000..aa28d90b0 --- /dev/null +++ b/recruitment/templates/candidate/interview_list.html @@ -0,0 +1,216 @@ +{% load i18n static %} +{% include 'filter_tags.html' %} + {% if data %} +
+
+ +
+
+
+ +
+
    +
+
+
+
+
+ +
+
+ +
+
+
+
{% trans "Candidate" %}
+
{% trans "Interviewer" %}
+
{% trans "Interview Date" %}
+
{% trans "Interview Time" %}
+
{% trans "Status" %}
+
{% trans "Actions" %}
+
+
+
+ {% for interview in data %} +
+
+
+ + {{interview.candidate_id}} +
+
+
+ {% for employee in interview.employee_id.all %} +
+ +
+
+ Baby C. +
+ {{employee.get_full_name|truncatechars:15}} +
+ {% if perms.recruitment.change_interviewschedule or request.user.employee_get in interview.employee_id.all %} + + + + {% endif %} +
+
+ {% endfor %} + {{interview.employee_id.all|length}} {% trans "Interviewers" %} + +
+
+ {{interview.interview_date}} +
+
+ {{interview.interview_time}} +
+
+ {% if interview.completed %} +
+ check_circle + {% trans "Interview Completed" %} +
+ {% elif interview.interview_date|date:"Y-m-d" < now|date:"Y-m-d" %} +
+ dangerous + {% trans "Expired Interview" %} +
+ {% elif interview.interview_date|date:"Y-m-d" > now|date:"Y-m-d" %} +
+ schedule + {% trans "Upcoming Interview" %} +
+ {% elif interview.interview_date|date:"Y-m-d" == now|date:"Y-m-d" and not interview.completed %} +
+ today + {% trans "Interview Today" %} +
+ {% endif %} +
+
+
+ + {% if perms.recruitment.change_interviewschedule or request.user.employee_get in interview.employee_id.all %} + + {% endif %} + + {% if perms.recruitment.delete_interviewschedule %} +
+ {% csrf_token %} + +
+ {% endif %} +
+
+
+ {% endfor %} +
+
+ +
+
+
+ +
+ + {% trans "Page" %} {{ data.number }} {% trans "of" %} {{ data.paginator.num_pages }}. + + +
+
+ {% else %} +
+ + groups + +
{% trans "No Interviews Found." %}
+
+ {% endif %} + + diff --git a/recruitment/templates/candidate/interview_view.html b/recruitment/templates/candidate/interview_view.html new file mode 100644 index 000000000..9fafb48c0 --- /dev/null +++ b/recruitment/templates/candidate/interview_view.html @@ -0,0 +1,54 @@ +{% extends 'index.html' %} +{% block content %} +{% load static %} +{% load i18n %} + + + + +{% include 'candidate/interview_nav.html' %} +
+
+ {% include 'candidate/interview_list.html' %} +
+
+ + + + + + +{% endblock %} \ No newline at end of file diff --git a/recruitment/templates/pipeline/components/candidate_stage_component.html b/recruitment/templates/pipeline/components/candidate_stage_component.html index d91822db6..bd41e5bc2 100644 --- a/recruitment/templates/pipeline/components/candidate_stage_component.html +++ b/recruitment/templates/pipeline/components/candidate_stage_component.html @@ -90,6 +90,17 @@
+ {% for interview_schedule in cand.candidate_interview.all %} + {% if interview_schedule.interview_date|date:"Y-m-d" == now|date:"Y-m-d" %} +
+ + alarm_on + +
+ {% endif %} + {% endfor %}
diff --git a/recruitment/templates/pipeline/filters.html b/recruitment/templates/pipeline/filters.html index b9b8e1b9e..4f337547c 100644 --- a/recruitment/templates/pipeline/filters.html +++ b/recruitment/templates/pipeline/filters.html @@ -140,7 +140,7 @@ {{ candidate_filter_obj.form.rejected_candidate__reject_reason_id }}
- + {{ candidate_filter_obj.form.hired }}
@@ -163,19 +163,19 @@ >
- + {{ candidate_filter_obj.form.offer_letter_status }}
- + {{ candidate_filter_obj.form.canceled }}
- + {{ candidate_filter_obj.form.candidate_rating__rating }}
- + {{ candidate_filter_obj.form.job_position_id__department_id }}
diff --git a/recruitment/templates/pipeline/pipeline_components/schedule_interview.html b/recruitment/templates/pipeline/pipeline_components/schedule_interview.html index ceb24fd34..74fdf282d 100644 --- a/recruitment/templates/pipeline/pipeline_components/schedule_interview.html +++ b/recruitment/templates/pipeline/pipeline_components/schedule_interview.html @@ -1,6 +1,6 @@ {% load i18n %}
', { + value: key, + text: value + })); + + }); + + // Set the selected Managers back to the dropdown + if (selectedmanagers) { + $('[name=employee_id]').val(selectedmanagers); + } + } + }); + }); + + \ No newline at end of file diff --git a/recruitment/templates/pipeline/pipeline_components/schedule_interview_update.html b/recruitment/templates/pipeline/pipeline_components/schedule_interview_update.html index 9836a2638..6ee591903 100644 --- a/recruitment/templates/pipeline/pipeline_components/schedule_interview_update.html +++ b/recruitment/templates/pipeline/pipeline_components/schedule_interview_update.html @@ -1,6 +1,6 @@ {% load i18n %} ', { + value: key, + text: value + })); + + }); + + // Set the selected Managers back to the dropdown + if (selectedmanagers) { + $('[name=employee_id]').val(selectedmanagers); + } + } + }); + }); + \ No newline at end of file diff --git a/recruitment/urls.py b/recruitment/urls.py index 5a779bfff..f629ab885 100644 --- a/recruitment/urls.py +++ b/recruitment/urls.py @@ -200,10 +200,18 @@ urlpatterns = [ path("send-mail//", views.form_send_mail, name="send-mail"), path("send-mail/", views.form_send_mail, name="send-mail"), path("interview-schedule//", views.interview_schedule, name="interview-schedule"), + path("create-interview-schedule", views.create_interview_schedule, name="create-interview-schedule"), path("edit-interview//", views.interview_edit, name="edit-interview"), path("delete-interview//", views.interview_delete, name="delete-interview"), path("get_managers", views.get_managers, name="get_managers"), path("candidate-view/", views.candidate_view, name="candidate-view"), + path("interview-view/", views.interview_view, name="interview-view"), + path("interview-filter-view/", views.interview_filter_view, name="interview-filter-view"), + path( + "interview-employee-remove//", + views.interview_employee_remove, + name="interview-employee-remove", + ), path( "candidate-filter-view", recruitment.views.search.candidate_filter_view, diff --git a/recruitment/views/views.py b/recruitment/views/views.py index fc1ef5330..5a089f2e4 100644 --- a/recruitment/views/views.py +++ b/recruitment/views/views.py @@ -14,6 +14,7 @@ provide the main entry points for interacting with the application's functionali import datetime from django import template from django.core.mail import EmailMessage +from django.utils import timezone import os import json import contextlib @@ -66,6 +67,7 @@ from recruitment.models import ( from recruitment.filters import ( CandidateFilter, CandidateReGroup, + InterviewFilter, RecruitmentFilter, SkillZoneCandFilter, SkillZoneFilter, @@ -367,6 +369,8 @@ def recruitment_pipeline(request): recruitments = paginator_qry_recruitment_limited( filter_obj.qs, request.GET.get("page") ) + now = timezone.now() + return render( request, template, @@ -375,6 +379,8 @@ def recruitment_pipeline(request): "recruitment": recruitments, "stage_filter_obj": stage_filter, "candidate_filter_obj": candidate_filter, + 'now' : now, + }, ) @@ -523,6 +529,7 @@ def candidate_component(request): if cache[request.user.id]["filter_query"].get("view") == "card": template = "pipeline/kanban_components/candidate_kanban_components.html" + now = timezone.now() return render( request, template, @@ -532,6 +539,7 @@ def candidate_component(request): ), "stage": stage, "rec": getattr(candidates.first(), "recruitment_id", {}), + "now" : now, }, ) @@ -1243,6 +1251,82 @@ def candidate_view(request): ) +@login_required +def interview_filter_view(request): + """ + This method is used to filter Disciplinary Action. + """ + + previous_data = request.GET.urlencode() + + if request.user.has_perm("view_interviewschedule"): + interviews = InterviewSchedule.objects.all() + else: + interviews = InterviewSchedule.objects.filter(employee_id = request.user.employee_get.id) + + dis_filter = InterviewFilter(request.GET, queryset = interviews).qs + + page_number = request.GET.get("page") + page_obj = paginator_qry(dis_filter, page_number) + data_dict = parse_qs(previous_data) + get_key_instances(InterviewSchedule, data_dict) + now=timezone.now() + return render( + request, + "candidate/interview_list.html", + { + "data": page_obj, + "pd": previous_data, + "filter_dict": data_dict, + "now" : now, + }, + ) + +def interview_view(request): + """ + This method render all interviews to the template + """ + previous_data = request.GET.urlencode() + + if request.user.has_perm("view_interviewschedule"): + interviews = InterviewSchedule.objects.all() + else: + interviews = InterviewSchedule.objects.filter(employee_id = request.user.employee_get.id) + + form = InterviewFilter(request.GET, queryset=interviews) + page_number = request.GET.get("page") + page_obj = paginator_qry(form.qs, page_number) + previous_data = request.GET.urlencode() + template = "candidate/interview_view.html" + now=timezone.now() + + return render( + request, + template, + { + "data": page_obj, + "pd": previous_data, + "f": form, + "now":now, + }, + ) + + +@login_required +def interview_employee_remove(request,interview_id,employee_id): + """ + This view is used to remove the employees from the meeting , + Args: + interview_id(int) : primarykey of the interview. + employee_id(int) : primarykey of the employee + """ + interview = InterviewSchedule.objects.filter(id=interview_id).first() + interview.employee_id.remove(employee_id) + messages.success(request, "Interviewer removed succesfully.") + interview.save() + return HttpResponse("") + + @login_required def candidate_export(request): """ @@ -1320,6 +1404,8 @@ def candidate_view_individual(request, cand_id, **kwargs): if len(rating_list) != 0: avg_rate = round(sum(rating_list) / len(rating_list)) + now = timezone.now() + return render( request, "candidate/individual.html", @@ -1327,6 +1413,8 @@ def candidate_view_individual(request, cand_id, **kwargs): "candidate": candidate_obj, "emp_list": existing_emails, "average_rate": avg_rate, + 'now' : now, + }, ) @@ -1514,7 +1602,7 @@ def interview_schedule(request, cand_id): verb_es=f"Estás programado como entrevistador para una entrevista con {cand_id.name} el {interview_date} a las {interview_time}.", verb_fr=f"Vous êtes programmé en tant qu'intervieweur pour un entretien avec {cand_id.name} le {interview_date} à {interview_time}.", icon="people-circle", - redirect=f"/recruitment/candidate-view/{cand_id.id}/", + redirect=f"/recruitment/interview-view/", ) messages.success(request, "Interview Scheduled successfully.") @@ -1522,6 +1610,44 @@ def interview_schedule(request, cand_id): return render(request, template, {"form": form, "cand_id": cand_id}) +@login_required +@manager_can_enter(perm="recruitment.add_interviewschedule") +def create_interview_schedule(request): + """ + This method is used to Schedule interview to candidate + Args: + cand_id : candidate instance id + """ + candidates = Candidate.objects.all() + template = "candidate/interview_form.html" + form = ScheduleInterviewForm() + form.fields["candidate_id"].queryset = candidates + if request.method == "POST": + form = ScheduleInterviewForm(request.POST) + if form.is_valid(): + form.save() + emp_ids = form.cleaned_data["employee_id"] + cand_id = form.cleaned_data["candidate_id"] + interview_date = form.cleaned_data["interview_date"] + interview_time = form.cleaned_data["interview_time"] + users = [employee.employee_user_id for employee in emp_ids] + notify.send( + request.user.employee_get, + recipient=users, + verb=f"You are scheduled as an interviewer for an interview with {cand_id.name} on {interview_date} at {interview_time}.", + verb_ar=f"أنت مجدول كمقابلة مع {cand_id.name} يوم {interview_date} في توقيت {interview_time}.", + verb_de=f"Sie sind als Interviewer für ein Interview mit {cand_id.name} am {interview_date} um {interview_time} eingeplant.", + verb_es=f"Estás programado como entrevistador para una entrevista con {cand_id.name} el {interview_date} a las {interview_time}.", + verb_fr=f"Vous êtes programmé en tant qu'intervieweur pour un entretien avec {cand_id.name} le {interview_date} à {interview_time}.", + icon="people-circle", + redirect=f"/recruitment/interview-view/", + ) + + messages.success(request, "Interview Scheduled successfully.") + return HttpResponse("") + return render(request, template, {"form": form}) + + @login_required @manager_can_enter(perm="recruitment.delete_interviewschedule") def interview_delete(request, interview_id): @@ -1530,10 +1656,14 @@ def interview_delete(request, interview_id): Args: interview_id : interview schedule instance id """ + view = request.GET['view'] interview = InterviewSchedule.objects.get(id=interview_id) interview.delete() messages.success(request, "Interview deleted successfully.") - return HttpResponse("") + if view == 'true': + return HttpResponseRedirect(request.META.get("HTTP_REFERER", "/")) + else: + return HttpResponse("") @login_required @@ -1545,7 +1675,13 @@ def interview_edit(request, interview_id): interview_id : interview schedule instance id """ interview = InterviewSchedule.objects.get(id=interview_id) - candidates = Candidate.objects.filter(id=interview.candidate_id.id) + view = request.GET['view'] + if view == 'true': + candidates = Candidate.objects.all() + view = 'true' + else: + candidates = Candidate.objects.filter(id=interview.candidate_id.id) + view = 'false' template = "pipeline/pipeline_components/schedule_interview_update.html" form = ScheduleInterviewForm(instance=interview) form.fields["candidate_id"].queryset = candidates @@ -1567,11 +1703,11 @@ def interview_edit(request, interview_id): verb_es=f"Estás programado como entrevistador para una entrevista con {cand_id.name} el {interview_date} a las {interview_time}.", verb_fr=f"Vous êtes programmé en tant qu'intervieweur pour un entretien avec {cand_id.name} le {interview_date} à {interview_time}.", icon="people-circle", - redirect=f"/recruitment/candidate-view/{cand_id.id}/", + redirect=f"/recruitment/interview-view/", ) messages.success(request, "Interview updated successfully.") return HttpResponse("") - return render(request, template, {"form": form, "interview_id": interview_id}) + return render(request, template, {"form": form, "interview_id": interview_id, 'view' : view,}) def get_managers(request):