Merge branch 'main' of http://10.10.1.136:3000/marwan/kaauh_ats into frontend

This commit is contained in:
Faheed 2025-12-01 13:50:17 +03:00
commit 0bfbd80e15
13 changed files with 362 additions and 265 deletions

View File

@ -2804,3 +2804,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']

View File

@ -1334,6 +1334,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)

View File

@ -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()

View File

@ -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) ---

View File

@ -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")

View File

@ -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,
) )
@ -1209,6 +1209,19 @@ def application_submit_form(request, template_slug):
return redirect("job_application_detail", slug=job.slug) return redirect("job_application_detail", slug=job.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")
@ -2034,43 +2047,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, application_id, meeting_id): @staff_user_required
# job = get_object_or_404(JobPosting, slug=slug) def reschedule_meeting_for_application(request, slug):
# application = get_object_or_404(Application, pk=application_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,
# application_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
@ -4888,6 +4892,15 @@ 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"""
"""
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
""" """
Deletes a message using a POST request, primarily designed for HTMX. Deletes a message using a POST request, primarily designed for HTMX.
Redirects to the message list on success (either via standard redirect Redirects to the message list on success (either via standard redirect
@ -5117,6 +5130,26 @@ def document_delete(request, document_id):
content_object = document.content_object 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)
# Initialize variables for redirection outside of the complex logic
is_htmx = "HX-Request" in request.headers
# 1. Permission and Context Initialization
has_permission = False
content_object = document.content_object
# Case A: Document linked to an Application (via content_object) # Case A: Document linked to an Application (via content_object)
if hasattr(content_object, "job"): if hasattr(content_object, "job"):
# Staff/Superuser checking against Application's Job assignment # Staff/Superuser checking against Application's Job assignment
@ -5160,12 +5193,51 @@ def document_delete(request, document_id):
return HttpResponse(status=403) return HttpResponse(status=403)
# 3. Handle POST Request (Deletion)
# 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) # 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!')
# --- HTMX / AJAX Response ---
if is_htmx or request.headers.get("X-Requested-With") == "XMLHttpRequest":
# For HTMX, return a 200 OK. The front-end is expected to use hx-swap='outerHTML'
# to remove the element, or hx-redirect to navigate.
return HttpResponse(status=200)
# --- Standard Navigation Fallback ---
# --- HTMX / AJAX Response --- # --- HTMX / AJAX Response ---
if is_htmx or request.headers.get("X-Requested-With") == "XMLHttpRequest": if is_htmx or request.headers.get("X-Requested-With") == "XMLHttpRequest":
# For HTMX, return a 200 OK. The front-end is expected to use hx-swap='outerHTML' # For HTMX, return a 200 OK. The front-end is expected to use hx-swap='outerHTML'
@ -5184,6 +5256,19 @@ def document_delete(request, document_id):
# If no specific redirect_view_name was set (e.g., Case C failure) # If no specific redirect_view_name was set (e.g., Case C failure)
return redirect("dashboard") 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
try:
# Use the calculated redirect view name and arguments
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) # 4. Handle non-POST (e.g., GET)
# The delete view should not be accessed via GET. # The delete view should not be accessed via GET.
return HttpResponse(status=405) # Method Not Allowed return HttpResponse(status=405) # Method Not Allowed
@ -5231,6 +5316,7 @@ def document_download(request, document_id):
@login_required @login_required
def portal_logout(request): def portal_logout(request):
"""Logout from portal""" """Logout from portal"""
@ -5242,6 +5328,9 @@ def portal_logout(request):
# Interview Creation Views # Interview Creation Views
@staff_user_required @staff_user_required
def interview_create_type_selection(request, application_slug):
"""Show interview type selection page for a application"""
application = get_object_or_404(Application, slug=application_slug)
def interview_create_type_selection(request, application_slug): def interview_create_type_selection(request, application_slug):
"""Show interview type selection page for a application""" """Show interview type selection page for a application"""
application = get_object_or_404(Application, slug=application_slug) application = get_object_or_404(Application, slug=application_slug)
@ -5254,11 +5343,16 @@ def interview_create_type_selection(request, application_slug):
context = { context = {
'application': application, 'application': application,
'job': application.job, 'job': application.job,
'application': application,
'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, application_slug):
"""Create remote interview for a candidate"""
application = get_object_or_404(Application, slug=application_slug)
def interview_create_remote(request, application_slug): def interview_create_remote(request, application_slug):
"""Create remote interview for a application""" """Create remote interview for a application"""
application = get_object_or_404(Application, slug=application_slug) application = get_object_or_404(Application, slug=application_slug)
@ -5279,40 +5373,16 @@ def interview_create_remote(request, application_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 = {
'application': application,
'application': application, 'application': application,
'job': application.job, 'job': application.job,
'form': form, 'form': form,
@ -5322,14 +5392,9 @@ def interview_create_remote(request, application_slug):
@staff_user_required @staff_user_required
def interview_create_onsite(request, application_slug): def interview_create_onsite(request, application_slug):
"""Create onsite interview for a application""" """Create onsite interview for a candidate"""
application = get_object_or_404(Application, slug=application_slug) application = get_object_or_404(Application, slug=application_slug)
# Validate application is in Interview stage
# if application.stage != 'Interview':
# messages.error(request, f"application {application.name} is not in Interview stage.")
# return redirect('application_interview_view', slug=application.job.slug)
if request.method == 'POST': if request.method == 'POST':
from .models import Interview from .models import Interview
@ -5343,33 +5408,6 @@ def interview_create_onsite(request, application_slug):
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=application,job=application.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
# 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 {application.name}") messages.success(request, f"Onsite interview scheduled for {application.name}")
return redirect('interview_detail', slug=schedule.slug) return redirect('interview_detail', slug=schedule.slug)
@ -5379,9 +5417,13 @@ def interview_create_onsite(request, application_slug):
else: else:
# Pre-populate topic # Pre-populate topic
form.initial['topic'] = f"Interview for {application.job.title} - {application.name}" form.initial['topic'] = f"Interview for {application.job.title} - {application.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 = {
'application': application,
'job': application.job,
'application': application, 'application': application,
'job': application.job, 'job': application.job,
'form': form, 'form': form,
@ -5390,10 +5432,39 @@ def interview_create_onsite(request, application_slug):
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):
@ -5530,6 +5601,10 @@ def compose_application_email(request, job_slug):
candidate_ids = request.POST.getlist('candidate_ids') candidate_ids = request.POST.getlist('candidate_ids')
print("candidate_ids from post:", candidate_ids) print("candidate_ids from post:", candidate_ids)
applications=Application.objects.filter(id__in=candidate_ids)
form = CandidateEmailForm(job, applications, request.POST)
print("candidate_ids from post:", candidate_ids)
applications=Application.objects.filter(id__in=candidate_ids) applications=Application.objects.filter(id__in=candidate_ids)
form = CandidateEmailForm(job, applications, request.POST) form = CandidateEmailForm(job, applications, request.POST)
if form.is_valid(): if form.is_valid():
@ -5567,6 +5642,8 @@ def compose_application_email(request, job_slug):
) )
if email_result["success"]: if email_result["success"]:
for application in applications:
if hasattr(application, 'person') and application.person:
for application in applications: for application in applications:
if hasattr(application, 'person') and application.person: if hasattr(application, 'person') and application.person:
try: try:
@ -5576,9 +5653,16 @@ def compose_application_email(request, job_slug):
print(message) print(message)
print(job) print(job)
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=application.person.user, recipient=application.person.user,
recipient=application.person.user,
subject=subject, subject=subject,
content=message, content=message,
job=job, job=job,
@ -5923,10 +6007,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)

View File

@ -11,7 +11,7 @@
<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>
{% blocktrans %}Create Onsite Interview for {{ application.name }}{% endblocktrans %} Create Onsite Interview for {{ application.name }}
</h4> </h4>
<a href="{% url 'interview_create_type_selection' application.slug %}" <a href="{% url 'interview_create_type_selection' application.slug %}"
class="btn btn-outline-primary"> class="btn btn-outline-primary">
@ -21,8 +21,8 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<p class="text-muted mb-3"> <p class="text-muted mb-3">
{% blocktrans %}Schedule an onsite interview for <strong>{{ application.name }}</strong> Schedule an onsite interview for <strong>{{ application.name }}</strong>
for the position of <strong>{{ job.title }}</strong>.{% endblocktrans %} for the position of <strong>{{ job.title }}</strong>.
</p> </p>
{% if messages %} {% if messages %}
@ -34,7 +34,7 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<form method="post" action="{% url 'interview_create_onsite' application_slug=application.slug %}"> <form method="post" action="{% url 'interview_create_onsite' application.slug %}">
{% csrf_token %} {% csrf_token %}
<div class="row"> <div class="row">
@ -100,36 +100,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>
@ -159,48 +131,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>

View File

@ -11,7 +11,7 @@
<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>
{% blocktrans %}Create Remote Interview for {{ application.name }}{% endblocktrans %} Create Remote Interview for {{ application.name }}
</h4> </h4>
<a href="{% url 'interview_create_type_selection' application.slug %}" <a href="{% url 'interview_create_type_selection' application.slug %}"
class="btn btn-outline-primary"> class="btn btn-outline-primary">
@ -21,8 +21,8 @@
</div> </div>
<div class="card-body"> <div class="card-body">
<p class="text-muted mb-3"> <p class="text-muted mb-3">
{% blocktrans %}Schedule a remote interview for <strong>{{ application.name }}</strong> Schedule a remote interview for <strong>{{ application.name }}</strong>
for the position of <strong>{{ job.title }}</strong>.{% endblocktrans %} for the position of <strong>{{ job.title }}</strong>.
</p> </p>
{% if messages %} {% if messages %}
@ -34,7 +34,7 @@
{% endfor %} {% endfor %}
{% endif %} {% endif %}
<form method="post" action="{% url 'interview_create_remote' application_slug=application.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">

View File

@ -11,17 +11,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>
{% blocktrans %}Create Interview for {{ application.name }}{% endblocktrans %} 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">
{% blocktrans %}Select the type of interview you want to schedule for <strong>{{ application.name }}</strong> Select the type of interview you want to schedule for <strong>{{ application.name }}</strong>
for the position of <strong>{{ job.title }}</strong>.{% endblocktrans %} 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' application_slug=application.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>
@ -30,7 +30,7 @@
</div> </div>
</a> </a>
<a href="{% url 'interview_create_onsite' application_slug=application.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>
@ -42,7 +42,7 @@
</div> </div>
<div class="mt-4"> <div class="mt-4">
<a href="{% url 'applications_interview_view' slug=job.slug %}" <a href="#"
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>
{% trans "Back to application List" %} {% trans "Back to application List" %}

View File

@ -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 %}

View File

@ -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" %}

View File

@ -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>

View File

@ -227,7 +227,7 @@
<div class="vr" style="height: 28px;"></div> <div class="vr" style="height: 28px;"></div>
{# Form 2: Schedule Interviews #} {# Form 2: Schedule Interviews #}
<form hx-boost="true" hx-include="#application-form" action="{% url 'schedule_interviews' job.slug %}" method="get" class="action-group"> <form hx-boost="true" hx-include="#application-form" action="#" method="get" class="action-group">
<button type="submit" class="btn btn-main-action btn-sm"> <button type="submit" class="btn btn-main-action btn-sm">
<i class="fas fa-calendar-plus me-1"></i> {% trans "Schedule Interviews" %} <i class="fas fa-calendar-plus me-1"></i> {% trans "Schedule Interviews" %}
</button> </button>
@ -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,37 +434,19 @@
{% 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"
hx-get="{% url 'interview_create_type_selection' application_slug=application.slug %}" hx-get="{% url 'interview_create_type_selection' application.slug %}"
hx-select=".card-body" hx-select=".card-body"
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 %}