fix the bulk remote meetings creation
This commit is contained in:
parent
3ae6d66dbd
commit
444eedc208
6
.env
6
.env
@ -1,3 +1,3 @@
|
|||||||
DB_NAME=haikal_db
|
DB_NAME=norahuniversity
|
||||||
DB_USER=faheed
|
DB_USER=norahuniversity
|
||||||
DB_PASSWORD=Faheed@215
|
DB_PASSWORD=norahuniversity
|
||||||
@ -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
|
||||||
|
|||||||
35
recruitment/migrations/0004_interviewquestion.py
Normal file
35
recruitment/migrations/0004_interviewquestion.py
Normal 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')],
|
||||||
|
},
|
||||||
|
),
|
||||||
|
]
|
||||||
@ -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):
|
||||||
|
|||||||
@ -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}."
|
||||||
})
|
# })
|
||||||
|
|||||||
@ -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.",
|
||||||
|
|||||||
@ -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
|
||||||
@ -4291,6 +4258,8 @@ def update_interview_result(request,slug):
|
|||||||
|
|
||||||
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}."))
|
||||||
|
|||||||
@ -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)
|
||||||
|
|||||||
@ -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>
|
||||||
|
|||||||
@ -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
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user