add ai interview questions
This commit is contained in:
parent
444eedc208
commit
85f895c891
@ -33,12 +33,12 @@ urlpatterns = [
|
||||
path('api/v1/templates/save/', views.save_form_template, name='save_form_template'),
|
||||
path('api/v1/templates/<slug:template_slug>/', views.load_form_template, name='load_form_template'),
|
||||
path('api/v1/templates/<slug:template_slug>/delete/', views.delete_form_template, name='delete_form_template'),
|
||||
path('api/v1/webhooks/zoom/', views.zoom_webhook_view, name='zoom_webhook_view'),
|
||||
|
||||
path('api/v1/sync/task/<str:task_id>/status/', views.sync_task_status, name='sync_task_status'),
|
||||
path('api/v1/sync/history/', views.sync_history, name='sync_history'),
|
||||
path('api/v1/sync/history/<slug:job_slug>/', views.sync_history, name='sync_history_job'),
|
||||
|
||||
path('api/v1/webhooks/zoom/', views.zoom_webhook_view, name='zoom_webhook_view'),
|
||||
]
|
||||
|
||||
urlpatterns += i18n_patterns(
|
||||
|
||||
@ -27,4 +27,5 @@ admin.site.register(IntegrationLog)
|
||||
admin.site.register(HiringAgency)
|
||||
admin.site.register(JobPosting)
|
||||
admin.site.register(Settings)
|
||||
admin.site.register(FormSubmission)
|
||||
admin.site.register(FormSubmission)
|
||||
# admin.site.register(InterviewQuestion)
|
||||
@ -0,0 +1,14 @@
|
||||
# Generated by Django 6.0 on 2025-12-16 08:41
|
||||
|
||||
from django.db import migrations
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0004_interviewquestion'),
|
||||
('recruitment', '0004_settings_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
]
|
||||
18
recruitment/migrations/0006_interview_join_url.py
Normal file
18
recruitment/migrations/0006_interview_join_url.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0 on 2025-12-16 09:52
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0005_merge_0004_interviewquestion_0004_settings_name'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='interview',
|
||||
name='join_url',
|
||||
field=models.URLField(blank=True, max_length=2048, null=True, verbose_name='Meeting/Location URL'),
|
||||
),
|
||||
]
|
||||
18
recruitment/migrations/0007_alter_interview_status.py
Normal file
18
recruitment/migrations/0007_alter_interview_status.py
Normal file
@ -0,0 +1,18 @@
|
||||
# Generated by Django 6.0 on 2025-12-16 10:11
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0006_interview_join_url'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='interview',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('waiting', 'Waiting'), ('started', 'Started'), ('updated', 'Updated'), ('ended', 'Ended'), ('deleted', 'Deleted')], db_index=True, default='waiting', max_length=20),
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,48 @@
|
||||
# Generated by Django 6.0 on 2025-12-16 10:39
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0007_alter_interview_status'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveIndex(
|
||||
model_name='interviewquestion',
|
||||
name='recruitment_schedul_b09a70_idx',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='interviewquestion',
|
||||
name='data',
|
||||
field=models.JSONField(blank=True, default=1, verbose_name='Question Data'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='interview',
|
||||
name='status',
|
||||
field=models.CharField(choices=[('waiting', 'Waiting'), ('started', 'Started'), ('updated', 'Updated'), ('deleted', 'Deleted'), ('ended', 'Ended')], db_index=True, default='waiting', max_length=20),
|
||||
),
|
||||
migrations.AddIndex(
|
||||
model_name='interviewquestion',
|
||||
index=models.Index(fields=['schedule'], name='recruitment_schedul_dbb350_idx'),
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='interviewquestion',
|
||||
name='category',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='interviewquestion',
|
||||
name='difficulty_level',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='interviewquestion',
|
||||
name='question_text',
|
||||
),
|
||||
migrations.RemoveField(
|
||||
model_name='interviewquestion',
|
||||
name='question_type',
|
||||
),
|
||||
]
|
||||
@ -0,0 +1,27 @@
|
||||
# Generated by Django 6.0 on 2025-12-16 10:48
|
||||
|
||||
import django.utils.timezone
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('recruitment', '0008_remove_interviewquestion_recruitment_schedul_b09a70_idx_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.RemoveIndex(
|
||||
model_name='interviewquestion',
|
||||
name='recruitment_schedul_dbb350_idx',
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='scheduledinterview',
|
||||
name='interview_questions',
|
||||
field=models.JSONField(blank=True, default={}, verbose_name='Question Data'),
|
||||
preserve_default=False,
|
||||
),
|
||||
migrations.DeleteModel(
|
||||
name='InterviewQuestion',
|
||||
),
|
||||
]
|
||||
@ -1120,8 +1120,9 @@ class Interview(Base):
|
||||
class Status(models.TextChoices):
|
||||
WAITING = "waiting", _("Waiting")
|
||||
STARTED = "started", _("Started")
|
||||
UPDATED = "updated", _("Updated")
|
||||
DELETED = "deleted", _("Deleted")
|
||||
ENDED = "ended", _("Ended")
|
||||
CANCELLED = "cancelled", _("Cancelled")
|
||||
|
||||
class InterviewResult(models.TextChoices):
|
||||
PASSED="passed",_("Passed")
|
||||
@ -1154,7 +1155,7 @@ class Interview(Base):
|
||||
blank=True,
|
||||
help_text=_("e.g., 'Zoom Topic: Software Interview' or 'Main Conference Room'"),
|
||||
)
|
||||
details_url = models.URLField(
|
||||
join_url = models.URLField(
|
||||
verbose_name=_("Meeting/Location URL"), max_length=2048, blank=True, null=True
|
||||
)
|
||||
timezone = models.CharField(
|
||||
@ -1351,6 +1352,10 @@ class ScheduledInterview(Base):
|
||||
choices=InterviewStatus.choices,
|
||||
default=InterviewStatus.SCHEDULED,
|
||||
)
|
||||
interview_questions = models.JSONField(
|
||||
verbose_name=_("Question Data"),
|
||||
blank=True
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return (
|
||||
@ -2591,58 +2596,6 @@ class Document(Base):
|
||||
return ""
|
||||
|
||||
|
||||
class InterviewQuestion(Base):
|
||||
"""Model to store AI-generated interview questions"""
|
||||
|
||||
class QuestionType(models.TextChoices):
|
||||
TECHNICAL = "technical", _("Technical")
|
||||
BEHAVIORAL = "behavioral", _("Behavioral")
|
||||
SITUATIONAL = "situational", _("Situational")
|
||||
|
||||
schedule = models.ForeignKey(
|
||||
'ScheduledInterview',
|
||||
on_delete=models.CASCADE,
|
||||
related_name="ai_questions",
|
||||
verbose_name=_("Interview Schedule")
|
||||
)
|
||||
question_text = models.TextField(
|
||||
verbose_name=_("Question Text")
|
||||
)
|
||||
question_type = models.CharField(
|
||||
max_length=20,
|
||||
choices=QuestionType.choices,
|
||||
default=QuestionType.TECHNICAL,
|
||||
verbose_name=_("Question Type")
|
||||
)
|
||||
difficulty_level = models.CharField(
|
||||
max_length=20,
|
||||
choices=[
|
||||
("easy", _("Easy")),
|
||||
("medium", _("Medium")),
|
||||
("hard", _("Hard")),
|
||||
],
|
||||
default="medium",
|
||||
verbose_name=_("Difficulty Level")
|
||||
)
|
||||
category = models.CharField(
|
||||
max_length=100,
|
||||
blank=True,
|
||||
verbose_name=_("Category")
|
||||
)
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Interview Question")
|
||||
verbose_name_plural = _("Interview Questions")
|
||||
ordering = ["created_at"]
|
||||
indexes = [
|
||||
models.Index(fields=["schedule", "question_type"]),
|
||||
]
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.get_question_type_display()} Question for {self.schedule}"
|
||||
|
||||
|
||||
class Settings(Base):
|
||||
"""Model to store key-value pair settings"""
|
||||
name = models.CharField(
|
||||
@ -2662,7 +2615,7 @@ class Settings(Base):
|
||||
verbose_name=_("Setting Value"),
|
||||
help_text=_("Value for the setting"),
|
||||
)
|
||||
|
||||
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Setting")
|
||||
|
||||
@ -732,7 +732,7 @@ def create_interview_and_meeting(schedule_id):
|
||||
|
||||
if result["status"] == "success":
|
||||
interview.meeting_id = result["meeting_details"]["meeting_id"]
|
||||
interview.details_url = result["meeting_details"]["join_url"]
|
||||
interview.join_url = result["meeting_details"]["join_url"]
|
||||
interview.host_email = result["meeting_details"]["host_email"]
|
||||
interview.password = result["meeting_details"]["password"]
|
||||
interview.zoom_gateway_response = result["zoom_gateway_response"]
|
||||
@ -757,32 +757,20 @@ def handle_zoom_webhook_event(payload):
|
||||
event_type = payload.get("event")
|
||||
object_data = payload["payload"]["object"]
|
||||
|
||||
# Zoom often uses a long 'id' for the scheduled meeting and sometimes a 'uuid'.
|
||||
# We rely on the unique 'id' that maps to your ZoomMeeting.meeting_id field.
|
||||
meeting_id_zoom = str(object_data.get("id"))
|
||||
if not meeting_id_zoom:
|
||||
meeting_id = str(object_data.get("id"))
|
||||
if not meeting_id:
|
||||
logger.warning(f"Webhook received without a valid Meeting ID: {event_type}")
|
||||
return False
|
||||
|
||||
try:
|
||||
# Use filter().first() to avoid exceptions if the meeting doesn't exist yet,
|
||||
# and to simplify the logic flow.
|
||||
meeting_instance = "" # TODO:update #ZoomMeetingDetails.objects.filter(meeting_id=meeting_id_zoom).first()
|
||||
print(meeting_instance)
|
||||
# --- 1. Creation and Update Events ---
|
||||
meeting_instance = Interview.objects.filter(meeting_id=meeting_id).first()
|
||||
if event_type == "meeting.updated":
|
||||
logger.info(f"Zoom meeting updated: {meeting_id}")
|
||||
if meeting_instance:
|
||||
# Update key fields from the webhook payload
|
||||
meeting_instance.topic = object_data.get(
|
||||
"topic", meeting_instance.topic
|
||||
)
|
||||
|
||||
# Check for and update status and time details
|
||||
# if event_type == 'meeting.created':
|
||||
# meeting_instance.status = 'scheduled'
|
||||
# elif event_type == 'meeting.updated':
|
||||
# Only update time fields if they are in the payload
|
||||
print(object_data)
|
||||
meeting_instance.start_time = object_data.get(
|
||||
"start_time", meeting_instance.start_time
|
||||
)
|
||||
@ -792,7 +780,6 @@ def handle_zoom_webhook_event(payload):
|
||||
meeting_instance.timezone = object_data.get(
|
||||
"timezone", meeting_instance.timezone
|
||||
)
|
||||
|
||||
meeting_instance.status = object_data.get(
|
||||
"status", meeting_instance.status
|
||||
)
|
||||
@ -807,31 +794,19 @@ def handle_zoom_webhook_event(payload):
|
||||
]
|
||||
)
|
||||
|
||||
# --- 2. Status Change Events (Start/End) ---
|
||||
elif event_type == "meeting.started":
|
||||
if meeting_instance:
|
||||
meeting_instance.status = "started"
|
||||
meeting_instance.save(update_fields=["status"])
|
||||
|
||||
elif event_type == "meeting.ended":
|
||||
if meeting_instance:
|
||||
meeting_instance.status = "ended"
|
||||
meeting_instance.save(update_fields=["status"])
|
||||
|
||||
# --- 3. Deletion Event (User Action) ---
|
||||
elif event_type == "meeting.deleted":
|
||||
elif event_type in ["meeting.started","meeting.ended","meeting.deleted"]:
|
||||
if meeting_instance:
|
||||
try:
|
||||
meeting_instance.status = "cancelled"
|
||||
meeting_instance.status = event_type.split(".")[-1]
|
||||
meeting_instance.save(update_fields=["status"])
|
||||
except Exception as e:
|
||||
logger.error(f"Failed to mark Zoom meeting as cancelled: {e}")
|
||||
|
||||
return True
|
||||
|
||||
except Exception as e:
|
||||
logger.error(
|
||||
f"Failed to process Zoom webhook for {event_type} (ID: {meeting_id_zoom}): {e}",
|
||||
f"Failed to process Zoom webhook for {event_type} (ID: {meeting_id}): {e}",
|
||||
exc_info=True,
|
||||
)
|
||||
return False
|
||||
@ -1578,7 +1553,7 @@ def generate_interview_questions(schedule_id: int) -> dict:
|
||||
Returns:
|
||||
dict: Result containing status and generated questions or error message
|
||||
"""
|
||||
from .models import ScheduledInterview, InterviewQuestion
|
||||
from .models import ScheduledInterview
|
||||
|
||||
try:
|
||||
# Get the scheduled interview with related data
|
||||
@ -1619,7 +1594,7 @@ def generate_interview_questions(schedule_id: int) -> dict:
|
||||
{candidate_resume_text}
|
||||
|
||||
TASK:
|
||||
Generate 8-10 interview questions that are:
|
||||
Generate 8-10 interview questions in english and arabic that are:
|
||||
1. Technical questions related to the job requirements
|
||||
2. Behavioral questions to assess soft skills and cultural fit
|
||||
3. Situational questions to evaluate problem-solving abilities
|
||||
@ -1634,17 +1609,27 @@ def generate_interview_questions(schedule_id: int) -> dict:
|
||||
OUTPUT FORMAT:
|
||||
Return a JSON object with the following structure:
|
||||
{{
|
||||
"questions": [
|
||||
"questions": {{
|
||||
"en":[
|
||||
{{
|
||||
"question_text": "The actual question text",
|
||||
"question_type": "technical|behavioral|situational",
|
||||
"difficulty_level": "easy|medium|hard",
|
||||
"category": "Category name"
|
||||
}}
|
||||
]
|
||||
],
|
||||
"ar":[
|
||||
{{
|
||||
"question_text": "The actual question text",
|
||||
"question_type": "technical|behavioral|situational",
|
||||
"difficulty_level": "easy|medium|hard",
|
||||
"category": "Category name"
|
||||
}}
|
||||
]}}
|
||||
}}
|
||||
|
||||
Make questions specific to the job requirements and candidate background. Avoid generic questions.
|
||||
Output only valid JSON — no markdown, no extra text.
|
||||
"""
|
||||
|
||||
# Call AI handler
|
||||
@ -1664,33 +1649,14 @@ def generate_interview_questions(schedule_id: int) -> dict:
|
||||
if not questions:
|
||||
return {"status": "error", "message": "No questions generated"}
|
||||
|
||||
# Clear existing questions for this schedule
|
||||
InterviewQuestion.objects.filter(schedule=schedule).delete()
|
||||
schedule.interview_questions.update(questions)
|
||||
schedule.save(update_fields=["interview_questions"])
|
||||
|
||||
# Save generated questions to database
|
||||
created_questions = []
|
||||
for q_data in questions:
|
||||
question = InterviewQuestion.objects.create(
|
||||
schedule=schedule,
|
||||
question_text=q_data.get("question_text", ""),
|
||||
question_type=q_data.get("question_type", "technical"),
|
||||
difficulty_level=q_data.get("difficulty_level", "medium"),
|
||||
category=q_data.get("category", "General")
|
||||
)
|
||||
created_questions.append({
|
||||
"id": question.id,
|
||||
"text": question.question_text,
|
||||
"type": question.question_type,
|
||||
"difficulty": question.difficulty_level,
|
||||
"category": question.category
|
||||
})
|
||||
|
||||
logger.info(f"Successfully generated {len(created_questions)} questions for schedule {schedule_id}")
|
||||
logger.info(f"Successfully generated questions for schedule {schedule_id}")
|
||||
|
||||
return {
|
||||
"status": "success",
|
||||
"questions": created_questions,
|
||||
"message": f"Generated {len(created_questions)} interview questions"
|
||||
"message": f"Generated interview questions"
|
||||
}
|
||||
|
||||
except ScheduledInterview.DoesNotExist:
|
||||
|
||||
@ -870,7 +870,7 @@ def update_meeting(instance, updated_data):
|
||||
|
||||
instance.topic = zoom_details.get("topic", instance.topic)
|
||||
instance.duration = zoom_details.get("duration", instance.duration)
|
||||
instance.details_url = zoom_details.get("join_url", instance.details_url)
|
||||
instance.join_url = zoom_details.get("join_url", instance.join_url)
|
||||
instance.password = zoom_details.get("password", instance.password)
|
||||
instance.status = zoom_details.get("status")
|
||||
|
||||
|
||||
@ -1605,7 +1605,6 @@ def _handle_preview_submission(request, slug, job):
|
||||
"""
|
||||
SESSION_DATA_KEY = "interview_schedule_data"
|
||||
form = BulkInterviewTemplateForm(slug, request.POST)
|
||||
# break_formset = BreakTimeFormSet(request.POST,prefix='breaktime')
|
||||
|
||||
if form.is_valid():
|
||||
# Get the form data
|
||||
@ -1622,9 +1621,6 @@ def _handle_preview_submission(request, slug, job):
|
||||
schedule_interview_type = form.cleaned_data["schedule_interview_type"]
|
||||
physical_address = form.cleaned_data["physical_address"]
|
||||
|
||||
# Create a temporary schedule object (not saved to DB)
|
||||
# if start_date == datetime.now().date():
|
||||
# start_time = (datetime.now() + timedelta(minutes=30)).time()
|
||||
temp_schedule = BulkInterviewTemplate(
|
||||
job=job,
|
||||
start_date=start_date,
|
||||
@ -2110,7 +2106,6 @@ def reschedule_meeting_for_application(request, slug):
|
||||
|
||||
if request.method == "POST":
|
||||
if interview.location_type == "Remote":
|
||||
|
||||
form = ScheduledInterviewForm(request.POST)
|
||||
else:
|
||||
form = OnsiteScheduleInterviewUpdateForm(request.POST)
|
||||
@ -2123,7 +2118,7 @@ def reschedule_meeting_for_application(request, slug):
|
||||
if interview.location_type == "Remote":
|
||||
updated_data = {
|
||||
"topic": topic,
|
||||
"start_time": start_time.isoformat() + "Z",
|
||||
"start_time": start_time.strftime("%Y-%m-%dT%H:%M:%S"),
|
||||
"duration": duration,
|
||||
}
|
||||
result = update_meeting(schedule.interview, updated_data)
|
||||
@ -2504,13 +2499,15 @@ def account_toggle_status(request, pk):
|
||||
|
||||
@csrf_exempt
|
||||
def zoom_webhook_view(request):
|
||||
from .utils import get_setting
|
||||
api_key = request.headers.get("X-Zoom-API-KEY")
|
||||
if api_key != settings.ZOOM_WEBHOOK_API_KEY:
|
||||
if api_key != get_setting("ZOOM_WEBHOOK_API_KEY"):
|
||||
return HttpResponse(status=405)
|
||||
|
||||
if request.method == "POST":
|
||||
try:
|
||||
payload = json.loads(request.body)
|
||||
logger.info(payload)
|
||||
async_task("recruitment.tasks.handle_zoom_webhook_event", payload)
|
||||
return HttpResponse(status=200)
|
||||
except Exception:
|
||||
@ -4775,7 +4772,6 @@ def interview_list(request):
|
||||
def generate_ai_questions(request, slug):
|
||||
"""Generate AI-powered interview questions for a scheduled interview"""
|
||||
from django_q.tasks import async_task
|
||||
from .models import InterviewQuestion
|
||||
|
||||
schedule = get_object_or_404(ScheduledInterview, slug=slug)
|
||||
|
||||
@ -4783,40 +4779,41 @@ def generate_ai_questions(request, slug):
|
||||
# Queue the AI question generation task
|
||||
task_id = async_task(
|
||||
"recruitment.tasks.generate_interview_questions",
|
||||
schedule.id
|
||||
schedule.id,
|
||||
sync=True
|
||||
)
|
||||
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
return JsonResponse({
|
||||
"status": "success",
|
||||
"message": "AI question generation started in background",
|
||||
"task_id": task_id
|
||||
})
|
||||
else:
|
||||
messages.success(
|
||||
request,
|
||||
"AI question generation started. Questions will appear shortly."
|
||||
)
|
||||
return redirect("interview_detail", slug=slug)
|
||||
# if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
# return JsonResponse({
|
||||
# "status": "success",
|
||||
# "message": "AI question generation started in background",
|
||||
# "task_id": task_id
|
||||
# })
|
||||
# else:
|
||||
# messages.success(
|
||||
# request,
|
||||
# "AI question generation started. Questions will appear shortly."
|
||||
# )
|
||||
# return redirect("interview_detail", slug=slug)
|
||||
|
||||
# For GET requests, return existing questions if any
|
||||
questions = schedule.ai_questions.all().order_by("created_at")
|
||||
# # For GET requests, return existing questions if any
|
||||
# questions = schedule.ai_questions.all().order_by("created_at")
|
||||
|
||||
if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
return JsonResponse({
|
||||
"status": "success",
|
||||
"questions": [
|
||||
{
|
||||
"id": q.id,
|
||||
"text": q.question_text,
|
||||
"type": q.question_type,
|
||||
"difficulty": q.difficulty_level,
|
||||
"category": q.category,
|
||||
"created_at": q.created_at.isoformat()
|
||||
}
|
||||
for q in questions
|
||||
]
|
||||
})
|
||||
# if request.headers.get("X-Requested-With") == "XMLHttpRequest":
|
||||
# return JsonResponse({
|
||||
# "status": "success",
|
||||
# "questions": [
|
||||
# {
|
||||
# "id": q.id,
|
||||
# "text": q.question_text,
|
||||
# "type": q.question_type,
|
||||
# "difficulty": q.difficulty_level,
|
||||
# "category": q.category,
|
||||
# "created_at": q.created_at.isoformat()
|
||||
# }
|
||||
# for q in questions
|
||||
# ]
|
||||
# })
|
||||
|
||||
return redirect("interview_detail", slug=slug)
|
||||
|
||||
@ -4829,24 +4826,20 @@ def interview_detail(request, slug):
|
||||
ScheduledInterviewUpdateStatusForm,
|
||||
OnsiteScheduleInterviewUpdateForm,
|
||||
)
|
||||
|
||||
|
||||
|
||||
schedule = get_object_or_404(ScheduledInterview, slug=slug)
|
||||
interview = schedule.interview
|
||||
interview_result_form=InterviewResultForm(instance=interview)
|
||||
application = schedule.application
|
||||
job = schedule.job
|
||||
print(interview.location_type)
|
||||
if interview.location_type == "Remote":
|
||||
reschedule_form = ScheduledInterviewForm()
|
||||
else:
|
||||
reschedule_form = OnsiteScheduleInterviewUpdateForm()
|
||||
reschedule_form.initial["physical_address"] = interview.physical_address
|
||||
reschedule_form.initial["room_number"] = interview.room_number
|
||||
reschedule_form.initial["topic"] = interview.topic
|
||||
reschedule_form.initial["start_time"] = interview.start_time
|
||||
reschedule_form.initial["duration"] = interview.duration
|
||||
reschedule_form.initial["topic"] = interview.topic
|
||||
reschedule_form.initial["start_time"] = interview.start_time
|
||||
reschedule_form.initial["duration"] = interview.duration
|
||||
|
||||
meeting = interview
|
||||
interview_email_form = InterviewEmailForm(job, application, schedule)
|
||||
@ -6601,7 +6594,10 @@ def compose_application_email(request, slug):
|
||||
if not email_addresses:
|
||||
messages.error(request, "No email selected")
|
||||
referer = request.META.get("HTTP_REFERER")
|
||||
|
||||
if "HX-Request" in request.headers:
|
||||
response = HttpResponse()
|
||||
response.headers["HX-Refresh"] = "true"
|
||||
return response
|
||||
if referer:
|
||||
# Redirect back to the referring page
|
||||
return redirect(referer)
|
||||
@ -6628,21 +6624,21 @@ def compose_application_email(request, slug):
|
||||
|
||||
},
|
||||
)
|
||||
if "HX-Request" in request.headers:
|
||||
response = HttpResponse()
|
||||
response.headers["HX-Refresh"] = "true"
|
||||
return response
|
||||
return redirect(request.path)
|
||||
|
||||
|
||||
else:
|
||||
# Form validation errors
|
||||
messages.error(request, "Please correct the errors below.")
|
||||
|
||||
# For HTMX requests, return error response
|
||||
if "HX-Request" in request.headers:
|
||||
return JsonResponse(
|
||||
{
|
||||
"success": False,
|
||||
"error": "Please correct the form errors and try again.",
|
||||
}
|
||||
)
|
||||
response = HttpResponse()
|
||||
response.headers["HX-Refresh"] = "true"
|
||||
return response
|
||||
|
||||
return render(
|
||||
request,
|
||||
|
||||
@ -459,7 +459,7 @@
|
||||
});
|
||||
}
|
||||
|
||||
//form_loader();
|
||||
form_loader();
|
||||
|
||||
try{
|
||||
document.body.addEventListener('htmx:afterRequest', function(evt) {
|
||||
|
||||
@ -305,11 +305,12 @@
|
||||
text-align: center;
|
||||
padding: 2rem;
|
||||
}
|
||||
.htmx-indicator {
|
||||
display: none;
|
||||
.htmx-indicator {
|
||||
opacity: 0;
|
||||
transition: opacity 200ms ease-in;
|
||||
}
|
||||
.htmx-indicator.htmx-request {
|
||||
display: block;
|
||||
.htmx-request .htmx-indicator {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
/* Responsive adjustments */
|
||||
@ -459,7 +460,7 @@
|
||||
<span class="detail-label">{% trans "Status:" %}</span>
|
||||
<span class="detail-value">
|
||||
<span class="badge bg-primary-theme">
|
||||
{{ schedule.status }}</span>
|
||||
{{ interview.status }}</span>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
@ -481,9 +482,9 @@
|
||||
<span class="detail-label">{% trans "Password:" %}</span>
|
||||
<span class="detail-value">{{ interview.password }}</span>
|
||||
</div>
|
||||
{% if interview.details_url %}
|
||||
{% if interview.join_url %}
|
||||
<div class="mt-3">
|
||||
<a href="{{ interview.zoommeetingdetails.details_url }}"
|
||||
<a href="{{ interview.join_url }}"
|
||||
target="_blank"
|
||||
class="btn btn-main-action btn-sm w-100">
|
||||
<i class="fas fa-video me-1"></i> {% trans "Join Meeting" %}
|
||||
@ -517,28 +518,84 @@
|
||||
<h5 class="mb-0" style="color: var(--kaauh-teal-dark); font-weight: 600;">
|
||||
<i class="fas fa-brain me-2"></i> {% trans "AI Generated Questions" %}
|
||||
</h5>
|
||||
<div class="d-flex gap-2">
|
||||
<button type="button"
|
||||
class="btn btn-main-action btn-sm"
|
||||
id="generateQuestionsBtn"
|
||||
hx-post="{% url 'generate_ai_questions' schedule.slug %}"
|
||||
hx-target="#aiQuestionsContainer"
|
||||
hx-indicator="#generateQuestionsSpinner">
|
||||
<i class="fas fa-magic me-1"></i> {% trans "Generate Questions" %}
|
||||
</button>
|
||||
<button type="button"
|
||||
class="btn btn-outline-secondary btn-sm"
|
||||
id="refreshQuestionsBtn"
|
||||
hx-get="{% url 'generate_ai_questions' schedule.slug %}"
|
||||
hx-target="#aiQuestionsContainer"
|
||||
hx-indicator="#refreshQuestionsSpinner">
|
||||
<i class="fas fa-sync-alt me-1"></i> {% trans "Refresh" %}
|
||||
</button>
|
||||
<div class="d-flex gap-2">
|
||||
<form action="{% url 'generate_ai_questions' schedule.slug %}" method="post">
|
||||
{% csrf_token %}
|
||||
<button type="submit" class="btn btn-main-action btn-sm">
|
||||
<span id="button-text-content">
|
||||
<i class="fas fa-magic me-1"></i> {% trans "Generate Interview Questions" %}
|
||||
</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="accordion" id="aiQuestionsAccordion">
|
||||
<div class="accordion-item">
|
||||
<h2 class="accordion-header" id="aiQuestionsHeading">
|
||||
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#aiQuestionsCollapse" aria-expanded="false" aria-controls="aiQuestionsCollapse">
|
||||
<span class="accordion-icon"></span>
|
||||
<span class="accordion-header-text">{% trans "Interview Questions" %}</span>
|
||||
</button>
|
||||
</h2>
|
||||
<div id="aiQuestionsCollapse" class="accordion-collapse collapse show" aria-labelledby="aiQuestionsHeading">
|
||||
<div class="accordion-body table-view">
|
||||
<div class="table-responsive d-none d-lg-block">
|
||||
<table class="table table-hover align-middle mb-0 ">
|
||||
<thead>
|
||||
<tr>
|
||||
<th scope="col">{% trans "Question" %}</th>
|
||||
<th scope="col">{% trans "Type" %}</th>
|
||||
<th scope="col">{% trans "Difficulty" %}</th>
|
||||
<th scope="col">{% trans "Category" %}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% if LANGUAGE_CODE == "ar" %}
|
||||
{% for question in schedule.interview_questions.ar %}
|
||||
<tr>
|
||||
<td class="text-break">
|
||||
<span class="d-block" style="font-size: 0.8rem; color: #757575">{{ question.question_text }}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge rounded-pill bg-primary-theme">{{ question.question_type|capfirst }}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge rounded-pill bg-primary-theme">{{ question.difficulty_level|capfirst }}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge rounded-pill bg-primary-theme">{{ question.category|capfirst }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
{% for question in schedule.interview_questions.en %}
|
||||
<tr>
|
||||
<td class="text-break">
|
||||
<span class="d-block" style="font-size: 0.8rem; color: #757575">{{ question.question_text }}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge rounded-pill bg-primary-theme">{{ question.question_type|capfirst }}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge rounded-pill bg-primary-theme">{{ question.difficulty_level|capfirst }}</span>
|
||||
</td>
|
||||
<td class="text-center">
|
||||
<span class="badge rounded-pill bg-primary-theme">{{ question.category|capfirst }}</span>
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
{% endif %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Loading Spinners -->
|
||||
<div class="text-center py-3" id="generateQuestionsSpinner" class="htmx-indicator d-none">
|
||||
{% comment %} <div class="text-center py-3" id="generateQuestionsSpinner" class="htmx-indicator d-none">
|
||||
<div class="spinner-border text-primary" role="status">
|
||||
<span class="visually-hidden">{% trans "Generating questions..." %}</span>
|
||||
</div>
|
||||
@ -558,7 +615,7 @@
|
||||
<i class="fas fa-brain fa-2x mb-3"></i>
|
||||
<p class="mb-0">{% trans "No AI questions generated yet. Click 'Generate Questions' to create personalized interview questions based on the candidate's profile and job requirements." %}</p>
|
||||
</div>
|
||||
</div>
|
||||
</div> {% endcomment %}
|
||||
</div>
|
||||
|
||||
<div class="kaauh-card shadow-sm p-4">
|
||||
@ -682,13 +739,13 @@
|
||||
|
||||
<div class="action-buttons">
|
||||
{% if schedule.status != 'cancelled' and schedule.status != 'completed' %}
|
||||
<button type="button" class="btn btn-main-action btn-sm"
|
||||
<button type="button" class="btn btn-main-action"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#rescheduleModal">
|
||||
<i class="fas fa-redo-alt me-1"></i> {% trans "Reschedule" %}
|
||||
</button>
|
||||
|
||||
<button type="button" class="btn btn-outline-danger btn-sm"
|
||||
<button type="button" class="btn btn-outline-danger"
|
||||
data-bs-toggle="modal"
|
||||
data-bs-target="#cancelModal">
|
||||
<i class="fas fa-times me-1"></i> {% trans "Cancel" %}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user