Compare commits
4 Commits
3749fbd3ef
...
f45e9b75c4
| Author | SHA1 | Date | |
|---|---|---|---|
| f45e9b75c4 | |||
| b158cb3e7b | |||
| f168ab4ba8 | |||
| a3348e1199 |
@ -2787,3 +2787,51 @@ class OnsiteInterviewForm(forms.Form):
|
|||||||
# instance.save()
|
# instance.save()
|
||||||
|
|
||||||
# return instance
|
# return instance
|
||||||
|
|
||||||
|
class ScheduledInterviewForm(forms.Form):
|
||||||
|
topic = forms.CharField(
|
||||||
|
max_length=255,
|
||||||
|
required=False,
|
||||||
|
widget=forms.TextInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'e.g., Interview Topic'
|
||||||
|
}),
|
||||||
|
label=_('Interview Topic')
|
||||||
|
)
|
||||||
|
start_time = forms.DateTimeField(
|
||||||
|
widget=forms.DateTimeInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'type': 'datetime-local',
|
||||||
|
'required': True
|
||||||
|
}),
|
||||||
|
label=_('Start Time')
|
||||||
|
)
|
||||||
|
duration = forms.IntegerField(
|
||||||
|
min_value=1,
|
||||||
|
required=False,
|
||||||
|
widget=forms.NumberInput(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'placeholder': 'Duration in minutes'
|
||||||
|
}),
|
||||||
|
label=_('Duration (minutes)')
|
||||||
|
)
|
||||||
|
|
||||||
|
def clean_start_time(self):
|
||||||
|
"""Validate start time is not in the past"""
|
||||||
|
start_time = self.cleaned_data.get('start_time')
|
||||||
|
if start_time and start_time < timezone.now():
|
||||||
|
raise forms.ValidationError(_('Start time cannot be in the past.'))
|
||||||
|
return start_time
|
||||||
|
|
||||||
|
class ScheduledInterviewUpdateStatusForm(forms.Form):
|
||||||
|
status = forms.ChoiceField(
|
||||||
|
choices=ScheduledInterview.InterviewStatus.choices,
|
||||||
|
widget=forms.Select(attrs={
|
||||||
|
'class': 'form-control',
|
||||||
|
'required': True
|
||||||
|
}),
|
||||||
|
label=_('Interview Status')
|
||||||
|
)
|
||||||
|
class Meta:
|
||||||
|
model = ScheduledInterview
|
||||||
|
fields = ['status']
|
||||||
@ -1305,6 +1305,7 @@ class Interview(Base):
|
|||||||
)
|
)
|
||||||
password = models.CharField(max_length=20, blank=True, null=True)
|
password = models.CharField(max_length=20, blank=True, null=True)
|
||||||
zoom_gateway_response = models.JSONField(blank=True, null=True)
|
zoom_gateway_response = models.JSONField(blank=True, null=True)
|
||||||
|
details_url = models.JSONField(blank=True, null=True)
|
||||||
participant_video = models.BooleanField(default=True)
|
participant_video = models.BooleanField(default=True)
|
||||||
join_before_host = models.BooleanField(default=False)
|
join_before_host = models.BooleanField(default=False)
|
||||||
host_email = models.CharField(max_length=255, blank=True, null=True)
|
host_email = models.CharField(max_length=255, blank=True, null=True)
|
||||||
|
|||||||
@ -711,8 +711,8 @@ def create_interview_and_meeting(
|
|||||||
password=result["meeting_details"]["password"],
|
password=result["meeting_details"]["password"],
|
||||||
location_type="Remote"
|
location_type="Remote"
|
||||||
)
|
)
|
||||||
schedule.interviews = interview
|
schedule.interview = interview
|
||||||
schedule.status = "Remote"
|
schedule.status = "scheduled"
|
||||||
|
|
||||||
schedule.save()
|
schedule.save()
|
||||||
|
|
||||||
|
|||||||
@ -205,13 +205,11 @@ urlpatterns = [
|
|||||||
views_frontend.test_source_connection,
|
views_frontend.test_source_connection,
|
||||||
name="test_source_connection",
|
name="test_source_connection",
|
||||||
),
|
),
|
||||||
|
path(
|
||||||
|
"jobs/<slug:slug>/reschedule_meeting_for_application/",
|
||||||
# path(
|
views.reschedule_meeting_for_application,
|
||||||
# "jobs/<slug:slug>/<int:application_id>/reschedule_meeting_for_application/<int:meeting_id>/",
|
name="reschedule_meeting_for_application",
|
||||||
# views.reschedule_meeting_for_application,
|
),
|
||||||
# name="reschedule_meeting_for_application",
|
|
||||||
# ),
|
|
||||||
path(
|
path(
|
||||||
"jobs/<slug:slug>/update_application_exam_status/",
|
"jobs/<slug:slug>/update_application_exam_status/",
|
||||||
views.update_application_exam_status,
|
views.update_application_exam_status,
|
||||||
@ -573,11 +571,13 @@ urlpatterns = [
|
|||||||
# Interview URLs
|
# Interview URLs
|
||||||
path('interviews/', views.interview_list, name='interview_list'),
|
path('interviews/', views.interview_list, name='interview_list'),
|
||||||
path('interviews/<slug:slug>/', views.interview_detail, name='interview_detail'),
|
path('interviews/<slug:slug>/', views.interview_detail, name='interview_detail'),
|
||||||
|
path('interviews/<slug:slug>/update_interview_status', views.update_interview_status, name='update_interview_status'),
|
||||||
|
path('interviews/<slug:slug>/cancel_interview_for_application', views.cancel_interview_for_application, name='cancel_interview_for_application'),
|
||||||
|
|
||||||
# Interview Creation URLs
|
# Interview Creation URLs
|
||||||
path('interviews/create/<slug:candidate_slug>/', views.interview_create_type_selection, name='interview_create_type_selection'),
|
path('interviews/create/<slug:application_slug>/', views.interview_create_type_selection, name='interview_create_type_selection'),
|
||||||
path('interviews/create/<slug:candidate_slug>/remote/', views.interview_create_remote, name='interview_create_remote'),
|
path('interviews/create/<slug:application_slug>/remote/', views.interview_create_remote, name='interview_create_remote'),
|
||||||
path('interviews/create/<slug:candidate_slug>/onsite/', views.interview_create_onsite, name='interview_create_onsite'),
|
path('interviews/create/<slug:application_slug>/onsite/', views.interview_create_onsite, name='interview_create_onsite'),
|
||||||
path('interviews/<slug:job_slug>/get_interview_list', views.get_interview_list, name='get_interview_list'),
|
path('interviews/<slug:job_slug>/get_interview_list', views.get_interview_list, name='get_interview_list'),
|
||||||
|
|
||||||
# # --- SCHEDULED INTERVIEW URLS (New Centralized Management) ---
|
# # --- SCHEDULED INTERVIEW URLS (New Centralized Management) ---
|
||||||
|
|||||||
@ -594,7 +594,7 @@ def update_meeting(instance, updated_data):
|
|||||||
instance.topic = zoom_details.get("topic", instance.topic)
|
instance.topic = zoom_details.get("topic", instance.topic)
|
||||||
|
|
||||||
instance.duration = zoom_details.get("duration", instance.duration)
|
instance.duration = zoom_details.get("duration", instance.duration)
|
||||||
instance.details_url = zoom_details.get("join_url", instance.details_url)
|
# instance.details_url = zoom_details.get("join_url", instance.details_url)
|
||||||
instance.password = zoom_details.get("password", instance.password)
|
instance.password = zoom_details.get("password", instance.password)
|
||||||
# Corrected status assignment: instance.status, not instance.password
|
# Corrected status assignment: instance.status, not instance.password
|
||||||
instance.status = zoom_details.get("status")
|
instance.status = zoom_details.get("status")
|
||||||
|
|||||||
@ -78,6 +78,7 @@ from .forms import (
|
|||||||
PortalLoginForm,
|
PortalLoginForm,
|
||||||
MessageForm,
|
MessageForm,
|
||||||
PersonForm,
|
PersonForm,
|
||||||
|
ScheduledInterviewForm
|
||||||
# OnsiteLocationForm,
|
# OnsiteLocationForm,
|
||||||
# OnsiteReshuduleForm,
|
# OnsiteReshuduleForm,
|
||||||
# OnsiteScheduleForm,
|
# OnsiteScheduleForm,
|
||||||
@ -131,8 +132,7 @@ from .models import (
|
|||||||
Source,
|
Source,
|
||||||
Message,
|
Message,
|
||||||
Document,
|
Document,
|
||||||
# InterviewLocation,
|
Interview
|
||||||
# InterviewNote,
|
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
@ -1196,6 +1196,19 @@ def application_submit_form(request, template_slug):
|
|||||||
"""Display the form as a step-by-step wizard"""
|
"""Display the form as a step-by-step wizard"""
|
||||||
if not request.user.is_authenticated:
|
if not request.user.is_authenticated:
|
||||||
return redirect("application_signup",slug=template_slug)
|
return redirect("application_signup",slug=template_slug)
|
||||||
|
job = get_object_or_404(JobPosting, form_template__slug=template_slug)
|
||||||
|
if request.user.user_type == "candidate":
|
||||||
|
person=request.user.person_profile
|
||||||
|
if job.has_already_applied_to_this_job(person):
|
||||||
|
messages.error(
|
||||||
|
request,
|
||||||
|
_(
|
||||||
|
"You have already applied to this job: Multiple applications are not allowed."
|
||||||
|
),
|
||||||
|
)
|
||||||
|
return redirect("job_application_detail", slug=job.slug)
|
||||||
|
|
||||||
|
|
||||||
template = get_object_or_404(FormTemplate, slug=template_slug, is_active=True)
|
template = get_object_or_404(FormTemplate, slug=template_slug, is_active=True)
|
||||||
stage = template.stages.filter(name="Contact Information")
|
stage = template.stages.filter(name="Contact Information")
|
||||||
|
|
||||||
@ -2018,43 +2031,34 @@ def applications_document_review_view(request, slug):
|
|||||||
return render(request, "recruitment/applications_document_review_view.html", context)
|
return render(request, "recruitment/applications_document_review_view.html", context)
|
||||||
|
|
||||||
|
|
||||||
# @staff_user_required
|
@require_POST
|
||||||
# def reschedule_meeting_for_application(request, slug, candidate_id, meeting_id):
|
@staff_user_required
|
||||||
# job = get_object_or_404(JobPosting, slug=slug)
|
def reschedule_meeting_for_application(request, slug):
|
||||||
# candidate = get_object_or_404(Application, pk=candidate_id)
|
schedule_interview = get_object_or_404(ScheduledInterview, slug=slug)
|
||||||
# meeting = get_object_or_404(ZoomMeetingDetails, pk=meeting_id)
|
if request.method == "POST":
|
||||||
# form = ZoomMeetingForm(instance=meeting)
|
form = ScheduledInterviewForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
topic = form.cleaned_data.get("topic")
|
||||||
|
start_time = form.cleaned_data.get("start_time")
|
||||||
|
duration = form.cleaned_data.get("duration")
|
||||||
|
updated_data = {
|
||||||
|
"topic": topic,
|
||||||
|
"start_time": start_time.isoformat() + "Z",
|
||||||
|
"duration": duration,
|
||||||
|
}
|
||||||
|
result = update_meeting(schedule_interview.interview, updated_data)
|
||||||
|
|
||||||
# if request.method == "POST":
|
if result["status"] == "success":
|
||||||
# form = ZoomMeetingForm(request.POST, instance=meeting)
|
messages.success(request, result["message"])
|
||||||
# if form.is_valid():
|
else:
|
||||||
# instance = form.save(commit=False)
|
messages.error(request, result["message"])
|
||||||
# updated_data = {
|
else:
|
||||||
# "topic": instance.topic,
|
print(form.errors)
|
||||||
# "start_time": instance.start_time.isoformat() + "Z",
|
messages.error(request, "Invalid data submitted.")
|
||||||
# "duration": instance.duration,
|
return redirect("interview_detail", slug=schedule_interview.slug)
|
||||||
# }
|
|
||||||
# if instance.start_time < timezone.now():
|
|
||||||
# messages.error(request, "Start time must be in the future.")
|
|
||||||
# return redirect(
|
|
||||||
# "reschedule_meeting_for_application",
|
|
||||||
# slug=job.slug,
|
|
||||||
# candidate_id=candidate_id,
|
|
||||||
# meeting_id=meeting_id,
|
|
||||||
# )
|
|
||||||
|
|
||||||
# result = update_meeting(instance, updated_data)
|
# context = {"job": job, "application": application, "meeting": meeting, "form": form}
|
||||||
|
# return render(request, "meetings/reschedule_meeting.html", context)
|
||||||
# if result["status"] == "success":
|
|
||||||
# messages.success(request, result["message"])
|
|
||||||
# else:
|
|
||||||
# messages.error(request, result["message"])
|
|
||||||
# return redirect(
|
|
||||||
# reverse("applications_interview_view", kwargs={"slug": job.slug})
|
|
||||||
# )
|
|
||||||
|
|
||||||
# context = {"job": job, "candidate": candidate, "meeting": meeting, "form": form}
|
|
||||||
# return render(request, "meetings/reschedule_meeting.html", context)
|
|
||||||
|
|
||||||
|
|
||||||
# @staff_user_required
|
# @staff_user_required
|
||||||
@ -4514,6 +4518,8 @@ def agency_assignment_detail_admin(request, slug):
|
|||||||
return render(request, "recruitment/agency_assignment_detail.html", context)
|
return render(request, "recruitment/agency_assignment_detail.html", context)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
#will check the changes application to appliaction in this function
|
||||||
@agency_user_required
|
@agency_user_required
|
||||||
def agency_portal_edit_application(request, candidate_id):
|
def agency_portal_edit_application(request, candidate_id):
|
||||||
"""Edit a candidate for agency portal"""
|
"""Edit a candidate for agency portal"""
|
||||||
@ -4870,6 +4876,14 @@ def message_mark_unread(request, message_id):
|
|||||||
@login_required
|
@login_required
|
||||||
def message_delete(request, message_id):
|
def message_delete(request, message_id):
|
||||||
"""Delete a message"""
|
"""Delete a message"""
|
||||||
|
"""
|
||||||
|
Deletes a message using a POST request, primarily designed for HTMX.
|
||||||
|
Redirects to the message list on success (either via standard redirect
|
||||||
|
or HTMX's hx-redirect header).
|
||||||
|
"""
|
||||||
|
|
||||||
|
# 1. Retrieve the message
|
||||||
|
# Use select_related to fetch linked objects efficiently for checks/logging
|
||||||
message = get_object_or_404(
|
message = get_object_or_404(
|
||||||
Message.objects.select_related("sender", "recipient"), id=message_id
|
Message.objects.select_related("sender", "recipient"), id=message_id
|
||||||
)
|
)
|
||||||
@ -4877,6 +4891,14 @@ def message_delete(request, message_id):
|
|||||||
# Check if user has permission to delete this message
|
# Check if user has permission to delete this message
|
||||||
if message.sender != request.user and message.recipient != request.user:
|
if message.sender != request.user and message.recipient != request.user:
|
||||||
messages.error(request, "You don't have permission to delete this message.")
|
messages.error(request, "You don't have permission to delete this message.")
|
||||||
|
|
||||||
|
# HTMX requests should handle redirection via client-side logic (hx-redirect)
|
||||||
|
if "HX-Request" in request.headers:
|
||||||
|
# Returning 403 or 400 is ideal, but 200 with an empty body is often accepted
|
||||||
|
# by HTMX and the message is shown on the next page/refresh.
|
||||||
|
return HttpResponse(status=403)
|
||||||
|
|
||||||
|
# Standard navigation redirect
|
||||||
return redirect("message_list")
|
return redirect("message_list")
|
||||||
|
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
@ -5029,49 +5051,84 @@ def document_delete(request, document_id):
|
|||||||
"""Delete a document"""
|
"""Delete a document"""
|
||||||
document = get_object_or_404(Document, id=document_id)
|
document = get_object_or_404(Document, id=document_id)
|
||||||
|
|
||||||
# Check permission - document is now linked to Application or Person via Generic Foreign Key
|
# Initialize variables for redirection outside of the complex logic
|
||||||
if hasattr(document.content_object, "job"):
|
is_htmx = "HX-Request" in request.headers
|
||||||
# Application document
|
|
||||||
if (
|
|
||||||
document.content_object.job.assigned_to != request.user
|
|
||||||
and not request.user.is_superuser
|
|
||||||
):
|
|
||||||
messages.error(
|
|
||||||
request, "You don't have permission to delete this document."
|
|
||||||
)
|
|
||||||
return JsonResponse({"success": False, "error": "Permission denied"})
|
|
||||||
job_slug = document.content_object.job.slug
|
|
||||||
redirect_url = "applicant_portal_dashboard" if request.user.user_type == "candidate" else "job_detail"
|
|
||||||
elif hasattr(document.content_object, "person"):
|
|
||||||
# Person document
|
|
||||||
if request.user.user_type == "candidate":
|
|
||||||
candidate = request.user.person_profile
|
|
||||||
if document.content_object != candidate:
|
|
||||||
messages.error(
|
|
||||||
request, "You can only delete your own documents."
|
|
||||||
)
|
|
||||||
return JsonResponse({"success": False, "error": "Permission denied"})
|
|
||||||
redirect_url = "applicant_portal_dashboard"
|
|
||||||
else:
|
|
||||||
# Handle other content object types
|
|
||||||
messages.error(request, "You don't have permission to delete this document.")
|
|
||||||
return JsonResponse({"success": False, "error": "Permission denied"})
|
|
||||||
|
|
||||||
|
# 1. Permission and Context Initialization
|
||||||
|
has_permission = False
|
||||||
|
|
||||||
|
content_object = document.content_object
|
||||||
|
|
||||||
|
# Case A: Document linked to an Application (via content_object)
|
||||||
|
if hasattr(content_object, "job"):
|
||||||
|
# Staff/Superuser checking against Application's Job assignment
|
||||||
|
if (content_object.job.assigned_to == request.user) or request.user.is_superuser:
|
||||||
|
has_permission = True
|
||||||
|
|
||||||
|
# Candidate checking if the Application belongs to them
|
||||||
|
elif request.user.user_type == "candidate" and content_object.person.user == request.user:
|
||||||
|
has_permission = True
|
||||||
|
|
||||||
|
# Determine redirect URL for non-HTMX requests (fallback)
|
||||||
|
if request.user.user_type == "candidate":
|
||||||
|
# Assuming you redirect to the candidate's main dashboard after deleting their app document
|
||||||
|
redirect_view_name = "applicant_portal_dashboard"
|
||||||
|
else:
|
||||||
|
# Assuming you redirect to the job detail page for staff
|
||||||
|
redirect_view_name = "job_detail"
|
||||||
|
redirect_args = [content_object.job.slug] # Pass the job slug
|
||||||
|
|
||||||
|
# Case B: Document linked directly to a Person (e.g., profile document)
|
||||||
|
elif hasattr(content_object, "user"):
|
||||||
|
# Check if the document belongs to the requesting candidate
|
||||||
|
if request.user.user_type == "candidate" and content_object.user == request.user:
|
||||||
|
has_permission = True
|
||||||
|
redirect_view_name = "applicant_portal_dashboard"
|
||||||
|
# Check if the requesting user is staff/superuser (Staff can delete profile docs)
|
||||||
|
elif request.user.is_staff or request.user.is_superuser:
|
||||||
|
has_permission = True
|
||||||
|
# Staff should probably go to the person's profile detail, but defaulting to a safe spot.
|
||||||
|
redirect_view_name = "dashboard"
|
||||||
|
|
||||||
|
# Case C: No clear content object linkage or unhandled type
|
||||||
|
else:
|
||||||
|
has_permission = request.user.is_superuser # Only superuser can delete unlinked docs
|
||||||
|
|
||||||
|
|
||||||
|
# 2. Enforce Permissions
|
||||||
|
if not has_permission:
|
||||||
|
messages.error(request, "Permission denied: You cannot delete this document.")
|
||||||
|
# Return a 403 response for HTMX/AJAX
|
||||||
|
return HttpResponse(status=403)
|
||||||
|
|
||||||
|
|
||||||
|
# 3. Handle POST Request (Deletion)
|
||||||
if request.method == "POST":
|
if request.method == "POST":
|
||||||
file_name = document.file.name if document.file else "Unknown"
|
file_name = document.file.name if document.file else "Unknown"
|
||||||
document.delete()
|
document.delete()
|
||||||
messages.success(request, f'Document "{file_name}" deleted successfully!')
|
messages.success(request, f'Document "{file_name}" deleted successfully!')
|
||||||
|
|
||||||
# Handle AJAX requests
|
# --- HTMX / AJAX Response ---
|
||||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
if is_htmx or request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||||
return JsonResponse(
|
# For HTMX, return a 200 OK. The front-end is expected to use hx-swap='outerHTML'
|
||||||
{"success": True, "message": "Document deleted successfully!"}
|
# to remove the element, or hx-redirect to navigate.
|
||||||
)
|
return HttpResponse(status=200)
|
||||||
|
|
||||||
|
# --- Standard Navigation Fallback ---
|
||||||
else:
|
else:
|
||||||
return redirect("application_detail", slug=job_slug)
|
try:
|
||||||
|
# Use the calculated redirect view name and arguments
|
||||||
return JsonResponse({"success": False, "error": "Method not allowed"})
|
if 'redirect_args' in locals():
|
||||||
|
return redirect(redirect_view_name, *redirect_args)
|
||||||
|
else:
|
||||||
|
return redirect(redirect_view_name)
|
||||||
|
except NameError:
|
||||||
|
# If no specific redirect_view_name was set (e.g., Case C failure)
|
||||||
|
return redirect("dashboard")
|
||||||
|
|
||||||
|
# 4. Handle non-POST (e.g., GET)
|
||||||
|
# The delete view should not be accessed via GET.
|
||||||
|
return HttpResponse(status=405) # Method Not Allowed
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def document_download(request, document_id):
|
def document_download(request, document_id):
|
||||||
@ -5114,14 +5171,6 @@ def document_download(request, document_id):
|
|||||||
|
|
||||||
return JsonResponse({"success": False, "error": "File not found"})
|
return JsonResponse({"success": False, "error": "File not found"})
|
||||||
|
|
||||||
if document.file:
|
|
||||||
response = HttpResponse(
|
|
||||||
document.file.read(), content_type="application/octet-stream"
|
|
||||||
)
|
|
||||||
response["Content-Disposition"] = f'attachment; filename="{document.file.name}"'
|
|
||||||
return response
|
|
||||||
|
|
||||||
return JsonResponse({"success": False, "error": "File not found"})
|
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
@ -5135,30 +5184,21 @@ def portal_logout(request):
|
|||||||
|
|
||||||
# Interview Creation Views
|
# Interview Creation Views
|
||||||
@staff_user_required
|
@staff_user_required
|
||||||
def interview_create_type_selection(request, candidate_slug):
|
def interview_create_type_selection(request, application_slug):
|
||||||
"""Show interview type selection page for a candidate"""
|
"""Show interview type selection page for a application"""
|
||||||
candidate = get_object_or_404(Application, slug=candidate_slug)
|
application = get_object_or_404(Application, slug=application_slug)
|
||||||
# Validate candidate is in Interview stage
|
|
||||||
# if candidate.stage != 'Interview':
|
|
||||||
# messages.error(request, f"Candidate {candidate.name} is not in Interview stage.")
|
|
||||||
# return redirect('candidate_interview_view', slug=candidate.job.slug)
|
|
||||||
|
|
||||||
context = {
|
context = {
|
||||||
'candidate': candidate,
|
'application': application,
|
||||||
'job': candidate.job,
|
'job': application.job,
|
||||||
}
|
}
|
||||||
return render(request, 'interviews/interview_create_type_selection.html', context)
|
return render(request, 'interviews/interview_create_type_selection.html', context)
|
||||||
|
|
||||||
|
|
||||||
@staff_user_required
|
@staff_user_required
|
||||||
def interview_create_remote(request, candidate_slug):
|
def interview_create_remote(request, application_slug):
|
||||||
"""Create remote interview for a candidate"""
|
"""Create remote interview for a candidate"""
|
||||||
application = get_object_or_404(Application, slug=candidate_slug)
|
application = get_object_or_404(Application, slug=application_slug)
|
||||||
|
|
||||||
# Validate candidate is in Interview stage
|
|
||||||
# if candidate.stage != 'Interview':
|
|
||||||
# messages.error(request, f"Candidate {candidate.name} is not in Interview stage.")
|
|
||||||
# return redirect('candidate_interview_view', slug=candidate.job.slug)
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
form = RemoteInterviewForm(request.POST)
|
form = RemoteInterviewForm(request.POST)
|
||||||
@ -5171,41 +5211,16 @@ def interview_create_remote(request, candidate_slug):
|
|||||||
"recruitment.tasks.create_interview_and_meeting",
|
"recruitment.tasks.create_interview_and_meeting",
|
||||||
application.pk, application.job.pk, schedule.pk, schedule.interview_date,schedule.interview_time, form.cleaned_data['duration']
|
application.pk, application.job.pk, schedule.pk, schedule.interview_date,schedule.interview_time, form.cleaned_data['duration']
|
||||||
)
|
)
|
||||||
# interview.interview_type = 'REMOTE'
|
|
||||||
# interview.status = 'SCHEDULED'
|
|
||||||
# interview.save()
|
|
||||||
|
|
||||||
# Create ZoomMeetingDetails record
|
|
||||||
# from .models import ZoomMeetingDetails
|
|
||||||
# zoom_meeting = ZoomMeetingDetails.objects.create(
|
|
||||||
# topic=form.cleaned_data['topic'],
|
|
||||||
# start_time=timezone.make_aware(
|
|
||||||
# timezone.datetime.combine(
|
|
||||||
# form.cleaned_data['interview_date'],
|
|
||||||
# form.cleaned_data['interview_time']
|
|
||||||
# ),
|
|
||||||
# timezone.get_current_timezone()
|
|
||||||
# ),
|
|
||||||
# duration=form.cleaned_data['duration'],
|
|
||||||
# meeting_id=f"KAUH-{interview.id}-{timezone.now().timestamp()}",
|
|
||||||
# join_url=f"https://zoom.us/j/{interview.id}",
|
|
||||||
# password=secrets.token_urlsafe(16),
|
|
||||||
# status='scheduled'
|
|
||||||
# )
|
|
||||||
|
|
||||||
# Link Zoom meeting to interview
|
|
||||||
# interview.interview_location = zoom_meeting
|
|
||||||
# interview.save()
|
|
||||||
|
|
||||||
messages.success(request, f"Remote interview scheduled for {application.name}")
|
messages.success(request, f"Remote interview scheduled for {application.name}")
|
||||||
return redirect('interview_list')
|
return redirect('interview_detail', slug=schedule.slug)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messages.error(request, f"Error creating remote interview: {str(e)}")
|
messages.error(request, f"Error creating remote interview: {str(e)}")
|
||||||
form = RemoteInterviewForm()
|
form = RemoteInterviewForm()
|
||||||
form.initial['topic'] = f"Interview for {application.job.title} - {application.name}"
|
form.initial['topic'] = f"Interview for {application.job.title} - {application.name}"
|
||||||
context = {
|
context = {
|
||||||
'candidate': application,
|
'application': application,
|
||||||
'job': application.job,
|
'job': application.job,
|
||||||
'form': form,
|
'form': form,
|
||||||
}
|
}
|
||||||
@ -5213,14 +5228,9 @@ def interview_create_remote(request, candidate_slug):
|
|||||||
|
|
||||||
|
|
||||||
@staff_user_required
|
@staff_user_required
|
||||||
def interview_create_onsite(request, candidate_slug):
|
def interview_create_onsite(request, application_slug):
|
||||||
"""Create onsite interview for a candidate"""
|
"""Create onsite interview for a candidate"""
|
||||||
candidate = get_object_or_404(Application, slug=candidate_slug)
|
application = get_object_or_404(Application, slug=application_slug)
|
||||||
|
|
||||||
# Validate candidate is in Interview stage
|
|
||||||
# if candidate.stage != 'Interview':
|
|
||||||
# messages.error(request, f"Candidate {candidate.name} is not in Interview stage.")
|
|
||||||
# return redirect('candidate_interview_view', slug=candidate.job.slug)
|
|
||||||
|
|
||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
from .models import Interview
|
from .models import Interview
|
||||||
@ -5234,58 +5244,61 @@ def interview_create_onsite(request, candidate_slug):
|
|||||||
physical_address=form.cleaned_data["physical_address"],
|
physical_address=form.cleaned_data["physical_address"],
|
||||||
duration=form.cleaned_data["duration"],location_type="Onsite",status="SCHEDULED")
|
duration=form.cleaned_data["duration"],location_type="Onsite",status="SCHEDULED")
|
||||||
|
|
||||||
schedule = ScheduledInterview.objects.create(application=candidate,job=candidate.job,interview=interview,interview_date=form.cleaned_data["interview_date"],interview_time=form.cleaned_data["interview_time"])
|
schedule = ScheduledInterview.objects.create(application=application,job=application.job,interview=interview,interview_date=form.cleaned_data["interview_date"],interview_time=form.cleaned_data["interview_time"])
|
||||||
# Create ScheduledInterview record
|
|
||||||
# interview = form.save(commit=False)
|
|
||||||
# interview.interview_type = 'ONSITE'
|
|
||||||
# interview.status = 'SCHEDULED'
|
|
||||||
# interview.save()
|
|
||||||
|
|
||||||
# Create OnsiteLocationDetails record
|
messages.success(request, f"Onsite interview scheduled for {application.name}")
|
||||||
# from .models import OnsiteLocationDetails
|
|
||||||
# onsite_location = OnsiteLocationDetails.objects.create(
|
|
||||||
# topic=form.cleaned_data['topic'],
|
|
||||||
# start_time=timezone.make_aware(
|
|
||||||
# timezone.datetime.combine(
|
|
||||||
# form.cleaned_data['interview_date'],
|
|
||||||
# form.cleaned_data['interview_time']
|
|
||||||
# ),
|
|
||||||
# timezone.get_current_timezone()
|
|
||||||
# ),
|
|
||||||
# duration=form.cleaned_data['duration'],
|
|
||||||
# physical_address=form.cleaned_data['physical_address'],
|
|
||||||
# room_number=form.cleaned_data.get('room_number', ''),
|
|
||||||
# location_type='ONSITE',
|
|
||||||
# status='scheduled'
|
|
||||||
# )
|
|
||||||
|
|
||||||
# # Link onsite location to interview
|
|
||||||
# interview.interview_location = onsite_location
|
|
||||||
# interview.save()
|
|
||||||
|
|
||||||
messages.success(request, f"Onsite interview scheduled for {candidate.name}")
|
|
||||||
return redirect('interview_detail', slug=schedule.slug)
|
return redirect('interview_detail', slug=schedule.slug)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
messages.error(request, f"Error creating onsite interview: {str(e)}")
|
messages.error(request, f"Error creating onsite interview: {str(e)}")
|
||||||
else:
|
else:
|
||||||
# Pre-populate topic
|
# Pre-populate topic
|
||||||
form.initial['topic'] = f"Interview for {candidate.job.title} - {candidate.name}"
|
form.initial['topic'] = f"Interview for {application.job.title} - {application.name}"
|
||||||
|
|
||||||
form = OnsiteInterviewForm()
|
form = OnsiteInterviewForm()
|
||||||
|
form.initial['topic'] = f"Interview for {application.job.title} - {application.name}"
|
||||||
context = {
|
context = {
|
||||||
'candidate': candidate,
|
'application': application,
|
||||||
'job': candidate.job,
|
'job': application.job,
|
||||||
'form': form,
|
'form': form,
|
||||||
}
|
}
|
||||||
return render(request, 'interviews/interview_create_onsite.html', context)
|
return render(request, 'interviews/interview_create_onsite.html', context)
|
||||||
|
|
||||||
|
|
||||||
def get_interview_list(request, job_slug):
|
def get_interview_list(request, job_slug):
|
||||||
|
from .forms import ScheduledInterviewUpdateStatusForm
|
||||||
application = Application.objects.get(slug=job_slug)
|
application = Application.objects.get(slug=job_slug)
|
||||||
interviews = ScheduledInterview.objects.filter(application=application).order_by("interview_date","interview_time").select_related('interview')
|
interviews = ScheduledInterview.objects.filter(application=application).order_by("interview_date","interview_time").select_related('interview')
|
||||||
print(interviews)
|
interview_status_form = ScheduledInterviewUpdateStatusForm()
|
||||||
return render(request, 'interviews/partials/interview_list.html', {'interviews': interviews, 'application': application})
|
return render(request, 'interviews/partials/interview_list.html', {'interviews': interviews, 'application': application,'interview_status_form':interview_status_form})
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
def update_interview_status(request,slug):
|
||||||
|
from .forms import ScheduledInterviewUpdateStatusForm
|
||||||
|
|
||||||
|
if request.method == 'POST':
|
||||||
|
form = ScheduledInterviewUpdateStatusForm(request.POST)
|
||||||
|
if form.is_valid():
|
||||||
|
scheduled_interview = get_object_or_404(ScheduledInterview, slug=slug)
|
||||||
|
scheduled_interview.status = form.cleaned_data['status']
|
||||||
|
scheduled_interview.save(update_fields=['status'])
|
||||||
|
messages.success(request, "Interview status updated successfully.")
|
||||||
|
return redirect('interview_detail', slug=slug)
|
||||||
|
|
||||||
|
@require_POST
|
||||||
|
def cancel_interview_for_application(request,slug):
|
||||||
|
scheduled_interview = get_object_or_404(ScheduledInterview, slug=slug)
|
||||||
|
if request.method == 'POST':
|
||||||
|
if scheduled_interview.interview_type == 'REMOTE':
|
||||||
|
result = delete_zoom_meeting(scheduled_interview.interview.meeting_id)
|
||||||
|
if result["status"] != "success":
|
||||||
|
messages.error(request, f"Error cancelling Zoom meeting: {result.get('message', 'Unknown error')}")
|
||||||
|
return redirect('interview_detail', slug=slug)
|
||||||
|
|
||||||
|
scheduled_interview.delete()
|
||||||
|
messages.success(request, "Interview cancelled successfully.")
|
||||||
|
return redirect('interview_list')
|
||||||
|
|
||||||
|
|
||||||
@login_required
|
@login_required
|
||||||
def agency_access_link_deactivate(request, slug):
|
def agency_access_link_deactivate(request, slug):
|
||||||
@ -5416,8 +5429,10 @@ def compose_application_email(request, job_slug):
|
|||||||
if request.method == 'POST':
|
if request.method == 'POST':
|
||||||
|
|
||||||
candidate_ids = request.POST.getlist('candidate_ids')
|
candidate_ids = request.POST.getlist('candidate_ids')
|
||||||
candidates=Application.objects.filter(id__in=candidate_ids)
|
print("candidate_ids from post:", candidate_ids)
|
||||||
form = CandidateEmailForm(job, candidates, request.POST)
|
|
||||||
|
applications=Application.objects.filter(id__in=candidate_ids)
|
||||||
|
form = CandidateEmailForm(job, applications, request.POST)
|
||||||
if form.is_valid():
|
if form.is_valid():
|
||||||
print("form is valid ...")
|
print("form is valid ...")
|
||||||
# Get email addresses
|
# Get email addresses
|
||||||
@ -5453,18 +5468,24 @@ def compose_application_email(request, job_slug):
|
|||||||
)
|
)
|
||||||
|
|
||||||
if email_result["success"]:
|
if email_result["success"]:
|
||||||
for candidate in candidates:
|
for application in applications:
|
||||||
if hasattr(candidate, 'person') and candidate.person:
|
if hasattr(application, 'person') and application.person:
|
||||||
try:
|
try:
|
||||||
|
print(request.user)
|
||||||
|
print(application.person.user)
|
||||||
|
print(subject)
|
||||||
|
print(message)
|
||||||
|
print(job)
|
||||||
|
|
||||||
Message.objects.create(
|
Message.objects.create(
|
||||||
sender=request.user,
|
sender=request.user,
|
||||||
recipient=candidate.person.user,
|
recipient=application.person.user,
|
||||||
subject=subject,
|
subject=subject,
|
||||||
content=message,
|
content=message,
|
||||||
job=job,
|
job=job,
|
||||||
message_type='email',
|
message_type='email',
|
||||||
is_email_sent=True,
|
is_email_sent=True,
|
||||||
email_address=candidate.person.email if candidate.person.email else candidate.email
|
email_address=application.person.email if application.person.email else application.email
|
||||||
)
|
)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
@ -5804,10 +5825,16 @@ def interview_list(request):
|
|||||||
@staff_user_required
|
@staff_user_required
|
||||||
def interview_detail(request, slug):
|
def interview_detail(request, slug):
|
||||||
"""View details of a specific interview"""
|
"""View details of a specific interview"""
|
||||||
|
from .forms import ScheduledInterviewUpdateStatusForm
|
||||||
|
|
||||||
interview = get_object_or_404(ScheduledInterview, slug=slug)
|
interview = get_object_or_404(ScheduledInterview, slug=slug)
|
||||||
|
|
||||||
|
reschedule_form = ScheduledInterviewForm()
|
||||||
|
reschedule_form.initial['topic'] = interview.interview.topic
|
||||||
context = {
|
context = {
|
||||||
'interview': interview,
|
'interview': interview,
|
||||||
|
'reschedule_form':reschedule_form,
|
||||||
|
'interview_status_form':ScheduledInterviewUpdateStatusForm()
|
||||||
}
|
}
|
||||||
return render(request, 'interviews/interview_detail.html', context)
|
return render(request, 'interviews/interview_detail.html', context)
|
||||||
|
|
||||||
|
|||||||
@ -10,9 +10,9 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h4 class="mb-0">
|
<h4 class="mb-0">
|
||||||
<i class="fas fa-building me-2"></i>
|
<i class="fas fa-building me-2"></i>
|
||||||
Create Onsite Interview for {{ candidate.name }}
|
Create Onsite Interview for {{ application.name }}
|
||||||
</h4>
|
</h4>
|
||||||
<a href="{% url 'interview_create_type_selection' candidate.slug %}"
|
<a href="{% url 'interview_create_type_selection' application.slug %}"
|
||||||
class="btn btn-outline-primary">
|
class="btn btn-outline-primary">
|
||||||
<i class="fas fa-arrow-left me-2"></i>
|
<i class="fas fa-arrow-left me-2"></i>
|
||||||
Back to Candidate List
|
Back to Candidate List
|
||||||
@ -20,7 +20,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="text-muted mb-3">
|
<p class="text-muted mb-3">
|
||||||
Schedule an onsite interview for <strong>{{ candidate.name }}</strong>
|
Schedule an onsite interview for <strong>{{ application.name }}</strong>
|
||||||
for the position of <strong>{{ job.title }}</strong>.
|
for the position of <strong>{{ job.title }}</strong>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -33,7 +33,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form method="post" action="{% url 'interview_create_onsite' candidate_slug=candidate.slug %}">
|
<form method="post" action="{% url 'interview_create_onsite' application.slug %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
|
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@ -99,36 +99,8 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% comment %} <div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.interviewer.id_for_label }}" class="form-label">
|
|
||||||
<i class="fas fa-user me-1"></i>
|
|
||||||
Interviewer
|
|
||||||
</label>
|
|
||||||
{{ form.interviewer }}
|
|
||||||
{% if form.interviewer.errors %}
|
|
||||||
<div class="text-danger small">
|
|
||||||
{{ form.interviewer.errors }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div> {% endcomment %}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% comment %} <div class="mb-3">
|
|
||||||
<label for="{{ form.topic.id_for_label }}" class="form-label">
|
|
||||||
<i class="fas fa-comment me-1"></i>
|
|
||||||
Meeting Topic
|
|
||||||
</label>
|
|
||||||
{{ form.topic }}
|
|
||||||
{% if form.topic.errors %}
|
|
||||||
<div class="text-danger small">
|
|
||||||
{{ form.topic.errors }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div> {% endcomment %}
|
|
||||||
|
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="{{ form.physical_address.id_for_label }}" class="form-label">
|
<label for="{{ form.physical_address.id_for_label }}" class="form-label">
|
||||||
<i class="fas fa-map-marker-alt me-1"></i>
|
<i class="fas fa-map-marker-alt me-1"></i>
|
||||||
@ -158,48 +130,6 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% comment %} <div class="col-md-6">
|
|
||||||
<div class="mb-3">
|
|
||||||
<label for="{{ form.floor_number.id_for_label }}" class="form-label">
|
|
||||||
<i class="fas fa-layer-group me-1"></i>
|
|
||||||
Floor Number
|
|
||||||
</label>
|
|
||||||
{{ form.floor_number }}
|
|
||||||
{% if form.floor_number.errors %}
|
|
||||||
<div class="text-danger small">
|
|
||||||
{{ form.floor_number.errors }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div>
|
|
||||||
</div> {% endcomment %}
|
|
||||||
{% comment %} </div> {% endcomment %}
|
|
||||||
|
|
||||||
{% comment %} <div class="mb-3">
|
|
||||||
<label for="{{ form.parking_info.id_for_label }}" class="form-label">
|
|
||||||
<i class="fas fa-parking me-1"></i>
|
|
||||||
Parking Information
|
|
||||||
</label>
|
|
||||||
{{ form.parking_info }}
|
|
||||||
{% if form.parking_info.errors %}
|
|
||||||
<div class="text-danger small">
|
|
||||||
{{ form.parking_info.errors }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div> {% endcomment %}
|
|
||||||
|
|
||||||
{% comment %} <div class="mb-3">
|
|
||||||
<label for="{{ form.notes.id_for_label }}" class="form-label">
|
|
||||||
<i class="fas fa-sticky-note me-1"></i>
|
|
||||||
Notes
|
|
||||||
</label>
|
|
||||||
{{ form.notes }}
|
|
||||||
{% if form.notes.errors %}
|
|
||||||
<div class="text-danger small">
|
|
||||||
{{ form.notes.errors }}
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
</div> {% endcomment %}
|
|
||||||
|
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
<button type="submit" class="btn btn-main-action">
|
<button type="submit" class="btn btn-main-action">
|
||||||
<i class="fas fa-save me-2"></i>
|
<i class="fas fa-save me-2"></i>
|
||||||
|
|||||||
@ -11,9 +11,9 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h4 class="mb-0">
|
<h4 class="mb-0">
|
||||||
<i class="fas fa-video me-2"></i>
|
<i class="fas fa-video me-2"></i>
|
||||||
Create Remote Interview for {{ candidate.name }}
|
Create Remote Interview for {{ application.name }}
|
||||||
</h4>
|
</h4>
|
||||||
<a href="{% url 'interview_create_type_selection' candidate.slug %}"
|
<a href="{% url 'interview_create_type_selection' application.slug %}"
|
||||||
class="btn btn-outline-primary">
|
class="btn btn-outline-primary">
|
||||||
<i class="fas fa-arrow-left me-2"></i>
|
<i class="fas fa-arrow-left me-2"></i>
|
||||||
Back to Candidate List
|
Back to Candidate List
|
||||||
@ -21,7 +21,7 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<p class="text-muted mb-3">
|
<p class="text-muted mb-3">
|
||||||
Schedule a remote interview for <strong>{{ candidate.name }}</strong>
|
Schedule a remote interview for <strong>{{ application.name }}</strong>
|
||||||
for the position of <strong>{{ job.title }}</strong>.
|
for the position of <strong>{{ job.title }}</strong>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
@ -34,7 +34,7 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
<form method="post" action="{% url 'interview_create_remote' candidate_slug=candidate.slug %}">
|
<form method="post" action="{% url 'interview_create_remote' application.slug %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
{{form|crispy}}
|
{{form|crispy}}
|
||||||
<div class="d-flex justify-content-between">
|
<div class="d-flex justify-content-between">
|
||||||
|
|||||||
@ -10,17 +10,17 @@
|
|||||||
<div class="card-header">
|
<div class="card-header">
|
||||||
<h4 class="mb-0">
|
<h4 class="mb-0">
|
||||||
<i class="fas fa-calendar-plus me-2"></i>
|
<i class="fas fa-calendar-plus me-2"></i>
|
||||||
Create Interview for {{ candidate.name }}
|
Create Interview for {{ application.name }}
|
||||||
</h4>
|
</h4>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body" hx-boost="true" hx-push-url="false" hx-select=".card-body" hx-swap="innerHTML" hx-target="#candidateviewModalBody">
|
<div class="card-body" hx-boost="true" hx-push-url="false" hx-select=".card-body" hx-swap="innerHTML" hx-target="#candidateviewModalBody">
|
||||||
<p class="text-muted mb-3">
|
<p class="text-muted mb-3">
|
||||||
Select the type of interview you want to schedule for <strong>{{ candidate.name }}</strong>
|
Select the type of interview you want to schedule for <strong>{{ application.name }}</strong>
|
||||||
for the position of <strong>{{ job.title }}</strong>.
|
for the position of <strong>{{ job.title }}</strong>.
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div class="d-grid gap-3" style="grid-template-columns: 1fr 1fr;">
|
<div class="d-grid gap-3" style="grid-template-columns: 1fr 1fr;">
|
||||||
<a href="{% url 'interview_create_remote' candidate_slug=candidate.slug %}"
|
<a href="{% url 'interview_create_remote' application.slug %}"
|
||||||
class="btn btn-outline-primary btn-lg h-100 p-3 text-decoration-none">
|
class="btn btn-outline-primary btn-lg h-100 p-3 text-decoration-none">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<i class="fas fa-video me-2"></i>
|
<i class="fas fa-video me-2"></i>
|
||||||
@ -29,7 +29,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</a>
|
</a>
|
||||||
|
|
||||||
<a href="{% url 'interview_create_onsite' candidate_slug=candidate.slug %}"
|
<a href="{% url 'interview_create_onsite' application.slug %}"
|
||||||
class="btn btn-outline-primary btn-lg h-100 p-3 text-decoration-none">
|
class="btn btn-outline-primary btn-lg h-100 p-3 text-decoration-none">
|
||||||
<div class="text-center">
|
<div class="text-center">
|
||||||
<i class="fas fa-building me-2"></i>
|
<i class="fas fa-building me-2"></i>
|
||||||
|
|||||||
@ -1,5 +1,5 @@
|
|||||||
{% extends 'base.html' %}
|
{% extends 'base.html' %}
|
||||||
{% load static i18n %}
|
{% load static i18n crispy_forms_tags %}
|
||||||
|
|
||||||
{% block title %}{{ interview.application.name }} - {% trans "Interview Details" %} - ATS{% endblock %}
|
{% block title %}{{ interview.application.name }} - {% trans "Interview Details" %} - ATS{% endblock %}
|
||||||
|
|
||||||
@ -224,6 +224,11 @@
|
|||||||
<a href="{% url 'job_detail' interview.job.slug %}" class="btn btn-outline-secondary">
|
<a href="{% url 'job_detail' interview.job.slug %}" class="btn btn-outline-secondary">
|
||||||
<i class="fas fa-briefcase me-1"></i> {% trans "View Job" %}
|
<i class="fas fa-briefcase me-1"></i> {% trans "View Job" %}
|
||||||
</a>
|
</a>
|
||||||
|
<button type="button" class="btn btn-outline-secondary"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#statusModal">
|
||||||
|
<i class="fas fa-redo-alt me-1"></i> {% trans "Update Interview status" %}
|
||||||
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@ -245,7 +250,7 @@
|
|||||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#candidateModal"
|
data-bs-target="#candidateModal"
|
||||||
hx-get="{% url 'candidate_criteria_view_htmx' interview.application.pk %}"
|
hx-get="#"
|
||||||
hx-target="#candidateModalBody">
|
hx-target="#candidateModalBody">
|
||||||
<i class="fas fa-eye me-1"></i> {% trans "AI Scoring" %}
|
<i class="fas fa-eye me-1"></i> {% trans "AI Scoring" %}
|
||||||
</button>
|
</button>
|
||||||
@ -323,6 +328,13 @@
|
|||||||
<span class="detail-label">{% trans "Duration:" %}</span>
|
<span class="detail-label">{% trans "Duration:" %}</span>
|
||||||
<span class="detail-value">{{ interview.interview.duration }} {% trans "minutes" %}</span>
|
<span class="detail-value">{{ interview.interview.duration }} {% trans "minutes" %}</span>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="detail-item">
|
||||||
|
<span class="detail-label">{% trans "Status:" %}</span>
|
||||||
|
<span class="detail-value">
|
||||||
|
<span class="badge bg-primary-theme">
|
||||||
|
{{ interview.status }}</span>
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="col-md-6">
|
<div class="col-md-6">
|
||||||
@ -643,21 +655,21 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form method="post" action="#">
|
<form method="post" action="{% url 'reschedule_meeting_for_application' interview.slug %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="mb-3">
|
{{reschedule_form|crispy}}
|
||||||
<label for="new_date" class="form-label">{% trans "New Date" %}</label>
|
{% comment %} <div class="mb-3">
|
||||||
<input type="date" class="form-control" id="new_date" name="new_date" required>
|
<label for="topic" class="form-label">{% trans "topic" %}</label>
|
||||||
|
<input type="text" class="form-control" id="topic" name="topic" value="{{interview.interview.topic}}" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="new_time" class="form-label">{% trans "New Time" %}</label>
|
<label for="start_time" class="form-label">{% trans "Start Time" %}</label>
|
||||||
<input type="time" class="form-control" id="new_time" name="new_time" required>
|
<input type="datetime-local" class="form-control" id="start_time" name="start_time" required>
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
<div class="mb-3">
|
||||||
<label for="reschedule_reason" class="form-label">{% trans "Reason for Rescheduling" %}</label>
|
<label for="duration" class="form-label">{% trans "Duration" %}</label>
|
||||||
<textarea class="form-control" id="reschedule_reason" name="reason" rows="3"
|
<input type="number" class="form-control" id="duration" name="duration" required>
|
||||||
placeholder="{% trans 'Optional: Provide reason for rescheduling' %}"></textarea>
|
</div> {% endcomment %}
|
||||||
</div>
|
|
||||||
<button type="submit" class="btn btn-main-action btn-sm">
|
<button type="submit" class="btn btn-main-action btn-sm">
|
||||||
<i class="fas fa-redo-alt me-1"></i> {% trans "Reschedule" %}
|
<i class="fas fa-redo-alt me-1"></i> {% trans "Reschedule" %}
|
||||||
</button>
|
</button>
|
||||||
@ -678,17 +690,12 @@
|
|||||||
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<form method="post" action="#">
|
<form method="post" action="{% url 'cancel_interview_for_application' interview.slug %}">
|
||||||
{% csrf_token %}
|
{% csrf_token %}
|
||||||
<div class="alert alert-warning">
|
<div class="alert alert-warning">
|
||||||
<i class="fas fa-exclamation-triangle me-2"></i>
|
<i class="fas fa-exclamation-triangle me-2"></i>
|
||||||
{% trans "Are you sure you want to cancel this interview? This action cannot be undone." %}
|
{% trans "Are you sure you want to cancel this interview? This action cannot be undone." %}
|
||||||
</div>
|
</div>
|
||||||
<div class="mb-3">
|
|
||||||
<label for="cancel_reason" class="form-label">{% trans "Reason for Cancellation" %}</label>
|
|
||||||
<textarea class="form-control" id="cancel_reason" name="reason" rows="3" required
|
|
||||||
placeholder="{% trans 'Please provide a reason for cancellation' %}"></textarea>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex gap-2">
|
<div class="d-flex gap-2">
|
||||||
<button type="submit" class="btn btn-danger btn-sm">
|
<button type="submit" class="btn btn-danger btn-sm">
|
||||||
<i class="fas fa-times me-1"></i> {% trans "Cancel Interview" %}
|
<i class="fas fa-times me-1"></i> {% trans "Cancel Interview" %}
|
||||||
@ -739,6 +746,34 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- Update Status Modal -->
|
||||||
|
<div class="modal fade" id="statusModal" tabindex="-1" aria-labelledby="statusModalLabel" aria-hidden="true">
|
||||||
|
<div class="modal-dialog">
|
||||||
|
<div class="modal-content kaauh-card">
|
||||||
|
<div class="modal-header" style="border-bottom: 1px solid var(--kaauh-border);">
|
||||||
|
<h5 class="modal-title" id="statusModalLabel" style="color: var(--kaauh-teal-dark);">
|
||||||
|
<i class="fas fa-sync-alt me-2"></i> {% trans "Update Interview Status" %}
|
||||||
|
</h5>
|
||||||
|
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form method="post" action="{% url 'update_interview_status' interview.slug %}">
|
||||||
|
{% csrf_token %}
|
||||||
|
{{interview_status_form|crispy}}
|
||||||
|
<div class="d-flex gap-2">
|
||||||
|
<button type="submit" class="btn btn-main-action">
|
||||||
|
<i class="fas fa-check me-1"></i> {% trans "Update Status" %}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="btn btn-outline-secondary" data-bs-dismiss="modal">
|
||||||
|
{% trans "Close" %}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|
||||||
{% block customJS %}
|
{% block customJS %}
|
||||||
|
|||||||
@ -230,9 +230,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
</div>
|
</div>
|
||||||
{{meetings}}
|
|
||||||
{# Using 'meetings' based on the context_object_name provided #}
|
{# Using 'meetings' based on the context_object_name provided #}
|
||||||
{% if meetings %}
|
{% if interviews %}
|
||||||
<div id="meetings-list">
|
<div id="meetings-list">
|
||||||
{# View Switcher (kept the name for simplicity) #}
|
{# View Switcher (kept the name for simplicity) #}
|
||||||
{% include "includes/_list_view_switcher.html" with list_id="meetings-list" %}
|
{% include "includes/_list_view_switcher.html" with list_id="meetings-list" %}
|
||||||
|
|||||||
@ -2,12 +2,12 @@
|
|||||||
<table class="table candidate-table align-middle">
|
<table class="table candidate-table align-middle">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th style="width: 40%;"><i class="fas fa-user me-1"></i> {% trans "Topic" %}</th>
|
<th style="width: 35%;"><i class="fas fa-user me-1"></i> {% trans "Topic" %}</th>
|
||||||
<th style="width: 15%;"><i class="fas fa-calendar-alt me-1"></i> {% trans "Date" %}</th>
|
<th style="width: 15%;"><i class="fas fa-calendar-alt me-1"></i> {% trans "Date" %}</th>
|
||||||
<th style="width: 5%;"><i class="fas fa-map-marker-alt me-1"></i> {% trans "Duration" %}</th>
|
<th style="width: 10%;"><i class="fas fa-hourglass-half me-1"></i> {% trans "Duration" %}</th>
|
||||||
<th style="width: 10%;"><i class="fas fa-map-marker-alt me-1"></i> {% trans "Location" %}</th>
|
<th style="width: 10%;"><i class="fas fa-location-dot me-1"></i> {% trans "Location" %}</th>
|
||||||
<th style="width: 10%;"><i class="fas fa-info-circle me-1"></i> {% trans "Status" %}</th>
|
<th style="width: 10%;"><i class="fas fa-info-circle me-1"></i> {% trans "Status" %}</th>
|
||||||
<th style="width: 10%;"><i class="fas fa-ellipsis-h me-1"></i> {% trans "Actions" %}</th>
|
<th style="width: 10%;"> {% trans "Actions" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
|
|||||||
@ -266,12 +266,13 @@
|
|||||||
</th>
|
</th>
|
||||||
<th style="width: 13%"><i class="fas fa-user me-1"></i> {% trans "Name" %}</th>
|
<th style="width: 13%"><i class="fas fa-user me-1"></i> {% trans "Name" %}</th>
|
||||||
<th style="width: 15%"><i class="fas fa-phone me-1"></i> {% trans "Contact" %}</th>
|
<th style="width: 15%"><i class="fas fa-phone me-1"></i> {% trans "Contact" %}</th>
|
||||||
<th style="width: 15%"><i class="fas fa-tag me-1"></i> {% trans "Topic" %}</th>
|
{% comment %} <th style="width: 15%"><i class="fas fa-tag me-1"></i> {% trans "Topic" %}</th>
|
||||||
<th style="width: 15%"><i class="fas fa-clock me-1"></i> {% trans "Duration" %}</th>
|
<th style="width: 15%"><i class="fas fa-clock me-1"></i> {% trans "Duration" %}</th>
|
||||||
<th style="width: 10%"><i class="fas fa-calendar me-1"></i> {% trans "Meeting Date" %}</th>
|
<th style="width: 10%"><i class="fas fa-calendar me-1"></i> {% trans "Meeting Date" %}</th>
|
||||||
<th style="width: 7%"><i class="fas fa-video me-1"></i> {% trans "Link" %}</th>
|
<th style="width: 7%"><i class="fas fa-video me-1"></i> {% trans "Link" %}</th>
|
||||||
<th style="width: 8%"><i class="fas fa-check-circle me-1"></i> {% trans "Meeting Status" %}</th>
|
<th style="width: 8%"><i class="fas fa-check-circle me-1"></i> {% trans "Meeting Status" %}</th> {% endcomment %}
|
||||||
<th style="width: 5%"><i class="fas fa-check-circle me-1"></i> {% trans "Interview Result"%}</th>
|
<th style="width: 15%"><i class="fas fa-check-circle me-1"></i> {% trans "Interview Result"%}</th>
|
||||||
|
<th style="width: 15%"><i class="fas fa-check-circle me-1"></i> {% trans "Interview List"%}</th>
|
||||||
<th style="width: 10%"><i class="fas fa-cog me-1"></i> {% trans "Actions" %}</th>
|
<th style="width: 10%"><i class="fas fa-cog me-1"></i> {% trans "Actions" %}</th>
|
||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
@ -301,7 +302,7 @@
|
|||||||
<i class="fas fa-phone me-1"></i> {{ application.phone }}
|
<i class="fas fa-phone me-1"></i> {{ application.phone }}
|
||||||
</div>
|
</div>
|
||||||
</td>
|
</td>
|
||||||
<td class="application-details text-muted">
|
{% comment %} <td class="application-details text-muted">
|
||||||
{% if application.get_latest_meeting %}
|
{% if application.get_latest_meeting %}
|
||||||
{{ application.get_latest_meeting }}
|
{{ application.get_latest_meeting }}
|
||||||
{% else %}
|
{% else %}
|
||||||
@ -323,8 +324,8 @@
|
|||||||
<span class="text-muted">--</span>
|
<span class="text-muted">--</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</td>
|
</td> {% endcomment %}
|
||||||
<td>
|
{% comment %} <td>
|
||||||
{% with latest_meeting=application.get_latest_meeting %}
|
{% with latest_meeting=application.get_latest_meeting %}
|
||||||
{% if latest_meeting and latest_meeting.details_url %}
|
{% if latest_meeting and latest_meeting.details_url %}
|
||||||
<a href="{{ latest_meeting.details_url }}" target="_blank" class="btn btn-sm bg-primary-theme text-white" title="Join Interview"
|
<a href="{{ latest_meeting.details_url }}" target="_blank" class="btn btn-sm bg-primary-theme text-white" title="Join Interview"
|
||||||
@ -336,8 +337,8 @@
|
|||||||
<span class="text-muted">--</span>
|
<span class="text-muted">--</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</td>
|
</td> {% endcomment %}
|
||||||
<td>
|
{% comment %} <td>
|
||||||
{{ latest_meeting.status }}
|
{{ latest_meeting.status }}
|
||||||
{% with latest_meeting=application.get_latest_meeting %}
|
{% with latest_meeting=application.get_latest_meeting %}
|
||||||
{% if latest_meeting %}
|
{% if latest_meeting %}
|
||||||
@ -351,7 +352,7 @@
|
|||||||
<span class="text-muted">--</span>
|
<span class="text-muted">--</span>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% endwith %}
|
{% endwith %}
|
||||||
</td>
|
</td> {% endcomment %}
|
||||||
<td class="text-center" id="interview-result-{{ application.pk }}">
|
<td class="text-center" id="interview-result-{{ application.pk }}">
|
||||||
{% if not application.interview_status %}
|
{% if not application.interview_status %}
|
||||||
<button type="button" class="btn btn-warning btn-sm"
|
<button type="button" class="btn btn-warning btn-sm"
|
||||||
@ -377,6 +378,17 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</td>
|
</td>
|
||||||
|
<td>
|
||||||
|
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||||
|
data-bs-toggle="modal"
|
||||||
|
data-bs-target="#candidateviewModal"
|
||||||
|
hx-get="{% url 'get_interview_list' application.slug %}"
|
||||||
|
hx-target="#candidateviewModalBody">
|
||||||
|
Interview List
|
||||||
|
<i class="fas fa-list"></i>
|
||||||
|
{{candidate.get_interviews}}
|
||||||
|
</button>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
|
|
||||||
{% if application.get_latest_meeting %}
|
{% if application.get_latest_meeting %}
|
||||||
@ -422,12 +434,7 @@
|
|||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
{% else %}
|
{% else %}
|
||||||
{% comment %} <a href="{% url 'interview_create_type_selection' candidate_slug=candidate.slug %}"
|
|
||||||
class="btn btn-main-action btn-sm"
|
|
||||||
title="Schedule Interview">
|
|
||||||
<i class="fas fa-calendar-plus me-1"></i>
|
|
||||||
Schedule
|
|
||||||
</a> {% endcomment %}
|
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
<button type="button" class="btn btn-outline-primary btn-sm"
|
||||||
data-bs-toggle="modal"
|
data-bs-toggle="modal"
|
||||||
data-bs-target="#candidateviewModal"
|
data-bs-target="#candidateviewModal"
|
||||||
@ -436,23 +443,10 @@
|
|||||||
hx-swap="innerHTML"
|
hx-swap="innerHTML"
|
||||||
hx-target="#candidateviewModalBody">
|
hx-target="#candidateviewModalBody">
|
||||||
<i class="fas fa-calendar-plus me-1"></i>
|
<i class="fas fa-calendar-plus me-1"></i>
|
||||||
Schedule
|
Schedule Interview
|
||||||
</button>
|
</button>
|
||||||
{% comment %} <a href="{% url 'interview_create_type_selection' candidate_slug=candidate.slug %}"
|
|
||||||
class="btn btn-main-action btn-sm"
|
|
||||||
title="Schedule Interview">
|
|
||||||
<i class="fas fa-calendar-plus me-1"></i>
|
|
||||||
Schedule
|
|
||||||
</a> {% endcomment %}
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<button type="button" class="btn btn-outline-primary btn-sm"
|
|
||||||
data-bs-toggle="modal"
|
|
||||||
data-bs-target="#candidateviewModal"
|
|
||||||
hx-get="{% url 'get_interview_list' application.slug %}"
|
|
||||||
hx-target="#candidateviewModalBody">
|
|
||||||
<i class="fas fa-list"></i>
|
|
||||||
</button>
|
|
||||||
{{candidate.get_interviews}}
|
|
||||||
</td>
|
</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user