fix the bulk remote meetings creation

This commit is contained in:
ismail 2025-12-16 11:12:09 +03:00
parent 3ae6d66dbd
commit 444eedc208
10 changed files with 225 additions and 150 deletions

6
.env
View File

@ -1,3 +1,3 @@
DB_NAME=haikal_db DB_NAME=norahuniversity
DB_USER=faheed DB_USER=norahuniversity
DB_PASSWORD=Faheed@215 DB_PASSWORD=norahuniversity

View File

@ -690,20 +690,40 @@ class BulkInterviewTemplateForm(forms.ModelForm):
self.fields["applications"].queryset.first().job.title self.fields["applications"].queryset.first().job.title
) )
self.fields["start_date"].initial = timezone.now().date() self.fields["start_date"].initial = timezone.now().date()
working_days_initial = [0, 1, 2, 3, 6] # Monday to Friday working_days_initial = [0, 1, 2, 3, 6]
self.fields["working_days"].initial = working_days_initial self.fields["working_days"].initial = working_days_initial
self.fields["start_time"].initial = "08:00" self.fields["start_time"].initial = "08:00"
self.fields["end_time"].initial = "14:00" self.fields["end_time"].initial = "14:00"
self.fields["interview_duration"].initial = 30 self.fields["interview_duration"].initial = 30
self.fields["buffer_time"].initial = 10 self.fields["buffer_time"].initial = 10
self.fields["break_start_time"].initial = "11:30" self.fields["break_start_time"].initial = "11:30"
self.fields["break_end_time"].initial = "12:00" self.fields["break_end_time"].initial = "12:30"
self.fields["physical_address"].initial = "Airport Road, King Khalid International Airport, Riyadh 11564, Saudi Arabia" self.fields["physical_address"].initial = "Airport Road, King Khalid International Airport, Riyadh 11564, Saudi Arabia"
def clean_working_days(self): def clean_working_days(self):
working_days = self.cleaned_data.get("working_days") working_days = self.cleaned_data.get("working_days")
return [int(day) for day in working_days] return [int(day) for day in working_days]
def clean_start_date(self):
start_date = self.cleaned_data.get("start_date")
if start_date and start_date <= timezone.now().date():
raise forms.ValidationError(_("Start date must be in the future"))
return start_date
def clean_end_date(self):
start_date = self.cleaned_data.get("start_date")
end_date = self.cleaned_data.get("end_date")
if end_date and start_date and end_date < start_date:
raise forms.ValidationError(_("End date must be after start date"))
return end_date
def clean_end_time(self):
start_time = self.cleaned_data.get("start_time")
end_time = self.cleaned_data.get("end_time")
if end_time and start_time and end_time < start_time:
raise forms.ValidationError(_("End time must be after start time"))
return end_time
class InterviewCancelForm(forms.ModelForm): class InterviewCancelForm(forms.ModelForm):
class Meta: class Meta:
model = ScheduledInterview model = ScheduledInterview
@ -1536,7 +1556,7 @@ class MessageForm(forms.ModelForm):
print(person) print(person)
applications=person.applications.all() applications=person.applications.all()
print(applications) print(applications)
self.fields["job"].queryset = JobPosting.objects.filter( self.fields["job"].queryset = JobPosting.objects.filter(
applications__in=applications, applications__in=applications,
).distinct().order_by("-created_at") ).distinct().order_by("-created_at")
@ -2167,7 +2187,7 @@ KAAUH Hiring Team
class InterviewResultForm(forms.ModelForm): class InterviewResultForm(forms.ModelForm):
class Meta: class Meta:
model = Interview model = Interview
fields = ['interview_result', 'result_comments'] fields = ['interview_result', 'result_comments']
widgets = { widgets = {
'interview_result': forms.Select(attrs={ 'interview_result': forms.Select(attrs={

View File

@ -0,0 +1,35 @@
# Generated by Django 6.0 on 2025-12-15 13:59
import django.db.models.deletion
import django_extensions.db.fields
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('recruitment', '0003_interview_interview_result_interview_result_comments'),
]
operations = [
migrations.CreateModel(
name='InterviewQuestion',
fields=[
('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('created_at', models.DateTimeField(auto_now_add=True, verbose_name='Created at')),
('updated_at', models.DateTimeField(auto_now=True, verbose_name='Updated at')),
('slug', django_extensions.db.fields.RandomCharField(blank=True, editable=False, length=8, unique=True, verbose_name='Slug')),
('question_text', models.TextField(verbose_name='Question Text')),
('question_type', models.CharField(choices=[('technical', 'Technical'), ('behavioral', 'Behavioral'), ('situational', 'Situational')], default='technical', max_length=20, verbose_name='Question Type')),
('difficulty_level', models.CharField(choices=[('easy', 'Easy'), ('medium', 'Medium'), ('hard', 'Hard')], default='medium', max_length=20, verbose_name='Difficulty Level')),
('category', models.CharField(blank=True, max_length=100, verbose_name='Category')),
('schedule', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='ai_questions', to='recruitment.scheduledinterview', verbose_name='Interview Schedule')),
],
options={
'verbose_name': 'Interview Question',
'verbose_name_plural': 'Interview Questions',
'ordering': ['created_at'],
'indexes': [models.Index(fields=['schedule', 'question_type'], name='recruitment_schedul_b09a70_idx')],
},
),
]

View File

@ -1122,7 +1122,7 @@ class Interview(Base):
STARTED = "started", _("Started") STARTED = "started", _("Started")
ENDED = "ended", _("Ended") ENDED = "ended", _("Ended")
CANCELLED = "cancelled", _("Cancelled") CANCELLED = "cancelled", _("Cancelled")
class InterviewResult(models.TextChoices): class InterviewResult(models.TextChoices):
PASSED="passed",_("Passed") PASSED="passed",_("Passed")
FAILED="failed",_("Failed") FAILED="failed",_("Failed")
@ -2591,7 +2591,7 @@ class Document(Base):
return "" return ""
class InterviewQuestion(models.Model): class InterviewQuestion(Base):
"""Model to store AI-generated interview questions""" """Model to store AI-generated interview questions"""
class QuestionType(models.TextChoices): class QuestionType(models.TextChoices):
@ -2629,10 +2629,7 @@ class InterviewQuestion(models.Model):
blank=True, blank=True,
verbose_name=_("Category") verbose_name=_("Category")
) )
created_at = models.DateTimeField(
auto_now_add=True,
verbose_name=_("Created At")
)
class Meta: class Meta:
verbose_name = _("Interview Question") verbose_name = _("Interview Question")
@ -2640,7 +2637,6 @@ class InterviewQuestion(models.Model):
ordering = ["created_at"] ordering = ["created_at"]
indexes = [ indexes = [
models.Index(fields=["schedule", "question_type"]), models.Index(fields=["schedule", "question_type"]),
models.Index(fields=["created_at"]),
] ]
def __str__(self): def __str__(self):

View File

@ -722,22 +722,26 @@ def create_interview_and_meeting(schedule_id):
try: try:
schedule = ScheduledInterview.objects.get(pk=schedule_id) schedule = ScheduledInterview.objects.get(pk=schedule_id)
interview = schedule.interview interview = schedule.interview
result = create_zoom_meeting(
interview.topic, interview.start_time, interview.duration logger.info(f"Processing schedule {schedule_id} with interview {interview.id}")
) logger.info(f"Interview topic: {interview.topic}")
logger.info(f"Interview start_time: {interview.start_time}")
logger.info(f"Interview duration: {interview.duration}")
result = create_zoom_meeting(interview.topic, interview.start_time, interview.duration)
if result["status"] == "success": if result["status"] == "success":
interview.meeting_id = result["meeting_details"]["meeting_id"] interview.meeting_id = result["meeting_details"]["meeting_id"]
interview.details_url = result["meeting_details"]["join_url"] interview.details_url = result["meeting_details"]["join_url"]
interview.zoom_gateway_response = result["zoom_gateway_response"]
interview.host_email = result["meeting_details"]["host_email"] interview.host_email = result["meeting_details"]["host_email"]
interview.password = result["meeting_details"]["password"] interview.password = result["meeting_details"]["password"]
interview.zoom_gateway_response = result["zoom_gateway_response"]
interview.save() interview.save()
logger.info(f"Successfully scheduled interview for {Application.name}") logger.info(f"Successfully scheduled interview for {schedule.application.name}")
return True return True
else: else:
# Handle Zoom API failure (e.g., log it or notify administrator) # Handle Zoom API failure (e.g., log it or notify administrator)
logger.error(f"Zoom API failed for {Application.name}: {result['message']}") logger.error(f"Zoom API failed for {schedule.application.name}: {result['message']}")
return False # Task failed return False # Task failed
except Exception as e: except Exception as e:
@ -745,7 +749,6 @@ def create_interview_and_meeting(schedule_id):
logger.error(f"Critical error scheduling interview: {e}") logger.error(f"Critical error scheduling interview: {e}")
return False # Task failed return False # Task failed
def handle_zoom_webhook_event(payload): def handle_zoom_webhook_event(payload):
""" """
Background task to process a Zoom webhook event and update the local ZoomMeeting status. Background task to process a Zoom webhook event and update the local ZoomMeeting status.
@ -1701,33 +1704,33 @@ def generate_interview_questions(schedule_id: int) -> dict:
return {"status": "error", "message": error_msg} return {"status": "error", "message": error_msg}
def send_single_email_task( # def send_single_email_task(
recipient_emails, # recipient_emails,
subject: str, # subject: str,
template_name: str, # template_name: str,
context: dict, # context: dict,
) -> str: # ) -> str:
""" # """
Django-Q task to send a bulk email asynchronously. # Django-Q task to send a bulk email asynchronously.
""" # """
from .services.email_service import EmailService # from .services.email_service import EmailService
# if not recipient_emails: # # if not recipient_emails:
# return json.dumps({"status": "error", "message": "No recipients provided."}) # # return json.dumps({"status": "error", "message": "No recipients provided."})
# service = EmailService() # # service = EmailService()
# # Execute the bulk sending method # # # Execute the bulk sending method
# processed_count = service.send_bulk_email( # # processed_count = service.send_bulk_email(
# recipient_emails=recipient_emails, # # recipient_emails=recipient_emails,
# subject=subject, # # subject=subject,
# template_name=template_name, # # template_name=template_name,
# context=context, # # context=context,
# ) # # )
# The return value is stored in the result object for monitoring # # The return value is stored in the result object for monitoring
return json.dumps({ # return json.dumps({
"status": "success", # "status": "success",
"count": processed_count, # "count": processed_count,
"message": f"Attempted to send email to {len(recipient_emails)} recipients. Service reported processing {processed_count}." # "message": f"Attempted to send email to {len(recipient_emails)} recipients. Service reported processing {processed_count}."
}) # })

View File

@ -4,12 +4,9 @@ Utility functions for recruitment app
from recruitment import models from recruitment import models
from django.conf import settings from django.conf import settings
from datetime import datetime, timedelta, time, date from datetime import datetime, timedelta
from django.utils import timezone from django.utils import timezone
from .models import ScheduledInterview from .models import ScheduledInterview
from django.template.loader import render_to_string
from django.core.mail import send_mail
import random
import os import os
import json import json
import logging import logging
@ -417,12 +414,15 @@ def create_zoom_meeting(topic, start_time, duration):
try: try:
access_token = get_access_token() access_token = get_access_token()
zoom_start_time = start_time.strftime("%Y-%m-%dT%H:%M:%S")
logger.info(zoom_start_time)
meeting_details = { meeting_details = {
"topic": topic, "topic": topic,
"type": 2, "type": 2,
"start_time": start_time.isoformat() + "Z", "start_time": zoom_start_time,
"duration": duration, "duration": duration,
"timezone": "UTC", "timezone": "Asia/Riyadh",
"settings": { "settings": {
"host_video": True, "host_video": True,
"participant_video": True, "participant_video": True,
@ -440,7 +440,7 @@ def create_zoom_meeting(topic, start_time, duration):
"Content-Type": "application/json", "Content-Type": "application/json",
} }
ZOOM_MEETING_URL = get_setting("ZOOM_MEETING_URL") ZOOM_MEETING_URL = get_setting("ZOOM_MEETING_URL")
print(ZOOM_MEETING_URL)
response = requests.post( response = requests.post(
ZOOM_MEETING_URL, headers=headers, json=meeting_details ZOOM_MEETING_URL, headers=headers, json=meeting_details
) )
@ -448,6 +448,7 @@ def create_zoom_meeting(topic, start_time, duration):
# Check response status # Check response status
if response.status_code == 201: if response.status_code == 201:
meeting_data = response.json() meeting_data = response.json()
logger.info(meeting_data)
return { return {
"status": "success", "status": "success",
"message": "Meeting created successfully.", "message": "Meeting created successfully.",

View File

@ -1623,6 +1623,8 @@ def _handle_preview_submission(request, slug, job):
physical_address = form.cleaned_data["physical_address"] physical_address = form.cleaned_data["physical_address"]
# Create a temporary schedule object (not saved to DB) # 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( temp_schedule = BulkInterviewTemplate(
job=job, job=job,
start_date=start_date, start_date=start_date,
@ -1640,7 +1642,6 @@ def _handle_preview_submission(request, slug, job):
# Get available slots (temp_breaks logic moved into get_available_time_slots if needed) # Get available slots (temp_breaks logic moved into get_available_time_slots if needed)
available_slots = get_available_time_slots(temp_schedule) available_slots = get_available_time_slots(temp_schedule)
if len(available_slots) < len(applications): if len(available_slots) < len(applications):
messages.error( messages.error(
request, request,
@ -1760,76 +1761,46 @@ def _handle_confirm_schedule(request, slug, job):
schedule.applications.set(applications) schedule.applications.set(applications)
available_slots = get_available_time_slots(schedule) available_slots = get_available_time_slots(schedule)
if schedule_data.get("schedule_interview_type") == "Remote": for i, application in enumerate(applications):
queued_count = 0 if i >= len(available_slots):
for i, application in enumerate(applications): continue
if i < len(available_slots):
slot = available_slots[i]
# schedule=ScheduledInterview.objects.create(application=application,job=job)
async_task(
"recruitment.tasks.create_interview_and_meeting",
application.pk,
job.pk,
schedule.pk,
slot["date"],
slot["time"],
schedule.interview_duration,
)
queued_count += 1
messages.success( slot = available_slots[i]
request,
f"Schedule successfully created. Queued {queued_count} interviews to be booked asynchronously. Check back in a moment!", # start_dt = datetime.combine(slot["date"], slot["time"])
start_time = timezone.make_aware(datetime.combine(slot["date"], slot["time"]))
logger.info(f"Creating interview for {application.person.full_name} at {start_time}")
interview = Interview.objects.create(
topic=schedule.topic,
start_time=start_time,
duration=schedule.interview_duration,
location_type="Onsite",
physical_address=schedule.physical_address,
) )
if SESSION_DATA_KEY in request.session: scheduled = ScheduledInterview.objects.create(
del request.session[SESSION_DATA_KEY] application=application,
if SESSION_ID_KEY in request.session: job=job,
del request.session[SESSION_ID_KEY] schedule=schedule,
interview_date=slot["date"],
interview_time=slot["time"],
interview=interview,
)
return redirect("applications_interview_view", slug=slug) if schedule_data.get("schedule_interview_type") == "Remote":
interview.location_type = "Remote"
interview.save(update_fields=["location_type"])
async_task("recruitment.tasks.create_interview_and_meeting",scheduled.pk)
elif schedule_data.get("schedule_interview_type") == "Onsite": messages.success(request,f"Schedule successfully created.")
try:
for i, application in enumerate(applications):
if i < len(available_slots):
slot = available_slots[i]
start_dt = datetime.combine(slot["date"], schedule.start_time) if SESSION_DATA_KEY in request.session:
del request.session[SESSION_DATA_KEY]
if SESSION_ID_KEY in request.session:
del request.session[SESSION_ID_KEY]
interview = Interview.objects.create( return redirect("applications_interview_view", slug=slug)
topic=schedule.topic,
start_time=start_dt,
duration=schedule.interview_duration,
location_type="Onsite",
physical_address=schedule.physical_address,
)
# 2. Create the ScheduledInterview, linking the unique location
ScheduledInterview.objects.create(
application=application,
job=job,
schedule=schedule,
interview_date=slot["date"],
interview_time=slot["time"],
interview=interview,
)
messages.success(
request, f"created successfully for {len(applications)} application."
)
# Clear session data keys upon successful completion
if SESSION_DATA_KEY in request.session:
del request.session[SESSION_DATA_KEY]
if SESSION_ID_KEY in request.session:
del request.session[SESSION_ID_KEY]
return redirect("applications_interview_view", slug=slug)
except Exception as e:
messages.error(request, f"Error creating onsite interviews: {e}")
return redirect("schedule_interviews", slug=slug)
@login_required @login_required
@ -1837,13 +1808,9 @@ def _handle_confirm_schedule(request, slug, job):
def schedule_interviews_view(request, slug): def schedule_interviews_view(request, slug):
job = get_object_or_404(JobPosting, slug=slug) job = get_object_or_404(JobPosting, slug=slug)
if request.method == "POST": if request.method == "POST":
# return _handle_confirm_schedule(request, slug, job)
return _handle_preview_submission(request, slug, job) return _handle_preview_submission(request, slug, job)
else: else:
# if request.session.get("interview_schedule_data"):
print(request.session.get("interview_schedule_data"))
return _handle_get_request(request, slug, job) return _handle_get_request(request, slug, job)
# return redirect("applications_interview_view", slug=slug)
@login_required @login_required
@ -2143,7 +2110,7 @@ def reschedule_meeting_for_application(request, slug):
if request.method == "POST": if request.method == "POST":
if interview.location_type == "Remote": if interview.location_type == "Remote":
form = ScheduledInterviewForm(request.POST) form = ScheduledInterviewForm(request.POST)
else: else:
form = OnsiteScheduleInterviewUpdateForm(request.POST) form = OnsiteScheduleInterviewUpdateForm(request.POST)
@ -3058,7 +3025,7 @@ def applicant_portal_dashboard(request):
# Get candidate's documents using the Person documents property # Get candidate's documents using the Person documents property
documents = applicant.documents.order_by("-created_at") documents = applicant.documents.order_by("-created_at")
print(documents) print(documents)
# Add password change form for modal # Add password change form for modal
@ -3682,11 +3649,11 @@ def message_create(request):
# from .services.email_service import UnifiedEmailService # from .services.email_service import UnifiedEmailService
# from .dto.email_dto import EmailConfig, EmailPriority # from .dto.email_dto import EmailConfig, EmailPriority
email_addresses = [message.recipient.email] email_addresses = [message.recipient.email]
subject=message.subject subject=message.subject
email_result=async_task( email_result=async_task(
"recruitment.tasks.send_email_task", "recruitment.tasks.send_email_task",
email_addresses, email_addresses,
@ -3700,7 +3667,7 @@ def message_create(request):
}, },
) )
# Send email using unified service # Send email using unified service
if email_result: if email_result:
messages.success( messages.success(
request, "Message sent successfully via email!" request, "Message sent successfully via email!"
@ -3755,7 +3722,7 @@ def message_create(request):
and "HX-Request" in request.headers and "HX-Request" in request.headers
and request.user.user_type in ["candidate", "agency"] and request.user.user_type in ["candidate", "agency"]
): ):
job_id = request.GET.get("job") job_id = request.GET.get("job")
if job_id: if job_id:
job = get_object_or_404(JobPosting, id=job_id) job = get_object_or_404(JobPosting, id=job_id)
@ -4288,9 +4255,11 @@ def update_interview_result(request,slug):
interview = get_object_or_404(Interview,slug=slug) interview = get_object_or_404(Interview,slug=slug)
schedule=interview.scheduled_interview schedule=interview.scheduled_interview
form = InterviewResultForm(request.POST, instance=interview) form = InterviewResultForm(request.POST, instance=interview)
if form.is_valid(): if form.is_valid():
interview.save(update_fields=['interview_result', 'result_comments'])
form.save() # Saves form data form.save() # Saves form data
messages.success(request, _(f"Interview result updated successfully to {interview.interview_result}.")) messages.success(request, _(f"Interview result updated successfully to {interview.interview_result}."))
@ -4453,7 +4422,7 @@ def api_application_detail(request, candidate_id):
# subject = form.cleaned_data.get("subject") # subject = form.cleaned_data.get("subject")
# message = form.get_formatted_message() # message = form.get_formatted_message()
# async_task( # async_task(
# "recruitment.tasks.send_bulk_email_task", # "recruitment.tasks.send_bulk_email_task",
# email_addresses, # email_addresses,
@ -4468,7 +4437,7 @@ def api_application_detail(request, candidate_id):
# }, # },
# ) # )
# return redirect(request.path) # return redirect(request.path)
# else: # else:
# # Form validation errors # # Form validation errors
@ -4754,7 +4723,7 @@ def application_signup(request, slug):
@login_required @login_required
@staff_user_required @staff_user_required
def interview_list(request): def interview_list(request):
"""List all interviews with filtering and pagination""" """List all interviews with filtering and pagination"""
interviews = ScheduledInterview.objects.select_related( interviews = ScheduledInterview.objects.select_related(
"application", "application",
@ -4861,7 +4830,7 @@ def interview_detail(request, slug):
OnsiteScheduleInterviewUpdateForm, OnsiteScheduleInterviewUpdateForm,
) )
schedule = get_object_or_404(ScheduledInterview, slug=slug) schedule = get_object_or_404(ScheduledInterview, slug=slug)
interview = schedule.interview interview = schedule.interview
@ -6513,7 +6482,7 @@ def sync_history(request, job_slug=None):
# sender_user = request.user # sender_user = request.user
# job = job # job = job
# try: # try:
# # Send email using background task # # Send email using background task
# email_result= async_task( # email_result= async_task(
# "recruitment.tasks.send_bulk_email_task", # "recruitment.tasks.send_bulk_email_task",
@ -6553,18 +6522,18 @@ def sync_history(request, job_slug=None):
def send_interview_email(request, slug): def send_interview_email(request, slug):
from django.conf import settings from django.conf import settings
from django_q.tasks import async_task from django_q.tasks import async_task
schedule = get_object_or_404(ScheduledInterview, slug=slug) schedule = get_object_or_404(ScheduledInterview, slug=slug)
application = schedule.application application = schedule.application
job = application.job job = application.job
if request.method == "POST": if request.method == "POST":
form = InterviewEmailForm(job, application, schedule, request.POST) form = InterviewEmailForm(job, application, schedule, request.POST)
if form.is_valid(): if form.is_valid():
# 1. Ensure recipient is a list (fixes the "@" error) # 1. Ensure recipient is a list (fixes the "@" error)
recipient_str = form.cleaned_data.get("to").strip() recipient_str = form.cleaned_data.get("to").strip()
recipient_list = [recipient_str] recipient_list = [recipient_str]
body_message = form.cleaned_data.get("message") body_message = form.cleaned_data.get("message")
subject = form.cleaned_data.get("subject") subject = form.cleaned_data.get("subject")
@ -6585,7 +6554,7 @@ def send_interview_email(request, slug):
"logo_url": settings.STATIC_URL + "image/kaauh.png", "logo_url": settings.STATIC_URL + "image/kaauh.png",
}, },
) )
messages.success(request, "Interview email enqueued successfully!") messages.success(request, "Interview email enqueued successfully!")
return redirect("interview_detail", slug=schedule.slug) return redirect("interview_detail", slug=schedule.slug)
@ -6597,14 +6566,14 @@ def send_interview_email(request, slug):
# GET request # GET request
form = InterviewEmailForm(job, application, schedule) form = InterviewEmailForm(job, application, schedule)
# 3. FIX: Instead of always redirecting, render the template # 3. FIX: Instead of always redirecting, render the template
# This allows users to see validation errors. # This allows users to see validation errors.
return render( return render(
request, request,
"recruitment/interview_email_form.html", # Replace with your actual template path "recruitment/interview_email_form.html", # Replace with your actual template path
{ {
"form": form, "form": form,
"schedule": schedule, "schedule": schedule,
"job": job "job": job
} }
) )
@ -6642,7 +6611,7 @@ def compose_application_email(request, slug):
subject = form.cleaned_data.get("subject") subject = form.cleaned_data.get("subject")
message = form.get_formatted_message() message = form.get_formatted_message()
async_task( async_task(
"recruitment.tasks.send_email_task", "recruitment.tasks.send_email_task",
email_addresses, email_addresses,
@ -6660,7 +6629,7 @@ def compose_application_email(request, slug):
}, },
) )
return redirect(request.path) return redirect(request.path)
else: else:
# Form validation errors # Form validation errors

View File

@ -1,6 +1,7 @@
import requests import requests
import jwt import jwt
import time import time
from datetime import timezone
from .utils import get_zoom_config from .utils import get_zoom_config
@ -22,13 +23,30 @@ def create_zoom_meeting(topic, start_time, duration, host_email):
'Authorization': f'Bearer {jwt_token}', 'Authorization': f'Bearer {jwt_token}',
'Content-Type': 'application/json' 'Content-Type': 'application/json'
} }
# Format start_time according to Zoom API requirements
# Convert datetime to ISO 8601 format with Z suffix for UTC
if hasattr(start_time, 'isoformat'):
# If it's a datetime object, format it properly
if hasattr(start_time, 'tzinfo') and start_time.tzinfo is not None:
# Timezone-aware datetime: convert to UTC and format with Z suffix
utc_time = start_time.astimezone(timezone.utc)
zoom_start_time = utc_time.strftime("%Y-%m-%dT%H:%M:%S") + "Z"
else:
# Naive datetime: assume it's in UTC and format with Z suffix
zoom_start_time = start_time.strftime("%Y-%m-%dT%H:%M:%S") + "Z"
else:
# If it's already a string, use as-is (assuming it's properly formatted)
zoom_start_time = str(start_time)
data = { data = {
"topic": topic, "topic": topic,
"type": 2, "type": 2,
"start_time": start_time, "start_time": zoom_start_time,
"duration": duration, "duration": duration,
"schedule_for": host_email, "schedule_for": host_email,
"settings": {"join_before_host": True} "settings": {"join_before_host": True},
"timezone": "UTC" # Explicitly set timezone to UTC
} }
url = f"https://api.zoom.us/v2/users/{host_email}/meetings" url = f"https://api.zoom.us/v2/users/{host_email}/meetings"
return requests.post(url, json=data, headers=headers) return requests.post(url, json=data, headers=headers)

View File

@ -144,6 +144,9 @@
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="{{ form.topic.id_for_label }}">{% trans "Topic" %}</label> <label for="{{ form.topic.id_for_label }}">{% trans "Topic" %}</label>
{{ form.topic }} {{ form.topic }}
{% if form.topic.errors %}
<div class="text-danger small mt-1">{{ form.topic.errors }}</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -152,6 +155,9 @@
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="{{ form.schedule_interview_type.id_for_label }}">{% trans "Interview Type" %}</label> <label for="{{ form.schedule_interview_type.id_for_label }}">{% trans "Interview Type" %}</label>
{{ form.schedule_interview_type }} {{ form.schedule_interview_type }}
{% if form.schedule_interview_type.errors %}
<div class="text-danger small mt-1">{{ form.schedule_interview_type.errors }}</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -160,6 +166,9 @@
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="{{ form.start_date.id_for_label }}">{% trans "Start Date" %}</label> <label for="{{ form.start_date.id_for_label }}">{% trans "Start Date" %}</label>
{{ form.start_date }} {{ form.start_date }}
{% if form.start_date.errors %}
<div class="text-danger small mt-1">{{ form.start_date.errors }}</div>
{% endif %}
</div> </div>
</div> </div>
@ -167,6 +176,9 @@
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="{{ form.end_date.id_for_label }}">{% trans "End Date" %}</label> <label for="{{ form.end_date.id_for_label }}">{% trans "End Date" %}</label>
{{ form.end_date }} {{ form.end_date }}
{% if form.end_date.errors %}
<div class="text-danger small mt-1">{{ form.end_date.errors }}</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -175,6 +187,9 @@
<label>{% trans "Working Days" %}</label> <label>{% trans "Working Days" %}</label>
<div class="d-flex flex-wrap gap-3 p-2 border rounded" style="background-color: #f8f9fa;"> <div class="d-flex flex-wrap gap-3 p-2 border rounded" style="background-color: #f8f9fa;">
{{ form.working_days }} {{ form.working_days }}
{% if form.working_days.errors %}
<div class="text-danger small mt-1">{{ form.working_days.errors }}</div>
{% endif %}
</div> </div>
</div> </div>
@ -183,6 +198,9 @@
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="{{ form.start_time.id_for_label }}">{% trans "Start Time" %}</label> <label for="{{ form.start_time.id_for_label }}">{% trans "Start Time" %}</label>
{{ form.start_time }} {{ form.start_time }}
{% if form.start_time.errors %}
<div class="text-danger small mt-1">{{ form.start_time.errors }}</div>
{% endif %}
</div> </div>
</div> </div>
@ -190,6 +208,9 @@
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="{{ form.end_time.id_for_label }}">{% trans "End Time" %}</label> <label for="{{ form.end_time.id_for_label }}">{% trans "End Time" %}</label>
{{ form.end_time }} {{ form.end_time }}
{% if form.end_time.errors %}
<div class="text-danger small mt-1">{{ form.end_time.errors }}</div>
{% endif %}
</div> </div>
</div> </div>
@ -197,6 +218,9 @@
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="{{ form.interview_duration.id_for_label }}">{% trans "Duration (min)" %}</label> <label for="{{ form.interview_duration.id_for_label }}">{% trans "Duration (min)" %}</label>
{{ form.interview_duration }} {{ form.interview_duration }}
{% if form.interview_duration.errors %}
<div class="text-danger small mt-1">{{ form.interview_duration.errors }}</div>
{% endif %}
</div> </div>
</div> </div>
@ -204,6 +228,9 @@
<div class="form-group mb-3"> <div class="form-group mb-3">
<label for="{{ form.buffer_time.id_for_label }}">{% trans "Buffer (min)" %}</label> <label for="{{ form.buffer_time.id_for_label }}">{% trans "Buffer (min)" %}</label>
{{ form.buffer_time }} {{ form.buffer_time }}
{% if form.buffer_time.errors %}
<div class="text-danger small mt-1">{{ form.buffer_time.errors }}</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>
@ -215,10 +242,16 @@
<div class="col-5"> <div class="col-5">
<label for="{{ form.break_start_time.id_for_label }}">{% trans "Start Time" %}</label> <label for="{{ form.break_start_time.id_for_label }}">{% trans "Start Time" %}</label>
{{ form.break_start_time }} {{ form.break_start_time }}
{% if form.break_start_time.errors %}
<div class="text-danger small mt-1">{{ form.break_start_time.errors }}</div>
{% endif %}
</div> </div>
<div class="col-5"> <div class="col-5">
<label for="{{ form.break_end_time.id_for_label }}">{% trans "End Time" %}</label> <label for="{{ form.break_end_time.id_for_label }}">{% trans "End Time" %}</label>
{{ form.break_end_time }} {{ form.break_end_time }}
{% if form.break_end_time.errors %}
<div class="text-danger small mt-1">{{ form.break_end_time.errors }}</div>
{% endif %}
</div> </div>
</div> </div>
</div> </div>

View File

@ -369,7 +369,7 @@
data-bs-toggle="modal" data-bs-toggle="modal"
data-bs-target="#noteModal" data-bs-target="#noteModal"
hx-get="{% url 'application_add_note' application.slug %}" hx-get="{% url 'application_add_note' application.slug %}"
hx-swap="outerHTML" hx-swap="innerHTML"
hx-target=".notemodal"> hx-target=".notemodal">
<i class="fas fa-calendar-plus me-1"></i> <i class="fas fa-calendar-plus me-1"></i>
Add note Add note